Commitcde9983unknown_key
feat(onboarding): auto-seed ai:build issue on first repo — shows autopilot immediately
feat(onboarding): auto-seed ai:build issue on first repo — shows autopilot immediately Adds `ensureAiBuildSeedIssue` to repo-bootstrap.ts: on a user's first repository creation, seeds an open issue titled "Add a welcome README with project overview" labelled `ai:build` so the K3 autopilot immediately picks it up and demonstrates the build-from-issues feature. Wired into `POST /api/repos` as a fire-and-forget call that never blocks the response. Also adds `ai:build` to the DEFAULT_LABELS set so every repo gets the label regardless of seed issue creation. https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
3 files changed+128−1cde9983022ffbbd42ed8b7d17c3ea4c60a5d8e22
3 changed files+128−1
Addedsrc/__tests__/ai-build-seed.test.ts+46−0View fileUnifiedSplit
@@ -0,0 +1,46 @@
1/**
2 * Tests for the ai:build seed-issue feature.
3 *
4 * ensureAiBuildSeedIssue is fire-and-forget — it must never throw even when
5 * the DB is unavailable or returns unexpected data. These tests verify the
6 * contract without mocks or a live database connection.
7 */
8
9import { describe, it, expect } from "bun:test";
10import { ensureAiBuildSeedIssue } from "../lib/repo-bootstrap";
11
12describe("ensureAiBuildSeedIssue", () => {
13 it("is exported from repo-bootstrap", () => {
14 expect(typeof ensureAiBuildSeedIssue).toBe("function");
15 });
16
17 it("returns a Promise", () => {
18 const result = ensureAiBuildSeedIssue(
19 "00000000-0000-0000-0000-000000000001",
20 "00000000-0000-0000-0000-000000000002"
21 );
22 expect(result).toBeInstanceOf(Promise);
23 // Swallow the expected DB error — the important thing is it doesn't throw.
24 return result.catch(() => {});
25 });
26
27 it("resolves without throwing when DB is unavailable", async () => {
28 // The function must catch all DB errors internally and return void.
29 await expect(
30 ensureAiBuildSeedIssue(
31 "00000000-0000-0000-0000-000000000001",
32 "00000000-0000-0000-0000-000000000002"
33 )
34 ).resolves.toBeUndefined();
35 });
36
37 it("resolves without throwing for a second call with the same fake IDs", async () => {
38 // Idempotency check — calling twice on the same IDs should not throw.
39 await expect(
40 ensureAiBuildSeedIssue(
41 "00000000-0000-0000-0000-000000000003",
42 "00000000-0000-0000-0000-000000000004"
43 )
44 ).resolves.toBeUndefined();
45 });
46});
Modifiedsrc/lib/repo-bootstrap.ts+77−0View fileUnifiedSplit
@@ -16,6 +16,8 @@ import {
1616 labels,
1717 issues,
1818 issueComments,
19 issueLabels,
20 repositories,
1921} from "../db/schema";
2022import { audit } from "./notify";
2123
@@ -29,6 +31,7 @@ const DEFAULT_LABELS = [
2931 { name: "question", color: "#8b949e", description: "Further info requested" },
3032 { name: "good first issue", color: "#7ee787", description: "Suitable for new contributors" },
3133 { name: "ai-triaged", color: "#bc8cff", description: "Auto-triaged by GlueCron AI" },
34 { name: "ai:build", color: "#f0883e", description: "Autopilot will implement this and open a PR" },
3235];
3336
3437const WELCOME_BODY = `Welcome to your new GlueCron repository.
@@ -180,6 +183,80 @@ export async function bootstrapRepository(opts: {
180183 };
181184}
182185
186const AI_BUILD_SEED_TITLE = "Add a welcome README with project overview";
187
188const AI_BUILD_SEED_BODY = `## What to build
189
190Add a \`README.md\` to this repository with:
191- A one-paragraph description of what this project does
192- Quick start instructions (install + run)
193- A brief note about the tech stack
194
195## Why this issue exists
196
197This issue is labelled \`ai:build\`. Gluecron's autopilot will automatically:
1981. Read this spec
1992. Ask Claude to implement it
2003. Open a draft pull request with the code
201
202You don't need to do anything. Just watch the PR appear. ✨
203
204To build your own features this way, open an issue, describe what you want, and add the \`ai:build\` label.`;
205
206/**
207 * Seeds an `ai:build`-labelled issue on a user's FIRST repository so they
208 * immediately discover the autopilot feature.
209 *
210 * Silently skips if this is not the owner's first repo, or if anything fails.
211 * Must be called fire-and-forget — it never throws.
212 */
213export async function ensureAiBuildSeedIssue(
214 repositoryId: string,
215 ownerId: string
216): Promise<void> {
217 try {
218 const { eq, and, sql } = await import("drizzle-orm");
219
220 // 1. Only seed on the owner's first repo.
221 const [{ n }] = await db
222 .select({ n: sql<number>`count(*)::int` })
223 .from(repositories)
224 .where(eq(repositories.ownerId, ownerId));
225 if ((n ?? 0) > 1) return;
226
227 // 2. Find the ai:build label for this repo (seeded by bootstrapRepository).
228 const [label] = await db
229 .select({ id: labels.id })
230 .from(labels)
231 .where(and(eq(labels.repositoryId, repositoryId), eq(labels.name, "ai:build")))
232 .limit(1);
233
234 if (!label) {
235 console.warn("[ai-build-seed] ai:build label not found for repo", repositoryId);
236 return;
237 }
238
239 // 3. Create the seed issue.
240 const [issue] = await db
241 .insert(issues)
242 .values({
243 repositoryId,
244 authorId: ownerId,
245 title: AI_BUILD_SEED_TITLE,
246 body: AI_BUILD_SEED_BODY,
247 state: "open",
248 })
249 .returning();
250
251 if (!issue) return;
252
253 // 4. Attach the ai:build label.
254 await db.insert(issueLabels).values({ issueId: issue.id, labelId: label.id });
255 } catch (err) {
256 console.warn("[ai-build-seed] failed:", (err as Error)?.message);
257 }
258}
259
183260/**
184261 * Convenience helper to load settings (creates defaults if missing).
185262 */
Modifiedsrc/routes/api.ts+5−1View fileUnifiedSplit
@@ -123,11 +123,15 @@ api.post("/repos", async (c) => {
123123
124124 // Green-ecosystem bootstrap: settings, protection, labels, welcome issue
125125 if (repo) {
126 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
126 const { bootstrapRepository, ensureAiBuildSeedIssue } = await import("../lib/repo-bootstrap");
127127 await bootstrapRepository({
128128 repositoryId: repo.id,
129129 ownerUserId: owner.id,
130130 });
131 // Fire-and-forget — never block the response
132 ensureAiBuildSeedIssue(repo.id, owner.id).catch(err =>
133 console.warn('[ai-build-seed]', err?.message)
134 );
131135 }
132136
133137 return c.json(repo, 201);
134138