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.
- package/README.md +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- 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.
|