opencode-swarm 7.33.0 → 7.33.2

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/index.js CHANGED
@@ -48,7 +48,7 @@ var package_default;
48
48
  var init_package = __esm(() => {
49
49
  package_default = {
50
50
  name: "opencode-swarm",
51
- version: "7.33.0",
51
+ version: "7.33.2",
52
52
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
53
53
  main: "dist/index.js",
54
54
  types: "dist/index.d.ts",
@@ -17370,6 +17370,32 @@ async function appendLedgerEvent(directory, eventInput, options) {
17370
17370
  fs4.renameSync(tempPath, ledgerPath);
17371
17371
  return event;
17372
17372
  }
17373
+ async function takeSnapshotWithRetry(directory, plan, options) {
17374
+ const MAX_RETRIES = 3;
17375
+ const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
17376
+ const telemetrySource = options?.source ?? "save_plan_tool";
17377
+ const snapshotOptions = { planHashAfter: options?.planHashAfter };
17378
+ let lastError;
17379
+ for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
17380
+ try {
17381
+ await takeSnapshotEvent(directory, plan, snapshotOptions);
17382
+ return;
17383
+ } catch (err2) {
17384
+ lastError = err2 instanceof Error ? err2 : new Error(String(err2));
17385
+ if (attempt < TOTAL_ATTEMPTS) {
17386
+ await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
17387
+ }
17388
+ }
17389
+ }
17390
+ console.warn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
17391
+ try {
17392
+ emit("snapshot_failed", {
17393
+ error: lastError.message,
17394
+ retries: MAX_RETRIES,
17395
+ source: telemetrySource
17396
+ });
17397
+ } catch {}
17398
+ }
17373
17399
  async function takeSnapshotEvent(directory, plan, options) {
17374
17400
  const payloadHash = computePlanHash(plan);
17375
17401
  const snapshotPayload = {
@@ -17603,6 +17629,7 @@ async function loadLastApprovedPlan(directory, expectedPlanId) {
17603
17629
  var LEDGER_SCHEMA_VERSION = "1.1.0", LEDGER_FILENAME = "plan-ledger.jsonl", PLAN_JSON_FILENAME = "plan.json", LedgerStaleWriterError;
17604
17630
  var init_ledger = __esm(() => {
17605
17631
  init_plan_schema();
17632
+ init_telemetry();
17606
17633
  LedgerStaleWriterError = class LedgerStaleWriterError extends Error {
17607
17634
  constructor(message) {
17608
17635
  super(message);
@@ -17796,7 +17823,9 @@ async function loadPlan(directory) {
17796
17823
  try {
17797
17824
  const rebuilt = await replayFromLedger(directory);
17798
17825
  if (rebuilt) {
17799
- await rebuildPlan(directory, rebuilt);
17826
+ await rebuildPlan(directory, rebuilt, {
17827
+ reason: "ledger_hash_mismatch_recovery"
17828
+ });
17800
17829
  warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at .swarm/SWARM_PLAN.md if it exists.");
17801
17830
  return rebuilt;
17802
17831
  }
@@ -17804,7 +17833,9 @@ async function loadPlan(directory) {
17804
17833
  try {
17805
17834
  const approved = await loadLastApprovedPlan(directory, currentPlanId);
17806
17835
  if (approved) {
17807
- await rebuildPlan(directory, approved.plan);
17836
+ await rebuildPlan(directory, approved.plan, {
17837
+ reason: "approved_snapshot_fallback"
17838
+ });
17808
17839
  try {
17809
17840
  await takeSnapshotEvent(directory, approved.plan, {
17810
17841
  source: "recovery_from_approved_snapshot",
@@ -17881,7 +17912,9 @@ async function loadPlan(directory) {
17881
17912
  } else if (catchFirstEvent !== null && rawPlanId !== null) {
17882
17913
  const rebuilt = await replayFromLedger(directory);
17883
17914
  if (rebuilt) {
17884
- await rebuildPlan(directory, rebuilt);
17915
+ await rebuildPlan(directory, rebuilt, {
17916
+ reason: "validation_failure_recovery"
17917
+ });
17885
17918
  warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
17886
17919
  return rebuilt;
17887
17920
  }
@@ -18242,12 +18275,9 @@ async function savePlan(directory, plan, options) {
18242
18275
  const SNAPSHOT_INTERVAL = 50;
18243
18276
  const latestSeq = await getLatestLedgerSeq(directory);
18244
18277
  if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
18245
- await takeSnapshotEvent(directory, validated, {
18246
- planHashAfter: hashAfter
18247
- }).catch((err2) => {
18248
- if (process.env.DEBUG_SWARM) {
18249
- warn(`[savePlan] Periodic snapshot write failed (non-fatal): ${err2 instanceof Error ? err2.message : String(err2)}`);
18250
- }
18278
+ await takeSnapshotWithRetry(directory, validated, {
18279
+ planHashAfter: hashAfter,
18280
+ source: "savePlan_manager"
18251
18281
  });
18252
18282
  }
18253
18283
  const swarmDir = path6.resolve(directory, ".swarm");
@@ -18261,6 +18291,17 @@ async function savePlan(directory, plan, options) {
18261
18291
  unlinkSync(tempPath);
18262
18292
  } catch {}
18263
18293
  }
18294
+ try {
18295
+ const markerPath = path6.join(swarmDir, ".plan-write-marker");
18296
+ const inProgressMarker = JSON.stringify({
18297
+ source: "plan_manager",
18298
+ timestamp: new Date().toISOString(),
18299
+ phases_count: validated.phases.length,
18300
+ tasks_count: validated.phases.reduce((sum, p) => sum + p.tasks.length, 0),
18301
+ in_progress: true
18302
+ });
18303
+ await bunWrite(markerPath, inProgressMarker);
18304
+ } catch {}
18264
18305
  try {
18265
18306
  const contentHash = computePlanContentHash(validated);
18266
18307
  const markdown = derivePlanMarkdown(validated);
@@ -18294,41 +18335,146 @@ ${markdown}`;
18294
18335
  source: "plan_manager",
18295
18336
  timestamp: new Date().toISOString(),
18296
18337
  phases_count: validated.phases.length,
18297
- tasks_count: tasksCount
18338
+ tasks_count: tasksCount,
18339
+ in_progress: false
18298
18340
  });
18299
18341
  await bunWrite(markerPath, marker);
18300
18342
  } catch {}
18301
18343
  }
18302
- async function rebuildPlan(directory, plan) {
18344
+ async function rebuildPlan(directory, plan, options) {
18303
18345
  const targetPlan = plan ?? await replayFromLedger(directory);
18304
18346
  if (!targetPlan)
18305
18347
  return null;
18306
18348
  const swarmDir = path6.join(directory, ".swarm");
18307
18349
  const planPath = path6.join(swarmDir, "plan.json");
18308
18350
  const mdPath = path6.join(swarmDir, "plan.md");
18309
- const tempPlanPath = path6.join(swarmDir, `plan.json.rebuild.${Date.now()}`);
18351
+ const tempPlanPath = path6.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
18310
18352
  await bunWrite(tempPlanPath, JSON.stringify(targetPlan, null, 2));
18311
18353
  renameSync3(tempPlanPath, planPath);
18312
- const contentHash = computePlanContentHash(targetPlan);
18313
- const markdown = derivePlanMarkdown(targetPlan);
18314
- const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
18315
- ${markdown}`;
18316
- const tempMdPath = path6.join(swarmDir, `plan.md.rebuild.${Date.now()}`);
18317
- await bunWrite(tempMdPath, markdownWithHash);
18318
- renameSync3(tempMdPath, mdPath);
18319
18354
  try {
18320
18355
  const markerPath = path6.join(swarmDir, ".plan-write-marker");
18321
- const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
18322
- const marker = JSON.stringify({
18356
+ const inProgressMarker = JSON.stringify({
18323
18357
  source: "plan_manager",
18324
18358
  timestamp: new Date().toISOString(),
18325
18359
  phases_count: targetPlan.phases.length,
18326
- tasks_count: tasksCount
18360
+ tasks_count: targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
18361
+ in_progress: true
18327
18362
  });
18328
- await bunWrite(markerPath, marker);
18363
+ await bunWrite(markerPath, inProgressMarker);
18364
+ } catch {}
18365
+ try {
18366
+ const contentHash = computePlanContentHash(targetPlan);
18367
+ const markdown = derivePlanMarkdown(targetPlan);
18368
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
18369
+ ${markdown}`;
18370
+ const tempMdPath = path6.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
18371
+ await bunWrite(tempMdPath, markdownWithHash);
18372
+ renameSync3(tempMdPath, mdPath);
18373
+ } finally {
18374
+ try {
18375
+ const markerPath = path6.join(swarmDir, ".plan-write-marker");
18376
+ const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
18377
+ const marker = JSON.stringify({
18378
+ source: "plan_manager",
18379
+ timestamp: new Date().toISOString(),
18380
+ phases_count: targetPlan.phases.length,
18381
+ tasks_count: tasksCount,
18382
+ in_progress: false
18383
+ });
18384
+ await bunWrite(markerPath, marker);
18385
+ } catch {}
18386
+ }
18387
+ try {
18388
+ const planId = derivePlanId(targetPlan);
18389
+ const planHashAfter = computePlanHash(targetPlan);
18390
+ await appendLedgerEvent(directory, {
18391
+ event_type: "plan_rebuilt",
18392
+ source: "rebuildPlan",
18393
+ plan_id: planId,
18394
+ payload: {
18395
+ reason: options?.reason ?? "ledger_replay_recovery",
18396
+ phases_count: targetPlan.phases.length,
18397
+ tasks_count: targetPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0)
18398
+ }
18399
+ }, { planHashAfter });
18329
18400
  } catch {}
18330
18401
  return targetPlan;
18331
18402
  }
18403
+ async function closePlanTerminalState(directory, plan, options) {
18404
+ const planId = derivePlanId(plan);
18405
+ const validated = PlanSchema.parse(plan);
18406
+ const hashAfter = computePlanHash(validated);
18407
+ for (const taskId of options.closedTaskIds) {
18408
+ let taskPhaseId;
18409
+ for (const phase of validated.phases) {
18410
+ if (phase.tasks.some((t) => t.id === taskId)) {
18411
+ taskPhaseId = phase.id;
18412
+ break;
18413
+ }
18414
+ }
18415
+ const fromStatus = options.originalStatuses?.get(taskId) ?? "in_progress";
18416
+ await appendLedgerEvent(directory, {
18417
+ plan_id: planId,
18418
+ event_type: "task_status_changed",
18419
+ task_id: taskId,
18420
+ phase_id: taskPhaseId,
18421
+ from_status: fromStatus,
18422
+ to_status: "closed",
18423
+ source: "close_terminal"
18424
+ }, { planHashAfter: hashAfter });
18425
+ }
18426
+ for (const phaseId of options.closedPhaseIds) {
18427
+ await appendLedgerEvent(directory, {
18428
+ plan_id: planId,
18429
+ event_type: "phase_completed",
18430
+ phase_id: phaseId,
18431
+ source: "close_terminal"
18432
+ }, { planHashAfter: hashAfter });
18433
+ }
18434
+ await takeSnapshotEvent(directory, validated, {
18435
+ planHashAfter: hashAfter,
18436
+ source: "close_terminal"
18437
+ });
18438
+ const swarmDir = path6.join(directory, ".swarm");
18439
+ const planPath = path6.join(swarmDir, "plan.json");
18440
+ const tempPlanPath = path6.join(swarmDir, `plan.json.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
18441
+ await bunWrite(tempPlanPath, JSON.stringify(validated, null, 2));
18442
+ renameSync3(tempPlanPath, planPath);
18443
+ try {
18444
+ const markerPath = path6.join(swarmDir, ".plan-write-marker");
18445
+ const inProgressMarker = JSON.stringify({
18446
+ source: "plan_manager_close",
18447
+ timestamp: new Date().toISOString(),
18448
+ phases_count: validated.phases.length,
18449
+ tasks_count: validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
18450
+ in_progress: true
18451
+ });
18452
+ await bunWrite(markerPath, inProgressMarker);
18453
+ } catch {}
18454
+ try {
18455
+ const mdPath = path6.join(swarmDir, "plan.md");
18456
+ const contentHash = computePlanContentHash(validated);
18457
+ const markdown = derivePlanMarkdown(validated);
18458
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
18459
+ ${markdown}`;
18460
+ const mdTempPath = path6.join(swarmDir, `plan.md.close.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
18461
+ await bunWrite(mdTempPath, markdownWithHash);
18462
+ renameSync3(mdTempPath, mdPath);
18463
+ } finally {
18464
+ try {
18465
+ const markerPath = path6.join(swarmDir, ".plan-write-marker");
18466
+ const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
18467
+ const marker = JSON.stringify({
18468
+ source: "plan_manager_close",
18469
+ timestamp: new Date().toISOString(),
18470
+ phases_count: validated.phases.length,
18471
+ tasks_count: tasksCount,
18472
+ in_progress: false
18473
+ });
18474
+ await bunWrite(markerPath, marker);
18475
+ } catch {}
18476
+ }
18477
+ }
18332
18478
  async function updateTaskStatus(directory, taskId, status) {
18333
18479
  const derivePhaseStatusFromTasks = (tasks) => {
18334
18480
  if (tasks.length > 0 && tasks.every((task) => task.status === "completed")) {
@@ -58844,6 +58990,12 @@ async function handleCloseCommand(directory, args2, options = {}) {
58844
58990
  }
58845
58991
  }
58846
58992
  if (planExists) {
58993
+ const originalStatuses = new Map;
58994
+ for (const phase of planData.phases ?? []) {
58995
+ for (const task of phase.tasks ?? []) {
58996
+ originalStatuses.set(task.id, task.status);
58997
+ }
58998
+ }
58847
58999
  const guaranteeResult = guaranteeAllPlansComplete(planData);
58848
59000
  for (const phaseId of guaranteeResult.closedPhaseIds) {
58849
59001
  if (!closedPhases.includes(phaseId)) {
@@ -58857,11 +59009,15 @@ async function handleCloseCommand(directory, args2, options = {}) {
58857
59009
  }
58858
59010
  if (!planAlreadyDone || guaranteeResult.closedPhaseIds.length > 0 || guaranteeResult.closedTaskIds.length > 0) {
58859
59011
  try {
58860
- await fs13.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
59012
+ await closePlanTerminalState(directory, planData, {
59013
+ closedPhaseIds: guaranteeResult.closedPhaseIds,
59014
+ closedTaskIds: guaranteeResult.closedTaskIds,
59015
+ originalStatuses
59016
+ });
58861
59017
  } catch (error93) {
58862
59018
  const msg = error93 instanceof Error ? error93.message : String(error93);
58863
- warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
58864
- console.warn("[close-command] Failed to write plan.json:", error93);
59019
+ warnings.push(`Failed to persist terminal plan state: ${msg}`);
59020
+ console.warn("[close-command] Failed to write terminal plan state:", error93);
58865
59021
  }
58866
59022
  }
58867
59023
  }
@@ -59149,6 +59305,7 @@ var init_close = __esm(() => {
59149
59305
  init_knowledge_curator();
59150
59306
  init_knowledge_store();
59151
59307
  init_utils2();
59308
+ init_manager();
59152
59309
  init_scope_persistence();
59153
59310
  init_skill_improver();
59154
59311
  init_state();
@@ -59386,14 +59543,16 @@ async function parseGitLog(directory, maxCommits) {
59386
59543
  }
59387
59544
  return commitMap;
59388
59545
  }
59389
- function buildCoChangeMatrix(commitMap) {
59546
+ function buildCoChangeMatrix(commitMap, maxFilesPerCommit = 500) {
59390
59547
  const matrix = new Map;
59391
59548
  const fileCommitCount = new Map;
59392
59549
  for (const files of commitMap.values()) {
59393
- const fileArray = Array.from(files).sort();
59394
- for (const file3 of fileArray) {
59550
+ for (const file3 of files) {
59395
59551
  fileCommitCount.set(file3, (fileCommitCount.get(file3) || 0) + 1);
59396
59552
  }
59553
+ if (files.size > maxFilesPerCommit)
59554
+ continue;
59555
+ const fileArray = Array.from(files).sort();
59397
59556
  for (let i2 = 0;i2 < fileArray.length; i2++) {
59398
59557
  for (let j = i2 + 1;j < fileArray.length; j++) {
59399
59558
  const fileA = fileArray[i2];
@@ -59556,6 +59715,7 @@ async function detectDarkMatter(directory, options) {
59556
59715
  const minCoChanges = options?.minCoChanges ?? 3;
59557
59716
  const npmiThreshold = options?.npmiThreshold ?? 0.5;
59558
59717
  const maxCommitsToAnalyze = options?.maxCommitsToAnalyze ?? 500;
59718
+ const maxFilesPerCommit = options?.maxFilesPerCommit ?? 500;
59559
59719
  try {
59560
59720
  const { stdout } = await getExecFileAsync()("git", ["rev-list", "--count", "HEAD"], {
59561
59721
  cwd: directory,
@@ -59569,7 +59729,7 @@ async function detectDarkMatter(directory, options) {
59569
59729
  return [];
59570
59730
  }
59571
59731
  const commitMap = await _internals18.parseGitLog(directory, maxCommitsToAnalyze);
59572
- const matrix = _internals18.buildCoChangeMatrix(commitMap);
59732
+ const matrix = _internals18.buildCoChangeMatrix(commitMap, maxFilesPerCommit);
59573
59733
  const staticEdges = await _internals18.getStaticEdges(directory);
59574
59734
  const results = [];
59575
59735
  for (const entry of matrix.values()) {
@@ -59891,6 +60051,120 @@ function getPluginCachePaths() {
59891
60051
  }
59892
60052
  var init_cache_paths = () => {};
59893
60053
 
60054
+ // src/evidence/gate-bridge.ts
60055
+ async function readDurableGateEvidence(directory, taskId) {
60056
+ try {
60057
+ return await readTaskEvidence(directory, taskId);
60058
+ } catch {
60059
+ return null;
60060
+ }
60061
+ }
60062
+ function getDurableGateEvidenceStatus(evidence) {
60063
+ if (!evidence?.gates || typeof evidence.gates !== "object") {
60064
+ return {
60065
+ isComplete: false,
60066
+ missingGates: [],
60067
+ evidenceExists: evidence != null,
60068
+ invalid: false
60069
+ };
60070
+ }
60071
+ if (!Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
60072
+ return {
60073
+ isComplete: false,
60074
+ missingGates: ["required_gates"],
60075
+ evidenceExists: true,
60076
+ invalid: false
60077
+ };
60078
+ }
60079
+ const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
60080
+ return {
60081
+ isComplete: missingGates.length === 0,
60082
+ missingGates,
60083
+ evidenceExists: true,
60084
+ invalid: false
60085
+ };
60086
+ }
60087
+ async function getDurableGateEvidenceStatusForTask(directory, taskId) {
60088
+ if (!isValidTaskId(taskId)) {
60089
+ return {
60090
+ isComplete: false,
60091
+ missingGates: [],
60092
+ evidenceExists: false,
60093
+ invalid: false
60094
+ };
60095
+ }
60096
+ try {
60097
+ return getDurableGateEvidenceStatus(readTaskEvidenceRaw(directory, taskId));
60098
+ } catch {
60099
+ return {
60100
+ isComplete: false,
60101
+ missingGates: ["invalid_gate_evidence"],
60102
+ evidenceExists: true,
60103
+ invalid: true
60104
+ };
60105
+ }
60106
+ }
60107
+ function gateEvidenceToEntry(taskId, gate, type, evidence) {
60108
+ const gateRecord = evidence.gates[gate];
60109
+ if (!gateRecord) {
60110
+ return null;
60111
+ }
60112
+ const base = {
60113
+ task_id: taskId,
60114
+ timestamp: gateRecord.timestamp,
60115
+ agent: gateRecord.agent || gate,
60116
+ verdict: "pass",
60117
+ summary: `Gate evidence recorded by ${gate}`,
60118
+ metadata: { source: "durable_gate_evidence", gate }
60119
+ };
60120
+ if (type === "review") {
60121
+ return {
60122
+ ...base,
60123
+ type,
60124
+ risk: "low",
60125
+ issues: []
60126
+ };
60127
+ }
60128
+ if (type === "approval") {
60129
+ return {
60130
+ ...base,
60131
+ type
60132
+ };
60133
+ }
60134
+ return {
60135
+ ...base,
60136
+ type,
60137
+ tests_passed: 0,
60138
+ tests_failed: 0,
60139
+ failures: []
60140
+ };
60141
+ }
60142
+ function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
60143
+ if (!evidence?.gates) {
60144
+ return entries;
60145
+ }
60146
+ const merged = [...entries];
60147
+ for (const gate of Object.keys(evidence.gates).sort()) {
60148
+ const type = GATE_EVIDENCE_TYPE_BY_GATE[gate] ?? "approval";
60149
+ if ((type === "review" || type === "test") && merged.some((entry2) => entry2.type === type)) {
60150
+ continue;
60151
+ }
60152
+ const entry = gateEvidenceToEntry(taskId, gate, type, evidence);
60153
+ if (entry) {
60154
+ merged.push(entry);
60155
+ }
60156
+ }
60157
+ return merged;
60158
+ }
60159
+ var GATE_EVIDENCE_TYPE_BY_GATE;
60160
+ var init_gate_bridge = __esm(() => {
60161
+ init_gate_evidence();
60162
+ GATE_EVIDENCE_TYPE_BY_GATE = {
60163
+ reviewer: "review",
60164
+ test_engineer: "test"
60165
+ };
60166
+ });
60167
+
59894
60168
  // src/services/version-check.ts
59895
60169
  import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
59896
60170
  import { homedir as homedir5 } from "node:os";
@@ -60038,7 +60312,21 @@ async function checkEvidenceCompleteness(directory, plan) {
60038
60312
  }
60039
60313
  if (completedTaskIds.length > 0) {
60040
60314
  const evidenceTaskIds = new Set(await listEvidenceTaskIds(directory));
60041
- const missingEvidence = completedTaskIds.filter((id) => !evidenceTaskIds.has(id));
60315
+ const missingEvidence = [];
60316
+ for (const id of completedTaskIds) {
60317
+ const gateStatus = await getDurableGateEvidenceStatusForTask(directory, id);
60318
+ if (gateStatus.isComplete) {
60319
+ continue;
60320
+ }
60321
+ if (gateStatus.evidenceExists && gateStatus.missingGates.length > 0) {
60322
+ missingEvidence.push(id);
60323
+ continue;
60324
+ }
60325
+ if (evidenceTaskIds.has(id)) {
60326
+ continue;
60327
+ }
60328
+ missingEvidence.push(id);
60329
+ }
60042
60330
  if (missingEvidence.length === 0) {
60043
60331
  return {
60044
60332
  name: "Evidence",
@@ -60793,6 +61081,7 @@ var init_diagnose_service = __esm(() => {
60793
61081
  init_package();
60794
61082
  init_cache_paths();
60795
61083
  init_loader();
61084
+ init_gate_bridge();
60796
61085
  init_manager2();
60797
61086
  init_utils2();
60798
61087
  init_manager();
@@ -63359,8 +63648,7 @@ function getTaskStatus(task, bundle) {
63359
63648
  }
63360
63649
  return "pending";
63361
63650
  }
63362
- function isEvidenceComplete(bundle) {
63363
- const entries = _internals20.normalizeBundleEntries(bundle);
63651
+ function evidenceCompleteFromEntries(entries) {
63364
63652
  if (entries.length === 0) {
63365
63653
  return {
63366
63654
  isComplete: false,
@@ -63379,6 +63667,9 @@ function isEvidenceComplete(bundle) {
63379
63667
  missingEvidence: missing
63380
63668
  };
63381
63669
  }
63670
+ function isEvidenceComplete(bundle) {
63671
+ return evidenceCompleteFromEntries(_internals20.normalizeBundleEntries(bundle));
63672
+ }
63382
63673
  function getTaskBlockers(task, summary, status) {
63383
63674
  const blockers = [];
63384
63675
  if (task?.blocked_reason) {
@@ -63395,11 +63686,19 @@ function getTaskBlockers(task, summary, status) {
63395
63686
  async function buildTaskSummary(directory, task, taskId) {
63396
63687
  const result = await loadEvidence(directory, taskId);
63397
63688
  const bundle = result.status === "found" ? result.bundle : null;
63689
+ const gateEvidence = await readDurableGateEvidence(directory, taskId);
63398
63690
  const phase = task?.phase ?? 0;
63399
63691
  const status = _internals20.getTaskStatus(task, bundle);
63400
- const evidenceCheck = _internals20.isEvidenceComplete(bundle);
63692
+ const entries = mergeDurableGateEntriesFromEvidence(taskId, _internals20.normalizeBundleEntries(bundle), gateEvidence);
63693
+ let evidenceCheck = _internals20.evidenceCompleteFromEntries(entries);
63694
+ if (gateEvidence) {
63695
+ const gateStatus = getDurableGateEvidenceStatus(gateEvidence);
63696
+ evidenceCheck = gateStatus.isComplete ? { isComplete: true, missingEvidence: [] } : {
63697
+ isComplete: false,
63698
+ missingEvidence: gateStatus.missingGates.map((gate) => `gate:${gate}`)
63699
+ };
63700
+ }
63401
63701
  const blockers = _internals20.getTaskBlockers(task, evidenceCheck, status);
63402
- const entries = _internals20.normalizeBundleEntries(bundle);
63403
63702
  const hasReview = entries.some((e) => e.type === "review");
63404
63703
  const hasTest = entries.some((e) => e.type === "test");
63405
63704
  const hasApproval = entries.some((e) => e.type === "approval");
@@ -63577,6 +63876,7 @@ function isAutoSummaryEnabled(automationConfig) {
63577
63876
  }
63578
63877
  var VALID_EVIDENCE_TYPES2, REQUIRED_EVIDENCE_TYPES, EVIDENCE_SUMMARY_VERSION = "1.0.0", _internals20;
63579
63878
  var init_evidence_summary_service = __esm(() => {
63879
+ init_gate_bridge();
63580
63880
  init_manager2();
63581
63881
  init_manager();
63582
63882
  init_utils();
@@ -63594,6 +63894,7 @@ var init_evidence_summary_service = __esm(() => {
63594
63894
  isAutoSummaryEnabled,
63595
63895
  normalizeBundleEntries,
63596
63896
  getTaskStatus,
63897
+ evidenceCompleteFromEntries,
63597
63898
  isEvidenceComplete,
63598
63899
  getTaskBlockers,
63599
63900
  buildTaskSummary,
@@ -71204,7 +71505,22 @@ async function runEvidenceCheck(dir) {
71204
71505
  };
71205
71506
  }
71206
71507
  const evidenceTaskIds = new Set(await listEvidenceTaskIds(dir));
71207
- const missingEvidence = completedTaskIds.filter((id) => !evidenceTaskIds.has(id));
71508
+ const missingEvidence = [];
71509
+ for (const id of completedTaskIds) {
71510
+ const gateStatus = await getDurableGateEvidenceStatusForTask(dir, id);
71511
+ if (gateStatus.isComplete) {
71512
+ continue;
71513
+ }
71514
+ if (gateStatus.evidenceExists && gateStatus.missingGates.length > 0) {
71515
+ missingEvidence.push(id);
71516
+ continue;
71517
+ }
71518
+ if (evidenceTaskIds.has(id)) {
71519
+ continue;
71520
+ }
71521
+ missingEvidence.push(id);
71522
+ }
71523
+ const completedWithEvidence = completedTaskIds.length - missingEvidence.length;
71208
71524
  if (missingEvidence.length > 0) {
71209
71525
  return {
71210
71526
  type: "evidence",
@@ -71212,7 +71528,7 @@ async function runEvidenceCheck(dir) {
71212
71528
  message: `${missingEvidence.length} completed task(s) missing evidence`,
71213
71529
  details: {
71214
71530
  totalCompleted: completedTaskIds.length,
71215
- totalWithEvidence: evidenceTaskIds.size,
71531
+ totalWithEvidence: completedWithEvidence,
71216
71532
  missingTasks: missingEvidence.slice(0, 10),
71217
71533
  missingCount: missingEvidence.length
71218
71534
  },
@@ -71225,7 +71541,7 @@ async function runEvidenceCheck(dir) {
71225
71541
  message: `All ${completedTaskIds.length} completed tasks have evidence`,
71226
71542
  details: {
71227
71543
  totalCompleted: completedTaskIds.length,
71228
- totalWithEvidence: evidenceTaskIds.size
71544
+ totalWithEvidence: completedWithEvidence
71229
71545
  },
71230
71546
  durationMs: Date.now() - startTime
71231
71547
  };
@@ -71456,6 +71772,7 @@ async function handlePreflightCommand(directory, _args) {
71456
71772
  }
71457
71773
  var MIN_CHECK_TIMEOUT_MS = 5000, MAX_CHECK_TIMEOUT_MS = 300000, DEFAULT_CONFIG, _internals34;
71458
71774
  var init_preflight_service = __esm(() => {
71775
+ init_gate_bridge();
71459
71776
  init_manager2();
71460
71777
  init_manager();
71461
71778
  init_lint();
@@ -76401,121 +76718,14 @@ Do NOT share other agents' responses at this stage.
76401
76718
  ### MODE: DEEP_DIVE
76402
76719
  Activates when: architect receives \`[MODE: DEEP_DIVE profile=X max_explorers=N output=X update_main=X allow_dirty=X] <scope>\` signal from the deep-dive command handler.
76403
76720
 
76404
- Purpose: Perform a read-only deep audit of the specified codebase scope using parallel explorer waves, always 2 parallel reviewers, and sequential critic challenge. This mode does NOT mutate source code, does NOT delegate to coder, and does NOT call declare_scope.
76405
-
76406
- #### STEP 0 PARSE HEADER
76407
- Parse the MODE: DEEP_DIVE header to extract:
76408
- - \`scope\`: the codebase area to audit (e.g., "auth", "payment flow", "src/hooks/")
76409
- - \`profile\`: one of standard | security | ux | architecture | full (default: standard)
76410
- - \`max_explorers\`: integer 1..8 (default: 6, or 8 for full profile)
76411
- - \`output\`: markdown | json (default: markdown)
76412
- - \`update_main\`: boolean (default: true) — whether to fetch/ff-only main before starting
76413
- - \`allow_dirty\`: boolean (default: false) — whether to proceed with uncommitted changes
76414
-
76415
- If the header is malformed or missing required fields, report the error and stop.
76416
-
76417
- #### STEP 1 — REPO READINESS
76418
- 1. Check git working tree status. If dirty and \`allow_dirty\` is false, warn the user and ask whether to proceed. Do NOT proceed automatically.
76419
- 2. If \`update_main\` is true and tree is clean: check current branch. If not on \`main\`, report current branch to user and ASK FOR CONFIRMATION before switching. Only after explicit user approval: \`git fetch origin main && git checkout main && git merge --ff-only origin/main\`. If ff-only fails, warn the user and ask before proceeding.
76420
- 3. Record the current HEAD commit hash for the report.
76421
-
76422
- #### STEP 2 — SCOPE RESOLUTION
76423
- Use the following tools to map the audit scope:
76424
- 1. \`repo_map\` with action "build" to establish the code graph
76425
- 2. \`repo_map\` with action "localization" for the scope target
76426
- 3. \`symbols\` and \`batch_symbols\` on key files identified by localization
76427
- 4. \`imports\` to trace dependency boundaries
76428
- 5. \`doc_scan\` if documentation coverage is relevant
76429
- 6. \`knowledge_recall\` with query matching the scope domain
76430
-
76431
- Produce a SCOPE MAP: list of files, modules, and interfaces within the audit boundary. Cap at 50 files total.
76432
-
76433
- #### STEP 3 — EXPLORER MISSIONS (Parallel Waves)
76434
- Dispatch explorer waves using parallel Task calls. Each wave contains up to \`max_explorers\` missions.
76435
-
76436
- **File caps per mission:**
76437
- - 8 files maximum per mission
76438
- - ~3500 total lines across all files in a mission
76439
- - Group files by import proximity (files that import each other go in the same mission)
76440
-
76441
- **Profile-based lane selection — each profile activates specific lanes:**
76442
-
76443
- | Lane | Template | standard | security | ux | architecture | full |
76444
- |------|----------|----------|----------|----|-------------|------|
76445
- | SCOPE_MAP | Map structure, exports, boundaries | ✓ | ✓ | ✓ | ✓ | ✓ |
76446
- | WIRING_DATAFLOW | Trace data flow, API contracts, state propagation | ✓ | ✓ | | ✓ | ✓ |
76447
- | RUNTIME_BEHAVIOR | Error handling, edge cases, lifecycle, async patterns | ✓ | | | ✓ | ✓ |
76448
- | UX_FLOW | User-facing behavior, accessibility, responsiveness | | | ✓ | | ✓ |
76449
- | SECURITY_TRUST | Auth boundaries, input validation, trust transitions | | ✓ | | | ✓ |
76450
- | TEST_COVERAGE | Coverage gaps, flaky tests, missing assertions | ✓ | | | | ✓ |
76451
- | PERFORMANCE_RELIABILITY | Resource leaks, N+1 queries, race conditions | | | | ✓ | ✓ |
76452
- | DOCS_CONFIG_DEPLOYMENT | Config consistency, docs accuracy, deployment drift | | | | | ✓ |
76453
-
76454
- Each explorer mission receives:
76455
- - Lane template name and description
76456
- - Assigned files (8 max, grouped by import proximity)
76457
- - The scope map context from Step 2
76458
- - Instruction: "You are performing a [LANE] audit. Report findings as candidate observations with severity (INFO/LOW/MEDIUM/HIGH/CRITICAL), location, and evidence."
76459
-
76460
- Explorer missions are dispatched in parallel waves. Wait for ALL missions in a wave to complete before dispatching the next wave.
76461
-
76462
- Explorers generate CANDIDATE FINDINGS only — they do NOT make verdicts. All findings are unverified until Step 5.
76463
-
76464
- #### STEP 4 — NORMALIZE CANDIDATES
76465
- 1. Collect all candidate findings from all explorer missions.
76466
- 2. Deduplicate: merge findings that reference the same location and issue.
76467
- 3. Assign DD-C001 through DD-CNNN identifiers to unique findings.
76468
- 4. Cap at 10 findings per shard (see Step 5 for sharding).
76469
- 5. Sort by severity (CRITICAL → HIGH → MEDIUM → LOW → INFO).
76470
-
76471
- #### STEP 5 — ALWAYS 2 PARALLEL REVIEWERS
76472
- Split the verified candidates into 2 shards of ≤10 candidates each. Dispatch 2 parallel \`{{AGENT_PREFIX}}reviewer\` calls.
76473
-
76474
- Each reviewer receives:
76475
- - Their shard of candidates (up to 10)
76476
- - The scope map context
76477
- - The original scope description
76478
- - Instruction: "Verify or reject each candidate finding. For each: verdict (VERIFIED / REJECTED / NEEDS_MORE_EVIDENCE), confidence (0-1), and brief reasoning."
76479
-
76480
- Reviewers MUST NOT suggest fixes — they verify findings only.
76481
-
76482
- #### STEP 5b — REVIEWER MERGE/DEDUP
76483
- After both reviewers return, perform a lightweight sync pass:
76484
- 1. Cross-reference findings between reviewers — flag correlations
76485
- 2. Deduplicate any findings both reviewers verified independently
76486
- 3. For NEEDS_MORE_EVIDENCE findings: if the other reviewer verified a related finding, merge
76487
- 4. Produce a unified findings list with verified/rejected status
76488
-
76489
- #### STEP 6 — CRITIC CHALLENGE (HIGH/CRITICAL only)
76490
- For verified findings rated HIGH or CRITICAL, dispatch sequential critic passes:
76491
-
76492
- **Pass 1 — False-positive / root-cause challenge:**
76493
- - \`{{AGENT_PREFIX}}critic\` receives each HIGH/CRITICAL finding
76494
- - Challenge: "Is this a false positive? Is the root cause correctly identified? Provide verdict: SURVIVES / DOWNGRADE / REJECT"
76495
- - Only findings that SURVIVE proceed to Pass 2
76496
-
76497
- **Pass 2 — Impact / severity challenge:**
76498
- - \`{{AGENT_PREFIX}}critic\` receives surviving findings
76499
- - Challenge: "Is the severity correctly rated? Could this be lower impact than claimed? Provide verdict: SURVIVES / DOWNGRADE / REJECT"
76500
- - Final severity is the critic's assessed severity
76501
-
76502
- CRITICAL: Do NOT challenge MEDIUM/LOW/INFO findings. Only HIGH and CRITICAL go through critic review.
76503
-
76504
- #### STEP 7 — FINAL REPORT
76505
- Assemble and present the audit report:
76506
-
76507
- 1. **Wiring Map**: Visual summary of the scope's module structure and data flow
76508
- 2. **Functionality Assessment**: High-level summary of what the scope does and how well
76509
- 3. **Verified Findings Table**: DD-ID, severity, location, description, evidence
76510
- 4. **Rejected Candidates**: Brief list with rejection reasons
76511
- 5. **Enhancements**: Non-blocking improvement suggestions
76512
- 6. **Recommended Implementation Phases**: If findings suggest follow-up work, outline phases
76513
- 7. **JSON Block** (when output=json): Structured machine-readable findings
76514
-
76515
- IMPORTANT CONSTRAINTS for MODE: DEEP_DIVE:
76516
- - Do NOT mutate source code under any circumstances
76721
+ Purpose: Read-only deep audit of the specified codebase scope using parallel explorer waves, always 2 parallel reviewers, and sequential critic challenge. This mode does NOT mutate source code, does NOT delegate to coder, and does NOT call declare_scope.
76722
+
76723
+ ACTION: Load skill \`file:.opencode/skills/deep-dive/SKILL.md\` immediately and follow its protocol.
76724
+
76725
+ HARD CONSTRAINTS (apply regardless of skill load success):
76517
76726
  - Do NOT delegate to coder
76518
76727
  - Do NOT call declare_scope
76728
+ - Do NOT mutate source code
76519
76729
  - Do NOT create or modify any files outside .swarm/
76520
76730
  - No final finding may appear in the report without reviewer verification
76521
76731
  - Explorers generate candidate findings only — reviewers verify or reject
@@ -84996,6 +85206,10 @@ class PlanSyncWorker {
84996
85206
  const planMtimeMs = Math.floor(planStats.mtimeMs);
84997
85207
  const markerContent = fs37.readFileSync(markerPath, "utf8");
84998
85208
  const marker = JSON.parse(markerContent);
85209
+ if (marker.in_progress === true) {
85210
+ log("[PlanSyncWorker] Skipping unauthorized-write check - plan write in progress");
85211
+ return;
85212
+ }
84999
85213
  const markerTimestampMs = new Date(marker.timestamp).getTime();
85000
85214
  if (planMtimeMs > markerTimestampMs + 5000) {
85001
85215
  log("[PlanSyncWorker] WARNING: plan.json may have been written outside save_plan/savePlan - unauthorized direct write suspected", { planMtimeMs, markerTimestampMs });
@@ -91802,10 +92016,13 @@ function createSystemEnhancerHook(config3, directory) {
91802
92016
  minCommits: 20,
91803
92017
  minCoChanges: 3
91804
92018
  });
91805
- if (darkMatter && darkMatter.length > 0) {
91806
- const darkMatterReport = formatDarkMatterOutput2(darkMatter);
91807
- await fs54.promises.writeFile(darkMatterPath, darkMatterReport, "utf-8");
91808
- warn(`[system-enhancer] Dark matter scan complete: ${darkMatter.length} co-change patterns found`);
92019
+ await fs54.promises.mkdir(path81.dirname(darkMatterPath), {
92020
+ recursive: true
92021
+ });
92022
+ const darkMatterReport = formatDarkMatterOutput2(darkMatter);
92023
+ await fs54.promises.writeFile(darkMatterPath, darkMatterReport, "utf-8");
92024
+ warn(`[system-enhancer] Dark matter scan complete: ${darkMatter.length} co-change patterns found`);
92025
+ if (darkMatter.length > 0) {
91809
92026
  try {
91810
92027
  const projectName = path81.basename(path81.resolve(directory));
91811
92028
  const knowledgeEntries = darkMatterToKnowledgeEntries2(darkMatter, projectName);
@@ -112516,6 +112733,9 @@ init_ledger();
112516
112733
  init_manager();
112517
112734
  init_state();
112518
112735
  init_create_tool();
112736
+ function executionProfilesEqual(a, b) {
112737
+ return a.parallelization_enabled === b.parallelization_enabled && a.max_concurrent_tasks === b.max_concurrent_tasks && a.council_parallel === b.council_parallel && a.locked === b.locked;
112738
+ }
112519
112739
  function detectPlaceholderContent(args2) {
112520
112740
  const issues = [];
112521
112741
  const placeholderPattern = /^\[\w[\w\s]*\]$/;
@@ -112680,18 +112900,51 @@ async function executeSavePlan(args2, fallbackDir) {
112680
112900
  }
112681
112901
  }
112682
112902
  }
112683
- if (existing.execution_profile?.locked) {
112684
- if (args2.execution_profile !== undefined && !args2.reset_statuses) {
112903
+ if (args2.confirm_identity_change !== true) {
112904
+ const existingId = derivePlanId(existing);
112905
+ const incomingId = derivePlanId({
112906
+ swarm: args2.swarm_id,
112907
+ title: args2.title
112908
+ });
112909
+ if (existingId !== incomingId) {
112685
112910
  return {
112686
112911
  success: false,
112687
- message: "EXECUTION_PROFILE_LOCKED: The execution_profile for this plan is locked and cannot be changed.",
112912
+ message: "PLAN_IDENTITY_MISMATCH: The incoming plan identity does not match the existing plan. " + "To overwrite with a new identity, set confirm_identity_change: true.",
112688
112913
  errors: [
112689
- "execution_profile.locked is true — to change the profile you must first unlock it via a separate plan revision that explicitly sets locked: false, or reset the plan with reset_statuses."
112914
+ `Existing plan identity: ${existingId} (swarm: "${existing.swarm}", title: "${existing.title}")`,
112915
+ `Incoming plan identity: ${incomingId} (swarm: "${args2.swarm_id}", title: "${args2.title}")`
112690
112916
  ],
112691
- recovery_guidance: "Remove the execution_profile field from this save_plan call to preserve the locked profile, " + "or use reset_statuses: true to start fresh (this clears the lock). " + "Never modify execution_profile directly in plan.json."
112917
+ recovery_guidance: "Verify the title and swarm_id match the intended plan. " + "If the identity change is intentional, retry with confirm_identity_change: true. " + "Never write .swarm/plan.json or .swarm/plan.md directly."
112692
112918
  };
112693
112919
  }
112694
- if (!args2.reset_statuses) {
112920
+ }
112921
+ if (existing.execution_profile?.locked) {
112922
+ if (args2.execution_profile !== undefined && !args2.reset_statuses) {
112923
+ const requestedProfile = ExecutionProfileSchema.safeParse({
112924
+ ...existing.execution_profile,
112925
+ ...args2.execution_profile
112926
+ });
112927
+ if (!requestedProfile.success) {
112928
+ return {
112929
+ success: false,
112930
+ message: "Invalid execution_profile: schema validation failed",
112931
+ errors: requestedProfile.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`),
112932
+ recovery_guidance: "Check execution_profile fields: parallelization_enabled (boolean), " + "max_concurrent_tasks (integer 1-64), council_parallel (boolean), locked (boolean)."
112933
+ };
112934
+ }
112935
+ if (executionProfilesEqual(existing.execution_profile, requestedProfile.data)) {
112936
+ preservedExecutionProfile = existing.execution_profile;
112937
+ } else {
112938
+ return {
112939
+ success: false,
112940
+ message: "EXECUTION_PROFILE_LOCKED: The execution_profile for this plan is locked and cannot be changed.",
112941
+ errors: [
112942
+ "execution_profile.locked is true — to change the profile you must first unlock it via a separate plan revision that explicitly sets locked: false, or reset the plan with reset_statuses."
112943
+ ],
112944
+ recovery_guidance: "Remove the execution_profile field from this save_plan call to preserve the locked profile, " + "or use reset_statuses: true to start fresh (this clears the lock). " + "Never modify execution_profile directly in plan.json."
112945
+ };
112946
+ }
112947
+ } else if (!args2.reset_statuses) {
112695
112948
  preservedExecutionProfile = existing.execution_profile;
112696
112949
  }
112697
112950
  } else {
@@ -112863,7 +113116,7 @@ async function executeSavePlan(args2, fallbackDir) {
112863
113116
  });
112864
113117
  const savedPlan = await loadPlanJsonOnly(dir);
112865
113118
  if (savedPlan) {
112866
- await takeSnapshotEvent(dir, savedPlan).catch(() => {});
113119
+ await takeSnapshotWithRetry(dir, savedPlan);
112867
113120
  }
112868
113121
  if (resolvedProfile !== undefined && savedPlan) {
112869
113122
  const planId = derivePlanId(plan);
@@ -112961,6 +113214,7 @@ var save_plan = createSwarmTool({
112961
113214
  removed_task_ids: exports_external.array(exports_external.string()).optional().describe("Task IDs that are present in the prior plan but intentionally being " + "removed by this save. Every task missing from `phases` MUST be enumerated " + "here, otherwise save_plan rejects with PLAN_TASK_REMOVAL_NOT_ACKNOWLEDGED. " + "Tasks not yet finished (status pending/in_progress/blocked) MUST NOT be " + "removed without explicit user confirmation."),
112962
113215
  removal_reason: exports_external.string().optional().describe("Required when removed_task_ids is non-empty. Human-readable reason recorded " + "on each task_removed ledger event."),
112963
113216
  confirm_destructive_reset: exports_external.boolean().optional().describe("Required when reset_statuses is true AND at least one task is missing from " + "the new plan. Set true to acknowledge that the destructive reset drops " + "unfinished work. When set together with reset_statuses, save_plan auto-" + "populates removed_task_ids from the missing set."),
113217
+ confirm_identity_change: exports_external.boolean().optional().describe("When true, allows overwriting an existing plan that has a different " + "identity (swarm_id + title). Without this flag, save_plan rejects " + "with PLAN_IDENTITY_MISMATCH if the identity differs."),
112964
113218
  execution_profile: exports_external.object({
112965
113219
  parallelization_enabled: exports_external.boolean().optional().describe("When true, enables parallel task dispatch for this plan. Default false (serial)."),
112966
113220
  max_concurrent_tasks: exports_external.number().int().min(1).max(64).optional().describe("Maximum tasks that may run concurrently when parallelization is enabled. Default 1."),