mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02: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>
650 lines
21 KiB
Markdown
650 lines
21 KiB
Markdown
# Pipelines Tutorial: Release to Published Content
|
|
|
|
This walkthrough is the CLI/API version of the 12-step release-to-content worked example. It uses three linked pipelines:
|
|
|
|
- `release-coverage`: one release case answers "is this release covered?"
|
|
- `feature-content`: one feature case per approved feature rolls up content coverage.
|
|
- `content-production`: one content-piece case moves through drafting, assets, assembly, final review, publishing, and a terminal result.
|
|
|
|
The walkthrough intentionally labels conventions separately from primitives. Those conventions are future primitive candidates: if they hurt, we want to see exactly where.
|
|
|
|
## Prerequisites
|
|
|
|
Run this against a dev Paperclip instance with a board token or an agent token that can manage pipelines, routines, and issues.
|
|
|
|
```sh
|
|
export PAPERCLIP_API_URL=http://localhost:3100
|
|
export PAPERCLIP_COMPANY_ID=<company-id>
|
|
export PAPERCLIP_API_KEY=<token>
|
|
|
|
# Optional: assign routine-created drafting issues to a specific agent.
|
|
export DRAFTING_AGENT_ID=<agent-id>
|
|
|
|
export RUN_KEY="$(date +%Y%m%d%H%M%S)"
|
|
export RELEASE_PIPELINE="release-coverage-$RUN_KEY"
|
|
export FEATURE_PIPELINE="feature-content-$RUN_KEY"
|
|
export CONTENT_PIPELINE="content-production-$RUN_KEY"
|
|
```
|
|
|
|
## Step 1: Setup The Three Pipelines
|
|
|
|
Create Release Coverage. It is thin on purpose: the release case stays in `intake` until its feature children are terminal, then `autoAdvanceOnChildrenTerminal` moves it to `covered`.
|
|
|
|
```sh
|
|
cat > /tmp/release-stages.json <<'JSON'
|
|
[
|
|
{
|
|
"key": "intake",
|
|
"name": "Intake",
|
|
"kind": "open",
|
|
"position": 100,
|
|
"config": { "autoAdvanceOnChildrenTerminal": "covered" }
|
|
},
|
|
{ "key": "covered", "name": "Covered", "kind": "done", "position": 900 },
|
|
{ "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
|
|
]
|
|
JSON
|
|
|
|
paperclipai pipelines create \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
--key "$RELEASE_PIPELINE" \
|
|
--name "Release Coverage $RUN_KEY" \
|
|
--stages-file /tmp/release-stages.json
|
|
```
|
|
|
|
Create Feature Content. The review stage lets the human approve features into production or drop them from this release.
|
|
|
|
```sh
|
|
cat > /tmp/feature-stages.json <<'JSON'
|
|
[
|
|
{ "key": "suggesting", "name": "Suggesting", "kind": "open", "position": 100 },
|
|
{
|
|
"key": "suggestion_review",
|
|
"name": "Suggestion Review",
|
|
"kind": "review",
|
|
"position": 200,
|
|
"config": {
|
|
"approveToStageKey": "producing",
|
|
"rejectToStageKey": "cancelled",
|
|
"requestChangesToStageKey": "suggesting",
|
|
"requireRejectReason": true,
|
|
"reviewerKind": "human"
|
|
}
|
|
},
|
|
{
|
|
"key": "producing",
|
|
"name": "Producing",
|
|
"kind": "working",
|
|
"position": 300,
|
|
"config": { "autoAdvanceOnChildrenTerminal": "covered" }
|
|
},
|
|
{ "key": "covered", "name": "Covered", "kind": "done", "position": 900 },
|
|
{ "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
|
|
]
|
|
JSON
|
|
|
|
paperclipai pipelines create \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
--key "$FEATURE_PIPELINE" \
|
|
--name "Feature Content $RUN_KEY" \
|
|
--stages-file /tmp/feature-stages.json
|
|
```
|
|
|
|
Create Content Production. `Assets` and `Assembly` are `working` stages, not review stages. `Final Review` is the review stage and has all three exits: approve, request changes, and drop.
|
|
|
|
```sh
|
|
cat > /tmp/content-stages.json <<'JSON'
|
|
[
|
|
{
|
|
"key": "drafting",
|
|
"name": "Drafting",
|
|
"kind": "working",
|
|
"position": 100,
|
|
"config": { "autonomy": "suggest" }
|
|
},
|
|
{
|
|
"key": "assets",
|
|
"name": "Assets",
|
|
"kind": "working",
|
|
"position": 200
|
|
},
|
|
{
|
|
"key": "assembly",
|
|
"name": "Assembly",
|
|
"kind": "working",
|
|
"position": 300,
|
|
"config": { "autoAdvanceOnChildrenTerminal": "final_review" }
|
|
},
|
|
{
|
|
"key": "final_review",
|
|
"name": "Final Review",
|
|
"kind": "review",
|
|
"position": 400,
|
|
"config": {
|
|
"approveToStageKey": "publishing",
|
|
"rejectToStageKey": "dropped",
|
|
"requestChangesToStageKey": "drafting",
|
|
"requireRejectReason": true,
|
|
"reviewerKind": "human"
|
|
}
|
|
},
|
|
{ "key": "publishing", "name": "Publishing", "kind": "working", "position": 500 },
|
|
{ "key": "published", "name": "Published", "kind": "done", "position": 900 },
|
|
{ "key": "dropped", "name": "Dropped", "kind": "cancelled", "position": 1000 }
|
|
]
|
|
JSON
|
|
|
|
paperclipai pipelines create \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
--key "$CONTENT_PIPELINE" \
|
|
--name "Content Production $RUN_KEY" \
|
|
--stages-file /tmp/content-stages.json
|
|
```
|
|
|
|
Show `enforceTransitions` on one pipeline. The release case can only auto-cover or cancel.
|
|
|
|
```sh
|
|
cat > /tmp/release-transitions.json <<'JSON'
|
|
{
|
|
"enforceTransitions": true,
|
|
"transitions": [
|
|
{ "fromStageKey": "intake", "toStageKey": "covered", "label": "all features terminal" },
|
|
{ "fromStageKey": "intake", "toStageKey": "cancelled", "label": "cancel release coverage" }
|
|
]
|
|
}
|
|
JSON
|
|
|
|
paperclipai pipelines set-transitions \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$RELEASE_PIPELINE" \
|
|
--file /tmp/release-transitions.json
|
|
```
|
|
|
|
Add guidance and a drafting routine. The guidance document carries the rubric.
|
|
|
|
```sh
|
|
cat > /tmp/content-guidance.md <<'MD'
|
|
# Content Production guidance
|
|
|
|
Final Review has three exits:
|
|
|
|
- approve to Publishing when the pinned revisions are ready to ship
|
|
- request changes back to Drafting when the same work issue should continue
|
|
- drop to Dropped when the content should not ship
|
|
|
|
Convention: asset cases store `briefedFromVersion` in `fields` so assembly review can compare a pinned brief against the current upstream case `version`.
|
|
MD
|
|
|
|
paperclipai pipelines guidance put \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CONTENT_PIPELINE" \
|
|
--file /tmp/content-guidance.md
|
|
```
|
|
|
|
```sh
|
|
cat > /tmp/drafting-routine.json <<JSON
|
|
{
|
|
"title": "Draft content production case",
|
|
"description": "Template convention: draft the content case from the Pipeline Case Context, keep typed work references in case fields, and suggest Drafting -> Assets when ready.",
|
|
"priority": "medium",
|
|
"status": "active",
|
|
"concurrencyPolicy": "always_enqueue",
|
|
"catchUpPolicy": "skip_missed"
|
|
${DRAFTING_AGENT_ID:+, "assigneeAgentId": "$DRAFTING_AGENT_ID"}
|
|
}
|
|
JSON
|
|
|
|
export DRAFTING_ROUTINE_ID="$(
|
|
paperclipai routine create \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
--payload-json "$(jq -c . /tmp/drafting-routine.json)" \
|
|
--json | jq -r '.id'
|
|
)"
|
|
|
|
paperclipai pipelines set-automation \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CONTENT_PIPELINE" \
|
|
--stage drafting \
|
|
--routine "$DRAFTING_ROUTINE_ID" \
|
|
--note "Template-versioned with the routine prompt."
|
|
```
|
|
|
|
**Convention:** v1 "templates" version with the routine prompt plus the batch file below, not with the pipeline. The pipeline `guidance` document carries the durable rubric. This is the accepted divergence from the long-term template-on-pipeline shape.
|
|
|
|
## Step 2: Trigger, Intake, And Gate
|
|
|
|
A real system would start with a release-cut routine. Today, the routine fires on a timer or API trigger and creates an intake issue. On that issue, the agent writes a proposal document and asks the board for a checkbox confirmation.
|
|
|
|
The accepted checkbox selection is represented here by the batch files. That batch file plus the routine prompt is the v1 template convention.
|
|
|
|
Create the release root:
|
|
|
|
```sh
|
|
export RELEASE_CASE_ID="$(
|
|
paperclipai pipelines ingest \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$RELEASE_PIPELINE" \
|
|
--case-key "release-$RUN_KEY" \
|
|
--stage intake \
|
|
--title "Release $RUN_KEY: Pipeline primitives" \
|
|
--summary "Rollup root for release content coverage." \
|
|
--fields-json '{"release":"v0.pipeline-tutorial","templateVersionConvention":"routine-prompt"}' \
|
|
--json | jq -r '.case.id'
|
|
)"
|
|
```
|
|
|
|
Create two feature cases parented to the release. One is approved, one is dropped.
|
|
|
|
```sh
|
|
jq -n --arg parent "$RELEASE_CASE_ID" '{
|
|
items: [
|
|
{
|
|
caseKey: "feature-pipelines-ui",
|
|
title: "Feature: Pipelines UI",
|
|
summary: "Worth a content package.",
|
|
parentCaseId: $parent,
|
|
stageKey: "suggestion_review",
|
|
fields: { releaseTag: "v0.pipeline-tutorial", source: "release-notes" }
|
|
},
|
|
{
|
|
caseKey: "feature-routine-webhooks",
|
|
title: "Feature: Routine webhooks",
|
|
summary: "Rejected by the gate for this release.",
|
|
parentCaseId: $parent,
|
|
stageKey: "suggestion_review",
|
|
fields: { releaseTag: "v0.pipeline-tutorial", source: "release-notes" }
|
|
}
|
|
]
|
|
}' > /tmp/feature-cases.json
|
|
|
|
paperclipai pipelines ingest-batch \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$FEATURE_PIPELINE" \
|
|
--file /tmp/feature-cases.json \
|
|
--json | tee /tmp/feature-cases-result.json
|
|
```
|
|
|
|
```sh
|
|
feature_case_id() {
|
|
jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' /tmp/feature-cases-result.json
|
|
}
|
|
|
|
export FEATURE_MAIN="$(feature_case_id feature-pipelines-ui)"
|
|
export FEATURE_DROP="$(feature_case_id feature-routine-webhooks)"
|
|
|
|
jq -n --arg main "$FEATURE_MAIN" --arg drop "$FEATURE_DROP" '{
|
|
items: [
|
|
{ caseId: $main, decision: "approve", expectedVersion: 1 },
|
|
{ caseId: $drop, decision: "reject", reason: "Fold webhooks into the broader launch post.", expectedVersion: 1 }
|
|
]
|
|
}' > /tmp/feature-review.json
|
|
|
|
paperclipai pipelines review-bulk \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
--file /tmp/feature-review.json
|
|
```
|
|
|
|
Create content cases under the approved feature. `launch-tweet` declares `blockedByCaseKeys: ["blog-post"]`; the CLI resolves that key to the blog case in the same batch.
|
|
|
|
```sh
|
|
jq -n --arg parent "$FEATURE_MAIN" '{
|
|
items: [
|
|
{
|
|
caseKey: "blog-post",
|
|
title: "Launch blog post",
|
|
summary: "Draft the release narrative.",
|
|
parentCaseId: $parent,
|
|
stageKey: "drafting",
|
|
fields: {
|
|
contentType: "blog",
|
|
typedWorkRefs: { draftPath: "workspaces/release/blog.md" },
|
|
briefedFromVersion: null
|
|
}
|
|
},
|
|
{
|
|
caseKey: "changelog-entry",
|
|
title: "Product changelog",
|
|
summary: "Compact changelog entry.",
|
|
parentCaseId: $parent,
|
|
stageKey: "drafting",
|
|
fields: {
|
|
contentType: "changelog",
|
|
typedWorkRefs: { draftPath: "workspaces/release/changelog.md" },
|
|
briefedFromVersion: null
|
|
}
|
|
},
|
|
{
|
|
caseKey: "launch-tweet",
|
|
title: "Launch tweet",
|
|
summary: "Tweet after the blog is approved.",
|
|
parentCaseId: $parent,
|
|
stageKey: "drafting",
|
|
blockedByCaseKeys: ["blog-post"],
|
|
fields: {
|
|
contentType: "social",
|
|
typedWorkRefs: { draftPath: "workspaces/release/tweet.md" },
|
|
briefedFromVersion: 1
|
|
}
|
|
}
|
|
]
|
|
}' > /tmp/content-cases.json
|
|
|
|
paperclipai pipelines ingest-batch \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CONTENT_PIPELINE" \
|
|
--file /tmp/content-cases.json \
|
|
--json | tee /tmp/content-cases-result.json
|
|
```
|
|
|
|
**Convention:** `typedWorkRefs` and `briefedFromVersion` are ordinary case `fields`, not new primitives. They document how this case type points at work and how downstream asset briefs pin an upstream version.
|
|
|
|
## Step 3: Readiness Suggestion
|
|
|
|
The drafting agent should not silently move the case. It suggests `Drafting -> Assets` with a rationale, and the human accepts it.
|
|
|
|
```sh
|
|
content_case_id() {
|
|
jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' /tmp/content-cases-result.json
|
|
}
|
|
|
|
export BLOG_CASE="$(content_case_id blog-post)"
|
|
export CHANGELOG_CASE="$(content_case_id changelog-entry)"
|
|
export TWEET_CASE="$(content_case_id launch-tweet)"
|
|
|
|
export SUGGESTION_ID="$(
|
|
paperclipai pipelines case suggest \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--to assets \
|
|
--rationale "Draft is stable enough to brief asset work." \
|
|
--confidence 0.9 \
|
|
--json | jq -r '.suggestion.id'
|
|
)"
|
|
|
|
paperclipai pipelines case resolve-suggestion \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--suggestion "$SUGGESTION_ID" \
|
|
--accept \
|
|
--expected-version 1
|
|
```
|
|
|
|
## Step 4: Parallel Editing And Drift
|
|
|
|
The draft can still change while dependent work exists. A material update to the upstream case posts a drift comment on dependent linked work issues.
|
|
|
|
```sh
|
|
export TWEET_WORK_ISSUE="$(
|
|
paperclipai issue create \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
--title "Work issue for launch tweet $RUN_KEY" \
|
|
--description "Receives drift comments from the upstream blog case." \
|
|
--status todo \
|
|
--priority low \
|
|
--json | jq -r '.id'
|
|
)"
|
|
|
|
curl -sS -X POST \
|
|
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$(jq -cn --arg issueId "$TWEET_WORK_ISSUE" '{ issueId: $issueId, role: "work" }')" \
|
|
"$PAPERCLIP_API_URL/api/cases/$TWEET_CASE/issue-links" >/dev/null
|
|
|
|
paperclipai pipelines case edit \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--expected-version 2 \
|
|
--summary "Draft changed while dependent tweet work was already briefed." \
|
|
--fields-json '{"contentType":"blog","typedWorkRefs":{"draftPath":"workspaces/release/blog.md"},"briefedFromVersion":null,"materialChange":"new-positioning"}'
|
|
```
|
|
|
|
If a worker tries to patch with the stale version, the API returns `409` with `code=version_conflict`, the current version, and the current stage. Recovery is to re-read the case and retry against the current version.
|
|
|
|
```sh
|
|
paperclipai pipelines case edit \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--expected-version 2 \
|
|
--title "Stale edit"
|
|
|
|
# Recovery:
|
|
paperclipai pipelines case get -C "$PAPERCLIP_COMPANY_ID" "$BLOG_CASE" --json
|
|
```
|
|
|
|
## Step 5: Assets
|
|
|
|
The Assets automation creates asset cases under the feature. In v1 the tutorial uses an explicit batch file; in the product, this is the stage-template convention.
|
|
|
|
```sh
|
|
export BLOG_VERSION="$(paperclipai pipelines case get -C "$PAPERCLIP_COMPANY_ID" "$BLOG_CASE" --json | jq -r '.case.version')"
|
|
|
|
jq -n --arg parent "$FEATURE_MAIN" --argjson briefVersion "$BLOG_VERSION" '{
|
|
items: [
|
|
{
|
|
caseKey: "blog-hero-image",
|
|
title: "Hero image",
|
|
parentCaseId: $parent,
|
|
stageKey: "assets",
|
|
fields: { assetType: "image", briefedFromVersion: $briefVersion }
|
|
},
|
|
{
|
|
caseKey: "blog-social-card",
|
|
title: "Social card",
|
|
parentCaseId: $parent,
|
|
stageKey: "assets",
|
|
fields: { assetType: "image", briefedFromVersion: $briefVersion }
|
|
}
|
|
]
|
|
}' > /tmp/asset-cases.json
|
|
|
|
paperclipai pipelines ingest-batch \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CONTENT_PIPELINE" \
|
|
--file /tmp/asset-cases.json \
|
|
--json | tee /tmp/asset-cases-result.json
|
|
```
|
|
|
|
```sh
|
|
asset_case_id() {
|
|
jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' /tmp/asset-cases-result.json
|
|
}
|
|
|
|
export HERO_CASE="$(asset_case_id blog-hero-image)"
|
|
export CARD_CASE="$(asset_case_id blog-social-card)"
|
|
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$HERO_CASE" --to published --expected-version 1 --reason "Hero image done."
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CARD_CASE" --to dropped --expected-version 1 --reason "Social card not needed."
|
|
```
|
|
|
|
When both asset cases are terminal, move the blog case to `assembly`.
|
|
|
|
```sh
|
|
export BLOG_ASSETS_VERSION="$(paperclipai pipelines case get -C "$PAPERCLIP_COMPANY_ID" "$BLOG_CASE" --json | jq -r '.case.version')"
|
|
|
|
paperclipai pipelines case transition \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--to assembly \
|
|
--expected-version "$BLOG_ASSETS_VERSION" \
|
|
--reason "Assets complete; assemble the package."
|
|
```
|
|
|
|
## Step 6: Assembly And Auto-Advance To Final Review
|
|
|
|
Assembly is also a `working` stage. This is the `autoAdvanceOnChildrenTerminal` gate: create a package child case, complete it, and the blog case auto-advances into `final_review`.
|
|
|
|
```sh
|
|
jq -n --arg parent "$BLOG_CASE" '{
|
|
items: [
|
|
{
|
|
caseKey: "blog-assembly-package",
|
|
title: "Assembled blog package",
|
|
parentCaseId: $parent,
|
|
stageKey: "assembly",
|
|
fields: { packageType: "blog", assembledFrom: ["blog-hero-image", "blog-social-card"] }
|
|
}
|
|
]
|
|
}' > /tmp/assembly-cases.json
|
|
|
|
paperclipai pipelines ingest-batch \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CONTENT_PIPELINE" \
|
|
--file /tmp/assembly-cases.json \
|
|
--json | tee /tmp/assembly-cases-result.json
|
|
|
|
export ASSEMBLY_CASE="$(jq -r '.[0].case.id' /tmp/assembly-cases-result.json)"
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$ASSEMBLY_CASE" --to published --expected-version 1 --reason "Assembly complete."
|
|
```
|
|
|
|
## Step 7: Blocker Guard
|
|
|
|
The tweet is blocked by the blog case through `blockedByCaseKeys`. This transition fails with `409 code=blocked` until the blog reaches a `done` terminal stage.
|
|
|
|
```sh
|
|
paperclipai pipelines case transition \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$TWEET_CASE" \
|
|
--to assets \
|
|
--expected-version 1 \
|
|
--reason "Try before upstream blog is published."
|
|
```
|
|
|
|
## Step 8: Final Review Approve
|
|
|
|
Approve the blog in Final Review, then publish it.
|
|
|
|
```sh
|
|
export BLOG_REVIEW_VERSION="$(paperclipai pipelines case get -C "$PAPERCLIP_COMPANY_ID" "$BLOG_CASE" --json | jq -r '.case.version')"
|
|
|
|
paperclipai pipelines case review \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--approve \
|
|
--expected-version "$BLOG_REVIEW_VERSION"
|
|
|
|
paperclipai pipelines case transition \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$BLOG_CASE" \
|
|
--to published \
|
|
--expected-version "$((BLOG_REVIEW_VERSION + 1))" \
|
|
--reason "Approved package published."
|
|
```
|
|
|
|
## Step 9: Final Review Request Changes
|
|
|
|
The changelog demonstrates the edit loop: Final Review requests changes, the same case re-enters `drafting`, the same work references continue, and the case comes back to Final Review for approval.
|
|
|
|
```sh
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CHANGELOG_CASE" --to final_review --expected-version 1 --reason "Draft ready for final review."
|
|
|
|
paperclipai pipelines case review \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CHANGELOG_CASE" \
|
|
--request-changes \
|
|
--reason "Tighten the framing before publishing." \
|
|
--expected-version 2
|
|
|
|
paperclipai pipelines case edit \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CHANGELOG_CASE" \
|
|
--expected-version 3 \
|
|
--summary "Revised changelog entry after requested changes." \
|
|
--fields-json '{"contentType":"changelog","typedWorkRefs":{"draftPath":"workspaces/release/changelog.md"},"changeRequestAddressed":true}'
|
|
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CHANGELOG_CASE" --to final_review --expected-version 4 --reason "Revised draft ready."
|
|
paperclipai pipelines case review -C "$PAPERCLIP_COMPANY_ID" "$CHANGELOG_CASE" --approve --expected-version 5
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CHANGELOG_CASE" --to published --expected-version 6 --reason "Published after request-changes loop."
|
|
```
|
|
|
|
## Step 10: Final Review Drop
|
|
|
|
Now that the blog blocker is done, the tweet can reach Final Review. The reviewer drops it, which is terminal and still counts toward rollup completion.
|
|
|
|
```sh
|
|
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$TWEET_CASE" --to final_review --expected-version 1 --reason "Blog blocker is now done."
|
|
|
|
paperclipai pipelines case review \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$TWEET_CASE" \
|
|
--reject \
|
|
--reason "Drop this tweet; blog already covers the announcement." \
|
|
--expected-version 2
|
|
```
|
|
|
|
## Step 11: Rollup
|
|
|
|
At this point:
|
|
|
|
- content cases are `published` or `dropped`
|
|
- the approved feature case auto-advanced to `covered`
|
|
- the dropped feature case is terminal
|
|
- the release case auto-advanced to `covered`
|
|
|
|
Inspect the release rollup:
|
|
|
|
```sh
|
|
paperclipai pipelines case rollup \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$RELEASE_CASE_ID" \
|
|
--json
|
|
```
|
|
|
|
Expected shape:
|
|
|
|
```json
|
|
{
|
|
"total": 8,
|
|
"done": 5,
|
|
"cancelled": 3,
|
|
"open": 0,
|
|
"complete": true
|
|
}
|
|
```
|
|
|
|
## Step 12: Reflection Feed
|
|
|
|
Reflection can pull provenance from case events:
|
|
|
|
```sh
|
|
paperclipai pipelines case events \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$CHANGELOG_CASE" \
|
|
--json
|
|
```
|
|
|
|
Look for `review_decided` events where `payload.decision` is `request_changes`, `approve`, or `reject`. Rejection and change-request reasons are the feed for improving skills, routine prompts, and pipeline guidance.
|
|
|
|
For rollup provenance:
|
|
|
|
```sh
|
|
paperclipai pipelines case events \
|
|
-C "$PAPERCLIP_COMPANY_ID" \
|
|
"$RELEASE_CASE_ID" \
|
|
--json
|
|
```
|
|
|
|
Look for `children_terminal` followed by the auto `transitioned` event.
|
|
|
|
## Scripted Smoke
|
|
|
|
Run the same flow end to end:
|
|
|
|
```sh
|
|
PAPERCLIP_API_URL=http://localhost:3100 \
|
|
PAPERCLIP_COMPANY_ID=<company-id> \
|
|
PAPERCLIP_API_KEY=<token> \
|
|
pnpm smoke:pipelines-tutorial
|
|
```
|
|
|
|
The smoke asserts:
|
|
|
|
- the three pipelines are created with the expected stages
|
|
- feature review approves one feature and rejects one
|
|
- batch ingest wires `blockedByCaseKeys`
|
|
- readiness uses `suggest-transition` plus acceptance
|
|
- upstream drift posts a system comment to a linked work issue
|
|
- stale edits fail with `409 code=version_conflict`
|
|
- the Assembly child-terminal gate auto-advances the parent into Final Review
|
|
- Final Review approve, request-changes, and drop outcomes all work
|
|
- the release rollup is complete with the expected done/cancelled split
|