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/cli/index.js CHANGED
@@ -14204,22 +14204,26 @@ async function readLedgerEvents(directory) {
14204
14204
  return [];
14205
14205
  }
14206
14206
  }
14207
- async function initLedger(directory, planId, initialPlanHash) {
14207
+ async function initLedger(directory, planId, initialPlanHash, initialPlan) {
14208
14208
  const ledgerPath = getLedgerPath(directory);
14209
14209
  const planJsonPath = getPlanJsonPath(directory);
14210
14210
  if (fs.existsSync(ledgerPath)) {
14211
14211
  throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
14212
14212
  }
14213
14213
  let planHashAfter = initialPlanHash ?? "";
14214
+ let embeddedPlan = initialPlan;
14214
14215
  if (!initialPlanHash) {
14215
14216
  try {
14216
14217
  if (fs.existsSync(planJsonPath)) {
14217
14218
  const content = fs.readFileSync(planJsonPath, "utf8");
14218
14219
  const plan = JSON.parse(content);
14219
14220
  planHashAfter = computePlanHash(plan);
14221
+ if (!embeddedPlan)
14222
+ embeddedPlan = plan;
14220
14223
  }
14221
14224
  } catch {}
14222
14225
  }
14226
+ const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
14223
14227
  const event = {
14224
14228
  seq: 1,
14225
14229
  timestamp: new Date().toISOString(),
@@ -14228,7 +14232,8 @@ async function initLedger(directory, planId, initialPlanHash) {
14228
14232
  source: "initLedger",
14229
14233
  plan_hash_before: "",
14230
14234
  plan_hash_after: planHashAfter,
14231
- schema_version: LEDGER_SCHEMA_VERSION
14235
+ schema_version: LEDGER_SCHEMA_VERSION,
14236
+ ...payload ? { payload } : {}
14232
14237
  };
14233
14238
  fs.mkdirSync(path2.join(directory, ".swarm"), { recursive: true });
14234
14239
  const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
@@ -14315,7 +14320,7 @@ async function takeSnapshotEvent(directory, plan, options) {
14315
14320
  payload: snapshotPayload
14316
14321
  }, { planHashAfter: options?.planHashAfter });
14317
14322
  }
14318
- async function replayFromLedger(directory, options) {
14323
+ async function replayFromLedger(directory, _options) {
14319
14324
  const events = await readLedgerEvents(directory);
14320
14325
  if (events.length === 0) {
14321
14326
  return null;
@@ -14338,6 +14343,20 @@ async function replayFromLedger(directory, options) {
14338
14343
  return plan2;
14339
14344
  }
14340
14345
  }
14346
+ const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
14347
+ if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
14348
+ const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
14349
+ if (parseResult.success) {
14350
+ let plan2 = parseResult.data;
14351
+ const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
14352
+ for (const event of eventsAfterCreated) {
14353
+ if (plan2 === null)
14354
+ return null;
14355
+ plan2 = applyEventToPlan(plan2, event);
14356
+ }
14357
+ return plan2;
14358
+ }
14359
+ }
14341
14360
  const planJsonPath = getPlanJsonPath(directory);
14342
14361
  if (!fs.existsSync(planJsonPath)) {
14343
14362
  return null;
@@ -14360,6 +14379,11 @@ async function replayFromLedger(directory, options) {
14360
14379
  function applyEventToPlan(plan, event) {
14361
14380
  switch (event.event_type) {
14362
14381
  case "plan_created":
14382
+ if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
14383
+ const parsed = PlanSchema.safeParse(event.payload.plan);
14384
+ if (parsed.success)
14385
+ return parsed.data;
14386
+ }
14363
14387
  return plan;
14364
14388
  case "task_status_changed":
14365
14389
  if (event.task_id && event.to_status) {
@@ -14448,7 +14472,13 @@ var init_ledger = __esm(() => {
14448
14472
  });
14449
14473
 
14450
14474
  // src/plan/manager.ts
14451
- import { copyFileSync, existsSync as existsSync2, renameSync as renameSync2, unlinkSync } from "fs";
14475
+ import {
14476
+ copyFileSync,
14477
+ existsSync as existsSync2,
14478
+ readdirSync,
14479
+ renameSync as renameSync2,
14480
+ unlinkSync
14481
+ } from "fs";
14452
14482
  import * as fsPromises from "fs/promises";
14453
14483
  import * as path3 from "path";
14454
14484
  async function loadPlanJsonOnly(directory) {
@@ -14700,35 +14730,53 @@ async function loadPlan(directory) {
14700
14730
  return migrated;
14701
14731
  }
14702
14732
  if (await ledgerExists(directory)) {
14703
- const rebuilt = await replayFromLedger(directory);
14704
- if (rebuilt) {
14705
- await savePlan(directory, rebuilt);
14706
- return rebuilt;
14707
- }
14733
+ const resolvedDir = path3.resolve(directory);
14734
+ const existingMutex = recoveryMutexes.get(resolvedDir);
14735
+ if (existingMutex) {
14736
+ await existingMutex;
14737
+ const postRecoveryPlan = await loadPlanJsonOnly(directory);
14738
+ if (postRecoveryPlan)
14739
+ return postRecoveryPlan;
14740
+ }
14741
+ let resolveRecovery;
14742
+ const mutex = new Promise((r) => {
14743
+ resolveRecovery = r;
14744
+ });
14745
+ recoveryMutexes.set(resolvedDir, mutex);
14708
14746
  try {
14709
- const anchorEvents = await readLedgerEvents(directory);
14710
- if (anchorEvents.length === 0) {
14711
- warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
14712
- return null;
14713
- }
14714
- const expectedPlanId = anchorEvents[0].plan_id;
14715
- const approved = await loadLastApprovedPlan(directory, expectedPlanId);
14716
- if (approved) {
14717
- const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
14718
- 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.`);
14719
- await savePlan(directory, approved.plan);
14720
- try {
14721
- await takeSnapshotEvent(directory, approved.plan, {
14722
- source: "recovery_from_approved_snapshot",
14723
- approvalMetadata: approved.approval
14724
- });
14725
- } catch (healError) {
14726
- warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
14747
+ const rebuilt = await replayFromLedger(directory);
14748
+ if (rebuilt) {
14749
+ await savePlan(directory, rebuilt);
14750
+ return rebuilt;
14751
+ }
14752
+ try {
14753
+ const anchorEvents = await readLedgerEvents(directory);
14754
+ if (anchorEvents.length === 0) {
14755
+ warn("[loadPlan] Ledger present but no events readable \u2014 refusing approved-snapshot recovery (cannot verify plan identity).");
14756
+ return null;
14727
14757
  }
14728
- return approved.plan;
14758
+ const expectedPlanId = anchorEvents[0].plan_id;
14759
+ const approved = await loadLastApprovedPlan(directory, expectedPlanId);
14760
+ if (approved) {
14761
+ const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
14762
+ 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.`);
14763
+ await savePlan(directory, approved.plan);
14764
+ try {
14765
+ await takeSnapshotEvent(directory, approved.plan, {
14766
+ source: "recovery_from_approved_snapshot",
14767
+ approvalMetadata: approved.approval
14768
+ });
14769
+ } catch (healError) {
14770
+ warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
14771
+ }
14772
+ return approved.plan;
14773
+ }
14774
+ } catch (recoveryError) {
14775
+ warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
14729
14776
  }
14730
- } catch (recoveryError) {
14731
- warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
14777
+ } finally {
14778
+ resolveRecovery();
14779
+ recoveryMutexes.delete(resolvedDir);
14732
14780
  }
14733
14781
  }
14734
14782
  return null;
@@ -14777,7 +14825,7 @@ async function savePlan(directory, plan, options) {
14777
14825
  const planId = `${validated.swarm}-${validated.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
14778
14826
  const planHashForInit = computePlanHash(validated);
14779
14827
  if (!await ledgerExists(directory)) {
14780
- await initLedger(directory, planId, planHashForInit);
14828
+ await initLedger(directory, planId, planHashForInit, validated);
14781
14829
  } else {
14782
14830
  const existingEvents = await readLedgerEvents(directory);
14783
14831
  if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
@@ -14796,7 +14844,7 @@ async function savePlan(directory, plan, options) {
14796
14844
  let initSucceeded = false;
14797
14845
  if (backupExists) {
14798
14846
  try {
14799
- await initLedger(directory, planId, planHashForInit);
14847
+ await initLedger(directory, planId, planHashForInit, validated);
14800
14848
  initSucceeded = true;
14801
14849
  } catch (initErr) {
14802
14850
  const errorMessage = String(initErr);
@@ -14838,6 +14886,19 @@ async function savePlan(directory, plan, options) {
14838
14886
  unlinkSync(oldLedgerBackupPath);
14839
14887
  } catch {}
14840
14888
  }
14889
+ const MAX_ARCHIVED_SIBLINGS = 5;
14890
+ try {
14891
+ const allFiles = readdirSync(swarmDir2);
14892
+ const archivedSiblings = allFiles.filter((f) => f.startsWith("plan-ledger.archived-") && f.endsWith(".jsonl")).sort();
14893
+ if (archivedSiblings.length > MAX_ARCHIVED_SIBLINGS) {
14894
+ const toRemove = archivedSiblings.slice(0, archivedSiblings.length - MAX_ARCHIVED_SIBLINGS);
14895
+ for (const file2 of toRemove) {
14896
+ try {
14897
+ unlinkSync(path3.join(swarmDir2, file2));
14898
+ } catch {}
14899
+ }
14900
+ }
14901
+ } catch {}
14841
14902
  }
14842
14903
  }
14843
14904
  const currentHash = computeCurrentPlanHash(directory);
@@ -14887,7 +14948,7 @@ async function savePlan(directory, plan, options) {
14887
14948
  }
14888
14949
  } catch (error49) {
14889
14950
  if (error49 instanceof LedgerStaleWriterError) {
14890
- throw new Error(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
14951
+ throw new PlanConcurrentModificationError(`Concurrent plan modification detected after retries: ${error49.message}. Please retry the operation.`);
14891
14952
  }
14892
14953
  throw error49;
14893
14954
  }
@@ -14897,7 +14958,11 @@ async function savePlan(directory, plan, options) {
14897
14958
  if (latestSeq > 0 && latestSeq % SNAPSHOT_INTERVAL === 0) {
14898
14959
  await takeSnapshotEvent(directory, validated, {
14899
14960
  planHashAfter: hashAfter
14900
- }).catch(() => {});
14961
+ }).catch((err) => {
14962
+ if (process.env.DEBUG_SWARM) {
14963
+ warn(`[savePlan] Periodic snapshot write failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
14964
+ }
14965
+ });
14901
14966
  }
14902
14967
  const swarmDir = path3.resolve(directory, ".swarm");
14903
14968
  const planPath = path3.join(swarmDir, "plan.json");
@@ -14910,19 +14975,23 @@ async function savePlan(directory, plan, options) {
14910
14975
  unlinkSync(tempPath);
14911
14976
  } catch {}
14912
14977
  }
14913
- const contentHash = computePlanContentHash(validated);
14914
- const markdown = derivePlanMarkdown(validated);
14915
- const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
14916
- ${markdown}`;
14917
- const mdPath = path3.join(swarmDir, "plan.md");
14918
- const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
14919
14978
  try {
14920
- await Bun.write(mdTempPath, markdownWithHash);
14921
- renameSync2(mdTempPath, mdPath);
14922
- } finally {
14979
+ const contentHash = computePlanContentHash(validated);
14980
+ const markdown = derivePlanMarkdown(validated);
14981
+ const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
14982
+ ${markdown}`;
14983
+ const mdPath = path3.join(swarmDir, "plan.md");
14984
+ const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
14923
14985
  try {
14924
- unlinkSync(mdTempPath);
14925
- } catch {}
14986
+ await Bun.write(mdTempPath, markdownWithHash);
14987
+ renameSync2(mdTempPath, mdPath);
14988
+ } finally {
14989
+ try {
14990
+ unlinkSync(mdTempPath);
14991
+ } catch {}
14992
+ }
14993
+ } catch (mdError) {
14994
+ warn(`[savePlan] plan.md write failed (non-fatal, plan.json is authoritative): ${mdError instanceof Error ? mdError.message : String(mdError)}`);
14926
14995
  }
