feat(onboarding): generated Agent Brief — state-aware, repo-pinnable #5452
2 changed files+180−1
Modifiedsrc/routes/connect-claude.tsx+161−0View fileUnifiedSplit
@@ -855,6 +855,33 @@ connectClaude.get("/connect/claude", async (c) => {
855855 }
856856 const host = config.appBaseUrl || "https://gluecron.com";
857857
858 // ?repo=<name> pins the Agent Brief to one repository — so a brief handed
859 // to the Vapron agent can never be confused with the davenroe agent's
860 // (owner misdelivered a hand-written brief between platforms 2026-08-08;
861 // a brief that names its own assignment is misdelivery-proof). Validated
862 // against the user's own repos, case-insensitively.
863 let pinnedRepo: string | null = null;
864 const repoQuery = (c.req.query("repo") || "").trim();
865 if (repoQuery) {
866 try {
867 const { repositories } = await import("../db/schema");
868 const { and: andOp, eq: eqOp, sql: sqlOp } = await import("drizzle-orm");
869 const [row] = await db
870 .select({ name: repositories.name })
871 .from(repositories)
872 .where(
873 andOp(
874 eqOp(repositories.ownerId, user.id),
875 sqlOp`lower(${repositories.name}) = ${repoQuery.toLowerCase()}`
876 )
877 )
878 .limit(1);
879 pinnedRepo = row?.name ?? null;
880 } catch {
881 /* brief falls back to account-wide */
882 }
883 }
884
858885 // Best-effort: look up the most recent MCP audit row for this user.
859886 let lastCalledAt: string | null = null;
860887 let totalCalls = 0;
@@ -1181,12 +1208,146 @@ connectClaude.get("/connect/claude", async (c) => {
11811208 </ul>
11821209 </section>
11831210 )}
1211
1212 {/* ─── Agent Brief ────────────────────────────────────────────────
1213 The owner had to hand-write a working brief for every newly
1214 onboarded agent account (2026-08-08, davenroe). This section
1215 generates it: one copy-paste block that tells any AI agent how
1216 to work on this account — remote, auth, PR flow, house rules.
1217 Paste it into the agent's CLAUDE.md / system prompt and it's
1218 oriented. */}
1219 <section class="connect-claude-section">
1220 <div class="connect-claude-section-head">
1221 <div class="connect-claude-step-row">
1222 <span class="connect-claude-step-num">5</span>
1223 <h2 class="connect-claude-section-title">Agent working brief</h2>
1224 </div>
1225 <p class="connect-claude-section-desc">
1226 Paste this into your agent's instructions (CLAUDE.md, system
1227 prompt, or team runbook). It carries everything an agent needs
1228 to work on your account without hand-holding.
1229 </p>
1230 </div>
1231 <div class="connect-claude-section-body">
1232 <button
1233 type="button"
1234 class="btn"
1235 id="cc-brief-copy"
1236 style="margin-bottom: 10px"
1237 >
1238 Copy brief
1239 </button>
1240 <pre
1241 id="cc-brief"
1242 style="font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; padding: 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--r-md); overflow-x: auto; white-space: pre-wrap"
1243 >{agentBrief(
1244 user.username,
1245 config.appBaseUrl || "https://gluecron.com",
1246 firstTaskRepos.map((r) => r.name),
1247 pinnedRepo
1248 )}</pre>
1249 </div>
1250 </section>
11841251 </div>
11851252 <script dangerouslySetInnerHTML={{ __html: clientScript() }} />
1253
1254 dangerouslySetInnerHTML={{
1255 __html: `document.getElementById('cc-brief-copy').addEventListener('click', function(){ var t = document.getElementById('cc-brief').textContent; navigator.clipboard.writeText(t).then(() => { this.textContent = 'Copied ✓'; setTimeout(() => { this.textContent = 'Copy brief'; }, 1500); }); }.bind(document.getElementById('cc-brief-copy')));`,
1256 }}
1257 />
11861258 </Layout>
11871259 );
11881260});
11891261
1262/**
1263 * The generated agent working brief — the hand-written davenroe brief,
1264 * templated. Markdown so it drops straight into a CLAUDE.md.
1265 *
1266 * STATE-AWARE (owner catch, 2026-08-08): a brief that assumes repos exist
1267 * on Gluecron is dangerous for an account that has none — an agent could
1268 * follow it into pushing work at a remote that doesn't exist, or worse,
1269 * start shipping to a Gluecron copy while the product still deploys from
1270 * GitHub (split-brain). With no repos, the brief LEADS with import and the
1271 * canonical-vs-mirror rule; with repos, it names them.
1272 */
1273function agentBrief(
1274 username: string,
1275 host: string,
1276 repoNames: string[],
1277 pinnedRepo: string | null = null
1278): string {
1279 const bare = host.replace("https://", "");
1280 const hasRepos = repoNames.length > 0;
1281
1282 const whereSection = pinnedRepo
1283 ? `## Your assignment: ${username}/${pinnedRepo}
1284- You work on THIS repository and no other:
1285 ${host}/${username}/${pinnedRepo}
1286- Git remote: ${host}/${username}/${pinnedRepo}.git
1287- Issues: ${host}/${username}/${pinnedRepo}/issues · PRs:
1288 ${host}/${username}/${pinnedRepo}/pulls
1289- If a task seems to require touching another repository, stop and ask the
1290 human — do not wander across the account.`
1291 : hasRepos
1292 ? `## Where things live
1293- This account's repositories on Gluecron: ${repoNames
1294 .slice(0, 5)
1295 .map((r) => `${username}/${r}`)
1296 .join(", ")}${repoNames.length > 5 ? ", …" : ""}.
1297- Canonical git remote: ${host}/${username}/<repo>.git — clone, branch, and
1298 push HERE.`
1299 : `## FIRST: get the code onto Gluecron
1300- This account has NO repositories on Gluecron yet. Do not invent remote
1301 URLs — nothing exists to push to. Start at ${host}/import (single repo,
1302 with issues + PRs) or ${host}/import/bulk (a whole GitHub account/org),
1303 or create fresh at ${host}/new.
1304- Until an import completes, the source of truth is wherever the code
1305 lives today. Do not start work here before that.`;
1306
1307 return `# Working on Gluecron — brief for AI agents (@${username})
1308
1309${whereSection}
1310
1311## Canonical vs mirror — decide it, say it, never blur it
1312- Exactly ONE side is canonical per repository. After importing to
1313 Gluecron, work (branches, PRs, merges) happens on Gluecron ONLY.
1314- If the product still DEPLOYS from GitHub (Vercel etc.), GitHub becomes a
1315 mirror: after each Gluecron merge, the default branch is pushed to GitHub
1316 so deploys pick it up. Never merge work on both sides independently —
1317 that forks history and loses someone's work.
1318- If you cannot tell which side is canonical for a repo, STOP and ask the
1319 human. This is the one question never to guess.
1320
1321## Web, API, MCP
1322- Everything runs at ${host}. The MCP server (${host}/mcp) exposes 60 tools
1323 (search, read, issues, PRs, workflows); connect via the token from
1324 ${host}/connect/claude.
1325
1326## Auth
1327- Personal access token from ${host}/settings/tokens (choose "never
1328 expires" for agent use; "repo" scope covers everything you need).
1329- Git over HTTPS: https://x:<TOKEN>@${bare}/${username}/<repo>.git
1330- REST: Authorization: Bearer <TOKEN> against ${host}/api/v2/...
1331
1332## How work ships (non-negotiable)
13331. Never commit to the default branch. Branch per change:
1334 <type>/<short-slug> (fix/login-redirect, feat/export-csv).
13352. Push the branch, then open a PR (MCP: gluecron_create_pr, or the web UI).
13363. Gates run on every PR: GateTest, secret scan, security scan, merge check,
1337 AI review. A red gate is yours to fix, not bypass.
13384. Merging is a human decision unless the repo has auto-merge configured.
13395. Issues live on the repo (${host}/${username}/<repo>/issues). Reference
1340 them from PRs ("closes #N") so they close on merge.
1341
1342## House rules
1343- Small, reviewable diffs; match the repo's existing conventions and idiom.
1344- Run the repo's tests before opening the PR when a test setup exists.
1345- Never commit secrets — the push-time secret gate will reject them.
1346- If something looks broken platform-side, file an issue on the repo rather
1347 than working around it silently.
1348`;
1349}
1350
11901351// ─── Alias: /settings/claude → /connect/claude ─────────────────────────────
11911352connectClaude.get("/settings/claude", (c) => c.redirect("/connect/claude"));
11921353
Modifiedsrc/routes/onboarding.tsx+19−1View fileUnifiedSplit
@@ -516,6 +516,15 @@ const gettingStartedHandler = async (c: any) => {
516516 },
517517 {
518518 n: 4,
519 title: "Connect your AI agent",
520 desc: "Claude Code, Claude Desktop, or any MCP agent — one page mints the token, the config, and a copy-paste working brief.",
521 cta: { href: "/connect/claude", label: "Connect an agent", primary: false },
522 skip: { href: "/dashboard", label: "Skip" },
523 icon: <IconShip />,
524 done: false,
525 },
526 {
527 n: 5,
519528 title: "Ship to production",
520529 desc: "Configure auto-merge + deploy webhooks. Your push lands live in ~25 seconds.",
521530 cta: { href: "/help", label: "Read the guide", primary: false },
@@ -562,6 +571,15 @@ const gettingStartedHandler = async (c: any) => {
562571 },
563572 {
564573 n: 4,
574 title: "Connect your AI agent",
575 desc: "Claude Code, Claude Desktop, or any MCP agent — one page mints the token, the config, and a copy-paste working brief.",
576 cta: { href: "/connect/claude", label: "Connect an agent", primary: false },
577 skip: { href: "/dashboard", label: "Skip" },
578 icon: <IconShip />,
579 done: false,
580 },
581 {
582 n: 5,
565583 title: "Ship to production",
566584 desc: "Configure auto-merge + deploy webhooks. Your push lands live in ~25 seconds.",
567585 cta: { href: "/help", label: "Read the guide", primary: false },
@@ -595,7 +613,7 @@ const gettingStartedHandler = async (c: any) => {
595613 </h1>
596614 <p class="onb-sub">
597615 Ship safer code with AI-native hosting, automated CI, and push-time
598 gates. Four short steps — under a minute end-to-end.
616 gates. Five short steps — under a minute end-to-end.
599617 </p>
600618 </div>
601619 </section>
602620
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts