mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The pipeline subsystem models repeatable work as items moving through stages, with agent automation, review gates, blockers, drift notices, and linked work. > - Operators need this to be usable as one coherent workflow surface, not just as backend primitives or disconnected route experiments. > - The branch now carries the pipeline data model, service/routes, CLI/tutorial path, aggregation feeds, operator UI, stage automation controls, liveness/retry handling, and follow-up polish that make the primitive reviewable end to end. > - This pull request is the single review target for that pipeline workflow primitive stack. > - The benefit is that reviewers can evaluate the full operator experience and server contract together against `master`. ## Linked Issues or Issue Description No public GitHub issue exists for this work. The underlying feature request is described inline. ### Problem or motivation Paperclip needs a first-class way to model multi-stage agent/company workflows where upstream items can spawn downstream work, request review, carry fields across pipelines, surface drift, retry automation, and show operators where work is blocked or active. Without a unified pipeline primitive, these workflows spread across ad hoc issues, routines, and comments, making the state hard to inspect or operate. ### Proposed solution Add the pipeline workflow primitive stack: database schema and migrations, shared validators/types, server services and REST routes, aggregation and liveness helpers, CLI/tutorial smoke support, and the React operator UI for pipeline lists, boards, item detail, review/learnings views, settings, stage automation, secrets, carry-over fields, and retry/recovery flows. ### Alternatives considered - Keep workflows as loosely linked issues and routines: rejected because operators need a single board/detail/settings surface for repeated workflow patterns. - Ship backend primitives first and defer UI: rejected for this branch because the operator experience is the main way to validate the primitive. - Add a narrower one-off content workflow: rejected because the same primitives are useful across future company processes. ## What Changed - Added and evolved pipeline schema, migrations, shared contracts, server services, REST routes, route tests, and CLI/tutorial smoke support. - Added pipeline aggregation, health/liveness, drift acknowledgment, blocker/carry-over, automation retry, stage automation environment, and permission recovery behavior. - Added the operator UI for pipeline index/board/item detail/settings/review/learnings flows, including stage secrets, automation controls, markdown/item descriptions, linked issue assets, liveness banners, and source automation metadata. - Refactored issue document frame rendering through the shared `DocumentFrameHeader` component to keep document controls consistent with the pipeline document surfaces. - Kept this PR as the single base-branch review target for the current pipeline branch. ## Verification Current branch refresh: - `pnpm vitest run server/src/__tests__/pipelines-service.test.ts` — 31 passed - `pnpm vitest run server/src/__tests__/pipelines-routes.test.ts` — 19 passed - `pnpm --filter ./server typecheck` — passed - `pnpm --filter ./ui typecheck` — passed - Verified Pipelines remains gated by `enablePipelines === true`: sidebar item is hidden unless the flag is enabled, direct pipeline routes redirect to `/dashboard` when disabled, and the Experimental settings UI still has no Pipelines toggle. - GitHub status checks on `df071c710646de625131064c3fb6588b5e97964a` — all complete with no failing conclusions, including Actions, Socket, Superagent/Security, and Greptile Review - Greptile summary on `df071c710646de625131064c3fb6588b5e97964a` — Confidence Score 5/5 - GitHub review-thread sweep — 0 unresolved Greptile threads Previously recorded during branch development: - Server pipeline service/route and aggregation tests - Shared validator tests - UI pipeline page/settings/item-detail/learnings/liveness tests - Pipeline tutorial smoke path ## Risks - High review surface: this is a large feature branch spanning database, shared contracts, server behavior, CLI/docs, and UI. - Migration ordering and schema compatibility need reviewer attention because this branch has been kept current across multiple `master` syncs. - GitHub still reports merge state `BLOCKED` because the PR is awaiting normal human review/branch-protection completion; all current status checks are green. - Branch-name checklist exception: this PR uses the pre-existing requested branch name, which predates the current public-branch naming rule. The PR title/body avoid internal issue references. > 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 based on GPT-5, with repository tool use, shell execution, git/GitHub CLI operations, and local verification commands. Earlier commits in this branch were assisted by Paperclip agents and other AI coding agents as recorded in commit authorship. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
178 lines
6.3 KiB
TypeScript
178 lines
6.3 KiB
TypeScript
import { AlertTriangle, ExternalLink, Loader2, Lock, RefreshCw } from "lucide-react";
|
|
import type { PipelineCaseLiveness } from "@paperclipai/shared";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Link } from "@/lib/router";
|
|
import { cn } from "../lib/utils";
|
|
import { createIssueDetailPath } from "../lib/issueDetailBreadcrumb";
|
|
import {
|
|
derivePipelineLivenessBanner,
|
|
type LivenessBannerLink,
|
|
type LivenessBannerTone,
|
|
type LivenessRetryKind,
|
|
} from "../lib/pipeline-liveness";
|
|
|
|
interface TonePalette {
|
|
section: string;
|
|
icon: string;
|
|
pulse: string;
|
|
link: string;
|
|
button: string;
|
|
Icon: typeof AlertTriangle;
|
|
}
|
|
|
|
const TONE_PALETTES: Record<LivenessBannerTone, TonePalette> = {
|
|
blocked: {
|
|
section:
|
|
"border-amber-300 bg-amber-50 text-amber-950 dark:border-amber-900/70 dark:bg-amber-950/30 dark:text-amber-100",
|
|
icon: "text-amber-700 dark:text-amber-300",
|
|
pulse: "bg-amber-500",
|
|
link: "text-amber-900 dark:text-amber-100",
|
|
button:
|
|
"border-amber-300 bg-transparent hover:bg-amber-100 dark:border-amber-900/70 dark:hover:bg-amber-950/40",
|
|
Icon: AlertTriangle,
|
|
},
|
|
permission: {
|
|
section:
|
|
"border-purple-300 bg-purple-50 text-purple-950 dark:border-purple-900/70 dark:bg-purple-950/30 dark:text-purple-100",
|
|
icon: "text-purple-700 dark:text-purple-300",
|
|
pulse: "bg-purple-500",
|
|
link: "text-purple-900 dark:text-purple-100",
|
|
button:
|
|
"border-purple-300 bg-transparent hover:bg-purple-100 dark:border-purple-900/70 dark:hover:bg-purple-950/40",
|
|
Icon: Lock,
|
|
},
|
|
retry: {
|
|
section:
|
|
"border-indigo-300 bg-indigo-50 text-indigo-950 dark:border-indigo-900/70 dark:bg-indigo-950/30 dark:text-indigo-100",
|
|
icon: "text-indigo-700 dark:text-indigo-300",
|
|
pulse: "bg-indigo-500",
|
|
link: "text-indigo-900 dark:text-indigo-100",
|
|
button:
|
|
"border-indigo-300 bg-transparent hover:bg-indigo-100 dark:border-indigo-900/70 dark:hover:bg-indigo-950/40",
|
|
Icon: RefreshCw,
|
|
},
|
|
attention: {
|
|
section:
|
|
"border-orange-300 bg-orange-50 text-orange-950 dark:border-orange-900/70 dark:bg-orange-950/30 dark:text-orange-100",
|
|
icon: "text-orange-700 dark:text-orange-300",
|
|
pulse: "bg-orange-500",
|
|
link: "text-orange-900 dark:text-orange-100",
|
|
button:
|
|
"border-orange-300 bg-transparent hover:bg-orange-100 dark:border-orange-900/70 dark:hover:bg-orange-950/40",
|
|
Icon: AlertTriangle,
|
|
},
|
|
};
|
|
|
|
function blockerLinkLabel(link: LivenessBannerLink): string {
|
|
if (link.identifier) return `Open ${link.identifier}`;
|
|
return "Open blocker";
|
|
}
|
|
|
|
function automationLinkLabel(link: LivenessBannerLink): string {
|
|
if (link.identifier) return `Open ${link.identifier}`;
|
|
return "Open automation task";
|
|
}
|
|
|
|
export function PipelineLivenessBanner({
|
|
liveness,
|
|
onRetry,
|
|
retryPending = false,
|
|
retryError = null,
|
|
}: {
|
|
liveness: PipelineCaseLiveness | null | undefined;
|
|
onRetry?: (kind: LivenessRetryKind) => void;
|
|
retryPending?: boolean;
|
|
retryError?: string | null;
|
|
}) {
|
|
const view = derivePipelineLivenessBanner(liveness);
|
|
if (!view) return null;
|
|
|
|
const palette = TONE_PALETTES[view.tone];
|
|
const { Icon } = palette;
|
|
const showRetry = view.showRetry && typeof onRetry === "function";
|
|
|
|
return (
|
|
<section
|
|
role="status"
|
|
aria-label={view.title}
|
|
className={cn(
|
|
"mb-5 flex flex-col gap-3 border-y py-4 md:flex-row md:items-start md:justify-between",
|
|
palette.section,
|
|
)}
|
|
>
|
|
<div className="flex min-w-0 gap-3">
|
|
<Icon className={cn("mt-0.5 h-4 w-4 shrink-0", palette.icon)} aria-hidden="true" />
|
|
<div className="min-w-0 space-y-1">
|
|
<h2 className="flex items-center gap-2 text-sm font-semibold">
|
|
{view.tone === "retry" ? (
|
|
<span
|
|
className={cn("h-1.5 w-1.5 animate-pulse rounded-full", palette.pulse)}
|
|
aria-hidden="true"
|
|
/>
|
|
) : null}
|
|
{view.title}
|
|
</h2>
|
|
<p className="text-sm opacity-85">{view.body}</p>
|
|
{view.permissionKey ? (
|
|
<p className="text-sm opacity-85">
|
|
Required permission:{" "}
|
|
<code className="rounded-sm bg-black/10 px-1 py-0.5 text-xs font-medium dark:bg-white/10">
|
|
{view.permissionKey}
|
|
</code>{" "}
|
|
on the target pipeline.
|
|
</p>
|
|
) : null}
|
|
{view.blockerLink || view.automationLink ? (
|
|
<p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
|
{view.blockerLink ? (
|
|
<Link
|
|
to={createIssueDetailPath(view.blockerLink.identifier ?? view.blockerLink.issueId)}
|
|
className={cn("inline-flex items-center gap-1 font-medium underline-offset-2 hover:underline", palette.link)}
|
|
>
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
{blockerLinkLabel(view.blockerLink)}
|
|
{view.blockerLink.title ? `: ${view.blockerLink.title}` : ""}
|
|
</Link>
|
|
) : null}
|
|
{view.automationLink ? (
|
|
<Link
|
|
to={createIssueDetailPath(view.automationLink.identifier ?? view.automationLink.issueId)}
|
|
className={cn("inline-flex items-center gap-1 font-medium underline-offset-2 hover:underline", palette.link)}
|
|
>
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
{automationLinkLabel(view.automationLink)}
|
|
</Link>
|
|
) : null}
|
|
</p>
|
|
) : null}
|
|
{view.helperNote ? (
|
|
<p className="text-xs italic opacity-70">{view.helperNote}</p>
|
|
) : null}
|
|
{retryError ? (
|
|
<p role="alert" className="text-sm font-medium text-destructive">
|
|
{retryError}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
{showRetry ? (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
className={cn("shrink-0", palette.button)}
|
|
disabled={retryPending}
|
|
onClick={() => onRetry?.(view.retryKind)}
|
|
>
|
|
{retryPending ? (
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
)}
|
|
{retryPending ? "Retrying…" : view.retryLabel}
|
|
</Button>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|