nexus-agents 6.1.1 → 6.1.3

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 (27) hide show
  1. package/dist/chunk-2FXH2HEW.js +7 -0
  2. package/dist/{chunk-OHQ5JDJX.js → chunk-2PEHRCZ2.js} +8 -2
  3. package/dist/{chunk-OHQ5JDJX.js.map → chunk-2PEHRCZ2.js.map} +1 -1
  4. package/dist/{chunk-3K6DNH62.js → chunk-4FQGYWI4.js} +6 -6
  5. package/dist/{chunk-TPO3KDE3.js → chunk-767D24JM.js} +2 -2
  6. package/dist/{chunk-HE76P4LG.js → chunk-BZUZ6P6O.js} +3 -3
  7. package/dist/{chunk-PE4W7G7S.js → chunk-KTVP7FRA.js} +2 -2
  8. package/dist/{chunk-FWRH2IEJ.js → chunk-LLGYR7KF.js} +2 -2
  9. package/dist/cli.js +34 -18
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{consensus-vote-RULORCL2.js → consensus-vote-M6LEXIR7.js} +4 -4
  12. package/dist/{improvement-review-36HJKKCR.js → improvement-review-YW7KHPFJ.js} +4 -4
  13. package/dist/index.js +7 -7
  14. package/dist/{pr-reviewer-helpers-ZQJE6G6S.js → pr-reviewer-helpers-BVQYKJQC.js} +4 -2
  15. package/dist/{setup-data-dir-XRUWUIYA.js → setup-data-dir-SUFIDLFX.js} +3 -3
  16. package/package.json +1 -1
  17. package/dist/chunk-YVRYIIHX.js +0 -7
  18. /package/dist/{chunk-YVRYIIHX.js.map → chunk-2FXH2HEW.js.map} +0 -0
  19. /package/dist/{chunk-3K6DNH62.js.map → chunk-4FQGYWI4.js.map} +0 -0
  20. /package/dist/{chunk-TPO3KDE3.js.map → chunk-767D24JM.js.map} +0 -0
  21. /package/dist/{chunk-HE76P4LG.js.map → chunk-BZUZ6P6O.js.map} +0 -0
  22. /package/dist/{chunk-PE4W7G7S.js.map → chunk-KTVP7FRA.js.map} +0 -0
  23. /package/dist/{chunk-FWRH2IEJ.js.map → chunk-LLGYR7KF.js.map} +0 -0
  24. /package/dist/{consensus-vote-RULORCL2.js.map → consensus-vote-M6LEXIR7.js.map} +0 -0
  25. /package/dist/{improvement-review-36HJKKCR.js.map → improvement-review-YW7KHPFJ.js.map} +0 -0
  26. /package/dist/{pr-reviewer-helpers-ZQJE6G6S.js.map → pr-reviewer-helpers-BVQYKJQC.js.map} +0 -0
  27. /package/dist/{setup-data-dir-XRUWUIYA.js.map → setup-data-dir-SUFIDLFX.js.map} +0 -0
@@ -0,0 +1,7 @@
1
+ // src/version.ts
2
+ var VERSION = true ? "6.1.3" : "dev";
3
+
4
+ export {
5
+ VERSION
6
+ };
7
+ //# sourceMappingURL=chunk-2FXH2HEW.js.map
@@ -276,6 +276,11 @@ function createFailedReview(expertId, category, durationMs, error) {
276
276
  confidence: 0
277
277
  };
278
278
  }
279
+ function reviewPostingBlock(verdict) {
280
+ if (verdict.allowed) return void 0;
281
+ const label = verdict.hasRuleOfTwoViolation ? "Rule of Two" : "Policy gate";
282
+ return { label, reason: `${label}: ${verdict.violations.map((v) => v.rule).join(", ")}` };
283
+ }
279
284
 
