opencode-swarm 7.25.1 → 7.26.0

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
@@ -38,6 +38,8 @@ Most AI coding tools let one model write code and ask that same model whether th
38
38
  - 🔁 **Resumable sessions** — all state saved to `.swarm/`; pick up any project any day
39
39
  - 🌐 **20 languages** — TypeScript, Python, Go, Rust, Java, Kotlin, C/C++, C#, Ruby, Swift, Dart, PHP, JavaScript, CSS, Bash, PowerShell, INI, Regex (extending: see [docs/adding-a-language.md](docs/adding-a-language.md))
40
40
  - 🛡️ **Built-in security** — SAST, secrets scanning, dependency audit per task
41
+ - 📝 **Shell write detection** — Static analysis of POSIX/PowerShell/cmd commands to detect file writes (redirects, builtins, in-place editors, network downloads, archive extraction, git destructive ops) before execution
42
+ - 🔒 **Scope enforcement** — Validates write targets against declared scope with cross-process persistence and TTL expiry
41
43
  - 🆓 **Free tier** — works with OpenCode Zen's free model roster
42
44
  - ⚙️ **Fully configurable** — override any agent's model, disable agents, tune guardrails
43
45
 
@@ -45,16 +47,44 @@ Most AI coding tools let one model write code and ask that same model whether th
45
47
 
46
48
  ---
47
49
 
48
- ## What Swarm Catches
50
+ ## Shell Write Detection
49
51
 
50
- Concrete classes of failure that Swarm gates exist to stop every item ties to an agent or pipeline gate that already runs in this repo:
52
+ Swarm includes comprehensive static analysis for shell commands to detect and intercept file write operations before execution.
51
53
 
52
- - **Hallucinated APIs and citations** — `critic_hallucination_verifier` verifies referenced APIs and citations against real sources before they reach the codebase.
53
- - **Missing tests and regressions** — `test_engineer` writes and runs tests on every task; the architect runs a regression sweep across the graph after each task (pipeline step `5l`).
54
- - **Unsafe secret and logging patterns** — `secretscan` and `sast_scan` (63+ rules across 9 languages, offline) run as part of the per-task `pre_check_batch`.
55
- - **Plan and spec drift** `critic_drift_verifier` is a blocking phase-completion gate; `curator_phase` also flags workflow drift across phases.
56
- - **Placeholders and TODO stubs** — `placeholder_scan` runs in the per-task pipeline (step `5d`) and rejects code that ships incomplete stubs.
57
- - **Untrusted plans** `critic` reviews the plan before any code is written; `completion-verify` is a deterministic phase-close gate that checks plan task identifiers actually exist in source files.
54
+ ### Shell Write Detection Features
55
+
56
+ - **POSIX shell detection** — Parses commands with `bash-parser` AST for accurate detection of:
57
+ - Redirect operators (`>`, `>>`, `>|`, `<<`, `<<-`)
58
+ - Here-documents and here-strings
59
+ - Write-effect builtins (`cp`, `mv`, `install`, `ln`, `truncate`, `dd`)
60
+ - In-place editors (`sed -i`, `perl -i`, `awk -i`)
61
+ - Interpreter eval (`python -c`, `node -e`, `bun -e`, `ruby -e`, `php -r`)
62
+ - Network downloaders (`curl -o`, `wget -O`, `scp`)
63
+ - Archive extraction (`tar -x`, `unzip`, `gunzip`)
64
+ - Git destructive operations (`git clean -fd`, `git reset --hard`)
65
+
66
+ - **Windows shell detection** — Uses regex heuristics for PowerShell and cmd.exe:
67
+ - PowerShell cmdlets: `Out-File`, `Set-Content`, `Add-Content`, `Copy-Item`, `Move-Item`
68
+ - cmd.exe builtins: `copy`, `move`, `ren`, `del`, `rd`, `md`
69
+ - Redirect operators (`>`, `>>`)
70
+
71
+ - **Interactive session denial** — Blocks commands that create persistent or open-ended sessions:
72
+ - POSIX: `watch`, `screen`, `tmux new-session`
73
+ - PowerShell: `Start-Process`
74
+
75
+ - **Cross-process scope enforcement** — Declared scope is persisted to `.swarm/scopes/scope-{taskId}.json` with:
76
+ - TTL expiry (default 24 hours)
77
+ - Symlink guards (O_NOFOLLOW + realpath containment)
78
+ - Schema versioning and fail-closed validation
79
+
80
+ ### Security Patterns
81
+
82
+ The guardrails system blocks destructive shell commands targeting:
83
+ - System paths (`/root`, `/etc`, `C:\Windows`, etc.)
84
+ - Symlink/junction creation with external targets
85
+ - File operations under `.swarm/` directory
86
+ - Fork bombs and infinite loops
87
+ - Disk wiping and ransomware-grade operations
58
88
 
