PAP-2822 Walk back visible disposition badges per board feedback

The board accepted the canonical executionDisposition payload + classifier
but found the visible badges in IssueRow / IssueProperties / Storybook too
noisy on production surfaces. UI-only walkback: keep the data layer, drop
every visible badge.

- Delete IssueDispositionBadge component and its test.
- Drop the badge import, suppression rule, and trailing-slot references in
  IssueRow (desktop + mobile). Existing waiting pill, status-icon ring, and
  blockerAttention copy remain unchanged.
- Drop the badge IIFE and related imports from IssueProperties so the
  Status row only renders the StatusIcon again.
- Prune the badge cases from IssueRow.test (Live, Stalled, dispatchable,
  Resuming, explicit-waiting suppression) so the file still type-checks.
- Storybook: remove "Canonical disposition badges" and "IssueRow with
  disposition badge inline" sections plus DispositionRowSurface and
  dispositionRowFixtures. The rest of Status Language is unchanged.

Kept: executionDisposition on issue payloads, the resolver and its test,
and the PAP-2818 contract doc — future features can consume the field
without re-running the classifier work.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-29 13:25:59 -05:00
1 parent 7690c21148
commit 4c5eaeadd5
6 files changed
+2 -719

No files matched your search

@@ -1,170 +0,0 @@
// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import type { IssueExecutionDisposition } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
IssueDispositionBadge,
dispositionCategory,
shouldShowDispositionBadge,
} from "./IssueDispositionBadge";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
describe("IssueDispositionBadge", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
function render(disposition: IssueExecutionDisposition | null) {
const root = createRoot(container);
act(() => {
root.render(<IssueDispositionBadge disposition={disposition} />);
});
return root;
}
it("renders nothing for null disposition", () => {
const root = render(null);
expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull();
act(() => root.unmount());
});
it("renders nothing for terminal and resting kinds by default", () => {
let root = render({ kind: "terminal" });
expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull();
act(() => root.unmount());
root = render({ kind: "resting" });
expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull();
act(() => root.unmount());
});
it("renders nothing for dispatchable (transient bookkeeping signal)", () => {
const root = render({ kind: "dispatchable", wakeTarget: "agent-1" });
expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull();
act(() => root.unmount());
});
it("renders Live category for live disposition", () => {
const root = render({ kind: "live", path: "active_run" });
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-kind")).toBe("live");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("live");
expect(badge?.textContent).toContain("Live");
expect(badge?.getAttribute("title")).toContain("Active run");
expect(badge?.getAttribute("aria-label")).toContain("Active run");
act(() => root.unmount());
});
it("distinguishes blocked_chain from generic waiting", () => {
const root = render({ kind: "waiting", path: "blocker_chain" });
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("blocked_chain");
expect(badge?.textContent).toContain("Blocked");
act(() => root.unmount());
});
it("renders agent_continuable as Resuming with attempt counter on the visible label", () => {
const root = render({ kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 });
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("resuming");
expect(badge?.textContent).toContain("Resuming · 1/2");
// muted indigo treatment, not rose
expect(badge?.className).toContain("indigo");
expect(badge?.className).not.toContain("rose");
act(() => root.unmount());
});
it("differentiates human_escalation_required by owner in the visible label", () => {
const owners = [
{ owner: "board", label: "Needs board" },
{ owner: "manager", label: "Needs manager" },
{ owner: "recovery_owner", label: "Needs owner" },
{ owner: "external", label: "Needs external" },
] as const;
for (const { owner, label } of owners) {
const root = render({ kind: "human_escalation_required", owner });
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("needs_attention");
expect(badge?.textContent).toContain(label);
// shared rose family for the escalation category
expect(badge?.className).toContain("rose");
act(() => root.unmount());
}
});
it("renders invalid (Stalled) with solid rose-600 fill so it stands out", () => {
const root = render({
kind: "invalid",
reason: "in_review_without_action_path",
suggestedCorrection: "fix it",
});
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("invalid");
expect(badge?.textContent).toContain("Stalled");
expect(badge?.getAttribute("title")).toContain("Review without action path");
expect(badge?.className).toContain("bg-rose-600");
expect(badge?.className).toContain("text-white");
act(() => root.unmount());
});
it("dispositionCategory routes recovery and continuable to distinct categories", () => {
expect(dispositionCategory({ kind: "recoverable_by_control_plane", recovery: "dispatch" })).toBe(
"recovery",
);
expect(
dispositionCategory({ kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 }),
).toBe("resuming");
});
it("dispositionCategory returns null for dispatchable", () => {
expect(dispositionCategory({ kind: "dispatchable", wakeTarget: "agent-1" })).toBeNull();
});
describe("shouldShowDispositionBadge", () => {
it("returns false for null, terminal, resting, and dispatchable", () => {
expect(shouldShowDispositionBadge(null)).toBe(false);
expect(shouldShowDispositionBadge({ kind: "terminal" })).toBe(false);
expect(shouldShowDispositionBadge({ kind: "resting" })).toBe(false);
expect(shouldShowDispositionBadge({ kind: "dispatchable", wakeTarget: "a" })).toBe(false);
});
it("suppresses generic waiting when the explicit waiting pill is shown", () => {
const waiting: IssueExecutionDisposition = { kind: "waiting", path: "interaction" };
expect(shouldShowDispositionBadge(waiting, { isExplicitWaiting: true })).toBe(false);
expect(shouldShowDispositionBadge(waiting, { isExplicitWaiting: false })).toBe(true);
});
it("suppresses recovery when the IssueBlockedNotice already shows recovery_needed copy", () => {
const recovery: IssueExecutionDisposition = {
kind: "recoverable_by_control_plane",
recovery: "continuation",
};
expect(
shouldShowDispositionBadge(recovery, { blockerAttentionState: "recovery_needed" }),
).toBe(false);
expect(shouldShowDispositionBadge(recovery, { blockerAttentionState: "covered" })).toBe(true);
});
it("does not suppress resuming when blockerAttention is recovery_needed", () => {
const resuming: IssueExecutionDisposition = {
kind: "agent_continuable",
continuationAttempt: 1,
maxAttempts: 2,
};
expect(
shouldShowDispositionBadge(resuming, { blockerAttentionState: "recovery_needed" }),
).toBe(true);
});
});
});
-256
View File
@@ -1,256 +0,0 @@
import type {
IssueBlockerAttention,
IssueExecutionDisposition,
IssueExecutionHumanEscalationOwner,
IssueExecutionLivePath,
IssueExecutionRecoveryKind,
IssueExecutionWaitingPath,
} from "@paperclipai/shared";
import {
AlertOctagon,
AlertTriangle,
CircleDashed,
Hourglass,
Lock,
Play,
RotateCw,
Wrench,
} from "lucide-react";
import { cn } from "../lib/utils";
export type IssueDispositionCategory =
| "live"
| "waiting"
| "blocked_chain"
| "resuming"
| "recovery"
| "needs_attention"
| "invalid"
| "terminal"
| "resting";
const CATEGORY_LABEL: Record<IssueDispositionCategory, string> = {
live: "Live",
waiting: "Waiting",
blocked_chain: "Blocked",
resuming: "Resuming",
recovery: "Recovery",
needs_attention: "Needs attention",
invalid: "Stalled",
terminal: "Done",
resting: "Resting",
};
const LIVE_PATH_LABEL: Record<IssueExecutionLivePath, string> = {
active_run: "Active run",
queued_wake: "Queued wake",
scheduled_retry: "Scheduled retry",
deferred_execution: "Deferred execution",
};
const WAITING_PATH_LABEL: Record<IssueExecutionWaitingPath, string> = {
participant: "Review participant",
interaction: "Interaction response",
approval: "Board approval",
human_owner: "Human owner",
blocker_chain: "Blocker chain",
pause_hold: "Tree paused",
review_artifact: "Recovery work",
external_owner_action: "External owner",
};
const RECOVERY_LABEL: Record<IssueExecutionRecoveryKind, string> = {
dispatch: "Awaiting dispatch repair",
continuation: "Awaiting continuation",
repair_wait: "Repairing wait state",
};
const ESCALATION_VISIBLE_LABEL: Record<IssueExecutionHumanEscalationOwner, string> = {
board: "Needs board",
manager: "Needs manager",
recovery_owner: "Needs owner",
external: "Needs external",
};
const ESCALATION_DETAIL_LABEL: Record<IssueExecutionHumanEscalationOwner, string> = {
board: "Board must act",
manager: "Manager must act",
recovery_owner: "Recovery owner must act",
external: "External owner must act",
};
export function dispositionCategory(
disposition: IssueExecutionDisposition | null | undefined,
): IssueDispositionCategory | null {
if (!disposition) return null;
switch (disposition.kind) {
case "terminal":
return "terminal";
case "resting":
return "resting";
case "live":
return "live";
case "dispatchable":
// Dispatchable is the system's "ready to wake" state — it's a transient bookkeeping signal,
// not something the board needs to scan for. We surface no badge for it.
return null;
case "waiting":
return disposition.path === "blocker_chain" ? "blocked_chain" : "waiting";
case "agent_continuable":
return "resuming";
case "recoverable_by_control_plane":
return "recovery";
case "human_escalation_required":
return "needs_attention";
case "invalid":
return "invalid";
default:
return null;
}
}
export function dispositionDetailLabel(
disposition: IssueExecutionDisposition | null | undefined,
): string | null {
if (!disposition) return null;
switch (disposition.kind) {
case "live":
return LIVE_PATH_LABEL[disposition.path];
case "waiting":
return WAITING_PATH_LABEL[disposition.path];
case "recoverable_by_control_plane":
return RECOVERY_LABEL[disposition.recovery];
case "agent_continuable":
return `Attempt ${disposition.continuationAttempt} of ${disposition.maxAttempts}`;
case "human_escalation_required":
return ESCALATION_DETAIL_LABEL[disposition.owner];
case "invalid":
return invalidReasonLabel(disposition.reason);
default:
return null;
}
}
export function dispositionVisibleLabel(
disposition: IssueExecutionDisposition | null | undefined,
category: IssueDispositionCategory,
): string {
if (disposition?.kind === "agent_continuable") {
return `Resuming · ${disposition.continuationAttempt}/${disposition.maxAttempts}`;
}
if (disposition?.kind === "human_escalation_required") {
return ESCALATION_VISIBLE_LABEL[disposition.owner];
}
return CATEGORY_LABEL[category];
}
function invalidReasonLabel(reason: string): string {
switch (reason) {
case "in_review_without_action_path":
return "Review without action path";
case "invalid_review_participant":
return "Invalid review participant";
case "blocked_by_invalid_issue":
return "Blocked by invalid issue";
case "blocked_by_cancelled_issue":
return "Blocked by cancelled issue";
case "blocked_by_unassigned_issue":
return "Blocked by unassigned issue";
case "blocked_by_resting_issue":
return "Blocked by resting issue";
case "blocked_without_action_path":
return "Blocked without action path";
case "dual_assignee":
return "Dual assignee";
default:
return reason.replace(/_/g, " ");
}
}
const CATEGORY_PILL: Record<IssueDispositionCategory, string> = {
live: "border-emerald-300/70 bg-emerald-50 text-emerald-800 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200",
waiting: "border-sky-300/70 bg-sky-50 text-sky-800 dark:border-sky-500/40 dark:bg-sky-500/10 dark:text-sky-200",
blocked_chain: "border-amber-300/70 bg-amber-50 text-amber-800 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-200",
// Resuming is benign self-recovery — keep it muted (indigo) so it does not compete with sky/Waiting
// or land in the rose alarm family alongside Recovery / Needs attention / Stalled.
resuming: "border-indigo-300/70 bg-indigo-50 text-indigo-800 dark:border-indigo-500/40 dark:bg-indigo-500/10 dark:text-indigo-200",
recovery: "border-rose-300/70 bg-rose-50 text-rose-800 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-200",
needs_attention: "border-rose-300/70 bg-rose-50 text-rose-800 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-200",
// Stalled is the canary. Solid fill makes it the visual outlier in the rose family so a board
// scanning the inbox spots it without reading the label.
invalid: "border-rose-600 bg-rose-600 text-white dark:border-rose-500 dark:bg-rose-600 dark:text-white",
terminal: "border-border bg-muted text-muted-foreground",
resting: "border-border bg-muted text-muted-foreground",
};
const CATEGORY_ICON: Record<IssueDispositionCategory, React.ComponentType<{ className?: string }>> = {
live: Play,
waiting: Hourglass,
blocked_chain: Lock,
resuming: RotateCw,
recovery: Wrench,
needs_attention: AlertTriangle,
invalid: AlertOctagon,
terminal: CircleDashed,
resting: CircleDashed,
};
export interface ShouldShowDispositionBadgeOptions {
isExplicitWaiting?: boolean;
blockerAttentionState?: IssueBlockerAttention["state"] | null;
}
export function shouldShowDispositionBadge(
disposition: IssueExecutionDisposition | null | undefined,
options: ShouldShowDispositionBadgeOptions = {},
): boolean {
const category = dispositionCategory(disposition);
if (!category) return false;
if (category === "terminal" || category === "resting") return false;
if (category === "waiting" && options.isExplicitWaiting) return false;
if (category === "recovery" && options.blockerAttentionState === "recovery_needed") return false;
return true;
}
export interface IssueDispositionBadgeProps {
disposition: IssueExecutionDisposition | null | undefined;
className?: string;
hideLabel?: boolean;
hideForResting?: boolean;
hideForTerminal?: boolean;
}
export function IssueDispositionBadge({
disposition,
className,
hideLabel = false,
hideForResting = true,
hideForTerminal = true,
}: IssueDispositionBadgeProps) {
const category = dispositionCategory(disposition);
if (!category) return null;
if (hideForResting && category === "resting") return null;
if (hideForTerminal && category === "terminal") return null;
const detail = dispositionDetailLabel(disposition);
const label = dispositionVisibleLabel(disposition, category);
const Icon = CATEGORY_ICON[category];
const tooltip = detail ? `${label} · ${detail}` : label;
return (
<span
className={cn(
"inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium",
CATEGORY_PILL[category],
className,
)}
title={tooltip}
aria-label={tooltip}
data-execution-disposition-kind={disposition?.kind}
data-execution-disposition-category={category}
>
<Icon className="h-3 w-3" aria-hidden />
{hideLabel ? null : <span>{label}</span>}
</span>
);
}
-26
View File
@@ -27,7 +27,6 @@ import { formatAssigneeUserLabel } from "../lib/assignees";
import { buildExecutionPolicy, stageParticipantValues } from "../lib/issue-execution-policy";
import { StatusIcon } from "./StatusIcon";
import { PriorityIcon } from "./PriorityIcon";
import { IssueDispositionBadge, dispositionDetailLabel, shouldShowDispositionBadge } from "./IssueDispositionBadge";
import { Identity } from "./Identity";
import { IssueReferencePill } from "./IssueReferencePill";
import { formatDate, cn, projectUrl } from "../lib/utils";
@@ -1049,31 +1048,6 @@ export function IssueProperties({
onChange={(status) => onUpdate({ status })}
showLabel
/>
{(() => {
// Mirror IssueRow's suppression: if the chat-thread IssueBlockedNotice already
// conveys the same signal (explicit waiting or recovery_needed), skip the badge so
// the detail page doesn't double-render the same state.
const isExplicitWaiting =
issue.blockerAttention?.state === "covered"
&& issue.blockerAttention?.reason === "explicit_waiting";
if (
!shouldShowDispositionBadge(issue.executionDisposition, {
isExplicitWaiting,
blockerAttentionState: issue.blockerAttention?.state ?? null,
})
) {
return null;
}
const detail = dispositionDetailLabel(issue.executionDisposition);
return (
<span className="inline-flex items-center gap-1.5">
<IssueDispositionBadge disposition={issue.executionDisposition ?? null} />
{detail ? (
<span className="text-xs text-muted-foreground">{detail}</span>
) : null}
</span>
);
})()}
</PropertyRow>
<PropertyRow label="Priority">
-81
View File
@@ -261,87 +261,6 @@ describe("IssueRow", () => {
});
});
it("renders the disposition badge for live and invalid dispositions", () => {
const root = createRoot(container);
const liveIssue = createIssue({
executionDisposition: { kind: "live", path: "active_run" },
});
act(() => {
root.render(<IssueRow issue={liveIssue} />);
});
let badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("live");
act(() => root.unmount());
const root2 = createRoot(container);
const stalledIssue = createIssue({
status: "in_review",
executionDisposition: {
kind: "invalid",
reason: "in_review_without_action_path",
suggestedCorrection: "fix",
},
});
act(() => {
root2.render(<IssueRow issue={stalledIssue} />);
});
badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("invalid");
expect(badge?.textContent).toContain("Stalled");
act(() => root2.unmount());
});
it("does not render a disposition badge for the dispatchable bookkeeping signal", () => {
const root = createRoot(container);
const issue = createIssue({
executionDisposition: { kind: "dispatchable", wakeTarget: "agent-1" },
});
act(() => {
root.render(<IssueRow issue={issue} />);
});
expect(container.querySelector("[data-execution-disposition-kind]")).toBeNull();
act(() => root.unmount());
});
it("renders the resuming badge with the attempt counter on the visible label", () => {
const root = createRoot(container);
const issue = createIssue({
executionDisposition: { kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 },
});
act(() => {
root.render(<IssueRow issue={issue} />);
});
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge?.getAttribute("data-execution-disposition-category")).toBe("resuming");
expect(badge?.textContent).toContain("Resuming · 1/2");
act(() => root.unmount());
});
it("hides the disposition badge when the explicit waiting pill already conveys the same signal", () => {
const root = createRoot(container);
const issue = createIssue({
blockerAttention: {
state: "covered",
reason: "explicit_waiting",
unresolvedBlockerCount: 0,
coveredBlockerCount: 0,
stalledBlockerCount: 0,
attentionBlockerCount: 0,
sampleBlockerIdentifier: null,
sampleStalledBlockerIdentifier: null,
nextActionOwner: { type: "user", agentId: null, userId: null },
nextActionHint: "needs_human_review",
},
executionDisposition: { kind: "waiting", path: "interaction" },
});
act(() => {
root.render(<IssueRow issue={issue} />);
});
const badge = container.querySelector("[data-execution-disposition-kind]");
expect(badge).toBeNull();
act(() => root.unmount());
});
it("renders without error when titleSuffix is omitted", () => {
const root = createRoot(container);
+2 -15
View File
@@ -11,7 +11,6 @@ import { formatAssigneeUserLabel } from "../lib/assignees";
import { cn } from "../lib/utils";
import { StatusIcon } from "./StatusIcon";
import { productivityReviewTriggerLabel } from "./ProductivityReviewBadge";
import { IssueDispositionBadge, shouldShowDispositionBadge } from "./IssueDispositionBadge";
type UnreadState = "hidden" | "visible" | "fading";
@@ -131,16 +130,6 @@ export function IssueRow({
Waiting
</span>
) : null;
// Skip the disposition badge when the existing waiting/recovery affordance already conveys the
// same signal (explicit waiting pill or blockerAttention recovery_needed copy), so we don't
// double up.
const showDispositionBadge = shouldShowDispositionBadge(issue.executionDisposition, {
isExplicitWaiting,
blockerAttentionState: issue.blockerAttention?.state ?? null,
});
const dispositionBadge = showDispositionBadge ? (
<IssueDispositionBadge disposition={issue.executionDisposition ?? null} />
) : null;
const productivityReviewIndicator = productivityReview ? (
<span
className={cn(
@@ -224,20 +213,18 @@ export function IssueRow({
) : null}
</span>
</span>
{(desktopTrailing || trailingMeta || desktopWaitingPill || dispositionBadge) ? (
{(desktopTrailing || trailingMeta || desktopWaitingPill) ? (
<span className="ml-auto hidden shrink-0 items-center gap-2 sm:order-3 sm:flex sm:gap-3">
{desktopWaitingPill}
{dispositionBadge}
{desktopTrailing}
{trailingMeta ? (
<span className="text-xs text-muted-foreground">{trailingMeta}</span>
) : null}
</span>
) : null}
{mobileWaitingPill || dispositionBadge ? (
{mobileWaitingPill ? (
<span className="ml-auto inline-flex shrink-0 items-center gap-1.5 sm:hidden">
{mobileWaitingPill}
{dispositionBadge}
</span>
) : null}
{showUnreadSlot ? (
@@ -15,7 +15,6 @@ import { IssueRow } from "@/components/IssueRow";
import { MetricCard } from "@/components/MetricCard";
import { PriorityIcon } from "@/components/PriorityIcon";
import { ProductivityReviewBadge } from "@/components/ProductivityReviewBadge";
import { IssueDispositionBadge } from "@/components/IssueDispositionBadge";
import { QuotaBar } from "@/components/QuotaBar";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusIcon } from "@/components/StatusIcon";
@@ -291,106 +290,6 @@ const coveredBlockedIssue = createIssue({
updatedAt: new Date("2026-04-24T13:40:00.000Z"),
});
const dispositionRowFixtures = [
{
caption: "Live · active run — emerald",
issue: createIssue({
id: "issue-live-row",
identifier: "PAP-2820",
issueNumber: 2820,
title: "Phase 4 wiring · active execution",
status: "in_progress",
priority: "medium",
executionDisposition: { kind: "live", path: "active_run" },
lastActivityAt: new Date("2026-04-29T13:40:00.000Z"),
updatedAt: new Date("2026-04-29T13:40:00.000Z"),
}),
},
{
caption: "Resuming · 1/2 — muted indigo, attempt counter inline",
issue: createIssue({
id: "issue-resuming-row",
identifier: "PAP-2821",
issueNumber: 2821,
title: "Continuation queued after transient failure",
status: "in_progress",
priority: "medium",
executionDisposition: { kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 },
lastActivityAt: new Date("2026-04-29T13:42:00.000Z"),
updatedAt: new Date("2026-04-29T13:42:00.000Z"),
}),
},
{
caption: "Needs board — rose, owner-specific label",
issue: createIssue({
id: "issue-needs-board-row",
identifier: "PAP-2823",
issueNumber: 2823,
title: "Spend approval blocked on board response",
status: "in_review",
priority: "high",
executionDisposition: { kind: "human_escalation_required", owner: "board" },
lastActivityAt: new Date("2026-04-29T13:44:00.000Z"),
updatedAt: new Date("2026-04-29T13:44:00.000Z"),
}),
},
{
caption: "Needs manager — rose, owner-specific label",
issue: createIssue({
id: "issue-needs-manager-row",
identifier: "PAP-2824",
issueNumber: 2824,
title: "Specialty handoff blocked — manager must act",
status: "in_review",
priority: "high",
executionDisposition: { kind: "human_escalation_required", owner: "manager" },
lastActivityAt: new Date("2026-04-29T13:45:00.000Z"),
updatedAt: new Date("2026-04-29T13:45:00.000Z"),
}),
},
{
caption: "Stalled — solid rose-600 fill, the canary outlier",
issue: createIssue({
id: "issue-stalled-row",
identifier: "PAP-2825",
issueNumber: 2825,
title: "Review without action path — operator must intervene",
status: "in_review",
priority: "high",
executionDisposition: {
kind: "invalid",
reason: "in_review_without_action_path",
suggestedCorrection: "Add participant or move out of review",
},
lastActivityAt: new Date("2026-04-29T13:50:00.000Z"),
updatedAt: new Date("2026-04-29T13:50:00.000Z"),
}),
},
];
function DispositionRowSurface({ mode, size }: { mode: "light" | "dark"; size: "desktop" | "mobile" }) {
const isDark = mode === "dark";
const isMobile = size === "mobile";
return (
<div className={isDark ? "dark" : undefined}>
<div className="rounded-lg border border-border bg-background text-foreground">
<div className="border-b border-border px-3 py-2 text-xs font-medium text-muted-foreground">
{size} · {mode}
</div>
<div className={isMobile ? "max-w-[358px]" : "min-w-[620px]"}>
{dispositionRowFixtures.map((fixture) => (
<IssueRow
key={fixture.issue.id}
issue={fixture.issue}
mobileMeta={<StatusBadge status={fixture.issue.status} />}
/>
))}
</div>
</div>
</div>
);
}
function summaryBlocker(
partial: Partial<IssueRelationIssueSummary> & Pick<IssueRelationIssueSummary, "id" | "title" | "status">,
): IssueRelationIssueSummary {
@@ -958,76 +857,6 @@ function StatusLanguage() {
</p>
</Section>
<Section eyebrow="Execution disposition" title="Canonical disposition badges">
<p className="mb-4 max-w-3xl text-sm leading-6 text-muted-foreground">
The badge derives every category from the API's <code>executionDisposition</code> field —
the same classifier the recovery and transition guard services use. The board reads this
instead of inferring health from status alone.
</p>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{[
{ label: "Live · active run", disposition: { kind: "live", path: "active_run" } as const },
{ label: "Live · queued wake", disposition: { kind: "live", path: "queued_wake" } as const },
{ label: "Waiting · interaction", disposition: { kind: "waiting", path: "interaction" } as const },
{ label: "Waiting · approval", disposition: { kind: "waiting", path: "approval" } as const },
{ label: "Waiting · participant", disposition: { kind: "waiting", path: "participant" } as const },
{ label: "Blocked · chain", disposition: { kind: "waiting", path: "blocker_chain" } as const },
{ label: "Resuming · 1/2", disposition: { kind: "agent_continuable", continuationAttempt: 1, maxAttempts: 2 } as const },
{ label: "Resuming · 2/2", disposition: { kind: "agent_continuable", continuationAttempt: 2, maxAttempts: 2 } as const },
{ label: "Recovery · continuation", disposition: { kind: "recoverable_by_control_plane", recovery: "continuation" } as const },
{ label: "Needs board", disposition: { kind: "human_escalation_required", owner: "board" } as const },
{ label: "Needs manager", disposition: { kind: "human_escalation_required", owner: "manager" } as const },
{ label: "Needs owner", disposition: { kind: "human_escalation_required", owner: "recovery_owner" } as const },
{ label: "Needs external", disposition: { kind: "human_escalation_required", owner: "external" } as const },
{
label: "Stalled review",
disposition: {
kind: "invalid",
reason: "in_review_without_action_path",
suggestedCorrection: "Add participant or move out of review",
} as const,
},
{
label: "Stalled chain",
disposition: {
kind: "invalid",
reason: "blocked_without_action_path",
suggestedCorrection: "Resolve or remove the orphan blocker",
} as const,
},
].map((item) => (
<div
key={item.label}
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-background/70 p-4"
>
<span className="text-sm">{item.label}</span>
<IssueDispositionBadge disposition={item.disposition} hideForResting={false} hideForTerminal={false} />
</div>
))}
</div>
<p className="mt-3 max-w-3xl text-xs text-muted-foreground">
Stalled uses a solid rose-600 fill so it stands out against the rose family at scan distance.
Resuming sits in muted indigo (clearly off the rose alarm family) and surfaces the
continuation counter on the visible badge. Each escalation owner gets its own visible
label so the board can see who is blocking work without hovering. Dispatchable is
intentionally invisible — it's a transient bookkeeping signal, not something to scan for.
</p>
</Section>
<Section eyebrow="Execution disposition" title="IssueRow with disposition badge inline">
<div className="grid gap-4 xl:grid-cols-2">
<DispositionRowSurface mode="light" size="desktop" />
<DispositionRowSurface mode="dark" size="desktop" />
<DispositionRowSurface mode="light" size="mobile" />
<DispositionRowSurface mode="dark" size="mobile" />
</div>
<p className="mt-3 max-w-3xl text-xs text-muted-foreground">
Verifies the badge in production layout. Stalled is visually distinct from any rose-family
row at full-page scan, Resuming reads as benign muted indigo with the attempt counter,
and Needs board / Needs manager are independently legible without hover.
</p>
</Section>
<Section eyebrow="Productivity review" title="Yellow accountability state on source issues">
<ProductivityReviewMatrix />
</Section>