opencode-swarm 7.98.0 → 7.98.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.
@@ -6,10 +6,10 @@ import {
6
6
  loadPlanJsonOnly,
7
7
  mergeDurableGateEntriesFromEvidence,
8
8
  readDurableGateEvidence
9
- } from "./index-4c5jpmn9.js";
9
+ } from "./index-yrzfbqqh.js";
10
10
  import"./index-mh1ej70w.js";
11
11
  import"./index-kv4dd5c5.js";
12
- import"./index-gn8n22th.js";
12
+ import"./index-we94wkty.js";
13
13
  import"./index-jtqkh8jf.js";
14
14
  import"./index-5e4e2hvv.js";
15
15
  import"./index-p0arc26j.js";
@@ -1,21 +1,21 @@
1
1
  // @bun
2
2
  import {
3
3
  handleGuardrailExplain
4
- } from "./index-wmm21nsk.js";
5
- import"./index-pc10e4d7.js";
4
+ } from "./index-rvweaxsd.js";
5
+ import"./index-72k57f21.js";
6
6
  import"./index-5z2e78tv.js";
7
7
  import"./index-2a6ppa65.js";
8
8
  import"./index-fjxjb66n.js";
9
9
  import"./index-hb10a2g8.js";
10
10
  import"./index-zy22fg5h.js";
11
11
  import"./index-471qxz9g.js";
12
- import"./index-4c5jpmn9.js";
12
+ import"./index-yrzfbqqh.js";
13
13
  import"./index-adz3nk9b.js";
14
14
  import"./index-v4fcn4tr.js";
15
15
  import"./index-mh1ej70w.js";
16
16
  import"./index-kv4dd5c5.js";
17
- import"./index-sf08zj91.js";
18
- import"./index-gn8n22th.js";
17
+ import"./index-a0dv2c7k.js";
18
+ import"./index-we94wkty.js";
19
19
  import"./index-jtqkh8jf.js";
20
20
  import"./index-5e4e2hvv.js";
21
21
  import"./index-p0arc26j.js";
@@ -114,7 +114,7 @@ import {
114
114
  transientBackoff,
115
115
  validateProjectRoot,
116
116
  writeProjectedSpecSync
117
- } from "./index-4c5jpmn9.js";
117
+ } from "./index-yrzfbqqh.js";
118
118
  import {
119
119
  _internals as _internals2,
120
120
  _internals1 as _internals3,
@@ -138,12 +138,12 @@ import {
138
138
  listActive,
139
139
  subscribe,
140
140
  unsubscribe
141
- } from "./index-sf08zj91.js";
141
+ } from "./index-a0dv2c7k.js";
142
142
  import {
143
143
  readSwarmFileAsync,
144
144
  safeHook,
145
145
  validateSwarmPath
146
- } from "./index-gn8n22th.js";
146
+ } from "./index-we94wkty.js";
147
147
  import {
148
148
  deepMerge
149
149
  } from "./index-p0arc26j.js";
