opencode-swarm 7.3.3 → 7.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -788,6 +788,17 @@ Prefixed agents (e.g., `paid_coder`, `mega_reviewer`, `local_architect`) inherit
788
788
 
789
789
  In this example, `paid_coder` gets its own explicit rule, while other prefixed coders (e.g., `mega_coder`) fall back to `coder`.
790
790
 
791
+ #### Selecting the primary agent in multi-swarm configs (`default_agent`)
792
+
793
+ The top-level `default_agent` field controls which generated agents OpenCode treats as primary. **It is optional.** Behavior:
794
+
795
+ - **Omitted** — every architect-role agent is primary. In a multi-swarm config that means each swarm exposes its own architect (`local_architect`, `mega_architect`, `paid_architect`, `modelrelay_architect`, …) as a selectable session default. This is the v7.0.0-compatible behavior and the recommended setup.
796
+ - **Base role** (e.g. `"coder"`) — every generated agent whose canonical base role matches becomes primary (`local_coder`, `mega_coder`, …).
797
+ - **Exact generated name** (e.g. `"local_architect"`) — only that agent is primary.
798
+ - **Unknown / invalid value** — a one-time warning is logged and the resolver falls back to architect-role primaries (or the first generated agent if architects are disabled). The plugin never produces zero primaries when at least one agent exists.
799
+
800
+ See [`docs/configuration.md`](docs/configuration.md) for the full table.
801
+
791
802
  ### Runtime Enforcement
792
803
 
793
804
  Architect direct writes are enforced at runtime via `toolBefore` hook. This tracks writes to source code paths outside `.swarm/` and protects `.swarm/plan.md` and `.swarm/plan.json` from direct modification.
