Remove the Pipelines experiment and preserve its data (#16834)

## Thinking Path

> - Paperclip manages AI agents and their work.
> - Pipelines was an unused experiment with its own UI, API, CLI, and
automation.
> - Keeping it leaves unsupported product surfaces and extra task-detail
queries.
> - Removing the experiment must preserve data that users already saved.
> - This pull request removes the feature and keeps its database schema
and migration history.
> - Cases and Routines remain available as separate features.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Remove the unused Pipelines experiment across the application.

**Current behavior**

The experiment includes pipeline boards, stage settings, case
automation, review queues, learnings, permissions, and CLI commands.
Task detail also queries pipeline links.

**Proposed behavior**

Remove all these product surfaces and their implementation. Preserve
existing tables, records, and documents. Ignore the retired boolean in
older managed configurations so upgrades still start.

**Breaking changes**

Pipeline URLs, API endpoints, CLI commands, and `pipelines:write` are
removed. Task detail no longer includes the old `linkedCases` field. The
independent Cases API remains available. No database migration or
cleanup is included.

Related feature work: #7907, #9110, #10905, #11375, #13004, #14671,
#16133. Related issues: #9663, #10310, #10312. This removal supersedes
the feature; it does not implement those pending enhancements or delete
their saved data.

## What Changed

- Remove Pipeline UI pages, navigation, gates, clients, query keys,
helpers, and unused design tokens.
- Remove Pipeline REST routes, services, automation dispatch,
task-detail queries, and OpenAPI discovery.
- Remove the CLI commands, shared contracts, experimental flag, catalog
entry, and permission defaults.
- Remove feature-specific tests, tutorials, smoke scripts, maps, and
Storybook examples.
- Preserve database schema, migration history, and the reserved document
key used by old records.
- Keep Cases routes independent and cover obsolete stored and managed
settings.

## Verification

- Passed: `pnpm build` and `pnpm -r typecheck`.
- Passed: targeted settings, managed configuration, sidebar, settings
UI, routing, and feature-catalog tests. A first sidebar run caught a
shared Workspaces icon import; the import was restored and the sidebar
rerun passed.
- Passed: 40 tests covering Cases routes, OpenAPI discovery, retained
Pipeline table constraints, and schema snapshot drift.
- Passed: token gates, map validation, generated API metadata check,
Storybook catalog validation, and 28 map/catalog tests.
- Passed: isolated local browser verification of navigation and
Experimental settings with Pipelines absent; Cases and Routines still
open normally. Live GET/POST requests to retired Pipeline endpoints
return 404, and the retired setting is absent. CLI help has no Pipeline
commands.
- Passed: 22 access-service and agent-default-grant tests after
restoring unrelated permission assertions flagged in review.
- Local full run: `pnpm test:run` reported one failure in the unchanged
workspace-runtime port-allocation test. That test passed in an isolated
rerun. The long local run was stopped after full CI passed; it is not
reported as a complete local pass.
- Passed: all 56 checks on `def9181f3a4491d95fca1d38e9739f5b821aae3e` (2
conditional checks skipped), including build, typecheck, test shards,
and browser suites. Eleven interrupted CI jobs reported “The operation
was canceled”; their one-time retry passed. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/39255211006).
- Greptile: 5/5 on the final commit, with the only review finding fixed
and resolved.
- Sandbox socket restrictions affected the first build/API test
attempts. The same commands ran with local sockets available.

## Risks

- Existing Pipeline clients must stop using the removed endpoints and
commands.
- Existing data remains in the database and backups, but the removed UI
no longer displays it.
- Historical boolean `enablePipelines` values are ignored. They cannot
restore the feature.
- Cases and Routines must retain their separate behavior. Targeted route
tests cover this boundary.

## Model Used

OpenAI Codex, GPT-6, with repository inspection, editing, shell
execution, and browser verification. The exact deployed model ID and
context-window size are not exposed in this session.

## 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)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-19 08:09:46 -05:00
1 parent f96a1e6406
commit 9077748894
95 files changed
+136 -30173

No files matched your search

