opencode-swarm 6.62.0 → 6.63.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
@@ -15788,22 +15788,26 @@ async function readLedgerEvents(directory) {
15788
15788
  return [];
15789
15789
  }
15790
15790
  }
15791
- async function initLedger(directory, planId, initialPlanHash) {
15791
+ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
15792
15792
  const ledgerPath = getLedgerPath(directory);
15793
15793
  const planJsonPath = getPlanJsonPath(directory);
15794
15794
  if (fs3.existsSync(ledgerPath)) {
15795
15795
  throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
15796
15796
  }
15797
15797
  let planHashAfter = initialPlanHash ?? "";
15798
+ let embeddedPlan = initialPlan;
15798
15799
  if (!initialPlanHash) {
15799
15800
  try {
15800
15801
  if (fs3.existsSync(planJsonPath)) {
15801
15802
  const content = fs3.readFileSync(planJsonPath, "utf8");
15802
15803
  const plan = JSON.parse(content);
15803
15804
  planHashAfter = computePlanHash(plan);
15805
+ if (!embeddedPlan)
15806
+ embeddedPlan = plan;
15804
15807
  }
15805
15808
  } catch {}
15806
15809
  }
15810
+ const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
15807
15811
  const event = {
15808
15812
  seq: 1,
15809
15813
  timestamp: new Date().toISOString(),
@@ -15812,7 +15816,8 @@ async function initLedger(directory, planId, initialPlanHash) {
15812
15816
  source: "initLedger",
15813
15817
  plan_hash_before: "",
15814
15818
  plan_hash_after: planHashAfter,
15815
- schema_version: LEDGER_SCHEMA_VERSION
15819
+ schema_version: LEDGER_SCHEMA_VERSION,
15820
+ ...payload ? { payload } : {}
15816
15821
  };
15817
15822
  fs3.mkdirSync(path3.join(directory, ".swarm"), { recursive: true });
15818
15823
  const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
@@ -15899,7 +15904,7 @@ async function takeSnapshotEvent(directory, plan, options) {
15899
15904
  payload: snapshotPayload
15900
15905
  }, { planHashAfter: options?.planHashAfter });
15901
15906
  }
