opencode-swarm 7.27.3 → 7.27.4

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
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.27.3",
37
+ version: "7.27.4",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -20932,7 +20932,7 @@ var init_scope_persistence = __esm(() => {
20932
20932
 
20933
20933
  // src/hooks/shell-write-detect.ts
20934
20934
  import parse5 from "bash-parser";
20935
- var REDIRECT_WRITE_TOKENS, REDIRECT_HERE_TOKENS, REDIRECT_ALL_WRITE_TOKENS, BUILTIN_WRITE_COMMANDS, INPLACE_EDIT_COMMANDS, INTERPRETER_EVAL_COMMANDS, NETWORK_DOWNLOAD_COMMANDS, ARCHIVE_EXTRACT_COMMANDS, PS_WRITE_CMDLETS, PS_WRITE_ALIASES, CMD_WRITE_BUILTINS;
20935
+ var REDIRECT_WRITE_TOKENS, REDIRECT_HERE_TOKENS, REDIRECT_ALL_WRITE_TOKENS, BUILTIN_WRITE_COMMANDS, INPLACE_EDIT_COMMANDS, INTERPRETER_EVAL_COMMANDS, NETWORK_DOWNLOAD_COMMANDS, ARCHIVE_EXTRACT_COMMANDS, _PS_WRITE_CMDLETS, _PS_WRITE_ALIASES, _CMD_WRITE_BUILTINS;
20936
20936
  var init_shell_write_detect = __esm(() => {
20937
20937
  REDIRECT_WRITE_TOKENS = new Set([
20938
20938
  "GREAT",
@@ -20974,7 +20974,7 @@ var init_shell_write_detect = __esm(() => {
20974
20974
  "7z",
20975
20975
  "rar"
20976
20976
  ]);
20977
- PS_WRITE_CMDLETS = new Set([
20977
+ _PS_WRITE_CMDLETS = new Set([
20978
20978
  "Out-File",
20979
20979
  "Set-Content",
20980
20980
  "Add-Content",
@@ -20985,8 +20985,8 @@ var init_shell_write_detect = __esm(() => {
20985
20985
  "Invoke-WebRequest",
20986
20986
  "Start-Process"
20987
20987
  ]);
20988
- PS_WRITE_ALIASES = new Set(["echo", "write"]);
20989
- CMD_WRITE_BUILTINS = new Set([
20988
+ _PS_WRITE_ALIASES = new Set(["echo", "write"]);
20989
+ _CMD_WRITE_BUILTINS = new Set([
20990
20990
  "copy",
20991
20991
  "move",
20992
20992
  "type",
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var package_default;
48
48
  var init_package = __esm(() => {
49
49
  package_default = {
50
50
  name: "opencode-swarm",
51
- version: "7.27.3",
51
+ version: "7.27.4",
52
52
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
53
53
  main: "dist/index.js",
54
54
  types: "dist/index.d.ts",
@@ -34311,8 +34311,8 @@ function wordText(node) {
34311
34311
  if (!node || typeof node !== "object")
34312
34312
  return null;
34313
34313
  const n = node;
34314
- if (n["type"] === "Word" && typeof n["text"] === "string") {
34315
- return n["text"];
34314
+ if (n.type === "Word" && typeof n.text === "string") {
34315
+ return n.text;
34316
34316
  }
34317
34317
  return null;
34318
34318
  }
@@ -34320,64 +34320,62 @@ function collectLeafCommands(node, out2) {
34320
34320
  if (!node || typeof node !== "object")
34321
34321
  return;
34322
34322
  const n = node;
34323
- const type = n["type"];
34323
+ const type = n.type;
34324
34324
  switch (type) {
34325
34325
  case "Script":
34326
- if (Array.isArray(n["commands"])) {
34327
- for (const cmd of n["commands"]) {
34326
+ if (Array.isArray(n.commands)) {
34327
+ for (const cmd of n.commands) {
34328
34328
  collectLeafCommands(cmd, out2);
34329
34329
  }
34330
34330
  }
34331
34331
  break;
34332
34332
  case "Pipeline":
34333
34333
  case "Sequence":
34334
- if (Array.isArray(n["commands"])) {
34335
- for (const cmd of n["commands"]) {
34334
+ if (Array.isArray(n.commands)) {
34335
+ for (const cmd of n.commands) {
34336
34336
  collectLeafCommands(cmd, out2);
34337
34337
  }
34338
34338
  }
34339
34339
  break;
34340
34340
  case "CompoundList":
34341
34341
  out2.push(node);
34342
- if (Array.isArray(n["commands"])) {
34343
- for (const cmd of n["commands"]) {
34342
+ if (Array.isArray(n.commands)) {
34343
+ for (const cmd of n.commands) {
34344
34344
  collectLeafCommands(cmd, out2);
34345
34345
  }
34346
34346
  }
34347
34347
  break;
34348
34348
  case "LogicalExpression":
34349
- if (n["left"])
34350
- collectLeafCommands(n["left"], out2);
34351
- if (n["right"])
34352
- collectLeafCommands(n["right"], out2);
34349
+ if (n.left)
34350
+ collectLeafCommands(n.left, out2);
34351
+ if (n.right)
34352
+ collectLeafCommands(n.right, out2);
34353
34353
  break;
34354
34354
  case "And":
34355
34355
  case "Or":
34356
- if (n["left"])
34357
- collectLeafCommands(n["left"], out2);
34358
- if (n["right"])
34359
- collectLeafCommands(n["right"], out2);
34356
+ if (n.left)
34357
+ collectLeafCommands(n.left, out2);
34358
+ if (n.right)
34359
+ collectLeafCommands(n.right, out2);
34360
34360
  break;
34361
34361
  case "Subshell":
34362
34362
  out2.push(node);
34363
- if (n["list"])
34364
- collectLeafCommands(n["list"], out2);
34363
+ if (n.list)
34364
+ collectLeafCommands(n.list, out2);
34365
34365
  break;
34366
34366
  case "ProcessSubstitution":
34367
34367
  {
34368
- const op = n["op"];
34368
+ const op = n.op;
34369
34369
  if (op && typeof op === "object") {
34370
- const opType = op["type"]?.toUpperCase();
34370
+ const opType = op.type?.toUpperCase();
34371
34371
  if (opType === "GREAT") {
34372
- if (n["command"]) {
34373
- collectLeafCommands(n["command"], out2);
34372
+ if (n.command) {
34373
+ collectLeafCommands(n.command, out2);
34374
34374
  }
34375
34375
  }
34376
34376
  }
34377
34377
  }
34378
34378
  break;
34379
- case "Command":
34380
- case "SimpleCommand":
34381
34379
  default:
34382
34380
  out2.push(node);
34383
34381
  break;
@@ -34387,15 +34385,15 @@ function getCommandName(cmd) {
34387
34385
  if (!cmd || typeof cmd !== "object")
34388
34386
  return null;
34389
34387
  const c = cmd;
34390
- if (typeof c["name"] === "string")
34391
- return c["name"];
34392
- return wordText(c["name"]);
34388
+ if (typeof c.name === "string")
34389
+ return c.name;
34390
+ return wordText(c.name);
34393
34391
  }
34394
34392
  function getSuffixWords(cmd) {
34395
34393
  if (!cmd || typeof cmd !== "object")
34396
34394
  return [];
34397
34395
  const c = cmd;
34398
- const suffix = c["suffix"];
34396
+ const suffix = c.suffix;
34399
34397
  if (!Array.isArray(suffix))
34400
34398
  return [];
34401
34399
  return suffix.map((s) => wordText(s) ?? "");
@@ -34404,12 +34402,12 @@ function getRedirections(cmd) {
34404
34402
  if (!cmd || typeof cmd !== "object")
34405
34403
  return [];
34406
34404
  const c = cmd;
34407
- if (Array.isArray(c["redirections"])) {
34408
- return c["redirections"];
34405
+ if (Array.isArray(c.redirections)) {
34406
+ return c.redirections;
34409
34407
  }
34410
34408
  const results = [];
34411
- const suffix = c["suffix"];
34412
- const prefix = c["prefix"];
34409
+ const suffix = c.suffix;
34410
+ const prefix = c.prefix;
34413
34411
  if (Array.isArray(suffix)) {
34414
34412
  for (const item of suffix) {
34415
34413
  if (isRedirectNode(item))
@@ -34425,13 +34423,13 @@ function getRedirections(cmd) {
34425
34423
  return results;
34426
34424
  }
34427
34425
  function isRedirectNode(node) {
34428
- return !!(node && typeof node === "object" && node["type"] === "Redirect");
34426
+ return !!(node && typeof node === "object" && node.type === "Redirect");
34429
34427
  }
34430
34428
  function operatorLabel(op) {
34431
34429
  if (!op || typeof op !== "object")
34432
34430
  return "unknown";
34433
34431
  const o = op;
34434
- const opType = o["type"]?.toUpperCase();
34432
+ const opType = o.type?.toUpperCase();
34435
34433
  switch (opType) {
34436
34434
  case "GREAT":
34437
34435
  return ">";
@@ -34475,13 +34473,13 @@ function detectRedirects(cmd) {
34475
34473
  if (!redirect || typeof redirect !== "object")
34476
34474
  continue;
34477
34475
  const r = redirect;
34478
- const opNode = r["op"];
34476
+ const opNode = r.op;
34479
34477
  if (!opNode || typeof opNode !== "object")
34480
34478
  continue;
34481
- const opType = opNode["type"]?.toUpperCase();
34479
+ const opType = opNode.type?.toUpperCase();
34482
34480
  if (!opType || !REDIRECT_ALL_WRITE_TOKENS.has(opType))
34483
34481
  continue;
34484
- const fileNode = r["file"];
34482
+ const fileNode = r.file;
34485
34483
  const path11 = extractRedirectPath(fileNode);
34486
34484
  const opLabel = operatorLabel(opNode);
34487
34485
  if (REDIRECT_WRITE_TOKENS.has(opType)) {
@@ -34497,16 +34495,16 @@ function detectHereDocMarkers(cmd) {
34497
34495
  if (!cmd || typeof cmd !== "object")
34498
34496
  return results;
34499
34497
  const c = cmd;
34500
- const suffix = c["suffix"];
34498
+ const suffix = c.suffix;
34501
34499
  if (!Array.isArray(suffix))
34502
34500
  return results;
34503
34501
  for (const item of suffix) {
34504
34502
  if (!item || typeof item !== "object")
34505
34503
  continue;
34506
34504
  const n = item;
34507
- const type = n["type"]?.toLowerCase();
34505
+ const type = n.type?.toLowerCase();
34508
34506
  if (type === "dless" || type === "dlessdash") {
34509
- const text = n["text"];
34507
+ const text = n.text;
34510
34508
  results.push({
34511
34509
  category: "here_doc",
34512
34510
  operator: text === "<<-" ? "<<-" : "<<",
@@ -34893,7 +34891,7 @@ function detectPowerShellWrites(command) {
34893
34891
  const op = redirectMatch[2];
34894
34892
  const path11 = redirectMatch[3];
34895
34893
  const fdRedirectPattern = /\d>\s*$|\s\d>&$/;
34896
- if (!fdRedirectPattern.test(beforeRedirect + " " + op)) {
34894
+ if (!fdRedirectPattern.test(`${beforeRedirect} ${op}`)) {
34897
34895
  results.push({ category: "redirect", operator: op, path: path11 });
34898
34896
  }
34899
34897
  }
@@ -35034,8 +35032,8 @@ function detectPowerShellWrites(command) {
35034
35032
  }
35035
35033
  const aliasRedirectMatch = trimmed.match(/^(echo|write)\s+(\S+.*?)\s*(?:>|\s{2,})/i);
35036
35034
  if (aliasRedirectMatch) {
35037
- const aliasPart = aliasRedirectMatch[0];
35038
- const afterAlias = trimmed.slice(aliasRedirectMatch[0].length);
35035
+ const _aliasPart = aliasRedirectMatch[0];
35036
+ const _afterAlias = trimmed.slice(aliasRedirectMatch[0].length);
35039
35037
  const fullRedirectMatch = trimmed.match(/((?:>){1,2})\s*(\S+)$/);
35040
35038
  if (fullRedirectMatch) {
35041
35039
  results.push({
@@ -35295,28 +35293,28 @@ function getCdTarget(cmd) {
35295
35293
  if (!cmd || typeof cmd !== "object")
35296
35294
  return null;
35297
35295
  const c = cmd;
35298
- const nodeType = c["type"];
35296
+ const nodeType = c.type;
35299
35297
  if (nodeType !== "SimpleCommand" && nodeType !== "Command")
35300
35298
  return null;
35301
- const name2 = c["name"];
35299
+ const name2 = c.name;
35302
35300
  if (!name2 || typeof name2 !== "object")
35303
35301
  return null;
35304
35302
  const n = name2;
35305
- if (n["type"] !== "Word")
35303
+ if (n.type !== "Word")
35306
35304
  return null;
35307
- const text = n["text"];
35305
+ const text = n.text;
35308
35306
  if (text !== "cd")
35309
35307
  return null;
35310
- const suffix = c["suffix"];
35308
+ const suffix = c.suffix;
35311
35309
  if (!Array.isArray(suffix) || suffix.length === 0)
35312
35310
  return null;
35313
35311
  const first = suffix[0];
35314
35312
  if (!first || typeof first !== "object")
35315
35313
  return null;
35316
35314
  const firstWord = first;
35317
- if (firstWord["type"] !== "Word")
35315
+ if (firstWord.type !== "Word")
35318
35316
  return null;
35319
- const target = firstWord["text"];
35317
+ const target = firstWord.text;
35320
35318
  if (target === "-" || target === "~" || target === "~" + "/")
35321
35319
  return null;
35322
35320
  return target;
@@ -35325,12 +35323,12 @@ function collectWritesWithNodes(node, out2, cwdStack) {
35325
35323
  if (!node || typeof node !== "object")
35326
35324
  return;
35327
35325
  const n = node;
35328
- const type = n["type"];
35326
+ const type = n.type;
35329
35327
  switch (type) {
35330
35328
  case "Script": {
35331
- const globalCwd = cwdStack[0] ?? "/";
35332
- if (Array.isArray(n["commands"])) {
35333
- for (const cmd of n["commands"]) {
35329
+ const _globalCwd = cwdStack[0] ?? "/";
35330
+ if (Array.isArray(n.commands)) {
35331
+ for (const cmd of n.commands) {
35334
35332
  collectWritesWithNodes(cmd, out2, cwdStack);
35335
35333
  }
35336
35334
  }
@@ -35338,7 +35336,7 @@ function collectWritesWithNodes(node, out2, cwdStack) {
35338
35336
  }
35339
35337
  case "Subshell": {
35340
35338
  const effectiveCwd = cwdStack[0] ?? "/";
35341
- const subshellRedirs = n["redirections"];
35339
+ const subshellRedirs = n.redirections;
35342
35340
  if (Array.isArray(subshellRedirs)) {
35343
35341
  for (const redirect of subshellRedirs) {
35344
35342
  const writesFromRedirect = getWritesFromRedirectNode(redirect, effectiveCwd);
@@ -35347,16 +35345,16 @@ function collectWritesWithNodes(node, out2, cwdStack) {
35347
35345
  }
35348
35346
  }
35349
35347
  }
35350
- if (n["list"]) {
35348
+ if (n.list) {
35351
35349
  const subStack = [...cwdStack];
35352
- collectWritesWithNodes(n["list"], out2, subStack);
35350
+ collectWritesWithNodes(n.list, out2, subStack);
35353
35351
  }
35354
35352
  break;
35355
35353
  }
35356
35354
  case "CompoundList": {
35357
- if (Array.isArray(n["commands"])) {
35355
+ if (Array.isArray(n.commands)) {
35358
35356
  const currentStack = cwdStack;
35359
- for (const cmd of n["commands"]) {
35357
+ for (const cmd of n.commands) {
35360
35358
  const cdTarget = getCdTarget(cmd);
35361
35359
  if (cdTarget) {
35362
35360
  currentStack[0] = path10.posix.resolve(currentStack[0] ?? "/", cdTarget);
@@ -35370,32 +35368,32 @@ function collectWritesWithNodes(node, out2, cwdStack) {
35370
35368
  }
35371
35369
  case "Pipeline":
35372
35370
  case "Sequence": {
35373
- if (Array.isArray(n["commands"])) {
35374
- for (const cmd of n["commands"]) {
35371
+ if (Array.isArray(n.commands)) {
35372
+ for (const cmd of n.commands) {
35375
35373
  collectWritesWithNodes(cmd, out2, cwdStack);
35376
35374
  }
35377
35375
  }
35378
35376
  break;
35379
35377
  }
35380
35378
  case "LogicalExpression": {
35381
- if (n["left"]) {
35382
- collectWritesWithNodes(n["left"], out2, cwdStack);
35379
+ if (n.left) {
35380
+ collectWritesWithNodes(n.left, out2, cwdStack);
35383
35381
  const leftContext = cwdStack[0] ?? "/";
35384
- if (n["right"]) {
35382
+ if (n.right) {
35385
35383
  const rightStack = [leftContext];
35386
- collectWritesWithNodes(n["right"], out2, rightStack);
35384
+ collectWritesWithNodes(n.right, out2, rightStack);
35387
35385
  }
35388
35386
  }
35389
35387
  break;
35390
35388
  }
35391
35389
  case "And":
35392
35390
  case "Or": {
35393
- if (n["left"]) {
35394
- collectWritesWithNodes(n["left"], out2, cwdStack);
35391
+ if (n.left) {
35392
+ collectWritesWithNodes(n.left, out2, cwdStack);
35395
35393
  const leftContext = cwdStack[0] ?? "/";
35396
- if (n["right"]) {
35394
+ if (n.right) {
35397
35395
  const rightStack = [leftContext];
35398
- collectWritesWithNodes(n["right"], out2, rightStack);
35396
+ collectWritesWithNodes(n.right, out2, rightStack);
35399
35397
  }
35400
35398
  }
35401
35399
  break;
@@ -35444,13 +35442,13 @@ function getWritesFromRedirectNode(redirect, _context) {
35444
35442
  if (!redirect || typeof redirect !== "object")
35445
35443
  return [];
35446
35444
  const r = redirect;
35447
- const opNode = r["op"];
35445
+ const opNode = r.op;
35448
35446
  if (!opNode || typeof opNode !== "object")
35449
35447
  return [];
35450
- const opType = opNode["type"]?.toUpperCase();
35448
+ const opType = opNode.type?.toUpperCase();
35451
35449
  if (!opType || !REDIRECT_ALL_WRITE_TOKENS.has(opType))
35452
35450
  return [];
35453
- const fileNode = r["file"];
35451
+ const fileNode = r.file;
35454
35452
  const path11 = extractRedirectPath(fileNode);
35455
35453
  const opLabel = operatorLabel(opNode);
35456
35454
  if (REDIRECT_WRITE_TOKENS.has(opType)) {
@@ -35466,8 +35464,8 @@ function getWritesFromCommandRedirs(cmd, context) {
35466
35464
  const c = cmd;
35467
35465
  const results = [];
35468
35466
  results.push(...detectHereDocMarkers(cmd));
35469
- const suffix = c["suffix"];
35470
- const prefix = c["prefix"];
35467
+ const suffix = c.suffix;
35468
+ const prefix = c.prefix;
35471
35469
  if (Array.isArray(suffix)) {
35472
35470
  for (const item of suffix) {
35473
35471
  if (isRedirectNode(item)) {
@@ -35544,7 +35542,7 @@ function resolveWriteTargets(command, writes, cwd) {
35544
35542
  };
35545
35543
  });
35546
35544
  }
35547
- var import_bash_parser, REDIRECT_WRITE_TOKENS, REDIRECT_HERE_TOKENS, REDIRECT_ALL_WRITE_TOKENS, BUILTIN_WRITE_COMMANDS, INPLACE_EDIT_COMMANDS, INTERPRETER_EVAL_COMMANDS, NETWORK_DOWNLOAD_COMMANDS, ARCHIVE_EXTRACT_COMMANDS, GIT_COMMAND = "git", PS_WRITE_CMDLETS, PS_WRITE_ALIASES, CMD_WRITE_BUILTINS;
35545
+ var import_bash_parser, REDIRECT_WRITE_TOKENS, REDIRECT_HERE_TOKENS, REDIRECT_ALL_WRITE_TOKENS, BUILTIN_WRITE_COMMANDS, INPLACE_EDIT_COMMANDS, INTERPRETER_EVAL_COMMANDS, NETWORK_DOWNLOAD_COMMANDS, ARCHIVE_EXTRACT_COMMANDS, GIT_COMMAND = "git", _PS_WRITE_CMDLETS, _PS_WRITE_ALIASES, _CMD_WRITE_BUILTINS;
35548
35546
  var init_shell_write_detect = __esm(() => {
35549
35547
  import_bash_parser = __toESM(require_src(), 1);
35550
35548
  REDIRECT_WRITE_TOKENS = new Set([
@@ -35587,7 +35585,7 @@ var init_shell_write_detect = __esm(() => {
35587
35585
  "7z",
35588
35586
  "rar"
35589
35587
  ]);
35590
- PS_WRITE_CMDLETS = new Set([
35588
+ _PS_WRITE_CMDLETS = new Set([
35591
35589
  "Out-File",
35592
35590
  "Set-Content",
35593
35591
  "Add-Content",
@@ -35598,8 +35596,8 @@ var init_shell_write_detect = __esm(() => {
35598
35596
  "Invoke-WebRequest",
35599
35597
  "Start-Process"
35600
35598
  ]);
35601
- PS_WRITE_ALIASES = new Set(["echo", "write"]);
35602
- CMD_WRITE_BUILTINS = new Set([
35599
+ _PS_WRITE_ALIASES = new Set(["echo", "write"]);
35600
+ _CMD_WRITE_BUILTINS = new Set([
35603
35601
  "copy",
35604
35602
  "move",
35605
35603
  "type",
@@ -38848,6 +38846,73 @@ function isTaskCompletedForParallelGuidance(task) {
38848
38846
  const status = task.status ?? "pending";
38849
38847
  return status === "completed" || status === "closed";
38850
38848
  }
38849
+ function getPlanTaskStatus(plan, taskId) {
38850
+ for (const phase of plan.phases) {
38851
+ const task = phase.tasks.find((candidate) => candidate.id === taskId);
38852
+ if (task)
38853
+ return task.status ?? "pending";
38854
+ }
38855
+ return null;
38856
+ }
38857
+ function resolveDelegatedPlanTaskId(args2, knownPlanTaskIds) {
38858
+ const rawTaskId = args2.task_id ?? args2.taskId;
38859
+ if (typeof rawTaskId === "string") {
38860
+ const trimmed = rawTaskId.trim();
38861
+ if (trimmed.length <= 20 && isStrictTaskId(trimmed))
38862
+ return trimmed;
38863
+ return null;
38864
+ }
38865
+ const candidateTextFields = [
38866
+ args2.prompt,
38867
+ args2.description,
38868
+ args2.task,
38869
+ args2.input
38870
+ ];
38871
+ const seen = new Set;
38872
+ for (const field of candidateTextFields) {
38873
+ if (typeof field !== "string")
38874
+ continue;
38875
+ for (const m of field.matchAll(/\b(\d+\.\d+(?:\.\d+)*)\b/g)) {
38876
+ const candidate = m[1];
38877
+ if (isStrictTaskId(candidate)) {
38878
+ if (knownPlanTaskIds && !knownPlanTaskIds.has(candidate))
38879
+ continue;
38880
+ seen.add(candidate);
38881
+ }
38882
+ }
38883
+ }
38884
+ if (seen.size === 1)
38885
+ return seen.values().next().value;
38886
+ return null;
38887
+ }
38888
+ async function findTaskAwaitingCompletion(directory, session, requestedTaskId) {
38889
+ if (!directory)
38890
+ return null;
38891
+ let plan = null;
38892
+ try {
38893
+ plan = await loadPlanJsonOnly(directory);
38894
+ } catch {
38895
+ return null;
38896
+ }
38897
+ if (!plan)
38898
+ return null;
38899
+ for (const [taskId, state] of session.taskWorkflowStates) {
38900
+ if (state !== "tests_run")
38901
+ continue;
38902
+ if (requestedTaskId && requestedTaskId === taskId)
38903
+ continue;
38904
+ const planStatus = getPlanTaskStatus(plan, taskId);
38905
+ if (!planStatus)
38906
+ continue;
38907
+ if (planStatus === "completed" || planStatus === "closed")
38908
+ continue;
38909
+ return taskId;
38910
+ }
38911
+ return null;
38912
+ }
38913
+ function completionGateViolationMessage(taskAwaitingCompletion) {
38914
+ return `TASK_COMPLETION_GATE_VIOLATION: Task ${taskAwaitingCompletion} reached tests_run but is not marked completed in plan.json/plan.md. ` + `Call update_task_status with task_id="${taskAwaitingCompletion}" and status="completed" before starting another task.`;
38915
+ }
38851
38916
  async function buildParallelExecutionGuidance(directory, sessionID, session) {
38852
38917
  if (!directory)
38853
38918
  return null;
@@ -38961,6 +39026,27 @@ function createDelegationGateHook(config2, directory) {
38961
39026
  if (!input.sessionID)
38962
39027
  return;
38963
39028
  const normalized = normalizeToolName(input.tool);
39029
+ const completionArgs = output.args;
39030
+ if (completionArgs) {
39031
+ let completionPlanTaskIds;
39032
+ try {
39033
+ const plan = await loadPlanJsonOnly(directory);
39034
+ if (plan) {
39035
+ completionPlanTaskIds = new Set(plan.phases.flatMap((p) => p.tasks.map((t) => t.id)));
39036
+ }
39037
+ } catch {}
39038
+ const requestedTaskId = resolveDelegatedPlanTaskId(completionArgs, completionPlanTaskIds);
39039
+ const completionSession = ensureAgentSession(input.sessionID);
39040
+ const taskAwaitingCompletion = await findTaskAwaitingCompletion(directory, completionSession, requestedTaskId);
39041
+ if (taskAwaitingCompletion) {
39042
+ const allowingSameTaskRetry = requestedTaskId === taskAwaitingCompletion;
39043
+ const requestedTaskIsAwaitingCompletion = requestedTaskId && completionSession.taskWorkflowStates.get(requestedTaskId) === "tests_run";
39044
+ const allowCompletionUpdate = normalized === "update_task_status" && completionArgs.status === "completed" && requestedTaskIsAwaitingCompletion;
39045
+ if (!allowingSameTaskRetry && !allowCompletionUpdate) {
39046
+ throw new Error(completionGateViolationMessage(taskAwaitingCompletion));
39047
+ }
39048
+ }
39049
+ }
38964
39050
  if (normalized !== "Task" && normalized !== "task")
38965
39051
  return;
38966
39052
  const args2 = output.args;
@@ -39018,6 +39104,23 @@ function createDelegationGateHook(config2, directory) {
39018
39104
  return;
39019
39105
  const normalized = normalizeToolName(input.tool);
39020
39106
  const councilActive = await isCouncilGateActive(directory, config2.council);
39107
+ if (normalized === "update_task_status") {
39108
+ const directArgs = input.args;
39109
+ const storedArgs = getStoredInputArgs(input.callID);
39110
+ const completionArgs = directArgs ?? storedArgs;
39111
+ if (completionArgs && completionArgs.status === "completed") {
39112
+ const rawTaskId = completionArgs.task_id ?? completionArgs.taskId;
39113
+ const completionTaskId = typeof rawTaskId === "string" ? rawTaskId.trim() : null;
39114
+ if (completionTaskId && isStrictTaskId(completionTaskId)) {
39115
+ try {
39116
+ const completionSession = ensureAgentSession(input.sessionID);
39117
+ await advanceTaskStateAndPersist(completionSession, completionTaskId, "complete", directory, { telemetrySessionId: input.sessionID }, config2.council);
39118
+ } catch (err2) {
39119
+ warn(`[delegation-gate] toolAfter completion advancement: could not advance ${completionTaskId} → complete: ${err2 instanceof Error ? err2.message : String(err2)}`);
39120
+ }
39121
+ }
39122
+ }
39123
+ }
39021
39124
  if (normalized === "submit_council_verdicts") {
39022
39125
  try {
39023
39126
  const parsed = typeof _output === "string" ? JSON.parse(_output) : _output;
@@ -39290,6 +39393,18 @@ function createDelegationGateHook(config2, directory) {
39290
39393
  console.warn(`[delegation-gate] fallback evidence recording failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
39291
39394
  }
39292
39395
  }
39396
+ if (session.taskWorkflowStates) {
39397
+ for (const [, state] of session.taskWorkflowStates) {
39398
+ if (state === "tests_run") {
39399
+ const taskAwaiting = await findTaskAwaitingCompletion(directory, session);
39400
+ if (taskAwaiting) {
39401
+ session.pendingAdvisoryMessages ??= [];
39402
+ session.pendingAdvisoryMessages.push(completionGateViolationMessage(taskAwaiting));
39403
+ }
39404
+ break;
39405
+ }
39406
+ }
39407
+ }
39293
39408
  }
39294
39409
  };
39295
39410
  return {
@@ -39414,8 +39529,12 @@ ${trimComment}${after}`;
39414
39529
  const deliberationSession = ensureAgentSession(deliberationSessionID);
39415
39530
  const lastGate = deliberationSession.lastGateOutcome;
39416
39531
  const parallelGuidance = await buildParallelExecutionGuidance(directory, deliberationSessionID, deliberationSession);
39532
+ const taskAwaitingCompletion = await findTaskAwaitingCompletion(directory, deliberationSession);
39417
39533
  let guidance;
39418
- if (lastGate?.taskId) {
39534
+ if (taskAwaitingCompletion) {
39535
+ guidance = `[TASK COMPLETION REQUIRED] Task ${taskAwaitingCompletion} has completed reviewer/test_engineer gates and is awaiting durable plan update.
39536
+ [NEXT] Print the task completion checklist, then call update_task_status with task_id="${taskAwaitingCompletion}" and status="completed" before declare_scope or starting another task.`;
39537
+ } else if (lastGate?.taskId) {
39419
39538
  const gateResult = lastGate.passed ? "PASSED" : "FAILED";
39420
39539
  const sanitizedGate = lastGate.gate.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\[ \]/g, "()").replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 64);
39421
39540
  const sanitizedTaskId = lastGate.taskId.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 32);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.27.3",
3
+ "version": "7.27.4",
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",