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;