opencode-swarm 6.58.0 → 6.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -389,7 +389,8 @@ var init_constants = __esm(() => {
389
389
  retrieve_summary: "retrieve the full content of a stored tool output summary",
390
390
  search: "Workspace-scoped ripgrep-style text search with structured JSON output. Supports literal and regex modes, glob filtering, and result limits. NOTE: This is text search, not structural AST search \u2014 use symbols and imports tools for structural queries.",
391
391
  batch_symbols: "Batched symbol extraction across multiple files. Returns per-file symbol summaries with isolated error handling.",
392
- suggest_patch: "Reviewer-safe structured patch suggestion tool. Produces context-anchored patch artifacts without file modification. Returns structured diagnostics on context mismatch."
392
+ suggest_patch: "Reviewer-safe structured patch suggestion tool. Produces context-anchored patch artifacts without file modification. Returns structured diagnostics on context mismatch.",
393
+ lint_spec: "validate .swarm/spec.md format and required fields"
393
394
  };
394
395
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
395
396
  const invalidTools = tools.filter((tool) => !TOOL_NAME_SET.has(tool));
@@ -15848,19 +15849,50 @@ async function appendLedgerEvent(directory, eventInput, options) {
15848
15849
  fs3.renameSync(tempPath, ledgerPath);
15849
15850
  return event;
15850
15851
  }
15852
+ async function appendLedgerEventWithRetry(directory, eventInput, options) {
15853
+ const maxRetries = options.maxRetries ?? 3;
15854
+ const backoffBase = options.backoffMs ?? 10;
15855
+ let currentExpected = options.expectedHash;
15856
+ let attempt = 0;
15857
+ while (true) {
15858
+ try {
15859
+ return await appendLedgerEvent(directory, eventInput, {
15860
+ expectedHash: currentExpected,
15861
+ planHashAfter: options.planHashAfter
15862
+ });
15863
+ } catch (error49) {
15864
+ if (!(error49 instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
15865
+ throw error49;
15866
+ }
15867
+ attempt++;
15868
+ const delayMs = backoffBase * 2 ** (attempt - 1);
15869
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
15870
+ if (options.verifyValid) {
15871
+ const stillValid = await options.verifyValid();
15872
+ if (!stillValid) {
15873
+ return null;
15874
+ }
15875
+ }
15876
+ currentExpected = computeCurrentPlanHash(directory);
15877
+ }
15878
+ }
15879
+ }
15851
15880
  async function takeSnapshotEvent(directory, plan, options) {
15852
15881
  const payloadHash = computePlanHash(plan);
15853
15882
  const snapshotPayload = {
15854
15883
  plan,
15855
15884
  payload_hash: payloadHash
15856
15885
  };
15886
+ if (options?.approvalMetadata) {
15887
+ snapshotPayload.approval = options.approvalMetadata;
15888
+ }
15857
15889
  const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
15858
15890
  return appendLedgerEvent(directory, {
15859
15891
  event_type: "snapshot",
15860
- source: "takeSnapshotEvent",
15892
+ source: options?.source ?? "takeSnapshotEvent",
15861
15893
  plan_id: planId,
15862
15894
  payload: snapshotPayload
15863
- }, options);
15895
+ }, { planHashAfter: options?.planHashAfter });
15864
15896
  }
15865
15897
  async function replayFromLedger(directory, options) {
15866
15898
  const events = await readLedgerEvents(directory);
@@ -15949,6 +15981,40 @@ function applyEventToPlan(plan, event) {
15949
15981
  throw new Error(`applyEventToPlan: unhandled event type "${event.event_type}" at seq ${event.seq}`);
15950
15982
  }
15951
15983
  }
15984
+ async function loadLastApprovedPlan(directory, expectedPlanId) {
15985
+ const events = await readLedgerEvents(directory);
15986
+ if (events.length === 0) {
15987
+ return null;
15988
+ }
15989
+ for (let i2 = events.length - 1;i2 >= 0; i2--) {
15990
+ const event = events[i2];
15991
+ if (event.event_type !== "snapshot")
15992
+ continue;
15993
+ if (event.source !== "critic_approved")
15994
+ continue;
15995
+ if (expectedPlanId !== undefined && event.plan_id !== expectedPlanId) {
15996
+ continue;
15997
+ }
15998
+ const payload = event.payload;
15999
+ if (!payload || typeof payload !== "object" || !payload.plan) {
16000
+ continue;
16001
+ }
16002
+ if (expectedPlanId !== undefined) {
16003
+ const payloadPlanId = `${payload.plan.swarm}-${payload.plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16004
+ if (payloadPlanId !== expectedPlanId) {
16005
+ continue;
16006
+ }
16007
+ }
16008
+ return {
16009
+ plan: payload.plan,
16010
+ seq: event.seq,
16011
+ timestamp: event.timestamp,
16012
+ approval: payload.approval,
16013
+ payloadHash: payload.payload_hash
16014
+ };
16015
+ }
16016
+ return null;
16017
+ }
15952
16018
  var LEDGER_SCHEMA_VERSION = "1.0.0", LEDGER_FILENAME = "plan-ledger.jsonl", PLAN_JSON_FILENAME = "plan.json", LedgerStaleWriterError;
15953
16019
  var init_ledger = __esm(() => {
15954
16020
  init_plan_schema();
@@ -16111,6 +16177,23 @@ async function loadPlan(directory) {
16111
16177
  return rebuilt;
16112
16178
  }
16113
16179
  } catch (replayError) {
16180
+ try {
16181
+ const approved = await loadLastApprovedPlan(directory, currentPlanId);
16182
+ if (approved) {
16183
+ await rebuildPlan(directory, approved.plan);
16184
+ try {
16185
+ await takeSnapshotEvent(directory, approved.plan, {
16186
+ source: "recovery_from_approved_snapshot",
16187
+ approvalMetadata: approved.approval
16188
+ });
16189
+ } catch (healError) {
16190
+ warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
16191
+ }
16192
+ const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
16193
+ warn(`[loadPlan] Ledger replay failed (${replayError instanceof Error ? replayError.message : String(replayError)}) \u2014 recovered from critic-approved snapshot seq=${approved.seq} (approval phase=${approvedPhase ?? "unknown"}, timestamp=${approved.timestamp}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
16194
+ return approved.plan;
16195
+ }
16196
+ } catch {}
16114
16197
  warn(`[loadPlan] Ledger replay failed during hash-mismatch rebuild: ${replayError instanceof Error ? replayError.message : String(replayError)}. Returning stale plan.json. To recover: check SWARM_PLAN.md for a checkpoint, or run /swarm reset-session.`);
16115
16198
  }
16116
16199
  }
