opencode-swarm 7.33.0 → 7.33.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.
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.1",
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();
@@ -59891,6 +60048,120 @@ function getPluginCachePaths() {
59891
60048
  }
59892
60049
  var init_cache_paths = () => {};
59893
60050
 
60051
+ // src/evidence/gate-bridge.ts
60052
+ async function readDurableGateEvidence(directory, taskId) {
60053
+ try {
60054
+ return await readTaskEvidence(directory, taskId);
60055
+ } catch {
60056
+ return null;
60057
+ }
60058
+ }
60059
+ function getDurableGateEvidenceStatus(evidence) {
60060
+ if (!evidence?.gates || typeof evidence.gates !== "object") {
60061
+ return {
60062
+ isComplete: false,
60063
+ missingGates: [],
60064
+ evidenceExists: evidence != null,
60065
+ invalid: false
60066
+ };
60067
+ }
60068
+ if (!Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
60069
+ return {
60070
+ isComplete: false,
60071
+ missingGates: ["required_gates"],
60072
+ evidenceExists: true,
60073
+ invalid: false
60074
+ };
60075
+ }
60076
+ const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
60077
+ return {
60078
+ isComplete: missingGates.length === 0,
60079
+ missingGates,
60080
+ evidenceExists: true,
60081
+ invalid: false
60082
+ };
60083
+ }
60084
+ async function getDurableGateEvidenceStatusForTask(directory, taskId) {
60085
+ if (!isValidTaskId(taskId)) {
60086
+ return {
60087
+ isComplete: false,
60088
+ missingGates: [],
60089
+ evidenceExists: false,
60090
+ invalid: false
60091
+ };
60092
+ }
60093
+ try {
60094
+ return getDurableGateEvidenceStatus(readTaskEvidenceRaw(directory, taskId));
60095
+ } catch {
60096
+ return {
60097
+ isComplete: false,
60098
+ missingGates: ["invalid_gate_evidence"],
60099
+ evidenceExists: true,
60100
+ invalid: true
60101
+ };
60102
+ }
60103
+ }
60104
+ function gateEvidenceToEntry(taskId, gate, type, evidence) {
60105
+ const gateRecord = evidence.gates[gate];
60106
+ if (!gateRecord) {
60107
+ return null;
60108
+ }
60109
+ const base = {
60110
+ task_id: taskId,
60111
+ timestamp: gateRecord.timestamp,
60112
+ agent: gateRecord.agent || gate,
60113
+ verdict: "pass",
60114
+ summary: `Gate evidence recorded by ${gate}`,
60115
+ metadata: { source: "durable_gate_evidence", gate }
60116
+ };
60117
+ if (type === "review") {
60118
+ return {
60119
+ ...base,
60120
+ type,
60121
+ risk: "low",
60122
+ issues: []
60123
+ };
60124
+ }
60125
+ if (type === "approval") {
60126
+ return {
60127
+ ...base,
60128
+ type
60129
+ };
60130
+ }
60131
+ return {
60132
+ ...base,
60133
+ type,
60134
+ tests_passed: 0,
60135
+ tests_failed: 0,
60136
+ failures: []
60137
+ };
60138
+ }
60139
+ function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
60140
+ if (!evidence?.gates) {
60141
+ return entries;
60142
+ }
60143
+ const merged = [...entries];
60144
+ for (const gate of Object.keys(evidence.gates).sort()) {
60145
+ const type = GATE_EVIDENCE_TYPE_BY_GATE[gate] ?? "approval";
60146
+ if ((type === "review" || type === "test") && merged.some((entry2) => entry2.type === type)) {
60147
+ continue;
60148
+ }
60149
+ const entry = gateEvidenceToEntry(taskId, gate, type, evidence);
60150
+ if (entry) {
60151
+ merged.push(entry);
60152
+ }
60153
+ }
60154
+ return merged;
60155
+ }
60156
+ var GATE_EVIDENCE_TYPE_BY_GATE;
60157
+ var init_gate_bridge = __esm(() => {
60158
+ init_gate_evidence();
60159
+ GATE_EVIDENCE_TYPE_BY_GATE = {
60160
+ reviewer: "review",
60161
+ test_engineer: "test"
60162
+ };
60163
+ });
60164
+
59894
60165
  // src/services/version-check.ts
59895
60166
  import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
59896
60167
  import { homedir as homedir5 } from "node:os";
@@ -60038,7 +60309,21 @@ async function checkEvidenceCompleteness(directory, plan) {
60038
60309
  }
