opencode-swarm 6.22.21 → 6.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -33333,8 +33333,9 @@ function isHighEntropyString(str) {
33333
33333
  function containsPathTraversal(str) {
33334
33334
  if (/\.\.[/\\]/.test(str))
33335
33335
  return true;
33336
- const normalized = path12.normalize(str);
33337
- if (/\.\.[/\\]/.test(normalized))
33336
+ if (/[/\\]\.\.$/.test(str) || str === "..")
33337
+ return true;
33338
+ if (/\.\.[/\\]/.test(path12.normalize(str.replace(/\*/g, "x"))))
33338
33339
  return true;
33339
33340
  if (str.includes("%2e%2e") || str.includes("%2E%2E"))
33340
33341
  return true;
@@ -33342,6 +33343,58 @@ function containsPathTraversal(str) {
33342
33343
  return true;
33343
33344
  return false;
33344
33345
  }
33346
+ function validateExcludePattern(exc) {
33347
+ if (exc.length === 0)
33348
+ return null;
33349
+ if (exc.length > MAX_FILE_PATH_LENGTH) {
33350
+ return `invalid exclude path: exceeds maximum length of ${MAX_FILE_PATH_LENGTH}`;
33351
+ }
33352
+ if (containsControlChars(exc)) {
33353
+ return "invalid exclude path: contains path traversal or control characters";
33354
+ }
33355
+ if (containsPathTraversal(exc)) {
33356
+ return "invalid exclude path: contains path traversal or control characters";
33357
+ }
33358
+ if (exc.startsWith("!")) {
33359
+ return "invalid exclude path: negation patterns are not supported";
33360
+ }
33361
+ if (exc.startsWith("/") || exc.startsWith("\\")) {
33362
+ return "invalid exclude path: absolute paths are not supported";
33363
+ }
33364
+ return null;
33365
+ }
33366
+ function isGlobOrPathPattern(pattern) {
33367
+ return pattern.includes("/") || pattern.includes("\\") || /[*?[\]{}]/.test(pattern);
33368
+ }
33369
+ function loadSecretScanIgnore(scanDir) {
33370
+ const ignorePath = path12.join(scanDir, ".secretscanignore");
33371
+ try {
33372
+ if (!fs4.existsSync(ignorePath))
33373
+ return [];
33374
+ const content = fs4.readFileSync(ignorePath, "utf8");
33375
+ const patterns = [];
33376
+ for (const rawLine of content.split(/\r?\n/)) {
33377
+ const line = rawLine.trim();
33378
+ if (!line || line.startsWith("#"))
33379
+ continue;
33380
+ if (validateExcludePattern(line) === null) {
33381
+ patterns.push(line);
33382
+ }
33383
+ }
33384
+ return patterns;
33385
+ } catch {
33386
+ return [];
33387
+ }
33388
+ }
33389
+ function isExcluded(entry, relPath, exactNames, globPatterns) {
33390
+ if (exactNames.has(entry))
33391
+ return true;
33392
+ for (const pattern of globPatterns) {
33393
+ if (path12.matchesGlob(relPath, pattern))
33394
+ return true;
33395
+ }
33396
+ return false;
33397
+ }
33345
33398
  function containsControlChars(str) {
33346
33399
  return /[\0\r]/.test(str);
33347
33400
  }
@@ -33502,7 +33555,7 @@ function isPathWithinScope(realPath, scanDir) {
33502
33555
  const resolvedRealPath = path12.resolve(realPath);
33503
33556
  return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path12.sep) || resolvedRealPath.startsWith(`${resolvedScanDir}/`) || resolvedRealPath.startsWith(`${resolvedScanDir}\\`);
33504
33557
  }
33505
- function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33558
+ function findScannableFiles(dir, excludeExact, excludeGlobs, scanDir, visited, stats = {
33506
33559
  skippedDirs: 0,
33507
33560
  skippedFiles: 0,
33508
33561
  fileErrors: 0,
@@ -33526,11 +33579,12 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33526
33579
  return a.localeCompare(b);
33527
33580
  });
33528
33581
  for (const entry of entries) {
33529
- if (excludeDirs.has(entry)) {
33582
+ const fullPath = path12.join(dir, entry);
33583
+ const relPath = path12.relative(scanDir, fullPath).replace(/\\/g, "/");
33584
+ if (isExcluded(entry, relPath, excludeExact, excludeGlobs)) {
33530
33585
  stats.skippedDirs++;
33531
33586
  continue;
33532
33587
  }
33533
- const fullPath = path12.join(dir, entry);
33534
33588
  let lstat;
33535
33589
  try {
33536
33590
  lstat = fs4.lstatSync(fullPath);
@@ -33558,7 +33612,7 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33558
33612
  stats.symlinkSkipped++;
33559
33613
  continue;
33560
33614
  }
33561
- const subFiles = findScannableFiles(fullPath, excludeDirs, scanDir, visited, stats);
33615
+ const subFiles = findScannableFiles(fullPath, excludeExact, excludeGlobs, scanDir, visited, stats);
33562
33616
  files.push(...subFiles);
33563
33617
  } else if (lstat.isFile()) {
33564
33618
  const ext = path12.extname(fullPath).toLowerCase();
@@ -33572,10 +33626,10 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33572
33626
  return files;
33573
33627
  }
33574
33628
  var secretscan = tool({
33575
- description: "Scan directory for potential secrets (API keys, tokens, passwords) using regex patterns and entropy heuristics. Returns metadata-only findings with redacted previews - NEVER returns raw secrets. Excludes common directories (node_modules, .git, dist, etc.) by default.",
33629
+ description: "Scan directory for potential secrets (API keys, tokens, passwords) using regex patterns and entropy heuristics. Returns metadata-only findings with redacted previews - NEVER returns raw secrets. Excludes common directories (node_modules, .git, dist, etc.) by default. Supports glob patterns (e.g. **/.svelte-kit/**, **/*.test.ts) and reads .secretscanignore at the scan root.",
33576
33630
  args: {
33577
33631
  directory: tool.schema.string().describe('Directory to scan for secrets (e.g., "." or "./src")'),
33578
- exclude: tool.schema.array(tool.schema.string()).optional().describe("Additional directories to exclude (added to default exclusions like node_modules, .git, dist)")
33632
+ exclude: tool.schema.array(tool.schema.string()).optional().describe("Patterns to exclude: plain directory names (e.g. node_modules), relative paths, or globs (e.g. **/.svelte-kit/**, **/*.test.ts). Added to default exclusions.")
33579
33633
  },
33580
33634
  async execute(args, _context) {
33581
33635
  let directory;
@@ -33611,20 +33665,10 @@ var secretscan = tool({
33611
33665
  }
33612
33666
  if (exclude) {
33613
33667
  for (const exc of exclude) {
33614
- if (exc.length > MAX_FILE_PATH_LENGTH) {
33615
- const errorResult = {
33616
- error: `invalid exclude path: exceeds maximum length of ${MAX_FILE_PATH_LENGTH}`,
33617
- scan_dir: directory,
33618
- findings: [],
33619
- count: 0,
33620
- files_scanned: 0,
33621
- skipped_files: 0
33622
- };
33623
- return JSON.stringify(errorResult, null, 2);
33624
- }
33625
- if (containsPathTraversal(exc) || containsControlChars(exc)) {
33668
+ const err = validateExcludePattern(exc);
33669
+ if (err) {
33626
33670
  const errorResult = {
33627
- error: `invalid exclude path: contains path traversal or control characters`,
33671
+ error: err,
33628
33672
  scan_dir: directory,
33629
33673
  findings: [],
33630
33674
  count: 0,
@@ -33660,10 +33704,20 @@ var secretscan = tool({
33660
33704
  };
33661
33705
  return JSON.stringify(errorResult, null, 2);
33662
33706
  }
33663
- const excludeDirs = new Set(DEFAULT_EXCLUDE_DIRS);
33664
- if (exclude) {
33665
- for (const exc of exclude) {
33666
- excludeDirs.add(exc);
33707
+ const excludeExact = new Set(DEFAULT_EXCLUDE_DIRS);
33708
+ const excludeGlobs = [];
33709
+ const ignoreFilePatterns = loadSecretScanIgnore(scanDir);
33710
+ const allUserPatterns = [
33711
+ ...exclude ?? [],
33712
+ ...ignoreFilePatterns
33713
+ ];
33714
+ for (const exc of allUserPatterns) {
33715
+ if (exc.length === 0)
33716
+ continue;
33717
+ if (isGlobOrPathPattern(exc)) {
33718
+ excludeGlobs.push(exc);
33719
+ } else {
33720
+ excludeExact.add(exc);
33667
33721
  }
33668
33722
  }
33669
33723
  const stats = {
@@ -33673,7 +33727,7 @@ var secretscan = tool({
33673
33727
  symlinkSkipped: 0
33674
33728
  };
33675
33729
  const visited = new Set;
33676
- const files = findScannableFiles(scanDir, excludeDirs, scanDir, visited, stats);
33730
+ const files = findScannableFiles(scanDir, excludeExact, excludeGlobs, scanDir, visited, stats);
33677
33731
  files.sort((a, b) => {
33678
33732
  const aLower = a.toLowerCase();
33679
33733
  const bLower = b.toLowerCase();
package/dist/index.js CHANGED
@@ -33574,8 +33574,9 @@ function isHighEntropyString(str) {
33574
33574
  function containsPathTraversal(str) {
33575
33575
  if (/\.\.[/\\]/.test(str))
33576
33576
  return true;
33577
- const normalized = path21.normalize(str);
33578
- if (/\.\.[/\\]/.test(normalized))
33577
+ if (/[/\\]\.\.$/.test(str) || str === "..")
33578
+ return true;
33579
+ if (/\.\.[/\\]/.test(path21.normalize(str.replace(/\*/g, "x"))))
33579
33580
  return true;
33580
33581
  if (str.includes("%2e%2e") || str.includes("%2E%2E"))
33581
33582
  return true;
@@ -33583,6 +33584,58 @@ function containsPathTraversal(str) {
33583
33584
  return true;
33584
33585
  return false;
33585
33586
  }
33587
+ function validateExcludePattern(exc) {
33588
+ if (exc.length === 0)
33589
+ return null;
33590
+ if (exc.length > MAX_FILE_PATH_LENGTH) {
33591
+ return `invalid exclude path: exceeds maximum length of ${MAX_FILE_PATH_LENGTH}`;
33592
+ }
33593
+ if (containsControlChars(exc)) {
33594
+ return "invalid exclude path: contains path traversal or control characters";
33595
+ }
33596
+ if (containsPathTraversal(exc)) {
33597
+ return "invalid exclude path: contains path traversal or control characters";
33598
+ }
33599
+ if (exc.startsWith("!")) {
33600
+ return "invalid exclude path: negation patterns are not supported";
33601
+ }
33602
+ if (exc.startsWith("/") || exc.startsWith("\\")) {
33603
+ return "invalid exclude path: absolute paths are not supported";
33604
+ }
33605
+ return null;
33606
+ }
33607
+ function isGlobOrPathPattern(pattern) {
33608
+ return pattern.includes("/") || pattern.includes("\\") || /[*?[\]{}]/.test(pattern);
33609
+ }
33610
+ function loadSecretScanIgnore(scanDir) {
33611
+ const ignorePath = path21.join(scanDir, ".secretscanignore");
33612
+ try {
33613
+ if (!fs9.existsSync(ignorePath))
33614
+ return [];
33615
+ const content = fs9.readFileSync(ignorePath, "utf8");
33616
+ const patterns = [];
33617
+ for (const rawLine of content.split(/\r?\n/)) {
33618
+ const line = rawLine.trim();
33619
+ if (!line || line.startsWith("#"))
33620
+ continue;
33621
+ if (validateExcludePattern(line) === null) {
33622
+ patterns.push(line);
33623
+ }
33624
+ }
33625
+ return patterns;
33626
+ } catch {
33627
+ return [];
33628
+ }
33629
+ }
33630
+ function isExcluded(entry, relPath, exactNames, globPatterns) {
33631
+ if (exactNames.has(entry))
33632
+ return true;
33633
+ for (const pattern of globPatterns) {
33634
+ if (path21.matchesGlob(relPath, pattern))
33635
+ return true;
33636
+ }
33637
+ return false;
33638
+ }
33586
33639
  function containsControlChars(str) {
33587
33640
  return /[\0\r]/.test(str);
33588
33641
  }
@@ -33742,7 +33795,7 @@ function isPathWithinScope(realPath, scanDir) {
33742
33795
  const resolvedRealPath = path21.resolve(realPath);
33743
33796
  return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path21.sep) || resolvedRealPath.startsWith(`${resolvedScanDir}/`) || resolvedRealPath.startsWith(`${resolvedScanDir}\\`);
33744
33797
  }
33745
- function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33798
+ function findScannableFiles(dir, excludeExact, excludeGlobs, scanDir, visited, stats = {
33746
33799
  skippedDirs: 0,
33747
33800
  skippedFiles: 0,
33748
33801
  fileErrors: 0,
@@ -33766,11 +33819,12 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33766
33819
  return a.localeCompare(b);
33767
33820
  });
33768
33821
  for (const entry of entries) {
33769
- if (excludeDirs.has(entry)) {
33822
+ const fullPath = path21.join(dir, entry);
33823
+ const relPath = path21.relative(scanDir, fullPath).replace(/\\/g, "/");
33824
+ if (isExcluded(entry, relPath, excludeExact, excludeGlobs)) {
33770
33825
  stats.skippedDirs++;
33771
33826
  continue;
33772
33827
  }
33773
- const fullPath = path21.join(dir, entry);
33774
33828
  let lstat;
33775
33829
  try {
33776
33830
  lstat = fs9.lstatSync(fullPath);
@@ -33798,7 +33852,7 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
33798
33852
  stats.symlinkSkipped++;
33799
33853
  continue;
33800
33854
  }
33801
- const subFiles = findScannableFiles(fullPath, excludeDirs, scanDir, visited, stats);
33855
+ const subFiles = findScannableFiles(fullPath, excludeExact, excludeGlobs, scanDir, visited, stats);
33802
33856
  files.push(...subFiles);
33803
33857
  } else if (lstat.isFile()) {
33804
33858
  const ext = path21.extname(fullPath).toLowerCase();
@@ -34003,10 +34057,10 @@ var init_secretscan = __esm(() => {
34003
34057
  ];
34004
34058
  O_NOFOLLOW = process.platform !== "win32" ? fs9.constants.O_NOFOLLOW : undefined;
34005
34059
  secretscan = tool({
34006
- description: "Scan directory for potential secrets (API keys, tokens, passwords) using regex patterns and entropy heuristics. Returns metadata-only findings with redacted previews - NEVER returns raw secrets. Excludes common directories (node_modules, .git, dist, etc.) by default.",
34060
+ description: "Scan directory for potential secrets (API keys, tokens, passwords) using regex patterns and entropy heuristics. Returns metadata-only findings with redacted previews - NEVER returns raw secrets. Excludes common directories (node_modules, .git, dist, etc.) by default. Supports glob patterns (e.g. **/.svelte-kit/**, **/*.test.ts) and reads .secretscanignore at the scan root.",
34007
34061
  args: {
34008
34062
  directory: tool.schema.string().describe('Directory to scan for secrets (e.g., "." or "./src")'),
34009
- exclude: tool.schema.array(tool.schema.string()).optional().describe("Additional directories to exclude (added to default exclusions like node_modules, .git, dist)")
34063
+ exclude: tool.schema.array(tool.schema.string()).optional().describe("Patterns to exclude: plain directory names (e.g. node_modules), relative paths, or globs (e.g. **/.svelte-kit/**, **/*.test.ts). Added to default exclusions.")
34010
34064
  },
34011
34065
  async execute(args2, _context) {
34012
34066
  let directory;
@@ -34042,20 +34096,10 @@ var init_secretscan = __esm(() => {
34042
34096
  }
34043
34097
  if (exclude) {
34044
34098
  for (const exc of exclude) {
34045
- if (exc.length > MAX_FILE_PATH_LENGTH) {
34046
- const errorResult = {
34047
- error: `invalid exclude path: exceeds maximum length of ${MAX_FILE_PATH_LENGTH}`,
34048
- scan_dir: directory,
34049
- findings: [],
34050
- count: 0,
34051
- files_scanned: 0,
34052
- skipped_files: 0
34053
- };
34054
- return JSON.stringify(errorResult, null, 2);
34055
- }
34056
- if (containsPathTraversal(exc) || containsControlChars(exc)) {
34099
+ const err2 = validateExcludePattern(exc);
34100
+ if (err2) {
34057
34101
  const errorResult = {
34058
- error: `invalid exclude path: contains path traversal or control characters`,
34102
+ error: err2,
34059
34103
  scan_dir: directory,
34060
34104
  findings: [],
34061
34105
  count: 0,
@@ -34091,10 +34135,20 @@ var init_secretscan = __esm(() => {
34091
34135
  };
34092
34136
  return JSON.stringify(errorResult, null, 2);
34093
34137
  }
34094
- const excludeDirs = new Set(DEFAULT_EXCLUDE_DIRS);
34095
- if (exclude) {
34096
- for (const exc of exclude) {
34097
- excludeDirs.add(exc);
34138
+ const excludeExact = new Set(DEFAULT_EXCLUDE_DIRS);
34139
+ const excludeGlobs = [];
34140
+ const ignoreFilePatterns = loadSecretScanIgnore(scanDir);
34141
+ const allUserPatterns = [
34142
+ ...exclude ?? [],
34143
+ ...ignoreFilePatterns
34144
+ ];
34145
+ for (const exc of allUserPatterns) {
34146
+ if (exc.length === 0)
34147
+ continue;
34148
+ if (isGlobOrPathPattern(exc)) {
34149
+ excludeGlobs.push(exc);
34150
+ } else {
34151
+ excludeExact.add(exc);
34098
34152
  }
34099
34153
  }
34100
34154
  const stats = {
@@ -34104,7 +34158,7 @@ var init_secretscan = __esm(() => {
34104
34158
  symlinkSkipped: 0
34105
34159
  };
34106
34160
  const visited = new Set;
34107
- const files = findScannableFiles(scanDir, excludeDirs, scanDir, visited, stats);
34161
+ const files = findScannableFiles(scanDir, excludeExact, excludeGlobs, scanDir, visited, stats);
34108
34162
  files.sort((a, b) => {
34109
34163
  const aLower = a.toLowerCase();
34110
34164
  const bLower = b.toLowerCase();
@@ -47948,8 +48002,8 @@ function isOutsideSwarmDir(filePath, directory) {
47948
48002
  return false;
47949
48003
  const swarmDir = path26.resolve(directory, ".swarm");
47950
48004
  const resolved = path26.resolve(directory, filePath);
47951
- const relative3 = path26.relative(swarmDir, resolved);
47952
- return relative3.startsWith("..") || path26.isAbsolute(relative3);
48005
+ const relative4 = path26.relative(swarmDir, resolved);
48006
+ return relative4.startsWith("..") || path26.isAbsolute(relative4);
47953
48007
  }
47954
48008
  function isSourceCodePath(filePath) {
47955
48009
  if (!filePath)
@@ -48734,10 +48788,8 @@ function createDelegationGateHook(config3) {
48734
48788
  break;
48735
48789
  }
48736
48790
  }
48737
- if (lastCoderIndex === -1) {
48738
- return;
48739
- }
48740
- const afterCoder = delegationChain.slice(lastCoderIndex);
48791
+ const searchStart = lastCoderIndex === -1 ? 0 : lastCoderIndex;
48792
+ const afterCoder = delegationChain.slice(searchStart);
48741
48793
  for (const delegation of afterCoder) {
48742
48794
  const target = stripKnownSwarmPrefix(delegation.to);
48743
48795
  if (target === "reviewer")
@@ -48745,7 +48797,7 @@ function createDelegationGateHook(config3) {
48745
48797
  if (target === "test_engineer")
48746
48798
  hasTestEngineer = true;
48747
48799
  }
48748
- if (hasReviewer && hasTestEngineer) {
48800
+ if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
48749
48801
  session.qaSkipCount = 0;
48750
48802
  session.qaSkipTaskIds = [];
48751
48803
  }
@@ -58085,13 +58137,13 @@ function validatePath(inputPath, baseDir, workspaceDir) {
58085
58137
  resolved = path41.resolve(baseDir, inputPath);
58086
58138
  }
58087
58139
  const workspaceResolved = path41.resolve(workspaceDir);
58088
- let relative4;
58140
+ let relative5;
58089
58141
  if (isWinAbs) {
58090
- relative4 = path41.win32.relative(workspaceResolved, resolved);
58142
+ relative5 = path41.win32.relative(workspaceResolved, resolved);
58091
58143
  } else {
58092
- relative4 = path41.relative(workspaceResolved, resolved);
58144
+ relative5 = path41.relative(workspaceResolved, resolved);
58093
58145
  }
58094
- if (relative4.startsWith("..")) {
58146
+ if (relative5.startsWith("..")) {
58095
58147
  return "path traversal detected";
58096
58148
  }
58097
58149
  return null;
@@ -60900,6 +60952,7 @@ var todo_extract = createSwarmTool({
60900
60952
  });
60901
60953
  // src/tools/update-task-status.ts
60902
60954
  init_tool();
60955
+ init_schema();
60903
60956
  init_manager2();
60904
60957
  import * as fs35 from "fs";
60905
60958
  import * as path47 from "path";
@@ -60949,10 +61002,6 @@ function checkReviewerGate(taskId, workingDirectory) {
60949
61002
  const state = getTaskState(session, taskId);
60950
61003
  stateEntries.push(`${sessionId}: ${state}`);
60951
61004
  }
60952
- const allIdle = stateEntries.length > 0 && stateEntries.every((e) => e.endsWith(": idle"));
60953
- if (allIdle) {
60954
- return { blocked: false, reason: "" };
60955
- }
60956
61005
  try {
60957
61006
  const resolvedDir = workingDirectory ?? process.cwd();
60958
61007
  const planPath = path47.join(resolvedDir, ".swarm", "plan.json");
@@ -60975,6 +61024,49 @@ function checkReviewerGate(taskId, workingDirectory) {
60975
61024
  return { blocked: false, reason: "" };
60976
61025
  }
60977
61026
  }
61027
+ function recoverTaskStateFromDelegations(taskId) {
61028
+ let hasReviewer = false;
61029
+ let hasTestEngineer = false;
61030
+ for (const [, chain] of swarmState.delegationChains) {
61031
+ for (const delegation of chain) {
61032
+ const target = stripKnownSwarmPrefix(delegation.to);
61033
+ if (target === "reviewer")
61034
+ hasReviewer = true;
61035
+ if (target === "test_engineer")
61036
+ hasTestEngineer = true;
61037
+ }
61038
+ }
61039
+ if (!hasReviewer && !hasTestEngineer)
61040
+ return;
61041
+ for (const [, session] of swarmState.agentSessions) {
61042
+ if (!(session.taskWorkflowStates instanceof Map))
61043
+ continue;
61044
+ const currentState = getTaskState(session, taskId);
61045
+ if (currentState === "tests_run" || currentState === "complete")
61046
+ continue;
61047
+ if (hasReviewer && currentState === "idle") {
61048
+ try {
61049
+ advanceTaskState(session, taskId, "coder_delegated");
61050
+ } catch {}
61051
+ }
61052
+ if (hasReviewer) {
61053
+ const stateNow = getTaskState(session, taskId);
61054
+ if (stateNow === "coder_delegated" || stateNow === "pre_check_passed") {
61055
+ try {
61056
+ advanceTaskState(session, taskId, "reviewer_run");
61057
+ } catch {}
61058
+ }
61059
+ }
61060
+ if (hasTestEngineer) {
61061
+ const stateNow = getTaskState(session, taskId);
61062
+ if (stateNow === "reviewer_run") {
61063
+ try {
61064
+ advanceTaskState(session, taskId, "tests_run");
61065
+ } catch {}
61066
+ }
61067
+ }
61068
+ }
61069
+ }
60978
61070
  async function executeUpdateTaskStatus(args2, fallbackDir) {
60979
61071
  const statusError = validateStatus(args2.status);
60980
61072
  if (statusError) {
@@ -61058,6 +61150,7 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
61058
61150
  directory = fallbackDir ?? process.cwd();
61059
61151
  }
61060
61152
  if (args2.status === "completed") {
61153
+ recoverTaskStateFromDelegations(args2.task_id);
61061
61154
  const reviewerCheck = checkReviewerGate(args2.task_id, directory);
61062
61155
  if (reviewerCheck.blocked) {
61063
61156
  return {
@@ -50,6 +50,17 @@ export interface ReviewerGateResult {
50
50
  * @returns ReviewerGateResult indicating whether the gate is blocked
51
51
  */
52
52
  export declare function checkReviewerGate(taskId: string, workingDirectory?: string): ReviewerGateResult;
53
+ /**
54
+ * Recovery mechanism: reconcile task state with delegation history.
55
+ * When reviewer/test_engineer delegations occurred but the state machine
56
+ * was not advanced (e.g., toolAfter didn't fire, subagent_type missing,
57
+ * cross-session gaps, or pure verification tasks without coder delegation),
58
+ * this function walks all delegation chains and advances the task state
59
+ * so that checkReviewerGate can make an accurate decision.
60
+ *
61
+ * @param taskId - The task ID to recover state for
62
+ */
63
+ export declare function recoverTaskStateFromDelegations(taskId: string): void;
53
64
  /**
54
65
  * Execute the update_task_status tool.
55
66
  * Validates the task_id and status, then updates the task status in the plan.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.22.21",
3
+ "version": "6.23.1",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",