P4.2b — loop_proposal draft approval UI Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Let an AM/Expert review and approve (publish) the loop_proposal kb_golden_answer drafts that the P4.2a producer creates — by making the existing /ops/config-assets Ops surface surface them clearly, show the source-correction context, and reuse the existing publish action (which runs the P4.1 eval gate).
Architecture: Pure-frontend changes to the existing /ops/config-assets list + detail pages (Next.js 16 / React 19). Zero backend changes — the list endpoint already returns loop_proposal drafts with source + metadata, and POST /admin/config-assets/:id/publish already supports AM auth and doesn't care about source (it forks only by scope through the P4.1 eval gate). We add: a source Badge + a "pending proposals" client-side filter on the list, a source-correction provenance panel on the detail page (the wrong originalResponse vs the proposed golden answer), a typed metadata.sourceCorrection shape, and a stale publish-dialog copy fix.
Tech Stack: Next.js 16 App Router, React 19, the existing frontend/src/lib/api.ts client + Badge/Select/RequireRole Ops components.
Context the implementer needs (verified against real code)
ADR-037 Decision 2: the improvement loop PROPOSES only; a human approval (= publishing the draft) is what applies it. P4.2a built the producer (drafts land as source="loop_proposal" kb_golden_answer, unpublished). P4.2b is the human-approval surface. Publishing a loop_proposal draft is identical to publishing any manual draft — same requestPublish, forks only by scope, org_instance → P4.1 hallucination eval gate → publishing → published/blocked. Approval ≠ a new endpoint; it IS the existing publish button.
Verified frontend facts (worktree feat/specialist-value-output-p2plus):
- List page:
frontend/src/app/ops/config-assets/page.tsx. Has org→specialist→assetType selects (TYPE_FILTERSat :38-45, no source filter).AssetTable(:64-137) columns Name/Slug/Type/Status/Source/Updated. Source column already renders{a.source}as bare text (:114) — loop_proposal drafts ALREADY appear (no source filter inloadAssets:181-198 →adminListConfigAssets({orgId, specialistId, assetType})). Status badge (:103-113) only checksdraftVersionId/publishedVersionIdpointers (does not showpublishing/blocked). - Detail page:
frontend/src/app/ops/config-assets/[id]/page.tsx. Dispatches editor byassetType(kb_golden_answer→KbGoldenAnswerEditor, :248-256).PageHeadersubtitle (:214) showsassetType · slug · catalog?— does NOT showsourceormetadata.handlePublish(:130-137) →adminPublishConfigAsset(id, {orgId, specialistId, versionId}).VersionHistoryat the bottom (:289-297). VersionHistory.tsx: renderspublishing(warning badge :42-43) +blocked(danger :44-45); draft row shows Publish/Discard buttons (isDraftRow=publishStatus === "draft":118-119). Publish confirm copy (:71-74) still says "eval gate lands in Phase 4" — STALE, P4.1 shipped.frontend/src/lib/api.ts:ConfigAsset(:5113-5128) already hassource: "manual" | "imported_repo" | "loop_proposal"(:5121) +metadata?: Record<string, unknown> | null(:5125).ConfigAssetDetail extends ConfigAsset(:5131).ConfigVersionStatus(:5095-5101) includespublishing/blocked.adminPublishConfigAsset(:5209),adminDiscardConfigAssetDraft(:5229).- Auth: both pages wrapped in
RequireRole allow={["superadmin","account_manager"]}. org dropdown =listAmOrgs()(AM sees only their orgs; SA sees all). No backend change needed for auth —assertOrgAccessalready lets an AM publish their own org's assets. - P4.2a producer writes
metadata.sourceCorrection = { correctionId, correctionType, originalResponse }(verifiedloop-proposal-producer.service.ts:106-123);contentholds the golden{question, answer, ...}. slugloop-<correctionId>, displayNameProposed answer — <correctionType>.
Frontend CI gates (memory frontend-ci-gates-gotchas — do NOT trip these):
- ESLint runs with a hard
--max-warnings <N>budget. Do NOT introduce new warnings and do NOT raise the budget. Runnpm run lintinfrontend/before committing. regression-gate.shscans the whole repo for aconfirm(regex (#1961). If you add a nativeconfirm(...)call it trips the gate — use the existing dialog helper / aconfirmDialog-named wrapper instead. Check how VersionHistory currently does its publish confirmation (it already has a confirm dialog — reuse that pattern, don't addwindow.confirm).
DECISION POINTS (reversible defaults — user may override)
- DP-1 — Modify the existing surface, don't build a new "proposals inbox" route. Verified the existing list already shows loop_proposal drafts and the detail page already publishes. A separate inbox is more code + a new route to secure for no functional gain at this scale. Seam: if proposal volume later warrants a dedicated inbox, the client-side filter added here is the query it would formalise.
- DP-2 — Client-side source filter, not a server-side
?source=param. Avoids a backend DTO/query change (smaller demo-period blast radius). Proposal drafts are low-volume;assets.filter(...)is sufficient. Seam: the filter predicate is one function — promoting it to a server param later is contained. - DP-3 — Provenance panel shows
originalResponse(the wrong reply) +correctionType+correctionId, read-only. This is the context an approver needs: "here's what the AI got wrong; here's the proposed canonical answer — is the proposal right?" We do NOT show the full correction thread (would need a new fetch/endpoint) — just what P4.2a already stamped into metadata. Seam: a "view full correction" deep-link is a future add.
File structure
- Modify
frontend/src/lib/api.ts— add a typedSourceCorrectionMetashape + a narrow helperreadSourceCorrection(metadata). - Modify
frontend/src/app/ops/config-assets/page.tsx— SourceBadge+ a "Pending proposals" client-side toggle. - Create
frontend/src/app/ops/config-assets/SourceCorrectionPanel.tsx— the read-only provenance panel. - Modify
frontend/src/app/ops/config-assets/[id]/page.tsx— renderSourceCorrectionPanelwhensource === "loop_proposal". - Modify
frontend/src/app/ops/config-assets/VersionHistory.tsx— update the stale publish-confirm copy.
Task 1: Typed sourceCorrection metadata + narrow helper
Files:
-
Modify:
frontend/src/lib/api.ts -
Step 1: Read the current
ConfigAssetinterface (api.ts:5113-5128) to see the exactmetadatafield declaration and surrounding style (indentation, export conventions). -
Step 2: Add the typed shape + narrow helper near the
ConfigAssetinterface (do NOT changemetadata's existingRecord<string, unknown> | nulltype — keep it permissive; add a reader):
/** P4.2a stamps this onto a loop_proposal asset's metadata for approval provenance. */
export interface SourceCorrectionMeta {
correctionId: string;
correctionType: string;
originalResponse: string;
}
/**
* Narrow an asset's untyped `metadata` to the P4.2a source-correction shape.
* Returns null when absent or malformed (never throws) — the approval panel
* simply doesn't render provenance for a non-loop_proposal / hand-made asset.
*/
export function readSourceCorrection(
metadata: Record<string, unknown> | null | undefined,
): SourceCorrectionMeta | null {
const sc = (metadata as { sourceCorrection?: unknown } | null | undefined)?.sourceCorrection;
if (!sc || typeof sc !== "object") return null;
const o = sc as Record<string, unknown>;
if (
typeof o.correctionId === "string" &&
typeof o.correctionType === "string" &&
typeof o.originalResponse === "string"
) {
return { correctionId: o.correctionId, correctionType: o.correctionType, originalResponse: o.originalResponse };
}
return null;
}
-
Step 3: Typecheck —
cd frontend && npx tsc --noEmit(or the project's typecheck script; checkfrontend/package.jsonscripts). Expected: no new errors. -
Step 4: Commit
cd /Users/daniel/Project/hp/humanwork/.worktrees/specialist-value-output-p2plus
git add frontend/src/lib/api.ts
git commit -m "feat(frontend): typed sourceCorrection metadata + narrow helper (P4.2b)"
Task 2: Source-correction provenance panel
Files:
-
Create:
frontend/src/app/ops/config-assets/SourceCorrectionPanel.tsx -
Step 1: Look at an existing sibling editor (
KbGoldenAnswerEditor.tsxin the same dir) to match the project's component style, imports (Badge, card/section wrappers), and Tailwind class conventions. Mirror them — do NOT invent a new visual language. -
Step 2: Create the panel — a read-only card shown above the golden editor when the asset came from the improvement loop. Match the real component/style conventions you just read (the code below is the shape; align class names + primitives to the sibling):
"use client";
import type { SourceCorrectionMeta } from "@/lib/api";
/**
* P4.2b — read-only provenance for a loop_proposal golden-answer draft.
* Shows the approver what the AI originally got wrong (the correction that
* seeded this proposal) so they can judge whether the proposed canonical
* answer is right before publishing. Read-only: the correction itself is
* immutable source signal (ADR-037); approval = publishing the draft.
*/
export function SourceCorrectionPanel({ meta }: { meta: SourceCorrectionMeta }) {
return (
<section className="rounded-lg border border-amber-300 bg-amber-50 p-4" data-testid="source-correction-panel">
<div className="mb-2 flex items-center gap-2">
<span className="rounded bg-amber-200 px-2 py-0.5 text-xs font-medium text-amber-900">
From improvement loop
</span>
<span className="text-xs text-gray-600">correction type: {meta.correctionType}</span>
</div>
<p className="mb-1 text-xs font-medium text-gray-700">Original (incorrect) AI reply that triggered this proposal:</p>
<blockquote className="whitespace-pre-wrap rounded bg-white p-3 text-sm text-gray-800">
{meta.originalResponse}
</blockquote>
<p className="mt-2 text-xs text-gray-500">Source correction: {meta.correctionId}</p>
</section>
);
}
-
Step 3: Lint + typecheck —
cd frontend && npm run lint && npx tsc --noEmit. Expected: zero new warnings/errors (the--max-warningsbudget must not be exceeded — if the new file emits any warning, fix it, do NOT raise the budget). -
Step 4: Commit
git add frontend/src/app/ops/config-assets/SourceCorrectionPanel.tsx
git commit -m "feat(frontend): loop_proposal source-correction provenance panel (P4.2b)"
Task 3: Wire the panel into the detail page
Files:
-
Modify:
frontend/src/app/ops/config-assets/[id]/page.tsx -
Step 1: Read the detail page around the editor dispatch (:214, :248-256) to find where to insert the panel — it should render ABOVE the
KbGoldenAnswerEditor, only whendetail.source === "loop_proposal"AND the metadata narrows to a realSourceCorrectionMeta. -
Step 2: Add the import + conditional render:
import { readSourceCorrection } from "@/lib/api";
import { SourceCorrectionPanel } from "../SourceCorrectionPanel";
Where detail is available (the loaded ConfigAssetDetail), compute:
const sourceCorrection =
detail.source === "loop_proposal" ? readSourceCorrection(detail.metadata) : null;
Render {sourceCorrection && <SourceCorrectionPanel meta={sourceCorrection} />} immediately before the editor block (so an approver sees the wrong original reply above the proposed golden answer). Match the exact variable name the page uses for the loaded detail object (it may be detail, asset, or from a hook — read the file and use the real name).
-
Step 3: Lint + typecheck + build —
cd frontend && npm run lint && npx tsc --noEmit. If the project has a fast page-level build check, run it. Expected: clean, no new warnings. -
Step 4: Commit
git add frontend/src/app/ops/config-assets/[id]/page.tsx
git commit -m "feat(frontend): show source-correction panel on loop_proposal detail (P4.2b)"
Task 4: List-page source badge + pending-proposals filter
Files:
-
Modify:
frontend/src/app/ops/config-assets/page.tsx -
Step 1: Read the
AssetTableSource column (:114,<td>{a.source}</td>) and the existing Status badge (:103-113) to match badge styling. -
Step 2: Render
sourceas a badge — replace the bare{a.source}text with a small badge, visually distinguishingloop_proposal(e.g. amber) frommanual/imported_repo(neutral). Reuse the same badge primitive the Status column uses (do NOT introduce a new badge component if one exists). Keep it a<td>. -
Step 3: Add a "Pending proposals" client-side toggle near
TYPE_FILTERS(:38-45). A checkbox/toggle whose state (showProposalsOnly) filters the rendered rows:
const visibleAssets = showProposalsOnly
? assets.filter((a) => a.source === "loop_proposal" && a.draftVersionId)
: assets;
Render visibleAssets in the table instead of assets. Label it "Pending proposals (loop)". This is pure client-side (DP-2) — no new API call, no new query param. Match the existing select/filter control styling.
-
Step 4: Lint + typecheck —
cd frontend && npm run lint && npx tsc --noEmit. Zero new warnings. -
Step 5: Commit
git add frontend/src/app/ops/config-assets/page.tsx
git commit -m "feat(frontend): source badge + pending-proposals filter on config-assets list (P4.2b)"
Task 5: Fix the stale publish-confirm copy
Files:
-
Modify:
frontend/src/app/ops/config-assets/VersionHistory.tsx -
Step 1: Read the publish confirm text (:71-74) — it says the eval gate "lands in Phase 4". P4.1 shipped; the gate is live for org_instance publishes.
-
Step 2: Update the copy to reflect the real behaviour, e.g.: "Publishing runs the hallucination eval gate. If it passes, this version goes live; if it fails, it's blocked (an admin can override with a note)." Keep it in the existing confirm-dialog mechanism (do NOT switch to
window.confirm— that tripsregression-gate.sh'sconfirm(regex; reuse whatever dialog the component already uses). -
Step 3: Lint + typecheck —
cd frontend && npm run lint && npx tsc --noEmit. Also grep to be safe:grep -n "confirm(" frontend/src/app/ops/config-assets/VersionHistory.tsx— ensure you did not add a nativeconfirm(. -
Step 4: Commit
git add frontend/src/app/ops/config-assets/VersionHistory.tsx
git commit -m "fix(frontend): update publish-gate confirm copy — P4.1 eval gate is live (P4.2b)"
Self-review checklist (run after implementation)
- Spec coverage: approver can (1) find loop_proposal drafts (badge + filter, Task 4), (2) see the wrong original reply vs the proposed golden (panel, Tasks 2-3), (3) approve = publish via the existing button (no change needed — reused), (4) see accurate gate copy (Task 5).
- Zero backend changes — grep the diff:
git diff <base>..HEAD -- api/must be EMPTY. - No new route — everything is on
/ops/config-assets(DP-1). - CI gates:
cd frontend && npm run lintpasses within the existing--max-warningsbudget; no nativeconfirm(added (git diff | grep -n 'confirm('shows only existing dialog helpers). - Type safety:
readSourceCorrectionnever throws on a hand-made / non-loop_proposal asset (returns null → panel not rendered). - Auth unchanged: publish still gated by
RequireRole+ backendassertOrgAccess; AM only sees/publishes their own orgs (no change).
Verification (verify skill, after all tasks)
cd frontend && npm run lint && npx tsc --noEmit→ clean.- If the frontend has a component/e2e test harness for
/ops/config-assets, run the relevant subset; otherwise a manual/dev-server smoke: the list shows a loop_proposal draft with an amber source badge, the "Pending proposals" toggle filters to it, its detail page shows the source-correction panel above the golden editor, and the Publish button drives it intopublishing(P4.1 gate). Note (memoryverification-env-gotchas): dev frontend is on Vercel (Preview dev), not Railway; PostHog dropsHeadless/webdrivertraffic. A localnext devsmoke against the local API is the cleaner check if the local stack is up. git diff <base>..HEAD -- api/→ empty (proves zero backend change).