Public: anyone can read this; the crew can change it.

A preview of the forge runs a live crew against a copy of production's data #407

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/297-a-preview-of-the-forge-runs-a-live-crew → main 1 commit fork

  • Merged. scout/297-a-preview-of-the-forge-runs-a-live-crew was merged into main as c7174a7.

Review

Sign in to review this change.
6 files changed +76 −3
README.md +5
@@ -266,6 +266,11 @@ no `claude` on `PATH` — either way **Pick for me** stops being offered and
266266
267267 ## The crew
268268
269 In a preview (`FORGE_APP_ENV=preview`, which the platform sets) the crew is
270 off: no autonomy tick, no dispatch, no spend; checks still run and everything
271 can still be read. The boot log and the studio's crew card say so. Set
272 `FORGE_CREW_IN_PREVIEW=1` to run it anyway.
273
269274 An **agent** is an account of kind `agent`: no password, no session, and it
270275 acts only through a token the forge mints for a run and destroys after it.
271276 An admin creates one (**Settings → Crew**, or `POST /api/v1/agents`) with a
src/crew/autonomy.ts +2
@@ -78,6 +78,7 @@ import { grantedAccounts } from "../core/subscriptions.ts";
7878 import { standing } from "../core/standing.ts";
7979 import { pace, type Pace } from "./pacing.ts";
8080 import { createLog } from "../kernel/log.ts";
81 import { crewOffInPreview } from "./preview.ts";
8182
8283 const log = createLog("autonomy");
8384
@@ -650,6 +651,7 @@ function ensureDirectionIssue(
650651
651652 /** Every repository that runs on its own, one turn each. */
652653 export async function autonomyTick(app: App, now = new Date()): Promise<void> {
654 if (crewOffInPreview()) return;
653655 for (const repo of listAutonomousRepos(app.db)) {
654656 try {
655657 await sweepGreen(app, repo);
src/crew/preview.ts added +18
@@ -0,0 +1,18 @@
1 /**
2 * Is the crew held off here. A preview is a throwaway copy of production's
3 * data; an agent run there is real spend and a real push to a copy nobody
4 * watches. The platform says which environment this is in `FORGE_APP_ENV`
5 * (never the hostname); only the word `preview` holds the crew off, and
6 * `FORGE_CREW_IN_PREVIEW=1` turns it back on for a preview of a crew change.
7 */
8
9 export const CREW_OFF_IN_PREVIEW = "the crew is off in a preview";
10
11 export function crewOffInPreview(
12 env: Record<string, string | undefined> = process.env,
13 ): boolean {
14 return (
15 (env.FORGE_APP_ENV ?? "").trim().toLowerCase() === "preview" &&
16 (env.FORGE_CREW_IN_PREVIEW ?? "").trim() !== "1"
17 );
18 }
src/main.ts +7 −1
@@ -24,6 +24,7 @@ import { CHECK_KINDS, failAbandonedRuns } from "./core/runs.ts";
2424 import { health } from "./http/health.ts";
2525 import { STALE_AFTER_MS } from "./runs/lease.ts";
2626 import { startCrew } from "./crew/dispatch.ts";
27 import { CREW_OFF_IN_PREVIEW, crewOffInPreview } from "./crew/preview.ts";
2728 import { typeSafeDecider } from "./crew/decide.ts";
2829 import { graphInstagram } from "./crew/instagram.ts";
2930 import { startNotifier } from "./crew/notify.ts";
@@ -178,7 +179,12 @@ const snapshots = startSnapshots(
178179 passphrase: process.env["FORGE_SNAPSHOT_PASSPHRASE"] || undefined,
179180 },
180181 );
181 const crew = appsOn
182 const crewHeldOff = crewOffInPreview();
183 if (appsOn && crewHeldOff)
184 app.log.info(CREW_OFF_IN_PREVIEW, {
185 why: "FORGE_APP_ENV=preview; set FORGE_CREW_IN_PREVIEW=1 to run it",
186 });
187 const crew = appsOn && !crewHeldOff
182188 ? await startCrew(app, {
183189 forgeUrl,
184190 localUrl: servers.url,
src/web/studio/crew.ts +7 −2
@@ -21,6 +21,7 @@ import { CREW_ROLES, listCrewRoles, type CrewRole } from "../../core/agents.ts";
2121 import type { AppRecord } from "../../core/apps.ts";
2222 import { canAdmin, canSeeOwnerFacts, type Repo } from "../../core/repos.ts";
2323 import { autonomyStatus } from "../../crew/autonomy.ts";
24 import { crewOffInPreview } from "../../crew/preview.ts";
2425 import type { Ctx } from "../../http/context.ts";
2526 import { crumbsFor, repoBase, repoHeader, repoPalette } from "../chrome.ts";
2627 import { html, raw, type Html } from "../html.ts";
@@ -169,8 +170,12 @@ function crewCard(ctx: Ctx, repo: Repo, base: string, admin: boolean): Html {
169170
170171 function autonomyCard(ctx: Ctx, repo: Repo, base: string, admin: boolean): Html {
171172 const status = repo.autonomy === "off" ? undefined : autonomyStatus(ctx.app, repo);
172 const state =
173 status === undefined
173 const state = crewOffInPreview()
174 ? html`<p class="muted small">
175 The crew is off in a preview: nothing here plans, picks or runs an
176 agent. Set <code>FORGE_CREW_IN_PREVIEW=1</code> to turn it on.
177 </p>`
178 : status === undefined
174179 ? html`<p class="muted small">
175180 Off. Every run starts because a person assigned, commented, or a
176181 check failed.
test/autonomy.test.ts +37
@@ -606,3 +606,40 @@ describe("the slot count is the rule the loop enforces", () => {
606606 expect(await driveRepo(app, fresh())).toBe("full");
607607 });
608608 });
609
610 describe("the crew in a preview", () => {
611 test("crewOffInPreview holds off only for preview, unless turned back on", async () => {
612 const { crewOffInPreview } = await import("../src/crew/preview.ts");
613 expect(crewOffInPreview({ FORGE_APP_ENV: "preview" })).toBe(true);
614 expect(crewOffInPreview({ FORGE_APP_ENV: "production" })).toBe(false);
615 expect(crewOffInPreview({})).toBe(false);
616 expect(
617 crewOffInPreview({ FORGE_APP_ENV: "preview", FORGE_CREW_IN_PREVIEW: "1" }),
618 ).toBe(false);
619 });
620
621 test("an autonomy tick is a no-op in a preview", async () => {
622 const { autonomyTick } = await import("../src/crew/autonomy.ts");
623 const dir = await mkdtemp(join(tmpdir(), "forge-prev-"));
624 const app = await createApp({ dataDir: dir } as Parameters<typeof createApp>[0]);
625 let touched = 0;
626 const real = app.db.query.bind(app.db);
627 (app.db as { query: unknown }).query = (...a: unknown[]) => {
628 touched++;
629 return (real as (...x: unknown[]) => unknown)(...a);
630 };
631 const before = process.env.FORGE_APP_ENV;
632 process.env.FORGE_APP_ENV = "preview";
633 try {
634 await autonomyTick(app);
635 expect(touched).toBe(0);
636 delete process.env.FORGE_APP_ENV;
637 await autonomyTick(app);
638 expect(touched).toBeGreaterThan(0);
639 } finally {
640 if (before === undefined) delete process.env.FORGE_APP_ENV;
641 else process.env.FORGE_APP_ENV = before;
642 await rm(dir, { recursive: true, force: true });
643 }
644 });
645 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs