supipowers 1.2.6 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +118 -56
  2. package/bin/install.ts +48 -128
  3. package/package.json +11 -3
  4. package/skills/code-review/SKILL.md +137 -40
  5. package/skills/context-mode/SKILL.md +67 -56
  6. package/skills/creating-supi-agents/SKILL.md +204 -0
  7. package/skills/debugging/SKILL.md +86 -40
  8. package/skills/fix-pr/SKILL.md +96 -65
  9. package/skills/planning/SKILL.md +103 -46
  10. package/skills/qa-strategy/SKILL.md +68 -46
  11. package/skills/receiving-code-review/SKILL.md +60 -53
  12. package/skills/release/SKILL.md +111 -39
  13. package/skills/tdd/SKILL.md +118 -67
  14. package/skills/verification/SKILL.md +71 -37
  15. package/src/bootstrap.ts +27 -5
  16. package/src/commands/agents.ts +249 -0
  17. package/src/commands/ai-review.ts +1113 -0
  18. package/src/commands/config.ts +224 -95
  19. package/src/commands/doctor.ts +19 -13
  20. package/src/commands/fix-pr.ts +8 -11
  21. package/src/commands/generate.ts +200 -0
  22. package/src/commands/model-picker.ts +5 -15
  23. package/src/commands/model.ts +4 -5
  24. package/src/commands/optimize-context.ts +202 -0
  25. package/src/commands/plan.ts +148 -92
  26. package/src/commands/qa.ts +14 -23
  27. package/src/commands/release.ts +504 -275
  28. package/src/commands/review.ts +643 -86
  29. package/src/commands/status.ts +44 -17
  30. package/src/commands/supi.ts +69 -41
  31. package/src/commands/update.ts +57 -2
  32. package/src/config/defaults.ts +6 -39
  33. package/src/config/loader.ts +388 -40
  34. package/src/config/model-resolver.ts +26 -22
  35. package/src/config/schema.ts +113 -48
  36. package/src/context/analyzer.ts +61 -2
  37. package/src/context/optimizer.ts +199 -0
  38. package/src/context-mode/compressor.ts +14 -11
  39. package/src/context-mode/detector.ts +16 -54
  40. package/src/context-mode/event-extractor.ts +45 -16
  41. package/src/context-mode/event-store.ts +225 -16
  42. package/src/context-mode/hooks.ts +195 -22
  43. package/src/context-mode/knowledge/chunker.ts +235 -0
  44. package/src/context-mode/knowledge/store.ts +187 -0
  45. package/src/context-mode/routing.ts +12 -23
  46. package/src/context-mode/sandbox/executor.ts +183 -0
  47. package/src/context-mode/sandbox/runners.ts +40 -0
  48. package/src/context-mode/snapshot-builder.ts +243 -7
  49. package/src/context-mode/tools.ts +440 -0
  50. package/src/context-mode/web/fetcher.ts +117 -0
  51. package/src/context-mode/web/html-to-md.ts +293 -0
  52. package/src/debug/logger.ts +107 -0
  53. package/src/deps/registry.ts +0 -20
  54. package/src/docs/drift.ts +454 -0
  55. package/src/fix-pr/fetch-comments.ts +66 -0
  56. package/src/git/commit-msg.ts +2 -1
  57. package/src/git/commit.ts +123 -141
  58. package/src/git/conventions.ts +2 -2
  59. package/src/git/status.ts +4 -1
  60. package/src/lsp/bridge.ts +138 -12
  61. package/src/planning/approval-flow.ts +125 -19
  62. package/src/planning/plan-writer-prompt.ts +4 -11
  63. package/src/planning/planning-ask-tool.ts +81 -0
  64. package/src/planning/prompt-builder.ts +9 -169
  65. package/src/planning/system-prompt.ts +290 -0
  66. package/src/platform/omp.ts +50 -4
  67. package/src/platform/progress.ts +182 -0
  68. package/src/platform/test-utils.ts +4 -1
  69. package/src/platform/tui-colors.ts +30 -0
  70. package/src/platform/types.ts +1 -0
  71. package/src/qa/detect-app-type.ts +102 -0
  72. package/src/qa/discover-routes.ts +353 -0
  73. package/src/quality/ai-session.ts +96 -0
  74. package/src/quality/ai-setup.ts +86 -0
  75. package/src/quality/gates/ai-review.ts +129 -0
  76. package/src/quality/gates/build.ts +8 -0
  77. package/src/quality/gates/command.ts +150 -0
  78. package/src/quality/gates/format.ts +28 -0
  79. package/src/quality/gates/lint.ts +22 -0
  80. package/src/quality/gates/lsp-diagnostics.ts +84 -0
  81. package/src/quality/gates/test-suite.ts +8 -0
  82. package/src/quality/gates/typecheck.ts +22 -0
  83. package/src/quality/registry.ts +25 -0
  84. package/src/quality/review-gates.ts +33 -0
  85. package/src/quality/runner.ts +268 -0
  86. package/src/quality/schemas.ts +48 -0
  87. package/src/quality/setup.ts +227 -0
  88. package/src/release/changelog.ts +7 -3
  89. package/src/release/channels/custom.ts +43 -0
  90. package/src/release/channels/gitea.ts +35 -0
  91. package/src/release/channels/github.ts +35 -0
  92. package/src/release/channels/gitlab.ts +35 -0
  93. package/src/release/channels/registry.ts +52 -0
  94. package/src/release/channels/types.ts +27 -0
  95. package/src/release/detector.ts +10 -63
  96. package/src/release/executor.ts +61 -51
  97. package/src/release/prompt.ts +38 -38
  98. package/src/release/version.ts +129 -10
  99. package/src/review/agent-loader.ts +331 -0
  100. package/src/review/consolidator.ts +180 -0
  101. package/src/review/default-agents/correctness.md +72 -0
  102. package/src/review/default-agents/maintainability.md +64 -0
  103. package/src/review/default-agents/security.md +67 -0
  104. package/src/review/fixer.ts +219 -0
  105. package/src/review/multi-agent-runner.ts +135 -0
  106. package/src/review/output.ts +147 -0
  107. package/src/review/prompts/agent-review-wrapper.md +36 -0
  108. package/src/review/prompts/fix-findings.md +32 -0
  109. package/src/review/prompts/fix-output-schema.md +18 -0
  110. package/src/review/prompts/invalid-output-retry.md +22 -0
  111. package/src/review/prompts/output-instructions.md +14 -0
  112. package/src/review/prompts/review-output-schema.md +38 -0
  113. package/src/review/prompts/single-review.md +53 -0
  114. package/src/review/prompts/validation-review.md +30 -0
  115. package/src/review/runner.ts +128 -0
  116. package/src/review/scope.ts +353 -0
  117. package/src/review/template.ts +15 -0
  118. package/src/review/types.ts +296 -0
  119. package/src/review/validator.ts +160 -0
  120. package/src/storage/plans.ts +5 -3
  121. package/src/storage/reports.ts +50 -7
  122. package/src/storage/review-sessions.ts +117 -0
  123. package/src/text.ts +19 -0
  124. package/src/types.ts +336 -26
  125. package/src/utils/paths.ts +39 -0
  126. package/src/visual/companion.ts +5 -3
  127. package/src/visual/start-server.ts +101 -0
  128. package/src/visual/stop-server.ts +39 -0
  129. package/bin/ctx-mode-wrapper.mjs +0 -66
  130. package/src/config/profiles.ts +0 -64
  131. package/src/context-mode/installer.ts +0 -38
  132. package/src/quality/ai-review-gate.ts +0 -43
  133. package/src/quality/gate-runner.ts +0 -67
  134. package/src/quality/lsp-gate.ts +0 -24
  135. package/src/quality/test-gate.ts +0 -39
  136. package/src/visual/scripts/start-server.sh +0 -98
  137. package/src/visual/scripts/stop-server.sh +0 -21
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: security
3
+ description: Security-focused code reviewer aligned with OWASP Top 10 and CWE Top 25
4
+ focus: Access control, injection, authentication, cryptography, secrets management, supply chain, error handling
5
+ ---
6
+
7
+ You are a security-focused code reviewer. Analyze the provided code diff for security vulnerabilities, referencing OWASP Top 10 and CWE Top 25 categories where applicable.
8
+
9
+ ## What to Check
10
+
11
+ ### Broken Access Control
12
+ - Missing or incorrect authorization checks on endpoints and data access
13
+ - Insecure direct object references (IDOR) — user-controlled IDs used without ownership validation
14
+ - Privilege escalation paths — role checks that can be bypassed or are missing entirely
15
+ - Server-side request forgery (SSRF) — user-controlled URLs passed to server-side fetchers
16
+
17
+ ### Injection
18
+ - SQL/NoSQL injection — string concatenation or template literals in queries instead of parameterized statements
19
+ - Cross-site scripting (XSS) — `innerHTML`, `document.write`, `dangerouslySetInnerHTML`, or unescaped user input in templates
20
+ - OS command injection — user input passed to `exec()`, `spawn()`, or shell commands without sanitization
21
+ - Template injection — user input interpolated into server-side templates
22
+
23
+ ### Authentication Failures
24
+ - Weak session handling — predictable session IDs, missing expiration, no rotation after privilege change
25
+ - Insecure token storage — JWTs or session tokens in localStorage, missing HttpOnly/Secure flags on cookies
26
+ - Credential stuffing exposure — missing rate limiting or account lockout on authentication endpoints
27
+
28
+ ### Cryptographic Failures
29
+ - Deprecated algorithms — MD5, SHA-1, DES, RC4, or ECB mode used for security purposes
30
+ - Hardcoded cryptographic keys or initialization vectors
31
+ - Weak randomness — `Math.random()` or other non-CSPRNG sources used for security-sensitive values
32
+ - Missing or misconfigured TLS — allowing downgrade, self-signed certs in production
33
+
34
+ ### Secrets Management
35
+ - Hardcoded credentials, API keys, tokens, or connection strings in source code
36
+ - Secrets written to logs, error messages, or HTTP responses
37
+ - Missing rotation patterns for long-lived credentials
38
+
39
+ ### Supply Chain Risks
40
+ - Untrusted or unvetted dependencies introduced without justification
41
+ - Missing integrity checks (lockfile changes without corresponding package.json changes)
42
+ - Unsafe dynamic imports — `import()` or `require()` with user-controlled paths
43
+
44
+ ### Insecure Error Handling
45
+ - Stack traces, internal paths, or debug information leaked to end users
46
+ - Fail-open patterns — exceptions that grant access or skip validation instead of denying
47
+ - Sensitive data (credentials, tokens, PII) included in error messages or responses
48
+
49
+ ### Security Logging Gaps
50
+ - Missing audit trails for authentication events, authorization failures, or sensitive operations
51
+ - PII or secrets written to application logs
52
+ - Insufficient context in security-relevant log entries (missing user ID, IP, or action)
53
+
54
+ ## Severity Guide
55
+
56
+ - **error**: Exploitable vulnerability that can lead to unauthorized access, data exposure, or code execution (e.g., injection, auth bypass, exposed secrets)
57
+ - **warning**: Context-dependent risk that may be exploitable depending on deployment, configuration, or input source (e.g., missing validation on internal input, weak-but-not-broken crypto config)
58
+ - **info**: Hardening opportunity or defense-in-depth suggestion that improves security posture without addressing an active vulnerability (e.g., missing security header, logging improvement)
59
+
60
+ ## Out of Scope
61
+
62
+ - Correctness or logic bugs (handled by correctness agent)
63
+ - Code style or formatting (handled by linter)
64
+ - Maintainability concerns (handled by maintainability agent)
65
+ - Performance optimizations
66
+
67
+ {output_instructions}
@@ -0,0 +1,219 @@
1
+ import fixFindingsPrompt from "./prompts/fix-findings.md" with { type: "text" };
2
+ import fixOutputSchemaPrompt from "./prompts/fix-output-schema.md" with { type: "text" };
3
+ import type {
4
+ GateExecutionContext,
5
+ ReviewFinding,
6
+ ReviewFixOutput,
7
+ ReviewFixRecord,
8
+ ReviewOutput,
9
+ ReviewScope,
10
+ } from "../types.js";
11
+ import { parseStructuredOutput, runWithOutputValidation } from "./output.js";
12
+ import { ReviewFixOutputSchema } from "./types.js";
13
+ import { renderReviewTemplate } from "./template.js";
14
+
15
+ export interface ReviewFixInput {
16
+ cwd: string;
17
+ scope: ReviewScope;
18
+ findings: ReviewFinding[];
19
+ createAgentSession: GateExecutionContext["createAgentSession"];
20
+ model?: string;
21
+ thinkingLevel?: string | null;
22
+ timeoutMs?: number;
23
+ }
24
+
25
+ export interface ReviewFixRunResult {
26
+ output: ReviewFixOutput;
27
+ attempts: number;
28
+ rawOutputs: string[];
29
+ }
30
+
31
+ export interface ReviewOutputDelta {
32
+ resolved: ReviewFinding[];
33
+ remaining: ReviewFinding[];
34
+ newFindings: ReviewFinding[];
35
+ }
36
+
37
+ function fingerprintFinding(finding: ReviewFinding): string {
38
+ return [
39
+ finding.file ?? "(unknown)",
40
+ finding.lineStart ?? "?",
41
+ finding.lineEnd ?? finding.lineStart ?? "?",
42
+ finding.title.toLowerCase().replace(/\s+/g, " ").trim(),
43
+ ].join("|");
44
+ }
45
+
46
+ function selectFixableFindings(findings: ReviewFinding[]): ReviewFinding[] {
47
+ return findings.filter((finding) => finding.file !== null);
48
+ }
49
+
50
+ function createSkippedRecord(finding: ReviewFinding, summary: string): ReviewFixRecord {
51
+ return {
52
+ findingIds: [finding.id],
53
+ file: finding.file,
54
+ status: "skipped",
55
+ summary,
56
+ };
57
+ }
58
+
59
+ function summarizeFixRecords(records: ReviewFixRecord[]): string {
60
+ const counts = records.reduce(
61
+ (summary, record) => {
62
+ summary[record.status] += 1;
63
+ return summary;
64
+ },
65
+ { applied: 0, skipped: 0, failed: 0 },
66
+ );
67
+
68
+ return `Fix pass: ${counts.applied} applied, ${counts.skipped} skipped, ${counts.failed} failed.`;
69
+ }
70
+
71
+ function deriveFixStatus(records: ReviewFixRecord[], requestedCount: number): ReviewFixOutput["status"] {
72
+ if (requestedCount === 0) {
73
+ return "skipped";
74
+ }
75
+
76
+ const applied = records.filter((record) => record.status === "applied").length;
77
+ const skipped = records.filter((record) => record.status === "skipped").length;
78
+ const failed = records.filter((record) => record.status === "failed").length;
79
+
80
+ if (applied === 0 && failed === 0 && skipped > 0) {
81
+ return "skipped";
82
+ }
83
+ if (applied > 0 && skipped === 0 && failed === 0) {
84
+ return "applied";
85
+ }
86
+ if (applied === 0 && failed > 0 && skipped === 0) {
87
+ return "blocked";
88
+ }
89
+ return "partial";
90
+ }
91
+
92
+ function normalizeFixOutput(
93
+ requestedFindings: ReviewFinding[],
94
+ reported: ReviewFixOutput,
95
+ carriedSkips: ReviewFixRecord[],
96
+ ): ReviewFixOutput {
97
+ const knownIds = new Set(requestedFindings.map((finding) => finding.id));
98
+ const handledIds = new Set<string>();
99
+ const fixes: ReviewFixRecord[] = [];
100
+
101
+ for (const record of reported.fixes) {
102
+ const findingIds = [...new Set(record.findingIds.filter((id) => knownIds.has(id) && !handledIds.has(id)))];
103
+ if (findingIds.length === 0) {
104
+ continue;
105
+ }
106
+ findingIds.forEach((id) => handledIds.add(id));
107
+ fixes.push({
108
+ ...record,
109
+ findingIds,
110
+ });
111
+ }
112
+
113
+ for (const finding of requestedFindings) {
114
+ if (!handledIds.has(finding.id)) {
115
+ fixes.push(createSkippedRecord(finding, "Fixer did not report handling this finding."));
116
+ }
117
+ }
118
+
119
+ fixes.push(...carriedSkips);
120
+ return {
121
+ fixes,
122
+ summary: reported.summary || summarizeFixRecords(fixes),
123
+ status: reported.status === "blocked"
124
+ ? "blocked"
125
+ : deriveFixStatus(fixes, requestedFindings.length + carriedSkips.length),
126
+ };
127
+ }
128
+
129
+ export function buildFixPrompt(scope: ReviewScope, findings: ReviewFinding[]): string {
130
+ return renderReviewTemplate(fixFindingsPrompt, {
131
+ scope,
132
+ findingsJson: JSON.stringify(findings, null, 2),
133
+ fixOutputSchema: fixOutputSchemaPrompt.trim(),
134
+ });
135
+ }
136
+
137
+ export async function runAutoFix(input: ReviewFixInput): Promise<ReviewFixRunResult> {
138
+ const skippedFindings = input.findings.filter((finding) => finding.file === null);
139
+ const carriedSkips = skippedFindings.map((finding) =>
140
+ createSkippedRecord(finding, "Skipped because the finding does not identify a concrete file to edit."),
141
+ );
142
+ const fixableFindings = selectFixableFindings(input.findings);
143
+
144
+ if (fixableFindings.length === 0) {
145
+ return {
146
+ output: {
147
+ fixes: carriedSkips,
148
+ summary: carriedSkips.length > 0
149
+ ? summarizeFixRecords(carriedSkips)
150
+ : "No findings were eligible for automatic fixing.",
151
+ status: "skipped",
152
+ },
153
+ attempts: 0,
154
+ rawOutputs: [],
155
+ };
156
+ }
157
+
158
+ const result = await runWithOutputValidation(input.createAgentSession, {
159
+ cwd: input.cwd,
160
+ prompt: buildFixPrompt(input.scope, fixableFindings),
161
+ schema: fixOutputSchemaPrompt.trim(),
162
+ parse(raw) {
163
+ return parseStructuredOutput<ReviewFixOutput>(raw, ReviewFixOutputSchema);
164
+ },
165
+ model: input.model,
166
+ thinkingLevel: input.thinkingLevel ?? null,
167
+ timeoutMs: input.timeoutMs ?? 180_000,
168
+ });
169
+
170
+ if (result.status === "blocked") {
171
+ return {
172
+ output: {
173
+ fixes: [
174
+ ...fixableFindings.map((finding) => createSkippedRecord(finding, result.error)),
175
+ ...carriedSkips,
176
+ ],
177
+ summary: result.error,
178
+ status: "blocked",
179
+ },
180
+ attempts: result.attempts,
181
+ rawOutputs: result.rawOutputs,
182
+ };
183
+ }
184
+
185
+ return {
186
+ output: normalizeFixOutput(fixableFindings, result.output, carriedSkips),
187
+ attempts: result.attempts,
188
+ rawOutputs: [result.rawOutput],
189
+ };
190
+ }
191
+
192
+ export function compareReviewOutputs(previous: ReviewOutput, next: ReviewOutput): ReviewOutputDelta {
193
+ const previousMap = new Map(previous.findings.map((finding) => [fingerprintFinding(finding), finding]));
194
+ const nextMap = new Map(next.findings.map((finding) => [fingerprintFinding(finding), finding]));
195
+
196
+ const resolved: ReviewFinding[] = [];
197
+ const remaining: ReviewFinding[] = [];
198
+ const newFindings: ReviewFinding[] = [];
199
+
200
+ for (const [fingerprint, finding] of previousMap) {
201
+ if (nextMap.has(fingerprint)) {
202
+ remaining.push(nextMap.get(fingerprint)!);
203
+ } else {
204
+ resolved.push(finding);
205
+ }
206
+ }
207
+
208
+ for (const [fingerprint, finding] of nextMap) {
209
+ if (!previousMap.has(fingerprint)) {
210
+ newFindings.push(finding);
211
+ }
212
+ }
213
+
214
+ return {
215
+ resolved,
216
+ remaining,
217
+ newFindings,
218
+ };
219
+ }
@@ -0,0 +1,135 @@
1
+ import agentReviewWrapperPrompt from "./prompts/agent-review-wrapper.md" with { type: "text" };
2
+ import outputInstructionsPrompt from "./prompts/output-instructions.md" with { type: "text" };
3
+ import reviewOutputSchema from "./prompts/review-output-schema.md" with { type: "text" };
4
+ import type { ConfiguredReviewAgent, GateExecutionContext, ReviewOutput, ReviewScope } from "../types.js";
5
+ import { explainReviewOutputFailure, parseReviewOutput, runWithOutputValidation } from "./output.js";
6
+ import { renderReviewTemplate } from "./template.js";
7
+
8
+ export interface MultiAgentReviewInput {
9
+ cwd: string;
10
+ scope: ReviewScope;
11
+ agents: ConfiguredReviewAgent[];
12
+ createAgentSession: GateExecutionContext["createAgentSession"];
13
+ model?: string;
14
+ thinkingLevel?: string | null;
15
+ timeoutMs?: number;
16
+ onAgentStart?: (agent: ConfiguredReviewAgent) => void;
17
+ onAgentComplete?: (result: MultiAgentAgentResult) => void;
18
+ }
19
+
20
+ export interface MultiAgentAgentResult {
21
+ agent: ConfiguredReviewAgent;
22
+ output: ReviewOutput;
23
+ attempts: number;
24
+ rawOutputs: string[];
25
+ }
26
+
27
+ export interface MultiAgentReviewResult {
28
+ agents: MultiAgentAgentResult[];
29
+ output: ReviewOutput;
30
+ }
31
+
32
+ function renderOutputInstructions(): string {
33
+ return renderReviewTemplate(outputInstructionsPrompt, {
34
+ outputSchema: reviewOutputSchema.trim(),
35
+ });
36
+ }
37
+
38
+ export function buildConfiguredAgentPrompt(agent: ConfiguredReviewAgent, scope: ReviewScope): string {
39
+ if (!agent.prompt.includes("{output_instructions}")) {
40
+ throw new Error(`Review agent ${agent.name} is missing the {output_instructions} placeholder.`);
41
+ }
42
+
43
+ const outputInstructions = renderOutputInstructions();
44
+ const agentPrompt = renderReviewTemplate(
45
+ agent.prompt.replaceAll("{output_instructions}", "{{outputInstructions}}"),
46
+ { outputInstructions },
47
+ );
48
+
49
+ return renderReviewTemplate(agentReviewWrapperPrompt, {
50
+ agent,
51
+ agentPrompt,
52
+ scope,
53
+ });
54
+ }
55
+
56
+ function aggregateAgentOutputs(results: MultiAgentAgentResult[]): ReviewOutput {
57
+ const findings = results.flatMap((result) =>
58
+ result.output.findings.map((finding) => ({
59
+ ...finding,
60
+ agent: finding.agent ?? result.agent.name,
61
+ })),
62
+ );
63
+ const blockedAgents = results.filter((result) => result.output.status === "blocked").length;
64
+ const summary = `Ran ${results.length} review agents: ${findings.length} findings, ${blockedAgents} blocked.`;
65
+
66
+ return {
67
+ findings,
68
+ summary,
69
+ status: blockedAgents > 0 ? "blocked" : findings.length > 0 ? "failed" : "passed",
70
+ };
71
+ }
72
+
73
+ async function runConfiguredAgent(
74
+ input: Omit<MultiAgentReviewInput, "agents">,
75
+ agent: ConfiguredReviewAgent,
76
+ ): Promise<MultiAgentAgentResult> {
77
+ input.onAgentStart?.(agent);
78
+
79
+ const result = await runWithOutputValidation(input.createAgentSession, {
80
+ cwd: input.cwd,
81
+ prompt: buildConfiguredAgentPrompt(agent, input.scope),
82
+ schema: reviewOutputSchema.trim(),
83
+ parse(raw) {
84
+ const output = parseReviewOutput(raw);
85
+ return {
86
+ output,
87
+ error: output ? null : explainReviewOutputFailure(raw),
88
+ };
89
+ },
90
+ model: agent.model ?? input.model,
91
+ thinkingLevel: input.thinkingLevel ?? null,
92
+ timeoutMs: input.timeoutMs ?? 120_000,
93
+ });
94
+
95
+ if (result.status === "blocked") {
96
+ const blockedResult = {
97
+ agent,
98
+ output: {
99
+ findings: [],
100
+ summary: result.error,
101
+ status: "blocked",
102
+ },
103
+ attempts: result.attempts,
104
+ rawOutputs: result.rawOutputs,
105
+ } satisfies MultiAgentAgentResult;
106
+ input.onAgentComplete?.(blockedResult);
107
+ return blockedResult;
108
+ }
109
+
110
+ const completedResult = {
111
+ agent,
112
+ output: {
113
+ ...result.output,
114
+ findings: result.output.findings.map((finding) => ({
115
+ ...finding,
116
+ agent: finding.agent ?? agent.name,
117
+ })),
118
+ },
119
+ attempts: result.attempts,
120
+ rawOutputs: [result.rawOutput],
121
+ } satisfies MultiAgentAgentResult;
122
+ input.onAgentComplete?.(completedResult);
123
+ return completedResult;
124
+ }
125
+
126
+ export async function runMultiAgentReview(input: MultiAgentReviewInput): Promise<MultiAgentReviewResult> {
127
+ const results = await Promise.all(
128
+ input.agents.map((agent) => runConfiguredAgent(input, agent)),
129
+ );
130
+
131
+ return {
132
+ agents: results,
133
+ output: aggregateAgentOutputs(results),
134
+ };
135
+ }
@@ -0,0 +1,147 @@
1
+ import type { TSchema } from "@sinclair/typebox";
2
+ import { Value } from "@sinclair/typebox/value";
3
+ import invalidOutputRetryPrompt from "./prompts/invalid-output-retry.md" with { type: "text" };
4
+ import { runStructuredAgentSession } from "../quality/ai-session.js";
5
+ import { stripMarkdownCodeFence } from "../text.js";
6
+ import type { GateExecutionContext, ReviewOutput } from "../types.js";
7
+ import {
8
+ ReviewOutputSchema,
9
+ collectReviewValidationErrors,
10
+ formatReviewValidationErrors,
11
+ } from "./types.js";
12
+ import { renderReviewTemplate } from "./template.js";
13
+
14
+ export interface StructuredParseResult<T> {
15
+ output: T | null;
16
+ error: string | null;
17
+ }
18
+
19
+ export interface OutputValidationRunOptions<T> {
20
+ cwd: string;
21
+ prompt: string;
22
+ schema: string;
23
+ parse: (raw: string) => StructuredParseResult<T>;
24
+ model?: string;
25
+ thinkingLevel?: string | null;
26
+ timeoutMs?: number;
27
+ maxAttempts?: number;
28
+ }
29
+
30
+ export type OutputValidationRunResult<T> =
31
+ | {
32
+ status: "ok";
33
+ output: T;
34
+ rawOutput: string;
35
+ attempts: number;
36
+ }
37
+ | {
38
+ status: "blocked";
39
+ error: string;
40
+ rawOutputs: string[];
41
+ attempts: number;
42
+ };
43
+
44
+ function truncateForPrompt(text: string, maxLength = 1200): string {
45
+ const normalized = text.trim();
46
+ if (normalized.length <= maxLength) {
47
+ return normalized;
48
+ }
49
+
50
+ return `${normalized.slice(0, maxLength - 1)}…`;
51
+ }
52
+
53
+ export function parseStructuredOutput<T>(raw: string, schema: TSchema): StructuredParseResult<T> {
54
+ let parsed: unknown;
55
+
56
+ try {
57
+ parsed = JSON.parse(stripMarkdownCodeFence(raw));
58
+ } catch (error) {
59
+ return {
60
+ output: null,
61
+ error: error instanceof Error ? `Invalid JSON: ${error.message}` : "Invalid JSON.",
62
+ };
63
+ }
64
+
65
+ if (!Value.Check(schema, parsed)) {
66
+ const errors = formatReviewValidationErrors(collectReviewValidationErrors(schema, parsed));
67
+ return {
68
+ output: null,
69
+ error: errors.length > 0 ? errors.join("; ") : "Output does not match the required schema.",
70
+ };
71
+ }
72
+
73
+ return {
74
+ output: parsed as T,
75
+ error: null,
76
+ };
77
+ }
78
+
79
+ export function parseReviewOutput(raw: string): ReviewOutput | null {
80
+ return parseStructuredOutput<ReviewOutput>(raw, ReviewOutputSchema).output;
81
+ }
82
+
83
+ export function explainReviewOutputFailure(raw: string): string | null {
84
+ return parseStructuredOutput<ReviewOutput>(raw, ReviewOutputSchema).error;
85
+ }
86
+
87
+ export async function runWithOutputValidation<T>(
88
+ createAgentSession: GateExecutionContext["createAgentSession"],
89
+ options: OutputValidationRunOptions<T>,
90
+ ): Promise<OutputValidationRunResult<T>> {
91
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
92
+ const rawOutputs: string[] = [];
93
+ let prompt = options.prompt;
94
+
95
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
96
+ const result = await runStructuredAgentSession(createAgentSession, {
97
+ cwd: options.cwd,
98
+ prompt,
99
+ model: options.model,
100
+ thinkingLevel: options.thinkingLevel ?? null,
101
+ timeoutMs: options.timeoutMs,
102
+ });
103
+
104
+ if (result.status !== "ok") {
105
+ return {
106
+ status: "blocked",
107
+ error: result.error,
108
+ rawOutputs,
109
+ attempts: attempt,
110
+ };
111
+ }
112
+
113
+ rawOutputs.push(result.finalText);
114
+ const parsed = options.parse(result.finalText);
115
+ if (parsed.output) {
116
+ return {
117
+ status: "ok",
118
+ output: parsed.output,
119
+ rawOutput: result.finalText,
120
+ attempts: attempt,
121
+ };
122
+ }
123
+
124
+ if (attempt === maxAttempts) {
125
+ return {
126
+ status: "blocked",
127
+ error: parsed.error ?? "Agent output was invalid.",
128
+ rawOutputs,
129
+ attempts: attempt,
130
+ };
131
+ }
132
+
133
+ prompt = renderReviewTemplate(invalidOutputRetryPrompt, {
134
+ prompt: options.prompt,
135
+ error: parsed.error ?? "Agent output was invalid.",
136
+ previousOutput: truncateForPrompt(result.finalText),
137
+ schema: options.schema,
138
+ });
139
+ }
140
+
141
+ return {
142
+ status: "blocked",
143
+ error: "Output validation exhausted without producing a valid result.",
144
+ rawOutputs,
145
+ attempts: maxAttempts,
146
+ };
147
+ }
@@ -0,0 +1,36 @@
1
+ You are part of a structured multi-agent code review pipeline.
2
+
3
+ Agent name: {{agent.name}}
4
+ Agent description: {{agent.description}}
5
+ {{#if agent.focus}}
6
+ Agent focus: {{agent.focus}}
7
+ {{/if}}
8
+
9
+ Agent-specific instructions:
10
+ {{agentPrompt}}
11
+
12
+ Review scope: {{scope.description}}
13
+ Reviewable files: {{scope.stats.filesChanged}}
14
+ Excluded files: {{scope.stats.excludedFiles}}
15
+ Additions: {{scope.stats.additions}}
16
+ Deletions: {{scope.stats.deletions}}
17
+ {{#if scope.baseBranch}}
18
+ Base branch: {{scope.baseBranch}}
19
+ {{/if}}
20
+ {{#if scope.commit}}
21
+ Commit: {{scope.commit}}
22
+ {{/if}}
23
+ {{#if scope.customInstructions}}
24
+ Custom review focus:
25
+ {{scope.customInstructions}}
26
+ {{/if}}
27
+
28
+ Files in scope:
29
+ {{#each scope.files}}
30
+ - {{path}} (+{{additions}} -{{deletions}})
31
+ {{/each}}
32
+
33
+ Unified diff:
34
+ ```diff
35
+ {{scope.diff}}
36
+ ```
@@ -0,0 +1,32 @@
1
+ You are applying safe automatic fixes for reviewed findings.
2
+
3
+ Use tools to inspect and edit the actual files.
4
+ Fix only findings you can resolve confidently and safely.
5
+ Do not change unrelated code.
6
+
7
+ Review scope: {{scope.description}}
8
+ Reviewable files: {{scope.stats.filesChanged}}
9
+ Additions: {{scope.stats.additions}}
10
+ Deletions: {{scope.stats.deletions}}
11
+
12
+ Findings selected for automatic fixing:
13
+ ```json
14
+ {{findingsJson}}
15
+ ```
16
+
17
+ Return JSON only matching this schema:
18
+ ```json
19
+ {{fixOutputSchema}}
20
+ ```
21
+
22
+ Rules:
23
+ - Group related edits by file when possible.
24
+ - Each entry in `fixes` must reference one or more handled `findingIds`.
25
+ - Use `applied` when you changed code to address the finding.
26
+ - Use `skipped` when no safe automatic fix exists.
27
+ - Use `failed` when you attempted a fix but could not complete it.
28
+ - If every handled finding was skipped, set overall `status` to `skipped`.
29
+ - If some fixes applied and others were skipped or failed, set overall `status` to `partial`.
30
+ - If all safe fixes applied, set overall `status` to `applied`.
31
+ - Use `blocked` only if you cannot complete a truthful fixing pass.
32
+ - After editing, return JSON only. Do not wrap it in markdown fences.
@@ -0,0 +1,18 @@
1
+ {
2
+ "fixes": [
3
+ {
4
+ "findingIds": ["F001"],
5
+ "file": "src/auth.ts",
6
+ "status": "applied",
7
+ "summary": "Added a null guard before dereferencing the user object."
8
+ },
9
+ {
10
+ "findingIds": ["F002"],
11
+ "file": null,
12
+ "status": "skipped",
13
+ "summary": "Skipped because the finding does not identify a concrete file to edit."
14
+ }
15
+ ],
16
+ "summary": "Applied 1 fix and skipped 1 finding.",
17
+ "status": "partial"
18
+ }
@@ -0,0 +1,22 @@
1
+ {{prompt}}
2
+
3
+ ---
4
+
5
+ Your previous output was invalid.
6
+
7
+ Validation error:
8
+ {{error}}
9
+
10
+ {{#if previousOutput}}
11
+ Previous output (truncated):
12
+ ```text
13
+ {{previousOutput}}
14
+ ```
15
+
16
+ {{/if}}
17
+ Return valid JSON only matching this schema:
18
+ ```json
19
+ {{schema}}
20
+ ```
21
+
22
+ Do not wrap the JSON in markdown fences.
@@ -0,0 +1,14 @@
1
+ Return JSON only matching this schema:
2
+ ```json
3
+ {{outputSchema}}
4
+ ```
5
+
6
+ Rules:
7
+ - Tell the truth. If file, line, or suggestion is unknown, use `null` instead of guessing.
8
+ - Every finding must be actionable and grounded in the provided scope.
9
+ - `confidence` must be between 0 and 1.
10
+ - Use priority `P0`..`P3`, where `P0` is critical and `P3` is low urgency.
11
+ - Use `status: "failed"` when you found actionable issues.
12
+ - Use `status: "passed"` only when no actionable issues remain.
13
+ - Use `status: "blocked"` only if you cannot complete a truthful review.
14
+ - Do not wrap the JSON in markdown fences.