mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Feat/v0.13.0 (#270)
* feat(release): add release-manager feature flag (default off) * feat(release): change classification + semver-bump derivation * feat(release): readiness audit (changelog/version-ref/docs/migration/gate) * feat(release): release-manager engine proposes a gated release * feat(release): fail-closed release executor (bump, gate, publish) * feat(release): CEO approve/reject release-proposal surface * docs(release): document the gated release manager * feat(memory): add org-memory feature flags (default off) * feat(memory): add playbooks table + status enum + migration * feat(memory): playbook service with auditor curation transitions * feat(memory): playbooks RAG index plugin * feat(memory): index a playbook into RAG on approval * feat(memory): distill a high-signal lesson at task completion * feat(memory): keep private journal reflections out of the shared RAG corpus * feat(memory): draft_playbook verb + auditor curation verbs * fix(ci): resolve mypy tests/ errors blocking the gate (UUID casts, annotations) * feat(memory): auto-inject similar lessons/playbooks into the briefing * feat(memory): auditor playbook review queue (api + panel) * docs(memory): document the org-memory loop + playbook verbs * fix(provisioning): idempotent pitch provisioning (reuse product/project by slug on re-approval) * fix(memory): add chunks_playbooks to the chunk schema + isolate release route tests - Migration 030's CHUNK_TABLES was missing chunks_playbooks, breaking the IndexType<->migration parity guard once the PLAYBOOKS index landed. The upgrade is ALTER ... IF EXISTS so adding it is safe on any DB shape. - The release-route fixture's approve/reject paths call db.commit() (real behavior), so a held proposal outlived the per-test rollback and leaked into engine tests that read the global list_open_release_proposals(). Tear down source=release_manager rows after each test. - Make the gather_snapshot real-repo smoke version-agnostic (semver match) so it stops pinning the literal repo version. * chore(release): 0.13.0 * ++ --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "roboco-panel",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { RecentActivityFeed } from "./recent-activity-feed";
|
||||
import { QuickActionsBar } from "./quick-actions-bar";
|
||||
import { CeoApprovalQueue } from "./ceo-approval-queue";
|
||||
import { PrReviewQueue } from "./pr-review-queue";
|
||||
import { ReleaseProposalCard } from "./release-proposal-card";
|
||||
import { PlaybookReviewQueue } from "./playbook-review-queue";
|
||||
import { StrategySignalsPanel } from "./strategy-signals-panel";
|
||||
import type { Activity } from "./activity-item";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -89,6 +91,12 @@ export function CommandCenter() {
|
||||
{/* External-PR review decision queue (hidden when empty) */}
|
||||
<PrReviewQueue />
|
||||
|
||||
{/* Gated release proposal (hidden when none open) */}
|
||||
<ReleaseProposalCard />
|
||||
|
||||
{/* Playbook review queue (hidden when no drafts) */}
|
||||
<PlaybookReviewQueue />
|
||||
|
||||
{/* Metrics, Alerts, and Usage Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-3 gap-6">
|
||||
<KeyMetricsPanel
|
||||
|
||||
@@ -10,5 +10,7 @@ export { ActivityItem } from "./activity-item";
|
||||
export { QuickActionsBar } from "./quick-actions-bar";
|
||||
export { HealthIndicator } from "./health-indicator";
|
||||
export { CeoApprovalQueue } from "./ceo-approval-queue";
|
||||
export { ReleaseProposalCard } from "./release-proposal-card";
|
||||
export { PlaybookReviewQueue } from "./playbook-review-queue";
|
||||
export { StrategySignalsPanel } from "./strategy-signals-panel";
|
||||
export { UsageOverviewPanel } from "./usage-overview-panel";
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { playbooksApi } from "@/lib/api";
|
||||
import type { Playbook } from "@/lib/api/playbooks";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { BookOpen, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const _MIN_REASON = 4;
|
||||
|
||||
// Auditor/CEO review queue for drafted playbooks. Hidden when none are pending
|
||||
// (mirrors the PR-review + release-proposal cards).
|
||||
export function PlaybookReviewQueue({ className }: { className?: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [rejecting, setRejecting] = useState<Playbook | null>(null);
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
const { data: drafts, isLoading } = useQuery({
|
||||
queryKey: ["playbooks", "drafts"],
|
||||
queryFn: () => playbooksApi.listDrafts(),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const invalidate = () =>
|
||||
queryClient.invalidateQueries({ queryKey: ["playbooks", "drafts"] });
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: (id: string) => playbooksApi.approve(id),
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
toast.success("Playbook approved and indexed");
|
||||
},
|
||||
onError: (e) =>
|
||||
toast.error(`Approve failed: ${e instanceof Error ? e.message : "error"}`),
|
||||
});
|
||||
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: ({ id, reason }: { id: string; reason: string }) =>
|
||||
playbooksApi.reject(id, reason),
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
toast.success("Playbook rejected (archived)");
|
||||
closeReject();
|
||||
},
|
||||
onError: (e) =>
|
||||
toast.error(`Reject failed: ${e instanceof Error ? e.message : "error"}`),
|
||||
});
|
||||
|
||||
const closeReject = () => {
|
||||
setRejecting(null);
|
||||
setReason("");
|
||||
};
|
||||
|
||||
const confirmReject = () => {
|
||||
if (!rejecting) return;
|
||||
if (reason.trim().length < _MIN_REASON) {
|
||||
toast.error("Give a brief reason for rejecting");
|
||||
return;
|
||||
}
|
||||
rejectMutation.mutate({ id: rejecting.id, reason: reason.trim() });
|
||||
};
|
||||
|
||||
if (isLoading || !drafts || drafts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
Playbook Review
|
||||
<Badge variant="secondary">{drafts.length}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Drafted playbooks awaiting your approval — approved ones are indexed
|
||||
and auto-suggested to agents.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{drafts.map((pb) => (
|
||||
<div
|
||||
key={pb.id}
|
||||
className="rounded-lg border p-4 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="font-medium">{pb.title}</span>
|
||||
{pb.team && <Badge variant="outline">{pb.team}</Badge>}
|
||||
{pb.tags.map((t) => (
|
||||
<Badge key={t} variant="secondary" className="text-xs">
|
||||
{t}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-semibold">When:</span> {pb.problem}
|
||||
</p>
|
||||
<pre className="mt-2 max-h-40 overflow-auto rounded bg-muted p-2 text-xs whitespace-pre-wrap">
|
||||
{pb.procedure}
|
||||
</pre>
|
||||
<div className="mt-3 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => setRejecting(pb)}
|
||||
>
|
||||
<XCircle className="mr-1 h-4 w-4" />
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
disabled={approveMutation.isPending}
|
||||
onClick={() => approveMutation.mutate(pb.id)}
|
||||
>
|
||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={!!rejecting} onOpenChange={() => closeReject()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reject playbook</DialogTitle>
|
||||
<DialogDescription>
|
||||
This archives the playbook. Give a brief reason (it is recorded).
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reject-reason">Reason</Label>
|
||||
<Textarea
|
||||
id="reject-reason"
|
||||
placeholder="e.g. duplicate of an existing playbook; too task-specific..."
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={closeReject}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmReject}
|
||||
disabled={rejectMutation.isPending}
|
||||
>
|
||||
{rejectMutation.isPending ? "Rejecting..." : "Reject & Archive"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { releaseApi } from "@/lib/api";
|
||||
import type { ReleaseExecuteResult } from "@/lib/api/release";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CheckCircle2, XCircle, Rocket, AlertTriangle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const _MIN_REJECT_CHARS = 10;
|
||||
|
||||
function gateBadgeVariant(
|
||||
gate: string,
|
||||
): "default" | "secondary" | "destructive" | "outline" {
|
||||
if (gate === "green") return "default";
|
||||
if (gate === "red") return "destructive";
|
||||
return "secondary";
|
||||
}
|
||||
|
||||
// A red gate / open gaps make publishing risky — the CEO should resolve them
|
||||
// first. Approval still runs the fail-closed executor, so it can't ship a bad
|
||||
// release; this only steers the CEO.
|
||||
export function ReleaseProposalCard({ className }: { className?: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [action, setAction] = useState<"approve" | "reject" | null>(null);
|
||||
const [requiredChanges, setRequiredChanges] = useState("");
|
||||
|
||||
const { data: proposal, isLoading } = useQuery({
|
||||
queryKey: ["release", "proposal"],
|
||||
queryFn: () => releaseApi.getProposal(),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: () => releaseApi.approve(),
|
||||
onSuccess: (result: ReleaseExecuteResult) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["release", "proposal"] });
|
||||
if (result.status === "published") {
|
||||
toast.success(
|
||||
`Published v${result.version}` +
|
||||
(result.release_url ? "" : " (no release URL returned)"),
|
||||
);
|
||||
} else {
|
||||
toast.warning(`Release halted (${result.status}): ${result.detail}`);
|
||||
}
|
||||
closeDialog();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
`Approve failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: (changes: string) => releaseApi.reject(changes),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["release", "proposal"] });
|
||||
toast.success("Proposal sent back with required changes");
|
||||
closeDialog();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
`Reject failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const closeDialog = () => {
|
||||
setAction(null);
|
||||
setRequiredChanges("");
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (action === "approve") {
|
||||
approveMutation.mutate();
|
||||
} else if (action === "reject") {
|
||||
if (requiredChanges.trim().length < _MIN_REJECT_CHARS) {
|
||||
toast.error("Describe the required changes (≥ 10 characters)");
|
||||
return;
|
||||
}
|
||||
rejectMutation.mutate(requiredChanges.trim());
|
||||
}
|
||||
};
|
||||
|
||||
// Hidden entirely when there is no open proposal (mirrors PrReviewQueue).
|
||||
if (isLoading || !proposal) return null;
|
||||
|
||||
const { report } = proposal;
|
||||
const pending = approveMutation.isPending || rejectMutation.isPending;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Rocket className="h-5 w-5" />
|
||||
Release Proposal
|
||||
<Badge variant="outline">v{report.proposed_version}</Badge>
|
||||
<Badge variant="secondary">{report.bump_kind}</Badge>
|
||||
<Badge variant={gateBadgeVariant(report.gate_state)}>
|
||||
gate: {report.gate_state}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{report.change_summary.length} change(s) since the last tag · review
|
||||
and approve to cut the release (nothing publishes until you do).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{report.gaps.length > 0 && (
|
||||
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3">
|
||||
<p className="flex items-center gap-1.5 text-sm font-medium text-amber-600">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{report.gaps.length} gap(s) to resolve before publishing
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 text-sm text-muted-foreground">
|
||||
{report.gaps.map((gap, i) => (
|
||||
<li key={`${gap.category}-${i}`}>
|
||||
<span className="font-mono text-xs uppercase">
|
||||
[{gap.category}]
|
||||
</span>{" "}
|
||||
{gap.detail}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Drafted CHANGELOG
|
||||
</p>
|
||||
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs whitespace-pre-wrap">
|
||||
{report.drafted_changelog}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Version bump plan ({report.version_bump_plan.length} files)
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{report.version_bump_plan.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{report.migration_notes.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Migrations
|
||||
</p>
|
||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||
{report.migration_notes.map((note, i) => (
|
||||
<li key={i}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.required_changes && (
|
||||
<p className="text-sm text-amber-600">
|
||||
Awaiting revision — you requested: {proposal.required_changes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => setAction("reject")}
|
||||
>
|
||||
<XCircle className="mr-1 h-4 w-4" />
|
||||
Reject with changes
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => setAction("approve")}
|
||||
>
|
||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||
Approve & publish
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={!!action} onOpenChange={() => closeDialog()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{action === "approve"
|
||||
? `Approve release v${report.proposed_version}?`
|
||||
: "Reject with required changes"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{action === "approve"
|
||||
? "This runs the fail-closed executor: write the bumps + CHANGELOG, run make quality, commit, wait for green CI, then publish. It aborts on a red gate or red CI."
|
||||
: "Record what must change. The proposal stays open for revision; nothing is published."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{action === "reject" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="required-changes">Required changes</Label>
|
||||
<Textarea
|
||||
id="required-changes"
|
||||
placeholder="e.g. tighten the CHANGELOG wording for the API change; hold for the migration fix..."
|
||||
value={requiredChanges}
|
||||
onChange={(e) => setRequiredChanges(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={closeDialog} disabled={pending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={pending}
|
||||
variant={action === "reject" ? "destructive" : "default"}
|
||||
className={
|
||||
action === "approve" ? "bg-green-600 hover:bg-green-700" : ""
|
||||
}
|
||||
>
|
||||
{pending
|
||||
? "Processing..."
|
||||
: action === "approve"
|
||||
? "Approve & publish"
|
||||
: "Send back for revision"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,14 @@ const FLAG_DESCRIPTIONS: Record<string, string> = {
|
||||
transcript_prune_enabled: "Run the background sweep that prunes old transcripts.",
|
||||
gateway_health_enabled:
|
||||
"Recover an agent whose MCP gateway has broken (it can run no tools) while its container stays up — kill + respawn it instead of shielding it from the reaper forever.",
|
||||
ci_watch_enabled:
|
||||
"Watch every opted-in project's CI and open a fix task when its default branch goes red (per-project opt-in; never auto-merges).",
|
||||
dep_update_enabled:
|
||||
"Periodically probe opted-in projects for dependency updates and open an update task when a lockfile would change (per-project opt-in; never auto-merges).",
|
||||
release_manager_enabled:
|
||||
"Run the deterministic release-readiness sweep and propose a release for you to approve or reject — it never publishes without your approval, and the executor is fail-closed on a red gate.",
|
||||
org_memory_enabled:
|
||||
"Close the learn→reuse loop: distill a lesson at task completion, index journal reflections, and auto-inject similar past lessons + approved playbooks into an agent's briefing on claim.",
|
||||
};
|
||||
|
||||
export function FeatureFlagsCard() {
|
||||
|
||||
@@ -15,3 +15,12 @@ export { streamApi } from "./stream";
|
||||
export { groupsApi } from "./groups";
|
||||
export { settingsApi } from "./settings";
|
||||
export { companyGoalsApi } from "./company-goals";
|
||||
export { releaseApi } from "./release";
|
||||
export type {
|
||||
ReleaseProposal,
|
||||
ReleaseReport,
|
||||
ReleaseGap,
|
||||
ReleaseExecuteResult,
|
||||
} from "./release";
|
||||
export { playbooksApi } from "./playbooks";
|
||||
export type { Playbook } from "./playbooks";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import api from "./client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Playbooks — curated, reusable procedures. Delivery agents draft them; the
|
||||
// Auditor (or CEO, via this panel) approves → indexed + auto-suggested, or
|
||||
// rejects → archived.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Playbook {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
problem: string;
|
||||
procedure: string;
|
||||
tags: string[];
|
||||
team?: string | null;
|
||||
scope: string;
|
||||
status: string;
|
||||
created_at?: string | null;
|
||||
}
|
||||
|
||||
export const playbooksApi = {
|
||||
listDrafts: async (): Promise<Playbook[]> => {
|
||||
const { data } = await api.get<Playbook[]>("/playbooks", {
|
||||
params: { status: "draft" },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
approve: async (id: string): Promise<Playbook> => {
|
||||
const { data } = await api.post<Playbook>(`/playbooks/${id}/approve`);
|
||||
return data;
|
||||
},
|
||||
reject: async (id: string, reason: string): Promise<Playbook> => {
|
||||
const { data } = await api.post<Playbook>(`/playbooks/${id}/reject`, { reason });
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import axios from "axios";
|
||||
import api from "./client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Release manager — the CEO approves or rejects a held release proposal that
|
||||
// the release-manager engine prepared (deterministic readiness sweep). Nothing
|
||||
// publishes until the CEO approves; the executor is fail-closed on a red gate.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ReleaseGap {
|
||||
category: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface ReleaseReport {
|
||||
proposed_version: string;
|
||||
bump_kind: string;
|
||||
change_summary: string[];
|
||||
drafted_changelog: string;
|
||||
version_bump_plan: string[];
|
||||
gaps: ReleaseGap[];
|
||||
migration_notes: string[];
|
||||
gate_state: string;
|
||||
}
|
||||
|
||||
export interface ReleaseProposal {
|
||||
task_id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
required_changes?: string | null;
|
||||
report: ReleaseReport;
|
||||
}
|
||||
|
||||
export interface ReleaseExecuteResult {
|
||||
status: string;
|
||||
version: string;
|
||||
files_changed: string[];
|
||||
commit_sha?: string | null;
|
||||
release_url?: string | null;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export const releaseApi = {
|
||||
// 404 means "no open proposal" — a normal empty state, returned as null.
|
||||
getProposal: async (): Promise<ReleaseProposal | null> => {
|
||||
try {
|
||||
const { data } = await api.get<ReleaseProposal>("/release/proposal");
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
approve: async (): Promise<ReleaseExecuteResult> => {
|
||||
const { data } = await api.post<ReleaseExecuteResult>(
|
||||
"/release/proposal/approve",
|
||||
);
|
||||
return data;
|
||||
},
|
||||
reject: async (requiredChanges: string): Promise<ReleaseProposal> => {
|
||||
const { data } = await api.post<ReleaseProposal>("/release/proposal/reject", {
|
||||
required_changes: requiredChanges,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user