@formthefog/stratus 2026.2.20 → 2026.3.19
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/.github/sentinel/action.yml +100 -0
- package/.github/sentinel/dist/codebase.d.ts +3 -0
- package/.github/sentinel/dist/codebase.d.ts.map +1 -0
- package/.github/sentinel/dist/context.d.ts +6 -0
- package/.github/sentinel/dist/context.d.ts.map +1 -0
- package/.github/sentinel/dist/fixer.d.ts +6 -0
- package/.github/sentinel/dist/fixer.d.ts.map +1 -0
- package/.github/sentinel/dist/index.d.ts +1 -0
- package/.github/sentinel/dist/index.d.ts.map +1 -0
- package/.github/sentinel/dist/index.js +68808 -0
- package/.github/sentinel/dist/index.js.map +1 -0
- package/.github/sentinel/dist/licenses.txt +1152 -0
- package/.github/sentinel/dist/models/anthropic.d.ts +26 -0
- package/.github/sentinel/dist/models/anthropic.d.ts.map +1 -0
- package/.github/sentinel/dist/models/openai.d.ts +26 -0
- package/.github/sentinel/dist/models/openai.d.ts.map +1 -0
- package/.github/sentinel/dist/models/openrouter.d.ts +31 -0
- package/.github/sentinel/dist/models/openrouter.d.ts.map +1 -0
- package/.github/sentinel/dist/models/types.d.ts +37 -0
- package/.github/sentinel/dist/models/types.d.ts.map +1 -0
- package/.github/sentinel/dist/orchestrator.d.ts +3 -0
- package/.github/sentinel/dist/orchestrator.d.ts.map +1 -0
- package/.github/sentinel/dist/policy.d.ts +15 -0
- package/.github/sentinel/dist/policy.d.ts.map +1 -0
- package/.github/sentinel/dist/reporter.d.ts +8 -0
- package/.github/sentinel/dist/reporter.d.ts.map +1 -0
- package/.github/sentinel/dist/responder.d.ts +6 -0
- package/.github/sentinel/dist/responder.d.ts.map +1 -0
- package/.github/sentinel/dist/router.d.ts +2 -0
- package/.github/sentinel/dist/router.d.ts.map +1 -0
- package/.github/sentinel/dist/schemas/config.d.ts +195 -0
- package/.github/sentinel/dist/schemas/config.d.ts.map +1 -0
- package/.github/sentinel/dist/schemas/fix.d.ts +130 -0
- package/.github/sentinel/dist/schemas/fix.d.ts.map +1 -0
- package/.github/sentinel/dist/schemas/review.d.ts +275 -0
- package/.github/sentinel/dist/schemas/review.d.ts.map +1 -0
- package/.github/sentinel/dist/sourcemap-register.js +1 -0
- package/.github/sentinel/dist/subway.d.ts +31 -0
- package/.github/sentinel/dist/subway.d.ts.map +1 -0
- package/.github/sentinel/dist/types.d.ts +210 -0
- package/.github/sentinel/dist/types.d.ts.map +1 -0
- package/.github/sentinel/package-lock.json +2389 -0
- package/.github/sentinel/package.json +29 -0
- package/.github/sentinel/src/codebase.ts +265 -0
- package/.github/sentinel/src/context.ts +182 -0
- package/.github/sentinel/src/fixer.ts +353 -0
- package/.github/sentinel/src/index.ts +263 -0
- package/.github/sentinel/src/models/anthropic.ts +244 -0
- package/.github/sentinel/src/models/openai.ts +242 -0
- package/.github/sentinel/src/models/openrouter.ts +319 -0
- package/.github/sentinel/src/models/types.ts +35 -0
- package/.github/sentinel/src/orchestrator.ts +287 -0
- package/.github/sentinel/src/policy.ts +133 -0
- package/.github/sentinel/src/reporter.ts +666 -0
- package/.github/sentinel/src/responder.ts +156 -0
- package/.github/sentinel/src/router.ts +308 -0
- package/.github/sentinel/src/schemas/config.ts +84 -0
- package/.github/sentinel/src/schemas/fix.ts +44 -0
- package/.github/sentinel/src/schemas/review.ts +73 -0
- package/.github/sentinel/src/subway.ts +250 -0
- package/.github/sentinel/src/types.ts +234 -0
- package/.github/sentinel/tsconfig.json +19 -0
- package/.github/sentinel.yml +34 -0
- package/.github/workflows/publish.yml +28 -0
- package/.github/workflows/sentinel.yml +55 -0
- package/README.md +90 -41
- package/SECURITY.md +85 -0
- package/TROUBLESHOOTING.md +2 -2
- package/index.ts +219 -109
- package/openclaw.plugin.json +50 -26
- package/package.json +1 -1
- package/skills/stratus-info/SKILL.md +70 -10
- package/src/client.ts +78 -18
- package/src/config.ts +29 -8
- package/src/setup.ts +53 -61
- package/src/types.ts +11 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk"
|
|
2
|
+
import * as core from "@actions/core"
|
|
3
|
+
import type { ModelClient, ReviewRequest, CritiqueRequest, CritiqueResponseRequest } from "./types"
|
|
4
|
+
import type { ModelReview, CritiqueOutput, CritiqueResponse, ReviewFinding, TokenUsage } from "../types"
|
|
5
|
+
import { parseModelReview, parseCritiqueOutput, parseCritiqueResponse } from "../schemas/review"
|
|
6
|
+
|
|
7
|
+
const REVIEW_SYSTEM = `You are a principal engineer conducting a code review. You are thorough, skeptical, and focused on long-term code health.
|
|
8
|
+
|
|
9
|
+
Your priorities:
|
|
10
|
+
1. Intent mismatch — does the code do what the PR says it does?
|
|
11
|
+
2. Hidden edge cases — what breaks under unusual input or concurrency?
|
|
12
|
+
3. Correctness risks — logic errors, off-by-one, race conditions, null derefs
|
|
13
|
+
4. Security — injection, XSS, credential exposure, unsafe deserialization
|
|
14
|
+
5. Test gaps — what should be tested that isn't?
|
|
15
|
+
6. Maintainability — will this be clear in 6 months?
|
|
16
|
+
|
|
17
|
+
You are NOT a style checker. Only flag real problems that affect correctness, security, or maintainability.
|
|
18
|
+
|
|
19
|
+
Respond with a JSON object matching this exact schema:
|
|
20
|
+
{
|
|
21
|
+
"summary": "2-3 sentence overall assessment",
|
|
22
|
+
"severity": "low | medium | high | critical",
|
|
23
|
+
"confidence": 0.0-1.0,
|
|
24
|
+
"findings": [
|
|
25
|
+
{
|
|
26
|
+
"type": "bug | security | performance | maintainability | test_gap | architecture",
|
|
27
|
+
"severity": "low | medium | high | critical",
|
|
28
|
+
"title": "short title",
|
|
29
|
+
"file": "path/to/file",
|
|
30
|
+
"line_start": 1,
|
|
31
|
+
"line_end": 1,
|
|
32
|
+
"explanation": "detailed explanation of the issue",
|
|
33
|
+
"suggested_fix": "optional concrete fix",
|
|
34
|
+
"confidence": 0.0-1.0
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"merge_blocking": true/false,
|
|
38
|
+
"needs_human_attention": true/false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
If the code is clean, return an empty findings array with a positive summary.
|
|
42
|
+
Output ONLY the JSON object, no markdown fences.`
|
|
43
|
+
|
|
44
|
+
const CRITIQUE_SYSTEM = `You are a principal engineer reviewing another engineer's code review findings.
|
|
45
|
+
|
|
46
|
+
For each finding from the other reviewer:
|
|
47
|
+
1. If you agree, acknowledge it and explain why it matters
|
|
48
|
+
2. If you disagree, explain specifically why with code references
|
|
49
|
+
3. Identify any issues they missed entirely
|
|
50
|
+
|
|
51
|
+
You MUST acknowledge the strongest points from the other review, even if you disagree with others.
|
|
52
|
+
|
|
53
|
+
Respond with a JSON object:
|
|
54
|
+
{
|
|
55
|
+
"agreed_findings": ["description of each finding you agree with"],
|
|
56
|
+
"disputed_findings": [{"finding": "what they said", "reason": "why you disagree"}],
|
|
57
|
+
"missed_issues": [same schema as review findings above],
|
|
58
|
+
"overall_assessment": "your synthesis",
|
|
59
|
+
"revised_severity": "low | medium | high | critical"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Output ONLY the JSON object, no markdown fences.`
|
|
63
|
+
|
|
64
|
+
export class AnthropicClient implements ModelClient {
|
|
65
|
+
name = "anthropic" as const
|
|
66
|
+
private client: Anthropic
|
|
67
|
+
private model: string
|
|
68
|
+
|
|
69
|
+
constructor(apiKey: string, model: string) {
|
|
70
|
+
this.client = new Anthropic({ apiKey })
|
|
71
|
+
this.model = model
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async review(req: ReviewRequest): Promise<{ review: ModelReview; usage: TokenUsage }> {
|
|
75
|
+
const contextBlock = buildContextBlock(req)
|
|
76
|
+
|
|
77
|
+
const response = await this.call(
|
|
78
|
+
REVIEW_SYSTEM,
|
|
79
|
+
`Review this pull request:\n\n${contextBlock}\n\n${req.userPrompt}`
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const parsed = parseModelReview(response.text)
|
|
83
|
+
return {
|
|
84
|
+
review: normalizeReview(parsed, "anthropic"),
|
|
85
|
+
usage: response.usage,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async critique(req: CritiqueRequest): Promise<{ critique: CritiqueOutput; usage: TokenUsage }> {
|
|
90
|
+
const findingsJson = JSON.stringify(req.otherModelReview, null, 2)
|
|
91
|
+
|
|
92
|
+
const response = await this.call(
|
|
93
|
+
CRITIQUE_SYSTEM,
|
|
94
|
+
`Here are the findings from another code reviewer:\n\n${findingsJson}\n\nCritique these findings. What did they get right? What did they get wrong? What did they miss?`
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const parsed = parseCritiqueOutput(response.text)
|
|
98
|
+
return {
|
|
99
|
+
critique: {
|
|
100
|
+
agreedFindings: parsed.agreed_findings,
|
|
101
|
+
disputedFindings: parsed.disputed_findings,
|
|
102
|
+
missedIssues: parsed.missed_issues.map((f) => normalizeFinding(f, "anthropic")),
|
|
103
|
+
overallAssessment: parsed.overall_assessment,
|
|
104
|
+
revisedSeverity: parsed.revised_severity,
|
|
105
|
+
},
|
|
106
|
+
usage: response.usage,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async respondToCritique(req: CritiqueResponseRequest): Promise<{
|
|
111
|
+
response: CritiqueResponse
|
|
112
|
+
usage: TokenUsage
|
|
113
|
+
}> {
|
|
114
|
+
const critiqueJson = JSON.stringify(req.critique, null, 2)
|
|
115
|
+
const originalJson = JSON.stringify(req.originalReview, null, 2)
|
|
116
|
+
|
|
117
|
+
const response = await this.call(
|
|
118
|
+
REVIEW_SYSTEM,
|
|
119
|
+
`Your original review:\n${originalJson}\n\nA senior reviewer critiqued your findings:\n${critiqueJson}\n\nRespond to the critique. Accept valid points. Defend findings you believe are correct. Output revised findings.`
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const text = response.text
|
|
123
|
+
let parsed
|
|
124
|
+
try {
|
|
125
|
+
parsed = parseCritiqueResponse(text)
|
|
126
|
+
} catch {
|
|
127
|
+
return {
|
|
128
|
+
response: {
|
|
129
|
+
accepted: [],
|
|
130
|
+
disputed: [],
|
|
131
|
+
revisedFindings: req.originalReview.findings,
|
|
132
|
+
finalSummary: text.substring(0, 500),
|
|
133
|
+
},
|
|
134
|
+
usage: response.usage,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
response: {
|
|
140
|
+
accepted: parsed.accepted,
|
|
141
|
+
disputed: parsed.disputed,
|
|
142
|
+
revisedFindings: parsed.revised_findings.map((f) => normalizeFinding(f, "anthropic")),
|
|
143
|
+
finalSummary: parsed.final_summary,
|
|
144
|
+
},
|
|
145
|
+
usage: response.usage,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async chat(system: string, user: string): Promise<{ text: string; usage: TokenUsage }> {
|
|
150
|
+
return this.call(system, user)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async call(
|
|
154
|
+
system: string,
|
|
155
|
+
user: string,
|
|
156
|
+
retries = 1
|
|
157
|
+
): Promise<{ text: string; usage: TokenUsage }> {
|
|
158
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await this.client.messages.create({
|
|
161
|
+
model: this.model,
|
|
162
|
+
max_tokens: 4096,
|
|
163
|
+
system: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
|
|
164
|
+
messages: [{ role: "user", content: user }],
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const text = response.content
|
|
168
|
+
.filter((b): b is Anthropic.TextBlock => b.type === "text")
|
|
169
|
+
.map((b) => b.text)
|
|
170
|
+
.join("")
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
text,
|
|
174
|
+
usage: {
|
|
175
|
+
input: response.usage.input_tokens,
|
|
176
|
+
output: response.usage.output_tokens,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
} catch (err: unknown) {
|
|
180
|
+
const isRetryable = err instanceof Error && ("status" in err) &&
|
|
181
|
+
[429, 500, 529].includes((err as { status: number }).status)
|
|
182
|
+
|
|
183
|
+
if (attempt < retries && isRetryable) {
|
|
184
|
+
core.warning(`Anthropic call failed (attempt ${attempt + 1}), retrying...`)
|
|
185
|
+
await sleep(2000 * (attempt + 1))
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
throw err
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
throw new Error("Anthropic call exhausted retries")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildContextBlock(req: ReviewRequest): string {
|
|
197
|
+
const parts: string[] = []
|
|
198
|
+
const ctx = req.context
|
|
199
|
+
|
|
200
|
+
if (ctx.repoPolicies.reviewRulesMarkdown) {
|
|
201
|
+
parts.push(`## Repository Review Rules\n${ctx.repoPolicies.reviewRulesMarkdown}`)
|
|
202
|
+
}
|
|
203
|
+
if (ctx.repoPolicies.architectureNotes) {
|
|
204
|
+
parts.push(`## Architecture Notes\n${ctx.repoPolicies.architectureNotes}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return parts.join("\n\n")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeReview(
|
|
211
|
+
parsed: ReturnType<typeof parseModelReview>,
|
|
212
|
+
source: "anthropic" | "openai"
|
|
213
|
+
): ModelReview {
|
|
214
|
+
return {
|
|
215
|
+
summary: parsed.summary,
|
|
216
|
+
severity: parsed.severity,
|
|
217
|
+
confidence: parsed.confidence,
|
|
218
|
+
findings: parsed.findings.map((f) => normalizeFinding(f, source)),
|
|
219
|
+
mergeBlocking: parsed.merge_blocking,
|
|
220
|
+
needsHumanAttention: parsed.needs_human_attention,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeFinding(
|
|
225
|
+
f: { type: string; severity: string; title: string; file: string; line_start?: number; line_end?: number; explanation: string; suggested_fix?: string; confidence: number },
|
|
226
|
+
source: "anthropic" | "openai"
|
|
227
|
+
): ReviewFinding {
|
|
228
|
+
return {
|
|
229
|
+
type: f.type as ReviewFinding["type"],
|
|
230
|
+
severity: f.severity as ReviewFinding["severity"],
|
|
231
|
+
title: f.title,
|
|
232
|
+
file: f.file,
|
|
233
|
+
lineStart: f.line_start,
|
|
234
|
+
lineEnd: f.line_end,
|
|
235
|
+
explanation: f.explanation,
|
|
236
|
+
suggestedFix: f.suggested_fix,
|
|
237
|
+
confidence: f.confidence,
|
|
238
|
+
source,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sleep(ms: number): Promise<void> {
|
|
243
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
244
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import OpenAI from "openai"
|
|
2
|
+
import * as core from "@actions/core"
|
|
3
|
+
import type { ModelClient, ReviewRequest, CritiqueRequest, CritiqueResponseRequest } from "./types"
|
|
4
|
+
import type { ModelReview, CritiqueOutput, CritiqueResponse, ReviewFinding, TokenUsage } from "../types"
|
|
5
|
+
import { parseModelReview, parseCritiqueOutput, parseCritiqueResponse } from "../schemas/review"
|
|
6
|
+
|
|
7
|
+
const REVIEW_SYSTEM = `You are a repo-aware implementation engineer reviewing code changes. You are precise, concrete, and focused on correctness at the code level.
|
|
8
|
+
|
|
9
|
+
Your priorities:
|
|
10
|
+
1. Concrete bugs — specific lines with specific problems
|
|
11
|
+
2. Input validation — missing checks, unhandled errors, boundary conditions
|
|
12
|
+
3. Type safety — incorrect types, unsafe casts, missing null checks
|
|
13
|
+
4. Resource handling — unclosed connections, memory leaks, missing cleanup
|
|
14
|
+
5. Test coverage — specific test cases that should exist for this change
|
|
15
|
+
6. Convention violations — does this match the rest of the codebase?
|
|
16
|
+
|
|
17
|
+
Focus on code-level specifics, not high-level architecture. Be precise about file paths and line numbers.
|
|
18
|
+
|
|
19
|
+
Respond with a JSON object matching this exact schema:
|
|
20
|
+
{
|
|
21
|
+
"summary": "2-3 sentence code-level assessment",
|
|
22
|
+
"severity": "low | medium | high | critical",
|
|
23
|
+
"confidence": 0.0-1.0,
|
|
24
|
+
"findings": [
|
|
25
|
+
{
|
|
26
|
+
"type": "bug | security | performance | maintainability | test_gap | architecture",
|
|
27
|
+
"severity": "low | medium | high | critical",
|
|
28
|
+
"title": "short title",
|
|
29
|
+
"file": "path/to/file",
|
|
30
|
+
"line_start": 1,
|
|
31
|
+
"line_end": 1,
|
|
32
|
+
"explanation": "detailed explanation with code reference",
|
|
33
|
+
"suggested_fix": "concrete code fix",
|
|
34
|
+
"confidence": 0.0-1.0
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"merge_blocking": true/false,
|
|
38
|
+
"needs_human_attention": true/false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
If the code is clean, return an empty findings array with a positive summary.
|
|
42
|
+
Output ONLY the JSON object, no markdown fences.`
|
|
43
|
+
|
|
44
|
+
const CRITIQUE_RESPONSE_SYSTEM = `You are an implementation engineer responding to a senior reviewer's critique of your code review.
|
|
45
|
+
|
|
46
|
+
For each critique:
|
|
47
|
+
1. If they're right, accept it explicitly
|
|
48
|
+
2. If you disagree, provide a specific rebuttal with code references
|
|
49
|
+
3. Update your findings list based on valid critiques
|
|
50
|
+
|
|
51
|
+
You MUST accept valid critiques. Do not be defensive.
|
|
52
|
+
|
|
53
|
+
Respond with a JSON object:
|
|
54
|
+
{
|
|
55
|
+
"accepted": ["description of each accepted critique"],
|
|
56
|
+
"disputed": [{"critique": "what they said", "rebuttal": "your specific rebuttal"}],
|
|
57
|
+
"revised_findings": [same schema as review findings — your FINAL revised list],
|
|
58
|
+
"final_summary": "updated assessment incorporating valid critiques"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Output ONLY the JSON object, no markdown fences.`
|
|
62
|
+
|
|
63
|
+
export class OpenAIClient implements ModelClient {
|
|
64
|
+
name = "openai" as const
|
|
65
|
+
private client: OpenAI
|
|
66
|
+
private model: string
|
|
67
|
+
|
|
68
|
+
constructor(apiKey: string, model: string) {
|
|
69
|
+
this.client = new OpenAI({ apiKey })
|
|
70
|
+
this.model = model
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async review(req: ReviewRequest): Promise<{ review: ModelReview; usage: TokenUsage }> {
|
|
74
|
+
const contextBlock = buildContextBlock(req)
|
|
75
|
+
|
|
76
|
+
const response = await this.call(
|
|
77
|
+
REVIEW_SYSTEM,
|
|
78
|
+
`Review this pull request:\n\n${contextBlock}\n\n${req.userPrompt}`
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const parsed = parseModelReview(response.text)
|
|
82
|
+
return {
|
|
83
|
+
review: normalizeReview(parsed, "openai"),
|
|
84
|
+
usage: response.usage,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async critique(req: CritiqueRequest): Promise<{ critique: CritiqueOutput; usage: TokenUsage }> {
|
|
89
|
+
const findingsJson = JSON.stringify(req.otherModelReview, null, 2)
|
|
90
|
+
|
|
91
|
+
const response = await this.call(
|
|
92
|
+
REVIEW_SYSTEM,
|
|
93
|
+
`Here are findings from another reviewer:\n\n${findingsJson}\n\nCritique these findings. What's correct? What's wrong? What's missing?\n\nRespond as JSON: {"agreed_findings":[],"disputed_findings":[{"finding":"","reason":""}],"missed_issues":[],"overall_assessment":"","revised_severity":""}`
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const parsed = parseCritiqueOutput(response.text)
|
|
97
|
+
return {
|
|
98
|
+
critique: {
|
|
99
|
+
agreedFindings: parsed.agreed_findings,
|
|
100
|
+
disputedFindings: parsed.disputed_findings,
|
|
101
|
+
missedIssues: parsed.missed_issues.map((f) => normalizeFinding(f, "openai")),
|
|
102
|
+
overallAssessment: parsed.overall_assessment,
|
|
103
|
+
revisedSeverity: parsed.revised_severity,
|
|
104
|
+
},
|
|
105
|
+
usage: response.usage,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async respondToCritique(req: CritiqueResponseRequest): Promise<{
|
|
110
|
+
response: CritiqueResponse
|
|
111
|
+
usage: TokenUsage
|
|
112
|
+
}> {
|
|
113
|
+
const critiqueJson = JSON.stringify(req.critique, null, 2)
|
|
114
|
+
const originalJson = JSON.stringify(req.originalReview, null, 2)
|
|
115
|
+
|
|
116
|
+
const response = await this.call(
|
|
117
|
+
CRITIQUE_RESPONSE_SYSTEM,
|
|
118
|
+
`Your original review:\n${originalJson}\n\nA senior reviewer critiqued your findings:\n${critiqueJson}\n\nRespond to the critique.`
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
let parsed
|
|
122
|
+
try {
|
|
123
|
+
parsed = parseCritiqueResponse(response.text)
|
|
124
|
+
} catch {
|
|
125
|
+
return {
|
|
126
|
+
response: {
|
|
127
|
+
accepted: [],
|
|
128
|
+
disputed: [],
|
|
129
|
+
revisedFindings: req.originalReview.findings,
|
|
130
|
+
finalSummary: response.text.substring(0, 500),
|
|
131
|
+
},
|
|
132
|
+
usage: response.usage,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
response: {
|
|
138
|
+
accepted: parsed.accepted,
|
|
139
|
+
disputed: parsed.disputed,
|
|
140
|
+
revisedFindings: parsed.revised_findings.map((f) => normalizeFinding(f, "openai")),
|
|
141
|
+
finalSummary: parsed.final_summary,
|
|
142
|
+
},
|
|
143
|
+
usage: response.usage,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async chat(system: string, user: string): Promise<{ text: string; usage: import("../types").TokenUsage }> {
|
|
148
|
+
return this.call(system, user)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async call(
|
|
152
|
+
system: string,
|
|
153
|
+
user: string,
|
|
154
|
+
retries = 1
|
|
155
|
+
): Promise<{ text: string; usage: TokenUsage }> {
|
|
156
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
157
|
+
try {
|
|
158
|
+
const response = await this.client.chat.completions.create({
|
|
159
|
+
model: this.model,
|
|
160
|
+
messages: [
|
|
161
|
+
{ role: "system", content: system },
|
|
162
|
+
{ role: "user", content: user },
|
|
163
|
+
],
|
|
164
|
+
max_tokens: 4096,
|
|
165
|
+
temperature: 0.1,
|
|
166
|
+
response_format: { type: "json_object" },
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const text = response.choices[0]?.message?.content || ""
|
|
170
|
+
return {
|
|
171
|
+
text,
|
|
172
|
+
usage: {
|
|
173
|
+
input: response.usage?.prompt_tokens || 0,
|
|
174
|
+
output: response.usage?.completion_tokens || 0,
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
const status = (err as { status?: number }).status
|
|
179
|
+
const isRetryable = status === 429 || status === 500 || status === 503
|
|
180
|
+
|
|
181
|
+
if (attempt < retries && isRetryable) {
|
|
182
|
+
core.warning(`OpenAI call failed (attempt ${attempt + 1}), retrying...`)
|
|
183
|
+
await sleep(2000 * (attempt + 1))
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error("OpenAI call exhausted retries")
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildContextBlock(req: ReviewRequest): string {
|
|
195
|
+
const parts: string[] = []
|
|
196
|
+
const ctx = req.context
|
|
197
|
+
|
|
198
|
+
if (ctx.repoPolicies.reviewRulesMarkdown) {
|
|
199
|
+
parts.push(`## Repository Review Rules\n${ctx.repoPolicies.reviewRulesMarkdown}`)
|
|
200
|
+
}
|
|
201
|
+
if (ctx.repoPolicies.architectureNotes) {
|
|
202
|
+
parts.push(`## Architecture Notes\n${ctx.repoPolicies.architectureNotes}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return parts.join("\n\n")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeReview(
|
|
209
|
+
parsed: ReturnType<typeof parseModelReview>,
|
|
210
|
+
source: "anthropic" | "openai"
|
|
211
|
+
): ModelReview {
|
|
212
|
+
return {
|
|
213
|
+
summary: parsed.summary,
|
|
214
|
+
severity: parsed.severity,
|
|
215
|
+
confidence: parsed.confidence,
|
|
216
|
+
findings: parsed.findings.map((f) => normalizeFinding(f, source)),
|
|
217
|
+
mergeBlocking: parsed.merge_blocking,
|
|
218
|
+
needsHumanAttention: parsed.needs_human_attention,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeFinding(
|
|
223
|
+
f: { type: string; severity: string; title: string; file: string; line_start?: number; line_end?: number; explanation: string; suggested_fix?: string; confidence: number },
|
|
224
|
+
source: "anthropic" | "openai"
|
|
225
|
+
): ReviewFinding {
|
|
226
|
+
return {
|
|
227
|
+
type: f.type as ReviewFinding["type"],
|
|
228
|
+
severity: f.severity as ReviewFinding["severity"],
|
|
229
|
+
title: f.title,
|
|
230
|
+
file: f.file,
|
|
231
|
+
lineStart: f.line_start,
|
|
232
|
+
lineEnd: f.line_end,
|
|
233
|
+
explanation: f.explanation,
|
|
234
|
+
suggestedFix: f.suggested_fix,
|
|
235
|
+
confidence: f.confidence,
|
|
236
|
+
source,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sleep(ms: number): Promise<void> {
|
|
241
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
242
|
+
}
|