mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The issue graph and liveness recovery system decide whether assigned work is executable or parked > - Assigned issues created without an explicit status could silently land in backlog, making parents look blocked with no productive wake path > - The server, shared validators, recovery analysis, and UI all need to agree on that execution semantic > - This pull request makes assigned issue creation default to `todo`, flags assigned backlog blockers, and surfaces the state in the board > - The benefit is that parked assigned work becomes intentional and visible instead of creating silent liveness stalls ## What Changed - Adds contract tests for assigned issue creation defaults. - Defaults assigned issue creation to `todo` when status is omitted while preserving explicit `backlog` parking. - Exposes `resolveCreateIssueStatusDefault` through shared validators. - Teaches liveness/blocker attention paths to distinguish assigned backlog blockers. - Adds UI notices, row/header badges, and issue detail safeguards for assigned backlog blockers. - Adds Storybook fixtures and execution-semantics documentation for the assigned-backlog behavior. ## Verification - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/validators/issue.test.ts server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts server/src/__tests__/issue-blocker-attention.test.ts server/src/__tests__/issue-liveness.test.ts server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts ui/src/components/IssueAssignedBacklogNotice.test.tsx ui/src/components/IssueRow.test.tsx` — 50 passed, 23 skipped. - Skipped tests were embedded Postgres suites on this host with the repo skip message: `Postgres init script exited with code null. Please check the logs for extra info. The data directory might already exist.` - Pairwise merge check against the issue-controls PR branch completed without conflicts via `git merge --no-commit --no-ff` in a temporary worktree. - Screenshots for assigned-backlog UI states: [light](docs/pr-screenshots/pr-5428/assigned-backlog-light.png), [dark](docs/pr-screenshots/pr-5428/assigned-backlog-dark.png). - Follow-up checks: `pnpm --filter /ui typecheck`; `pnpm --filter /mcp-server build`; `pnpm --filter /mcp-server test`; `pnpm exec vitest run packages/shared/src/validators/issue.test.ts`; focused UI component tests. - Remote PR checks on head `6300b3c`: policy, verify, serialized server shards 1/4-4/4, Canary Dry Run, e2e, Greptile Review, and Snyk all passed. ## Risks - Medium: changes status defaulting for assigned issue creation when the caller omits status. Explicit `backlog` remains supported, and server/shared tests cover both paths. - Medium: liveness classification changes can affect blocker attention labels; focused service and UI tests cover the new assigned-backlog state. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent, GPT-5 model family (`gpt-5`), tool-enabled Paperclip heartbeat environment. Context window and internal reasoning mode are not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
245 lines
9.3 KiB
TypeScript
245 lines
9.3 KiB
TypeScript
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import type { ReactNode } from "react";
|
|
import { CircleDot, Flag, MoreHorizontal, Paperclip } from "lucide-react";
|
|
import type { IssueRelationIssueSummary } from "@paperclipai/shared";
|
|
import { IssueAssignedBacklogNotice } from "@/components/IssueAssignedBacklogNotice";
|
|
import { IssueBlockedNotice } from "@/components/IssueBlockedNotice";
|
|
import { IssueRow } from "@/components/IssueRow";
|
|
import { storybookAgents, createIssue } from "../fixtures/paperclipData";
|
|
|
|
const codexAgent = storybookAgents.find((agent) => agent.id === "agent-codex") ?? storybookAgents[0]!;
|
|
const qaAgent = storybookAgents.find((agent) => agent.id === "agent-qa") ?? storybookAgents[0]!;
|
|
|
|
function StoryFrame({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<main className="min-h-screen bg-background p-4 text-foreground sm:p-8">
|
|
<div className="mx-auto max-w-5xl space-y-5">
|
|
<div>
|
|
<div className="text-xs font-medium uppercase text-muted-foreground">Assigned-backlog UI safeguards</div>
|
|
<h1 className="mt-1 text-2xl font-semibold">{title}</h1>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function CreationFormPanel() {
|
|
return (
|
|
<div className="rounded-lg border border-border bg-card p-4">
|
|
<div className="mb-3 text-sm font-medium text-muted-foreground">A. Issue creation chip bar with intent note</div>
|
|
|
|
<div className="space-y-3 rounded-md border border-border/60 bg-background p-3">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<span className="w-6 shrink-0 text-center">For</span>
|
|
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
|
|
ClaudeCoder
|
|
</span>
|
|
<span>in</span>
|
|
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
|
|
Paperclip App
|
|
</span>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<div className="text-sm font-semibold">Fix flaky deploy step on the worker pipeline</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Investigate the intermittent timeout the worker pipeline hit during the last release rehearsal.
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 border-t border-border pt-3 flex-wrap">
|
|
<span className="inline-flex items-center gap-1.5 rounded-md border border-border bg-amber-100/40 px-2 py-1 text-xs dark:bg-amber-500/10">
|
|
<CircleDot className="h-3 w-3 text-purple-500" />
|
|
Backlog
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
|
|
<CircleDot className="h-3 w-3 text-amber-500" />
|
|
High
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground">
|
|
<Paperclip className="h-3 w-3" />
|
|
Upload
|
|
</span>
|
|
<span className="inline-flex items-center justify-center rounded-md border border-border p-1 text-xs text-muted-foreground">
|
|
<MoreHorizontal className="h-3 w-3" />
|
|
</span>
|
|
</div>
|
|
<div className="flex items-start gap-2 rounded-md border border-amber-300/70 bg-amber-50/90 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100">
|
|
<Flag className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-300" />
|
|
<span className="leading-snug">
|
|
Assigning implies executable intent — leave status as <span className="font-medium">Backlog</span> only to deliberately park this. The assignee will not be woken until status moves to <span className="font-medium">Todo</span> or <span className="font-medium">In Progress</span>.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 rounded-md border border-border bg-popover p-1 text-xs">
|
|
<div className="px-2 pb-1 text-[10px] uppercase text-muted-foreground">Status options</div>
|
|
<div className="flex w-full items-start gap-2 rounded px-2 py-1.5 hover:bg-accent/50">
|
|
<CircleDot className="h-3 w-3 mt-0.5 shrink-0 text-purple-500" />
|
|
<span className="flex flex-col text-left leading-tight">
|
|
<span>Backlog</span>
|
|
<span className="text-[10px] text-muted-foreground">Parked — assignee will not be woken</span>
|
|
</span>
|
|
</div>
|
|
<div className="flex w-full items-start gap-2 rounded bg-accent px-2 py-1.5">
|
|
<CircleDot className="h-3 w-3 mt-0.5 shrink-0 text-blue-500" />
|
|
<span className="flex flex-col text-left leading-tight">
|
|
<span>Todo</span>
|
|
<span className="text-[10px] text-muted-foreground">Executable — assignee will be woken</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AssignedBacklogNoticePanel() {
|
|
return (
|
|
<div className="rounded-lg border border-border bg-card p-4">
|
|
<div className="mb-3 text-sm font-medium text-muted-foreground">B. Issue panel banner — parked with assignee</div>
|
|
<IssueAssignedBacklogNotice
|
|
issueStatus="backlog"
|
|
assigneeAgent={qaAgent}
|
|
assigneeUserId={null}
|
|
onResume={() => undefined}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BlockedByParkedWorkPanel() {
|
|
const parkedBlocker: IssueRelationIssueSummary = {
|
|
id: "blocker-parked",
|
|
identifier: "PAP-3683",
|
|
title: "Adapter restart fails after upgrade",
|
|
status: "backlog",
|
|
priority: "critical",
|
|
assigneeAgentId: codexAgent.id,
|
|
assigneeUserId: null,
|
|
};
|
|
return (
|
|
<div className="rounded-lg border border-border bg-card p-4">
|
|
<div className="mb-3 text-sm font-medium text-muted-foreground">C. Parent issue blocked by parked work</div>
|
|
<IssueBlockedNotice
|
|
issueStatus="blocked"
|
|
blockers={[parkedBlocker]}
|
|
blockerAttention={{
|
|
state: "needs_attention",
|
|
reason: "attention_required",
|
|
unresolvedBlockerCount: 1,
|
|
coveredBlockerCount: 0,
|
|
stalledBlockerCount: 0,
|
|
attentionBlockerCount: 1,
|
|
sampleBlockerIdentifier: parkedBlocker.identifier,
|
|
sampleStalledBlockerIdentifier: null,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ListRowsPanel() {
|
|
return (
|
|
<div className="rounded-lg border border-border bg-card p-4">
|
|
<div className="mb-3 text-sm font-medium text-muted-foreground">D. Issue list row indicators</div>
|
|
<div className="rounded-md border border-border">
|
|
<IssueRow
|
|
issue={createIssue({
|
|
id: "issue-blocked-parent",
|
|
identifier: "PAP-3643",
|
|
issueNumber: 3643,
|
|
title: "Restart deploy run after fixed adapter",
|
|
status: "blocked",
|
|
priority: "high",
|
|
blockedBy: [
|
|
{
|
|
id: "blocker-parked-leaf",
|
|
identifier: "PAP-3683",
|
|
title: "Adapter restart fails after upgrade",
|
|
status: "backlog",
|
|
priority: "critical",
|
|
assigneeAgentId: codexAgent.id,
|
|
assigneeUserId: null,
|
|
},
|
|
],
|
|
blockerAttention: {
|
|
state: "needs_attention",
|
|
reason: "attention_required",
|
|
unresolvedBlockerCount: 1,
|
|
coveredBlockerCount: 0,
|
|
stalledBlockerCount: 0,
|
|
attentionBlockerCount: 1,
|
|
sampleBlockerIdentifier: "PAP-3683",
|
|
sampleStalledBlockerIdentifier: null,
|
|
},
|
|
})}
|
|
/>
|
|
<IssueRow
|
|
issue={createIssue({
|
|
id: "issue-healthy",
|
|
identifier: "PAP-3644",
|
|
issueNumber: 3644,
|
|
title: "Compute new deploy budget for next cycle",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
blockedBy: [],
|
|
})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AllStates() {
|
|
return (
|
|
<StoryFrame title="Assigned-backlog liveness UI">
|
|
<section className="grid gap-4 lg:grid-cols-[1fr_1fr]">
|
|
<CreationFormPanel />
|
|
<AssignedBacklogNoticePanel />
|
|
</section>
|
|
<section className="grid gap-4 lg:grid-cols-[1fr_1fr]">
|
|
<BlockedByParkedWorkPanel />
|
|
<ListRowsPanel />
|
|
</section>
|
|
</StoryFrame>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Paperclip/Assigned Backlog Safeguards",
|
|
component: AllStates,
|
|
parameters: { layout: "fullscreen" },
|
|
} satisfies Meta<typeof AllStates>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const Overview: Story = {};
|
|
export const CreationForm: Story = {
|
|
render: () => (
|
|
<StoryFrame title="Issue creation chip bar with intent note">
|
|
<CreationFormPanel />
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
export const AssignedBacklogBanner: Story = {
|
|
render: () => (
|
|
<StoryFrame title="Issue panel banner — parked with assignee">
|
|
<AssignedBacklogNoticePanel />
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
export const BlockedByParkedWork: Story = {
|
|
render: () => (
|
|
<StoryFrame title="Parent issue blocked by parked work">
|
|
<BlockedByParkedWorkPanel />
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
export const ListRows: Story = {
|
|
render: () => (
|
|
<StoryFrame title="Issue list row indicators">
|
|
<ListRowsPanel />
|
|
</StoryFrame>
|
|
),
|
|
};
|