@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.
Files changed (76) hide show
  1. package/.github/sentinel/action.yml +100 -0
  2. package/.github/sentinel/dist/codebase.d.ts +3 -0
  3. package/.github/sentinel/dist/codebase.d.ts.map +1 -0
  4. package/.github/sentinel/dist/context.d.ts +6 -0
  5. package/.github/sentinel/dist/context.d.ts.map +1 -0
  6. package/.github/sentinel/dist/fixer.d.ts +6 -0
  7. package/.github/sentinel/dist/fixer.d.ts.map +1 -0
  8. package/.github/sentinel/dist/index.d.ts +1 -0
  9. package/.github/sentinel/dist/index.d.ts.map +1 -0
  10. package/.github/sentinel/dist/index.js +68808 -0
  11. package/.github/sentinel/dist/index.js.map +1 -0
  12. package/.github/sentinel/dist/licenses.txt +1152 -0
  13. package/.github/sentinel/dist/models/anthropic.d.ts +26 -0
  14. package/.github/sentinel/dist/models/anthropic.d.ts.map +1 -0
  15. package/.github/sentinel/dist/models/openai.d.ts +26 -0
  16. package/.github/sentinel/dist/models/openai.d.ts.map +1 -0
  17. package/.github/sentinel/dist/models/openrouter.d.ts +31 -0
  18. package/.github/sentinel/dist/models/openrouter.d.ts.map +1 -0
  19. package/.github/sentinel/dist/models/types.d.ts +37 -0
  20. package/.github/sentinel/dist/models/types.d.ts.map +1 -0
  21. package/.github/sentinel/dist/orchestrator.d.ts +3 -0
  22. package/.github/sentinel/dist/orchestrator.d.ts.map +1 -0
  23. package/.github/sentinel/dist/policy.d.ts +15 -0
  24. package/.github/sentinel/dist/policy.d.ts.map +1 -0
  25. package/.github/sentinel/dist/reporter.d.ts +8 -0
  26. package/.github/sentinel/dist/reporter.d.ts.map +1 -0
  27. package/.github/sentinel/dist/responder.d.ts +6 -0
  28. package/.github/sentinel/dist/responder.d.ts.map +1 -0
  29. package/.github/sentinel/dist/router.d.ts +2 -0
  30. package/.github/sentinel/dist/router.d.ts.map +1 -0
  31. package/.github/sentinel/dist/schemas/config.d.ts +195 -0
  32. package/.github/sentinel/dist/schemas/config.d.ts.map +1 -0
  33. package/.github/sentinel/dist/schemas/fix.d.ts +130 -0
  34. package/.github/sentinel/dist/schemas/fix.d.ts.map +1 -0
  35. package/.github/sentinel/dist/schemas/review.d.ts +275 -0
  36. package/.github/sentinel/dist/schemas/review.d.ts.map +1 -0
  37. package/.github/sentinel/dist/sourcemap-register.js +1 -0
  38. package/.github/sentinel/dist/subway.d.ts +31 -0
  39. package/.github/sentinel/dist/subway.d.ts.map +1 -0
  40. package/.github/sentinel/dist/types.d.ts +210 -0
  41. package/.github/sentinel/dist/types.d.ts.map +1 -0
  42. package/.github/sentinel/package-lock.json +2389 -0
  43. package/.github/sentinel/package.json +29 -0
  44. package/.github/sentinel/src/codebase.ts +265 -0
  45. package/.github/sentinel/src/context.ts +182 -0
  46. package/.github/sentinel/src/fixer.ts +353 -0
  47. package/.github/sentinel/src/index.ts +263 -0
  48. package/.github/sentinel/src/models/anthropic.ts +244 -0
  49. package/.github/sentinel/src/models/openai.ts +242 -0
  50. package/.github/sentinel/src/models/openrouter.ts +319 -0
  51. package/.github/sentinel/src/models/types.ts +35 -0
  52. package/.github/sentinel/src/orchestrator.ts +287 -0
  53. package/.github/sentinel/src/policy.ts +133 -0
  54. package/.github/sentinel/src/reporter.ts +666 -0
  55. package/.github/sentinel/src/responder.ts +156 -0
  56. package/.github/sentinel/src/router.ts +308 -0
  57. package/.github/sentinel/src/schemas/config.ts +84 -0
  58. package/.github/sentinel/src/schemas/fix.ts +44 -0
  59. package/.github/sentinel/src/schemas/review.ts +73 -0
  60. package/.github/sentinel/src/subway.ts +250 -0
  61. package/.github/sentinel/src/types.ts +234 -0
  62. package/.github/sentinel/tsconfig.json +19 -0
  63. package/.github/sentinel.yml +34 -0
  64. package/.github/workflows/publish.yml +28 -0
  65. package/.github/workflows/sentinel.yml +55 -0
  66. package/README.md +90 -41
  67. package/SECURITY.md +85 -0
  68. package/TROUBLESHOOTING.md +2 -2
  69. package/index.ts +219 -109
  70. package/openclaw.plugin.json +50 -26
  71. package/package.json +1 -1
  72. package/skills/stratus-info/SKILL.md +70 -10
  73. package/src/client.ts +78 -18
  74. package/src/config.ts +29 -8
  75. package/src/setup.ts +53 -61
  76. 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 }}