14927
14996
  try {
14928
14997
  const markerPath = path3.join(swarmDir, ".plan-write-marker");
@@ -15232,14 +15301,21 @@ function migrateLegacyPlan(planContent, swarmId) {
15232
15301
  };
15233
15302
  return plan;
15234
15303
  }
15235
- var startupLedgerCheckedWorkspaces;
15304
+ var PlanConcurrentModificationError, startupLedgerCheckedWorkspaces, recoveryMutexes;
15236
15305
  var init_manager = __esm(() => {
15237
15306
  init_plan_schema();
15238
15307
  init_utils2();
15239
15308
  init_utils();
15240
15309
  init_spec_hash();
15241
15310
  init_ledger();
15311
+ PlanConcurrentModificationError = class PlanConcurrentModificationError extends Error {
15312
+ constructor(message) {
15313
+ super(message);
15314
+ this.name = "PlanConcurrentModificationError";
15315
+ }
15316
+ };
15242
15317
  startupLedgerCheckedWorkspaces = new Set;
15318
+ recoveryMutexes = new Map;
15243
15319
  });
15244
15320
 
15245
15321
  // src/config/evidence-schema.ts
@@ -15492,44 +15568,51 @@ var init_evidence_schema = __esm(() => {
15492
15568
  });
15493
15569
  });
