opencode-swarm 6.19.6 → 6.19.8

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
@@ -14490,16 +14490,68 @@ async function saveEvidence(directory, taskId, evidence) {
14490
14490
  }
14491
14491
  return updatedBundle;
14492
14492
  }
14493
+ function isFlatRetrospective(parsed) {
14494
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.type === "retrospective" && !parsed.schema_version;
14495
+ }
14496
+ function remapLegacyTaskComplexity(entry) {
14497
+ const taskComplexity = entry.task_complexity;
14498
+ if (typeof taskComplexity === "string" && taskComplexity in LEGACY_TASK_COMPLEXITY_MAP) {
14499
+ return {
14500
+ ...entry,
14501
+ task_complexity: LEGACY_TASK_COMPLEXITY_MAP[taskComplexity]
14502
+ };
14503
+ }
14504
+ return entry;
14505
+ }
14506
+ function wrapFlatRetrospective(flatEntry, taskId) {
14507
+ const now = new Date().toISOString();
14508
+ const remappedEntry = remapLegacyTaskComplexity(flatEntry);
14509
+ return {
14510
+ schema_version: "1.0.0",
14511
+ task_id: remappedEntry.task_id ?? taskId,
14512
+ created_at: remappedEntry.timestamp ?? now,
14513
+ updated_at: remappedEntry.timestamp ?? now,
14514
+ entries: [remappedEntry]
14515
+ };
14516
+ }
14493
14517
  async function loadEvidence(directory, taskId) {
14494
14518
  const sanitizedTaskId = sanitizeTaskId(taskId);
14495
14519
  const relativePath = path3.join("evidence", sanitizedTaskId, "evidence.json");
14496
- validateSwarmPath(directory, relativePath);
14520
+ const evidencePath = validateSwarmPath(directory, relativePath);
14497
14521
  const content = await readSwarmFileAsync(directory, relativePath);
14498
14522
  if (content === null) {
14499
14523
  return { status: "not_found" };
14500
14524
  }
14525
+ let parsed;
14526
+ try {
14527
+ parsed = JSON.parse(content);
14528
+ } catch {
14529
+ return { status: "invalid_schema", errors: ["Invalid JSON"] };
14530
+ }
14531
+ if (isFlatRetrospective(parsed)) {
14532
+ const wrappedBundle = wrapFlatRetrospective(parsed, sanitizedTaskId);
14533
+ try {
14534
+ const validated = EvidenceBundleSchema.parse(wrappedBundle);
14535
+ const evidenceDir = path3.dirname(evidencePath);
14536
+ const bundleJson = JSON.stringify(validated);
14537
+ const tempPath = path3.join(evidenceDir, `evidence.json.tmp.${Date.now()}.${process.pid}`);
14538
+ try {
14539
+ await Bun.write(tempPath, bundleJson);
14540
+ renameSync(tempPath, evidencePath);
14541
+ } catch (writeError) {
14542
+ try {
14543
+ rmSync(tempPath, { force: true });
14544
+ } catch {}
14545
+ warn(`Failed to persist repaired flat retrospective for task ${sanitizedTaskId}: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
14546
+ }
14547
+ return { status: "found", bundle: validated };
14548
+ } catch (error49) {
14549
+ warn(`Wrapped flat retrospective failed validation for task ${sanitizedTaskId}: ${error49 instanceof Error ? error49.message : String(error49)}`);
14550
+ const errors3 = error49 instanceof ZodError ? error49.issues.map((e) => e.path.join(".") + ": " + e.message) : [String(error49)];
14551
+ return { status: "invalid_schema", errors: errors3 };
14552
+ }
14553
+ }
14501
14554
  try {
14502
- const parsed = JSON.parse(content);
14503
14555
  const validated = EvidenceBundleSchema.parse(parsed);
14504
14556
  return { status: "found", bundle: validated };
14505
14557
  } catch (error49) {
@@ -14592,7 +14644,7 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
14592
14644
  }
14593
14645
  return archived;
14594
14646
  }
14595
- var VALID_EVIDENCE_TYPES, TASK_ID_REGEX;
14647
+ var VALID_EVIDENCE_TYPES, TASK_ID_REGEX, LEGACY_TASK_COMPLEXITY_MAP;
14596
14648
  var init_manager = __esm(() => {
14597
14649
  init_zod();
14598
14650
  init_evidence_schema();
@@ -14613,6 +14665,11 @@ var init_manager = __esm(() => {
14613
14665
  "quality_budget"
14614
14666
  ];
14615
14667
  TASK_ID_REGEX = /^[\w-]+(\.[\w-]+)*$/;
14668
+ LEGACY_TASK_COMPLEXITY_MAP = {
14669
+ low: "simple",
14670
+ medium: "moderate",
14671
+ high: "complex"
14672
+ };
14616
14673
  });
14617
14674
 
14618
14675
  // src/plan/manager.ts
@@ -14783,6 +14840,29 @@ ${markdown}`;
14783
14840
  } catch {}
14784
14841
  }
14785
14842
  }
14843
+ async function updateTaskStatus(directory, taskId, status) {
14844
+ const plan = await loadPlan(directory);
14845
+ if (plan === null) {
14846
+ throw new Error(`Plan not found in directory: ${directory}`);
14847
+ }
14848
+ let taskFound = false;
14849
+ const updatedPhases = plan.phases.map((phase) => {
14850
+ const updatedTasks = phase.tasks.map((task) => {
14851
+ if (task.id === taskId) {
14852
+ taskFound = true;
14853
+ return { ...task, status };
14854
+ }
14855
+ return task;
14856
+ });
14857
+ return { ...phase, tasks: updatedTasks };
14858
+ });
14859
+ if (!taskFound) {
14860
+ throw new Error(`Task not found: ${taskId}`);
14861
+ }
14862
+ const updatedPlan = { ...plan, phases: updatedPhases };
14863
+ await savePlan(directory, updatedPlan);
14864
+ return updatedPlan;
14865
+ }
14786
14866
  function derivePlanMarkdown(plan) {
14787
14867
  const statusMap = {
14788
14868
  pending: "PENDING",
@@ -31812,7 +31892,7 @@ var init_detector = __esm(() => {
31812
31892
 
31813
31893
  // src/build/discovery.ts
31814
31894
  import * as fs6 from "fs";
31815
- import * as path16 from "path";
31895
+ import * as path17 from "path";
31816
31896
  function isCommandAvailable(command) {
31817
31897
  if (toolchainCache.has(command)) {
31818
31898
  return toolchainCache.get(command);
@@ -31847,11 +31927,11 @@ function findBuildFiles(workingDir, patterns) {
31847
31927
  return regex.test(f);
31848
31928
  });
31849
31929
  if (matches.length > 0) {
31850
- return path16.join(dir, matches[0]);
31930
+ return path17.join(dir, matches[0]);
31851
31931
  }
31852
31932
  } catch {}
31853
31933
  } else {
31854
- const filePath = path16.join(workingDir, pattern);
31934
+ const filePath = path17.join(workingDir, pattern);
31855
31935
  if (fs6.existsSync(filePath)) {
31856
31936
  return filePath;
31857
31937
  }
@@ -31860,7 +31940,7 @@ function findBuildFiles(workingDir, patterns) {
31860
31940
  return null;
31861
31941
  }
31862
31942
  function getRepoDefinedScripts(workingDir, scripts) {
31863
- const packageJsonPath = path16.join(workingDir, "package.json");
31943
+ const packageJsonPath = path17.join(workingDir, "package.json");
31864
31944
  if (!fs6.existsSync(packageJsonPath)) {
31865
31945
  return [];
31866
31946
  }
@@ -31901,7 +31981,7 @@ function findAllBuildFiles(workingDir) {
31901
31981
  const regex = new RegExp(`^${pattern.replace(/\*/g, ".*")}$`);
31902
31982
  findFilesRecursive(workingDir, regex, allBuildFiles);
31903
31983
  } else {
31904
- const filePath = path16.join(workingDir, pattern);
31984
+ const filePath = path17.join(workingDir, pattern);
31905
31985
  if (fs6.existsSync(filePath)) {
31906
31986
  allBuildFiles.add(filePath);
31907
31987
  }
@@ -31914,7 +31994,7 @@ function findFilesRecursive(dir, regex, results) {
31914
31994
  try {
31915
31995
  const entries = fs6.readdirSync(dir, { withFileTypes: true });
31916
31996
  for (const entry of entries) {
31917
- const fullPath = path16.join(dir, entry.name);
31997
+ const fullPath = path17.join(dir, entry.name);
31918
31998
  if (entry.isDirectory() && !["node_modules", ".git", "dist", "build", "target"].includes(entry.name)) {
31919
31999
  findFilesRecursive(fullPath, regex, results);
31920
32000
  } else if (entry.isFile() && regex.test(entry.name)) {
@@ -31937,7 +32017,7 @@ async function discoverBuildCommandsFromProfiles(workingDir) {
31937
32017
  let foundCommand = false;
31938
32018
  for (const cmd of sortedCommands) {
31939
32019
  if (cmd.detectFile) {
31940
- const detectFilePath = path16.join(workingDir, cmd.detectFile);
32020
+ const detectFilePath = path17.join(workingDir, cmd.detectFile);
31941
32021
  if (!fs6.existsSync(detectFilePath)) {
31942
32022
  continue;
31943
32023
  }
@@ -32147,7 +32227,7 @@ var init_discovery = __esm(() => {
32147
32227
 
32148
32228
  // src/tools/lint.ts
32149
32229
  import * as fs7 from "fs";
32150
- import * as path17 from "path";
32230
+ import * as path18 from "path";
32151
32231
  function validateArgs(args2) {
32152
32232
  if (typeof args2 !== "object" || args2 === null)
32153
32233
  return false;
@@ -32158,9 +32238,9 @@ function validateArgs(args2) {
32158
32238
  }
32159
32239
  function getLinterCommand(linter, mode) {
32160
32240
  const isWindows = process.platform === "win32";
32161
- const binDir = path17.join(process.cwd(), "node_modules", ".bin");
32162
- const biomeBin = isWindows ? path17.join(binDir, "biome.EXE") : path17.join(binDir, "biome");
32163
- const eslintBin = isWindows ? path17.join(binDir, "eslint.cmd") : path17.join(binDir, "eslint");
32241
+ const binDir = path18.join(process.cwd(), "node_modules", ".bin");
32242
+ const biomeBin = isWindows ? path18.join(binDir, "biome.EXE") : path18.join(binDir, "biome");
32243
+ const eslintBin = isWindows ? path18.join(binDir, "eslint.cmd") : path18.join(binDir, "eslint");
32164
32244
  switch (linter) {
32165
32245
  case "biome":
32166
32246
  if (mode === "fix") {
@@ -32176,7 +32256,7 @@ function getLinterCommand(linter, mode) {
32176
32256
  }
32177
32257
  function getAdditionalLinterCommand(linter, mode, cwd) {
32178
32258
  const gradlewName = process.platform === "win32" ? "gradlew.bat" : "gradlew";
32179
- const gradlew = fs7.existsSync(path17.join(cwd, gradlewName)) ? path17.join(cwd, gradlewName) : null;
32259
+ const gradlew = fs7.existsSync(path18.join(cwd, gradlewName)) ? path18.join(cwd, gradlewName) : null;
32180
32260
  switch (linter) {
32181
32261
  case "ruff":
32182
32262
  return mode === "fix" ? ["ruff", "check", "--fix", "."] : ["ruff", "check", "."];
@@ -32210,10 +32290,10 @@ function getAdditionalLinterCommand(linter, mode, cwd) {
32210
32290
  }
32211
32291
  }
32212
32292
  function detectRuff(cwd) {
32213
- if (fs7.existsSync(path17.join(cwd, "ruff.toml")))
32293
+ if (fs7.existsSync(path18.join(cwd, "ruff.toml")))
32214
32294
  return isCommandAvailable("ruff");
32215
32295
  try {
32216
- const pyproject = path17.join(cwd, "pyproject.toml");
32296
+ const pyproject = path18.join(cwd, "pyproject.toml");
32217
32297
  if (fs7.existsSync(pyproject)) {
32218
32298
  const content = fs7.readFileSync(pyproject, "utf-8");
32219
32299
  if (content.includes("[tool.ruff]"))
@@ -32223,19 +32303,19 @@ function detectRuff(cwd) {
32223
32303
  return false;
32224
32304
  }
32225
32305
  function detectClippy(cwd) {
32226
- return fs7.existsSync(path17.join(cwd, "Cargo.toml")) && isCommandAvailable("cargo");
32306
+ return fs7.existsSync(path18.join(cwd, "Cargo.toml")) && isCommandAvailable("cargo");
32227
32307
  }
32228
32308
  function detectGolangciLint(cwd) {
32229
- return fs7.existsSync(path17.join(cwd, "go.mod")) && isCommandAvailable("golangci-lint");
32309
+ return fs7.existsSync(path18.join(cwd, "go.mod")) && isCommandAvailable("golangci-lint");
32230
32310
  }
32231
32311
  function detectCheckstyle(cwd) {
32232
- const hasMaven = fs7.existsSync(path17.join(cwd, "pom.xml"));
32233
- const hasGradle = fs7.existsSync(path17.join(cwd, "build.gradle")) || fs7.existsSync(path17.join(cwd, "build.gradle.kts"));
32234
- const hasBinary = hasMaven && isCommandAvailable("mvn") || hasGradle && (fs7.existsSync(path17.join(cwd, "gradlew")) || isCommandAvailable("gradle"));
32312
+ const hasMaven = fs7.existsSync(path18.join(cwd, "pom.xml"));
32313
+ const hasGradle = fs7.existsSync(path18.join(cwd, "build.gradle")) || fs7.existsSync(path18.join(cwd, "build.gradle.kts"));
32314
+ const hasBinary = hasMaven && isCommandAvailable("mvn") || hasGradle && (fs7.existsSync(path18.join(cwd, "gradlew")) || isCommandAvailable("gradle"));
32235
32315
  return (hasMaven || hasGradle) && hasBinary;
32236
32316
  }
32237
32317
  function detectKtlint(cwd) {
32238
- const hasKotlin = fs7.existsSync(path17.join(cwd, "build.gradle.kts")) || fs7.existsSync(path17.join(cwd, "build.gradle")) || (() => {
32318
+ const hasKotlin = fs7.existsSync(path18.join(cwd, "build.gradle.kts")) || fs7.existsSync(path18.join(cwd, "build.gradle")) || (() => {
32239
32319
  try {
32240
32320
  return fs7.readdirSync(cwd).some((f) => f.endsWith(".kt") || f.endsWith(".kts"));
32241
32321
  } catch {
@@ -32254,11 +32334,11 @@ function detectDotnetFormat(cwd) {
32254
32334
  }
32255
32335
  }
32256
32336
  function detectCppcheck(cwd) {
32257
- if (fs7.existsSync(path17.join(cwd, "CMakeLists.txt"))) {
32337
+ if (fs7.existsSync(path18.join(cwd, "CMakeLists.txt"))) {
32258
32338
  return isCommandAvailable("cppcheck");
32259
32339
  }
32260
32340
  try {
32261
- const dirsToCheck = [cwd, path17.join(cwd, "src")];
32341
+ const dirsToCheck = [cwd, path18.join(cwd, "src")];
32262
32342
  const hasCpp = dirsToCheck.some((dir) => {
32263
32343
  try {
32264
32344
  return fs7.readdirSync(dir).some((f) => /\.(c|cpp|cc|cxx|h|hpp)$/.test(f));
@@ -32272,13 +32352,13 @@ function detectCppcheck(cwd) {
32272
32352
  }
32273
32353
  }
32274
32354
  function detectSwiftlint(cwd) {
32275
- return fs7.existsSync(path17.join(cwd, "Package.swift")) && isCommandAvailable("swiftlint");
32355
+ return fs7.existsSync(path18.join(cwd, "Package.swift")) && isCommandAvailable("swiftlint");
32276
32356
  }
32277
32357
  function detectDartAnalyze(cwd) {
32278
- return fs7.existsSync(path17.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
32358
+ return fs7.existsSync(path18.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
32279
32359
  }
32280
32360
  function detectRubocop(cwd) {
32281
- return (fs7.existsSync(path17.join(cwd, "Gemfile")) || fs7.existsSync(path17.join(cwd, "gems.rb")) || fs7.existsSync(path17.join(cwd, ".rubocop.yml"))) && (isCommandAvailable("rubocop") || isCommandAvailable("bundle"));
32361
+ return (fs7.existsSync(path18.join(cwd, "Gemfile")) || fs7.existsSync(path18.join(cwd, "gems.rb")) || fs7.existsSync(path18.join(cwd, ".rubocop.yml"))) && (isCommandAvailable("rubocop") || isCommandAvailable("bundle"));
32282
32362
  }
32283
32363
  function detectAdditionalLinter(cwd) {
32284
32364
  if (detectRuff(cwd))
@@ -32498,7 +32578,7 @@ For Rust: rustup component add clippy`
32498
32578
 
32499
32579
  // src/tools/secretscan.ts
32500
32580
  import * as fs8 from "fs";
32501
- import * as path18 from "path";
32581
+ import * as path19 from "path";
32502
32582
  function calculateShannonEntropy(str) {
32503
32583
  if (str.length === 0)
32504
32584
  return 0;
@@ -32525,7 +32605,7 @@ function isHighEntropyString(str) {
32525
32605
  function containsPathTraversal(str) {
32526
32606
  if (/\.\.[/\\]/.test(str))
32527
32607
  return true;
32528
- const normalized = path18.normalize(str);
32608
+ const normalized = path19.normalize(str);
32529
32609
  if (/\.\.[/\\]/.test(normalized))
32530
32610
  return true;
32531
32611
  if (str.includes("%2e%2e") || str.includes("%2E%2E"))
@@ -32553,7 +32633,7 @@ function validateDirectoryInput(dir) {
32553
32633
  return null;
32554
32634
  }
32555
32635
  function isBinaryFile(filePath, buffer) {
32556
- const ext = path18.extname(filePath).toLowerCase();
32636
+ const ext = path19.extname(filePath).toLowerCase();
32557
32637
  if (DEFAULT_EXCLUDE_EXTENSIONS.has(ext)) {
32558
32638
  return true;
32559
32639
  }
@@ -32689,9 +32769,9 @@ function isSymlinkLoop(realPath, visited) {
32689
32769
  return false;
32690
32770
  }
32691
32771
  function isPathWithinScope(realPath, scanDir) {
32692
- const resolvedScanDir = path18.resolve(scanDir);
32693
- const resolvedRealPath = path18.resolve(realPath);
32694
- return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path18.sep) || resolvedRealPath.startsWith(`${resolvedScanDir}/`) || resolvedRealPath.startsWith(`${resolvedScanDir}\\`);
32772
+ const resolvedScanDir = path19.resolve(scanDir);
32773
+ const resolvedRealPath = path19.resolve(realPath);
32774
+ return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path19.sep) || resolvedRealPath.startsWith(`${resolvedScanDir}/`) || resolvedRealPath.startsWith(`${resolvedScanDir}\\`);
32695
32775
  }
32696
32776
  function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
32697
32777
  skippedDirs: 0,
@@ -32721,7 +32801,7 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
32721
32801
  stats.skippedDirs++;
32722
32802
  continue;
32723
32803
  }
32724
- const fullPath = path18.join(dir, entry);
32804
+ const fullPath = path19.join(dir, entry);
32725
32805
  let lstat;
32726
32806
  try {
32727
32807
  lstat = fs8.lstatSync(fullPath);
@@ -32752,7 +32832,7 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
32752
32832
  const subFiles = findScannableFiles(fullPath, excludeDirs, scanDir, visited, stats);
32753
32833
  files.push(...subFiles);
32754
32834
  } else if (lstat.isFile()) {
32755
- const ext = path18.extname(fullPath).toLowerCase();
32835
+ const ext = path19.extname(fullPath).toLowerCase();
32756
32836
  if (!DEFAULT_EXCLUDE_EXTENSIONS.has(ext)) {
32757
32837
  files.push(fullPath);
32758
32838
  } else {
@@ -33018,7 +33098,7 @@ var init_secretscan = __esm(() => {
33018
33098
  }
33019
33099
  }
33020
33100
  try {
33021
- const scanDir = path18.resolve(directory);
33101
+ const scanDir = path19.resolve(directory);
33022
33102
  if (!fs8.existsSync(scanDir)) {
33023
33103
  const errorResult = {
33024
33104
  error: "directory not found",
@@ -33147,7 +33227,7 @@ var init_secretscan = __esm(() => {
33147
33227
 
33148
33228
  // src/tools/test-runner.ts
33149
33229
  import * as fs9 from "fs";
33150
- import * as path19 from "path";
33230
+ import * as path20 from "path";
33151
33231
  function containsPathTraversal2(str) {
33152
33232
  if (/\.\.[/\\]/.test(str))
33153
33233
  return true;
@@ -33240,14 +33320,14 @@ function hasDevDependency(devDeps, ...patterns) {
33240
33320
  return hasPackageJsonDependency(devDeps, ...patterns);
33241
33321
  }
33242
33322
  function detectGoTest(cwd) {
33243
- return fs9.existsSync(path19.join(cwd, "go.mod")) && isCommandAvailable("go");
33323
+ return fs9.existsSync(path20.join(cwd, "go.mod")) && isCommandAvailable("go");
33244
33324
  }
33245
33325
  function detectJavaMaven(cwd) {
33246
- return fs9.existsSync(path19.join(cwd, "pom.xml")) && isCommandAvailable("mvn");
33326
+ return fs9.existsSync(path20.join(cwd, "pom.xml")) && isCommandAvailable("mvn");
33247
33327
  }
33248
33328
  function detectGradle(cwd) {
33249
- const hasBuildFile = fs9.existsSync(path19.join(cwd, "build.gradle")) || fs9.existsSync(path19.join(cwd, "build.gradle.kts"));
33250
- const hasGradlew = fs9.existsSync(path19.join(cwd, "gradlew")) || fs9.existsSync(path19.join(cwd, "gradlew.bat"));
33329
+ const hasBuildFile = fs9.existsSync(path20.join(cwd, "build.gradle")) || fs9.existsSync(path20.join(cwd, "build.gradle.kts"));
33330
+ const hasGradlew = fs9.existsSync(path20.join(cwd, "gradlew")) || fs9.existsSync(path20.join(cwd, "gradlew.bat"));
33251
33331
  return hasBuildFile && (hasGradlew || isCommandAvailable("gradle"));
33252
33332
  }
33253
33333
  function detectDotnetTest(cwd) {
@@ -33260,30 +33340,30 @@ function detectDotnetTest(cwd) {
33260
33340
  }
33261
33341
  }
33262
33342
  function detectCTest(cwd) {
33263
- const hasSource = fs9.existsSync(path19.join(cwd, "CMakeLists.txt"));
33264
- const hasBuildCache = fs9.existsSync(path19.join(cwd, "CMakeCache.txt")) || fs9.existsSync(path19.join(cwd, "build", "CMakeCache.txt"));
33343
+ const hasSource = fs9.existsSync(path20.join(cwd, "CMakeLists.txt"));
33344
+ const hasBuildCache = fs9.existsSync(path20.join(cwd, "CMakeCache.txt")) || fs9.existsSync(path20.join(cwd, "build", "CMakeCache.txt"));
33265
33345
  return (hasSource || hasBuildCache) && isCommandAvailable("ctest");
33266
33346
  }
33267
33347
  function detectSwiftTest(cwd) {
33268
- return fs9.existsSync(path19.join(cwd, "Package.swift")) && isCommandAvailable("swift");
33348
+ return fs9.existsSync(path20.join(cwd, "Package.swift")) && isCommandAvailable("swift");
33269
33349
  }
33270
33350
  function detectDartTest(cwd) {
33271
- return fs9.existsSync(path19.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
33351
+ return fs9.existsSync(path20.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
33272
33352
  }
33273
33353
  function detectRSpec(cwd) {
33274
- const hasRSpecFile = fs9.existsSync(path19.join(cwd, ".rspec"));
33275
- const hasGemfile = fs9.existsSync(path19.join(cwd, "Gemfile"));
33276
- const hasSpecDir = fs9.existsSync(path19.join(cwd, "spec"));
33354
+ const hasRSpecFile = fs9.existsSync(path20.join(cwd, ".rspec"));
33355
+ const hasGemfile = fs9.existsSync(path20.join(cwd, "Gemfile"));
33356
+ const hasSpecDir = fs9.existsSync(path20.join(cwd, "spec"));
33277
33357
  const hasRSpec = hasRSpecFile || hasGemfile && hasSpecDir;
33278
33358
  return hasRSpec && (isCommandAvailable("bundle") || isCommandAvailable("rspec"));
33279
33359
  }
33280
33360
  function detectMinitest(cwd) {
33281
- return fs9.existsSync(path19.join(cwd, "test")) && (fs9.existsSync(path19.join(cwd, "Gemfile")) || fs9.existsSync(path19.join(cwd, "Rakefile"))) && isCommandAvailable("ruby");
33361
+ return fs9.existsSync(path20.join(cwd, "test")) && (fs9.existsSync(path20.join(cwd, "Gemfile")) || fs9.existsSync(path20.join(cwd, "Rakefile"))) && isCommandAvailable("ruby");
33282
33362
  }
33283
33363
  async function detectTestFramework(cwd) {
33284
33364
  const baseDir = cwd || process.cwd();
33285
33365
  try {
33286
- const packageJsonPath = path19.join(baseDir, "package.json");
33366
+ const packageJsonPath = path20.join(baseDir, "package.json");
33287
33367
  if (fs9.existsSync(packageJsonPath)) {
33288
33368
  const content = fs9.readFileSync(packageJsonPath, "utf-8");
33289
33369
  const pkg = JSON.parse(content);
@@ -33304,16 +33384,16 @@ async function detectTestFramework(cwd) {
33304
33384
  return "jest";
33305
33385
  if (hasDevDependency(devDeps, "mocha", "@types/mocha"))
33306
33386
  return "mocha";
33307
- if (fs9.existsSync(path19.join(baseDir, "bun.lockb")) || fs9.existsSync(path19.join(baseDir, "bun.lock"))) {
33387
+ if (fs9.existsSync(path20.join(baseDir, "bun.lockb")) || fs9.existsSync(path20.join(baseDir, "bun.lock"))) {
33308
33388
  if (scripts.test?.includes("bun"))
33309
33389
  return "bun";
33310
33390
  }
33311
33391
  }
33312
33392
  } catch {}
33313
33393
  try {
33314
- const pyprojectTomlPath = path19.join(baseDir, "pyproject.toml");
33315
- const setupCfgPath = path19.join(baseDir, "setup.cfg");
33316
- const requirementsTxtPath = path19.join(baseDir, "requirements.txt");
33394
+ const pyprojectTomlPath = path20.join(baseDir, "pyproject.toml");
33395
+ const setupCfgPath = path20.join(baseDir, "setup.cfg");
33396
+ const requirementsTxtPath = path20.join(baseDir, "requirements.txt");
33317
33397
  if (fs9.existsSync(pyprojectTomlPath)) {
33318
33398
  const content = fs9.readFileSync(pyprojectTomlPath, "utf-8");
33319
33399
  if (content.includes("[tool.pytest"))
@@ -33333,7 +33413,7 @@ async function detectTestFramework(cwd) {
33333
33413
  }
33334
33414
  } catch {}
33335
33415
  try {
33336
- const cargoTomlPath = path19.join(baseDir, "Cargo.toml");
33416
+ const cargoTomlPath = path20.join(baseDir, "Cargo.toml");
33337
33417
  if (fs9.existsSync(cargoTomlPath)) {
33338
33418
  const content = fs9.readFileSync(cargoTomlPath, "utf-8");
33339
33419
  if (content.includes("[dev-dependencies]")) {
@@ -33344,9 +33424,9 @@ async function detectTestFramework(cwd) {
33344
33424
  }
33345
33425
  } catch {}
33346
33426
  try {
33347
- const pesterConfigPath = path19.join(baseDir, "pester.config.ps1");
33348
- const pesterConfigJsonPath = path19.join(baseDir, "pester.config.ps1.json");
33349
- const pesterPs1Path = path19.join(baseDir, "tests.ps1");
33427
+ const pesterConfigPath = path20.join(baseDir, "pester.config.ps1");
33428
+ const pesterConfigJsonPath = path20.join(baseDir, "pester.config.ps1.json");
33429
+ const pesterPs1Path = path20.join(baseDir, "tests.ps1");
33350
33430
  if (fs9.existsSync(pesterConfigPath) || fs9.existsSync(pesterConfigJsonPath) || fs9.existsSync(pesterPs1Path)) {
33351
33431
  return "pester";
33352
33432
  }
@@ -33379,8 +33459,8 @@ function getTestFilesFromConvention(sourceFiles) {
33379
33459
  const testFiles = [];
33380
33460
  for (const file3 of sourceFiles) {
33381
33461
  const normalizedPath = file3.replace(/\\/g, "/");
33382
- const basename4 = path19.basename(file3);
33383
- const dirname7 = path19.dirname(file3);
33462
+ const basename4 = path20.basename(file3);
33463
+ const dirname8 = path20.dirname(file3);
33384
33464
  if (hasCompoundTestExtension(basename4) || basename4.includes(".spec.") || basename4.includes(".test.") || normalizedPath.includes("/__tests__/") || normalizedPath.includes("/tests/") || normalizedPath.includes("/test/")) {
33385
33465
  if (!testFiles.includes(file3)) {
33386
33466
  testFiles.push(file3);
@@ -33389,13 +33469,13 @@ function getTestFilesFromConvention(sourceFiles) {
33389
33469
  }
33390
33470
  for (const _pattern of TEST_PATTERNS) {
33391
33471
  const nameWithoutExt = basename4.replace(/\.[^.]+$/, "");
33392
- const ext = path19.extname(basename4);
33472
+ const ext = path20.extname(basename4);
33393
33473
  const possibleTestFiles = [
33394
- path19.join(dirname7, `${nameWithoutExt}.spec${ext}`),
33395
- path19.join(dirname7, `${nameWithoutExt}.test${ext}`),
33396
- path19.join(dirname7, "__tests__", `${nameWithoutExt}${ext}`),
33397
- path19.join(dirname7, "tests", `${nameWithoutExt}${ext}`),
33398
- path19.join(dirname7, "test", `${nameWithoutExt}${ext}`)
33474
+ path20.join(dirname8, `${nameWithoutExt}.spec${ext}`),
33475
+ path20.join(dirname8, `${nameWithoutExt}.test${ext}`),
33476
+ path20.join(dirname8, "__tests__", `${nameWithoutExt}${ext}`),
33477
+ path20.join(dirname8, "tests", `${nameWithoutExt}${ext}`),
33478
+ path20.join(dirname8, "test", `${nameWithoutExt}${ext}`)
33399
33479
  ];
33400
33480
  for (const testFile of possibleTestFiles) {
33401
33481
  if (fs9.existsSync(testFile) && !testFiles.includes(testFile)) {
@@ -33415,7 +33495,7 @@ async function getTestFilesFromGraph(sourceFiles) {
33415
33495
  for (const testFile of candidateTestFiles) {
33416
33496
  try {
33417
33497
  const content = fs9.readFileSync(testFile, "utf-8");
33418
- const testDir = path19.dirname(testFile);
33498
+ const testDir = path20.dirname(testFile);
33419
33499
  const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
33420
33500
  let match;
33421
33501
  match = importRegex.exec(content);
@@ -33423,8 +33503,8 @@ async function getTestFilesFromGraph(sourceFiles) {
33423
33503
  const importPath = match[1];
33424
33504
  let resolvedImport;
33425
33505
  if (importPath.startsWith(".")) {
33426
- resolvedImport = path19.resolve(testDir, importPath);
33427
- const existingExt = path19.extname(resolvedImport);
33506
+ resolvedImport = path20.resolve(testDir, importPath);
33507
+ const existingExt = path20.extname(resolvedImport);
33428
33508
  if (!existingExt) {
33429
33509
  for (const extToTry of [
33430
33510
  ".ts",
@@ -33444,12 +33524,12 @@ async function getTestFilesFromGraph(sourceFiles) {
33444
33524
  } else {
33445
33525
  continue;
33446
33526
  }
33447
- const importBasename = path19.basename(resolvedImport, path19.extname(resolvedImport));
33448
- const importDir = path19.dirname(resolvedImport);
33527
+ const importBasename = path20.basename(resolvedImport, path20.extname(resolvedImport));
33528
+ const importDir = path20.dirname(resolvedImport);
33449
33529
  for (const sourceFile of sourceFiles) {
33450
- const sourceDir = path19.dirname(sourceFile);
33451
- const sourceBasename = path19.basename(sourceFile, path19.extname(sourceFile));
33452
- const isRelatedDir = importDir === sourceDir || importDir === path19.join(sourceDir, "__tests__") || importDir === path19.join(sourceDir, "tests") || importDir === path19.join(sourceDir, "test");
33530
+ const sourceDir = path20.dirname(sourceFile);
33531
+ const sourceBasename = path20.basename(sourceFile, path20.extname(sourceFile));
33532
+ const isRelatedDir = importDir === sourceDir || importDir === path20.join(sourceDir, "__tests__") || importDir === path20.join(sourceDir, "tests") || importDir === path20.join(sourceDir, "test");
33453
33533
  if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
33454
33534
  if (!testFiles.includes(testFile)) {
33455
33535
  testFiles.push(testFile);
@@ -33464,8 +33544,8 @@ async function getTestFilesFromGraph(sourceFiles) {
33464
33544
  while (match !== null) {
33465
33545
  const importPath = match[1];
33466
33546
  if (importPath.startsWith(".")) {
33467
- let resolvedImport = path19.resolve(testDir, importPath);
33468
- const existingExt = path19.extname(resolvedImport);
33547
+ let resolvedImport = path20.resolve(testDir, importPath);
33548
+ const existingExt = path20.extname(resolvedImport);
33469
33549
  if (!existingExt) {
33470
33550
  for (const extToTry of [
33471
33551
  ".ts",
@@ -33482,12 +33562,12 @@ async function getTestFilesFromGraph(sourceFiles) {
33482
33562
  }
33483
33563
  }
33484
33564
  }
33485
- const importDir = path19.dirname(resolvedImport);
33486
- const importBasename = path19.basename(resolvedImport, path19.extname(resolvedImport));
33565
+ const importDir = path20.dirname(resolvedImport);
33566
+ const importBasename = path20.basename(resolvedImport, path20.extname(resolvedImport));
33487
33567
  for (const sourceFile of sourceFiles) {
33488
- const sourceDir = path19.dirname(sourceFile);
33489
- const sourceBasename = path19.basename(sourceFile, path19.extname(sourceFile));
33490
- const isRelatedDir = importDir === sourceDir || importDir === path19.join(sourceDir, "__tests__") || importDir === path19.join(sourceDir, "tests") || importDir === path19.join(sourceDir, "test");
33568
+ const sourceDir = path20.dirname(sourceFile);
33569
+ const sourceBasename = path20.basename(sourceFile, path20.extname(sourceFile));
33570
+ const isRelatedDir = importDir === sourceDir || importDir === path20.join(sourceDir, "__tests__") || importDir === path20.join(sourceDir, "tests") || importDir === path20.join(sourceDir, "test");
33491
33571
  if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
33492
33572
  if (!testFiles.includes(testFile)) {
33493
33573
  testFiles.push(testFile);
@@ -33572,8 +33652,8 @@ function buildTestCommand(framework, scope, files, coverage, baseDir) {
33572
33652
  return ["mvn", "test"];
33573
33653
  case "gradle": {
33574
33654
  const isWindows = process.platform === "win32";
33575
- const hasGradlewBat = fs9.existsSync(path19.join(baseDir, "gradlew.bat"));
33576
- const hasGradlew = fs9.existsSync(path19.join(baseDir, "gradlew"));
33655
+ const hasGradlewBat = fs9.existsSync(path20.join(baseDir, "gradlew.bat"));
33656
+ const hasGradlew = fs9.existsSync(path20.join(baseDir, "gradlew"));
33577
33657
  if (hasGradlewBat && isWindows)
33578
33658
  return ["gradlew.bat", "test"];
33579
33659
  if (hasGradlew)
@@ -33590,7 +33670,7 @@ function buildTestCommand(framework, scope, files, coverage, baseDir) {
33590
33670
  "cmake-build-release",
33591
33671
  "out"
33592
33672
  ];
33593
- const actualBuildDir = buildDirCandidates.find((d) => fs9.existsSync(path19.join(baseDir, d, "CMakeCache.txt"))) ?? "build";
33673
+ const actualBuildDir = buildDirCandidates.find((d) => fs9.existsSync(path20.join(baseDir, d, "CMakeCache.txt"))) ?? "build";
33594
33674
  return ["ctest", "--test-dir", actualBuildDir];
33595
33675
  }
33596
33676
  case "swift-test":
@@ -33943,7 +34023,7 @@ function findSourceFiles(dir, files = []) {
33943
34023
  for (const entry of entries) {
33944
34024
  if (SKIP_DIRECTORIES.has(entry))
33945
34025
  continue;
33946
- const fullPath = path19.join(dir, entry);
34026
+ const fullPath = path20.join(dir, entry);
33947
34027
  let stat2;
33948
34028
  try {
33949
34029
  stat2 = fs9.statSync(fullPath);
@@ -33953,7 +34033,7 @@ function findSourceFiles(dir, files = []) {
33953
34033
  if (stat2.isDirectory()) {
33954
34034
  findSourceFiles(fullPath, files);
33955
34035
  } else if (stat2.isFile()) {
33956
- const ext = path19.extname(fullPath).toLowerCase();
34036
+ const ext = path20.extname(fullPath).toLowerCase();
33957
34037
  if (SOURCE_EXTENSIONS.has(ext)) {
33958
34038
  files.push(fullPath);
33959
34039
  }
@@ -34122,13 +34202,13 @@ var init_test_runner = __esm(() => {
34122
34202
  testFiles = [];
34123
34203
  } else if (scope === "convention") {
34124
34204
  const sourceFiles = args2.files && args2.files.length > 0 ? args2.files.filter((f) => {
34125
- const ext = path19.extname(f).toLowerCase();
34205
+ const ext = path20.extname(f).toLowerCase();
34126
34206
  return SOURCE_EXTENSIONS.has(ext);
34127
34207
  }) : findSourceFiles(workingDir);
34128
34208
  testFiles = getTestFilesFromConvention(sourceFiles);
34129
34209
  } else if (scope === "graph") {
34130
34210
  const sourceFiles = args2.files && args2.files.length > 0 ? args2.files.filter((f) => {
34131
- const ext = path19.extname(f).toLowerCase();
34211
+ const ext = path20.extname(f).toLowerCase();
34132
34212
  return SOURCE_EXTENSIONS.has(ext);
34133
34213
  }) : findSourceFiles(workingDir);
34134
34214
  const graphTestFiles = await getTestFilesFromGraph(sourceFiles);
@@ -34151,7 +34231,7 @@ var init_test_runner = __esm(() => {
34151
34231
 
34152
34232
  // src/services/preflight-service.ts
34153
34233
  import * as fs10 from "fs";
34154
- import * as path20 from "path";
34234
+ import * as path21 from "path";
34155
34235
  function validateDirectoryPath(dir) {
34156
34236
  if (!dir || typeof dir !== "string") {
34157
34237
  throw new Error("Directory path is required");
@@ -34159,8 +34239,8 @@ function validateDirectoryPath(dir) {
34159
34239
  if (dir.includes("..")) {
34160
34240
  throw new Error("Directory path must not contain path traversal sequences");
34161
34241
  }
34162
- const normalized = path20.normalize(dir);
34163
- const absolutePath = path20.isAbsolute(normalized) ? normalized : path20.resolve(normalized);
34242
+ const normalized = path21.normalize(dir);
34243
+ const absolutePath = path21.isAbsolute(normalized) ? normalized : path21.resolve(normalized);
34164
34244
  return absolutePath;
34165
34245
  }
34166
34246
  function validateTimeout(timeoutMs, defaultValue) {
@@ -34183,7 +34263,7 @@ function validateTimeout(timeoutMs, defaultValue) {
34183
34263
  }
34184
34264
  function getPackageVersion(dir) {
34185
34265
  try {
34186
- const packagePath = path20.join(dir, "package.json");
34266
+ const packagePath = path21.join(dir, "package.json");
34187
34267
  if (fs10.existsSync(packagePath)) {
34188
34268
  const content = fs10.readFileSync(packagePath, "utf-8");
34189
34269
  const pkg = JSON.parse(content);
@@ -34194,7 +34274,7 @@ function getPackageVersion(dir) {
34194
34274
  }
34195
34275
  function getChangelogVersion(dir) {
34196
34276
  try {
34197
- const changelogPath = path20.join(dir, "CHANGELOG.md");
34277
+ const changelogPath = path21.join(dir, "CHANGELOG.md");
34198
34278
  if (fs10.existsSync(changelogPath)) {
34199
34279
  const content = fs10.readFileSync(changelogPath, "utf-8");
34200
34280
  const match = content.match(/^##\s*\[?(\d+\.\d+\.\d+)\]?/m);
@@ -34208,7 +34288,7 @@ function getChangelogVersion(dir) {
34208
34288
  function getVersionFileVersion(dir) {
34209
34289
  const possibleFiles = ["VERSION.txt", "version.txt", "VERSION", "version"];
34210
34290
  for (const file3 of possibleFiles) {
34211
- const filePath = path20.join(dir, file3);
34291
+ const filePath = path21.join(dir, file3);
34212
34292
  if (fs10.existsSync(filePath)) {
34213
34293
  try {
34214
34294
  const content = fs10.readFileSync(filePath, "utf-8").trim();
@@ -36194,8 +36274,8 @@ var init_tree_sitter = __esm(() => {
36194
36274
  bytes = Promise.resolve(input);
36195
36275
  } else {
36196
36276
  if (globalThis.process?.versions.node) {
36197
- const fs23 = await import("fs/promises");
36198
- bytes = fs23.readFile(input);
36277
+ const fs24 = await import("fs/promises");
36278
+ bytes = fs24.readFile(input);
36199
36279
  } else {
36200
36280
  bytes = fetch(input).then((response) => response.arrayBuffer().then((buffer) => {
36201
36281
  if (response.ok) {
@@ -38002,7 +38082,7 @@ var init_runtime = __esm(() => {
38002
38082
  });
38003
38083
 
38004
38084
  // src/index.ts
38005
- import * as path43 from "path";
38085
+ import * as path44 from "path";
38006
38086
 
38007
38087
  // src/tools/tool-names.ts
38008
38088
  var TOOL_NAMES = [
@@ -38030,7 +38110,9 @@ var TOOL_NAMES = [
38030
38110
  "retrieve_summary",
38031
38111
  "extract_code_blocks",
38032
38112
  "phase_complete",
38033
- "save_plan"
38113
+ "save_plan",
38114
+ "update_task_status",
38115
+ "write_retro"
38034
38116
  ];
38035
38117
  var TOOL_NAME_SET = new Set(TOOL_NAMES);
38036
38118
 
@@ -38068,7 +38150,9 @@ var AGENT_TOOL_MAP = {
38068
38150
  "secretscan",
38069
38151
  "symbols",
38070
38152
  "test_runner",
38071
- "todo_extract"
38153
+ "todo_extract",
38154
+ "update_task_status",
38155
+ "write_retro"
38072
38156
  ],
38073
38157
  explorer: [
38074
38158
  "complexity_hotspots",
@@ -39009,9 +39093,10 @@ writeCount > 0 on source files from the Architect is equivalent to GATE_DELEGATI
39009
39093
 
39010
39094
  PLAN STATE PROTECTION
39011
39095
  .swarm/plan.md and .swarm/plan.json are READABLE but NOT DIRECTLY WRITABLE for state transitions.
39012
- Task status changes (- [ ] to - [x], "pending" to "complete") must go through phase_complete() ONLY.
39096
+ Task-level status changes (marking individual tasks as "completed") must use update_task_status().
39097
+ Phase-level completion (marking an entire phase as done) must use phase_complete().
39013
39098
  You may write to plan.md/plan.json for STRUCTURAL changes (adding tasks, updating descriptions).
39014
- You may NOT write to plan.md/plan.json to change task completion status or phase status.
39099
+ You may NOT write to plan.md/plan.json to change task completion status or phase status directly.
39015
39100
  "I'll just mark it done directly" is a bypass \u2014 equivalent to GATE_DELEGATION_BYPASS.
39016
39101
 
39017
39102
  6i. **DELEGATION DISCIPLINE**
@@ -39047,7 +39132,7 @@ It is the same severity as skipping all gates. The QA gate is ALL steps or NONE.
39047
39132
  - Target file is in: pages/, components/, views/, screens/, ui/, layouts/
39048
39133
  If triggered: delegate to {{AGENT_PREFIX}}designer FIRST to produce a code scaffold. Then pass the scaffold to {{AGENT_PREFIX}}coder as INPUT alongside the task. The coder implements the TODOs in the scaffold without changing component structure or accessibility attributes.
39049
39134
  If not triggered: delegate directly to {{AGENT_PREFIX}}coder as normal.
39050
- 10. **RETROSPECTIVE TRACKING**: At the end of every phase, record phase metrics in .swarm/context.md under "## Phase Metrics" and write a retrospective evidence entry via the evidence manager. Track: phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned (max 5). Reset Phase Metrics to 0 after writing.
39135
+ 10. **RETROSPECTIVE TRACKING**: At the end of every phase, record phase metrics in .swarm/context.md under "## Phase Metrics" and write a retrospective evidence entry via write_retro. Track: phase, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned (max 5). Reset Phase Metrics to 0 after writing.
39051
39136
  11. **CHECKPOINTS**: Before delegating multi-file refactor tasks (3+ files), create a checkpoint save. On critical failures when redo is faster than iterative fixes, restore from checkpoint. Use checkpoint tool: \`checkpoint save\` before risky operations, \`checkpoint restore\` on failure.
39052
39137
 
39053
39138
  SECURITY_KEYWORDS: password, secret, token, credential, auth, login, encryption, hash, key, certificate, ssl, tls, jwt, oauth, session, csrf, xss, injection, sanitization, permission, access, vulnerable, exploit, privilege, authorization, roles, authentication, mfa, 2fa, totp, otp, salt, iv, nonce, hmac, aes, rsa, sha256, bcrypt, scrypt, argon2, api_key, apikey, private_key, public_key, rbac, admin, superuser, sqli, rce, ssrf, xxe, nosql, command_injection
@@ -39072,7 +39157,7 @@ Outside OpenCode, invoke any plugin command via: \`bunx opencode-swarm run <comm
39072
39157
 
39073
39158
  SMEs advise only. Reviewer and critic review only. None of them write code.
39074
39159
 
39075
- Available Tools: symbols (code symbol search), checkpoint (state snapshots), diff (structured git diff with contract change detection), imports (dependency audit), lint (code quality), placeholder_scan (placeholder/todo detection), secretscan (secret detection), sast_scan (static analysis security scan), syntax_check (syntax validation), test_runner (auto-detect and run tests), pkg_audit (dependency vulnerability scan \u2014 npm/pip/cargo), complexity_hotspots (git churn \xD7 complexity risk map), schema_drift (OpenAPI spec vs route drift), todo_extract (structured TODO/FIXME extraction), evidence_check (verify task evidence completeness), sbom_generate (SBOM generation for dependency inventory), build_check (build verification), quality_budget (code quality budget check), pre_check_batch (parallel verification: lint:check + secretscan + sast_scan + quality_budget)
39160
+ Available Tools: symbols (code symbol search), checkpoint (state snapshots), diff (structured git diff with contract change detection), imports (dependency audit), lint (code quality), placeholder_scan (placeholder/todo detection), secretscan (secret detection), sast_scan (static analysis security scan), syntax_check (syntax validation), test_runner (auto-detect and run tests), pkg_audit (dependency vulnerability scan \u2014 npm/pip/cargo), complexity_hotspots (git churn \xD7 complexity risk map), schema_drift (OpenAPI spec vs route drift), todo_extract (structured TODO/FIXME extraction), evidence_check (verify task evidence completeness), sbom_generate (SBOM generation for dependency inventory), build_check (build verification), quality_budget (code quality budget check), pre_check_batch (parallel verification: lint:check + secretscan + sast_scan + quality_budget), update_task_status (mark tasks complete, track phase progress), write_retro (document phase retrospectives via phase_complete workflow, capture lessons learned)
39076
39161
 
39077
39162
  ## DELEGATION FORMAT
39078
39163
 
@@ -39455,6 +39540,9 @@ The ONLY exception: lint tool in fix mode (step 5g) auto-corrects by design.
39455
39540
  All other gates: failure \u2192 return to coder. No self-fixes. No workarounds.
39456
39541
 
39457
39542
  5a. **UI DESIGN GATE** (conditional \u2014 Rule 9): If task matches UI trigger \u2192 {{AGENT_PREFIX}}designer produces scaffold \u2192 pass scaffold to coder as INPUT. If no match \u2192 skip.
39543
+
39544
+ \u2192 After step 5a (or immediately if no UI task applies): Call update_task_status with status in_progress for the current task. Then proceed to step 5b.
39545
+
39458
39546
  5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
39459
39547
  5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
39460
39548
  \u2192 REQUIRED: Print "diff: [PASS | CONTRACT CHANGE \u2014 details]"
@@ -39533,7 +39621,7 @@ PRE-COMMIT RULE \u2014 Before ANY commit or push:
39533
39621
  Any blank "value: ___" field = gate was not run = task is NOT complete.
39534
39622
  Filling this checklist from memory ("I think I ran it") is INVALID. Each value must come from actual tool/agent output in this session.
39535
39623
 
39536
- 5o. Update plan.md [x], proceed to next task.
39624
+ 5o. Call update_task_status with status "completed", proceed to next task.
39537
39625
 
39538
39626
  ## \u26D4 RETROSPECTIVE GATE
39539
39627
 
@@ -39541,31 +39629,26 @@ PRE-COMMIT RULE \u2014 Before ANY commit or push:
39541
39629
 
39542
39630
  **How to write the retrospective:**
39543
39631
 
39544
- Use the evidence manager tool to write a bundle at \`retro-{N}\` (where N is the phase number being completed):
39545
-
39546
- \`\`\`json
39547
- {
39548
- "type": "retrospective",
39549
- "phase_number": <N>,
39550
- "verdict": "pass",
39551
- "reviewer_rejections": <count>,
39552
- "coder_revisions": <count>,
39553
- "test_failures": <count>,
39554
- "security_findings": <count>,
39555
- "lessons_learned": ["lesson 1 (max 5)", "lesson 2"],
39556
- "top_rejection_reasons": ["reason 1"],
39557
- "user_directives": [],
39558
- "approaches_tried": [],
39559
- "task_complexity": "low|medium|high",
39560
- "timestamp": "<ISO 8601>",
39561
- "agent": "architect",
39562
- "metadata": { "plan_id": "<current plan title from .swarm/plan.json>" }
39563
- }
39564
- \`\`\`
39632
+ Call the \`write_retro\` tool with the required fields:
39633
+ - \`phase\`: The phase number being completed (e.g., 1, 2, 3)
39634
+ - \`summary\`: Human-readable summary of the phase
39635
+ - \`task_count\`: Count of tasks completed in this phase
39636
+ - \`task_complexity\`: One of \`trivial\` | \`simple\` | \`moderate\` | \`complex\`
39637
+ - \`total_tool_calls\`: Total number of tool calls in this phase
39638
+ - \`coder_revisions\`: Number of coder revisions made
39639
+ - \`reviewer_rejections\`: Number of reviewer rejections received
39640
+ - \`test_failures\`: Number of test failures encountered
39641
+ - \`security_findings\`: Number of security findings
39642
+ - \`integration_issues\`: Number of integration issues
39643
+ - \`lessons_learned\`: (optional) Key lessons learned from this phase (max 5)
39644
+ - \`top_rejection_reasons\`: (optional) Top reasons for reviewer rejections
39645
+ - \`metadata\`: (optional) Additional metadata, e.g., \`{ "plan_id": "<current plan title from .swarm/plan.json>" }\`
39646
+
39647
+ The tool will automatically write the retrospective to \`.swarm/evidence/retro-{phase}/evidence.json\` with the correct schema wrapper.
39565
39648
 
39566
39649
  **Required field rules:**
39567
- - \`verdict\` MUST be \`"pass"\` \u2014 a verdict of \`"fail"\` or missing verdict blocks phase_complete
39568
- - \`phase_number\` MUST match the phase number you are completing
39650
+ - \`verdict\` is auto-generated by write_retro with value \`"pass"\`. The resulting retrospective entry will have verdict \`"pass"\`; this is required for phase_complete to succeed.
39651
+ - \`phase\` MUST match the phase number you are completing
39569
39652
  - \`lessons_learned\` should be 3-5 concrete, actionable items from this phase
39570
39653
  - Write the bundle as task_id \`retro-{N}\` (e.g., \`retro-1\` for Phase 1, \`retro-2\` for Phase 2)
39571
39654
  - \`metadata.plan_id\` should be set to the current project's plan title (from \`.swarm/plan.json\` header). This enables cross-project filtering in the retrospective injection system.
@@ -39590,7 +39673,7 @@ Use the evidence manager tool to write a bundle at \`retro-{N}\` (where N is the
39590
39673
  - Summary of what was added/modified/removed
39591
39674
  - List of doc files that may need updating (README.md, CONTRIBUTING.md, docs/)
39592
39675
  3. Update context.md
39593
- 4. Write retrospective evidence: record phase_number, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned to .swarm/evidence/ via the evidence manager. Reset Phase Metrics in context.md to 0.
39676
+ 4. Write retrospective evidence: record phase, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned to .swarm/evidence/ via write_retro. Reset Phase Metrics in context.md to 0.
39594
39677
  4.5. Run \`evidence_check\` to verify all completed tasks have required evidence (review + test). If gaps found, note in retrospective lessons_learned. Optionally run \`pkg_audit\` if dependencies were modified during this phase. Optionally run \`schema_drift\` if API routes were modified during this phase.
39595
39678
  5. Run \`sbom_generate\` with scope='changed' to capture post-implementation dependency snapshot (saved to \`.swarm/evidence/sbom/\`). This is a non-blocking step - always proceeds to summary.
39596
39679
  5.5. If \`.swarm/spec.md\` exists: delegate {{AGENT_PREFIX}}critic with DRIFT-CHECK context \u2014 include phase number, list of completed task IDs and descriptions, and evidence path (\`.swarm/evidence/\`). If SIGNIFICANT DRIFT is returned: surface as a warning to the user before proceeding. If spec.md does not exist: skip silently.
@@ -40757,6 +40840,9 @@ function getAgentConfigs(config2) {
40757
40840
  } else {
40758
40841
  sdkConfig.mode = "subagent";
40759
40842
  }
40843
+ if (sdkConfig.mode === "primary") {
40844
+ delete sdkConfig.model;
40845
+ }
40760
40846
  const baseAgentName = stripKnownSwarmPrefix(agent.name);
40761
40847
  if (!toolFilterEnabled) {
40762
40848
  sdkConfig.tools = agent.config.tools ?? {};
@@ -43721,6 +43807,416 @@ async function handleExportCommand(directory, _args) {
43721
43807
  const exportData = await getExportData(directory);
43722
43808
  return formatExportMarkdown(exportData);
43723
43809
  }
43810
+ // src/commands/handoff.ts
43811
+ init_utils2();
43812
+ import { renameSync as renameSync4 } from "fs";
43813
+
43814
+ // src/services/handoff-service.ts
43815
+ init_utils2();
43816
+ init_manager2();
43817
+ var RTL_OVERRIDE_PATTERN = /[\u202e\u202d\u202c\u200f]/g;
43818
+ var MAX_TASK_ID_LENGTH = 100;
43819
+ var MAX_DECISION_LENGTH = 500;
43820
+ var MAX_INCOMPLETE_TASKS = 20;
43821
+ function escapeHtml(str) {
43822
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
43823
+ }
43824
+ function sanitizeString(str, maxLength) {
43825
+ if (!str)
43826
+ return "";
43827
+ const sanitized = String(str).replace(RTL_OVERRIDE_PATTERN, "");
43828
+ if (sanitized.length > maxLength) {
43829
+ return sanitized.substring(0, maxLength - 3) + "...";
43830
+ }
43831
+ return sanitized;
43832
+ }
43833
+ function validatePlanPhases(plan) {
43834
+ if (!plan || typeof plan !== "object")
43835
+ return false;
43836
+ const p = plan;
43837
+ if (!Array.isArray(p.phases))
43838
+ return false;
43839
+ for (const phase of p.phases) {
43840
+ if (!phase || typeof phase !== "object")
43841
+ return false;
43842
+ const phaseObj = phase;
43843
+ if (!Array.isArray(phaseObj.tasks))
43844
+ return false;
43845
+ }
43846
+ return true;
43847
+ }
43848
+ function extractCurrentPhaseFromPlan(plan) {
43849
+ if (!plan) {
43850
+ return { currentPhase: null, currentTask: null, incompleteTasks: [] };
43851
+ }
43852
+ if (!validatePlanPhases(plan)) {
43853
+ return { currentPhase: null, currentTask: null, incompleteTasks: [] };
43854
+ }
43855
+ let currentPhase = null;
43856
+ const currentPhaseNum = plan.current_phase;
43857
+ if (currentPhaseNum) {
43858
+ const phase = plan.phases.find((p) => p.id === currentPhaseNum);
43859
+ currentPhase = phase ? `Phase ${phase.id}: ${phase.name}` : null;
43860
+ } else {
43861
+ const inProgressPhase = plan.phases.find((p) => p.status === "in_progress");
43862
+ if (inProgressPhase) {
43863
+ currentPhase = `Phase ${inProgressPhase.id}: ${inProgressPhase.name}`;
43864
+ } else if (plan.phases.length > 0) {
43865
+ currentPhase = `Phase ${plan.phases[0].id}: ${plan.phases[0].name}`;
43866
+ }
43867
+ }
43868
+ let currentTask = null;
43869
+ const incompleteTasks = [];
43870
+ for (const phase of plan.phases) {
43871
+ for (const task of phase.tasks) {
43872
+ if (task.status === "in_progress") {
43873
+ currentTask = sanitizeString(task.id, MAX_TASK_ID_LENGTH);
43874
+ }
43875
+ if (task.status !== "completed") {
43876
+ if (incompleteTasks.length < MAX_INCOMPLETE_TASKS) {
43877
+ incompleteTasks.push(sanitizeString(task.id, MAX_TASK_ID_LENGTH));
43878
+ }
43879
+ }
43880
+ }
43881
+ }
43882
+ if (!currentTask && incompleteTasks.length > 0) {
43883
+ currentTask = incompleteTasks[0];
43884
+ }
43885
+ return { currentPhase, currentTask, incompleteTasks };
43886
+ }
43887
+ function parseSessionState(content) {
43888
+ if (!content)
43889
+ return null;
43890
+ try {
43891
+ const state = JSON.parse(content);
43892
+ let activeAgent = null;
43893
+ if (state.activeAgent && typeof state.activeAgent === "object") {
43894
+ const entries = Object.entries(state.activeAgent);
43895
+ if (entries.length > 0) {
43896
+ activeAgent = sanitizeString(entries[entries.length - 1][1], MAX_TASK_ID_LENGTH);
43897
+ }
43898
+ }
43899
+ let delegationState = null;
43900
+ if (state.delegationChains && typeof state.delegationChains === "object") {
43901
+ const chains = Object.entries(state.delegationChains);
43902
+ const activeChains = [];
43903
+ let maxDepth = 0;
43904
+ for (const [, chain] of chains) {
43905
+ if (Array.isArray(chain) && chain.length > 0) {
43906
+ const sanitizedChain = chain.map((e) => `${sanitizeString(e.from, MAX_TASK_ID_LENGTH)}->${sanitizeString(e.to, MAX_TASK_ID_LENGTH)}`).join(" | ");
43907
+ activeChains.push(sanitizedChain);
43908
+ maxDepth = Math.max(maxDepth, chain.length);
43909
+ }
43910
+ }
43911
+ if (activeChains.length > 0) {
43912
+ delegationState = {
43913
+ activeChains,
43914
+ delegationDepth: maxDepth,
43915
+ pendingHandoffs: []
43916
+ };
43917
+ }
43918
+ }
43919
+ let pendingQA = null;
43920
+ if (state.agentSessions && typeof state.agentSessions === "object") {
43921
+ for (const [, session] of Object.entries(state.agentSessions)) {
43922
+ const sess = session;
43923
+ if (sess.lastGateFailure && sess.currentTaskId) {
43924
+ pendingQA = {
43925
+ taskId: sanitizeString(sess.lastGateFailure.taskId, MAX_TASK_ID_LENGTH),
43926
+ lastFailure: sanitizeString(sess.lastGateFailure.tool, MAX_TASK_ID_LENGTH)
43927
+ };
43928
+ break;
43929
+ }
43930
+ }
43931
+ }
43932
+ return { activeAgent, delegationState, pendingQA };
43933
+ } catch {
43934
+ return null;
43935
+ }
43936
+ }
43937
+ function extractDecisions(content) {
43938
+ if (!content)
43939
+ return [];
43940
+ const decisions = [];
43941
+ const lines = content.split(`
43942
+ `);
43943
+ let inDecisionsSection = false;
43944
+ for (const line of lines) {
43945
+ if (line.trim() === "## Decisions") {
43946
+ inDecisionsSection = true;
43947
+ continue;
43948
+ }
43949
+ if (inDecisionsSection && line.startsWith("## ") && line.trim() !== "## Decisions") {
43950
+ break;
43951
+ }
43952
+ if (inDecisionsSection && line.trim().startsWith("- ")) {
43953
+ const text = line.trim().substring(2);
43954
+ const cleaned = text.replace(/\s*\[.*?\]\s*/g, "").replace(/\u2705/g, "").replace(/\[confirmed\]/g, "").trim();
43955
+ if (cleaned) {
43956
+ const sanitized = sanitizeString(cleaned, MAX_DECISION_LENGTH);
43957
+ if (sanitized) {
43958
+ decisions.push(sanitized);
43959
+ }
43960
+ }
43961
+ }
43962
+ }
43963
+ return decisions.slice(-5);
43964
+ }
43965
+ function extractPhaseMetrics(content) {
43966
+ if (!content)
43967
+ return "";
43968
+ const lines = content.split(`
43969
+ `);
43970
+ let inPhaseMetrics = false;
43971
+ const metricsLines = [];
43972
+ for (const line of lines) {
43973
+ if (line.trim() === "## Phase Metrics") {
43974
+ inPhaseMetrics = true;
43975
+ continue;
43976
+ }
43977
+ if (inPhaseMetrics && line.startsWith("## ")) {
43978
+ break;
43979
+ }
43980
+ if (inPhaseMetrics) {
43981
+ metricsLines.push(line);
43982
+ }
43983
+ }
43984
+ const lastFive = metricsLines.slice(-5);
43985
+ return lastFive.join(`
43986
+ `).trim();
43987
+ }
43988
+ async function getHandoffData(directory) {
43989
+ const now = new Date().toISOString();
43990
+ const sessionContent = await readSwarmFileAsync(directory, "session/state.json");
43991
+ const sessionState = parseSessionState(sessionContent);
43992
+ const plan = await loadPlanJsonOnly(directory);
43993
+ const planInfo = extractCurrentPhaseFromPlan(plan);
43994
+ if (!plan) {
43995
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md");
43996
+ if (planMdContent) {
43997
+ const phaseMatch = planMdContent.match(/^## Phase (\d+):?\s*(.+)?$/m);
43998
+ const taskMatch = planMdContent.match(/^- \[ \] (\d+\.\d+)/g);
43999
+ if (phaseMatch) {
44000
+ planInfo.currentPhase = sanitizeString(`Phase ${phaseMatch[1]}${phaseMatch[2] ? ": " + phaseMatch[2] : ""}`, MAX_TASK_ID_LENGTH);
44001
+ }
44002
+ if (taskMatch) {
44003
+ const rawTasks = taskMatch.map((t) => t.replace("- [ ] ", ""));
44004
+ planInfo.incompleteTasks = rawTasks.map((t) => sanitizeString(t, MAX_TASK_ID_LENGTH)).slice(0, MAX_INCOMPLETE_TASKS);
44005
+ if (!planInfo.currentTask && planInfo.incompleteTasks.length > 0) {
44006
+ planInfo.currentTask = planInfo.incompleteTasks[0];
44007
+ }
44008
+ }
44009
+ }
44010
+ }
44011
+ const contextContent = await readSwarmFileAsync(directory, "context.md");
44012
+ const recentDecisions = extractDecisions(contextContent);
44013
+ const rawPhaseMetrics = extractPhaseMetrics(contextContent);
44014
+ const phaseMetrics = sanitizeString(rawPhaseMetrics, 1000);
44015
+ let delegationState = null;
44016
+ if (sessionState?.delegationState) {
44017
+ delegationState = {
44018
+ ...sessionState.delegationState,
44019
+ pendingHandoffs: phaseMetrics ? [phaseMetrics] : []
44020
+ };
44021
+ }
44022
+ let pendingQA = null;
44023
+ if (sessionState?.pendingQA) {
44024
+ pendingQA = {
44025
+ taskId: escapeHtml(sessionState.pendingQA.taskId),
44026
+ lastFailure: sessionState.pendingQA.lastFailure ? escapeHtml(sessionState.pendingQA.lastFailure) : null
44027
+ };
44028
+ }
44029
+ const escapedDecisions = recentDecisions.map((d) => escapeHtml(d));
44030
+ let escapedDelegationState = null;
44031
+ if (delegationState) {
44032
+ escapedDelegationState = {
44033
+ ...delegationState,
44034
+ activeChains: delegationState.activeChains.map((c) => escapeHtml(c)),
44035
+ pendingHandoffs: delegationState.pendingHandoffs.map((p) => escapeHtml(p))
44036
+ };
44037
+ }
44038
+ const escapedIncompleteTasks = planInfo.incompleteTasks.map((t) => escapeHtml(t));
44039
+ return {
44040
+ generated: now,
44041
+ currentPhase: planInfo.currentPhase ? escapeHtml(planInfo.currentPhase) : null,
44042
+ currentTask: planInfo.currentTask ? escapeHtml(planInfo.currentTask) : null,
44043
+ incompleteTasks: escapedIncompleteTasks,
44044
+ pendingQA,
44045
+ activeAgent: sessionState?.activeAgent ? escapeHtml(sessionState.activeAgent) : null,
44046
+ recentDecisions: escapedDecisions,
44047
+ delegationState: escapedDelegationState
44048
+ };
44049
+ }
44050
+ function formatHandoffMarkdown(data) {
44051
+ const lines = [];
44052
+ lines.push("## Swarm Handoff");
44053
+ lines.push("");
44054
+ lines.push(`**Generated**: ${data.generated}`);
44055
+ lines.push("");
44056
+ lines.push("### Current State");
44057
+ if (data.currentPhase) {
44058
+ lines.push(`- **Phase**: ${data.currentPhase}`);
44059
+ }
44060
+ if (data.currentTask) {
44061
+ lines.push(`- **Task**: ${data.currentTask}`);
44062
+ }
44063
+ if (data.activeAgent) {
44064
+ lines.push(`- **Active Agent**: ${data.activeAgent}`);
44065
+ }
44066
+ lines.push("");
44067
+ if (data.incompleteTasks.length > 0) {
44068
+ lines.push("### Incomplete Tasks");
44069
+ const displayTasks = data.incompleteTasks.slice(0, 10);
44070
+ for (const taskId of displayTasks) {
44071
+ lines.push(`- ${taskId}`);
44072
+ }
44073
+ if (data.incompleteTasks.length > 10) {
44074
+ lines.push(`- ... and ${data.incompleteTasks.length - 10} more`);
44075
+ }
44076
+ lines.push("");
44077
+ }
44078
+ if (data.pendingQA) {
44079
+ lines.push("### Pending QA");
44080
+ lines.push(`- **Task**: ${data.pendingQA.taskId}`);
44081
+ if (data.pendingQA.lastFailure) {
44082
+ lines.push(`- **Last Failure**: ${data.pendingQA.lastFailure}`);
44083
+ }
44084
+ lines.push("");
44085
+ }
44086
+ if (data.delegationState && data.delegationState.activeChains.length > 0) {
44087
+ lines.push("### Delegation");
44088
+ lines.push(`- **Depth**: ${data.delegationState.delegationDepth}`);
44089
+ for (const chain of data.delegationState.activeChains.slice(0, 3)) {
44090
+ lines.push(`- ${chain}`);
44091
+ }
44092
+ lines.push("");
44093
+ }
44094
+ if (data.recentDecisions.length > 0) {
44095
+ lines.push("### Recent Decisions");
44096
+ for (const decision of data.recentDecisions.slice(0, 5)) {
44097
+ lines.push(`- ${decision}`);
44098
+ }
44099
+ lines.push("");
44100
+ }
44101
+ if (data.delegationState?.pendingHandoffs && data.delegationState.pendingHandoffs.length > 0) {
44102
+ lines.push("### Phase Metrics");
44103
+ lines.push("```");
44104
+ lines.push(data.delegationState.pendingHandoffs[0]);
44105
+ lines.push("```");
44106
+ }
44107
+ return lines.join(`
44108
+ `);
44109
+ }
44110
+
44111
+ // src/session/snapshot-writer.ts
44112
+ init_utils2();
44113
+ import { mkdirSync as mkdirSync5, renameSync as renameSync3 } from "fs";
44114
+ import * as path14 from "path";
44115
+ function serializeAgentSession(s) {
44116
+ const gateLog = {};
44117
+ const rawGateLog = s.gateLog ?? new Map;
44118
+ for (const [taskId, gates] of rawGateLog) {
44119
+ gateLog[taskId] = Array.from(gates ?? []);
44120
+ }
44121
+ const reviewerCallCount = {};
44122
+ const rawReviewerCallCount = s.reviewerCallCount ?? new Map;
44123
+ for (const [phase, count] of rawReviewerCallCount) {
44124
+ reviewerCallCount[String(phase)] = count;
44125
+ }
44126
+ const partialGateWarningsIssuedForTask = Array.from(s.partialGateWarningsIssuedForTask ?? new Set);
44127
+ const catastrophicPhaseWarnings = Array.from(s.catastrophicPhaseWarnings ?? new Set);
44128
+ const phaseAgentsDispatched = Array.from(s.phaseAgentsDispatched ?? new Set);
44129
+ const windows = {};
44130
+ const rawWindows = s.windows ?? {};
44131
+ for (const [key, win] of Object.entries(rawWindows)) {
44132
+ windows[key] = {
44133
+ id: win.id,
44134
+ agentName: win.agentName,
44135
+ startedAtMs: win.startedAtMs,
44136
+ toolCalls: win.toolCalls,
44137
+ consecutiveErrors: win.consecutiveErrors,
44138
+ hardLimitHit: win.hardLimitHit,
44139
+ lastSuccessTimeMs: win.lastSuccessTimeMs,
44140
+ recentToolCalls: win.recentToolCalls,
44141
+ warningIssued: win.warningIssued,
44142
+ warningReason: win.warningReason
44143
+ };
44144
+ }
44145
+ return {
44146
+ agentName: s.agentName,
44147
+ lastToolCallTime: s.lastToolCallTime,
44148
+ lastAgentEventTime: s.lastAgentEventTime,
44149
+ delegationActive: s.delegationActive,
44150
+ activeInvocationId: s.activeInvocationId,
44151
+ lastInvocationIdByAgent: s.lastInvocationIdByAgent ?? {},
44152
+ windows,
44153
+ lastCompactionHint: s.lastCompactionHint ?? 0,
44154
+ architectWriteCount: s.architectWriteCount ?? 0,
44155
+ lastCoderDelegationTaskId: s.lastCoderDelegationTaskId ?? null,
44156
+ currentTaskId: s.currentTaskId ?? null,
44157
+ gateLog,
44158
+ reviewerCallCount,
44159
+ lastGateFailure: s.lastGateFailure ?? null,
44160
+ partialGateWarningsIssuedForTask,
44161
+ selfFixAttempted: s.selfFixAttempted ?? false,
44162
+ catastrophicPhaseWarnings,
44163
+ lastPhaseCompleteTimestamp: s.lastPhaseCompleteTimestamp ?? 0,
44164
+ lastPhaseCompletePhase: s.lastPhaseCompletePhase ?? 0,
44165
+ phaseAgentsDispatched,
44166
+ qaSkipCount: s.qaSkipCount ?? 0,
44167
+ qaSkipTaskIds: s.qaSkipTaskIds ?? []
44168
+ };
44169
+ }
44170
+ async function writeSnapshot(directory, state) {
44171
+ try {
44172
+ const snapshot = {
44173
+ version: 1,
44174
+ writtenAt: Date.now(),
44175
+ toolAggregates: Object.fromEntries(state.toolAggregates),
44176
+ activeAgent: Object.fromEntries(state.activeAgent),
44177
+ delegationChains: Object.fromEntries(state.delegationChains),
44178
+ agentSessions: {}
44179
+ };
44180
+ for (const [sessionId, sessionState] of state.agentSessions) {
44181
+ snapshot.agentSessions[sessionId] = serializeAgentSession(sessionState);
44182
+ }
44183
+ const content = JSON.stringify(snapshot, null, 2);
44184
+ const resolvedPath = validateSwarmPath(directory, "session/state.json");
44185
+ const dir = path14.dirname(resolvedPath);
44186
+ mkdirSync5(dir, { recursive: true });
44187
+ const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
44188
+ await Bun.write(tempPath, content);
44189
+ renameSync3(tempPath, resolvedPath);
44190
+ } catch {}
44191
+ }
44192
+ function createSnapshotWriterHook(directory) {
44193
+ return async (_input, _output) => {
44194
+ try {
44195
+ await writeSnapshot(directory, swarmState);
44196
+ } catch {}
44197
+ };
44198
+ }
44199
+
44200
+ // src/commands/handoff.ts
44201
+ async function handleHandoffCommand(directory, _args) {
44202
+ const handoffData = await getHandoffData(directory);
44203
+ const markdown = formatHandoffMarkdown(handoffData);
44204
+ const resolvedPath = validateSwarmPath(directory, "handoff.md");
44205
+ const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
44206
+ await Bun.write(tempPath, markdown);
44207
+ renameSync4(tempPath, resolvedPath);
44208
+ await writeSnapshot(directory, swarmState);
44209
+ return `## Handoff Brief Written
44210
+
44211
+ Brief written to \`.swarm/handoff.md\`.
44212
+
44213
+ ${markdown}
44214
+
44215
+ ---
44216
+
44217
+ **Next Step:** Start a new OpenCode session, switch to your target model, and send: \`continue the previous work\``;
44218
+ }
44219
+
43724
44220
  // src/services/history-service.ts
43725
44221
  init_utils2();
43726
44222
  init_manager2();
@@ -43850,12 +44346,12 @@ async function handleHistoryCommand(directory, _args) {
43850
44346
  import { randomUUID as randomUUID2 } from "crypto";
43851
44347
  import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
43852
44348
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
43853
- import * as path15 from "path";
44349
+ import * as path16 from "path";
43854
44350
 
43855
44351
  // src/hooks/knowledge-validator.ts
43856
44352
  var import_proper_lockfile2 = __toESM(require_proper_lockfile(), 1);
43857
44353
  import { appendFile as appendFile2, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
43858
- import * as path14 from "path";
44354
+ import * as path15 from "path";
43859
44355
  var DANGEROUS_COMMAND_PATTERNS = [
43860
44356
  /\brm\s+-rf\b/,
43861
44357
  /\bsudo\s+rm\b/,
@@ -44107,10 +44603,10 @@ async function quarantineEntry(directory, entryId, reason, reportedBy) {
44107
44603
  return;
44108
44604
  }
44109
44605
  const sanitizedReason = reason.slice(0, 500).replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f\x0d]/g, "");
44110
- const knowledgePath = path14.join(directory, ".swarm", "knowledge.jsonl");
44111
- const quarantinePath = path14.join(directory, ".swarm", "knowledge-quarantined.jsonl");
44112
- const rejectedPath = path14.join(directory, ".swarm", "knowledge-rejected.jsonl");
44113
- const swarmDir = path14.join(directory, ".swarm");
44606
+ const knowledgePath = path15.join(directory, ".swarm", "knowledge.jsonl");
44607
+ const quarantinePath = path15.join(directory, ".swarm", "knowledge-quarantined.jsonl");
44608
+ const rejectedPath = path15.join(directory, ".swarm", "knowledge-rejected.jsonl");
44609
+ const swarmDir = path15.join(directory, ".swarm");
44114
44610
  await mkdir2(swarmDir, { recursive: true });
44115
44611
  let release;
44116
44612
  try {
@@ -44167,10 +44663,10 @@ async function restoreEntry(directory, entryId) {
44167
44663
  console.warn("[knowledge-validator] restoreEntry: invalid entryId rejected");
44168
44664
  return;
44169
44665
  }
44170
- const knowledgePath = path14.join(directory, ".swarm", "knowledge.jsonl");
44171
- const quarantinePath = path14.join(directory, ".swarm", "knowledge-quarantined.jsonl");
44172
- const rejectedPath = path14.join(directory, ".swarm", "knowledge-rejected.jsonl");
44173
- const swarmDir = path14.join(directory, ".swarm");
44666
+ const knowledgePath = path15.join(directory, ".swarm", "knowledge.jsonl");
44667
+ const quarantinePath = path15.join(directory, ".swarm", "knowledge-quarantined.jsonl");
44668
+ const rejectedPath = path15.join(directory, ".swarm", "knowledge-rejected.jsonl");
44669
+ const swarmDir = path15.join(directory, ".swarm");
44174
44670
  await mkdir2(swarmDir, { recursive: true });
44175
44671
  let release;
44176
44672
  try {
@@ -44205,8 +44701,8 @@ async function restoreEntry(directory, entryId) {
44205
44701
 
44206
44702
  // src/hooks/knowledge-migrator.ts
44207
44703
  async function migrateContextToKnowledge(directory, config3) {
44208
- const sentinelPath = path15.join(directory, ".swarm", ".knowledge-migrated");
44209
- const contextPath = path15.join(directory, ".swarm", "context.md");
44704
+ const sentinelPath = path16.join(directory, ".swarm", ".knowledge-migrated");
44705
+ const contextPath = path16.join(directory, ".swarm", "context.md");
44210
44706
  const knowledgePath = resolveSwarmKnowledgePath(directory);
44211
44707
  if (existsSync8(sentinelPath)) {
44212
44708
  return {
@@ -44402,7 +44898,7 @@ function truncateLesson(text) {
44402
44898
  return `${text.slice(0, 277)}...`;
44403
44899
  }
44404
44900
  function inferProjectName(directory) {
44405
- const packageJsonPath = path15.join(directory, "package.json");
44901
+ const packageJsonPath = path16.join(directory, "package.json");
44406
44902
  if (existsSync8(packageJsonPath)) {
44407
44903
  try {
44408
44904
  const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
@@ -44411,7 +44907,7 @@ function inferProjectName(directory) {
44411
44907
  }
44412
44908
  } catch {}
44413
44909
  }
44414
- return path15.basename(directory);
44910
+ return path16.basename(directory);
44415
44911
  }
44416
44912
  async function writeSentinel(sentinelPath, migrated, dropped) {
44417
44913
  const sentinel = {
@@ -44423,7 +44919,7 @@ async function writeSentinel(sentinelPath, migrated, dropped) {
44423
44919
  schema_version: 1,
44424
44920
  migration_tool: "knowledge-migrator.ts"
44425
44921
  };
44426
- await mkdir3(path15.dirname(sentinelPath), { recursive: true });
44922
+ await mkdir3(path16.dirname(sentinelPath), { recursive: true });
44427
44923
  await writeFile3(sentinelPath, JSON.stringify(sentinel, null, 2), "utf-8");
44428
44924
  }
44429
44925
 
@@ -44660,7 +45156,7 @@ async function handlePlanCommand(directory, args2) {
44660
45156
  init_preflight_service();
44661
45157
 
44662
45158
  // src/hooks/hive-promoter.ts
44663
- import path21 from "path";
45159
+ import path22 from "path";
44664
45160
  init_utils2();
44665
45161
  function isAlreadyInHive(entry, hiveEntries, threshold) {
44666
45162
  return findNearDuplicate(entry.lesson, hiveEntries, threshold) !== undefined;
@@ -44818,7 +45314,7 @@ async function promoteToHive(directory, lesson, category) {
44818
45314
  schema_version: 1,
44819
45315
  created_at: new Date().toISOString(),
44820
45316
  updated_at: new Date().toISOString(),
44821
- source_project: path21.basename(directory) || "unknown"
45317
+ source_project: path22.basename(directory) || "unknown"
44822
45318
  };
44823
45319
  await appendKnowledge(resolveHiveKnowledgePath(), newHiveEntry);
44824
45320
  return `Promoted to hive: "${trimmedLesson.slice(0, 50)}${trimmedLesson.length > 50 ? "..." : ""}" (confidence: 1.0, source: manual)`;
@@ -44978,8 +45474,8 @@ async function handleResetCommand(directory, args2) {
44978
45474
  // src/summaries/manager.ts
44979
45475
  init_utils2();
44980
45476
  init_utils();
44981
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync7 } from "fs";
44982
- import * as path22 from "path";
45477
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync7 } from "fs";
45478
+ import * as path23 from "path";
44983
45479
  var SUMMARY_ID_REGEX = /^S\d+$/;
44984
45480
  function sanitizeSummaryId(id) {
44985
45481
  if (!id || id.length === 0) {
@@ -45007,9 +45503,9 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
45007
45503
  if (outputBytes > maxStoredBytes) {
45008
45504
  throw new Error(`Summary fullOutput size (${outputBytes} bytes) exceeds maximum (${maxStoredBytes} bytes)`);
45009
45505
  }
45010
- const relativePath = path22.join("summaries", `${sanitizedId}.json`);
45506
+ const relativePath = path23.join("summaries", `${sanitizedId}.json`);
45011
45507
  const summaryPath = validateSwarmPath(directory, relativePath);
45012
- const summaryDir = path22.dirname(summaryPath);
45508
+ const summaryDir = path23.dirname(summaryPath);
45013
45509
  const entry = {
45014
45510
  id: sanitizedId,
45015
45511
  summaryText,
@@ -45018,11 +45514,11 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
45018
45514
  originalBytes: outputBytes
45019
45515
  };
45020
45516
  const entryJson = JSON.stringify(entry);
45021
- mkdirSync5(summaryDir, { recursive: true });
45022
- const tempPath = path22.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
45517
+ mkdirSync6(summaryDir, { recursive: true });
45518
+ const tempPath = path23.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
45023
45519
  try {
45024
45520
  await Bun.write(tempPath, entryJson);
45025
- renameSync3(tempPath, summaryPath);
45521
+ renameSync5(tempPath, summaryPath);
45026
45522
  } catch (error93) {
45027
45523
  try {
45028
45524
  rmSync3(tempPath, { force: true });
@@ -45032,7 +45528,7 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
45032
45528
  }
45033
45529
  async function loadFullOutput(directory, id) {
45034
45530
  const sanitizedId = sanitizeSummaryId(id);
45035
- const relativePath = path22.join("summaries", `${sanitizedId}.json`);
45531
+ const relativePath = path23.join("summaries", `${sanitizedId}.json`);
45036
45532
  validateSwarmPath(directory, relativePath);
45037
45533
  const content = await readSwarmFileAsync(directory, relativePath);
45038
45534
  if (content === null) {
@@ -45086,7 +45582,7 @@ ${error93 instanceof Error ? error93.message : String(error93)}`;
45086
45582
  // src/commands/rollback.ts
45087
45583
  init_utils2();
45088
45584
  import * as fs12 from "fs";
45089
- import * as path23 from "path";
45585
+ import * as path24 from "path";
45090
45586
  async function handleRollbackCommand(directory, args2) {
45091
45587
  const phaseArg = args2[0];
45092
45588
  if (!phaseArg) {
@@ -45134,8 +45630,8 @@ async function handleRollbackCommand(directory, args2) {
45134
45630
  const successes = [];
45135
45631
  const failures = [];
45136
45632
  for (const file3 of checkpointFiles) {
45137
- const src = path23.join(checkpointDir, file3);
45138
- const dest = path23.join(swarmDir, file3);
45633
+ const src = path24.join(checkpointDir, file3);
45634
+ const dest = path24.join(swarmDir, file3);
45139
45635
  try {
45140
45636
  fs12.cpSync(src, dest, { recursive: true, force: true });
45141
45637
  successes.push(file3);
@@ -45198,9 +45694,9 @@ async function handleSimulateCommand(directory, args2) {
45198
45694
  const report = reportLines.filter(Boolean).join(`
45199
45695
  `);
45200
45696
  const fs13 = await import("fs/promises");
45201
- const path24 = await import("path");
45202
- const reportPath = path24.join(directory, ".swarm", "simulate-report.md");
45203
- await fs13.mkdir(path24.dirname(reportPath), { recursive: true });
45697
+ const path25 = await import("path");
45698
+ const reportPath = path25.join(directory, ".swarm", "simulate-report.md");
45699
+ await fs13.mkdir(path25.dirname(reportPath), { recursive: true });
45204
45700
  await fs13.writeFile(reportPath, report, "utf-8");
45205
45701
  return `${darkMatterPairs.length} hidden coupling pairs detected`;
45206
45702
  }
@@ -45263,7 +45759,7 @@ function extractCurrentTask(planContent) {
45263
45759
  }
45264
45760
  return null;
45265
45761
  }
45266
- function extractDecisions(contextContent, maxChars = 500) {
45762
+ function extractDecisions2(contextContent, maxChars = 500) {
45267
45763
  if (!contextContent) {
45268
45764
  return null;
45269
45765
  }
@@ -45359,7 +45855,7 @@ function extractPatterns(contextContent, maxChars = 500) {
45359
45855
  }
45360
45856
  return `${trimmed.slice(0, maxChars)}...`;
45361
45857
  }
45362
- function extractCurrentPhaseFromPlan(plan) {
45858
+ function extractCurrentPhaseFromPlan2(plan) {
45363
45859
  const phase = plan.phases.find((p) => p.id === plan.current_phase);
45364
45860
  if (!phase)
45365
45861
  return null;
@@ -45559,7 +46055,7 @@ init_manager2();
45559
46055
  async function getStatusData(directory, agents) {
45560
46056
  const plan = await loadPlan(directory);
45561
46057
  if (plan && plan.migration_status !== "migration_failed") {
45562
- const currentPhase2 = extractCurrentPhaseFromPlan(plan) || "Unknown";
46058
+ const currentPhase2 = extractCurrentPhaseFromPlan2(plan) || "Unknown";
45563
46059
  let completedTasks2 = 0;
45564
46060
  let totalTasks2 = 0;
45565
46061
  for (const phase of plan.phases) {
@@ -45859,6 +46355,7 @@ var HELP_TEXT = [
45859
46355
  "- `/swarm knowledge restore <id>` \u2014 Restore a quarantined knowledge entry",
45860
46356
  "- `/swarm knowledge migrate` \u2014 Migrate knowledge entries to the current format",
45861
46357
  '- `/swarm promote "<lesson>" | --category <cat> | --from-swarm <id> \u2014 Manually promote lesson to hive knowledge',
46358
+ "- `/swarm handoff` \u2014 Prepare state for clean model switch (new session)",
45862
46359
  "- `/swarm write-retro <json>` \u2014 Write a retrospective evidence bundle for a completed phase"
45863
46360
  ].join(`
45864
46361
  `);
@@ -45960,6 +46457,9 @@ function createSwarmCommandHandler(directory, agents) {
45960
46457
  case "diagnose":
45961
46458
  text = await handleDiagnoseCommand(directory, args2);
45962
46459
  break;
46460
+ case "handoff":
46461
+ text = await handleHandoffCommand(directory, args2);
46462
+ break;
45963
46463
  default:
45964
46464
  text = HELP_TEXT;
45965
46465
  break;
@@ -45971,7 +46471,7 @@ function createSwarmCommandHandler(directory, agents) {
45971
46471
  }
45972
46472
 
45973
46473
  // src/hooks/agent-activity.ts
45974
- import { renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
46474
+ import { renameSync as renameSync6, unlinkSync as unlinkSync3 } from "fs";
45975
46475
  init_utils();
45976
46476
  init_utils2();
45977
46477
  function createAgentActivityHooks(config3, directory) {
@@ -46041,11 +46541,11 @@ async function doFlush(directory) {
46041
46541
  const activitySection = renderActivitySection();
46042
46542
  const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
46043
46543
  const flushedCount = swarmState.pendingEvents;
46044
- const path24 = `${directory}/.swarm/context.md`;
46045
- const tempPath = `${path24}.tmp`;
46544
+ const path25 = `${directory}/.swarm/context.md`;
46545
+ const tempPath = `${path25}.tmp`;
46046
46546
  try {
46047
46547
  await Bun.write(tempPath, updated);
46048
- renameSync4(tempPath, path24);
46548
+ renameSync6(tempPath, path25);
46049
46549
  } catch (writeError) {
46050
46550
  try {
46051
46551
  unlinkSync3(tempPath);
@@ -46105,7 +46605,7 @@ function createCompactionCustomizerHook(config3, directory) {
46105
46605
  const contextContent = await readSwarmFileAsync(directory, "context.md");
46106
46606
  const plan = await loadPlan(directory);
46107
46607
  if (plan && plan.migration_status !== "migration_failed") {
46108
- const currentPhase = extractCurrentPhaseFromPlan(plan);
46608
+ const currentPhase = extractCurrentPhaseFromPlan2(plan);
46109
46609
  if (currentPhase) {
46110
46610
  output.context.push(`[SWARM PLAN] ${currentPhase}`);
46111
46611
  }
@@ -46127,7 +46627,7 @@ function createCompactionCustomizerHook(config3, directory) {
46127
46627
  }
46128
46628
  }
46129
46629
  if (contextContent) {
46130
- const decisionsSummary = extractDecisions(contextContent);
46630
+ const decisionsSummary = extractDecisions2(contextContent);
46131
46631
  if (decisionsSummary) {
46132
46632
  output.context.push(`[SWARM DECISIONS] ${decisionsSummary}`);
46133
46633
  }
@@ -46885,7 +47385,7 @@ function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
46885
47385
  };
46886
47386
  }
46887
47387
  // src/hooks/guardrails.ts
46888
- import * as path24 from "path";
47388
+ import * as path25 from "path";
46889
47389
  init_manager2();
46890
47390
  init_utils();
46891
47391
  function extractPhaseNumber(phaseString) {
@@ -46927,10 +47427,10 @@ function isArchitect(sessionId) {
46927
47427
  function isOutsideSwarmDir(filePath, directory) {
46928
47428
  if (!filePath)
46929
47429
  return false;
46930
- const swarmDir = path24.resolve(directory, ".swarm");
46931
- const resolved = path24.resolve(directory, filePath);
46932
- const relative3 = path24.relative(swarmDir, resolved);
46933
- return relative3.startsWith("..") || path24.isAbsolute(relative3);
47430
+ const swarmDir = path25.resolve(directory, ".swarm");
47431
+ const resolved = path25.resolve(directory, filePath);
47432
+ const relative3 = path25.relative(swarmDir, resolved);
47433
+ return relative3.startsWith("..") || path25.isAbsolute(relative3);
46934
47434
  }
46935
47435
  function isSourceCodePath(filePath) {
46936
47436
  if (!filePath)
@@ -47221,7 +47721,7 @@ function createGuardrailsHooks(directory, config3) {
47221
47721
  try {
47222
47722
  const plan = await loadPlan(directory);
47223
47723
  if (plan) {
47224
- const phaseString = extractCurrentPhaseFromPlan(plan);
47724
+ const phaseString = extractCurrentPhaseFromPlan2(plan);
47225
47725
  currentPhase = extractPhaseNumber(phaseString);
47226
47726
  }
47227
47727
  } catch {}
@@ -47305,7 +47805,7 @@ function createGuardrailsHooks(directory, config3) {
47305
47805
  try {
47306
47806
  const plan = await loadPlan(directory);
47307
47807
  if (plan) {
47308
- const phaseString = extractCurrentPhaseFromPlan(plan);
47808
+ const phaseString = extractCurrentPhaseFromPlan2(plan);
47309
47809
  currentPhaseForCheck = extractPhaseNumber(phaseString);
47310
47810
  }
47311
47811
  } catch {}
@@ -47543,7 +48043,7 @@ function createPipelineTrackerHook(config3) {
47543
48043
  try {
47544
48044
  const plan = await loadPlan(process.cwd());
47545
48045
  if (plan) {
47546
- const phaseString = extractCurrentPhaseFromPlan(plan);
48046
+ const phaseString = extractCurrentPhaseFromPlan2(plan);
47547
48047
  phaseNumber = parsePhaseNumber(phaseString);
47548
48048
  }
47549
48049
  } catch {
@@ -47560,6 +48060,7 @@ ${originalText}`;
47560
48060
  };
47561
48061
  }
47562
48062
  // src/hooks/system-enhancer.ts
48063
+ import * as fs15 from "fs";
47563
48064
  init_manager();
47564
48065
  init_detector();
47565
48066
  init_manager2();
@@ -47568,7 +48069,7 @@ init_manager2();
47568
48069
  init_utils2();
47569
48070
  init_manager2();
47570
48071
  import * as fs14 from "fs";
47571
- import * as path25 from "path";
48072
+ import * as path26 from "path";
47572
48073
  var DEFAULT_DRIFT_CONFIG = {
47573
48074
  staleThresholdPhases: 1,
47574
48075
  detectContradictions: true,
@@ -47722,7 +48223,7 @@ async function analyzeDecisionDrift(directory, config3 = {}) {
47722
48223
  currentPhase = legacyPhase;
47723
48224
  }
47724
48225
  }
47725
- const contextPath = path25.join(directory, ".swarm", "context.md");
48226
+ const contextPath = path26.join(directory, ".swarm", "context.md");
47726
48227
  let contextContent = "";
47727
48228
  try {
47728
48229
  if (fs14.existsSync(contextPath)) {
@@ -47834,6 +48335,165 @@ function formatDriftForContext(result) {
47834
48335
 
47835
48336
  // src/services/index.ts
47836
48337
  init_config_doctor();
48338
+
48339
+ // src/services/context-budget-service.ts
48340
+ init_utils2();
48341
+ function validateDirectory(directory) {
48342
+ if (!directory || directory.trim() === "") {
48343
+ throw new Error("Invalid directory: empty");
48344
+ }
48345
+ if (/\.\.[/\\]/.test(directory)) {
48346
+ throw new Error("Invalid directory: path traversal detected");
48347
+ }
48348
+ if (directory.startsWith("/") || directory.startsWith("\\")) {
48349
+ throw new Error("Invalid directory: absolute path");
48350
+ }
48351
+ if (/^[A-Za-z]:[\\/]/.test(directory)) {
48352
+ throw new Error("Invalid directory: Windows absolute path");
48353
+ }
48354
+ }
48355
+ var COST_PER_1K_TOKENS = 0.003;
48356
+ function estimateTokens2(text) {
48357
+ if (!text || typeof text !== "string") {
48358
+ return 0;
48359
+ }
48360
+ return Math.ceil(text.length / 3.5);
48361
+ }
48362
+ async function readBudgetState(directory) {
48363
+ const content = await readSwarmFileAsync(directory, "session/budget-state.json");
48364
+ if (!content) {
48365
+ return null;
48366
+ }
48367
+ try {
48368
+ return JSON.parse(content);
48369
+ } catch {
48370
+ return null;
48371
+ }
48372
+ }
48373
+ async function writeBudgetState(directory, state) {
48374
+ const resolvedPath = validateSwarmPath(directory, "session/budget-state.json");
48375
+ const content = JSON.stringify(state, null, 2);
48376
+ await Bun.write(resolvedPath, content);
48377
+ }
48378
+ async function countEvents(directory) {
48379
+ const content = await readSwarmFileAsync(directory, "events.jsonl");
48380
+ if (!content) {
48381
+ return 0;
48382
+ }
48383
+ const lines = content.split(`
48384
+ `).filter((line) => line.trim().length > 0);
48385
+ return lines.length;
48386
+ }
48387
+ async function getPlanCursorContent(directory) {
48388
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
48389
+ if (!planContent) {
48390
+ return "";
48391
+ }
48392
+ const lines = planContent.split(`
48393
+ `);
48394
+ const cursorLines = [];
48395
+ let inCurrentSection = false;
48396
+ for (const line of lines) {
48397
+ if (line.includes("in_progress") || line.includes("**Current**")) {
48398
+ inCurrentSection = true;
48399
+ }
48400
+ if (inCurrentSection) {
48401
+ cursorLines.push(line);
48402
+ if (cursorLines.length > 30) {
48403
+ break;
48404
+ }
48405
+ }
48406
+ }
48407
+ return cursorLines.join(`
48408
+ `) || planContent.substring(0, 1000);
48409
+ }
48410
+ async function getContextBudgetReport(directory, assembledSystemPrompt, config3) {
48411
+ validateDirectory(directory);
48412
+ const timestamp = new Date().toISOString();
48413
+ const systemPromptTokens = estimateTokens2(assembledSystemPrompt);
48414
+ const planCursorContent = await getPlanCursorContent(directory);
48415
+ const planCursorTokens = estimateTokens2(planCursorContent);
48416
+ const knowledgeContent = await readSwarmFileAsync(directory, "knowledge.jsonl");
48417
+ const knowledgeTokens = estimateTokens2(knowledgeContent || "");
48418
+ const runMemoryContent = await readSwarmFileAsync(directory, "run-memory.jsonl");
48419
+ const runMemoryTokens = estimateTokens2(runMemoryContent || "");
48420
+ const handoffContent = await readSwarmFileAsync(directory, "handoff.md");
48421
+ const handoffTokens = estimateTokens2(handoffContent || "");
48422
+ const contextMdContent = await readSwarmFileAsync(directory, "context.md");
48423
+ const contextMdTokens = estimateTokens2(contextMdContent || "");
48424
+ const swarmTotalTokens = systemPromptTokens + planCursorTokens + knowledgeTokens + runMemoryTokens + handoffTokens + contextMdTokens;
48425
+ const estimatedTurnCount = await countEvents(directory);
48426
+ const budgetPct = swarmTotalTokens / config3.budgetTokens * 100;
48427
+ let status;
48428
+ let recommendation = null;
48429
+ if (budgetPct < config3.warningPct) {
48430
+ status = "ok";
48431
+ } else if (budgetPct < config3.criticalPct) {
48432
+ status = "warning";
48433
+ recommendation = "Consider wrapping up current phase and running /swarm handoff before starting new work.";
48434
+ } else {
48435
+ status = "critical";
48436
+ recommendation = "Run /swarm handoff and start a new session to avoid cost escalation.";
48437
+ }
48438
+ const estimatedSessionTokens = swarmTotalTokens * Math.max(1, estimatedTurnCount);
48439
+ return {
48440
+ timestamp,
48441
+ systemPromptTokens,
48442
+ planCursorTokens,
48443
+ knowledgeTokens,
48444
+ runMemoryTokens,
48445
+ handoffTokens,
48446
+ contextMdTokens,
48447
+ swarmTotalTokens,
48448
+ estimatedTurnCount,
48449
+ estimatedSessionTokens,
48450
+ budgetPct,
48451
+ status,
48452
+ recommendation
48453
+ };
48454
+ }
48455
+ async function formatBudgetWarning(report, directory, config3) {
48456
+ validateDirectory(directory);
48457
+ if (report.status === "ok") {
48458
+ return null;
48459
+ }
48460
+ if (!directory || directory.trim() === "") {
48461
+ return formatWarningMessage(report);
48462
+ }
48463
+ const budgetState = await readBudgetState(directory);
48464
+ const state = budgetState || {
48465
+ warningFiredAtTurn: null,
48466
+ criticalFiredAtTurn: null,
48467
+ lastInjectedAtTurn: null
48468
+ };
48469
+ const currentTurn = report.estimatedTurnCount;
48470
+ if (report.status === "warning") {
48471
+ if (config3.warningMode === "once" && state.warningFiredAtTurn !== null) {
48472
+ return null;
48473
+ }
48474
+ if (config3.warningMode === "interval" && state.warningFiredAtTurn !== null && currentTurn - state.warningFiredAtTurn < config3.warningIntervalTurns) {
48475
+ return null;
48476
+ }
48477
+ state.warningFiredAtTurn = currentTurn;
48478
+ state.lastInjectedAtTurn = currentTurn;
48479
+ await writeBudgetState(directory, state);
48480
+ } else if (report.status === "critical") {
48481
+ state.criticalFiredAtTurn = currentTurn;
48482
+ state.lastInjectedAtTurn = currentTurn;
48483
+ }
48484
+ return formatWarningMessage(report);
48485
+ }
48486
+ function formatWarningMessage(report) {
48487
+ const budgetPctStr = report.budgetPct.toFixed(1);
48488
+ const tokensPerTurn = report.swarmTotalTokens.toLocaleString();
48489
+ if (report.status === "warning") {
48490
+ return `[CONTEXT BUDGET: ${budgetPctStr}% \u2014 swarm injecting ~${tokensPerTurn} tokens/turn. Consider wrapping current phase and running /swarm handoff before starting new work.]`;
48491
+ }
48492
+ const costPerTurn = (report.swarmTotalTokens / 1000 * COST_PER_1K_TOKENS).toFixed(3);
48493
+ return `[CONTEXT BUDGET: ${budgetPctStr}% CRITICAL \u2014 swarm injecting ~${tokensPerTurn} tokens/turn. Run /swarm handoff and start a new session to avoid cost escalation. Estimated session cost scaling: ~$${costPerTurn}/turn at current context size.]`;
48494
+ }
48495
+
48496
+ // src/services/index.ts
47837
48497
  init_evidence_summary_service();
47838
48498
  init_preflight_integration();
47839
48499
  init_preflight_service();
@@ -48191,7 +48851,7 @@ function createSystemEnhancerHook(config3, directory) {
48191
48851
  let planContent = null;
48192
48852
  let phaseHeader = "";
48193
48853
  if (plan2 && plan2.migration_status !== "migration_failed") {
48194
- phaseHeader = extractCurrentPhaseFromPlan(plan2) || "";
48854
+ phaseHeader = extractCurrentPhaseFromPlan2(plan2) || "";
48195
48855
  planContent = await readSwarmFileAsync(directory, "plan.md");
48196
48856
  } else {
48197
48857
  planContent = await readSwarmFileAsync(directory, "plan.md");
@@ -48204,8 +48864,32 @@ function createSystemEnhancerHook(config3, directory) {
48204
48864
  const planCursor = extractPlanCursor(planContent);
48205
48865
  tryInject(planCursor);
48206
48866
  }
48867
+ if (mode !== "DISCOVER") {
48868
+ try {
48869
+ const handoffContent = await readSwarmFileAsync(directory, "handoff.md");
48870
+ if (handoffContent) {
48871
+ const handoffPath = validateSwarmPath(directory, "handoff.md");
48872
+ const consumedPath = validateSwarmPath(directory, "handoff-consumed.md");
48873
+ if (fs15.existsSync(consumedPath)) {
48874
+ warn("Duplicate handoff detected: handoff-consumed.md already exists");
48875
+ fs15.unlinkSync(consumedPath);
48876
+ }
48877
+ fs15.renameSync(handoffPath, consumedPath);
48878
+ const handoffBlock = `## HANDOFF \u2014 Resuming from model switch
48879
+ The previous model's session ended. Here is your starting context:
48880
+
48881
+ ${handoffContent}`;
48882
+ tryInject(`[HANDOFF BRIEF]
48883
+ ${handoffBlock}`);
48884
+ }
48885
+ } catch (error93) {
48886
+ if (error93?.code !== "ENOENT") {
48887
+ warn("Handoff injection failed:", error93);
48888
+ }
48889
+ }
48890
+ }
48207
48891
  if (mode !== "DISCOVER" && contextContent) {
48208
- const decisions = extractDecisions(contextContent, 200);
48892
+ const decisions = extractDecisions2(contextContent, 200);
48209
48893
  if (decisions) {
48210
48894
  tryInject(`[SWARM CONTEXT] Key decisions: ${decisions}`);
48211
48895
  }
@@ -48363,6 +49047,37 @@ function createSystemEnhancerHook(config3, directory) {
48363
49047
  }
48364
49048
  }
48365
49049
  }
49050
+ const userConfig = config3.context_budget;
49051
+ const defaultConfig = {
49052
+ enabled: true,
49053
+ budgetTokens: 40000,
49054
+ warningPct: 70,
49055
+ criticalPct: 90,
49056
+ warningMode: "once",
49057
+ warningIntervalTurns: 20
49058
+ };
49059
+ const contextBudgetConfig = userConfig ? {
49060
+ ...defaultConfig,
49061
+ ...userConfig,
49062
+ warningPct: userConfig.warn_threshold ? userConfig.warn_threshold * 100 : defaultConfig.warningPct,
49063
+ criticalPct: userConfig.critical_threshold ? userConfig.critical_threshold * 100 : defaultConfig.criticalPct,
49064
+ budgetTokens: userConfig.model_limits?.default ?? defaultConfig.budgetTokens
49065
+ } : defaultConfig;
49066
+ if (contextBudgetConfig.enabled !== false) {
49067
+ const assembledSystemPrompt = output.system.join(`
49068
+ `);
49069
+ const budgetReport = await getContextBudgetReport(directory, assembledSystemPrompt, contextBudgetConfig);
49070
+ const budgetWarning = await formatBudgetWarning(budgetReport, directory, contextBudgetConfig);
49071
+ if (budgetWarning) {
49072
+ const sessionId_cb = _input.sessionID;
49073
+ const activeAgent_cb = sessionId_cb ? swarmState.activeAgent.get(sessionId_cb) : null;
49074
+ const isArchitect_cb = !activeAgent_cb || stripKnownSwarmPrefix(activeAgent_cb) === "architect";
49075
+ if (isArchitect_cb) {
49076
+ output.system.push(`[FOR: architect]
49077
+ ${budgetWarning}`);
49078
+ }
49079
+ }
49080
+ }
48366
49081
  return;
48367
49082
  }
48368
49083
  const mode_b = await detectArchitectMode(directory);
@@ -48384,7 +49099,7 @@ function createSystemEnhancerHook(config3, directory) {
48384
49099
  let currentPhase = null;
48385
49100
  let currentTask = null;
48386
49101
  if (plan && plan.migration_status !== "migration_failed") {
48387
- currentPhase = extractCurrentPhaseFromPlan(plan);
49102
+ currentPhase = extractCurrentPhaseFromPlan2(plan);
48388
49103
  currentTask = extractCurrentTaskFromPlan(plan);
48389
49104
  } else {
48390
49105
  planContentForCursor = await readSwarmFileAsync(directory, "plan.md");
@@ -48429,8 +49144,40 @@ function createSystemEnhancerHook(config3, directory) {
48429
49144
  metadata: { contentType: "markdown" }
48430
49145
  });
48431
49146
  }
49147
+ if (mode_b !== "DISCOVER") {
49148
+ try {
49149
+ const handoffContent = await readSwarmFileAsync(directory, "handoff.md");
49150
+ if (handoffContent) {
49151
+ const handoffPath = validateSwarmPath(directory, "handoff.md");
49152
+ const consumedPath = validateSwarmPath(directory, "handoff-consumed.md");
49153
+ if (fs15.existsSync(consumedPath)) {
49154
+ warn("Duplicate handoff detected: handoff-consumed.md already exists");
49155
+ fs15.unlinkSync(consumedPath);
49156
+ }
49157
+ fs15.renameSync(handoffPath, consumedPath);
49158
+ const handoffBlock = `## HANDOFF \u2014 Resuming from model switch
49159
+ The previous model's session ended. Here is your starting context:
49160
+
49161
+ ${handoffContent}`;
49162
+ const handoffText = `[HANDOFF BRIEF]
49163
+ ${handoffBlock}`;
49164
+ candidates.push({
49165
+ id: `candidate-${idCounter++}`,
49166
+ kind: "phase",
49167
+ text: handoffText,
49168
+ tokens: estimateTokens(handoffText),
49169
+ priority: 1,
49170
+ metadata: { contentType: "markdown" }
49171
+ });
49172
+ }
49173
+ } catch (error93) {
49174
+ if (error93?.code !== "ENOENT") {
49175
+ warn("Handoff injection failed:", error93);
49176
+ }
49177
+ }
49178
+ }
48432
49179
  if (contextContent) {
48433
- const decisions = extractDecisions(contextContent, 200);
49180
+ const decisions = extractDecisions2(contextContent, 200);
48434
49181
  if (decisions) {
48435
49182
  const text = `[SWARM CONTEXT] Key decisions: ${decisions}`;
48436
49183
  candidates.push({
@@ -48700,6 +49447,37 @@ function createSystemEnhancerHook(config3, directory) {
48700
49447
  output.system.push(candidate.text);
48701
49448
  injectedTokens += candidate.tokens;
48702
49449
  }
49450
+ const userConfig_b = config3.context_budget;
49451
+ const defaultConfig_b = {
49452
+ enabled: true,
49453
+ budgetTokens: 40000,
49454
+ warningPct: 70,
49455
+ criticalPct: 90,
49456
+ warningMode: "once",
49457
+ warningIntervalTurns: 20
49458
+ };
49459
+ const contextBudgetConfig_b = userConfig_b ? {
49460
+ ...defaultConfig_b,
49461
+ ...userConfig_b,
49462
+ warningPct: userConfig_b.warn_threshold ? userConfig_b.warn_threshold * 100 : defaultConfig_b.warningPct,
49463
+ criticalPct: userConfig_b.critical_threshold ? userConfig_b.critical_threshold * 100 : defaultConfig_b.criticalPct,
49464
+ budgetTokens: userConfig_b.model_limits?.default ?? defaultConfig_b.budgetTokens
49465
+ } : defaultConfig_b;
49466
+ if (contextBudgetConfig_b.enabled !== false) {
49467
+ const assembledSystemPrompt_b = output.system.join(`
49468
+ `);
49469
+ const budgetReport_b = await getContextBudgetReport(directory, assembledSystemPrompt_b, contextBudgetConfig_b);
49470
+ const budgetWarning_b = await formatBudgetWarning(budgetReport_b, directory, contextBudgetConfig_b);
49471
+ if (budgetWarning_b) {
49472
+ const sessionId_cb_b = _input.sessionID;
49473
+ const activeAgent_cb_b = sessionId_cb_b ? swarmState.activeAgent.get(sessionId_cb_b) : null;
49474
+ const isArchitect_cb_b = !activeAgent_cb_b || stripKnownSwarmPrefix(activeAgent_cb_b) === "architect";
49475
+ if (isArchitect_cb_b) {
49476
+ output.system.push(`[FOR: architect]
49477
+ ${budgetWarning_b}`);
49478
+ }
49479
+ }
49480
+ }
48703
49481
  } catch (error93) {
48704
49482
  warn("System enhancer failed:", error93);
48705
49483
  }
@@ -49048,9 +49826,9 @@ function createDarkMatterDetectorHook(directory) {
49048
49826
  }
49049
49827
 
49050
49828
  // src/hooks/knowledge-reader.ts
49051
- import { existsSync as existsSync17 } from "fs";
49829
+ import { existsSync as existsSync18 } from "fs";
49052
49830
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
49053
- import * as path26 from "path";
49831
+ import * as path27 from "path";
49054
49832
  var JACCARD_THRESHOLD = 0.6;
49055
49833
  var HIVE_TIER_BOOST = 0.05;
49056
49834
  var SAME_PROJECT_PENALTY = -0.05;
@@ -49098,15 +49876,15 @@ function inferCategoriesFromPhase(phaseDescription) {
49098
49876
  return ["process", "tooling"];
49099
49877
  }
49100
49878
  async function recordLessonsShown(directory, lessonIds, currentPhase) {
49101
- const shownFile = path26.join(directory, ".swarm", ".knowledge-shown.json");
49879
+ const shownFile = path27.join(directory, ".swarm", ".knowledge-shown.json");
49102
49880
  try {
49103
49881
  let shownData = {};
49104
- if (existsSync17(shownFile)) {
49882
+ if (existsSync18(shownFile)) {
49105
49883
  const content = await readFile4(shownFile, "utf-8");
49106
49884
  shownData = JSON.parse(content);
49107
49885
  }
49108
49886
  shownData[currentPhase] = lessonIds;
49109
- await mkdir4(path26.dirname(shownFile), { recursive: true });
49887
+ await mkdir4(path27.dirname(shownFile), { recursive: true });
49110
49888
  await writeFile4(shownFile, JSON.stringify(shownData, null, 2), "utf-8");
49111
49889
  } catch {
49112
49890
  console.warn("[swarm] Knowledge: failed to record shown lessons");
@@ -49201,9 +49979,9 @@ async function readMergedKnowledge(directory, config3, context) {
49201
49979
  return topN;
49202
49980
  }
49203
49981
  async function updateRetrievalOutcome(directory, phaseInfo, phaseSucceeded) {
49204
- const shownFile = path26.join(directory, ".swarm", ".knowledge-shown.json");
49982
+ const shownFile = path27.join(directory, ".swarm", ".knowledge-shown.json");
49205
49983
  try {
49206
- if (!existsSync17(shownFile)) {
49984
+ if (!existsSync18(shownFile)) {
49207
49985
  return;
49208
49986
  }
49209
49987
  const content = await readFile4(shownFile, "utf-8");
@@ -49565,6 +50343,111 @@ function createKnowledgeCuratorHook(directory, config3) {
49565
50343
 
49566
50344
  // src/hooks/knowledge-injector.ts
49567
50345
  init_manager2();
50346
+
50347
+ // src/services/run-memory.ts
50348
+ init_utils2();
50349
+ function validateDirectory2(directory) {
50350
+ if (!directory || directory.trim() === "") {
50351
+ throw new Error("Invalid directory: empty");
50352
+ }
50353
+ if (/\.\.[/\\]/.test(directory)) {
50354
+ throw new Error("Invalid directory: path traversal detected");
50355
+ }
50356
+ if (directory.startsWith("/") || directory.startsWith("\\")) {
50357
+ throw new Error("Invalid directory: absolute path");
50358
+ }
50359
+ if (/^[A-Za-z]:[\\/]/.test(directory)) {
50360
+ throw new Error("Invalid directory: Windows absolute path");
50361
+ }
50362
+ }
50363
+ var RUN_MEMORY_FILENAME = "run-memory.jsonl";
50364
+ var MAX_SUMMARY_TOKENS = 500;
50365
+ function groupByTaskId(entries) {
50366
+ const groups = new Map;
50367
+ for (const entry of entries) {
50368
+ const existing = groups.get(entry.taskId) || [];
50369
+ existing.push(entry);
50370
+ groups.set(entry.taskId, existing);
50371
+ }
50372
+ return groups;
50373
+ }
50374
+ function summarizeTask(taskId, entries) {
50375
+ const failures = entries.filter((e) => e.outcome === "fail" || e.outcome === "retry");
50376
+ const passes = entries.filter((e) => e.outcome === "pass");
50377
+ if (failures.length === 0) {
50378
+ return null;
50379
+ }
50380
+ failures.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
50381
+ const lastFailure = failures[0];
50382
+ passes.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
50383
+ const lastPass = passes[0];
50384
+ const failCount = failures.length;
50385
+ if (lastPass) {
50386
+ const passAttempt = lastPass.attemptNumber;
50387
+ const failAttempt = lastFailure.attemptNumber;
50388
+ return `Task ${taskId}: FAILED attempt ${failAttempt} \u2014 ${lastFailure.failureReason || "unknown"}. Passed on attempt ${passAttempt}.`;
50389
+ } else {
50390
+ return `Task ${taskId}: FAILED ${failCount} times \u2014 last: ${lastFailure.failureReason || "unknown"}. Still failing.`;
50391
+ }
50392
+ }
50393
+ async function getRunMemorySummary(directory) {
50394
+ validateDirectory2(directory);
50395
+ const content = await readSwarmFileAsync(directory, RUN_MEMORY_FILENAME);
50396
+ if (!content) {
50397
+ return null;
50398
+ }
50399
+ const entries = [];
50400
+ const lines = content.split(`
50401
+ `);
50402
+ for (const line of lines) {
50403
+ if (!line.trim())
50404
+ continue;
50405
+ try {
50406
+ const entry = JSON.parse(line);
50407
+ entries.push(entry);
50408
+ } catch {}
50409
+ }
50410
+ if (entries.length === 0) {
50411
+ return null;
50412
+ }
50413
+ const groups = groupByTaskId(entries);
50414
+ const summaries = [];
50415
+ for (const [taskId, taskEntries] of groups) {
50416
+ const summary = summarizeTask(taskId, taskEntries);
50417
+ if (summary) {
50418
+ summaries.push(summary);
50419
+ }
50420
+ }
50421
+ if (summaries.length === 0) {
50422
+ return null;
50423
+ }
50424
+ const prefix = `[FOR: architect, coder]
50425
+ ## RUN MEMORY \u2014 Previous Task Outcomes
50426
+ `;
50427
+ const suffix = `
50428
+ Use this data to avoid repeating known failure patterns.`;
50429
+ let summaryText = summaries.join(`
50430
+ `);
50431
+ const estimateTokens3 = (text) => {
50432
+ return Math.ceil(text.length * 0.33);
50433
+ };
50434
+ const totalText = prefix + summaryText + suffix;
50435
+ const estimatedTokens = estimateTokens3(totalText);
50436
+ if (estimatedTokens > MAX_SUMMARY_TOKENS) {
50437
+ const prefixTokens = estimateTokens3(prefix);
50438
+ const suffixTokens = estimateTokens3(suffix);
50439
+ const availableContentTokens = MAX_SUMMARY_TOKENS - prefixTokens - suffixTokens;
50440
+ if (availableContentTokens > 0) {
50441
+ const maxContentChars = Math.floor(availableContentTokens / 0.33);
50442
+ summaryText = summaryText.slice(-maxContentChars);
50443
+ } else {
50444
+ summaryText = "";
50445
+ }
50446
+ }
50447
+ return prefix + summaryText + suffix;
50448
+ }
50449
+
50450
+ // src/hooks/knowledge-injector.ts
49568
50451
  init_utils2();
49569
50452
  function formatStars(confidence) {
49570
50453
  if (confidence >= 0.9)
@@ -49638,7 +50521,7 @@ function createKnowledgeInjectorHook(directory, config3) {
49638
50521
  lastSeenPhase = currentPhase;
49639
50522
  cachedInjectionText = null;
49640
50523
  }
49641
- const phaseDescription = extractCurrentPhaseFromPlan(plan) ?? `Phase ${currentPhase}`;
50524
+ const phaseDescription = extractCurrentPhaseFromPlan2(plan) ?? `Phase ${currentPhase}`;
49642
50525
  const context = {
49643
50526
  projectName: plan.title,
49644
50527
  currentPhase: phaseDescription
@@ -49646,6 +50529,7 @@ function createKnowledgeInjectorHook(directory, config3) {
49646
50529
  const entries = await readMergedKnowledge(directory, config3, context);
49647
50530
  if (entries.length === 0)
49648
50531
  return;
50532
+ const runMemory = await getRunMemorySummary(directory);
49649
50533
  const lines = entries.map((entry) => {
49650
50534
  const stars = formatStars(entry.confidence);
49651
50535
  const tierLabel = entry.tier === "hive" ? "[HIVE]" : "[SWARM]";
@@ -49655,7 +50539,7 @@ function createKnowledgeInjectorHook(directory, config3) {
49655
50539
  const source = rawSource !== null ? ` \u2014 Source: ${sanitizeLessonForContext(rawSource)}` : "";
49656
50540
  return `${stars} ${tierLabel} ${sanitizeLessonForContext(entry.lesson)}${source}${confirmText}`;
49657
50541
  });
49658
- cachedInjectionText = [
50542
+ const knowledgeSection = [
49659
50543
  `\uD83D\uDCDA Knowledge (${entries.length} relevant lesson${entries.length > 1 ? "s" : ""}):`,
49660
50544
  "",
49661
50545
  ...lines,
@@ -49663,6 +50547,13 @@ function createKnowledgeInjectorHook(directory, config3) {
49663
50547
  "These are lessons learned from this project and past projects. Consider them as context but use your judgment \u2014 they may not all apply."
49664
50548
  ].join(`
49665
50549
  `);
50550
+ if (runMemory) {
50551
+ cachedInjectionText = runMemory + `
50552
+
50553
+ ` + knowledgeSection;
50554
+ } else {
50555
+ cachedInjectionText = knowledgeSection;
50556
+ }
49666
50557
  const rejected = await readRejectedLessons(directory);
49667
50558
  if (rejected.length > 0) {
49668
50559
  const recentRejected = rejected.slice(-3);
@@ -49679,7 +50570,7 @@ function createKnowledgeInjectorHook(directory, config3) {
49679
50570
 
49680
50571
  // src/hooks/steering-consumed.ts
49681
50572
  init_utils2();
49682
- import * as fs15 from "fs";
50573
+ import * as fs16 from "fs";
49683
50574
  function recordSteeringConsumed(directory, directiveId) {
49684
50575
  try {
49685
50576
  const eventsPath = validateSwarmPath(directory, "events.jsonl");
@@ -49688,7 +50579,7 @@ function recordSteeringConsumed(directory, directiveId) {
49688
50579
  directiveId,
49689
50580
  timestamp: new Date().toISOString()
49690
50581
  };
49691
- fs15.appendFileSync(eventsPath, `${JSON.stringify(event)}
50582
+ fs16.appendFileSync(eventsPath, `${JSON.stringify(event)}
49692
50583
  `, "utf-8");
49693
50584
  } catch {}
49694
50585
  }
@@ -49833,95 +50724,6 @@ async function loadSnapshot(directory) {
49833
50724
  } catch {}
49834
50725
  }
49835
50726
 
49836
- // src/session/snapshot-writer.ts
49837
- init_utils2();
49838
- import { mkdirSync as mkdirSync6, renameSync as renameSync5 } from "fs";
49839
- import * as path27 from "path";
49840
- function serializeAgentSession(s) {
49841
- const gateLog = {};
49842
- const rawGateLog = s.gateLog ?? new Map;
49843
- for (const [taskId, gates] of rawGateLog) {
49844
- gateLog[taskId] = Array.from(gates ?? []);
49845
- }
49846
- const reviewerCallCount = {};
49847
- const rawReviewerCallCount = s.reviewerCallCount ?? new Map;
49848
- for (const [phase, count] of rawReviewerCallCount) {
49849
- reviewerCallCount[String(phase)] = count;
49850
- }
49851
- const partialGateWarningsIssuedForTask = Array.from(s.partialGateWarningsIssuedForTask ?? new Set);
49852
- const catastrophicPhaseWarnings = Array.from(s.catastrophicPhaseWarnings ?? new Set);
49853
- const phaseAgentsDispatched = Array.from(s.phaseAgentsDispatched ?? new Set);
49854
- const windows = {};
49855
- const rawWindows = s.windows ?? {};
49856
- for (const [key, win] of Object.entries(rawWindows)) {
49857
- windows[key] = {
49858
- id: win.id,
49859
- agentName: win.agentName,
49860
- startedAtMs: win.startedAtMs,
49861
- toolCalls: win.toolCalls,
49862
- consecutiveErrors: win.consecutiveErrors,
49863
- hardLimitHit: win.hardLimitHit,
49864
- lastSuccessTimeMs: win.lastSuccessTimeMs,
49865
- recentToolCalls: win.recentToolCalls,
49866
- warningIssued: win.warningIssued,
49867
- warningReason: win.warningReason
49868
- };
49869
- }
49870
- return {
49871
- agentName: s.agentName,
49872
- lastToolCallTime: s.lastToolCallTime,
49873
- lastAgentEventTime: s.lastAgentEventTime,
49874
- delegationActive: s.delegationActive,
49875
- activeInvocationId: s.activeInvocationId,
49876
- lastInvocationIdByAgent: s.lastInvocationIdByAgent ?? {},
49877
- windows,
49878
- lastCompactionHint: s.lastCompactionHint ?? 0,
49879
- architectWriteCount: s.architectWriteCount ?? 0,
49880
- lastCoderDelegationTaskId: s.lastCoderDelegationTaskId ?? null,
49881
- currentTaskId: s.currentTaskId ?? null,
49882
- gateLog,
49883
- reviewerCallCount,
49884
- lastGateFailure: s.lastGateFailure ?? null,
49885
- partialGateWarningsIssuedForTask,
49886
- selfFixAttempted: s.selfFixAttempted ?? false,
49887
- catastrophicPhaseWarnings,
49888
- lastPhaseCompleteTimestamp: s.lastPhaseCompleteTimestamp ?? 0,
49889
- lastPhaseCompletePhase: s.lastPhaseCompletePhase ?? 0,
49890
- phaseAgentsDispatched,
49891
- qaSkipCount: s.qaSkipCount ?? 0,
49892
- qaSkipTaskIds: s.qaSkipTaskIds ?? []
49893
- };
49894
- }
49895
- async function writeSnapshot(directory, state) {
49896
- try {
49897
- const snapshot = {
49898
- version: 1,
49899
- writtenAt: Date.now(),
49900
- toolAggregates: Object.fromEntries(state.toolAggregates),
49901
- activeAgent: Object.fromEntries(state.activeAgent),
49902
- delegationChains: Object.fromEntries(state.delegationChains),
49903
- agentSessions: {}
49904
- };
49905
- for (const [sessionId, sessionState] of state.agentSessions) {
49906
- snapshot.agentSessions[sessionId] = serializeAgentSession(sessionState);
49907
- }
49908
- const content = JSON.stringify(snapshot, null, 2);
49909
- const resolvedPath = validateSwarmPath(directory, "session/state.json");
49910
- const dir = path27.dirname(resolvedPath);
49911
- mkdirSync6(dir, { recursive: true });
49912
- const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
49913
- await Bun.write(tempPath, content);
49914
- renameSync5(tempPath, resolvedPath);
49915
- } catch {}
49916
- }
49917
- function createSnapshotWriterHook(directory) {
49918
- return async (_input, _output) => {
49919
- try {
49920
- await writeSnapshot(directory, swarmState);
49921
- } catch {}
49922
- };
49923
- }
49924
-
49925
50727
  // src/tools/build-check.ts
49926
50728
  init_dist();
49927
50729
  init_discovery();
@@ -50097,7 +50899,7 @@ var build_check = createSwarmTool({
50097
50899
  init_tool();
50098
50900
  init_create_tool();
50099
50901
  import { spawnSync } from "child_process";
50100
- import * as fs16 from "fs";
50902
+ import * as fs17 from "fs";
50101
50903
  import * as path28 from "path";
50102
50904
  var CHECKPOINT_LOG_PATH = ".swarm/checkpoints.json";
50103
50905
  var MAX_LABEL_LENGTH = 100;
@@ -50154,8 +50956,8 @@ function getCheckpointLogPath(directory) {
50154
50956
  function readCheckpointLog(directory) {
50155
50957
  const logPath = getCheckpointLogPath(directory);
50156
50958
  try {
50157
- if (fs16.existsSync(logPath)) {
50158
- const content = fs16.readFileSync(logPath, "utf-8");
50959
+ if (fs17.existsSync(logPath)) {
50960
+ const content = fs17.readFileSync(logPath, "utf-8");
50159
50961
  const parsed = JSON.parse(content);
50160
50962
  if (!parsed.checkpoints || !Array.isArray(parsed.checkpoints)) {
50161
50963
  return { version: 1, checkpoints: [] };
@@ -50168,12 +50970,12 @@ function readCheckpointLog(directory) {
50168
50970
  function writeCheckpointLog(log2, directory) {
50169
50971
  const logPath = getCheckpointLogPath(directory);
50170
50972
  const dir = path28.dirname(logPath);
50171
- if (!fs16.existsSync(dir)) {
50172
- fs16.mkdirSync(dir, { recursive: true });
50973
+ if (!fs17.existsSync(dir)) {
50974
+ fs17.mkdirSync(dir, { recursive: true });
50173
50975
  }
50174
50976
  const tempPath = `${logPath}.tmp`;
50175
- fs16.writeFileSync(tempPath, JSON.stringify(log2, null, 2), "utf-8");
50176
- fs16.renameSync(tempPath, logPath);
50977
+ fs17.writeFileSync(tempPath, JSON.stringify(log2, null, 2), "utf-8");
50978
+ fs17.renameSync(tempPath, logPath);
50177
50979
  }
50178
50980
  function gitExec(args2) {
50179
50981
  const result = spawnSync("git", args2, {
@@ -50374,7 +51176,7 @@ var checkpoint = createSwarmTool({
50374
51176
  // src/tools/complexity-hotspots.ts
50375
51177
  init_dist();
50376
51178
  init_create_tool();
50377
- import * as fs17 from "fs";
51179
+ import * as fs18 from "fs";
50378
51180
  import * as path29 from "path";
50379
51181
  var MAX_FILE_SIZE_BYTES2 = 256 * 1024;
50380
51182
  var DEFAULT_DAYS = 90;
@@ -50503,11 +51305,11 @@ function estimateComplexity(content) {
50503
51305
  }
50504
51306
  function getComplexityForFile(filePath) {
50505
51307
  try {
50506
- const stat2 = fs17.statSync(filePath);
51308
+ const stat2 = fs18.statSync(filePath);
50507
51309
  if (stat2.size > MAX_FILE_SIZE_BYTES2) {
50508
51310
  return null;
50509
51311
  }
50510
- const content = fs17.readFileSync(filePath, "utf-8");
51312
+ const content = fs18.readFileSync(filePath, "utf-8");
50511
51313
  return estimateComplexity(content);
50512
51314
  } catch {
50513
51315
  return null;
@@ -50528,7 +51330,7 @@ async function analyzeHotspots(days, topN, extensions) {
50528
51330
  let analyzedFiles = 0;
50529
51331
  for (const [file3, churnCount] of filteredChurn) {
50530
51332
  let fullPath = file3;
50531
- if (!fs17.existsSync(fullPath)) {
51333
+ if (!fs18.existsSync(fullPath)) {
50532
51334
  fullPath = path29.join(cwd, file3);
50533
51335
  }
50534
51336
  const complexity = getComplexityForFile(fullPath);
@@ -51000,7 +51802,7 @@ Use these as DOMAIN values when delegating to @sme.`;
51000
51802
  // src/tools/evidence-check.ts
51001
51803
  init_dist();
51002
51804
  init_create_tool();
51003
- import * as fs18 from "fs";
51805
+ import * as fs19 from "fs";
51004
51806
  import * as path30 from "path";
51005
51807
  var MAX_FILE_SIZE_BYTES3 = 1024 * 1024;
51006
51808
  var MAX_EVIDENCE_FILES = 1000;
@@ -51042,12 +51844,12 @@ function parseCompletedTasks(planContent) {
51042
51844
  }
51043
51845
  function readEvidenceFiles(evidenceDir, _cwd) {
51044
51846
  const evidence = [];
51045
- if (!fs18.existsSync(evidenceDir) || !fs18.statSync(evidenceDir).isDirectory()) {
51847
+ if (!fs19.existsSync(evidenceDir) || !fs19.statSync(evidenceDir).isDirectory()) {
51046
51848
  return evidence;
51047
51849
  }
51048
51850
  let files;
51049
51851
  try {
51050
- files = fs18.readdirSync(evidenceDir);
51852
+ files = fs19.readdirSync(evidenceDir);
51051
51853
  } catch {
51052
51854
  return evidence;
51053
51855
  }
@@ -51063,7 +51865,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
51063
51865
  if (!resolvedPath.startsWith(evidenceDirResolved)) {
51064
51866
  continue;
51065
51867
  }
51066
- const stat2 = fs18.lstatSync(filePath);
51868
+ const stat2 = fs19.lstatSync(filePath);
51067
51869
  if (!stat2.isFile()) {
51068
51870
  continue;
51069
51871
  }
@@ -51072,7 +51874,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
51072
51874
  }
51073
51875
  let fileStat;
51074
51876
  try {
51075
- fileStat = fs18.statSync(filePath);
51877
+ fileStat = fs19.statSync(filePath);
51076
51878
  if (fileStat.size > MAX_FILE_SIZE_BYTES3) {
51077
51879
  continue;
51078
51880
  }
@@ -51081,7 +51883,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
51081
51883
  }
51082
51884
  let content;
51083
51885
  try {
51084
- content = fs18.readFileSync(filePath, "utf-8");
51886
+ content = fs19.readFileSync(filePath, "utf-8");
51085
51887
  } catch {
51086
51888
  continue;
51087
51889
  }
@@ -51180,7 +51982,7 @@ var evidence_check = createSwarmTool({
51180
51982
  }
51181
51983
  let planContent;
51182
51984
  try {
51183
- planContent = fs18.readFileSync(planPath, "utf-8");
51985
+ planContent = fs19.readFileSync(planPath, "utf-8");
51184
51986
  } catch {
51185
51987
  const result2 = {
51186
51988
  message: "No completed tasks found in plan.",
@@ -51215,7 +52017,7 @@ var evidence_check = createSwarmTool({
51215
52017
  // src/tools/file-extractor.ts
51216
52018
  init_tool();
51217
52019
  init_create_tool();
51218
- import * as fs19 from "fs";
52020
+ import * as fs20 from "fs";
51219
52021
  import * as path31 from "path";
51220
52022
  var EXT_MAP = {
51221
52023
  python: ".py",
@@ -51278,8 +52080,8 @@ var extract_code_blocks = createSwarmTool({
51278
52080
  execute: async (args2, directory) => {
51279
52081
  const { content, output_dir, prefix } = args2;
51280
52082
  const targetDir = output_dir || directory;
51281
- if (!fs19.existsSync(targetDir)) {
51282
- fs19.mkdirSync(targetDir, { recursive: true });
52083
+ if (!fs20.existsSync(targetDir)) {
52084
+ fs20.mkdirSync(targetDir, { recursive: true });
51283
52085
  }
51284
52086
  if (!content) {
51285
52087
  return "Error: content is required";
@@ -51301,12 +52103,12 @@ var extract_code_blocks = createSwarmTool({
51301
52103
  const base = path31.basename(filepath, path31.extname(filepath));
51302
52104
  const ext = path31.extname(filepath);
51303
52105
  let counter = 1;
51304
- while (fs19.existsSync(filepath)) {
52106
+ while (fs20.existsSync(filepath)) {
51305
52107
  filepath = path31.join(targetDir, `${base}_${counter}${ext}`);
51306
52108
  counter++;
51307
52109
  }
51308
52110
  try {
51309
- fs19.writeFileSync(filepath, code.trim(), "utf-8");
52111
+ fs20.writeFileSync(filepath, code.trim(), "utf-8");
51310
52112
  savedFiles.push(filepath);
51311
52113
  } catch (error93) {
51312
52114
  errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
@@ -51414,7 +52216,7 @@ var gitingest = tool({
51414
52216
  });
51415
52217
  // src/tools/imports.ts
51416
52218
  init_dist();
51417
- import * as fs20 from "fs";
52219
+ import * as fs21 from "fs";
51418
52220
  import * as path32 from "path";
51419
52221
  var MAX_FILE_PATH_LENGTH2 = 500;
51420
52222
  var MAX_SYMBOL_LENGTH = 256;
@@ -51583,7 +52385,7 @@ var SKIP_DIRECTORIES2 = new Set([
51583
52385
  function findSourceFiles2(dir, files = [], stats = { skippedDirs: [], skippedFiles: 0, fileErrors: [] }) {
51584
52386
  let entries;
51585
52387
  try {
51586
- entries = fs20.readdirSync(dir);
52388
+ entries = fs21.readdirSync(dir);
51587
52389
  } catch (e) {
51588
52390
  stats.fileErrors.push({
51589
52391
  path: dir,
@@ -51600,7 +52402,7 @@ function findSourceFiles2(dir, files = [], stats = { skippedDirs: [], skippedFil
51600
52402
  const fullPath = path32.join(dir, entry);
51601
52403
  let stat2;
51602
52404
  try {
51603
- stat2 = fs20.statSync(fullPath);
52405
+ stat2 = fs21.statSync(fullPath);
51604
52406
  } catch (e) {
51605
52407
  stats.fileErrors.push({
51606
52408
  path: fullPath,
@@ -51668,7 +52470,7 @@ var imports = tool({
51668
52470
  }
51669
52471
  try {
51670
52472
  const targetFile = path32.resolve(file3);
51671
- if (!fs20.existsSync(targetFile)) {
52473
+ if (!fs21.existsSync(targetFile)) {
51672
52474
  const errorResult = {
51673
52475
  error: `target file not found: ${file3}`,
51674
52476
  target: file3,
@@ -51678,7 +52480,7 @@ var imports = tool({
51678
52480
  };
51679
52481
  return JSON.stringify(errorResult, null, 2);
51680
52482
  }
51681
- const targetStat = fs20.statSync(targetFile);
52483
+ const targetStat = fs21.statSync(targetFile);
51682
52484
  if (!targetStat.isFile()) {
51683
52485
  const errorResult = {
51684
52486
  error: "target must be a file, not a directory",
@@ -51704,12 +52506,12 @@ var imports = tool({
51704
52506
  if (consumers.length >= MAX_CONSUMERS)
51705
52507
  break;
51706
52508
  try {
51707
- const stat2 = fs20.statSync(filePath);
52509
+ const stat2 = fs21.statSync(filePath);
51708
52510
  if (stat2.size > MAX_FILE_SIZE_BYTES4) {
51709
52511
  skippedFileCount++;
51710
52512
  continue;
51711
52513
  }
51712
- const buffer = fs20.readFileSync(filePath);
52514
+ const buffer = fs21.readFileSync(filePath);
51713
52515
  if (isBinaryFile2(filePath, buffer)) {
51714
52516
  skippedFileCount++;
51715
52517
  continue;
@@ -51778,7 +52580,7 @@ init_lint();
51778
52580
 
51779
52581
  // src/tools/phase-complete.ts
51780
52582
  init_dist();
51781
- import * as fs21 from "fs";
52583
+ import * as fs22 from "fs";
51782
52584
  import * as path33 from "path";
51783
52585
  init_manager();
51784
52586
  init_utils2();
@@ -51890,10 +52692,17 @@ async function executePhaseComplete(args2, workingDirectory) {
51890
52692
  let retroFound = false;
51891
52693
  let retroEntry = null;
51892
52694
  let invalidSchemaErrors = [];
52695
+ let loadedRetroTaskId = null;
52696
+ let loadedRetroBundle = null;
52697
+ const primaryRetroTaskId = `retro-${phase}`;
51893
52698
  if (retroResult.status === "found") {
51894
52699
  const validEntry = retroResult.bundle.entries?.find((entry) => isValidRetroEntry(entry, phase));
51895
- retroFound = !!validEntry;
51896
- retroEntry = validEntry;
52700
+ if (validEntry) {
52701
+ retroFound = true;
52702
+ retroEntry = validEntry;
52703
+ loadedRetroTaskId = primaryRetroTaskId;
52704
+ loadedRetroBundle = retroResult.bundle;
52705
+ }
51897
52706
  } else if (retroResult.status === "invalid_schema") {
51898
52707
  invalidSchemaErrors = retroResult.errors;
51899
52708
  }
@@ -51909,9 +52718,11 @@ async function executePhaseComplete(args2, workingDirectory) {
51909
52718
  continue;
51910
52719
  }
51911
52720
  const validEntry = bundleResult.bundle.entries?.find((entry) => isValidRetroEntry(entry, phase));
51912
- retroFound = !!validEntry;
51913
- if (retroFound) {
52721
+ if (validEntry) {
52722
+ retroFound = true;
51914
52723
  retroEntry = validEntry;
52724
+ loadedRetroTaskId = taskId;
52725
+ loadedRetroBundle = bundleResult.bundle;
51915
52726
  break;
51916
52727
  }
51917
52728
  }
@@ -51989,6 +52800,11 @@ async function executePhaseComplete(args2, workingDirectory) {
51989
52800
  }
51990
52801
  const agentsMissing = effectiveRequired.filter((req) => !crossSessionResult.agents.has(req));
51991
52802
  const warnings = [];
52803
+ const VALID_TASK_COMPLEXITY = ["trivial", "simple", "moderate", "complex"];
52804
+ const firstEntry = loadedRetroBundle?.entries?.[0];
52805
+ if (loadedRetroTaskId?.startsWith("retro-") && loadedRetroBundle?.schema_version === "1.0.0" && firstEntry?.task_complexity && VALID_TASK_COMPLEXITY.includes(firstEntry.task_complexity)) {
52806
+ warnings.push(`Retrospective data for phase ${phase} may have been automatically migrated to current schema format.`);
52807
+ }
51992
52808
  let success3 = true;
51993
52809
  let status = "success";
51994
52810
  const safeSummary = summary?.trim().slice(0, 500);
@@ -52016,7 +52832,7 @@ async function executePhaseComplete(args2, workingDirectory) {
52016
52832
  };
52017
52833
  try {
52018
52834
  const eventsPath = validateSwarmPath(dir, "events.jsonl");
52019
- fs21.appendFileSync(eventsPath, `${JSON.stringify(event)}
52835
+ fs22.appendFileSync(eventsPath, `${JSON.stringify(event)}
52020
52836
  `, "utf-8");
52021
52837
  } catch (writeError) {
52022
52838
  warnings.push(`Warning: failed to write phase complete event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
@@ -52074,7 +52890,7 @@ init_dist();
52074
52890
  init_discovery();
52075
52891
  init_utils();
52076
52892
  init_create_tool();
52077
- import * as fs22 from "fs";
52893
+ import * as fs23 from "fs";
52078
52894
  import * as path34 from "path";
52079
52895
  var MAX_OUTPUT_BYTES5 = 52428800;
52080
52896
  var AUDIT_TIMEOUT_MS = 120000;
@@ -52093,28 +52909,28 @@ function validateArgs3(args2) {
52093
52909
  function detectEcosystems() {
52094
52910
  const ecosystems = [];
52095
52911
  const cwd = process.cwd();
52096
- if (fs22.existsSync(path34.join(cwd, "package.json"))) {
52912
+ if (fs23.existsSync(path34.join(cwd, "package.json"))) {
52097
52913
  ecosystems.push("npm");
52098
52914
  }
52099
- if (fs22.existsSync(path34.join(cwd, "pyproject.toml")) || fs22.existsSync(path34.join(cwd, "requirements.txt"))) {
52915
+ if (fs23.existsSync(path34.join(cwd, "pyproject.toml")) || fs23.existsSync(path34.join(cwd, "requirements.txt"))) {
52100
52916
  ecosystems.push("pip");
52101
52917
  }
52102
- if (fs22.existsSync(path34.join(cwd, "Cargo.toml"))) {
52918
+ if (fs23.existsSync(path34.join(cwd, "Cargo.toml"))) {
52103
52919
  ecosystems.push("cargo");
52104
52920
  }
52105
- if (fs22.existsSync(path34.join(cwd, "go.mod"))) {
52921
+ if (fs23.existsSync(path34.join(cwd, "go.mod"))) {
52106
52922
  ecosystems.push("go");
52107
52923
  }
52108
52924
  try {
52109
- const files = fs22.readdirSync(cwd);
52925
+ const files = fs23.readdirSync(cwd);
52110
52926
  if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
52111
52927
  ecosystems.push("dotnet");
52112
52928
  }
52113
52929
  } catch {}
52114
- if (fs22.existsSync(path34.join(cwd, "Gemfile")) || fs22.existsSync(path34.join(cwd, "Gemfile.lock"))) {
52930
+ if (fs23.existsSync(path34.join(cwd, "Gemfile")) || fs23.existsSync(path34.join(cwd, "Gemfile.lock"))) {
52115
52931
  ecosystems.push("ruby");
52116
52932
  }
52117
- if (fs22.existsSync(path34.join(cwd, "pubspec.yaml"))) {
52933
+ if (fs23.existsSync(path34.join(cwd, "pubspec.yaml"))) {
52118
52934
  ecosystems.push("dart");
52119
52935
  }
52120
52936
  return ecosystems;
@@ -53169,7 +53985,7 @@ var SUPPORTED_PARSER_EXTENSIONS = new Set([
53169
53985
  ]);
53170
53986
  // src/tools/pre-check-batch.ts
53171
53987
  init_dist();
53172
- import * as fs25 from "fs";
53988
+ import * as fs26 from "fs";
53173
53989
  import * as path37 from "path";
53174
53990
 
53175
53991
  // node_modules/yocto-queue/index.js
@@ -53337,7 +54153,7 @@ init_lint();
53337
54153
  init_manager();
53338
54154
 
53339
54155
  // src/quality/metrics.ts
53340
- import * as fs23 from "fs";
54156
+ import * as fs24 from "fs";
53341
54157
  import * as path35 from "path";
53342
54158
  var MAX_FILE_SIZE_BYTES5 = 256 * 1024;
53343
54159
  var MIN_DUPLICATION_LINES = 10;
@@ -53376,11 +54192,11 @@ function estimateCyclomaticComplexity(content) {
53376
54192
  }
53377
54193
  function getComplexityForFile2(filePath) {
53378
54194
  try {
53379
- const stat2 = fs23.statSync(filePath);
54195
+ const stat2 = fs24.statSync(filePath);
53380
54196
  if (stat2.size > MAX_FILE_SIZE_BYTES5) {
53381
54197
  return null;
53382
54198
  }
53383
- const content = fs23.readFileSync(filePath, "utf-8");
54199
+ const content = fs24.readFileSync(filePath, "utf-8");
53384
54200
  return estimateCyclomaticComplexity(content);
53385
54201
  } catch {
53386
54202
  return null;
@@ -53391,7 +54207,7 @@ async function computeComplexityDelta(files, workingDir) {
53391
54207
  const analyzedFiles = [];
53392
54208
  for (const file3 of files) {
53393
54209
  const fullPath = path35.isAbsolute(file3) ? file3 : path35.join(workingDir, file3);
53394
- if (!fs23.existsSync(fullPath)) {
54210
+ if (!fs24.existsSync(fullPath)) {
53395
54211
  continue;
53396
54212
  }
53397
54213
  const complexity = getComplexityForFile2(fullPath);
@@ -53512,7 +54328,7 @@ function countGoExports(content) {
53512
54328
  }
53513
54329
  function getExportCountForFile(filePath) {
53514
54330
  try {
53515
- const content = fs23.readFileSync(filePath, "utf-8");
54331
+ const content = fs24.readFileSync(filePath, "utf-8");
53516
54332
  const ext = path35.extname(filePath).toLowerCase();
53517
54333
  switch (ext) {
53518
54334
  case ".ts":
@@ -53540,7 +54356,7 @@ async function computePublicApiDelta(files, workingDir) {
53540
54356
  const analyzedFiles = [];
53541
54357
  for (const file3 of files) {
53542
54358
  const fullPath = path35.isAbsolute(file3) ? file3 : path35.join(workingDir, file3);
53543
- if (!fs23.existsSync(fullPath)) {
54359
+ if (!fs24.existsSync(fullPath)) {
53544
54360
  continue;
53545
54361
  }
53546
54362
  const exports = getExportCountForFile(fullPath);
@@ -53574,15 +54390,15 @@ async function computeDuplicationRatio(files, workingDir) {
53574
54390
  const analyzedFiles = [];
53575
54391
  for (const file3 of files) {
53576
54392
  const fullPath = path35.isAbsolute(file3) ? file3 : path35.join(workingDir, file3);
53577
- if (!fs23.existsSync(fullPath)) {
54393
+ if (!fs24.existsSync(fullPath)) {
53578
54394
  continue;
53579
54395
  }
53580
54396
  try {
53581
- const stat2 = fs23.statSync(fullPath);
54397
+ const stat2 = fs24.statSync(fullPath);
53582
54398
  if (stat2.size > MAX_FILE_SIZE_BYTES5) {
53583
54399
  continue;
53584
54400
  }
53585
- const content = fs23.readFileSync(fullPath, "utf-8");
54401
+ const content = fs24.readFileSync(fullPath, "utf-8");
53586
54402
  const lines = content.split(`
53587
54403
  `).filter((line) => line.trim().length > 0);
53588
54404
  if (lines.length < MIN_DUPLICATION_LINES) {
@@ -53758,7 +54574,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
53758
54574
  let testLines = 0;
53759
54575
  let codeLines = 0;
53760
54576
  const srcDir = path35.join(workingDir, "src");
53761
- if (fs23.existsSync(srcDir)) {
54577
+ if (fs24.existsSync(srcDir)) {
53762
54578
  await scanDirectoryForLines(srcDir, enforceGlobs, excludeGlobs, false, (lines) => {
53763
54579
  codeLines += lines;
53764
54580
  });
@@ -53766,14 +54582,14 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
53766
54582
  const possibleSrcDirs = ["lib", "app", "source", "core"];
53767
54583
  for (const dir of possibleSrcDirs) {
53768
54584
  const dirPath = path35.join(workingDir, dir);
53769
- if (fs23.existsSync(dirPath)) {
54585
+ if (fs24.existsSync(dirPath)) {
53770
54586
  await scanDirectoryForLines(dirPath, enforceGlobs, excludeGlobs, false, (lines) => {
53771
54587
  codeLines += lines;
53772
54588
  });
53773
54589
  }
53774
54590
  }
53775
54591
  const testsDir = path35.join(workingDir, "tests");
53776
- if (fs23.existsSync(testsDir)) {
54592
+ if (fs24.existsSync(testsDir)) {
53777
54593
  await scanDirectoryForLines(testsDir, ["**"], ["node_modules", "dist"], true, (lines) => {
53778
54594
  testLines += lines;
53779
54595
  });
@@ -53781,7 +54597,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
53781
54597
  const possibleTestDirs = ["test", "__tests__", "specs"];
53782
54598
  for (const dir of possibleTestDirs) {
53783
54599
  const dirPath = path35.join(workingDir, dir);
53784
- if (fs23.existsSync(dirPath) && dirPath !== testsDir) {
54600
+ if (fs24.existsSync(dirPath) && dirPath !== testsDir) {
53785
54601
  await scanDirectoryForLines(dirPath, ["**"], ["node_modules", "dist"], true, (lines) => {
53786
54602
  testLines += lines;
53787
54603
  });
@@ -53793,7 +54609,7 @@ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
53793
54609
  }
53794
54610
  async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTestScan, callback) {
53795
54611
  try {
53796
- const entries = fs23.readdirSync(dirPath, { withFileTypes: true });
54612
+ const entries = fs24.readdirSync(dirPath, { withFileTypes: true });
53797
54613
  for (const entry of entries) {
53798
54614
  const fullPath = path35.join(dirPath, entry.name);
53799
54615
  if (entry.isDirectory()) {
@@ -53839,7 +54655,7 @@ async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTest
53839
54655
  continue;
53840
54656
  }
53841
54657
  try {
53842
- const content = fs23.readFileSync(fullPath, "utf-8");
54658
+ const content = fs24.readFileSync(fullPath, "utf-8");
53843
54659
  const lines = countCodeLines(content);
53844
54660
  callback(lines);
53845
54661
  } catch {}
@@ -54053,7 +54869,7 @@ async function qualityBudget(input, directory) {
54053
54869
  init_dist();
54054
54870
  init_manager();
54055
54871
  init_detector();
54056
- import * as fs24 from "fs";
54872
+ import * as fs25 from "fs";
54057
54873
  import * as path36 from "path";
54058
54874
  import { extname as extname9 } from "path";
54059
54875
 
@@ -54921,17 +55737,17 @@ var SEVERITY_ORDER = {
54921
55737
  };
54922
55738
  function shouldSkipFile(filePath) {
54923
55739
  try {
54924
- const stats = fs24.statSync(filePath);
55740
+ const stats = fs25.statSync(filePath);
54925
55741
  if (stats.size > MAX_FILE_SIZE_BYTES6) {
54926
55742
  return { skip: true, reason: "file too large" };
54927
55743
  }
54928
55744
  if (stats.size === 0) {
54929
55745
  return { skip: true, reason: "empty file" };
54930
55746
  }
54931
- const fd = fs24.openSync(filePath, "r");
55747
+ const fd = fs25.openSync(filePath, "r");
54932
55748
  const buffer = Buffer.alloc(8192);
54933
- const bytesRead = fs24.readSync(fd, buffer, 0, 8192, 0);
54934
- fs24.closeSync(fd);
55749
+ const bytesRead = fs25.readSync(fd, buffer, 0, 8192, 0);
55750
+ fs25.closeSync(fd);
54935
55751
  if (bytesRead > 0) {
54936
55752
  let nullCount = 0;
54937
55753
  for (let i2 = 0;i2 < bytesRead; i2++) {
@@ -54970,7 +55786,7 @@ function countBySeverity(findings) {
54970
55786
  }
54971
55787
  function scanFileWithTierA(filePath, language) {
54972
55788
  try {
54973
- const content = fs24.readFileSync(filePath, "utf-8");
55789
+ const content = fs25.readFileSync(filePath, "utf-8");
54974
55790
  const findings = executeRulesSync(filePath, content, language);
54975
55791
  return findings.map((f) => ({
54976
55792
  rule_id: f.rule_id,
@@ -55018,7 +55834,7 @@ async function sastScan(input, directory, config3) {
55018
55834
  continue;
55019
55835
  }
55020
55836
  const resolvedPath = path36.isAbsolute(filePath) ? filePath : path36.resolve(directory, filePath);
55021
- if (!fs24.existsSync(resolvedPath)) {
55837
+ if (!fs25.existsSync(resolvedPath)) {
55022
55838
  _filesSkipped++;
55023
55839
  continue;
55024
55840
  }
@@ -55234,7 +56050,7 @@ function validatePath(inputPath, baseDir, workspaceDir) {
55234
56050
  }
55235
56051
  return null;
55236
56052
  }
55237
- function validateDirectory(dir, workspaceDir) {
56053
+ function validateDirectory3(dir, workspaceDir) {
55238
56054
  if (!dir || dir.length === 0) {
55239
56055
  return "directory is required";
55240
56056
  }
@@ -55476,7 +56292,7 @@ async function runSecretscanWithFiles(files, directory) {
55476
56292
  }
55477
56293
  let stat2;
55478
56294
  try {
55479
- stat2 = fs25.statSync(file3);
56295
+ stat2 = fs26.statSync(file3);
55480
56296
  } catch {
55481
56297
  skippedFiles++;
55482
56298
  continue;
@@ -55487,7 +56303,7 @@ async function runSecretscanWithFiles(files, directory) {
55487
56303
  }
55488
56304
  let content;
55489
56305
  try {
55490
- const buffer = fs25.readFileSync(file3);
56306
+ const buffer = fs26.readFileSync(file3);
55491
56307
  if (buffer.includes(0)) {
55492
56308
  skippedFiles++;
55493
56309
  continue;
@@ -55590,7 +56406,7 @@ async function runQualityBudgetWrapped(changedFiles, directory, _config) {
55590
56406
  async function runPreCheckBatch(input, workspaceDir) {
55591
56407
  const effectiveWorkspaceDir = workspaceDir || input.directory || process.cwd();
55592
56408
  const { files, directory, sast_threshold = "medium", config: config3 } = input;
55593
- const dirError = validateDirectory(directory, effectiveWorkspaceDir);
56409
+ const dirError = validateDirectory3(directory, effectiveWorkspaceDir);
55594
56410
  if (dirError) {
55595
56411
  warn(`pre_check_batch: Invalid directory: ${dirError}`);
55596
56412
  return {
@@ -55754,7 +56570,7 @@ var pre_check_batch = createSwarmTool({
55754
56570
  }
55755
56571
  const resolvedDirectory = path37.resolve(typedArgs.directory);
55756
56572
  const workspaceAnchor = resolvedDirectory;
55757
- const dirError = validateDirectory(resolvedDirectory, workspaceAnchor);
56573
+ const dirError = validateDirectory3(resolvedDirectory, workspaceAnchor);
55758
56574
  if (dirError) {
55759
56575
  const errorResult = {
55760
56576
  gates_passed: false,
@@ -55944,7 +56760,7 @@ var save_plan = createSwarmTool({
55944
56760
  // src/tools/sbom-generate.ts
55945
56761
  init_dist();
55946
56762
  init_manager();
55947
- import * as fs26 from "fs";
56763
+ import * as fs27 from "fs";
55948
56764
  import * as path39 from "path";
55949
56765
 
55950
56766
  // src/sbom/detectors/dart.ts
@@ -56791,7 +57607,7 @@ function findManifestFiles(rootDir) {
56791
57607
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
56792
57608
  function searchDir(dir) {
56793
57609
  try {
56794
- const entries = fs26.readdirSync(dir, { withFileTypes: true });
57610
+ const entries = fs27.readdirSync(dir, { withFileTypes: true });
56795
57611
  for (const entry of entries) {
56796
57612
  const fullPath = path39.join(dir, entry.name);
56797
57613
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
@@ -56820,7 +57636,7 @@ function findManifestFilesInDirs(directories, workingDir) {
56820
57636
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
56821
57637
  for (const dir of directories) {
56822
57638
  try {
56823
- const entries = fs26.readdirSync(dir, { withFileTypes: true });
57639
+ const entries = fs27.readdirSync(dir, { withFileTypes: true });
56824
57640
  for (const entry of entries) {
56825
57641
  const fullPath = path39.join(dir, entry.name);
56826
57642
  if (entry.isFile()) {
@@ -56858,7 +57674,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
56858
57674
  }
56859
57675
  function ensureOutputDir(outputDir) {
56860
57676
  try {
56861
- fs26.mkdirSync(outputDir, { recursive: true });
57677
+ fs27.mkdirSync(outputDir, { recursive: true });
56862
57678
  } catch (error93) {
56863
57679
  if (!error93 || error93.code !== "EEXIST") {
56864
57680
  throw error93;
@@ -56951,10 +57767,10 @@ var sbom_generate = createSwarmTool({
56951
57767
  for (const manifestFile of manifestFiles) {
56952
57768
  try {
56953
57769
  const fullPath = path39.isAbsolute(manifestFile) ? manifestFile : path39.join(workingDir, manifestFile);
56954
- if (!fs26.existsSync(fullPath)) {
57770
+ if (!fs27.existsSync(fullPath)) {
56955
57771
  continue;
56956
57772
  }
56957
- const content = fs26.readFileSync(fullPath, "utf-8");
57773
+ const content = fs27.readFileSync(fullPath, "utf-8");
56958
57774
  const components = detectComponents(manifestFile, content);
56959
57775
  processedFiles.push(manifestFile);
56960
57776
  if (components.length > 0) {
@@ -56968,7 +57784,7 @@ var sbom_generate = createSwarmTool({
56968
57784
  const bomJson = serializeCycloneDX(bom);
56969
57785
  const filename = generateSbomFilename();
56970
57786
  const outputPath = path39.join(outputDir, filename);
56971
- fs26.writeFileSync(outputPath, bomJson, "utf-8");
57787
+ fs27.writeFileSync(outputPath, bomJson, "utf-8");
56972
57788
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
56973
57789
  try {
56974
57790
  const timestamp = new Date().toISOString();
@@ -57010,7 +57826,7 @@ var sbom_generate = createSwarmTool({
57010
57826
  // src/tools/schema-drift.ts
57011
57827
  init_dist();
57012
57828
  init_create_tool();
57013
- import * as fs27 from "fs";
57829
+ import * as fs28 from "fs";
57014
57830
  import * as path40 from "path";
57015
57831
  var SPEC_CANDIDATES = [
57016
57832
  "openapi.json",
@@ -57052,19 +57868,19 @@ function discoverSpecFile(cwd, specFileArg) {
57052
57868
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
57053
57869
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
57054
57870
  }
57055
- const stats = fs27.statSync(resolvedPath);
57871
+ const stats = fs28.statSync(resolvedPath);
57056
57872
  if (stats.size > MAX_SPEC_SIZE) {
57057
57873
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
57058
57874
  }
57059
- if (!fs27.existsSync(resolvedPath)) {
57875
+ if (!fs28.existsSync(resolvedPath)) {
57060
57876
  throw new Error(`Spec file not found: ${resolvedPath}`);
57061
57877
  }
57062
57878
  return resolvedPath;
57063
57879
  }
57064
57880
  for (const candidate of SPEC_CANDIDATES) {
57065
57881
  const candidatePath = path40.resolve(cwd, candidate);
57066
- if (fs27.existsSync(candidatePath)) {
57067
- const stats = fs27.statSync(candidatePath);
57882
+ if (fs28.existsSync(candidatePath)) {
57883
+ const stats = fs28.statSync(candidatePath);
57068
57884
  if (stats.size <= MAX_SPEC_SIZE) {
57069
57885
  return candidatePath;
57070
57886
  }
@@ -57073,7 +57889,7 @@ function discoverSpecFile(cwd, specFileArg) {
57073
57889
  return null;
57074
57890
  }
57075
57891
  function parseSpec(specFile) {
57076
- const content = fs27.readFileSync(specFile, "utf-8");
57892
+ const content = fs28.readFileSync(specFile, "utf-8");
57077
57893
  const ext = path40.extname(specFile).toLowerCase();
57078
57894
  if (ext === ".json") {
57079
57895
  return parseJsonSpec(content);
@@ -57140,7 +57956,7 @@ function extractRoutes(cwd) {
57140
57956
  function walkDir(dir) {
57141
57957
  let entries;
57142
57958
  try {
57143
- entries = fs27.readdirSync(dir, { withFileTypes: true });
57959
+ entries = fs28.readdirSync(dir, { withFileTypes: true });
57144
57960
  } catch {
57145
57961
  return;
57146
57962
  }
@@ -57173,7 +57989,7 @@ function extractRoutes(cwd) {
57173
57989
  }
57174
57990
  function extractRoutesFromFile(filePath) {
57175
57991
  const routes = [];
57176
- const content = fs27.readFileSync(filePath, "utf-8");
57992
+ const content = fs28.readFileSync(filePath, "utf-8");
57177
57993
  const lines = content.split(/\r?\n/);
57178
57994
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
57179
57995
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -57324,7 +58140,7 @@ init_secretscan();
57324
58140
  // src/tools/symbols.ts
57325
58141
  init_tool();
57326
58142
  init_create_tool();
57327
- import * as fs28 from "fs";
58143
+ import * as fs29 from "fs";
57328
58144
  import * as path41 from "path";
57329
58145
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
57330
58146
  var WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
@@ -57355,8 +58171,8 @@ function containsWindowsAttacks(str) {
57355
58171
  function isPathInWorkspace(filePath, workspace) {
57356
58172
  try {
57357
58173
  const resolvedPath = path41.resolve(workspace, filePath);
57358
- const realWorkspace = fs28.realpathSync(workspace);
57359
- const realResolvedPath = fs28.realpathSync(resolvedPath);
58174
+ const realWorkspace = fs29.realpathSync(workspace);
58175
+ const realResolvedPath = fs29.realpathSync(resolvedPath);
57360
58176
  const relativePath = path41.relative(realWorkspace, realResolvedPath);
57361
58177
  if (relativePath.startsWith("..") || path41.isAbsolute(relativePath)) {
57362
58178
  return false;
@@ -57376,11 +58192,11 @@ function extractTSSymbols(filePath, cwd) {
57376
58192
  }
57377
58193
  let content;
57378
58194
  try {
57379
- const stats = fs28.statSync(fullPath);
58195
+ const stats = fs29.statSync(fullPath);
57380
58196
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
57381
58197
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
57382
58198
  }
57383
- content = fs28.readFileSync(fullPath, "utf-8");
58199
+ content = fs29.readFileSync(fullPath, "utf-8");
57384
58200
  } catch {
57385
58201
  return [];
57386
58202
  }
@@ -57528,11 +58344,11 @@ function extractPythonSymbols(filePath, cwd) {
57528
58344
  }
57529
58345
  let content;
57530
58346
  try {
57531
- const stats = fs28.statSync(fullPath);
58347
+ const stats = fs29.statSync(fullPath);
57532
58348
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
57533
58349
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
57534
58350
  }
57535
- content = fs28.readFileSync(fullPath, "utf-8");
58351
+ content = fs29.readFileSync(fullPath, "utf-8");
57536
58352
  } catch {
57537
58353
  return [];
57538
58354
  }
@@ -57672,11 +58488,133 @@ var MAX_FILE_SIZE2 = 5 * 1024 * 1024;
57672
58488
  // src/tools/index.ts
57673
58489
  init_test_runner();
57674
58490
 
58491
+ // src/tools/update-task-status.ts
58492
+ init_tool();
58493
+ init_manager2();
58494
+ init_create_tool();
58495
+ import * as fs30 from "fs";
58496
+ import * as path42 from "path";
58497
+ var VALID_STATUSES = [
58498
+ "pending",
58499
+ "in_progress",
58500
+ "completed",
58501
+ "blocked"
58502
+ ];
58503
+ function validateStatus(status) {
58504
+ if (!VALID_STATUSES.includes(status)) {
58505
+ return `Invalid status "${status}". Must be one of: ${VALID_STATUSES.join(", ")}`;
58506
+ }
58507
+ return;
58508
+ }
58509
+ function validateTaskId(taskId) {
58510
+ const taskIdPattern = /^\d+\.\d+(\.\d+)*$/;
58511
+ if (!taskIdPattern.test(taskId)) {
58512
+ return `Invalid task_id "${taskId}". Must match pattern N.M or N.M.P (e.g., "1.1", "1.2.3")`;
58513
+ }
58514
+ return;
58515
+ }
58516
+ async function executeUpdateTaskStatus(args2, fallbackDir) {
58517
+ const statusError = validateStatus(args2.status);
58518
+ if (statusError) {
58519
+ return {
58520
+ success: false,
58521
+ message: "Validation failed",
58522
+ errors: [statusError]
58523
+ };
58524
+ }
58525
+ const taskIdError = validateTaskId(args2.task_id);
58526
+ if (taskIdError) {
58527
+ return {
58528
+ success: false,
58529
+ message: "Validation failed",
58530
+ errors: [taskIdError]
58531
+ };
58532
+ }
58533
+ let normalizedDir;
58534
+ if (args2.working_directory != null) {
58535
+ if (args2.working_directory.includes("\x00")) {
58536
+ return {
58537
+ success: false,
58538
+ message: "Invalid working_directory: null bytes are not allowed"
58539
+ };
58540
+ }
58541
+ if (process.platform === "win32") {
58542
+ const devicePathPattern = /^\\\\|^(NUL|CON|AUX|COM[1-9]|LPT[1-9])(\..*)?$/i;
58543
+ if (devicePathPattern.test(args2.working_directory)) {
58544
+ return {
58545
+ success: false,
58546
+ message: "Invalid working_directory: Windows device paths are not allowed"
58547
+ };
58548
+ }
58549
+ }
58550
+ normalizedDir = path42.normalize(args2.working_directory);
58551
+ const pathParts = normalizedDir.split(path42.sep);
58552
+ if (pathParts.includes("..")) {
58553
+ return {
58554
+ success: false,
58555
+ message: "Invalid working_directory: path traversal sequences (..) are not allowed",
58556
+ errors: [
58557
+ "Invalid working_directory: path traversal sequences (..) are not allowed"
58558
+ ]
58559
+ };
58560
+ }
58561
+ const resolvedDir = path42.resolve(normalizedDir);
58562
+ try {
58563
+ const realPath = fs30.realpathSync(resolvedDir);
58564
+ const planPath = path42.join(realPath, ".swarm", "plan.json");
58565
+ if (!fs30.existsSync(planPath)) {
58566
+ return {
58567
+ success: false,
58568
+ message: `Invalid working_directory: plan not found in "${realPath}"`,
58569
+ errors: [
58570
+ `Invalid working_directory: plan not found in "${realPath}"`
58571
+ ]
58572
+ };
58573
+ }
58574
+ } catch {
58575
+ return {
58576
+ success: false,
58577
+ message: `Invalid working_directory: path "${resolvedDir}" does not exist or is inaccessible`,
58578
+ errors: [
58579
+ `Invalid working_directory: path "${resolvedDir}" does not exist or is inaccessible`
58580
+ ]
58581
+ };
58582
+ }
58583
+ }
58584
+ const directory = normalizedDir ?? fallbackDir ?? process.cwd();
58585
+ try {
58586
+ const updatedPlan = await updateTaskStatus(directory, args2.task_id, args2.status);
58587
+ return {
58588
+ success: true,
58589
+ message: "Task status updated successfully",
58590
+ task_id: args2.task_id,
58591
+ new_status: args2.status,
58592
+ current_phase: updatedPlan.current_phase
58593
+ };
58594
+ } catch (error93) {
58595
+ return {
58596
+ success: false,
58597
+ message: "Failed to update task status",
58598
+ errors: [String(error93)]
58599
+ };
58600
+ }
58601
+ }
58602
+ var update_task_status = createSwarmTool({
58603
+ description: "Update the status of a specific task in the implementation plan. " + "Task status can be one of: pending, in_progress, completed, blocked.",
58604
+ args: {
58605
+ task_id: tool.schema.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format").describe('Task ID in N.M format, e.g. "1.1", "1.2.3"'),
58606
+ status: tool.schema.enum(["pending", "in_progress", "completed", "blocked"]).describe("New status for the task: pending, in_progress, completed, or blocked"),
58607
+ working_directory: tool.schema.string().optional().describe("Working directory where the plan is located")
58608
+ },
58609
+ execute: async (args2, _directory) => {
58610
+ return JSON.stringify(await executeUpdateTaskStatus(args2, _directory), null, 2);
58611
+ }
58612
+ });
57675
58613
  // src/tools/todo-extract.ts
57676
58614
  init_dist();
57677
58615
  init_create_tool();
57678
- import * as fs29 from "fs";
57679
- import * as path42 from "path";
58616
+ import * as fs31 from "fs";
58617
+ import * as path43 from "path";
57680
58618
  var MAX_TEXT_LENGTH = 200;
57681
58619
  var MAX_FILE_SIZE_BYTES8 = 1024 * 1024;
57682
58620
  var SUPPORTED_EXTENSIONS2 = new Set([
@@ -57747,9 +58685,9 @@ function validatePathsInput(paths, cwd) {
57747
58685
  return { error: "paths contains path traversal", resolvedPath: null };
57748
58686
  }
57749
58687
  try {
57750
- const resolvedPath = path42.resolve(paths);
57751
- const normalizedCwd = path42.resolve(cwd);
57752
- const normalizedResolved = path42.resolve(resolvedPath);
58688
+ const resolvedPath = path43.resolve(paths);
58689
+ const normalizedCwd = path43.resolve(cwd);
58690
+ const normalizedResolved = path43.resolve(resolvedPath);
57753
58691
  if (!normalizedResolved.startsWith(normalizedCwd)) {
57754
58692
  return {
57755
58693
  error: "paths must be within the current working directory",
@@ -57765,13 +58703,13 @@ function validatePathsInput(paths, cwd) {
57765
58703
  }
57766
58704
  }
57767
58705
  function isSupportedExtension(filePath) {
57768
- const ext = path42.extname(filePath).toLowerCase();
58706
+ const ext = path43.extname(filePath).toLowerCase();
57769
58707
  return SUPPORTED_EXTENSIONS2.has(ext);
57770
58708
  }
57771
58709
  function findSourceFiles3(dir, files = []) {
57772
58710
  let entries;
57773
58711
  try {
57774
- entries = fs29.readdirSync(dir);
58712
+ entries = fs31.readdirSync(dir);
57775
58713
  } catch {
57776
58714
  return files;
57777
58715
  }
@@ -57780,10 +58718,10 @@ function findSourceFiles3(dir, files = []) {
57780
58718
  if (SKIP_DIRECTORIES3.has(entry)) {
57781
58719
  continue;
57782
58720
  }
57783
- const fullPath = path42.join(dir, entry);
58721
+ const fullPath = path43.join(dir, entry);
57784
58722
  let stat2;
57785
58723
  try {
57786
- stat2 = fs29.statSync(fullPath);
58724
+ stat2 = fs31.statSync(fullPath);
57787
58725
  } catch {
57788
58726
  continue;
57789
58727
  }
@@ -57876,7 +58814,7 @@ var todo_extract = createSwarmTool({
57876
58814
  return JSON.stringify(errorResult, null, 2);
57877
58815
  }
57878
58816
  const scanPath = resolvedPath;
57879
- if (!fs29.existsSync(scanPath)) {
58817
+ if (!fs31.existsSync(scanPath)) {
57880
58818
  const errorResult = {
57881
58819
  error: `path not found: ${pathsInput}`,
57882
58820
  total: 0,
@@ -57886,13 +58824,13 @@ var todo_extract = createSwarmTool({
57886
58824
  return JSON.stringify(errorResult, null, 2);
57887
58825
  }
57888
58826
  const filesToScan = [];
57889
- const stat2 = fs29.statSync(scanPath);
58827
+ const stat2 = fs31.statSync(scanPath);
57890
58828
  if (stat2.isFile()) {
57891
58829
  if (isSupportedExtension(scanPath)) {
57892
58830
  filesToScan.push(scanPath);
57893
58831
  } else {
57894
58832
  const errorResult = {
57895
- error: `unsupported file extension: ${path42.extname(scanPath)}`,
58833
+ error: `unsupported file extension: ${path43.extname(scanPath)}`,
57896
58834
  total: 0,
57897
58835
  byPriority: { high: 0, medium: 0, low: 0 },
57898
58836
  entries: []
@@ -57905,11 +58843,11 @@ var todo_extract = createSwarmTool({
57905
58843
  const allEntries = [];
57906
58844
  for (const filePath of filesToScan) {
57907
58845
  try {
57908
- const fileStat = fs29.statSync(filePath);
58846
+ const fileStat = fs31.statSync(filePath);
57909
58847
  if (fileStat.size > MAX_FILE_SIZE_BYTES8) {
57910
58848
  continue;
57911
58849
  }
57912
- const content = fs29.readFileSync(filePath, "utf-8");
58850
+ const content = fs31.readFileSync(filePath, "utf-8");
57913
58851
  const entries = parseTodoComments(content, filePath, tagsSet);
57914
58852
  allEntries.push(...entries);
57915
58853
  } catch {}
@@ -58017,7 +58955,7 @@ var OpenCodeSwarm = async (ctx) => {
58017
58955
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
58018
58956
  preflightTriggerManager = new PTM(automationConfig);
58019
58957
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
58020
- const swarmDir = path43.resolve(ctx.directory, ".swarm");
58958
+ const swarmDir = path44.resolve(ctx.directory, ".swarm");
58021
58959
  statusArtifact = new ASA(swarmDir);
58022
58960
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
58023
58961
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -58129,7 +59067,9 @@ var OpenCodeSwarm = async (ctx) => {
58129
59067
  secretscan,
58130
59068
  symbols,
58131
59069
  test_runner,
58132
- todo_extract
59070
+ todo_extract,
59071
+ update_task_status,
59072
+ write_retro
58133
59073
  },
58134
59074
  config: async (opencodeConfig) => {
58135
59075
  if (!opencodeConfig.agent) {
@@ -58167,6 +59107,10 @@ var OpenCodeSwarm = async (ctx) => {
58167
59107
  template: "/swarm evidence $ARGUMENTS",
58168
59108
  description: "View evidence bundles and summaries"
58169
59109
  },
59110
+ "swarm-handoff": {
59111
+ template: "/swarm handoff",
59112
+ description: "Prepare handoff brief for switching models mid-task"
59113
+ },
58170
59114
  "swarm-archive": {
58171
59115
  template: "/swarm archive",
58172
59116
  description: "Archive old evidence bundles"