nexus-agents 11.0.2 → 11.1.1

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 (28) hide show
  1. package/dist/{ast-rule-runner-RL6XHXJB.js → ast-rule-runner-XESMURYB.js} +2 -2
  2. package/dist/{chunk-G667EEZ7.js → chunk-6P3NXE55.js} +2 -2
  3. package/dist/{chunk-ZBWKLJNT.js → chunk-EE5IX5B2.js} +3 -3
  4. package/dist/{chunk-YUA3UBXT.js → chunk-EIWQGMWN.js} +2 -2
  5. package/dist/{chunk-LSQ2IFMP.js → chunk-IR5NUHQU.js} +3 -3
  6. package/dist/{chunk-QKWZZM2D.js → chunk-KVTLV44O.js} +13 -7
  7. package/dist/chunk-KVTLV44O.js.map +1 -0
  8. package/dist/{chunk-TGUUUWJO.js → chunk-KZBDFSDB.js} +75 -27
  9. package/dist/{chunk-TGUUUWJO.js.map → chunk-KZBDFSDB.js.map} +1 -1
  10. package/dist/{chunk-Y34BCT36.js → chunk-Z5MTFYMM.js} +2 -2
  11. package/dist/cli.js +85 -28
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{consensus-vote-O7WRAIMH.js → consensus-vote-L27LHIDJ.js} +4 -4
  14. package/dist/{improvement-review-UQUJ4ZR5.js → improvement-review-BZRA3DG7.js} +4 -4
  15. package/dist/index.d.ts +25 -2
  16. package/dist/index.js +7 -7
  17. package/dist/{setup-data-dir-5YHUK3MZ.js → setup-data-dir-YFHCEJC7.js} +3 -3
  18. package/package.json +1 -1
  19. package/dist/chunk-QKWZZM2D.js.map +0 -1
  20. /package/dist/{ast-rule-runner-RL6XHXJB.js.map → ast-rule-runner-XESMURYB.js.map} +0 -0
  21. /package/dist/{chunk-G667EEZ7.js.map → chunk-6P3NXE55.js.map} +0 -0
  22. /package/dist/{chunk-ZBWKLJNT.js.map → chunk-EE5IX5B2.js.map} +0 -0
  23. /package/dist/{chunk-YUA3UBXT.js.map → chunk-EIWQGMWN.js.map} +0 -0
  24. /package/dist/{chunk-LSQ2IFMP.js.map → chunk-IR5NUHQU.js.map} +0 -0
  25. /package/dist/{chunk-Y34BCT36.js.map → chunk-Z5MTFYMM.js.map} +0 -0
  26. /package/dist/{consensus-vote-O7WRAIMH.js.map → consensus-vote-L27LHIDJ.js.map} +0 -0
  27. /package/dist/{improvement-review-UQUJ4ZR5.js.map → improvement-review-BZRA3DG7.js.map} +0 -0
  28. /package/dist/{setup-data-dir-5YHUK3MZ.js.map → setup-data-dir-YFHCEJC7.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
@@ -5,7 +5,7 @@ import {
5
5
  getCurrentRequestContext,
6
6
  runWithRequestContext,
7
7
  serverCallerInfo
8
- } from "./chunk-Y34BCT36.js";
8
+ } from "./chunk-Z5MTFYMM.js";
9
9
  import {
10
10
  DEFAULT_EXECUTION_MODE
11
11
  } from "./chunk-TYLE7Z2S.js";
@@ -2343,4 +2343,4 @@ export {
2343
2343
  DEFAULT_CONSENSUS_CONFIG,
2344
2344
  ConsensusMetricsSchema
2345
2345
  };
