From 883cf329e5d714160148b23599342ac7bb8f50f8 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 31 May 2026 05:27:11 -0700 Subject: [PATCH] =?UTF-8?q?feat(fleet):=20PR=20deliverables=20=E2=80=94=20?= =?UTF-8?q?jobs=20target=20a=20repo,=20factory=20opens=20a=20PR,=20link=20?= =?UTF-8?q?recorded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make "shipped" produce a real artifact. A job can now carry an optional repo (owner/name or clone URL) + baseBranch; the factory's PR mode runs the agent in an isolated checkout, opens a PR, and records the link. Backend: - SubmitJobSchema + FleetJobDoc: optional repo/baseBranch (recorded on submit). - FleetRunDoc: optional prUrl/branch. - ReleaseLease report carries prUrl/branch -> stored on the run. - +2 coordinator tests. UI (tracker-web): - New Job form gains optional Repo + Base branch fields (and fixes the priority options to the valid critical/high/medium/low; "normal" was rejected by the API). - Job detail Runs table shows a PR ↗ link from run.prUrl. - fleet-client: submitJob repo/baseBranch; FleetRun prUrl/branch; OperatorAction +ship. Docs: FLEET_CONTROL_PLANE.md "PR deliverable (PR mode)" section. Verified: tsc clean; fleet suite 182; tracker-web 230. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../app/dashboard/fleet/jobs/[id]/page.tsx | 18 ++++++++- .../src/app/dashboard/fleet/jobs/page.tsx | 39 +++++++++++++++++-- .../tracker-web/src/lib/fleet-client.ts | 7 +++- docs/GIGAFACTORY/FLEET_CONTROL_PLANE.md | 12 ++++++ .../src/modules/fleet/coordinator.test.ts | 30 ++++++++++++++ .../src/modules/fleet/coordinator.ts | 15 +++++-- .../src/modules/fleet/routes.ts | 2 + .../src/modules/fleet/types.ts | 17 ++++++++ 8 files changed, 131 insertions(+), 9 deletions(-) diff --git a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx index a9275db9..01957990 100644 --- a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx @@ -351,7 +351,8 @@ export default function FleetJobDetailPage() { Duration Model Tokens (in/out) - Cost + Cost + PR @@ -401,6 +402,21 @@ export default function FleetJobDetailPage() { '—' )} + + {r.prUrl ? ( + + PR ↗ + + ) : ( + '—' + )} + ); })} diff --git a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx index d6192338..0e673370 100644 --- a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx @@ -30,8 +30,10 @@ export default function FleetJobsPage() { // New-job form state const [showForm, setShowForm] = useState(false); const [body, setBody] = useState(''); - const [priority, setPriority] = useState<'low' | 'normal' | 'high'>('high'); + const [priority, setPriority] = useState<'critical' | 'high' | 'medium' | 'low'>('high'); const [caps, setCaps] = useState('build'); + const [repo, setRepo] = useState(''); + const [baseBranch, setBaseBranch] = useState('main'); const [submitting, setSubmitting] = useState(false); const [submitMsg, setSubmitMsg] = useState<{ ok: boolean; text: string } | null>(null); @@ -72,6 +74,7 @@ export default function FleetJobsPage() { bodyMd: body.trim(), priority, capabilities, + ...(repo.trim() ? { repo: repo.trim(), baseBranch: baseBranch.trim() || 'main' } : {}), }); setSubmitMsg({ ok: true, text: `Submitted ${job.id} (stage: ${job.stage}).` }); setBody(''); @@ -82,7 +85,7 @@ export default function FleetJobsPage() { } finally { setSubmitting(false); } - }, [body, caps, priority, refresh]); + }, [body, caps, priority, repo, baseBranch, refresh]); return (
@@ -122,11 +125,14 @@ export default function FleetJobsPage() {
@@ -141,6 +147,31 @@ export default function FleetJobsPage() { className="rounded border bg-background px-2 py-1 text-sm" /> +
+ + setRepo(e.target.value)} + placeholder="owner/name" + className="rounded border bg-background px-2 py-1 text-sm font-mono" + /> +
+ {repo.trim() && ( +
+ + setBaseBranch(e.target.value)} + className="rounded border bg-background px-2 py-1 text-sm font-mono" + /> +
+ )} diff --git a/dashboards/tracker-web/src/lib/fleet-client.ts b/dashboards/tracker-web/src/lib/fleet-client.ts index 397f4042..ea02164b 100644 --- a/dashboards/tracker-web/src/lib/fleet-client.ts +++ b/dashboards/tracker-web/src/lib/fleet-client.ts @@ -63,6 +63,8 @@ export interface FleetRun { endedAt?: string; result?: string; insights: FleetRunInsights; + prUrl?: string; + branch?: string; } export interface FleetEvent { @@ -195,8 +197,11 @@ export async function getJob(id: string): Promise { export interface SubmitJobBody { idempotencyKey: string; bodyMd: string; - priority?: 'low' | 'normal' | 'high'; + priority?: 'critical' | 'high' | 'medium' | 'low'; capabilities?: string[]; + /** PR mode: open a PR against this repo (`owner/name` or clone URL) + base branch. */ + repo?: string; + baseBranch?: string; } /** Submit a new fleet job. Returns the created job. */ diff --git a/docs/GIGAFACTORY/FLEET_CONTROL_PLANE.md b/docs/GIGAFACTORY/FLEET_CONTROL_PLANE.md index 5c9977d8..e6c7df9f 100644 --- a/docs/GIGAFACTORY/FLEET_CONTROL_PLANE.md +++ b/docs/GIGAFACTORY/FLEET_CONTROL_PLANE.md @@ -150,6 +150,18 @@ release), the coordinator mirrors the outcome onto the latest **run** run's `insights.costUsd`. So the dashboard's per-run result/cost/tokens stay consistent with the job stage. +### PR deliverable (PR mode) + +A job may carry an optional **`repo`** (`owner/name` or a clone URL) + **`baseBranch`**. +When the factory runs with `AQ_FLEET_PR=1`, it runs the agent in an isolated checkout +on branch `aq/job/`, then commits, pushes, and opens a PR via `gh`. The PR URL + +- branch are reported on lease release and recorded on the **run** (`run.prUrl`, + `run.branch`) — the dashboard shows a **PR ↗** link in the job's Runs table. Submit + `repo`/`baseBranch` from the dashboard "New Job" form or the `POST /fleet/jobs` body. + This round opens the PR (merge stays a human/CI step); opt-in auto-merge is a planned + follow-up. + ## API Reference Summary | Endpoint | Method | Phase | Notes | diff --git a/services/platform-service/src/modules/fleet/coordinator.test.ts b/services/platform-service/src/modules/fleet/coordinator.test.ts index 41a7e7d6..9ca93f55 100644 --- a/services/platform-service/src/modules/fleet/coordinator.test.ts +++ b/services/platform-service/src/modules/fleet/coordinator.test.ts @@ -1295,3 +1295,33 @@ describe('fleet coordinator — DAG submitChildren cycle detection', () => { expect(result.parent.stage).toBe('blocked'); }); }); + +describe('fleet coordinator — PR target + deliverable (§PR mode)', () => { + beforeEach(() => setProvider(new MemoryDatastoreProvider())); + afterEach(() => _resetDatastoreProvider()); + + it('submit records repo/baseBranch on the job', async () => { + await coord.submitJob( + PID, + input({ idempotencyKey: 'pr-1', repo: 'acme/widgets', baseBranch: 'develop' }) + ); + const claim = await coord.claimNextJob(factory()); + expect(claim!.job.repo).toBe('acme/widgets'); + expect(claim!.job.baseBranch).toBe('develop'); + }); + + it('release report records prUrl + branch on the run', async () => { + await coord.submitJob(PID, input({ idempotencyKey: 'pr-2', repo: 'acme/widgets' })); + const claim = await coord.claimNextJob(factory()); + const c = claim!.job; + const rel = await coord.releaseLease(c.id, PID, c.leaseEpoch, 'review', { + result: 'shipped', + prUrl: 'https://github.com/acme/widgets/pull/42', + branch: 'aq/job/pr-2', + }); + expect(rel.ok).toBe(true); + const runs = await repo.listRunsByJob(c.id); + expect(runs[0]!.prUrl).toBe('https://github.com/acme/widgets/pull/42'); + expect(runs[0]!.branch).toBe('aq/job/pr-2'); + }); +}); diff --git a/services/platform-service/src/modules/fleet/coordinator.ts b/services/platform-service/src/modules/fleet/coordinator.ts index dacf02df..04e481f0 100644 --- a/services/platform-service/src/modules/fleet/coordinator.ts +++ b/services/platform-service/src/modules/fleet/coordinator.ts @@ -212,6 +212,8 @@ export async function submitJob(productId: string, input: SubmitJobInput): Promi kind: input.kind, parentId: input.parentId, trackerItemId: input.trackerItemId, + repo: input.repo, + baseBranch: input.baseBranch, attempts: 0, leaseEpoch: 0, rev: 0, @@ -886,7 +888,12 @@ export async function releaseLease( productId: string, leaseEpoch: number, stage?: FleetStage, - report?: { insights?: FleetRunDoc['insights']; result?: FleetRunDoc['result'] } + report?: { + insights?: FleetRunDoc['insights']; + result?: FleetRunDoc['result']; + prUrl?: string; + branch?: string; + } ): Promise> { const job = await repo.getJob(jobId, productId); if (!job) return { ok: false, reason: 'not_found' }; @@ -896,12 +903,14 @@ export async function releaseLease( const res = await repo.revUpdateLease(jobId, lease.rev, { status: 'released' }); if (!res.ok) return { ok: false, reason: 'conflict' }; if (stage) await repo.revUpdateJob(jobId, productId, job.rev, { stage }); - // Record the factory's reported cost/token metrics + outcome on the current run. - if (report?.insights || report?.result) { + // Record the factory's reported metrics + outcome + PR deliverable on the run. + if (report?.insights || report?.result || report?.prUrl || report?.branch) { const runId = `${jobId}:run:${job.attempts}`; await repo.updateRun(runId, jobId, { ...(report.insights ? { insights: report.insights } : {}), ...(report.result ? { result: report.result } : {}), + ...(report.prUrl ? { prUrl: report.prUrl } : {}), + ...(report.branch ? { branch: report.branch } : {}), endedAt: new Date().toISOString(), }); } diff --git a/services/platform-service/src/modules/fleet/routes.ts b/services/platform-service/src/modules/fleet/routes.ts index 84ce2380..a9576afd 100644 --- a/services/platform-service/src/modules/fleet/routes.ts +++ b/services/platform-service/src/modules/fleet/routes.ts @@ -261,6 +261,8 @@ export async function fleetRoutes(app: FastifyInstance) { const res = await coordinator.releaseLease(id, pid, parsed.data.leaseEpoch, parsed.data.stage, { insights: parsed.data.insights, result: parsed.data.result, + prUrl: parsed.data.prUrl, + branch: parsed.data.branch, }); if (!res.ok) { if (res.reason === 'not_found') throw new NotFoundError('Job or lease not found'); diff --git a/services/platform-service/src/modules/fleet/types.ts b/services/platform-service/src/modules/fleet/types.ts index fa908415..ef87bc3d 100644 --- a/services/platform-service/src/modules/fleet/types.ts +++ b/services/platform-service/src/modules/fleet/types.ts @@ -177,6 +177,14 @@ export const FleetJobDocSchema = z.object({ kind: z.enum(JOB_KINDS).default('leaf'), parentId: z.string().optional(), trackerItemId: z.string().optional(), + /** + * Optional PR target: the repo (e.g. `owner/name` or a clone URL) and base + * branch the factory should open a pull request against (§PR mode). When set + * and the factory runs with PR mode on, its work is committed to a job branch, + * pushed, and a PR is opened; the PR URL is recorded on the run. + */ + repo: z.string().optional(), + baseBranch: z.string().optional(), checkpoint: CheckpointSchema.optional(), attempts: z.number().int().nonnegative().default(0), leaseEpoch: z.number().int().nonnegative().default(0), @@ -214,6 +222,9 @@ export const FleetRunDocSchema = z.object({ verifyResult: z.enum(['pass', 'fail']).optional(), result: z.enum(RUN_RESULTS).optional(), insights: InsightsSchema.default({}), + /** PR deliverable produced by this run (§PR mode). */ + prUrl: z.string().optional(), + branch: z.string().optional(), }); export type FleetRunDoc = z.infer; @@ -338,6 +349,9 @@ export const SubmitJobSchema = z.object({ kind: z.enum(JOB_KINDS).default('leaf'), parentId: z.string().optional(), trackerItemId: z.string().optional(), + /** Optional PR target (§PR mode): repo (`owner/name` or clone URL) + base branch. */ + repo: z.string().optional(), + baseBranch: z.string().optional(), /** Phase 3 DAG: inline children to create atomically with the parent. */ children: z .array( @@ -412,6 +426,9 @@ export const ReleaseLeaseSchema = z.object({ // releases the lease at the end of a work unit. Recorded on the current run. insights: InsightsSchema.optional(), result: z.enum(RUN_RESULTS).optional(), + // PR deliverable (§PR mode): the pull request the run opened + its branch. + prUrl: z.string().optional(), + branch: z.string().optional(), }); export type ReleaseLeaseInput = z.infer;