280
285
  export {
281
286
  fetchAccountAgeDays,
@@ -295,6 +300,7 @@ export {
295
300
  sumFindings,
296
301
  generateSummary,
297
302
  formatReviewComment,
298
- createFailedReview
303
+ createFailedReview,
304
+ reviewPostingBlock
299
305
  };
300
- //# sourceMappingURL=chunk-OHQ5JDJX.js.map
306
+ //# sourceMappingURL=chunk-2PEHRCZ2.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/dogfooding/pr-reviewer-helpers.ts"],"sourcesContent":["/**\n * nexus-agents/dogfooding - PR Reviewer Helpers\n *\n * Helper functions for PR review formatting and aggregation.\n *\n * @module dogfooding/pr-reviewer-helpers\n * (Source: Issue #161, Alignment Roadmap Phase 3)\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { Result } from '../core/index.js';\nimport { getTimeProvider, createLogger } from '../core/index.js';\nimport type { ScmUserMetadata } from '../scm/types.js';\nimport type {\n PRMetadata,\n PRReviewDraft,\n PRTrustAssessment,\n ExpertReviewResult,\n ReviewFinding,\n ReviewCategory,\n ReviewSeverity,\n ReviewDecision,\n} from './pr-review-types.js';\nimport {\n SEVERITY_ORDER,\n CATEGORY_DISPLAY_NAMES,\n SEVERITY_EMOJI,\n DECISION_EMOJI,\n} from './pr-review-types.js';\nimport { sanitizeInput } from '../security/input-sanitizer.js';\nimport { allOf } from '../utils/verdict-aggregation.js';\nimport {\n assessReputation,\n gateWithReputation,\n resolveReputationGatingMode,\n} from '../security/reputation-model.js';\nimport type {\n ReputationCache,\n ReputationGateDecision,\n ReputationAssessment,\n GitHubUserMetadata,\n} from '../security/reputation-model.js';\nimport type { ClassifyResult } from '../security/trust-classifier.js';\n\nconst repLogger = createLogger({ component: 'PRReviewer.reputation' });\n\n// =============================================================================\n// Reputation Gating Helpers (#3123, epic #3118 Phase 5)\n// =============================================================================\n\n/**\n * Best-effort account-age lookup for a PR author (#3133, Phase-3 equivalent for\n * the PR path). Returns the author's real account age in days via the provider's\n * `fetchUserMetadata`, or `undefined` on any failure (err Result, unparseable\n * `createdAt`, or an unexpected rejection) — never fabricated, never throws, so\n * the review is never blocked by this lookup.\n */\nexport async function fetchAccountAgeDays(\n provider: { fetchUserMetadata: (u: string) => Promise<Result<ScmUserMetadata, Error>> },\n username: string\n): Promise<number | undefined> {\n try {\n const result = await provider.fetchUserMetadata(username);\n if (!result.ok) return undefined;\n const createdMs = Date.parse(result.value.createdAt);\n if (!Number.isFinite(createdMs)) return undefined;\n return Math.floor((getTimeProvider().now() - createdMs) / 86_400_000);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Assesses the PR author's reputation from the signals available in the PR\n * event: author association + injection flags from the (sanitized) PR body, plus\n * the author's real account age when it was fetched (#3133). `accountAgeDays` is\n * OMITTED when the lookup failed — never fabricated, so the engine skips the\n * `new_account` signal. Returns undefined when reputation is disabled.\n */\nexport function assessPRReputation(\n pr: PRMetadata,\n cache: ReputationCache,\n enableReputation: boolean,\n accountAgeDays: number | undefined\n): ReputationAssessment | undefined {\n if (!enableReputation) return undefined;\n // Only `injectionFlags` is consumed here, and injection detection is\n // role-independent — the userRole arg ('unknown') and the sanitizer's own\n // trustTier output are intentionally irrelevant to this call.\n const sanitizeResult = sanitizeInput(pr.body, 'unknown', pr.author);\n const metadata: GitHubUserMetadata = {\n username: pr.author,\n ...(accountAgeDays !== undefined ? { accountAgeDays } : {}),\n authorAssociation: pr.authorAssociation,\n injectionFlags: sanitizeResult.injectionFlags,\n };\n return assessReputation(metadata, cache);\n}\n\n/** Builds the observability assessment surfaced on the review result (#3123). */\nexport function buildPRTrustAssessment(\n trustResult: ClassifyResult,\n reputation: ReputationAssessment | undefined,\n gateDecision: ReputationGateDecision\n): PRTrustAssessment {\n // Tier-1 (owner/allowlisted) authors cannot be suspicious.\n const isTier1 = trustResult.trustTier === '1';\n return {\n trustTier: trustResult.trustTier,\n userRole: trustResult.userRole,\n isAllowlisted: trustResult.isAllowlisted,\n reputationScore: reputation?.reputationScore,\n suspiciousSignals: isTier1 ? [] : (reputation?.suspiciousSignals ?? []),\n isSuspicious: isTier1 ? false : (reputation?.isSuspicious ?? false),\n enforcedTrustTier: gateDecision.enforcedTier,\n reputationReconciledTier: gateDecision.reconciledTier,\n gatingMode: gateDecision.mode,\n };\n}\n\n/**\n * Assesses the PR author's reputation and applies the gating rollout mode\n * (#3123). Returns the gate decision (for the policy gate) and the assessment\n * surfaced on the result. A suppressed demotion (audit/off) is logged.\n */\nexport function gatePRAuthor(\n pr: PRMetadata,\n trustResult: ClassifyResult,\n accountAgeDays: number | undefined,\n cache: ReputationCache,\n enableReputation: boolean\n): { gateDecision: ReputationGateDecision; trustAssessment: PRTrustAssessment } {\n const reputation = assessPRReputation(pr, cache, enableReputation, accountAgeDays);\n const gateDecision = gateWithReputation(\n trustResult.trustTier,\n reputation,\n resolveReputationGatingMode()\n );\n if (gateDecision.demotionSuppressed) {\n repLogger.warn('Reputation demotion suppressed by gating mode (would block under enforce)', {\n prNumber: pr.number,\n author: pr.author,\n mode: gateDecision.mode,\n classifierTier: trustResult.trustTier,\n reconciledTier: gateDecision.reconciledTier,\n });\n }\n return {\n gateDecision,\n trustAssessment: buildPRTrustAssessment(trustResult, reputation, gateDecision),\n };\n}\n\n// =============================================================================\n// Parsing Helpers\n// =============================================================================\n\nexport function parseSeverity(value: unknown): ReviewSeverity {\n if (typeof value === 'string') {\n const lower = value.toLowerCase();\n if (lower in SEVERITY_ORDER) return lower as ReviewSeverity;\n }\n return 'medium';\n}\n\nexport function parseCategory(value: unknown): ReviewCategory {\n if (typeof value === 'string') {\n const lower = value.toLowerCase();\n if (lower in CATEGORY_DISPLAY_NAMES) return lower as ReviewCategory;\n }\n return 'code_quality';\n}\n\nexport function extractSummary(output: Record<string, unknown>): string {\n if (typeof output.summary === 'string') return output.summary;\n if (typeof output.content === 'string') return output.content;\n if (typeof output.message === 'string') return output.message;\n return 'Review completed';\n}\n\nexport function extractStringField(\n record: Record<string, unknown>,\n ...keys: string[]\n): string | undefined {\n for (const key of keys) {\n const value = record[key];\n if (typeof value === 'string') return value;\n }\n return undefined;\n}\n\n// =============================================================================\n// Finding Parsing\n// =============================================================================\n\nexport function parseFindings(\n output: Record<string, unknown>,\n expertId: string,\n minSeverity: ReviewSeverity\n): ReviewFinding[] {\n const minOrder = SEVERITY_ORDER[minSeverity];\n const sources = collectSources(output);\n\n const findings: ReviewFinding[] = [];\n for (const source of sources) {\n if (!Array.isArray(source)) continue;\n for (const item of source) {\n const finding = parseOneFinding(item, expertId, minOrder);\n if (finding !== null) findings.push(finding);\n }\n }\n return findings;\n}\n\nfunction collectSources(output: Record<string, unknown>): unknown[] {\n return [\n output.findings,\n output.vulnerabilities,\n output.issues,\n (output as { content?: { findings?: unknown } }).content,\n ];\n}\n\nfunction parseOneFinding(item: unknown, expertId: string, minOrder: number): ReviewFinding | null {\n if (typeof item !== 'object' || item === null) return null;\n\n const record = item as Record<string, unknown>;\n const severity = parseSeverity(record.severity);\n if (SEVERITY_ORDER[severity] < minOrder) return null;\n\n return {\n id: randomUUID(),\n category: parseCategory(record.category),\n severity,\n title: extractStringField(record, 'title', 'name') ?? 'Finding',\n description: extractStringField(record, 'description', 'message') ?? '',\n file: typeof record.file === 'string' ? record.file : undefined,\n line: typeof record.line === 'number' ? record.line : undefined,\n suggestion: typeof record.suggestion === 'string' ? record.suggestion : undefined,\n expertId,\n confidence: typeof record.confidence === 'number' ? record.confidence : 0.7,\n };\n}\n\n// =============================================================================\n// Decision Helpers\n// =============================================================================\n\nexport function determineApproval(findings: ReviewFinding[]): boolean {\n const hasBlocking = findings.some((f) => f.severity === 'critical' || f.severity === 'high');\n return !hasBlocking;\n}\n\nexport function determineDecision(\n reviews: ExpertReviewResult[],\n findings: ReviewFinding[]\n): ReviewDecision {\n // #5012: an expert that errored produced no verdict, so it can neither\n // approve nor object. `allOf(reviews, …, false)` already refuses to call\n // ZERO reviews unanimous approval (#4581); feeding it synthetic approvals\n // for failed experts defeated that guard by making the list non-empty.\n const verdicts = reviews.filter((r) => r.errored !== true);\n const hasCritical = findings.some((f) => f.severity === 'critical');\n const hasHigh = findings.some((f) => f.severity === 'high');\n // Zero expert reviews is not unanimous approval (#4581): with `true` here the\n // `hasHigh && !allApproved` branch could never fire on an unreviewed PR, so a\n // HIGH finding silently downgraded from request_changes to comment.\n const allApproved = allOf(verdicts, (r) => r.approved, false);\n\n if (hasCritical) return 'request_changes';\n if (hasHigh && !allApproved) return 'request_changes';\n if (findings.length > 0) return 'comment';\n // Nothing read the diff, so there is nothing to approve. `comment` posts the\n // failure summaries without asserting the change is fine.\n if (verdicts.length === 0) return 'comment';\n return 'approve';\n}\n\nexport function calculateConsensus(reviews: ExpertReviewResult[]): number {\n if (reviews.length === 0) return 1;\n const approvals = reviews.filter((r) => r.approved).length;\n return approvals / reviews.length;\n}\n\n// =============================================================================\n// Counting Helpers\n// =============================================================================\n\nexport function countBySeverity(findings: ReviewFinding[]): Record<ReviewSeverity, number> {\n const counts: Record<ReviewSeverity, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n\n for (const f of findings) {\n counts[f.severity]++;\n }\n\n return counts;\n}\n\nexport function countByCategory(findings: ReviewFinding[]): Record<ReviewCategory, number> {\n const counts: Record<ReviewCategory, number> = {\n security: 0,\n performance: 0,\n code_quality: 0,\n testing: 0,\n documentation: 0,\n architecture: 0,\n };\n\n for (const f of findings) {\n counts[f.category]++;\n }\n\n return counts;\n}\n\nexport function sumFindings(counts: Record<ReviewSeverity, number>): number {\n return Object.values(counts).reduce((a, b) => a + b, 0);\n}\n\n// =============================================================================\n// Summary Generation\n// =============================================================================\n\nexport function generateSummary(\n pr: PRMetadata,\n reviews: ExpertReviewResult[],\n decision: ReviewDecision\n): string {\n const expertSummaries = reviews\n .map((r) => `- **${CATEGORY_DISPLAY_NAMES[r.expertType as ReviewCategory]}**: ${r.summary}`)\n .join('\\n');\n\n return `Reviewed PR #${String(pr.number)}: ${pr.title}\n\n**Decision:** ${decision.replaceAll('_', ' ')}\n**Experts consulted:** ${String(reviews.length)}\n\n${expertSummaries}`;\n}\n\n// =============================================================================\n// GitHub Comment Formatting\n// =============================================================================\n\n/**\n * Formats the review result as a GitHub comment.\n */\nexport function formatReviewComment(result: PRReviewDraft): string {\n const emoji = DECISION_EMOJI[result.decision];\n const decisionText = result.decision.replaceAll('_', ' ').toUpperCase();\n\n const findingsSection = formatFindingsSection(result);\n const statsSection = formatStatsSection(result);\n\n return `## ${emoji} Nexus Agents Review: ${decisionText}\n\n${result.summary}\n\n${findingsSection}\n\n${statsSection}\n\n---\n*Reviewed by [nexus-agents](https://github.com/nexus-substrate/nexus-agents) in ${String(result.totalDurationMs)}ms*`;\n}\n\nfunction formatFindingsSection(result: PRReviewDraft): string {\n const allFindings = result.expertReviews.flatMap((r) => r.findings);\n\n if (allFindings.length === 0) {\n return '_No issues found._';\n }\n\n const sorted = [...allFindings].sort(\n (a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity]\n );\n\n const lines = ['### Findings', ''];\n\n for (const f of sorted) {\n const emoji = SEVERITY_EMOJI[f.severity];\n const loc =\n f.file !== undefined\n ? ` (\\`${f.file}${f.line !== undefined ? `:${String(f.line)}` : ''}\\`)`\n : '';\n lines.push(`${emoji} **${f.title}**${loc}`);\n lines.push(`> ${f.description}`);\n if (f.suggestion !== undefined) {\n lines.push(`> 💡 ${f.suggestion}`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction formatStatsSection(result: PRReviewDraft): string {\n const { findingsBySeverity } = result;\n const total = sumFindings(findingsBySeverity);\n\n const parts: string[] = [];\n for (const severity of ['critical', 'high', 'medium', 'low', 'info'] as ReviewSeverity[]) {\n const count = findingsBySeverity[severity];\n if (count > 0) {\n parts.push(`${SEVERITY_EMOJI[severity]} ${String(count)} ${severity}`);\n }\n }\n\n return `<details>\n<summary>Review Statistics (${String(total)} findings)</summary>\n\n- Experts: ${String(result.expertCount)}\n- Consensus: ${(result.consensusScore * 100).toFixed(0)}%\n- Duration: ${String(result.totalDurationMs)}ms\n- Findings: ${parts.join(', ') || 'none'}\n\n</details>`;\n}\n\n// =============================================================================\n// Failed Review Factory\n// =============================================================================\n\nexport function createFailedReview(\n expertId: string,\n category: ReviewCategory,\n durationMs: number,\n error: string\n): ExpertReviewResult {\n return {\n expertId,\n expertType: category,\n // #5012: NOT `approved: true`. \"Don't block on failures\" made an expert\n // that never ran indistinguishable from one that read the diff and had no\n // objection, and `determineDecision` turned three of those into an APPROVE\n // posted to GitHub.\n approved: false,\n errored: true,\n summary: `Review failed: ${error}`,\n findings: [],\n durationMs,\n confidence: 0,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/dogfooding/pr-reviewer-helpers.ts"],"sourcesContent":["/**\n * nexus-agents/dogfooding - PR Reviewer Helpers\n *\n * Helper functions for PR review formatting and aggregation.\n *\n * @module dogfooding/pr-reviewer-helpers\n * (Source: Issue #161, Alignment Roadmap Phase 3)\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { Result } from '../core/index.js';\nimport { getTimeProvider, createLogger } from '../core/index.js';\nimport type { ScmUserMetadata } from '../scm/types.js';\nimport type {\n PRMetadata,\n PRReviewDraft,\n PRTrustAssessment,\n ExpertReviewResult,\n ReviewFinding,\n ReviewCategory,\n ReviewSeverity,\n ReviewDecision,\n} from './pr-review-types.js';\nimport {\n SEVERITY_ORDER,\n CATEGORY_DISPLAY_NAMES,\n SEVERITY_EMOJI,\n DECISION_EMOJI,\n} from './pr-review-types.js';\nimport { sanitizeInput } from '../security/input-sanitizer.js';\nimport { allOf } from '../utils/verdict-aggregation.js';\nimport {\n assessReputation,\n gateWithReputation,\n resolveReputationGatingMode,\n} from '../security/reputation-model.js';\nimport type {\n ReputationCache,\n ReputationGateDecision,\n ReputationAssessment,\n GitHubUserMetadata,\n} from '../security/reputation-model.js';\nimport type { ClassifyResult } from '../security/trust-classifier.js';\n\nconst repLogger = createLogger({ component: 'PRReviewer.reputation' });\n\n// =============================================================================\n// Reputation Gating Helpers (#3123, epic #3118 Phase 5)\n// =============================================================================\n\n/**\n * Best-effort account-age lookup for a PR author (#3133, Phase-3 equivalent for\n * the PR path). Returns the author's real account age in days via the provider's\n * `fetchUserMetadata`, or `undefined` on any failure (err Result, unparseable\n * `createdAt`, or an unexpected rejection) — never fabricated, never throws, so\n * the review is never blocked by this lookup.\n */\nexport async function fetchAccountAgeDays(\n provider: { fetchUserMetadata: (u: string) => Promise<Result<ScmUserMetadata, Error>> },\n username: string\n): Promise<number | undefined> {\n try {\n const result = await provider.fetchUserMetadata(username);\n if (!result.ok) return undefined;\n const createdMs = Date.parse(result.value.createdAt);\n if (!Number.isFinite(createdMs)) return undefined;\n return Math.floor((getTimeProvider().now() - createdMs) / 86_400_000);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Assesses the PR author's reputation from the signals available in the PR\n * event: author association + injection flags from the (sanitized) PR body, plus\n * the author's real account age when it was fetched (#3133). `accountAgeDays` is\n * OMITTED when the lookup failed — never fabricated, so the engine skips the\n * `new_account` signal. Returns undefined when reputation is disabled.\n */\nexport function assessPRReputation(\n pr: PRMetadata,\n cache: ReputationCache,\n enableReputation: boolean,\n accountAgeDays: number | undefined\n): ReputationAssessment | undefined {\n if (!enableReputation) return undefined;\n // Only `injectionFlags` is consumed here, and injection detection is\n // role-independent — the userRole arg ('unknown') and the sanitizer's own\n // trustTier output are intentionally irrelevant to this call.\n const sanitizeResult = sanitizeInput(pr.body, 'unknown', pr.author);\n const metadata: GitHubUserMetadata = {\n username: pr.author,\n ...(accountAgeDays !== undefined ? { accountAgeDays } : {}),\n authorAssociation: pr.authorAssociation,\n injectionFlags: sanitizeResult.injectionFlags,\n };\n return assessReputation(metadata, cache);\n}\n\n/** Builds the observability assessment surfaced on the review result (#3123). */\nexport function buildPRTrustAssessment(\n trustResult: ClassifyResult,\n reputation: ReputationAssessment | undefined,\n gateDecision: ReputationGateDecision\n): PRTrustAssessment {\n // Tier-1 (owner/allowlisted) authors cannot be suspicious.\n const isTier1 = trustResult.trustTier === '1';\n return {\n trustTier: trustResult.trustTier,\n userRole: trustResult.userRole,\n isAllowlisted: trustResult.isAllowlisted,\n reputationScore: reputation?.reputationScore,\n suspiciousSignals: isTier1 ? [] : (reputation?.suspiciousSignals ?? []),\n isSuspicious: isTier1 ? false : (reputation?.isSuspicious ?? false),\n enforcedTrustTier: gateDecision.enforcedTier,\n reputationReconciledTier: gateDecision.reconciledTier,\n gatingMode: gateDecision.mode,\n };\n}\n\n/**\n * Assesses the PR author's reputation and applies the gating rollout mode\n * (#3123). Returns the gate decision (for the policy gate) and the assessment\n * surfaced on the result. A suppressed demotion (audit/off) is logged.\n */\nexport function gatePRAuthor(\n pr: PRMetadata,\n trustResult: ClassifyResult,\n accountAgeDays: number | undefined,\n cache: ReputationCache,\n enableReputation: boolean\n): { gateDecision: ReputationGateDecision; trustAssessment: PRTrustAssessment } {\n const reputation = assessPRReputation(pr, cache, enableReputation, accountAgeDays);\n const gateDecision = gateWithReputation(\n trustResult.trustTier,\n reputation,\n resolveReputationGatingMode()\n );\n if (gateDecision.demotionSuppressed) {\n repLogger.warn('Reputation demotion suppressed by gating mode (would block under enforce)', {\n prNumber: pr.number,\n author: pr.author,\n mode: gateDecision.mode,\n classifierTier: trustResult.trustTier,\n reconciledTier: gateDecision.reconciledTier,\n });\n }\n return {\n gateDecision,\n trustAssessment: buildPRTrustAssessment(trustResult, reputation, gateDecision),\n };\n}\n\n// =============================================================================\n// Parsing Helpers\n// =============================================================================\n\nexport function parseSeverity(value: unknown): ReviewSeverity {\n if (typeof value === 'string') {\n const lower = value.toLowerCase();\n if (lower in SEVERITY_ORDER) return lower as ReviewSeverity;\n }\n return 'medium';\n}\n\nexport function parseCategory(value: unknown): ReviewCategory {\n if (typeof value === 'string') {\n const lower = value.toLowerCase();\n if (lower in CATEGORY_DISPLAY_NAMES) return lower as ReviewCategory;\n }\n return 'code_quality';\n}\n\nexport function extractSummary(output: Record<string, unknown>): string {\n if (typeof output.summary === 'string') return output.summary;\n if (typeof output.content === 'string') return output.content;\n if (typeof output.message === 'string') return output.message;\n return 'Review completed';\n}\n\nexport function extractStringField(\n record: Record<string, unknown>,\n ...keys: string[]\n): string | undefined {\n for (const key of keys) {\n const value = record[key];\n if (typeof value === 'string') return value;\n }\n return undefined;\n}\n\n// =============================================================================\n// Finding Parsing\n// =============================================================================\n\nexport function parseFindings(\n output: Record<string, unknown>,\n expertId: string,\n minSeverity: ReviewSeverity\n): ReviewFinding[] {\n const minOrder = SEVERITY_ORDER[minSeverity];\n const sources = collectSources(output);\n\n const findings: ReviewFinding[] = [];\n for (const source of sources) {\n if (!Array.isArray(source)) continue;\n for (const item of source) {\n const finding = parseOneFinding(item, expertId, minOrder);\n if (finding !== null) findings.push(finding);\n }\n }\n return findings;\n}\n\nfunction collectSources(output: Record<string, unknown>): unknown[] {\n return [\n output.findings,\n output.vulnerabilities,\n output.issues,\n (output as { content?: { findings?: unknown } }).content,\n ];\n}\n\nfunction parseOneFinding(item: unknown, expertId: string, minOrder: number): ReviewFinding | null {\n if (typeof item !== 'object' || item === null) return null;\n\n const record = item as Record<string, unknown>;\n const severity = parseSeverity(record.severity);\n if (SEVERITY_ORDER[severity] < minOrder) return null;\n\n return {\n id: randomUUID(),\n category: parseCategory(record.category),\n severity,\n title: extractStringField(record, 'title', 'name') ?? 'Finding',\n description: extractStringField(record, 'description', 'message') ?? '',\n file: typeof record.file === 'string' ? record.file : undefined,\n line: typeof record.line === 'number' ? record.line : undefined,\n suggestion: typeof record.suggestion === 'string' ? record.suggestion : undefined,\n expertId,\n confidence: typeof record.confidence === 'number' ? record.confidence : 0.7,\n };\n}\n\n// =============================================================================\n// Decision Helpers\n// =============================================================================\n\nexport function determineApproval(findings: ReviewFinding[]): boolean {\n const hasBlocking = findings.some((f) => f.severity === 'critical' || f.severity === 'high');\n return !hasBlocking;\n}\n\nexport function determineDecision(\n reviews: ExpertReviewResult[],\n findings: ReviewFinding[]\n): ReviewDecision {\n // #5012: an expert that errored produced no verdict, so it can neither\n // approve nor object. `allOf(reviews, …, false)` already refuses to call\n // ZERO reviews unanimous approval (#4581); feeding it synthetic approvals\n // for failed experts defeated that guard by making the list non-empty.\n const verdicts = reviews.filter((r) => r.errored !== true);\n const hasCritical = findings.some((f) => f.severity === 'critical');\n const hasHigh = findings.some((f) => f.severity === 'high');\n // Zero expert reviews is not unanimous approval (#4581): with `true` here the\n // `hasHigh && !allApproved` branch could never fire on an unreviewed PR, so a\n // HIGH finding silently downgraded from request_changes to comment.\n const allApproved = allOf(verdicts, (r) => r.approved, false);\n\n if (hasCritical) return 'request_changes';\n if (hasHigh && !allApproved) return 'request_changes';\n if (findings.length > 0) return 'comment';\n // Nothing read the diff, so there is nothing to approve. `comment` posts the\n // failure summaries without asserting the change is fine.\n if (verdicts.length === 0) return 'comment';\n return 'approve';\n}\n\nexport function calculateConsensus(reviews: ExpertReviewResult[]): number {\n if (reviews.length === 0) return 1;\n const approvals = reviews.filter((r) => r.approved).length;\n return approvals / reviews.length;\n}\n\n// =============================================================================\n// Counting Helpers\n// =============================================================================\n\nexport function countBySeverity(findings: ReviewFinding[]): Record<ReviewSeverity, number> {\n const counts: Record<ReviewSeverity, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n\n for (const f of findings) {\n counts[f.severity]++;\n }\n\n return counts;\n}\n\nexport function countByCategory(findings: ReviewFinding[]): Record<ReviewCategory, number> {\n const counts: Record<ReviewCategory, number> = {\n security: 0,\n performance: 0,\n code_quality: 0,\n testing: 0,\n documentation: 0,\n architecture: 0,\n };\n\n for (const f of findings) {\n counts[f.category]++;\n }\n\n return counts;\n}\n\nexport function sumFindings(counts: Record<ReviewSeverity, number>): number {\n return Object.values(counts).reduce((a, b) => a + b, 0);\n}\n\n// =============================================================================\n// Summary Generation\n// =============================================================================\n\nexport function generateSummary(\n pr: PRMetadata,\n reviews: ExpertReviewResult[],\n decision: ReviewDecision\n): string {\n const expertSummaries = reviews\n .map((r) => `- **${CATEGORY_DISPLAY_NAMES[r.expertType as ReviewCategory]}**: ${r.summary}`)\n .join('\\n');\n\n return `Reviewed PR #${String(pr.number)}: ${pr.title}\n\n**Decision:** ${decision.replaceAll('_', ' ')}\n**Experts consulted:** ${String(reviews.length)}\n\n${expertSummaries}`;\n}\n\n// =============================================================================\n// GitHub Comment Formatting\n// =============================================================================\n\n/**\n * Formats the review result as a GitHub comment.\n */\nexport function formatReviewComment(result: PRReviewDraft): string {\n const emoji = DECISION_EMOJI[result.decision];\n const decisionText = result.decision.replaceAll('_', ' ').toUpperCase();\n\n const findingsSection = formatFindingsSection(result);\n const statsSection = formatStatsSection(result);\n\n return `## ${emoji} Nexus Agents Review: ${decisionText}\n\n${result.summary}\n\n${findingsSection}\n\n${statsSection}\n\n---\n*Reviewed by [nexus-agents](https://github.com/nexus-substrate/nexus-agents) in ${String(result.totalDurationMs)}ms*`;\n}\n\nfunction formatFindingsSection(result: PRReviewDraft): string {\n const allFindings = result.expertReviews.flatMap((r) => r.findings);\n\n if (allFindings.length === 0) {\n return '_No issues found._';\n }\n\n const sorted = [...allFindings].sort(\n (a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity]\n );\n\n const lines = ['### Findings', ''];\n\n for (const f of sorted) {\n const emoji = SEVERITY_EMOJI[f.severity];\n const loc =\n f.file !== undefined\n ? ` (\\`${f.file}${f.line !== undefined ? `:${String(f.line)}` : ''}\\`)`\n : '';\n lines.push(`${emoji} **${f.title}**${loc}`);\n lines.push(`> ${f.description}`);\n if (f.suggestion !== undefined) {\n lines.push(`> 💡 ${f.suggestion}`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction formatStatsSection(result: PRReviewDraft): string {\n const { findingsBySeverity } = result;\n const total = sumFindings(findingsBySeverity);\n\n const parts: string[] = [];\n for (const severity of ['critical', 'high', 'medium', 'low', 'info'] as ReviewSeverity[]) {\n const count = findingsBySeverity[severity];\n if (count > 0) {\n parts.push(`${SEVERITY_EMOJI[severity]} ${String(count)} ${severity}`);\n }\n }\n\n return `<details>\n<summary>Review Statistics (${String(total)} findings)</summary>\n\n- Experts: ${String(result.expertCount)}\n- Consensus: ${(result.consensusScore * 100).toFixed(0)}%\n- Duration: ${String(result.totalDurationMs)}ms\n- Findings: ${parts.join(', ') || 'none'}\n\n</details>`;\n}\n\n// =============================================================================\n// Failed Review Factory\n// =============================================================================\n\nexport function createFailedReview(\n expertId: string,\n category: ReviewCategory,\n durationMs: number,\n error: string\n): ExpertReviewResult {\n return {\n expertId,\n expertType: category,\n // #5012: NOT `approved: true`. \"Don't block on failures\" made an expert\n // that never ran indistinguishable from one that read the diff and had no\n // objection, and `determineDecision` turned three of those into an APPROVE\n // posted to GitHub.\n approved: false,\n errored: true,\n summary: `Review failed: ${error}`,\n findings: [],\n durationMs,\n confidence: 0,\n };\n}\n\n/** The policy gate's verdict on posting a review, as the caller needs it. */\ninterface ReviewPostingVerdict {\n readonly allowed: boolean;\n readonly hasRuleOfTwoViolation: boolean;\n readonly violations: readonly { rule: string }[];\n}\n\n/**\n * Decide whether review posting is blocked, and how to describe it.\n *\n * Consumes the gate's own `allowed` rather than re-deriving a narrower\n * condition. The caller previously blocked on `hasRuleOfTwoViolation` alone\n * while `evaluatePolicy` had already computed `allowed`.\n *\n * For the action `auditReviewAction` builds, the two are currently EQUIVALENT:\n * tiers 1-2 yield no violations, tiers 3-4 yield INSUFFICIENT_TRUST +\n * UNTRUSTED_INFLUENCE + RULE_OF_TWO together. So this changes no behaviour\n * today. It makes the equivalence guaranteed rather than accidental — it holds\n * only because that context hardcodes `hasWriteAccess` and `hasSecretAccess`\n * to true, which is what makes `checkRuleOfTwo` fire at tier 3+. Make either\n * conditional and RULE_OF_TWO stops firing while the other two blocking rules\n * still do, and a review would post against `allowed: false`.\n *\n * Returns `undefined` when posting may proceed. Rule of Two keeps its own\n * label: it is the distinctive condition (untrusted input + write access +\n * secrets at once) and reads very differently from a trust-tier block.\n */\nexport function reviewPostingBlock(\n verdict: ReviewPostingVerdict\n): { label: string; reason: string } | undefined {\n if (verdict.allowed) return undefined;\n const label = verdict.hasRuleOfTwoViolation ? 'Rule of Two' : 'Policy gate';\n return { label, reason: `${label}: ${verdict.violations.map((v) => v.rule).join(', ')}` };\n}\n"],"mappings":"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","names":[]}
@@ -21,7 +21,7 @@ import {
21
21
  import {
22
22
  DEFAULT_TASK_TTL_MS,
23
23
  clampTaskTtl
24
- } from "./chunk-HE76P4LG.js";
24
+ } from "./chunk-BZUZ6P6O.js";
25
25
  import {
26
26
  executeExpert
27
27
  } from "./chunk-GHD3CUV7.js";
@@ -47,7 +47,7 @@ import {
47
47
  simulationDeniedResult,
48
48
  toJobSummary,
49
49
  writeJobCancelled
50
- } from "./chunk-PE4W7G7S.js";
50
+ } from "./chunk-KTVP7FRA.js";
51
51
  import {
52
52
  allOf
53
53
  } from "./chunk-OQB3XC5O.js";