2346
- //# sourceMappingURL=chunk-G667EEZ7.js.map
2346
+ //# sourceMappingURL=chunk-6P3NXE55.js.map
@@ -7,7 +7,7 @@ import {
7
7
  VERSION,
8
8
  isNodeVersionSupported,
9
9
  recordServerTransport
10
- } from "./chunk-Y34BCT36.js";
10
+ } from "./chunk-Z5MTFYMM.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-5YHUK3MZ.js");
2254
+ const { initDataDirectories: initDataDirectories2, describeDataDirFix: describeDataDirFix2 } = await import("./setup-data-dir-YFHCEJC7.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-ZBWKLJNT.js.map
2374
+ //# sourceMappingURL=chunk-EE5IX5B2.js.map
@@ -7,7 +7,7 @@ import {
7
7
  toSdkCallback,
8
8
  toolStructuredError,
9
9
  wrapToolWithTimeout
10
- } from "./chunk-G667EEZ7.js";
10
+ } from "./chunk-6P3NXE55.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-YUA3UBXT.js.map
2434
+ //# sourceMappingURL=chunk-EIWQGMWN.js.map
@@ -21,7 +21,7 @@ import {
21
21
  toolSuccess,
22
22
  withProgressHeartbeat,
23
23
  wrapToolWithTimeout
24
- } from "./chunk-G667EEZ7.js";
24
+ } from "./chunk-6P3NXE55.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-Y34BCT36.js";
33
+ } from "./chunk-Z5MTFYMM.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-LSQ2IFMP.js.map
9908
+ //# sourceMappingURL=chunk-IR5NUHQU.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":[]}
@@ -58,7 +58,7 @@ import {
58
58
  updateStage,
59
59
  withFileLock,
60
60
  writeJobCancelled
61
- } from "./chunk-LSQ2IFMP.js";
61
+ } from "./chunk-IR5NUHQU.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-YUA3UBXT.js";
80
+ } from "./chunk-EIWQGMWN.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-G667EEZ7.js";
108
+ } from "./chunk-6P3NXE55.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-ZBWKLJNT.js";
190
+ } from "./chunk-EE5IX5B2.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-Y34BCT36.js";
199
+ } from "./chunk-Z5MTFYMM.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-O7WRAIMH.js");
46660
+ const { executeVoting } = await import("./consensus-vote-L27LHIDJ.js");
46661
46661
  const input = buildVotingInput(plan, config);
46662
46662
  const result = await executeVoting(input, log);
46663
46663
  return parseVotingResult(result);
@@ -47758,9 +47758,7 @@ function buildDryRunResult(planResult) {
47758
47758
  securityPassed: false,
47759
47759
  securityRan: false,
47760
47760
  ...planResult.planStatus !== void 0 ? { planStatus: planResult.planStatus } : {},
47761
- ...planResult.planVoteReason !== void 0 ? { planVoteReason: planResult.planVoteReason } : {},
47762
- ...planResult.planVoteApprovalPercentage !== void 0 ? { planVoteApprovalPercentage: planResult.planVoteApprovalPercentage } : {},
47763
- ...planResult.planVoteFeedback !== void 0 ? { planVoteFeedback: planResult.planVoteFeedback } : {}
47761
+ ...planVoteEvidence(planResult)
47764
47762
  };
47765
47763
  }
47766
47764
  function buildPlanFailureResult(planResult) {
@@ -47773,9 +47771,16 @@ function buildPlanFailureResult(planResult) {
47773
47771
  securityPassed: false,
47774
47772
  securityRan: false,
47775
47773
  planStatus: planResult.planStatus ?? "unapproved",
47774
+ ...planVoteEvidence(planResult)
47775
+ };
47776
+ }
47777
+ function planVoteEvidence(planResult) {
47778
+ return {
47779
+ ...planResult.planVoteDecision !== void 0 ? { planVoteDecision: planResult.planVoteDecision } : {},
47776
47780
  ...planResult.planVoteReason !== void 0 ? { planVoteReason: planResult.planVoteReason } : {},
47777
47781
  ...planResult.planVoteApprovalPercentage !== void 0 ? { planVoteApprovalPercentage: planResult.planVoteApprovalPercentage } : {},
47778
- ...planResult.planVoteFeedback !== void 0 ? { planVoteFeedback: planResult.planVoteFeedback } : {}
47782
+ ...planResult.planVoteFeedback !== void 0 ? { planVoteFeedback: planResult.planVoteFeedback } : {},
47783
+ ...planResult.planVoteRecordId !== void 0 ? { planVoteRecordId: planResult.planVoteRecordId } : {}
47779
47784
  };
47780
47785
  }
