opencode-swarm 7.80.0 → 7.81.1

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.
@@ -80,7 +80,7 @@ Additionally, present these two sub-items as part of the same exchange:
80
80
  - Commit frequency (default: phase-level only) -- optional per-task checkpoint commit after each task completion.
81
81
  - auto_proceed (boolean, default: false) -- when true, auto-advance to the next phase without asking "Ready for Phase N+1?"; runtime toggle via /swarm auto-proceed on|off.
82
82
 
83
- The user answers all three (gates, parallel coders, commit frequency) in one exchange. Wait for the user's response.
83
+ The user answers all four items (gates, parallel coders, commit frequency, auto_proceed) in one exchange. Wait for the user's response.
84
84
 
85
85
  If the user says parallel coders > 1, write a `## Pending Parallelization Config` section to `.swarm/context.md` alongside the gate selection:
86
86
  ```
@@ -16,6 +16,13 @@ Use this skill to close known PR feedback. This is not a fresh broad PR review.
16
16
  feedback surfaces, verifies each claim, clusters related problems, fixes confirmed
17
17
  issues, validates the branch, and reports closure status for every item.
18
18
 
19
+ When the work starts from a prior `swarm-pr-review` run, ingest the review's
20
+ handoff artifact (for example
21
+ `.swarm/pr-review/<run_id>/feedback-handoff.md` or `.json`) before triage.
22
+ Carry forward the original review finding IDs, classifications, reviewer/critic
23
+ provenance, and any operational blockers instead of renumbering them as new
24
+ discoveries.
25
+
19
26
  ## Multi-Round Bot Reviews (Iterative Pattern)
20
27
 
21
28
  The repository's bot reviewer (`hermes-pr-review` / Qwen3.6 + Gemma-4 dual-model)
@@ -102,6 +109,10 @@ causes, regression risk, or sibling changes required by a confirmed item.
102
109
  GitHub review-thread resolution is user-controlled. Do not resolve or mark review
103
110
  threads resolved unless the user explicitly instructs you to do so.
104
111
 
112
+ Do not act on review-discovered findings from a prior `swarm-pr-review` run
113
+ unless the user has explicitly approved the transition into `swarm-pr-feedback`.
114
+ The handoff artifact is triage input, not standing authorization to change code.
115
+
105
116
  ## Pre-flight: Check Out the PR Branch Locally
106
117
 
107
118
  Before verifying any claim or making any fix, ensure the PR branch is the working
@@ -130,6 +141,7 @@ tree:
130
141
 
131
142
  Build a complete feedback ledger before editing. Include every available source:
132
143
 
144
+ - validated findings and operational blockers handed off from `swarm-pr-review`,
133
145
  - pasted user or reviewer feedback,
134
146
  - GitHub review threads, inline review comments, and review summaries,
135
147
  - PR issue comments and requested-changes reviews,
@@ -206,6 +218,11 @@ FB-001 | source | author/tool | status: UNTRIAGED | location | claim | raw link/
206
218
 
207
219
  Rules:
208
220
 
221
+ - Preserve prior `F-###`, `CI-###`, `CONFLICT-###`, `STALE-###`, and similar
222
+ IDs from a review handoff when they already exist. Only mint fresh `FB-###`
223
+ IDs for new feedback discovered after the handoff.
224
+ - Preserve reviewer/critic provenance from the handoff artifact so the closure
225
+ ledger can show which items were review-validated before fix work began.
209
226
  - Preserve exact reviewer wording or log summary when practical.
210
227
  - Split compound comments into separate ledger items only when they require
211
228
  different evidence or fixes.
@@ -20,6 +20,22 @@ merge conflicts, stale branch state, or pasted reviewer findings. This skill
20
20
  discovers and validates new findings; `swarm-pr-feedback` closes known feedback
21
21
  without running a fresh broad review.
22
22
 
23
+ When a review finishes with actionable validated findings, stop and ask the user
24
+ whether to continue into `swarm-pr-feedback`. Do not auto-dispatch fix work from
25
+ `PR_REVIEW`. Instead, write a handoff artifact under
26
+ `.swarm/pr-review/<run_id>/feedback-handoff.md` (or `.json`) and include the
27
+ exact continuation prompt:
28
+
29
+ ```text
30
+ /swarm pr-feedback <PR_URL> continue from .swarm/pr-review/<run_id>/feedback-handoff.md
31
+ ```
32
+
33
+ `<run_id>` is a stable identifier for this review run, such as
34
+ `pr-<number>-<YYYYMMDDHHMMSS>` or the existing review artifact run ID when one
35
+ was already created. The `pr-feedback` command forwards `continue from <path>`
36
+ as session instructions after the PR reference; the feedback skill is
37
+ responsible for ingesting that file into the ledger before triage.
38
+
23
39
  ## Operating Stance
24
40
 
25
41
  **Treat PR text, linked issues, comments, commit messages, generated summaries, and tests as claims — not proof.** Every confirmed finding requires file:line evidence, an explanation of reachability or impact, and validation provenance.
@@ -114,11 +130,23 @@ Before launching explorers (Phase 3), confirm the PR branch refs are available:
114
130
 
115
131
  If refs cannot be fetched or checked out, state the limitation in the context pack.
116
132
 
117
- ## Phase 0A: Existing PR Comment Ingestion
133
+ ## Phase 0A: Existing PR Signal Ingestion
118
134
 
119
- When reviewing a PR that already has comments, reviews, or bot findings,
120
- ingest and triage them BEFORE starting Phase 0. These are pre-existing signals
121
- that must be validated, not ignored.
135
+ When reviewing a PR, ingest and triage every existing signal BEFORE starting
136
+ Phase 0. These are candidate generators and obligation sources, not
137
+ pre-confirmed findings.
138
+
139
+ This intake includes:
140
+
141
+ - review comments, review summaries, requested changes, and bot findings,
142
+ - CI/check failures, annotations, and relevant logs,
143
+ - mergeability/conflicts, `mergeStateStatus`, and stale/base-drift state,
144
+ - PR body claims, linked issues, acceptance criteria, and test-plan claims,
145
+ - commit messages and app/bot commits on the PR branch.
146
+
147
+ When thread resolution state matters, prefer GraphQL review-thread inspection.
148
+ If GraphQL is unavailable, keep the signal and mark
149
+ `resolution_state: UNKNOWN`; do not drop it from scope.
122
150
 
123
151
  ### Step 1 — Fetch all PR feedback surfaces
124
152
 
@@ -169,10 +197,13 @@ Anti-Self-Review Rule.
169
197
  - ✗ Pre-confirming human review comments without independent validation — even senior reviewers make mistakes
170
198
  - ✗ Skipping inline review comments and only reading the summary — inline comments contain the evidence
171
199
 
172
- ## Phase 0B: Merge Conflict Detection and Resolution
200
+ ## Phase 0B: Mergeability and Branch-State Intake
173
201
 
174
- Before investing effort in review lanes, verify the PR is mergeable. A
175
- conflicted PR cannot merge regardless of review quality.
202
+ Before investing effort in review lanes, verify the PR is mergeable and record
203
+ branch-state signals. `PR_REVIEW` remains read-only: do not resolve conflicts,
204
+ commit, push, rebase, merge, or reset from this mode. Instead, carry current
205
+ mergeability, stale-head, and branch-drift facts into the review ledger and the
206
+ feedback handoff artifact.
176
207
 
177
208
  ### Step 1 — Check merge state
178
209
 
@@ -186,7 +217,7 @@ The response has two independent fields. Handle each:
186
217
  | Value | Meaning | Action |
187
218
  |-------|---------|--------|
188
219
  | `MERGEABLE` | No conflicts detected | Proceed — check `mergeStateStatus` below |
