opencode-swarm 6.47.0 → 6.47.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/cli/index.js CHANGED
@@ -16208,20 +16208,22 @@ async function readLedgerEvents(directory) {
16208
16208
  return [];
16209
16209
  }
16210
16210
  }
16211
- async function initLedger(directory, planId) {
16211
+ async function initLedger(directory, planId, initialPlanHash) {
16212
16212
  const ledgerPath = getLedgerPath(directory);
16213
16213
  const planJsonPath = getPlanJsonPath(directory);
16214
16214
  if (fs4.existsSync(ledgerPath)) {
16215
16215
  throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
16216
16216
  }
16217
- let planHashAfter = "";
16218
- try {
16219
- if (fs4.existsSync(planJsonPath)) {
16220
- const content = fs4.readFileSync(planJsonPath, "utf8");
16221
- const plan = JSON.parse(content);
16222
- planHashAfter = computePlanHash(plan);
16223
- }
16224
- } catch {}
16217
+ let planHashAfter = initialPlanHash ?? "";
16218
+ if (!initialPlanHash) {
16219
+ try {
16220
+ if (fs4.existsSync(planJsonPath)) {
16221
+ const content = fs4.readFileSync(planJsonPath, "utf8");
16222
+ const plan = JSON.parse(content);
16223
+ planHashAfter = computePlanHash(plan);
16224
+ }
16225
+ } catch {}
16226
+ }
16225
16227
  const event = {
16226
16228
  seq: 1,
16227
16229
  timestamp: new Date().toISOString(),
@@ -16233,7 +16235,7 @@ async function initLedger(directory, planId) {
16233
16235
  schema_version: LEDGER_SCHEMA_VERSION
16234
16236
  };
16235
16237
  fs4.mkdirSync(path7.join(directory, ".swarm"), { recursive: true });
16236
- const tempPath = `${ledgerPath}.tmp`;
16238
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
16237
16239
  const line = `${JSON.stringify(event)}
16238
16240
  `;
16239
16241
  fs4.writeFileSync(tempPath, line, "utf8");
@@ -16260,7 +16262,7 @@ async function appendLedgerEvent(directory, eventInput, options) {
16260
16262
  schema_version: LEDGER_SCHEMA_VERSION
16261
16263
  };
16262
16264
  fs4.mkdirSync(path7.join(directory, ".swarm"), { recursive: true });
16263
- const tempPath = `${ledgerPath}.tmp`;
16265
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
16264
16266
  const line = `${JSON.stringify(event)}
16265
16267
  `;
16266
16268
  if (fs4.existsSync(ledgerPath)) {
@@ -16278,10 +16280,11 @@ async function takeSnapshotEvent(directory, plan, options) {
16278
16280
  plan,
16279
16281
  payload_hash: payloadHash
16280
16282
  };
16283
+ const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16281
16284
  return appendLedgerEvent(directory, {
16282
16285
  event_type: "snapshot",
16283
16286
  source: "takeSnapshotEvent",
16284
- plan_id: plan.title,
16287
+ plan_id: planId,
16285
16288
  payload: snapshotPayload
16286
16289
  }, options);
16287
16290
  }
@@ -16290,13 +16293,15 @@ async function replayFromLedger(directory, options) {
16290
16293
  if (events.length === 0) {
16291
16294
  return null;
16292
16295
  }
16296
+ const targetPlanId = events[0].plan_id;
16297
+ const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
16293
16298
  {
16294
- const snapshotEvents = events.filter((e) => e.event_type === "snapshot");
16299
+ const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
16295
16300
  if (snapshotEvents.length > 0) {
16296
16301
  const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
16297
16302
  const snapshotPayload = latestSnapshotEvent.payload;
16298
16303
  let plan2 = snapshotPayload.plan;
16299
- const eventsAfterSnapshot = events.filter((e) => e.seq > latestSnapshotEvent.seq);
16304
+ const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
16300
16305
  for (const event of eventsAfterSnapshot) {
16301
16306
  plan2 = applyEventToPlan(plan2, event);
16302
16307
  if (plan2 === null) {
@@ -16317,7 +16322,7 @@ async function replayFromLedger(directory, options) {
16317
16322
  } catch {
16318
16323
  return null;
16319
16324
  }
16320
- for (const event of events) {
16325
+ for (const event of relevantEvents) {
16321
16326
  if (plan === null) {
16322
16327
  return null;
16323
16328
  }
@@ -16331,10 +16336,14 @@ function applyEventToPlan(plan, event) {
16331
16336
  return plan;
16332
16337
  case "task_status_changed":
16333
16338
  if (event.task_id && event.to_status) {
16339
+ const parseResult = TaskStatusSchema.safeParse(event.to_status);
16340
+ if (!parseResult.success) {
16341
+ return plan;
16342
+ }
16334
16343
  for (const phase of plan.phases) {
16335
16344
  const task = phase.tasks.find((t) => t.id === event.task_id);
16336
16345
  if (task) {
16337
- task.status = event.to_status;
16346
+ task.status = parseResult.data;
16338
16347
  break;
16339
16348
  }
16340
16349
  }
@@ -16368,6 +16377,7 @@ function applyEventToPlan(plan, event) {
16368
16377
  }
16369
16378
  var LEDGER_SCHEMA_VERSION = "1.0.0", LEDGER_FILENAME = "plan-ledger.jsonl", PLAN_JSON_FILENAME = "plan.json", LedgerStaleWriterError;
16370
16379
  var init_ledger = __esm(() => {
16380
+ init_plan_schema();
16371
16381
  LedgerStaleWriterError = class LedgerStaleWriterError extends Error {
16372
16382
  constructor(message) {
16373
16383
  super(message);
@@ -16377,7 +16387,7 @@ var init_ledger = __esm(() => {
16377
16387
  });
16378
16388
 
16379
16389
  // src/plan/manager.ts
16380
- import { renameSync as renameSync3, unlinkSync } from "fs";
16390
+ import { existsSync as existsSync5, renameSync as renameSync3, unlinkSync } from "fs";
16381
16391
  import * as path8 from "path";
16382
16392
  async function loadPlanJsonOnly(directory) {
16383
16393
  const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
@@ -16508,28 +16518,49 @@ async function loadPlan(directory) {
16508
16518
  const planHash = computePlanHash(validated);
16509
16519
  const ledgerHash = await getLatestLedgerHash(directory);
16510
16520
  if (ledgerHash !== "" && planHash !== ledgerHash) {
16511
- warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
16512
- try {
16513
- const rebuilt = await replayFromLedger(directory);
16514
- if (rebuilt) {
16515
- await rebuildPlan(directory, rebuilt);
16516
- warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at SWARM_PLAN.md if it exists.");
16517
- return rebuilt;
16521
+ const currentPlanId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16522
+ const ledgerEvents = await readLedgerEvents(directory);
16523
+ const firstEvent = ledgerEvents.length > 0 ? ledgerEvents[0] : null;
16524
+ if (firstEvent && firstEvent.plan_id !== currentPlanId) {
16525
+ warn(`[loadPlan] Ledger identity mismatch (ledger: ${firstEvent.plan_id}, plan: ${currentPlanId}) \u2014 skipping ledger rebuild (migration detected). Use /swarm reset-session to reinitialize the ledger.`);
16526
+ } else {
16527
+ warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
16528
+ try {
16529
+ const rebuilt = await replayFromLedger(directory);
16530
+ if (rebuilt) {
16531
+ await rebuildPlan(directory, rebuilt);
16532
+ warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at SWARM_PLAN.md if it exists.");
16533
+ return rebuilt;
16534
+ }
16535
+ } catch (replayError) {
16536
+ 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.`);
16518
16537
  }
16519
- } catch (replayError) {
16520
- 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.`);
16521
16538
  }
16522
16539
  }
16523
16540
  }
16524
16541
  return validated;
16525
16542
  } catch (error93) {
16526
16543
  warn(`[loadPlan] plan.json validation failed: ${error93 instanceof Error ? error93.message : String(error93)}. Attempting rebuild from ledger. If rebuild fails, check SWARM_PLAN.md for a checkpoint.`);
16544
+ let rawPlanId = null;
16545
+ try {
16546
+ const rawParsed = JSON.parse(planJsonContent);
16547
+ if (typeof rawParsed?.swarm === "string" && typeof rawParsed?.title === "string") {
16548
+ rawPlanId = `${rawParsed.swarm}-${rawParsed.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16549
+ }
16550
+ } catch {}
16527
16551
  if (await ledgerExists(directory)) {
16528
- const rebuilt = await replayFromLedger(directory);
16529
- if (rebuilt) {
16530
- await rebuildPlan(directory, rebuilt);
16531
- warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
16532
- return rebuilt;
16552
+ const ledgerEventsForCatch = await readLedgerEvents(directory);
16553
+ const catchFirstEvent = ledgerEventsForCatch.length > 0 ? ledgerEventsForCatch[0] : null;
16554
+ const identityMatch = rawPlanId === null || catchFirstEvent === null || catchFirstEvent.plan_id === rawPlanId;
16555
+ if (!identityMatch) {
16556
+ warn(`[loadPlan] Ledger identity mismatch in validation-failure path (ledger: ${catchFirstEvent?.plan_id}, plan: ${rawPlanId}) \u2014 skipping ledger rebuild (migration detected).`);
16557
+ } else if (catchFirstEvent !== null && rawPlanId !== null) {
16558
+ const rebuilt = await replayFromLedger(directory);
16559
+ if (rebuilt) {
16560
+ await rebuildPlan(directory, rebuilt);
16561
+ warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
16562
+ return rebuilt;
16563
+ }
16533
16564
  }
16534
16565
  }
16535
16566
  const planMdContent2 = await readSwarmFileAsync(directory, "plan.md");
@@ -16597,9 +16628,28 @@ async function savePlan(directory, plan, options) {
16597
16628
  }
16598
16629
  }
16599
16630
  const currentPlan = await loadPlanJsonOnly(directory);
16631
+ const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16632
+ const planHashForInit = computePlanHash(validated);
16600
16633
  if (!await ledgerExists(directory)) {
16601
- const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16602
- await initLedger(directory, planId);
16634
+ await initLedger(directory, planId, planHashForInit);
16635
+ } else {
16636
+ const existingEvents = await readLedgerEvents(directory);
16637
+ if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
16638
+ const swarmDir2 = path8.resolve(directory, ".swarm");
16639
+ const oldLedgerPath = path8.join(swarmDir2, "plan-ledger.jsonl");
16640
+ const archivePath = path8.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
16641
+ if (existsSync5(oldLedgerPath)) {
16642
+ renameSync3(oldLedgerPath, archivePath);
16643
+ warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
16644
+ }
16645
+ try {
16646
+ await initLedger(directory, planId, planHashForInit);
16647
+ } catch (initErr) {
16648
+ if (!(initErr instanceof Error && initErr.message.includes("already initialized"))) {
16649
+ throw initErr;
16650
+ }
16651
+ }
16652
+ }
16603
16653
  }
16604
16654
  const currentHash = computeCurrentPlanHash(directory);
16605
16655
  const hashAfter = computePlanHash(validated);
@@ -16692,10 +16742,24 @@ async function rebuildPlan(directory, plan) {
16692
16742
  const tempPlanPath = path8.join(swarmDir, `plan.json.rebuild.${Date.now()}`);
16693
16743
  await Bun.write(tempPlanPath, JSON.stringify(targetPlan, null, 2));
16694
16744
  renameSync3(tempPlanPath, planPath);
16745
+ const contentHash = computePlanContentHash(targetPlan);
16695
16746
  const markdown = derivePlanMarkdown(targetPlan);
16747
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
16748
+ ${markdown}`;
16696
16749
  const tempMdPath = path8.join(swarmDir, `plan.md.rebuild.${Date.now()}`);
16697
- await Bun.write(tempMdPath, markdown);
16750
+ await Bun.write(tempMdPath, markdownWithHash);
16698
16751
  renameSync3(tempMdPath, mdPath);
16752
+ try {
16753
+ const markerPath = path8.join(swarmDir, ".plan-write-marker");
16754
+ const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
16755
+ const marker = JSON.stringify({
16756
+ source: "plan_manager",
16757
+ timestamp: new Date().toISOString(),
16758
+ phases_count: targetPlan.phases.length,
16759
+ tasks_count: tasksCount
16760
+ });
16761
+ await Bun.write(markerPath, marker);
16762
+ } catch {}
16699
16763
  return targetPlan;
16700
16764
  }
16701
16765
  function derivePlanMarkdown(plan) {
@@ -33996,7 +34060,7 @@ async function handleDarkMatterCommand(directory, args) {
33996
34060
 
33997
34061
  // src/services/diagnose-service.ts
33998
34062
  import * as child_process3 from "child_process";
33999
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
34063
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
34000
34064
  import path15 from "path";
34001
34065
  import { fileURLToPath } from "url";
34002
34066
  init_manager();
@@ -34233,7 +34297,7 @@ async function checkConfigBackups(directory) {
34233
34297
  }
34234
34298
  async function checkGitRepository(directory) {
34235
34299
  try {
34236
- if (!existsSync5(directory) || !statSync3(directory).isDirectory()) {
34300
+ if (!existsSync6(directory) || !statSync3(directory).isDirectory()) {
34237
34301
  return {
34238
34302
  name: "Git Repository",
34239
34303
  status: "\u274C",
@@ -34298,7 +34362,7 @@ async function checkSpecStaleness(directory, plan) {
34298
34362
  }
34299
34363
  async function checkConfigParseability(directory) {
34300
34364
  const configPath = path15.join(directory, ".opencode/opencode-swarm.json");
34301
- if (!existsSync5(configPath)) {
34365
+ if (!existsSync6(configPath)) {
34302
34366
  return {
34303
34367
  name: "Config Parseability",
34304
34368
  status: "\u2705",
@@ -34348,11 +34412,11 @@ async function checkGrammarWasmFiles() {
34348
34412
  const isSource = thisDir.replace(/\\/g, "/").endsWith("/src/services");
34349
34413
  const grammarDir = isSource ? path15.join(thisDir, "..", "lang", "grammars") : path15.join(thisDir, "lang", "grammars");
34350
34414
  const missing = [];
34351
- if (!existsSync5(path15.join(grammarDir, "tree-sitter.wasm"))) {
34415
+ if (!existsSync6(path15.join(grammarDir, "tree-sitter.wasm"))) {
34352
34416
  missing.push("tree-sitter.wasm (core runtime)");
34353
34417
  }
34354
34418
  for (const file3 of grammarFiles) {
34355
- if (!existsSync5(path15.join(grammarDir, file3))) {
34419
+ if (!existsSync6(path15.join(grammarDir, file3))) {
34356
34420
  missing.push(file3);
34357
34421
  }
34358
34422
  }
@@ -34371,7 +34435,7 @@ async function checkGrammarWasmFiles() {
34371
34435
  }
34372
34436
  async function checkCheckpointManifest(directory) {
34373
34437
  const manifestPath = path15.join(directory, ".swarm/checkpoints.json");
34374
- if (!existsSync5(manifestPath)) {
34438
+ if (!existsSync6(manifestPath)) {
34375
34439
  return {
34376
34440
  name: "Checkpoint Manifest",
34377
34441
  status: "\u2705",
@@ -34423,7 +34487,7 @@ async function checkCheckpointManifest(directory) {
34423
34487
  }
34424
34488
  async function checkEventStreamIntegrity(directory) {
34425
34489
  const eventsPath = path15.join(directory, ".swarm/events.jsonl");
34426
- if (!existsSync5(eventsPath)) {
34490
+ if (!existsSync6(eventsPath)) {
34427
34491
  return {
34428
34492
  name: "Event Stream",
34429
34493
  status: "\u2705",
@@ -34464,7 +34528,7 @@ async function checkEventStreamIntegrity(directory) {
34464
34528
  }
34465
34529
  async function checkSteeringDirectives(directory) {
34466
34530
  const eventsPath = path15.join(directory, ".swarm/events.jsonl");
34467
- if (!existsSync5(eventsPath)) {
34531
+ if (!existsSync6(eventsPath)) {
34468
34532
  return {
34469
34533
  name: "Steering Directives",
34470
34534
  status: "\u2705",
@@ -34520,7 +34584,7 @@ async function checkCurator(directory) {
34520
34584
  };
34521
34585
  }
34522
34586
  const summaryPath = path15.join(directory, ".swarm/curator-summary.json");
34523
- if (!existsSync5(summaryPath)) {
34587
+ if (!existsSync6(summaryPath)) {
34524
34588
  return {
34525
34589
  name: "Curator",
34526
34590
  status: "\u2705",
@@ -35414,14 +35478,14 @@ async function handleHistoryCommand(directory, _args) {
35414
35478
  }
35415
35479
  // src/hooks/knowledge-migrator.ts
35416
35480
  import { randomUUID as randomUUID2 } from "crypto";
35417
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
35481
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
35418
35482
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
35419
35483
  import * as path17 from "path";
35420
35484
  async function migrateContextToKnowledge(directory, config3) {
35421
35485
  const sentinelPath = path17.join(directory, ".swarm", ".knowledge-migrated");
35422
35486
  const contextPath = path17.join(directory, ".swarm", "context.md");
35423
35487
  const knowledgePath = resolveSwarmKnowledgePath(directory);
35424
- if (existsSync7(sentinelPath)) {
35488
+ if (existsSync8(sentinelPath)) {
35425
35489
  return {
35426
35490
  migrated: false,
35427
35491
  entriesMigrated: 0,
@@ -35430,7 +35494,7 @@ async function migrateContextToKnowledge(directory, config3) {
35430
35494
  skippedReason: "sentinel-exists"
35431
35495
  };
35432
35496
  }
35433
- if (!existsSync7(contextPath)) {
35497
+ if (!existsSync8(contextPath)) {
35434
35498
  return {
35435
35499
  migrated: false,
35436
35500
  entriesMigrated: 0,
@@ -35616,7 +35680,7 @@ function truncateLesson(text) {
35616
35680
  }
35617
35681
  function inferProjectName(directory) {
35618
35682
  const packageJsonPath = path17.join(directory, "package.json");
35619
- if (existsSync7(packageJsonPath)) {
35683
+ if (existsSync8(packageJsonPath)) {
35620
35684
  try {
35621
35685
  const pkg = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
35622
35686
  if (pkg.name && typeof pkg.name === "string") {
@@ -3,6 +3,8 @@
3
3
  * Maps to: plan service (loadPlan which triggers auto-heal/sync)
4
4
  *
5
5
  * This command ensures plan.json and plan.md are in sync.
6
- * The loadPlan function automatically regenerates plan.md from plan.json if needed.
6
+ * loadPlan() is safe here: the migration-aware ledger guard in loadPlan()
7
+ * now prevents false reverts caused by swarm identity changes, so the
8
+ * full auto-heal path (including legacy plan.md migration) is correct.
7
9
  */
8
10
  export declare function handleSyncPlanCommand(directory: string, _args: string[]): Promise<string>;
package/dist/index.js CHANGED
@@ -16046,20 +16046,22 @@ async function readLedgerEvents(directory) {
16046
16046
  return [];
16047
16047
  }
16048
16048
  }
16049
- async function initLedger(directory, planId) {
16049
+ async function initLedger(directory, planId, initialPlanHash) {
16050
16050
  const ledgerPath = getLedgerPath(directory);
16051
16051
  const planJsonPath = getPlanJsonPath(directory);
16052
16052
  if (fs6.existsSync(ledgerPath)) {
16053
16053
  throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
16054
16054
  }
16055
- let planHashAfter = "";
16056
- try {
16057
- if (fs6.existsSync(planJsonPath)) {
16058
- const content = fs6.readFileSync(planJsonPath, "utf8");
16059
- const plan = JSON.parse(content);
16060
- planHashAfter = computePlanHash(plan);
16061
- }
16062
- } catch {}
16055
+ let planHashAfter = initialPlanHash ?? "";
16056
+ if (!initialPlanHash) {
16057
+ try {
16058
+ if (fs6.existsSync(planJsonPath)) {
16059
+ const content = fs6.readFileSync(planJsonPath, "utf8");
16060
+ const plan = JSON.parse(content);
16061
+ planHashAfter = computePlanHash(plan);
16062
+ }
16063
+ } catch {}
16064
+ }
16063
16065
  const event = {
16064
16066
  seq: 1,
16065
16067
  timestamp: new Date().toISOString(),
@@ -16071,7 +16073,7 @@ async function initLedger(directory, planId) {
16071
16073
  schema_version: LEDGER_SCHEMA_VERSION
16072
16074
  };
16073
16075
  fs6.mkdirSync(path6.join(directory, ".swarm"), { recursive: true });
16074
- const tempPath = `${ledgerPath}.tmp`;
16076
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
16075
16077
  const line = `${JSON.stringify(event)}
16076
16078
  `;
16077
16079
  fs6.writeFileSync(tempPath, line, "utf8");
@@ -16098,7 +16100,7 @@ async function appendLedgerEvent(directory, eventInput, options) {
16098
16100
  schema_version: LEDGER_SCHEMA_VERSION
16099
16101
  };
16100
16102
  fs6.mkdirSync(path6.join(directory, ".swarm"), { recursive: true });
16101
- const tempPath = `${ledgerPath}.tmp`;
16103
+ const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
16102
16104
  const line = `${JSON.stringify(event)}
16103
16105
  `;
16104
16106
  if (fs6.existsSync(ledgerPath)) {
@@ -16116,10 +16118,11 @@ async function takeSnapshotEvent(directory, plan, options) {
16116
16118
  plan,
16117
16119
  payload_hash: payloadHash
16118
16120
  };
16121
+ const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16119
16122
  return appendLedgerEvent(directory, {
16120
16123
  event_type: "snapshot",
16121
16124
  source: "takeSnapshotEvent",
16122
- plan_id: plan.title,
16125
+ plan_id: planId,
16123
16126
  payload: snapshotPayload
16124
16127
  }, options);
16125
16128
  }
@@ -16128,13 +16131,15 @@ async function replayFromLedger(directory, options) {
16128
16131
  if (events.length === 0) {
16129
16132
  return null;
16130
16133
  }
16134
+ const targetPlanId = events[0].plan_id;
16135
+ const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
16131
16136
  {
16132
- const snapshotEvents = events.filter((e) => e.event_type === "snapshot");
16137
+ const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
16133
16138
  if (snapshotEvents.length > 0) {
16134
16139
  const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
16135
16140
  const snapshotPayload = latestSnapshotEvent.payload;
16136
16141
  let plan2 = snapshotPayload.plan;
16137
- const eventsAfterSnapshot = events.filter((e) => e.seq > latestSnapshotEvent.seq);
16142
+ const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
16138
16143
  for (const event of eventsAfterSnapshot) {
16139
16144
  plan2 = applyEventToPlan(plan2, event);
16140
16145
  if (plan2 === null) {
@@ -16155,7 +16160,7 @@ async function replayFromLedger(directory, options) {
16155
16160
  } catch {
16156
16161
  return null;
16157
16162
  }
16158
- for (const event of events) {
16163
+ for (const event of relevantEvents) {
16159
16164
  if (plan === null) {
16160
16165
  return null;
16161
16166
  }
@@ -16169,10 +16174,14 @@ function applyEventToPlan(plan, event) {
16169
16174
  return plan;
16170
16175
  case "task_status_changed":
16171
16176
  if (event.task_id && event.to_status) {
16177
+ const parseResult = TaskStatusSchema.safeParse(event.to_status);
16178
+ if (!parseResult.success) {
16179
+ return plan;
16180
+ }
16172
16181
  for (const phase of plan.phases) {
16173
16182
  const task = phase.tasks.find((t) => t.id === event.task_id);
16174
16183
  if (task) {
16175
- task.status = event.to_status;
16184
+ task.status = parseResult.data;
16176
16185
  break;
16177
16186
  }
16178
16187
  }
@@ -16206,6 +16215,7 @@ function applyEventToPlan(plan, event) {
16206
16215
  }
16207
16216
  var LEDGER_SCHEMA_VERSION = "1.0.0", LEDGER_FILENAME = "plan-ledger.jsonl", PLAN_JSON_FILENAME = "plan.json", LedgerStaleWriterError;
16208
16217
  var init_ledger = __esm(() => {
16218
+ init_plan_schema();
16209
16219
  LedgerStaleWriterError = class LedgerStaleWriterError extends Error {
16210
16220
  constructor(message) {
16211
16221
  super(message);
@@ -16215,7 +16225,7 @@ var init_ledger = __esm(() => {
16215
16225
  });
16216
16226
 
16217
16227
  // src/plan/manager.ts
16218
- import { renameSync as renameSync3, unlinkSync } from "fs";
16228
+ import { existsSync as existsSync4, renameSync as renameSync3, unlinkSync } from "fs";
16219
16229
  import * as path7 from "path";
16220
16230
  async function loadPlanJsonOnly(directory) {
16221
16231
  const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
@@ -16346,28 +16356,49 @@ async function loadPlan(directory) {
16346
16356
  const planHash = computePlanHash(validated);
16347
16357
  const ledgerHash = await getLatestLedgerHash(directory);
16348
16358
  if (ledgerHash !== "" && planHash !== ledgerHash) {
16349
- warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
16350
- try {
16351
- const rebuilt = await replayFromLedger(directory);
16352
- if (rebuilt) {
16353
- await rebuildPlan(directory, rebuilt);
16354
- warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at SWARM_PLAN.md if it exists.");
16355
- return rebuilt;
16359
+ const currentPlanId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16360
+ const ledgerEvents = await readLedgerEvents(directory);
16361
+ const firstEvent = ledgerEvents.length > 0 ? ledgerEvents[0] : null;
16362
+ if (firstEvent && firstEvent.plan_id !== currentPlanId) {
16363
+ warn(`[loadPlan] Ledger identity mismatch (ledger: ${firstEvent.plan_id}, plan: ${currentPlanId}) \u2014 skipping ledger rebuild (migration detected). Use /swarm reset-session to reinitialize the ledger.`);
16364
+ } else {
16365
+ warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
16366
+ try {
16367
+ const rebuilt = await replayFromLedger(directory);
16368
+ if (rebuilt) {
16369
+ await rebuildPlan(directory, rebuilt);
16370
+ warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at SWARM_PLAN.md if it exists.");
16371
+ return rebuilt;
16372
+ }
16373
+ } catch (replayError) {
16374
+ 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.`);
16356
16375
  }
16357
- } catch (replayError) {
16358
- 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.`);
16359
16376
  }
16360
16377
  }
16361
16378
  }
16362
16379
  return validated;
16363
16380
  } catch (error49) {
16364
16381
  warn(`[loadPlan] plan.json validation failed: ${error49 instanceof Error ? error49.message : String(error49)}. Attempting rebuild from ledger. If rebuild fails, check SWARM_PLAN.md for a checkpoint.`);
16382
+ let rawPlanId = null;
16383
+ try {
16384
+ const rawParsed = JSON.parse(planJsonContent);
16385
+ if (typeof rawParsed?.swarm === "string" && typeof rawParsed?.title === "string") {
16386
+ rawPlanId = `${rawParsed.swarm}-${rawParsed.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16387
+ }
16388
+ } catch {}
16365
16389
  if (await ledgerExists(directory)) {
16366
- const rebuilt = await replayFromLedger(directory);
16367
- if (rebuilt) {
16368
- await rebuildPlan(directory, rebuilt);
16369
- warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
16370
- return rebuilt;
16390
+ const ledgerEventsForCatch = await readLedgerEvents(directory);
16391
+ const catchFirstEvent = ledgerEventsForCatch.length > 0 ? ledgerEventsForCatch[0] : null;
16392
+ const identityMatch = rawPlanId === null || catchFirstEvent === null || catchFirstEvent.plan_id === rawPlanId;
16393
+ if (!identityMatch) {
16394
+ warn(`[loadPlan] Ledger identity mismatch in validation-failure path (ledger: ${catchFirstEvent?.plan_id}, plan: ${rawPlanId}) \u2014 skipping ledger rebuild (migration detected).`);
16395
+ } else if (catchFirstEvent !== null && rawPlanId !== null) {
16396
+ const rebuilt = await replayFromLedger(directory);
16397
+ if (rebuilt) {
16398
+ await rebuildPlan(directory, rebuilt);
16399
+ warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
16400
+ return rebuilt;
16401
+ }
16371
16402
  }
16372
16403
  }
16373
16404
  const planMdContent2 = await readSwarmFileAsync(directory, "plan.md");
@@ -16435,9 +16466,28 @@ async function savePlan(directory, plan, options) {
16435
16466
  }
16436
16467
  }
16437
16468
  const currentPlan = await loadPlanJsonOnly(directory);
16469
+ const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16470
+ const planHashForInit = computePlanHash(validated);
16438
16471
  if (!await ledgerExists(directory)) {
16439
- const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16440
- await initLedger(directory, planId);
16472
+ await initLedger(directory, planId, planHashForInit);
16473
+ } else {
16474
+ const existingEvents = await readLedgerEvents(directory);
16475
+ if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
16476
+ const swarmDir2 = path7.resolve(directory, ".swarm");
16477
+ const oldLedgerPath = path7.join(swarmDir2, "plan-ledger.jsonl");
16478
+ const archivePath = path7.join(swarmDir2, `plan-ledger.archived-${Date.now()}-${Math.floor(Math.random() * 1e9)}.jsonl`);
16479
+ if (existsSync4(oldLedgerPath)) {
16480
+ renameSync3(oldLedgerPath, archivePath);
16481
+ warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived old ledger to ${archivePath} and reinitializing.`);
16482
+ }
16483
+ try {
16484
+ await initLedger(directory, planId, planHashForInit);
16485
+ } catch (initErr) {
16486
+ if (!(initErr instanceof Error && initErr.message.includes("already initialized"))) {
16487
+ throw initErr;
16488
+ }
16489
+ }
16490
+ }
16441
16491
  }
16442
16492
  const currentHash = computeCurrentPlanHash(directory);
16443
16493
  const hashAfter = computePlanHash(validated);
@@ -16530,10 +16580,24 @@ async function rebuildPlan(directory, plan) {
16530
16580
  const tempPlanPath = path7.join(swarmDir, `plan.json.rebuild.${Date.now()}`);
16531
16581
  await Bun.write(tempPlanPath, JSON.stringify(targetPlan, null, 2));
16532
16582
  renameSync3(tempPlanPath, planPath);
16583
+ const contentHash = computePlanContentHash(targetPlan);
16533
16584
  const markdown = derivePlanMarkdown(targetPlan);
16585
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
16586
+ ${markdown}`;
16534
16587
  const tempMdPath = path7.join(swarmDir, `plan.md.rebuild.${Date.now()}`);
16535
- await Bun.write(tempMdPath, markdown);
16588
+ await Bun.write(tempMdPath, markdownWithHash);
16536
16589
  renameSync3(tempMdPath, mdPath);
16590
+ try {
16591
+ const markerPath = path7.join(swarmDir, ".plan-write-marker");
16592
+ const tasksCount = targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
16593
+ const marker = JSON.stringify({
16594
+ source: "plan_manager",
16595
+ timestamp: new Date().toISOString(),
16596
+ phases_count: targetPlan.phases.length,
16597
+ tasks_count: tasksCount
16598
+ });
16599
+ await Bun.write(markerPath, marker);
16600
+ } catch {}
16537
16601
  return targetPlan;
16538
16602
  }
16539
16603
  async function updateTaskStatus(directory, taskId, status) {
@@ -17134,11 +17198,11 @@ __export(exports_evidence_summary_integration, {
17134
17198
  createEvidenceSummaryIntegration: () => createEvidenceSummaryIntegration,
17135
17199
  EvidenceSummaryIntegration: () => EvidenceSummaryIntegration
17136
17200
  });
17137
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
17201
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
17138
17202
  import * as path8 from "path";
17139
17203
  function persistSummary(swarmDir, artifact, filename) {
17140
17204
  const swarmPath = path8.join(swarmDir, ".swarm");
17141
- if (!existsSync4(swarmPath)) {
17205
+ if (!existsSync5(swarmPath)) {
17142
17206
  mkdirSync4(swarmPath, { recursive: true });
17143
17207
  }
17144
17208
  const artifactPath = path8.join(swarmPath, filename);
@@ -32356,7 +32420,7 @@ var require_proper_lockfile = __commonJS((exports, module2) => {
32356
32420
  });
32357
32421
 
32358
32422
  // src/hooks/knowledge-store.ts
32359
- import { existsSync as existsSync8 } from "fs";
32423
+ import { existsSync as existsSync9 } from "fs";
32360
32424
  import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
32361
32425
  import * as os3 from "os";
32362
32426
  import * as path12 from "path";
@@ -32384,7 +32448,7 @@ function resolveHiveRejectedPath() {
32384
32448
  return path12.join(path12.dirname(hivePath), "shared-learnings-rejected.jsonl");
32385
32449
  }
32386
32450
  async function readKnowledge(filePath) {
32387
- if (!existsSync8(filePath))
32451
+ if (!existsSync9(filePath))
32388
32452
  return [];
32389
32453
  const content = await readFile2(filePath, "utf-8");
32390
32454
  const results = [];
@@ -41867,8 +41931,8 @@ async function loadGrammar(languageId) {
41867
41931
  const parser = new Parser;
41868
41932
  const wasmFileName = getWasmFileName(normalizedId);
41869
41933
  const wasmPath = path56.join(getGrammarsDirAbsolute(), wasmFileName);
41870
- const { existsSync: existsSync33 } = await import("fs");
41871
- if (!existsSync33(wasmPath)) {
41934
+ const { existsSync: existsSync34 } = await import("fs");
41935
+ if (!existsSync34(wasmPath)) {
41872
41936
  throw new Error(`Grammar file not found for ${languageId}: ${wasmPath}
41873
41937
  Make sure to run 'bun run build' to copy grammar files to dist/lang/grammars/`);
41874
41938
  }
@@ -46021,8 +46085,9 @@ class PlanSyncWorker {
46021
46085
  try {
46022
46086
  log("[PlanSyncWorker] Syncing plan...");
46023
46087
  this.checkForUnauthorizedWrite();
46024
- const plan = await this.withTimeout(loadPlan(this.directory), this.syncTimeoutMs, "Sync operation timed out");
46088
+ const plan = await this.withTimeout(loadPlanJsonOnly(this.directory), this.syncTimeoutMs, "Sync operation timed out");
46025
46089
  if (plan) {
46090
+ await regeneratePlanMarkdown(this.directory, plan);
46026
46091
  log("[PlanSyncWorker] Sync complete", {
46027
46092
  title: plan.title,
46028
46093
  phase: plan.current_phase
@@ -46686,7 +46751,7 @@ import path17 from "path";
46686
46751
 
46687
46752
  // src/hooks/knowledge-reader.ts
46688
46753
  init_knowledge_store();
46689
- import { existsSync as existsSync9 } from "fs";
46754
+ import { existsSync as existsSync10 } from "fs";
46690
46755
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
46691
46756
  import * as path13 from "path";
46692
46757
  var JACCARD_THRESHOLD = 0.6;
@@ -46739,7 +46804,7 @@ async function recordLessonsShown(directory, lessonIds, currentPhase) {
46739
46804
  const shownFile = path13.join(directory, ".swarm", ".knowledge-shown.json");
46740
46805
  try {
46741
46806
  let shownData = {};
46742
- if (existsSync9(shownFile)) {
46807
+ if (existsSync10(shownFile)) {
46743
46808
  const content = await readFile3(shownFile, "utf-8");
46744
46809
  shownData = JSON.parse(content);
46745
46810
  }
@@ -46845,7 +46910,7 @@ async function readMergedKnowledge(directory, config3, context) {
46845
46910
  async function updateRetrievalOutcome(directory, phaseInfo, phaseSucceeded) {
46846
46911
  const shownFile = path13.join(directory, ".swarm", ".knowledge-shown.json");
46847
46912
  try {
46848
- if (!existsSync9(shownFile)) {
46913
+ if (!existsSync10(shownFile)) {
46849
46914
  return;
46850
46915
  }
46851
46916
  const content = await readFile3(shownFile, "utf-8");
@@ -49150,7 +49215,7 @@ init_manager();
49150
49215
  init_utils2();
49151
49216
  init_manager2();
49152
49217
  import * as child_process3 from "child_process";
49153
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
49218
+ import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
49154
49219
  import path22 from "path";
49155
49220
  import { fileURLToPath } from "url";
49156
49221
  function validateTaskDag(plan) {
@@ -49384,7 +49449,7 @@ async function checkConfigBackups(directory) {
49384
49449
  }
49385
49450
  async function checkGitRepository(directory) {
49386
49451
  try {
49387
- if (!existsSync10(directory) || !statSync5(directory).isDirectory()) {
49452
+ if (!existsSync11(directory) || !statSync5(directory).isDirectory()) {
49388
49453
  return {
49389
49454
  name: "Git Repository",
49390
49455
  status: "\u274C",
@@ -49449,7 +49514,7 @@ async function checkSpecStaleness(directory, plan) {
49449
49514
  }
49450
49515
  async function checkConfigParseability(directory) {
49451
49516
  const configPath = path22.join(directory, ".opencode/opencode-swarm.json");
49452
- if (!existsSync10(configPath)) {
49517
+ if (!existsSync11(configPath)) {
49453
49518
  return {
49454
49519
  name: "Config Parseability",
49455
49520
  status: "\u2705",
@@ -49499,11 +49564,11 @@ async function checkGrammarWasmFiles() {
49499
49564
  const isSource = thisDir.replace(/\\/g, "/").endsWith("/src/services");
49500
49565
  const grammarDir = isSource ? path22.join(thisDir, "..", "lang", "grammars") : path22.join(thisDir, "lang", "grammars");
49501
49566
  const missing = [];
49502
- if (!existsSync10(path22.join(grammarDir, "tree-sitter.wasm"))) {
49567
+ if (!existsSync11(path22.join(grammarDir, "tree-sitter.wasm"))) {
49503
49568
  missing.push("tree-sitter.wasm (core runtime)");
49504
49569
  }
49505
49570
  for (const file3 of grammarFiles) {
49506
- if (!existsSync10(path22.join(grammarDir, file3))) {
49571
+ if (!existsSync11(path22.join(grammarDir, file3))) {
49507
49572
  missing.push(file3);
49508
49573
  }
49509
49574
  }
@@ -49522,7 +49587,7 @@ async function checkGrammarWasmFiles() {
49522
49587
  }
49523
49588
  async function checkCheckpointManifest(directory) {
49524
49589
  const manifestPath = path22.join(directory, ".swarm/checkpoints.json");
49525
- if (!existsSync10(manifestPath)) {
49590
+ if (!existsSync11(manifestPath)) {
49526
49591
  return {
49527
49592
  name: "Checkpoint Manifest",
49528
49593
  status: "\u2705",
@@ -49574,7 +49639,7 @@ async function checkCheckpointManifest(directory) {
49574
49639
  }
49575
49640
  async function checkEventStreamIntegrity(directory) {
49576
49641
  const eventsPath = path22.join(directory, ".swarm/events.jsonl");
49577
- if (!existsSync10(eventsPath)) {
49642
+ if (!existsSync11(eventsPath)) {
49578
49643
  return {
49579
49644
  name: "Event Stream",
49580
49645
  status: "\u2705",
@@ -49615,7 +49680,7 @@ async function checkEventStreamIntegrity(directory) {
49615
49680
  }
49616
49681
  async function checkSteeringDirectives(directory) {
49617
49682
  const eventsPath = path22.join(directory, ".swarm/events.jsonl");
49618
- if (!existsSync10(eventsPath)) {
49683
+ if (!existsSync11(eventsPath)) {
49619
49684
  return {
49620
49685
  name: "Steering Directives",
49621
49686
  status: "\u2705",
@@ -49671,7 +49736,7 @@ async function checkCurator(directory) {
49671
49736
  };
49672
49737
  }
49673
49738
  const summaryPath = path22.join(directory, ".swarm/curator-summary.json");
49674
- if (!existsSync10(summaryPath)) {
49739
+ if (!existsSync11(summaryPath)) {
49675
49740
  return {
49676
49741
  name: "Curator",
49677
49742
  status: "\u2705",
@@ -50570,14 +50635,14 @@ init_schema();
50570
50635
  // src/hooks/knowledge-migrator.ts
50571
50636
  init_knowledge_store();
50572
50637
  import { randomUUID as randomUUID3 } from "crypto";
50573
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
50638
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
50574
50639
  import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
50575
50640
  import * as path24 from "path";
50576
50641
  async function migrateContextToKnowledge(directory, config3) {
50577
50642
  const sentinelPath = path24.join(directory, ".swarm", ".knowledge-migrated");
50578
50643
  const contextPath = path24.join(directory, ".swarm", "context.md");
50579
50644
  const knowledgePath = resolveSwarmKnowledgePath(directory);
50580
- if (existsSync12(sentinelPath)) {
50645
+ if (existsSync13(sentinelPath)) {
50581
50646
  return {
50582
50647
  migrated: false,
50583
50648
  entriesMigrated: 0,
@@ -50586,7 +50651,7 @@ async function migrateContextToKnowledge(directory, config3) {
50586
50651
  skippedReason: "sentinel-exists"
50587
50652
  };
50588
50653
  }
50589
- if (!existsSync12(contextPath)) {
50654
+ if (!existsSync13(contextPath)) {
50590
50655
  return {
50591
50656
  migrated: false,
50592
50657
  entriesMigrated: 0,
@@ -50772,7 +50837,7 @@ function truncateLesson(text) {
50772
50837
  }
50773
50838
  function inferProjectName(directory) {
50774
50839
  const packageJsonPath = path24.join(directory, "package.json");
50775
- if (existsSync12(packageJsonPath)) {
50840
+ if (existsSync13(packageJsonPath)) {
50776
50841
  try {
50777
50842
  const pkg = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
50778
50843
  if (pkg.name && typeof pkg.name === "string") {
@@ -62573,7 +62638,7 @@ init_dist();
62573
62638
  init_config();
62574
62639
  init_knowledge_store();
62575
62640
  init_create_tool();
62576
- import { existsSync as existsSync36 } from "fs";
62641
+ import { existsSync as existsSync37 } from "fs";
62577
62642
  var DEFAULT_LIMIT = 10;
62578
62643
  var MAX_LESSON_LENGTH = 200;
62579
62644
  var VALID_CATEGORIES3 = [
@@ -62642,14 +62707,14 @@ function validateLimit(limit) {
62642
62707
  }
62643
62708
  async function readSwarmKnowledge(directory) {
62644
62709
  const swarmPath = resolveSwarmKnowledgePath(directory);
62645
- if (!existsSync36(swarmPath)) {
62710
+ if (!existsSync37(swarmPath)) {
62646
62711
  return [];
62647
62712
  }
62648
62713
  return readKnowledge(swarmPath);
62649
62714
  }
62650
62715
  async function readHiveKnowledge() {
62651
62716
  const hivePath = resolveHiveKnowledgePath();
62652
- if (!existsSync36(hivePath)) {
62717
+ if (!existsSync37(hivePath)) {
62653
62718
  return [];
62654
62719
  }
62655
62720
  return readKnowledge(hivePath);
@@ -4,7 +4,7 @@
4
4
  * Provides durable, immutable audit trail of plan evolution events.
5
5
  * Each event is written as a JSON line to .swarm/plan-ledger.jsonl
6
6
  */
7
- import type { Plan } from '../config/plan-schema';
7
+ import { type Plan } from '../config/plan-schema';
8
8
  /**
9
9
  * Ledger schema version
10
10
  */
@@ -108,7 +108,7 @@ export declare function readLedgerEvents(directory: string): Promise<LedgerEvent
108
108
  * @param directory - The working directory
109
109
  * @param planId - Unique identifier for the plan
110
110
  */
111
- export declare function initLedger(directory: string, planId: string): Promise<void>;
111
+ export declare function initLedger(directory: string, planId: string, initialPlanHash?: string): Promise<void>;
112
112
  /**
113
113
  * Append a new event to the ledger.
114
114
  * Uses atomic write: write to temp file then rename.
@@ -5,6 +5,10 @@ import { type Plan, type TaskStatus } from '../config/plan-schema';
5
5
  * Use this when you want to check for structured plans without triggering migration.
6
6
  */
7
7
  export declare function loadPlanJsonOnly(directory: string): Promise<Plan | null>;
8
+ /**
9
+ * Regenerate plan.md from valid plan.json (auto-heal case 1).
10
+ */
11
+ export declare function regeneratePlanMarkdown(directory: string, plan: Plan): Promise<void>;
8
12
  /**
9
13
  * Load and validate plan from .swarm/plan.json with auto-heal sync.
10
14
  *
@@ -38,6 +42,14 @@ export declare function rebuildPlan(directory: string, plan?: Plan): Promise<Pla
38
42
  /**
39
43
  * Load plan → find task by ID → update status → save → return updated plan.
40
44
  * Throw if plan not found or task not found.
45
+ *
46
+ * Uses loadPlan() (not loadPlanJsonOnly) so that legitimate same-identity ledger
47
+ * drift is detected and healed before the status update is applied. Without this,
48
+ * a stale plan.json would silently overwrite ledger-ahead task state with only the
49
+ * one targeted status change applied on top.
50
+ *
51
+ * The migration guard in loadPlan() (plan_id identity check) prevents destructive
52
+ * revert after a swarm rename — so this is safe even in post-migration scenarios.
41
53
  */
42
54
  export declare function updateTaskStatus(directory: string, taskId: string, status: TaskStatus): Promise<Plan>;
43
55
  /**
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Tests for the migration-aware identity guard in loadPlan()'s validation-failure
3
+ * catch path (lines ~299-323 of manager.ts).
4
+ *
5
+ * When plan.json fails schema validation, the old code unconditionally called
6
+ * replayFromLedger(). This allowed a post-migration ledger (old identity) to
7
+ * overwrite a schema-invalid but correctly migrated plan.json.
8
+ *
9
+ * The fix: extract swarm+title from the raw JSON (even if schema validation
10
+ * fails), compare against the first ledger event's plan_id, and only replay
11
+ * when identities match.
12
+ */
13
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Regression tests for GitHub issues #383/#384:
3
+ * PlanSyncWorker Aggressively Reverts Plan Files
4
+ *
5
+ * These tests verify that the fixes introduced in the debug-issues-383-384 branch
6
+ * prevent the destructive revert behavior and related edge cases.
7
+ */
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.47.0",
3
+ "version": "6.47.1",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",