From b7df779e1d78e148f3924a153bff37c1ca1f8b70 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 1 Jun 2026 00:44:10 -0700 Subject: [PATCH] feat(fleet): draft jobs + editable prompt (save-as-draft, submit, lock on pickup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (platform-service): - New `draft` stage (not claimable; scheduler only takes queued/blocked). - submitJob accepts `draft: true` → parks a new/superseded job as a draft. - updateDraft(): edit prompt/config in place while draft/queued/blocked; recomputes contentHash; rejected (conflict) once picked up (assigned+). - submitDraft(): promote draft → queued (or blocked on unmet deps); idempotent. - Routes: PATCH /fleet/jobs/:id/draft, POST /fleet/jobs/:id/submit. - tracker-bridge: map draft → item status `open`. Tests + FLEET_STAGES updated. Frontend (tracker-web): - New-Job form: add "Save as draft" alongside "Submit". - Job detail: edit the prompt + Save while draft/queued/blocked, "Submit" a draft, and lock it read-only once a factory picks it up. 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 | 135 +++++++++++++++--- .../src/app/dashboard/fleet/jobs/page.tsx | 122 +++++++++------- .../tracker-web/src/lib/fleet-client.ts | 23 +++ .../src/modules/fleet/coordinator.test.ts | 64 +++++++++ .../src/modules/fleet/coordinator.ts | 75 +++++++++- .../src/modules/fleet/routes.ts | 30 ++++ .../src/modules/fleet/tracker-bridge.ts | 3 + .../src/modules/fleet/types.test.ts | 1 + .../src/modules/fleet/types.ts | 19 ++- 9 files changed, 398 insertions(+), 74 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 8a2e7f08..48c2d6e5 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 @@ -16,6 +16,8 @@ import { getJobExplain, patchJob, operatorAction, + updateDraft, + submitDraft, requestReview, submitReview, subscribeJobEvents, @@ -256,7 +258,7 @@ export default function FleetJobDetailPage() { {/* Prompt (the job body) + PR/target config */} - + {/* Review gate (multi-reviewer human gate) */} {job.stage === 'review' && ( @@ -502,11 +504,48 @@ function MetaCard({ label, value }: { label: string; value: string }) { /** * The job's prompt (verbatim `bodyMd`) + its PR/target config. The prompt is only - * mutable before a factory picks the job up; once it leaves `queued`/`draft` it is - * locked (a worker may already be acting on it) so it renders read-only. + * mutable before a factory picks the job up (stage `draft`/`queued`/`blocked`): + * it can be edited + saved in place, and a `draft` can be submitted from here. + * Once a factory claims it (assigned+) the prompt is locked and renders read-only. */ -function PromptCard({ job }: { job: FleetJob }) { - const locked = job.stage !== 'queued' && job.stage !== 'draft'; +function PromptCard({ job, onChanged }: { job: FleetJob; onChanged: () => void | Promise }) { + const editable = job.stage === 'draft' || job.stage === 'queued' || job.stage === 'blocked'; + const [editing, setEditing] = useState(false); + const [draftBody, setDraftBody] = useState(job.bodyMd ?? ''); + const [busy, setBusy] = useState(null); + const [err, setErr] = useState(null); + + const save = async () => { + if (!draftBody.trim()) { + setErr('Prompt cannot be empty.'); + return; + } + setBusy('save'); + setErr(null); + try { + await updateDraft(job.id, { bodyMd: draftBody.trim() }); + setEditing(false); + await onChanged(); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Save failed.'); + } finally { + setBusy(null); + } + }; + + const submit = async () => { + setBusy('submit'); + setErr(null); + try { + await submitDraft(job.id); + await onChanged(); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Submit failed.'); + } finally { + setBusy(null); + } + }; + const cfg: Array<[string, string | undefined]> = [ ['Repo', job.repo], ['Base branch', job.repo ? (job.baseBranch ?? 'main') : undefined], @@ -517,31 +556,91 @@ function PromptCard({ job }: { job: FleetJob }) { ['Idempotency key', job.idempotencyKey], ]; const shown = cfg.filter(([, v]) => v != null && v !== ''); + return (

Prompt

- {locked ? 'read-only — picked up' : 'queued — not yet picked up'} + {editable + ? job.stage === 'draft' + ? 'draft — editable' + : 'editable until picked up' + : 'read-only — picked up'} +
+ {editable && !editing && ( + + )} + {job.stage === 'draft' && !editing && ( + + )} +
-
-        {job.bodyMd?.trim() ? job.bodyMd : 'No prompt body.'}
-      
+ + {editing ? ( +
+