diff options
| author | soryu <soryu@soryu.co> | 2026-05-08 12:12:21 +0100 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-05-08 12:12:21 +0100 |
| commit | bd1f21fe387ec57da76c300cbb1ebc0db48553a7 (patch) | |
| tree | 1ffe451c3dec2fbb91f1e71f55abed37083ec62a /makima/frontend/src/lib/api.ts | |
| parent | e00be74c8b575c725829677aadeb755ee81454d0 (diff) | |
| download | soryu-bd1f21fe387ec57da76c300cbb1ebc0db48553a7.tar.gz soryu-bd1f21fe387ec57da76c300cbb1ebc0db48553a7.zip | |
feat(contracts): lifecycle — Lock/Start/Pause/Complete/Unlock + queue schedulercontract-lifecycle
Adds the contract lifecycle layer on top of the unified-contracts
backbone (#128). State machine:
draft → queued → active → shipped → archived
At most one contract per directive sits in `active` at any time —
the queue is serialised because each directive owns a single shared
worktree. Repository helpers handle the transition checks AND
auto-promote the next-up `queued` contract whenever the active slot
frees (pause / complete / unlock-from-active / archive-from-active).
Endpoints (all under /api/v1/contracts/{id}):
POST /start draft → queued | active (depending on slot)
POST /pause active → queued; promotes next queued
POST /complete active → shipped; optional pr_url + pr_branch
POST /unlock queued | active → draft; promotes if was active
Frontend wiring:
* `DirectiveContractStatus` now includes `queued`.
* Migration adds `queued` to the CHECK constraint on
directive_documents.status.
* `ContractHeader` component renders breadcrumb + status pill +
status-driven action buttons + a merge-mode (shared / own_pr)
radio. Merge mode is editable only while draft / queued so a
running flow's branch target can't change mid-stream.
* RepositoryError gains a `Validation(String)` arm; the three
existing exhaustive matches (files, mesh, versions) get a
400 BAD_REQUEST response for it.
Drag-to-reorder UI deferred to a small follow-up — the backend
endpoint already exists from the backbone PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/lib/api.ts')
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 54 |
1 files changed, 53 insertions, 1 deletions
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 17cf1d0..f777ba0 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3597,7 +3597,7 @@ export async function pickUpOrders(directiveId: string): Promise<PickUpOrdersRes // to plain `Contract`. URLs and the user-facing UI already say "contract". // ============================================================================= -export type DirectiveContractStatus = "draft" | "active" | "shipped" | "archived"; +export type DirectiveContractStatus = "draft" | "queued" | "active" | "shipped" | "archived"; /** How a contract's commits land. `shared` (default): on the directive's * branch — multiple contracts feed one PR. `own_pr`: a contract-specific @@ -3700,6 +3700,58 @@ export async function reorderDirectiveContract( return res.json(); } +// ----- Lifecycle transitions ----------------------------------------------- +// +// Lock & Start: draft → queued (if a sibling is active) or active. +// Pause: active → queued; next queued sibling auto-promotes. +// Complete: active → shipped, optionally records pr_url + pr_branch. +// Unlock: queued or active → draft (reactivates editing). +// +// All four reach the same /contracts/{id}/<action> endpoint and surface +// 400 with a plain message string when the transition is invalid. + +async function postContractAction( + contractId: string, + action: "start" | "pause" | "complete" | "unlock", + body?: object, +): Promise<DirectiveContract> { + const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/${action}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body ?? {}), + }); + if (!res.ok) { + let detail = res.statusText; + try { + const err = await res.json(); + if (err?.message) detail = err.message; + } catch { + /* fall through to statusText */ + } + throw new Error(`Failed to ${action} contract: ${detail}`); + } + return res.json(); +} + +export async function startDirectiveContract(contractId: string): Promise<DirectiveContract> { + return postContractAction(contractId, "start"); +} + +export async function pauseDirectiveContract(contractId: string): Promise<DirectiveContract> { + return postContractAction(contractId, "pause"); +} + +export async function completeDirectiveContract( + contractId: string, + opts: { prUrl?: string; prBranch?: string } = {}, +): Promise<DirectiveContract> { + return postContractAction(contractId, "complete", opts); +} + +export async function unlockDirectiveContract(contractId: string): Promise<DirectiveContract> { + return postContractAction(contractId, "unlock"); +} + /** Steps and tasks attached to a single contract. Drives the per-contract * `tasks/` subfolder in the sidebar — when the contract ships, its * tasks visually move with it. */ |