+1 -8
View File
@@ -69,7 +69,7 @@ const NEW_TOKENS = [
{ name: "hex-64748b", value: "#64748b", comment: "ActivityCharts.tsx status 'backlog'; also the widely-repeated 'no project assigned' muted-slate fallback color (TOKEN-AUDIT.md 1.3) across Routines/MarkdownEditor/RoutineRunVariablesDialog/RoutineList/IssueColumns/editable-sections." },
// --- Project-color-fallback indigo cluster (TOKEN-AUDIT.md 1.3) ---
{ name: "hex-6366f1", value: "#6366f1", comment: "Project-color-fallback indigo seed default (ProjectDetail/PipelineSettings/IssueProperties/NewIssueDialog) — new-project-color-picker-seed family per TOKEN-AUDIT.md 1.3." },
{ name: "hex-6366f1", value: "#6366f1", comment: "Project-color-fallback indigo seed default (ProjectDetail/IssueProperties/NewIssueDialog) — new-project-color-picker-seed family per TOKEN-AUDIT.md 1.3." },
// --- Gradient tokens (verbatim, one per distinct gradient string; DESIGN.md: mint, don't collapse) ---
{ name: "gradient-extract-1", value: "linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))", comment: "Dashboard.tsx budget-alert card gradient." },
@@ -194,13 +194,6 @@ const SITES = [
['backgroundColor: project.color ?? "#6366f1"', 'backgroundColor: project.color ?? "var(--hex-6366f1)"'],
],
},
{
file: "pages/PipelineSettings.tsx",
replaceAll: [
['backgroundColor: selectedAutomationProject.color ?? "#6366f1"', 'backgroundColor: selectedAutomationProject.color ?? "var(--hex-6366f1)"'],
['backgroundColor: project?.color ?? "#6366f1"', 'backgroundColor: project?.color ?? "var(--hex-6366f1)"'],
],
},
{
file: "components/issue-properties/IssueProperties.tsx",
replaceAll: [
-1
View File
@@ -25,7 +25,6 @@
"tests/e2e/nux-phase4-screenshots.spec.ts": 18776,
"tests/e2e/onboarding.spec.ts": 21295,
"tests/e2e/paused-composer.spec.ts": 33211,
"tests/e2e/pipelines-tutorial-flow.spec.ts": 30545,
"tests/e2e/planning-mode-visual-verification.spec.ts": 26193,
"tests/e2e/project-repositories.spec.ts": 21634,
"tests/e2e/runner-e2e-dashboard.spec.ts": 2028,
@@ -398,7 +398,6 @@
"server/src/__tests__/pi-local-adapter-environment.test.ts": 912,
"server/src/__tests__/pi-local-execute.test.ts": 1068,
"server/src/__tests__/pi-local-skill-sync.test.ts": 806,
"server/src/__tests__/pipelines-service.test.ts": 13114,
"server/src/__tests__/plan-task-fixture.test.ts": 12967,
"server/src/__tests__/plugin-access-authorization-host-services.test.ts": 10822,
"server/src/__tests__/plugin-agent-sessions.test.ts": 2409,
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
const root = new URL("../", import.meta.url);
const metadata = {};
for (const name of ["pipelines", "cases", "smoke-lab"]) {
for (const name of ["cases", "smoke-lab"]) {
const source = await readFile(new URL(`server/src/routes/${name}.ts`, root), "utf8");
const routes = [...source.matchAll(/router\.(get|post|put|patch|delete)\(\s*"([^"]+)"/g)];
for (const [index, route] of routes.entries()) {
-1
View File
@@ -124,7 +124,6 @@
"server/src/__tests__/openclaw-invite-prompt-route.test.ts": 5131,
"server/src/__tests__/opencode-local-adapter-environment.test.ts": 8676,
"server/src/__tests__/permissions-upgrade-boundary-routes.test.ts": 16917,
"server/src/__tests__/pipelines-routes.test.ts": 20408,
"server/src/__tests__/plugin-install-route-security.test.ts": 13924,
"server/src/__tests__/plugin-routes-authz.test.ts": 5693,
"server/src/__tests__/plugin-scoped-api-routes.test.ts": 6245,
-424
View File
@@ -1,424 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v jq >/dev/null 2>&1; then
echo "jq is required for the pipeline tutorial smoke." >&2
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)}"
RELEASE_PIPELINE="release-coverage-${RUN_KEY}"
FEATURE_PIPELINE="feature-content-${RUN_KEY}"
CONTENT_PIPELINE="content-production-${RUN_KEY}"
TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
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
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
echo ""
}
require_json() {
local json="$1"
local filter="$2"
local message="$3"
if ! jq -e "$filter" >/dev/null <<<"$json"; then
echo "$message" >&2
echo "$json" | jq . >&2
exit 1
fi
}
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": "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
cat >"$TMP_DIR/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
cat >"$TMP_DIR/release-transitions.json" <<'JSON'
{
"enforceTransitions": true,
"transitions": [
{ "fromStageKey": "intake", "toStageKey": "covered", "label": "all features terminal" },
{ "fromStageKey": "intake", "toStageKey": "cancelled", "label": "cancel release coverage" }
]
}
JSON
cat >"$TMP_DIR/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
agent_id="$(pick_agent_id)"
routine_payload="$(jq -cn --arg agentId "$agent_id" '{
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",
catchUpPolicy: "skip_missed"
} + (if $agentId != "" then { assigneeAgentId: $agentId } else {} end)')"
routine="$(pc_json routine create --payload-json "$routine_payload")"
routine_id="$(jq -r '.id' <<<"$routine")"
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 "$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 "$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")"
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-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/feature-cases.json"
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)"
jq -n --arg main "$feature_main" --arg dropped "$feature_dropped" '{
items: [
{ caseId: $main, decision: "approve", expectedVersion: 1 },
{ caseId: $dropped, decision: "reject", reason: "Fold webhooks into the broader launch post.", expectedVersion: 1 }
]
}' >"$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."
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"
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)"
set +e
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
echo "Expected blocked transition to fail with code=blocked." >&2
echo "$blocked_output" >&2
exit 1
fi
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 == 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 "$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."
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"