@@ -34,6 +34,34 @@ export declare function getSwarmAgents(): Record<string, {
34
34
  * Create all agent definitions with configuration applied
35
35
  */
36
36
  export declare function createAgents(config?: PluginConfig): AgentDefinition[];
37
+ /**
38
+ * Resolve the set of generated agent names that should be marked as primary
39
+ * for OpenCode's session-default-agent resolution.
40
+ *
41
+ * Resolution rules (see schema.ts default_agent comment for full semantics):
42
+ * - default_agent omitted ⇒ every architect-role agent is primary
43
+ * (canonical base role === "architect"). This restores v7.0.0 behavior in
44
+ * multi-swarm configs where there is no unprefixed `architect` agent.
45
+ * - default_agent exactly matches a generated agent name ⇒ only that agent.
46
+ * Exact match wins over base-role match — `local_architect` resolves to
47
+ * just `local_architect`, never the entire architect role.
48
+ * - default_agent is a base role in ALL_AGENT_NAMES ⇒ every generated agent
49
+ * whose canonical base role matches that role.
50
+ * - default_agent is invalid (matches nothing) ⇒ fall back to architect-role
51
+ * primaries; if no architect roles exist (architects disabled), fall back
52
+ * to the first generated agent. Always warns. Never returns empty when
53
+ * `agentNames` is non-empty.
54
+ *
55
+ * Important matching detail: a value like "not_an_architect" is NOT treated
56
+ * as a base-role request even though stripKnownSwarmPrefix() returns
57
+ * "architect" for it. Base-role matching only fires when the user-supplied
58
+ * value is itself one of ALL_AGENT_NAMES.
59
+ */
60
+ export declare function resolvePrimaryAgentNames(agentNames: string[], defaultAgent?: string): {
61
+ primaryNames: Set<string>;
62
+ reason: 'implicit-architects' | 'exact' | 'base-role' | 'fallback-architects' | 'fallback-first';
63
+ warning?: string;
64
+ };
37
65
  /**
38
66
  * Get agent configurations formatted for the OpenCode SDK.
39
67
  */
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.5",
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",
@@ -16915,7 +16915,12 @@ var init_schema = __esm(() => {
16915
16915
  });
16916
16916
  PluginConfigSchema = exports_external.object({
16917
16917
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
16918
- default_agent: exports_external.enum(ALL_AGENT_NAMES).default("architect").optional(),
16918
+ default_agent: exports_external.string().optional().transform((v) => {
16919
+ if (v === undefined)
16920
+ return;
16921
+ const trimmed = v.trim();
16922
+ return trimmed === "" ? undefined : trimmed;
16923
+ }),
16919
16924
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
16920
16925
  max_iterations: exports_external.number().min(1).max(10).default(5),
16921
16926
  pipeline: PipelineConfigSchema.optional(),
@@ -643,26 +643,7 @@ export declare const PluginConfigSchema: z.ZodObject<{
643
643
  disabled: z.ZodOptional<z.ZodBoolean>;
644
644
  fallback_models: z.ZodOptional<z.ZodArray<z.ZodString>>;
645
645
  }, z.core.$strip>>>;
646
- default_agent: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
647
- architect: "architect";
648
- sme: "sme";
649
- docs: "docs";
650
- designer: "designer";
651
- critic_sounding_board: "critic_sounding_board";
652
- critic_drift_verifier: "critic_drift_verifier";
653
- critic_hallucination_verifier: "critic_hallucination_verifier";
654
- curator_init: "curator_init";
655
- curator_phase: "curator_phase";
656
- council_generalist: "council_generalist";
657
- council_skeptic: "council_skeptic";
658
- council_domain_expert: "council_domain_expert";
659
- reviewer: "reviewer";
660
- critic: "critic";
661
- critic_oversight: "critic_oversight";
662
- explorer: "explorer";
663
- coder: "coder";
664
- test_engineer: "test_engineer";
665
- }>>>;
646
+ default_agent: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>;
666
647
  swarms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
667
648
  name: z.ZodOptional<z.ZodString>;
668
649
  agents: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -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.5",
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",
@@ -15352,7 +15352,12 @@ var init_schema = __esm(() => {
15352
15352
  });
15353
15353
  PluginConfigSchema = exports_external.object({
15354
15354
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
15355
- default_agent: exports_external.enum(ALL_AGENT_NAMES).default("architect").optional(),
15355
+ default_agent: exports_external.string().optional().transform((v) => {
15356
+ if (v === undefined)
15357
+ return;
15358
+ const trimmed = v.trim();
15359
+ return trimmed === "" ? undefined : trimmed;
15360
+ }),
15356
15361
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
15357
15362
  max_iterations: exports_external.number().min(1).max(10).default(5),
15358
15363
  pipeline: PipelineConfigSchema.optional(),
@@ -59986,6 +59991,52 @@ function createAgents(config3) {
59986
59991
  }
59987
59992
  return allAgents;
59988
59993
  }
59994
+ function resolvePrimaryAgentNames(agentNames, defaultAgent) {
59995
+ const collectArchitectRole = () => agentNames.filter((n) => stripKnownSwarmPrefix(n) === "architect");
59996
+ const trimmed = typeof defaultAgent === "string" ? defaultAgent.trim() : undefined;
59997
+ const value = trimmed === "" ? undefined : trimmed;
59998
+ if (agentNames.length === 0) {
59999
+ return { primaryNames: new Set, reason: "implicit-architects" };
60000
+ }
60001
+ if (value === undefined) {
60002
+ const architects2 = collectArchitectRole();
60003
+ if (architects2.length > 0) {
60004
+ return {
60005
+ primaryNames: new Set(architects2),
60006
+ reason: "implicit-architects"
60007
+ };
60008
+ }
60009
+ const first2 = agentNames[0];
60010
+ return {
60011
+ primaryNames: new Set([first2]),
60012
+ reason: "fallback-first",
60013
+ warning: `[swarm] No architect-role agents are registered and default_agent is unset; falling back to '${first2}' as primary. Re-enable an architect agent or set default_agent to silence this warning.`
60014
+ };
60015
+ }
60016
+ if (ALL_AGENT_NAMES.includes(value)) {
60017
+ const matching = agentNames.filter((n) => stripKnownSwarmPrefix(n) === value);
60018
+ if (matching.length > 0) {
60019
+ return { primaryNames: new Set(matching), reason: "base-role" };
60020
+ }
60021
+ }
60022
+ if (agentNames.includes(value)) {
60023
+ return { primaryNames: new Set([value]), reason: "exact" };
60024
+ }
60025
+ const architects = collectArchitectRole();
60026
+ if (architects.length > 0) {
60027
+ return {
60028
+ primaryNames: new Set(architects),
60029
+ reason: "fallback-architects",
60030
+ warning: `[swarm] default_agent '${value}' did not match any registered agent; falling back to architect-role primaries: ${architects.join(", ")}.`
60031
+ };
60032
+ }
60033
+ const first = agentNames[0];
60034
+ return {
60035
+ primaryNames: new Set([first]),
60036
+ reason: "fallback-first",
60037
+ warning: `[swarm] default_agent '${value}' did not match any registered agent and no architect-role agents are registered; falling back to '${first}' as primary.`
60038
+ };
60039
+ }
59989
60040
  function getAgentConfigs(config3, directory, sessionId) {
59990
60041
  const agents = createAgents(config3);
59991
60042
  const toolFilterEnabled = config3?.tool_filter?.enabled ?? true;
@@ -59993,21 +60044,29 @@ function getAgentConfigs(config3, directory, sessionId) {
59993
60044
  const quiet = config3?.quiet ?? true;
59994
60045
  const warnedMissingWhitelist = new Set;
59995
60046
  const agentToolSnapshot = {};
60047
+ const resolution = resolvePrimaryAgentNames(agents.map((a) => a.name), config3?.default_agent);
60048
+ if (resolution.warning) {
60049
+ if (!quiet) {
60050
+ console.warn(resolution.warning);
60051
+ } else {
60052
+ addDeferredWarning(resolution.warning);
60053
+ }
60054
+ }
60055
+ if (agents.length > 0 && resolution.primaryNames.size === 0) {
60056
+ const generated = agents.map((a) => a.name).join(", ");
60057
+ const diagnostic = `[swarm] DIAGNOSTIC: ${agents.length} generated agents but zero primaries. Likely cause: a regression in resolvePrimaryAgentNames. Generated: ${generated}.`;
60058
+ if (!quiet) {
60059
+ console.warn(diagnostic);
60060
+ } else {
60061
+ addDeferredWarning(diagnostic);
60062
+ }
60063
+ }
59996
60064
  const result = Object.fromEntries(agents.map((agent) => {
59997
60065
  const sdkConfig = {
59998
60066
  ...agent.config,
59999
60067
  description: agent.description
60000
60068
  };
60001
- let defaultAgent = config3?.default_agent ?? "architect";
60002
- if (defaultAgent !== "architect" && !ALL_AGENT_NAMES.includes(defaultAgent)) {
60003
- if (!quiet) {
60004
- console.warn(`[swarm] Invalid default_agent '${defaultAgent}' — falling back to 'architect'. Valid values: ${ALL_AGENT_NAMES.join(", ")}`);
60005
- } else {
60006
- addDeferredWarning(`[swarm] Invalid default_agent '${defaultAgent}' — falling back to 'architect'. Valid values: ${ALL_AGENT_NAMES.join(", ")}`);
60007
- }
60008
- defaultAgent = "architect";
60009
- }
60010
- const isPrimaryAgent = agent.name === defaultAgent;
60069
+ const isPrimaryAgent = resolution.primaryNames.has(agent.name);
60011
60070
  if (isPrimaryAgent) {
60012
60071
  sdkConfig.mode = "primary";
60013
60072
  sdkConfig.permission = { task: "allow" };
@@ -89610,19 +89669,141 @@ init_loader();
89610
89669
  init_schema();
89611
89670
  init_qa_gate_profile();
89612
89671
  init_gate_evidence();
89672
+ import * as fs86 from "node:fs";
89673
+ import * as path106 from "node:path";
89674
+
89675
+ // src/hooks/diff-scope.ts
89676
+ init_bun_compat();
89613
89677
  import * as fs85 from "node:fs";
89614
89678
  import * as path105 from "node:path";
89615
89679
 
89616
- // src/hooks/diff-scope.ts
89680
+ // src/utils/gitignore-warning.ts
89617
89681
  init_bun_compat();
89618
89682
  import * as fs84 from "node:fs";
89619
89683
  import * as path104 from "node:path";
89684
+ var _internals = { bunSpawn };
89685
+ var _swarmGitExcludedChecked = false;
89686
+ function fileCoversSwarm(content) {
89687
+ for (const rawLine of content.split(`
89688
+ `)) {
89689
+ const line = rawLine.trim();
89690
+ if (line.startsWith("#") || line.length === 0)
89691
+ continue;
89692
+ if (line === ".swarm" || line === ".swarm/")
89693
+ return true;
89694
+ }
89695
+ return false;
89696
+ }
89697
+ var ENSURE_SWARM_GIT_EXCLUDED_OUTER_TIMEOUT_MS = 3000;
89698
+ var ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS = 1500;
89699
+ var GIT_SPAWN_OPTIONS = {
89700
+ timeout: ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS,
89701
+ stdin: "ignore",
89702
+ stdout: "pipe",
89703
+ stderr: "pipe"
89704
+ };
89705
+ async function ensureSwarmGitExcluded(directory, options = {}) {
89706
+ if (_swarmGitExcludedChecked)
89707
+ return;
89708
+ _swarmGitExcludedChecked = true;
89709
+ const { quiet = false } = options;
89710
+ try {
89711
+ const gitRootProc = _internals.bunSpawn(["git", "-C", directory, "rev-parse", "--show-toplevel"], GIT_SPAWN_OPTIONS);
89712
+ let gitRootExitCode;
89713
+ let gitRootOutput;
89714
+ try {
89715
+ [gitRootExitCode, gitRootOutput] = await Promise.all([
89716
+ gitRootProc.exited,
89717
+ gitRootProc.stdout.text()
89718
+ ]);
89719
+ } finally {
89720
+ try {
89721
+ gitRootProc.kill();
89722
+ } catch {}
89723
+ }
89724
+ if (gitRootExitCode !== 0)
89725
+ return;
89726
+ const gitRoot = gitRootOutput.trim();
89727
+ if (!gitRoot)
89728
+ return;
89729
+ const excludePathProc = _internals.bunSpawn(["git", "-C", directory, "rev-parse", "--git-path", "info/exclude"], GIT_SPAWN_OPTIONS);
89730
+ let excludePathExitCode;
89731
+ let excludePathRaw;
89732
+ try {
89733
+ [excludePathExitCode, excludePathRaw] = await Promise.all([
89734
+ excludePathProc.exited,
89735
+ excludePathProc.stdout.text()
89736
+ ]);
89737
+ } finally {
89738
+ try {
89739
+ excludePathProc.kill();
89740
+ } catch {}
89741
+ }
89742
+ if (excludePathExitCode !== 0)
89743
+ return;
89744
+ const excludeRelPath = excludePathRaw.trim();
89745
+ if (!excludeRelPath)
89746
+ return;
89747
+ const excludePath = path104.isAbsolute(excludeRelPath) ? excludeRelPath : path104.join(directory, excludeRelPath);
89748
+ const checkIgnoreProc = _internals.bunSpawn(["git", "-C", directory, "check-ignore", "-q", ".swarm/.gitkeep"], GIT_SPAWN_OPTIONS);
89749
+ let checkIgnoreExitCode;
89750
+ try {
89751
+ checkIgnoreExitCode = await checkIgnoreProc.exited;
89752
+ } finally {
89753
+ try {
89754
+ checkIgnoreProc.kill();
89755
+ } catch {}
89756
+ }
89757
+ if (checkIgnoreExitCode !== 0) {
89758
+ try {
89759
+ fs84.mkdirSync(path104.dirname(excludePath), { recursive: true });
89760
+ let existing = "";
89761
+ try {
89762
+ existing = fs84.readFileSync(excludePath, "utf8");
89763
+ } catch {}
89764
+ if (!fileCoversSwarm(existing)) {
89765
+ fs84.appendFileSync(excludePath, `
89766
+ # opencode-swarm local runtime state
89767
+ .swarm/
89768
+ `, "utf8");
89769
+ if (!quiet) {
89770
+ console.warn("[opencode-swarm] Added .swarm/ to .git/info/exclude to prevent runtime state from appearing in git status.");
89771
+ }
89772
+ }
89773
+ } catch {}
89774
+ }
89775
+ const trackedProc = _internals.bunSpawn(["git", "-C", directory, "ls-files", "--", ".swarm"], GIT_SPAWN_OPTIONS);
89776
+ let trackedExitCode;
89777
+ let trackedOutput;
89778
+ try {
89779
+ [trackedExitCode, trackedOutput] = await Promise.all([
89780
+ trackedProc.exited,
89781
+ trackedProc.stdout.text()
89782
+ ]);
89783
+ } finally {
89784
+ try {
89785
+ trackedProc.kill();
89786
+ } catch {}
89787
+ }
89788
+ if (trackedExitCode === 0 && trackedOutput.trim().length > 0) {
89789
+ console.warn(`[opencode-swarm] WARNING: .swarm/ files are tracked by Git.
89790
+ ` + `.swarm/ contains local runtime state and may contain sensitive session data.
89791
+ ` + `Ignoring will not affect already-tracked files. To stop tracking them, run:
89792
+ ` + ` git rm -r --cached .swarm
89793
+ ` + ` echo ".swarm/" >> .gitignore
89794
+ ` + ' git commit -m "Stop tracking opencode-swarm runtime state"');
89795
+ }
89796
+ } catch {}
89797
+ }
89798
+
89799
+ // src/hooks/diff-scope.ts
89800
+ var _internals2 = { bunSpawn };
89620
89801
  function getDeclaredScope(taskId, directory) {
89621
89802
  try {
89622
- const planPath = path104.join(directory, ".swarm", "plan.json");
89623
- if (!fs84.existsSync(planPath))
89803
+ const planPath = path105.join(directory, ".swarm", "plan.json");
89804
+ if (!fs85.existsSync(planPath))
89624
89805
  return null;
89625
- const raw = fs84.readFileSync(planPath, "utf-8");
89806
+ const raw = fs85.readFileSync(planPath, "utf-8");
89626
89807
  const plan = JSON.parse(raw);
89627
89808
  for (const phase of plan.phases ?? []) {
89628
89809
  for (const task of phase.tasks ?? []) {
@@ -89643,30 +89824,47 @@ function getDeclaredScope(taskId, directory) {
89643
89824
  return null;
89644
89825
  }
89645
89826
  }
89827
+ var GIT_DIFF_SPAWN_OPTIONS = {
89828
+ timeout: ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS,
89829
+ stdin: "ignore",
89830
+ stdout: "pipe",
89831
+ stderr: "pipe"
89832
+ };
89646
89833
  async function getChangedFiles(directory) {
89647
89834
  try {
89648
- const proc = bunSpawn(["git", "diff", "--name-only", "HEAD~1"], {
89835
+ const proc = _internals2.bunSpawn(["git", "diff", "--name-only", "HEAD~1"], {
89649
89836
  cwd: directory,
89650
- stdout: "pipe",
89651
- stderr: "pipe"
89837
+ ...GIT_DIFF_SPAWN_OPTIONS
89652
89838
  });
89653
- const [exitCode, stdout] = await Promise.all([
89654
- proc.exited,
89655
- proc.stdout.text()
89656
- ]);
89839
+ let exitCode;
89840
+ let stdout;
89841
+ try {
89842
+ [exitCode, stdout] = await Promise.all([proc.exited, proc.stdout.text()]);
89843
+ } finally {
89844
+ try {
89845
+ proc.kill();
89846
+ } catch {}
89847
+ }
89657
89848
  if (exitCode === 0) {
89658
89849
  return stdout.trim().split(`
89659
89850
  `).map((f) => f.trim()).filter((f) => f.length > 0);
89660
89851
  }
89661
- const proc2 = bunSpawn(["git", "diff", "--name-only", "HEAD"], {
89852
+ const proc2 = _internals2.bunSpawn(["git", "diff", "--name-only", "HEAD"], {
89662
89853
  cwd: directory,
89663
- stdout: "pipe",
89664
- stderr: "pipe"
89854
+ ...GIT_DIFF_SPAWN_OPTIONS
89665
89855
  });
89666
- const [exitCode2, stdout2] = await Promise.all([
89667
- proc2.exited,
89668
- proc2.stdout.text()
89669
- ]);
89856
+ let exitCode2;
89857
+ let stdout2;
89858
+ try {
89859
+ [exitCode2, stdout2] = await Promise.all([
89860
+ proc2.exited,
89861
+ proc2.stdout.text()
89862
+ ]);
89863
+ } finally {
89864
+ try {
89865
+ proc2.kill();
89866
+ } catch {}
89867
+ }
89670
89868
  if (exitCode2 === 0) {
89671
89869
  return stdout2.trim().split(`
89672
89870
  `).map((f) => f.trim()).filter((f) => f.length > 0);
@@ -89739,7 +89937,7 @@ var TIER_3_PATTERNS = [
89739
89937
  ];
89740
89938
  function matchesTier3Pattern(files) {
89741
89939
  for (const file3 of files) {
89742
- const fileName = path105.basename(file3);
89940
+ const fileName = path106.basename(file3);
89743
89941
  for (const pattern of TIER_3_PATTERNS) {
89744
89942
  if (pattern.test(fileName)) {
89745
89943
  return true;
@@ -89753,8 +89951,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
89753
89951
  if (hasActiveTurboMode()) {
89754
89952
  const resolvedDir2 = workingDirectory;
89755
89953
  try {
89756
- const planPath = path105.join(resolvedDir2, ".swarm", "plan.json");
89757
- const planRaw = fs85.readFileSync(planPath, "utf-8");
89954
+ const planPath = path106.join(resolvedDir2, ".swarm", "plan.json");
89955
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
89758
89956
  const plan = JSON.parse(planRaw);
89759
89957
  for (const planPhase of plan.phases ?? []) {
89760
89958
  for (const task of planPhase.tasks ?? []) {
@@ -89823,8 +90021,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
89823
90021
  }
89824
90022
  try {
89825
90023
  const resolvedDir2 = workingDirectory;
89826
- const planPath = path105.join(resolvedDir2, ".swarm", "plan.json");
89827
- const planRaw = fs85.readFileSync(planPath, "utf-8");
90024
+ const planPath = path106.join(resolvedDir2, ".swarm", "plan.json");
90025
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
89828
90026
  const plan = JSON.parse(planRaw);
89829
90027
  for (const planPhase of plan.phases ?? []) {
89830
90028
  for (const task of planPhase.tasks ?? []) {
@@ -90013,8 +90211,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90013
90211
  };
90014
90212
  }
90015
90213
  }
90016
- normalizedDir = path105.normalize(args2.working_directory);
90017
- const pathParts = normalizedDir.split(path105.sep);
90214
+ normalizedDir = path106.normalize(args2.working_directory);
90215
+ const pathParts = normalizedDir.split(path106.sep);
90018
90216
  if (pathParts.includes("..")) {
90019
90217
  return {
90020
90218
  success: false,
@@ -90024,11 +90222,11 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90024
90222
  ]
90025
90223
  };
90026
90224
  }
90027
- const resolvedDir = path105.resolve(normalizedDir);
90225
+ const resolvedDir = path106.resolve(normalizedDir);
90028
90226
  try {
90029
- const realPath = fs85.realpathSync(resolvedDir);
90030
- const planPath = path105.join(realPath, ".swarm", "plan.json");
90031
- if (!fs85.existsSync(planPath)) {
90227
+ const realPath = fs86.realpathSync(resolvedDir);
90228
+ const planPath = path106.join(realPath, ".swarm", "plan.json");
90229
+ if (!fs86.existsSync(planPath)) {
90032
90230
  return {
90033
90231
  success: false,
90034
90232
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -90059,22 +90257,22 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90059
90257
  }
90060
90258
  if (args2.status === "in_progress") {
90061
90259
  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");
90260
+ const evidencePath = path106.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
90261
+ fs86.mkdirSync(path106.dirname(evidencePath), { recursive: true });
90262
+ const fd = fs86.openSync(evidencePath, "wx");
90065
90263
  let writeOk = false;
90066
90264
  try {
90067
- fs85.writeSync(fd, JSON.stringify({
90265
+ fs86.writeSync(fd, JSON.stringify({
90068
90266
  taskId: args2.task_id,
90069
90267
  required_gates: ["reviewer", "test_engineer"],
90070
90268
  gates: {}
90071
90269
  }, null, 2));
90072
90270
  writeOk = true;
90073
90271
  } finally {
90074
- fs85.closeSync(fd);
90272
+ fs86.closeSync(fd);
90075
90273
  if (!writeOk) {
90076
90274
  try {
90077
- fs85.unlinkSync(evidencePath);
90275
+ fs86.unlinkSync(evidencePath);
90078
90276
  } catch {}
90079
90277
  }
90080
90278
  }
@@ -90084,8 +90282,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
90084
90282
  recoverTaskStateFromDelegations(args2.task_id);
90085
90283
  let phaseRequiresReviewer = true;
90086
90284
  try {
90087
- const planPath = path105.join(directory, ".swarm", "plan.json");
90088
- const planRaw = fs85.readFileSync(planPath, "utf-8");
90285
+ const planPath = path106.join(directory, ".swarm", "plan.json");
90286
+ const planRaw = fs86.readFileSync(planPath, "utf-8");
90089
90287
  const plan = JSON.parse(planRaw);
90090
90288
  const taskPhase = plan.phases.find((p) => p.tasks.some((t) => t.id === args2.task_id));
90091
90289
  if (taskPhase?.required_agents && !taskPhase.required_agents.includes("reviewer")) {
@@ -90395,8 +90593,8 @@ init_utils2();
90395
90593
  init_ledger();
90396
90594
  init_manager();
90397
90595
  init_create_tool();
90398
- import fs86 from "node:fs";
90399
- import path106 from "node:path";
90596
+ import fs87 from "node:fs";
90597
+ import path107 from "node:path";
90400
90598
  function derivePlanId5(plan) {
90401
90599
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
90402
90600
  }
@@ -90447,7 +90645,7 @@ async function executeWriteDriftEvidence(args2, directory) {
90447
90645
  entries: [evidenceEntry]
90448
90646
  };
90449
90647
  const filename = "drift-verifier.json";
90450
- const relativePath = path106.join("evidence", String(phase), filename);
90648
+ const relativePath = path107.join("evidence", String(phase), filename);
90451
90649
  let validatedPath;
90452
90650
  try {
90453
90651
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90458,12 +90656,12 @@ async function executeWriteDriftEvidence(args2, directory) {
90458
90656
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90459
90657
  }, null, 2);
90460
90658
  }
90461
- const evidenceDir = path106.dirname(validatedPath);
90659
+ const evidenceDir = path107.dirname(validatedPath);
90462
90660
  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);
90661
+ await fs87.promises.mkdir(evidenceDir, { recursive: true });
90662
+ const tempPath = path107.join(evidenceDir, `.${filename}.tmp`);
90663
+ await fs87.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90664
+ await fs87.promises.rename(tempPath, validatedPath);
90467
90665
  let snapshotInfo;
90468
90666
  let snapshotError;
90469
90667
  let qaProfileLocked;
@@ -90557,8 +90755,8 @@ var write_drift_evidence = createSwarmTool({
90557
90755
  init_zod();
90558
90756
  init_utils2();
90559
90757
  init_create_tool();
90560
- import fs87 from "node:fs";
90561
- import path107 from "node:path";
90758
+ import fs88 from "node:fs";
90759
+ import path108 from "node:path";
90562
90760
  function normalizeVerdict2(verdict) {
90563
90761
  switch (verdict) {
90564
90762
  case "APPROVED":
@@ -90606,7 +90804,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
90606
90804
  entries: [evidenceEntry]
90607
90805
  };
90608
90806
  const filename = "hallucination-guard.json";
90609
- const relativePath = path107.join("evidence", String(phase), filename);
90807
+ const relativePath = path108.join("evidence", String(phase), filename);
90610
90808
  let validatedPath;
90611
90809
  try {
90612
90810
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90617,12 +90815,12 @@ async function executeWriteHallucinationEvidence(args2, directory) {
90617
90815
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90618
90816
  }, null, 2);
90619
90817
  }
90620
- const evidenceDir = path107.dirname(validatedPath);
90818
+ const evidenceDir = path108.dirname(validatedPath);
90621
90819
  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);
90820
+ await fs88.promises.mkdir(evidenceDir, { recursive: true });
90821
+ const tempPath = path108.join(evidenceDir, `.${filename}.tmp`);
90822
+ await fs88.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90823
+ await fs88.promises.rename(tempPath, validatedPath);
90626
90824
  return JSON.stringify({
90627
90825
  success: true,
90628
90826
  phase,
@@ -90668,8 +90866,8 @@ var write_hallucination_evidence = createSwarmTool({
90668
90866
  init_zod();
90669
90867
  init_utils2();
90670
90868
  init_create_tool();
90671
- import fs88 from "node:fs";
90672
- import path108 from "node:path";
90869
+ import fs89 from "node:fs";
90870
+ import path109 from "node:path";
90673
90871
  function normalizeVerdict3(verdict) {
90674
90872
  switch (verdict) {
90675
90873
  case "PASS":
@@ -90743,7 +90941,7 @@ async function executeWriteMutationEvidence(args2, directory) {
90743
90941
  entries: [evidenceEntry]
90744
90942
  };
90745
90943
  const filename = "mutation-gate.json";
90746
- const relativePath = path108.join("evidence", String(phase), filename);
90944
+ const relativePath = path109.join("evidence", String(phase), filename);
90747
90945
  let validatedPath;
90748
90946
  try {
90749
90947
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -90754,12 +90952,12 @@ async function executeWriteMutationEvidence(args2, directory) {
90754
90952
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
90755
90953
  }, null, 2);
90756
90954
  }
90757
- const evidenceDir = path108.dirname(validatedPath);
90955
+ const evidenceDir = path109.dirname(validatedPath);
90758
90956
  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);
90957
+ await fs89.promises.mkdir(evidenceDir, { recursive: true });
90958
+ const tempPath = path109.join(evidenceDir, `.${filename}.tmp`);
90959
+ await fs89.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
90960
+ await fs89.promises.rename(tempPath, validatedPath);
90763
90961
  return JSON.stringify({
90764
90962
  success: true,
90765
90963
  phase,
@@ -90812,85 +91010,6 @@ init_write_retro();
90812
91010
  // src/index.ts
90813
91011
  init_utils();
90814
91012
 
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
91013
  // src/utils/tool-output.ts
90895
91014
  function truncateToolOutput(output, maxLines, toolName, tailLines = 10) {
90896
91015
  if (!output) {
@@ -90994,7 +91113,12 @@ async function initializeOpenCodeSwarm(ctx) {
90994
91113
  }
90995
91114
  repoGraphHook.init().catch(() => {}).finally(() => clearTimeout(watchdog));
90996
91115
  });
90997
- await ensureSwarmGitExcluded(ctx.directory, { quiet: config3.quiet });
91116
+ 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) => {
91117
+ const msg = err3 instanceof Error ? err3.message : String(err3);
91118
+ log("ensureSwarmGitExcluded timed out or failed (non-fatal)", {
91119
+ error: msg
91120
+ });
91121
+ });
90998
91122
  initTelemetry(ctx.directory);
90999
91123
  writeSwarmConfigExampleIfNew(ctx.directory);
91000
91124
  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.5",
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",