@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,250 @@
|
|
|
1
|
+
import * as https from "https"
|
|
2
|
+
import * as http from "http"
|
|
3
|
+
import * as fs from "fs"
|
|
4
|
+
import * as path from "path"
|
|
5
|
+
import * as cp from "child_process"
|
|
6
|
+
import * as core from "@actions/core"
|
|
7
|
+
import type { SubwayContact, FinalDecision, FindingSeverity } from "./types"
|
|
8
|
+
|
|
9
|
+
const CONTACT_FILE = ".subway/pr-contact"
|
|
10
|
+
const DIRECT_CALL_MAX_AGE_MS = 60 * 60 * 1000
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 10_000
|
|
12
|
+
const SUBWAY_TRAILER = "Subway-Agent"
|
|
13
|
+
|
|
14
|
+
export interface SubwayNotifyContext {
|
|
15
|
+
prNumber: number
|
|
16
|
+
prUrl: string
|
|
17
|
+
repo: string
|
|
18
|
+
runUrl: string
|
|
19
|
+
headSha: string
|
|
20
|
+
prState: "open" | "closed" | "merged"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SubwayPayload {
|
|
24
|
+
pr_number: number
|
|
25
|
+
pr_url: string
|
|
26
|
+
repo: string
|
|
27
|
+
head_sha: string
|
|
28
|
+
pr_state: "open" | "closed" | "merged"
|
|
29
|
+
action: string
|
|
30
|
+
has_blockers: boolean
|
|
31
|
+
findings_count: number
|
|
32
|
+
critical: number
|
|
33
|
+
high: number
|
|
34
|
+
medium: number
|
|
35
|
+
low: number
|
|
36
|
+
quality_score: number
|
|
37
|
+
model_agreement: number
|
|
38
|
+
run_url: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readPrContact(workspaceDir = process.cwd()): SubwayContact | null {
|
|
42
|
+
try {
|
|
43
|
+
const contactPath = path.join(workspaceDir, CONTACT_FILE)
|
|
44
|
+
if (!fs.existsSync(contactPath)) return null
|
|
45
|
+
const raw = fs.readFileSync(contactPath, "utf-8").trim()
|
|
46
|
+
const parsed = JSON.parse(raw) as Partial<SubwayContact>
|
|
47
|
+
if (!parsed.name || typeof parsed.name !== "string") {
|
|
48
|
+
core.warning("Subway: .subway/pr-contact missing required 'name' field")
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
name: parsed.name,
|
|
53
|
+
relay: parsed.relay ?? "relay.subway.dev",
|
|
54
|
+
registered_at: parsed.registered_at ?? new Date(0).toISOString(),
|
|
55
|
+
source: parsed.source ?? "cli",
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
core.info(`Subway: could not read .subway/pr-contact — ${(err as Error).message}`)
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseTrailerContact(msg: string): SubwayContact | null {
|
|
64
|
+
for (const line of msg.split("\n").reverse()) {
|
|
65
|
+
const match = line.match(new RegExp(`^${SUBWAY_TRAILER}:\\s*(.+)$`, "i"))
|
|
66
|
+
if (match) {
|
|
67
|
+
const name = match[1].trim()
|
|
68
|
+
if (!name) continue
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
relay: "relay.subway.dev",
|
|
72
|
+
registered_at: new Date().toISOString(),
|
|
73
|
+
source: "cli",
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readCommitContact(headSha = "HEAD"): SubwayContact | null {
|
|
81
|
+
try {
|
|
82
|
+
const result = cp.spawnSync("git", ["log", "-1", "--format=%B", headSha], {
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (result.error) {
|
|
88
|
+
core.info(`Subway: git spawn failed — ${result.error.message}`)
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
if (result.status !== 0) {
|
|
92
|
+
core.info(`Subway: git log failed — ${result.stderr?.trim() || "unknown error"}`)
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const msg = (result.stdout ?? "").trim()
|
|
97
|
+
const contact = parseTrailerContact(msg)
|
|
98
|
+
if (contact) core.info(`Subway: found ${SUBWAY_TRAILER} trailer → ${contact.name}`)
|
|
99
|
+
return contact
|
|
100
|
+
} catch (err) {
|
|
101
|
+
core.info(`Subway: could not read commit message — ${(err as Error).message}`)
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isContactFresh(contact: SubwayContact, maxAgeMs = DIRECT_CALL_MAX_AGE_MS): boolean {
|
|
107
|
+
try {
|
|
108
|
+
const registeredAt = new Date(contact.registered_at).getTime()
|
|
109
|
+
if (isNaN(registeredAt)) return false
|
|
110
|
+
return Date.now() - registeredAt < maxAgeMs
|
|
111
|
+
} catch {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildPayload(decision: FinalDecision, ctx: SubwayNotifyContext): SubwayPayload {
|
|
117
|
+
const counts = countBySeverity(decision.findings)
|
|
118
|
+
|
|
119
|
+
const qualityArtifact = computeQuality(decision)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
pr_number: ctx.prNumber,
|
|
123
|
+
pr_url: ctx.prUrl,
|
|
124
|
+
repo: ctx.repo,
|
|
125
|
+
head_sha: ctx.headSha,
|
|
126
|
+
pr_state: ctx.prState,
|
|
127
|
+
action: decision.action,
|
|
128
|
+
has_blockers: decision.action === "request_changes" || decision.action === "needs_human_review",
|
|
129
|
+
findings_count: decision.findings.length,
|
|
130
|
+
critical: counts.critical,
|
|
131
|
+
high: counts.high,
|
|
132
|
+
medium: counts.medium,
|
|
133
|
+
low: counts.low,
|
|
134
|
+
quality_score: qualityArtifact.quality_score,
|
|
135
|
+
model_agreement: qualityArtifact.model_agreement,
|
|
136
|
+
run_url: ctx.runUrl,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function safeBroadcastTopic(repo: string, prNumber: number): string {
|
|
141
|
+
const safeRepo = repo.replace(/[^a-zA-Z0-9]/g, ".")
|
|
142
|
+
return `ci.sentinel.${safeRepo}.pr${prNumber}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function notifySubwayAgent(
|
|
146
|
+
contact: SubwayContact | null,
|
|
147
|
+
decision: FinalDecision,
|
|
148
|
+
ctx: SubwayNotifyContext,
|
|
149
|
+
bridgeUrl: string
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const payload = buildPayload(decision, ctx)
|
|
152
|
+
const payloadJson = JSON.stringify(payload)
|
|
153
|
+
const topic = safeBroadcastTopic(ctx.repo, ctx.prNumber)
|
|
154
|
+
const base = bridgeUrl.replace(/\/$/, "")
|
|
155
|
+
|
|
156
|
+
let directCallSucceeded = false
|
|
157
|
+
|
|
158
|
+
if (contact && isContactFresh(contact)) {
|
|
159
|
+
core.info(`Subway: agent contact found — ${contact.name} (registered ${contact.registered_at})`)
|
|
160
|
+
try {
|
|
161
|
+
await post(base + "/v1/call", JSON.stringify({
|
|
162
|
+
to: contact.name,
|
|
163
|
+
method: "ci_sentinel_result",
|
|
164
|
+
payload: payloadJson,
|
|
165
|
+
}))
|
|
166
|
+
core.info(`Subway: direct call delivered to ${contact.name}`)
|
|
167
|
+
directCallSucceeded = true
|
|
168
|
+
} catch (err) {
|
|
169
|
+
core.info(`Subway: direct call to ${contact.name} failed (${(err as Error).message}) — falling back to broadcast`)
|
|
170
|
+
}
|
|
171
|
+
} else if (contact) {
|
|
172
|
+
core.info(`Subway: contact ${contact.name} is stale (registered ${contact.registered_at}) — broadcast only`)
|
|
173
|
+
} else {
|
|
174
|
+
core.info("Subway: no .subway/pr-contact — broadcast only")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await post(base + "/v1/broadcast", JSON.stringify({
|
|
179
|
+
topic,
|
|
180
|
+
message_type: "ci_sentinel_result",
|
|
181
|
+
payload: payloadJson,
|
|
182
|
+
}))
|
|
183
|
+
core.info(`Subway: broadcast → ${topic}`)
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (!directCallSucceeded) {
|
|
186
|
+
core.warning(`Subway: broadcast also failed — ${(err as Error).message}. Is the bridge reachable at ${base}?`)
|
|
187
|
+
} else {
|
|
188
|
+
core.info(`Subway: broadcast failed (${(err as Error).message}) but direct call succeeded`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function post(url: string, body: string): Promise<string> {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const parsed = new URL(url)
|
|
196
|
+
const transport = parsed.protocol === "https:" ? https : http
|
|
197
|
+
const options = {
|
|
198
|
+
hostname: parsed.hostname,
|
|
199
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
200
|
+
path: parsed.pathname + parsed.search,
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
"Content-Length": Buffer.byteLength(body),
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const req = transport.request(options, (res) => {
|
|
209
|
+
let data = ""
|
|
210
|
+
res.on("data", (chunk) => { data += chunk })
|
|
211
|
+
res.on("end", () => {
|
|
212
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
213
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`))
|
|
214
|
+
} else {
|
|
215
|
+
resolve(data)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
221
|
+
req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`))
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
req.on("error", reject)
|
|
225
|
+
req.write(body)
|
|
226
|
+
req.end()
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function countBySeverity(findings: { severity: FindingSeverity }[]): Record<FindingSeverity, number> {
|
|
231
|
+
const counts: Record<FindingSeverity, number> = { critical: 0, high: 0, medium: 0, low: 0 }
|
|
232
|
+
for (const f of findings) counts[f.severity]++
|
|
233
|
+
return counts
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function computeQuality(decision: FinalDecision): { quality_score: number; model_agreement: number } {
|
|
237
|
+
const counts = countBySeverity(decision.findings)
|
|
238
|
+
const penalties = counts.critical * 0.25 + counts.high * 0.15 + counts.medium * 0.05 + counts.low * 0.01
|
|
239
|
+
const quality_score = Math.max(0, Math.min(1, 1.0 - penalties))
|
|
240
|
+
|
|
241
|
+
let model_agreement = 1.0
|
|
242
|
+
if (decision.anthropicReview && decision.openaiReview && decision.critique) {
|
|
243
|
+
const agreed = decision.critique.agreedFindings.length
|
|
244
|
+
const disputed = decision.critique.disputedFindings.length
|
|
245
|
+
const total = agreed + disputed
|
|
246
|
+
model_agreement = total > 0 ? agreed / total : 1.0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { quality_score, model_agreement }
|
|
250
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
export type EventType = "pull_request" | "issue" | "issue_comment" | "pull_request_review_comment"
|
|
2
|
+
|
|
3
|
+
export interface SubwayContact {
|
|
4
|
+
name: string
|
|
5
|
+
relay: string
|
|
6
|
+
registered_at: string
|
|
7
|
+
source: "pi-extension" | "claude-code" | "cli"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ReviewMode =
|
|
11
|
+
| "review"
|
|
12
|
+
| "review_and_suggest"
|
|
13
|
+
| "review_and_patch"
|
|
14
|
+
| "issue_triage"
|
|
15
|
+
| "issue_fix"
|
|
16
|
+
| "manual_only"
|
|
17
|
+
|
|
18
|
+
export type ActionType =
|
|
19
|
+
| "pr_review"
|
|
20
|
+
| "pr_fix"
|
|
21
|
+
| "issue_triage"
|
|
22
|
+
| "issue_fix"
|
|
23
|
+
| "slash_command"
|
|
24
|
+
| "respond"
|
|
25
|
+
| "noop"
|
|
26
|
+
|
|
27
|
+
export type FixMode = "propose_only" | "propose_and_pr" | "yolo"
|
|
28
|
+
|
|
29
|
+
export type FindingSeverity = "low" | "medium" | "high" | "critical"
|
|
30
|
+
|
|
31
|
+
export type FindingType =
|
|
32
|
+
| "bug"
|
|
33
|
+
| "security"
|
|
34
|
+
| "performance"
|
|
35
|
+
| "maintainability"
|
|
36
|
+
| "test_gap"
|
|
37
|
+
| "architecture"
|
|
38
|
+
|
|
39
|
+
export type FinalAction =
|
|
40
|
+
| "comment_only"
|
|
41
|
+
| "request_changes"
|
|
42
|
+
| "suggest_patch"
|
|
43
|
+
| "open_fix_pr"
|
|
44
|
+
| "push_to_branch"
|
|
45
|
+
| "needs_human_review"
|
|
46
|
+
| "decline"
|
|
47
|
+
|
|
48
|
+
export interface ChangedFile {
|
|
49
|
+
path: string
|
|
50
|
+
patch?: string
|
|
51
|
+
status: "added" | "modified" | "removed" | "renamed"
|
|
52
|
+
additions: number
|
|
53
|
+
deletions: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ReviewContext {
|
|
57
|
+
repository: {
|
|
58
|
+
owner: string
|
|
59
|
+
name: string
|
|
60
|
+
defaultBranch: string
|
|
61
|
+
}
|
|
62
|
+
event: {
|
|
63
|
+
type: EventType
|
|
64
|
+
action: string
|
|
65
|
+
actor: string
|
|
66
|
+
trustedActor: boolean
|
|
67
|
+
isFork: boolean
|
|
68
|
+
authorAssociation?: string
|
|
69
|
+
}
|
|
70
|
+
pullRequest?: {
|
|
71
|
+
number: number
|
|
72
|
+
title: string
|
|
73
|
+
body: string
|
|
74
|
+
baseRef: string
|
|
75
|
+
headRef: string
|
|
76
|
+
labels: string[]
|
|
77
|
+
changedFiles: ChangedFile[]
|
|
78
|
+
ciStatus?: string
|
|
79
|
+
}
|
|
80
|
+
issue?: {
|
|
81
|
+
number: number
|
|
82
|
+
title: string
|
|
83
|
+
body: string
|
|
84
|
+
labels: string[]
|
|
85
|
+
comments?: string[]
|
|
86
|
+
}
|
|
87
|
+
repoPolicies: RepoPolicies
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RepoPolicies {
|
|
91
|
+
mode: ReviewMode
|
|
92
|
+
autoFixEnabled: boolean
|
|
93
|
+
restrictedPaths: string[]
|
|
94
|
+
testCommands: string[]
|
|
95
|
+
maxFiles: number
|
|
96
|
+
maxPatchChars: number
|
|
97
|
+
reviewRulesMarkdown?: string
|
|
98
|
+
architectureNotes?: string
|
|
99
|
+
severityThreshold: FindingSeverity
|
|
100
|
+
blockForkMutation: boolean
|
|
101
|
+
inlineComments: boolean
|
|
102
|
+
commentStyle: "concise" | "comprehensive"
|
|
103
|
+
models: {
|
|
104
|
+
anthropic: { enabled: boolean; model: string }
|
|
105
|
+
openai: { enabled: boolean; model: string }
|
|
106
|
+
}
|
|
107
|
+
trigger: {
|
|
108
|
+
requireLabel: string
|
|
109
|
+
respondToMentions: boolean
|
|
110
|
+
respondToReplies: boolean
|
|
111
|
+
botName: string
|
|
112
|
+
}
|
|
113
|
+
fix: {
|
|
114
|
+
mode: FixMode
|
|
115
|
+
confidenceThreshold: number
|
|
116
|
+
createDraftPr: boolean
|
|
117
|
+
maxRetryCount: number
|
|
118
|
+
}
|
|
119
|
+
review: {
|
|
120
|
+
summaryOnClean: boolean
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ReviewFinding {
|
|
125
|
+
type: FindingType
|
|
126
|
+
severity: FindingSeverity
|
|
127
|
+
title: string
|
|
128
|
+
file: string
|
|
129
|
+
lineStart?: number
|
|
130
|
+
lineEnd?: number
|
|
131
|
+
explanation: string
|
|
132
|
+
suggestedFix?: string
|
|
133
|
+
confidence: number
|
|
134
|
+
source: "anthropic" | "openai" | "merged"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ModelReview {
|
|
138
|
+
summary: string
|
|
139
|
+
severity: FindingSeverity
|
|
140
|
+
confidence: number
|
|
141
|
+
findings: ReviewFinding[]
|
|
142
|
+
mergeBlocking: boolean
|
|
143
|
+
needsHumanAttention: boolean
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface CritiqueOutput {
|
|
147
|
+
agreedFindings: string[]
|
|
148
|
+
disputedFindings: Array<{ finding: string; reason: string }>
|
|
149
|
+
missedIssues: ReviewFinding[]
|
|
150
|
+
overallAssessment: string
|
|
151
|
+
revisedSeverity: FindingSeverity
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface CritiqueResponse {
|
|
155
|
+
accepted: string[]
|
|
156
|
+
disputed: Array<{ critique: string; rebuttal: string }>
|
|
157
|
+
revisedFindings: ReviewFinding[]
|
|
158
|
+
finalSummary: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface FinalDecision {
|
|
162
|
+
action: FinalAction
|
|
163
|
+
rationale: string
|
|
164
|
+
findings: ReviewFinding[]
|
|
165
|
+
anthropicReview?: ModelReview
|
|
166
|
+
openaiReview?: ModelReview
|
|
167
|
+
critique?: CritiqueOutput
|
|
168
|
+
critiqueResponse?: CritiqueResponse
|
|
169
|
+
tokenUsage: {
|
|
170
|
+
anthropic: { input: number; output: number }
|
|
171
|
+
openai: { input: number; output: number }
|
|
172
|
+
}
|
|
173
|
+
durationMs: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface SlashCommand {
|
|
177
|
+
command: string
|
|
178
|
+
args: string[]
|
|
179
|
+
actor: string
|
|
180
|
+
issueNumber: number
|
|
181
|
+
isPR: boolean
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RoutedEvent {
|
|
185
|
+
actionType: ActionType
|
|
186
|
+
context: ReviewContext
|
|
187
|
+
slashCommand?: SlashCommand
|
|
188
|
+
responseContext?: ResponseContext
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface ResponseContext {
|
|
192
|
+
parentCommentBody: string
|
|
193
|
+
replyBody: string
|
|
194
|
+
commentId: number
|
|
195
|
+
isPRReviewComment: boolean
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface TokenUsage {
|
|
199
|
+
input: number
|
|
200
|
+
output: number
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface FileChange {
|
|
204
|
+
path: string
|
|
205
|
+
action: "modify" | "create" | "delete"
|
|
206
|
+
changes?: Array<{ search: string; replace: string }>
|
|
207
|
+
content?: string
|
|
208
|
+
explanation: string
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface FixPlan {
|
|
212
|
+
analysis: string
|
|
213
|
+
fixable: boolean
|
|
214
|
+
confidence: number
|
|
215
|
+
files: FileChange[]
|
|
216
|
+
commitMessage: string
|
|
217
|
+
testSuggestions: string[]
|
|
218
|
+
riskNotes: string[]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface FixResult {
|
|
222
|
+
success: boolean
|
|
223
|
+
fixPlan?: FixPlan
|
|
224
|
+
branch?: string
|
|
225
|
+
prNumber?: number
|
|
226
|
+
prUrl?: string
|
|
227
|
+
error?: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface CodeContext {
|
|
231
|
+
files: Array<{ path: string; content: string; relevance: string }>
|
|
232
|
+
structure: string
|
|
233
|
+
dependencies: string
|
|
234
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "__tests__"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
mode: review
|
|
2
|
+
|
|
3
|
+
models:
|
|
4
|
+
anthropic:
|
|
5
|
+
enabled: true
|
|
6
|
+
model: claude-sonnet-4-20250514
|
|
7
|
+
openai:
|
|
8
|
+
enabled: true
|
|
9
|
+
model: gpt-4o
|
|
10
|
+
|
|
11
|
+
trigger:
|
|
12
|
+
require_label: ""
|
|
13
|
+
respond_to_mentions: true
|
|
14
|
+
respond_to_replies: true
|
|
15
|
+
bot_name: sentinel
|
|
16
|
+
|
|
17
|
+
review:
|
|
18
|
+
max_files: 50
|
|
19
|
+
max_patch_chars: 200000
|
|
20
|
+
comment_style: comprehensive
|
|
21
|
+
inline_comments: true
|
|
22
|
+
severity_threshold: low
|
|
23
|
+
|
|
24
|
+
fix:
|
|
25
|
+
mode: propose_and_pr
|
|
26
|
+
confidence_threshold: 0.7
|
|
27
|
+
max_retry_count: 2
|
|
28
|
+
create_draft_pr: true
|
|
29
|
+
|
|
30
|
+
security:
|
|
31
|
+
restricted_paths:
|
|
32
|
+
- ".github/workflows/**"
|
|
33
|
+
- ".github/sentinel.yml"
|
|
34
|
+
block_fork_mutation: true
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: npm-publish
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '20'
|
|
22
|
+
registry-url: 'https://registry.npmjs.org'
|
|
23
|
+
|
|
24
|
+
- run: npm install -g npm@latest
|
|
25
|
+
|
|
26
|
+
- run: npm ci
|
|
27
|
+
|
|
28
|
+
- run: npm publish --provenance --access public
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Sentinel
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, labeled]
|
|
6
|
+
issues:
|
|
7
|
+
types: [opened, labeled]
|
|
8
|
+
issue_comment:
|
|
9
|
+
types: [created]
|
|
10
|
+
pull_request_review_comment:
|
|
11
|
+
types: [created]
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
issues: write
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: sentinel-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
sentinel:
|
|
24
|
+
name: Sentinel
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 15
|
|
27
|
+
if: |
|
|
28
|
+
(github.event_name == 'pull_request') ||
|
|
29
|
+
(github.event_name == 'issues') ||
|
|
30
|
+
(github.event_name == 'issue_comment' && (
|
|
31
|
+
contains(github.event.comment.body, '@sentinel') ||
|
|
32
|
+
contains(github.event.comment.body, '/bot')
|
|
33
|
+
) && (
|
|
34
|
+
github.event.comment.author_association == 'MEMBER' ||
|
|
35
|
+
github.event.comment.author_association == 'OWNER' ||
|
|
36
|
+
github.event.comment.author_association == 'COLLABORATOR'
|
|
37
|
+
)) ||
|
|
38
|
+
(github.event_name == 'pull_request_review_comment' && (
|
|
39
|
+
github.event.comment.author_association == 'MEMBER' ||
|
|
40
|
+
github.event.comment.author_association == 'OWNER' ||
|
|
41
|
+
github.event.comment.author_association == 'COLLABORATOR'
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
with:
|
|
47
|
+
fetch-depth: 0
|
|
48
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
|
|
50
|
+
- uses: ./.github/sentinel
|
|
51
|
+
with:
|
|
52
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
53
|
+
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
|
|
54
|
+
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
|
|
55
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|