Skip to content

Commit b93293b

Browse files
authored
Merge pull request #453 from jmbish04/claude/sentinel-engine
2 parents 2ff26d0 + 951a823 commit b93293b

26 files changed

Lines changed: 2575 additions & 215 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"db:reset": "python3 scripts/db/reset_d1.py && pnpm run db:generate:all && pnpm run migrate:remote:all && pnpm run deploy",
5252
"db:seed:prep": "python3 scripts/db/seed_prep.py",
5353
"db:seed:run": "python3 scripts/db/seed_run.py",
54-
"secrets:audit": "node scripts/secrets/audit-secrets.mjs"
54+
"secrets:audit": "node scripts/secrets/audit-secrets.mjs",
55+
"db:auto": "pnpm run db:generate:all && pnpm run migrate:remote:all && pnpm dlx wrangler@latest types"
5556
},
5657
"dependencies": {
5758
"@ai-sdk/anthropic": "^3.0.58",

src/backend/src/ai/agents/LearningAgent.ts

Lines changed: 292 additions & 81 deletions
Large diffs are not rendered by default.

src/backend/src/ai/agents/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ export { StandardizationAgent } from './StandardizationAgent';
3333
export { JulesPrReviewer } from './pr-reviewer/JulesPrReviewer';
3434
export { UxResearcher } from './workshop/UxResearcher';
3535
export { SandboxAgent } from './SandboxAgent';
36+
export { LearningAgent } from './LearningAgent';

src/backend/src/automations/core/AutomationRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { RepoStandardization } from '@/automations/repository/standardization';
2222
import { RepoSync } from '@/automations/repository/sync';
2323
import { StatsUpdate } from '@/automations/repository/stats-update';
2424
import { LeakPlumber } from '@/automations/security/leak-plumber';
25+
import { SentinelInterceptor } from '@/automations/pr/SentinelInterceptor';
26+
import { SentinelPostMerge } from '@/automations/pr/SentinelPostMerge';
2527
import { SlashCommand } from '@/automations/shared/colby';
2628
import { TelemetryIngestion } from '@/automations/telemetry/ingest';
2729

@@ -51,6 +53,8 @@ export const REGISTERED_AUTOMATIONS: RegisteredAutomation[] = [
5153
StandardsCheckPush,
5254
RepoStandardization,
5355
LeakPlumber,
56+
SentinelInterceptor,
57+
SentinelPostMerge,
5458
];
5559

5660
const AUTOMATIONS_BY_CLASS = new Map<string, RegisteredAutomation>(
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* @file backend/src/automations/pr/SentinelInterceptor.ts
3+
* @description Active PR Interceptor — analyzes PRs against architectural memory
4+
* and posts findings as comments using the human persona token.
5+
*
6+
* Triggers on pull_request opened/synchronize events.
7+
* Uses GITHUB_PERSONAL_ACCESS_TOKEN so comments appear from a human account
8+
* and aren't ignored by bot filters.
9+
*
10+
* @module Automations/PR/SentinelInterceptor
11+
*/
12+
13+
import { z } from "zod";
14+
import {
15+
BaseAutomation,
16+
type AutomationMetadata,
17+
} from "@/core/BaseAutomation";
18+
import { getDb } from "@db";
19+
import { learningAiInsights } from "@db/schemas/github/learning";
20+
import { eq, and } from "drizzle-orm";
21+
22+
const PullRequestPayloadSchema = z.object({
23+
action: z.enum(["opened", "synchronize"]),
24+
repository: z.object({
25+
name: z.string(),
26+
full_name: z.string(),
27+
owner: z.object({ login: z.string() }),
28+
}),
29+
pull_request: z.object({
30+
number: z.number(),
31+
title: z.string(),
32+
body: z.string().nullable(),
33+
html_url: z.string(),
34+
diff_url: z.string(),
35+
user: z.object({ login: z.string(), type: z.string().optional() }),
36+
head: z.object({ ref: z.string() }),
37+
base: z.object({ ref: z.string() }),
38+
}),
39+
});
40+
41+
type SentinelPayload = z.infer<typeof PullRequestPayloadSchema>;
42+
43+
export class SentinelInterceptor extends BaseAutomation<SentinelPayload> {
44+
static readonly metadata: AutomationMetadata = {
45+
key: "sentinel-interceptor",
46+
domain: "pr",
47+
description:
48+
"Analyzes PRs against architectural memory and posts AI-driven findings.",
49+
events: ["pull_request"],
50+
alwaysOn: true,
51+
authPolicy: "pat",
52+
};
53+
54+
async shouldRun(): Promise<boolean> {
55+
const parsed = PullRequestPayloadSchema.safeParse(this.payload);
56+
if (!parsed.success) return false;
57+
return (
58+
this.action === "opened" || this.action === "synchronize"
59+
);
60+
}
61+
62+
async run(): Promise<void> {
63+
const payload = PullRequestPayloadSchema.parse(this.payload);
64+
const { repository, pull_request: pr } = payload;
65+
const repoFullName = repository.full_name;
66+
const owner = repository.owner.login;
67+
const repo = repository.name;
68+
69+
try {
70+
const octokit = await this.getGitHubClient();
71+
72+
// Step 1: Post initial analysis comment
73+
await octokit.rest.issues.createComment({
74+
owner,
75+
repo,
76+
issue_number: pr.number,
77+
body: `🔍 **Sentinel** is crunching architectural history to optimize this PR...`,
78+
});
79+
80+
// Step 2: Fetch the PR diff for analysis
81+
const { data: diffData } = await octokit.rest.pulls.get({
82+
owner,
83+
repo,
84+
pull_number: pr.number,
85+
mediaType: { format: "diff" },
86+
});
87+
const diff = typeof diffData === "string" ? diffData : JSON.stringify(diffData);
88+
89+
// Step 3: Query architectural memory
90+
const db = getDb(this.env.DB);
91+
const repoInsights = await db
92+
.select()
93+
.from(learningAiInsights)
94+
.where(
95+
and(
96+
eq(learningAiInsights.repo, repoFullName),
97+
eq(learningAiInsights.status, "proposed")
98+
)
99+
)
100+
.limit(10);
101+
102+
// Step 4: Check Vectorize for similar patterns
103+
let vectorMatches: string[] = [];
104+
try {
105+
const embedding = await this.env.AI.run(
106+
"@cf/baai/bge-large-en-v1.5" as any,
107+
{ text: [pr.title + "\n" + (pr.body || "")] }
108+
);
109+
const vectors = (embedding as any).data?.[0];
110+
if (vectors) {
111+
const matches = await this.env.VECTORIZE.query(vectors, {
112+
topK: 5,
113+
namespace: "learning",
114+
});
115+
vectorMatches = (matches.matches || [])
116+
.filter((m: any) => m.score > 0.75)
117+
.map(
118+
(m: any) =>
119+
`- ${m.metadata?.text?.substring(0, 200) || "Similar pattern detected"} (score: ${m.score.toFixed(2)})`
120+
);
121+
}
122+
} catch (err) {
123+
console.warn("[SentinelInterceptor] Vectorize query failed:", err);
124+
}
125+
126+
// Step 5: Run AI analysis
127+
const analysisPrompt = `Analyze this PR diff for architectural anti-patterns, style drift, or improvements based on these known patterns:
128+
129+
**Known Immunized Insights for ${repoFullName}:**
130+
${repoInsights.map((i) => `- [${i.patternType}/${i.severity}] ${i.description?.substring(0, 200)}`).join("\n") || "None yet."}
131+
132+
**Vector Similarity Matches:**
133+
${vectorMatches.join("\n") || "No similar prior patterns found."}
134+
135+
**PR Title:** ${pr.title}
136+
**PR Description:** ${pr.body || "No description provided."}
137+
138+
**Diff (truncated to 50000 chars):**
139+
\`\`\`
140+
${diff.substring(0, 50000)}
141+
\`\`\`
142+
143+
Respond with a concise analysis. If you detect anti-patterns or potential issues, list them as bullet points. If the PR looks clean, say so briefly. Include severity (low/medium/high) for each finding.`;
144+
145+
const aiResponse = await this.env.AI.run(
146+
"@cf/meta/llama-3.3-70b-instruct-fp8-fast" as any,
147+
{
148+
messages: [
149+
{
150+
role: "system",
151+
content:
152+
"You are Sentinel, an architectural analysis bot. Be concise, actionable, and constructive. Format findings as GitHub-flavored markdown.",
153+
},
154+
{ role: "user", content: analysisPrompt },
155+
],
156+
max_tokens: 1000,
157+
}
158+
);
159+
160+
const analysis = (aiResponse as any).response || "Analysis unavailable.";
161+
162+
// Step 6: Post summary comment
163+
const baseUrl = (this.env as any).BASE_URL || "https://core-github-api.hacolby.workers.dev";
164+
const summaryBody = `## 🛡️ Sentinel Analysis
165+
166+
${analysis}
167+
168+
---
169+
170+
<details>
171+
<summary>📊 Context</summary>
172+
173+
- **Immunized patterns for this repo:** ${repoInsights.length}
174+
- **Similar prior patterns:** ${vectorMatches.length}
175+
- **PR Author:** ${pr.user.login} ${pr.user.type === "Bot" ? "(Bot)" : ""}
176+
177+
[View full insights →](${baseUrl}/sentinel)
178+
179+
</details>
180+
181+
*Powered by Sentinel Learning Engine*`;
182+
183+
// Update the initial comment (or post new one)
184+
await octokit.rest.issues.createComment({
185+
owner,
186+
repo,
187+
issue_number: pr.number,
188+
body: summaryBody,
189+
});
190+
191+
await this.logExecution(
192+
"success",
193+
`Sentinel analysis posted for PR #${pr.number}`,
194+
pr.number
195+
);
196+
} catch (err: any) {
197+
console.error(
198+
`[SentinelInterceptor] Failed to analyze PR #${pr.number}:`,
199+
err
200+
);
201+
await this.logExecution(
202+
"failure",
203+
`Failed: ${err.message}`,
204+
pr.number
205+
);
206+
}
207+
}
208+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* @file backend/src/automations/pr/SentinelPostMerge.ts
3+
* @description Post-merge learning automation — when a PR is merged, creates
4+
* a reflection record and signals the LearningAgent to ingest it.
5+
*
6+
* Triggers on pull_request closed events where merged=true.
7+
*
8+
* @module Automations/PR/SentinelPostMerge
9+
*/
10+
11+
import { z } from "zod";
12+
import {
13+
BaseAutomation,
14+
type AutomationMetadata,
15+
} from "@/core/BaseAutomation";
16+
import { getDb } from "@db";
17+
import { learningAiInsightPrs } from "@db/schemas/github/learning";
18+
19+
const PullRequestClosedPayloadSchema = z.object({
20+
action: z.literal("closed"),
21+
repository: z.object({
22+
name: z.string(),
23+
full_name: z.string(),
24+
owner: z.object({ login: z.string() }),
25+
}),
26+
pull_request: z.object({
27+
number: z.number(),
28+
title: z.string(),
29+
body: z.string().nullable(),
30+
html_url: z.string(),
31+
merged: z.boolean(),
32+
user: z.object({ login: z.string() }),
33+
}),
34+
});
35+
36+
type PostMergePayload = z.infer<typeof PullRequestClosedPayloadSchema>;
37+
38+
export class SentinelPostMerge extends BaseAutomation<PostMergePayload> {
39+
static readonly metadata: AutomationMetadata = {
40+
key: "sentinel-post-merge",
41+
domain: "pr",
42+
description:
43+
"Creates learning reflection records when PRs are merged for the Contemplation Gate.",
44+
events: ["pull_request"],
45+
alwaysOn: true,
46+
authPolicy: "app",
47+
};
48+
49+
async shouldRun(): Promise<boolean> {
50+
if (this.action !== "closed") return false;
51+
const parsed = PullRequestClosedPayloadSchema.safeParse(this.payload);
52+
return parsed.success && parsed.data.pull_request.merged === true;
53+
}
54+
55+
async run(): Promise<void> {
56+
const payload = PullRequestClosedPayloadSchema.parse(this.payload);
57+
const { repository, pull_request: pr } = payload;
58+
59+
try {
60+
const db = getDb(this.env.DB);
61+
62+
// Record the merged PR in the learning database
63+
await db.insert(learningAiInsightPrs).values({
64+
id: crypto.randomUUID(),
65+
insightId: "", // Linked during analysis
66+
prNumber: pr.number,
67+
repo: `${repository.owner.login}/${repository.name}`,
68+
status: "merged",
69+
outcome: "merged",
70+
createdAt: new Date(),
71+
});
72+
73+
// Signal the LearningAgent to ingest this PR
74+
try {
75+
const agentId = this.env.LEARNING_AGENT.idFromName("default");
76+
const agentStub = this.env.LEARNING_AGENT.get(agentId);
77+
await agentStub.fetch(
78+
new Request("http://internal/ingest-pr", {
79+
method: "POST",
80+
headers: { "content-type": "application/json" },
81+
body: JSON.stringify({
82+
prNumber: pr.number,
83+
repoOwner: repository.owner.login,
84+
repoName: repository.name,
85+
prUrl: pr.html_url,
86+
prDescription: pr.body?.substring(0, 2000),
87+
merged: true,
88+
}),
89+
})
90+
);
91+
} catch (err) {
92+
console.error(
93+
"[SentinelPostMerge] Failed to signal LearningAgent:",
94+
err
95+
);
96+
}
97+
98+
await this.logExecution(
99+
"success",
100+
`Recorded merged PR #${pr.number} for learning`,
101+
pr.number
102+
);
103+
} catch (err: any) {
104+
console.error(
105+
`[SentinelPostMerge] Failed for PR #${pr.number}:`,
106+
err
107+
);
108+
await this.logExecution("failure", `Failed: ${err.message}`, pr.number);
109+
}
110+
}
111+
}

src/backend/src/routes/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ export { default as cloudflareServicesApi } from './services/cloudflare';
3737
export { default as sandboxApi } from './sandbox';
3838
export { default as researchProjectsApi } from './frontend/research/one-time';
3939
export { default as sentinelApi } from './projects/sentinel/index';
40+
export { default as sentinelInsightsApi } from './sentinel/index';

0 commit comments

Comments
 (0)