From 57e935eefc9a5984b55ce0ac794e17c4024e61b1 Mon Sep 17 00:00:00 2001 From: Igor Gerasimov Date: Sat, 25 Jul 2026 13:43:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(consilium):=20develop-scope=20chooser=20?= =?UTF-8?q?=E2=80=94=20ship=20P0s=20as=20one=20small=20MR=20per=20round?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A develop round implemented EVERY verdict action point in one branch, producing a single huge MR (5-10 heavyweight tasks) that no human can review. The loop is round-based by design — use that: let the operator scope a round to the P0s, merge a small MR, and let the next review round re-derive and carry the remainder. - controller.develop(loopId, {scope?: 'p0'|'all'}) filters the verdict's action points to the P0 slice on 'p0'; absent/'all' is byte-identical. - route: optional body.scope validated ('p0'|'all' else 400); absent scope keeps the historical single-arg controller call. - UI: 'Hand off to SDLC' opens a scope chooser when the verdict has both P0s and non-P0s — 'Only P0s (n) — small MR' vs 'All action points (m)'; no meaningful choice ⇒ direct hand-off as before. develop-route/fsm/autoplan: 166 passed (2 new). tsc + vite build clean. --- client/src/hooks/use-consilium-loops.ts | 9 ++- client/src/pages/ConsiliumLoopDetail.tsx | 58 +++++++++++++++++-- server/routes/consilium-loops.ts | 11 +++- .../consilium/consilium-loop-controller.ts | 15 ++++- .../consilium-loops-develop-route.test.ts | 21 +++++++ 5 files changed, 103 insertions(+), 11 deletions(-) diff --git a/client/src/hooks/use-consilium-loops.ts b/client/src/hooks/use-consilium-loops.ts index 870552b..12a9a40 100644 --- a/client/src/hooks/use-consilium-loops.ts +++ b/client/src/hooks/use-consilium-loops.ts @@ -518,9 +518,12 @@ export function useApproveMerge() { */ export function useDevelopLoop() { const qc = useQueryClient(); - return useMutation({ - mutationFn: (id) => apiRequest("POST", `${LIST_KEY}/${id}/develop`), - onSuccess: (_data, id) => { + return useMutation({ + // MR-size control: `scope: "p0"` develops only the P0s (small reviewable MR); + // omitted/"all" keeps the historical full-verdict round. + mutationFn: ({ id, scope }) => + apiRequest("POST", `${LIST_KEY}/${id}/develop`, scope ? { scope } : undefined), + onSuccess: (_data, { id }) => { qc.invalidateQueries({ queryKey: [LIST_KEY, id] }); qc.invalidateQueries({ queryKey: [LIST_KEY] }); }, diff --git a/client/src/pages/ConsiliumLoopDetail.tsx b/client/src/pages/ConsiliumLoopDetail.tsx index b14ba87..826dedd 100644 --- a/client/src/pages/ConsiliumLoopDetail.tsx +++ b/client/src/pages/ConsiliumLoopDetail.tsx @@ -1586,7 +1586,7 @@ function ReviewGateActions({ async function handleDevelop() { try { - await developLoop.mutateAsync(loopId); + await developLoop.mutateAsync({ id: loopId }); toast({ title: "Handed off to SDLC", description: "The developing round has started — follow the progress below.", @@ -2757,6 +2757,8 @@ export default function ConsiliumLoopDetail() { const approveMerge = useApproveMerge(); const developLoop = useDevelopLoop(); const [confirmOpen, setConfirmOpen] = useState(false); + // MR-size control: the develop-scope chooser (Only P0s vs all action points). + const [developScopeOpen, setDevelopScopeOpen] = useState(false); if (isLoading) { return ( @@ -2804,6 +2806,11 @@ export default function ConsiliumLoopDetail() { const latestActionPointCount = Array.isArray(latestRound?.openActionPoints) ? latestRound!.openActionPoints.length : 0; + // MR-size control: P0 slice of the latest verdict — offered as a smaller round. + const latestP0Count = Array.isArray(latestRound?.openActionPoints) + ? (latestRound!.openActionPoints as ActionPoint[]).filter((ap) => ap.priority === "P0") + .length + : 0; const canDevelop = isVerdictTerminalLoopState(loop.state) && latestActionPointCount > 0; @@ -2877,12 +2884,19 @@ export default function ConsiliumLoopDetail() { } } - async function handleDevelop() { + async function handleDevelop(scopeArg?: unknown) { + // Normalize: plain onClick callers pass a click event — only the two literal + // scopes ride to the server; anything else means "all" (historical behaviour). + const scope = scopeArg === "p0" || scopeArg === "all" ? scopeArg : undefined; + setDevelopScopeOpen(false); try { - await developLoop.mutateAsync(id); + await developLoop.mutateAsync({ id, scope }); toast({ title: "Handed off to SDLC", - description: "The developing round has started — follow the progress below.", + description: + scope === "p0" + ? `Developing only the P0 action points — merge that MR and the next round carries the rest.` + : "The developing round has started — follow the progress below.", }); } catch (err) { // 400 (NO_ACTION_POINTS / REPO_NOT_*) | 409 (WRONG_STATE / @@ -2950,7 +2964,13 @@ export default function ConsiliumLoopDetail() { {canDevelop && (