opencode-swarm 6.21.0 → 6.21.2

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/README.md CHANGED
@@ -688,6 +688,24 @@ When truncation is active, a footer is appended:
688
688
  [output truncated to {maxLines} lines – use `tool_output.per_tool.<tool>` to adjust]
689
689
  ```
690
690
 
691
+ ## Summarization Settings
692
+
693
+ Control how tool outputs are summarized for LLM context.
694
+
695
+ ```json
696
+ {
697
+ "summaries": {
698
+ "threshold_bytes": 102400,
699
+ "exempt_tools": ["retrieve_summary", "task", "read"]
700
+ }
701
+ }
702
+ ```
703
+
704
+ - **threshold_bytes** – Output size threshold in bytes before summarization is triggered (default 102400 = 100KB).
705
+ - **exempt_tools** – Tools whose outputs are never summarized. Defaults to `["retrieve_summary", "task", "read"]` to prevent re-summarization loops.
706
+
707
+ > **Note:** The `retrieve_summary` tool supports paginated retrieval via `offset` and `limit` parameters to fetch large summarized outputs in chunks.
708
+
691
709
  ---
692
710
 
693
711
  ### Disabling Agents
@@ -717,7 +735,7 @@ When truncation is active, a footer is appended:
717
735
  | `/swarm evidence [task]` | Evidence bundles for a task or all tasks |
718
736
  | `/swarm archive [--dry-run]` | Archive old evidence with retention policy |
719
737
  | `/swarm benchmark` | Performance benchmarks |
720
- | `/swarm retrieve [id]` | Retrieve auto-summarized tool outputs |
738
+ | `/swarm retrieve [id]` | Retrieve auto-summarized tool outputs (supports offset/limit pagination) |
721
739
  | `/swarm reset --confirm` | Clear swarm state files |
722
740
  | `/swarm preflight` | Run phase preflight checks |
723
741
  | `/swarm config doctor [--fix]` | Config validation with optional auto-fix |
@@ -110,6 +110,11 @@ export declare class PlanSyncWorker {
110
110
  * to prevent callback errors from affecting worker stability
111
111
  */
112
112
  private safeCallback;
113
+ /**
114
+ * Advisory: check for unauthorized writes to plan.json outside of save_plan/savePlan
115
+ * Logs a warning if plan.json appears to have been modified after the write marker
116
+ */
117
+ private checkForUnauthorizedWrite;
113
118
  /**
114
119
  * Wrap a promise with a timeout
115
120
  */
package/dist/cli/index.js CHANGED
@@ -16231,6 +16231,17 @@ ${markdown}`;
16231
16231
  unlinkSync(mdTempPath);
16232
16232
  } catch {}
16233
16233
  }
16234
+ try {
16235
+ const markerPath = path6.join(swarmDir, ".plan-write-marker");
16236
+ const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
16237
+ const marker = JSON.stringify({
16238
+ source: "plan_manager",
16239
+ timestamp: new Date().toISOString(),
16240
+ phases_count: validated.phases.length,
16241
+ tasks_count: tasksCount
16242
+ });
16243
+ await Bun.write(markerPath, marker);
16244
+ } catch {}
16234
16245
  }