47781
47786
  async function resolveResearch(prior, task, stages, options) {
@@ -48044,7 +48049,16 @@ function buildApprovedPlanResult(plan, iterations, vote, sessionId) {
48044
48049
  approval: vote.approvalPercentage,
48045
48050
  sessionId
48046
48051
  });
48047
- return { plan, iterations };
48052
+ return {
48053
+ plan,
48054
+ iterations,
48055
+ planVoteDecision: "approved",
48056
+ planVoteApprovalPercentage: vote.approvalPercentage,
48057
+ ...recordIdOf(vote)
48058
+ };
48059
+ }
48060
+ function recordIdOf(vote) {
48061
+ return vote?.voteRecordId !== void 0 ? { planVoteRecordId: vote.voteRecordId } : {};
48048
48062
  }
48049
48063
  async function runPlanVote(plan, research, stages, iteration) {
48050
48064
  return withStep({ name: `vote (i=${String(iteration)})`, attrs: { iteration } }, async (ctx) => {
@@ -48071,8 +48085,10 @@ function buildNoQuorumPlanResult(plan, iterations, vote, retries) {
48071
48085
  plan,
48072
48086
  iterations,
48073
48087
  planStatus: "no_quorum",
48088
+ planVoteDecision: "no_quorum",
48074
48089
  planVoteReason: vote.reason,
48075
- planVoteApprovalPercentage: vote.approvalPercentage
48090
+ planVoteApprovalPercentage: vote.approvalPercentage,
48091
+ ...recordIdOf(vote)
48076
48092
  };
48077
48093
  }
48078
48094
  function buildUnapprovedPlanResult(plan, iterations, vote, sessionId) {
@@ -48088,8 +48104,11 @@ function buildUnapprovedPlanResult(plan, iterations, vote, sessionId) {
48088
48104
  plan,
48089
48105
  iterations,
48090
48106
  planStatus: "unapproved",
48107
+ // No vote ran at all only when the iteration limit is zero; don't claim one.
48108
+ ...vote !== void 0 ? { planVoteDecision: "rejected" } : {},
48091
48109
  planVoteApprovalPercentage: approvalPercentage,
48092
- planVoteFeedback: feedback
48110
+ planVoteFeedback: feedback,
48111
+ ...recordIdOf(vote)
48093
48112
  };
48094
48113
  }
48095
48114
  async function implementSingleTask(task, stages, limits) {
@@ -48433,6 +48452,13 @@ function classifyVoteStageResult(votingResult) {
48433
48452
  }
48434
48453
  return { vote: { kind: "approved", approvalPercentage: pct }, label: "Approved" };
48435
48454
  }
48455
+ function withRecordId(classified, recorded) {
48456
+ if (!recorded.voteRecord.persisted) return classified;
48457
+ return {
48458
+ vote: { ...classified.vote, voteRecordId: recorded.voteRecord.record.id },
48459
+ label: classified.label
48460
+ };
48461
+ }
48436
48462
  async function failClosedVote(config, start, error) {
48437
48463
  const msg = error instanceof Error ? error.message : String(error);
48438
48464
  emitStageEvent2("vote", "failed", { error: msg });
@@ -48463,7 +48489,7 @@ function createVoteStage({ config, startStage }) {
48463
48489
  const strategy = config.votingStrategy ?? "higher_order";
48464
48490
  await postProgress(config, "Vote", `Running consensus with ${strategy} strategy...`);
48465
48491
  try {
48466
- const { executeVoting } = await import("./consensus-vote-O7WRAIMH.js");
48492
+ const { executeVoting } = await import("./consensus-vote-L27LHIDJ.js");
48467
48493
  const proposal = buildVoteProposal(plan, research);
48468
48494
  const votingResult = await executeVoting(
48469
48495
  {
@@ -48479,8 +48505,11 @@ function createVoteStage({ config, startStage }) {
48479
48505
  // stage deadline or a job cancel.
48480
48506
  signal !== void 0 ? { signal } : void 0
48481
48507
  );
48482
- await recordCompletedVote(proposal, votingResult, logger46, { ...PIPELINE_VOTE, signal });
48483
- const { vote, label } = classifyVoteStageResult(votingResult);
48508
+ const recorded = await recordCompletedVote(proposal, votingResult, logger46, {
48509
+ ...PIPELINE_VOTE,
48510
+ signal
48511
+ });
48512
+ const { vote, label } = withRecordId(classifyVoteStageResult(votingResult), recorded);
48484
48513
  const ms = getTimeProvider().now() - start;
48485
48514
  emitStageEvent2("vote", "completed", { durationMs: ms });
48486
48515
  recordOutcome({
@@ -52931,10 +52960,11 @@ function classifyConsensusResult(record, mode) {
52931
52960
  const wouldBlock = typeof enforcement === "object" && enforcement !== null && enforcement["wouldBlock"] === true;
52932
52961
  if (mode === "enforce" && (BLOCKING_CONSENSUS_DECISIONS.has(record["decision"]) || wouldBlock)) {
52933
52962
  const reason = consensusReason(record);
52934
- return {
52935
- success: false,
52936
- failureReason: reason === record["decision"] ? `Consensus ${reason}` : `Consensus ${String(record["decision"])}: ${reason}`
52937
- };
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 };
52938
52968
  }
52939
52969
  return { success: true };
52940
52970
  }
@@ -52954,14 +52984,16 @@ function classifyEngineResult(result, mode = "audit") {
52954
52984
  }
52955
52985
  return classifyConsensusResult(record, mode);
52956
52986
  }
52957
- function recordOutcome2(deps, decision, start, success, failureReason) {
52987
+ function recordOutcome2(deps, decision, start, verdict) {
52988
+ const refused = verdict.gateRefusal !== void 0;
52958
52989
  const record = {
52959
52990
  decisionId: decision.decisionId,
52960
52991
  timestamp: new Date(getTimeProvider().now()).toISOString(),
52961
52992
  strategy: decision.strategy,
52962
- success,
52993
+ success: verdict.success || refused,
52963
52994
  durationMs: Math.max(0, getTimeProvider().now() - start),
52964
- ...failureReason !== void 0 ? { failureReason } : {}
52995
+ ...!refused && verdict.failureReason !== void 0 ? { failureReason: verdict.failureReason } : {},
52996
+ ...refused ? { gateRefusal: verdict.gateRefusal } : {}
52965
52997
  };
52966
52998
  deps.outcomeSink.recordOutcome(record);
52967
52999
  if (deps.onOutcome !== void 0) {
@@ -52981,7 +53013,7 @@ async function dispatchDecision(decision, input, deps) {
52981
53013
  const executor = deps.executors[strategy];
52982
53014
  if (executor === void 0) {
52983
53015
  const reason = `No executor registered for strategy "${strategy}"`;
52984
- recordOutcome2(deps, decision, start, false, reason);
53016
+ recordOutcome2(deps, decision, start, { success: false, failureReason: reason });
52985
53017
  deps.logger.error("MetaDispatcher dispatch failed", void 0, {
52986
53018
  decisionId,
52987
53019
  strategy,
@@ -52992,7 +53024,7 @@ async function dispatchDecision(decision, input, deps) {
52992
53024
  try {
52993
53025
  const result = await executor(decision, input);
52994
53026
  const classification = deps.classifyResult(result);
52995
- recordOutcome2(deps, decision, start, classification.success, classification.failureReason);
53027
+ recordOutcome2(deps, decision, start, classification);
52996
53028
  return {
52997
53029
  decisionId,
52998
53030
  strategy,
@@ -53001,7 +53033,7 @@ async function dispatchDecision(decision, input, deps) {
53001
53033
  };
53002
53034
  } catch (err2) {
53003
53035
  const reason = errorMessage(err2);
53004
- recordOutcome2(deps, decision, start, false, reason);
53036
+ recordOutcome2(deps, decision, start, { success: false, failureReason: reason });
53005
53037
  deps.logger.error(
53006
53038
  "MetaDispatcher strategy executor threw",
53007
53039
  err2 instanceof Error ? err2 : void 0,
@@ -53916,7 +53948,10 @@ function buildStructuredOutput(result, simulated) {
53916
53948
  ...simulated ? { simulated: true } : {},
53917
53949
  completed: result.completed,
53918
53950
  ...changesField(result),
53951
+ // Kept for compatibility: `false` here is NOT a verdict unless `securityRan`
53952
+ // is true. `security` below is the field that cannot misread (#7181).
53919
53953
  securityPassed: result.securityPassed,
53954
+ security: securityStatus(result),
53920
53955
  // #4772: these two are what make `completed: false` legible. Without them a
53921
53956
  // caller cannot tell a failed planner from a successful dry run, or a
53922
53957
  // security rejection from a gate that never ran — which is the whole point
@@ -53928,6 +53963,8 @@ function buildStructuredOutput(result, simulated) {
53928
53963
  ...result.planVoteReason !== void 0 ? { planVoteReason: result.planVoteReason } : {},
53929
53964
  ...result.planVoteApprovalPercentage !== void 0 ? { planVoteApprovalPercentage: result.planVoteApprovalPercentage } : {},
53930
53965
  ...result.planVoteFeedback !== void 0 ? { planVoteFeedback: result.planVoteFeedback } : {},
53966
+ // #7181: the dry run's verdict, which otherwise reached only the ledger.
53967
+ ...planVoteVerdictFields(result),
53931
53968
  // #4993 added `dryRun` to DevPipelineResult for exactly the reason above —
53932
53969
  // it says `completed: false` was the request, not a fault — and then did
53933
53970
  // not list it here either. Same omission, same function, under the comment
@@ -53955,6 +53992,16 @@ function changesField(result) {
53955
53992
  function warningsField(result) {
53956
53993
  return result.warnings !== void 0 ? { warnings: result.warnings } : {};
53957
53994
  }
53995
+ function securityStatus(result) {
53996
+ if (result.securityRan !== true) return { status: "unmeasured" };
53997
+ return { status: result.securityPassed ? "passed" : "failed" };
53998
+ }
53999
+ function planVoteVerdictFields(result) {
54000
+ return {
54001
+ ...result.planVoteDecision !== void 0 ? { planVoteDecision: result.planVoteDecision } : {},
54002
+ ...result.planVoteRecordId !== void 0 ? { planVoteRecordId: result.planVoteRecordId } : {}
54003
+ };
54004
+ }
53958
54005
 
53959
54006
  // src/mcp/tools/dev-pipeline-tool.ts
53960
54007
  import { resolve as resolve10 } from "node:path";
@@ -54850,6 +54897,7 @@ function buildShadowTrainObserver(logger58) {
54850
54897
  decisionId: decision.decisionId,
54851
54898
  strategy: decision.strategy,
54852
54899
  success: record.success,
54900
+ ...record.gateRefusal !== void 0 ? { gateRefusal: record.gateRefusal } : {},
54853
54901
  armStats: getShadowSelector().stats()
54854
54902
  });
54855
54903
  }
@@ -58425,4 +58473,4 @@ export {
58425
58473
  initializeSandbox,
58426
58474
  getSandboxMode
58427
58475
  };
58428
- //# sourceMappingURL=chunk-TGUUUWJO.js.map
58476
+ //# sourceMappingURL=chunk-KZBDFSDB.js.map