Pipeline View
Overview
The Pipeline View is a phased development visualization that tracks issues through a four-stage readiness pipeline: research → architecture → grooming → ready. It complements the Kanban Board by showing progress through design phases rather than execution status. Teams use it to understand work queued for design, underway in specification, ready for implementation, or unlabeled and awaiting phase assignment.
How It Works
View Toggle
The project page (app/pages/p/[slug]/index.vue) has a "Board" / "Pipeline" toggle button. Selecting "Pipeline" switches the display from the Kanban Board to the Pipeline View component.
Two-Layer Data Flow
Layer 1: API Endpoint
GET /api/projects/{projectId}/pipeline
→ Route: server/api/projects/[projectId]/pipeline.get.ts
→ Service: server/services/pipelineService.ts
→ Returns: PipelineResponse { phases: PipelinePhase[] }Layer 2: Component Visualization
The component fetches the pipeline data on mount and polls every 5 seconds (via usePolling). When data arrives, it computes two derived values:
- maxCount: The largest phase bucket size (used for proportional bar sizing)
- allIssues: Flattened array of all phased issues for the table
Data Model
Phase Labels
Each issue can have exactly one phase label: phase:research, phase:architecture, phase:grooming, phase:ready, or none (shown as "unlabeled").
- Phase labels stored in
issues.labels(JSON array) - Extracted via
extractPhase(label)fromphaseLabels.tsutilities - Issues without a phase label appear in the "unlabeled" bucket
Artifacts
Each issue has an artifacts array tracking submissions per phase:
interface IssueArtifact {
phase: string // 'research', 'architecture', etc.
path: string // File path to artifact (e.g., docs/tickets/SYM-001/research.md)
status: 'draft' | 'submitted' | 'approved' | 'rejected' | 'superseded'
revision: number
hash?: string
}Artifacts are updated by agents calling complete_phase MCP tool. The artifact dot on each row shows the approval status visually (green=approved, red=rejected, yellow=draft/submitted, gray=none).
Phase Metrics
When an issue enters a phase, orchestrator creates a phaseMetrics object:
interface PhaseMetrics {
entered_phase_at: string // ISO timestamp
phase_rejection_count: number // Cumulative rejections
last_completion_hash: string | null
last_verdict: string | null
}The "Rejections" column displays phase_rejection_count.
Grouping Algorithm
The service groups all issues by phase in one pass:
- Fetch all issues for the project (one query)
- For each issue, extract its phase label
- Add issue to corresponding bucket (research/architecture/grooming/ready/unlabeled)
- Return buckets in canonical order, omitting empty phases except unlabeled (if populated)
Visualization
Phase Distribution Funnel — Proportional bar chart showing count per phase
- Bar width:
(count / maxCount) * 100%(minimum 4% if count > 0) - Colors: research=blue, architecture=purple, grooming=orange, ready=green, unlabeled=gray
- Responsive to dark mode
Issues Table — Five columns per issue:
| Column | Shows | Cell Logic |
|---|---|---|
| Ticket | ID + title | Links to issue detail page |
| Phase | Current phase label | Badge with phase color |
| Artifacts | Mini dots per phase | Colored dots (green/red/yellow/gray) for artifact status |
| Rejections | Count of phase rejections | Red text if > 0, gray "0" if none |
| Status | Execution status | Badge with status color (todo/in_progress/review/done/etc) |
Clicking any row navigates to the issue detail page (/p/{slug}/issues/{identifier}).
Key Components
| File | Responsibility |
|---|---|
app/pages/p/[slug]/index.vue | View toggle button ("Board" / "Pipeline") |
app/components/PipelineView.vue | Funnel visualization + issues table, polling, computed bars |
server/api/projects/[projectId]/pipeline.get.ts | HTTP endpoint, delegates to service |
server/services/pipelineService.ts | Phase grouping logic, data transformation |
server/utils/phaseLabels.ts | Phase label parsing (isPhaseLabel, extractPhase) |
app/utils/phaseLabels.ts | Client mirror of server utilities (Nuxt auto-import scoped) |
server/database/schema.ts | issues.labels, issues.artifacts, issues.phaseMetrics JSON columns |
Design Decisions
1. Separate View Toggle (Board vs Pipeline)
Decision: Show both Kanban Board and Pipeline View as tabs, not merged.
Rationale: The execution pipeline (backlog → todo → in_progress → review → done) and readiness pipeline (research → architecture → grooming → ready) are distinct mental models. Some teams care more about design readiness; others track execution throughput. Toggle lets both coexist.
Trade-off: User must switch contexts manually; no single unified timeline. Offset by clearer semantics per view.
2. Single-Query Fetch for All Issues
Decision: Fetch all issues for the project in one query, then group in memory.
Rationale: Simpler than streaming or paginating. Phase distribution is quick to compute (linear scan + map insertion). Works well with SQLite WAL mode eventual consistency.
Trade-off: Large projects (10k+ issues) fetch full dataset. Offset by no N+1 queries, simple caching via polling interval.
3. Phase Label as Canonical Phase Source
Decision: Extract phase from issue.labels array, not a separate issue.phase column.
Rationale: Phase is one of many label types (can have priority:high, team:frontend, phase:architecture together). Enforcing exactly-one-phase-per-issue via label namespace is cleaner than a nullable column; label semantics are explicit.
Trade-off: Must parse labels on every fetch. Offset by fast string matching (extractPhase is O(1) prefix check).
4. Artifacts Array for Revision Tracking
Decision: Store all artifact submissions per phase in an array, not a single "current artifact" record.
Rationale: Audit trail — can show revision 1 rejected, revision 2 approved. Simpler than a separate artifacts table with foreign keys. JSON append pattern matches agent workflow (agents call complete_phase, MCP tool appends artifact to array).
Trade-off: Querying "artifact status for phase X" requires scanning array in memory. Offset by small arrays (typically 1-3 artifacts per phase per issue).
5. Proportional Funnel Bars
Decision: Bar width proportional to phase count, with 4% floor if count > 0.
Rationale: Visualizes flow — widest bar = most work in that phase. Floor prevents zero-count bars from being invisible. CSS transitions smooth the bars on update.
Trade-off: Absolute counts less obvious; requires text label. Offset by label on right side.
6. Client-Side Mirror of Phase Utils
Decision: Duplicate phaseLabels.ts in both server/utils/ and app/utils/.
Rationale: Nuxt auto-import scopes prevent importing server/utils/ from app/ code. Duplication ensures both layers can parse phase labels identically without cross-boundary imports.
Trade-off: Maintenance burden if label format changes. Offset by comment linking files, small function set (3 functions).
Known Gaps
- Large dataset performance: No pagination or virtualization. Projects with 1000+ phased issues may render slowly.
- Artifact filtering: UI shows all artifacts per issue but doesn't filter by latest status or sort by revision.
- Phase metrics:
entered_phase_atstored but not displayed; could show "in phase for 2 days". - Unlabeled issues: Bucket created even if empty; could optimize to skip rendering if zero count.