15494
15570
 
15495
- // src/evidence/manager.ts
15496
- import { mkdirSync as mkdirSync2, readdirSync, rmSync, statSync as statSync2 } from "fs";
15497
- import * as fs3 from "fs/promises";
15498
- import * as path5 from "path";
15499
- function isValidEvidenceType(type) {
15500
- return VALID_EVIDENCE_TYPES.includes(type);
15501
- }
15502
- function sanitizeTaskId(taskId) {
15571
+ // src/validation/task-id.ts
15572
+ function checkUnsafeChars(taskId) {
15503
15573
  if (!taskId || taskId.length === 0) {
15504
- throw new Error("Invalid task ID: empty string");
15574
+ return "Invalid task ID: empty string";
15505
15575
  }
15506
15576
  if (/\0/.test(taskId)) {
15507
- throw new Error("Invalid task ID: contains null bytes");
15577
+ return "Invalid task ID: contains null bytes";
15508
15578
  }
15509
15579
  for (let i = 0;i < taskId.length; i++) {
15510
15580
  if (taskId.charCodeAt(i) < 32) {
15511
- throw new Error("Invalid task ID: contains control characters");
15581
+ return "Invalid task ID: contains control characters";
15512
15582
  }
15513
15583
  }
15514
- if (taskId.includes("..") || taskId.includes("../") || taskId.includes("..\\")) {
15515
- throw new Error("Invalid task ID: path traversal detected");
15516
- }
15517
- if (TASK_ID_REGEX.test(taskId)) {
15518
- return taskId;
15519
- }
15520
- if (RETRO_TASK_ID_REGEX.test(taskId)) {
15521
- return taskId;
15584
+ if (taskId.includes("..") || taskId.includes("/") || taskId.includes("\\")) {
15585
+ return "Invalid task ID: path traversal detected";
15522
15586
  }
15523
- if (INTERNAL_TOOL_ID_REGEX.test(taskId)) {
15524
- return taskId;
15587
+ return;
15588
+ }
15589
+ function sanitizeTaskId(taskId) {
15590
+ const unsafeMsg = checkUnsafeChars(taskId);
15591
+ if (unsafeMsg) {
15592
+ throw new Error(unsafeMsg);
15525
15593
  }
15526
- if (GENERAL_TASK_ID_REGEX.test(taskId)) {
15594
+ 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)) {
15527
15595
  return taskId;
15528
15596
  }
15529
15597
  throw new Error(`Invalid task ID: must be alphanumeric (ASCII) with optional hyphens, underscores, or dots, got "${taskId}"`);
15530
15598
  }
15599
+ var STRICT_TASK_ID_PATTERN, RETRO_TASK_ID_REGEX, INTERNAL_TOOL_ID_REGEX, GENERAL_TASK_ID_REGEX;
15600
+ var init_task_id = __esm(() => {
15601
+ STRICT_TASK_ID_PATTERN = /^\d+\.\d+(\.\d+)*$/;
15602
+ RETRO_TASK_ID_REGEX = /^retro-\d+$/;
15603
+ INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
15604
+ GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
15605
+ });
15606
+
15607
+ // src/evidence/manager.ts
15608
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
15609
+ import * as fs3 from "fs/promises";
15610
+ import * as path5 from "path";
15611
+ function isValidEvidenceType(type) {
15612
+ return VALID_EVIDENCE_TYPES.includes(type);
15613
+ }
15531
15614
  async function saveEvidence(directory, taskId, evidence) {
15532
- const sanitizedTaskId = sanitizeTaskId(taskId);
15615
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
15533
15616
  const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
15534
15617
  const evidencePath = validateSwarmPath(directory, relativePath);
15535
15618
  const evidenceDir = path5.dirname(evidencePath);
@@ -15560,9 +15643,14 @@ async function saveEvidence(directory, taskId, evidence) {
15560
15643
  updated_at: now
15561
15644
  };
15562
15645
  }
15646
+ const MAX_BUNDLE_ENTRIES = 100;
15647
+ let entries = [...bundle.entries, evidence];
15648
+ if (entries.length > MAX_BUNDLE_ENTRIES) {
15649
+ entries = entries.slice(entries.length - MAX_BUNDLE_ENTRIES);
15650
+ }
15563
15651
  const updatedBundle = {
15564
15652
  ...bundle,
15565
- entries: [...bundle.entries, evidence],
15653
+ entries,
15566
15654
  updated_at: new Date().toISOString()
15567
15655
  };
15568
15656
  const bundleJson = JSON.stringify(updatedBundle);
@@ -15607,7 +15695,7 @@ function wrapFlatRetrospective(flatEntry, taskId) {
15607
15695
  };
15608
15696
  }
15609
15697
  async function loadEvidence(directory, taskId) {
15610
- const sanitizedTaskId = sanitizeTaskId(taskId);
15698
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
15611
15699
  const relativePath = path5.join("evidence", sanitizedTaskId, "evidence.json");
15612
15700
  const evidencePath = validateSwarmPath(directory, relativePath);
15613
15701
  const content = await readSwarmFileAsync(directory, relativePath);
@@ -15661,7 +15749,7 @@ async function listEvidenceTaskIds(directory) {
15661
15749
  }
15662
15750
  let entries;
15663
15751
  try {
15664
- entries = readdirSync(evidenceBasePath);
15752
+ entries = readdirSync2(evidenceBasePath);
15665
15753
  } catch {
15666
15754
  return [];
15667
15755
  }
@@ -15673,7 +15761,7 @@ async function listEvidenceTaskIds(directory) {
15673
15761
  if (!stats.isDirectory()) {
15674
15762
  continue;
15675
15763
  }
15676
- sanitizeTaskId(entry);
15764
+ sanitizeTaskId2(entry);
15677
15765
  taskIds.push(entry);
15678
15766
  } catch (error49) {
15679
15767
  if (error49 instanceof Error && !error49.message.startsWith("Invalid task ID")) {
@@ -15684,7 +15772,7 @@ async function listEvidenceTaskIds(directory) {
15684
15772
  return taskIds.sort();
15685
15773
  }
15686
15774
  async function deleteEvidence(directory, taskId) {
15687
- const sanitizedTaskId = sanitizeTaskId(taskId);
15775
+ const sanitizedTaskId = sanitizeTaskId2(taskId);
15688
15776
  const relativePath = path5.join("evidence", sanitizedTaskId);
15689
15777
  const evidenceDir = validateSwarmPath(directory, relativePath);
15690
15778
  try {
@@ -15746,12 +15834,13 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
15746
15834
  }
15747
15835
  return archived;
15748
15836
  }
15749
- var VALID_EVIDENCE_TYPES, TASK_ID_REGEX, RETRO_TASK_ID_REGEX, INTERNAL_TOOL_ID_REGEX, GENERAL_TASK_ID_REGEX, LEGACY_TASK_COMPLEXITY_MAP;
15837
+ var VALID_EVIDENCE_TYPES, sanitizeTaskId2, LEGACY_TASK_COMPLEXITY_MAP;
15750
15838
  var init_manager2 = __esm(() => {
15751
15839
  init_zod();
15752
15840
  init_evidence_schema();
15753
15841
  init_utils2();
15754
15842
  init_utils();
15843
+ init_task_id();
15755
15844
  VALID_EVIDENCE_TYPES = [
15756
15845
  "review",
15757
15846
  "test",
@@ -15767,10 +15856,7 @@ var init_manager2 = __esm(() => {
15767
15856
  "quality_budget",
15768
15857
  "secretscan"
15769
15858
  ];
15770
- TASK_ID_REGEX = /^\d+\.\d+(\.\d+)*$/;
15771
- RETRO_TASK_ID_REGEX = /^retro-\d+$/;
15772
- INTERNAL_TOOL_ID_REGEX = /^(?:sast_scan|quality_budget|syntax_check|placeholder_scan|sbom_generate|build|secretscan)$/;
15773
- GENERAL_TASK_ID_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
15859
+ sanitizeTaskId2 = sanitizeTaskId;
15774
15860
  LEGACY_TASK_COMPLEXITY_MAP = {
15775
15861
  low: "simple",
15776
15862
  medium: "moderate",
@@ -33569,11 +33655,14 @@ async function executeWriteRetro(args, directory) {
33569
33655
  try {
33570
33656
  const allTaskIds = await listEvidenceTaskIds(directory);
33571
33657
  const phaseTaskIds = allTaskIds.filter((id) => id.startsWith(`${phase}.`));
33658
+ const sessionStart = args.metadata && typeof args.metadata.session_start === "string" ? args.metadata.session_start : undefined;
33572
33659
  for (const phaseTaskId of phaseTaskIds) {
33573
33660
  const result = await loadEvidence(directory, phaseTaskId);
33574
33661
  if (result.status !== "found")
33575
33662
  continue;
33576
33663
  const bundle = result.bundle;
33664
+ if (sessionStart && bundle.updated_at < sessionStart)
33665
+ continue;
33577
33666
  for (const entry of bundle.entries) {
33578
33667
  const e = entry;
33579
33668
  if (e.type === "review" && e.verdict === "fail") {
@@ -33765,6 +33854,18 @@ async function handleCloseCommand(directory, args) {
33765
33854
  }
33766
33855
  }
33767
33856
  }
33857
+ let sessionStart;
33858
+ {
33859
+ let earliest = Infinity;
33860
+ for (const [, session] of swarmState.agentSessions) {
33861
+ if (session.lastAgentEventTime > 0 && session.lastAgentEventTime < earliest) {
33862
+ earliest = session.lastAgentEventTime;
33863
+ }
33864
+ }
33865
+ if (earliest < Infinity) {
33866
+ sessionStart = new Date(earliest).toISOString();
33867
+ }
33868
+ }
33768
33869
  const wrotePhaseRetro = closedPhases.length > 0;
33769
33870
  if (!wrotePhaseRetro && !planExists) {
33770
33871
  try {
@@ -33780,7 +33881,10 @@ async function handleCloseCommand(directory, args) {
33780
33881
  test_failures: 0,
33781
33882
  security_findings: 0,
33782
33883
  integration_issues: 0,
33783
- metadata: { session_scope: "plan_free" }
33884
+ metadata: {
33885
+ session_scope: "plan_free",
33886
+ ...sessionStart ? { session_start: sessionStart } : {}
33887
+ }
33784
33888
  }, directory);
33785
33889
  try {
33786
33890
  const parsed = JSON.parse(sessionRetroResult);
@@ -33837,7 +33941,8 @@ async function handleCloseCommand(directory, args) {
33837
33941
  }
33838
33942
  }
33839
33943
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
33840
- const archiveDir = path11.join(swarmDir, "archive", `swarm-${timestamp}`);
33944
+ const suffix = Math.random().toString(36).slice(2, 8);
33945
+ const archiveDir = path11.join(swarmDir, "archive", `swarm-${timestamp}-${suffix}`);
33841
33946
  let archiveResult = "";
33842
33947
  let archivedFileCount = 0;
33843
33948
  const archivedActiveStateFiles = new Set;
@@ -34101,11 +34206,23 @@ async function handleCloseCommand(directory, args) {
34101
34206
  if (pruneErrors.length > 0) {
34102
34207
  warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
34103
34208
  }
34104
- const warningMsg = warnings.length > 0 ? `
34209
+ const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
34210
+ const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
34211
+ let warningMsg = "";
34212
+ if (retroWarnings.length > 0) {
34213
+ warningMsg += `
34214
+
34215
+ **\u26A0 Retrospective evidence incomplete:**
34216
+ ${retroWarnings.map((w) => `- ${w}`).join(`
34217
+ `)}`;
34218
+ }
34219
+ if (otherWarnings.length > 0) {
34220
+ warningMsg += `
34105
34221
 
34106
34222
  **Warnings:**
34107
- ${warnings.map((w) => `- ${w}`).join(`
34108
- `)}` : "";
34223
+ ${otherWarnings.map((w) => `- ${w}`).join(`
34224
+ `)}`;
34225
+ }
34109
34226
  if (planAlreadyDone) {
34110
34227
  return `\u2705 Session finalized. Plan was already in a terminal state \u2014 cleanup and archive applied.
34111
34228
 
@@ -34777,7 +34894,7 @@ async function handleDarkMatterCommand(directory, args) {
34777
34894
 
34778
34895
  // src/services/diagnose-service.ts
34779
34896
  import * as child_process4 from "child_process";
34780
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
34897
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
34781
34898
  import path15 from "path";
34782
34899
  import { fileURLToPath } from "url";
34783
34900
  init_manager2();
@@ -34983,7 +35100,7 @@ async function checkPlanSync(directory, plan) {
34983
35100
  }
34984
35101
  async function checkConfigBackups(directory) {
34985
35102
  try {
34986
- const files = readdirSync2(directory);
35103
+ const files = readdirSync3(directory);
34987
35104
  const backupCount = files.filter((f) => /\.opencode-swarm\.yaml\.bak/.test(f)).length;
34988
35105
  if (backupCount <= 5) {
34989
35106
  return {
@@ -35449,7 +35566,7 @@ async function getDiagnoseData(directory) {
35449
35566
  checks5.push(await checkCurator(directory));
35450
35567
  try {
35451
35568
  const evidenceDir = path15.join(directory, ".swarm", "evidence");
35452
- const snapshotFiles = existsSync6(evidenceDir) ? readdirSync2(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
35569
+ const snapshotFiles = existsSync6(evidenceDir) ? readdirSync3(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
35453
35570
  if (snapshotFiles.length > 0) {
35454
35571
  const latest = snapshotFiles.sort().pop();
35455
35572
  checks5.push({
@@ -36,18 +36,8 @@ export declare function isQualityBudgetEvidence(evidence: Evidence): evidence is
36
36
  * Type guard for secretscan evidence
37
37
  */
38
38
  export declare function isSecretscanEvidence(evidence: Evidence): evidence is SecretscanEvidence;
39
- /**
40
- * Validate and sanitize task ID.
41
- * Accepts four formats:
42
- * 1. Canonical N.M or N.M.P numeric format (matches TASK_ID_REGEX)
43
- * 2. Retrospective format: retro-<number> (matches RETRO_TASK_ID_REGEX)
44
- * 3. Internal automated-tool format: specific tool IDs (sast_scan, quality_budget, etc.)
45
- * 4. General safe alphanumeric IDs: ASCII letter/digit start, body of letters/digits/dots/hyphens/underscores
46
- * Rejects: empty string, null bytes, control characters, path traversal (..), spaces, and any
47
- * character outside the ASCII alphanumeric + [._-] set.
48
- * @throws Error with descriptive message on failure
49
- */
50
- export declare function sanitizeTaskId(taskId: string): string;
39
+ import { sanitizeTaskId as _sanitizeTaskId } from '../validation/task-id';
40
+ export declare const sanitizeTaskId: typeof _sanitizeTaskId;
51
41
  /**
52
42
  * Save evidence to a task's evidence bundle.
53
43
  * Creates new bundle if doesn't exist, appends to existing.
@@ -24,15 +24,8 @@ export interface TaskEvidence {
24
24
  export declare const DEFAULT_REQUIRED_GATES: string[];
25
25
  /**
26
26
  * Canonical task-id validation helper.
27
- * Returns true if the taskId is a valid numeric format (N.M or N.M.P),
28
- * false otherwise.
29
- *
30
- * Validates:
31
- * - Non-empty string
32
- * - Matches N.M or N.M.P numeric pattern (e.g., "1.1", "1.2.3")
33
- * - No path traversal (..)
34
- * - No path separators (/, \)
35
- * - No null bytes
27
+ * Delegates to the shared strict validator (#452 item 2).
28
+ * Re-exported for backward compatibility with existing callers.
36
29
  */
37
30
  export declare function isValidTaskId(taskId: string): boolean;
38
31
  /**