15902
- async function replayFromLedger(directory, options) {
15907
+ async function replayFromLedger(directory, _options) {
15903
15908
  const events = await readLedgerEvents(directory);
15904
15909
  if (events.length === 0) {
15905
15910
  return null;
@@ -15922,6 +15927,20 @@ async function replayFromLedger(directory, options) {
15922
15927
  return plan2;
15923
15928
  }
15924
15929
  }
15930
+ const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
15931
+ if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
15932
+ const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
15933
+ if (parseResult.success) {
15934
+ let plan2 = parseResult.data;
15935
+ const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
15936
+ for (const event of eventsAfterCreated) {
15937
+ if (plan2 === null)
15938
+ return null;
15939
+ plan2 = applyEventToPlan(plan2, event);
15940
+ }
15941
+ return plan2;
15942
+ }
15943
+ }
15925
15944
  const planJsonPath = getPlanJsonPath(directory);
15926
15945
  if (!fs3.existsSync(planJsonPath)) {
15927
15946
  return null;
@@ -15944,6 +15963,11 @@ async function replayFromLedger(directory, options) {
15944
15963
  function applyEventToPlan(plan, event) {
15945
15964
  switch (event.event_type) {
15946
15965
  case "plan_created":
15966
+ if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
15967
+ const parsed = PlanSchema.safeParse(event.payload.plan);
15968
+ if (parsed.success)
15969
+ return parsed.data;
15970
+ }
15947
15971
  return plan;
15948
15972
  case "task_status_changed":
15949
15973
  if (event.task_id && event.to_status) {
@@ -16032,7 +16056,13 @@ var init_ledger = __esm(() => {
16032
16056
  });
16033
16057
 
16034
16058
  // src/plan/manager.ts
16035
- import { copyFileSync, existsSync as existsSync3, renameSync as renameSync2, unlinkSync } from "fs";
16059
+ import {
16060
+ copyFileSync,
16061
+ existsSync as existsSync3,
16062
+ readdirSync,
16063
+ renameSync as renameSync2,
16064
+ unlinkSync
16065
+ } from "fs";
16036
16066
  import * as fsPromises from "fs/promises";
16037
16067
  import * as path4 from "path";
16038
16068
  async function loadPlanJsonOnly(directory) {
@@ -16284,35 +16314,53 @@ async function loadPlan(directory) {
16284
16314
  return migrated;
16285
16315
  }
16286
16316
  if (await ledgerExists(directory)) {
16287
- const rebuilt = await replayFromLedger(directory);
16288
- if (rebuilt) {
16289
- await savePlan(directory, rebuilt);
16290
- return rebuilt;
16291
- }
16317
+ const resolvedDir = path4.resolve(directory);
16318
+ const existingMutex = recoveryMutexes.get(resolvedDir);
16319
+ if (existingMutex) {
16320
+ await existingMutex;
16321
+ const postRecoveryPlan = await loadPlanJsonOnly(directory);
16322
+ if (postRecoveryPlan)
16323
+ return postRecoveryPlan;
16324
+ }
16325
+ let resolveRecovery;
16326
+ const mutex = new Promise((r) => {
16327
+ resolveRecovery = r;
16328
+ });
16329
+ recoveryMutexes.set(resolvedDir, mutex);
16292
16330
  try {
16293
- const anchorEvents = await readLedgerEvents(directory);
16294
- if (anchorEvents.length === 0) {
16295
- warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
16296
- return null;
16331
+ const rebuilt = await replayFromLedger(directory);
16332
+ if (rebuilt) {
16333
+ await savePlan(directory, rebuilt);
16334
+ return rebuilt;
16297
16335
  }
16298
- const expectedPlanId = anchorEvents[0].plan_id;
16299
- const approved = await loadLastApprovedPlan(directory, expectedPlanId);
16300
- if (approved) {
16301
- const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
16302
- 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.`);
16303
- await savePlan(directory, approved.plan);
16304
- try {
16305
- await takeSnapshotEvent(directory, approved.plan, {
16306
- source: "recovery_from_approved_snapshot",
16307
- approvalMetadata: approved.approval
16308
- });
16309
- } catch (healError) {
16310
- warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
16336
+ try {
16337
+ const anchorEvents = await readLedgerEvents(directory);
16338
+ if (anchorEvents.length === 0) {
16339
+ warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
16340
+ return null;
16341
+ }
16342
+ const expectedPlanId = anchorEvents[0].plan_id;
16343
+ const approved = await loadLastApprovedPlan(directory, expectedPlanId);
16344
+ if (approved) {
16345
+ const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
16346
+ 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.`);
16347
+ await savePlan(directory, approved.plan);
16348
+ try {
16349
+ await takeSnapshotEvent(directory, approved.plan, {
16350
+ source: "recovery_from_approved_snapshot",
16351
+ approvalMetadata: approved.approval
16352
+ });
16353
+ } catch (healError) {
16354
+ warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
16355
+ }
16356
+ return approved.plan;
16311
16357
  }
16312
- return approved.plan;
16358
+ } catch (recoveryError) {
16359
+ warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
16313
16360
  }
16314
- } catch (recoveryError) {
16315
- warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
16361
+ } finally {
16362
+ resolveRecovery();
16363
+ recoveryMutexes.delete(resolvedDir);
16316
16364
  }
16317
16365
  }
16318
16366
  return null;
@@ -16361,7 +16409,7 @@ async function savePlan(directory, plan, options) {
16361
16409
  const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
16362
16410
  const planHashForInit = computePlanHash(validated);
16363
16411
  if (!await ledgerExists(directory)) {
16364
- await initLedger(directory, planId, planHashForInit);
16412
+ await initLedger(directory, planId, planHashForInit, validated);
16365
16413
  } else {
16366
16414
  const existingEvents = await readLedgerEvents(directory);
16367
16415
  if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
@@ -16380,7 +16428,7 @@ async function savePlan(directory, plan, options) {
16380
16428
  let initSucceeded = false;
16381
16429
  if (backupExists) {
16382
16430
  try {
16383
- await initLedger(directory, planId, planHashForInit);
16431
+ await initLedger(directory, planId, planHashForInit, validated);
16384
16432
  initSucceeded = true;
16385
16433
  } catch (initErr) {
16386
16434
  const errorMessage = String(initErr);
@@ -16422,6 +16470,19 @@ async function savePlan(directory, plan, options) {
16422
16470
  unlinkSync(oldLedgerBackupPath);
16423
16471
  } catch {}
16424
16472
  }
16473
+ const MAX_ARCHIVED_SIBLINGS = 5;
16474
+ try {
16475
+ const allFiles = readdirSync(swarmDir2);
16476
+ const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
16477
+ if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
16478
+ const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
16479
+ for (const file2 of toRemove) {
16480
+ try {
16481
+ unlinkSync(path4.join(swarmDir2, file2));
16482
+ } catch {}
16483
+ }
16484
+ }
16485
+ } catch {}
16425
16486
  }
16426
16487
  }
16427
16488
  const currentHash = computeCurrentPlanHash(directory);
@@ -16471,7 +16532,7 @@ async function savePlan(directory, plan, options) {
16471
16532
  }
16472
16533
  } catch (error49) {
16473
16534
  if (error49 instanceof LedgerStaleWriterError) {
16474
- throw new Error(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
16535
+ throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
16475
16536
  }
16476
16537
  throw error49;
16477
16538
  }
@@ -16481,7 +16542,11 @@ async function savePlan(directory, plan, options) {
16481
16542
  if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
16482
16543
  await takeSnapshotEvent(directory, validated, {
16483
16544
  planHashAfter: hashAfter
16484
- }).catch(() => {});
16545
+ }).catch((err2) => {
16546
+ if (process.env.DEBUG_SWARM) {
16547
+ warn(`[savePlan] Periodic snapshot write failed (non-fatal): ${err2 instanceof Error ? err2.message : String(err2)}`);
16548
+ }
16549
+ });
16485
16550
  }
16486
16551
  const swarmDir = path4.resolve(directory, ".swarm");
16487
16552
  const planPath = path4.join(swarmDir, "plan.json");
@@ -16494,19 +16559,23 @@ async function savePlan(directory, plan, options) {
16494
16559
  unlinkSync(tempPath);
16495
16560
  } catch {}
16496
16561
  }
16497
- const contentHash = computePlanContentHash(validated);
16498
- const markdown = derivePlanMarkdown(validated);
16499
- const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
16500
- ${markdown}`;
16501
- const mdPath = path4.join(swarmDir, "plan.md");
16502
- const mdTempPath = path4.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
16503
16562
  try {
16504
- await Bun.write(mdTempPath, markdownWithHash);
16505
- renameSync2(mdTempPath, mdPath);
16506
- } finally {
16563
+ const contentHash = computePlanContentHash(validated);
16564
+ const markdown = derivePlanMarkdown(validated);
16565
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
16566
+ ${markdown}`;
16567
+ const mdPath = path4.join(swarmDir, "plan.md");
16568
+ const mdTempPath = path4.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
16507
16569
  try {
16508
- unlinkSync(mdTempPath);
16509
- } catch {}
16570
+ await Bun.write(mdTempPath, markdownWithHash);
16571
+ renameSync2(mdTempPath, mdPath);
16572
+ } finally {
16573
+ try {
16574
+ unlinkSync(mdTempPath);
16575
+ } catch {}
16576
+ }
16577
+ } catch (mdError) {
16578
+ warn(`[savePlan] plan.md write failed (non-fatal, plan.json is authoritative): ${mdError instanceof Error ? mdError.message : String(mdError)}`);
16510
16579
  }
16511
16580
  try {
16512
16581
  const markerPath = path4.join(swarmDir, ".plan-write-marker");
@@ -16551,11 +16620,6 @@ ${markdown}`;
16551
16620
  return targetPlan;
16552
16621
  }
16553
16622
  async function updateTaskStatus(directory, taskId, status) {
16554
- const plan = await loadPlan(directory);
16555
- if (plan === null) {
16556
- throw new Error(`Plan not found in directory: ${directory}`);
16557
- }
16558
- let taskFound = false;
16559
16623
  const derivePhaseStatusFromTasks = (tasks) => {
16560
16624
  if (tasks.length > 0 && tasks.every((task) => task.status === "completed")) {
16561
16625
  return "complete";
@@ -16568,26 +16632,44 @@ async function updateTaskStatus(directory, taskId, status) {
16568
16632
  }
16569
16633
  return "pending";
16570
16634
  };
16571
- const updatedPhases = plan.phases.map((phase) => {
16572
- const updatedTasks = phase.tasks.map((task) => {
16573
- if (task.id === taskId) {
16574
- taskFound = true;
16575
- return { ...task, status };
16576
- }
16577
- return task;
16635
+ const MAX_OUTER_RETRIES = 1;
16636
+ for (let attempt = 0;attempt <= MAX_OUTER_RETRIES; attempt++) {
16637
+ const plan = await loadPlan(directory);
16638
+ if (plan === null) {
16639
+ throw new Error(`Plan not found in directory: ${directory}`);
16640
+ }
16641
+ let taskFound = false;
16642
+ const updatedPhases = plan.phases.map((phase) => {
16643
+ const updatedTasks = phase.tasks.map((task) => {
16644
+ if (task.id === taskId) {
16645
+ taskFound = true;
16646
+ return { ...task, status };
16647
+ }
16648
+ return task;
16649
+ });
16650
+ return {
16651
+ ...phase,
16652
+ status: derivePhaseStatusFromTasks(updatedTasks),
16653
+ tasks: updatedTasks
16654
+ };
16578
16655
  });
16579
- return {
16580
- ...phase,
16581
- status: derivePhaseStatusFromTasks(updatedTasks),
16582
- tasks: updatedTasks
16583
- };
16584
- });
16585
- if (!taskFound) {
16586
- throw new Error(`Task not found: ${taskId}`);
16656
+ if (!taskFound) {
16657
+ throw new Error(`Task not found: ${taskId}`);
16658
+ }
16659
+ const updatedPlan = { ...plan, phases: updatedPhases };
16660
+ try {
16661
+ await savePlan(directory, updatedPlan, {
16662
+ preserveCompletedStatuses: true
16663
+ });
16664
+ return updatedPlan;
16665
+ } catch (error49) {
16666
+ if (error49 instanceof PlanConcurrentModificationError && attempt < MAX_OUTER_RETRIES) {
16667
+ continue;
16668
+ }
16669
+ throw error49;
16670
+ }
16587
16671
  }
16588
- const updatedPlan = { ...plan, phases: updatedPhases };
16589
- await savePlan(directory, updatedPlan, { preserveCompletedStatuses: true });
16590
- return updatedPlan;
16672
+ throw new Error("updateTaskStatus: unexpected loop exit");
16591
16673
  }
16592
16674
  function derivePlanMarkdown(plan) {
16593
16675
  const statusMap = {
@@ -16855,57 +16937,90 @@ function migrateLegacyPlan(planContent, swarmId) {
16855
16937
  };
16856
16938
  return plan;
16857
16939
  }
16858
- var startupLedgerCheckedWorkspaces;
16940
+ var PlanConcurrentModificationError, startupLedgerCheckedWorkspaces, recoveryMutexes;
16859
16941
  var init_manager = __esm(() => {
16860
16942
  init_plan_schema();
16861
16943
  init_utils2();
16862
16944
  init_utils();
16863
16945
  init_spec_hash();
16864
16946
  init_ledger();
16947
+ PlanConcurrentModificationError = class PlanConcurrentModificationError extends Error {
16948
+ constructor(message) {
16949
+ super(message);
16950
+ this.name = "PlanConcurrentModificationError";
16951
+ }
16952
+ };
16865
16953
  startupLedgerCheckedWorkspaces = new Set;
16954
+ recoveryMutexes = new Map;
16866
16955
  });
16867
16956
 
16868
- // src/evidence/manager.ts
16869
- import { mkdirSync as mkdirSync2, readdirSync, rmSync, statSync as statSync2 } from "fs";
16870
- import * as fs4 from "fs/promises";
16871
- import * as path5 from "path";
16872
- function isValidEvidenceType(type) {
16873
- return VALID_EVIDENCE_TYPES.includes(type);
16874
- }
16875
- function isSecretscanEvidence(evidence) {
16876
- return evidence.type === "secretscan";
16877
- }
16878
- function sanitizeTaskId(taskId) {
16957
+ // src/validation/task-id.ts
16958
+ function checkUnsafeChars(taskId) {
16879
16959
  if (!taskId || taskId.length === 0) {
16880
- throw new Error("Invalid task ID: empty string");
16960
+ return "Invalid task ID: empty string";
16881
16961
  }
16882
16962
  if (/\0/.test(taskId)) {
16883
- throw new Error("Invalid task ID: contains null bytes");
16963
+ return "Invalid task ID: contains null bytes";
16884
16964
  }
16885
16965
  for (let i2 = 0;i2 < taskId.length; i2++) {
16886
16966
  if (taskId.charCodeAt(i2) < 32) {
16887
- throw new Error("Invalid task ID: contains control characters");
16967
+ return "Invalid task ID: contains control characters";
16888
16968
  }
16889
16969
  }
16890
- if (taskId.includes("..") || taskId.includes("../") || taskId.includes("..\\")) {
16891
- throw new Error("Invalid task ID: path traversal detected");
16892
- }
16893
- if (TASK_ID_REGEX.test(taskId)) {
16894
- return taskId;
16970
+ if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
16971
+ return "Invalid task ID: path traversal detected";
16895
16972
  }
16896
- if (RETRO_TASK_ID_REGEX.test(taskId)) {
16897
- return taskId;
16973
+ return;
16974
+ }
16975
+ function isStrictTaskId(taskId) {
16976
+ if (!taskId)
16977
+ return false;
16978
+ const unsafeMsg = checkUnsafeChars(taskId);
16979
+ if (unsafeMsg)
16980
+ return false;
16981
+ return STRICT_TASK_ID_PATTERN.test(taskId);
16982
+ }
16983
+ function assertStrictTaskId(taskId) {
16984
+ if (!isStrictTaskId(taskId)) {
16985
+ throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
16898
16986
  }
16899
- if (INTERNAL_TOOL_ID_REGEX.test(taskId)) {
16900
- return taskId;
16987
+ }
16988
+ function sanitizeTaskId(taskId) {
16989
+ const unsafeMsg = checkUnsafeChars(taskId);
16990
+ if (unsafeMsg) {
16991
+ throw new Error(unsafeMsg);
16901
16992
  }
16902
- if (GENERAL_TASK_ID_REGEX.test(taskId)) {
16993
+ if (STRICT_TASK_ID_PATTERN.test(taskId) || RETRO_TASK_ID_REGEX.test(taskId) || INTERNAL_TOOL_ID_REGEX.test(taskId) || GENERAL_TASK_ID_REGEX.test(taskId)) {
16903
16994
  return taskId;
16904
16995
  }
16905
16996
  throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
16906
16997
  }
16998
+ function validateTaskIdFormat(taskId) {
16999
+ if (!STRICT_TASK_ID_PATTERN.test(taskId)) {
17000
+ return `Invalid taskId "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
17001
+ }
17002
+ return;
17003
+ }
17004
+ var STRICT_TASK_ID_PATTERN, RETRO_TASK_ID_REGEX, INTERNAL_TOOL_ID_REGEX, GENERAL_TASK_ID_REGEX;
17005
+ var init_task_id = __esm(() => {
17006
+ STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
17007
+ RETRO_TASK_ID_REGEX = /^retro-\d+$/;
17008
+ INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
17009
+ GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
17010
+ });
17011
+
17012
+ // src/evidence/manager.ts
17013
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
17014
+ import * as fs4 from "fs/promises";
17015
+ import * as path5 from "path";
17016
+ function isValidEvidenceType(type) {
17017
+ return VALID_EVIDENCE_TYPES.includes(type);
17018
+ }
17019
+ function isSecretscanEvidence(evidence) {
17020
+ return evidence.type === "secretscan";
17021
+ }
16907
17022
  async function saveEvidence(directory, taskId, evidence) {
16908
- const sanitizedTaskId = sanitizeTaskId(taskId);
17023
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
16909
17024
  const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
16910
17025
  const evidencePath = validateSwarmPath(directory, relativePath);
16911
17026
  const evidenceDir = path5.dirname(evidencePath);
@@ -16936,9 +17051,14 @@ async function saveEvidence(directory, taskId, evidence) {
16936
17051
  updated_at: now
16937
17052
  };
16938
17053
  }