59
89
  ---
60
90
 
@@ -349,6 +379,9 @@ graph TB
349
379
  | Plan reviewed before coding | ✅ | ❌ | ❌ |
350
380
  | Every task reviewed + tested | ✅ | ❌ | ❌ |
351
381
  | Different model for review vs. code | ✅ | ❌ | ❌ |
382
+ | Shell write detection (POSIX/PowerShell/cmd) | ✅ | ❌ | ❌ |
383
+ | Scope enforcement with cross-process persistence | ✅ | ❌ | ❌ |
384
+ | Interactive session detection and blocking | ✅ | ❌ | ❌ |
352
385
  | Resumable sessions | ✅ | ❌ | ❌ |
353
386
  | Built-in security scanning | ✅ | ❌ | ❌ |
354
387
  | Learns from mistakes | ✅ | ❌ | ❌ |
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.25.1",
37
+ version: "7.26.0",
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",
@@ -72,7 +72,7 @@ var init_package = __esm(() => {
72
72
  ],
73
73
  scripts: {
74
74
  clean: `bun -e "require('fs').rmSync('dist',{recursive:true,force:true})"`,
75
- build: "bun run clean && bun run scripts/copy-grammars.ts && bun build src/index.ts --outdir dist --target node --format esm && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm && bun run scripts/copy-grammars.ts --to-dist && tsc --emitDeclarationOnly",
75
+ build: "bun run clean && bun run scripts/copy-grammars.ts && bun build src/index.ts --outdir dist --target node --format esm --external bash-parser && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external bash-parser && bun run scripts/copy-grammars.ts --to-dist && tsc --emitDeclarationOnly",
76
76
  typecheck: "tsc --noEmit",
77
77
  test: "bun test",
78
78
  lint: "biome lint .",
@@ -86,6 +86,7 @@ var init_package = __esm(() => {
86
86
  "@opencode-ai/plugin": "^1.1.53",
87
87
  "@opencode-ai/sdk": "^1.1.53",
88
88
  "@vscode/tree-sitter-wasm": "^0.3.0",
89
+ "bash-parser": "^0.5.0",
89
90
  "p-limit": "^7.3.0",
90
91
  picomatch: "^4.0.4",
91
92
  "proper-lockfile": "^4.1.2",
@@ -20894,6 +20895,73 @@ var init_scope_persistence = __esm(() => {
20894
20895
  ]);
20895
20896
  });
20896
20897
 
20898
+ // src/hooks/shell-write-detect.ts
20899
+ import parse5 from "bash-parser";
20900
+ var REDIRECT_WRITE_TOKENS, REDIRECT_HERE_TOKENS, REDIRECT_ALL_WRITE_TOKENS, BUILTIN_WRITE_COMMANDS, INPLACE_EDIT_COMMANDS, INTERPRETER_EVAL_COMMANDS, NETWORK_DOWNLOAD_COMMANDS, ARCHIVE_EXTRACT_COMMANDS, PS_WRITE_CMDLETS, PS_WRITE_ALIASES, CMD_WRITE_BUILTINS;
20901
+ var init_shell_write_detect = __esm(() => {
20902
+ REDIRECT_WRITE_TOKENS = new Set([
20903
+ "GREAT",
20904
+ "DGREAT",
20905
+ "CLOBBER",
20906
+ "LESSGREAT"
20907
+ ]);
20908
+ REDIRECT_HERE_TOKENS = new Set(["DLESS", "DLESSDASH"]);
20909
+ REDIRECT_ALL_WRITE_TOKENS = new Set([
20910
+ ...REDIRECT_WRITE_TOKENS,
20911
+ ...REDIRECT_HERE_TOKENS
20912
+ ]);
20913
+ BUILTIN_WRITE_COMMANDS = new Set([
20914
+ "cp",
20915
+ "mv",
20916
+ "install",
20917
+ "ln",
20918
+ "truncate"
20919
+ ]);
20920
+ INPLACE_EDIT_COMMANDS = new Set(["sed", "perl", "awk"]);
20921
+ INTERPRETER_EVAL_COMMANDS = new Set([
20922
+ "python",
20923
+ "python3",
20924
+ "python2",
20925
+ "node",
20926
+ "bun",
20927
+ "ruby",
20928
+ "perl",
20929
+ "php"
20930
+ ]);
20931
+ NETWORK_DOWNLOAD_COMMANDS = new Set(["curl", "wget", "scp"]);
20932
+ ARCHIVE_EXTRACT_COMMANDS = new Set([
20933
+ "tar",
20934
+ "unzip",
20935
+ "gunzip",
20936
+ "gzip",
20937
+ "bzip2",
20938
+ "xz",
20939
+ "7z",
20940
+ "rar"
20941
+ ]);
20942
+ PS_WRITE_CMDLETS = new Set([
20943
+ "Out-File",
20944
+ "Set-Content",
20945
+ "Add-Content",
20946
+ "Clear-Content",
20947
+ "Copy-Item",
20948
+ "Move-Item",
20949
+ "Remove-Item",
20950
+ "Invoke-WebRequest",
20951
+ "Start-Process"
20952
+ ]);
20953
+ PS_WRITE_ALIASES = new Set(["echo", "write"]);
20954
+ CMD_WRITE_BUILTINS = new Set([
20955
+ "copy",
20956
+ "move",
20957
+ "type",
20958
+ "del",
20959
+ "rd",
20960
+ "md",
20961
+ "ren"
20962
+ ]);
20963
+ });
20964
+
20897
20965
  // src/hooks/conflict-resolution.ts
20898
20966
  var init_conflict_resolution = __esm(() => {
20899
20967
  init_state();
@@ -20968,6 +21036,7 @@ var init_guardrails = __esm(() => {
20968
21036
  init_state();
20969
21037
  init_telemetry();
20970
21038
  init_utils();
21039
+ init_shell_write_detect();
20971
21040
  init_bun_compat();
20972
21041
  init_logger();
20973
21042
  init_conflict_resolution();
@@ -22370,7 +22439,7 @@ var _parse2 = (_Err) => (schema, value, _ctx, _params) => {
22370
22439
  throw e;
22371
22440
  }
22372
22441
  return result.value;
22373
- }, parse5, _parseAsync2 = (_Err) => async (schema, value, _ctx, params) => {
22442
+ }, parse6, _parseAsync2 = (_Err) => async (schema, value, _ctx, params) => {
22374
22443
  const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };
22375
22444
  let result = schema._zod.run({ value, issues: [] }, ctx);
22376
22445
  if (result instanceof Promise)
@@ -22425,7 +22494,7 @@ var init_parse3 = __esm(() => {
22425
22494
  init_core3();
22426
22495
  init_errors4();
22427
22496
  init_util2();
22428
- parse5 = /* @__PURE__ */ _parse2($ZodRealError2);
22497
+ parse6 = /* @__PURE__ */ _parse2($ZodRealError2);
22429
22498
  parseAsync3 = /* @__PURE__ */ _parseAsync2($ZodRealError2);
22430
22499
  safeParse3 = /* @__PURE__ */ _safeParse2($ZodRealError2);
22431
22500
  safeParseAsync3 = /* @__PURE__ */ _safeParseAsync2($ZodRealError2);
@@ -24915,10 +24984,10 @@ var init_schemas3 = __esm(() => {
24915
24984
  throw new Error("implement() must be called with a function");
24916
24985
  }
24917
24986
  return function(...args) {
24918
- const parsedArgs = inst._def.input ? parse5(inst._def.input, args) : args;
24987
+ const parsedArgs = inst._def.input ? parse6(inst._def.input, args) : args;
24919
24988
  const result = Reflect.apply(func, this, parsedArgs);
24920
24989
  if (inst._def.output) {
24921
- return parse5(inst._def.output, result);
24990
+ return parse6(inst._def.output, result);
24922
24991
  }
24923
24992
  return result;
24924
24993
  };
@@ -32494,7 +32563,7 @@ __export(exports_core4, {
32494
32563
  regexes: () => exports_regexes2,
32495
32564
  prettifyError: () => prettifyError2,
32496
32565
  parseAsync: () => parseAsync3,
32497
- parse: () => parse5,
32566
+ parse: () => parse6,
32498
32567
  locales: () => exports_locales2,
32499
32568
  isValidJWT: () => isValidJWT2,
32500
32569
  isValidBase64URL: () => isValidBase64URL2,
@@ -32847,11 +32916,11 @@ var init_errors5 = __esm(() => {
32847
32916
  });
32848
32917
 
32849
32918
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/parse.js
32850
- var parse7, parseAsync4, safeParse4, safeParseAsync4, encode4, decode4, encodeAsync4, decodeAsync4, safeEncode4, safeDecode4, safeEncodeAsync4, safeDecodeAsync4;
32919
+ var parse8, parseAsync4, safeParse4, safeParseAsync4, encode4, decode4, encodeAsync4, decodeAsync4, safeEncode4, safeDecode4, safeEncodeAsync4, safeDecodeAsync4;
32851
32920
  var init_parse4 = __esm(() => {
32852
32921
  init_core4();
32853
32922
  init_errors5();
32854
- parse7 = /* @__PURE__ */ _parse2(ZodRealError2);
32923
+ parse8 = /* @__PURE__ */ _parse2(ZodRealError2);
32855
32924
  parseAsync4 = /* @__PURE__ */ _parseAsync2(ZodRealError2);
32856
32925
  safeParse4 = /* @__PURE__ */ _safeParse2(ZodRealError2);
32857
32926
  safeParseAsync4 = /* @__PURE__ */ _safeParseAsync2(ZodRealError2);
@@ -33323,7 +33392,7 @@ var init_schemas4 = __esm(() => {
33323
33392
  reg.add(inst, meta3);
33324
33393
  return inst;
33325
33394
  };
33326
- inst.parse = (data, params) => parse7(inst, data, params, { callee: inst.parse });
33395
+ inst.parse = (data, params) => parse8(inst, data, params, { callee: inst.parse });
33327
33396
  inst.safeParse = (data, params) => safeParse4(inst, data, params);
33328
33397
  inst.parseAsync = async (data, params) => parseAsync4(inst, data, params, { callee: inst.parseAsync });
33329
33398
  inst.safeParseAsync = async (data, params) => safeParseAsync4(inst, data, params);
@@ -33963,7 +34032,7 @@ __export(exports_external2, {
33963
34032
  pipe: () => pipe2,
33964
34033
  partialRecord: () => partialRecord2,
33965
34034
  parseAsync: () => parseAsync4,
33966
- parse: () => parse7,
34035
+ parse: () => parse8,
33967
34036
  overwrite: () => _overwrite2,
33968
34037
  optional: () => optional2,
33969
34038
  object: () => object2,
@@ -45999,14 +46068,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
45999
46068
  return args;
46000
46069
  }
46001
46070
  case "vitest": {
46002
- const args = [
46003
- "npx",
46004
- "vitest",
46005
- "run",
46006
- "--reporter=json",
46007
- "--outputFile",
46008
- ".swarm/cache/test-runner-vitest.json"
46009
- ];
46071
+ const args = ["npx", "vitest", "run"];
46010
46072
  if (coverage)
46011
46073
  args.push("--coverage");
46012
46074
  if (scope !== "all" && files.length > 0)
@@ -46014,7 +46076,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
46014
46076
  return args;
46015
46077
  }
46016
46078
  case "jest": {
46017
- const args = ["npx", "jest", "--json"];
46079
+ const args = ["npx", "jest"];
46018
46080
  if (coverage)
46019
46081
  args.push("--coverage");
46020
46082
  if (scope !== "all" && files.length > 0)
@@ -47249,42 +47311,6 @@ function sanitizeChangedFiles(changedFiles) {
47249
47311
  const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !DANGEROUS_PROPERTY_NAMES.has(f));
47250
47312
  return validFiles.slice(0, MAX_CHANGED_FILES);
47251
47313
  }
47252
- function isTestRunResult(value) {
47253
- return value === "pass" || value === "fail" || value === "skip";
47254
- }
47255
- function parseStoredRecord(value) {
47256
- if (typeof value !== "object" || value === null)
47257
- return null;
47258
- const record3 = value;
47259
- if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
47260
- return null;
47261
- }
47262
- if (typeof record3.testName !== "string" || record3.testName.length === 0) {
47263
- return null;
47264
- }
47265
- if (typeof record3.taskId !== "string" || record3.taskId.length === 0) {
47266
- return null;
47267
- }
47268
- if (!isTestRunResult(record3.result))
47269
- return null;
47270
- if (typeof record3.durationMs !== "number" || !Number.isFinite(record3.durationMs)) {
47271
- return null;
47272
- }
47273
- if (typeof record3.timestamp !== "string" || Number.isNaN(Date.parse(record3.timestamp))) {
47274
- return null;
47275
- }
47276
- return {
47277
- timestamp: record3.timestamp,
47278
- taskId: record3.taskId,
47279
- testFile: record3.testFile,
47280
- testName: record3.testName,
47281
- result: record3.result,
47282
- durationMs: Math.max(0, record3.durationMs),
47283
- errorMessage: typeof record3.errorMessage === "string" ? sanitizeErrorMessage(record3.errorMessage) : undefined,
47284
- stackPrefix: typeof record3.stackPrefix === "string" ? sanitizeStackPrefix(record3.stackPrefix) : undefined,
47285
- changedFiles: sanitizeChangedFiles(Array.isArray(record3.changedFiles) ? record3.changedFiles : [])
47286
- };
47287
- }
47288
47314
  function appendTestRun(record3, workingDir) {
47289
47315
  if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
47290
47316
  throw new TypeError("testFile must be a non-empty string");
@@ -47322,16 +47348,16 @@ function appendTestRun(record3, workingDir) {
47322
47348
  }
47323
47349
  const existingRecords = readAllRecords(historyPath);
47324
47350
  existingRecords.push(sanitizedRecord);
47325
- const recordsByTest = new Map;
47351
+ const recordsByFile = new Map;
47326
47352
  for (const rec of existingRecords) {
47327
- const normalizedKey = `${rec.testFile.toLowerCase()}|${rec.testName.toLowerCase()}`;
47328
- if (!recordsByTest.has(normalizedKey)) {
47329
- recordsByTest.set(normalizedKey, []);
47353
+ const normalizedFile = rec.testFile.toLowerCase();
47354
+ if (!recordsByFile.has(normalizedFile)) {
47355
+ recordsByFile.set(normalizedFile, []);
47330
47356
  }
47331
- recordsByTest.get(normalizedKey).push(rec);
47357
+ recordsByFile.get(normalizedFile).push(rec);
47332
47358
  }
47333
47359
  const prunedRecords = [];
47334
- for (const [, records] of recordsByTest) {
47360
+ for (const [, records] of recordsByFile) {
47335
47361
  records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
47336
47362
  const toKeep = records.slice(-MAX_HISTORY_PER_TEST);
47337
47363
  prunedRecords.push(...toKeep);
@@ -47371,9 +47397,8 @@ function readAllRecords(historyPath) {
47371
47397
  }
47372
47398
  try {
47373
47399
  const parsed = JSON.parse(trimmed);
47374
- const record3 = parseStoredRecord(parsed);
47375
- if (record3) {
47376
- records.push(record3);
47400
+ if (typeof parsed === "object" && parsed !== null && "testFile" in parsed && "testName" in parsed && "result" in parsed) {
47401
+ records.push(parsed);
47377
47402
  }
47378
47403
  } catch {}
47379
47404
  }
@@ -48412,14 +48437,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48412
48437
  return args;
48413
48438
  }
48414
48439
  case "vitest": {
48415
- const args = [
48416
- "npx",
48417
- "vitest",
48418
- "run",
48419
- "--reporter=json",
48420
- "--outputFile",
48421
- VITEST_JSON_OUTPUT_RELATIVE_PATH
48422
- ];
48440
+ const args = ["npx", "vitest", "run"];
48423
48441
  if (coverage)
48424
48442
  args.push("--coverage");
48425
48443
  if (scope !== "all" && files.length > 0) {
@@ -48428,7 +48446,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48428
48446
  return args;
48429
48447
  }
48430
48448
  case "jest": {
48431
- const args = ["npx", "jest", "--json"];
48449
+ const args = ["npx", "jest"];
48432
48450
  if (coverage)
48433
48451
  args.push("--coverage");
48434
48452
  if (scope !== "all" && files.length > 0) {
@@ -48524,122 +48542,6 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48524
48542
  return null;
48525
48543
  }
48526
48544
  }
48527
- function mapFrameworkStatusToResult(status) {
48528
- if (typeof status !== "string")
48529
- return null;
48530
- const normalized = status.toLowerCase();
48531
- if (normalized === "pass" || normalized === "passed")
48532
- return "pass";
48533
- if (normalized === "fail" || normalized === "failed")
48534
- return "fail";
48535
- if (normalized === "skip" || normalized === "skipped" || normalized === "pending" || normalized === "todo") {
48536
- return "skip";
48537
- }
48538
- return null;
48539
- }
48540
- function firstLine(value) {
48541
- if (typeof value !== "string")
48542
- return;
48543
- const line = value.split(`
48544
- `).find((part) => part.trim().length > 0)?.trim();
48545
- return line && line.length > 0 ? line : undefined;
48546
- }
48547
- function parseJestLikeJsonTestResults(payload) {
48548
- if (typeof payload !== "object" || payload === null)
48549
- return [];
48550
- const rawSuites = payload.testResults;
48551
- if (!Array.isArray(rawSuites))
48552
- return [];
48553
- const parsed = [];
48554
- for (const suite of rawSuites) {
48555
- if (typeof suite !== "object" || suite === null)
48556
- continue;
48557
- const suiteObj = suite;
48558
- const rawFile = typeof suiteObj.name === "string" ? suiteObj.name : typeof suiteObj.testFilePath === "string" ? suiteObj.testFilePath : undefined;
48559
- if (!rawFile)
48560
- continue;
48561
- const testFile = rawFile.replace(/\\/g, "/");
48562
- const assertionResults = suiteObj.assertionResults;
48563
- if (!Array.isArray(assertionResults))
48564
- continue;
48565
- for (const assertion of assertionResults) {
48566
- if (typeof assertion !== "object" || assertion === null)
48567
- continue;
48568
- const assertionObj = assertion;
48569
- const result = mapFrameworkStatusToResult(assertionObj.status);
48570
- const testName = typeof assertionObj.fullName === "string" ? assertionObj.fullName : typeof assertionObj.title === "string" ? assertionObj.title : undefined;
48571
- if (!result || !testName || testName.length === 0)
48572
- continue;
48573
- const failureMessages = Array.isArray(assertionObj.failureMessages) ? assertionObj.failureMessages : [];
48574
- const firstFailure = failureMessages.find((entry) => typeof entry === "string" && entry.length > 0);
48575
- const durationMs = typeof assertionObj.duration === "number" && Number.isFinite(assertionObj.duration) ? Math.max(assertionObj.duration, 0) : 0;
48576
- parsed.push({
48577
- testFile,
48578
- testName,
48579
- result,
48580
- durationMs,
48581
- errorMessage: firstLine(firstFailure),
48582
- stackPrefix: firstLine(firstFailure)
48583
- });
48584
- }
48585
- }
48586
- return parsed;
48587
- }
48588
- function parseBunJsonLines(output) {
48589
- const parsed = [];
48590
- for (const line of output.split(`
48591
- `)) {
48592
- const trimmed = line.trim();
48593
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
48594
- continue;
48595
- try {
48596
- const obj = JSON.parse(trimmed);
48597
- const rawFile = typeof obj.file === "string" ? obj.file : typeof obj.testFile === "string" ? obj.testFile : typeof obj.path === "string" ? obj.path : undefined;
48598
- const rawName = typeof obj.testName === "string" ? obj.testName : typeof obj.fullName === "string" ? obj.fullName : typeof obj.name === "string" ? obj.name : undefined;
48599
- const result = mapFrameworkStatusToResult(typeof obj.status === "string" ? obj.status : obj.result);
48600
- if (!rawFile || !rawName || !result)
48601
- continue;
48602
- const errorObj = typeof obj.error === "object" && obj.error !== null ? obj.error : undefined;
48603
- const durationMs = typeof obj.durationMs === "number" && Number.isFinite(obj.durationMs) ? Math.max(obj.durationMs, 0) : typeof obj.duration === "number" && Number.isFinite(obj.duration) ? Math.max(obj.duration, 0) : 0;
48604
- parsed.push({
48605
- testFile: rawFile.replace(/\\/g, "/"),
48606
- testName: rawName,
48607
- result,
48608
- durationMs,
48609
- errorMessage: firstLine(errorObj?.message ?? obj.errorMessage),
48610
- stackPrefix: firstLine(errorObj?.stack)
48611
- });
48612
- } catch {}
48613
- }
48614
- return parsed;
48615
- }
48616
- function parseFrameworkJsonTestResults(framework, output) {
48617
- const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
48618
- if (jsonMatch) {
48619
- try {
48620
- const parsed = JSON.parse(jsonMatch[0]);
48621
- const testResults = parseJestLikeJsonTestResults(parsed);
48622
- if (testResults.length > 0)
48623
- return testResults;
48624
- } catch {}
48625
- }
48626
- for (const line of output.split(`
48627
- `)) {
48628
- const trimmed = line.trim();
48629
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
48630
- continue;
48631
- try {
48632
- const parsed = JSON.parse(trimmed);
48633
- const testResults = parseJestLikeJsonTestResults(parsed);
48634
- if (testResults.length > 0)
48635
- return testResults;
48636
- } catch {}
48637
- }
48638
- if (framework === "bun") {
48639
- return parseBunJsonLines(output);
48640
- }
48641
- return [];
48642
- }
48643
48545
  function parseTestOutput2(framework, output) {
48644
48546
  const totals = {
48645
48547
  passed: 0,
@@ -48927,16 +48829,7 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48927
48829
  };
48928
48830
  }
48929
48831
  const startTime = Date.now();
48930
- const vitestJsonOutputPath = framework === "vitest" ? path39.join(cwd, ".swarm", "cache", "test-runner-vitest.json") : undefined;
48931
48832
  try {
48932
- if (vitestJsonOutputPath) {
48933
- try {
48934
- fs22.mkdirSync(path39.dirname(vitestJsonOutputPath), { recursive: true });
48935
- if (fs22.existsSync(vitestJsonOutputPath)) {
48936
- fs22.unlinkSync(vitestJsonOutputPath);
48937
- }
48938
- } catch {}
48939
- }
48940
48833
  const proc = bunSpawn(command, {
48941
48834
  stdout: "pipe",
48942
48835
  stderr: "pipe",
@@ -48957,37 +48850,13 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48957
48850
  output += (output ? `
48958
48851
  ` : "") + stderrResult.text;
48959
48852
  }
48960
- if (vitestJsonOutputPath) {
48961
- try {
48962
- if (fs22.existsSync(vitestJsonOutputPath)) {
48963
- const vitestJsonOutput = fs22.readFileSync(vitestJsonOutputPath, "utf-8");
48964
- if (vitestJsonOutput.trim().length > 0) {
48965
- output += (output ? `
48966
- ` : "") + vitestJsonOutput;
48967
- }
48968
- }
48969
- } catch {}
48970
- }
48971
48853
  if (stdoutResult.truncated || stderrResult.truncated) {
48972
48854
  output += `
48973
48855
  ... (output truncated at stream read limit)`;
48974
48856
  }
48975
48857
  const useDispatchParse = process.env.SWARM_LANG_BACKEND !== "legacy";
48976
48858
  const parsed = useDispatchParse ? await parseTestOutputViaDispatch(framework, output, cwd) ?? parseTestOutput2(framework, output) : parseTestOutput2(framework, output);
48977
- const parsedTestCases = parseFrameworkJsonTestResults(framework, output);
48978
- const totals = { ...parsed.totals };
48979
- const { coveragePercent } = parsed;
48980
- if (totals.total === 0 && parsedTestCases.length > 0) {
48981
- for (const entry of parsedTestCases) {
48982
- if (entry.result === "pass")
48983
- totals.passed++;
48984
- else if (entry.result === "fail")
48985
- totals.failed++;
48986
- else
48987
- totals.skipped++;
48988
- }
48989
- totals.total = parsedTestCases.length;
48990
- }
48859
+ const { totals, coveragePercent } = parsed;
48991
48860
  const isTimeout = exitCode === -1;
48992
48861
  const testPassed = exitCode === 0 && totals.failed === 0;
48993
48862
  if (testPassed) {
@@ -49000,8 +48869,7 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
49000
48869
  duration_ms,
49001
48870
  totals,
49002
48871
  rawOutput: output,
49003
- outcome: "pass",
49004
- testCases: parsedTestCases
48872
+ outcome: "pass"
49005
48873
  };
49006
48874
  if (coveragePercent !== undefined) {
49007
48875
  result.coveragePercent = coveragePercent;
@@ -49023,8 +48891,7 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
49023
48891
  rawOutput: output,
49024
48892
  error: isTimeout ? `Tests timed out after ${timeout_ms}ms` : `Tests failed with ${totals.failed} failures`,
49025
48893
  message: isTimeout ? `${framework} tests timed out after ${timeout_ms}ms` : `${framework} tests failed (${totals.failed}/${totals.total} failed)`,
49026
- outcome: isTimeout ? "error" : "regression",
49027
- testCases: parsedTestCases
48894
+ outcome: isTimeout ? "error" : "regression"
49028
48895
  };
49029
48896
  if (coveragePercent !== undefined) {
49030
48897
  result.coveragePercent = coveragePercent;
@@ -49045,78 +48912,25 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
49045
48912
  };
49046
48913
  }
49047
48914
  }
49048
- function normalizeHistoryTestFile(testFile, workingDir) {
49049
- const normalized = testFile.replace(/\\/g, "/");
49050
- if (!path39.isAbsolute(testFile))
49051
- return normalized;
49052
- const relative8 = path39.relative(workingDir, testFile);
49053
- if (relative8.startsWith("..") || path39.isAbsolute(relative8)) {
49054
- return normalized;
49055
- }
49056
- return relative8.replace(/\\/g, "/");
49057
- }
49058
- function combineAggregateResult(current, next) {
49059
- if (current === "fail" || next === "fail")
49060
- return "fail";
49061
- if (current === "pass" || next === "pass")
49062
- return "pass";
49063
- return "skip";
49064
- }
49065
- function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles, parsedTestCases) {
48915
+ function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles) {
49066
48916
  if (!result.totals || result.totals.total === 0)
49067
48917
  return;
49068
48918
  const now = new Date().toISOString();
49069
48919
  const changedFiles = (sourceFiles && sourceFiles.length > 0 ? sourceFiles : testFiles).map((f) => f.replace(/\\/g, "/"));
49070
- const aggregateResultsByFile = new Map;
49071
- const validParsedCases = parsedTestCases?.filter((parsedCase) => parsedCase.testFile.length > 0 && parsedCase.testName.length > 0) ?? [];
49072
- for (const parsedCase of validParsedCases) {
49073
- const normalizedTestFile = normalizeHistoryTestFile(parsedCase.testFile, workingDir);
49074
- try {
49075
- appendTestRun({
49076
- timestamp: now,
49077
- taskId: "auto",
49078
- testFile: normalizedTestFile,
49079
- testName: parsedCase.testName,
49080
- result: parsedCase.result,
49081
- durationMs: parsedCase.durationMs,
49082
- errorMessage: parsedCase.errorMessage,
49083
- stackPrefix: parsedCase.stackPrefix,
49084
- changedFiles
49085
- }, workingDir);
49086
- } catch {}
49087
- aggregateResultsByFile.set(normalizedTestFile, combineAggregateResult(aggregateResultsByFile.get(normalizedTestFile), parsedCase.result));
49088
- }
49089
- if (aggregateResultsByFile.size === 0) {
49090
- const aggregateResult = result.success ? "pass" : "fail";
49091
- for (const testFile of testFiles) {
49092
- aggregateResultsByFile.set(testFile.replace(/\\/g, "/"), aggregateResult);
49093
- }
49094
- }
49095
- for (const [testFile, aggregateResult] of aggregateResultsByFile) {
48920
+ for (const testFile of testFiles) {
49096
48921
  try {
49097
48922
  appendTestRun({
49098
48923
  timestamp: now,
49099
48924
  taskId: "auto",
49100
- testFile,
49101
- testName: AGGREGATE_TEST_NAME,
49102
- result: aggregateResult,
48925
+ testFile: testFile.replace(/\\/g, "/"),
48926
+ testName: "(aggregate)",
48927
+ result: result.success ? "pass" : "fail",
49103
48928
  durationMs: result.duration_ms || 0,
49104
48929
  changedFiles
49105
48930
  }, workingDir);
49106
48931
  } catch {}
49107
48932
  }
49108
48933
  }
49109
- function selectHistoryForAnalysis(history) {
49110
- const filesWithIndividualRecords = new Set;
49111
- for (const record3 of history) {
49112
- if (record3.testName !== AGGREGATE_TEST_NAME) {
49113
- filesWithIndividualRecords.add(record3.testFile.toLowerCase());
49114
- }
49115
- }
49116
- if (filesWithIndividualRecords.size === 0)
49117
- return history;
49118
- return history.filter((record3) => record3.testName !== AGGREGATE_TEST_NAME || !filesWithIndividualRecords.has(record3.testFile.toLowerCase()));
49119
- }
49120
48934
  function analyzeFailures(workingDir) {
49121
48935
  const report = {
49122
48936
  flakyTests: [],
@@ -49124,7 +48938,7 @@ function analyzeFailures(workingDir) {
49124
48938
  quarantinedFailures: []
49125
48939
  };
49126
48940
  try {
49127
- const history = selectHistoryForAnalysis(getAllHistory(workingDir));
48941
+ const history = getAllHistory(workingDir);
49128
48942
  if (history.length === 0)
49129
48943
  return report;
49130
48944
  report.flakyTests = detectFlakyTests(history);
@@ -49145,7 +48959,7 @@ function analyzeFailures(workingDir) {
49145
48959
  } catch {}
49146
48960
  return report;
49147
48961
  }
49148
- var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, AGGREGATE_TEST_NAME = "(aggregate)", VITEST_JSON_OUTPUT_RELATIVE_PATH = ".swarm/cache/test-runner-vitest.json", POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
48962
+ var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
49149
48963
  var init_test_runner = __esm(() => {
49150
48964
  init_zod();
49151
48965
  init_discovery();
@@ -49591,7 +49405,7 @@ var init_test_runner = __esm(() => {
49591
49405
  return JSON.stringify(errorResult, null, 2);
49592
49406
  }
49593
49407
  const result = await runTests(framework, effectiveScope, testFiles, coverage, timeout_ms, workingDir);
49594
- recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined, result.testCases);
49408
+ recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined);
49595
49409
  let historyReport;
49596
49410
  if (!result.success && result.totals && result.totals.failed > 0) {
49597
49411
  historyReport = analyzeFailures(workingDir);