16235
16246
  function derivePlanMarkdown(plan) {
16236
16247
  const statusMap = {
@@ -16865,11 +16876,11 @@ var PhaseCompleteConfigSchema = exports_external.object({
16865
16876
  });
16866
16877
  var SummaryConfigSchema = exports_external.object({
16867
16878
  enabled: exports_external.boolean().default(true),
16868
- threshold_bytes: exports_external.number().min(1024).max(1048576).default(20480),
16879
+ threshold_bytes: exports_external.number().min(1024).max(1048576).default(102400),
16869
16880
  max_summary_chars: exports_external.number().min(100).max(5000).default(1000),
16870
16881
  max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
16871
16882
  retention_days: exports_external.number().min(1).max(365).default(7),
16872
- exempt_tools: exports_external.array(exports_external.string()).default(["retrieve_summary", "task"])
16883
+ exempt_tools: exports_external.array(exports_external.string()).default(["retrieve_summary", "task", "read"])
16873
16884
  });
16874
16885
  var ReviewPassesConfigSchema = exports_external.object({
16875
16886
  always_security_review: exports_external.boolean().default(false),
@@ -35655,7 +35666,7 @@ var HELP_TEXT = [
35655
35666
 
35656
35667
  // src/cli/index.ts
35657
35668
  var CONFIG_DIR = path14.join(process.env.XDG_CONFIG_HOME || path14.join(os2.homedir(), ".config"), "opencode");
35658
- var OPENCODE_CONFIG_PATH = path14.join(CONFIG_DIR, "config.json");
35669
+ var OPENCODE_CONFIG_PATH = path14.join(CONFIG_DIR, "opencode.json");
35659
35670
  var PLUGIN_CONFIG_PATH = path14.join(CONFIG_DIR, "opencode-swarm.json");
35660
35671
  var PROMPTS_DIR = path14.join(CONFIG_DIR, "opencode-swarm");
35661
35672
  function ensureDir(dir) {
@@ -35681,9 +35692,16 @@ async function install() {
35681
35692
  `);
35682
35693
  ensureDir(CONFIG_DIR);
35683
35694
  ensureDir(PROMPTS_DIR);
35695
+ const LEGACY_CONFIG_PATH = path14.join(CONFIG_DIR, "config.json");
35684
35696
  let opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
35685
35697
  if (!opencodeConfig) {
35686
- opencodeConfig = {};
35698
+ const legacyConfig = loadJson(LEGACY_CONFIG_PATH);
35699
+ if (legacyConfig) {
35700
+ console.log("\u26A0 Migrating existing config from config.json to opencode.json...");
35701
+ opencodeConfig = legacyConfig;
35702
+ } else {
35703
+ opencodeConfig = {};
35704
+ }
35687
35705
  }
35688
35706
  if (!opencodeConfig.plugin) {
35689
35707
  opencodeConfig.plugin = [];
package/dist/index.js CHANGED
@@ -14841,6 +14841,17 @@ ${markdown}`;
14841
14841
  unlinkSync(mdTempPath);
14842
14842
  } catch {}
14843
14843
  }
14844
+ try {
14845
+ const markerPath = path4.join(swarmDir, ".plan-write-marker");
14846
+ const tasksCount = validated.phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
14847
+ const marker = JSON.stringify({
14848
+ source: "plan_manager",
14849
+ timestamp: new Date().toISOString(),
14850
+ phases_count: validated.phases.length,
14851
+ tasks_count: tasksCount
14852
+ });
14853
+ await Bun.write(markerPath, marker);
14854
+ } catch {}
14844
14855
  }
14845
14856
  async function updateTaskStatus(directory, taskId, status) {
14846
14857
  const plan = await loadPlan(directory);
@@ -38306,7 +38317,8 @@ var KNOWN_SWARM_PREFIXES = [
38306
38317
  "custom",
38307
38318
  "team",
38308
38319
  "project",
38309
- "swarm"
38320
+ "swarm",
38321
+ "synthetic"
38310
38322
  ];
38311
38323
  var SEPARATORS = ["_", "-", " "];
38312
38324
  function stripKnownSwarmPrefix(agentName) {
@@ -38485,11 +38497,11 @@ var PhaseCompleteConfigSchema = exports_external.object({
38485
38497
  });
38486
38498
  var SummaryConfigSchema = exports_external.object({
38487
38499
  enabled: exports_external.boolean().default(true),
38488
- threshold_bytes: exports_external.number().min(1024).max(1048576).default(20480),
38500
+ threshold_bytes: exports_external.number().min(1024).max(1048576).default(102400),
38489
38501
  max_summary_chars: exports_external.number().min(100).max(5000).default(1000),
38490
38502
  max_stored_bytes: exports_external.number().min(10240).max(104857600).default(10485760),
38491
38503
  retention_days: exports_external.number().min(1).max(365).default(7),
38492
- exempt_tools: exports_external.array(exports_external.string()).default(["retrieve_summary", "task"])
38504
+ exempt_tools: exports_external.array(exports_external.string()).default(["retrieve_summary", "task", "read"])
38493
38505
  });
38494
38506
  var ReviewPassesConfigSchema = exports_external.object({
38495
38507
  always_security_review: exports_external.boolean().default(false),
@@ -41666,6 +41678,7 @@ class PlanSyncWorker {
41666
41678
  this.syncing = true;
41667
41679
  try {
41668
41680
  log("[PlanSyncWorker] Syncing plan...");
41681
+ this.checkForUnauthorizedWrite();
41669
41682
  const plan = await this.withTimeout(loadPlan(this.directory), this.syncTimeoutMs, "Sync operation timed out");
41670
41683
  if (plan) {
41671
41684
  log("[PlanSyncWorker] Sync complete", {
@@ -41707,6 +41720,21 @@ class PlanSyncWorker {
41707
41720
  }
41708
41721
  }
41709
41722
  }
41723
+ checkForUnauthorizedWrite() {
41724
+ try {
41725
+ const swarmDir = this.getSwarmDir();
41726
+ const planJsonPath = path6.join(swarmDir, "plan.json");
41727
+ const markerPath = path6.join(swarmDir, ".plan-write-marker");
41728
+ const planStats = fs3.statSync(planJsonPath);
41729
+ const planMtimeMs = planStats.mtimeMs;
41730
+ const markerContent = fs3.readFileSync(markerPath, "utf8");
41731
+ const marker = JSON.parse(markerContent);
41732
+ const markerTimestampMs = new Date(marker.timestamp).getTime();
41733
+ if (planMtimeMs > markerTimestampMs + 5000) {
41734
+ log("[PlanSyncWorker] WARNING: plan.json may have been written outside save_plan/savePlan - unauthorized direct write suspected", { planMtimeMs, markerTimestampMs });
41735
+ }
41736
+ } catch {}
41737
+ }
41710
41738
  withTimeout(promise2, ms, timeoutMessage) {
41711
41739
  return new Promise((resolve4, reject) => {
41712
41740
  const timer = setTimeout(() => {
@@ -42074,6 +42102,9 @@ function recordPhaseAgentDispatch(sessionId, agentName) {
42074
42102
  session.phaseAgentsDispatched.add(normalizedName);
42075
42103
  }
42076
42104
  function advanceTaskState(session, taskId, newState) {
42105
+ if (!session.taskWorkflowStates) {
42106
+ session.taskWorkflowStates = new Map;
42107
+ }
42077
42108
  const STATE_ORDER = [
42078
42109
  "idle",
42079
42110
  "coder_delegated",
@@ -42094,6 +42125,9 @@ function advanceTaskState(session, taskId, newState) {
42094
42125
  session.taskWorkflowStates.set(taskId, newState);
42095
42126
  }
42096
42127
  function getTaskState(session, taskId) {
42128
+ if (!session.taskWorkflowStates) {
42129
+ session.taskWorkflowStates = new Map;
42130
+ }
42097
42131
  return session.taskWorkflowStates.get(taskId) ?? "idle";
42098
42132
  }
42099
42133
 
@@ -42985,7 +43019,7 @@ async function handleDarkMatterCommand(directory, args2) {
42985
43019
 
42986
43020
  // src/services/diagnose-service.ts
42987
43021
  import { execSync } from "child_process";
42988
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync4 } from "fs";
43022
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
42989
43023
  import path12 from "path";
42990
43024
  init_manager();
42991
43025
  init_utils2();
@@ -43291,7 +43325,7 @@ async function checkConfigParseability(directory) {
43291
43325
  };
43292
43326
  }
43293
43327
  try {
43294
- const content = readFileSync3(configPath, "utf-8");
43328
+ const content = readFileSync4(configPath, "utf-8");
43295
43329
  JSON.parse(content);
43296
43330
  return {
43297
43331
  name: "Config Parseability",
@@ -43358,7 +43392,7 @@ async function checkCheckpointManifest(directory) {
43358
43392
  };
43359
43393
  }
43360
43394
  try {
43361
- const content = readFileSync3(manifestPath, "utf-8");
43395
+ const content = readFileSync4(manifestPath, "utf-8");
43362
43396
  const parsed = JSON.parse(content);
43363
43397
  if (!parsed.checkpoints || !Array.isArray(parsed.checkpoints)) {
43364
43398
  return {
@@ -43410,7 +43444,7 @@ async function checkEventStreamIntegrity(directory) {
43410
43444
  };
43411
43445
  }
43412
43446
  try {
43413
- const content = readFileSync3(eventsPath, "utf-8");
43447
+ const content = readFileSync4(eventsPath, "utf-8");
43414
43448
  const lines = content.split(`
43415
43449
  `).filter((line) => line.trim() !== "");
43416
43450
  let malformedCount = 0;
@@ -43451,7 +43485,7 @@ async function checkSteeringDirectives(directory) {
43451
43485
  };
43452
43486
  }
43453
43487
  try {
43454
- const content = readFileSync3(eventsPath, "utf-8");
43488
+ const content = readFileSync4(eventsPath, "utf-8");
43455
43489
  const lines = content.split(`
43456
43490
  `).filter((line) => line.trim() !== "");
43457
43491
  const directivesIssued = [];
@@ -44419,7 +44453,7 @@ async function handleHistoryCommand(directory, _args) {
44419
44453
  }
44420
44454
  // src/hooks/knowledge-migrator.ts
44421
44455
  import { randomUUID as randomUUID2 } from "crypto";
44422
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
44456
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
44423
44457
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
44424
44458
  import * as path16 from "path";
44425
44459
 
@@ -44976,7 +45010,7 @@ function inferProjectName(directory) {
44976
45010
  const packageJsonPath = path16.join(directory, "package.json");
44977
45011
  if (existsSync8(packageJsonPath)) {
44978
45012
  try {
44979
- const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
45013
+ const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
44980
45014
  if (pkg.name && typeof pkg.name === "string") {
44981
45015
  return pkg.name;
44982
45016
  }
@@ -47161,7 +47195,7 @@ function shouldMaskToolOutput(msg, index, totalMessages, recentWindowSize, thres
47161
47195
  return false;
47162
47196
  }
47163
47197
  const toolName = extractToolName(text);
47164
- if (toolName && ["retrieve_summary", "task"].includes(toolName.toLowerCase())) {
47198
+ if (toolName && ["retrieve_summary", "task", "read"].includes(toolName.toLowerCase())) {
47165
47199
  return false;
47166
47200
  }
47167
47201
  const age = totalMessages - 1 - index;
@@ -47235,6 +47269,24 @@ function createDelegationGateHook(config3) {
47235
47269
  session.qaSkipCount = 0;
47236
47270
  session.qaSkipTaskIds = [];
47237
47271
  }
47272
+ if (hasReviewer && session.taskWorkflowStates) {
47273
+ for (const [taskId, state] of session.taskWorkflowStates) {
47274
+ if (state === "coder_delegated" || state === "pre_check_passed") {
47275
+ try {
47276
+ advanceTaskState(session, taskId, "reviewer_run");
47277
+ } catch {}
47278
+ }
47279
+ }
47280
+ }
47281
+ if (hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
47282
+ for (const [taskId, state] of session.taskWorkflowStates) {
47283
+ if (state === "reviewer_run") {
47284
+ try {
47285
+ advanceTaskState(session, taskId, "tests_run");
47286
+ } catch {}
47287
+ }
47288
+ }
47289
+ }
47238
47290
  }
47239
47291
  }
47240
47292
  };
@@ -47543,7 +47595,9 @@ function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
47543
47595
  if (!isArchitect && guardrailsEnabled) {
47544
47596
  beginInvocation(input.sessionID, agentName);
47545
47597
  }
47546
- if (config3.hooks?.delegation_tracker === true && previousAgent && previousAgent !== agentName) {
47598
+ const delegationTrackerEnabled = config3.hooks?.delegation_tracker === true;
47599
+ const delegationGateEnabled = config3.hooks?.delegation_gate !== false;
47600
+ if ((delegationTrackerEnabled || delegationGateEnabled) && previousAgent && previousAgent !== agentName) {
47547
47601
  const entry = {
47548
47602
  from: previousAgent,
47549
47603
  to: agentName,
@@ -47554,7 +47608,9 @@ function createDelegationTrackerHook(config3, guardrailsEnabled = true) {
47554
47608
  }
47555
47609
  const chain = swarmState.delegationChains.get(input.sessionID);
47556
47610
  chain?.push(entry);
47557
- swarmState.pendingEvents++;
47611
+ if (delegationTrackerEnabled) {
47612
+ swarmState.pendingEvents++;
47613
+ }
47558
47614
  }
47559
47615
  };
47560
47616
  }
@@ -47725,13 +47781,14 @@ function createGuardrailsHooks(directoryOrConfig, config3) {
47725
47781
  if (typeof targetPath === "string" && targetPath.length > 0) {
47726
47782
  const resolvedTarget = path25.resolve(directory, targetPath).toLowerCase();
47727
47783
  const planMdPath = path25.resolve(directory, ".swarm", "plan.md").toLowerCase();
47728
- if (resolvedTarget === planMdPath) {
47729
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47784
+ const planJsonPath = path25.resolve(directory, ".swarm", "plan.json").toLowerCase();
47785
+ if (resolvedTarget === planMdPath || resolvedTarget === planJsonPath) {
47786
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47730
47787
  }
47731
47788
  }
47732
47789
  if (!targetPath && (input.tool === "apply_patch" || input.tool === "patch")) {
47733
47790
  const patchText = args2?.input ?? args2?.patch ?? (Array.isArray(args2?.cmd) ? args2.cmd[1] : undefined);
47734
- if (typeof patchText === "string") {
47791
+ if (typeof patchText === "string" && patchText.length <= 1e6) {
47735
47792
  const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
47736
47793
  const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
47737
47794
  const paths = new Set;
@@ -47743,11 +47800,41 @@ function createGuardrailsHooks(directoryOrConfig, config3) {
47743
47800
  if (p !== "/dev/null")
47744
47801
  paths.add(p);
47745
47802
  }
47803
+ const gitDiffPathPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
47804
+ for (const match of patchText.matchAll(gitDiffPathPattern)) {
47805
+ const aPath = match[1].trim();
47806
+ const bPath = match[2].trim();
47807
+ if (aPath !== "/dev/null")
47808
+ paths.add(aPath);
47809
+ if (bPath !== "/dev/null")
47810
+ paths.add(bPath);
47811
+ }
47812
+ const minusPathPattern = /^---\s+a\/(.+)$/gm;
47813
+ for (const match of patchText.matchAll(minusPathPattern)) {
47814
+ const p = match[1].trim();
47815
+ if (p !== "/dev/null")
47816
+ paths.add(p);
47817
+ }
47818
+ const traditionalMinusPattern = /^---\s+([^\s].+?)(?:\t.*)?$/gm;
47819
+ const traditionalPlusPattern = /^\+\+\+\s+([^\s].+?)(?:\t.*)?$/gm;
47820
+ for (const match of patchText.matchAll(traditionalMinusPattern)) {
47821
+ const p = match[1].trim();
47822
+ if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
47823
+ paths.add(p);
47824
+ }
47825
+ }
47826
+ for (const match of patchText.matchAll(traditionalPlusPattern)) {
47827
+ const p = match[1].trim();
47828
+ if (p !== "/dev/null" && !p.startsWith("a/") && !p.startsWith("b/")) {
47829
+ paths.add(p);
47830
+ }
47831
+ }
47746
47832
  for (const p of paths) {
47747
47833
  const resolvedP = path25.resolve(directory, p);
47748
- const planMdPath = path25.resolve(directory, ".swarm", "plan.md");
47749
- if (resolvedP.toLowerCase() === planMdPath.toLowerCase()) {
47750
- throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47834
+ const planMdPath = path25.resolve(directory, ".swarm", "plan.md").toLowerCase();
47835
+ const planJsonPath = path25.resolve(directory, ".swarm", "plan.json").toLowerCase();
47836
+ if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
47837
+ throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use update_task_status() to mark tasks complete, " + "phase_complete() for phase transitions, or " + "save_plan to create/restructure plans.");
47751
47838
  }
47752
47839
  if (isOutsideSwarmDir(p, directory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
47753
47840
  const session2 = swarmState.agentSessions.get(input.sessionID);
@@ -47937,6 +48024,26 @@ function createGuardrailsHooks(directoryOrConfig, config3) {
47937
48024
  };
47938
48025
  } else {
47939
48026
  session.lastGateFailure = null;
48027
+ if (input.tool === "pre_check_batch") {
48028
+ const successStr = typeof output.output === "string" ? output.output : "";
48029
+ let isPassed = false;
48030
+ try {
48031
+ const result = JSON.parse(successStr);
48032
+ isPassed = result.gates_passed === true;
48033
+ } catch {
48034
+ isPassed = false;
48035
+ }
48036
+ if (isPassed && session.currentTaskId) {
48037
+ try {
48038
+ advanceTaskState(session, session.currentTaskId, "pre_check_passed");
48039
+ } catch (err2) {
48040
+ warn("Failed to advance task state after pre_check_batch pass", {
48041
+ taskId: session.currentTaskId,
48042
+ error: String(err2)
48043
+ });
48044
+ }
48045
+ }
48046
+ }
47940
48047
  }
47941
48048
  }
47942
48049
  const inputArgs = inputArgsByCallID.get(input.callID);
@@ -49937,7 +50044,11 @@ function createToolSummarizerHook(config3, directory) {
49937
50044
  if (typeof output.output !== "string" || output.output.length === 0) {
49938
50045
  return;
49939
50046
  }
49940
- const exemptTools = config3.exempt_tools ?? ["retrieve_summary", "task"];
50047
+ const exemptTools = config3.exempt_tools ?? [
50048
+ "retrieve_summary",
50049
+ "task",
50050
+ "read"
50051
+ ];
49941
50052
  if (exemptTools.includes(input.tool)) {
49942
50053
  return;
49943
50054
  }
@@ -57119,10 +57230,14 @@ var RETRIEVE_MAX_BYTES = 10 * 1024 * 1024;
57119
57230
  var retrieve_summary = tool({
57120
57231
  description: "Retrieve the full content of a stored tool output summary by its ID (e.g. S1, S2). Use this when a prior tool output was summarized and you need the full content.",
57121
57232
  args: {
57122
- id: tool.schema.string().describe("The summary ID to retrieve (e.g. S1, S2, S99). Must match pattern S followed by digits.")
57233
+ id: tool.schema.string().describe("The summary ID to retrieve (e.g. S1, S2, S99). Must match pattern S followed by digits."),
57234
+ offset: tool.schema.number().min(0).default(0).describe("Line offset to start from (default: 0)."),
57235
+ limit: tool.schema.number().min(1).max(500).default(100).describe("Number of lines to return (default: 100, max: 500).")
57123
57236
  },
57124
57237
  async execute(args2, context) {
57125
57238
  const directory = context.directory;
57239
+ const offset = args2.offset ?? 0;
57240
+ const limit = Math.min(args2.limit ?? 100, 500);
57126
57241
  let sanitizedId;
57127
57242
  try {
57128
57243
  sanitizedId = sanitizeSummaryId(args2.id);
@@ -57141,13 +57256,46 @@ var retrieve_summary = tool({
57141
57256
  if (fullOutput.length > RETRIEVE_MAX_BYTES) {
57142
57257
  return `Error: summary content exceeds maximum size limit (10 MB).`;
57143
57258
  }
57144
- return fullOutput;
57259
+ if (fullOutput.length === 0) {
57260
+ return `--- No content (0 lines) ---
57261
+
57262
+ (Summary is empty)`;
57263
+ }
57264
+ const lines = fullOutput.split(`
57265
+ `);
57266
+ const totalLines = lines.length;
57267
+ const clampedOffset = Math.max(0, offset);
57268
+ if (clampedOffset >= totalLines) {
57269
+ const response2 = `--- Offset beyond range ---
57270
+
57271
+ (Range exhausted. Valid offset range: 0-${totalLines - 1})
57272
+ (Content has ${totalLines} line${totalLines === 1 ? "" : "s"})`;
57273
+ return response2;
57274
+ }
57275
+ const startLine = Math.min(clampedOffset, totalLines);
57276
+ const endLine = Math.min(startLine + limit, totalLines);
57277
+ const paginatedLines = lines.slice(startLine, endLine);
57278
+ const paginatedContent = paginatedLines.join(`
57279
+ `);
57280
+ const headerStart = startLine + 1;
57281
+ const headerEnd = endLine;
57282
+ const rangeHeader = `--- Lines ${headerStart}-${headerEnd} of ${totalLines} ---`;
57283
+ let response = `${rangeHeader}
57284
+ ${paginatedContent}`;
57285
+ if (endLine < totalLines) {
57286
+ const remaining = totalLines - endLine;
57287
+ response += `
57288
+
57289
+ ... ${remaining} more line${remaining === 1 ? "" : "s"}. Use offset=${endLine} to retrieve more.`;
57290
+ }
57291
+ return response;
57145
57292
  }
57146
57293
  });
57147
57294
  // src/tools/save-plan.ts
57148
57295
  init_tool();
57149
57296
  init_manager2();
57150
57297
  init_create_tool();
57298
+ import * as fs28 from "fs";
57151
57299
  import * as path39 from "path";
57152
57300
  function detectPlaceholderContent(args2) {
57153
57301
  const issues = [];
@@ -57182,12 +57330,33 @@ function validateTargetWorkspace(target, source) {
57182
57330
  return;
57183
57331
  }
57184
57332
  async function executeSavePlan(args2, fallbackDir) {
57333
+ const validationErrors = [];
57334
+ for (const phase of args2.phases) {
57335
+ if (!Number.isInteger(phase.id) || phase.id <= 0) {
57336
+ validationErrors.push(`Phase ${phase.id} has invalid id: must be a positive integer`);
57337
+ }
57338
+ const taskIdPattern = /^\d+\.\d+(\.\d+)*$/;
57339
+ for (const task of phase.tasks) {
57340
+ if (!taskIdPattern.test(task.id)) {
57341
+ validationErrors.push(`Task '${task.id}' in phase ${phase.id} has invalid id format: must match N.M pattern (e.g. '1.1', '2.3')`);
57342
+ }
57343
+ }
57344
+ }
57345
+ if (validationErrors.length > 0) {
57346
+ return {
57347
+ success: false,
57348
+ message: "Plan rejected: invalid phase or task IDs",
57349
+ errors: validationErrors,
57350
+ recovery_guidance: "Use save_plan with corrected inputs to create or restructure plans. Never write .swarm/plan.json or .swarm/plan.md directly."
57351
+ };
57352
+ }
57185
57353
  const placeholderIssues = detectPlaceholderContent(args2);
57186
57354
  if (placeholderIssues.length > 0) {
57187
57355
  return {
57188
57356
  success: false,
57189
57357
  message: "Plan rejected: contains template placeholder content",
57190
- errors: placeholderIssues
57358
+ errors: placeholderIssues,
57359
+ recovery_guidance: "Use save_plan with corrected inputs to create or restructure plans. Never write .swarm/plan.json or .swarm/plan.md directly."
57191
57360
  };
57192
57361
  }
57193
57362
  const targetWorkspace = args2.working_directory ?? fallbackDir;
@@ -57195,8 +57364,9 @@ async function executeSavePlan(args2, fallbackDir) {
57195
57364
  if (workspaceError) {
57196
57365
  return {
57197
57366
  success: false,
57198
- message: "Target workspace validation failed",
57199
- errors: [workspaceError]
57367
+ message: "Target workspace validation failed: provide working_directory parameter to save_plan",
57368
+ errors: [workspaceError],
57369
+ recovery_guidance: "Use save_plan with corrected inputs to create or restructure plans. Never write .swarm/plan.json or .swarm/plan.md directly."
57200
57370
  };
57201
57371
  }
57202
57372
  const plan = {
@@ -57229,6 +57399,16 @@ async function executeSavePlan(args2, fallbackDir) {
57229
57399
  const dir = targetWorkspace;
57230
57400
  try {
57231
57401
  await savePlan(dir, plan);
57402
+ try {
57403
+ const markerPath = path39.join(dir, ".swarm", ".plan-write-marker");
57404
+ const marker = JSON.stringify({
57405
+ source: "save_plan",
57406
+ timestamp: new Date().toISOString(),
57407
+ phases_count: plan.phases.length,
57408
+ tasks_count: tasksCount
57409
+ });
57410
+ await fs28.promises.writeFile(markerPath, marker, "utf8");
57411
+ } catch {}
57232
57412
  return {
57233
57413
  success: true,
57234
57414
  message: "Plan saved successfully",
@@ -57239,8 +57419,9 @@ async function executeSavePlan(args2, fallbackDir) {
57239
57419
  } catch (error93) {
57240
57420
  return {
57241
57421
  success: false,
57242
- message: "Failed to save plan",
57243
- errors: [String(error93)]
57422
+ message: "Failed to save plan: retry with save_plan after resolving the error above",
57423
+ errors: [String(error93)],
57424
+ recovery_guidance: "Use save_plan with corrected inputs to create or restructure plans. Never write .swarm/plan.json or .swarm/plan.md directly."
57244
57425
  };
57245
57426
  }
57246
57427
  }
@@ -57269,7 +57450,7 @@ var save_plan = createSwarmTool({
57269
57450
  // src/tools/sbom-generate.ts
57270
57451
  init_dist();
57271
57452
  init_manager();
57272
- import * as fs28 from "fs";
57453
+ import * as fs29 from "fs";
57273
57454
  import * as path40 from "path";
57274
57455
 
57275
57456
  // src/sbom/detectors/dart.ts
@@ -58116,7 +58297,7 @@ function findManifestFiles(rootDir) {
58116
58297
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
58117
58298
  function searchDir(dir) {
58118
58299
  try {
58119
- const entries = fs28.readdirSync(dir, { withFileTypes: true });
58300
+ const entries = fs29.readdirSync(dir, { withFileTypes: true });
58120
58301
  for (const entry of entries) {
58121
58302
  const fullPath = path40.join(dir, entry.name);
58122
58303
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
@@ -58145,7 +58326,7 @@ function findManifestFilesInDirs(directories, workingDir) {
58145
58326
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
58146
58327
  for (const dir of directories) {
58147
58328
  try {
58148
- const entries = fs28.readdirSync(dir, { withFileTypes: true });
58329
+ const entries = fs29.readdirSync(dir, { withFileTypes: true });
58149
58330
  for (const entry of entries) {
58150
58331
  const fullPath = path40.join(dir, entry.name);
58151
58332
  if (entry.isFile()) {
@@ -58183,7 +58364,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
58183
58364
  }
58184
58365
  function ensureOutputDir(outputDir) {
58185
58366
  try {
58186
- fs28.mkdirSync(outputDir, { recursive: true });
58367
+ fs29.mkdirSync(outputDir, { recursive: true });
58187
58368
  } catch (error93) {
58188
58369
  if (!error93 || error93.code !== "EEXIST") {
58189
58370
  throw error93;
@@ -58276,10 +58457,10 @@ var sbom_generate = createSwarmTool({
58276
58457
  for (const manifestFile of manifestFiles) {
58277
58458
  try {
58278
58459
  const fullPath = path40.isAbsolute(manifestFile) ? manifestFile : path40.join(workingDir, manifestFile);
58279
- if (!fs28.existsSync(fullPath)) {
58460
+ if (!fs29.existsSync(fullPath)) {
58280
58461
  continue;
58281
58462
  }
58282
- const content = fs28.readFileSync(fullPath, "utf-8");
58463
+ const content = fs29.readFileSync(fullPath, "utf-8");
58283
58464
  const components = detectComponents(manifestFile, content);
58284
58465
  processedFiles.push(manifestFile);
58285
58466
  if (components.length > 0) {
@@ -58293,7 +58474,7 @@ var sbom_generate = createSwarmTool({
58293
58474
  const bomJson = serializeCycloneDX(bom);
58294
58475
  const filename = generateSbomFilename();
58295
58476
  const outputPath = path40.join(outputDir, filename);
58296
- fs28.writeFileSync(outputPath, bomJson, "utf-8");
58477
+ fs29.writeFileSync(outputPath, bomJson, "utf-8");
58297
58478
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
58298
58479
  try {
58299
58480
  const timestamp = new Date().toISOString();
@@ -58335,7 +58516,7 @@ var sbom_generate = createSwarmTool({
58335
58516
  // src/tools/schema-drift.ts
58336
58517
  init_dist();
58337
58518
  init_create_tool();
58338
- import * as fs29 from "fs";
58519
+ import * as fs30 from "fs";
58339
58520
  import * as path41 from "path";
58340
58521
  var SPEC_CANDIDATES = [
58341
58522
  "openapi.json",
@@ -58377,19 +58558,19 @@ function discoverSpecFile(cwd, specFileArg) {
58377
58558
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
58378
58559
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
58379
58560
  }
58380
- const stats = fs29.statSync(resolvedPath);
58561
+ const stats = fs30.statSync(resolvedPath);
58381
58562
  if (stats.size > MAX_SPEC_SIZE) {
58382
58563
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
58383
58564
  }
58384
- if (!fs29.existsSync(resolvedPath)) {
58565
+ if (!fs30.existsSync(resolvedPath)) {
58385
58566
  throw new Error(`Spec file not found: ${resolvedPath}`);
58386
58567
  }
58387
58568
  return resolvedPath;
58388
58569
  }
58389
58570
  for (const candidate of SPEC_CANDIDATES) {
58390
58571
  const candidatePath = path41.resolve(cwd, candidate);
58391
- if (fs29.existsSync(candidatePath)) {
58392
- const stats = fs29.statSync(candidatePath);
58572
+ if (fs30.existsSync(candidatePath)) {
58573
+ const stats = fs30.statSync(candidatePath);
58393
58574
  if (stats.size <= MAX_SPEC_SIZE) {
58394
58575
  return candidatePath;
58395
58576
  }
@@ -58398,7 +58579,7 @@ function discoverSpecFile(cwd, specFileArg) {
58398
58579
  return null;
58399
58580
  }
58400
58581
  function parseSpec(specFile) {
58401
- const content = fs29.readFileSync(specFile, "utf-8");
58582
+ const content = fs30.readFileSync(specFile, "utf-8");
58402
58583
  const ext = path41.extname(specFile).toLowerCase();
58403
58584
  if (ext === ".json") {
58404
58585
  return parseJsonSpec(content);
@@ -58465,7 +58646,7 @@ function extractRoutes(cwd) {
58465
58646
  function walkDir(dir) {
58466
58647
  let entries;
58467
58648
  try {
58468
- entries = fs29.readdirSync(dir, { withFileTypes: true });
58649
+ entries = fs30.readdirSync(dir, { withFileTypes: true });
58469
58650
  } catch {
58470
58651
  return;
58471
58652
  }
@@ -58498,7 +58679,7 @@ function extractRoutes(cwd) {
58498
58679
  }
58499
58680
  function extractRoutesFromFile(filePath) {
58500
58681
  const routes = [];
58501
- const content = fs29.readFileSync(filePath, "utf-8");
58682
+ const content = fs30.readFileSync(filePath, "utf-8");
58502
58683
  const lines = content.split(/\r?\n/);
58503
58684
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
58504
58685
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -58649,7 +58830,7 @@ init_secretscan();
58649
58830
  // src/tools/symbols.ts
58650
58831
  init_tool();
58651
58832
  init_create_tool();
58652
- import * as fs30 from "fs";
58833
+ import * as fs31 from "fs";
58653
58834
  import * as path42 from "path";
58654
58835
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
58655
58836
  var WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
@@ -58680,8 +58861,8 @@ function containsWindowsAttacks(str) {
58680
58861
  function isPathInWorkspace(filePath, workspace) {
58681
58862
  try {
58682
58863
  const resolvedPath = path42.resolve(workspace, filePath);
58683
- const realWorkspace = fs30.realpathSync(workspace);
58684
- const realResolvedPath = fs30.realpathSync(resolvedPath);
58864
+ const realWorkspace = fs31.realpathSync(workspace);
58865
+ const realResolvedPath = fs31.realpathSync(resolvedPath);
58685
58866
  const relativePath = path42.relative(realWorkspace, realResolvedPath);
58686
58867
  if (relativePath.startsWith("..") || path42.isAbsolute(relativePath)) {
58687
58868
  return false;
@@ -58701,11 +58882,11 @@ function extractTSSymbols(filePath, cwd) {
58701
58882
  }
58702
58883
  let content;
58703
58884
  try {
58704
- const stats = fs30.statSync(fullPath);
58885
+ const stats = fs31.statSync(fullPath);
58705
58886
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
58706
58887
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
58707
58888
  }
58708
- content = fs30.readFileSync(fullPath, "utf-8");
58889
+ content = fs31.readFileSync(fullPath, "utf-8");
58709
58890
  } catch {
58710
58891
  return [];
58711
58892
  }
@@ -58853,11 +59034,11 @@ function extractPythonSymbols(filePath, cwd) {
58853
59034
  }
58854
59035
  let content;
58855
59036
  try {
58856
- const stats = fs30.statSync(fullPath);
59037
+ const stats = fs31.statSync(fullPath);
58857
59038
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
58858
59039
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
58859
59040
  }
58860
- content = fs30.readFileSync(fullPath, "utf-8");
59041
+ content = fs31.readFileSync(fullPath, "utf-8");
58861
59042
  } catch {
58862
59043
  return [];
58863
59044
  }
@@ -59000,7 +59181,7 @@ init_test_runner();
59000
59181
  // src/tools/todo-extract.ts
59001
59182
  init_dist();
59002
59183
  init_create_tool();
59003
- import * as fs31 from "fs";
59184
+ import * as fs32 from "fs";
59004
59185
  import * as path43 from "path";
59005
59186
  var MAX_TEXT_LENGTH = 200;
59006
59187
  var MAX_FILE_SIZE_BYTES8 = 1024 * 1024;
@@ -59096,7 +59277,7 @@ function isSupportedExtension(filePath) {
59096
59277
  function findSourceFiles3(dir, files = []) {
59097
59278
  let entries;
59098
59279
  try {
59099
- entries = fs31.readdirSync(dir);
59280
+ entries = fs32.readdirSync(dir);
59100
59281
  } catch {
59101
59282
  return files;
59102
59283
  }
@@ -59108,7 +59289,7 @@ function findSourceFiles3(dir, files = []) {
59108
59289
  const fullPath = path43.join(dir, entry);
59109
59290
  let stat2;
59110
59291
  try {
59111
- stat2 = fs31.statSync(fullPath);
59292
+ stat2 = fs32.statSync(fullPath);
59112
59293
  } catch {
59113
59294
  continue;
59114
59295
  }
@@ -59201,7 +59382,7 @@ var todo_extract = createSwarmTool({
59201
59382
  return JSON.stringify(errorResult, null, 2);
59202
59383
  }
59203
59384
  const scanPath = resolvedPath;
59204
- if (!fs31.existsSync(scanPath)) {
59385
+ if (!fs32.existsSync(scanPath)) {
59205
59386
  const errorResult = {
59206
59387
  error: `path not found: ${pathsInput}`,
59207
59388
  total: 0,
@@ -59211,7 +59392,7 @@ var todo_extract = createSwarmTool({
59211
59392
  return JSON.stringify(errorResult, null, 2);
59212
59393
  }
59213
59394
  const filesToScan = [];
59214
- const stat2 = fs31.statSync(scanPath);
59395
+ const stat2 = fs32.statSync(scanPath);
59215
59396
  if (stat2.isFile()) {
59216
59397
  if (isSupportedExtension(scanPath)) {
59217
59398
  filesToScan.push(scanPath);
@@ -59230,11 +59411,11 @@ var todo_extract = createSwarmTool({
59230
59411
  const allEntries = [];
59231
59412
  for (const filePath of filesToScan) {
59232
59413
  try {
59233
- const fileStat = fs31.statSync(filePath);
59414
+ const fileStat = fs32.statSync(filePath);
59234
59415
  if (fileStat.size > MAX_FILE_SIZE_BYTES8) {
59235
59416
  continue;
59236
59417
  }
59237
- const content = fs31.readFileSync(filePath, "utf-8");
59418
+ const content = fs32.readFileSync(filePath, "utf-8");
59238
59419
  const entries = parseTodoComments(content, filePath, tagsSet);
59239
59420
  allEntries.push(...entries);
59240
59421
  } catch {}
@@ -59262,7 +59443,7 @@ var todo_extract = createSwarmTool({
59262
59443
  // src/tools/update-task-status.ts
59263
59444
  init_tool();
59264
59445
  init_manager2();
59265
- import * as fs32 from "fs";
59446
+ import * as fs33 from "fs";
59266
59447
  import * as path44 from "path";
59267
59448
  init_create_tool();
59268
59449
  var VALID_STATUSES = [
@@ -59295,9 +59476,15 @@ function checkReviewerGate(taskId) {
59295
59476
  return { blocked: false, reason: "" };
59296
59477
  }
59297
59478
  }
59479
+ const stateEntries = [];
59480
+ for (const [sessionId, session] of swarmState.agentSessions) {
59481
+ const state = getTaskState(session, taskId);
59482
+ stateEntries.push(`${sessionId}: ${state}`);
59483
+ }
59484
+ const currentStateStr = stateEntries.length > 0 ? stateEntries.join(", ") : "no active sessions";
59298
59485
  return {
59299
59486
  blocked: true,
59300
- reason: `Task ${taskId} has not passed QA gates (state machine requires tests_run or complete, current state indicates gates not yet passed). Call mega_reviewer and mega_test_engineer before marking task as completed.`
59487
+ reason: `Task ${taskId} has not passed QA gates. Current state: [${currentStateStr}]. Required state: tests_run or complete. Do not write directly to plan files \u2014 use update_task_status after running mega_reviewer and mega_test_engineer.`
59301
59488
  };
59302
59489
  } catch {
59303
59490
  return { blocked: false, reason: "" };
@@ -59320,6 +59507,16 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
59320
59507
  errors: [taskIdError]
59321
59508
  };
59322
59509
  }
59510
+ if (args2.status === "in_progress") {
59511
+ for (const [_sessionId, session] of swarmState.agentSessions) {
59512
+ const currentState = getTaskState(session, args2.task_id);
59513
+ if (currentState === "idle") {
59514
+ try {
59515
+ advanceTaskState(session, args2.task_id, "coder_delegated");
59516
+ } catch {}
59517
+ }
59518
+ }
59519
+ }
59323
59520
  if (args2.status === "completed") {
59324
59521
  const reviewerCheck = checkReviewerGate(args2.task_id);
59325
59522
  if (reviewerCheck.blocked) {
@@ -59360,9 +59557,9 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
59360
59557
  }
59361
59558
  const resolvedDir = path44.resolve(normalizedDir);
59362
59559
  try {
59363
- const realPath = fs32.realpathSync(resolvedDir);
59560
+ const realPath = fs33.realpathSync(resolvedDir);
59364
59561
  const planPath = path44.join(realPath, ".swarm", "plan.json");
59365
- if (!fs32.existsSync(planPath)) {
59562
+ if (!fs33.existsSync(planPath)) {
59366
59563
  return {
59367
59564
  success: false,
59368
59565
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -32,6 +32,7 @@ export interface SavePlanResult {
32
32
  phases_count?: number;
33
33
  tasks_count?: number;
34
34
  errors?: string[];
35
+ recovery_guidance?: string;
35
36
  }
36
37
  /**
37
38
  * Detect template placeholder content (e.g., [task], [Project], [description], [N]).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "6.21.0",
3
+ "version": "6.21.2",
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",