nexus-agents 11.1.0 → 11.1.2

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.
Files changed (30) hide show
  1. package/dist/{ast-rule-runner-RL6XHXJB.js → ast-rule-runner-XESMURYB.js} +2 -2
  2. package/dist/{chunk-PNYCRRT6.js → chunk-7WT6TFEK.js} +3 -3
  3. package/dist/{chunk-3RUQILL4.js → chunk-A622LB4J.js} +3 -3
  4. package/dist/{chunk-QKWZZM2D.js → chunk-KVTLV44O.js} +13 -7
  5. package/dist/chunk-KVTLV44O.js.map +1 -0
  6. package/dist/{chunk-I7S3R5SQ.js → chunk-OOAPDIEW.js} +2 -2
  7. package/dist/{chunk-2JBWGPU6.js → chunk-QAWELEGH.js} +2 -2
  8. package/dist/{chunk-7HPCEKK7.js → chunk-QKEC2EDG.js} +22 -18
  9. package/dist/{chunk-7HPCEKK7.js.map → chunk-QKEC2EDG.js.map} +1 -1
  10. package/dist/{chunk-GAMT4NFP.js → chunk-YTV3O55W.js} +2 -2
  11. package/dist/cli.js +12 -11
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{consensus-vote-YVJGZGGG.js → consensus-vote-UREYCWLL.js} +4 -4
  14. package/dist/{improvement-review-VYT24FPH.js → improvement-review-SXL6EJ6T.js} +4 -4
  15. package/dist/index.js +7 -7
  16. package/dist/{setup-custom-api-U3IGIJI7.js → setup-custom-api-W3645WL2.js} +1 -1
  17. package/dist/setup-custom-api-W3645WL2.js.map +1 -0
  18. package/dist/{setup-data-dir-VCH62L7T.js → setup-data-dir-7AH5ID4F.js} +3 -3
  19. package/package.json +3 -2
  20. package/dist/chunk-QKWZZM2D.js.map +0 -1
  21. package/dist/setup-custom-api-U3IGIJI7.js.map +0 -1
  22. /package/dist/{ast-rule-runner-RL6XHXJB.js.map → ast-rule-runner-XESMURYB.js.map} +0 -0
  23. /package/dist/{chunk-PNYCRRT6.js.map → chunk-7WT6TFEK.js.map} +0 -0
  24. /package/dist/{chunk-3RUQILL4.js.map → chunk-A622LB4J.js.map} +0 -0
  25. /package/dist/{chunk-I7S3R5SQ.js.map → chunk-OOAPDIEW.js.map} +0 -0
  26. /package/dist/{chunk-2JBWGPU6.js.map → chunk-QAWELEGH.js.map} +0 -0
  27. /package/dist/{chunk-GAMT4NFP.js.map → chunk-YTV3O55W.js.map} +0 -0
  28. /package/dist/{consensus-vote-YVJGZGGG.js.map → consensus-vote-UREYCWLL.js.map} +0 -0
  29. /package/dist/{improvement-review-VYT24FPH.js.map → improvement-review-SXL6EJ6T.js.map} +0 -0
  30. /package/dist/{setup-data-dir-VCH62L7T.js.map → setup-data-dir-7AH5ID4F.js.map} +0 -0
@@ -9,7 +9,7 @@ import {
9
9
  getBuiltInAstRulesPath,
10
10
  loadRules,
11
11
  runAstQaRules
12
- } from "./chunk-QKWZZM2D.js";
12
+ } from "./chunk-KVTLV44O.js";
13
13
  import "./chunk-EBOAFTOG.js";
14
14
  import "./chunk-XBEKMUTO.js";
15
15
  import "./chunk-RUHKJC3X.js";
@@ -28,4 +28,4 @@ export {
28
28
  loadRules,
29
29
  runAstQaRules
30
30
  };
31
- //# sourceMappingURL=ast-rule-runner-RL6XHXJB.js.map
31
+ //# sourceMappingURL=ast-rule-runner-XESMURYB.js.map
@@ -21,7 +21,7 @@ import {
21
21
  toolSuccess,
22
22
  withProgressHeartbeat,
23
23
  wrapToolWithTimeout
24
- } from "./chunk-GAMT4NFP.js";
24
+ } from "./chunk-YTV3O55W.js";
25
25
  import {
26
26
  getToolMemory
27
27
  } from "./chunk-RR2Y2HVZ.js";
@@ -30,7 +30,7 @@ import {
30
30
  } from "./chunk-EZNIYU5T.js";
