summaryrefslogtreecommitdiff
path: root/makima/frontend/src/lib/api.ts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-08 12:12:51 +0100
committerGitHub <noreply@github.com>2026-05-08 12:12:51 +0100
commit6690b714c64aaef5781bc0aac41b777ab72e9070 (patch)
tree1ffe451c3dec2fbb91f1e71f55abed37083ec62a /makima/frontend/src/lib/api.ts
parente00be74c8b575c725829677aadeb755ee81454d0 (diff)
downloadsoryu-6690b714c64aaef5781bc0aac41b777ab72e9070.tar.gz
soryu-6690b714c64aaef5781bc0aac41b777ab72e9070.zip
feat(contracts): lifecycle — Lock/Start/Pause/Complete/Unlock + queue scheduler (#129)
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.ts54
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. */