PolyPrint × Agentic SDLC

A software development lifecycle with no humans in the hot path.

Linear ticket → orchestrator picks it up → Claude Code writes the feature in an isolated worktree → CodeRabbit reviews it → auto-merge to main. The system runs continuously, handles dozens of tickets in parallel, and costs roughly zero minutes of direct human time per ticket.

8 specialized sub-agents 3 enforcement gates SvelteKit 5 + Cloudflare Workers Durable Objects + D1 Vitest + Playwright
~6 min
Median ticket → merged
Measured across feature phases
3–5×
Parallel Claudes
DAG-scheduled concurrency cap
~0%
Fix-ticket ratio
After gates; down from ~60% pre-gate
60s
Orchestrator tick
Exact-cadence sleep
The Pipeline

End-to-end ticket lifecycle

Every ticket flows through the same path. The orchestrator on PKX owns scheduling and spawning. Claude Code owns implementation. CodeRabbit owns review. Auto-merge owns landing.

Scheduling layer

Linear GraphQL Queries Ready tickets ordered by createdAt ASC. Up to 100 per tick.
DAG rules Phase gating · file overlap · critical paths · target_branch serialization · post-merge cooldown.
Worktree isolation /home/paulk/polyprint-worktrees/pol-N per ticket. node_modules symlinked from main when package-lock matches.
Concurrency cap 5 in-flight Claudes max. DAG may defer for file or critical-path collisions.
Linear ticket createdReady

Title has [Phase N] prefix. Body may include target_branch: token.

Orchestrator tick60s

Python daemon on PKX, systemd service. DAG scheduler decides: pick up, defer, or escalate.

Spawn Claude Codesonnet

Headless, --permission-mode bypassPermissions. Worktree has CLAUDE.md + failure-modes.md loaded.

Researcher sub-agenthaiku

Invoked in parallel when ticket touches unfamiliar framework surface. Returns ~500-word distilled brief.

Implementation (inline)

Main Claude writes the feature. Writes are sequential — reads and verifies are parallel.

Pre-push-verifier sub-agenthaiku

Runs lint + check + test. PASSED or FAILED. Never touches files. Loops with main Claude on failure.

Husky pre-push hookmechanical

Runs the same battery on git push. Rejects push if any step fails. Cannot be bypassed without --no-verify.

Stop hook.claude/settings.json

Fires on task completion signal. Third battery of the same checks. Belt-and-braces: catches "declared done, never pushed" cases.

CodeRabbit review

Inline comments on the PR. Findings ranked 🔴 Critical / 🟠 Major / 🟡 Minor.

CR drain turn (conditional)

If 🔴/🟠 findings exist, a second Claude Code turn launches with findings JSON. Addresses them, pushes. Max 1 drain per PR.

CI: lint · check · test · miniflare · playwright

GitHub Actions. Runs independently of pre-push hook. Auto-merge is gated on green CI.

GitHub auto-mergesquash

Refuses if last commit author is coderabbitai[bot] (prevents autofix ping-pong). Squashes to main, deletes branch.

Linear: In Review → Done

Orchestrator detects merge on next tick, advances state. Discord notification fires.

autodeploy.sh cron (PKX)2 min

When orchestrator/ changed: backup + syntax-check + rsync + restart. Zero-touch orchestrator updates.

Guardrails

CLAUDE.md ≤300 lines. XML-tagged MUST/NEVER rules at top. Imports .claude/failure-modes.md for the 747-line stack error catalog.
target_branch mode Ticket bodies with target_branch: pol-X-orchestrator attach to an existing PR instead of opening a new one.
Sub-agent model split Read-only → Haiku. Write → Sonnet. Fresh 200K per call. Parent sees summary only.
Depth = 1 Sub-agents cannot spawn sub-agents. Coordination always returns to main.
Auto-escalation Tickets modifying .claude/ or .husky/ are intercepted and pinged to a human — Claude Code hard-blocks writes to those paths.
Auto-nudge CI When GitHub's synchronize event misses, the orchestrator pushes an empty [ci-nudge] commit. Max 1 per PR.
Scheduling + agents
Enforcement gates
Merge + deploy
Retry / self-heal loops
Defense in depth

Three enforcement gates

Defensive layers that compound. Advisory guidance in CLAUDE.md + mechanical enforcement via hooks + sub-agent verification before completion. Each layer catches a different class of failure.

1
CLAUDE.md — Declarative rules
repo root · ≤300 lines · XML-tagged · MUST / NEVER front-loaded