31
31
  import {
32
32
  VERSION
33
- } from "./chunk-I7S3R5SQ.js";
33
+ } from "./chunk-OOAPDIEW.js";
34
34
  import {
35
35
  ModelBoundAdapter,
36
36
  UNVERIFIABLE_STDERR_RE,
@@ -9905,4 +9905,4 @@ export {
9905
9905
  CONSENSUS_VOTE_TOOL_SCHEMA,
9906
9906
  registerConsensusVoteTool
9907
9907
  };
9908
- //# sourceMappingURL=chunk-PNYCRRT6.js.map
9908
+ //# sourceMappingURL=chunk-7WT6TFEK.js.map
@@ -7,7 +7,7 @@ import {
7
7
  VERSION,
8
8
  isNodeVersionSupported,
9
9
  recordServerTransport
10
- } from "./chunk-I7S3R5SQ.js";
10
+ } from "./chunk-OOAPDIEW.js";
11
11
  import {
12
12
  checkGatewayHealth,
13
13
  cliFailsVerdict,
@@ -2251,7 +2251,7 @@ async function runDoctorFix(result) {
2251
2251
  writeLine2("\u2500".repeat(40));
2252
2252
  let fixCount = 0;
2253
2253
  if (!result.dataDirectory.rootExists || result.dataDirectory.subdirectories.some((d) => !d.exists || !d.writable)) {
2254
- const { initDataDirectories: initDataDirectories2, describeDataDirFix: describeDataDirFix2 } = await import("./setup-data-dir-VCH62L7T.js");
2254
+ const { initDataDirectories: initDataDirectories2, describeDataDirFix: describeDataDirFix2 } = await import("./setup-data-dir-7AH5ID4F.js");
2255
2255
  const outcome = describeDataDirFix2(initDataDirectories2());
2256
2256
  writeLine2(outcome.line);
2257
2257
  if (outcome.counted) fixCount++;
@@ -2371,4 +2371,4 @@ export {
2371
2371
  startStdioServer,
2372
2372
  closeServer
2373
2373
  };
2374
- //# sourceMappingURL=chunk-3RUQILL4.js.map
2374
+ //# sourceMappingURL=chunk-A622LB4J.js.map
@@ -100,7 +100,10 @@ function resolveWithinCwd(target) {
100
100
  }
101
101
  return { resolved };
102
102
  }
103
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
103
+ var EXCLUDED_DIR_NAMES = /* @__PURE__ */ new Set(["node_modules", "dist"]);
104
+ function isWalkedDir(name) {
105
+ return !name.startsWith(".") && !EXCLUDED_DIR_NAMES.has(name);
106
+ }
104
107
  var POLYGLOT_EXT_TO_LANG = {
105
108
  ".py": "python",
106
109
  ".go": "go"
@@ -109,25 +112,28 @@ function inferRuleLang(file) {
109
112
  return POLYGLOT_EXT_TO_LANG[extname(file).toLowerCase()] ?? null;
110
113
  }
111
114
  async function findPolyglotFiles(dir, maxDepth) {
112
- if (maxDepth <= 0) return [];
113
115
  const out = [];
116
+ await collectPolyglotFiles(dir, maxDepth, out);
117
+ return out;
118
+ }
119
+ async function collectPolyglotFiles(dir, maxDepth, out) {
120
+ if (maxDepth <= 0) return;
114
121
  let entries;
115
122
  try {
116
123
  entries = await readdir(dir, { withFileTypes: true });
117
124
  } catch (caught) {
118
125
  const e = caught instanceof Error ? caught : new Error(String(caught));
119
126
  logger.warn(`ast-rule-runner: skipped unreadable directory ${dir}: ${e.message}`);
120
- return out;
127
+ return;
121
128
  }
122
129
  for (const entry of entries) {
123
130
  const fullPath = join(dir, entry.name);
124
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
125
- out.push(...await findPolyglotFiles(fullPath, maxDepth - 1));
131
+ if (entry.isDirectory() && isWalkedDir(entry.name)) {
132
+ await collectPolyglotFiles(fullPath, maxDepth - 1, out);
126
133
  } else if (entry.isFile() && inferRuleLang(entry.name) !== null) {
127
134
  out.push(fullPath);
128
135
  }
129
136
  }
130
- return out;
131
137
  }
132
138
  function langToParseArg(lang) {
133
139
  ensurePolyglotLangs();
@@ -241,4 +247,4 @@ export {
241
247
  collectAstQaFindings,
242
248
  runAstQaRules
243
249
  };
244
- //# sourceMappingURL=chunk-QKWZZM2D.js.map
250
+ //# sourceMappingURL=chunk-KVTLV44O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/security/ast-rule-runner.ts"],"sourcesContent":["/**\n * nexus-agents/security - Polyglot (Python/Go) AST QA/Security Rule Runner (#4249 child C)\n *\n * Runs YAML-defined `@ast-grep/napi` rules against Python/Go source, detecting\n * common QA/security anti-patterns (dynamic `eval`/`exec`, shell injection,\n * unchecked external command execution). Same engine already used by\n * `search_usages` (indexer/usage-ast.ts, #4265) and the ast-fixer rewrites\n * (agents/collaboration/ast-rewrites.ts, #4243/#4249 child B) — no new AST\n * dependency, just new language grammars for it.\n *\n * **Language support gap.** `Lang` in `@ast-grep/napi@0.44.1` ships only\n * Html/JavaScript/Tsx/Css/TypeScript — no Python, no Go. This module closes\n * that gap via napi's `@experimental registerDynamicLanguage` API, backed by\n * the `@ast-grep/lang-python`/`@ast-grep/lang-go` prebuilt tree-sitter\n * grammars. Both are EXACT-pinned in package.json (no caret): these are\n * early/experimental packages and a caret range could silently swap in an\n * incompatible native `.so` grammar build underneath a \"safe\" semver-minor\n * bump. `registerDynamicLanguage` throws if called more than once per\n * process (napi-rs's own documented constraint) — {@link ensurePolyglotLangs}\n * is a lazy, module-level, once-only guard so repeated calls (tests, repeat\n * runner invocations within one process) never trip it twice.\n *\n * **Read-only.** Reads rule YAML and target source files, walks their ASTs,\n * performs NO writes. ast-grep YAML rule files support a `fix:` key for\n * autofixing, but napi's `SgNode.findAll` (used here) never applies it —\n * only ast-grep's separate rewrite/CLI machinery does — so even a rule\n * author who adds `fix:` cannot turn this runner into a writer by accident.\n * MCP exposure is deliberately deferred (see the follow-up issue referenced\n * in the #4249 child C PR) to keep this surface a plain function until a\n * named consumer needs it wired through a tool.\n *\n * @module security/ast-rule-runner\n * @see Issue #4249 - epic: ast-grep adoption (Child C: polyglot QA/security rules)\n */\n\nimport { existsSync } from 'node:fs';\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { dirname, extname, join, relative, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parse, registerDynamicLanguage } from '@ast-grep/napi';\nimport type { NapiConfig } from '@ast-grep/napi';\n// Both packages are CJS `export = <LanguageRegistration>` modules — the\n// default import is the whole registration object (`libraryPath` getter,\n// `extensions`, `languageSymbol`, `expandoChar`). See their `index.d.ts`.\nimport goLangRegistration from '@ast-grep/lang-go';\nimport pythonLangRegistration from '@ast-grep/lang-python';\nimport { parse as parseYaml } from 'yaml';\nimport { z } from 'zod';\nimport { createLogger, SecurityError, ValidationError, formatZodError } from '../core/index.js';\nimport { resolveInsideRoot } from './safe-path.js';\n\n// ============================================================================\n// Dynamic language registration (@experimental, call-once)\n// ============================================================================\n\nlet polyglotLangsRegistered = false;\n\n/**\n * Lazily register the Python/Go tree-sitter grammars with ast-grep's\n * `registerDynamicLanguage`. That API throws if called more than once in a\n * process; this guard makes every call after the first a no-op so repeated\n * runner invocations (or a test suite that constructs the runner many times)\n * never trip the constraint.\n */\nexport function ensurePolyglotLangs(): void {\n if (polyglotLangsRegistered) return;\n registerDynamicLanguage({ python: pythonLangRegistration, go: goLangRegistration });\n polyglotLangsRegistered = true;\n}\n\n// ============================================================================\n// Rule schema (fail-closed)\n// ============================================================================\n\n/** Languages a rule file may declare. Deliberately narrowed to the two this\n * runner actually SCANS (python/go): admitting `typescript`/`javascript` here\n * would let a TS/JS rule file validate at load time and then silently never\n * fire (there is no TS/JS extension in {@link POLYGLOT_EXT_TO_LANG}), which is\n * a fail-OPEN gap for a security scanner where \"no findings\" reads as \"clean\".\n * TS/JS structural analysis is already covered by `search_usages` (#4265) and\n * the ast-fixer rewrites (#4243); a TS/JS rule here would fail LOUD at load\n * (unknown-language Zod error) instead of loading dead. */\nexport const AST_RULE_LANGUAGES = ['python', 'go'] as const;\nexport type AstRuleLanguage = (typeof AST_RULE_LANGUAGES)[number];\n\nexport const AST_RULE_SEVERITIES = ['error', 'warning', 'info'] as const;\nexport type AstRuleSeverity = (typeof AST_RULE_SEVERITIES)[number];\n\n/** Schema for one `*.yml` rule file. `rule` is passed through as ast-grep's\n * own `Rule` object (napi already validates it structurally at match time);\n * Zod only pins down the envelope so an unknown `language` or a missing\n * field FAILS CLOSED instead of silently loading a partial/broken rule. */\nconst RuleFileSchema = z.object({\n id: z.string().min(1),\n language: z.enum(AST_RULE_LANGUAGES),\n severity: z.enum(AST_RULE_SEVERITIES),\n message: z.string().min(1),\n rule: z.record(z.string(), z.unknown()),\n});\n\nexport type AstQaRuleFile = z.infer<typeof RuleFileSchema>;\n\n/** Per-match snippet char cap (matches indexer/usage-ast.ts's discipline). */\nconst MAX_SNIPPET_CHARS = 200;\n\n/** Default cap on emitted findings. Excess is counted + reported, never silently dropped. */\nexport const DEFAULT_AST_QA_LIMIT = 200;\n/** Hard upper bound a caller may request for the finding cap. */\nexport const MAX_AST_QA_LIMIT = 2000;\n/** Directory walk depth for the target-dir scan (not caller-configurable — no\n * named consumer needs a deeper/shallower walk yet; YAGNI). */\nconst AST_QA_MAX_DEPTH = 24;\n/** Upper bound on files parsed in a single run, to bound worst-case cost.\n * Exceeding it makes the scan PARTIAL — surfaced via {@link AstQaCollectResult}'s\n * `filesTruncated`/`filesSkipped` + a warn, never silently dropped. */\nexport const MAX_FILES_SCANNED = 5000;\n\nconst logger = createLogger({ component: 'ast-rule-runner' });\n\n// ============================================================================\n// Rule loading\n// ============================================================================\n\n/**\n * Load and Zod-validate every `*.yml` rule file in `dir`. FAILS CLOSED: a\n * single malformed YAML file, or a file with an unrecognized `language` or\n * missing field, throws a {@link ValidationError} — there is no\n * \"load what parsed, skip the rest\" partial mode.\n */\nexport async function loadRules(dir: string): Promise<AstQaRuleFile[]> {\n let entries: string[];\n try {\n entries = (await readdir(dir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')).sort();\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n throw new ValidationError(`ast-rule-runner: cannot read rules dir ${dir}: ${e.message}`, {\n cause: e,\n });\n }\n\n const rules: AstQaRuleFile[] = [];\n for (const file of entries) {\n const path = join(dir, file);\n const raw = await readFile(path, 'utf8');\n\n let parsed: unknown;\n try {\n parsed = parseYaml(raw);\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n throw new ValidationError(`ast-rule-runner: malformed YAML in ${path}: ${e.message}`, {\n cause: e,\n });\n }\n\n const result = RuleFileSchema.safeParse(parsed);\n if (!result.success) {\n throw new ValidationError(\n `ast-rule-runner: invalid rule file ${path}: ${formatZodError(result.error)}`\n );\n }\n rules.push(result.data);\n }\n return rules;\n}\n\n// ============================================================================\n// Built-in rules dir resolution (mirrors workflows/template-loader.ts)\n// ============================================================================\n\n/**\n * Resolve the built-in `ast-rules/` directory, handling both dev (unbundled,\n * `src/security/ast-rule-runner.ts`) and published (bundled,\n * `dist/index.js` + `dist/security/ast-rules/*.yml` copied by tsup's\n * `onSuccess` step) layouts — the same two-layout problem\n * `getBuiltInTemplatesPath` solves for workflow templates.\n */\nexport function getBuiltInAstRulesPath(): string {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n\n if (currentFile.includes(`${sep}dist${sep}`) || currentFile.endsWith(`${sep}dist`)) {\n const distIndex = currentFile.lastIndexOf(`${sep}dist${sep}`);\n if (distIndex !== -1) {\n const distDir = currentFile.substring(0, distIndex + 5); // +5 for \"/dist\"\n const bundledPath = join(distDir, 'security', 'ast-rules');\n if (existsSync(bundledPath)) return bundledPath;\n }\n }\n\n const possiblePaths = [\n join(currentDir, 'ast-rules'), // Unbundled: sibling of this file in src/security/\n join(currentDir, 'security', 'ast-rules'), // Bundled chunk: dist/security sibling\n join(dirname(currentDir), 'security', 'ast-rules'),\n ];\n for (const path of possiblePaths) {\n if (existsSync(path)) return path;\n }\n return join(currentDir, 'ast-rules'); // Fallback (for error reporting)\n}\n\n// ============================================================================\n// Path-traversal guard (same shape as mcp/tools/search-usages-tool.ts)\n// ============================================================================\n\n/**\n * Resolve a caller path against a path-traversal guard (must stay within cwd).\n * Realpath-aware: a symlink inside cwd whose target is outside it is refused,\n * and the returned path is the canonical one the scan then walks.\n */\nfunction resolveWithinCwd(target: string): { resolved: string } | { error: string } {\n const resolved = resolveInsideRoot(target);\n if (resolved === null) {\n return { error: `Path traversal denied: scope must be within ${process.cwd()}` };\n }\n return { resolved };\n}\n\n// ============================================================================\n// File walk (LOCAL extension map — python/go only; does NOT reuse\n// indexer/usage-ast.ts's TS/JS inferLang or indexer/codebase-search.ts's\n// TS/JS-only findSourceFiles)\n// ============================================================================\n\n/** Directory names the walk never descends into, in addition to every dot-directory. */\nconst EXCLUDED_DIR_NAMES: ReadonlySet<string> = new Set(['node_modules', 'dist']);\n\n/**\n * Whether the walk descends into a directory entry. Dot-directories are skipped\n * as a class (#7184), the same rule `findSourceFiles` and `governance/source-scan.ts`\n * apply: `.git` and the gitignored `.nexus-agents/worktrees` copies are never the\n * caller's source, and scanning the worktrees duplicated every finding. The rule\n * applies to entries only, so a caller may still root the scan at a dot-dir.\n */\nfunction isWalkedDir(name: string): boolean {\n return !name.startsWith('.') && !EXCLUDED_DIR_NAMES.has(name);\n}\n\n/** file-extension -> ast-rule language, for the two languages this runner scans. */\nconst POLYGLOT_EXT_TO_LANG: Readonly<Record<string, AstRuleLanguage>> = {\n '.py': 'python',\n '.go': 'go',\n};\n\n/** Infer the ast-rule language for a file from its extension, or `null` if unsupported. */\nfunction inferRuleLang(file: string): AstRuleLanguage | null {\n return POLYGLOT_EXT_TO_LANG[extname(file).toLowerCase()] ?? null;\n}\n\n/**\n * Recursively collect `.py`/`.go` files under `dir`, bounded by `maxDepth`,\n * skipping dot-directories and `node_modules`/`dist`.\n * A subdirectory whose `readdir` fails mid-walk (e.g. a `chmod 000` dir) is\n * WARNED about per-dir and skipped rather than silently swallowed — the scan\n * continues, but the partiality is surfaced in the log. The SCAN ROOT's own\n * readability is validated up front by {@link collectAstQaFindings} (fail\n * loud), so this soft-skip only ever applies to descendant dirs.\n */\nasync function findPolyglotFiles(dir: string, maxDepth: number): Promise<string[]> {\n const out: string[] = [];\n await collectPolyglotFiles(dir, maxDepth, out);\n return out;\n}\n\n/**\n * Walk into a shared accumulator. Appending per file, rather than\n * `out.push(...sub)`, keeps the walk safe on large trees: a spread passes every\n * element as a call argument and throws \"Maximum call stack size exceeded\"\n * past roughly 120k entries (#7184, the #7178 class).\n */\nasync function collectPolyglotFiles(dir: string, maxDepth: number, out: string[]): Promise<void> {\n if (maxDepth <= 0) return;\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n logger.warn(`ast-rule-runner: skipped unreadable directory ${dir}: ${e.message}`);\n return;\n }\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory() && isWalkedDir(entry.name)) {\n await collectPolyglotFiles(fullPath, maxDepth - 1, out);\n } else if (entry.isFile() && inferRuleLang(entry.name) !== null) {\n out.push(fullPath);\n }\n }\n}\n\n/** Map a rule's declared language to the `parse()` first argument, registering\n * the dynamic grammars on first use. Both supported languages (python/go) are\n * dynamically-registered CustomLangs, so the language name string IS the\n * `parse()` key set up in {@link ensurePolyglotLangs}. */\nfunction langToParseArg(lang: AstRuleLanguage): AstRuleLanguage {\n ensurePolyglotLangs();\n return lang; // the CustomLang string keys used in registerDynamicLanguage()\n}\n\n// ============================================================================\n// Findings\n// ============================================================================\n\n/** One ast-grep rule match against a source file. */\nexport interface AstRuleFinding {\n ruleId: string;\n severity: AstRuleSeverity;\n message: string;\n /** Path relative to the resolved `targetDir`. */\n file: string;\n /** 1-based line number. */\n line: number;\n /** 1-based column number. */\n column: number;\n /** Trimmed, length-capped source line for the match. */\n snippet: string;\n}\n\nexport interface RunAstQaRulesOptions {\n /** Directory containing `*.yml` rule files (default: the built-in bundled rules). */\n rulesDir?: string;\n /** Directory to scan for `.py`/`.go` source files (must stay within cwd). */\n targetDir: string;\n /** Max findings emitted (default {@link DEFAULT_AST_QA_LIMIT}). Excess is counted + reported. */\n limit?: number;\n}\n\n/** {@link collectAstQaFindings}'s full result — findings plus the true total,\n * so overflow beyond `limit` is counted and reported, never silently dropped. */\nexport interface AstQaCollectResult {\n findings: AstRuleFinding[];\n /** True count of matches found, before the `limit` cap was applied. */\n total: number;\n limit: number;\n /** Number of `.py`/`.go` files actually scanned (after the file cap). */\n filesScanned: number;\n /** Number of discovered files DROPPED because the file cap ({@link MAX_FILES_SCANNED})\n * was exceeded. Non-zero means the scan was PARTIAL — a downstream consumer\n * must not treat empty/low findings as a clean bill of health. */\n filesSkipped: number;\n /** True iff `filesSkipped > 0` — the scan did not cover every candidate file. */\n filesTruncated: boolean;\n}\n\n/** Findings + true-match-count from scanning ONE already-read source file. */\ninterface FileScanResult {\n findings: AstRuleFinding[];\n total: number;\n}\n\n/** Bundles {@link scanFile}'s cap-tracking inputs (max-params discipline). */\ninterface FileScanBudget {\n /** Path emitted on each finding (relative to the resolved `targetDir`). */\n rel: string;\n /** Max TOTAL findings across the whole run. */\n limit: number;\n /** Findings already accepted by earlier files in this run. */\n alreadyFound: number;\n}\n\n/**\n * Scan one already-read source file against the rules applicable to its\n * language, pushing findings only while the running total (`budget.alreadyFound`\n * + findings pushed so far) stays under `budget.limit` — while still counting\n * every true match, so the caller can report overflow instead of silently\n * dropping it.\n */\nfunction scanFile(\n src: string,\n lang: AstRuleLanguage,\n applicable: readonly AstQaRuleFile[],\n budget: FileScanBudget\n): FileScanResult {\n const { rel, limit, alreadyFound } = budget;\n const root = parse(langToParseArg(lang), src).root();\n const lines = src.split('\\n');\n const findings: AstRuleFinding[] = [];\n let total = 0;\n\n for (const rule of applicable) {\n const config: NapiConfig = { rule: rule.rule };\n for (const node of root.findAll(config)) {\n total += 1;\n if (alreadyFound + findings.length < limit) {\n const { start } = node.range();\n const rawLine = lines[start.line] ?? node.text();\n findings.push({\n ruleId: rule.id,\n severity: rule.severity,\n message: rule.message,\n file: rel,\n line: start.line + 1,\n column: start.column + 1,\n snippet: rawLine.trim().slice(0, MAX_SNIPPET_CHARS),\n });\n }\n }\n }\n return { findings, total };\n}\n\n/**\n * Fail LOUD on an unreadable / non-directory scan root. `loadRules` already\n * throws on an unreadable RULES dir; the TARGET traversal must not fail OPEN —\n * a lone file path, a bad path, or a `000` root would otherwise return an empty\n * finding set that reads as \"clean\" for a security scanner.\n */\nasync function assertScanRootIsDirectory(resolved: string): Promise<void> {\n let rootStat;\n try {\n rootStat = await stat(resolved);\n } catch (caught: unknown) {\n const e = caught instanceof Error ? caught : new Error(String(caught));\n throw new ValidationError(`ast-rule-runner: cannot read targetDir ${resolved}: ${e.message}`, {\n cause: e,\n });\n }\n if (!rootStat.isDirectory()) {\n throw new ValidationError(\n `ast-rule-runner: targetDir must be a directory, got a non-directory path: ${resolved}`\n );\n }\n}\n\n/** Result of resolving the candidate file set under a scan root, with the\n * file-cap truncation signal surfaced (never a silent `.slice`). */\ninterface ScanFileSet {\n files: string[];\n filesSkipped: number;\n filesTruncated: boolean;\n}\n\n/** Discover candidate `.py`/`.go` files, applying + SURFACING the file cap. */\nasync function resolveScanFiles(root: string): Promise<ScanFileSet> {\n const discovered = await findPolyglotFiles(root, AST_QA_MAX_DEPTH);\n const files = discovered.slice(0, MAX_FILES_SCANNED);\n const filesSkipped = discovered.length - files.length;\n const filesTruncated = filesSkipped > 0;\n if (filesTruncated) {\n logger.warn(\n `ast-rule-runner: ${String(filesSkipped)} file(s) NOT scanned — file cap ` +\n `${String(MAX_FILES_SCANNED)} exceeded (discovered=${String(discovered.length)}). ` +\n `Scan is PARTIAL; do not treat findings as exhaustive.`\n );\n }\n return { files, filesSkipped, filesTruncated };\n}\n\n/**\n * Run every applicable rule in `rulesDir` against every `.py`/`.go` file under\n * `targetDir`, returning capped findings plus the true total (so callers that\n * need the overflow count — e.g. a future MCP tool wrapper — can report it).\n * {@link runAstQaRules} is the capped-array convenience wrapper over this.\n */\nexport async function collectAstQaFindings(\n opts: RunAstQaRulesOptions\n): Promise<AstQaCollectResult> {\n const guard = resolveWithinCwd(opts.targetDir);\n if ('error' in guard) {\n throw new SecurityError(guard.error);\n }\n await assertScanRootIsDirectory(guard.resolved);\n\n const rulesDir = opts.rulesDir ?? getBuiltInAstRulesPath();\n const rules = await loadRules(rulesDir);\n // Clamp into [1, MAX]: a limit <= 0 would report total > 0 with zero findings\n // (fail-open \"clean\" signal), so the floor is load-bearing, not cosmetic.\n const limit = Math.min(Math.max(opts.limit ?? DEFAULT_AST_QA_LIMIT, 1), MAX_AST_QA_LIMIT);\n\n const { files, filesSkipped, filesTruncated } = await resolveScanFiles(guard.resolved);\n\n const findings: AstRuleFinding[] = [];\n let total = 0;\n\n for (const file of files) {\n const lang = inferRuleLang(file);\n if (lang === null) continue;\n const applicable = rules.filter((r) => r.language === lang);\n if (applicable.length === 0) continue;\n\n let src: string;\n try {\n src = await readFile(file, 'utf8');\n } catch {\n logger.warn(`ast-rule-runner: skipped unreadable file ${file}`);\n continue;\n }\n\n const rel = relative(guard.resolved, file) || file;\n const scanned = scanFile(src, lang, applicable, { rel, limit, alreadyFound: findings.length });\n findings.push(...scanned.findings);\n total += scanned.total;\n }\n\n if (total > findings.length) {\n logger.warn(\n `ast-rule-runner: ${String(total - findings.length)} finding(s) omitted beyond ` +\n `limit=${String(limit)} (total=${String(total)})`\n );\n }\n\n return { findings, total, limit, filesScanned: files.length, filesSkipped, filesTruncated };\n}\n\n/**\n * Run the polyglot QA/security ast-grep rules and return the capped findings\n * array. Overflow beyond `limit` is counted and logged (never silently\n * dropped) — use {@link collectAstQaFindings} directly when the caller needs\n * the true total/overflow count programmatically.\n */\nexport async function runAstQaRules(opts: RunAstQaRulesOptions): Promise<AstRuleFinding[]> {\n const { findings } = await collectAstQaFindings(opts);\n return findings;\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
 
5
5
  // src/version.ts
6
6
  import semver from "semver";
7
- var VERSION = true ? "11.1.0" : "dev";
7
+ var VERSION = true ? "11.1.2" : "dev";
8
8
  var NODE_ENGINE_RANGE = ">=24";
9
9
  function isNodeVersionSupported(version) {
10
10
  return semver.satisfies(version, NODE_ENGINE_RANGE);
@@ -106,4 +106,4 @@ export {
106
106
  contextForLogging,
107
107
  measuredTrustTier
108
108
  };
109
- //# sourceMappingURL=chunk-I7S3R5SQ.js.map
109
+ //# sourceMappingURL=chunk-OOAPDIEW.js.map
@@ -7,7 +7,7 @@ import {
7
7
  toSdkCallback,
8
8
  toolStructuredError,
9
9
  wrapToolWithTimeout
10
- } from "./chunk-GAMT4NFP.js";
10
+ } from "./chunk-YTV3O55W.js";
11
11
  import {
12
12
  JsonlStore,
13
13
  SECURITY_KEYWORDS,
@@ -2431,4 +2431,4 @@ export {
2431
2431
  runImprovementReview,
2432
2432
  registerImprovementReviewTool
2433
2433
  };
2434
- //# sourceMappingURL=chunk-2JBWGPU6.js.map
2434
+ //# sourceMappingURL=chunk-QAWELEGH.js.map
@@ -58,7 +58,7 @@ import {
58
58
  updateStage,
59
59
  withFileLock,
60
60
  writeJobCancelled
61
- } from "./chunk-PNYCRRT6.js";
61
+ } from "./chunk-7WT6TFEK.js";
62
62
  import {
63
63
  normalizeTopicToCanonical,
64
64
  synthesizeResearch
@@ -77,7 +77,7 @@ import {
77
77
  } from "./chunk-NY7IMAG5.js";
78
78
  import {
79
79
  withPrerequisite
80
- } from "./chunk-2JBWGPU6.js";
80
+ } from "./chunk-QAWELEGH.js";
81
81
  import {
82
82
  NOOP_NOTIFIER,
83
83
  RateLimiter,
@@ -105,7 +105,7 @@ import {
105
105
  toolSuccess,
106
106
  withProgressHeartbeat,
107
107
  wrapToolWithTimeout
108
- } from "./chunk-GAMT4NFP.js";
108
+ } from "./chunk-YTV3O55W.js";
109
109
  import {
110
110
  generateSecurityPlan
111
111
  } from "./chunk-AX5DBV3M.js";
@@ -187,7 +187,7 @@ import {
187
187
  import {
188
188
  DEFAULT_TASK_TTL_MS,
189
189
  clampTaskTtl
190
- } from "./chunk-3RUQILL4.js";
190
+ } from "./chunk-A622LB4J.js";
191
191
  import {
192
192
  allOf,
193
193
  anyOf
@@ -196,7 +196,7 @@ import {
196
196
  VERSION,
197
197
  createRequestContext,
198
198
  measuredTrustTier
199
- } from "./chunk-I7S3R5SQ.js";
199
+ } from "./chunk-OOAPDIEW.js";
200
200
  import {
201
201
  BWRAP_READ_ONLY_ARGS,
202
202
  CliDetectionCache,
@@ -46657,7 +46657,7 @@ function buildVotingInput(plan, config) {
46657
46657
  }
46658
46658
  async function executeSingleVote(plan, config, log) {
46659
46659
  try {
46660
- const { executeVoting } = await import("./consensus-vote-YVJGZGGG.js");
46660
+ const { executeVoting } = await import("./consensus-vote-UREYCWLL.js");
46661
46661
  const input = buildVotingInput(plan, config);
46662
46662
  const result = await executeVoting(input, log);
46663
46663
  return parseVotingResult(result);
@@ -48489,7 +48489,7 @@ function createVoteStage({ config, startStage }) {
48489
48489
  const strategy = config.votingStrategy ?? "higher_order";
48490
48490
  await postProgress(config, "Vote", `Running consensus with ${strategy} strategy...`);
48491
48491
  try {
48492
- const { executeVoting } = await import("./consensus-vote-YVJGZGGG.js");
48492
+ const { executeVoting } = await import("./consensus-vote-UREYCWLL.js");
48493
48493
  const proposal = buildVoteProposal(plan, research);
48494
48494
  const votingResult = await executeVoting(
48495
48495
  {
@@ -52960,10 +52960,11 @@ function classifyConsensusResult(record, mode) {
52960
52960
  const wouldBlock = typeof enforcement === "object" && enforcement !== null && enforcement["wouldBlock"] === true;
52961
52961
  if (mode === "enforce" && (BLOCKING_CONSENSUS_DECISIONS.has(record["decision"]) || wouldBlock)) {
52962
52962
  const reason = consensusReason(record);
52963
- return {
52964
- success: false,
52965
- failureReason: reason === record["decision"] ? `Consensus ${reason}` : `Consensus ${String(record["decision"])}: ${reason}`
52966
- };
52963
+ const failureReason = reason === record["decision"] ? `Consensus ${reason}` : `Consensus ${String(record["decision"])}: ${reason}`;
52964
+ if (record["decision"] === "rejected" && reason === "rejected") {
52965
+ return { success: false, failureReason, gateRefusal: failureReason };
52966
+ }
52967
+ return { success: false, failureReason };
52967
52968
  }
52968
52969
  return { success: true };
52969
52970
  }
@@ -52983,14 +52984,16 @@ function classifyEngineResult(result, mode = "audit") {
52983
52984
  }
52984
52985
  return classifyConsensusResult(record, mode);
52985
52986
  }
52986
- function recordOutcome2(deps, decision, start, success, failureReason) {
52987
+ function recordOutcome2(deps, decision, start, verdict) {
52988
+ const refused = verdict.gateRefusal !== void 0;
52987
52989
  const record = {
52988
52990
  decisionId: decision.decisionId,
52989
52991
  timestamp: new Date(getTimeProvider().now()).toISOString(),
52990
52992
  strategy: decision.strategy,
52991
- success,
52993
+ success: verdict.success || refused,
52992
52994
  durationMs: Math.max(0, getTimeProvider().now() - start),
52993
- ...failureReason !== void 0 ? { failureReason } : {}
52995
+ ...!refused && verdict.failureReason !== void 0 ? { failureReason: verdict.failureReason } : {},
52996
+ ...refused ? { gateRefusal: verdict.gateRefusal } : {}
52994
52997
  };
52995
52998
  deps.outcomeSink.recordOutcome(record);
52996
52999
  if (deps.onOutcome !== void 0) {
@@ -53010,7 +53013,7 @@ async function dispatchDecision(decision, input, deps) {
53010
53013
  const executor = deps.executors[strategy];
53011
53014
  if (executor === void 0) {
53012
53015
  const reason = `No executor registered for strategy "${strategy}"`;
53013
- recordOutcome2(deps, decision, start, false, reason);
53016
+ recordOutcome2(deps, decision, start, { success: false, failureReason: reason });
53014
53017
  deps.logger.error("MetaDispatcher dispatch failed", void 0, {
53015
53018
  decisionId,
53016
53019
  strategy,
@@ -53021,7 +53024,7 @@ async function dispatchDecision(decision, input, deps) {
53021
53024
  try {
53022
53025
  const result = await executor(decision, input);
53023
53026
  const classification = deps.classifyResult(result);
53024
- recordOutcome2(deps, decision, start, classification.success, classification.failureReason);
53027
+ recordOutcome2(deps, decision, start, classification);
53025
53028
  return {
53026
53029
  decisionId,
53027
53030
  strategy,
@@ -53030,7 +53033,7 @@ async function dispatchDecision(decision, input, deps) {
53030
53033
  };
53031
53034
  } catch (err2) {
53032
53035
  const reason = errorMessage(err2);
53033
- recordOutcome2(deps, decision, start, false, reason);
53036
+ recordOutcome2(deps, decision, start, { success: false, failureReason: reason });
53034
53037
  deps.logger.error(
53035
53038
  "MetaDispatcher strategy executor threw",
53036
53039
  err2 instanceof Error ? err2 : void 0,
@@ -54894,6 +54897,7 @@ function buildShadowTrainObserver(logger58) {
54894
54897
  decisionId: decision.decisionId,
54895
54898
  strategy: decision.strategy,
54896
54899
  success: record.success,
54900
+ ...record.gateRefusal !== void 0 ? { gateRefusal: record.gateRefusal } : {},
54897
54901
  armStats: getShadowSelector().stats()
54898
54902
  });
54899
54903
  }
@@ -58469,4 +58473,4 @@ export {
58469
58473
  initializeSandbox,
58470
58474
  getSandboxMode
58471
58475
  };
58472
- //# sourceMappingURL=chunk-7HPCEKK7.js.map
58476
+ //# sourceMappingURL=chunk-QKEC2EDG.js.map