opencode-swarm 7.3.2 → 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.2",
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.2",
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",
@@ -65374,7 +65374,7 @@ function createAgentActivityHooks(config3, directory) {
65374
65374
  const duration5 = Date.now() - entry.startTime;
65375
65375
  const explicitSuccess = typeof output.success === "boolean" ? output.success : undefined;
65376
65376
  const explicitFailure = explicitSuccess === false || !!output.error;
65377
- const success3 = explicitFailure ? false : true;
65377
+ const success3 = !explicitFailure;
65378
65378
  const key = entry.tool;
65379
65379
  const existing = swarmState.toolAggregates.get(key) ?? {
65380
65380
  tool: key,
@@ -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);
@@ -89684,9 +89823,10 @@ async function validateDiffScope(taskId, directory) {
89684
89823
  const changedFiles = await getChangedFiles(directory);
89685
89824
  if (!changedFiles)
89686
89825
  return null;
89826
+ const nonSwarmFiles = changedFiles.filter((f) => !f.replace(/\\/g, "/").startsWith(".swarm/"));
89687
89827
  const normalise = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
89688
89828
  const normScope = new Set(declaredScope.map(normalise));
89689
- const undeclared = changedFiles.map(normalise).filter((f) => !normScope.has(f));
89829
+ const undeclared = nonSwarmFiles.map(normalise).filter((f) => !normScope.has(f));
89690
89830
  if (undeclared.length === 0)
89691
89831
  return null;
89692
89832
  const scopeStr = declaredScope.join(", ");
@@ -89738,7 +89878,7 @@ var TIER_3_PATTERNS = [
89738
89878
  ];
89739
89879
  function matchesTier3Pattern(files) {
89740
89880
  for (const file3 of files) {
89741
- const fileName = path105.basename(file3);
89881
+ const fileName = path106.basename(file3);
89742
89882
  for (const pattern of TIER_3_PATTERNS) {
89743
89883
  if (pattern.test(fileName)) {
89744
89884
  return true;
@@ -89752,8 +89892,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
89752
89892
  if (hasActiveTurboMode()) {
89753
89893
  const resolvedDir2 = workingDirectory;
89754
89894
  try {
89755
- const planPath = path105.join(resolvedDir2, ".swarm", "plan.json");
89756
- const planRaw = fs85.readFileSync(planPath, "utf-8");
89895
+ const planPath = path106.join(resolvedDir2, ".swarm", "plan.json");
89896
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
89757
89897
  const plan = JSON.parse(planRaw);
89758
89898
  for (const planPhase of plan.phases ?? []) {
89759
89899
  for (const task of planPhase.tasks ?? []) {
@@ -89822,8 +89962,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
89822
89962
  }
89823
89963
  try {
89824
89964
  const resolvedDir2 = workingDirectory;
89825
- const planPath = path105.join(resolvedDir2, ".swarm", "plan.json");
89826
- const planRaw = fs85.readFileSync(planPath, "utf-8");
89965
+ const planPath = path106.join(resolvedDir2, ".swarm", "plan.json");
89966
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
89827
89967
  const plan = JSON.parse(planRaw);
89828
89968
  for (const planPhase of plan.phases ?? []) {
89829
89969
  for (const task of planPhase.tasks ?? []) {
@@ -90012,8 +90152,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90012
90152
  };
90013
90153
  }
90014
90154
  }
90015
- normalizedDir = path105.normalize(args2.working_directory);
90016
- const pathParts = normalizedDir.split(path105.sep);
90155
+ normalizedDir = path106.normalize(args2.working_directory);
90156
+ const pathParts = normalizedDir.split(path106.sep);
90017
90157
  if (pathParts.includes("..")) {
90018
90158
  return {
90019
90159
  success: false,
@@ -90023,11 +90163,11 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90023
90163
  ]
90024
90164
  };
90025
90165
  }
90026
- const resolvedDir = path105.resolve(normalizedDir);
90166
+ const resolvedDir = path106.resolve(normalizedDir);
90027
90167
  try {
90028
- const realPath = fs85.realpathSync(resolvedDir);
90029
- const planPath = path105.join(realPath, ".swarm", "plan.json");
90030
- if (!fs85.existsSync(planPath)) {
90168
+ const realPath = fs86.realpathSync(resolvedDir);
90169
+ const planPath = path106.join(realPath, ".swarm", "plan.json");
90170
+ if (!fs86.existsSync(planPath)) {
90031
90171
  return {
90032
90172
  success: false,
90033
90173
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -90058,22 +90198,22 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90058
90198
  }
90059
90199
  if (args2.status === "in_progress") {
90060
90200
  try {
90061
- const evidencePath = path105.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
90062
- fs85.mkdirSync(path105.dirname(evidencePath), { recursive: true });
90063
- 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");
90064
90204
  let writeOk = false;
90065
90205
  try {
90066
- fs85.writeSync(fd, JSON.stringify({
90206
+ fs86.writeSync(fd, JSON.stringify({
90067
90207
  taskId: args2.task_id,
90068
90208
  required_gates: ["reviewer", "test_engineer"],
90069
90209
  gates: {}
90070
90210
  }, null, 2));
90071
90211
  writeOk = true;
90072
90212
  } finally {
90073
- fs85.closeSync(fd);
90213
+ fs86.closeSync(fd);
90074
90214
  if (!writeOk) {
90075
90215
  try {
90076
- fs85.unlinkSync(evidencePath);
90216
+ fs86.unlinkSync(evidencePath);
90077
90217
  } catch {}
90078
90218
  }
90079
90219
  }
@@ -90083,8 +90223,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90083
90223
  recoverTaskStateFromDelegations(args2.task_id);
90084
90224
  let phaseRequiresReviewer = true;
90085
90225
  try {
90086
- const planPath = path105.join(directory, ".swarm", "plan.json");
90087
- const planRaw = fs85.readFileSync(planPath, "utf-8");
90226
+ const planPath = path106.join(directory, ".swarm", "plan.json");
90227
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
90088
90228
  const plan = JSON.parse(planRaw);
90089
90229
  const taskPhase = plan.phases.find((p) => p.tasks.some((t) => t.id === args2.task_id));
90090
90230
  if (taskPhase?.required_agents && !taskPhase.required_agents.includes("reviewer")) {
@@ -90394,8 +90534,8 @@ init_utils2();
90394
90534
  init_ledger();
90395
90535
  init_manager();
90396
90536
  init_create_tool();
90397
- import fs86 from "node:fs";
90398
- import path106 from "node:path";
90537
+ import fs87 from "node:fs";
90538
+ import path107 from "node:path";
90399
90539
  function derivePlanId5(plan) {
90400
90540
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
90401
90541
  }
@@ -90446,7 +90586,7 @@ async function executeWriteDriftEvidence(args2, directory) {
90446
90586
  entries: [evidenceEntry]
90447
90587
  };
90448
90588
  const filename = "drift-verifier.json";
90449
- const relativePath = path106.join("evidence", String(phase), filename);
90589
+ const relativePath = path107.join("evidence", String(phase), filename);
90450
90590
  let validatedPath;
90451
90591
  try {
90452
90592
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90457,12 +90597,12 @@ async function executeWriteDriftEvidence(args2, directory) {
90457
90597
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90458
90598
  }, null, 2);
90459
90599
  }
90460
- const evidenceDir = path106.dirname(validatedPath);
90600
+ const evidenceDir = path107.dirname(validatedPath);
90461
90601
  try {
90462
- await fs86.promises.mkdir(evidenceDir, { recursive: true });
90463
- const tempPath = path106.join(evidenceDir, `.${filename}.tmp`);
90464
- await fs86.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90465
- 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);
90466
90606
  let snapshotInfo;
90467
90607
  let snapshotError;
90468
90608
  let qaProfileLocked;
@@ -90556,8 +90696,8 @@ var write_drift_evidence = createSwarmTool({
90556
90696
  init_zod();
90557
90697
  init_utils2();
90558
90698
  init_create_tool();
90559
- import fs87 from "node:fs";
90560
- import path107 from "node:path";
90699
+ import fs88 from "node:fs";
90700
+ import path108 from "node:path";
90561
90701
  function normalizeVerdict2(verdict) {
90562
90702
  switch (verdict) {
90563
90703
  case "APPROVED":
@@ -90605,7 +90745,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
90605
90745
  entries: [evidenceEntry]
90606
90746
  };
90607
90747
  const filename = "hallucination-guard.json";
90608
- const relativePath = path107.join("evidence", String(phase), filename);
90748
+ const relativePath = path108.join("evidence", String(phase), filename);
90609
90749
  let validatedPath;
90610
90750
  try {
90611
90751
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90616,12 +90756,12 @@ async function executeWriteHallucinationEvidence(args2, directory) {
90616
90756
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90617
90757
  }, null, 2);
90618
90758
  }
90619
- const evidenceDir = path107.dirname(validatedPath);
90759
+ const evidenceDir = path108.dirname(validatedPath);
90620
90760
  try {
90621
- await fs87.promises.mkdir(evidenceDir, { recursive: true });
90622
- const tempPath = path107.join(evidenceDir, `.${filename}.tmp`);
90623
- await fs87.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90624
- 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);
90625
90765
  return JSON.stringify({
90626
90766
  success: true,
90627
90767
  phase,
@@ -90667,8 +90807,8 @@ var write_hallucination_evidence = createSwarmTool({
90667
90807
  init_zod();
90668
90808
  init_utils2();
90669
90809
  init_create_tool();
90670
- import fs88 from "node:fs";
90671
- import path108 from "node:path";
90810
+ import fs89 from "node:fs";
90811
+ import path109 from "node:path";
90672
90812
  function normalizeVerdict3(verdict) {
90673
90813
  switch (verdict) {
90674
90814
  case "PASS":
@@ -90742,7 +90882,7 @@ async function executeWriteMutationEvidence(args2, directory) {
90742
90882
  entries: [evidenceEntry]
90743
90883
  };
90744
90884
  const filename = "mutation-gate.json";
90745
- const relativePath = path108.join("evidence", String(phase), filename);
90885
+ const relativePath = path109.join("evidence", String(phase), filename);
90746
90886
  let validatedPath;
90747
90887
  try {
90748
90888
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90753,12 +90893,12 @@ async function executeWriteMutationEvidence(args2, directory) {
90753
90893
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90754
90894
  }, null, 2);
90755
90895
  }
90756
- const evidenceDir = path108.dirname(validatedPath);
90896
+ const evidenceDir = path109.dirname(validatedPath);
90757
90897
  try {
90758
- await fs88.promises.mkdir(evidenceDir, { recursive: true });
90759
- const tempPath = path108.join(evidenceDir, `.${filename}.tmp`);
90760
- await fs88.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90761
- 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);
90762
90902
  return JSON.stringify({
90763
90903
  success: true,
90764
90904
  phase,
@@ -90811,69 +90951,6 @@ init_write_retro();
90811
90951
  // src/index.ts
90812
90952
  init_utils();
90813
90953
 
90814
- // src/utils/gitignore-warning.ts
90815
- import * as fs89 from "node:fs";
90816
- import * as path109 from "node:path";
90817
- var _gitignoreWarningEmitted = false;
90818
- function findGitRoot(startDir) {
90819
- let current = startDir;
90820
- while (true) {
90821
- try {
90822
- const gitPath = path109.join(current, ".git");
90823
- const stat6 = fs89.statSync(gitPath);
90824
- if (stat6.isDirectory()) {
90825
- return current;
90826
- }
90827
- } catch {}
90828
- const parent = path109.dirname(current);
90829
- if (parent === current) {
90830
- return null;
90831
- }
90832
- current = parent;
90833
- }
90834
- }
90835
- function fileCoversSwarm(content) {
90836
- for (const rawLine of content.split(`
90837
- `)) {
90838
- const line = rawLine.trim();
90839
- if (line.startsWith("#") || line.length === 0)
90840
- continue;
90841
- if (line === ".swarm" || line === ".swarm/")
90842
- return true;
90843
- }
90844
- return false;
90845
- }
90846
- function readFileSafe(filePath) {
90847
- try {
90848
- return fs89.readFileSync(filePath, "utf8");
90849
- } catch {
90850
- return null;
90851
- }
90852
- }
90853
- function warnIfSwarmNotGitignored(directory, quiet = false) {
90854
- if (_gitignoreWarningEmitted)
90855
- return;
90856
- try {
90857
- const gitRoot = findGitRoot(directory);
90858
- if (!gitRoot)
90859
- return;
90860
- const gitignoreContent = readFileSafe(path109.join(gitRoot, ".gitignore"));
90861
- if (gitignoreContent !== null && fileCoversSwarm(gitignoreContent)) {
90862
- _gitignoreWarningEmitted = true;
90863
- return;
90864
- }
90865
- const excludeContent = readFileSafe(path109.join(gitRoot, ".git", "info", "exclude"));
90866
- if (excludeContent !== null && fileCoversSwarm(excludeContent)) {
90867
- _gitignoreWarningEmitted = true;
90868
- return;
90869
- }
90870
- _gitignoreWarningEmitted = true;
90871
- if (!quiet) {
90872
- console.warn('[opencode-swarm] WARNING: .swarm/ is not in your .gitignore. Shell audit logs may contain API keys. Add ".swarm/" to your .gitignore to prevent accidental commits.');
90873
- }
90874
- } catch {}
90875
- }
90876
-
90877
90954
  // src/utils/tool-output.ts
90878
90955
  function truncateToolOutput(output, maxLines, toolName, tailLines = 10) {
90879
90956
  if (!output) {
@@ -90977,10 +91054,15 @@ async function initializeOpenCodeSwarm(ctx) {
90977
91054
  }
90978
91055
  repoGraphHook.init().catch(() => {}).finally(() => clearTimeout(watchdog));
90979
91056
  });
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
+ });
90980
91063
  initTelemetry(ctx.directory);
90981
91064
  writeSwarmConfigExampleIfNew(ctx.directory);
90982
91065
  writeProjectConfigIfNew(ctx.directory, config3.quiet);
90983
- warnIfSwarmNotGitignored(ctx.directory, config3.quiet);
90984
91066
  if (config3.version_check !== false) {
90985
91067
  scheduleVersionCheck(package_default.version, (msg) => {
90986
91068
  if (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.
@@ -7,11 +19,71 @@ export declare let _gitignoreWarningEmitted: boolean;
7
19
  * Reset the deduplication flag. Exposed for test isolation only.
8
20
  */
9
21
  export declare function resetGitignoreWarningState(): void;
22
+ /**
23
+ * Module-level flag for ensureSwarmGitExcluded deduplication.
24
+ * Exported for test reset purposes only.
25
+ */
26
+ export declare let _swarmGitExcludedChecked: boolean;
27
+ /**
28
+ * Reset the ensureSwarmGitExcluded deduplication flag. Exposed for test isolation only.
29
+ */
30
+ export declare function resetSwarmGitExcludedState(): void;
10
31
  /**
11
32
  * Checks whether `.swarm/` is covered by `.gitignore` or `.git/info/exclude`
12
33
  * in the git repo rooted at or above `directory`. If not covered, emits a
13
34
  * single `console.warn` (unless `quiet` is true). Fires at most once per process.
14
35
  *
15
36
  * Never throws — any file-system error silently skips the check.
37
+ *
38
+ * @deprecated Use `ensureSwarmGitExcluded` instead. This function only recognises
39
+ * `.git` as a directory and does NOT handle Git worktrees or submodules.
16
40
  */
17
41
  export declare function warnIfSwarmNotGitignored(directory: string, quiet?: boolean): void;
42
+ export interface EnsureSwarmGitExcludedOptions {
43
+ quiet?: boolean;
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;
70
+ /**
71
+ * Automatically protect `.swarm/` from Git pollution before any `.swarm/` write.
72
+ *
73
+ * Uses git CLI (not filesystem walks) so it correctly handles Git worktrees
74
+ * and submodules where `.git` is a file rather than a directory.
75
+ *
76
+ * Steps:
77
+ * 1. Resolve git root via `git rev-parse --show-toplevel`
78
+ * 2. Resolve local exclude path via `git rev-parse --git-path info/exclude`
79
+ * 3. Check if `.swarm/` is already ignored via `git check-ignore -q`
80
+ * 4. If not ignored: append `.swarm/` to the local exclude file (idempotent)
81
+ * 5. Detect tracked `.swarm/` files via `git ls-files -- .swarm`
82
+ * 6. If tracked: emit an unsuppressed remediation warning
83
+ *
84
+ * Never throws. Fires at most once per process.
85
+ *
86
+ * quiet option: only suppresses cosmetic logs. The exclude write and tracked-file
87
+ * warning are never suppressed regardless of quiet mode.
88
+ */
89
+ export declare function ensureSwarmGitExcluded(directory: string, options?: EnsureSwarmGitExcludedOptions): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.3.2",
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",