@@ -909,7 +909,7 @@ var init_executor = __esm(() => {
909
909
  // package.json
910
910
  var package_default = {
911
911
  name: "opencode-swarm",
912
- version: "7.98.0",
912
+ version: "7.98.2",
913
913
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
914
914
  main: "dist/index.js",
915
915
  types: "dist/index.d.ts",
@@ -5474,6 +5474,9 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000, dire
5474
5474
  swarmState.pendingRehydrations.add(rehydrationPromise);
5475
5475
  }
5476
5476
  }
5477
+ function endAgentSession(sessionId) {
5478
+ swarmState.agentSessions.delete(sessionId);
5479
+ }
5477
5480
  function getAgentSession(sessionId) {
5478
5481
  return swarmState.agentSessions.get(sessionId);
5479
5482
  }
@@ -5978,7 +5981,7 @@ function hasActiveEpicMode(sessionID) {
5978
5981
  async function rehydratePrSubscriptions(sessionID, directory) {
5979
5982
  const map = new Map;
5980
5983
  try {
5981
- const { listActive: listActive2 } = await import("./pr-subscriptions-2565fpsc.js");
5984
+ const { listActive: listActive2 } = await import("./pr-subscriptions-ttvr1ta9.js");
5982
5985
  const records = await listActive2(directory);
5983
5986
  for (const record of records) {
5984
5987
  if (record.sessionID !== sessionID)
@@ -10193,48 +10196,49 @@ function recordSeenRetroSection(key, value, timestamp) {
10193
10196
  function hashContent(content) {
10194
10197
  return createHash3("sha1").update(content).digest("hex");
10195
10198
  }
10196
- function isWriteToSwarmPlan(input) {
10197
- if (typeof input !== "object" || input === null)
10198
- return false;
10199
- const record = input;
10200
- const toolName = record.toolName;
10201
- if (typeof toolName !== "string")
10202
- return false;
10203
- if (!["write", "edit", "apply_patch", "swarm_apply_patch"].includes(toolName))
10204
- return false;
10205
- const rawPath = record.path;
10206
- const rawFile = record.file;
10207
- const pathField = typeof rawPath === "string" ? rawPath.replace(/\\/g, "/") : undefined;
10208
- const fileField = typeof rawFile === "string" ? rawFile.replace(/\\/g, "/") : undefined;
10209
- if (typeof pathField === "string" && pathField.includes(".swarm/plan.md")) {
10210
- return true;
10211
- }
10212
- if (typeof fileField === "string" && fileField.includes(".swarm/plan.md")) {
10213
- return true;
10214
- }
10215
- return false;
10216
- }
10217
10199
  function isWriteToEvidenceFile(input) {
10218
- if (typeof input !== "object" || input === null)
10219
- return false;
10220
- const record = input;
10221
- const toolName = record.toolName;
10222
- if (typeof toolName !== "string")
10223
- return false;
10224
- if (!["write", "edit", "apply_patch", "swarm_apply_patch"].includes(toolName))
10200
+ const trigger = normalizeWriteTrigger(input);
10201
+ return isEvidencePath(trigger?.filePath);
10202
+ }
10203
+ var WRITE_TOOLS = new Set([
10204
+ "write",
10205
+ "edit",
10206
+ "apply_patch",
10207
+ "swarm_apply_patch"
10208
+ ]);
10209
+ function isRecord(value) {
10210
+ return typeof value === "object" && value !== null;
10211
+ }
10212
+ function normalizePathField(value) {
10213
+ return typeof value === "string" ? value.replace(/\\/g, "/") : null;
10214
+ }
10215
+ function firstPathFromRecord(record) {
10216
+ return normalizePathField(record.path) ?? normalizePathField(record.filePath) ?? normalizePathField(record.file);
10217
+ }
10218
+ function normalizeWriteTrigger(input, output) {
10219
+ if (!isRecord(input))
10220
+ return null;
10221
+ const toolName = typeof input.toolName === "string" ? input.toolName : typeof input.tool === "string" ? input.tool : null;
10222
+ if (!toolName || !WRITE_TOOLS.has(toolName))
10223
+ return null;
10224
+ const inputArgs = isRecord(input.args) ? input.args : null;
10225
+ const outputArgs = isRecord(output) && isRecord(output.args) ? output.args : null;
10226
+ const filePath = firstPathFromRecord(input) ?? (inputArgs ? firstPathFromRecord(inputArgs) : null) ?? (outputArgs ? firstPathFromRecord(outputArgs) : null);
10227
+ if (!filePath)
10228
+ return null;
10229
+ return {
10230
+ toolName,
10231
+ filePath,
10232
+ sessionID: typeof input.sessionID === "string" ? input.sessionID : "default"
10233
+ };
10234
+ }
10235
+ function isEvidencePath(filePath) {
10236
+ if (!filePath)
10225
10237
  return false;
10226
- const rawPath = record.path;
10227
- const rawFile = record.file;
10228
- const pathField = typeof rawPath === "string" ? rawPath.replace(/\\/g, "/") : undefined;
10229
- const fileField = typeof rawFile === "string" ? rawFile.replace(/\\/g, "/") : undefined;
10230
- const evidenceRegex = /\.swarm\/+evidence\/+/i;
10231
- if (typeof pathField === "string" && evidenceRegex.test(pathField)) {
10232
- return true;
10233
- }
10234
- if (typeof fileField === "string" && evidenceRegex.test(fileField)) {
10235
- return true;
10236
- }
10237
- return false;
10238
+ return /(?:^|\/)\.swarm\/+evidence\//i.test(filePath);
10239
+ }
10240
+ function isPlanPath(filePath) {
10241
+ return filePath?.includes(".swarm/plan.md") ?? false;
10238
10242
  }
10239
10243
  function extractRetrospectiveSection(planContent) {
10240
10244
  const headingRegex = /^###\s+Lessons\s+Learned$/m;
@@ -10941,24 +10945,22 @@ async function runAutoPromotion(directory, config) {
10941
10945
  }
10942
10946
  }
10943
10947
  function createKnowledgeCuratorHook(directory, config, options = {}) {
10944
- const handler = async (input, _output) => {
10948
+ const handler = async (input, output) => {
10945
10949
  pruneSeenRetroSections();
10946
10950
  if (!config.enabled)
10947
10951
  return;
10948
- if (!isWriteToSwarmPlan(input) && !isWriteToEvidenceFile(input))
10952
+ const trigger = normalizeWriteTrigger(input, output);
10953
+ if (!trigger)
10954
+ return;
10955
+ const isPlanTrigger = isPlanPath(trigger.filePath);
10956
+ const isEvidenceTrigger = isEvidencePath(trigger.filePath) && !isPlanTrigger;
10957
+ if (!isPlanTrigger && !isEvidenceTrigger)
10949
10958
  return;
10950
- const sessionID = input?.sessionID ?? "default";
10951
- const isEvidenceTrigger = isWriteToEvidenceFile(input) && !isWriteToSwarmPlan(input);
10952
10959
  if (isEvidenceTrigger) {
10953
- const record = input;
10954
- const rawPath = record.path;
10955
- const rawFile = record.file;
10956
- const filePath = typeof rawPath === "string" ? rawPath.replace(/\\/g, "/") : typeof rawFile === "string" ? rawFile.replace(/\\/g, "/") : null;
10957
- if (!filePath)
10958
- return;
10959
- const evidenceKey = `evidence:${sessionID}:${filePath}`;
10960
+ const relativeEvidencePath = trigger.filePath.replace(/^.*\.swarm\//, "");
10961
+ const evidenceKey = `evidence:${trigger.sessionID}:${relativeEvidencePath}`;
10960
10962
  const lastSeenEvidence = seenRetroSections.get(evidenceKey);
10961
- const evidenceContent = await readSwarmFileAsync(directory, filePath.replace(/^.*\.swarm\//, ""));
10963
+ const evidenceContent = await readSwarmFileAsync(directory, relativeEvidencePath);
10962
10964
  if (!evidenceContent)
10963
10965
  return;
10964
10966
  let evidenceData;
@@ -10986,7 +10988,7 @@ function createKnowledgeCuratorHook(directory, config, options = {}) {
10986
10988
  const projectName2 = evidenceData.project_name ?? "unknown";
10987
10989
  const phaseNumber2 = typeof evidenceData.phase_number === "number" ? evidenceData.phase_number : 1;
10988
10990
  await _internals17.curateAndStoreSwarm(lessons, projectName2, { phase_number: phaseNumber2 }, directory, config, {
10989
- llmDelegate: options.llmDelegateFactory?.(sessionID),
10991
+ llmDelegate: options.llmDelegateFactory?.(trigger.sessionID),
10990
10992
  enrichmentQuota: options.enrichmentQuota
10991
10993
  });
10992
10994
  return;
@@ -10997,7 +10999,7 @@ function createKnowledgeCuratorHook(directory, config, options = {}) {
10997
10999
  const section = extractRetrospectiveSection(planContent);
10998
11000
  if (!section)
10999
11001
  return;
11000
- if (!checkRetroChanged(sessionID, section))
11002
+ if (!checkRetroChanged(trigger.sessionID, section))
11001
11003
  return;
11002
11004
  const allLessons = extractLessonsFromRetro(section);
11003
11005
  if (allLessons.length === 0)
@@ -11011,7 +11013,7 @@ function createKnowledgeCuratorHook(directory, config, options = {}) {
11011
11013
  const phaseMatch = /^Phase:\s*(\d+)/m.exec(planContent);
11012
11014
  const phaseNumber = phaseMatch ? parseInt(phaseMatch[1], 10) : 1;
11013
11015
  await _internals17.curateAndStoreSwarm(normalLessons, projectName, { phase_number: phaseNumber }, directory, config, {
11014
- llmDelegate: options.llmDelegateFactory?.(sessionID),
11016
+ llmDelegate: options.llmDelegateFactory?.(trigger.sessionID),
11015
11017
  enrichmentQuota: options.enrichmentQuota
11016
11018
  });
11017
11019
  };
@@ -13653,6 +13655,10 @@ This project was already finalized in a previous /swarm close run. The plan has
13653
13655
  ctx.warnings.push(`Failed to write close-summary.md: ${msg}`);
13654
13656
  console.warn("[close-command] Failed to write close-summary.md:", error2);
13655
13657
  }
13658
+ const sessionIdsToEnd = [...swarmState.agentSessions.keys()];
13659
+ for (const sessionId of sessionIdsToEnd) {
13660
+ _internals20.endAgentSession(sessionId);
13661
+ }
13656
13662
  _internals20.resetSwarmStatePreservingSingletons();
13657
13663
  const retroWarnings = ctx.warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
13658
13664
  const otherWarnings = ctx.warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
@@ -13745,7 +13751,8 @@ var _internals20 = {
13745
13751
  runCleanStage,
13746
13752
  runAlignStage,
13747
13753
  archiveEvidence,
13748
- closePlanTerminalState
13754
+ closePlanTerminalState,
13755
+ endAgentSession
13749
13756
  };
13750
13757
 
13751
13758
  // src/commands/codebase-review.ts
@@ -17606,7 +17613,7 @@ async function handleEvidenceCommand(directory, args) {
17606
17613
  return formatTaskEvidenceMarkdown(evidenceData);
17607
17614
  }
17608
17615
  async function handleEvidenceSummaryCommand(directory) {
17609
- const { buildEvidenceSummary } = await import("./evidence-summary-service-g4x5e3e3.js");
17616
+ const { buildEvidenceSummary } = await import("./evidence-summary-service-559nas1m.js");
17610
17617
  const artifact = await buildEvidenceSummary(directory);
17611
17618
  if (!artifact) {
17612
17619
  return "No plan found. Run `/swarm plan` to check plan status.";
@@ -31857,7 +31864,7 @@ function buildDetailedHelp(commandName, entry) {
31857
31864
  async function handleHelpCommand(ctx) {
31858
31865
  const targetCommand = ctx.args.join(" ");
31859
31866
  if (!targetCommand) {
31860
- const { buildHelpText } = await import("./index-h6h8qfsh.js");
31867
+ const { buildHelpText } = await import("./index-chan9mkv.js");
31861
31868
  return buildHelpText();
31862
31869
  }
31863
31870
  const tokens = targetCommand.split(/\s+/);
@@ -31866,7 +31873,7 @@ async function handleHelpCommand(ctx) {
31866
31873
  return _internals45.buildDetailedHelp(resolved.key, resolved.entry);
31867
31874
  }
31868
31875
  const similar = _internals45.findSimilarCommands(targetCommand);
31869
- const { buildHelpText: fullHelp } = await import("./index-h6h8qfsh.js");
31876
+ const { buildHelpText: fullHelp } = await import("./index-chan9mkv.js");
31870
31877
  if (similar.length > 0) {
31871
31878
  return `Command '/swarm ${targetCommand}' not found.
31872
31879
 
@@ -31999,7 +32006,7 @@ var COMMAND_REGISTRY = {
31999
32006
  },
32000
32007
  "guardrail explain": {
32001
32008
  handler: async (ctx) => {
32002
- const { handleGuardrailExplain } = await import("./guardrail-explain-xdv74tfk.js");
32009
+ const { handleGuardrailExplain } = await import("./guardrail-explain-xktjjh53.js");
32003
32010
  return handleGuardrailExplain(ctx.directory, ctx.args);
32004
32011
  },
32005
32012
  description: "Dry-run: show what the guardrails would do to a command or write target (executes nothing)",
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  validateSwarmPath
4
- } from "./index-gn8n22th.js";
4
+ } from "./index-we94wkty.js";
5
5
  import {
6
6
  log
7
7
  } from "./index-zgwm4ryv.js";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  handleGuardrailExplain
4
- } from "./index-wmm21nsk.js";
4
+ } from "./index-rvweaxsd.js";
5
5
  import {
6
6
  handleGuardrailLog
7
7
  } from "./index-gnd1280x.js";
@@ -78,7 +78,7 @@ import {
78
78
  handleWriteRetroCommand,
79
79
  normalizeSwarmCommandInput,
80
80
  resolveCommand
81
- } from "./index-pc10e4d7.js";
81
+ } from "./index-72k57f21.js";
82
82
  import"./index-5z2e78tv.js";
83
83
  import"./index-2a6ppa65.js";
84
84
  import"./index-fjxjb66n.js";
@@ -89,13 +89,13 @@ import {
89
89
  ORCHESTRATOR_NAME,
90
90
  stripKnownSwarmPrefix
91
91
  } from "./index-471qxz9g.js";
92
- import"./index-4c5jpmn9.js";
92
+ import"./index-yrzfbqqh.js";
93
93
  import"./index-adz3nk9b.js";
94
94
  import"./index-v4fcn4tr.js";
95
95
  import"./index-mh1ej70w.js";
96
96
  import"./index-kv4dd5c5.js";
97
- import"./index-sf08zj91.js";
98
- import"./index-gn8n22th.js";
97
+ import"./index-a0dv2c7k.js";
98
+ import"./index-we94wkty.js";
99
99
  import"./index-jtqkh8jf.js";
100
100
  import"./index-5e4e2hvv.js";
101
101
  import"./index-p0arc26j.js";
@@ -12,7 +12,7 @@ import {
12
12
  detectPosixWrites,
13
13
  detectWindowsWrites,
14
14
  resolveWriteTargets
15
- } from "./index-pc10e4d7.js";
15
+ } from "./index-72k57f21.js";
16
16
  import {
17
17
  checkFileAuthority,
18
18
  classifyFile,
@@ -14,7 +14,13 @@ import {
14
14
 
15
15
  // src/hooks/utils.ts
16
16
  import * as path from "path";
17
- var _internals = { safeHook, composeHandlers, validateSwarmPath, readSwarmFileAsync };
17
+ var _internals = {
18
+ safeHook,
19
+ composeHandlers,
20
+ validateSwarmPath,
21
+ readSwarmFileAsync,
22
+ readCachedTextFile
23
+ };
18
24
  function safeHook(fn) {
19
25
  return async (input, output) => {
20
26
  try {
@@ -67,13 +73,23 @@ function validateSwarmPath(directory, filename) {
67
73
  }
68
74
  return resolved;
69
75
  }
70
- async function readSwarmFileAsync(directory, filename) {
76
+ async function readSwarmFileAsync(directory, filename, cache) {
77
+ if (cache !== undefined) {
78
+ const key = `${directory}::${filename}`;
79
+ const cached = cache.get(key);
80
+ if (cached !== undefined) {
81
+ return cached;
82
+ }
83
+ const promise = readSwarmFileAsync(directory, filename);
84
+ cache.set(key, promise);
85
+ return promise;
86
+ }
71
87
  const maxAttempts = 5;
72
88
  const retryDelayMs = 10;
73
89
  for (let attempt = 0;attempt < maxAttempts; attempt++) {
74
90
  try {
75
91
  const resolvedPath = _internals.validateSwarmPath(directory, filename);
76
- return await readCachedTextFile(resolvedPath, async () => {
92
+ return await _internals.readCachedTextFile(resolvedPath, async () => {
77
93
  const file = bunFile(resolvedPath);
78
94
  return await file.text();
79
95
  });
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  readSwarmFileAsync,
10
10
  validateSwarmPath
11
- } from "./index-gn8n22th.js";
11
+ } from "./index-we94wkty.js";
12
12
  import {
13
13
  readCachedParsedFile
14
14
  } from "./index-jtqkh8jf.js";
@@ -2162,8 +2162,8 @@ function extractPlanHashFromMarkdown(markdown) {
2162
2162
  const match = markdown.match(/<!--\s*PLAN_HASH:\s*(\S+)\s*-->/);
2163
2163
  return match ? match[1] : null;
2164
2164
  }
2165
- async function isPlanMdInSync(directory, plan) {
2166
- const planMdContent = await readSwarmFileAsync(directory, "plan.md");
2165
+ async function isPlanMdInSync(directory, plan, cache) {
2166
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md", cache);
2167
2167
  if (planMdContent === null) {
2168
2168
  return false;
2169
2169
  }
@@ -2197,8 +2197,8 @@ ${markdown}`;
2197
2197
  } catch {}
2198
2198
  }
2199
2199
  }
2200
- async function loadPlan(directory) {
2201
- const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
2200
+ async function loadPlan(directory, cache) {
2201
+ const planJsonContent = await readSwarmFileAsync(directory, "plan.json", cache);
2202
2202
  if (planJsonContent !== null) {
2203
2203
  if (planJsonContent.includes("\x00") || planJsonContent.includes("\uFFFD")) {
2204
2204
  warn("Plan rejected: .swarm/plan.json contains null bytes or invalid encoding");
@@ -2208,7 +2208,7 @@ async function loadPlan(directory) {
2208
2208
  if (validated === null) {
2209
2209
  warn("[loadPlan] plan.json disappeared during cached parse. Falling back to plan.md migration or ledger recovery.");
2210
2210
  } else {
2211
- const inSync = await isPlanMdInSync(directory, validated);
2211
+ const inSync = await isPlanMdInSync(directory, validated, cache);
2212
2212
  if (!inSync) {
2213
2213
  try {
2214
2214
  await _internals4.regeneratePlanMarkdown(directory, validated);
@@ -2331,7 +2331,7 @@ async function loadPlan(directory) {
2331
2331
  }
2332
2332
  }
2333
2333
  }
2334
- const planMdContent2 = await readSwarmFileAsync(directory, "plan.md");
2334
+ const planMdContent2 = await readSwarmFileAsync(directory, "plan.md", cache);
2335
2335
  if (planMdContent2 !== null) {
2336
2336
  const migrated = migrateLegacyPlan(planMdContent2);
2337
2337
  const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json");
@@ -2346,7 +2346,7 @@ async function loadPlan(directory) {
2346
2346
  }
2347
2347
  }
2348
2348
  }
2349
- const planMdContent = await readSwarmFileAsync(directory, "plan.md");
2349
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md", cache);
2350
2350
  if (planMdContent !== null) {
2351
2351
  const migrated = migrateLegacyPlan(planMdContent);
2352
2352
  await savePlan(directory, migrated);
package/dist/cli/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  getPluginLockFilePaths,
8
8
  package_default,
9
9
  resolveCommand
10
- } from "./index-pc10e4d7.js";
10
+ } from "./index-72k57f21.js";
11
11
  import"./index-5z2e78tv.js";
12
12
  import"./index-2a6ppa65.js";
13
13
  import"./index-fjxjb66n.js";
@@ -16,13 +16,13 @@ import"./index-zy22fg5h.js";
16
16
  import {
17
17
  DEFAULT_AGENT_CONFIGS
18
18
  } from "./index-471qxz9g.js";
19
- import"./index-4c5jpmn9.js";
19
+ import"./index-yrzfbqqh.js";
20
20
  import"./index-adz3nk9b.js";
21
21
  import"./index-v4fcn4tr.js";
22
22
  import"./index-mh1ej70w.js";
23
23
  import"./index-kv4dd5c5.js";
24
- import"./index-sf08zj91.js";
25
- import"./index-gn8n22th.js";
24
+ import"./index-a0dv2c7k.js";
25
+ import"./index-we94wkty.js";
26
26
  import"./index-jtqkh8jf.js";
27
27
  import"./index-5e4e2hvv.js";
28
28
  import"./index-p0arc26j.js";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  validateSwarmPath
4
- } from "./index-gn8n22th.js";
4
+ } from "./index-we94wkty.js";
5
5
  import"./index-jtqkh8jf.js";
6
6
  import"./index-5e4e2hvv.js";
7
7
  import"./index-p0arc26j.js";
@@ -9,8 +9,8 @@ import {
9
9
  sweepStale,
10
10
  unsubscribe,
11
11
  updateSnapshot
12
- } from "./index-sf08zj91.js";
13
- import"./index-gn8n22th.js";
12
+ } from "./index-a0dv2c7k.js";
13
+ import"./index-we94wkty.js";
14
14
  import"./index-jtqkh8jf.js";
15
15
  import"./index-5e4e2hvv.js";
16
16
  import"./index-p0arc26j.js";
@@ -8,7 +8,7 @@ import { checkHivePromotions } from '../hooks/hive-promoter';
8
8
  import { curateAndStoreSwarm } from '../hooks/knowledge-curator';
9
9
  import { closePlanTerminalState } from '../plan/manager';
10
10
  import { type SessionReflectionResult } from '../services/session-reflection';
11
- import { resetSwarmStatePreservingSingletons } from '../state';
11
+ import { endAgentSession, resetSwarmStatePreservingSingletons } from '../state';
12
12
  interface PlanPhase {
13
13
  id: number;
14
14
  name: string;
@@ -197,5 +197,6 @@ export declare const _internals: {
197
197
  runAlignStage: typeof runAlignStage;
198
198
  archiveEvidence: typeof archiveEvidence;
199
199
  closePlanTerminalState: typeof closePlanTerminalState;
200
+ endAgentSession: typeof endAgentSession;
200
201
  };
201
202
  export {};
@@ -21,6 +21,7 @@ declare function hashContent(content: string): string;
21
21
  * Exported for testing purposes only.
22
22
  */
23
23
  export declare function isWriteToEvidenceFile(input: unknown): boolean;
24
+ export declare function isEvidencePath(filePath: string | undefined | null): boolean;
24
25
  /** Build the v3-schema enrichment prompt for a single prose lesson. */
25
26
  export declare function buildV3EnrichmentPrompt(lesson: string, category: string, tags: string[]): string;
26
27
  /**
@@ -46,4 +46,4 @@ export type ArchitectMode = 'DISCOVER' | 'PLAN' | 'EXECUTE' | 'PHASE-WRAP' | 'UN
46
46
  * @param directory - The project directory to check
47
47
  * @returns The current architect mode based on plan state
48
48
  */
49
- export declare function detectArchitectMode(directory: string): Promise<ArchitectMode>;
49
+ export declare function detectArchitectMode(directory: string, cache?: Map<string, Promise<string | null>>): Promise<ArchitectMode>;
@@ -5,6 +5,7 @@
5
5
  * including error handling, handler composition, file I/O, and
6
6
  * token estimation for swarm-related operations.
7
7
  */
8
+ import { readCachedTextFile } from '../utils/swarm-artifact-cache';
8
9
  /**
9
10
  * Test-only dependency-injection seam. Production code calls
10
11
  * `_internals.<fn>(...)` so tests can replace the function on this object
@@ -18,6 +19,7 @@ export declare const _internals: {
18
19
  composeHandlers: typeof composeHandlers;
19
20
  validateSwarmPath: typeof validateSwarmPath;
20
21
  readSwarmFileAsync: typeof readSwarmFileAsync;
22
+ readCachedTextFile: typeof readCachedTextFile;
21
23
  };
22
24
  export declare function safeHook<I, O>(fn: (input: I, output: O) => Promise<void>): (input: I, output: O) => Promise<void>;
23
25
  /**
@@ -70,5 +72,5 @@ export declare function composeBlockingHandlers<I, O>(...fns: Array<(input: I, o
70
72
  * @throws Error if the filename is invalid or attempts path traversal
71
73
  */
72
74
  export declare function validateSwarmPath(directory: string, filename: string): string;
73
- export declare function readSwarmFileAsync(directory: string, filename: string): Promise<string | null>;
75
+ export declare function readSwarmFileAsync(directory: string, filename: string, cache?: Map<string, Promise<string | null>>): Promise<string | null>;
74
76
  export declare function estimateTokens(text: string): number;
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ var package_default;
69
69
  var init_package = __esm(() => {
70
70
  package_default = {
71
71
  name: "opencode-swarm",
72
- version: "7.98.0",
72
+ version: "7.98.2",
73
73
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
74
74
  main: "dist/index.js",
75
75
  types: "dist/index.d.ts",
@@ -18167,13 +18167,23 @@ function validateSwarmPath(directory, filename) {
18167
18167
  }
18168
18168
  return resolved;
18169
18169
  }
18170
- async function readSwarmFileAsync(directory, filename) {
18170
+ async function readSwarmFileAsync(directory, filename, cache) {
18171
+ if (cache !== undefined) {
18172
+ const key = `${directory}::${filename}`;
18173
+ const cached2 = cache.get(key);
18174
+ if (cached2 !== undefined) {
18175
+ return cached2;
18176
+ }
18177
+ const promise2 = readSwarmFileAsync(directory, filename);
18178
+ cache.set(key, promise2);
18179
+ return promise2;
18180
+ }
18171
18181
  const maxAttempts = 5;
18172
18182
  const retryDelayMs = 10;
18173
18183
  for (let attempt = 0;attempt < maxAttempts; attempt++) {
18174
18184
  try {
18175
18185
  const resolvedPath = _internals3.validateSwarmPath(directory, filename);
18176
- return await readCachedTextFile(resolvedPath, async () => {
18186
+ return await _internals3.readCachedTextFile(resolvedPath, async () => {
18177
18187
  const file2 = bunFile(resolvedPath);
18178
18188
  return await file2.text();
18179
18189
  });
@@ -18198,7 +18208,13 @@ var init_utils2 = __esm(() => {
18198
18208
  init_utils();
18199
18209
  init_bun_compat();
18200
18210
  init_swarm_artifact_cache();
18201
- _internals3 = { safeHook, composeHandlers, validateSwarmPath, readSwarmFileAsync };
18211
+ _internals3 = {
18212
+ safeHook,
18213
+ composeHandlers,
18214
+ validateSwarmPath,
18215
+ readSwarmFileAsync,
18216
+ readCachedTextFile
18217
+ };
18202
18218
  });
18203
18219
 
18204
18220
  // src/utils/git-binary-missing-error.ts
@@ -20452,8 +20468,8 @@ function extractPlanHashFromMarkdown(markdown) {
20452
20468
  const match = markdown.match(/<!--\s*PLAN_HASH:\s*(\S+)\s*-->/);
20453
20469
  return match ? match[1] : null;
20454
20470
  }
20455
- async function isPlanMdInSync(directory, plan) {
20456
- const planMdContent = await readSwarmFileAsync(directory, "plan.md");
20471
+ async function isPlanMdInSync(directory, plan, cache) {
20472
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md", cache);
20457
20473
  if (planMdContent === null) {
20458
20474
  return false;
20459
20475
  }
@@ -20487,8 +20503,8 @@ ${markdown}`;
20487
20503
  } catch {}
20488
20504
  }
20489
20505
  }
20490
- async function loadPlan(directory) {
20491
- const planJsonContent = await readSwarmFileAsync(directory, "plan.json");
20506
+ async function loadPlan(directory, cache) {
20507
+ const planJsonContent = await readSwarmFileAsync(directory, "plan.json", cache);
20492
20508
  if (planJsonContent !== null) {
20493
20509
  if (planJsonContent.includes("\x00") || planJsonContent.includes("�")) {
20494
20510
  warn("Plan rejected: .swarm/plan.json contains null bytes or invalid encoding");
@@ -20498,7 +20514,7 @@ async function loadPlan(directory) {
20498
20514
  if (validated === null) {
20499
20515
  warn("[loadPlan] plan.json disappeared during cached parse. Falling back to plan.md migration or ledger recovery.");
20500
20516
  } else {
20501
- const inSync = await isPlanMdInSync(directory, validated);
20517
+ const inSync = await isPlanMdInSync(directory, validated, cache);
20502
20518
  if (!inSync) {
20503
20519
  try {
20504
20520
  await _internals8.regeneratePlanMarkdown(directory, validated);
@@ -20621,7 +20637,7 @@ async function loadPlan(directory) {
20621
20637
  }
20622
20638
  }
20623
20639
  }
20624
- const planMdContent2 = await readSwarmFileAsync(directory, "plan.md");
20640
+ const planMdContent2 = await readSwarmFileAsync(directory, "plan.md", cache);
20625
20641
  if (planMdContent2 !== null) {
20626
20642
  const migrated = migrateLegacyPlan(planMdContent2);
20627
20643
  const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json");
@@ -20636,7 +20652,7 @@ async function loadPlan(directory) {
20636
20652
  }
20637
20653
  }
20638
20654
  }
20639
- const planMdContent = await readSwarmFileAsync(directory, "plan.md");
20655
+ const planMdContent = await readSwarmFileAsync(directory, "plan.md", cache);
20640
20656
  if (planMdContent !== null) {
20641
20657
  const migrated = migrateLegacyPlan(planMdContent);
20642
20658
  await savePlan(directory, migrated);
@@ -70430,48 +70446,43 @@ function recordSeenRetroSection(key, value, timestamp) {
70430
70446
  function hashContent2(content) {
70431
70447
  return createHash9("sha1").update(content).digest("hex");
70432
70448
  }
70433
- function isWriteToSwarmPlan(input) {
70434
- if (typeof input !== "object" || input === null)
70435
- return false;
70436
- const record3 = input;
70437
- const toolName = record3.toolName;
70438
- if (typeof toolName !== "string")
70439
- return false;
70440
- if (!["write", "edit", "apply_patch", "swarm_apply_patch"].includes(toolName))
70441
- return false;
70442
- const rawPath = record3.path;
70443
- const rawFile = record3.file;
70444
- const pathField = typeof rawPath === "string" ? rawPath.replace(/\\/g, "/") : undefined;
70445
- const fileField = typeof rawFile === "string" ? rawFile.replace(/\\/g, "/") : undefined;
70446
- if (typeof pathField === "string" && pathField.includes(".swarm/plan.md")) {
70447
- return true;
70448
- }
70449
- if (typeof fileField === "string" && fileField.includes(".swarm/plan.md")) {
70450
- return true;
70451
- }
70452
- return false;
70453
- }
70454
70449
  function isWriteToEvidenceFile(input) {
70455
- if (typeof input !== "object" || input === null)
70456
- return false;
70457
- const record3 = input;
70458
- const toolName = record3.toolName;
70459
- if (typeof toolName !== "string")
70460
- return false;
70461
- if (!["write", "edit", "apply_patch", "swarm_apply_patch"].includes(toolName))
70450
+ const trigger = normalizeWriteTrigger(input);
70451
+ return isEvidencePath(trigger?.filePath);
70452
+ }
70453
+ function isRecord(value) {
70454
+ return typeof value === "object" && value !== null;
70455
+ }
70456
+ function normalizePathField(value) {
70457
+ return typeof value === "string" ? value.replace(/\\/g, "/") : null;
70458
+ }
70459
+ function firstPathFromRecord(record3) {
70460
+ return normalizePathField(record3.path) ?? normalizePathField(record3.filePath) ?? normalizePathField(record3.file);
70461
+ }
70462
+ function normalizeWriteTrigger(input, output) {
70463
+ if (!isRecord(input))
70464
+ return null;
70465
+ const toolName = typeof input.toolName === "string" ? input.toolName : typeof input.tool === "string" ? input.tool : null;
70466
+ if (!toolName || !WRITE_TOOLS.has(toolName))
70467
+ return null;
70468
+ const inputArgs = isRecord(input.args) ? input.args : null;
70469
+ const outputArgs = isRecord(output) && isRecord(output.args) ? output.args : null;
70470
+ const filePath = firstPathFromRecord(input) ?? (inputArgs ? firstPathFromRecord(inputArgs) : null) ?? (outputArgs ? firstPathFromRecord(outputArgs) : null);
70471
+ if (!filePath)
70472
+ return null;
70473
+ return {
70474
+ toolName,
70475
+ filePath,
70476
+ sessionID: typeof input.sessionID === "string" ? input.sessionID : "default"
70477
+ };
70478
+ }
70479
+ function isEvidencePath(filePath) {
70480
+ if (!filePath)
70462
70481
  return false;
70463
- const rawPath = record3.path;
70464
- const rawFile = record3.file;
70465
- const pathField = typeof rawPath === "string" ? rawPath.replace(/\\/g, "/") : undefined;
70466
- const fileField = typeof rawFile === "string" ? rawFile.replace(/\\/g, "/") : undefined;
70467
- const evidenceRegex = /\.swarm\/+evidence\/+/i;
70468
- if (typeof pathField === "string" && evidenceRegex.test(pathField)) {
70469
- return true;
70470
- }
70471
- if (typeof fileField === "string" && evidenceRegex.test(fileField)) {
70472
- return true;
70473
- }
70474
- return false;
70482
+ return /(?:^|\/)\.swarm\/+evidence\//i.test(filePath);
70483
+ }
70484
+ function isPlanPath(filePath) {
70485
+ return filePath?.includes(".swarm/plan.md") ?? false;
70475
70486
  }
70476
70487
  function extractRetrospectiveSection(planContent) {
70477
70488
  const headingRegex = /^###\s+Lessons\s+Learned$/m;
@@ -71153,24 +71164,22 @@ async function runAutoPromotion(directory, config3) {
71153
71164
  }
71154
71165
  }
71155
71166
  function createKnowledgeCuratorHook(directory, config3, options = {}) {
71156
- const handler = async (input, _output) => {
71167
+ const handler = async (input, output) => {
71157
71168
  pruneSeenRetroSections();
71158
71169
  if (!config3.enabled)
71159
71170
  return;
71160
- if (!isWriteToSwarmPlan(input) && !isWriteToEvidenceFile(input))
71171
+ const trigger = normalizeWriteTrigger(input, output);
71172
+ if (!trigger)
71173
+ return;
71174
+ const isPlanTrigger = isPlanPath(trigger.filePath);
71175
+ const isEvidenceTrigger = isEvidencePath(trigger.filePath) && !isPlanTrigger;
71176
+ if (!isPlanTrigger && !isEvidenceTrigger)
71161
71177
  return;
71162
- const sessionID = input?.sessionID ?? "default";
71163
- const isEvidenceTrigger = isWriteToEvidenceFile(input) && !isWriteToSwarmPlan(input);
71164
71178
  if (isEvidenceTrigger) {
71165
- const record3 = input;
71166
- const rawPath = record3.path;
71167
- const rawFile = record3.file;
71168
- const filePath = typeof rawPath === "string" ? rawPath.replace(/\\/g, "/") : typeof rawFile === "string" ? rawFile.replace(/\\/g, "/") : null;
71169
- if (!filePath)
71170
- return;
71171
- const evidenceKey = `evidence:${sessionID}:${filePath}`;
71179
+ const relativeEvidencePath = trigger.filePath.replace(/^.*\.swarm\//, "");
71180
+ const evidenceKey = `evidence:${trigger.sessionID}:${relativeEvidencePath}`;
71172
71181
  const lastSeenEvidence = seenRetroSections.get(evidenceKey);
71173
- const evidenceContent = await readSwarmFileAsync(directory, filePath.replace(/^.*\.swarm\//, ""));
71182
+ const evidenceContent = await readSwarmFileAsync(directory, relativeEvidencePath);
71174
71183
  if (!evidenceContent)
71175
71184
  return;
71176
71185
  let evidenceData;
@@ -71198,7 +71207,7 @@ function createKnowledgeCuratorHook(directory, config3, options = {}) {
71198
71207
  const projectName2 = evidenceData.project_name ?? "unknown";
71199
71208
  const phaseNumber2 = typeof evidenceData.phase_number === "number" ? evidenceData.phase_number : 1;
71200
71209
  await _internals35.curateAndStoreSwarm(lessons, projectName2, { phase_number: phaseNumber2 }, directory, config3, {
71201
- llmDelegate: options.llmDelegateFactory?.(sessionID),
71210
+ llmDelegate: options.llmDelegateFactory?.(trigger.sessionID),
71202
71211
  enrichmentQuota: options.enrichmentQuota
71203
71212
  });
71204
71213
  return;
@@ -71209,7 +71218,7 @@ function createKnowledgeCuratorHook(directory, config3, options = {}) {
71209
71218
  const section = extractRetrospectiveSection(planContent);
71210
71219
  if (!section)
71211
71220
  return;
71212
- if (!checkRetroChanged(sessionID, section))
71221
+ if (!checkRetroChanged(trigger.sessionID, section))
71213
71222
  return;
71214
71223
  const allLessons = extractLessonsFromRetro(section);
71215
71224
  if (allLessons.length === 0)
@@ -71223,13 +71232,13 @@ function createKnowledgeCuratorHook(directory, config3, options = {}) {
71223
71232
  const phaseMatch = /^Phase:\s*(\d+)/m.exec(planContent);
71224
71233
  const phaseNumber = phaseMatch ? parseInt(phaseMatch[1], 10) : 1;
71225
71234
  await _internals35.curateAndStoreSwarm(normalLessons, projectName, { phase_number: phaseNumber }, directory, config3, {
71226
- llmDelegate: options.llmDelegateFactory?.(sessionID),
71235
+ llmDelegate: options.llmDelegateFactory?.(trigger.sessionID),
71227
71236
  enrichmentQuota: options.enrichmentQuota
71228
71237
  });
71229
71238
  };
71230
71239
  return safeHook(handler);
71231
71240
  }
71232
- var seenRetroSections, MAX_TRACKED_RETRO_SECTIONS = 500, ENRICHMENT_ALLOWED_FIELDS, ENRICHMENT_LLM_TIMEOUT_MS = 60000, ENRICHMENT_BATCH_SIZE = 6, MESO_INSIGHT_BATCH_LIMIT = 20, KNOWLEDGE_CATEGORIES, OUTCOME_PROMOTION_BLOCK = -0.3, _internals35;
71241
+ var seenRetroSections, MAX_TRACKED_RETRO_SECTIONS = 500, WRITE_TOOLS, ENRICHMENT_ALLOWED_FIELDS, ENRICHMENT_LLM_TIMEOUT_MS = 60000, ENRICHMENT_BATCH_SIZE = 6, MESO_INSIGHT_BATCH_LIMIT = 20, KNOWLEDGE_CATEGORIES, OUTCOME_PROMOTION_BLOCK = -0.3, _internals35;
71233
71242
  var init_knowledge_curator = __esm(() => {
71234
71243
  init_skill_improver_quota();
71235
71244
  init_synonym_map();
@@ -71241,6 +71250,12 @@ var init_knowledge_curator = __esm(() => {
71241
71250
  init_micro_reflector();
71242
71251
  init_utils2();
71243
71252
  seenRetroSections = new Map;
71253
+ WRITE_TOOLS = new Set([
71254
+ "write",
71255
+ "edit",
71256
+ "apply_patch",
71257
+ "swarm_apply_patch"
71258
+ ]);
71244
71259
  ENRICHMENT_ALLOWED_FIELDS = [
71245
71260
  "triggers",
71246
71261
  "required_actions",
@@ -73876,6 +73891,10 @@ This project was already finalized in a previous /swarm close run. The plan has
73876
73891
  ctx.warnings.push(`Failed to write close-summary.md: ${msg}`);
73877
73892
  console.warn("[close-command] Failed to write close-summary.md:", error93);
73878
73893
  }
73894
+ const sessionIdsToEnd = [...swarmState.agentSessions.keys()];
73895
+ for (const sessionId of sessionIdsToEnd) {
73896
+ _internals38.endAgentSession(sessionId);
73897
+ }
73879
73898
  _internals38.resetSwarmStatePreservingSingletons();
73880
73899
  const retroWarnings = ctx.warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
73881
73900
  const otherWarnings = ctx.warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
@@ -74042,7 +74061,8 @@ var init_close = __esm(() => {
74042
74061
  runCleanStage,
74043
74062
  runAlignStage,
74044
74063
  archiveEvidence,
74045
- closePlanTerminalState
74064
+ closePlanTerminalState,
74065
+ endAgentSession
74046
74066
  };
74047
74067
  });
74048
74068
 
@@ -119726,6 +119746,7 @@ function createSystemEnhancerHook(config3, directory) {
119726
119746
  return;
119727
119747
  }
119728
119748
  }
119749
+ const planReadCache = new Map;
119729
119750
  const maxInjectionTokens = config3.context_budget?.max_injection_tokens ?? 4000;
119730
119751
  let injectedTokens = 0;
119731
119752
  let actualDemand = 0;
@@ -119740,7 +119761,7 @@ function createSystemEnhancerHook(config3, directory) {
119740
119761
  } else {
119741
119762
  seAllocation = maxInjectionTokens;
119742
119763
  }
119743
- const contextContent = await readSwarmFileAsync(directory, "context.md");
119764
+ const contextContent = await readSwarmFileAsync(directory, "context.md", planReadCache);
119744
119765
  try {
119745
119766
  const { scanDocIndex: scanDocIndex2 } = await Promise.resolve().then(() => (init_doc_scan(), exports_doc_scan));
119746
119767
  const { manifest, cached: cached3 } = await scanDocIndex2(directory);
@@ -119800,19 +119821,19 @@ function createSystemEnhancerHook(config3, directory) {
119800
119821
  if (!scoringEnabled) {
119801
119822
  let plan2 = null;
119802
119823
  try {
119803
- plan2 = await loadPlan(directory);
119824
+ plan2 = await loadPlan(directory, planReadCache);
119804
119825
  } catch (error93) {
119805
119826
  warn(`Failed to load plan: ${error93 instanceof Error ? error93.message : String(error93)}`);
119806
119827
  }
119807
119828
  maybeAppendSpecDriftAdvisory(output, directory, plan2);
119808
- const mode = await detectArchitectMode(directory);
119829
+ const mode = await detectArchitectMode(directory, planReadCache);
119809
119830
  let planContent = null;
119810
119831
  let phaseHeader = "";
119811
119832
  if (plan2 && plan2.migration_status !== "migration_failed") {
119812
119833
  phaseHeader = extractCurrentPhaseFromPlan(plan2) || "";
119813
- planContent = await readSwarmFileAsync(directory, "plan.md");
119834
+ planContent = await readSwarmFileAsync(directory, "plan.md", planReadCache);
119814
119835
  } else {
119815
- planContent = await readSwarmFileAsync(directory, "plan.md");
119836
+ planContent = await readSwarmFileAsync(directory, "plan.md", planReadCache);
119816
119837
  phaseHeader = planContent ? extractCurrentPhase(planContent) || "" : "";
119817
119838
  }
119818
119839
  if (phaseHeader) {
@@ -119824,7 +119845,7 @@ function createSystemEnhancerHook(config3, directory) {
119824
119845
  }
119825
119846
  if (mode !== "DISCOVER") {
119826
119847
  try {
119827
- const handoffContent = await readSwarmFileAsync(directory, "handoff.md");
119848
+ const handoffContent = await readSwarmFileAsync(directory, "handoff.md", planReadCache);
119828
119849
  if (handoffContent) {
119829
119850
  const handoffPath = validateSwarmPath(directory, "handoff.md");
119830
119851
  const consumedPath = validateSwarmPath(directory, "handoff-consumed.md");
@@ -120188,7 +120209,7 @@ ${budgetWarning}`);
120188
120209
  } else {}
120189
120210
  return;
120190
120211
  }
120191
- const mode_b = await detectArchitectMode(directory);
120212
+ const mode_b = await detectArchitectMode(directory, planReadCache);
120192
120213
  const userScoringConfig = config3.context_budget?.scoring;
120193
120214
  const candidates = [];
120194
120215
  let idCounter = 0;
@@ -120200,7 +120221,7 @@ ${budgetWarning}`);
120200
120221
  } : DEFAULT_SCORING_CONFIG;
120201
120222
  let plan = null;
120202
120223
  try {
120203
- plan = await loadPlan(directory);
120224
+ plan = await loadPlan(directory, planReadCache);
120204
120225
  } catch (error93) {
120205
120226
  warn(`Failed to load plan: ${error93 instanceof Error ? error93.message : String(error93)}`);
120206
120227
  }
@@ -120211,7 +120232,7 @@ ${budgetWarning}`);
120211
120232
  currentPhase = extractCurrentPhaseFromPlan(plan);
120212
120233
  currentTask = extractCurrentTaskFromPlan(plan);
120213
120234
  } else {
120214
- planContentForCursor = await readSwarmFileAsync(directory, "plan.md");
120235
+ planContentForCursor = await readSwarmFileAsync(directory, "plan.md", planReadCache);
120215
120236
  if (planContentForCursor) {
120216
120237
  currentPhase = extractCurrentPhase(planContentForCursor);
120217
120238
  currentTask = extractCurrentTask(planContentForCursor);
@@ -120255,7 +120276,7 @@ ${budgetWarning}`);
120255
120276
  }
120256
120277
  if (mode_b !== "DISCOVER") {
120257
120278
  try {
120258
- const handoffContent = await readSwarmFileAsync(directory, "handoff.md");
120279
+ const handoffContent = await readSwarmFileAsync(directory, "handoff.md", planReadCache);
120259
120280
  if (handoffContent) {
120260
120281
  const handoffPath = validateSwarmPath(directory, "handoff.md");
120261
120282
  const consumedPath = validateSwarmPath(directory, "handoff-consumed.md");
@@ -120754,9 +120775,9 @@ ${activitySection}`;
120754
120775
  }
120755
120776
  return contextSummary;
120756
120777
  }
120757
- async function detectArchitectMode(directory) {
120778
+ async function detectArchitectMode(directory, cache4) {
120758
120779
  try {
120759
- const plan = await loadPlan(directory);
120780
+ const plan = await loadPlan(directory, cache4);
120760
120781
  if (!plan) {
120761
120782
  return "DISCOVER";
120762
120783
  }
@@ -125232,7 +125253,7 @@ init_state2();
125232
125253
  init_delegation_gate();
125233
125254
  init_normalize_tool_name();
125234
125255
  import * as path154 from "node:path";
125235
- var WRITE_TOOLS = new Set(WRITE_TOOL_NAMES);
125256
+ var WRITE_TOOLS2 = new Set(WRITE_TOOL_NAMES);
125236
125257
  function createScopeGuardHook(config3, directory, injectAdvisory) {
125237
125258
  const enabled = config3.enabled ?? true;
125238
125259
  const _skipInTurbo = config3.skip_in_turbo ?? false;
@@ -125241,7 +125262,7 @@ function createScopeGuardHook(config3, directory, injectAdvisory) {
125241
125262
  if (!enabled)
125242
125263
  return;
125243
125264
  const toolName = normalizeToolName(input.tool);
125244
- if (!WRITE_TOOLS.has(toolName))
125265
+ if (!WRITE_TOOLS2.has(toolName))
125245
125266
  return;
125246
125267
  const sessionId = input.sessionID;
125247
125268
  const activeAgent = swarmState.activeAgent.get(sessionId);
@@ -135730,8 +135751,99 @@ function scanInvisibleFormatChars(text, fieldName) {
135730
135751
  }
135731
135752
  return findings;
135732
135753
  }
135733
- function stripMarkdownCodeForUnsafeScan(text) {
135734
- return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
135754
+ var CODE_SPAN_OR_FENCE_REGEX = /```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`/g;
135755
+ var CODE_WARNING_ONLY_PATTERNS = new Set([
135756
+ "backtick_execution",
135757
+ "shell_substitution",
135758
+ "disk_format",
135759
+ "force_kill",
135760
+ "process_group_kill",
135761
+ "kill_all_processes"
135762
+ ]);
135763
+ function splitMarkdownCodeSegments(text) {
135764
+ const segments = [];
135765
+ let cursor = 0;
135766
+ for (const match of text.matchAll(CODE_SPAN_OR_FENCE_REGEX)) {
135767
+ const index = match.index ?? 0;
135768
+ if (index > cursor) {
135769
+ segments.push({ value: text.slice(cursor, index), isCode: false });
135770
+ }
135771
+ segments.push({ value: match[0], isCode: true });
135772
+ cursor = index + match[0].length;
135773
+ }
135774
+ if (cursor < text.length) {
135775
+ segments.push({ value: text.slice(cursor), isCode: false });
135776
+ }
135777
+ return segments.length === 0 ? [{ value: text, isCode: false }] : segments;
135778
+ }
135779
+ function getUnsafeInstructionMetadata(name) {
135780
+ const entry = UNSAFE_INSTRUCTION_PATTERNS.find((item) => item.name === name);
135781
+ return {
135782
+ description: entry?.description ?? "Destructive file removal command",
135783
+ severity: entry?.severity ?? "error"
135784
+ };
135785
+ }
135786
+ function tokenizeShellArgs(input) {
135787
+ const tokens = [];
135788
+ const tokenRegex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|[^\s]+/g;
135789
+ for (const match of input.matchAll(tokenRegex)) {
135790
+ const token = match[1] ?? match[2] ?? match[0];
135791
+ if (token.length > 0)
135792
+ tokens.push(token);
135793
+ }
135794
+ return tokens;
135795
+ }
135796
+ function rmFlagHasShortOption(token, option) {
135797
+ return /^-[A-Za-z]+$/.test(token) && token.slice(1).includes(option);
135798
+ }
135799
+ function isRmOption(token) {
135800
+ return token.startsWith("-") && token !== "-";
135801
+ }
135802
+ function isDangerousRemovalTarget(rawTarget) {
135803
+ const target = rawTarget.replace(/\\/g, "/");
135804
+ if (/^[A-Za-z]:[/\\]?$/.test(target))
135805
+ return true;
135806
+ return target === "/" || target === "/*" || target === "." || target === ".." || target.startsWith(".swarm") || target.startsWith("/.swarm") || target.startsWith("~") || target.startsWith("$") || target.startsWith("${") || target.startsWith("%") || target.startsWith("/home") || target.startsWith("/Users") || target.startsWith("/etc") || target.startsWith("/bin") || target.startsWith("/sbin") || target.startsWith("/usr") || target.startsWith("/var") || /^[A-Za-z]:\/(?:Users|Windows)(?:\/|$)/.test(target);
135807
+ }
135808
+ function findDangerousRemovalCommands(text) {
135809
+ const commands = [];
135810
+ const rmCommandRegex = /\b(?<sudo>sudo\s+)?rm\b(?<args>[^\r\n`;&|]*)/gi;
135811
+ for (const match of text.matchAll(rmCommandRegex)) {
135812
+ const args2 = tokenizeShellArgs(match.groups?.args ?? "");
135813
+ const hasRecursive = args2.some((token) => token === "--recursive" || token === "-R" || rmFlagHasShortOption(token, "r") || rmFlagHasShortOption(token, "R"));
135814
+ const hasForce = args2.some((token) => token === "--force" || token === "-f" || rmFlagHasShortOption(token, "f"));
135815
+ if (!hasRecursive || !hasForce)
135816
+ continue;
135817
+ if (!args2.some((token) => !isRmOption(token) && isDangerousRemovalTarget(token))) {
135818
+ continue;
135819
+ }
135820
+ const pattern = match.groups?.sudo === undefined ? "destructive_file_removal" : "privileged_file_removal";
135821
+ commands.push({
135822
+ pattern,
135823
+ description: getUnsafeInstructionMetadata(pattern).description,
135824
+ match: match[0].trim().slice(0, 100)
135825
+ });
135826
+ }
135827
+ return commands;
135828
+ }
135829
+ function hasDangerousRemovalTarget(text) {
135830
+ return findDangerousRemovalCommands(text).length > 0;
135831
+ }
135832
+ function shouldScanUnsafePatternInSegment(entry, segment) {
135833
+ if (!segment.isCode)
135834
+ return true;
135835
+ if (CODE_WARNING_ONLY_PATTERNS.has(entry.name))
135836
+ return false;
135837
+ if (entry.name === "destructive_file_removal" || entry.name === "privileged_file_removal") {
135838
+ return false;
135839
+ }
135840
+ return true;
135841
+ }
135842
+ function getUnsafeScanSegments(field, value) {
135843
+ if (field !== "skill_body") {
135844
+ return [{ value, isCode: false }];
135845
+ }
135846
+ return splitMarkdownCodeSegments(value);
135735
135847
  }
135736
135848
  function scanPromptInjection(candidate, trustLevel = "low") {
135737
135849
  const fields = extractCandidateFields(candidate);
@@ -135799,17 +135911,37 @@ function scanUnsafeInstructions(candidate, trustLevel = "low") {
135799
135911
  const fieldsScanned = [];
135800
135912
  for (const { field, value } of fields) {
135801
135913
  fieldsScanned.push(field);
135802
- const scanValue = field === "skill_body" ? stripMarkdownCodeForUnsafeScan(value) : value;
135914
+ const scanSegments = getUnsafeScanSegments(field, value);
135915
+ if (field === "skill_body") {
135916
+ for (const segment of scanSegments) {
135917
+ if (!segment.isCode)
135918
+ continue;
135919
+ for (const command of findDangerousRemovalCommands(segment.value)) {
135920
+ findings.push({
135921
+ pattern: command.pattern,
135922
+ field,
135923
+ description: command.description,
135924
+ severity: "error",
135925
+ match: command.match
135926
+ });
135927
+ }
135928
+ }
135929
+ }
135803
135930
  for (const entry of UNSAFE_INSTRUCTION_PATTERNS) {
135804
- const match = entry.pattern.exec(scanValue);
135805
- if (match !== null) {
135806
- findings.push({
135807
- pattern: entry.name,
135808
- field,
135809
- description: entry.description,
135810
- severity: entry.severity,
135811
- match: match[0].slice(0, 100)
135812
- });
135931
+ for (const segment of scanSegments) {
135932
+ if (!shouldScanUnsafePatternInSegment(entry, segment))
135933
+ continue;
135934
+ const match = entry.pattern.exec(segment.value);
135935
+ if (match !== null) {
135936
+ findings.push({
135937
+ pattern: entry.name,
135938
+ field,
135939
+ description: entry.description,
135940
+ severity: entry.severity,
135941
+ match: match[0].slice(0, 100)
135942
+ });
135943
+ break;
135944
+ }
135813
135945
  }
135814
135946
  }
135815
135947
  }
@@ -135973,7 +136105,9 @@ function evaluateCandidate(candidate, options) {
135973
136105
  var _internals103 = {
135974
136106
  getTimestamp: () => new Date().toISOString(),
135975
136107
  computeSha256: (content) => createHash21("sha256").update(content).digest("hex"),
135976
- stripMarkdownCodeForUnsafeScan
136108
+ splitMarkdownCodeSegments,
136109
+ hasDangerousRemovalTarget,
136110
+ isDangerousRemovalTarget
135977
136111
  };
135978
136112
 
135979
136113
  // src/tools/external-skill-discover.ts
@@ -108,7 +108,7 @@ export declare function regeneratePlanMarkdown(directory: string, plan: Plan): P
108
108
  * 3. .swarm/plan.md exists only -> migrate from plan.md, save both files, return Plan
109
109
  * 4. Neither exists -> return null
110
110
  */
111
- export declare function loadPlan(directory: string): Promise<RuntimePlan | null>;
111
+ export declare function loadPlan(directory: string, cache?: Map<string, Promise<string | null>>): Promise<RuntimePlan | null>;
112
112
  /**
113
113
  * Recovery-path helper for callers that legitimately need to replace the
114
114
  * plan task set without explicit per-id acknowledgement (e.g. rebuilding
@@ -105,7 +105,13 @@ export declare const VALIDATION_RATE_LIMITS: {
105
105
  /** Timeout for individual fetch operations in milliseconds. */
106
106
  readonly fetch_timeout_ms: 30000;
107
107
  };
108
- declare function stripMarkdownCodeForUnsafeScan(text: string): string;
108
+ interface MarkdownSegment {
109
+ value: string;
110
+ isCode: boolean;
111
+ }
112
+ declare function splitMarkdownCodeSegments(text: string): MarkdownSegment[];
113
+ declare function isDangerousRemovalTarget(rawTarget: string): boolean;
114
+ declare function hasDangerousRemovalTarget(text: string): boolean;
109
115
  /**
110
116
  * Scan an external skill candidate for prompt-injection patterns.
111
117
  *
@@ -158,6 +164,8 @@ export declare function evaluateCandidate(candidate: ExternalSkillCandidate, opt
158
164
  export declare const _internals: {
159
165
  getTimestamp: () => string;
160
166
  computeSha256: (content: string) => string;
161
- stripMarkdownCodeForUnsafeScan: typeof stripMarkdownCodeForUnsafeScan;
167
+ splitMarkdownCodeSegments: typeof splitMarkdownCodeSegments;
168
+ hasDangerousRemovalTarget: typeof hasDangerousRemovalTarget;
169
+ isDangerousRemovalTarget: typeof isDangerousRemovalTarget;
162
170
  };
163
171
  export {};
package/dist/state.d.ts CHANGED
@@ -391,9 +391,9 @@ export declare function resetSwarmStatePreservingSingletons(): void;
391
391
  export declare function startAgentSession(sessionId: string, agentName: string, staleDurationMs?: number, directory?: string): void;
392
392
  /**
393
393
  * End an agent session by removing it from the state.
394
- * NOTE: Currently unused in production no session lifecycle teardown is wired up.
395
- * Sessions accumulate for the process lifetime. Callers should integrate this into
396
- * a session TTL or idle-timeout mechanism to prevent unbounded Map growth.
394
+ * Called at session terminal state transitions (task completion, error, or session
395
+ * teardown) to prevent unbounded Map growth. Double-calls are safe: Map.delete is
396
+ * a no-op for missing keys (FR-010).
397
397
  * @param sessionId - The session identifier to remove
398
398
  */
399
399
  export declare function endAgentSession(sessionId: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.98.0",
3
+ "version": "7.98.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",