17054
+ const MAX_BUNDLE_ENTRIES = 100;
17055
+ let entries = [...bundle.entries, evidence];
17056
+ if (entries.length > MAX_BUNDLE_ENTRIES) {
17057
+ entries = entries.slice(entries.length - MAX_BUNDLE_ENTRIES);
17058
+ }
16939
17059
  const updatedBundle = {
16940
17060
  ...bundle,
16941
- entries: [...bundle.entries, evidence],
17061
+ entries,
16942
17062
  updated_at: new Date().toISOString()
16943
17063
  };
16944
17064
  const bundleJson = JSON.stringify(updatedBundle);
@@ -16983,7 +17103,7 @@ function wrapFlatRetrospective(flatEntry, taskId) {
16983
17103
  };
16984
17104
  }
16985
17105
  async function loadEvidence(directory, taskId) {
16986
- const sanitizedTaskId = sanitizeTaskId(taskId);
17106
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
16987
17107
  const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
16988
17108
  const evidencePath = validateSwarmPath(directory, relativePath);
16989
17109
  const content = await readSwarmFileAsync(directory, relativePath);
@@ -17037,7 +17157,7 @@ async function listEvidenceTaskIds(directory) {
17037
17157
  }
17038
17158
  let entries;
17039
17159
  try {
17040
- entries = readdirSync(evidenceBasePath);
17160
+ entries = readdirSync2(evidenceBasePath);
17041
17161
  } catch {
17042
17162
  return [];
17043
17163
  }
@@ -17049,7 +17169,7 @@ async function listEvidenceTaskIds(directory) {
17049
17169
  if (!stats.isDirectory()) {
17050
17170
  continue;
17051
17171
  }
17052
- sanitizeTaskId(entry);
17172
+ sanitizeTaskId2(entry);
17053
17173
  taskIds.push(entry);
17054
17174
  } catch (error49) {
17055
17175
  if (error49 instanceof Error && !error49.message.startsWith("Invalid task ID")) {
@@ -17060,7 +17180,7 @@ async function listEvidenceTaskIds(directory) {
17060
17180
  return taskIds.sort();
17061
17181
  }
17062
17182
  async function deleteEvidence(directory, taskId) {
17063
- const sanitizedTaskId = sanitizeTaskId(taskId);
17183
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
17064
17184
  const relativePath = path5.join("evidence", sanitizedTaskId);
17065
17185
  const evidenceDir = validateSwarmPath(directory, relativePath);
17066
17186
  try {
@@ -17122,12 +17242,13 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
17122
17242
  }
17123
17243
  return archived;
17124
17244
  }
17125
- var VALID_EVIDENCE_TYPES, TASK_ID_REGEX, RETRO_TASK_ID_REGEX, INTERNAL_TOOL_ID_REGEX, GENERAL_TASK_ID_REGEX, LEGACY_TASK_COMPLEXITY_MAP;
17245
+ var VALID_EVIDENCE_TYPES, sanitizeTaskId2, LEGACY_TASK_COMPLEXITY_MAP;
17126
17246
  var init_manager2 = __esm(() => {
17127
17247
  init_zod();
17128
17248
  init_evidence_schema();
17129
17249
  init_utils2();
17130
17250
  init_utils();
17251
+ init_task_id();
17131
17252
  VALID_EVIDENCE_TYPES = [
17132
17253
  "review",
17133
17254
  "test",
@@ -17143,10 +17264,7 @@ var init_manager2 = __esm(() => {
17143
17264
  "quality_budget",
17144
17265
  "secretscan"
17145
17266
  ];
17146
- TASK_ID_REGEX = /^\d+\.\d+(\.\d+)*$/;
17147
- RETRO_TASK_ID_REGEX = /^retro-\d+$/;
17148
- INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
17149
- GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
17267
+ sanitizeTaskId2 = sanitizeTaskId;
17150
17268
  LEGACY_TASK_COMPLEXITY_MAP = {
17151
17269
  low: "simple",
17152
17270
  medium: "moderate",
@@ -40852,22 +40970,10 @@ __export(exports_gate_evidence, {
40852
40970
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync18, renameSync as renameSync10, unlinkSync as unlinkSync5 } from "fs";
40853
40971
  import * as path39 from "path";
40854
40972
  function isValidTaskId2(taskId) {
40855
- if (!taskId)
40856
- return false;
40857
- if (taskId.includes(".."))
40858
- return false;
40859
- if (taskId.includes("/"))
40860
- return false;
40861
- if (taskId.includes("\\"))
40862
- return false;
40863
- if (taskId.includes("\x00"))
40864
- return false;
40865
- return TASK_ID_PATTERN.test(taskId);
40973
+ return isStrictTaskId(taskId);
40866
40974
  }
40867
40975
  function assertValidTaskId(taskId) {
40868
- if (!isValidTaskId2(taskId)) {
40869
- throw new Error(`Invalid taskId: "${taskId}". Must match N.M or N.M.P (e.g. "1.1", "1.2.3").`);
40870
- }
40976
+ assertStrictTaskId(taskId);
40871
40977
  }
40872
40978
  function deriveRequiredGates(agentType) {
40873
40979
  switch (agentType) {
@@ -40900,6 +41006,7 @@ function getEvidenceDir(directory) {
40900
41006
  return path39.join(directory, ".swarm", "evidence");
40901
41007
  }
40902
41008
  function getEvidencePath(directory, taskId) {
41009
+ assertValidTaskId(taskId);
40903
41010
  return path39.join(getEvidenceDir(directory), `${taskId}.json`);
40904
41011
  }
40905
41012
  function readExisting(evidencePath) {
@@ -40987,11 +41094,11 @@ async function hasPassedAllGates(directory, taskId) {
40987
41094
  return false;
40988
41095
  return evidence.required_gates.every((gate) => evidence.gates[gate] != null);
40989
41096
  }
40990
- var DEFAULT_REQUIRED_GATES, TASK_ID_PATTERN;
41097
+ var DEFAULT_REQUIRED_GATES;
40991
41098
  var init_gate_evidence = __esm(() => {
40992
41099
  init_telemetry();
41100
+ init_task_id();
40993
41101
  DEFAULT_REQUIRED_GATES = ["reviewer", "test_engineer"];
40994
- TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
40995
41102
  });
40996
41103
 
40997
41104
  // src/hooks/review-receipt.ts
@@ -47288,11 +47395,14 @@ async function executeWriteRetro(args2, directory) {
47288
47395
  try {
47289
47396
  const allTaskIds = await listEvidenceTaskIds(directory);
47290
47397
  const phaseTaskIds = allTaskIds.filter((id) => id.startsWith(`${phase}.`));
47398
+ const sessionStart = args2.metadata && typeof args2.metadata.session_start === "string" ? args2.metadata.session_start : undefined;
47291
47399
  for (const phaseTaskId of phaseTaskIds) {
47292
47400
  const result = await loadEvidence(directory, phaseTaskId);
47293
47401
  if (result.status !== "found")
47294
47402
  continue;
47295
47403
  const bundle = result.bundle;
47404
+ if (sessionStart && bundle.updated_at < sessionStart)
47405
+ continue;
47296
47406
  for (const entry of bundle.entries) {
47297
47407
  const e = entry;
47298
47408
  if (e.type === "review" && e.verdict === "fail") {
@@ -47484,6 +47594,18 @@ async function handleCloseCommand(directory, args2) {
47484
47594
  }
47485
47595
  }
47486
47596
  }
47597
+ let sessionStart;
47598
+ {
47599
+ let earliest = Infinity;
47600
+ for (const [, session] of swarmState.agentSessions) {
47601
+ if (session.lastAgentEventTime > 0 && session.lastAgentEventTime < earliest) {
47602
+ earliest = session.lastAgentEventTime;
47603
+ }
47604
+ }
47605
+ if (earliest < Infinity) {
47606
+ sessionStart = new Date(earliest).toISOString();
47607
+ }
47608
+ }
47487
47609
  const wrotePhaseRetro = closedPhases.length > 0;
47488
47610
  if (!wrotePhaseRetro && !planExists) {
47489
47611
  try {
@@ -47499,7 +47621,10 @@ async function handleCloseCommand(directory, args2) {
47499
47621
  test_failures: 0,
47500
47622
  security_findings: 0,
47501
47623
  integration_issues: 0,
47502
- metadata: { session_scope: "plan_free" }
47624
+ metadata: {
47625
+ session_scope: "plan_free",
47626
+ ...sessionStart ? { session_start: sessionStart } : {}
47627
+ }
47503
47628
  }, directory);
47504
47629
  try {
47505
47630
  const parsed = JSON.parse(sessionRetroResult);
@@ -47556,7 +47681,8 @@ async function handleCloseCommand(directory, args2) {
47556
47681
  }
47557
47682
  }
47558
47683
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
47559
- const archiveDir = path14.join(swarmDir, "archive", `swarm-${timestamp}`);
47684
+ const suffix = Math.random().toString(36).slice(2, 8);
47685
+ const archiveDir = path14.join(swarmDir, "archive", `swarm-${timestamp}-${suffix}`);
47560
47686
  let archiveResult = "";
47561
47687
  let archivedFileCount = 0;
47562
47688
  const archivedActiveStateFiles = new Set;
@@ -47820,11 +47946,23 @@ async function handleCloseCommand(directory, args2) {
47820
47946
  if (pruneErrors.length > 0) {
47821
47947
  warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
47822
47948
  }
47823
- const warningMsg = warnings.length > 0 ? `
47949
+ const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
47950
+ const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
47951
+ let warningMsg = "";
47952
+ if (retroWarnings.length > 0) {
47953
+ warningMsg += `
47954
+
47955
+ **\u26A0 Retrospective evidence incomplete:**
47956
+ ${retroWarnings.map((w) => `- ${w}`).join(`
47957
+ `)}`;
47958
+ }
47959
+ if (otherWarnings.length > 0) {
47960
+ warningMsg += `
47824
47961
 
47825
47962
  **Warnings:**
47826
- ${warnings.map((w) => `- ${w}`).join(`
47827
- `)}` : "";
47963
+ ${otherWarnings.map((w) => `- ${w}`).join(`
47964
+ `)}`;
47965
+ }
47828
47966
  if (planAlreadyDone) {
47829
47967
  return `\u2705 Session finalized. Plan was already in a terminal state \u2014 cleanup and archive applied.
47830
47968
 
@@ -49074,7 +49212,7 @@ init_manager2();
49074
49212
  init_utils2();
49075
49213
  init_manager();
49076
49214
  import * as child_process4 from "child_process";
49077
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
49215
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
49078
49216
  import path19 from "path";
49079
49217
  import { fileURLToPath } from "url";
49080
49218
  function validateTaskDag(plan) {
@@ -49277,7 +49415,7 @@ async function checkPlanSync(directory, plan) {
49277
49415
  }
49278
49416
  async function checkConfigBackups(directory) {
49279
49417
  try {
49280
- const files = readdirSync2(directory);
49418
+ const files = readdirSync3(directory);
49281
49419
  const backupCount = files.filter((f) => /\.opencode-swarm\.yaml\.bak/.test(f)).length;
49282
49420
  if (backupCount <= 5) {
49283
49421
  return {
@@ -49743,7 +49881,7 @@ async function getDiagnoseData(directory) {
49743
49881
  checks5.push(await checkCurator(directory));
49744
49882
  try {
49745
49883
  const evidenceDir = path19.join(directory, ".swarm", "evidence");
49746
- const snapshotFiles = existsSync8(evidenceDir) ? readdirSync2(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
49884
+ const snapshotFiles = existsSync8(evidenceDir) ? readdirSync3(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
49747
49885
  if (snapshotFiles.length > 0) {
49748
49886
  const latest = snapshotFiles.sort().pop();
49749
49887
  checks5.push({
@@ -51975,7 +52113,7 @@ async function handleResetSessionCommand(directory, _args) {
51975
52113
  // src/summaries/manager.ts
51976
52114
  init_utils2();
51977
52115
  init_utils();
51978
- import { mkdirSync as mkdirSync9, readdirSync as readdirSync8, renameSync as renameSync8, rmSync as rmSync3, statSync as statSync7 } from "fs";
52116
+ import { mkdirSync as mkdirSync9, readdirSync as readdirSync9, renameSync as renameSync8, rmSync as rmSync3, statSync as statSync7 } from "fs";
51979
52117
  import * as path31 from "path";
51980
52118
  var SUMMARY_ID_REGEX = /^S\d+$/;
51981
52119
  function sanitizeSummaryId(id) {
@@ -64697,24 +64835,14 @@ var build_check = createSwarmTool({
64697
64835
  // src/tools/check-gate-status.ts
64698
64836
  init_dist();
64699
64837
  init_manager2();
64838
+ init_task_id();
64700
64839
  init_create_tool();
64701
64840
  init_resolve_working_directory();
64702
64841
  import * as fs42 from "fs";
64703
64842
  import * as path54 from "path";
64704
64843
  var EVIDENCE_DIR = ".swarm/evidence";
64705
- var TASK_ID_PATTERN2 = /^\d+\.\d+(\.\d+)*$/;
64706
64844
  function isValidTaskId3(taskId) {
64707
- if (!taskId)
64708
- return false;
64709
- if (taskId.includes(".."))
64710
- return false;
64711
- if (taskId.includes("/"))
64712
- return false;
64713
- if (taskId.includes("\\"))
64714
- return false;
64715
- if (taskId.includes("\x00"))
64716
- return false;
64717
- return TASK_ID_PATTERN2.test(taskId);
64845
+ return isStrictTaskId(taskId);
64718
64846
  }
64719
64847
  function isPathWithinSwarm(filePath, workspaceRoot) {
64720
64848
  const normalizedWorkspace = path54.resolve(workspaceRoot);
@@ -66182,15 +66310,12 @@ var curator_analyze = createSwarmTool({
66182
66310
  // src/tools/declare-scope.ts
66183
66311
  init_tool();
66184
66312
  init_state();
66313
+ init_task_id();
66185
66314
  init_create_tool();
66186
66315
  import * as fs46 from "fs";
66187
66316
  import * as path58 from "path";
66188
- function validateTaskIdFormat(taskId) {
66189
- const taskIdPattern = /^\d+\.\d+(\.\d+)*$/;
66190
- if (!taskIdPattern.test(taskId)) {
66191
- return `Invalid taskId "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
66192
- }
66193
- return;
66317
+ function validateTaskIdFormat2(taskId) {
66318
+ return validateTaskIdFormat(taskId);
66194
66319
  }
66195
66320
  function validateFiles(files) {
66196
66321
  const errors5 = [];
@@ -66208,7 +66333,7 @@ function validateFiles(files) {
66208
66333
  return errors5;
66209
66334
  }
66210
66335
  async function executeDeclareScope(args2, fallbackDir) {
66211
- const taskIdError = validateTaskIdFormat(args2.taskId);
66336
+ const taskIdError = validateTaskIdFormat2(args2.taskId);
66212
66337
  if (taskIdError) {
66213
66338
  return {
66214
66339
  success: false,
@@ -76367,6 +76492,7 @@ async function validateDiffScope(taskId, directory) {
76367
76492
  init_manager();
76368
76493
  init_state();
76369
76494
  init_telemetry();
76495
+ init_task_id();
76370
76496
  init_create_tool();
76371
76497
  var VALID_STATUSES2 = [
76372
76498
  "pending",
@@ -76381,8 +76507,8 @@ function validateStatus(status) {
76381
76507
  return;
76382
76508
  }
76383
76509
  function validateTaskId(taskId) {
76384
- const taskIdPattern = /^\d+\.\d+(\.\d+)*$/;
76385
- if (!taskIdPattern.test(taskId)) {
76510
+ const result = validateTaskIdFormat(taskId);
76511
+ if (result) {
76386
76512
  return `Invalid task_id "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
76387
76513
  }
76388
76514
  return;