opencode-swarm 6.9.0 → 6.11.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/dist/index.js CHANGED
@@ -29198,6 +29198,7 @@ var init_dist = __esm(() => {
29198
29198
  });
29199
29199
 
29200
29200
  // src/tools/lint.ts
29201
+ import * as path10 from "path";
29201
29202
  function validateArgs(args2) {
29202
29203
  if (typeof args2 !== "object" || args2 === null)
29203
29204
  return false;
@@ -29208,17 +29209,20 @@ function validateArgs(args2) {
29208
29209
  }
29209
29210
  function getLinterCommand(linter, mode) {
29210
29211
  const isWindows = process.platform === "win32";
29212
+ const binDir = path10.join(process.cwd(), "node_modules", ".bin");
29213
+ const biomeBin = isWindows ? path10.join(binDir, "biome.EXE") : path10.join(binDir, "biome");
29214
+ const eslintBin = isWindows ? path10.join(binDir, "eslint.cmd") : path10.join(binDir, "eslint");
29211
29215
  switch (linter) {
29212
29216
  case "biome":
29213
29217
  if (mode === "fix") {
29214
- return isWindows ? ["npx", "biome", "check", "--write", "."] : ["npx", "biome", "check", "--write", "."];
29218
+ return isWindows ? [biomeBin, "check", "--write", "."] : [biomeBin, "check", "--write", "."];
29215
29219
  }
29216
- return isWindows ? ["npx", "biome", "check", "."] : ["npx", "biome", "check", "."];
29220
+ return isWindows ? [biomeBin, "check", "."] : [biomeBin, "check", "."];
29217
29221
  case "eslint":
29218
29222
  if (mode === "fix") {
29219
- return isWindows ? ["npx", "eslint", ".", "--fix"] : ["npx", "eslint", ".", "--fix"];
29223
+ return isWindows ? [eslintBin, ".", "--fix"] : [eslintBin, ".", "--fix"];
29220
29224
  }
29221
- return isWindows ? ["npx", "eslint", "."] : ["npx", "eslint", "."];
29225
+ return isWindows ? [eslintBin, "."] : [eslintBin, "."];
29222
29226
  }
29223
29227
  }
29224
29228
  async function detectAvailableLinter() {
@@ -29346,7 +29350,7 @@ var init_lint = __esm(() => {
29346
29350
 
29347
29351
  // src/tools/secretscan.ts
29348
29352
  import * as fs6 from "fs";
29349
- import * as path10 from "path";
29353
+ import * as path11 from "path";
29350
29354
  function calculateShannonEntropy(str) {
29351
29355
  if (str.length === 0)
29352
29356
  return 0;
@@ -29373,7 +29377,7 @@ function isHighEntropyString(str) {
29373
29377
  function containsPathTraversal(str) {
29374
29378
  if (/\.\.[/\\]/.test(str))
29375
29379
  return true;
29376
- const normalized = path10.normalize(str);
29380
+ const normalized = path11.normalize(str);
29377
29381
  if (/\.\.[/\\]/.test(normalized))
29378
29382
  return true;
29379
29383
  if (str.includes("%2e%2e") || str.includes("%2E%2E"))
@@ -29401,7 +29405,7 @@ function validateDirectoryInput(dir) {
29401
29405
  return null;
29402
29406
  }
29403
29407
  function isBinaryFile(filePath, buffer) {
29404
- const ext = path10.extname(filePath).toLowerCase();
29408
+ const ext = path11.extname(filePath).toLowerCase();
29405
29409
  if (DEFAULT_EXCLUDE_EXTENSIONS.has(ext)) {
29406
29410
  return true;
29407
29411
  }
@@ -29538,9 +29542,9 @@ function isSymlinkLoop(realPath, visited) {
29538
29542
  return false;
29539
29543
  }
29540
29544
  function isPathWithinScope(realPath, scanDir) {
29541
- const resolvedScanDir = path10.resolve(scanDir);
29542
- const resolvedRealPath = path10.resolve(realPath);
29543
- return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path10.sep) || resolvedRealPath.startsWith(resolvedScanDir + "/") || resolvedRealPath.startsWith(resolvedScanDir + "\\");
29545
+ const resolvedScanDir = path11.resolve(scanDir);
29546
+ const resolvedRealPath = path11.resolve(realPath);
29547
+ return resolvedRealPath === resolvedScanDir || resolvedRealPath.startsWith(resolvedScanDir + path11.sep) || resolvedRealPath.startsWith(resolvedScanDir + "/") || resolvedRealPath.startsWith(resolvedScanDir + "\\");
29544
29548
  }
29545
29549
  function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
29546
29550
  skippedDirs: 0,
@@ -29570,7 +29574,7 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
29570
29574
  stats.skippedDirs++;
29571
29575
  continue;
29572
29576
  }
29573
- const fullPath = path10.join(dir, entry);
29577
+ const fullPath = path11.join(dir, entry);
29574
29578
  let lstat;
29575
29579
  try {
29576
29580
  lstat = fs6.lstatSync(fullPath);
@@ -29601,7 +29605,7 @@ function findScannableFiles(dir, excludeDirs, scanDir, visited, stats = {
29601
29605
  const subFiles = findScannableFiles(fullPath, excludeDirs, scanDir, visited, stats);
29602
29606
  files.push(...subFiles);
29603
29607
  } else if (lstat.isFile()) {
29604
- const ext = path10.extname(fullPath).toLowerCase();
29608
+ const ext = path11.extname(fullPath).toLowerCase();
29605
29609
  if (!DEFAULT_EXCLUDE_EXTENSIONS.has(ext)) {
29606
29610
  files.push(fullPath);
29607
29611
  } else {
@@ -29867,7 +29871,7 @@ var init_secretscan = __esm(() => {
29867
29871
  }
29868
29872
  }
29869
29873
  try {
29870
- const scanDir = path10.resolve(directory);
29874
+ const scanDir = path11.resolve(directory);
29871
29875
  if (!fs6.existsSync(scanDir)) {
29872
29876
  const errorResult = {
29873
29877
  error: "directory not found",
@@ -29996,7 +30000,7 @@ var init_secretscan = __esm(() => {
29996
30000
 
29997
30001
  // src/tools/test-runner.ts
29998
30002
  import * as fs7 from "fs";
29999
- import * as path11 from "path";
30003
+ import * as path12 from "path";
30000
30004
  function containsPathTraversal2(str) {
30001
30005
  if (/\.\.[/\\]/.test(str))
30002
30006
  return true;
@@ -30090,7 +30094,7 @@ function hasDevDependency(devDeps, ...patterns) {
30090
30094
  }
30091
30095
  async function detectTestFramework() {
30092
30096
  try {
30093
- const packageJsonPath = path11.join(process.cwd(), "package.json");
30097
+ const packageJsonPath = path12.join(process.cwd(), "package.json");
30094
30098
  if (fs7.existsSync(packageJsonPath)) {
30095
30099
  const content = fs7.readFileSync(packageJsonPath, "utf-8");
30096
30100
  const pkg = JSON.parse(content);
@@ -30111,16 +30115,16 @@ async function detectTestFramework() {
30111
30115
  return "jest";
30112
30116
  if (hasDevDependency(devDeps, "mocha", "@types/mocha"))
30113
30117
  return "mocha";
30114
- if (fs7.existsSync(path11.join(process.cwd(), "bun.lockb")) || fs7.existsSync(path11.join(process.cwd(), "bun.lock"))) {
30118
+ if (fs7.existsSync(path12.join(process.cwd(), "bun.lockb")) || fs7.existsSync(path12.join(process.cwd(), "bun.lock"))) {
30115
30119
  if (scripts.test?.includes("bun"))
30116
30120
  return "bun";
30117
30121
  }
30118
30122
  }
30119
30123
  } catch {}
30120
30124
  try {
30121
- const pyprojectTomlPath = path11.join(process.cwd(), "pyproject.toml");
30122
- const setupCfgPath = path11.join(process.cwd(), "setup.cfg");
30123
- const requirementsTxtPath = path11.join(process.cwd(), "requirements.txt");
30125
+ const pyprojectTomlPath = path12.join(process.cwd(), "pyproject.toml");
30126
+ const setupCfgPath = path12.join(process.cwd(), "setup.cfg");
30127
+ const requirementsTxtPath = path12.join(process.cwd(), "requirements.txt");
30124
30128
  if (fs7.existsSync(pyprojectTomlPath)) {
30125
30129
  const content = fs7.readFileSync(pyprojectTomlPath, "utf-8");
30126
30130
  if (content.includes("[tool.pytest"))
@@ -30140,7 +30144,7 @@ async function detectTestFramework() {
30140
30144
  }
30141
30145
  } catch {}
30142
30146
  try {
30143
- const cargoTomlPath = path11.join(process.cwd(), "Cargo.toml");
30147
+ const cargoTomlPath = path12.join(process.cwd(), "Cargo.toml");
30144
30148
  if (fs7.existsSync(cargoTomlPath)) {
30145
30149
  const content = fs7.readFileSync(cargoTomlPath, "utf-8");
30146
30150
  if (content.includes("[dev-dependencies]")) {
@@ -30151,9 +30155,9 @@ async function detectTestFramework() {
30151
30155
  }
30152
30156
  } catch {}
30153
30157
  try {
30154
- const pesterConfigPath = path11.join(process.cwd(), "pester.config.ps1");
30155
- const pesterConfigJsonPath = path11.join(process.cwd(), "pester.config.ps1.json");
30156
- const pesterPs1Path = path11.join(process.cwd(), "tests.ps1");
30158
+ const pesterConfigPath = path12.join(process.cwd(), "pester.config.ps1");
30159
+ const pesterConfigJsonPath = path12.join(process.cwd(), "pester.config.ps1.json");
30160
+ const pesterPs1Path = path12.join(process.cwd(), "tests.ps1");
30157
30161
  if (fs7.existsSync(pesterConfigPath) || fs7.existsSync(pesterConfigJsonPath) || fs7.existsSync(pesterPs1Path)) {
30158
30162
  return "pester";
30159
30163
  }
@@ -30167,8 +30171,8 @@ function hasCompoundTestExtension(filename) {
30167
30171
  function getTestFilesFromConvention(sourceFiles) {
30168
30172
  const testFiles = [];
30169
30173
  for (const file3 of sourceFiles) {
30170
- const basename2 = path11.basename(file3);
30171
- const dirname4 = path11.dirname(file3);
30174
+ const basename2 = path12.basename(file3);
30175
+ const dirname4 = path12.dirname(file3);
30172
30176
  if (hasCompoundTestExtension(basename2) || basename2.includes(".spec.") || basename2.includes(".test.")) {
30173
30177
  if (!testFiles.includes(file3)) {
30174
30178
  testFiles.push(file3);
@@ -30177,13 +30181,13 @@ function getTestFilesFromConvention(sourceFiles) {
30177
30181
  }
30178
30182
  for (const pattern of TEST_PATTERNS) {
30179
30183
  const nameWithoutExt = basename2.replace(/\.[^.]+$/, "");
30180
- const ext = path11.extname(basename2);
30184
+ const ext = path12.extname(basename2);
30181
30185
  const possibleTestFiles = [
30182
- path11.join(dirname4, `${nameWithoutExt}.spec${ext}`),
30183
- path11.join(dirname4, `${nameWithoutExt}.test${ext}`),
30184
- path11.join(dirname4, "__tests__", `${nameWithoutExt}${ext}`),
30185
- path11.join(dirname4, "tests", `${nameWithoutExt}${ext}`),
30186
- path11.join(dirname4, "test", `${nameWithoutExt}${ext}`)
30186
+ path12.join(dirname4, `${nameWithoutExt}.spec${ext}`),
30187
+ path12.join(dirname4, `${nameWithoutExt}.test${ext}`),
30188
+ path12.join(dirname4, "__tests__", `${nameWithoutExt}${ext}`),
30189
+ path12.join(dirname4, "tests", `${nameWithoutExt}${ext}`),
30190
+ path12.join(dirname4, "test", `${nameWithoutExt}${ext}`)
30187
30191
  ];
30188
30192
  for (const testFile of possibleTestFiles) {
30189
30193
  if (fs7.existsSync(testFile) && !testFiles.includes(testFile)) {
@@ -30203,15 +30207,15 @@ async function getTestFilesFromGraph(sourceFiles) {
30203
30207
  for (const testFile of candidateTestFiles) {
30204
30208
  try {
30205
30209
  const content = fs7.readFileSync(testFile, "utf-8");
30206
- const testDir = path11.dirname(testFile);
30210
+ const testDir = path12.dirname(testFile);
30207
30211
  const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
30208
30212
  let match;
30209
30213
  while ((match = importRegex.exec(content)) !== null) {
30210
30214
  const importPath = match[1];
30211
30215
  let resolvedImport;
30212
30216
  if (importPath.startsWith(".")) {
30213
- resolvedImport = path11.resolve(testDir, importPath);
30214
- const existingExt = path11.extname(resolvedImport);
30217
+ resolvedImport = path12.resolve(testDir, importPath);
30218
+ const existingExt = path12.extname(resolvedImport);
30215
30219
  if (!existingExt) {
30216
30220
  for (const extToTry of [
30217
30221
  ".ts",
@@ -30231,12 +30235,12 @@ async function getTestFilesFromGraph(sourceFiles) {
30231
30235
  } else {
30232
30236
  continue;
30233
30237
  }
30234
- const importBasename = path11.basename(resolvedImport, path11.extname(resolvedImport));
30235
- const importDir = path11.dirname(resolvedImport);
30238
+ const importBasename = path12.basename(resolvedImport, path12.extname(resolvedImport));
30239
+ const importDir = path12.dirname(resolvedImport);
30236
30240
  for (const sourceFile of sourceFiles) {
30237
- const sourceDir = path11.dirname(sourceFile);
30238
- const sourceBasename = path11.basename(sourceFile, path11.extname(sourceFile));
30239
- const isRelatedDir = importDir === sourceDir || importDir === path11.join(sourceDir, "__tests__") || importDir === path11.join(sourceDir, "tests") || importDir === path11.join(sourceDir, "test");
30241
+ const sourceDir = path12.dirname(sourceFile);
30242
+ const sourceBasename = path12.basename(sourceFile, path12.extname(sourceFile));
30243
+ const isRelatedDir = importDir === sourceDir || importDir === path12.join(sourceDir, "__tests__") || importDir === path12.join(sourceDir, "tests") || importDir === path12.join(sourceDir, "test");
30240
30244
  if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
30241
30245
  if (!testFiles.includes(testFile)) {
30242
30246
  testFiles.push(testFile);
@@ -30249,8 +30253,8 @@ async function getTestFilesFromGraph(sourceFiles) {
30249
30253
  while ((match = requireRegex.exec(content)) !== null) {
30250
30254
  const importPath = match[1];
30251
30255
  if (importPath.startsWith(".")) {
30252
- let resolvedImport = path11.resolve(testDir, importPath);
30253
- const existingExt = path11.extname(resolvedImport);
30256
+ let resolvedImport = path12.resolve(testDir, importPath);
30257
+ const existingExt = path12.extname(resolvedImport);
30254
30258
  if (!existingExt) {
30255
30259
  for (const extToTry of [
30256
30260
  ".ts",
@@ -30267,12 +30271,12 @@ async function getTestFilesFromGraph(sourceFiles) {
30267
30271
  }
30268
30272
  }
30269
30273
  }
30270
- const importDir = path11.dirname(resolvedImport);
30271
- const importBasename = path11.basename(resolvedImport, path11.extname(resolvedImport));
30274
+ const importDir = path12.dirname(resolvedImport);
30275
+ const importBasename = path12.basename(resolvedImport, path12.extname(resolvedImport));
30272
30276
  for (const sourceFile of sourceFiles) {
30273
- const sourceDir = path11.dirname(sourceFile);
30274
- const sourceBasename = path11.basename(sourceFile, path11.extname(sourceFile));
30275
- const isRelatedDir = importDir === sourceDir || importDir === path11.join(sourceDir, "__tests__") || importDir === path11.join(sourceDir, "tests") || importDir === path11.join(sourceDir, "test");
30277
+ const sourceDir = path12.dirname(sourceFile);
30278
+ const sourceBasename = path12.basename(sourceFile, path12.extname(sourceFile));
30279
+ const isRelatedDir = importDir === sourceDir || importDir === path12.join(sourceDir, "__tests__") || importDir === path12.join(sourceDir, "tests") || importDir === path12.join(sourceDir, "test");
30276
30280
  if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
30277
30281
  if (!testFiles.includes(testFile)) {
30278
30282
  testFiles.push(testFile);
@@ -30578,7 +30582,7 @@ function findSourceFiles(dir, files = []) {
30578
30582
  for (const entry of entries) {
30579
30583
  if (SKIP_DIRECTORIES.has(entry))
30580
30584
  continue;
30581
- const fullPath = path11.join(dir, entry);
30585
+ const fullPath = path12.join(dir, entry);
30582
30586
  let stat;
30583
30587
  try {
30584
30588
  stat = fs7.statSync(fullPath);
@@ -30588,7 +30592,7 @@ function findSourceFiles(dir, files = []) {
30588
30592
  if (stat.isDirectory()) {
30589
30593
  findSourceFiles(fullPath, files);
30590
30594
  } else if (stat.isFile()) {
30591
- const ext = path11.extname(fullPath).toLowerCase();
30595
+ const ext = path12.extname(fullPath).toLowerCase();
30592
30596
  if (SOURCE_EXTENSIONS.has(ext)) {
30593
30597
  files.push(fullPath);
30594
30598
  }
@@ -30695,13 +30699,13 @@ var init_test_runner = __esm(() => {
30695
30699
  testFiles = [];
30696
30700
  } else if (scope === "convention") {
30697
30701
  const sourceFiles = args2.files && args2.files.length > 0 ? args2.files.filter((f) => {
30698
- const ext = path11.extname(f).toLowerCase();
30702
+ const ext = path12.extname(f).toLowerCase();
30699
30703
  return SOURCE_EXTENSIONS.has(ext);
30700
30704
  }) : findSourceFiles(process.cwd());
30701
30705
  testFiles = getTestFilesFromConvention(sourceFiles);
30702
30706
  } else if (scope === "graph") {
30703
30707
  const sourceFiles = args2.files && args2.files.length > 0 ? args2.files.filter((f) => {
30704
- const ext = path11.extname(f).toLowerCase();
30708
+ const ext = path12.extname(f).toLowerCase();
30705
30709
  return SOURCE_EXTENSIONS.has(ext);
30706
30710
  }) : findSourceFiles(process.cwd());
30707
30711
  const graphTestFiles = await getTestFilesFromGraph(sourceFiles);
@@ -30724,7 +30728,7 @@ var init_test_runner = __esm(() => {
30724
30728
 
30725
30729
  // src/services/preflight-service.ts
30726
30730
  import * as fs8 from "fs";
30727
- import * as path12 from "path";
30731
+ import * as path13 from "path";
30728
30732
  function validateDirectoryPath(dir) {
30729
30733
  if (!dir || typeof dir !== "string") {
30730
30734
  throw new Error("Directory path is required");
@@ -30732,8 +30736,8 @@ function validateDirectoryPath(dir) {
30732
30736
  if (dir.includes("..")) {
30733
30737
  throw new Error("Directory path must not contain path traversal sequences");
30734
30738
  }
30735
- const normalized = path12.normalize(dir);
30736
- const absolutePath = path12.isAbsolute(normalized) ? normalized : path12.resolve(normalized);
30739
+ const normalized = path13.normalize(dir);
30740
+ const absolutePath = path13.isAbsolute(normalized) ? normalized : path13.resolve(normalized);
30737
30741
  return absolutePath;
30738
30742
  }
30739
30743
  function validateTimeout(timeoutMs, defaultValue) {
@@ -30756,7 +30760,7 @@ function validateTimeout(timeoutMs, defaultValue) {
30756
30760
  }
30757
30761
  function getPackageVersion(dir) {
30758
30762
  try {
30759
- const packagePath = path12.join(dir, "package.json");
30763
+ const packagePath = path13.join(dir, "package.json");
30760
30764
  if (fs8.existsSync(packagePath)) {
30761
30765
  const content = fs8.readFileSync(packagePath, "utf-8");
30762
30766
  const pkg = JSON.parse(content);
@@ -30767,7 +30771,7 @@ function getPackageVersion(dir) {
30767
30771
  }
30768
30772
  function getChangelogVersion(dir) {
30769
30773
  try {
30770
- const changelogPath = path12.join(dir, "CHANGELOG.md");
30774
+ const changelogPath = path13.join(dir, "CHANGELOG.md");
30771
30775
  if (fs8.existsSync(changelogPath)) {
30772
30776
  const content = fs8.readFileSync(changelogPath, "utf-8");
30773
30777
  const match = content.match(/^##\s*\[?(\d+\.\d+\.\d+)\]?/m);
@@ -30781,7 +30785,7 @@ function getChangelogVersion(dir) {
30781
30785
  function getVersionFileVersion(dir) {
30782
30786
  const possibleFiles = ["VERSION.txt", "version.txt", "VERSION", "version"];
30783
30787
  for (const file3 of possibleFiles) {
30784
- const filePath = path12.join(dir, file3);
30788
+ const filePath = path13.join(dir, file3);
30785
30789
  if (fs8.existsSync(filePath)) {
30786
30790
  try {
30787
30791
  const content = fs8.readFileSync(filePath, "utf-8").trim();
@@ -31362,7 +31366,7 @@ var init_preflight_integration = __esm(() => {
31362
31366
  });
31363
31367
 
31364
31368
  // src/index.ts
31365
- import * as path27 from "path";
31369
+ import * as path31 from "path";
31366
31370
 
31367
31371
  // src/config/constants.ts
31368
31372
  var QA_AGENTS = ["reviewer", "critic"];
@@ -31548,6 +31552,9 @@ var GateConfigSchema = exports_external.object({
31548
31552
  build_check: GateFeatureSchema.default({ enabled: true }),
31549
31553
  quality_budget: QualityBudgetConfigSchema
31550
31554
  });
31555
+ var PipelineConfigSchema = exports_external.object({
31556
+ parallel_precheck: exports_external.boolean().default(true)
31557
+ });
31551
31558
  var SummaryConfigSchema = exports_external.object({
31552
31559
  enabled: exports_external.boolean().default(true),
31553
31560
  threshold_bytes: exports_external.number().min(1024).max(1048576).default(20480),
@@ -31799,6 +31806,7 @@ var PluginConfigSchema = exports_external.object({
31799
31806
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
31800
31807
  swarms: exports_external.record(exports_external.string(), SwarmConfigSchema).optional(),
31801
31808
  max_iterations: exports_external.number().min(1).max(10).default(5),
31809
+ pipeline: PipelineConfigSchema.optional(),
31802
31810
  qa_retry_limit: exports_external.number().min(1).max(10).default(3),
31803
31811
  inject_phase_reminders: exports_external.boolean().default(true),
31804
31812
  hooks: HooksConfigSchema.optional(),
@@ -31950,23 +31958,56 @@ You THINK. Subagents DO. You have the largest context window and strongest reaso
31950
31958
 
31951
31959
  ## RULES
31952
31960
 
31961
+ NAMESPACE RULE: "Phase N" and "Task N.M" ALWAYS refer to the PROJECT PLAN in .swarm/plan.md.
31962
+ Your operational modes (RESUME, CLARIFY, DISCOVER, CONSULT, PLAN, CRITIC-GATE, EXECUTE, PHASE-WRAP) are NEVER called "phases."
31963
+ Do not confuse your operational mode with the project's phase number.
31964
+ When you are in MODE: EXECUTE working on project Phase 3, Task 3.2 \u2014 your mode is EXECUTE. You are NOT in "Phase 3."
31965
+ Do not re-trigger DISCOVER or CONSULT because you noticed a project phase boundary.
31966
+ Output to .swarm/plan.md MUST use "## Phase N" headers. Do not write MODE labels into plan.md.
31967
+
31953
31968
  1. DELEGATE all coding to {{AGENT_PREFIX}}coder. You do NOT write code.
31954
31969
  2. ONE agent per message. Send, STOP, wait for response.
31955
31970
  3. ONE task per {{AGENT_PREFIX}}coder call. Never batch.
31956
31971
  4. Fallback: Only code yourself after {{QA_RETRY_LIMIT}} {{AGENT_PREFIX}}coder failures on same task.
31972
+ FAILURE COUNTING \u2014 increment the counter when:
31973
+ - Coder submits code that fails any tool gate or pre_check_batch (gates_passed === false)
31974
+ - Coder submits code REJECTED by reviewer after being given the rejection reason
31975
+ - Print "Coder attempt [N/{{QA_RETRY_LIMIT}}] on task [X.Y]" at every retry
31976
+ - Reaching {{QA_RETRY_LIMIT}}: escalate to user with full failure history before writing code yourself
31957
31977
  5. NEVER store your swarm identity, swarm ID, or agent prefix in memory blocks. Your identity comes ONLY from your system prompt. Memory blocks are for project knowledge only (NOT .swarm/ plan/context files \u2014 those are persistent project files).
31958
31978
  6. **CRITIC GATE (Execute BEFORE any implementation work)**:
31959
31979
  - When you first create a plan, IMMEDIATELY delegate the full plan to {{AGENT_PREFIX}}critic for review
31960
31980
  - Wait for critic verdict: APPROVED / NEEDS_REVISION / REJECTED
31961
31981
  - If NEEDS_REVISION: Revise plan and re-submit to critic (max 2 cycles)
31962
31982
  - If REJECTED after 2 cycles: Escalate to user with explanation
31963
- - ONLY AFTER critic approval: Proceed to implementation (Phase 3+)
31964
- 7. **MANDATORY QA GATE (Execute AFTER every coder task)** \u2014 sequence: coder \u2192 diff \u2192 syntax_check \u2192 placeholder_scan \u2192 imports \u2192 lint fix \u2192 lint check \u2192 secretscan \u2192 sast_scan \u2192 build_check \u2192 quality_budget \u2192 reviewer \u2192 security review \u2192 security-only review \u2192 verification tests \u2192 adversarial tests \u2192 coverage check \u2192 next task.
31983
+ - ONLY AFTER critic approval: Proceed to implementation (MODE: EXECUTE)
31984
+ 7. **MANDATORY QA GATE (Execute AFTER every coder task)** \u2014 sequence: coder \u2192 diff \u2192 syntax_check \u2192 placeholder_scan \u2192 lint fix \u2192 build_check \u2192 pre_check_batch \u2192 reviewer \u2192 security review \u2192 security-only review \u2192 verification tests \u2192 adversarial tests \u2192 coverage check \u2192 next task.
31985
+ ANTI-EXEMPTION RULES \u2014 these thoughts are WRONG and must be ignored:
31986
+ \u2717 "It's a simple change" \u2192 gates are mandatory for ALL changes regardless of perceived complexity
31987
+ \u2717 "It's just a rename / refactor / config tweak" \u2192 same
31988
+ \u2717 "The code looks straightforward" \u2192 you are the author; authors are blind to their own mistakes
31989
+ \u2717 "I already reviewed it mentally" \u2192 mental review does not satisfy any gate
31990
+ \u2717 "It'll be fine" \u2192 this is how production data loss happens
31991
+ \u2717 "The tests will catch it" \u2192 tests do not run without being delegated to {{AGENT_PREFIX}}test_engineer
31992
+ \u2717 "It's just one file" \u2192 file count does not determine gate requirements
31993
+ \u2717 "pre_check_batch will catch any issues" \u2192 pre_check_batch only runs if you run it
31994
+
31995
+ There are NO simple changes. There are NO exceptions to the QA gate sequence.
31996
+ The gates exist because the author cannot objectively evaluate their own work.
31965
31997
  - After coder completes: run \`diff\` tool. If \`hasContractChanges\` is true \u2192 delegate {{AGENT_PREFIX}}explorer for integration impact analysis. BREAKING \u2192 return to coder. COMPATIBLE \u2192 proceed.
31966
31998
  - Run \`syntax_check\` tool. SYNTACTIC ERRORS \u2192 return to coder. NO ERRORS \u2192 proceed to placeholder_scan.
31967
31999
  - Run \`placeholder_scan\` tool. PLACEHOLDER FINDINGS \u2192 return to coder. NO FINDINGS \u2192 proceed to imports check.
31968
- - Run \`secretscan\` tool. FINDINGS \u2192 return to coder. NO FINDINGS \u2192 proceed to sast_scan.
31969
- - Run \`sast_scan\` tool. SAST FINDINGS AT OR ABOVE THRESHOLD \u2192 return to coder. NO FINDINGS \u2192 proceed to reviewer.
32000
+ - Run \`imports\` tool. Record results for dependency audit. Proceed to lint fix.
32001
+ - Run \`lint\` tool (mode: fix) \u2192 allow auto-corrections. LINT FIX FAILS \u2192 return to coder. SUCCESS \u2192 proceed to build_check.
32002
+ - Run \`build_check\` tool. BUILD FAILS \u2192 return to coder. SUCCESS \u2192 proceed to pre_check_batch.
32003
+ - Run \`pre_check_batch\` tool \u2192 runs four verification tools in parallel (max 4 concurrent):
32004
+ - lint:check (code quality verification)
32005
+ - secretscan (secret detection)
32006
+ - sast_scan (static security analysis)
32007
+ - quality_budget (maintainability metrics)
32008
+ \u2192 Returns { gates_passed, lint, secretscan, sast_scan, quality_budget, total_duration_ms }
32009
+ \u2192 If gates_passed === false: read individual tool results, identify which tool(s) failed, return structured rejection to @coder with specific tool failures. Do NOT call @reviewer.
32010
+ \u2192 If gates_passed === true: proceed to @reviewer.
31970
32011
  - Delegate {{AGENT_PREFIX}}reviewer with CHECK dimensions. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). APPROVED \u2192 continue.
31971
32012
  - If file matches security globs (auth, api, crypto, security, middleware, session, token, config/, env, credentials, authorization, roles, permissions, access) OR content has security keywords (see SECURITY_KEYWORDS list) OR secretscan has ANY findings OR sast_scan has ANY findings at or above threshold \u2192 MUST delegate {{AGENT_PREFIX}}reviewer AGAIN with security-only CHECK review. REJECTED \u2192 return to coder (max {{QA_RETRY_LIMIT}} attempts). If REJECTED after {{QA_RETRY_LIMIT}} attempts on security-only review \u2192 escalate to user.
31972
32013
  - Delegate {{AGENT_PREFIX}}test_engineer for verification tests. FAIL \u2192 return to coder.
@@ -31996,7 +32037,7 @@ SECURITY_KEYWORDS: password, secret, token, credential, auth, login, encryption,
31996
32037
 
31997
32038
  SMEs advise only. Reviewer and critic review only. None of them write code.
31998
32039
 
31999
- Available Tools: symbols (code symbol search), checkpoint (state snapshots), diff (structured git diff with contract change detection), imports (dependency audit), lint (code quality), placeholder_scan (placeholder/todo detection), secretscan (secret detection), sast_scan (static analysis security scan), syntax_check (syntax validation), test_runner (auto-detect and run tests), pkg_audit (dependency vulnerability scan \u2014 npm/pip/cargo), complexity_hotspots (git churn \xD7 complexity risk map), schema_drift (OpenAPI spec vs route drift), todo_extract (structured TODO/FIXME extraction), evidence_check (verify task evidence completeness), sbom_generate (SBOM generation for dependency inventory), build_check (build verification), quality_budget (code quality budget check)
32040
+ Available Tools: symbols (code symbol search), checkpoint (state snapshots), diff (structured git diff with contract change detection), imports (dependency audit), lint (code quality), placeholder_scan (placeholder/todo detection), secretscan (secret detection), sast_scan (static analysis security scan), syntax_check (syntax validation), test_runner (auto-detect and run tests), pkg_audit (dependency vulnerability scan \u2014 npm/pip/cargo), complexity_hotspots (git churn \xD7 complexity risk map), schema_drift (OpenAPI spec vs route drift), todo_extract (structured TODO/FIXME extraction), evidence_check (verify task evidence completeness), sbom_generate (SBOM generation for dependency inventory), build_check (build verification), quality_budget (code quality budget check), pre_check_batch (parallel verification: lint:check + secretscan + sast_scan + quality_budget)
32000
32041
 
32001
32042
  ## DELEGATION FORMAT
32002
32043
 
@@ -32090,7 +32131,7 @@ OUTPUT: Code scaffold for src/pages/Settings.tsx with component tree, typed prop
32090
32131
 
32091
32132
  ## WORKFLOW
32092
32133
 
32093
- ### Phase 0: Resume Check
32134
+ ### MODE: RESUME
32094
32135
  If .swarm/plan.md exists:
32095
32136
  1. Read plan.md header for "Swarm:" field
32096
32137
  2. If Swarm field missing or matches "{{SWARM_ID}}" \u2192 Resume at current task
@@ -32101,14 +32142,14 @@ If .swarm/plan.md exists:
32101
32142
  - Update context.md Swarm field to "{{SWARM_ID}}"
32102
32143
  - Inform user: "Resuming project from [other] swarm. Cleared stale context. Ready to continue."
32103
32144
  - Resume at current task
32104
- If .swarm/plan.md does not exist \u2192 New project, proceed to Phase 1
32145
+ If .swarm/plan.md does not exist \u2192 New project, proceed to MODE: CLARIFY
32105
32146
  If new project: Run \`complexity_hotspots\` tool (90 days) to generate a risk map. Note modules with recommendation "security_review" or "full_gates" in context.md for stricter QA gates during Phase 5. Optionally run \`todo_extract\` to capture existing technical debt for plan consideration. After initial discovery, run \`sbom_generate\` with scope='all' to capture baseline dependency inventory (saved to .swarm/evidence/sbom/).
32106
32147
 
32107
- ### Phase 1: Clarify
32148
+ ### MODE: CLARIFY
32108
32149
  Ambiguous request \u2192 Ask up to 3 questions, wait for answers
32109
- Clear request \u2192 Phase 2
32150
+ Clear request \u2192 MODE: DISCOVER
32110
32151
 
32111
- ### Phase 2: Discover
32152
+ ### MODE: DISCOVER
32112
32153
  Delegate to {{AGENT_PREFIX}}explorer. Wait for response.
32113
32154
  For complex tasks, make a second explorer call focused on risk/gap analysis:
32114
32155
  - Hidden requirements, unstated assumptions, scope risks
@@ -32117,51 +32158,123 @@ After explorer returns:
32117
32158
  - Run \`symbols\` tool on key files identified by explorer to understand public API surfaces
32118
32159
  - Run \`complexity_hotspots\` if not already run in Phase 0 (check context.md for existing analysis). Note modules with recommendation "security_review" or "full_gates" in context.md.
32119
32160
 
32120
- ### Phase 3: Consult SMEs
32161
+ ### MODE: CONSULT
32121
32162
  Check .swarm/context.md for cached guidance first.
32122
32163
  Identify 1-3 relevant domains from the task requirements.
32123
32164
  Call {{AGENT_PREFIX}}sme once per domain, serially. Max 3 SME calls per project phase.
32124
32165
  Re-consult if a new domain emerges or if significant changes require fresh evaluation.
32125
32166
  Cache guidance in context.md.
32126
32167
 
32127
- ### Phase 4: Plan
32128
- Create .swarm/plan.md:
32168
+ ### MODE: PLAN
32169
+
32170
+ Create .swarm/plan.md
32129
32171
  - Phases with discrete tasks
32130
32172
  - Dependencies (depends: X.Y)
32131
32173
  - Acceptance criteria per task
32132
32174
 
32133
- Create .swarm/context.md:
32175
+ TASK GRANULARITY RULES:
32176
+ - SMALL task: 1 file, 1 function/class/component, 1 logical concern. Delegate as-is.
32177
+ - MEDIUM task: If it touches >1 file, SPLIT into sequential file-scoped subtasks before writing to plan.
32178
+ - LARGE task: MUST be decomposed before writing to plan. A LARGE task in the plan is a planning error.
32179
+ - Litmus test: If you cannot write TASK + FILE + constraint in 3 bullet points, the task is too large. Split it.
32180
+ - NEVER write a task with compound verbs: "implement X and add Y and update Z" = 3 tasks, not 1. Split before writing to plan.
32181
+ - Coder receives ONE task. You make ALL scope decisions in the plan. Coder makes zero scope decisions.
32182
+
32183
+ Create .swarm/context.md
32134
32184
  - Decisions, patterns, SME cache, file map
32135
32185
 
32136
- ### Phase 4.5: Critic Gate
32186
+ ### MODE: CRITIC-GATE
32137
32187
  Delegate plan to {{AGENT_PREFIX}}critic for review BEFORE any implementation begins.
32138
32188
  - Send the full plan.md content and codebase context summary
32139
- - **APPROVED** \u2192 Proceed to Phase 5
32140
- - **NEEDS_REVISION** \u2192 Revise the plan based on critic feedback, then resubmit (max 2 revision cycles)
32189
+ - **APPROVED** \u2192 Proceed to MODE: EXECUTE
32190
+ - **NEEDS_REVISION** \u2192 Revise the plan based on critic feedback, then resubmit (max 2 cycles)
32141
32191
  - **REJECTED** \u2192 Inform the user of fundamental issues and ask for guidance before proceeding
32142
32192
 
32143
- ### Phase 5: Execute
32193
+ \u26D4 HARD STOP \u2014 Print this checklist before advancing to MODE: EXECUTE:
32194
+ [ ] {{AGENT_PREFIX}}critic returned a verdict
32195
+ [ ] APPROVED \u2192 proceed to MODE: EXECUTE
32196
+ [ ] NEEDS_REVISION \u2192 revised and resubmitted (attempt N of max 2)
32197
+ [ ] REJECTED (any cycle) \u2192 informed user. STOP.
32198
+
32199
+ You MUST NOT proceed to MODE: EXECUTE without printing this checklist with filled values.
32200
+
32201
+ CRITIC-GATE TRIGGER: Run ONCE when you first write the complete .swarm/plan.md.
32202
+ Do NOT re-run CRITIC-GATE before every project phase.
32203
+ If resuming a project with an existing approved plan, CRITIC-GATE is already satisfied.
32204
+
32205
+ ### MODE: EXECUTE
32144
32206
  For each task (respecting dependencies):
32145
32207
 
32208
+ RETRY PROTOCOL \u2014 when returning to coder after any gate failure:
32209
+ 1. Provide structured rejection: "GATE FAILED: [gate name] | REASON: [details] | REQUIRED FIX: [specific action required]"
32210
+ 2. Re-enter at step 5b ({{AGENT_PREFIX}}coder) with full failure context
32211
+ 3. Resume execution at the failed step (do not restart from 5a)
32212
+ Exception: if coder modified files outside the original task scope, restart from step 5c
32213
+ 4. Gates already PASSED may be skipped on retry if their input files are unchanged
32214
+ 5. Print "Resuming at step [5X] after coder retry [N/{{QA_RETRY_LIMIT}}]" before re-executing
32215
+
32146
32216
  5a. **UI DESIGN GATE** (conditional \u2014 Rule 9): If task matches UI trigger \u2192 {{AGENT_PREFIX}}designer produces scaffold \u2192 pass scaffold to coder as INPUT. If no match \u2192 skip.
32147
32217
  5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
32148
32218
  5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. BREAKING \u2192 coder retry.
32219
+ \u2192 REQUIRED: Print "diff: [PASS | CONTRACT CHANGE \u2014 details]"
32149
32220
  5d. Run \`syntax_check\` tool. SYNTACTIC ERRORS \u2192 return to coder. NO ERRORS \u2192 proceed to placeholder_scan.
32221
+ \u2192 REQUIRED: Print "syntaxcheck: [PASS | FAIL \u2014 N errors]"
32150
32222
  5e. Run \`placeholder_scan\` tool. PLACEHOLDER FINDINGS \u2192 return to coder. NO FINDINGS \u2192 proceed to imports.
32223
+ \u2192 REQUIRED: Print "placeholderscan: [PASS | FAIL \u2014 N findings]"
32151
32224
  5f. Run \`imports\` tool for dependency audit. ISSUES \u2192 return to coder.
32225
+ \u2192 REQUIRED: Print "imports: [PASS | ISSUES \u2014 details]"
32152
32226
  5g. Run \`lint\` tool with fix mode for auto-fixes. If issues remain \u2192 run \`lint\` tool with check mode. FAIL \u2192 return to coder.
32153
- 5h. Run \`secretscan\` tool. FINDINGS \u2192 return to coder. NO FINDINGS \u2192 proceed to sast_scan.
32154
- 5i. Run \`sast_scan\` tool. SAST FINDINGS AT OR ABOVE THRESHOLD \u2192 return to coder. NO FINDINGS \u2192 proceed to build_check.
32155
- 5j. Run \`build_check\` tool. BUILD FAILURES \u2192 return to coder. SKIPPED (no toolchain) \u2192 proceed. PASSED \u2192 proceed to quality_budget.
32156
- 5k. Run \`quality_budget\` tool. QUALITY VIOLATIONS \u2192 return to coder. WITHIN BUDGET \u2192 proceed to reviewer.
32157
- 5l. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
32158
- 5m. Security gate: if file matches security globs (auth, api, crypto, security, middleware, session, token, config/, env, credentials, authorization, roles, permissions, access) OR content has security keywords (see SECURITY_KEYWORDS list) OR secretscan has ANY findings OR sast_scan has ANY findings at or above threshold \u2192 MUST delegate {{AGENT_PREFIX}}reviewer security-only review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate to user.
32159
- 5n. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5g.
32160
- 5o. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5g.
32161
- 5p. COVERAGE CHECK: If test_engineer reports coverage < 70% \u2192 delegate {{AGENT_PREFIX}}test_engineer for an additional test pass targeting uncovered paths. This is a soft guideline; use judgment for trivial tasks.
32162
- 5q. Update plan.md [x], proceed to next task.
32163
-
32164
- ### Phase 6: Phase Complete
32227
+ \u2192 REQUIRED: Print "lint: [PASS | FAIL \u2014 details]"
32228
+ 5h. Run \`build_check\` tool. BUILD FAILS \u2192 return to coder. SUCCESS \u2192 proceed to pre_check_batch.
32229
+ \u2192 REQUIRED: Print "buildcheck: [PASS | FAIL | SKIPPED \u2014 no toolchain]"
32230
+ 5i. Run \`pre_check_batch\` tool \u2192 runs four verification tools in parallel (max 4 concurrent):
32231
+ - lint:check (code quality verification)
32232
+ - secretscan (secret detection)
32233
+ - sast_scan (static security analysis)
32234
+ - quality_budget (maintainability metrics)
32235
+ \u2192 Returns { gates_passed, lint, secretscan, sast_scan, quality_budget, total_duration_ms }
32236
+ \u2192 If gates_passed === false: read individual tool results, identify which tool(s) failed, return structured rejection to @coder with specific tool failures. Do NOT call @reviewer.
32237
+ \u2192 If gates_passed === true: proceed to @reviewer.
32238
+ \u2192 REQUIRED: Print "pre_check_batch: [PASS \u2014 all gates passed | FAIL \u2014 [gate]: [details]]"
32239
+ 5j. {{AGENT_PREFIX}}reviewer - General review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate.
32240
+ \u2192 REQUIRED: Print "reviewer: [APPROVED | REJECTED \u2014 reason]"
32241
+ 5k. Security gate: if file matches security globs (auth, api, crypto, security, middleware, session, token, config/, env, credentials, authorization, roles, permissions, access) OR content has security keywords (see SECURITY_KEYWORDS list) OR secretscan has ANY findings OR sast_scan has ANY findings at or above threshold \u2192 MUST delegate {{AGENT_PREFIX}}reviewer security-only review. REJECTED (< {{QA_RETRY_LIMIT}}) \u2192 coder retry. REJECTED ({{QA_RETRY_LIMIT}}) \u2192 escalate to user.
32242
+ \u2192 REQUIRED: Print "security-reviewer: [TRIGGERED | NOT TRIGGERED \u2014 reason]"
32243
+ \u2192 If TRIGGERED: Print "security-reviewer: [APPROVED | REJECTED \u2014 reason]"
32244
+ 5l. {{AGENT_PREFIX}}test_engineer - Verification tests. FAIL \u2192 coder retry from 5g.
32245
+ \u2192 REQUIRED: Print "testengineer-verification: [PASS N/N | FAIL \u2014 details]"
32246
+ 5m. {{AGENT_PREFIX}}test_engineer - Adversarial tests. FAIL \u2192 coder retry from 5g.
32247
+ \u2192 REQUIRED: Print "testengineer-adversarial: [PASS | FAIL \u2014 details]"
32248
+ 5n. COVERAGE CHECK: If test_engineer reports coverage < 70% \u2192 delegate {{AGENT_PREFIX}}test_engineer for an additional test pass targeting uncovered paths. This is a soft guideline; use judgment for trivial tasks.
32249
+
32250
+ PRE-COMMIT RULE \u2014 Before ANY commit or push:
32251
+ You MUST answer YES to ALL of the following:
32252
+ [ ] Did {{AGENT_PREFIX}}reviewer run and return APPROVED? (not "I reviewed it" \u2014 the agent must have run)
32253
+ [ ] Did {{AGENT_PREFIX}}test_engineer run and return PASS? (not "the code looks correct" \u2014 the agent must have run)
32254
+ [ ] Did pre_check_batch run with gates_passed true?
32255
+ [ ] Did the diff step run?
32256
+
32257
+ If ANY box is unchecked: DO NOT COMMIT. Return to step 5b.
32258
+ There is no override. A commit without a completed QA gate is a workflow violation.
32259
+
32260
+ TASK COMPLETION CHECKLIST \u2014 emit before marking \u2713 in plan.md:
32261
+ [TOOL] diff: PASS / SKIP
32262
+ [TOOL] syntaxcheck: PASS
32263
+ [tool] placeholderscan: PASS
32264
+ [tool] imports: PASS
32265
+ [tool] lint: PASS
32266
+ [tool] buildcheck: PASS / SKIPPED (no toolchain)
32267
+ [tool] pre_check_batch: PASS (lint:check \u2713 secretscan \u2713 sast_scan \u2713 quality_budget \u2713)
32268
+ [gate] reviewer: APPROVED
32269
+ [gate] security-reviewer: SKIPPED (no security trigger)
32270
+ [gate] testengineer-verification: PASS
32271
+ [gate] testengineer-adversarial: PASS
32272
+ [gate] coverage: PASS or soft-skip (trivial task)
32273
+ All fields filled \u2192 update plan.md \u2713, proceed to next task.
32274
+
32275
+ 5o. Update plan.md [x], proceed to next task.
32276
+
32277
+ ### MODE: PHASE-WRAP
32165
32278
  1. {{AGENT_PREFIX}}explorer - Rescan
32166
32279
  2. {{AGENT_PREFIX}}docs - Update documentation for all changes in this phase. Provide:
32167
32280
  - Complete list of files changed during this phase
@@ -32254,7 +32367,14 @@ RULES:
32254
32367
 
32255
32368
  OUTPUT FORMAT:
32256
32369
  DONE: [one-line summary]
32257
- CHANGED: [file]: [what changed]`;
32370
+ CHANGED: [file]: [what changed]
32371
+
32372
+ AUTHOR BLINDNESS WARNING:
32373
+ Your output is NOT reviewed, tested, or approved until the Architect runs the full QA gate.
32374
+ Do NOT add commentary like "this looks good," "should be fine," or "ready for production."
32375
+ You wrote the code. You cannot objectively evaluate it. That is what the gates are for.
32376
+ Output only: DONE [one-line summary] / CHANGED [file] [what changed]
32377
+ `;
32258
32378
  function createCoderAgent(model, customPrompt, customAppendPrompt) {
32259
32379
  let prompt = CODER_PROMPT;
32260
32380
  if (customPrompt) {
@@ -32293,10 +32413,11 @@ CONTEXT: [codebase summary, constraints]
32293
32413
  REVIEW CHECKLIST:
32294
32414
  - Completeness: Are all requirements addressed? Missing edge cases?
32295
32415
  - Feasibility: Can each task actually be implemented as described? Are file paths real?
32296
- - Scope: Is the plan doing too much or too little? Feature creep detection.
32416
+ - Scope: Is the plan doing too much or too little? Feature creep detection?
32297
32417
  - Dependencies: Are task dependencies correct? Will ordering work?
32298
32418
  - Risk: Are high-risk changes identified? Is there a rollback path?
32299
32419
  - AI-Slop Detection: Does the plan contain vague filler ("robust", "comprehensive", "leverage") without concrete specifics?
32420
+ - Task Atomicity: Does any single task touch 2+ files or contain compound verbs ("implement X and add Y and update Z")? Flag as MAJOR \u2014 oversized tasks blow coder's context and cause downstream gate failures. Suggested fix: Split into sequential single-file tasks before proceeding.
32300
32421
 
32301
32422
  OUTPUT FORMAT:
32302
32423
  VERDICT: APPROVED | NEEDS_REVISION | REJECTED
@@ -35138,7 +35259,7 @@ async function handleResetCommand(directory, args2) {
35138
35259
  init_utils2();
35139
35260
  init_utils();
35140
35261
  import { mkdirSync as mkdirSync5, readdirSync as readdirSync4, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync6 } from "fs";
35141
- import * as path13 from "path";
35262
+ import * as path14 from "path";
35142
35263
  var SUMMARY_ID_REGEX = /^S\d+$/;
35143
35264
  function sanitizeSummaryId(id) {
35144
35265
  if (!id || id.length === 0) {
@@ -35166,9 +35287,9 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
35166
35287
  if (outputBytes > maxStoredBytes) {
35167
35288
  throw new Error(`Summary fullOutput size (${outputBytes} bytes) exceeds maximum (${maxStoredBytes} bytes)`);
35168
35289
  }
35169
- const relativePath = path13.join("summaries", `${sanitizedId}.json`);
35290
+ const relativePath = path14.join("summaries", `${sanitizedId}.json`);
35170
35291
  const summaryPath = validateSwarmPath(directory, relativePath);
35171
- const summaryDir = path13.dirname(summaryPath);
35292
+ const summaryDir = path14.dirname(summaryPath);
35172
35293
  const entry = {
35173
35294
  id: sanitizedId,
35174
35295
  summaryText,
@@ -35178,7 +35299,7 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
35178
35299
  };
35179
35300
  const entryJson = JSON.stringify(entry);
35180
35301
  mkdirSync5(summaryDir, { recursive: true });
35181
- const tempPath = path13.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
35302
+ const tempPath = path14.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
35182
35303
  try {
35183
35304
  await Bun.write(tempPath, entryJson);
35184
35305
  renameSync2(tempPath, summaryPath);
@@ -35191,7 +35312,7 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
35191
35312
  }
35192
35313
  async function loadFullOutput(directory, id) {
35193
35314
  const sanitizedId = sanitizeSummaryId(id);
35194
- const relativePath = path13.join("summaries", `${sanitizedId}.json`);
35315
+ const relativePath = path14.join("summaries", `${sanitizedId}.json`);
35195
35316
  validateSwarmPath(directory, relativePath);
35196
35317
  const content = await readSwarmFileAsync(directory, relativePath);
35197
35318
  if (content === null) {
@@ -35695,8 +35816,8 @@ async function doFlush(directory) {
35695
35816
  const activitySection = renderActivitySection();
35696
35817
  const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
35697
35818
  const flushedCount = swarmState.pendingEvents;
35698
- const path14 = `${directory}/.swarm/context.md`;
35699
- await Bun.write(path14, updated);
35819
+ const path15 = `${directory}/.swarm/context.md`;
35820
+ await Bun.write(path15, updated);
35700
35821
  swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
35701
35822
  } catch (error93) {
35702
35823
  warn("Agent activity flush failed:", error93);
@@ -36252,14 +36373,14 @@ ${originalText}`;
36252
36373
  }
36253
36374
  // src/hooks/system-enhancer.ts
36254
36375
  import * as fs11 from "fs";
36255
- import * as path15 from "path";
36376
+ import * as path16 from "path";
36256
36377
  init_manager2();
36257
36378
 
36258
36379
  // src/services/decision-drift-analyzer.ts
36259
36380
  init_utils2();
36260
36381
  init_manager2();
36261
36382
  import * as fs10 from "fs";
36262
- import * as path14 from "path";
36383
+ import * as path15 from "path";
36263
36384
  var DEFAULT_DRIFT_CONFIG = {
36264
36385
  staleThresholdPhases: 1,
36265
36386
  detectContradictions: true,
@@ -36413,7 +36534,7 @@ async function analyzeDecisionDrift(directory, config3 = {}) {
36413
36534
  currentPhase = legacyPhase;
36414
36535
  }
36415
36536
  }
36416
- const contextPath = path14.join(directory, ".swarm", "context.md");
36537
+ const contextPath = path15.join(directory, ".swarm", "context.md");
36417
36538
  let contextContent = "";
36418
36539
  try {
36419
36540
  if (fs10.existsSync(contextPath)) {
@@ -36674,18 +36795,28 @@ function createSystemEnhancerHook(config3, directory) {
36674
36795
  if (config3.secretscan?.enabled === false) {
36675
36796
  tryInject("[SWARM CONFIG] Secretscan gate is DISABLED. Skip secretscan in QA sequence.");
36676
36797
  }
36798
+ const sessionId_preflight = _input.sessionID;
36799
+ const activeAgent_preflight = swarmState.activeAgent.get(sessionId_preflight ?? "");
36800
+ const isArchitectForPreflight = !activeAgent_preflight || stripKnownSwarmPrefix(activeAgent_preflight) === "architect";
36801
+ if (isArchitectForPreflight) {
36802
+ if (config3.pipeline?.parallel_precheck !== false) {
36803
+ tryInject("[SWARM HINT] Parallel pre-check enabled: call pre_check_batch(files, directory) after lint --fix and build_check to run lint:check + secretscan + sast_scan + quality_budget concurrently (max 4 parallel). Check gates_passed before calling @reviewer.");
36804
+ } else {
36805
+ tryInject("[SWARM HINT] Parallel pre-check disabled: run lint:check \u2192 secretscan \u2192 sast_scan \u2192 quality_budget sequentially.");
36806
+ }
36807
+ }
36677
36808
  const sessionId_retro = _input.sessionID;
36678
36809
  const activeAgent_retro = swarmState.activeAgent.get(sessionId_retro ?? "");
36679
36810
  const isArchitect = !activeAgent_retro || stripKnownSwarmPrefix(activeAgent_retro) === "architect";
36680
36811
  if (isArchitect) {
36681
36812
  try {
36682
- const evidenceDir = path15.join(directory, ".swarm", "evidence");
36813
+ const evidenceDir = path16.join(directory, ".swarm", "evidence");
36683
36814
  if (fs11.existsSync(evidenceDir)) {
36684
36815
  const files = fs11.readdirSync(evidenceDir).filter((f) => f.endsWith(".json")).sort().reverse();
36685
36816
  for (const file3 of files.slice(0, 5)) {
36686
36817
  let content;
36687
36818
  try {
36688
- content = JSON.parse(fs11.readFileSync(path15.join(evidenceDir, file3), "utf-8"));
36819
+ content = JSON.parse(fs11.readFileSync(path16.join(evidenceDir, file3), "utf-8"));
36689
36820
  } catch {
36690
36821
  continue;
36691
36822
  }
@@ -36900,18 +37031,32 @@ function createSystemEnhancerHook(config3, directory) {
36900
37031
  metadata: { contentType: "prose" }
36901
37032
  });
36902
37033
  }
37034
+ const sessionId_preflight_b = _input.sessionID;
37035
+ const activeAgent_preflight_b = swarmState.activeAgent.get(sessionId_preflight_b ?? "");
37036
+ const isArchitectForPreflight_b = !activeAgent_preflight_b || stripKnownSwarmPrefix(activeAgent_preflight_b) === "architect";
37037
+ if (isArchitectForPreflight_b) {
37038
+ const hintText_b = config3.pipeline?.parallel_precheck !== false ? "[SWARM HINT] Parallel pre-check enabled: call pre_check_batch(files, directory) after lint --fix and build_check to run lint:check + secretscan + sast_scan + quality_budget concurrently (max 4 parallel). Check gates_passed before calling @reviewer." : "[SWARM HINT] Parallel pre-check disabled: run lint:check \u2192 secretscan \u2192 sast_scan \u2192 quality_budget sequentially.";
37039
+ candidates.push({
37040
+ id: `candidate-${idCounter++}`,
37041
+ kind: "phase",
37042
+ text: hintText_b,
37043
+ tokens: estimateTokens(hintText_b),
37044
+ priority: 1,
37045
+ metadata: { contentType: "prose" }
37046
+ });
37047
+ }
36903
37048
  const sessionId_retro_b = _input.sessionID;
36904
37049
  const activeAgent_retro_b = swarmState.activeAgent.get(sessionId_retro_b ?? "");
36905
37050
  const isArchitect_b = !activeAgent_retro_b || stripKnownSwarmPrefix(activeAgent_retro_b) === "architect";
36906
37051
  if (isArchitect_b) {
36907
37052
  try {
36908
- const evidenceDir_b = path15.join(directory, ".swarm", "evidence");
37053
+ const evidenceDir_b = path16.join(directory, ".swarm", "evidence");
36909
37054
  if (fs11.existsSync(evidenceDir_b)) {
36910
37055
  const files_b = fs11.readdirSync(evidenceDir_b).filter((f) => f.endsWith(".json")).sort().reverse();
36911
37056
  for (const file3 of files_b.slice(0, 5)) {
36912
37057
  let content_b;
36913
37058
  try {
36914
- content_b = JSON.parse(fs11.readFileSync(path15.join(evidenceDir_b, file3), "utf-8"));
37059
+ content_b = JSON.parse(fs11.readFileSync(path16.join(evidenceDir_b, file3), "utf-8"));
36915
37060
  } catch {
36916
37061
  continue;
36917
37062
  }
@@ -37207,7 +37352,7 @@ init_dist();
37207
37352
  // src/build/discovery.ts
37208
37353
  init_dist();
37209
37354
  import * as fs12 from "fs";
37210
- import * as path16 from "path";
37355
+ import * as path17 from "path";
37211
37356
  var ECOSYSTEMS = [
37212
37357
  {
37213
37358
  ecosystem: "node",
@@ -37325,11 +37470,11 @@ function findBuildFiles(workingDir, patterns) {
37325
37470
  return regex.test(f);
37326
37471
  });
37327
37472
  if (matches.length > 0) {
37328
- return path16.join(dir, matches[0]);
37473
+ return path17.join(dir, matches[0]);
37329
37474
  }
37330
37475
  } catch {}
37331
37476
  } else {
37332
- const filePath = path16.join(workingDir, pattern);
37477
+ const filePath = path17.join(workingDir, pattern);
37333
37478
  if (fs12.existsSync(filePath)) {
37334
37479
  return filePath;
37335
37480
  }
@@ -37338,7 +37483,7 @@ function findBuildFiles(workingDir, patterns) {
37338
37483
  return null;
37339
37484
  }
37340
37485
  function getRepoDefinedScripts(workingDir, scripts) {
37341
- const packageJsonPath = path16.join(workingDir, "package.json");
37486
+ const packageJsonPath = path17.join(workingDir, "package.json");
37342
37487
  if (!fs12.existsSync(packageJsonPath)) {
37343
37488
  return [];
37344
37489
  }
@@ -37379,7 +37524,7 @@ function findAllBuildFiles(workingDir) {
37379
37524
  const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
37380
37525
  findFilesRecursive(workingDir, regex, allBuildFiles);
37381
37526
  } else {
37382
- const filePath = path16.join(workingDir, pattern);
37527
+ const filePath = path17.join(workingDir, pattern);
37383
37528
  if (fs12.existsSync(filePath)) {
37384
37529
  allBuildFiles.add(filePath);
37385
37530
  }
@@ -37392,7 +37537,7 @@ function findFilesRecursive(dir, regex, results) {
37392
37537
  try {
37393
37538
  const entries = fs12.readdirSync(dir, { withFileTypes: true });
37394
37539
  for (const entry of entries) {
37395
- const fullPath = path16.join(dir, entry.name);
37540
+ const fullPath = path17.join(dir, entry.name);
37396
37541
  if (entry.isDirectory() && !["node_modules", ".git", "dist", "build", "target"].includes(entry.name)) {
37397
37542
  findFilesRecursive(fullPath, regex, results);
37398
37543
  } else if (entry.isFile() && regex.test(entry.name)) {
@@ -37634,7 +37779,7 @@ var build_check = tool({
37634
37779
  init_tool();
37635
37780
  import { spawnSync } from "child_process";
37636
37781
  import * as fs13 from "fs";
37637
- import * as path17 from "path";
37782
+ import * as path18 from "path";
37638
37783
  var CHECKPOINT_LOG_PATH = ".swarm/checkpoints.json";
37639
37784
  var MAX_LABEL_LENGTH = 100;
37640
37785
  var GIT_TIMEOUT_MS = 30000;
@@ -37685,7 +37830,7 @@ function validateLabel(label) {
37685
37830
  return null;
37686
37831
  }
37687
37832
  function getCheckpointLogPath() {
37688
- return path17.join(process.cwd(), CHECKPOINT_LOG_PATH);
37833
+ return path18.join(process.cwd(), CHECKPOINT_LOG_PATH);
37689
37834
  }
37690
37835
  function readCheckpointLog() {
37691
37836
  const logPath = getCheckpointLogPath();
@@ -37703,7 +37848,7 @@ function readCheckpointLog() {
37703
37848
  }
37704
37849
  function writeCheckpointLog(log2) {
37705
37850
  const logPath = getCheckpointLogPath();
37706
- const dir = path17.dirname(logPath);
37851
+ const dir = path18.dirname(logPath);
37707
37852
  if (!fs13.existsSync(dir)) {
37708
37853
  fs13.mkdirSync(dir, { recursive: true });
37709
37854
  }
@@ -37910,7 +38055,7 @@ var checkpoint = tool({
37910
38055
  // src/tools/complexity-hotspots.ts
37911
38056
  init_dist();
37912
38057
  import * as fs14 from "fs";
37913
- import * as path18 from "path";
38058
+ import * as path19 from "path";
37914
38059
  var MAX_FILE_SIZE_BYTES2 = 256 * 1024;
37915
38060
  var DEFAULT_DAYS = 90;
37916
38061
  var DEFAULT_TOP_N = 20;
@@ -38053,7 +38198,7 @@ async function analyzeHotspots(days, topN, extensions) {
38053
38198
  const extSet = new Set(extensions.map((e) => e.startsWith(".") ? e : `.${e}`));
38054
38199
  const filteredChurn = new Map;
38055
38200
  for (const [file3, count] of churnMap) {
38056
- const ext = path18.extname(file3).toLowerCase();
38201
+ const ext = path19.extname(file3).toLowerCase();
38057
38202
  if (extSet.has(ext)) {
38058
38203
  filteredChurn.set(file3, count);
38059
38204
  }
@@ -38064,7 +38209,7 @@ async function analyzeHotspots(days, topN, extensions) {
38064
38209
  for (const [file3, churnCount] of filteredChurn) {
38065
38210
  let fullPath = file3;
38066
38211
  if (!fs14.existsSync(fullPath)) {
38067
- fullPath = path18.join(cwd, file3);
38212
+ fullPath = path19.join(cwd, file3);
38068
38213
  }
38069
38214
  const complexity = getComplexityForFile(fullPath);
38070
38215
  if (complexity !== null) {
@@ -38222,14 +38367,14 @@ function validateBase(base) {
38222
38367
  function validatePaths(paths) {
38223
38368
  if (!paths)
38224
38369
  return null;
38225
- for (const path19 of paths) {
38226
- if (!path19 || path19.length === 0) {
38370
+ for (const path20 of paths) {
38371
+ if (!path20 || path20.length === 0) {
38227
38372
  return "empty path not allowed";
38228
38373
  }
38229
- if (path19.length > MAX_PATH_LENGTH) {
38374
+ if (path20.length > MAX_PATH_LENGTH) {
38230
38375
  return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
38231
38376
  }
38232
- if (SHELL_METACHARACTERS2.test(path19)) {
38377
+ if (SHELL_METACHARACTERS2.test(path20)) {
38233
38378
  return "path contains shell metacharacters";
38234
38379
  }
38235
38380
  }
@@ -38292,8 +38437,8 @@ var diff = tool({
38292
38437
  if (parts2.length >= 3) {
38293
38438
  const additions = parseInt(parts2[0]) || 0;
38294
38439
  const deletions = parseInt(parts2[1]) || 0;
38295
- const path19 = parts2[2];
38296
- files.push({ path: path19, additions, deletions });
38440
+ const path20 = parts2[2];
38441
+ files.push({ path: path20, additions, deletions });
38297
38442
  }
38298
38443
  }
38299
38444
  const contractChanges = [];
@@ -38522,7 +38667,7 @@ Use these as DOMAIN values when delegating to @sme.`;
38522
38667
  // src/tools/evidence-check.ts
38523
38668
  init_dist();
38524
38669
  import * as fs15 from "fs";
38525
- import * as path19 from "path";
38670
+ import * as path20 from "path";
38526
38671
  var MAX_FILE_SIZE_BYTES3 = 1024 * 1024;
38527
38672
  var MAX_EVIDENCE_FILES = 1000;
38528
38673
  var EVIDENCE_DIR = ".swarm/evidence";
@@ -38545,9 +38690,9 @@ function validateRequiredTypes(input) {
38545
38690
  return null;
38546
38691
  }
38547
38692
  function isPathWithinSwarm(filePath, cwd) {
38548
- const normalizedCwd = path19.resolve(cwd);
38549
- const swarmPath = path19.join(normalizedCwd, ".swarm");
38550
- const normalizedPath = path19.resolve(filePath);
38693
+ const normalizedCwd = path20.resolve(cwd);
38694
+ const swarmPath = path20.join(normalizedCwd, ".swarm");
38695
+ const normalizedPath = path20.resolve(filePath);
38551
38696
  return normalizedPath.startsWith(swarmPath);
38552
38697
  }
38553
38698
  function parseCompletedTasks(planContent) {
@@ -38578,10 +38723,10 @@ function readEvidenceFiles(evidenceDir, cwd) {
38578
38723
  if (!VALID_EVIDENCE_FILENAME_REGEX.test(filename)) {
38579
38724
  continue;
38580
38725
  }
38581
- const filePath = path19.join(evidenceDir, filename);
38726
+ const filePath = path20.join(evidenceDir, filename);
38582
38727
  try {
38583
- const resolvedPath = path19.resolve(filePath);
38584
- const evidenceDirResolved = path19.resolve(evidenceDir);
38728
+ const resolvedPath = path20.resolve(filePath);
38729
+ const evidenceDirResolved = path20.resolve(evidenceDir);
38585
38730
  if (!resolvedPath.startsWith(evidenceDirResolved)) {
38586
38731
  continue;
38587
38732
  }
@@ -38688,7 +38833,7 @@ var evidence_check = tool({
38688
38833
  return JSON.stringify(errorResult, null, 2);
38689
38834
  }
38690
38835
  const requiredTypes = requiredTypesValue.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
38691
- const planPath = path19.join(cwd, PLAN_FILE);
38836
+ const planPath = path20.join(cwd, PLAN_FILE);
38692
38837
  if (!isPathWithinSwarm(planPath, cwd)) {
38693
38838
  const errorResult = {
38694
38839
  error: "plan file path validation failed",
@@ -38720,7 +38865,7 @@ var evidence_check = tool({
38720
38865
  };
38721
38866
  return JSON.stringify(result2, null, 2);
38722
38867
  }
38723
- const evidenceDir = path19.join(cwd, EVIDENCE_DIR);
38868
+ const evidenceDir = path20.join(cwd, EVIDENCE_DIR);
38724
38869
  const evidence = readEvidenceFiles(evidenceDir, cwd);
38725
38870
  const { tasksWithFullEvidence, gaps } = analyzeGaps(completedTasks, evidence, requiredTypes);
38726
38871
  const completeness = completedTasks.length > 0 ? Math.round(tasksWithFullEvidence.length / completedTasks.length * 100) / 100 : 1;
@@ -38737,7 +38882,7 @@ var evidence_check = tool({
38737
38882
  // src/tools/file-extractor.ts
38738
38883
  init_tool();
38739
38884
  import * as fs16 from "fs";
38740
- import * as path20 from "path";
38885
+ import * as path21 from "path";
38741
38886
  var EXT_MAP = {
38742
38887
  python: ".py",
38743
38888
  py: ".py",
@@ -38815,12 +38960,12 @@ var extract_code_blocks = tool({
38815
38960
  if (prefix) {
38816
38961
  filename = `${prefix}_${filename}`;
38817
38962
  }
38818
- let filepath = path20.join(targetDir, filename);
38819
- const base = path20.basename(filepath, path20.extname(filepath));
38820
- const ext = path20.extname(filepath);
38963
+ let filepath = path21.join(targetDir, filename);
38964
+ const base = path21.basename(filepath, path21.extname(filepath));
38965
+ const ext = path21.extname(filepath);
38821
38966
  let counter = 1;
38822
38967
  while (fs16.existsSync(filepath)) {
38823
- filepath = path20.join(targetDir, `${base}_${counter}${ext}`);
38968
+ filepath = path21.join(targetDir, `${base}_${counter}${ext}`);
38824
38969
  counter++;
38825
38970
  }
38826
38971
  try {
@@ -38933,7 +39078,7 @@ var gitingest = tool({
38933
39078
  // src/tools/imports.ts
38934
39079
  init_dist();
38935
39080
  import * as fs17 from "fs";
38936
- import * as path21 from "path";
39081
+ import * as path22 from "path";
38937
39082
  var MAX_FILE_PATH_LENGTH2 = 500;
38938
39083
  var MAX_SYMBOL_LENGTH = 256;
38939
39084
  var MAX_FILE_SIZE_BYTES4 = 1024 * 1024;
@@ -38987,7 +39132,7 @@ function validateSymbolInput(symbol3) {
38987
39132
  return null;
38988
39133
  }
38989
39134
  function isBinaryFile2(filePath, buffer) {
38990
- const ext = path21.extname(filePath).toLowerCase();
39135
+ const ext = path22.extname(filePath).toLowerCase();
38991
39136
  if (ext === ".json" || ext === ".md" || ext === ".txt") {
38992
39137
  return false;
38993
39138
  }
@@ -39011,15 +39156,15 @@ function parseImports(content, targetFile, targetSymbol) {
39011
39156
  const imports = [];
39012
39157
  let resolvedTarget;
39013
39158
  try {
39014
- resolvedTarget = path21.resolve(targetFile);
39159
+ resolvedTarget = path22.resolve(targetFile);
39015
39160
  } catch {
39016
39161
  resolvedTarget = targetFile;
39017
39162
  }
39018
- const targetBasename = path21.basename(targetFile, path21.extname(targetFile));
39163
+ const targetBasename = path22.basename(targetFile, path22.extname(targetFile));
39019
39164
  const targetWithExt = targetFile;
39020
39165
  const targetWithoutExt = targetFile.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
39021
- const normalizedTargetWithExt = path21.normalize(targetWithExt).replace(/\\/g, "/");
39022
- const normalizedTargetWithoutExt = path21.normalize(targetWithoutExt).replace(/\\/g, "/");
39166
+ const normalizedTargetWithExt = path22.normalize(targetWithExt).replace(/\\/g, "/");
39167
+ const normalizedTargetWithoutExt = path22.normalize(targetWithoutExt).replace(/\\/g, "/");
39023
39168
  const importRegex = /import\s+(?:\{[\s\S]*?\}|(?:\*\s+as\s+\w+)|\w+)\s+from\s+['"`]([^'"`]+)['"`]|import\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
39024
39169
  let match;
39025
39170
  while ((match = importRegex.exec(content)) !== null) {
@@ -39043,9 +39188,9 @@ function parseImports(content, targetFile, targetSymbol) {
39043
39188
  }
39044
39189
  const normalizedModule = modulePath.replace(/^\.\//, "").replace(/^\.\.\\/, "../");
39045
39190
  let isMatch = false;
39046
- const targetDir = path21.dirname(targetFile);
39047
- const targetExt = path21.extname(targetFile);
39048
- const targetBasenameNoExt = path21.basename(targetFile, targetExt);
39191
+ const targetDir = path22.dirname(targetFile);
39192
+ const targetExt = path22.extname(targetFile);
39193
+ const targetBasenameNoExt = path22.basename(targetFile, targetExt);
39049
39194
  const moduleNormalized = modulePath.replace(/\\/g, "/").replace(/^\.\//, "");
39050
39195
  const moduleName = modulePath.split(/[/\\]/).pop() || "";
39051
39196
  const moduleNameNoExt = moduleName.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
@@ -39113,10 +39258,10 @@ function findSourceFiles2(dir, files = [], stats = { skippedDirs: [], skippedFil
39113
39258
  entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
39114
39259
  for (const entry of entries) {
39115
39260
  if (SKIP_DIRECTORIES2.has(entry)) {
39116
- stats.skippedDirs.push(path21.join(dir, entry));
39261
+ stats.skippedDirs.push(path22.join(dir, entry));
39117
39262
  continue;
39118
39263
  }
39119
- const fullPath = path21.join(dir, entry);
39264
+ const fullPath = path22.join(dir, entry);
39120
39265
  let stat;
39121
39266
  try {
39122
39267
  stat = fs17.statSync(fullPath);
@@ -39130,7 +39275,7 @@ function findSourceFiles2(dir, files = [], stats = { skippedDirs: [], skippedFil
39130
39275
  if (stat.isDirectory()) {
39131
39276
  findSourceFiles2(fullPath, files, stats);
39132
39277
  } else if (stat.isFile()) {
39133
- const ext = path21.extname(fullPath).toLowerCase();
39278
+ const ext = path22.extname(fullPath).toLowerCase();
39134
39279
  if (SUPPORTED_EXTENSIONS.includes(ext)) {
39135
39280
  files.push(fullPath);
39136
39281
  }
@@ -39186,7 +39331,7 @@ var imports = tool({
39186
39331
  return JSON.stringify(errorResult, null, 2);
39187
39332
  }
39188
39333
  try {
39189
- const targetFile = path21.resolve(file3);
39334
+ const targetFile = path22.resolve(file3);
39190
39335
  if (!fs17.existsSync(targetFile)) {
39191
39336
  const errorResult = {
39192
39337
  error: `target file not found: ${file3}`,
@@ -39208,7 +39353,7 @@ var imports = tool({
39208
39353
  };
39209
39354
  return JSON.stringify(errorResult, null, 2);
39210
39355
  }
39211
- const baseDir = path21.dirname(targetFile);
39356
+ const baseDir = path22.dirname(targetFile);
39212
39357
  const scanStats = {
39213
39358
  skippedDirs: [],
39214
39359
  skippedFiles: 0,
@@ -39298,7 +39443,7 @@ init_lint();
39298
39443
  // src/tools/pkg-audit.ts
39299
39444
  init_dist();
39300
39445
  import * as fs18 from "fs";
39301
- import * as path22 from "path";
39446
+ import * as path23 from "path";
39302
39447
  var MAX_OUTPUT_BYTES5 = 52428800;
39303
39448
  var AUDIT_TIMEOUT_MS = 120000;
39304
39449
  function isValidEcosystem(value) {
@@ -39316,13 +39461,13 @@ function validateArgs3(args2) {
39316
39461
  function detectEcosystems() {
39317
39462
  const ecosystems = [];
39318
39463
  const cwd = process.cwd();
39319
- if (fs18.existsSync(path22.join(cwd, "package.json"))) {
39464
+ if (fs18.existsSync(path23.join(cwd, "package.json"))) {
39320
39465
  ecosystems.push("npm");
39321
39466
  }
39322
- if (fs18.existsSync(path22.join(cwd, "pyproject.toml")) || fs18.existsSync(path22.join(cwd, "requirements.txt"))) {
39467
+ if (fs18.existsSync(path23.join(cwd, "pyproject.toml")) || fs18.existsSync(path23.join(cwd, "requirements.txt"))) {
39323
39468
  ecosystems.push("pip");
39324
39469
  }
39325
- if (fs18.existsSync(path22.join(cwd, "Cargo.toml"))) {
39470
+ if (fs18.existsSync(path23.join(cwd, "Cargo.toml"))) {
39326
39471
  ecosystems.push("cargo");
39327
39472
  }
39328
39473
  return ecosystems;
@@ -41230,11 +41375,11 @@ var Module2 = (() => {
41230
41375
  throw toThrow;
41231
41376
  }, "quit_");
41232
41377
  var scriptDirectory = "";
41233
- function locateFile(path23) {
41378
+ function locateFile(path24) {
41234
41379
  if (Module["locateFile"]) {
41235
- return Module["locateFile"](path23, scriptDirectory);
41380
+ return Module["locateFile"](path24, scriptDirectory);
41236
41381
  }
41237
- return scriptDirectory + path23;
41382
+ return scriptDirectory + path24;
41238
41383
  }
41239
41384
  __name(locateFile, "locateFile");
41240
41385
  var readAsync, readBinary;
@@ -43023,6 +43168,9 @@ for (const definition of languageDefinitions) {
43023
43168
  extensionMap.set(extension, definition);
43024
43169
  }
43025
43170
  }
43171
+ function getLanguageForExtension(extension) {
43172
+ return extensionMap.get(extension.toLowerCase());
43173
+ }
43026
43174
 
43027
43175
  // src/tools/placeholder-scan.ts
43028
43176
  var MAX_FILE_SIZE = 1024 * 1024;
@@ -43043,54 +43191,799 @@ var SUPPORTED_PARSER_EXTENSIONS = new Set([
43043
43191
  ".php",
43044
43192
  ".rb"
43045
43193
  ]);
43194
+ // src/tools/pre-check-batch.ts
43195
+ init_dist();
43196
+ import * as path26 from "path";
43197
+
43198
+ // node_modules/yocto-queue/index.js
43199
+ class Node2 {
43200
+ value;
43201
+ next;
43202
+ constructor(value) {
43203
+ this.value = value;
43204
+ }
43205
+ }
43206
+
43207
+ class Queue {
43208
+ #head;
43209
+ #tail;
43210
+ #size;
43211
+ constructor() {
43212
+ this.clear();
43213
+ }
43214
+ enqueue(value) {
43215
+ const node = new Node2(value);
43216
+ if (this.#head) {
43217
+ this.#tail.next = node;
43218
+ this.#tail = node;
43219
+ } else {
43220
+ this.#head = node;
43221
+ this.#tail = node;
43222
+ }
43223
+ this.#size++;
43224
+ }
43225
+ dequeue() {
43226
+ const current = this.#head;
43227
+ if (!current) {
43228
+ return;
43229
+ }
43230
+ this.#head = this.#head.next;
43231
+ this.#size--;
43232
+ if (!this.#head) {
43233
+ this.#tail = undefined;
43234
+ }
43235
+ return current.value;
43236
+ }
43237
+ peek() {
43238
+ if (!this.#head) {
43239
+ return;
43240
+ }
43241
+ return this.#head.value;
43242
+ }
43243
+ clear() {
43244
+ this.#head = undefined;
43245
+ this.#tail = undefined;
43246
+ this.#size = 0;
43247
+ }
43248
+ get size() {
43249
+ return this.#size;
43250
+ }
43251
+ *[Symbol.iterator]() {
43252
+ let current = this.#head;
43253
+ while (current) {
43254
+ yield current.value;
43255
+ current = current.next;
43256
+ }
43257
+ }
43258
+ *drain() {
43259
+ while (this.#head) {
43260
+ yield this.dequeue();
43261
+ }
43262
+ }
43263
+ }
43264
+
43265
+ // node_modules/p-limit/index.js
43266
+ function pLimit(concurrency) {
43267
+ let rejectOnClear = false;
43268
+ if (typeof concurrency === "object") {
43269
+ ({ concurrency, rejectOnClear = false } = concurrency);
43270
+ }
43271
+ validateConcurrency(concurrency);
43272
+ if (typeof rejectOnClear !== "boolean") {
43273
+ throw new TypeError("Expected `rejectOnClear` to be a boolean");
43274
+ }
43275
+ const queue = new Queue;
43276
+ let activeCount = 0;
43277
+ const resumeNext = () => {
43278
+ if (activeCount < concurrency && queue.size > 0) {
43279
+ activeCount++;
43280
+ queue.dequeue().run();
43281
+ }
43282
+ };
43283
+ const next = () => {
43284
+ activeCount--;
43285
+ resumeNext();
43286
+ };
43287
+ const run2 = async (function_, resolve10, arguments_2) => {
43288
+ const result = (async () => function_(...arguments_2))();
43289
+ resolve10(result);
43290
+ try {
43291
+ await result;
43292
+ } catch {}
43293
+ next();
43294
+ };
43295
+ const enqueue = (function_, resolve10, reject, arguments_2) => {
43296
+ const queueItem = { reject };
43297
+ new Promise((internalResolve) => {
43298
+ queueItem.run = internalResolve;
43299
+ queue.enqueue(queueItem);
43300
+ }).then(run2.bind(undefined, function_, resolve10, arguments_2));
43301
+ if (activeCount < concurrency) {
43302
+ resumeNext();
43303
+ }
43304
+ };
43305
+ const generator = (function_, ...arguments_2) => new Promise((resolve10, reject) => {
43306
+ enqueue(function_, resolve10, reject, arguments_2);
43307
+ });
43308
+ Object.defineProperties(generator, {
43309
+ activeCount: {
43310
+ get: () => activeCount
43311
+ },
43312
+ pendingCount: {
43313
+ get: () => queue.size
43314
+ },
43315
+ clearQueue: {
43316
+ value() {
43317
+ if (!rejectOnClear) {
43318
+ queue.clear();
43319
+ return;
43320
+ }
43321
+ const abortError = AbortSignal.abort().reason;
43322
+ while (queue.size > 0) {
43323
+ queue.dequeue().reject(abortError);
43324
+ }
43325
+ }
43326
+ },
43327
+ concurrency: {
43328
+ get: () => concurrency,
43329
+ set(newConcurrency) {
43330
+ validateConcurrency(newConcurrency);
43331
+ concurrency = newConcurrency;
43332
+ queueMicrotask(() => {
43333
+ while (activeCount < concurrency && queue.size > 0) {
43334
+ resumeNext();
43335
+ }
43336
+ });
43337
+ }
43338
+ },
43339
+ map: {
43340
+ async value(iterable, function_) {
43341
+ const promises = Array.from(iterable, (value, index) => this(function_, value, index));
43342
+ return Promise.all(promises);
43343
+ }
43344
+ }
43345
+ });
43346
+ return generator;
43347
+ }
43348
+ function validateConcurrency(concurrency) {
43349
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
43350
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
43351
+ }
43352
+ }
43353
+
43354
+ // src/tools/pre-check-batch.ts
43355
+ init_utils();
43356
+ init_lint();
43357
+
43046
43358
  // src/tools/quality-budget.ts
43047
43359
  init_manager();
43048
43360
 
43049
43361
  // src/quality/metrics.ts
43362
+ import * as fs19 from "fs";
43363
+ import * as path24 from "path";
43050
43364
  var MAX_FILE_SIZE_BYTES5 = 256 * 1024;
43051
- // src/tools/retrieve-summary.ts
43052
- init_dist();
43053
- var RETRIEVE_MAX_BYTES = 10 * 1024 * 1024;
43054
- var retrieve_summary = tool({
43055
- description: "Retrieve the full content of a stored tool output summary by its ID (e.g. S1, S2). Use this when a prior tool output was summarized and you need the full content.",
43056
- args: {
43057
- id: tool.schema.string().describe("The summary ID to retrieve (e.g. S1, S2, S99). Must match pattern S followed by digits.")
43058
- },
43059
- async execute(args2, context) {
43060
- const directory = context.directory;
43061
- let sanitizedId;
43062
- try {
43063
- sanitizedId = sanitizeSummaryId(args2.id);
43064
- } catch {
43065
- return "Error: invalid summary ID format. Expected format: S followed by digits (e.g. S1, S2, S99).";
43365
+ var MIN_DUPLICATION_LINES = 10;
43366
+ function estimateCyclomaticComplexity(content) {
43367
+ let processed = content.replace(/\/\*[\s\S]*?\*\//g, "");
43368
+ processed = processed.replace(/\/\/.*/g, "");
43369
+ processed = processed.replace(/#.*/g, "");
43370
+ processed = processed.replace(/'[^']*'/g, "");
43371
+ processed = processed.replace(/"[^"]*"/g, "");
43372
+ processed = processed.replace(/`[^`]*`/g, "");
43373
+ let complexity = 1;
43374
+ const decisionPatterns = [
43375
+ /\bif\b/g,
43376
+ /\belse\s+if\b/g,
43377
+ /\bfor\b/g,
43378
+ /\bwhile\b/g,
43379
+ /\bswitch\b/g,
43380
+ /\bcase\b/g,
43381
+ /\bcatch\b/g,
43382
+ /\?\./g,
43383
+ /\?\?/g,
43384
+ /&&/g,
43385
+ /\|\|/g
43386
+ ];
43387
+ for (const pattern of decisionPatterns) {
43388
+ const matches = processed.match(pattern);
43389
+ if (matches) {
43390
+ complexity += matches.length;
43391
+ }
43392
+ }
43393
+ const ternaryMatches = processed.match(/\?[^:]/g);
43394
+ if (ternaryMatches) {
43395
+ complexity += ternaryMatches.length;
43396
+ }
43397
+ return complexity;
43398
+ }
43399
+ function getComplexityForFile2(filePath) {
43400
+ try {
43401
+ const stat = fs19.statSync(filePath);
43402
+ if (stat.size > MAX_FILE_SIZE_BYTES5) {
43403
+ return null;
43404
+ }
43405
+ const content = fs19.readFileSync(filePath, "utf-8");
43406
+ return estimateCyclomaticComplexity(content);
43407
+ } catch {
43408
+ return null;
43409
+ }
43410
+ }
43411
+ async function computeComplexityDelta(files, workingDir) {
43412
+ let totalComplexity = 0;
43413
+ const analyzedFiles = [];
43414
+ for (const file3 of files) {
43415
+ const fullPath = path24.isAbsolute(file3) ? file3 : path24.join(workingDir, file3);
43416
+ if (!fs19.existsSync(fullPath)) {
43417
+ continue;
43418
+ }
43419
+ const complexity = getComplexityForFile2(fullPath);
43420
+ if (complexity !== null) {
43421
+ totalComplexity += complexity;
43422
+ analyzedFiles.push(file3);
43423
+ }
43424
+ }
43425
+ return { delta: totalComplexity, analyzedFiles };
43426
+ }
43427
+ function countExportsInFile(content) {
43428
+ let count = 0;
43429
+ const exportFunctionMatches = content.match(/export\s+function\s+\w+/g);
43430
+ if (exportFunctionMatches)
43431
+ count += exportFunctionMatches.length;
43432
+ const exportClassMatches = content.match(/export\s+class\s+\w+/g);
43433
+ if (exportClassMatches)
43434
+ count += exportClassMatches.length;
43435
+ const exportConstMatches = content.match(/export\s+const\s+\w+/g);
43436
+ if (exportConstMatches)
43437
+ count += exportConstMatches.length;
43438
+ const exportLetMatches = content.match(/export\s+let\s+\w+/g);
43439
+ if (exportLetMatches)
43440
+ count += exportLetMatches.length;
43441
+ const exportVarMatches = content.match(/export\s+var\s+\w+/g);
43442
+ if (exportVarMatches)
43443
+ count += exportVarMatches.length;
43444
+ const exportNamedMatches = content.match(/export\s*\{[^}]+\}/g);
43445
+ if (exportNamedMatches) {
43446
+ for (const match of exportNamedMatches) {
43447
+ const names = match.match(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g);
43448
+ const filteredNames = names ? names.filter((n) => n !== "export") : [];
43449
+ count += filteredNames.length;
43450
+ }
43451
+ }
43452
+ const exportDefaultMatches = content.match(/export\s+default/g);
43453
+ if (exportDefaultMatches)
43454
+ count += exportDefaultMatches.length;
43455
+ const exportTypeMatches = content.match(/export\s+(type|interface)\s+\w+/g);
43456
+ if (exportTypeMatches)
43457
+ count += exportTypeMatches.length;
43458
+ const exportEnumMatches = content.match(/export\s+enum\s+\w+/g);
43459
+ if (exportEnumMatches)
43460
+ count += exportEnumMatches.length;
43461
+ const exportEqualsMatches = content.match(/export\s+=/g);
43462
+ if (exportEqualsMatches)
43463
+ count += exportEqualsMatches.length;
43464
+ return count;
43465
+ }
43466
+ function countPythonExports(content) {
43467
+ let count = 0;
43468
+ const noComments = content.replace(/#.*/g, "");
43469
+ const noStrings = noComments.replace(/"[^"]*"/g, "").replace(/'[^']*'/g, "");
43470
+ const functionMatches = noStrings.match(/^\s*def\s+\w+/gm);
43471
+ if (functionMatches)
43472
+ count += functionMatches.length;
43473
+ const classMatches = noStrings.match(/^\s*class\s+\w+/gm);
43474
+ if (classMatches)
43475
+ count += classMatches.length;
43476
+ const originalContent = content;
43477
+ const allMatchOriginal = originalContent.match(/__all__\s*=\s*\[([^\]]+)\]/);
43478
+ if (allMatchOriginal?.[1]) {
43479
+ const names = allMatchOriginal[1].match(/['"]?(\w+)['"]?/g);
43480
+ if (names) {
43481
+ const cleanNames = names.map((n) => n.replace(/['"]/g, ""));
43482
+ count += cleanNames.length;
43483
+ }
43484
+ }
43485
+ return count;
43486
+ }
43487
+ function countRustExports(content) {
43488
+ let count = 0;
43489
+ let processed = content.replace(/\/\/.*/g, "");
43490
+ processed = processed.replace(/\/\*[\s\S]*?\*\//g, "");
43491
+ const pubFnMatches = processed.match(/pub\s+fn\s+\w+/g);
43492
+ if (pubFnMatches)
43493
+ count += pubFnMatches.length;
43494
+ const pubStructMatches = processed.match(/pub\s+struct\s+\w+/g);
43495
+ if (pubStructMatches)
43496
+ count += pubStructMatches.length;
43497
+ const pubEnumMatches = processed.match(/pub\s+enum\s+\w+/g);
43498
+ if (pubEnumMatches)
43499
+ count += pubEnumMatches.length;
43500
+ const pubUseMatches = processed.match(/pub\s+use\s+/g);
43501
+ if (pubUseMatches)
43502
+ count += pubUseMatches.length;
43503
+ const pubConstMatches = processed.match(/pub\s+const\s+\w+/g);
43504
+ if (pubConstMatches)
43505
+ count += pubConstMatches.length;
43506
+ const pubModMatches = processed.match(/pub\s+mod\s+\w+/g);
43507
+ if (pubModMatches)
43508
+ count += pubModMatches.length;
43509
+ const pubTypeMatches = processed.match(/pub\s+type\s+\w+/g);
43510
+ if (pubTypeMatches)
43511
+ count += pubTypeMatches.length;
43512
+ return count;
43513
+ }
43514
+ function countGoExports(content) {
43515
+ let count = 0;
43516
+ let processed = content.replace(/\/\/.*/gm, "");
43517
+ processed = processed.replace(/\/\*[\s\S]*?\*\//g, "");
43518
+ const exportedFuncMatches = processed.match(/^\s*func\s+[A-Z]\w*/gm);
43519
+ if (exportedFuncMatches)
43520
+ count += exportedFuncMatches.length;
43521
+ const exportedVarMatches = processed.match(/^\s*var\s+[A-Z]\w*/gm);
43522
+ if (exportedVarMatches)
43523
+ count += exportedVarMatches.length;
43524
+ const exportedTypeMatches = processed.match(/^\s*type\s+[A-Z]\w*/gm);
43525
+ if (exportedTypeMatches)
43526
+ count += exportedTypeMatches.length;
43527
+ const exportedConstMatches = processed.match(/^\s*const\s+[A-Z]\w*/gm);
43528
+ if (exportedConstMatches)
43529
+ count += exportedConstMatches.length;
43530
+ const packageMatch = processed.match(/^\s*package\s+\w+/m);
43531
+ if (packageMatch)
43532
+ count += 1;
43533
+ return count;
43534
+ }
43535
+ function getExportCountForFile(filePath) {
43536
+ try {
43537
+ const content = fs19.readFileSync(filePath, "utf-8");
43538
+ const ext = path24.extname(filePath).toLowerCase();
43539
+ switch (ext) {
43540
+ case ".ts":
43541
+ case ".tsx":
43542
+ case ".js":
43543
+ case ".jsx":
43544
+ case ".mjs":
43545
+ case ".cjs":
43546
+ return countExportsInFile(content);
43547
+ case ".py":
43548
+ return countPythonExports(content);
43549
+ case ".rs":
43550
+ return countRustExports(content);
43551
+ case ".go":
43552
+ return countGoExports(content);
43553
+ default:
43554
+ return countExportsInFile(content);
43555
+ }
43556
+ } catch {
43557
+ return 0;
43558
+ }
43559
+ }
43560
+ async function computePublicApiDelta(files, workingDir) {
43561
+ let totalExports = 0;
43562
+ const analyzedFiles = [];
43563
+ for (const file3 of files) {
43564
+ const fullPath = path24.isAbsolute(file3) ? file3 : path24.join(workingDir, file3);
43565
+ if (!fs19.existsSync(fullPath)) {
43566
+ continue;
43567
+ }
43568
+ const exports = getExportCountForFile(fullPath);
43569
+ totalExports += exports;
43570
+ analyzedFiles.push(file3);
43571
+ }
43572
+ return { delta: totalExports, analyzedFiles };
43573
+ }
43574
+ function findDuplicateLines(content, minLines) {
43575
+ const lines = content.split(`
43576
+ `).filter((line) => line.trim().length > 0);
43577
+ if (lines.length < minLines) {
43578
+ return 0;
43579
+ }
43580
+ const lineCounts = new Map;
43581
+ for (const line of lines) {
43582
+ const normalized = line.trim();
43583
+ lineCounts.set(normalized, (lineCounts.get(normalized) || 0) + 1);
43584
+ }
43585
+ let duplicateCount = 0;
43586
+ for (const [_line, count] of lineCounts) {
43587
+ if (count > 1) {
43588
+ duplicateCount += count - 1;
43589
+ }
43590
+ }
43591
+ return duplicateCount;
43592
+ }
43593
+ async function computeDuplicationRatio(files, workingDir) {
43594
+ let totalLines = 0;
43595
+ let duplicateLines = 0;
43596
+ const analyzedFiles = [];
43597
+ for (const file3 of files) {
43598
+ const fullPath = path24.isAbsolute(file3) ? file3 : path24.join(workingDir, file3);
43599
+ if (!fs19.existsSync(fullPath)) {
43600
+ continue;
43066
43601
  }
43067
- let fullOutput;
43068
43602
  try {
43069
- fullOutput = await loadFullOutput(directory, sanitizedId);
43070
- } catch {
43071
- return "Error: failed to retrieve summary.";
43603
+ const stat = fs19.statSync(fullPath);
43604
+ if (stat.size > MAX_FILE_SIZE_BYTES5) {
43605
+ continue;
43606
+ }
43607
+ const content = fs19.readFileSync(fullPath, "utf-8");
43608
+ const lines = content.split(`
43609
+ `).filter((line) => line.trim().length > 0);
43610
+ if (lines.length < MIN_DUPLICATION_LINES) {
43611
+ analyzedFiles.push(file3);
43612
+ continue;
43613
+ }
43614
+ totalLines += lines.length;
43615
+ duplicateLines += findDuplicateLines(content, MIN_DUPLICATION_LINES);
43616
+ analyzedFiles.push(file3);
43617
+ } catch {}
43618
+ }
43619
+ const ratio = totalLines > 0 ? duplicateLines / totalLines : 0;
43620
+ return { ratio, analyzedFiles };
43621
+ }
43622
+ function countCodeLines(content) {
43623
+ let processed = content.replace(/\/\*[\s\S]*?\*\//g, "");
43624
+ processed = processed.replace(/\/\/.*/g, "");
43625
+ processed = processed.replace(/#.*/g, "");
43626
+ const lines = processed.split(`
43627
+ `).filter((line) => line.trim().length > 0);
43628
+ return lines.length;
43629
+ }
43630
+ function isTestFile(filePath) {
43631
+ const basename5 = path24.basename(filePath);
43632
+ const _ext = path24.extname(filePath).toLowerCase();
43633
+ const testPatterns = [
43634
+ ".test.",
43635
+ ".spec.",
43636
+ ".tests.",
43637
+ ".test.ts",
43638
+ ".test.js",
43639
+ ".spec.ts",
43640
+ ".spec.js",
43641
+ ".test.tsx",
43642
+ ".test.jsx",
43643
+ ".spec.tsx",
43644
+ ".spec.jsx"
43645
+ ];
43646
+ for (const pattern of testPatterns) {
43647
+ if (basename5.includes(pattern)) {
43648
+ return true;
43072
43649
  }
43073
- if (fullOutput === null) {
43074
- return `Summary \`${sanitizedId}\` not found. Use a valid summary ID (e.g. S1, S2).`;
43650
+ }
43651
+ const normalizedPath = filePath.replace(/\\/g, "/");
43652
+ if (normalizedPath.includes("/tests/") || normalizedPath.includes("/test/")) {
43653
+ return true;
43654
+ }
43655
+ if (normalizedPath.includes("/__tests__/")) {
43656
+ return true;
43657
+ }
43658
+ return false;
43659
+ }
43660
+ function shouldExcludeFile(filePath, excludeGlobs) {
43661
+ const normalizedPath = filePath.replace(/\\/g, "/");
43662
+ for (const glob of excludeGlobs) {
43663
+ const pattern = glob.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
43664
+ const regex = new RegExp(pattern);
43665
+ if (regex.test(normalizedPath)) {
43666
+ return true;
43075
43667
  }
43076
- if (fullOutput.length > RETRIEVE_MAX_BYTES) {
43077
- return `Error: summary content exceeds maximum size limit (10 MB).`;
43668
+ }
43669
+ return false;
43670
+ }
43671
+ async function computeTestToCodeRatio(workingDir, enforceGlobs, excludeGlobs) {
43672
+ let testLines = 0;
43673
+ let codeLines = 0;
43674
+ const srcDir = path24.join(workingDir, "src");
43675
+ if (fs19.existsSync(srcDir)) {
43676
+ await scanDirectoryForLines(srcDir, enforceGlobs, excludeGlobs, false, (lines) => {
43677
+ codeLines += lines;
43678
+ });
43679
+ }
43680
+ const possibleSrcDirs = ["lib", "app", "source", "core"];
43681
+ for (const dir of possibleSrcDirs) {
43682
+ const dirPath = path24.join(workingDir, dir);
43683
+ if (fs19.existsSync(dirPath)) {
43684
+ await scanDirectoryForLines(dirPath, enforceGlobs, excludeGlobs, false, (lines) => {
43685
+ codeLines += lines;
43686
+ });
43078
43687
  }
43079
- return fullOutput;
43080
43688
  }
43081
- });
43082
- // src/tools/sast-scan.ts
43083
- init_manager();
43084
-
43085
- // src/sast/rules/c.ts
43086
- var cRules = [
43087
- {
43088
- id: "sast/c-buffer-overflow",
43089
- name: "Buffer overflow vulnerability",
43090
- severity: "critical",
43091
- languages: ["c", "cpp"],
43092
- description: "strcpy/strcat to fixed-size buffer without bounds checking",
43093
- remediation: "Use strncpy, snprintf, or safer alternatives that include size limits.",
43689
+ const testsDir = path24.join(workingDir, "tests");
43690
+ if (fs19.existsSync(testsDir)) {
43691
+ await scanDirectoryForLines(testsDir, ["**"], ["node_modules", "dist"], true, (lines) => {
43692
+ testLines += lines;
43693
+ });
43694
+ }
43695
+ const possibleTestDirs = ["test", "__tests__", "specs"];
43696
+ for (const dir of possibleTestDirs) {
43697
+ const dirPath = path24.join(workingDir, dir);
43698
+ if (fs19.existsSync(dirPath) && dirPath !== testsDir) {
43699
+ await scanDirectoryForLines(dirPath, ["**"], ["node_modules", "dist"], true, (lines) => {
43700
+ testLines += lines;
43701
+ });
43702
+ }
43703
+ }
43704
+ const totalLines = testLines + codeLines;
43705
+ const ratio = totalLines > 0 ? testLines / totalLines : 0;
43706
+ return { ratio, testLines, codeLines };
43707
+ }
43708
+ async function scanDirectoryForLines(dirPath, includeGlobs, excludeGlobs, isTestScan, callback) {
43709
+ try {
43710
+ const entries = fs19.readdirSync(dirPath, { withFileTypes: true });
43711
+ for (const entry of entries) {
43712
+ const fullPath = path24.join(dirPath, entry.name);
43713
+ if (entry.isDirectory()) {
43714
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === ".git") {
43715
+ continue;
43716
+ }
43717
+ await scanDirectoryForLines(fullPath, includeGlobs, excludeGlobs, isTestScan, callback);
43718
+ } else if (entry.isFile()) {
43719
+ const relativePath = fullPath.replace(`${process.cwd()}/`, "");
43720
+ const ext = path24.extname(entry.name).toLowerCase();
43721
+ const validExts = [
43722
+ ".ts",
43723
+ ".tsx",
43724
+ ".js",
43725
+ ".jsx",
43726
+ ".py",
43727
+ ".rs",
43728
+ ".go",
43729
+ ".java",
43730
+ ".cs"
43731
+ ];
43732
+ if (!validExts.includes(ext)) {
43733
+ continue;
43734
+ }
43735
+ if (isTestScan && !isTestFile(fullPath)) {
43736
+ continue;
43737
+ }
43738
+ if (!isTestScan && isTestFile(fullPath)) {
43739
+ continue;
43740
+ }
43741
+ if (shouldExcludeFile(relativePath, excludeGlobs)) {
43742
+ continue;
43743
+ }
43744
+ if (!isTestScan && includeGlobs.length > 0 && !includeGlobs.includes("**")) {
43745
+ let matches = false;
43746
+ for (const glob of includeGlobs) {
43747
+ const pattern = glob.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
43748
+ const regex = new RegExp(pattern);
43749
+ if (regex.test(relativePath)) {
43750
+ matches = true;
43751
+ break;
43752
+ }
43753
+ }
43754
+ if (!matches)
43755
+ continue;
43756
+ }
43757
+ try {
43758
+ const content = fs19.readFileSync(fullPath, "utf-8");
43759
+ const lines = countCodeLines(content);
43760
+ callback(lines);
43761
+ } catch {}
43762
+ }
43763
+ }
43764
+ } catch {}
43765
+ }
43766
+ function detectViolations(metrics, thresholds) {
43767
+ const violations = [];
43768
+ if (metrics.complexity_delta > thresholds.max_complexity_delta) {
43769
+ violations.push({
43770
+ type: "complexity",
43771
+ message: `Complexity delta (${metrics.complexity_delta}) exceeds threshold (${thresholds.max_complexity_delta})`,
43772
+ severity: metrics.complexity_delta > thresholds.max_complexity_delta * 1.5 ? "error" : "warning",
43773
+ files: metrics.files_analyzed
43774
+ });
43775
+ }
43776
+ if (metrics.public_api_delta > thresholds.max_public_api_delta) {
43777
+ violations.push({
43778
+ type: "api",
43779
+ message: `Public API delta (${metrics.public_api_delta}) exceeds threshold (${thresholds.max_public_api_delta})`,
43780
+ severity: metrics.public_api_delta > thresholds.max_public_api_delta * 1.5 ? "error" : "warning",
43781
+ files: metrics.files_analyzed
43782
+ });
43783
+ }
43784
+ if (metrics.duplication_ratio > thresholds.max_duplication_ratio) {
43785
+ violations.push({
43786
+ type: "duplication",
43787
+ message: `Duplication ratio (${(metrics.duplication_ratio * 100).toFixed(1)}%) exceeds threshold (${(thresholds.max_duplication_ratio * 100).toFixed(1)}%)`,
43788
+ severity: metrics.duplication_ratio > thresholds.max_duplication_ratio * 1.5 ? "error" : "warning",
43789
+ files: metrics.files_analyzed
43790
+ });
43791
+ }
43792
+ if (metrics.files_analyzed.length > 0 && metrics.test_to_code_ratio < thresholds.min_test_to_code_ratio) {
43793
+ violations.push({
43794
+ type: "test_ratio",
43795
+ message: `Test-to-code ratio (${(metrics.test_to_code_ratio * 100).toFixed(1)}%) below threshold (${(thresholds.min_test_to_code_ratio * 100).toFixed(1)}%)`,
43796
+ severity: metrics.test_to_code_ratio < thresholds.min_test_to_code_ratio * 0.5 ? "error" : "warning",
43797
+ files: metrics.files_analyzed
43798
+ });
43799
+ }
43800
+ return violations;
43801
+ }
43802
+ async function computeQualityMetrics(changedFiles, thresholds, workingDir) {
43803
+ const config3 = {
43804
+ enabled: thresholds.enabled ?? true,
43805
+ max_complexity_delta: thresholds.max_complexity_delta ?? 5,
43806
+ max_public_api_delta: thresholds.max_public_api_delta ?? 10,
43807
+ max_duplication_ratio: thresholds.max_duplication_ratio ?? 0.05,
43808
+ min_test_to_code_ratio: thresholds.min_test_to_code_ratio ?? 0.3,
43809
+ enforce_on_globs: thresholds.enforce_on_globs ?? ["src/**"],
43810
+ exclude_globs: thresholds.exclude_globs ?? [
43811
+ "docs/**",
43812
+ "tests/**",
43813
+ "**/*.test.*"
43814
+ ]
43815
+ };
43816
+ const filteredFiles = changedFiles.filter((file3) => {
43817
+ const normalizedPath = file3.replace(/\\/g, "/");
43818
+ for (const glob of config3.enforce_on_globs) {
43819
+ const pattern = glob.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
43820
+ const regex = new RegExp(pattern);
43821
+ if (regex.test(normalizedPath)) {
43822
+ for (const exclude of config3.exclude_globs) {
43823
+ const excludePattern = exclude.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
43824
+ const excludeRegex = new RegExp(excludePattern);
43825
+ if (excludeRegex.test(normalizedPath)) {
43826
+ return false;
43827
+ }
43828
+ }
43829
+ return true;
43830
+ }
43831
+ }
43832
+ return false;
43833
+ });
43834
+ const [complexityResult, apiResult, duplicationResult, testRatioResult] = await Promise.all([
43835
+ computeComplexityDelta(filteredFiles, workingDir),
43836
+ computePublicApiDelta(filteredFiles, workingDir),
43837
+ computeDuplicationRatio(filteredFiles, workingDir),
43838
+ computeTestToCodeRatio(workingDir, config3.enforce_on_globs, config3.exclude_globs)
43839
+ ]);
43840
+ const allAnalyzedFiles = [
43841
+ ...new Set([
43842
+ ...complexityResult.analyzedFiles,
43843
+ ...apiResult.analyzedFiles,
43844
+ ...duplicationResult.analyzedFiles
43845
+ ])
43846
+ ];
43847
+ const violations = detectViolations({
43848
+ complexity_delta: complexityResult.delta,
43849
+ public_api_delta: apiResult.delta,
43850
+ duplication_ratio: duplicationResult.ratio,
43851
+ test_to_code_ratio: testRatioResult.ratio,
43852
+ files_analyzed: allAnalyzedFiles,
43853
+ thresholds: config3,
43854
+ violations: []
43855
+ }, config3);
43856
+ return {
43857
+ complexity_delta: complexityResult.delta,
43858
+ public_api_delta: apiResult.delta,
43859
+ duplication_ratio: duplicationResult.ratio,
43860
+ test_to_code_ratio: testRatioResult.ratio,
43861
+ files_analyzed: allAnalyzedFiles,
43862
+ thresholds: config3,
43863
+ violations
43864
+ };
43865
+ }
43866
+
43867
+ // src/tools/quality-budget.ts
43868
+ function validateInput(input) {
43869
+ if (!input || typeof input !== "object") {
43870
+ return { valid: false, error: "Input must be an object" };
43871
+ }
43872
+ const typedInput = input;
43873
+ if (!Array.isArray(typedInput.changed_files)) {
43874
+ return { valid: false, error: "changed_files must be an array" };
43875
+ }
43876
+ for (const file3 of typedInput.changed_files) {
43877
+ if (typeof file3 !== "string") {
43878
+ return { valid: false, error: "changed_files must contain strings" };
43879
+ }
43880
+ }
43881
+ if (typedInput.config !== undefined) {
43882
+ if (!typedInput.config || typeof typedInput.config !== "object") {
43883
+ return { valid: false, error: "config must be an object if provided" };
43884
+ }
43885
+ }
43886
+ return { valid: true };
43887
+ }
43888
+ async function qualityBudget(input, directory) {
43889
+ const validation = validateInput(input);
43890
+ if (!validation.valid) {
43891
+ throw new Error(`Invalid input: ${validation.error}`);
43892
+ }
43893
+ const { changed_files: changedFiles, config: config3 } = input;
43894
+ const thresholds = {
43895
+ enabled: config3?.enabled ?? true,
43896
+ max_complexity_delta: config3?.max_complexity_delta ?? 5,
43897
+ max_public_api_delta: config3?.max_public_api_delta ?? 10,
43898
+ max_duplication_ratio: config3?.max_duplication_ratio ?? 0.05,
43899
+ min_test_to_code_ratio: config3?.min_test_to_code_ratio ?? 0.3,
43900
+ enforce_on_globs: config3?.enforce_on_globs ?? ["src/**"],
43901
+ exclude_globs: config3?.exclude_globs ?? [
43902
+ "docs/**",
43903
+ "tests/**",
43904
+ "**/*.test.*"
43905
+ ]
43906
+ };
43907
+ if (!thresholds.enabled) {
43908
+ return {
43909
+ verdict: "pass",
43910
+ metrics: {
43911
+ complexity_delta: 0,
43912
+ public_api_delta: 0,
43913
+ duplication_ratio: 0,
43914
+ test_to_code_ratio: 0,
43915
+ files_analyzed: [],
43916
+ thresholds,
43917
+ violations: []
43918
+ },
43919
+ violations: [],
43920
+ summary: {
43921
+ files_analyzed: 0,
43922
+ violations_count: 0,
43923
+ errors_count: 0,
43924
+ warnings_count: 0
43925
+ }
43926
+ };
43927
+ }
43928
+ const metrics = await computeQualityMetrics(changedFiles, thresholds, directory);
43929
+ const errorsCount = metrics.violations.filter((v) => v.severity === "error").length;
43930
+ const warningsCount = metrics.violations.filter((v) => v.severity === "warning").length;
43931
+ const verdict = errorsCount > 0 ? "fail" : "pass";
43932
+ await saveEvidence(directory, "quality_budget", {
43933
+ task_id: "quality_budget",
43934
+ type: "quality_budget",
43935
+ timestamp: new Date().toISOString(),
43936
+ agent: "quality_budget",
43937
+ verdict,
43938
+ summary: `Quality budget check: ${metrics.files_analyzed.length} files analyzed, ${metrics.violations.length} violation(s) found (${errorsCount} errors, ${warningsCount} warnings)`,
43939
+ metrics: {
43940
+ complexity_delta: metrics.complexity_delta,
43941
+ public_api_delta: metrics.public_api_delta,
43942
+ duplication_ratio: metrics.duplication_ratio,
43943
+ test_to_code_ratio: metrics.test_to_code_ratio
43944
+ },
43945
+ thresholds: {
43946
+ max_complexity_delta: thresholds.max_complexity_delta,
43947
+ max_public_api_delta: thresholds.max_public_api_delta,
43948
+ max_duplication_ratio: thresholds.max_duplication_ratio,
43949
+ min_test_to_code_ratio: thresholds.min_test_to_code_ratio
43950
+ },
43951
+ violations: metrics.violations.map((v) => ({
43952
+ type: v.type,
43953
+ message: v.message,
43954
+ severity: v.severity,
43955
+ files: v.files
43956
+ })),
43957
+ files_analyzed: metrics.files_analyzed
43958
+ });
43959
+ return {
43960
+ verdict,
43961
+ metrics,
43962
+ violations: metrics.violations,
43963
+ summary: {
43964
+ files_analyzed: metrics.files_analyzed.length,
43965
+ violations_count: metrics.violations.length,
43966
+ errors_count: errorsCount,
43967
+ warnings_count: warningsCount
43968
+ }
43969
+ };
43970
+ }
43971
+
43972
+ // src/tools/sast-scan.ts
43973
+ init_manager();
43974
+ import * as fs20 from "fs";
43975
+ import * as path25 from "path";
43976
+ import { extname as extname7 } from "path";
43977
+
43978
+ // src/sast/rules/c.ts
43979
+ var cRules = [
43980
+ {
43981
+ id: "sast/c-buffer-overflow",
43982
+ name: "Buffer overflow vulnerability",
43983
+ severity: "critical",
43984
+ languages: ["c", "cpp"],
43985
+ description: "strcpy/strcat to fixed-size buffer without bounds checking",
43986
+ remediation: "Use strncpy, snprintf, or safer alternatives that include size limits.",
43094
43987
  pattern: /\b(?:strcpy|strcat)\s*\(\s*[a-zA-Z_]/
43095
43988
  },
43096
43989
  {
@@ -43697,20 +44590,768 @@ var allRules = [
43697
44590
  ...cRules,
43698
44591
  ...csharpRules
43699
44592
  ];
44593
+ function getRulesForLanguage(language) {
44594
+ const normalized = language.toLowerCase();
44595
+ return allRules.filter((rule) => rule.languages.some((lang) => lang.toLowerCase() === normalized));
44596
+ }
44597
+ function findPatternMatches(content, pattern) {
44598
+ const matches = [];
44599
+ const lines = content.split(`
44600
+ `);
44601
+ for (let lineNum = 0;lineNum < lines.length; lineNum++) {
44602
+ const line = lines[lineNum];
44603
+ const workPattern = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
44604
+ let match;
44605
+ while ((match = workPattern.exec(line)) !== null) {
44606
+ matches.push({
44607
+ text: match[0],
44608
+ line: lineNum + 1,
44609
+ column: match.index + 1
44610
+ });
44611
+ if (match[0].length === 0) {
44612
+ workPattern.lastIndex++;
44613
+ }
44614
+ }
44615
+ }
44616
+ return matches;
44617
+ }
44618
+ function executeRulesSync(filePath, content, language) {
44619
+ const findings = [];
44620
+ const normalizedLang = language.toLowerCase();
44621
+ const rules = getRulesForLanguage(normalizedLang);
44622
+ for (const rule of rules) {
44623
+ if (!rule.pattern)
44624
+ continue;
44625
+ const matches = findPatternMatches(content, rule.pattern);
44626
+ for (const match of matches) {
44627
+ if (rule.validate) {
44628
+ const context = {
44629
+ filePath,
44630
+ content,
44631
+ language: normalizedLang
44632
+ };
44633
+ if (!rule.validate(match, context)) {
44634
+ continue;
44635
+ }
44636
+ }
44637
+ const lines = content.split(`
44638
+ `);
44639
+ const excerpt = lines[match.line - 1]?.trim() || "";
44640
+ findings.push({
44641
+ rule_id: rule.id,
44642
+ severity: rule.severity,
44643
+ message: rule.description,
44644
+ location: {
44645
+ file: filePath,
44646
+ line: match.line,
44647
+ column: match.column
44648
+ },
44649
+ remediation: rule.remediation,
44650
+ excerpt
44651
+ });
44652
+ }
44653
+ }
44654
+ return findings;
44655
+ }
43700
44656
 
43701
44657
  // src/sast/semgrep.ts
43702
44658
  import { execFile, execFileSync, spawn } from "child_process";
43703
44659
  import { promisify } from "util";
43704
44660
  var execFileAsync = promisify(execFile);
44661
+ var semgrepAvailableCache = null;
44662
+ var DEFAULT_RULES_DIR = ".swarm/semgrep-rules";
44663
+ var DEFAULT_TIMEOUT_MS3 = 30000;
44664
+ function isSemgrepAvailable() {
44665
+ if (semgrepAvailableCache !== null) {
44666
+ return semgrepAvailableCache;
44667
+ }
44668
+ try {
44669
+ execFileSync("semgrep", ["--version"], {
44670
+ encoding: "utf-8",
44671
+ stdio: "pipe"
44672
+ });
44673
+ semgrepAvailableCache = true;
44674
+ return true;
44675
+ } catch {
44676
+ semgrepAvailableCache = false;
44677
+ return false;
44678
+ }
44679
+ }
44680
+ function parseSemgrepResults(semgrepOutput) {
44681
+ const findings = [];
44682
+ try {
44683
+ const parsed = JSON.parse(semgrepOutput);
44684
+ const results = parsed.results || parsed;
44685
+ for (const result of results) {
44686
+ const severity = mapSemgrepSeverity(result.extra?.severity || result.severity);
44687
+ findings.push({
44688
+ rule_id: result.check_id || result.rule_id || "unknown",
44689
+ severity,
44690
+ message: result.extra?.message || result.message || "Security issue detected",
44691
+ location: {
44692
+ file: result.path || result.start?.filename || result.file || "unknown",
44693
+ line: result.start?.line || result.line || 1,
44694
+ column: result.start?.col || result.column
44695
+ },
44696
+ remediation: result.extra?.fix,
44697
+ excerpt: result.extra?.lines || result.lines || ""
44698
+ });
44699
+ }
44700
+ } catch {
44701
+ return [];
44702
+ }
44703
+ return findings;
44704
+ }
44705
+ function mapSemgrepSeverity(severity) {
44706
+ const severityLower = (severity || "").toLowerCase();
44707
+ switch (severityLower) {
44708
+ case "error":
44709
+ case "critical":
44710
+ return "critical";
44711
+ case "warning":
44712
+ case "high":
44713
+ return "high";
44714
+ case "info":
44715
+ case "low":
44716
+ return "low";
44717
+ default:
44718
+ return "medium";
44719
+ }
44720
+ }
44721
+ async function executeWithTimeout(command, args2, options) {
44722
+ return new Promise((resolve10) => {
44723
+ const child = spawn(command, args2, {
44724
+ shell: false,
44725
+ cwd: options.cwd
44726
+ });
44727
+ let stdout = "";
44728
+ let stderr = "";
44729
+ const timeout = setTimeout(() => {
44730
+ child.kill("SIGTERM");
44731
+ resolve10({
44732
+ stdout,
44733
+ stderr: "Process timed out",
44734
+ exitCode: 124
44735
+ });
44736
+ }, options.timeoutMs);
44737
+ child.stdout?.on("data", (data) => {
44738
+ stdout += data.toString();
44739
+ });
44740
+ child.stderr?.on("data", (data) => {
44741
+ stderr += data.toString();
44742
+ });
44743
+ child.on("close", (code) => {
44744
+ clearTimeout(timeout);
44745
+ resolve10({
44746
+ stdout,
44747
+ stderr,
44748
+ exitCode: code ?? 0
44749
+ });
44750
+ });
44751
+ child.on("error", (err2) => {
44752
+ clearTimeout(timeout);
44753
+ resolve10({
44754
+ stdout,
44755
+ stderr: err2.message,
44756
+ exitCode: 1
44757
+ });
44758
+ });
44759
+ });
44760
+ }
44761
+ async function runSemgrep(options) {
44762
+ const files = options.files || [];
44763
+ const rulesDir = options.rulesDir || DEFAULT_RULES_DIR;
44764
+ const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS3;
44765
+ if (files.length === 0) {
44766
+ return {
44767
+ available: isSemgrepAvailable(),
44768
+ findings: [],
44769
+ engine: "tier_a"
44770
+ };
44771
+ }
44772
+ if (!isSemgrepAvailable()) {
44773
+ return {
44774
+ available: false,
44775
+ findings: [],
44776
+ error: "Semgrep is not installed or not available on PATH",
44777
+ engine: "tier_a"
44778
+ };
44779
+ }
44780
+ const args2 = [
44781
+ "--config=./" + rulesDir,
44782
+ "--json",
44783
+ "--quiet",
44784
+ ...files
44785
+ ];
44786
+ try {
44787
+ const result = await executeWithTimeout("semgrep", args2, {
44788
+ timeoutMs,
44789
+ cwd: options.cwd
44790
+ });
44791
+ if (result.exitCode !== 0) {
44792
+ if (result.exitCode === 1 && result.stdout) {
44793
+ const findings2 = parseSemgrepResults(result.stdout);
44794
+ return {
44795
+ available: true,
44796
+ findings: findings2,
44797
+ engine: "tier_a+tier_b"
44798
+ };
44799
+ }
44800
+ return {
44801
+ available: true,
44802
+ findings: [],
44803
+ error: result.stderr || `Semgrep exited with code ${result.exitCode}`,
44804
+ engine: "tier_a"
44805
+ };
44806
+ }
44807
+ const findings = parseSemgrepResults(result.stdout);
44808
+ return {
44809
+ available: true,
44810
+ findings,
44811
+ engine: "tier_a+tier_b"
44812
+ };
44813
+ } catch (error93) {
44814
+ const errorMessage = error93 instanceof Error ? error93.message : "Unknown error running Semgrep";
44815
+ return {
44816
+ available: true,
44817
+ findings: [],
44818
+ error: errorMessage,
44819
+ engine: "tier_a"
44820
+ };
44821
+ }
44822
+ }
43705
44823
 
43706
44824
  // src/tools/sast-scan.ts
43707
44825
  init_utils();
43708
44826
  var MAX_FILE_SIZE_BYTES6 = 512 * 1024;
44827
+ var MAX_FILES_SCANNED2 = 1000;
44828
+ var MAX_FINDINGS2 = 100;
44829
+ var SEVERITY_ORDER = {
44830
+ low: 0,
44831
+ medium: 1,
44832
+ high: 2,
44833
+ critical: 3
44834
+ };
44835
+ function shouldSkipFile(filePath) {
44836
+ try {
44837
+ const stats = fs20.statSync(filePath);
44838
+ if (stats.size > MAX_FILE_SIZE_BYTES6) {
44839
+ return { skip: true, reason: "file too large" };
44840
+ }
44841
+ if (stats.size === 0) {
44842
+ return { skip: true, reason: "empty file" };
44843
+ }
44844
+ const fd = fs20.openSync(filePath, "r");
44845
+ const buffer = Buffer.alloc(8192);
44846
+ const bytesRead = fs20.readSync(fd, buffer, 0, 8192, 0);
44847
+ fs20.closeSync(fd);
44848
+ if (bytesRead > 0) {
44849
+ let nullCount = 0;
44850
+ for (let i2 = 0;i2 < bytesRead; i2++) {
44851
+ if (buffer[i2] === 0) {
44852
+ nullCount++;
44853
+ }
44854
+ }
44855
+ if (nullCount / bytesRead > 0.1) {
44856
+ return { skip: true, reason: "binary file" };
44857
+ }
44858
+ }
44859
+ return { skip: false };
44860
+ } catch {
44861
+ return { skip: true, reason: "cannot read file" };
44862
+ }
44863
+ }
44864
+ function meetsThreshold(severity, threshold) {
44865
+ const severityLevel = SEVERITY_ORDER[severity] ?? 0;
44866
+ const thresholdLevel = SEVERITY_ORDER[threshold] ?? 1;
44867
+ return severityLevel >= thresholdLevel;
44868
+ }
44869
+ function countBySeverity(findings) {
44870
+ const counts = {
44871
+ critical: 0,
44872
+ high: 0,
44873
+ medium: 0,
44874
+ low: 0
44875
+ };
44876
+ for (const finding of findings) {
44877
+ const severity = finding.severity.toLowerCase();
44878
+ if (severity in counts) {
44879
+ counts[severity]++;
44880
+ }
44881
+ }
44882
+ return counts;
44883
+ }
44884
+ function scanFileWithTierA(filePath, language) {
44885
+ try {
44886
+ const content = fs20.readFileSync(filePath, "utf-8");
44887
+ const findings = executeRulesSync(filePath, content, language);
44888
+ return findings.map((f) => ({
44889
+ rule_id: f.rule_id,
44890
+ severity: f.severity,
44891
+ message: f.message,
44892
+ location: {
44893
+ file: f.location.file,
44894
+ line: f.location.line,
44895
+ column: f.location.column
44896
+ },
44897
+ remediation: f.remediation
44898
+ }));
44899
+ } catch {
44900
+ return [];
44901
+ }
44902
+ }
44903
+ async function sastScan(input, directory, config3) {
44904
+ const { changed_files, severity_threshold = "medium" } = input;
44905
+ if (config3?.gates?.sast_scan?.enabled === false) {
44906
+ return {
44907
+ verdict: "pass",
44908
+ findings: [],
44909
+ summary: {
44910
+ engine: "tier_a",
44911
+ files_scanned: 0,
44912
+ findings_count: 0,
44913
+ findings_by_severity: {
44914
+ critical: 0,
44915
+ high: 0,
44916
+ medium: 0,
44917
+ low: 0
44918
+ }
44919
+ }
44920
+ };
44921
+ }
44922
+ const allFindings = [];
44923
+ let filesScanned = 0;
44924
+ let filesSkipped = 0;
44925
+ const semgrepAvailable = isSemgrepAvailable();
44926
+ const engine = semgrepAvailable ? "tier_a+tier_b" : "tier_a";
44927
+ const filesByLanguage = new Map;
44928
+ for (const filePath of changed_files) {
44929
+ const resolvedPath = path25.isAbsolute(filePath) ? filePath : path25.resolve(directory, filePath);
44930
+ if (!fs20.existsSync(resolvedPath)) {
44931
+ filesSkipped++;
44932
+ continue;
44933
+ }
44934
+ const skipResult = shouldSkipFile(resolvedPath);
44935
+ if (skipResult.skip) {
44936
+ filesSkipped++;
44937
+ continue;
44938
+ }
44939
+ const ext = extname7(resolvedPath).toLowerCase();
44940
+ const langDef = getLanguageForExtension(ext);
44941
+ if (!langDef) {
44942
+ filesSkipped++;
44943
+ continue;
44944
+ }
44945
+ const language = langDef.id;
44946
+ const tierAFindings = scanFileWithTierA(resolvedPath, language);
44947
+ allFindings.push(...tierAFindings);
44948
+ if (semgrepAvailable) {
44949
+ const existing = filesByLanguage.get(language) || [];
44950
+ existing.push(resolvedPath);
44951
+ filesByLanguage.set(language, existing);
44952
+ }
44953
+ filesScanned++;
44954
+ if (filesScanned >= MAX_FILES_SCANNED2) {
44955
+ warn(`SAST Scan: Reached maximum files limit (${MAX_FILES_SCANNED2}), stopping`);
44956
+ break;
44957
+ }
44958
+ }
44959
+ if (semgrepAvailable && filesByLanguage.size > 0) {
44960
+ try {
44961
+ const allFilesForSemgrep = Array.from(filesByLanguage.values()).flat();
44962
+ if (allFilesForSemgrep.length > 0) {
44963
+ const semgrepResult = await runSemgrep({
44964
+ files: allFilesForSemgrep
44965
+ });
44966
+ if (semgrepResult.findings.length > 0) {
44967
+ const semgrepFindings = semgrepResult.findings.map((f) => ({
44968
+ rule_id: f.rule_id,
44969
+ severity: f.severity,
44970
+ message: f.message,
44971
+ location: {
44972
+ file: f.location.file,
44973
+ line: f.location.line,
44974
+ column: f.location.column
44975
+ },
44976
+ remediation: f.remediation
44977
+ }));
44978
+ const existingKeys = new Set(allFindings.map((f) => `${f.rule_id}:${f.location.file}:${f.location.line}`));
44979
+ for (const finding of semgrepFindings) {
44980
+ const key = `${finding.rule_id}:${finding.location.file}:${finding.location.line}`;
44981
+ if (!existingKeys.has(key)) {
44982
+ allFindings.push(finding);
44983
+ existingKeys.add(key);
44984
+ }
44985
+ }
44986
+ }
44987
+ }
44988
+ } catch (error93) {
44989
+ warn(`SAST Scan: Semgrep failed, falling back to Tier A: ${error93}`);
44990
+ }
44991
+ }
44992
+ let finalFindings = allFindings;
44993
+ if (allFindings.length > MAX_FINDINGS2) {
44994
+ finalFindings = allFindings.slice(0, MAX_FINDINGS2);
44995
+ warn(`SAST Scan: Found ${allFindings.length} findings, limiting to ${MAX_FINDINGS2}`);
44996
+ }
44997
+ const findingsBySeverity = countBySeverity(finalFindings);
44998
+ let verdict = "pass";
44999
+ for (const finding of finalFindings) {
45000
+ if (meetsThreshold(finding.severity, severity_threshold)) {
45001
+ verdict = "fail";
45002
+ break;
45003
+ }
45004
+ }
45005
+ const summary = {
45006
+ engine,
45007
+ files_scanned: filesScanned,
45008
+ findings_count: finalFindings.length,
45009
+ findings_by_severity: findingsBySeverity
45010
+ };
45011
+ await saveEvidence(directory, "sast_scan", {
45012
+ task_id: "sast_scan",
45013
+ type: "sast",
45014
+ timestamp: new Date().toISOString(),
45015
+ agent: "sast_scan",
45016
+ verdict,
45017
+ summary: `Scanned ${filesScanned} files, found ${finalFindings.length} finding(s) using ${engine}`,
45018
+ ...summary,
45019
+ findings: finalFindings
45020
+ });
45021
+ return {
45022
+ verdict,
45023
+ findings: finalFindings,
45024
+ summary
45025
+ };
45026
+ }
45027
+
45028
+ // src/tools/pre-check-batch.ts
45029
+ init_secretscan();
45030
+ var TOOL_TIMEOUT_MS = 60000;
45031
+ var MAX_COMBINED_BYTES = 500000;
45032
+ var MAX_CONCURRENT = 4;
45033
+ var MAX_FILES = 100;
45034
+ function validatePath(inputPath, baseDir) {
45035
+ if (!inputPath || inputPath.length === 0) {
45036
+ return "path is required";
45037
+ }
45038
+ const resolved = path26.resolve(baseDir, inputPath);
45039
+ const baseResolved = path26.resolve(baseDir);
45040
+ const relative2 = path26.relative(baseResolved, resolved);
45041
+ if (relative2.startsWith("..") || path26.isAbsolute(relative2)) {
45042
+ return "path traversal detected";
45043
+ }
45044
+ return null;
45045
+ }
45046
+ function validateDirectory(dir) {
45047
+ if (!dir || dir.length === 0) {
45048
+ return "directory is required";
45049
+ }
45050
+ if (dir.length > 500) {
45051
+ return "directory path too long";
45052
+ }
45053
+ const traversalCheck = validatePath(dir, process.cwd());
45054
+ if (traversalCheck) {
45055
+ return traversalCheck;
45056
+ }
45057
+ return null;
45058
+ }
45059
+ async function runWithTimeout(promise3, timeoutMs) {
45060
+ const timeoutPromise = new Promise((_, reject) => {
45061
+ setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
45062
+ });
45063
+ return Promise.race([promise3, timeoutPromise]);
45064
+ }
45065
+ async function runLintWrapped(_config) {
45066
+ const start2 = process.hrtime.bigint();
45067
+ try {
45068
+ const linter = await detectAvailableLinter();
45069
+ if (!linter) {
45070
+ return {
45071
+ ran: false,
45072
+ error: "No linter found (biome or eslint)",
45073
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45074
+ };
45075
+ }
45076
+ const result = await runWithTimeout(runLint(linter, "check"), TOOL_TIMEOUT_MS);
45077
+ return {
45078
+ ran: true,
45079
+ result,
45080
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45081
+ };
45082
+ } catch (error93) {
45083
+ return {
45084
+ ran: true,
45085
+ error: error93 instanceof Error ? error93.message : "Unknown error",
45086
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45087
+ };
45088
+ }
45089
+ }
45090
+ async function runSecretscanWrapped(directory, _config) {
45091
+ const start2 = process.hrtime.bigint();
45092
+ try {
45093
+ const result = await runWithTimeout(runSecretscan(directory), TOOL_TIMEOUT_MS);
45094
+ return {
45095
+ ran: true,
45096
+ result,
45097
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45098
+ };
45099
+ } catch (error93) {
45100
+ return {
45101
+ ran: true,
45102
+ error: error93 instanceof Error ? error93.message : "Unknown error",
45103
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45104
+ };
45105
+ }
45106
+ }
45107
+ async function runSastScanWrapped(changedFiles, directory, severityThreshold, config3) {
45108
+ const start2 = process.hrtime.bigint();
45109
+ try {
45110
+ const result = await runWithTimeout(sastScan({ changed_files: changedFiles, severity_threshold: severityThreshold }, directory, config3), TOOL_TIMEOUT_MS);
45111
+ return {
45112
+ ran: true,
45113
+ result,
45114
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45115
+ };
45116
+ } catch (error93) {
45117
+ return {
45118
+ ran: true,
45119
+ error: error93 instanceof Error ? error93.message : "Unknown error",
45120
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45121
+ };
45122
+ }
45123
+ }
45124
+ async function runQualityBudgetWrapped(changedFiles, directory, _config) {
45125
+ const start2 = process.hrtime.bigint();
45126
+ try {
45127
+ const result = await runWithTimeout(qualityBudget({ changed_files: changedFiles }, directory), TOOL_TIMEOUT_MS);
45128
+ return {
45129
+ ran: true,
45130
+ result,
45131
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45132
+ };
45133
+ } catch (error93) {
45134
+ return {
45135
+ ran: true,
45136
+ error: error93 instanceof Error ? error93.message : "Unknown error",
45137
+ duration_ms: Number(process.hrtime.bigint() - start2) / 1e6
45138
+ };
45139
+ }
45140
+ }
45141
+ async function runPreCheckBatch(input) {
45142
+ const { files, directory, sast_threshold = "medium", config: config3 } = input;
45143
+ const dirError = validateDirectory(directory);
45144
+ if (dirError) {
45145
+ warn(`pre_check_batch: Invalid directory: ${dirError}`);
45146
+ return {
45147
+ gates_passed: false,
45148
+ lint: { ran: false, error: dirError, duration_ms: 0 },
45149
+ secretscan: { ran: false, error: dirError, duration_ms: 0 },
45150
+ sast_scan: { ran: false, error: dirError, duration_ms: 0 },
45151
+ quality_budget: { ran: false, error: dirError, duration_ms: 0 },
45152
+ total_duration_ms: 0
45153
+ };
45154
+ }
45155
+ let changedFiles = [];
45156
+ if (files && files.length > 0) {
45157
+ for (const file3 of files) {
45158
+ const fileError = validatePath(file3, directory);
45159
+ if (fileError) {
45160
+ warn(`pre_check_batch: Invalid file path: ${file3}`);
45161
+ continue;
45162
+ }
45163
+ changedFiles.push(path26.resolve(directory, file3));
45164
+ }
45165
+ } else {
45166
+ changedFiles = [];
45167
+ }
45168
+ if (changedFiles.length === 0 && !files) {
45169
+ warn("pre_check_batch: No files provided, skipping SAST and quality_budget");
45170
+ return {
45171
+ gates_passed: true,
45172
+ lint: { ran: false, error: "No files provided", duration_ms: 0 },
45173
+ secretscan: { ran: false, error: "No files provided", duration_ms: 0 },
45174
+ sast_scan: { ran: false, error: "No files provided", duration_ms: 0 },
45175
+ quality_budget: {
45176
+ ran: false,
45177
+ error: "No files provided",
45178
+ duration_ms: 0
45179
+ },
45180
+ total_duration_ms: 0
45181
+ };
45182
+ }
45183
+ if (changedFiles.length > MAX_FILES) {
45184
+ throw new Error(`Input exceeds maximum file count: ${changedFiles.length} > ${MAX_FILES}`);
45185
+ }
45186
+ const limit = pLimit(MAX_CONCURRENT);
45187
+ const [lintResult, secretscanResult, sastScanResult, qualityBudgetResult] = await Promise.all([
45188
+ limit(() => runLintWrapped(config3)),
45189
+ limit(() => runSecretscanWrapped(directory, config3)),
45190
+ limit(() => runSastScanWrapped(changedFiles, directory, sast_threshold, config3)),
45191
+ limit(() => runQualityBudgetWrapped(changedFiles, directory, config3))
45192
+ ]);
45193
+ const totalDuration = lintResult.duration_ms + secretscanResult.duration_ms + sastScanResult.duration_ms + qualityBudgetResult.duration_ms;
45194
+ let gatesPassed = true;
45195
+ if (lintResult.ran && lintResult.result) {
45196
+ const lintRes = lintResult.result;
45197
+ if ("success" in lintRes && lintRes.success === false) {
45198
+ gatesPassed = false;
45199
+ warn("pre_check_batch: Lint has errors - GATE FAILED");
45200
+ }
45201
+ } else if (lintResult.error) {
45202
+ gatesPassed = false;
45203
+ warn(`pre_check_batch: Lint error - GATE FAILED: ${lintResult.error}`);
45204
+ }
45205
+ if (secretscanResult.ran && secretscanResult.result) {
45206
+ const scanResult = secretscanResult.result;
45207
+ if ("findings" in scanResult && scanResult.findings.length > 0) {
45208
+ gatesPassed = false;
45209
+ warn("pre_check_batch: Secretscan found secrets - GATE FAILED");
45210
+ }
45211
+ } else if (secretscanResult.error) {
45212
+ gatesPassed = false;
45213
+ warn(`pre_check_batch: Secretscan error - GATE FAILED: ${secretscanResult.error}`);
45214
+ }
45215
+ if (sastScanResult.ran && sastScanResult.result) {
45216
+ if (sastScanResult.result.verdict === "fail") {
45217
+ gatesPassed = false;
45218
+ warn("pre_check_batch: SAST scan found vulnerabilities - GATE FAILED");
45219
+ }
45220
+ } else if (sastScanResult.error) {
45221
+ gatesPassed = false;
45222
+ warn(`pre_check_batch: SAST scan error - GATE FAILED: ${sastScanResult.error}`);
45223
+ }
45224
+ const result = {
45225
+ gates_passed: gatesPassed,
45226
+ lint: lintResult,
45227
+ secretscan: secretscanResult,
45228
+ sast_scan: sastScanResult,
45229
+ quality_budget: qualityBudgetResult,
45230
+ total_duration_ms: Math.round(totalDuration)
45231
+ };
45232
+ const outputSize = JSON.stringify(result).length;
45233
+ if (outputSize > MAX_COMBINED_BYTES) {
45234
+ warn(`pre_check_batch: Large output (${outputSize} bytes)`);
45235
+ }
45236
+ return result;
45237
+ }
45238
+ var pre_check_batch = tool({
45239
+ description: "Run multiple verification tools in parallel: lint, secretscan, SAST scan, and quality budget. Returns unified result with gates_passed status. Security tools (secretscan, sast_scan) are HARD GATES - failures block merging.",
45240
+ args: {
45241
+ files: tool.schema.array(tool.schema.string()).optional().describe("Specific files to check (optional, scans directory if not provided)"),
45242
+ directory: tool.schema.string().describe('Directory to run checks in (e.g., "." or "./src")'),
45243
+ sast_threshold: tool.schema.enum(["low", "medium", "high", "critical"]).optional().describe("Minimum severity for SAST findings to cause failure (default: medium)")
45244
+ },
45245
+ async execute(args2) {
45246
+ if (!args2 || typeof args2 !== "object") {
45247
+ const errorResult = {
45248
+ gates_passed: false,
45249
+ lint: { ran: false, error: "Invalid arguments", duration_ms: 0 },
45250
+ secretscan: { ran: false, error: "Invalid arguments", duration_ms: 0 },
45251
+ sast_scan: { ran: false, error: "Invalid arguments", duration_ms: 0 },
45252
+ quality_budget: {
45253
+ ran: false,
45254
+ error: "Invalid arguments",
45255
+ duration_ms: 0
45256
+ },
45257
+ total_duration_ms: 0
45258
+ };
45259
+ return JSON.stringify(errorResult, null, 2);
45260
+ }
45261
+ const typedArgs = args2;
45262
+ if (!typedArgs.directory) {
45263
+ const errorResult = {
45264
+ gates_passed: false,
45265
+ lint: { ran: false, error: "directory is required", duration_ms: 0 },
45266
+ secretscan: {
45267
+ ran: false,
45268
+ error: "directory is required",
45269
+ duration_ms: 0
45270
+ },
45271
+ sast_scan: {
45272
+ ran: false,
45273
+ error: "directory is required",
45274
+ duration_ms: 0
45275
+ },
45276
+ quality_budget: {
45277
+ ran: false,
45278
+ error: "directory is required",
45279
+ duration_ms: 0
45280
+ },
45281
+ total_duration_ms: 0
45282
+ };
45283
+ return JSON.stringify(errorResult, null, 2);
45284
+ }
45285
+ const dirError = validateDirectory(typedArgs.directory);
45286
+ if (dirError) {
45287
+ const errorResult = {
45288
+ gates_passed: false,
45289
+ lint: { ran: false, error: dirError, duration_ms: 0 },
45290
+ secretscan: { ran: false, error: dirError, duration_ms: 0 },
45291
+ sast_scan: { ran: false, error: dirError, duration_ms: 0 },
45292
+ quality_budget: { ran: false, error: dirError, duration_ms: 0 },
45293
+ total_duration_ms: 0
45294
+ };
45295
+ return JSON.stringify(errorResult, null, 2);
45296
+ }
45297
+ try {
45298
+ const result = await runPreCheckBatch({
45299
+ files: typedArgs.files,
45300
+ directory: typedArgs.directory,
45301
+ sast_threshold: typedArgs.sast_threshold,
45302
+ config: typedArgs.config
45303
+ });
45304
+ return JSON.stringify(result, null, 2);
45305
+ } catch (error93) {
45306
+ const errorMessage = error93 instanceof Error ? error93.message : "Unknown error";
45307
+ const errorResult = {
45308
+ gates_passed: false,
45309
+ lint: { ran: false, error: errorMessage, duration_ms: 0 },
45310
+ secretscan: { ran: false, error: errorMessage, duration_ms: 0 },
45311
+ sast_scan: { ran: false, error: errorMessage, duration_ms: 0 },
45312
+ quality_budget: { ran: false, error: errorMessage, duration_ms: 0 },
45313
+ total_duration_ms: 0
45314
+ };
45315
+ return JSON.stringify(errorResult, null, 2);
45316
+ }
45317
+ }
45318
+ });
45319
+ // src/tools/retrieve-summary.ts
45320
+ init_dist();
45321
+ var RETRIEVE_MAX_BYTES = 10 * 1024 * 1024;
45322
+ var retrieve_summary = tool({
45323
+ description: "Retrieve the full content of a stored tool output summary by its ID (e.g. S1, S2). Use this when a prior tool output was summarized and you need the full content.",
45324
+ args: {
45325
+ id: tool.schema.string().describe("The summary ID to retrieve (e.g. S1, S2, S99). Must match pattern S followed by digits.")
45326
+ },
45327
+ async execute(args2, context) {
45328
+ const directory = context.directory;
45329
+ let sanitizedId;
45330
+ try {
45331
+ sanitizedId = sanitizeSummaryId(args2.id);
45332
+ } catch {
45333
+ return "Error: invalid summary ID format. Expected format: S followed by digits (e.g. S1, S2, S99).";
45334
+ }
45335
+ let fullOutput;
45336
+ try {
45337
+ fullOutput = await loadFullOutput(directory, sanitizedId);
45338
+ } catch {
45339
+ return "Error: failed to retrieve summary.";
45340
+ }
45341
+ if (fullOutput === null) {
45342
+ return `Summary \`${sanitizedId}\` not found. Use a valid summary ID (e.g. S1, S2).`;
45343
+ }
45344
+ if (fullOutput.length > RETRIEVE_MAX_BYTES) {
45345
+ return `Error: summary content exceeds maximum size limit (10 MB).`;
45346
+ }
45347
+ return fullOutput;
45348
+ }
45349
+ });
43709
45350
  // src/tools/sbom-generate.ts
43710
45351
  init_dist();
43711
45352
  init_manager();
43712
- import * as fs19 from "fs";
43713
- import * as path23 from "path";
45353
+ import * as fs21 from "fs";
45354
+ import * as path27 from "path";
43714
45355
 
43715
45356
  // src/sbom/detectors/dart.ts
43716
45357
  function parsePubspecLock(content) {
@@ -44555,9 +46196,9 @@ function findManifestFiles(rootDir) {
44555
46196
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
44556
46197
  function searchDir(dir) {
44557
46198
  try {
44558
- const entries = fs19.readdirSync(dir, { withFileTypes: true });
46199
+ const entries = fs21.readdirSync(dir, { withFileTypes: true });
44559
46200
  for (const entry of entries) {
44560
- const fullPath = path23.join(dir, entry.name);
46201
+ const fullPath = path27.join(dir, entry.name);
44561
46202
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
44562
46203
  continue;
44563
46204
  }
@@ -44567,7 +46208,7 @@ function findManifestFiles(rootDir) {
44567
46208
  for (const pattern of patterns) {
44568
46209
  const regex = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
44569
46210
  if (new RegExp(regex, "i").test(entry.name)) {
44570
- manifestFiles.push(path23.relative(cwd, fullPath));
46211
+ manifestFiles.push(path27.relative(cwd, fullPath));
44571
46212
  break;
44572
46213
  }
44573
46214
  }
@@ -44584,14 +46225,14 @@ function findManifestFilesInDirs(directories, workingDir) {
44584
46225
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
44585
46226
  for (const dir of directories) {
44586
46227
  try {
44587
- const entries = fs19.readdirSync(dir, { withFileTypes: true });
46228
+ const entries = fs21.readdirSync(dir, { withFileTypes: true });
44588
46229
  for (const entry of entries) {
44589
- const fullPath = path23.join(dir, entry.name);
46230
+ const fullPath = path27.join(dir, entry.name);
44590
46231
  if (entry.isFile()) {
44591
46232
  for (const pattern of patterns) {
44592
46233
  const regex = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
44593
46234
  if (new RegExp(regex, "i").test(entry.name)) {
44594
- found.push(path23.relative(workingDir, fullPath));
46235
+ found.push(path27.relative(workingDir, fullPath));
44595
46236
  break;
44596
46237
  }
44597
46238
  }
@@ -44604,11 +46245,11 @@ function findManifestFilesInDirs(directories, workingDir) {
44604
46245
  function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
44605
46246
  const dirs = new Set;
44606
46247
  for (const file3 of changedFiles) {
44607
- let currentDir = path23.dirname(file3);
46248
+ let currentDir = path27.dirname(file3);
44608
46249
  while (true) {
44609
- if (currentDir && currentDir !== "." && currentDir !== path23.sep) {
44610
- dirs.add(path23.join(workingDir, currentDir));
44611
- const parent = path23.dirname(currentDir);
46250
+ if (currentDir && currentDir !== "." && currentDir !== path27.sep) {
46251
+ dirs.add(path27.join(workingDir, currentDir));
46252
+ const parent = path27.dirname(currentDir);
44612
46253
  if (parent === currentDir)
44613
46254
  break;
44614
46255
  currentDir = parent;
@@ -44622,7 +46263,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
44622
46263
  }
44623
46264
  function ensureOutputDir(outputDir) {
44624
46265
  try {
44625
- fs19.mkdirSync(outputDir, { recursive: true });
46266
+ fs21.mkdirSync(outputDir, { recursive: true });
44626
46267
  } catch (error93) {
44627
46268
  if (!error93 || error93.code !== "EEXIST") {
44628
46269
  throw error93;
@@ -44715,11 +46356,11 @@ var sbom_generate = tool({
44715
46356
  const processedFiles = [];
44716
46357
  for (const manifestFile of manifestFiles) {
44717
46358
  try {
44718
- const fullPath = path23.isAbsolute(manifestFile) ? manifestFile : path23.join(workingDir, manifestFile);
44719
- if (!fs19.existsSync(fullPath)) {
46359
+ const fullPath = path27.isAbsolute(manifestFile) ? manifestFile : path27.join(workingDir, manifestFile);
46360
+ if (!fs21.existsSync(fullPath)) {
44720
46361
  continue;
44721
46362
  }
44722
- const content = fs19.readFileSync(fullPath, "utf-8");
46363
+ const content = fs21.readFileSync(fullPath, "utf-8");
44723
46364
  const components = detectComponents(manifestFile, content);
44724
46365
  processedFiles.push(manifestFile);
44725
46366
  if (components.length > 0) {
@@ -44732,8 +46373,8 @@ var sbom_generate = tool({
44732
46373
  const bom = generateCycloneDX(allComponents);
44733
46374
  const bomJson = serializeCycloneDX(bom);
44734
46375
  const filename = generateSbomFilename();
44735
- const outputPath = path23.join(outputDir, filename);
44736
- fs19.writeFileSync(outputPath, bomJson, "utf-8");
46376
+ const outputPath = path27.join(outputDir, filename);
46377
+ fs21.writeFileSync(outputPath, bomJson, "utf-8");
44737
46378
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
44738
46379
  try {
44739
46380
  const timestamp = new Date().toISOString();
@@ -44774,8 +46415,8 @@ var sbom_generate = tool({
44774
46415
  });
44775
46416
  // src/tools/schema-drift.ts
44776
46417
  init_dist();
44777
- import * as fs20 from "fs";
44778
- import * as path24 from "path";
46418
+ import * as fs22 from "fs";
46419
+ import * as path28 from "path";
44779
46420
  var SPEC_CANDIDATES = [
44780
46421
  "openapi.json",
44781
46422
  "openapi.yaml",
@@ -44807,28 +46448,28 @@ function normalizePath(p) {
44807
46448
  }
44808
46449
  function discoverSpecFile(cwd, specFileArg) {
44809
46450
  if (specFileArg) {
44810
- const resolvedPath = path24.resolve(cwd, specFileArg);
44811
- const normalizedCwd = cwd.endsWith(path24.sep) ? cwd : cwd + path24.sep;
46451
+ const resolvedPath = path28.resolve(cwd, specFileArg);
46452
+ const normalizedCwd = cwd.endsWith(path28.sep) ? cwd : cwd + path28.sep;
44812
46453
  if (!resolvedPath.startsWith(normalizedCwd) && resolvedPath !== cwd) {
44813
46454
  throw new Error("Invalid spec_file: path traversal detected");
44814
46455
  }
44815
- const ext = path24.extname(resolvedPath).toLowerCase();
46456
+ const ext = path28.extname(resolvedPath).toLowerCase();
44816
46457
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
44817
46458
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
44818
46459
  }
44819
- const stats = fs20.statSync(resolvedPath);
46460
+ const stats = fs22.statSync(resolvedPath);
44820
46461
  if (stats.size > MAX_SPEC_SIZE) {
44821
46462
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
44822
46463
  }
44823
- if (!fs20.existsSync(resolvedPath)) {
46464
+ if (!fs22.existsSync(resolvedPath)) {
44824
46465
  throw new Error(`Spec file not found: ${resolvedPath}`);
44825
46466
  }
44826
46467
  return resolvedPath;
44827
46468
  }
44828
46469
  for (const candidate of SPEC_CANDIDATES) {
44829
- const candidatePath = path24.resolve(cwd, candidate);
44830
- if (fs20.existsSync(candidatePath)) {
44831
- const stats = fs20.statSync(candidatePath);
46470
+ const candidatePath = path28.resolve(cwd, candidate);
46471
+ if (fs22.existsSync(candidatePath)) {
46472
+ const stats = fs22.statSync(candidatePath);
44832
46473
  if (stats.size <= MAX_SPEC_SIZE) {
44833
46474
  return candidatePath;
44834
46475
  }
@@ -44837,8 +46478,8 @@ function discoverSpecFile(cwd, specFileArg) {
44837
46478
  return null;
44838
46479
  }
44839
46480
  function parseSpec(specFile) {
44840
- const content = fs20.readFileSync(specFile, "utf-8");
44841
- const ext = path24.extname(specFile).toLowerCase();
46481
+ const content = fs22.readFileSync(specFile, "utf-8");
46482
+ const ext = path28.extname(specFile).toLowerCase();
44842
46483
  if (ext === ".json") {
44843
46484
  return parseJsonSpec(content);
44844
46485
  }
@@ -44904,12 +46545,12 @@ function extractRoutes(cwd) {
44904
46545
  function walkDir(dir) {
44905
46546
  let entries;
44906
46547
  try {
44907
- entries = fs20.readdirSync(dir, { withFileTypes: true });
46548
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
44908
46549
  } catch {
44909
46550
  return;
44910
46551
  }
44911
46552
  for (const entry of entries) {
44912
- const fullPath = path24.join(dir, entry.name);
46553
+ const fullPath = path28.join(dir, entry.name);
44913
46554
  if (entry.isSymbolicLink()) {
44914
46555
  continue;
44915
46556
  }
@@ -44919,7 +46560,7 @@ function extractRoutes(cwd) {
44919
46560
  }
44920
46561
  walkDir(fullPath);
44921
46562
  } else if (entry.isFile()) {
44922
- const ext = path24.extname(entry.name).toLowerCase();
46563
+ const ext = path28.extname(entry.name).toLowerCase();
44923
46564
  const baseName = entry.name.toLowerCase();
44924
46565
  if (![".ts", ".js", ".mjs"].includes(ext)) {
44925
46566
  continue;
@@ -44937,7 +46578,7 @@ function extractRoutes(cwd) {
44937
46578
  }
44938
46579
  function extractRoutesFromFile(filePath) {
44939
46580
  const routes = [];
44940
- const content = fs20.readFileSync(filePath, "utf-8");
46581
+ const content = fs22.readFileSync(filePath, "utf-8");
44941
46582
  const lines = content.split(/\r?\n/);
44942
46583
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
44943
46584
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -45084,8 +46725,8 @@ init_secretscan();
45084
46725
 
45085
46726
  // src/tools/symbols.ts
45086
46727
  init_tool();
45087
- import * as fs21 from "fs";
45088
- import * as path25 from "path";
46728
+ import * as fs23 from "fs";
46729
+ import * as path29 from "path";
45089
46730
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
45090
46731
  var WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
45091
46732
  function containsControlCharacters(str) {
@@ -45114,11 +46755,11 @@ function containsWindowsAttacks(str) {
45114
46755
  }
45115
46756
  function isPathInWorkspace(filePath, workspace) {
45116
46757
  try {
45117
- const resolvedPath = path25.resolve(workspace, filePath);
45118
- const realWorkspace = fs21.realpathSync(workspace);
45119
- const realResolvedPath = fs21.realpathSync(resolvedPath);
45120
- const relativePath = path25.relative(realWorkspace, realResolvedPath);
45121
- if (relativePath.startsWith("..") || path25.isAbsolute(relativePath)) {
46758
+ const resolvedPath = path29.resolve(workspace, filePath);
46759
+ const realWorkspace = fs23.realpathSync(workspace);
46760
+ const realResolvedPath = fs23.realpathSync(resolvedPath);
46761
+ const relativePath = path29.relative(realWorkspace, realResolvedPath);
46762
+ if (relativePath.startsWith("..") || path29.isAbsolute(relativePath)) {
45122
46763
  return false;
45123
46764
  }
45124
46765
  return true;
@@ -45130,17 +46771,17 @@ function validatePathForRead(filePath, workspace) {
45130
46771
  return isPathInWorkspace(filePath, workspace);
45131
46772
  }
45132
46773
  function extractTSSymbols(filePath, cwd) {
45133
- const fullPath = path25.join(cwd, filePath);
46774
+ const fullPath = path29.join(cwd, filePath);
45134
46775
  if (!validatePathForRead(fullPath, cwd)) {
45135
46776
  return [];
45136
46777
  }
45137
46778
  let content;
45138
46779
  try {
45139
- const stats = fs21.statSync(fullPath);
46780
+ const stats = fs23.statSync(fullPath);
45140
46781
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
45141
46782
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
45142
46783
  }
45143
- content = fs21.readFileSync(fullPath, "utf-8");
46784
+ content = fs23.readFileSync(fullPath, "utf-8");
45144
46785
  } catch {
45145
46786
  return [];
45146
46787
  }
@@ -45282,17 +46923,17 @@ function extractTSSymbols(filePath, cwd) {
45282
46923
  });
45283
46924
  }
45284
46925
  function extractPythonSymbols(filePath, cwd) {
45285
- const fullPath = path25.join(cwd, filePath);
46926
+ const fullPath = path29.join(cwd, filePath);
45286
46927
  if (!validatePathForRead(fullPath, cwd)) {
45287
46928
  return [];
45288
46929
  }
45289
46930
  let content;
45290
46931
  try {
45291
- const stats = fs21.statSync(fullPath);
46932
+ const stats = fs23.statSync(fullPath);
45292
46933
  if (stats.size > MAX_FILE_SIZE_BYTES7) {
45293
46934
  throw new Error(`File too large: ${stats.size} bytes (max: ${MAX_FILE_SIZE_BYTES7})`);
45294
46935
  }
45295
- content = fs21.readFileSync(fullPath, "utf-8");
46936
+ content = fs23.readFileSync(fullPath, "utf-8");
45296
46937
  } catch {
45297
46938
  return [];
45298
46939
  }
@@ -45364,7 +47005,7 @@ var symbols = tool({
45364
47005
  }, null, 2);
45365
47006
  }
45366
47007
  const cwd = process.cwd();
45367
- const ext = path25.extname(file3);
47008
+ const ext = path29.extname(file3);
45368
47009
  if (containsControlCharacters(file3)) {
45369
47010
  return JSON.stringify({
45370
47011
  file: file3,
@@ -45432,8 +47073,8 @@ init_test_runner();
45432
47073
 
45433
47074
  // src/tools/todo-extract.ts
45434
47075
  init_dist();
45435
- import * as fs22 from "fs";
45436
- import * as path26 from "path";
47076
+ import * as fs24 from "fs";
47077
+ import * as path30 from "path";
45437
47078
  var MAX_TEXT_LENGTH = 200;
45438
47079
  var MAX_FILE_SIZE_BYTES8 = 1024 * 1024;
45439
47080
  var SUPPORTED_EXTENSIONS2 = new Set([
@@ -45504,9 +47145,9 @@ function validatePathsInput(paths, cwd) {
45504
47145
  return { error: "paths contains path traversal", resolvedPath: null };
45505
47146
  }
45506
47147
  try {
45507
- const resolvedPath = path26.resolve(paths);
45508
- const normalizedCwd = path26.resolve(cwd);
45509
- const normalizedResolved = path26.resolve(resolvedPath);
47148
+ const resolvedPath = path30.resolve(paths);
47149
+ const normalizedCwd = path30.resolve(cwd);
47150
+ const normalizedResolved = path30.resolve(resolvedPath);
45510
47151
  if (!normalizedResolved.startsWith(normalizedCwd)) {
45511
47152
  return {
45512
47153
  error: "paths must be within the current working directory",
@@ -45522,13 +47163,13 @@ function validatePathsInput(paths, cwd) {
45522
47163
  }
45523
47164
  }
45524
47165
  function isSupportedExtension(filePath) {
45525
- const ext = path26.extname(filePath).toLowerCase();
47166
+ const ext = path30.extname(filePath).toLowerCase();
45526
47167
  return SUPPORTED_EXTENSIONS2.has(ext);
45527
47168
  }
45528
47169
  function findSourceFiles3(dir, files = []) {
45529
47170
  let entries;
45530
47171
  try {
45531
- entries = fs22.readdirSync(dir);
47172
+ entries = fs24.readdirSync(dir);
45532
47173
  } catch {
45533
47174
  return files;
45534
47175
  }
@@ -45537,10 +47178,10 @@ function findSourceFiles3(dir, files = []) {
45537
47178
  if (SKIP_DIRECTORIES3.has(entry)) {
45538
47179
  continue;
45539
47180
  }
45540
- const fullPath = path26.join(dir, entry);
47181
+ const fullPath = path30.join(dir, entry);
45541
47182
  let stat;
45542
47183
  try {
45543
- stat = fs22.statSync(fullPath);
47184
+ stat = fs24.statSync(fullPath);
45544
47185
  } catch {
45545
47186
  continue;
45546
47187
  }
@@ -45633,7 +47274,7 @@ var todo_extract = tool({
45633
47274
  return JSON.stringify(errorResult, null, 2);
45634
47275
  }
45635
47276
  const scanPath = resolvedPath;
45636
- if (!fs22.existsSync(scanPath)) {
47277
+ if (!fs24.existsSync(scanPath)) {
45637
47278
  const errorResult = {
45638
47279
  error: `path not found: ${pathsInput}`,
45639
47280
  total: 0,
@@ -45643,13 +47284,13 @@ var todo_extract = tool({
45643
47284
  return JSON.stringify(errorResult, null, 2);
45644
47285
  }
45645
47286
  const filesToScan = [];
45646
- const stat = fs22.statSync(scanPath);
47287
+ const stat = fs24.statSync(scanPath);
45647
47288
  if (stat.isFile()) {
45648
47289
  if (isSupportedExtension(scanPath)) {
45649
47290
  filesToScan.push(scanPath);
45650
47291
  } else {
45651
47292
  const errorResult = {
45652
- error: `unsupported file extension: ${path26.extname(scanPath)}`,
47293
+ error: `unsupported file extension: ${path30.extname(scanPath)}`,
45653
47294
  total: 0,
45654
47295
  byPriority: { high: 0, medium: 0, low: 0 },
45655
47296
  entries: []
@@ -45662,11 +47303,11 @@ var todo_extract = tool({
45662
47303
  const allEntries = [];
45663
47304
  for (const filePath of filesToScan) {
45664
47305
  try {
45665
- const fileStat = fs22.statSync(filePath);
47306
+ const fileStat = fs24.statSync(filePath);
45666
47307
  if (fileStat.size > MAX_FILE_SIZE_BYTES8) {
45667
47308
  continue;
45668
47309
  }
45669
- const content = fs22.readFileSync(filePath, "utf-8");
47310
+ const content = fs24.readFileSync(filePath, "utf-8");
45670
47311
  const entries = parseTodoComments(content, filePath, tagsSet);
45671
47312
  allEntries.push(...entries);
45672
47313
  } catch {}
@@ -45737,7 +47378,7 @@ var OpenCodeSwarm = async (ctx) => {
45737
47378
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
45738
47379
  preflightTriggerManager = new PTM(automationConfig);
45739
47380
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
45740
- const swarmDir = path27.resolve(ctx.directory, ".swarm");
47381
+ const swarmDir = path31.resolve(ctx.directory, ".swarm");
45741
47382
  statusArtifact = new ASA(swarmDir);
45742
47383
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
45743
47384
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -45840,6 +47481,7 @@ var OpenCodeSwarm = async (ctx) => {
45840
47481
  lint,
45841
47482
  diff,
45842
47483
  pkg_audit,
47484
+ pre_check_batch,
45843
47485
  retrieve_summary,
45844
47486
  schema_drift,
45845
47487
  secretscan,