Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commita7460bfunknown_key

Add gluecron[bot] synthetic user for autopilot action attribution

Add gluecron[bot] synthetic user for autopilot action attribution

Creates a non-loginable gluecron[bot] user row (password_hash='') so
autopilot / AI-review comments are credited to the bot rather than to
the PR/issue author. Falls back gracefully to the real author id before
migration 0078 has been applied.

Call sites updated:
- src/lib/ai-review.ts — AI review summary + inline + API-failure advisory
- src/lib/ai-review-trio.ts — trio persona + summary comments
- src/lib/autopilot.ts — auto-merge notification comment
- src/lib/stale-sweep.ts — PR/issue stale poke + close comments (4 sites)

UI: pulls.tsx and issues.tsx now show a "🤖 bot" pill badge beside the
username whenever a comment is authored by gluecron[bot].

https://claude.ai/code/session_01DzJMTFASjMHt2f5ze4cNLR
Claude committed on June 6, 2026Parent: 2455f0a
8 files changed+16817a7460bf4fa790df24672ea06f1affcdf5d244cf5
8 changed files+168−17
Addeddrizzle/0078_bot_user.sql+51−0View fileUnifiedSplit
1-- Migration 0078: Seed the gluecron[bot] synthetic user.
2--
3-- All autopilot / AI-review comments are credited to this row rather than
4-- the PR/issue author. The password_hash is deliberately empty so that no
5-- bcrypt comparison can ever succeed, making this account non-loginable.
6INSERT INTO users (
7 username,
8 email,
9 password_hash,
10 display_name,
11 bio,
12 is_admin,
13 notify_email_on_mention,
14 notify_email_on_assign,
15 notify_email_on_gate_fail,
16 notify_email_digest_weekly,
17 notify_email_on_pending_comment,
18 sleep_mode_enabled,
19 sleep_mode_digest_hour_utc,
20 notify_push_on_mention,
21 notify_push_on_assign,
22 notify_push_on_review_request,
23 notify_push_on_deploy_failed,
24 is_playground,
25 personal_semantic_index_enabled,
26 created_at,
27 updated_at
28) VALUES (
29 'gluecron[bot]',
30 'bot@gluecron.com',
31 '',
32 'Gluecron Bot',
33 'AI autopilot system',
34 false,
35 false,
36 false,
37 false,
38 false,
39 false,
40 false,
41 9,
42 false,
43 false,
44 false,
45 false,
46 false,
47 false,
48 NOW(),
49 NOW()
50)
51ON CONFLICT (username) DO NOTHING;
Modifiedsrc/lib/ai-review-trio.ts+10−7View fileUnifiedSplit
3333import { audit } from "./notify";
3434import { recordAiCost, extractUsage } from "./ai-cost-tracker";
3535import { assertAiQuota, AiQuotaExceededError } from "./billing";
36import { getBotUserIdOrFallback } from "./bot-user";
3637
3738// ---------------------------------------------------------------------------
3839// Public types
564565 pullRequestId: string;
565566 result: TrioReviewResult;
566567}): Promise<void> {
567 // Need the PR's author id to satisfy `prComments.authorId NOT NULL`.
568 // (`ai-review.ts` uses the same pattern.)
569 let authorId: string | null = null;
568 // Need a user id to satisfy `prComments.authorId NOT NULL`.
569 // Prefer the bot user; fall back to the PR author for pre-migration envs.
570 let prAuthorId: string | null = null;
570571 try {
571572 const [pr] = await db
572573 .select({ authorId: pullRequests.authorId })
573574 .from(pullRequests)
574575 .where(eq(pullRequests.id, args.pullRequestId))
575576 .limit(1);
576 if (pr) authorId = pr.authorId;
577 if (pr) prAuthorId = pr.authorId;
577578 } catch {
578579 /* tolerate */
579580 }
580 if (!authorId) return; // can't post comments without an author id
581 if (!prAuthorId) return; // can't post comments without an author id
582
583 const commentAuthorId = await getBotUserIdOrFallback(prAuthorId);
581584
582585 const verdicts: TrioVerdict[] = [
583586 args.result.securityVerdict,
590593 try {
591594 await db.insert(prComments).values({
592595 pullRequestId: args.pullRequestId,
593 authorId,
596 authorId: commentAuthorId,
594597 isAiReview: true,
595598 body,
596599 });
606609 try {
607610 await db.insert(prComments).values({
608611 pullRequestId: args.pullRequestId,
609 authorId,
612 authorId: commentAuthorId,
610613 isAiReview: true,
611614 body: renderSummaryCommentBody(args.result),
612615 });
Modifiedsrc/lib/ai-review.ts+8−3View fileUnifiedSplit
1818 runTrioReview,
1919} from "./ai-review-trio";
2020import { assertAiQuota, AiQuotaExceededError } from "./billing";
21import { getBotUserIdOrFallback } from "./bot-user";
2122
2223interface ReviewComment {
2324 filePath: string;
358359 return;
359360 }
360361
362 // Resolve the bot user id once; fall back to the PR author so that
363 // comment insertion never fails even before migration 0078 has run.
364 const commentAuthorId = await getBotUserIdOrFallback(pr.authorId);
365
361366 let result: ReviewResult;
362367 try {
363368 result = await reviewDiff(
376381 .insert(prComments)
377382 .values({
378383 pullRequestId: prId,
379 authorId: pr.authorId,
384 authorId: commentAuthorId,
380385 isAiReview: true,
381386 body: `${AI_REVIEW_MARKER}\n## AI review unavailable\n\nThe AI review attempt failed: ${reason}. The PR is otherwise unchanged.`,
382387 })
400405 .insert(prComments)
401406 .values({
402407 pullRequestId: prId,
403 authorId: pr.authorId,
408 authorId: commentAuthorId,
404409 isAiReview: true,
405410 body: summaryBody,
406411 })
423428 .insert(prComments)
424429 .values({
425430 pullRequestId: prId,
426 authorId: pr.authorId,
431 authorId: commentAuthorId,
427432 isAiReview: true,
428433 body: c.body,
429434 filePath,
Modifiedsrc/lib/autopilot.ts+3−1View fileUnifiedSplit
6969import { expireOldSandboxes } from "./pr-sandbox";
7070import { expireIdleEnvs } from "./dev-env";
7171import { expireOldPreviews } from "./branch-previews";
72import { getBotUserIdOrFallback } from "./bot-user";
7273
7374export interface AutopilotTaskResult {
7475 name: string;
11981199 console.error("[autopilot] auto-merge: merged audit failed:", err);
11991200 }
12001201 try {
1202 const commentAuthorId = await getBotUserIdOrFallback(cand.authorUserId);
12011203 await db.insert(prComments).values({
12021204 pullRequestId: cand.prId,
1203 authorId: cand.authorUserId,
1205 authorId: commentAuthorId,
12041206 isAiReview: true,
12051207 body: `${AUTO_MERGE_COMMENT_MARKER}\nAuto-merged by Gluecron autopilot — branch protection conditions satisfied.`,
12061208 });
Addedsrc/lib/bot-user.ts+55−0View fileUnifiedSplit
1/**
2 * Bot-user helper — resolves the UUID for the synthetic `gluecron[bot]`
3 * account so autopilot / AI-review actions are credited to it rather than
4 * to the PR / issue author.
5 *
6 * The row is seeded by drizzle/0078_bot_user.sql. If the migration has not
7 * run yet (e.g. a freshly-cloned dev environment) the helper returns `null`
8 * and callers must fall back to a real user id — the same behaviour as before
9 * this feature shipped.
10 *
11 * The result is module-level cached so every autopilot tick after the first
12 * one pays zero DB overhead.
13 */
14
15import { eq } from "drizzle-orm";
16import { db } from "../db";
17import { users } from "../db/schema";
18
19export const BOT_USERNAME = "gluecron[bot]";
20
21let _botUserId: string | null | undefined = undefined; // undefined = not yet fetched
22
23/**
24 * Lazily resolve and cache the `gluecron[bot]` user's UUID.
25 *
26 * Returns `null` when the row does not exist (migration not yet applied).
27 * Callers should fall back to `authorId` from the related PR/issue.
28 */
29export async function getBotUserId(): Promise<string | null> {
30 if (_botUserId !== undefined) return _botUserId;
31 try {
32 const [row] = await db
33 .select({ id: users.id })
34 .from(users)
35 .where(eq(users.username, BOT_USERNAME))
36 .limit(1);
37 _botUserId = row?.id ?? null;
38 } catch {
39 // DB unavailable — leave undefined so next call retries.
40 return null;
41 }
42 return _botUserId;
43}
44
45/**
46 * Resolve the bot user ID, falling back to `fallbackId` if the bot row
47 * does not exist yet. The fallback keeps every call site backward-
48 * compatible with pre-migration environments.
49 */
50export async function getBotUserIdOrFallback(
51 fallbackId: string
52): Promise<string> {
53 const botId = await getBotUserId();
54 return botId ?? fallbackId;
55}
Modifiedsrc/lib/stale-sweep.ts+12−6View fileUnifiedSplit
4242 repositories,
4343} from "../db/schema";
4444import { audit, notify } from "./notify";
45import { getBotUserIdOrFallback } from "./bot-user";
4546
4647// ---------------------------------------------------------------------------
4748// Marker constants — stable HTML comments. Versioned so a v2 contract can
300301
301302/** Default poke side-effect: comment + notify + audit. */
302303async function defaultPokePr(cand: StalePrCandidate): Promise<void> {
303 // 1. Post the marker comment as the PR author (avoids needing a system user).
304 // 1. Post the marker comment as the bot user (falls back to PR author if
305 // the bot row has not been seeded yet).
306 const commentAuthorId = await getBotUserIdOrFallback(cand.authorUserId);
304307 try {
305308 await db.insert(prComments).values({
306309 pullRequestId: cand.prId,
307 authorId: cand.authorUserId,
310 authorId: commentAuthorId,
308311 body: PR_POKE_BODY,
309312 isAiReview: false,
310313 });
340343
341344/** Default close side-effect: comment + state→closed + audit. */
342345async function defaultClosePr(cand: StalePrCandidate): Promise<void> {
343 // 1. Post the final close comment.
346 // 1. Post the final close comment as the bot user.
347 const commentAuthorId = await getBotUserIdOrFallback(cand.authorUserId);
344348 try {
345349 await db.insert(prComments).values({
346350 pullRequestId: cand.prId,
347 authorId: cand.authorUserId,
351 authorId: commentAuthorId,
348352 body: PR_CLOSE_BODY,
349353 isAiReview: false,
350354 });
448452
449453/** Default issue poke. */
450454async function defaultPokeIssue(cand: StaleIssueCandidate): Promise<void> {
455 const commentAuthorId = await getBotUserIdOrFallback(cand.authorUserId);
451456 try {
452457 await db.insert(issueComments).values({
453458 issueId: cand.issueId,
454 authorId: cand.authorUserId,
459 authorId: commentAuthorId,
455460 body: ISSUE_POKE_BODY,
456461 });
457462 } catch (err) {
483488
484489/** Default issue close. */
485490async function defaultCloseIssue(cand: StaleIssueCandidate): Promise<void> {
491 const commentAuthorId = await getBotUserIdOrFallback(cand.authorUserId);
486492 try {
487493 await db.insert(issueComments).values({
488494 issueId: cand.issueId,
489 authorId: cand.authorUserId,
495 authorId: commentAuthorId,
490496 body: ISSUE_CLOSE_BODY,
491497 });
492498 } catch (err) {
Modifiedsrc/routes/issues.tsx+15−0View fileUnifiedSplit
5656 formatRelative,
5757} from "../views/ui";
5858import { getDefaultBranch, resolveRef, updateRef } from "../git/repository";
59import { BOT_USERNAME } from "../lib/bot-user";
5960
6061const issueRoutes = new Hono<AuthEnv>();
6162
564565 box-shadow: 0 0 6px rgba(255,255,255,0.7);
565566 }
566567
568 .issues-bot-badge {
569 display: inline-flex; align-items: center; gap: 3px;
570 padding: 1px 7px;
571 font-size: 10px;
572 font-weight: 600;
573 color: var(--fg-muted);
574 background: var(--bg-elevated);
575 border: 1px solid var(--border);
576 border-radius: 9999px;
577 }
578
567579 /* Composer */
568580 .issues-composer {
569581 margin-top: 22px;
14981510 <article class={`issues-comment${isAi ? " is-ai" : ""}${isPending ? " modq-comment-pending" : ""}`}>
14991511 <header class="issues-comment-header">
15001512 <strong>{commentAuthor.username}</strong>
1513 {commentAuthor.username === BOT_USERNAME && (
1514 <span class="issues-bot-badge">&#x1F916; bot</span>
1515 )}
15011516 {isAi ? (
15021517 <span class="issues-ai-badge" title="Generated by Gluecron AI Triage">
15031518 AI Review
Modifiedsrc/routes/pulls.tsx+14−0View fileUnifiedSplit
122122
123123import { suggestReviewers, type ReviewerCandidate } from "../lib/reviewer-suggest";
124124import { computePrSize, type PrSizeInfo } from "../lib/pr-size";
125import { BOT_USERNAME } from "../lib/bot-user";
125126
126127const pulls = new Hono<AuthEnv>();
127128
697698 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 130%);
698699 border-radius: 9999px;
699700 }
701 .prs-bot-badge {
702 display: inline-flex; align-items: center; gap: 3px;
703 padding: 1px 7px;
704 font-size: 10px;
705 font-weight: 600;
706 color: var(--fg-muted);
707 background: var(--bg-elevated);
708 border: 1px solid var(--border);
709 border-radius: 9999px;
710 }
700711
701712 /* Files-changed link card on conversation tab. (Diff itself is in DiffView.) */
702713 .prs-files-card {
38873898 >
38883899 <div class="prs-comment-head">
38893900 <strong>{commentAuthor.username}</strong>
3901 {commentAuthor.username === BOT_USERNAME && (
3902 <span class="prs-bot-badge">&#x1F916; bot</span>
3903 )}
38903904 {comment.isAiReview && (
38913905 <span class="prs-ai-badge">AI Review</span>
38923906 )}
38933907