M19-1: /issues/pipeline endpoint + IssuePipeline shared types #174

Closed
opened 2026-04-20 18:44:09 +00:00 by code-lead · 0 comments
Collaborator

As an operator (backend), I want GET /issues/pipeline[?repo=…&milestone=…&state=open|closed|all] returning the canonical stage model for every issue the service has touched, so the new UI can render pipelines without talking directly to Forgejo.

Acceptance criteria

Stage model

  • packages/shared defines a canonical Stage string union: "breakdown" | "implement" | "pr" | "ci" | "review" | "approved" | "merge" | "closed" plus design track: "design" | "design_review"
  • StageState union: "pending" | "running" | "success" | "failure" | "skipped" | "stalled"
  • IssuePipeline shape frozen in packages/shared (see spec §Stage model for exact fields: repo, issue_number, title, labels, milestone, assignee, current_stage, stages[], updated_at, pr_number)
  • StageEntry carries stage, state, task_ids[], agent, started_at, finished_at, duration_ms, cost_usd, turns, round, force_merge, link, stalled_since

Derivation rules

  • Implement / design / merge / breakdown → rollup of task_history rows matching (repo, issue_number) and the right agent type
  • PR stage → present iff an open PR references #<issue_number> in body. link = PR URL
  • CI stage → listWorkflowRuns(sha) on the PR's head. State = worst of (pending / success / failure). link = run URL
  • Review stage → listPullReviews(pr) + requested_reviewers. Round counter = number of distinct REQUEST_CHANGES verdicts from the reviewer agent. state=stalled if review request older than config/agents.json::pipeline.stall_threshold_ms (default 600_000ms) without CI movement
  • Approved stage → state=success iff any APPROVED review at head. No task, no duration
  • Closed → reflects issue.state === "closed"

Endpoint

  • GET /issues/pipeline — default state=open, no repo filter. Returns { issues: IssuePipeline[], generated_at }
  • Query params: ?repo=<owner>/<name>, ?milestone=<id>, ?state=<open|closed|all>, ?limit=<n> (default 100, max 500)
  • Server-side cache, 5-second TTL keyed by (repo, milestone, state, limit) — prevents multiple tabs hammering Forgejo
  • Read-only, no auth beyond M18-8's LAN / Authelia split

SSE envelope

  • New SSE event pipeline_stage with { repo, issue_number, stage, state, agent?, task_id?, round?, link? }
  • Emitted alongside existing task_started / task_finished / result events whenever a stage transitions. Old events unchanged — purely additive so M18-3's renderer is untouched

Tests

  • pipeline.test.ts: derivation from fixture task_history + fake Forgejo responses. Design track, review loop (round counter), stall detection (>10 min)
  • pipeline.test.ts: cache invalidation — second call within 5s hits cache, after 5s refetches
  • pipeline-sse.test.ts: pipeline_stage fires when a task's lifecycle event fires, with the right stage mapping

Docs

  • README /issues/pipeline section with response shape
  • CLAUDE.md Modules table: apps/server/src/pipeline.ts

Out of scope

  • Any UI (that's #M19-2+)
  • Cross-repo unified pipeline (endpoint is per-repo; global view is a UI concern)
  • Historical replay beyond what task_history retains

Dependencies

  • Blocks on #M18-1 + #M18-2 (monorepo + shared types package exist)
  • Can parallel with #M18-3+ (endpoint-only; no UI work)
  • Unblocks every other M19 story

References

  • Spec: specs/m19-pipeline-monitor.md §Story M19-1
As an operator (backend), I want `GET /issues/pipeline[?repo=…&milestone=…&state=open|closed|all]` returning the canonical stage model for every issue the service has touched, so the new UI can render pipelines without talking directly to Forgejo. ## Acceptance criteria ### Stage model - [ ] `packages/shared` defines a canonical `Stage` string union: `"breakdown" | "implement" | "pr" | "ci" | "review" | "approved" | "merge" | "closed"` plus design track: `"design" | "design_review"` - [ ] `StageState` union: `"pending" | "running" | "success" | "failure" | "skipped" | "stalled"` - [ ] `IssuePipeline` shape frozen in `packages/shared` (see spec §Stage model for exact fields: `repo`, `issue_number`, `title`, `labels`, `milestone`, `assignee`, `current_stage`, `stages[]`, `updated_at`, `pr_number`) - [ ] `StageEntry` carries `stage`, `state`, `task_ids[]`, `agent`, `started_at`, `finished_at`, `duration_ms`, `cost_usd`, `turns`, `round`, `force_merge`, `link`, `stalled_since` ### Derivation rules - [ ] Implement / design / merge / breakdown → rollup of `task_history` rows matching `(repo, issue_number)` and the right agent type - [ ] PR stage → present iff an open PR references `#<issue_number>` in body. `link` = PR URL - [ ] CI stage → `listWorkflowRuns(sha)` on the PR's head. State = worst of (pending / success / failure). `link` = run URL - [ ] Review stage → `listPullReviews(pr)` + `requested_reviewers`. Round counter = number of distinct `REQUEST_CHANGES` verdicts from the reviewer agent. `state=stalled` if review request older than `config/agents.json::pipeline.stall_threshold_ms` (default 600_000ms) without CI movement - [ ] Approved stage → `state=success` iff any APPROVED review at head. No task, no duration - [ ] Closed → reflects `issue.state === "closed"` ### Endpoint - [ ] `GET /issues/pipeline` — default `state=open`, no repo filter. Returns `{ issues: IssuePipeline[], generated_at }` - [ ] Query params: `?repo=<owner>/<name>`, `?milestone=<id>`, `?state=<open|closed|all>`, `?limit=<n>` (default 100, max 500) - [ ] Server-side cache, 5-second TTL keyed by `(repo, milestone, state, limit)` — prevents multiple tabs hammering Forgejo - [ ] Read-only, no auth beyond M18-8's LAN / Authelia split ### SSE envelope - [ ] New SSE event `pipeline_stage` with `{ repo, issue_number, stage, state, agent?, task_id?, round?, link? }` - [ ] Emitted alongside existing `task_started` / `task_finished` / `result` events whenever a stage transitions. Old events unchanged — purely additive so M18-3's renderer is untouched ### Tests - [ ] `pipeline.test.ts`: derivation from fixture `task_history` + fake Forgejo responses. Design track, review loop (round counter), stall detection (>10 min) - [ ] `pipeline.test.ts`: cache invalidation — second call within 5s hits cache, after 5s refetches - [ ] `pipeline-sse.test.ts`: `pipeline_stage` fires when a task's lifecycle event fires, with the right stage mapping ### Docs - [ ] README `/issues/pipeline` section with response shape - [ ] CLAUDE.md Modules table: `apps/server/src/pipeline.ts` ## Out of scope - Any UI (that's #M19-2+) - Cross-repo unified pipeline (endpoint is per-repo; global view is a UI concern) - Historical replay beyond what `task_history` retains ## Dependencies - **Blocks on #M18-1 + #M18-2** (monorepo + shared types package exist) - **Can parallel with #M18-3+** (endpoint-only; no UI work) - **Unblocks every other M19 story** ## References - Spec: `specs/m19-pipeline-monitor.md` §Story M19-1
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#174
No description provided.