opencode-swarm 7.3.3 → 7.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.3.3",
37
+ version: "7.3.4",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -4,6 +4,16 @@
4
4
  * were modified, or null if in-scope, no scope declared, or git unavailable.
5
5
  * Never throws.
6
6
  */
7
+ import { bunSpawn } from '../utils/bun-compat';
8
+ /**
9
+ * Test-only dependency-injection seam — see `gitignore-warning.ts:_internals`
10
+ * for the rationale (`mock.module` from `bun:test` leaks across files in
11
+ * Bun's shared test-runner process). Mutating this local object is
12
+ * file-scoped and trivially restorable via `afterEach`.
13
+ */
14
+ export declare const _internals: {
15
+ bunSpawn: typeof bunSpawn;
16
+ };
7
17
  /**
8
18
  * Validate that git-changed files match the declared scope for a task.
9
19
  * Returns a warning string if undeclared files were modified, null otherwise.
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ var package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "opencode-swarm",
36
- version: "7.3.3",
36
+ version: "7.3.4",
37
37
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
38
38
  main: "dist/index.js",
39
39
  types: "dist/index.d.ts",
@@ -89610,19 +89610,141 @@ init_loader();
89610
89610
  init_schema();
89611
89611
  init_qa_gate_profile();
89612
89612
  init_gate_evidence();
89613
+ import * as fs86 from "node:fs";
89614
+ import * as path106 from "node:path";
89615
+
89616
+ // src/hooks/diff-scope.ts
89617
+ init_bun_compat();
89613
89618
  import * as fs85 from "node:fs";
89614
89619
  import * as path105 from "node:path";
89615
89620
 
89616
- // src/hooks/diff-scope.ts
89621
+ // src/utils/gitignore-warning.ts
89617
89622
  init_bun_compat();
89618
89623
  import * as fs84 from "node:fs";
89619
89624
  import * as path104 from "node:path";
89625
+ var _internals = { bunSpawn };
89626
+ var _swarmGitExcludedChecked = false;
89627
+ function fileCoversSwarm(content) {
89628
+ for (const rawLine of content.split(`
89629
+ `)) {
89630
+ const line = rawLine.trim();
89631
+ if (line.startsWith("#") || line.length === 0)
89632
+ continue;
89633
+ if (line === ".swarm" || line === ".swarm/")
89634
+ return true;
89635
+ }
89636
+ return false;
89637
+ }
89638
+ var ENSURE_SWARM_GIT_EXCLUDED_OUTER_TIMEOUT_MS = 3000;
89639
+ var ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS = 1500;
89640
+ var GIT_SPAWN_OPTIONS = {
89641
+ timeout: ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS,
89642
+ stdin: "ignore",
89643
+ stdout: "pipe",
89644
+ stderr: "pipe"
89645
+ };
89646
+ async function ensureSwarmGitExcluded(directory, options = {}) {
89647
+ if (_swarmGitExcludedChecked)
89648
+ return;
89649
+ _swarmGitExcludedChecked = true;
89650
+ const { quiet = false } = options;
89651
+ try {
89652
+ const gitRootProc = _internals.bunSpawn(["git", "-C", directory, "rev-parse", "--show-toplevel"], GIT_SPAWN_OPTIONS);
89653
+ let gitRootExitCode;
89654
+ let gitRootOutput;
89655
+ try {
89656
+ [gitRootExitCode, gitRootOutput] = await Promise.all([
89657
+ gitRootProc.exited,
89658
+ gitRootProc.stdout.text()
89659
+ ]);
89660
+ } finally {
89661
+ try {
89662
+ gitRootProc.kill();
89663
+ } catch {}
89664
+ }
89665
+ if (gitRootExitCode !== 0)
89666
+ return;
89667
+ const gitRoot = gitRootOutput.trim();
89668
+ if (!gitRoot)
89669
+ return;
89670
+ const excludePathProc = _internals.bunSpawn(["git", "-C", directory, "rev-parse", "--git-path", "info/exclude"], GIT_SPAWN_OPTIONS);
89671
+ let excludePathExitCode;
89672
+ let excludePathRaw;
89673
+ try {
89674
+ [excludePathExitCode, excludePathRaw] = await Promise.all([
89675
+ excludePathProc.exited,
89676
+ excludePathProc.stdout.text()
89677
+ ]);
89678
+ } finally {
89679
+ try {
89680
+ excludePathProc.kill();
89681
+ } catch {}
89682
+ }
89683
+ if (excludePathExitCode !== 0)
89684
+ return;
89685
+ const excludeRelPath = excludePathRaw.trim();
89686
+ if (!excludeRelPath)
89687
+ return;
89688
+ const excludePath = path104.isAbsolute(excludeRelPath) ? excludeRelPath : path104.join(directory, excludeRelPath);
89689
+ const checkIgnoreProc = _internals.bunSpawn(["git", "-C", directory, "check-ignore", "-q", ".swarm/.gitkeep"], GIT_SPAWN_OPTIONS);
89690
+ let checkIgnoreExitCode;
89691
+ try {
89692
+ checkIgnoreExitCode = await checkIgnoreProc.exited;
89693
+ } finally {
89694
+ try {
89695
+ checkIgnoreProc.kill();
89696
+ } catch {}
89697
+ }
89698
+ if (checkIgnoreExitCode !== 0) {
89699
+ try {
89700
+ fs84.mkdirSync(path104.dirname(excludePath), { recursive: true });
89701
+ let existing = "";
89702
+ try {
89703
+ existing = fs84.readFileSync(excludePath, "utf8");
89704
+ } catch {}
89705
+ if (!fileCoversSwarm(existing)) {
89706
+ fs84.appendFileSync(excludePath, `
89707
+ # opencode-swarm local runtime state
89708
+ .swarm/
89709
+ `, "utf8");
89710
+ if (!quiet) {
89711
+ console.warn("[opencode-swarm] Added .swarm/ to .git/info/exclude to prevent runtime state from appearing in git status.");
89712
+ }
89713
+ }
89714
+ } catch {}
89715
+ }
89716
+ const trackedProc = _internals.bunSpawn(["git", "-C", directory, "ls-files", "--", ".swarm"], GIT_SPAWN_OPTIONS);
89717
+ let trackedExitCode;
89718
+ let trackedOutput;
89719
+ try {
89720
+ [trackedExitCode, trackedOutput] = await Promise.all([
89721
+ trackedProc.exited,
89722
+ trackedProc.stdout.text()
89723
+ ]);
89724
+ } finally {
89725
+ try {
89726
+ trackedProc.kill();
89727
+ } catch {}
89728
+ }
89729
+ if (trackedExitCode === 0 && trackedOutput.trim().length > 0) {
89730
+ console.warn(`[opencode-swarm] WARNING: .swarm/ files are tracked by Git.
89731
+ ` + `.swarm/ contains local runtime state and may contain sensitive session data.
89732
+ ` + `Ignoring will not affect already-tracked files. To stop tracking them, run:
89733
+ ` + ` git rm -r --cached .swarm
89734
+ ` + ` echo ".swarm/" >> .gitignore
89735
+ ` + ' git commit -m "Stop tracking opencode-swarm runtime state"');
89736
+ }
89737
+ } catch {}
89738
+ }
89739
+
89740
+ // src/hooks/diff-scope.ts
89741
+ var _internals2 = { bunSpawn };
89620
89742
  function getDeclaredScope(taskId, directory) {
89621
89743
  try {
89622
- const planPath = path104.join(directory, ".swarm", "plan.json");
89623
- if (!fs84.existsSync(planPath))
89744
+ const planPath = path105.join(directory, ".swarm", "plan.json");
89745
+ if (!fs85.existsSync(planPath))
89624
89746
  return null;
89625
- const raw = fs84.readFileSync(planPath, "utf-8");
89747
+ const raw = fs85.readFileSync(planPath, "utf-8");
89626
89748
  const plan = JSON.parse(raw);
89627
89749
  for (const phase of plan.phases ?? []) {
89628
89750
  for (const task of phase.tasks ?? []) {
@@ -89643,30 +89765,47 @@ function getDeclaredScope(taskId, directory) {
89643
89765
  return null;
89644
89766
  }
89645
89767
  }
89768
+ var GIT_DIFF_SPAWN_OPTIONS = {
89769
+ timeout: ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS,
89770
+ stdin: "ignore",
89771
+ stdout: "pipe",
89772
+ stderr: "pipe"
89773
+ };
89646
89774
  async function getChangedFiles(directory) {
89647
89775
  try {
89648
- const proc = bunSpawn(["git", "diff", "--name-only", "HEAD~1"], {
89776
+ const proc = _internals2.bunSpawn(["git", "diff", "--name-only", "HEAD~1"], {
89649
89777
  cwd: directory,
89650
- stdout: "pipe",
89651
- stderr: "pipe"
89778
+ ...GIT_DIFF_SPAWN_OPTIONS
89652
89779
  });
89653
- const [exitCode, stdout] = await Promise.all([
89654
- proc.exited,
89655
- proc.stdout.text()
89656
- ]);
89780
+ let exitCode;
89781
+ let stdout;
89782
+ try {
89783
+ [exitCode, stdout] = await Promise.all([proc.exited, proc.stdout.text()]);
89784
+ } finally {
89785
+ try {
89786
+ proc.kill();
89787
+ } catch {}
89788
+ }
89657
89789
  if (exitCode === 0) {
89658
89790
  return stdout.trim().split(`
89659
89791
  `).map((f) => f.trim()).filter((f) => f.length > 0);
89660
89792
  }
89661
- const proc2 = bunSpawn(["git", "diff", "--name-only", "HEAD"], {
89793
+ const proc2 = _internals2.bunSpawn(["git", "diff", "--name-only", "HEAD"], {
89662
89794
  cwd: directory,
89663
- stdout: "pipe",
89664
- stderr: "pipe"
89795
+ ...GIT_DIFF_SPAWN_OPTIONS
89665
89796
  });
89666
- const [exitCode2, stdout2] = await Promise.all([
89667
- proc2.exited,
89668
- proc2.stdout.text()
89669
- ]);
89797
+ let exitCode2;
89798
+ let stdout2;
89799
+ try {
89800
+ [exitCode2, stdout2] = await Promise.all([
89801
+ proc2.exited,
89802
+ proc2.stdout.text()
89803
+ ]);
89804
+ } finally {
89805
+ try {
89806
+ proc2.kill();
89807
+ } catch {}
89808
+ }
89670
89809
  if (exitCode2 === 0) {
89671
89810
  return stdout2.trim().split(`
89672
89811
  `).map((f) => f.trim()).filter((f) => f.length > 0);
@@ -89739,7 +89878,7 @@ var TIER_3_PATTERNS = [
89739
89878
  ];
89740
89879
  function matchesTier3Pattern(files) {
89741
89880
  for (const file3 of files) {
89742
- const fileName = path105.basename(file3);
89881
+ const fileName = path106.basename(file3);
89743
89882
  for (const pattern of TIER_3_PATTERNS) {
89744
89883
  if (pattern.test(fileName)) {
89745
89884
  return true;
@@ -89753,8 +89892,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
89753
89892
  if (hasActiveTurboMode()) {
89754
89893
  const resolvedDir2 = workingDirectory;
89755
89894
  try {
89756
- const planPath = path105.join(resolvedDir2, ".swarm", "plan.json");
89757
- const planRaw = fs85.readFileSync(planPath, "utf-8");
89895
+ const planPath = path106.join(resolvedDir2, ".swarm", "plan.json");
89896
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
89758
89897
  const plan = JSON.parse(planRaw);
89759
89898
  for (const planPhase of plan.phases ?? []) {
89760
89899
  for (const task of planPhase.tasks ?? []) {
@@ -89823,8 +89962,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
89823
89962
  }
89824
89963
  try {
89825
89964
  const resolvedDir2 = workingDirectory;
89826
- const planPath = path105.join(resolvedDir2, ".swarm", "plan.json");
89827
- const planRaw = fs85.readFileSync(planPath, "utf-8");
89965
+ const planPath = path106.join(resolvedDir2, ".swarm", "plan.json");
89966
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
89828
89967
  const plan = JSON.parse(planRaw);
89829
89968
  for (const planPhase of plan.phases ?? []) {
89830
89969
  for (const task of planPhase.tasks ?? []) {
@@ -90013,8 +90152,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90013
90152
  };
90014
90153
  }
90015
90154
  }
90016
- normalizedDir = path105.normalize(args2.working_directory);
90017
- const pathParts = normalizedDir.split(path105.sep);
90155
+ normalizedDir = path106.normalize(args2.working_directory);
90156
+ const pathParts = normalizedDir.split(path106.sep);
90018
90157
  if (pathParts.includes("..")) {
90019
90158
  return {
90020
90159
  success: false,
@@ -90024,11 +90163,11 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90024
90163
  ]
90025
90164
  };
90026
90165
  }
90027
- const resolvedDir = path105.resolve(normalizedDir);
90166
+ const resolvedDir = path106.resolve(normalizedDir);
90028
90167
  try {
90029
- const realPath = fs85.realpathSync(resolvedDir);
90030
- const planPath = path105.join(realPath, ".swarm", "plan.json");
90031
- if (!fs85.existsSync(planPath)) {
90168
+ const realPath = fs86.realpathSync(resolvedDir);
90169
+ const planPath = path106.join(realPath, ".swarm", "plan.json");
90170
+ if (!fs86.existsSync(planPath)) {
90032
90171
  return {
90033
90172
  success: false,
90034
90173
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -90059,22 +90198,22 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90059
90198
  }
90060
90199
  if (args2.status === "in_progress") {
90061
90200
  try {
90062
- const evidencePath = path105.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
90063
- fs85.mkdirSync(path105.dirname(evidencePath), { recursive: true });
90064
- const fd = fs85.openSync(evidencePath, "wx");
90201
+ const evidencePath = path106.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
90202
+ fs86.mkdirSync(path106.dirname(evidencePath), { recursive: true });
90203
+ const fd = fs86.openSync(evidencePath, "wx");
90065
90204
  let writeOk = false;
90066
90205
  try {
90067
- fs85.writeSync(fd, JSON.stringify({
90206
+ fs86.writeSync(fd, JSON.stringify({
90068
90207
  taskId: args2.task_id,
90069
90208
  required_gates: ["reviewer", "test_engineer"],
90070
90209
  gates: {}
90071
90210
  }, null, 2));
90072
90211
  writeOk = true;
90073
90212
  } finally {
90074
- fs85.closeSync(fd);
90213
+ fs86.closeSync(fd);
90075
90214
  if (!writeOk) {
90076
90215
  try {
90077
- fs85.unlinkSync(evidencePath);
90216
+ fs86.unlinkSync(evidencePath);
90078
90217
  } catch {}
90079
90218
  }
90080
90219
  }
@@ -90084,8 +90223,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90084
90223
  recoverTaskStateFromDelegations(args2.task_id);
90085
90224
  let phaseRequiresReviewer = true;
90086
90225
  try {
90087
- const planPath = path105.join(directory, ".swarm", "plan.json");
90088
- const planRaw = fs85.readFileSync(planPath, "utf-8");
90226
+ const planPath = path106.join(directory, ".swarm", "plan.json");
90227
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
90089
90228
  const plan = JSON.parse(planRaw);
90090
90229
  const taskPhase = plan.phases.find((p) => p.tasks.some((t) => t.id === args2.task_id));
90091
90230
  if (taskPhase?.required_agents && !taskPhase.required_agents.includes("reviewer")) {
@@ -90395,8 +90534,8 @@ init_utils2();
90395
90534
  init_ledger();
90396
90535
  init_manager();
90397
90536
  init_create_tool();
90398
- import fs86 from "node:fs";
90399
- import path106 from "node:path";
90537
+ import fs87 from "node:fs";
90538
+ import path107 from "node:path";
90400
90539
  function derivePlanId5(plan) {
90401
90540
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
90402
90541
  }
@@ -90447,7 +90586,7 @@ async function executeWriteDriftEvidence(args2, directory) {
90447
90586
  entries: [evidenceEntry]
90448
90587
  };
90449
90588
  const filename = "drift-verifier.json";
90450
- const relativePath = path106.join("evidence", String(phase), filename);
90589
+ const relativePath = path107.join("evidence", String(phase), filename);
90451
90590
  let validatedPath;
90452
90591
  try {
90453
90592
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90458,12 +90597,12 @@ async function executeWriteDriftEvidence(args2, directory) {
90458
90597
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90459
90598
  }, null, 2);
90460
90599
  }
90461
- const evidenceDir = path106.dirname(validatedPath);
90600
+ const evidenceDir = path107.dirname(validatedPath);
90462
90601
  try {
90463
- await fs86.promises.mkdir(evidenceDir, { recursive: true });
90464
- const tempPath = path106.join(evidenceDir, `.${filename}.tmp`);
90465
- await fs86.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90466
- await fs86.promises.rename(tempPath, validatedPath);
90602
+ await fs87.promises.mkdir(evidenceDir, { recursive: true });
90603
+ const tempPath = path107.join(evidenceDir, `.${filename}.tmp`);
90604
+ await fs87.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90605
+ await fs87.promises.rename(tempPath, validatedPath);
90467
90606
  let snapshotInfo;
90468
90607
  let snapshotError;
90469
90608
  let qaProfileLocked;
@@ -90557,8 +90696,8 @@ var write_drift_evidence = createSwarmTool({
90557
90696
  init_zod();
90558
90697
  init_utils2();
90559
90698
  init_create_tool();
90560
- import fs87 from "node:fs";
90561
- import path107 from "node:path";
90699
+ import fs88 from "node:fs";
90700
+ import path108 from "node:path";
90562
90701
  function normalizeVerdict2(verdict) {
90563
90702
  switch (verdict) {
90564
90703
  case "APPROVED":
@@ -90606,7 +90745,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
90606
90745
  entries: [evidenceEntry]
90607
90746
  };
90608
90747
  const filename = "hallucination-guard.json";
90609
- const relativePath = path107.join("evidence", String(phase), filename);
90748
+ const relativePath = path108.join("evidence", String(phase), filename);
90610
90749
  let validatedPath;
90611
90750
  try {
90612
90751
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90617,12 +90756,12 @@ async function executeWriteHallucinationEvidence(args2, directory) {
90617
90756
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90618
90757
  }, null, 2);
90619
90758
  }
90620
- const evidenceDir = path107.dirname(validatedPath);
90759
+ const evidenceDir = path108.dirname(validatedPath);
90621
90760
  try {
90622
- await fs87.promises.mkdir(evidenceDir, { recursive: true });
90623
- const tempPath = path107.join(evidenceDir, `.${filename}.tmp`);
90624
- await fs87.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90625
- await fs87.promises.rename(tempPath, validatedPath);
90761
+ await fs88.promises.mkdir(evidenceDir, { recursive: true });
90762
+ const tempPath = path108.join(evidenceDir, `.${filename}.tmp`);
90763
+ await fs88.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90764
+ await fs88.promises.rename(tempPath, validatedPath);
90626
90765
  return JSON.stringify({
90627
90766
  success: true,
90628
90767
  phase,
@@ -90668,8 +90807,8 @@ var write_hallucination_evidence = createSwarmTool({
90668
90807
  init_zod();
90669
90808
  init_utils2();
90670
90809
  init_create_tool();
90671
- import fs88 from "node:fs";
90672
- import path108 from "node:path";
90810
+ import fs89 from "node:fs";
90811
+ import path109 from "node:path";
90673
90812
  function normalizeVerdict3(verdict) {
90674
90813
  switch (verdict) {
90675
90814
  case "PASS":
@@ -90743,7 +90882,7 @@ async function executeWriteMutationEvidence(args2, directory) {
90743
90882
  entries: [evidenceEntry]
90744
90883
  };
90745
90884
  const filename = "mutation-gate.json";
90746
- const relativePath = path108.join("evidence", String(phase), filename);
90885
+ const relativePath = path109.join("evidence", String(phase), filename);
90747
90886
  let validatedPath;
90748
90887
  try {
90749
90888
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90754,12 +90893,12 @@ async function executeWriteMutationEvidence(args2, directory) {
90754
90893
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90755
90894
  }, null, 2);
90756
90895
  }
90757
- const evidenceDir = path108.dirname(validatedPath);
90896
+ const evidenceDir = path109.dirname(validatedPath);
90758
90897
  try {
90759
- await fs88.promises.mkdir(evidenceDir, { recursive: true });
90760
- const tempPath = path108.join(evidenceDir, `.${filename}.tmp`);
90761
- await fs88.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90762
- await fs88.promises.rename(tempPath, validatedPath);
90898
+ await fs89.promises.mkdir(evidenceDir, { recursive: true });
90899
+ const tempPath = path109.join(evidenceDir, `.${filename}.tmp`);
90900
+ await fs89.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90901
+ await fs89.promises.rename(tempPath, validatedPath);
90763
90902
  return JSON.stringify({
90764
90903
  success: true,
90765
90904
  phase,
@@ -90812,85 +90951,6 @@ init_write_retro();
90812
90951
  // src/index.ts
90813
90952
  init_utils();
90814
90953
 
90815
- // src/utils/gitignore-warning.ts
90816
- init_bun_compat();
90817
- import * as fs89 from "node:fs";
90818
- import * as path109 from "node:path";
90819
- var _swarmGitExcludedChecked = false;
90820
- function fileCoversSwarm(content) {
90821
- for (const rawLine of content.split(`
90822
- `)) {
90823
- const line = rawLine.trim();
90824
- if (line.startsWith("#") || line.length === 0)
90825
- continue;
90826
- if (line === ".swarm" || line === ".swarm/")
90827
- return true;
90828
- }
90829
- return false;
90830
- }
90831
- async function ensureSwarmGitExcluded(directory, options = {}) {
90832
- if (_swarmGitExcludedChecked)
90833
- return;
90834
- _swarmGitExcludedChecked = true;
90835
- const { quiet = false } = options;
90836
- try {
90837
- const gitRootProc = bunSpawn(["git", "-C", directory, "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "pipe" });
90838
- const [gitRootExitCode, gitRootOutput] = await Promise.all([
90839
- gitRootProc.exited,
90840
- gitRootProc.stdout.text()
90841
- ]);
90842
- if (gitRootExitCode !== 0)
90843
- return;
90844
- const gitRoot = gitRootOutput.trim();
90845
- if (!gitRoot)
90846
- return;
90847
- const excludePathProc = bunSpawn(["git", "-C", directory, "rev-parse", "--git-path", "info/exclude"], { stdout: "pipe", stderr: "pipe" });
90848
- const [excludePathExitCode, excludePathRaw] = await Promise.all([
90849
- excludePathProc.exited,
90850
- excludePathProc.stdout.text()
90851
- ]);
90852
- if (excludePathExitCode !== 0)
90853
- return;
90854
- const excludeRelPath = excludePathRaw.trim();
90855
- if (!excludeRelPath)
90856
- return;
90857
- const excludePath = path109.isAbsolute(excludeRelPath) ? excludeRelPath : path109.join(directory, excludeRelPath);
90858
- const checkIgnoreProc = bunSpawn(["git", "-C", directory, "check-ignore", "-q", ".swarm/.gitkeep"], { stdout: "pipe", stderr: "pipe" });
90859
- const checkIgnoreExitCode = await checkIgnoreProc.exited;
90860
- if (checkIgnoreExitCode !== 0) {
90861
- try {
90862
- fs89.mkdirSync(path109.dirname(excludePath), { recursive: true });
90863
- let existing = "";
90864
- try {
90865
- existing = fs89.readFileSync(excludePath, "utf8");
90866
- } catch {}
90867
- if (!fileCoversSwarm(existing)) {
90868
- fs89.appendFileSync(excludePath, `
90869
- # opencode-swarm local runtime state
90870
- .swarm/
90871
- `, "utf8");
90872
- if (!quiet) {
90873
- console.warn("[opencode-swarm] Added .swarm/ to .git/info/exclude to prevent runtime state from appearing in git status.");
90874
- }
90875
- }
90876
- } catch {}
90877
- }
90878
- const trackedProc = bunSpawn(["git", "-C", directory, "ls-files", "--", ".swarm"], { stdout: "pipe", stderr: "pipe" });
90879
- const [trackedExitCode, trackedOutput] = await Promise.all([
90880
- trackedProc.exited,
90881
- trackedProc.stdout.text()
90882
- ]);
90883
- if (trackedExitCode === 0 && trackedOutput.trim().length > 0) {
90884
- console.warn(`[opencode-swarm] WARNING: .swarm/ files are tracked by Git.
90885
- ` + `.swarm/ contains local runtime state and may contain sensitive session data.
90886
- ` + `Ignoring will not affect already-tracked files. To stop tracking them, run:
90887
- ` + ` git rm -r --cached .swarm
90888
- ` + ` echo ".swarm/" >> .gitignore
90889
- ` + ' git commit -m "Stop tracking opencode-swarm runtime state"');
90890
- }
90891
- } catch {}
90892
- }
90893
-
90894
90954
  // src/utils/tool-output.ts
90895
90955
  function truncateToolOutput(output, maxLines, toolName, tailLines = 10) {
90896
90956
  if (!output) {
@@ -90994,7 +91054,12 @@ async function initializeOpenCodeSwarm(ctx) {
90994
91054
  }
90995
91055
  repoGraphHook.init().catch(() => {}).finally(() => clearTimeout(watchdog));
90996
91056
  });
90997
- await ensureSwarmGitExcluded(ctx.directory, { quiet: config3.quiet });
91057
+ await withTimeout(ensureSwarmGitExcluded(ctx.directory, { quiet: config3.quiet }), ENSURE_SWARM_GIT_EXCLUDED_OUTER_TIMEOUT_MS, new Error(`ensureSwarmGitExcluded exceeded ${ENSURE_SWARM_GIT_EXCLUDED_OUTER_TIMEOUT_MS}ms budget; continuing without git-hygiene check`)).catch((err3) => {
91058
+ const msg = err3 instanceof Error ? err3.message : String(err3);
91059
+ log("ensureSwarmGitExcluded timed out or failed (non-fatal)", {
91060
+ error: msg
91061
+ });
91062
+ });
90998
91063
  initTelemetry(ctx.directory);
90999
91064
  writeSwarmConfigExampleIfNew(ctx.directory);
91000
91065
  writeProjectConfigIfNew(ctx.directory, config3.quiet);
@@ -1,3 +1,15 @@
1
+ import { bunSpawn } from './bun-compat';
2
+ /**
3
+ * Test-only dependency-injection seam. Production code calls
4
+ * `_internals.bunSpawn(...)` so tests can replace the function on this object
5
+ * without touching the real `./bun-compat` module — `mock.module` from
6
+ * `bun:test` leaks across files in Bun's shared test-runner process, which
7
+ * would corrupt unrelated suites that import `bun-compat`. Mutating this
8
+ * local object is file-scoped and trivially restorable via `afterEach`.
9
+ */
10
+ export declare const _internals: {
11
+ bunSpawn: typeof bunSpawn;
12
+ };
1
13
  /**
2
14
  * Module-level flag so the warning fires at most once per process.
3
15
  * Exported for test reset purposes only — do not use in production code.
@@ -30,6 +42,31 @@ export declare function warnIfSwarmNotGitignored(directory: string, quiet?: bool
30
42
  export interface EnsureSwarmGitExcludedOptions {
31
43
  quiet?: boolean;
32
44
  }
45
+ /**
46
+ * Hard upper bound on the entire `ensureSwarmGitExcluded` operation when
47
+ * called from plugin init. The plugin host (OpenCode TUI / Desktop) will
48
+ * silently drop a plugin whose entry never resolves (issue #704); every
49
+ * awaited call on the init path therefore has an obligation to be bounded.
50
+ *
51
+ * 3_000 ms is ~30× the realistic worst-case duration on a healthy host (all
52
+ * four `git` calls land in well under 200 ms in aggregate) and ~6× the
53
+ * per-call budget below. Slower-than-3 s hosts are pathological (NFS-stalled
54
+ * `.git`, antivirus quarantine) and we deliberately fail-open: a debug log
55
+ * is emitted and the plugin continues to load without the hygiene exclude.
56
+ */
57
+ export declare const ENSURE_SWARM_GIT_EXCLUDED_OUTER_TIMEOUT_MS = 3000;
58
+ /**
59
+ * Hard upper bound on each individual `git` subprocess invoked by
60
+ * `ensureSwarmGitExcluded` (and reused by `validateDiffScope`). Both Bun's
61
+ * `Bun.spawn` and the Node fallback in `bunSpawn` honor this `timeout`
62
+ * option and kill the child on expiry (`bun-compat.ts` Node fallback calls
63
+ * `proc.kill('SIGKILL')`; Bun kills via `killSignal`).
64
+ *
65
+ * 1_500 ms gives a ~30× margin over the realistic worst case and is well
66
+ * below the outer wrapper budget so the inner kills fire first on a
67
+ * pathological host.
68
+ */
69
+ export declare const ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS = 1500;
33
70
  /**
34
71
  * Automatically protect `.swarm/` from Git pollution before any `.swarm/` write.
35
72
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.3.3",
3
+ "version": "7.3.4",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",