@@ -64,7 +64,7 @@ import {
64
64
  } from "./chunk-QNXB6PVL.js";
65
65
  import {
66
66
  withPrerequisite
67
- } from "./chunk-FWRH2IEJ.js";
67
+ } from "./chunk-LLGYR7KF.js";
68
68
  import {
69
69
  NOOP_NOTIFIER,
70
70
  RateLimiter,
@@ -88,7 +88,7 @@ import {
88
88
  withAuditTrail,
89
89
  withProgressHeartbeat,
90
90
  wrapToolWithTimeout
91
- } from "./chunk-TPO3KDE3.js";
91
+ } from "./chunk-767D24JM.js";
92
92
  import {
93
93
  createAuditTrail,
94
94
  createGraphAuditBridge,
@@ -47358,7 +47358,7 @@ ${contextBlock}`;
47358
47358
  const strategy = config.votingStrategy ?? "higher_order";
47359
47359
  await postProgress(config, "Vote", `Running consensus with ${strategy} strategy...`);
47360
47360
  try {
47361
- const { executeVoting } = await import("./consensus-vote-RULORCL2.js");
47361
+ const { executeVoting } = await import("./consensus-vote-M6LEXIR7.js");
47362
47362
  const votingResult = await executeVoting(
47363
47363
  {
47364
47364
  proposal: buildVoteProposal(plan, research),
@@ -55989,4 +55989,4 @@ export {
55989
55989
  shutdownFeedbackSubscriber,
55990
55990
  createEventBusBridge
55991
55991
  };
55992
- //# sourceMappingURL=chunk-3K6DNH62.js.map
55992
+ //# sourceMappingURL=chunk-4FQGYWI4.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-YVRYIIHX.js";
3
+ } from "./chunk-2FXH2HEW.js";
4
4
  import {
5
5
  emitClawGuardViolation
6
6
  } from "./chunk-MGXRJZUQ.js";
@@ -2135,4 +2135,4 @@ export {
2135
2135
  VOTING_THRESHOLDS,
2136
2136
  ConsensusMetricsSchema
2137
2137
  };
2138
- //# sourceMappingURL=chunk-TPO3KDE3.js.map
2138
+ //# sourceMappingURL=chunk-767D24JM.js.map
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-OQB3XC5O.js";
7
7
  import {
8
8
  VERSION
9
- } from "./chunk-YVRYIIHX.js";
9
+ } from "./chunk-2FXH2HEW.js";
10
10
  import {
11
11
  capitalize
12
12
  } from "./chunk-6T3EPABN.js";
@@ -1201,7 +1201,7 @@ async function runDoctorFix(result) {
1201
1201
  writeLine2("\u2500".repeat(40));
1202
1202
  let fixCount = 0;
1203
1203
  if (!result.dataDirectory.rootExists || result.dataDirectory.subdirectories.some((d) => !d.exists || !d.writable)) {
1204
- const { initDataDirectories: initDataDirectories2, describeDataDirFix: describeDataDirFix2 } = await import("./setup-data-dir-XRUWUIYA.js");
1204
+ const { initDataDirectories: initDataDirectories2, describeDataDirFix: describeDataDirFix2 } = await import("./setup-data-dir-SUFIDLFX.js");
1205
1205
  const outcome = describeDataDirFix2(initDataDirectories2());
1206
1206
  writeLine2(outcome.line);
1207
1207
  if (outcome.counted) fixCount++;
@@ -1303,4 +1303,4 @@ export {
1303
1303
  startStdioServer,
1304
1304
  closeServer
1305
1305
  };
1306
- //# sourceMappingURL=chunk-HE76P4LG.js.map
1306
+ //# sourceMappingURL=chunk-BZUZ6P6O.js.map
@@ -23,7 +23,7 @@ import {
23
23
  toolSuccessStructured,
24
24
  withProgressHeartbeat,
25
25
  wrapToolWithTimeout
26
- } from "./chunk-TPO3KDE3.js";
26
+ } from "./chunk-767D24JM.js";
27
27
  import {
28
28
  assessPanelIndependence,
29
29
  countDistinctModels,
@@ -6873,4 +6873,4 @@ export {
6873
6873
  CONSENSUS_VOTE_TOOL_SCHEMA,
6874
6874
  registerConsensusVoteTool
6875
6875
  };
6876
- //# sourceMappingURL=chunk-PE4W7G7S.js.map
6876
+ //# sourceMappingURL=chunk-KTVP7FRA.js.map
@@ -7,7 +7,7 @@ import {
7
7
  toolStructuredError,
8
8
  toolSuccessStructured,
9
9
  wrapToolWithTimeout
10
- } from "./chunk-TPO3KDE3.js";
10
+ } from "./chunk-767D24JM.js";
11
11
  import {
12
12
  JsonlStore,
13
13
  SECURITY_KEYWORDS,
@@ -2159,4 +2159,4 @@ export {
2159
2159
  runImprovementReview,
2160
2160
  registerImprovementReviewTool
2161
2161
  };
2162
- //# sourceMappingURL=chunk-FWRH2IEJ.js.map
2162
+ //# sourceMappingURL=chunk-LLGYR7KF.js.map
package/dist/cli.js CHANGED
@@ -17,8 +17,9 @@ import {
17
17
  gatePRAuthor,
18
18
  generateSummary,
19
19
  parseFindings,
20
+ reviewPostingBlock,
20
21
  sumFindings
21
- } from "./chunk-OHQ5JDJX.js";
22
+ } from "./chunk-2PEHRCZ2.js";
22
23
  import "./chunk-DDCKXILN.js";
23
24
  import {
24
25
  runConfigInitSync
@@ -152,7 +153,7 @@ import {
152
153
  validateNexusEnv,
153
154
  validateWorkflow,
154
155
  wrapInMarkdownFence
155
- } from "./chunk-3K6DNH62.js";
156
+ } from "./chunk-4FQGYWI4.js";
156
157
  import "./chunk-KQYXPAFQ.js";
157
158
  import "./chunk-HFOQKCD2.js";
158
159
  import "./chunk-RSMG2DPB.js";
@@ -171,7 +172,7 @@ import {
171
172
  doctorCommand,
172
173
  initDataDirectories,
173
174
  runDoctor
174
- } from "./chunk-HE76P4LG.js";
175
+ } from "./chunk-BZUZ6P6O.js";
175
176
  import {
176
177
  buildOpenAICompatAdapters,
177
178
  readOpenAICompatEnv
@@ -190,7 +191,7 @@ import {
190
191
  recordAuthenticVote,
191
192
  registerConsensusVoteTool,
192
193
  toRecordDecision
193
- } from "./chunk-PE4W7G7S.js";
194
+ } from "./chunk-KTVP7FRA.js";
194
195
  import {
195
196
  loadUsageEvents,
196
197
  rollupByModel
@@ -226,15 +227,15 @@ import {
226
227
  registerImprovementReviewTool,
227
228
  runImprovementReview,
228
229
  scanForSecrets
229
- } from "./chunk-FWRH2IEJ.js";
230
+ } from "./chunk-LLGYR7KF.js";
230
231
  import {
231
232
  createDefaultPolicyFirewall,
232
233
  setGlobalPolicyFirewall,
233
234
  stagePolicyFirewallForRollout
234
- } from "./chunk-TPO3KDE3.js";
235
+ } from "./chunk-767D24JM.js";
235
236
  import {
236
237
  VERSION
237
- } from "./chunk-YVRYIIHX.js";
238
+ } from "./chunk-2FXH2HEW.js";
238
239
  import "./chunk-MGXRJZUQ.js";
239
240
  import {
240
241
  createUnifiedRegistry,
@@ -4400,22 +4401,36 @@ ${file.patch}
4400
4401
  }
4401
4402
  /**
4402
4403
  * Posts review to GitHub after policy gate validation.
4403
- * The policy gate audits the action but only blocks on Rule of Two
4404
- * violations — the review itself is our internal analysis, not content
4405
- * from the untrusted PR author.
4404
+ *
4405
+ * Blocks on the gate's own `allowed` verdict rather than re-deriving a
4406
+ * narrower condition. It previously blocked on `hasRuleOfTwoViolation` alone
4407
+ * while `evaluatePolicy` had already computed `allowed`, on the rationale
4408
+ * that "the review itself is our internal analysis, not content from the
4409
+ * untrusted PR author".
4410
+ *
4411
+ * Measured: for the action `auditReviewAction` builds, the two are currently
4412
+ * EQUIVALENT — tiers 1-2 produce no violations, and tiers 3-4 produce
4413
+ * INSUFFICIENT_TRUST + UNTRUSTED_INFLUENCE + RULE_OF_TWO together. So this
4414
+ * changes no behaviour today. It makes the equivalence guaranteed rather
4415
+ * than accidental: it holds only because the context below hardcodes
4416
+ * `hasWriteAccess` and `hasSecretAccess` to true, which is what makes
4417
+ * `checkRuleOfTwo` fire at tier 3+. Make either conditional and RULE_OF_TWO
4418
+ * stops firing while the other two blocking rules still do.
4419
+ *
4420
+ * `requiresApproval` deliberately does NOT gate: it is true exactly when
4421
+ * `allowed` is true (DraftReply is always approval-required), so blocking on
4422
+ * it would refuse every review that passed.
4406
4423
  * (Source: Issue #828 — Wire policy gate into production pipeline)
4407
4424
  */
4408
4425
  async postReviewToGitHub(provider, pr, result, gateDecision) {
4409
4426
  const policyResult = this.auditReviewAction(gateDecision.enforcedTier);
4410
- if (policyResult.hasRuleOfTwoViolation) {
4411
- logger5.warn("Rule of Two: review posting blocked", {
4427
+ const blocked = reviewPostingBlock(policyResult);
4428
+ if (blocked !== void 0) {
4429
+ logger5.warn(`${blocked.label}: review posting blocked`, {
4412
4430
  prNumber: pr.prNumber,
4413
4431
  violations: policyResult.violations
4414
4432
  });
4415
- return {
4416
- status: "skipped",
4417
- reason: `Rule of Two: ${policyResult.violations.map((v) => v.rule).join(", ")}`
4418
- };
4433
+ return { status: "skipped", reason: blocked.reason };
4419
4434
  }
4420
4435
  if (policyResult.violations.length > 0) {
4421
4436
  logger5.info("Policy gate warnings for review posting", {
@@ -4423,7 +4438,7 @@ ${file.patch}
4423
4438
  violations: policyResult.violations
4424
4439
  });
4425
4440
  }
4426
- const { formatReviewComment: formatReviewComment2 } = await import("./pr-reviewer-helpers-ZQJE6G6S.js");
4441
+ const { formatReviewComment: formatReviewComment2 } = await import("./pr-reviewer-helpers-BVQYKJQC.js");
4427
4442
  const body = formatReviewComment2(result);
4428
4443
  const postResult = await provider.createReview(pr.prNumber, body, result.decision);
4429
4444
  if (!postResult.ok) {
@@ -4457,6 +4472,7 @@ ${file.patch}
4457
4472
  context
4458
4473
  );
4459
4474
  return {
4475
+ allowed: decision.allowed,
4460
4476
  hasRuleOfTwoViolation: decision.violations.some((v) => v.rule === "RULE_OF_TWO"),
4461
4477
  violations: decision.violations
4462
4478
  };
@@ -19563,7 +19579,7 @@ function startImprovementReviewScheduler(options) {
19563
19579
  let running = false;
19564
19580
  const runOnce = async () => {
19565
19581
  try {
19566
- const { runImprovementReview: runImprovementReview2, ImprovementReviewInputSchema: ImprovementReviewInputSchema2 } = await import("./improvement-review-36HJKKCR.js");
19582
+ const { runImprovementReview: runImprovementReview2, ImprovementReviewInputSchema: ImprovementReviewInputSchema2 } = await import("./improvement-review-YW7KHPFJ.js");
19567
19583
  const input = ImprovementReviewInputSchema2.parse({ fileIssues });
19568
19584
  const result = await runImprovementReview2(input, { logger: logger22 });
19569
19585
  logger22.info("Scheduled improvement_review complete", {