60039
60310
  if (completedTaskIds.length > 0) {
60040
60311
  const evidenceTaskIds = new Set(await listEvidenceTaskIds(directory));
60041
- const missingEvidence = completedTaskIds.filter((id) => !evidenceTaskIds.has(id));
60312
+ const missingEvidence = [];
60313
+ for (const id of completedTaskIds) {
60314
+ const gateStatus = await getDurableGateEvidenceStatusForTask(directory, id);
60315
+ if (gateStatus.isComplete) {
60316
+ continue;
60317
+ }
60318
+ if (gateStatus.evidenceExists && gateStatus.missingGates.length > 0) {
60319
+ missingEvidence.push(id);
60320
+ continue;
60321
+ }
60322
+ if (evidenceTaskIds.has(id)) {
60323
+ continue;
60324
+ }
60325
+ missingEvidence.push(id);
60326
+ }
60042
60327
  if (missingEvidence.length === 0) {
60043
60328
  return {
60044
60329
  name: "Evidence",
@@ -60793,6 +61078,7 @@ var init_diagnose_service = __esm(() => {
60793
61078
  init_package();
60794
61079
  init_cache_paths();
60795
61080
  init_loader();
61081
+ init_gate_bridge();
60796
61082
  init_manager2();
60797
61083
  init_utils2();
60798
61084
  init_manager();
@@ -63359,8 +63645,7 @@ function getTaskStatus(task, bundle) {
63359
63645
  }
63360
63646
  return "pending";
63361
63647
  }
63362
- function isEvidenceComplete(bundle) {
63363
- const entries = _internals20.normalizeBundleEntries(bundle);
63648
+ function evidenceCompleteFromEntries(entries) {
63364
63649
  if (entries.length === 0) {
63365
63650
  return {
63366
63651
  isComplete: false,
@@ -63379,6 +63664,9 @@ function isEvidenceComplete(bundle) {
63379
63664
  missingEvidence: missing
63380
63665
  };
63381
63666
  }
63667
+ function isEvidenceComplete(bundle) {
63668
+ return evidenceCompleteFromEntries(_internals20.normalizeBundleEntries(bundle));
63669
+ }
63382
63670
  function getTaskBlockers(task, summary, status) {
63383
63671
  const blockers = [];
63384
63672
  if (task?.blocked_reason) {
@@ -63395,11 +63683,19 @@ function getTaskBlockers(task, summary, status) {
63395
63683
  async function buildTaskSummary(directory, task, taskId) {
63396
63684
  const result = await loadEvidence(directory, taskId);
63397
63685
  const bundle = result.status === "found" ? result.bundle : null;
63686
+ const gateEvidence = await readDurableGateEvidence(directory, taskId);
63398
63687
  const phase = task?.phase ?? 0;
63399
63688
  const status = _internals20.getTaskStatus(task, bundle);
63400
- const evidenceCheck = _internals20.isEvidenceComplete(bundle);
63689
+ const entries = mergeDurableGateEntriesFromEvidence(taskId, _internals20.normalizeBundleEntries(bundle), gateEvidence);
63690
+ let evidenceCheck = _internals20.evidenceCompleteFromEntries(entries);
63691
+ if (gateEvidence) {
63692
+ const gateStatus = getDurableGateEvidenceStatus(gateEvidence);
63693
+ evidenceCheck = gateStatus.isComplete ? { isComplete: true, missingEvidence: [] } : {
63694
+ isComplete: false,
63695
+ missingEvidence: gateStatus.missingGates.map((gate) => `gate:${gate}`)
63696
+ };
63697
+ }
63401
63698
  const blockers = _internals20.getTaskBlockers(task, evidenceCheck, status);
63402
- const entries = _internals20.normalizeBundleEntries(bundle);
63403
63699
  const hasReview = entries.some((e) => e.type === "review");
63404
63700
  const hasTest = entries.some((e) => e.type === "test");
63405
63701
  const hasApproval = entries.some((e) => e.type === "approval");
@@ -63577,6 +63873,7 @@ function isAutoSummaryEnabled(automationConfig) {
63577
63873
  }
63578
63874
  var VALID_EVIDENCE_TYPES2, REQUIRED_EVIDENCE_TYPES, EVIDENCE_SUMMARY_VERSION = "1.0.0", _internals20;
63579
63875
  var init_evidence_summary_service = __esm(() => {
63876
+ init_gate_bridge();
63580
63877
  init_manager2();
63581
63878
  init_manager();
63582
63879
  init_utils();
@@ -63594,6 +63891,7 @@ var init_evidence_summary_service = __esm(() => {
63594
63891
  isAutoSummaryEnabled,
63595
63892
  normalizeBundleEntries,
63596
63893
  getTaskStatus,
63894
+ evidenceCompleteFromEntries,
63597
63895
  isEvidenceComplete,
63598
63896
  getTaskBlockers,
63599
63897
  buildTaskSummary,
@@ -71204,7 +71502,22 @@ async function runEvidenceCheck(dir) {
71204
71502
  };
71205
71503
  }
71206
71504
  const evidenceTaskIds = new Set(await listEvidenceTaskIds(dir));
71207
- const missingEvidence = completedTaskIds.filter((id) => !evidenceTaskIds.has(id));
71505
+ const missingEvidence = [];
71506
+ for (const id of completedTaskIds) {
71507
+ const gateStatus = await getDurableGateEvidenceStatusForTask(dir, id);
71508
+ if (gateStatus.isComplete) {
71509
+ continue;
71510
+ }
71511
+ if (gateStatus.evidenceExists && gateStatus.missingGates.length > 0) {
71512
+ missingEvidence.push(id);
71513
+ continue;
71514
+ }
71515
+ if (evidenceTaskIds.has(id)) {
71516
+ continue;
71517
+ }
71518
+ missingEvidence.push(id);
71519
+ }
71520
+ const completedWithEvidence = completedTaskIds.length - missingEvidence.length;
71208
71521
  if (missingEvidence.length > 0) {
71209
71522
  return {
71210
71523
  type: "evidence",
@@ -71212,7 +71525,7 @@ async function runEvidenceCheck(dir) {
71212
71525
  message: `${missingEvidence.length} completed task(s) missing evidence`,
71213
71526
  details: {
71214
71527
  totalCompleted: completedTaskIds.length,
71215
- totalWithEvidence: evidenceTaskIds.size,
71528
+ totalWithEvidence: completedWithEvidence,
71216
71529
  missingTasks: missingEvidence.slice(0, 10),
71217
71530
  missingCount: missingEvidence.length
71218
71531
  },
@@ -71225,7 +71538,7 @@ async function runEvidenceCheck(dir) {
71225
71538
  message: `All ${completedTaskIds.length} completed tasks have evidence`,
71226
71539
  details: {
71227
71540
  totalCompleted: completedTaskIds.length,
71228
- totalWithEvidence: evidenceTaskIds.size
71541
+ totalWithEvidence: completedWithEvidence
71229
71542
  },
71230
71543
  durationMs: Date.now() - startTime
71231
71544
  };
@@ -71456,6 +71769,7 @@ async function handlePreflightCommand(directory, _args) {
71456
71769
  }
71457
71770
  var MIN_CHECK_TIMEOUT_MS = 5000, MAX_CHECK_TIMEOUT_MS = 300000, DEFAULT_CONFIG, _internals34;
71458
71771
  var init_preflight_service = __esm(() => {
71772
+ init_gate_bridge();
71459
71773
  init_manager2();
71460
71774
  init_manager();
71461
71775
  init_lint();
@@ -76401,121 +76715,14 @@ Do NOT share other agents' responses at this stage.
76401
76715
  ### MODE: DEEP_DIVE
76402
76716
  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
76717
 
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
76718
+ 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.
76719
+
76720
+ ACTION: Load skill \`file:.opencode/skills/deep-dive/SKILL.md\` immediately and follow its protocol.
76721
+
76722
+ HARD CONSTRAINTS (apply regardless of skill load success):
76517
76723
  - Do NOT delegate to coder
76518
76724
  - Do NOT call declare_scope
76725
+ - Do NOT mutate source code
76519
76726
  - Do NOT create or modify any files outside .swarm/
76520
76727
  - No final finding may appear in the report without reviewer verification
76521
76728
  - Explorers generate candidate findings only — reviewers verify or reject
@@ -84996,6 +85203,10 @@ class PlanSyncWorker {
84996
85203
  const planMtimeMs = Math.floor(planStats.mtimeMs);
84997
85204
  const markerContent = fs37.readFileSync(markerPath, "utf8");
84998
85205
  const marker = JSON.parse(markerContent);
85206
+ if (marker.in_progress === true) {
85207
+ log("[PlanSyncWorker] Skipping unauthorized-write check - plan write in progress");
85208
+ return;
85209
+ }
84999
85210
  const markerTimestampMs = new Date(marker.timestamp).getTime();
85000
85211
  if (planMtimeMs > markerTimestampMs + 5000) {
85001
85212
  log("[PlanSyncWorker] WARNING: plan.json may have been written outside save_plan/savePlan - unauthorized direct write suspected", { planMtimeMs, markerTimestampMs });
@@ -112516,6 +112727,9 @@ init_ledger();
112516
112727
  init_manager();
112517
112728
  init_state();
112518
112729
  init_create_tool();
112730
+ function executionProfilesEqual(a, b) {
112731
+ 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;
112732
+ }
112519
112733
  function detectPlaceholderContent(args2) {
112520
112734
  const issues = [];
112521
112735
  const placeholderPattern = /^\[\w[\w\s]*\]$/;
@@ -112680,18 +112894,51 @@ async function executeSavePlan(args2, fallbackDir) {
112680
112894
  }
112681
112895
  }
112682
112896
  }
112683
- if (existing.execution_profile?.locked) {
112684
- if (args2.execution_profile !== undefined && !args2.reset_statuses) {
112897
+ if (args2.confirm_identity_change !== true) {
112898
+ const existingId = derivePlanId(existing);
112899
+ const incomingId = derivePlanId({
112900
+ swarm: args2.swarm_id,
112901
+ title: args2.title
112902
+ });
112903
+ if (existingId !== incomingId) {
112685
112904
  return {
112686
112905
  success: false,
112687
- message: "EXECUTION_PROFILE_LOCKED: The execution_profile for this plan is locked and cannot be changed.",
112906
+ 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
112907
  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."
112908
+ `Existing plan identity: ${existingId} (swarm: "${existing.swarm}", title: "${existing.title}")`,
112909
+ `Incoming plan identity: ${incomingId} (swarm: "${args2.swarm_id}", title: "${args2.title}")`
112690
112910
  ],
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."
112911
+ 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
112912
  };
112693
112913
  }
112694
- if (!args2.reset_statuses) {
112914
+ }
112915
+ if (existing.execution_profile?.locked) {
112916
+ if (args2.execution_profile !== undefined && !args2.reset_statuses) {
112917
+ const requestedProfile = ExecutionProfileSchema.safeParse({
112918
+ ...existing.execution_profile,
112919
+ ...args2.execution_profile
112920
+ });
112921
+ if (!requestedProfile.success) {
112922
+ return {
112923
+ success: false,
112924
+ message: "Invalid execution_profile: schema validation failed",
112925
+ errors: requestedProfile.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`),
112926
+ recovery_guidance: "Check execution_profile fields: parallelization_enabled (boolean), " + "max_concurrent_tasks (integer 1-64), council_parallel (boolean), locked (boolean)."
112927
+ };
112928
+ }
112929
+ if (executionProfilesEqual(existing.execution_profile, requestedProfile.data)) {
112930
+ preservedExecutionProfile = existing.execution_profile;
112931
+ } else {
112932
+ return {
112933
+ success: false,
112934
+ message: "EXECUTION_PROFILE_LOCKED: The execution_profile for this plan is locked and cannot be changed.",
112935
+ errors: [
112936
+ "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."
112937
+ ],
112938
+ 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."
112939
+ };
112940
+ }
112941
+ } else if (!args2.reset_statuses) {
112695
112942
  preservedExecutionProfile = existing.execution_profile;
112696
112943
  }
112697
112944
  } else {
@@ -112863,7 +113110,7 @@ async function executeSavePlan(args2, fallbackDir) {
112863
113110
  });
112864
113111
  const savedPlan = await loadPlanJsonOnly(dir);
112865
113112
  if (savedPlan) {
112866
- await takeSnapshotEvent(dir, savedPlan).catch(() => {});
113113
+ await takeSnapshotWithRetry(dir, savedPlan);
112867
113114
  }
112868
113115
  if (resolvedProfile !== undefined && savedPlan) {
112869
113116
  const planId = derivePlanId(plan);
@@ -112961,6 +113208,7 @@ var save_plan = createSwarmTool({
112961
113208
  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
113209
  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
113210
  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."),
113211
+ 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
113212
  execution_profile: exports_external.object({
112965
113213
  parallelization_enabled: exports_external.boolean().optional().describe("When true, enables parallel task dispatch for this plan. Default false (serial)."),
112966
113214
  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."),