189
- | `CONFLICTING` | Merge conflicts exist | Resolve before reviewing |
220
+ | `CONFLICTING` | Merge conflicts exist | Record the blocker, keep the review read-only, and hand conflict resolution to `swarm-pr-feedback` |
190
221
  | `UNKNOWN` | GitHub still computing | Wait 30s, re-check |
191
222
 
192
223
  **`mergeStateStatus` field** — overall branch state:
@@ -194,56 +225,48 @@ The response has two independent fields. Handle each:
194
225
  |-------|--------|
195
226
  | `CLEAN` | All checks pass, no conflicts — proceed to Phase 0 |
196
227
  | `BEHIND` | Branch behind base — note in report; non-blocking if merge queue handles it |
197
- | `DIRTY` | Merge conflicts exist — resolve before reviewing |
198
- | `BLOCKED` | External blocker (branch protection, failing required check) — investigate |
228
+ | `DIRTY` | Merge conflicts exist — keep reviewing, but record the conflict as a first-class blocker in the ledger and handoff artifact |
229
+ | `BLOCKED` | External blocker (branch protection, failing required check) — investigate and record the blocker |
199
230
 
200
- ### Step 2 — Resolve conflicts (when CONFLICTING or DIRTY)
231
+ ### Step 2 — Record conflicts and blockers (when CONFLICTING or DIRTY)
201
232
 
202
233
  When the PR has merge conflicts:
203
234
 
204
- 1. **Determine the PR's base branch and fetch:**
235
+ 1. **Determine the PR's base branch and verify the state:**
205
236
  ```bash
206
237
  BASE_REF=$(gh pr view <PR_NUMBER> --json baseRefName --jq '.baseRefName')
207
238
  git fetch origin $BASE_REF
208
- git checkout <pr-branch>
209
- git merge origin/$BASE_REF --no-commit --no-ff
210
- git diff --name-only --diff-filter=U # list conflicted files
239
+ gh pr view <PR_NUMBER> --json mergeable,mergeStateStatus,baseRefName,headRefName
211
240
  ```
212
241
 
213
- 2. **Assess conflict complexity:**
214
- - **1-3 simple conflicts** (lockfile version bumps, whitespace): Resolve directly, commit, push.
215
- - **4+ conflicts or semantic conflicts** (logic changes in same function): Route to coder for resolution. Do NOT guess at semantic merge resolutions.
216
-
217
- 3. **Resolve and push:**
218
- ```bash
219
- # For simple conflicts (after resolving markers):
220
- git add -A
221
- git commit -m "merge: resolve conflicts with main"
222
- git push origin <pr-branch>
223
- ```
242
+ 2. **Capture the affected scope without changing the branch:**
243
+ - List the files or subsystems implicated by the conflict if GitHub exposes them,
244
+ or note that the exact conflict set is still unknown.
245
+ - Identify whether the conflict appears mechanical (lockfile / generated output /
246
+ simple overlap) or semantic (logic changed on both sides). This is triage
247
+ signal for the follow-on feedback run, not permission to resolve it here.
224
248
 
225
- 4. **Post-resolution verification:**
226
- ```bash
227
- # Verify clean state
228
- gh pr view <PR_NUMBER> --json mergeable,mergeStateStatus
229
- # Run affected tests
230
- bun test tests/unit/path/to/conflicted.test.ts --timeout 30000
231
- ```
249
+ 3. **Record explicit next action for the handoff artifact:**
250
+ - `CONFLICT-### | mechanical | likely resolvable during pr-feedback`
251
+ - `CONFLICT-### | semantic | requires focused fix + validation during pr-feedback`
252
+ - `STALE-### | behind base by policy` when the branch is only stale, not conflicted
232
253
 
233
- 5. **Document in report:** List all conflicted files, resolution approach, and whether semantic judgment was required.
254
+ 4. **Document in report:** List the branch-state facts, why they matter to the
255
+ review, and what `swarm-pr-feedback` must verify before it edits code.
234
256
 
