mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 13:24:55 +01:00
Add pipeline workflow primitives and operator UI (#7903)
## 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>
This commit is contained in:
1 parent
c79d347abe
commit
43b005b704
94 files changed
+51184
-251
No files matched your search
@@ -0,0 +1,688 @@
|
||||
import type {
|
||||
Issue,
|
||||
PipelineAutomationRetryCleanupOptions,
|
||||
PipelineAutomationRetryPlan,
|
||||
PipelineAutomationRetryScope,
|
||||
PipelineCaseConversationSource,
|
||||
PipelineCaseDocumentPayload,
|
||||
PipelineCaseDocumentRevision,
|
||||
PipelineCaseLiveness,
|
||||
PipelineCaseOutputsResponse,
|
||||
PipelineHealthReport,
|
||||
RoutineEnvConfig,
|
||||
} from "@paperclipai/shared";
|
||||
import { api } from "./client";
|
||||
|
||||
export type { PipelineHealthReport, PipelineHealthWarning } from "@paperclipai/shared";
|
||||
|
||||
export type PipelineConnectionRef =
|
||||
| string
|
||||
| {
|
||||
id?: string | null;
|
||||
pipelineId?: string | null;
|
||||
upstreamPipelineId?: string | null;
|
||||
downstreamPipelineId?: string | null;
|
||||
feedsIntoPipelineId?: string | null;
|
||||
fedByPipelineId?: string | null;
|
||||
direction?: string | null;
|
||||
};
|
||||
|
||||
export interface PipelineConnections {
|
||||
upstreamPipelineIds?: string[];
|
||||
downstreamPipelineIds?: string[];
|
||||
feedsIntoPipelineId?: string | null;
|
||||
downstreamPipelineId?: string | null;
|
||||
feedsInto?: PipelineConnectionRef[];
|
||||
fedBy?: PipelineConnectionRef[];
|
||||
upstream?: PipelineConnectionRef[];
|
||||
downstream?: PipelineConnectionRef[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PipelineListItem {
|
||||
id: string;
|
||||
companyId: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
projectId: string | null;
|
||||
enforceTransitions: boolean;
|
||||
archivedAt: Date | string | null;
|
||||
stageCount: number;
|
||||
stages?: PipelineStage[];
|
||||
openCaseCount: number;
|
||||
attentionCount?: number | null;
|
||||
inMotionCount?: number | null;
|
||||
descendantActiveWorkCount?: number | null;
|
||||
lastActivityAt?: Date | string | null;
|
||||
connections?: PipelineConnections | null;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineStage {
|
||||
id: string;
|
||||
pipelineId: string;
|
||||
key: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
position: number;
|
||||
config?: Record<string, unknown> | null;
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineDetail extends PipelineListItem {
|
||||
stages: PipelineStage[];
|
||||
transitions: Array<{ fromStageId: string; toStageId: string; label?: string | null }>;
|
||||
documentKeys?: Array<{ key: string; documentId: string }>;
|
||||
}
|
||||
|
||||
export interface PipelineTransitionEdge {
|
||||
fromStageKey: string;
|
||||
toStageKey: string;
|
||||
label?: string | null;
|
||||
}
|
||||
|
||||
export interface PipelineDocumentPayload {
|
||||
link: { key: string; documentId: string; [key: string]: unknown };
|
||||
document: { id: string; title: string; latestBody?: string | null; [key: string]: unknown };
|
||||
revision?: { body?: string | null; title?: string | null; [key: string]: unknown } | null;
|
||||
}
|
||||
|
||||
export interface PipelineDocumentRevision {
|
||||
id: string;
|
||||
companyId: string;
|
||||
documentId: string;
|
||||
pipelineId: string;
|
||||
key: string;
|
||||
revisionNumber: number;
|
||||
title: string | null;
|
||||
format: string;
|
||||
body: string;
|
||||
changeSummary: string | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
createdAt: Date | string;
|
||||
}
|
||||
|
||||
export type PipelineIntakeFieldType = "select" | "text" | "multiline";
|
||||
|
||||
export interface PipelineIntakeField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: PipelineIntakeFieldType;
|
||||
options?: string[];
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PipelineIntakeForm {
|
||||
pipelineId: string;
|
||||
stageId: string | null;
|
||||
stageName?: string | null;
|
||||
fields: PipelineIntakeField[];
|
||||
}
|
||||
|
||||
export interface PipelineCase {
|
||||
id: string;
|
||||
companyId?: string;
|
||||
pipelineId: string;
|
||||
stageId: string | null;
|
||||
caseKey?: string | null;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
fields?: Record<string, unknown> | null;
|
||||
workspaceRef?: Record<string, unknown> | null;
|
||||
parentCaseId?: string | null;
|
||||
parentCaseVersion?: number | null;
|
||||
requestKey?: string | null;
|
||||
version?: number;
|
||||
pendingSuggestion?: PipelineCasePendingSuggestion | null;
|
||||
terminalKind?: string | null;
|
||||
terminalAt?: Date | string | null;
|
||||
childCount?: number;
|
||||
terminalChildCount?: number;
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineCaseActiveWork {
|
||||
issueId: string;
|
||||
issueIdentifier: string | null;
|
||||
issueTitle: string;
|
||||
issueRole?: "work" | "automation";
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
startedAt: Date | string | null;
|
||||
}
|
||||
|
||||
export interface PipelineCasePendingSuggestion {
|
||||
id: string;
|
||||
toStageKey: string;
|
||||
rationale: string;
|
||||
confidence?: number;
|
||||
suggestedByAgentId?: string;
|
||||
runId?: string;
|
||||
createdAt: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineCaseDetail {
|
||||
case: PipelineCase;
|
||||
/** Derived from the pipeline (invisible/internal): used for display + ingest checks. */
|
||||
caseType?: string;
|
||||
stage: PipelineStage;
|
||||
pipeline: PipelineDetail;
|
||||
allowedNextStages: PipelineStage[];
|
||||
links: PipelineCaseIssueLink[];
|
||||
blockers: PipelineCaseBlocker[];
|
||||
blocks: PipelineCaseBlocker[];
|
||||
childrenSummary: {
|
||||
childCount: number;
|
||||
terminalChildCount: number;
|
||||
loadedChildren: number;
|
||||
descendantActiveWorkCount?: number;
|
||||
};
|
||||
parentCase?: {
|
||||
case: PipelineCase;
|
||||
stage: PipelineStage;
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
} | null;
|
||||
builtFromAutomation?: {
|
||||
execution: {
|
||||
id: string;
|
||||
automationId: string;
|
||||
status: string;
|
||||
};
|
||||
routine: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
pipeline: {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
};
|
||||
stage: {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
} | null;
|
||||
case: {
|
||||
id: string;
|
||||
caseKey: string | null;
|
||||
title: string;
|
||||
pipelineId: string;
|
||||
};
|
||||
} | null;
|
||||
activeWork?: PipelineCaseActiveWork | null;
|
||||
liveness?: PipelineCaseLiveness | null;
|
||||
conversationSource?: PipelineCaseConversationSource | null;
|
||||
pendingSuggestion?: PipelineCasePendingSuggestion | null;
|
||||
}
|
||||
|
||||
export interface PipelineCaseIssueLink {
|
||||
id: string;
|
||||
companyId: string;
|
||||
caseId: string;
|
||||
issueId: string;
|
||||
role: "origin" | "conversation" | "work" | "automation";
|
||||
createdByRunId?: string | null;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineCaseIssueLinkWithIssue {
|
||||
link: PipelineCaseIssueLink;
|
||||
issue: Issue;
|
||||
}
|
||||
|
||||
export interface PipelineCaseBlocker {
|
||||
id: string;
|
||||
companyId: string;
|
||||
caseId: string;
|
||||
blockedByCaseId: string;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineCaseEvent {
|
||||
id: string;
|
||||
companyId: string;
|
||||
caseId: string;
|
||||
type: string;
|
||||
actorType: "user" | "agent" | "system";
|
||||
actorUserId?: string | null;
|
||||
actorAgentId?: string | null;
|
||||
runId?: string | null;
|
||||
fromStageId?: string | null;
|
||||
toStageId?: string | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
fromStage?: { id: string; key: string; name: string; kind: string } | null;
|
||||
toStage?: { id: string; key: string; name: string; kind: string } | null;
|
||||
actorAgent?: { id: string; name: string } | null;
|
||||
automation?: {
|
||||
routine: { id: string; title: string } | null;
|
||||
issue: { id: string; identifier: string | null; title: string; status: string } | null;
|
||||
routineRunId?: string | null;
|
||||
stage?: { id: string; key: string; name: string; kind: string } | null;
|
||||
};
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface PipelineCaseEventsPage {
|
||||
items: PipelineCaseEvent[];
|
||||
pagination: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
nextOffset: number | null;
|
||||
hasMore: boolean;
|
||||
order: "asc" | "desc";
|
||||
};
|
||||
}
|
||||
|
||||
export interface PipelineAttentionCaseRef {
|
||||
id: string;
|
||||
caseKey: string | null;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
version: number;
|
||||
terminalKind?: string | null;
|
||||
parentCaseId?: string | null;
|
||||
updatedAt: Date | string;
|
||||
createdAt: Date | string;
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
stage: { id: string; key: string; name: string; kind: string };
|
||||
}
|
||||
|
||||
export interface PipelineAttentionSuggestion {
|
||||
case: PipelineAttentionCaseRef;
|
||||
suggestion: {
|
||||
id: string;
|
||||
fromStageKey: string;
|
||||
fromStageName: string;
|
||||
toStageKey: string;
|
||||
toStageName: string | null;
|
||||
rationale: string;
|
||||
confidence?: number | null;
|
||||
createdAt: Date | string;
|
||||
suggestedBy: { agentId: string; agentName: string } | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PipelineAttentionReview {
|
||||
case: PipelineAttentionCaseRef;
|
||||
review: {
|
||||
expectedVersion: number;
|
||||
approveToStageKey: string | null;
|
||||
rejectToStageKey: string | null;
|
||||
requestChangesToStageKey: string | null;
|
||||
requireRejectReason: boolean;
|
||||
requireRequestChangesReason: boolean;
|
||||
reviewerKind: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PipelineAttentionHeadsUp {
|
||||
case: PipelineAttentionCaseRef;
|
||||
drift: {
|
||||
eventId: string;
|
||||
createdAt: Date | string;
|
||||
previousVersion: number | null;
|
||||
version: number | null;
|
||||
upstream: {
|
||||
caseId: string | null;
|
||||
caseKey: string | null;
|
||||
title: string | null;
|
||||
pipelineId: string | null;
|
||||
pipelineName: string | null;
|
||||
};
|
||||
};
|
||||
activeWork?: PipelineCaseActiveWork | null;
|
||||
workIssue?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface PipelineAttentionFeed {
|
||||
suggestions: PipelineAttentionSuggestion[];
|
||||
reviews: PipelineAttentionReview[];
|
||||
headsUp: PipelineAttentionHeadsUp[];
|
||||
counts: { suggestions: number; reviews: number; headsUp: number };
|
||||
}
|
||||
|
||||
export interface PipelineReviewConfig {
|
||||
approveToStageKey?: string | null;
|
||||
rejectToStageKey?: string | null;
|
||||
requestChangesToStageKey?: string | null;
|
||||
requireRejectReason?: boolean;
|
||||
requireRequestChangesReason?: boolean;
|
||||
reviewerKind?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PipelineReviewCaseRow {
|
||||
case: PipelineCase;
|
||||
pipeline: { id: string; key: string; name: string; [key: string]: unknown };
|
||||
stage: PipelineStage;
|
||||
parentCase?: PipelineCase | null;
|
||||
pendingSuggestion?: PipelineCasePendingSuggestion | null;
|
||||
reviewConfig: PipelineReviewConfig;
|
||||
}
|
||||
|
||||
export type PipelineReviewDecision = "approve" | "reject" | "request_changes";
|
||||
|
||||
export interface PipelineBulkReviewResult {
|
||||
results: Array<{
|
||||
caseId: string;
|
||||
ok: boolean;
|
||||
result?: unknown;
|
||||
error?: { status?: number; message?: string; code?: string; details?: Record<string, unknown> };
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PipelineCompanyCaseEvent extends PipelineCaseEvent {
|
||||
case: { id: string; caseKey: string | null; title: string; terminalKind?: string | null };
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
fromStage?: { id: string; key: string; name: string; kind: string } | null;
|
||||
toStage?: { id: string; key: string; name: string; kind: string } | null;
|
||||
actorAgent?: { id: string; name: string } | null;
|
||||
}
|
||||
|
||||
export interface PipelineCompanyCaseEventsPage {
|
||||
items: PipelineCompanyCaseEvent[];
|
||||
pagination: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
nextOffset: number | null;
|
||||
hasMore: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PipelineCaseChildrenTreeNode {
|
||||
id: string;
|
||||
caseKey: string | null;
|
||||
title: string;
|
||||
terminalKind?: string | null;
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
stage: { id: string; key: string; name: string; kind: string };
|
||||
rollup?: { total: number; done: number; dropped: number; inMotion: number } | null;
|
||||
childGroups?: Array<{
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
cases: PipelineCaseChildrenTreeNode[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PipelineCaseChildrenTree {
|
||||
case: PipelineCaseChildrenTreeNode;
|
||||
rollup?: { total: number; done: number; dropped: number; inMotion: number } | null;
|
||||
childGroups?: Array<{
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
cases: PipelineCaseChildrenTreeNode[];
|
||||
}>;
|
||||
truncated?: boolean;
|
||||
totalNodes?: number;
|
||||
}
|
||||
|
||||
export interface PipelineCaseParentSummary {
|
||||
case: {
|
||||
id: string;
|
||||
caseKey?: string | null;
|
||||
title: string;
|
||||
pipelineId: string;
|
||||
};
|
||||
pipeline: { id: string; key: string; name: string };
|
||||
}
|
||||
|
||||
export interface PipelineCaseChildRow {
|
||||
case: PipelineCase;
|
||||
stage: PipelineStage;
|
||||
parentCase?: PipelineCaseParentSummary | null;
|
||||
activeWork?: PipelineCaseActiveWork | null;
|
||||
descendantActiveWorkCount?: number;
|
||||
}
|
||||
|
||||
export type PipelineCaseChildrenResponse = PipelineCaseChildRow[];
|
||||
|
||||
export type PipelineBatchIngestResult =
|
||||
| { ok: true; case: PipelineCase; created: boolean }
|
||||
| {
|
||||
ok: false;
|
||||
caseKey: string | null;
|
||||
error?: {
|
||||
status?: number;
|
||||
message?: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
export const pipelinesApi = {
|
||||
list: (companyId: string) => api.get<PipelineListItem[]>(`/companies/${companyId}/pipelines`),
|
||||
create: (
|
||||
companyId: string,
|
||||
data: { key: string; name: string; description?: string | null; projectId?: string | null },
|
||||
) => api.post<PipelineListItem & { stages?: PipelineStage[] }>(`/companies/${companyId}/pipelines`, data),
|
||||
get: (pipelineId: string) => api.get<PipelineDetail>(`/pipelines/${pipelineId}`),
|
||||
getHealth: (pipelineId: string) => api.get<PipelineHealthReport>(`/pipelines/${pipelineId}/health`),
|
||||
update: (
|
||||
pipelineId: string,
|
||||
data: { name?: string; description?: string | null; enforceTransitions?: boolean; archived?: boolean },
|
||||
) => api.patch<PipelineListItem>(`/pipelines/${pipelineId}`, data),
|
||||
createStage: (
|
||||
pipelineId: string,
|
||||
data: { key: string; name: string; kind: string; position: number; config?: Record<string, unknown> },
|
||||
) => api.post<PipelineStage>(`/pipelines/${pipelineId}/stages`, data),
|
||||
updateStage: (
|
||||
pipelineId: string,
|
||||
stageId: string,
|
||||
data: { key?: string; name?: string; kind?: string; position?: number; config?: Record<string, unknown> },
|
||||
) => api.patch<PipelineStage>(`/pipelines/${pipelineId}/stages/${stageId}`, data),
|
||||
// Stage secrets live on the backing automation routine's env, not in stage
|
||||
// config. This narrow route updates only that env (and its secret bindings)
|
||||
// so saving secrets never clobbers unrelated stage settings.
|
||||
updateStageAutomationEnv: (
|
||||
pipelineId: string,
|
||||
stageId: string,
|
||||
data: { env: RoutineEnvConfig | null; baseRoutineRevisionId?: string | null },
|
||||
) => api.patch<PipelineStage>(`/pipelines/${pipelineId}/stages/${stageId}/automation-env`, data),
|
||||
deleteStage: (
|
||||
pipelineId: string,
|
||||
stageId: string,
|
||||
data?: { moveCasesToStageId?: string | null },
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
if (data?.moveCasesToStageId) params.set("moveCasesToStageId", data.moveCasesToStageId);
|
||||
const qs = params.toString();
|
||||
return api.delete<{ deleted: boolean }>(`/pipelines/${pipelineId}/stages/${stageId}${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
setTransitions: (
|
||||
pipelineId: string,
|
||||
data: { transitions: PipelineTransitionEdge[]; enforceTransitions?: boolean },
|
||||
) =>
|
||||
api.put<{ transitions: Array<{ fromStageId: string; toStageId: string; label?: string | null }> }>(
|
||||
`/pipelines/${pipelineId}/transitions`,
|
||||
data,
|
||||
),
|
||||
getDocument: (pipelineId: string, key: string) =>
|
||||
api.get<PipelineDocumentPayload>(`/pipelines/${pipelineId}/documents/${encodeURIComponent(key)}`),
|
||||
upsertDocument: (pipelineId: string, key: string, data: { title?: string; body: string; baseRevisionId?: string | null }) =>
|
||||
api.put<{ document: PipelineDocumentPayload["document"]; revision: NonNullable<PipelineDocumentPayload["revision"]> }>(
|
||||
`/pipelines/${pipelineId}/documents/${encodeURIComponent(key)}`,
|
||||
data,
|
||||
),
|
||||
listDocumentRevisions: (pipelineId: string, key: string) =>
|
||||
api.get<PipelineDocumentRevision[]>(`/pipelines/${pipelineId}/documents/${encodeURIComponent(key)}/revisions`),
|
||||
restoreDocumentRevision: (pipelineId: string, key: string, revisionId: string) =>
|
||||
api.post<{
|
||||
document: PipelineDocumentPayload["document"];
|
||||
revision: PipelineDocumentRevision;
|
||||
restoredFromRevisionId: string;
|
||||
restoredFromRevisionNumber: number;
|
||||
}>(`/pipelines/${pipelineId}/documents/${encodeURIComponent(key)}/revisions/${revisionId}/restore`, {}),
|
||||
getCaseDocument: (caseId: string, key: string) =>
|
||||
api.get<PipelineCaseDocumentPayload>(`/cases/${caseId}/documents/${encodeURIComponent(key)}`),
|
||||
upsertCaseDocument: (
|
||||
caseId: string,
|
||||
key: string,
|
||||
data: { title?: string; format?: string; body: string; changeSummary?: string | null; baseRevisionId?: string | null },
|
||||
) =>
|
||||
api.put<{ document: PipelineCaseDocumentPayload["document"]; revision: PipelineCaseDocumentRevision }>(
|
||||
`/cases/${caseId}/documents/${encodeURIComponent(key)}`,
|
||||
data,
|
||||
),
|
||||
listCaseDocumentRevisions: (caseId: string, key: string) =>
|
||||
api.get<PipelineCaseDocumentRevision[]>(`/cases/${caseId}/documents/${encodeURIComponent(key)}/revisions`),
|
||||
restoreCaseDocumentRevision: (caseId: string, key: string, revisionId: string) =>
|
||||
api.post<{
|
||||
document: PipelineCaseDocumentPayload["document"];
|
||||
revision: PipelineCaseDocumentRevision;
|
||||
restoredFromRevisionId: string;
|
||||
restoredFromRevisionNumber: number;
|
||||
}>(`/cases/${caseId}/documents/${encodeURIComponent(key)}/revisions/${revisionId}/restore`, {}),
|
||||
getIntakeForm: (pipelineId: string) => api.get<PipelineIntakeForm>(`/pipelines/${pipelineId}/intake-form`),
|
||||
listCases: (pipelineId: string, filters?: { parentCaseId?: string; terminal?: boolean }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.parentCaseId) params.set("parentCaseId", filters.parentCaseId);
|
||||
if (filters?.terminal !== undefined) params.set("terminal", filters.terminal ? "true" : "false");
|
||||
const qs = params.toString();
|
||||
return api.get<PipelineCaseChildRow[]>(`/pipelines/${pipelineId}/cases${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
getCase: (caseId: string) => api.get<PipelineCaseDetail>(`/cases/${caseId}`),
|
||||
getCaseChildren: (caseId: string) =>
|
||||
api.get<PipelineCaseChildrenResponse>(`/cases/${caseId}/children`),
|
||||
getCaseChildrenTree: (caseId: string) =>
|
||||
api.get<PipelineCaseChildrenTree>(`/cases/${caseId}/children/tree`),
|
||||
getCaseEvents: (caseId: string, filters?: { limit?: number; offset?: number; order?: "asc" | "desc" }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.limit !== undefined) params.set("limit", String(filters.limit));
|
||||
if (filters?.offset !== undefined) params.set("offset", String(filters.offset));
|
||||
if (filters?.order) params.set("order", filters.order);
|
||||
const qs = params.toString();
|
||||
return api.get<PipelineCaseEventsPage>(`/cases/${caseId}/events${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
getCaseIssueLinks: (caseId: string) =>
|
||||
api.get<PipelineCaseIssueLinkWithIssue[]>(`/cases/${caseId}/issue-links`),
|
||||
getCaseOutputs: (caseId: string) =>
|
||||
api.get<PipelineCaseOutputsResponse>(`/cases/${caseId}/outputs`),
|
||||
createIssueLink: (
|
||||
caseId: string,
|
||||
data:
|
||||
| { issueId: string; role: PipelineCaseIssueLink["role"] }
|
||||
| { role: "conversation"; issueId?: undefined },
|
||||
) => data.issueId
|
||||
? api.post<PipelineCaseIssueLink>(`/cases/${caseId}/issue-links`, data)
|
||||
: api.post<{ issue: Issue; created: boolean }>(`/cases/${caseId}/open-conversation`, {}),
|
||||
updateCase: (
|
||||
caseId: string,
|
||||
data: {
|
||||
title?: string;
|
||||
summary?: string | null;
|
||||
fields?: Record<string, unknown>;
|
||||
parentCaseId?: string | null;
|
||||
expectedVersion?: number;
|
||||
leaseToken?: string | null;
|
||||
},
|
||||
) => api.patch<{ case: PipelineCase; event?: PipelineCaseEvent | null } | PipelineCase>(`/cases/${caseId}`, data),
|
||||
acknowledgeDrift: (caseId: string, data?: { expectedVersion?: number }) =>
|
||||
api.post<{ case: PipelineCase; event: PipelineCaseEvent | null; acknowledged: boolean }>(
|
||||
`/cases/${caseId}/acknowledge-drift`,
|
||||
data ?? {},
|
||||
),
|
||||
resolveSuggestion: (
|
||||
caseId: string,
|
||||
data: {
|
||||
suggestionId: string;
|
||||
resolution: "accept" | "dismiss";
|
||||
expectedVersion?: number;
|
||||
reason?: string | null;
|
||||
leaseToken?: string | null;
|
||||
},
|
||||
) => api.post<unknown>(`/cases/${caseId}/resolve-suggestion`, data),
|
||||
transitionCase: (
|
||||
caseId: string,
|
||||
data: {
|
||||
toStageKey: string;
|
||||
expectedVersion: number;
|
||||
reason?: string | null;
|
||||
leaseToken?: string | null;
|
||||
acceptSuggestionId?: string;
|
||||
force?: boolean;
|
||||
},
|
||||
) => api.post<unknown>(`/cases/${caseId}/transition`, data),
|
||||
rerunCurrentStageAutomation: (caseId: string) =>
|
||||
api.post<unknown>(`/cases/${caseId}/automation/current-stage/rerun`, {}),
|
||||
getAutomationRetryPlan: (caseId: string, scope: PipelineAutomationRetryScope, targetStageId?: string | null) => {
|
||||
const params = new URLSearchParams({ scope });
|
||||
if (targetStageId) params.set("targetStageId", targetStageId);
|
||||
return api.get<PipelineAutomationRetryPlan>(`/cases/${caseId}/automation/retry-plan?${params.toString()}`);
|
||||
},
|
||||
retryStageAutomation: (
|
||||
caseId: string,
|
||||
data: {
|
||||
scope: PipelineAutomationRetryScope;
|
||||
targetStageId?: string | null;
|
||||
expectedVersion: number;
|
||||
cleanup: PipelineAutomationRetryCleanupOptions;
|
||||
},
|
||||
) => api.post<unknown>(`/cases/${caseId}/automation/retry`, data),
|
||||
retryAutomation: (caseId: string, automationId: string) =>
|
||||
api.post<unknown>(`/cases/${caseId}/automations/${automationId}/retry`, {}),
|
||||
ingestCasesBatch: (pipelineId: string, data: {
|
||||
items: Array<{
|
||||
caseKey?: string | null;
|
||||
title: string;
|
||||
fields?: Record<string, unknown>;
|
||||
stageKey?: string | null;
|
||||
parentCaseId?: string | null;
|
||||
requestKey?: string | null;
|
||||
blockedByCaseIds?: string[];
|
||||
blockedByCaseKeys?: string[];
|
||||
}>;
|
||||
}) =>
|
||||
api.post<PipelineBatchIngestResult[]>(`/pipelines/${pipelineId}/cases/batch`, data),
|
||||
listAttention: (companyId: string, options?: { limit?: number }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
const qs = params.toString();
|
||||
return api.get<PipelineAttentionFeed>(`/companies/${companyId}/pipelines-attention${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
listReviewCases: (companyId: string, filters?: { pipelineId?: string; parentCaseId?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.pipelineId) params.set("pipelineId", filters.pipelineId);
|
||||
if (filters?.parentCaseId) params.set("parentCaseId", filters.parentCaseId);
|
||||
const qs = params.toString();
|
||||
return api.get<PipelineReviewCaseRow[]>(`/companies/${companyId}/review-cases${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
reviewCase: (
|
||||
caseId: string,
|
||||
data: {
|
||||
decision: PipelineReviewDecision;
|
||||
reason?: string | null;
|
||||
expectedVersion: number;
|
||||
leaseToken?: string | null;
|
||||
},
|
||||
) => api.post<unknown>(`/cases/${caseId}/review`, data),
|
||||
bulkReviewCases: (
|
||||
companyId: string,
|
||||
data: {
|
||||
items: Array<{
|
||||
caseId: string;
|
||||
decision: PipelineReviewDecision;
|
||||
reason?: string | null;
|
||||
expectedVersion: number;
|
||||
}>;
|
||||
},
|
||||
) => api.post<PipelineBulkReviewResult>(`/companies/${companyId}/review-cases/bulk`, data),
|
||||
listCompanyCaseEvents: (
|
||||
companyId: string,
|
||||
filters?: { types?: string; limit?: number; offset?: number },
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.types) params.set("types", filters.types);
|
||||
if (filters?.limit !== undefined) params.set("limit", String(filters.limit));
|
||||
if (filters?.offset !== undefined) params.set("offset", String(filters.offset));
|
||||
const qs = params.toString();
|
||||
return api.get<PipelineCompanyCaseEventsPage>(`/companies/${companyId}/case-events${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user