@@ -16201,6 +16284,31 @@ async function loadPlan(directory) {
16201
16284
  await savePlan(directory, rebuilt);
16202
16285
  return rebuilt;
16203
16286
  }
16287
+ try {
16288
+ const anchorEvents = await readLedgerEvents(directory);
16289
+ if (anchorEvents.length === 0) {
16290
+ warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
16291
+ return null;
16292
+ }
16293
+ const expectedPlanId = anchorEvents[0].plan_id;
16294
+ const approved = await loadLastApprovedPlan(directory, expectedPlanId);
16295
+ if (approved) {
16296
+ const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
16297
+ warn(`[loadPlan] Ledger replay returned no plan \u2014 recovered from critic-approved snapshot seq=${approved.seq} timestamp=${approved.timestamp} (approval phase=${approvedPhase ?? "unknown"}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
16298
+ await savePlan(directory, approved.plan);
16299
+ try {
16300
+ await takeSnapshotEvent(directory, approved.plan, {
16301
+ source: "recovery_from_approved_snapshot",
16302
+ approvalMetadata: approved.approval
16303
+ });
16304
+ } catch (healError) {
16305
+ warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
16306
+ }
16307
+ return approved.plan;
16308
+ }
16309
+ } catch (recoveryError) {
16310
+ warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
16311
+ }
16204
16312
  }
16205
16313
  return null;
16206
16314
  }
@@ -16334,16 +16442,31 @@ async function savePlan(directory, plan, options) {
16334
16442
  to_status: task.status,
16335
16443
  source: "savePlan"
16336
16444
  };
16337
- await appendLedgerEvent(directory, eventInput, {
16445
+ const capturedFromStatus = oldTask.status;
16446
+ const capturedTaskId = task.id;
16447
+ await appendLedgerEventWithRetry(directory, eventInput, {
16338
16448
  expectedHash: currentHash,
16339
- planHashAfter: hashAfter
16449
+ planHashAfter: hashAfter,
16450
+ maxRetries: 3,
16451
+ verifyValid: async () => {
16452
+ const onDisk = await loadPlanJsonOnly(directory);
16453
+ if (!onDisk)
16454
+ return true;
16455
+ for (const p of onDisk.phases) {
16456
+ const t = p.tasks.find((x) => x.id === capturedTaskId);
16457
+ if (t) {
16458
+ return t.status === capturedFromStatus;
16459
+ }
16460
+ }
16461
+ return false;
16462
+ }
16340
16463
  });
16341
16464
  }
16342
16465
  }
16343
16466
  }
16344
16467
  } catch (error49) {
16345
16468
  if (error49 instanceof LedgerStaleWriterError) {
16346
- throw new Error(`Concurrent plan modification detected: ${error49.message}. Please retry the operation.`);
16469
+ throw new Error(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
16347
16470
  }
16348
16471
  throw error49;
16349
16472
  }
@@ -47025,7 +47148,7 @@ async function executeWriteRetro(args2, directory) {
47025
47148
  message: "Invalid task ID: path traversal detected"
47026
47149
  }, null, 2);
47027
47150
  }
47028
- const VALID_TASK_ID = /^(retro-\d+|\d+\.\d+(\.\d+)*)$/;
47151
+ const VALID_TASK_ID = /^(retro-[a-zA-Z0-9][a-zA-Z0-9_-]*|\d+\.\d+(\.\d+)*)$/;
47029
47152
  if (!VALID_TASK_ID.test(tid)) {
47030
47153
  return JSON.stringify({
47031
47154
  success: false,
@@ -47173,6 +47296,7 @@ var write_retro = createSwarmTool({
47173
47296
  var ARCHIVE_ARTIFACTS = [
47174
47297
  "plan.json",
47175
47298
  "plan.md",
47299
+ "plan-ledger.jsonl",
47176
47300
  "context.md",
47177
47301
  "events.jsonl",
47178
47302
  "handoff.md",
@@ -47182,7 +47306,9 @@ var ARCHIVE_ARTIFACTS = [
47182
47306
  "close-lessons.md"
47183
47307
  ];
47184
47308
  var ACTIVE_STATE_TO_CLEAN = [
47309
+ "plan.json",
47185
47310
  "plan.md",
47311
+ "plan-ledger.jsonl",
47186
47312
  "events.jsonl",
47187
47313
  "handoff.md",
47188
47314
  "handoff-prompt.md",
@@ -47257,6 +47383,33 @@ async function handleCloseCommand(directory, args2) {
47257
47383
  }
47258
47384
  }
47259
47385
  }
47386
+ const wrotePhaseRetro = closedPhases.length > 0;
47387
+ if (!wrotePhaseRetro && !planExists) {
47388
+ try {
47389
+ const sessionRetroResult = await executeWriteRetro({
47390
+ phase: 1,
47391
+ task_id: "retro-session",
47392
+ summary: isForced ? "Plan-free session force-closed via /swarm close --force" : "Plan-free session closed via /swarm close",
47393
+ task_count: 1,
47394
+ task_complexity: "simple",
47395
+ total_tool_calls: 0,
47396
+ coder_revisions: 0,
47397
+ reviewer_rejections: 0,
47398
+ test_failures: 0,
47399
+ security_findings: 0,
47400
+ integration_issues: 0,
47401
+ metadata: { session_scope: "plan_free" }
47402
+ }, directory);
47403
+ try {
47404
+ const parsed = JSON.parse(sessionRetroResult);
47405
+ if (parsed.success !== true) {
47406
+ warnings.push(`Session retrospective write failed: ${parsed.message ?? "unknown"}`);
47407
+ }
47408
+ } catch {}
47409
+ } catch (retroError) {
47410
+ warnings.push(`Session retrospective write threw: ${retroError instanceof Error ? retroError.message : String(retroError)}`);
47411
+ }
47412
+ }
47260
47413
  const lessonsFilePath = path14.join(swarmDir, "close-lessons.md");
47261
47414
  let explicitLessons = [];
47262
47415
  try {
@@ -47269,6 +47422,8 @@ async function handleCloseCommand(directory, args2) {
47269
47422
  await curateAndStoreSwarm(explicitLessons, projectName, { phase_number: 0 }, directory, config3);
47270
47423
  curationSucceeded = true;
47271
47424
  } catch (error93) {
47425
+ const msg = error93 instanceof Error ? error93.message : String(error93);
47426
+ warnings.push(`Lessons curation failed: ${msg}`);
47272
47427
  console.warn("[close-command] curateAndStoreSwarm error:", error93);
47273
47428
  }
47274
47429
  if (curationSucceeded && explicitLessons.length > 0) {
@@ -47294,6 +47449,8 @@ async function handleCloseCommand(directory, args2) {
47294
47449
  try {
47295
47450
  await fs9.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
47296
47451
  } catch (error93) {
47452
+ const msg = error93 instanceof Error ? error93.message : String(error93);
47453
+ warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
47297
47454
  console.warn("[close-command] Failed to write plan.json:", error93);
47298
47455
  }
47299
47456
  }
@@ -47355,6 +47512,8 @@ async function handleCloseCommand(directory, args2) {
47355
47512
  try {
47356
47513
  await archiveEvidence(directory, 30, 10);
47357
47514
  } catch (error93) {
47515
+ const msg = error93 instanceof Error ? error93.message : String(error93);
47516
+ warnings.push(`Evidence retention archive failed: ${msg}`);
47358
47517
  console.warn("[close-command] archiveEvidence error:", error93);
47359
47518
  }
47360
47519
  let configBackupsRemoved = 0;
@@ -47383,6 +47542,12 @@ async function handleCloseCommand(directory, args2) {
47383
47542
  configBackupsRemoved++;
47384
47543
  } catch {}
47385
47544
  }
47545
+ const ledgerSiblings = swarmFiles.filter((f) => (f.startsWith("plan-ledger.archived-") || f.startsWith("plan-ledger.backup-")) && f.endsWith(".jsonl"));
47546
+ for (const sibling of ledgerSiblings) {
47547
+ try {
47548
+ await fs9.unlink(path14.join(swarmDir, sibling));
47549
+ } catch {}
47550
+ }
47386
47551
  } catch {}
47387
47552
  const contextPath = path14.join(swarmDir, "context.md");
47388
47553
  const contextContent = [
@@ -47399,6 +47564,8 @@ async function handleCloseCommand(directory, args2) {
47399
47564
  try {
47400
47565
  await fs9.writeFile(contextPath, contextContent, "utf-8");
47401
47566
  } catch (error93) {
47567
+ const msg = error93 instanceof Error ? error93.message : String(error93);
47568
+ warnings.push(`Failed to reset context.md: ${msg}`);
47402
47569
  console.warn("[close-command] Failed to write context.md:", error93);
47403
47570
  }
47404
47571
  const pruneBranches = args2.includes("--prune-branches");
@@ -47528,16 +47695,27 @@ async function handleCloseCommand(directory, args2) {
47528
47695
  try {
47529
47696
  await fs9.writeFile(closeSummaryPath, summaryContent, "utf-8");
47530
47697
  } catch (error93) {
47698
+ const msg = error93 instanceof Error ? error93.message : String(error93);
47699
+ warnings.push(`Failed to write close-summary.md: ${msg}`);
47531
47700
  console.warn("[close-command] Failed to write close-summary.md:", error93);
47532
47701
  }
47533
47702
  try {
47534
47703
  await flushPendingSnapshot(directory);
47535
47704
  } catch (error93) {
47705
+ const msg = error93 instanceof Error ? error93.message : String(error93);
47706
+ warnings.push(`flushPendingSnapshot failed: ${msg}`);
47536
47707
  console.warn("[close-command] flushPendingSnapshot error:", error93);
47537
47708
  }
47538
47709
  await writeCheckpoint(directory).catch(() => {});
47539
- swarmState.agentSessions.clear();
47540
- swarmState.delegationChains.clear();
47710
+ const preservedClient = swarmState.opencodeClient;
47711
+ const preservedFullAutoFlag = swarmState.fullAutoEnabledInConfig;
47712
+ const preservedCuratorInitNames = swarmState.curatorInitAgentNames;
47713
+ const preservedCuratorPhaseNames = swarmState.curatorPhaseAgentNames;
47714
+ resetSwarmState();
47715
+ swarmState.opencodeClient = preservedClient;
47716
+ swarmState.fullAutoEnabledInConfig = preservedFullAutoFlag;
47717
+ swarmState.curatorInitAgentNames = preservedCuratorInitNames;
47718
+ swarmState.curatorPhaseAgentNames = preservedCuratorPhaseNames;
47541
47719
  if (pruneErrors.length > 0) {
47542
47720
  warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
47543
47721
  }
@@ -47632,17 +47810,21 @@ When exploring a codebase area, systematically report all four dimensions:
47632
47810
  - State management approach (global, module-level, passed through)
47633
47811
  - Configuration pattern (env vars, config files, hardcoded)
47634
47812
 
47635
- ### RISKS
47636
- - Files with high cyclomatic complexity or deep nesting
47637
- - Circular dependencies
47638
- - Missing error handling paths
47639
- - Dead code or unreachable branches
47813
+ ### COMPLEXITY INDICATORS
47814
+ - High cyclomatic complexity, deep nesting, or complex control flow
47815
+ - Large files (>500 lines) with many exported symbols
47816
+ - Deep inheritance hierarchies or complex type hierarchies
47817
+
47818
+ ### RUNTIME/BEHAVIORAL CONCERNS
47819
+ - Missing error handling paths or single-throw patterns
47640
47820
  - Platform-specific assumptions (path separators, line endings, OS APIs)
47641
47821
 
47642
- ### RELEVANT CONTEXT FOR TASK
47643
- - Existing tests that cover this area (paths and what they test)
47644
- - Related documentation files
47645
- - Similar implementations elsewhere in the codebase that should be consistent
47822
+ ### RELEVANT CONSTRAINTS
47823
+ - Architectural patterns observed (layered architecture, event-driven, microservice, etc.)
47824
+ - Error handling coverage patterns observed in the codebase
47825
+ - Platform-specific assumptions observed in the codebase
47826
+ - Established conventions (naming patterns, error handling approaches, testing strategies)
47827
+ - Configuration management approaches (env vars, config files, feature flags)
47646
47828
 
47647
47829
  OUTPUT FORMAT (MANDATORY \u2014 deviations will be rejected):
47648
47830
  Begin directly with PROJECT. Do NOT prepend "Here's my analysis..." or any conversational preamble.
@@ -47653,16 +47835,41 @@ FRAMEWORK: [if any]
47653
47835
 
47654
47836
  STRUCTURE:
47655
47837
  [key directories, 5-10 lines max]
47838
+ Example:
47839
+ src/agents/ \u2014 agent factories and definitions
47840
+ src/tools/ \u2014 CLI tool implementations
47841
+ src/config/ \u2014 plan schema and constants
47656
47842
 
47657
47843
  KEY FILES:
47658
47844
  - [path]: [purpose]
47845
+ Example:
47846
+ src/agents/explorer.ts \u2014 explorer agent factory and all prompt definitions
47847
+ src/agents/architect.ts \u2014 architect orchestrator with all mode handlers
47659
47848
 
47660
47849
  PATTERNS: [observations]
47850
+ Example: Factory pattern for agent creation; Result type for error handling; Module-level state via closure
47851
+
47852
+ COMPLEXITY INDICATORS:
47853
+ [structural complexity concerns: elevated cyclomatic complexity, deep nesting, large files, deep inheritance hierarchies, or similar \u2014 describe what is OBSERVED]
47854
+ Example: explorer.ts (289 lines, 12 exports); architect.ts (complex branching in mode handlers)
47855
+
47856
+ OBSERVED CHANGES:
47857
+ [if INPUT referenced specific files/changes: what changed in those targets; otherwise "none" or "general exploration"]
47858
+
47859
+ CONSUMERS_AFFECTED:
47860
+ [if integration impact mode: list files that import/use the changed symbols; otherwise "not applicable"]
47861
+
47862
+ RELEVANT CONSTRAINTS:
47863
+ [architectural patterns, error handling coverage patterns, platform-specific assumptions, established conventions observed in the codebase]
47864
+ Example: Layered architecture (agents \u2192 tools \u2192 filesystem); Bun-native path handling; Error-first callbacks in hooks
47661
47865
 
47662
47866
  DOMAINS: [relevant SME domains: powershell, security, python, etc.]
47867
+ Example: typescript, nodejs, cli-tooling, powershell
47663
47868
 
47664
- REVIEW NEEDED:
47665
- - [path]: [why, which SME]
47869
+ FOLLOW-UP CANDIDATE AREAS:
47870
+ - [path]: [observable condition, relevant domain]
47871
+ Example:
47872
+ src/tools/declare-scope.ts \u2014 function has 12 parameters, consider splitting; tool-authoring
47666
47873
 
47667
47874
  ## INTEGRATION IMPACT ANALYSIS MODE
47668
47875
  Activates when delegated with "Integration impact analysis" or INPUT lists contract changes.
@@ -47678,10 +47885,15 @@ OUTPUT FORMAT (MANDATORY \u2014 deviations will be rejected):
47678
47885
  Begin directly with BREAKING_CHANGES. Do NOT prepend conversational preamble.
47679
47886
 
47680
47887
  BREAKING_CHANGES: [list with affected consumer files, or "none"]
47888
+ Example: src/agents/explorer.ts \u2014 removed createExplorerAgent export (was used by 3 files)
47681
47889
  COMPATIBLE_CHANGES: [list, or "none"]
47890
+ Example: src/config/constants.ts \u2014 added new optional field to Config interface
47682
47891
  CONSUMERS_AFFECTED: [list of files that import/use changed exports, or "none"]
47683
- VERDICT: BREAKING | COMPATIBLE
47684
- MIGRATION_NEEDED: [yes \u2014 description of required caller updates | no]
47892
+ Example: src/agents/coder.ts, src/agents/reviewer.ts, src/main.ts
47893
+ COMPATIBILITY SIGNALS: [COMPATIBLE | INCOMPATIBLE | UNCERTAIN \u2014 based on observable contract changes]
47894
+ Example: INCOMPATIBLE \u2014 removeExport changes function arity from 3 to 2
47895
+ MIGRATION_SURFACE: [yes \u2014 list of observable call signatures affected | no \u2014 no observable impact detected]
47896
+ Example: yes \u2014 createExplorerAgent(model, customPrompt?, customAppendPrompt?) \u2192 createExplorerAgent(model)
47685
47897
 
47686
47898
  ## DOCUMENTATION DISCOVERY MODE
47687
47899
  Activates automatically during codebase reality check at plan ingestion.
@@ -47727,8 +47939,8 @@ PROJECT_CONTEXT: [context.md excerpt]
47727
47939
  ACTIONS:
47728
47940
  - Read the prior summary to understand session history
47729
47941
  - Cross-reference knowledge entries against project context
47730
- - Identify contradictions (knowledge says X, project state shows Y)
47731
- - Recommend rewrites for verbose or stale lessons
47942
+ - Note contradictions (knowledge says X, project state shows Y)
47943
+ - Observe where lessons could be tighter or stale
47732
47944
  - Produce a concise briefing for the architect
47733
47945
 
47734
47946
  RULES:
@@ -47744,13 +47956,13 @@ BRIEFING:
47744
47956
  CONTRADICTIONS:
47745
47957
  - [entry_id]: [description] (or "None detected")
47746
47958
 
47747
- KNOWLEDGE_UPDATES:
47748
- - promote <uuid>: <reason> (boost confidence, mark hive_eligible)
47749
- - archive <uuid>: <reason> (mark as archived \u2014 no longer injected)
47750
- - rewrite <uuid>: <new lesson text> (replace verbose/stale lesson with tighter version, max 280 chars)
47751
- - flag_contradiction <uuid>: <reason> (tag as contradicted)
47752
- - promote new: <new lesson text> (add a brand-new entry)
47753
- Use the UUID from KNOWLEDGE_ENTRIES when archiving, rewriting, or flagging an existing entry. Use "new" only when recommending a brand-new entry.
47959
+ OBSERVATIONS:
47960
+ - entry <uuid> appears high-confidence: [observable evidence] (suggests boost confidence, mark hive_eligible)
47961
+ - entry <uuid> appears stale: [observable evidence] (suggests archive \u2014 no longer injected)
47962
+ - entry <uuid> could be tighter: [what's verbose or duplicate] (suggests rewrite with tighter version, max 280 chars)
47963
+ - entry <uuid> contradicts project state: [observable conflict] (suggests tag as contradicted)
47964
+ - new candidate: [concise lesson text from observed patterns] (suggests new entry)
47965
+ Use the UUID from KNOWLEDGE_ENTRIES when observing about existing entries. Use "new candidate" only when observing a potential new entry.
47754
47966
 
47755
47967
  KNOWLEDGE_STATS:
47756
47968
  - Entries reviewed: [N]
@@ -47772,14 +47984,15 @@ KNOWLEDGE_ENTRIES: [JSON array of existing entries with UUIDs]
47772
47984
 
47773
47985
  ACTIONS:
47774
47986
  - Extend the prior digest with this phase's outcomes (do NOT regenerate from scratch)
47775
- - Identify workflow deviations: missing reviewer, missing retro, skipped test_engineer
47776
- - Recommend knowledge updates: entries to promote, archive, rewrite, or flag as contradicted
47987
+ - Observe workflow deviations: missing reviewer, missing retro, skipped test_engineer
47988
+ - Report knowledge update candidates with observable evidence: entries that appear promoted, archived, rewritten, or contradicted
47777
47989
  - Summarize key decisions and blockers resolved
47778
47990
 
47779
47991
  RULES:
47780
47992
  - Output under 2000 chars
47781
47993
  - No code modifications
47782
47994
  - Compliance observations are READ-ONLY \u2014 report, do not enforce
47995
+ - OBSERVATIONS should not contain directives \u2014 report what is observed, do not instruct the architect what to do
47783
47996
  - Extend the digest, never replace it
47784
47997
 
47785
47998
  OUTPUT FORMAT:
@@ -47792,15 +48005,15 @@ key_decisions: [list]
47792
48005
  blockers_resolved: [list]
47793
48006
 
47794
48007
  COMPLIANCE:
47795
- - [type]: [description] (or "No deviations observed")
48008
+ - [type] observed: [description] (or "No deviations observed")
47796
48009
 
47797
- KNOWLEDGE_UPDATES:
47798
- - promote <uuid>: <reason> (boost confidence, mark hive_eligible)
47799
- - archive <uuid>: <reason> (mark as archived \u2014 no longer injected)
47800
- - rewrite <uuid>: <new lesson text> (replace verbose/stale lesson with tighter version, max 280 chars)
47801
- - flag_contradiction <uuid>: <reason> (tag as contradicted)
47802
- - promote new: <new lesson text> (add a brand-new entry)
47803
- Use the UUID from KNOWLEDGE_ENTRIES when archiving, rewriting, or flagging an existing entry. Use "new" only when recommending a brand-new entry.
48010
+ OBSERVATIONS:
48011
+ - entry <uuid> appears high-confidence: [observable evidence] (suggests boost confidence, mark hive_eligible)
48012
+ - entry <uuid> appears stale: [observable evidence] (suggests archive \u2014 no longer injected)
48013
+ - entry <uuid> could be tighter: [what's verbose or duplicate] (suggests rewrite with tighter version, max 280 chars)
48014
+ - entry <uuid> contradicts project state: [observable conflict] (suggests tag as contradicted)
48015
+ - new candidate: [concise lesson text from observed patterns] (suggests new entry)
48016
+ Use the UUID from KNOWLEDGE_ENTRIES when observing about existing entries. Use "new candidate" only when observing a potential new entry.
47804
48017
 
47805
48018
  EXTENDED_DIGEST:
47806
48019
  [the full running digest with this phase appended]
@@ -47816,7 +48029,7 @@ ${customAppendPrompt}`;
47816
48029
  }
47817
48030
  return {
47818
48031
  name: "explorer",
47819
- description: "Fast codebase discovery and analysis. Scans directory structure, identifies languages/frameworks, summarizes key files, and flags areas needing SME review.",
48032
+ description: "Fast codebase discovery and analysis. Scans directory structure, identifies languages/frameworks, summarizes key files, and identifies areas where specialized domain knowledge may be beneficial.",
47820
48033
  config: {
47821
48034
  model,
47822
48035
  temperature: 0.1,
@@ -47838,7 +48051,7 @@ init_utils2();
47838
48051
  var DEFAULT_CURATOR_LLM_TIMEOUT_MS = 300000;
47839
48052
  function parseKnowledgeRecommendations(llmOutput) {
47840
48053
  const recommendations = [];
47841
- const section = llmOutput.match(/KNOWLEDGE_UPDATES:\s*\n([\s\S]*?)(?:\n\n|\n[A-Z_]+:|$)/);
48054
+ const section = llmOutput.match(/OBSERVATIONS:\s*\n([\s\S]*?)(?:\n\n|\n[A-Z_]+:|$)/);
47842
48055
  if (!section)
47843
48056
  return recommendations;
47844
48057
  const lines = section[1].split(`
@@ -47847,19 +48060,33 @@ function parseKnowledgeRecommendations(llmOutput) {
47847
48060
  const trimmed = line.trim();
47848
48061
  if (!trimmed.startsWith("-"))
47849
48062
  continue;
47850
- const match = trimmed.match(/^-\s+(promote|archive|flag_contradiction|rewrite)\s+(\S+):\s+(.+)$/i);
47851
- if (match) {
47852
- const action = match[1].toLowerCase();
47853
- const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
47854
- const entryId = match[2] === "new" || !UUID_V4.test(match[2]) ? undefined : match[2];
47855
- const reason = match[3].trim();
47856
- recommendations.push({
47857
- action,
47858
- entry_id: entryId,
47859
- lesson: reason,
47860
- reason
47861
- });
47862
- }
48063
+ const match = trimmed.match(/^-\s+entry\s+(\S+)\s+\(([^)]+)\):\s+(.+)$/i);
48064
+ if (!match)
48065
+ continue;
48066
+ const uuid8 = match[1];
48067
+ const parenthetical = match[2];
48068
+ const text = match[3].trim().replace(/\s+\([^)]+\)$/, "");
48069
+ const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
48070
+ const entryId = uuid8 === "new" || !UUID_V4.test(uuid8) ? undefined : uuid8;
48071
+ let action = "rewrite";
48072
+ const lowerParenthetical = parenthetical.toLowerCase();
48073
+ if (lowerParenthetical.includes("suggests boost confidence") || lowerParenthetical.includes("mark hive_eligible") || lowerParenthetical.includes("appears high-confidence")) {
48074
+ action = "promote";
48075
+ } else if (lowerParenthetical.includes("suggests archive") || lowerParenthetical.includes("appears stale")) {
48076
+ action = "archive";
48077
+ } else if (lowerParenthetical.includes("contradicts project state")) {
48078
+ action = "flag_contradiction";
48079
+ } else if (lowerParenthetical.includes("suggests rewrite") || lowerParenthetical.includes("could be tighter")) {
48080
+ action = "rewrite";
48081
+ } else if (lowerParenthetical.includes("new candidate")) {
48082
+ action = "promote";
48083
+ }
48084
+ recommendations.push({
48085
+ action,
48086
+ entry_id: entryId,
48087
+ lesson: text,
48088
+ reason: text
48089
+ });
47863
48090
  }
47864
48091
  return recommendations;
47865
48092
  }
@@ -52925,6 +53152,20 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
52925
53152
  - Never pass raw files - summarize relevant parts
52926
53153
  - Never assume subagents remember prior context
52927
53154
 
53155
+ ## EXPLORER ROLE BOUNDARIES (Phase 2+)
53156
+ Explorer is strictly a FACTUAL MAPPER \u2014 it observes and reports. It does NOT make judgments, verdicts, routing decisions, or enforcement actions.
53157
+
53158
+ Explorer outputs (COMPLEXITY INDICATORS, FOLLOW-UP CANDIDATE AREAS, DOMAINS, etc.) are CANDIDATE EVIDENCE. As Architect, YOU decide what to use, how to route, and what to prioritize.
53159
+
53160
+ Explorer should NEVER be treated as:
53161
+ - A verdict authority (its signals are informational, not binding)
53162
+ - A routing oracle (SME nominations and domain hints are suggestions, not assignments)
53163
+ - A compliance enforcer (workflow observations are read-only reports)
53164
+
53165
+ The architect makes dispatch and routing decisions. Explorer provides facts.
53166
+
53167
+ SPEED PRESERVATION: This change improves explorer precision by narrowing its job to factual mapping \u2014 it does NOT reduce explorer usage. All existing explorer calls and workflows remain intact. The goal is better signal quality, not fewer calls.
53168
+
52928
53169
  ## RULES
52929
53170
 
52930
53171
  NAMESPACE RULE: "Phase N" and "Task N.M" ALWAYS refer to the PROJECT PLAN in .swarm/plan.md.
@@ -53271,7 +53512,7 @@ OUTPUT: Test file + VERDICT: PASS/FAIL
53271
53512
  {{AGENT_PREFIX}}explorer
53272
53513
  TASK: Integration impact analysis
53273
53514
  INPUT: Contract changes detected: [list from diff tool]
53274
- OUTPUT: BREAKING_CHANGES + COMPATIBLE_CHANGES + CONSUMERS_AFFECTED + VERDICT: BREAKING/COMPATIBLE + MIGRATION_NEEDED
53515
+ OUTPUT: BREAKING_CHANGES + COMPATIBLE_CHANGES + CONSUMERS_AFFECTED + COMPATIBILITY SIGNALS: [COMPATIBLE | INCOMPATIBLE | UNCERTAIN] + MIGRATION_SURFACE: [yes \u2014 list of affected call signatures | no]
53275
53516
  CONSTRAINT: Read-only. use search to find imports/usages of changed exports.
53276
53517
 
53277
53518
  {{AGENT_PREFIX}}docs
@@ -53661,7 +53902,7 @@ All other gates: failure \u2192 return to coder. No self-fixes. No workarounds.
53661
53902
  5a-bis. **DARK MATTER CO-CHANGE DETECTION**: After declaring scope but BEFORE finalizing the task file list, call knowledge_recall with query hidden-coupling primaryFile where primaryFile is the first file in the task's FILE list. Extract primaryFile from the task's FILE list (first file = primary). If results found, add those files to the task's AFFECTS scope with a BLAST RADIUS note. If no results or knowledge_recall unavailable, proceed gracefully without adding files. This is advisory \u2014 the architect may exclude files from scope if they are unrelated to the current task. only after scope is declared.
53662
53903
 
53663
53904
  5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
53664
- 5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. If VERDICT=BREAKING or MIGRATION_NEEDED=yes \u2192 coder retry. If VERDICT=COMPATIBLE and MIGRATION_NEEDED=no \u2192 proceed.
53905
+ 5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. If COMPATIBILITY SIGNALS=INCOMPATIBLE or MIGRATION_SURFACE=yes \u2192 coder retry. If COMPATIBILITY SIGNALS=COMPATIBLE and MIGRATION_SURFACE=no \u2192 proceed.
53665
53906
  \u2192 REQUIRED: Print "diff: [PASS | CONTRACT CHANGE \u2014 details]"
53666
53907
  5d. Run \`syntax_check\` tool. SYNTACTIC ERRORS \u2192 return to coder. NO ERRORS \u2192 proceed to placeholder_scan.
53667
53908
  \u2192 REQUIRED: Print "syntaxcheck: [PASS | FAIL \u2014 N errors]"
@@ -53757,6 +53998,14 @@ PRE-COMMIT RULE \u2014 Before ANY commit or push:
53757
53998
  If ANY box is unchecked: DO NOT COMMIT. Return to step 5b.
53758
53999
  There is no override. A commit without a completed QA gate is a workflow violation.
53759
54000
 
54001
+ ## ROLE-BOUNDARY CHANGE VALIDATION (mandatory for prompt changes)
54002
+ When a task modifies agent prompts (especially explorer, reviewer, critic, or any agent involved in the mapper/validator/challenge hierarchy), add an explicit test validation step:
54003
+ - If new prompt contract tests exist (e.g., explorer-role-boundary.test.ts, explorer-consumer-contract.test.ts): Run them via test_runner
54004
+ - If no specific tests exist for the changed prompt: Run test_runner with scope "convention" on the changed file
54005
+ - Verify the new tests pass before completing the task
54006
+
54007
+ This step supplements (not replaces) the existing regression-sweep and test-drift checks. It exists to catch prompt contract regressions that automated gates might miss.
54008
+
53760
54009
  5o. \u26D4 TASK COMPLETION GATE \u2014 You MUST print this checklist with filled values before marking \u2713 in .swarm/plan.md:
53761
54010
  [TOOL] diff: PASS / SKIP \u2014 value: ___
53762
54011
  [TOOL] syntax_check: PASS \u2014 value: ___
@@ -54942,6 +55191,14 @@ DO NOT:
54942
55191
 
54943
55192
  Your unique value is catching LOGIC ERRORS, EDGE CASES, and SECURITY FLAWS that automated tools cannot detect. If your review only catches things a linter would catch, you are not adding value.
54944
55193
 
55194
+ ## EXPLORER FINDINGS \u2014 VALIDATE BEFORE REPORTING
55195
+ Explorer agent outputs (from @mega_explorer) may contain observations labeled as REVIEW NEEDED, RISKS, VERDICT, BREAKING, COMPATIBLE, or similar judgment language. Treat these as CANDIDATE OBSERVATIONS, not established facts.
55196
+ - BEFORE including any issue-like finding from explorer input in your final report: READ the relevant code yourself and verify the issue independently
55197
+ - Do NOT adopt the explorer's VERDICT, BREAKING, or COMPATIBLE labels as your own \u2014 you must reach your own conclusion
55198
+ - Explorer's RISKS section names potential concerns \u2014 you determine if they are actual issues through your own review
55199
+ - If explorer suggests "REVIEW NEEDED" for an area, treat it as a hint to look there, not as a confirmed problem
55200
+ - Your verdict must reflect YOUR verification, not the explorer's framing
55201
+
54945
55202
  DO (explicitly):
54946
55203
  - READ the changed files yourself \u2014 do not rely on the coder's self-report
54947
55204
  - VERIFY imports exist: if the coder added a new import, use search to verify the export exists in the source
@@ -65609,15 +65866,13 @@ async function executeDeclareScope(args2, fallbackDir) {
65609
65866
  errors: [`Task ${args2.taskId} does not exist in plan.json`]
65610
65867
  };
65611
65868
  }
65612
- for (const [_sessionId, session] of swarmState.agentSessions) {
65613
- const taskState = getTaskState(session, args2.taskId);
65614
- if (taskState === "complete") {
65615
- return {
65616
- success: false,
65617
- message: `Task ${args2.taskId} is already completed`,
65618
- errors: [`Cannot declare scope for completed task ${args2.taskId}`]
65619
- };
65620
- }
65869
+ const taskInPlan = allTasks.find((t) => t.id === args2.taskId);
65870
+ if (taskInPlan && taskInPlan.status === "completed") {
65871
+ return {
65872
+ success: false,
65873
+ message: `Task ${args2.taskId} is already completed`,
65874
+ errors: [`Cannot declare scope for completed task ${args2.taskId}`]
65875
+ };
65621
65876
  }
65622
65877
  const rawMergedFiles = [...args2.files, ...args2.whitelist ?? []];
65623
65878
  const warnings = [];
@@ -69562,6 +69817,10 @@ var DEFAULT_STRING_PATTERNS = [
69562
69817
  },
69563
69818
  { pattern: /`[^`]*\bstub\b[^`]*`/i, rule_id: "placeholder/text-placeholder" }
69564
69819
  ];
69820
+ var FILE_ALLOWLIST = [
69821
+ "src/tools/declare-scope.ts",
69822
+ "src/tools/placeholder-scan.ts"
69823
+ ];
69565
69824
  var DEFAULT_CODE_PATTERNS = [
69566
69825
  {
69567
69826
  pattern: /throw\s+new\s+Error\s*\(\s*["'][^"']*\bTODO\b[^"']*["']\s*\)/i,
@@ -69702,6 +69961,64 @@ function scanPlanFileForPlaceholders(content, filePath) {
69702
69961
  }
69703
69962
  return findings;
69704
69963
  }
69964
+ function isValidationPattern(lines, currentLineIdx) {
69965
+ const currentLine = lines[currentLineIdx];
69966
+ if (!/return\s+undefined\s*;/.test(currentLine)) {
69967
+ return false;
69968
+ }
69969
+ const MAX_SEARCH_LINES = 50;
69970
+ let jsdocContent = "";
69971
+ let foundFunction = false;
69972
+ const functionKeywords = /^(?:export\s+)?(?:async\s+)?function\s+\w+|^(?:export\s+)?(?:async\s+)?(?:\w+\s+)?\w+\s*\([^)]*\)\s*(?::\s*\w+\s*)?(?:\{|$)/;
69973
+ for (let i2 = currentLineIdx - 1;i2 >= 0 && i2 >= currentLineIdx - MAX_SEARCH_LINES; i2--) {
69974
+ const line = lines[i2].trim();
69975
+ if (line.startsWith("*") || line.startsWith("*/")) {
69976
+ const jsdocLine = line.replace(/^\*?\s?/, "").replace(/^\*\//, "");
69977
+ jsdocContent = jsdocLine + `
69978
+ ` + jsdocContent;
69979
+ } else if (line.includes("*/")) {
69980
+ break;
69981
+ } else if (functionKeywords.test(line) || line.startsWith("function ")) {
69982
+ foundFunction = true;
69983
+ break;
69984
+ } else if (line.length > 0 && !line.startsWith("//") && !line.startsWith("*")) {
69985
+ break;
69986
+ }
69987
+ }
69988
+ if (jsdocContent) {
69989
+ const returnsPattern = /@returns\s*(?:\{[^}]*\})?\s*(?:undefined|[A-Za-z_]\w*)/i;
69990
+ if (returnsPattern.test(jsdocContent)) {
69991
+ return true;
69992
+ }
69993
+ }
69994
+ let braceCount = 0;
69995
+ let inFunction = false;
69996
+ for (let i2 = currentLineIdx;i2 >= 0; i2--) {
69997
+ const line = lines[i2];
69998
+ for (const char of line) {
69999
+ if (char === "{") {
70000
+ braceCount++;
70001
+ inFunction = true;
70002
+ } else if (char === "}") {
70003
+ braceCount--;
70004
+ }
70005
+ }
70006
+ if (inFunction && braceCount === 0 && i2 < currentLineIdx) {
70007
+ break;
70008
+ }
70009
+ }
70010
+ const errorReturnPattern = /return\s+["'`][[:ascii:]]*["'`]\s*;/;
70011
+ for (let i2 = currentLineIdx - 1;i2 >= 0 && i2 >= currentLineIdx - MAX_SEARCH_LINES; i2--) {
70012
+ const line = lines[i2].trim();
70013
+ if (functionKeywords.test(line) || line.startsWith("function ")) {
70014
+ break;
70015
+ }
70016
+ if (errorReturnPattern.test(line)) {
70017
+ return true;
70018
+ }
70019
+ }
70020
+ return false;
70021
+ }
69705
70022
  function scanWithRegex(content, filePath, denyPatterns) {
69706
70023
  const findings = [];
69707
70024
  const lines = content.split(`
@@ -69761,6 +70078,11 @@ function scanWithRegex(content, filePath, denyPatterns) {
69761
70078
  for (const { pattern, rule_id } of denyPatterns.code) {
69762
70079
  const isTestLike = line.includes("describe(") || line.includes("it(") || line.includes("test(") || line.includes("expect(");
69763
70080
  if (!isTestLike && pattern.test(line)) {
70081
+ if (rule_id === "placeholder/code-stub-return" && /return\s+undefined\s*;/.test(line)) {
70082
+ if (isValidationPattern(lines, i2)) {
70083
+ continue;
70084
+ }
70085
+ }
69764
70086
  findings.push({
69765
70087
  path: filePath,
69766
70088
  line: lineNumber,
@@ -69868,6 +70190,10 @@ async function placeholderScan(input, directory) {
69868
70190
  if (isAllowedByGlobs(filePath, allow_globs)) {
69869
70191
  continue;
69870
70192
  }
70193
+ const relativeFilePath = path65.relative(directory, fullPath).replace(/\\/g, "/");
70194
+ if (FILE_ALLOWLIST.some((allowed) => relativeFilePath.endsWith(allowed))) {
70195
+ continue;
70196
+ }
69871
70197
  let content;
69872
70198
  try {
69873
70199
  const stat2 = fs52.statSync(fullPath);
@@ -71438,13 +71764,13 @@ function validatePath(inputPath, baseDir, workspaceDir) {
71438
71764
  resolved = path67.resolve(baseDir, inputPath);
71439
71765
  }
71440
71766
  const workspaceResolved = path67.resolve(workspaceDir);
71441
- let relative11;
71767
+ let relative12;
71442
71768
  if (isWinAbs) {
71443
- relative11 = path67.win32.relative(workspaceResolved, resolved);
71769
+ relative12 = path67.win32.relative(workspaceResolved, resolved);
71444
71770
  } else {
71445
- relative11 = path67.relative(workspaceResolved, resolved);
71771
+ relative12 = path67.relative(workspaceResolved, resolved);
71446
71772
  }
71447
- if (relative11.startsWith("..")) {
71773
+ if (relative12.startsWith("..")) {
71448
71774
  return "path traversal detected";
71449
71775
  }
71450
71776
  return null;
@@ -76026,6 +76352,8 @@ var update_task_status = createSwarmTool({
76026
76352
  // src/tools/write-drift-evidence.ts
76027
76353
  init_tool();
76028
76354
  init_utils2();
76355
+ init_ledger();
76356
+ init_manager();
76029
76357
  init_create_tool();
76030
76358
  import fs66 from "fs";
76031
76359
  import path79 from "path";
@@ -76093,11 +76421,40 @@ async function executeWriteDriftEvidence(args2, directory) {
76093
76421
  const tempPath = path79.join(evidenceDir, `.${filename}.tmp`);
76094
76422
  await fs66.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
76095
76423
  await fs66.promises.rename(tempPath, validatedPath);
76424
+ let snapshotInfo;
76425
+ let snapshotError;
76426
+ if (normalizedVerdict === "approved") {
76427
+ try {
76428
+ const currentPlan = await loadPlanJsonOnly(directory);
76429
+ if (currentPlan) {
76430
+ const snapshotEvent = await takeSnapshotEvent(directory, currentPlan, {
76431
+ source: "critic_approved",
76432
+ approvalMetadata: {
76433
+ phase,
76434
+ verdict: "APPROVED",
76435
+ summary: summary.trim(),
76436
+ approved_at: new Date().toISOString()
76437
+ }
76438
+ });
76439
+ snapshotInfo = {
76440
+ seq: snapshotEvent.seq,
76441
+ timestamp: snapshotEvent.timestamp
76442
+ };
76443
+ } else {
76444
+ snapshotError = "plan.json not available for snapshot";
76445
+ }
76446
+ } catch (err2) {
76447
+ snapshotError = err2 instanceof Error ? err2.message : String(err2);
76448
+ console.warn("[write_drift_evidence] critic-approved snapshot failed:", snapshotError);
76449
+ }
76450
+ }
76096
76451
  return JSON.stringify({
76097
76452
  success: true,
76098
76453
  phase,
76099
76454
  verdict: normalizedVerdict,
76100
- message: `Drift evidence written to .swarm/evidence/${phase}/drift-verifier.json`
76455
+ message: `Drift evidence written to .swarm/evidence/${phase}/drift-verifier.json`,
76456
+ approvedSnapshot: snapshotInfo,
76457
+ snapshotError
76101
76458
  }, null, 2);
76102
76459
  } catch (error93) {
76103
76460
  return JSON.stringify({