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 +1 -1
- package/dist/hooks/diff-scope.d.ts +10 -0
- package/dist/index.js +210 -128
- package/dist/utils/gitignore-warning.d.ts +72 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
|
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/
|
|
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 =
|
|
89623
|
-
if (!
|
|
89744
|
+
const planPath = path105.join(directory, ".swarm", "plan.json");
|
|
89745
|
+
if (!fs85.existsSync(planPath))
|
|
89624
89746
|
return null;
|
|
89625
|
-
const raw =
|
|
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
|
-
|
|
89651
|
-
stderr: "pipe"
|
|
89778
|
+
...GIT_DIFF_SPAWN_OPTIONS
|
|
89652
89779
|
});
|
|
89653
|
-
|
|
89654
|
-
|
|
89655
|
-
|
|
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
|
-
|
|
89664
|
-
stderr: "pipe"
|
|
89795
|
+
...GIT_DIFF_SPAWN_OPTIONS
|
|
89665
89796
|
});
|
|
89666
|
-
|
|
89667
|
-
|
|
89668
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
89756
|
-
const planRaw =
|
|
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 =
|
|
89826
|
-
const planRaw =
|
|
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 =
|
|
90016
|
-
const pathParts = normalizedDir.split(
|
|
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 =
|
|
90166
|
+
const resolvedDir = path106.resolve(normalizedDir);
|
|
90027
90167
|
try {
|
|
90028
|
-
const realPath =
|
|
90029
|
-
const planPath =
|
|
90030
|
-
if (!
|
|
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 =
|
|
90062
|
-
|
|
90063
|
-
const fd =
|
|
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
|
-
|
|
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
|
-
|
|
90213
|
+
fs86.closeSync(fd);
|
|
90074
90214
|
if (!writeOk) {
|
|
90075
90215
|
try {
|
|
90076
|
-
|
|
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 =
|
|
90087
|
-
const planRaw =
|
|
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
|
|
90398
|
-
import
|
|
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 =
|
|
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 =
|
|
90600
|
+
const evidenceDir = path107.dirname(validatedPath);
|
|
90461
90601
|
try {
|
|
90462
|
-
await
|
|
90463
|
-
const tempPath =
|
|
90464
|
-
await
|
|
90465
|
-
await
|
|
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
|
|
90560
|
-
import
|
|
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 =
|
|
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 =
|
|
90759
|
+
const evidenceDir = path108.dirname(validatedPath);
|
|
90620
90760
|
try {
|
|
90621
|
-
await
|
|
90622
|
-
const tempPath =
|
|
90623
|
-
await
|
|
90624
|
-
await
|
|
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
|
|
90671
|
-
import
|
|
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 =
|
|
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 =
|
|
90896
|
+
const evidenceDir = path109.dirname(validatedPath);
|
|
90757
90897
|
try {
|
|
90758
|
-
await
|
|
90759
|
-
const tempPath =
|
|
90760
|
-
await
|
|
90761
|
-
await
|
|
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.
|
|
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",
|