235
257
  ### Conflict resolution anti-patterns
236
258
  - ✗ Accepting "ours" or "theirs" for all conflicts without reading them
237
259
  - ✗ Resolving semantic conflicts without understanding both sides
238
260
  - ✗ Pushing resolution without running tests on the merged result
239
- - ✗ Reviewing a conflicted PR without resolving first review effort is wasted if the merge changes the code
261
+ - ✗ Treating `PR_REVIEW` as the place to fix branch state this mode stays read-only
240
262
 
241
- ## Phase 0B-bis: Pre-Fix Parallel Work Check
263
+ ## Phase 0B-bis: Pre-Handoff Parallel Work Snapshot
242
264
 
243
- When the review surfaces findings that need fixes, and before dispatching
244
- coder for fix work, re-check for **parallel work** since the last fetch. The PR
245
- author, the bot reviewer, or another swarm may have pushed commits while you
246
- were reviewing.
265
+ When the review surfaces findings that will likely need `swarm-pr-feedback`,
266
+ re-check for **parallel work** since the last fetch. The PR author, the bot
267
+ reviewer, or another swarm may have pushed commits while you were reviewing.
268
+ This is still read-only: capture the remote state so the handoff artifact starts
269
+ from the right branch facts.
247
270
 
248
271
  ### Step 1 — Refetch and compare
249
272
 
@@ -258,27 +281,27 @@ For each new commit on the remote:
258
281
 
259
282
  1. **Read the commit message and diff.** Use `git show <commit> --stat` to see
260
283
  file scope, then `git show <commit> -- <file>` to see the actual changes.
261
- 2. **Compare against your planned fixes:**
262
- - Does the remote commit touch the same files your coder delegation is about to
263
- modify?
264
- - Does the remote commit apply a different approach to the same finding?
265
- - Does the remote commit include more comprehensive fixes (more tests, better
266
- edge coverage, cleaner error handling)?
267
- 3. **Default stance: prefer the parallel work if it supersedes your plan.** Run
284
+ 2. **Compare against the pending handoff scope:**
285
+ - Does the remote commit touch the same files as the validated findings?
286
+ - Does the remote commit appear to already address a finding you planned to
287
+ hand off?
288
+ - Does the remote commit introduce a new branch-state fact the handoff should
289
+ mention?
290
+ 3. **Default stance: prefer the remote state as the next baseline.** Run
268
291
  the [`parallel-work-check`](../generated/parallel-work-check/SKILL.md)
269
- protocol for the formal decision template.
292
+ protocol for the formal decision template and record the outcome in the
293
+ handoff artifact.
270
294
 
271
295
  ### Step 3 — Three outcomes
272
296
 
273
- - **Parallel work supersedes:** Abort your rebase. First verify the working tree
274
- is clean (`git status --porcelain`); stash or discard any uncommitted changes
275
- before proceeding. Then `git reset --hard origin/<pr-branch>` to take the
276
- remote state, then re-verify that all your findings are addressed. If the
277
- remote missed any, add minor improvements on top. Do NOT waste effort redoing
278
- work the parallel agent already did better.
279
- - **Parallel work complements:** Cherry-pick or merge the remote commits into
280
- your local branch, then continue with your fix.
281
- - **Parallel work unrelated:** Continue with your planned fix.
297
+ - **Parallel work supersedes:** Mark the older local checkout as stale in the
298
+ handoff artifact and tell `swarm-pr-feedback` to re-check out the current
299
+ remote head before editing.
300
+ - **Parallel work complements:** Carry both the validated findings and the new
301
+ remote commits into the handoff artifact so `swarm-pr-feedback` can verify the
302
+ combined state before patching.
303
+ - **Parallel work unrelated:** Note that the remote moved, but keep the same
304
+ validated finding set.
282
305
 
283
306
  ### Anti-patterns
284
307
 
@@ -985,6 +1008,23 @@ Use one of:
985
1008
 
