mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
docs: rewrite pipelines tutorial smoke
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
99d8d1c890
commit
058d4589d1
2 files changed
+837
-360
No files matched your search
+522
-241
@@ -1,43 +1,191 @@
|
||||
# Pipelines Tutorial: Release to Content
|
||||
# Pipelines Tutorial: Release to Published Content
|
||||
|
||||
This walkthrough uses only the Paperclip CLI to run the release-to-content casework flow:
|
||||
This walkthrough is the CLI/API version of the 12-step release-to-content worked example. It uses three linked pipelines:
|
||||
|
||||
- one release case fans out into 10 content suggestion cases
|
||||
- a review inbox approves 7, rejects 2 with reasons, and edits then approves 1
|
||||
- approved cases fire a drafting routine on stage entry
|
||||
- a human accepts an agent transition suggestion
|
||||
- a blocker prevents premature publishing until the upstream case is done
|
||||
- the release rollup reports a complete done/cancelled split
|
||||
- event history shows provenance for the case
|
||||
- `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 and routines.
|
||||
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 the drafting routine to an existing agent.
|
||||
# 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"
|
||||
```
|
||||
|
||||
Use a unique suffix so the tutorial can be rerun without pipeline key collisions:
|
||||
## 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
|
||||
export RUN_KEY="$(date +%Y%m%d%H%M%S)"
|
||||
export RELEASES_PIPELINE="releases-$RUN_KEY"
|
||||
export CONTENT_PIPELINE="content-$RUN_KEY"
|
||||
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 The Drafting Routine
|
||||
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 approved content",
|
||||
"description": "Use the embedded Pipeline Case Context to draft the approved content case, then transition the case when the draft is ready.",
|
||||
"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",
|
||||
@@ -46,317 +194,440 @@ cat > /tmp/drafting-routine.json <<JSON
|
||||
}
|
||||
JSON
|
||||
|
||||
export DRAFTING_ROUTINE_PAYLOAD="$(jq -c . /tmp/drafting-routine.json)"
|
||||
export DRAFTING_ROUTINE_ID="$(
|
||||
paperclipai routine create \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
--payload-json "$DRAFTING_ROUTINE_PAYLOAD" \
|
||||
--payload-json "$(jq -c . /tmp/drafting-routine.json)" \
|
||||
--json | jq -r '.id'
|
||||
)"
|
||||
```
|
||||
|
||||
## Create Pipelines
|
||||
|
||||
```sh
|
||||
cat > /tmp/releases-stages.json <<'JSON'
|
||||
[
|
||||
{ "key": "released", "name": "Released", "kind": "open", "position": 100 },
|
||||
{ "key": "complete", "name": "Complete", "kind": "done", "position": 900 },
|
||||
{ "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
|
||||
]
|
||||
JSON
|
||||
|
||||
paperclipai pipelines create \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
--key "$RELEASES_PIPELINE" \
|
||||
--name "Releases $RUN_KEY" \
|
||||
--stages-file /tmp/releases-stages.json
|
||||
```
|
||||
|
||||
```sh
|
||||
cat > /tmp/content-stages.json <<'JSON'
|
||||
[
|
||||
{
|
||||
"key": "review",
|
||||
"name": "Human review",
|
||||
"kind": "review",
|
||||
"position": 100,
|
||||
"config": {
|
||||
"approveToStageKey": "drafting",
|
||||
"rejectToStageKey": "cancelled",
|
||||
"requireRejectReason": true,
|
||||
"reviewerKind": "human"
|
||||
}
|
||||
},
|
||||
{ "key": "drafting", "name": "Drafting", "kind": "working", "position": 200 },
|
||||
{ "key": "ready", "name": "Ready to publish", "kind": "working", "position": 300 },
|
||||
{ "key": "published", "name": "Published", "kind": "done", "position": 900 },
|
||||
{ "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
|
||||
]
|
||||
JSON
|
||||
|
||||
paperclipai pipelines create \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
--key "$CONTENT_PIPELINE" \
|
||||
--name "Content $RUN_KEY" \
|
||||
--stages-file /tmp/content-stages.json
|
||||
```
|
||||
|
||||
Configure transitions, guidance, and the drafting automation:
|
||||
|
||||
```sh
|
||||
cat > /tmp/content-transitions.json <<'JSON'
|
||||
{
|
||||
"transitions": [
|
||||
{ "fromStageKey": "review", "toStageKey": "drafting", "label": "approve" },
|
||||
{ "fromStageKey": "review", "toStageKey": "cancelled", "label": "reject" },
|
||||
{ "fromStageKey": "drafting", "toStageKey": "ready", "label": "draft ready" },
|
||||
{ "fromStageKey": "drafting", "toStageKey": "published", "label": "publish directly" },
|
||||
{ "fromStageKey": "drafting", "toStageKey": "cancelled", "label": "cancel" },
|
||||
{ "fromStageKey": "ready", "toStageKey": "published", "label": "publish" }
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
paperclipai pipelines set-transitions \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CONTENT_PIPELINE" \
|
||||
--file /tmp/content-transitions.json
|
||||
|
||||
cat > /tmp/content-guidance.md <<'MD'
|
||||
# Content pipeline guidance
|
||||
|
||||
Approve content suggestions that can teach users what changed and reject suggestions
|
||||
that are too narrow, duplicative, or disconnected from the release.
|
||||
MD
|
||||
|
||||
paperclipai pipelines guidance put \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CONTENT_PIPELINE" \
|
||||
--file /tmp/content-guidance.md
|
||||
|
||||
paperclipai pipelines set-automation \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CONTENT_PIPELINE" \
|
||||
--stage drafting \
|
||||
--routine "$DRAFTING_ROUTINE_ID"
|
||||
--routine "$DRAFTING_ROUTINE_ID" \
|
||||
--note "Template-versioned with the routine prompt."
|
||||
```
|
||||
|
||||
## Ingest The Release And Suggestions
|
||||
**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" \
|
||||
"$RELEASES_PIPELINE" \
|
||||
"$RELEASE_PIPELINE" \
|
||||
--case-key "release-$RUN_KEY" \
|
||||
--title "Release $RUN_KEY: Pipeline primitive" \
|
||||
--summary "A release case that fans out into content suggestions." \
|
||||
--fields-json '{"release":"v0.pipeline-tutorial"}' \
|
||||
--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 the 10 parented suggestion cases. This batch file is included verbatim; only `$RELEASE_CASE_ID` is substituted by the shell.
|
||||
Create two feature cases parented to the release. One is approved, one is dropped.
|
||||
|
||||
```sh
|
||||
jq -n --arg parent "$RELEASE_CASE_ID" '{
|
||||
items: [
|
||||
{ caseKey: "suggestion-01", title: "Launch blog post", summary: "Explain the release end to end.", fields: { channel: "blog" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-02", title: "Product changelog", summary: "Concise changelog entry.", fields: { channel: "docs" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-03", title: "Launch tweet", summary: "Short tweet after the blog is ready.", fields: { channel: "x" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-04", title: "Customer email", summary: "Email announcement.", fields: { channel: "email" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-05", title: "LinkedIn post", summary: "Professional network announcement.", fields: { channel: "linkedin" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-06", title: "Discord update", summary: "Community update.", fields: { channel: "discord" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-07", title: "Release notes", summary: "Detailed release notes.", fields: { channel: "release_notes" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-08", title: "Overly niche post", summary: "Reject: too niche.", fields: { channel: "blog" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-09", title: "Duplicate tweet", summary: "Reject: duplicate.", fields: { channel: "x" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-10", title: "Short-form video", summary: "Needs edit before approval.", fields: { channel: "video" }, parentCaseId: $parent, stageKey: "review" }
|
||||
{
|
||||
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/content-suggestions.json
|
||||
}' > /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-suggestions.json \
|
||||
--json | tee /tmp/content-suggestions-result.json
|
||||
--file /tmp/content-cases.json \
|
||||
--json | tee /tmp/content-cases-result.json
|
||||
```
|
||||
|
||||
## Review The Inbox
|
||||
**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
|
||||
paperclipai pipelines review-inbox \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
--pipeline "$CONTENT_PIPELINE"
|
||||
```
|
||||
|
||||
Approve seven and reject two in bulk:
|
||||
|
||||
```sh
|
||||
case_id() {
|
||||
jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' /tmp/content-suggestions-result.json
|
||||
content_case_id() {
|
||||
jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' /tmp/content-cases-result.json
|
||||
}
|
||||
|
||||
export CASE_01="$(case_id suggestion-01)"
|
||||
export CASE_02="$(case_id suggestion-02)"
|
||||
export CASE_03="$(case_id suggestion-03)"
|
||||
export CASE_04="$(case_id suggestion-04)"
|
||||
export CASE_05="$(case_id suggestion-05)"
|
||||
export CASE_06="$(case_id suggestion-06)"
|
||||
export CASE_07="$(case_id suggestion-07)"
|
||||
export CASE_08="$(case_id suggestion-08)"
|
||||
export CASE_09="$(case_id suggestion-09)"
|
||||
export CASE_10="$(case_id suggestion-10)"
|
||||
export BLOG_CASE="$(content_case_id blog-post)"
|
||||
export CHANGELOG_CASE="$(content_case_id changelog-entry)"
|
||||
export TWEET_CASE="$(content_case_id launch-tweet)"
|
||||
|
||||
jq -n \
|
||||
--arg c1 "$CASE_01" --arg c2 "$CASE_02" --arg c3 "$CASE_03" --arg c4 "$CASE_04" --arg c5 "$CASE_05" \
|
||||
--arg c6 "$CASE_06" --arg c7 "$CASE_07" --arg c8 "$CASE_08" --arg c9 "$CASE_09" '{
|
||||
items: [
|
||||
{ caseId: $c1, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c2, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c3, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c4, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c5, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c6, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c7, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c8, decision: "reject", reason: "Too niche for this launch.", expectedVersion: 1 },
|
||||
{ caseId: $c9, decision: "reject", reason: "Duplicates the launch tweet.", expectedVersion: 1 }
|
||||
]
|
||||
}' > /tmp/review-decisions.json
|
||||
|
||||
paperclipai pipelines review-bulk \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
--file /tmp/review-decisions.json
|
||||
```
|
||||
|
||||
Edit and approve the tenth item:
|
||||
|
||||
```sh
|
||||
paperclipai pipelines case review \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_10" \
|
||||
--approve \
|
||||
--expected-version 1 \
|
||||
--title "Edited short-form video" \
|
||||
--fields-json '{"channel":"video","edited":true}'
|
||||
```
|
||||
|
||||
Approved cases enter `drafting`, which fires the configured routine and links automation issues. Inspect one:
|
||||
|
||||
```sh
|
||||
paperclipai pipelines case get \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_01"
|
||||
```
|
||||
|
||||
## Accept An Agent Suggestion
|
||||
|
||||
```sh
|
||||
export SUGGESTION_ID="$(
|
||||
paperclipai pipelines case suggest \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_01" \
|
||||
--to ready \
|
||||
--rationale "Draft is complete enough to publish." \
|
||||
"$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" \
|
||||
"$CASE_01" \
|
||||
"$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 transition \
|
||||
paperclipai pipelines case edit \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_01" \
|
||||
--to published \
|
||||
"$CHANGELOG_CASE" \
|
||||
--expected-version 3 \
|
||||
--reason "Suggestion accepted and content published."
|
||||
--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."
|
||||
```
|
||||
|
||||
## Demonstrate Blockers And The 409 Guard
|
||||
## Step 10: Final Review Drop
|
||||
|
||||
The tweet waits for the blog post:
|
||||
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 block \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_03" \
|
||||
--by "$CASE_02"
|
||||
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$TWEET_CASE" --to final_review --expected-version 1 --reason "Blog blocker is now done."
|
||||
|
||||
# This fails with code=blocked and prints a recovery hint.
|
||||
paperclipai pipelines case transition \
|
||||
paperclipai pipelines case review \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_03" \
|
||||
--to published \
|
||||
--expected-version 2 \
|
||||
--reason "Try before blog is published."
|
||||
"$TWEET_CASE" \
|
||||
--reject \
|
||||
--reason "Drop this tweet; blog already covers the announcement." \
|
||||
--expected-version 2
|
||||
```
|
||||
|
||||
Resolve the blocker and publish the tweet:
|
||||
## 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 transition \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_02" \
|
||||
--to published \
|
||||
--expected-version 2 \
|
||||
--reason "Blog published; unblocks tweet."
|
||||
|
||||
paperclipai pipelines case transition \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_03" \
|
||||
--to published \
|
||||
--expected-version 2 \
|
||||
--reason "Blocker resolved."
|
||||
```
|
||||
|
||||
## Complete The Rollup
|
||||
|
||||
Cancel one approved case, publish the rest, then inspect the release rollup:
|
||||
|
||||
```sh
|
||||
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_04" --to cancelled --expected-version 2 --reason "Cancelled after review."
|
||||
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_05" --to published --expected-version 2 --reason "Published."
|
||||
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_06" --to published --expected-version 2 --reason "Published."
|
||||
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_07" --to published --expected-version 2 --reason "Published."
|
||||
paperclipai pipelines case transition -C "$PAPERCLIP_COMPANY_ID" "$CASE_10" --to published --expected-version 3 --reason "Edited and published."
|
||||
|
||||
paperclipai pipelines case rollup \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$RELEASE_CASE_ID" \
|
||||
--json
|
||||
```
|
||||
|
||||
Expected rollup:
|
||||
Expected shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 10,
|
||||
"done": 7,
|
||||
"total": 8,
|
||||
"done": 5,
|
||||
"cancelled": 3,
|
||||
"open": 0,
|
||||
"complete": true
|
||||
}
|
||||
```
|
||||
|
||||
## Dump Provenance
|
||||
## Step 12: Reflection Feed
|
||||
|
||||
Reflection can pull provenance from case events:
|
||||
|
||||
```sh
|
||||
paperclipai pipelines case events \
|
||||
-C "$PAPERCLIP_COMPANY_ID" \
|
||||
"$CASE_01" \
|
||||
"$CHANGELOG_CASE" \
|
||||
--json
|
||||
```
|
||||
|
||||
Every event includes an actor field. Human actions record a user actor, agent actions require a run id, and system actions record `system`. For this case, look for `ingested`, `review_decided`, `transition_suggested`, `suggestion_resolved`, and `transitioned`.
|
||||
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
|
||||
|
||||
The same flow is available as a smoke script:
|
||||
Run the same flow end to end:
|
||||
|
||||
```sh
|
||||
PAPERCLIP_API_URL=http://localhost:3100 \
|
||||
@@ -365,4 +636,14 @@ PAPERCLIP_API_KEY=<token> \
|
||||
pnpm smoke:pipelines-tutorial
|
||||
```
|
||||
|
||||
The smoke assigns the drafting routine to `DRAFTING_AGENT_ID` when set. If it is not set, it uses `PAPERCLIP_AGENT_ID` when present, otherwise the first non-terminated company agent returned by the CLI, so the routine automation can create linked drafting issues.
|
||||
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
|
||||
@@ -6,12 +6,15 @@ if ! command -v jq >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
: "${PAPERCLIP_API_URL:?Set PAPERCLIP_API_URL for the target dev instance.}"
|
||||
: "${PAPERCLIP_API_KEY:?Set PAPERCLIP_API_KEY for the target dev instance.}"
|
||||
: "${PAPERCLIP_COMPANY_ID:?Set PAPERCLIP_COMPANY_ID for the target dev company.}"
|
||||
|
||||
read -r -a PC_CMD <<< "${PAPERCLIPAI_CMD:-pnpm --silent paperclipai}"
|
||||
RUN_KEY="${PIPELINE_SMOKE_KEY:-$(date +%Y%m%d%H%M%S)}"
|
||||
RELEASES_KEY="releases-${RUN_KEY}"
|
||||
CONTENT_KEY="content-${RUN_KEY}"
|
||||
RELEASE_PIPELINE="release-coverage-${RUN_KEY}"
|
||||
FEATURE_PIPELINE="feature-content-${RUN_KEY}"
|
||||
CONTENT_PIPELINE="content-production-${RUN_KEY}"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
@@ -23,16 +26,35 @@ pc_json() {
|
||||
"${PC_CMD[@]}" "$@" --json -C "$PAPERCLIP_COMPANY_ID"
|
||||
}
|
||||
|
||||
api_json() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local body="${3:-}"
|
||||
if [[ -n "$body" ]]; then
|
||||
curl -sS -X "$method" \
|
||||
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$body" \
|
||||
"${PAPERCLIP_API_URL%/}$path"
|
||||
else
|
||||
curl -sS -X "$method" \
|
||||
-H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
||||
"${PAPERCLIP_API_URL%/}$path"
|
||||
fi
|
||||
}
|
||||
|
||||
pick_agent_id() {
|
||||
if [[ -n "${DRAFTING_AGENT_ID:-}" ]]; then
|
||||
echo "$DRAFTING_AGENT_ID"
|
||||
return
|
||||
fi
|
||||
if [[ -n "${PAPERCLIP_AGENT_ID:-}" ]]; then
|
||||
echo "$PAPERCLIP_AGENT_ID"
|
||||
local company_agent_id
|
||||
company_agent_id="$(pc_json agent list 2>/dev/null | jq -r 'map(select(.status != "terminated"))[0].id // empty')"
|
||||
if [[ -n "$company_agent_id" ]]; then
|
||||
echo "$company_agent_id"
|
||||
return
|
||||
fi
|
||||
pc_json agent list 2>/dev/null | jq -r 'map(select(.status != "terminated"))[0].id // empty'
|
||||
echo ""
|
||||
}
|
||||
|
||||
require_json() {
|
||||
@@ -46,10 +68,58 @@ require_json() {
|
||||
fi
|
||||
}
|
||||
|
||||
cat >"$TMP_DIR/releases-stages.json" <<'JSON'
|
||||
case_id() {
|
||||
local json="$1"
|
||||
local key="$2"
|
||||
jq -r --arg key "$key" '.[] | select(.case.caseKey == $key) | .case.id' <<<"$json"
|
||||
}
|
||||
|
||||
case_version() {
|
||||
pc_json pipelines case get "$1" | jq -r '.case.version'
|
||||
}
|
||||
|
||||
case_stage() {
|
||||
pc_json pipelines case get "$1" | jq -r '.stage.key'
|
||||
}
|
||||
|
||||
cat >"$TMP_DIR/release-stages.json" <<'JSON'
|
||||
[
|
||||
{ "key": "released", "name": "Released", "kind": "open", "position": 100 },
|
||||
{ "key": "complete", "name": "Complete", "kind": "done", "position": 900 },
|
||||
{
|
||||
"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
|
||||
|
||||
cat >"$TMP_DIR/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
|
||||
@@ -57,48 +127,70 @@ JSON
|
||||
cat >"$TMP_DIR/content-stages.json" <<'JSON'
|
||||
[
|
||||
{
|
||||
"key": "review",
|
||||
"name": "Human review",
|
||||
"kind": "review",
|
||||
"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": "drafting",
|
||||
"rejectToStageKey": "cancelled",
|
||||
"approveToStageKey": "publishing",
|
||||
"rejectToStageKey": "dropped",
|
||||
"requestChangesToStageKey": "drafting",
|
||||
"requireRejectReason": true,
|
||||
"reviewerKind": "human"
|
||||
}
|
||||
},
|
||||
{ "key": "drafting", "name": "Drafting", "kind": "working", "position": 200 },
|
||||
{ "key": "ready", "name": "Ready to publish", "kind": "working", "position": 300 },
|
||||
{ "key": "publishing", "name": "Publishing", "kind": "working", "position": 500 },
|
||||
{ "key": "published", "name": "Published", "kind": "done", "position": 900 },
|
||||
{ "key": "cancelled", "name": "Cancelled", "kind": "cancelled", "position": 1000 }
|
||||
{ "key": "dropped", "name": "Dropped", "kind": "cancelled", "position": 1000 }
|
||||
]
|
||||
JSON
|
||||
|
||||
cat >"$TMP_DIR/content-transitions.json" <<'JSON'
|
||||
cat >"$TMP_DIR/release-transitions.json" <<'JSON'
|
||||
{
|
||||
"enforceTransitions": true,
|
||||
"transitions": [
|
||||
{ "fromStageKey": "review", "toStageKey": "drafting", "label": "approve" },
|
||||
{ "fromStageKey": "review", "toStageKey": "cancelled", "label": "reject" },
|
||||
{ "fromStageKey": "drafting", "toStageKey": "ready", "label": "draft ready" },
|
||||
{ "fromStageKey": "drafting", "toStageKey": "published", "label": "publish directly" },
|
||||
{ "fromStageKey": "drafting", "toStageKey": "cancelled", "label": "cancel" },
|
||||
{ "fromStageKey": "ready", "toStageKey": "published", "label": "publish" }
|
||||
{ "fromStageKey": "intake", "toStageKey": "covered", "label": "all features terminal" },
|
||||
{ "fromStageKey": "intake", "toStageKey": "cancelled", "label": "cancel release coverage" }
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
cat >"$TMP_DIR/guidance.md" <<'MD'
|
||||
# Content pipeline guidance
|
||||
cat >"$TMP_DIR/content-guidance.md" <<'MD'
|
||||
# Content Production guidance
|
||||
|
||||
Approve content suggestions that can teach users what changed and reject suggestions
|
||||
that are too narrow, duplicative, or disconnected from the release.
|
||||
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
|
||||
|
||||
agent_id="$(pick_agent_id)"
|
||||
routine_payload="$(jq -cn --arg agentId "$agent_id" '{
|
||||
title: "Pipeline smoke drafting routine",
|
||||
description: "Draft the approved content case using the embedded Pipeline Case Context, then transition the case when the draft is ready.",
|
||||
title: "Pipeline tutorial drafting routine",
|
||||
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",
|
||||
@@ -107,99 +199,112 @@ routine_payload="$(jq -cn --arg agentId "$agent_id" '{
|
||||
routine="$(pc_json routine create --payload-json "$routine_payload")"
|
||||
routine_id="$(jq -r '.id' <<<"$routine")"
|
||||
|
||||
releases="$(pc_json pipelines create --key "$RELEASES_KEY" --name "Smoke Releases $RUN_KEY" --stages-file "$TMP_DIR/releases-stages.json")"
|
||||
content="$(pc_json pipelines create --key "$CONTENT_KEY" --name "Smoke Content $RUN_KEY" --stages-file "$TMP_DIR/content-stages.json")"
|
||||
require_json "$releases" '.id and (.stages | length == 3)' "Releases pipeline creation failed."
|
||||
require_json "$content" '.id and (.stages | length == 5)' "Content pipeline creation failed."
|
||||
release_pipeline="$(pc_json pipelines create --key "$RELEASE_PIPELINE" --name "Smoke Release Coverage $RUN_KEY" --stages-file "$TMP_DIR/release-stages.json")"
|
||||
feature_pipeline="$(pc_json pipelines create --key "$FEATURE_PIPELINE" --name "Smoke Feature Content $RUN_KEY" --stages-file "$TMP_DIR/feature-stages.json")"
|
||||
content_pipeline="$(pc_json pipelines create --key "$CONTENT_PIPELINE" --name "Smoke Content Production $RUN_KEY" --stages-file "$TMP_DIR/content-stages.json")"
|
||||
require_json "$release_pipeline" '.id and (.stages | length == 3)' "Release Coverage pipeline creation failed."
|
||||
require_json "$feature_pipeline" '.id and (.stages | length == 5)' "Feature Content pipeline creation failed."
|
||||
require_json "$content_pipeline" '.id and (.stages | length == 7)' "Content Production pipeline creation failed."
|
||||
|
||||
pc_json pipelines set-transitions "$CONTENT_KEY" --file "$TMP_DIR/content-transitions.json" >/dev/null
|
||||
pc_json pipelines guidance put "$CONTENT_KEY" --file "$TMP_DIR/guidance.md" >/dev/null
|
||||
pc_json pipelines set-automation "$CONTENT_KEY" --stage drafting --routine "$routine_id" >/dev/null
|
||||
pc_json pipelines set-transitions "$RELEASE_PIPELINE" --file "$TMP_DIR/release-transitions.json" >/dev/null
|
||||
pc_json pipelines guidance put "$CONTENT_PIPELINE" --file "$TMP_DIR/content-guidance.md" >/dev/null
|
||||
pc_json pipelines set-automation "$CONTENT_PIPELINE" --stage drafting --routine "$routine_id" --note "Template-versioned with the routine prompt." >/dev/null
|
||||
|
||||
release="$(pc_json pipelines ingest "$RELEASES_KEY" \
|
||||
--case-key "release-$RUN_KEY" \
|
||||
--title "Release $RUN_KEY: Pipeline primitive" \
|
||||
--summary "A release case that fans out into content suggestions." \
|
||||
--fields-json '{"release":"v0.pipeline-smoke","source":"tutorial-smoke"}')"
|
||||
release="$(pc_json pipelines ingest "$RELEASE_PIPELINE" \
|
||||
--case-key "release-${RUN_KEY}" \
|
||||
--stage intake \
|
||||
--title "Release $RUN_KEY: Pipeline primitives" \
|
||||
--summary "Rollup root for the tutorial smoke." \
|
||||
--fields-json '{"release":"v0.pipeline-smoke","templateVersionConvention":"routine-prompt"}')"
|
||||
release_case_id="$(jq -r '.case.id' <<<"$release")"
|
||||
|
||||
pc_json pipelines case edit "$release_case_id" --expected-version 1 --summary "Release case edited by the CLI smoke." >/dev/null
|
||||
claim="$(pc_json pipelines case claim "$release_case_id" --lease-seconds 60)"
|
||||
lease_token="$(jq -r '.leaseToken' <<<"$claim")"
|
||||
pc_json pipelines case release "$release_case_id" --lease-token "$lease_token" >/dev/null
|
||||
|
||||
jq -n --arg parent "$release_case_id" '{
|
||||
items: [
|
||||
{ caseKey: "suggestion-01", title: "Launch blog post", summary: "Explain the release end to end.", fields: { channel: "blog" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-02", title: "Product changelog", summary: "Concise changelog entry.", fields: { channel: "docs" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-03", title: "Launch tweet", summary: "Short tweet after the blog is ready.", fields: { channel: "x" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-04", title: "Customer email", summary: "Email announcement.", fields: { channel: "email" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-05", title: "LinkedIn post", summary: "Professional network announcement.", fields: { channel: "linkedin" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-06", title: "Discord update", summary: "Community update.", fields: { channel: "discord" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-07", title: "Release notes", summary: "Detailed release notes.", fields: { channel: "release_notes" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-08", title: "Overly niche post", summary: "Reject: too niche.", fields: { channel: "blog" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-09", title: "Duplicate tweet", summary: "Reject: duplicate.", fields: { channel: "x" }, parentCaseId: $parent, stageKey: "review" },
|
||||
{ caseKey: "suggestion-10", title: "Short-form video", summary: "Needs edit before approval.", fields: { channel: "video" }, parentCaseId: $parent, stageKey: "review" }
|
||||
{
|
||||
caseKey: "feature-pipelines-ui",
|
||||
title: "Feature: Pipelines UI",
|
||||
summary: "Worth a content package.",
|
||||
parentCaseId: $parent,
|
||||
stageKey: "suggestion_review",
|
||||
fields: { releaseTag: "v0.pipeline-smoke", 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-smoke", source: "release-notes" }
|
||||
}
|
||||
]
|
||||
}' >"$TMP_DIR/suggestions.json"
|
||||
}' >"$TMP_DIR/feature-cases.json"
|
||||
|
||||
batch="$(pc_json pipelines ingest-batch "$CONTENT_KEY" --file "$TMP_DIR/suggestions.json")"
|
||||
require_json "$batch" 'length == 10 and all(.ok == true)' "Batch ingest did not create/find 10 suggestions."
|
||||
features="$(pc_json pipelines ingest-batch "$FEATURE_PIPELINE" --file "$TMP_DIR/feature-cases.json")"
|
||||
require_json "$features" 'length == 2 and all(.ok == true)' "Feature batch ingest did not create two cases."
|
||||
feature_main="$(case_id "$features" feature-pipelines-ui)"
|
||||
feature_dropped="$(case_id "$features" feature-routine-webhooks)"
|
||||
|
||||
case_id() {
|
||||
jq -r --arg key "$1" '.[] | select(.case.caseKey == $key) | .case.id' <<<"$batch"
|
||||
}
|
||||
|
||||
case1="$(case_id suggestion-01)"
|
||||
case2="$(case_id suggestion-02)"
|
||||
case3="$(case_id suggestion-03)"
|
||||
case4="$(case_id suggestion-04)"
|
||||
case5="$(case_id suggestion-05)"
|
||||
case6="$(case_id suggestion-06)"
|
||||
case7="$(case_id suggestion-07)"
|
||||
case8="$(case_id suggestion-08)"
|
||||
case9="$(case_id suggestion-09)"
|
||||
case10="$(case_id suggestion-10)"
|
||||
|
||||
inbox="$(pc_json pipelines review-inbox --pipeline "$CONTENT_KEY")"
|
||||
require_json "$inbox" 'length == 10' "Review inbox should contain the 10 suggestion cases."
|
||||
|
||||
jq -n \
|
||||
--arg c1 "$case1" --arg c2 "$case2" --arg c3 "$case3" --arg c4 "$case4" --arg c5 "$case5" \
|
||||
--arg c6 "$case6" --arg c7 "$case7" --arg c8 "$case8" --arg c9 "$case9" '{
|
||||
jq -n --arg main "$feature_main" --arg dropped "$feature_dropped" '{
|
||||
items: [
|
||||
{ caseId: $c1, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c2, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c3, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c4, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c5, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c6, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c7, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $c8, decision: "reject", reason: "Too niche for this launch.", expectedVersion: 1 },
|
||||
{ caseId: $c9, decision: "reject", reason: "Duplicates the launch tweet.", expectedVersion: 1 }
|
||||
{ caseId: $main, decision: "approve", expectedVersion: 1 },
|
||||
{ caseId: $dropped, decision: "reject", reason: "Fold webhooks into the broader launch post.", expectedVersion: 1 }
|
||||
]
|
||||
}' >"$TMP_DIR/decisions.json"
|
||||
}' >"$TMP_DIR/feature-review.json"
|
||||
feature_review="$(pc_json pipelines review-bulk --file "$TMP_DIR/feature-review.json")"
|
||||
require_json "$feature_review" '.results | length == 2 and all(.ok == true)' "Feature review decisions failed."
|
||||
require_json "$(pc_json pipelines case get "$feature_main")" '.stage.key == "producing" and .case.version == 2' "Approved feature should enter Producing."
|
||||
require_json "$(pc_json pipelines case get "$feature_dropped")" '.stage.key == "cancelled" and .case.terminalKind == "cancelled"' "Rejected feature should be cancelled."
|
||||
|
||||
bulk="$(pc_json pipelines review-bulk --file "$TMP_DIR/decisions.json")"
|
||||
require_json "$bulk" '.results | length == 9 and all(.ok == true)' "Bulk review failed."
|
||||
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_DIR/content-cases.json"
|
||||
|
||||
pc_json pipelines case review "$case10" \
|
||||
--approve \
|
||||
--expected-version 1 \
|
||||
--title "Edited short-form video" \
|
||||
--fields-json '{"channel":"video","edited":true}' >/dev/null
|
||||
content_cases="$(pc_json pipelines ingest-batch "$CONTENT_PIPELINE" --file "$TMP_DIR/content-cases.json")"
|
||||
require_json "$content_cases" 'length == 3 and all(.ok == true)' "Content batch ingest did not create three cases."
|
||||
blog_case="$(case_id "$content_cases" blog-post)"
|
||||
changelog_case="$(case_id "$content_cases" changelog-entry)"
|
||||
tweet_case="$(case_id "$content_cases" launch-tweet)"
|
||||
|
||||
case1_detail="$(pc_json pipelines case get "$case1")"
|
||||
require_json "$case1_detail" '(.links | length) >= 1' "Approved case should have an automation issue link."
|
||||
|
||||
suggestion="$(pc_json pipelines case suggest "$case1" --to ready --rationale "Draft is complete enough to publish." --confidence 0.9)"
|
||||
suggestion_id="$(jq -r '.suggestion.id' <<<"$suggestion")"
|
||||
pc_json pipelines case resolve-suggestion "$case1" --suggestion "$suggestion_id" --accept --expected-version 2 >/dev/null
|
||||
pc_json pipelines case transition "$case1" --to published --expected-version 3 --reason "Suggestion accepted and content published." >/dev/null
|
||||
|
||||
pc_json pipelines case block "$case3" --by "$case2" >/dev/null
|
||||
set +e
|
||||
blocked_output="$(pc_json pipelines case transition "$case3" --to published --expected-version 2 --reason "Try before blog is published." 2>&1)"
|
||||
blocked_output="$(pc_json pipelines case transition "$tweet_case" --to assets --expected-version 1 --reason "Try before upstream blog is published." 2>&1)"
|
||||
blocked_status=$?
|
||||
set -e
|
||||
if [[ "$blocked_status" -eq 0 || "$blocked_output" != *"code=blocked"* ]]; then
|
||||
@@ -207,22 +312,113 @@ if [[ "$blocked_status" -eq 0 || "$blocked_output" != *"code=blocked"* ]]; then
|
||||
echo "$blocked_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
pc_json pipelines case transition "$case2" --to published --expected-version 2 --reason "Blog published; unblocks tweet." >/dev/null
|
||||
pc_json pipelines case transition "$case3" --to published --expected-version 2 --reason "Blocker resolved." >/dev/null
|
||||
|
||||
pc_json pipelines case transition "$case4" --to cancelled --expected-version 2 --reason "Cancelled after review." >/dev/null
|
||||
pc_json pipelines case transition "$case5" --to published --expected-version 2 --reason "Published." >/dev/null
|
||||
pc_json pipelines case transition "$case6" --to published --expected-version 2 --reason "Published." >/dev/null
|
||||
pc_json pipelines case transition "$case7" --to published --expected-version 2 --reason "Published." >/dev/null
|
||||
pc_json pipelines case transition "$case10" --to published --expected-version 3 --reason "Edited and published." >/dev/null
|
||||
work_issue="$(pc_json issue create \
|
||||
--title "Smoke work issue for launch tweet $RUN_KEY" \
|
||||
--description "Receives drift comments from the upstream blog case." \
|
||||
--status todo \
|
||||
--priority low)"
|
||||
work_issue_id="$(jq -r '.id' <<<"$work_issue")"
|
||||
api_json POST "/api/cases/$tweet_case/issue-links" "$(jq -cn --arg issueId "$work_issue_id" '{ issueId: $issueId, role: "work" }')" >/dev/null
|
||||
|
||||
suggestion="$(pc_json pipelines case suggest "$blog_case" --to assets --rationale "Draft is stable enough to brief asset work." --confidence 0.9)"
|
||||
suggestion_id="$(jq -r '.suggestion.id' <<<"$suggestion")"
|
||||
pc_json pipelines case resolve-suggestion "$blog_case" --suggestion "$suggestion_id" --accept --expected-version 1 >/dev/null
|
||||
require_json "$(pc_json pipelines case get "$blog_case")" '.stage.key == "assets" and .case.version == 2' "Accepted readiness suggestion should move blog to Assets."
|
||||
|
||||
pc_json pipelines case edit "$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"}' >/dev/null
|
||||
|
||||
comments="$(api_json GET "/api/issues/$work_issue_id/comments")"
|
||||
require_json "$comments" 'length >= 1 and (.[-1].body | contains("changed (v2"))' "Dependent work issue did not receive the upstream drift comment."
|
||||
|
||||
set +e
|
||||
conflict_output="$(pc_json pipelines case edit "$blog_case" --expected-version 2 --title "Stale edit" 2>&1)"
|
||||
conflict_status=$?
|
||||
set -e
|
||||
if [[ "$conflict_status" -eq 0 || "$conflict_output" != *"code=version_conflict"* ]]; then
|
||||
echo "Expected stale edit to fail with code=version_conflict." >&2
|
||||
echo "$conflict_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
blog_version="$(case_version "$blog_case")"
|
||||
|
||||
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_DIR/asset-cases.json"
|
||||
asset_cases="$(pc_json pipelines ingest-batch "$CONTENT_PIPELINE" --file "$TMP_DIR/asset-cases.json")"
|
||||
require_json "$asset_cases" 'length == 2 and all(.ok == true)' "Asset batch ingest failed."
|
||||
asset_hero="$(case_id "$asset_cases" blog-hero-image)"
|
||||
asset_card="$(case_id "$asset_cases" blog-social-card)"
|
||||
|
||||
pc_json pipelines case transition "$asset_hero" --to published --expected-version 1 --reason "Hero image done." >/dev/null
|
||||
pc_json pipelines case transition "$asset_card" --to dropped --expected-version 1 --reason "Social card not needed." >/dev/null
|
||||
blog_assets_version="$(case_version "$blog_case")"
|
||||
pc_json pipelines case transition "$blog_case" --to assembly --expected-version "$blog_assets_version" --reason "Assets complete; assemble the package." >/dev/null
|
||||
require_json "$(pc_json pipelines case get "$blog_case")" '.stage.key == "assembly"' "Blog should enter Assembly after assets are complete."
|
||||
|
||||
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_DIR/assembly-cases.json"
|
||||
assembly_cases="$(pc_json pipelines ingest-batch "$CONTENT_PIPELINE" --file "$TMP_DIR/assembly-cases.json")"
|
||||
require_json "$assembly_cases" 'length == 1 and all(.ok == true)' "Assembly batch ingest failed."
|
||||
assembly_package="$(case_id "$assembly_cases" blog-assembly-package)"
|
||||
pc_json pipelines case transition "$assembly_package" --to published --expected-version 1 --reason "Assembly complete." >/dev/null
|
||||
require_json "$(pc_json pipelines case get "$blog_case")" '.stage.key == "final_review"' "Blog should auto-advance from Assembly to Final Review."
|
||||
|
||||
blog_review_version="$(case_version "$blog_case")"
|
||||
pc_json pipelines case review "$blog_case" --approve --expected-version "$blog_review_version" >/dev/null
|
||||
pc_json pipelines case transition "$blog_case" --to published --expected-version "$((blog_review_version + 1))" --reason "Approved package published." >/dev/null
|
||||
|
||||
pc_json pipelines case transition "$changelog_case" --to final_review --expected-version 1 --reason "Draft ready for final review." >/dev/null
|
||||
pc_json pipelines case review "$changelog_case" --request-changes --reason "Tighten the framing before publishing." --expected-version 2 >/dev/null
|
||||
require_json "$(pc_json pipelines case get "$changelog_case")" '.stage.key == "drafting" and .case.version == 3' "Request changes should return changelog to Drafting."
|
||||
pc_json pipelines case edit "$changelog_case" \
|
||||
--expected-version 3 \
|
||||
--summary "Revised changelog entry after requested changes." \
|
||||
--fields-json '{"contentType":"changelog","typedWorkRefs":{"draftPath":"workspaces/release/changelog.md"},"changeRequestAddressed":true}' >/dev/null
|
||||
pc_json pipelines case transition "$changelog_case" --to final_review --expected-version 4 --reason "Revised draft ready." >/dev/null
|
||||
pc_json pipelines case review "$changelog_case" --approve --expected-version 5 >/dev/null
|
||||
pc_json pipelines case transition "$changelog_case" --to published --expected-version 6 --reason "Published after request-changes loop." >/dev/null
|
||||
|
||||
pc_json pipelines case transition "$tweet_case" --to final_review --expected-version 1 --reason "Blog blocker is now done." >/dev/null
|
||||
pc_json pipelines case review "$tweet_case" --reject --reason "Drop this tweet; blog already covers the announcement." --expected-version 2 >/dev/null
|
||||
|
||||
require_json "$(pc_json pipelines case get "$feature_main")" '.stage.key == "covered" and .case.terminalKind == "done"' "Feature case should be covered after content children are terminal."
|
||||
require_json "$(pc_json pipelines case get "$release_case_id")" '.stage.key == "covered" and .case.terminalKind == "done"' "Release case should be covered after feature children are terminal."
|
||||
|
||||
rollup="$(pc_json pipelines case rollup "$release_case_id")"
|
||||
require_json "$rollup" '.complete == true and .done == 7 and .cancelled == 3 and .open == 0' "Release rollup did not report the expected done/cancelled split."
|
||||
require_json "$rollup" '.complete == true and .done == 5 and .cancelled == 3 and .open == 0 and .total == 8' "Release rollup did not report the expected done/cancelled split."
|
||||
|
||||
events="$(pc_json pipelines case events "$case1")"
|
||||
require_json "$events" '.items | map(.type) | index("ingested") and index("review_decided") and index("automation_executed") and index("transition_suggested") and index("suggestion_resolved") and index("transitioned")' "Event history is missing expected provenance events."
|
||||
events="$(pc_json pipelines case events "$changelog_case")"
|
||||
require_json "$events" '([.items[].type] | index("review_decided")) and ([.items[] | select(.type == "review_decided") | .payload.decision] | index("request_changes") and index("approve"))' "Changelog event history is missing request-changes and approval decisions."
|
||||
|
||||
pc_json pipelines cases "$CONTENT_KEY" --parent "$release_case_id" --terminal >/dev/null
|
||||
pc_json pipelines case open-conversation "$case3" >/dev/null
|
||||
release_events="$(pc_json pipelines case events "$release_case_id")"
|
||||
require_json "$release_events" '.items | map(.type) | index("children_terminal") and index("transitioned")' "Release event history is missing rollup provenance."
|
||||
|
||||
echo "Pipeline tutorial smoke passed for $RUN_KEY"
|
||||
Reference in new issue
Block a user