Non-negotiable gates at the top. Failure-mode dictionary catalogs every stack-specific error Claude has ever hit (SvelteKit routing, Vitest $app/* imports, Durable Object migrations, D1 batch transactions). Imports .claude/failure-modes.md for the full catalog.

2
Husky pre-push hook — Mechanical enforcement
.husky/pre-push · runs on every git push · can't be bypassed without --no-verify

Runs pnpm lint && pnpm check && pnpm test -- --run && pnpm build locally before every push. The build step is non-negotiable — it catches server/client boundary violations, SSR errors, and bundle-time regressions that lint/check/test silently miss. If any step fails, the push is rejected. This is the load-bearing gate.

3
Stop hook — Task-completion gate
.claude/settings.json · fires when Claude signals "done" · blocks completion

Orthogonal to the git hook: fires when Claude says the task is complete, not when pushing. Runs the same lint+check+test+build battery. Catches "Claude wrote code, never pushed, but declared done" edge cases. Belt-and-braces complement.

Cognitive specialization

Eight sub-agents

Each sub-agent gets a fresh 200K context window, a narrow tool allowlist, and explicit "DO NOT" constraints. Main Claude delegates specialized work to preserve its context for implementation. Read-only agents use Haiku (cheap); write agents use Sonnet (capable).

pre-push-verifier

Haiku

Runs lint + check + test + build. Returns PASSED or FAILED with error. Never writes files, commits, or pushes.

Tools: Bash only

test-writer

Sonnet

Writes Vitest tests for a new feature given feature description and file paths. Returns test file content.

Tools: Read, Write, Bash

conflict-resolver

Sonnet

Rebases branches onto main. Resolves conflicts when semantics are clear, escalates when they aren't.

Tools: Bash, Read, Edit

spec-compliance-checker

Sonnet

Verifies PR diff matches ticket acceptance criteria. Returns checklist with 🔴 violations and file:line refs.

Tools: Read, Grep, Bash

researcher

Haiku

Fetches framework docs (SvelteKit, Workers, Vitest) and cross-references local patterns. Returns ~500 word distilled summary.

Tools: WebFetch, Read, Grep

migration-writer

Sonnet

Writes D1 migration SQL files from schema diffs. Enforces no-interactive-transactions rule automatically.

Tools: Read, Write, Bash

build-verifier

Haiku

Runs vite build + wrangler deploy --dry-run. Catches Workers-specific failures unit tests miss.

Tools: Bash only

architecture-reviewer

Sonnet

Reviews diff for repo convention violations: business logic in route handlers, DB queries outside src/lib/db/, DO state leaks.

Tools: Read, Grep, Bash
The scheduler

Orchestrator on PKX

A ~1200-line Python daemon running as a systemd user service. Ticks every 60s, polls Linear, applies DAG rules, spawns Claude Code per ticket in isolated worktrees.

📋
Polling

Linear GraphQL query every tick

Fetches up to 100 Ready tickets, orderBy createdAt ASC (oldest first). Phase N+1 tickets defer until all Phase <N+1 tickets are Done, Canceled, or Duplicate.

🌳
Isolation

Git worktrees

Each ticket gets its own worktree branched from origin/main. Symlinks node_modules from main when package-lock matches — saves 30-90s per pickup.

🎯
DAG scheduling

Phase gating + file overlap + critical paths

Won't pick a ticket if a Phase N-1 prereq isn't Done. Won't parallelize two tickets touching the same file. Serializes tickets that touch CRITICAL_PATHS (package.json, wrangler.toml, svelte config, etc.) — these run alone.

🔁
target_branch mode

Fix-existing-PR support

Tickets with target_branch: pol-X-orchestrator in their body attach the worktree to an existing remote branch and push to it instead of opening a new PR. Used for lint fixes and rebases.

🚦
Phase machine

claude_running → awaiting_cr → cr_draining → merged

State machine per in-flight ticket. 5-min CodeRabbit timeout. Detects "Claude Code exited without pushing" and marks the ticket Blocked with an escalation comment.

🤖
Auto-escalation

.claude/ and .husky/ paths

Tickets modifying these paths are auto-intercepted — Claude Code hard-blocks writes to .claude/ even in bypass mode. Orchestrator posts to Discord and moves to Blocked with "needs-morty-hands" marker.

Continuous integration + deploy

The feedback loops that close it

Three cron-driven loops keep everything live without intervention.

🚀
autodeploy.sh

PKX crontab every 2 min

Watches origin/main. When orchestrator/ changes, pulls, syntax-checks Python, rsyncs to ~/polyprint-orch/, and systemctl --user restart. Zero-touch orchestrator updates.

🔍
CodeRabbit followup

OpenClaw cron every hour

Scans recently-merged PRs for CR findings that weren't addressed during the initial drain. Files new Linear tickets for the un-addressed items so they get picked up next cycle.

🪝
Auto-nudge CI

Empty-commit workaround

When GitHub's pull_request.synchronize events go missing (GHA queue glitches), orchestrator pushes an empty [ci-nudge] commit to re-trigger. Max 1 per PR to avoid loops.

💬
Discord pipe

polyprint-orch-notify cron

Drains /tmp/polyprint-orch/pending_notifications.jsonl and posts to the team channel. Ticket picked up, completed, escalated, or merged — all show up in chat.

The product stack

What PolyPrint is actually built with

An internal prediction market for Blueprint Equity, written by the orchestrator. All on Cloudflare's edge.

🟧
Frontend

SvelteKit 5 + Svelte runes

$state, $derived, $effect. Typed routes with resolve(). No React, no Tailwind — plain CSS with design tokens.

☁️
Runtime

Cloudflare Pages + Workers

SSR at the edge. @sveltejs/adapter-cloudflare. Deploy via wrangler-action on merge to main.

🏛️
State

Durable Objects + D1

One MarketDO per market — holds live pool state + hibernatable WebSocket broadcast. D1 is source of truth for users, bets, resolutions.

🔐
Auth

Cloudflare Access

@blueprintequity.com Google Workspace domain. Edge-verified email injected via Cf-Access-Authenticated-User-Email header.

🧪
Testing

Vitest + Miniflare + Playwright

Two Vitest projects: client (jsdom + sveltekit plugin) and workers (@cloudflare/vitest-pool-workers). Playwright for smoke tests.

📐
Constitution

8 invariants enforced at review time

Parimutuel invariant (sum(outcome.pool) == total_pool), immutable resolutions, DO as source of truth, Access gates everything. Violations fail PR review.

The work

Product phases

Linear tickets are phase-tagged in their title. DAG scheduler enforces phase ordering.

Phase Name Focus
0Repo + infraSvelteKit, wrangler, D1 schema, MarketDO skeleton, CI pipeline
1Design system portCards, nav, shared primitives from mockup
2Auth + users + walletCloudflare Access integration, user bootstrap, 10k airdrop, wallet API
3Markets (read)List page SSR, detail page, WebSocket client, seed data, error pages
4Betting engineplaceBet in DO, API, WebSocket broadcast, UI modal, parimutuel math
5Positions pageUser's open bets, implied-payout drift calc, position card
6AdminCreate + resolve markets, admin dashboard, resolution flow
7LeaderboardCompute query, leaderboard page, badges / achievements
8Polish + launchResponsive, a11y audit, SEO, E2E test suite
Before and after

Why the gates matter

Two runs of the same orchestrator, same stack, different baseline. The gates are the delta.

Metric Pre-gate baseline Post-gate baseline
Fix-ticket ratio~60% of PRs needed a follow-up fix~0%
Wall-clock per ticket~15 min~6 min
Manual interventions per hour~30
Parallel Claudes saturatedRarely 3 / 33 / 3 consistently
Orchestrator self-healsNoautodeploy + auto-nudge + auto-escalate
What we learned

Principles the hard way

#1

Advisory instructions drift

CLAUDE.md is read and may be ignored around turn 30 of a long ticket. Every rule you care about needs a mechanical enforcer (hook, sub-agent, CI). Advisory prose is not a control.

#2

Lost in the middle is real

Hardest rules go at the TOP of CLAUDE.md. Imports go at the BOTTOM. Nothing load-bearing in the middle. Frontier models reliably follow ~150 instructions; burn your budget on the top third.

#3

Sub-agents aren't about speed

They're about context durability. Each gets a fresh 200K window; parent only sees the summary. For multi-hour tickets, this is the difference between completing and auto-compacting.

#4

Depth = 1

Sub-agents cannot spawn sub-agents. Everything that needs coordination comes back to main. Don't design towers of abstraction.

#5

Over-delegation is the trap

Sub-agents have 20-60K setup overhead and return lossy summaries. Rule: ≥10 files to explore, ≥3 independent chunks, or need an unbiased reviewer. Otherwise stay inline.

#6

Canceled counts as Done

If the DAG scheduler treats Canceled tickets as "not Done," the pipeline stalls permanently on superseded work. Canceled means "covered elsewhere, unblock."

#7

build is a pre-push gate, not a CI nicety

Lint + typecheck + unit tests can all pass while the production bundle fails on server/client boundary violations, SSR errors, or circular imports. Only npm run build catches those. It belongs in Husky, the Stop hook, AND the pre-push-verifier sub-agent — three independent gates. Removing it from any one is fine; removing it from all means the product silently fails to deploy while the board reports "complete."

#8

"Ticket Done" ≠ "product working"

The Linear board is the source of truth for the work. It is NOT the source of truth for the deploy. Every meaningful status report must additionally check: when was the last successful production deploy? Does the URL return the real app or a 404 behind auth? The pipeline's definition of done should include "the user can actually use it," not just "CI passed."

#9

continue-on-error is a landmine

CI jobs marked continue-on-error: true while waiting to "stabilize" become invisible. They fail silently for days while status dashboards report green. If a job is flaky, fix the flake. If it's new, require it from day one and let early failures be loud. Hiding failures is strictly worse than having them.