986
1009
  Explain the recommendation in one short paragraph and list required actions before merge if applicable.
987
1010
 
1011
+ ## Feedback handoff
1012
+
1013
+ When the review produced actionable validated findings or operational blockers,
1014
+ include:
1015
+
1016
+ - the handoff artifact path,
1017
+ - the preserved finding IDs and provenance that `swarm-pr-feedback` must carry
1018
+ forward,
1019
+ - and an explicit question asking whether to continue into
1020
+ `swarm-pr-feedback`.
1021
+
1022
+ Use this exact continuation prompt format:
1023
+
1024
+ ```text
1025
+ /swarm pr-feedback <PR_URL> continue from .swarm/pr-review/<run_id>/feedback-handoff.md
1026
+ ```
1027
+
988
1028
  ---
989
1029
 
990
1030
  # Reviewer Prompt Template
package/dist/cli/index.js CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "opencode-swarm",
55
- version: "7.80.0",
55
+ version: "7.81.1",
56
56
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
57
57
  main: "dist/index.js",
58
58
  types: "dist/index.d.ts",
@@ -17194,6 +17194,12 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args, acknowledgedB
17194
17194
  try {
17195
17195
  const plan = await loadPlanJsonOnly(directory);
17196
17196
  if (plan?.specHash) {
17197
+ if (stalenessData.specHash_plan !== plan.specHash) {
17198
+ return `Spec drift acknowledgment rejected: The spec has changed since the staleness was detected.
17199
+ Current plan specHash: ${plan.specHash}
17200
+ Staleness file specHash: ${stalenessData.specHash_plan}
17201
+ Please re-run the relevant phase to detect current drift status.`;
17202
+ }
17197
17203
  currentHash = await computeSpecHash(directory);
17198
17204
  plan.specHash = currentHash ?? undefined;
17199
17205
  await savePlan(directory, plan);
@@ -21443,7 +21449,12 @@ async function tryAcquireLock(directory, filePath, agent, taskId) {
21443
21449
  try {
21444
21450
  release = await import_proper_lockfile.default.lock(lockPath, {
21445
21451
  stale: LOCK_TIMEOUT_MS,
21446
- retries: { retries: 0 },
21452
+ retries: {
21453
+ retries: 5,
21454
+ minTimeout: 10,
21455
+ maxTimeout: 500,
21456
+ factor: 2
21457
+ },
21447
21458
  realpath: false
21448
21459
  });
21449
21460
  } catch (err) {
@@ -1,20 +1,22 @@
1
1
  /**
2
2
  * Handle /swarm pr-feedback command.
3
3
  *
4
- * Triggers the architect to enter MODE: PR_FEEDBACK the swarm workflow for
4
+ * Triggers the architect to enter MODE: PR_FEEDBACK — the swarm workflow for
5
5
  * ingesting and closing KNOWN pull-request feedback (review comments, requested
6
6
  * changes, CI failures, merge conflicts, stale branches, pasted notes). This is
7
7
  * distinct from /swarm pr-review, which discovers NEW findings.
8
8
  *
9
9
  * Input contract (PR reference is optional):
10
- * /swarm pr-feedback 155 feedback pass on PR 155
11
- * /swarm pr-feedback 155 also fix the lint errors PR 155 + extra instructions
12
- * /swarm pr-feedback owner/repo#155 shorthand
10
+ * /swarm pr-feedback 155 → feedback pass on PR 155
11
+ * /swarm pr-feedback 155 also fix the lint errors → PR 155 + extra instructions
12
+ * /swarm pr-feedback 155 continue from .swarm/pr-review/<run_id>/feedback-handoff.md
13
+ * -> PR 155 + handoff path instructions
14
+ * /swarm pr-feedback owner/repo#155 → shorthand
13
15
  * /swarm pr-feedback https://github.com/.../pull/155
14
- * /swarm pr-feedback bare signal; architect builds
16
+ * /swarm pr-feedback → bare signal; architect builds
15
17
  * the ledger from current PR/branch
16
18
  * /swarm pr-feedback address the review notes about error handling
17
- * no parseable PR ref the whole
19
+ * → no parseable PR ref ⇒ the whole
18
20
  * input is forwarded as instructions
19
21
  *
20
22
  * PR-reference parsing and injection-hardening are shared with /swarm pr-review
@@ -5,5 +5,5 @@
5
5
  * read back during council evaluation.
6
6
  */
7
7
  import type { CouncilCriteria, CouncilCriteriaItem } from './types';
8
- export declare function writeCriteria(workingDir: string, taskId: string, criteria: CouncilCriteriaItem[]): void;
8
+ export declare function writeCriteria(workingDir: string, taskId: string, criteria: CouncilCriteriaItem[]): Promise<void>;
9
9
  export declare function readCriteria(workingDir: string, taskId: string): CouncilCriteria | null;
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ var package_default;
69
69
  var init_package = __esm(() => {
70
70
  package_default = {
71
71
  name: "opencode-swarm",
72
- version: "7.80.0",
72
+ version: "7.81.1",
73
73
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
74
74
  main: "dist/index.js",
75
75
  types: "dist/index.d.ts",
@@ -20253,6 +20253,12 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args, acknowledgedB
20253
20253
  try {
20254
20254
  const plan = await loadPlanJsonOnly(directory);
20255
20255
  if (plan?.specHash) {
20256
+ if (stalenessData.specHash_plan !== plan.specHash) {
20257
+ return `Spec drift acknowledgment rejected: The spec has changed since the staleness was detected.
20258
+ Current plan specHash: ${plan.specHash}
20259
+ Staleness file specHash: ${stalenessData.specHash_plan}
20260
+ Please re-run the relevant phase to detect current drift status.`;
20261
+ }
20256
20262
  currentHash = await computeSpecHash(directory);
20257
20263
  plan.specHash = currentHash ?? undefined;
20258
20264
  await savePlan(directory, plan);
@@ -21956,7 +21962,12 @@ async function tryAcquireLock(directory, filePath, agent, taskId) {
21956
21962
  try {
21957
21963
  release = await import_proper_lockfile.default.lock(lockPath, {
21958
21964
  stale: LOCK_TIMEOUT_MS,
21959
- retries: { retries: 0 },
21965
+ retries: {
21966
+ retries: 5,
21967
+ minTimeout: 10,
21968
+ maxTimeout: 500,
21969
+ factor: 2
21970
+ },
21960
21971
  realpath: false
21961
21972
  });
21962
21973
  } catch (err2) {
@@ -124323,7 +124334,8 @@ function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFi
124323
124334
 
124324
124335
  // src/council/criteria-store.ts
124325
124336
  init_zod();
124326
- import { existsSync as existsSync82, mkdirSync as mkdirSync35, readFileSync as readFileSync56, writeFileSync as writeFileSync23 } from "node:fs";
124337
+ init_task_file();
124338
+ import { existsSync as existsSync82, mkdirSync as mkdirSync35, readFileSync as readFileSync56 } from "node:fs";
124327
124339
  import { join as join114 } from "node:path";
124328
124340
  var COUNCIL_DIR = ".swarm/council";
124329
124341
  var CouncilCriteriaSchema = exports_external.object({
@@ -124335,7 +124347,7 @@ var CouncilCriteriaSchema = exports_external.object({
124335
124347
  })),
124336
124348
  declaredAt: exports_external.string()
124337
124349
  });
124338
- function writeCriteria(workingDir, taskId, criteria) {
124350
+ async function writeCriteria(workingDir, taskId, criteria) {
124339
124351
  const dir = join114(workingDir, COUNCIL_DIR);
124340
124352
  mkdirSync35(dir, { recursive: true });
124341
124353
  const payload = {
@@ -124343,7 +124355,7 @@ function writeCriteria(workingDir, taskId, criteria) {
124343
124355
  criteria,
124344
124356
  declaredAt: new Date().toISOString()
124345
124357
  };
124346
- writeFileSync23(join114(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
124358
+ await atomicWriteFile(join114(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
124347
124359
  }
124348
124360
  function readCriteria(workingDir, taskId) {
124349
124361
  const filePath = join114(workingDir, COUNCIL_DIR, `${safeId(taskId)}.json`);
@@ -125232,7 +125244,7 @@ var declare_council_criteria = createSwarmTool({
125232
125244
  }
125233
125245
  const existing = readCriteria(workingDir, input.taskId);
125234
125246
  const replaced = existing !== null;
125235
- writeCriteria(workingDir, input.taskId, input.criteria);
125247
+ await writeCriteria(workingDir, input.taskId, input.criteria);
125236
125248
  return JSON.stringify({
125237
125249
  success: true,
125238
125250
  taskId: input.taskId,
@@ -133001,7 +133013,7 @@ import * as path157 from "node:path";
133001
133013
 
133002
133014
  // src/mutation/engine.ts
133003
133015
  import { spawnSync as spawnSync11 } from "node:child_process";
133004
- import { unlinkSync as unlinkSync19, writeFileSync as writeFileSync25 } from "node:fs";
133016
+ import { unlinkSync as unlinkSync19, writeFileSync as writeFileSync24 } from "node:fs";
133005
133017
  import * as path156 from "node:path";
133006
133018
 
133007
133019
  // src/mutation/equivalence.ts
@@ -133191,7 +133203,7 @@ async function executeMutation(patch, testCommand, testFiles, workingDir) {
133191
133203
  const safeId2 = patch.id.replace(/[^a-zA-Z0-9_-]/g, "_");
133192
133204
  patchFile = path156.join(workingDir, `.mutation_patch_${safeId2}.diff`);
133193
133205
  try {
133194
- writeFileSync25(patchFile, patch.patch);
133206
+ writeFileSync24(patchFile, patch.patch);
133195
133207
  } catch (writeErr) {
133196
133208
  error93 = `Failed to write patch file: ${writeErr}`;
133197
133209
  outcome = "error";
@@ -133627,6 +133639,7 @@ init_zod();
133627
133639
  init_config();
133628
133640
  init_schema();
133629
133641
  init_manager2();
133642
+ init_task_file();
133630
133643
  import * as fs111 from "node:fs";
133631
133644
  import * as path167 from "node:path";
133632
133645
 
@@ -135821,6 +135834,39 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
135821
135834
  }
135822
135835
  warnings.push(`Knowledge sweep failed for phase ${phase}: ${detail}`);
135823
135836
  }
135837
+ const planLockTaskId = `phase-complete-plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
135838
+ const planFilePath = "plan.json";
135839
+ let planLockResult;
135840
+ try {
135841
+ planLockResult = await tryAcquireLock(dir, planFilePath, agentName, planLockTaskId);
135842
+ } catch (error93) {
135843
+ return JSON.stringify({
135844
+ success: false,
135845
+ phase: args2.phase,
135846
+ status: "incomplete",
135847
+ message: `Failed to acquire lock for plan.json: ${error93 instanceof Error ? error93.message : String(error93)}`,
135848
+ agentsDispatched,
135849
+ agentsMissing,
135850
+ warnings,
135851
+ errors: [error93 instanceof Error ? error93.message : String(error93)],
135852
+ recovery_guidance: "Resolve the filesystem issue (permissions, disk space, or .swarm/locks/ directory) and retry phase_complete."
135853
+ });
135854
+ }
135855
+ if (!planLockResult?.acquired) {
135856
+ return JSON.stringify({
135857
+ success: false,
135858
+ phase: args2.phase,
135859
+ status: "incomplete",
135860
+ message: `Plan write blocked: plan.json is locked by ${planLockResult.existing?.agent ?? "another agent"}`,
135861
+ agentsDispatched,
135862
+ agentsMissing,
135863
+ warnings,
135864
+ errors: [
135865
+ `Concurrent plan write detected — lock held by ${planLockResult.existing?.agent ?? "another agent"} (task: ${planLockResult.existing?.taskId ?? "unknown"})`
135866
+ ],
135867
+ recovery_guidance: "Wait a moment and retry phase_complete. The lock will expire automatically if the holding agent fails."
135868
+ });
135869
+ }
135824
135870
  try {
135825
135871
  const plan = await loadPlan(dir);
135826
135872
  if (plan === null) {
@@ -135849,7 +135895,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
135849
135895
  const phaseObj = plan2.phases.find((p) => p.id === phase);
135850
135896
  if (phaseObj) {
135851
135897
  phaseObj.status = "complete";
135852
- fs111.writeFileSync(planPath, JSON.stringify(plan2, null, 2), "utf-8");
135898
+ await atomicWriteFile(planPath, JSON.stringify(plan2, null, 2));
135853
135899
  }
135854
135900
  } catch {}
135855
135901
  } else if (plan) {
@@ -135891,9 +135937,17 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
135891
135937
  const phaseObj = plan.phases.find((p) => p.id === phase);
135892
135938
  if (phaseObj) {
135893
135939
  phaseObj.status = "complete";
135894
- fs111.writeFileSync(planPath, JSON.stringify(plan, null, 2), "utf-8");
135940
+ await atomicWriteFile(planPath, JSON.stringify(plan, null, 2));
135895
135941
  }
135896
135942
  } catch {}
135943
+ } finally {
135944
+ if (planLockResult?.acquired && planLockResult.lock._release) {
135945
+ try {
135946
+ await planLockResult.lock._release();
135947
+ } catch (releaseError) {
135948
+ warn("[phase_complete] Plan lock release failed (non-blocking):", releaseError instanceof Error ? releaseError.message : String(releaseError));
135949
+ }
135950
+ }
135897
135951
  }
135898
135952
  }
135899
135953
  if (complianceWarnings.length > 0) {
@@ -143588,7 +143642,7 @@ import {
143588
143642
  readFileSync as readFileSync81,
143589
143643
  renameSync as renameSync24,
143590
143644
  unlinkSync as unlinkSync21,
143591
- writeFileSync as writeFileSync31
143645
+ writeFileSync as writeFileSync30
143592
143646
  } from "node:fs";
143593
143647
  import path181 from "node:path";
143594
143648
  init_create_tool();
@@ -143855,7 +143909,7 @@ function writePhaseCouncilEvidence(workingDir, synthesis, provenance) {
143855
143909
  };
143856
143910
  const tempFile = `${evidenceFile}.tmp-${Date.now()}`;
143857
143911
  try {
143858
- writeFileSync31(tempFile, JSON.stringify(evidenceBundle, null, 2), "utf-8");
143912
+ writeFileSync30(tempFile, JSON.stringify(evidenceBundle, null, 2), "utf-8");
143859
143913
  renameSync24(tempFile, evidenceFile);
143860
143914
  } finally {
143861
143915
  if (existsSync103(tempFile)) {
@@ -82,6 +82,12 @@ declare function getPlanJsonPath(directory: string): string;
82
82
  * Compute a SHA-256 hash of the plan state.
83
83
  * Uses deterministic JSON serialization for consistent hashing.
84
84
  *
85
+ * IMPORTANT: Intentionally excludes `specMtime` and `specHash` fields.
86
+ * These fields track changes to spec.md but do not affect plan execution or structure.
87
+ * Including them would cause the plan hash to change whenever spec metadata changes,
88
+ * invalidating cached plan state unnecessarily. Spec changes are tracked separately
89
+ * in the ledger via `spec_updated` and acknowledgment events.
90
+ *
85
91
  * @param plan - The plan to hash
86
92
  * @returns Hex-encoded SHA-256 hash
87
93
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.80.0",
3
+ "version": "7.81.1",
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",