Skip to content

Cancel pipelines on issue state change #68

@VincentShipsIt

Description

@VincentShipsIt

PRD: cancel-on-issue-state-change

Executive Summary

Adopt Symphony's reconciliation tick. Every N seconds, batch-fetch the GitHub state of all running pipelines' source issues. If the issue has been closed or has a terminal label (`wontfix`, `duplicate`, etc.), cancel the pipeline and clean its worktree. Stops shipcode from burning tokens on issues a human has already abandoned.

Implementation Checklist

  • New scheduler loop in `packages/pipeline/src/` (or extend existing `IssueGroupScheduler`) ticking every `reconcileIntervalMs`
  • Tick batches running threads by repo, calls `gh issue list --json number,state,labels --search ""` (or per-issue `gh issue view`)
  • For each running thread whose issue is `closed` or has any label in `terminalLabels`, invoke pipeline `cancel(threadId)`
  • Worktree cleanup proceeds via existing `WorktreeManager.remove(path, branch)`
  • API failure during reconcile must not crash; log and retry next tick

Problem Statement

If a teammate closes a GitHub issue while shipcode is running on it, the pipeline keeps planning, reviewing, executing, and verifying until done. Tokens are spent. Worktree litter accumulates. The user then has to notice and manually cancel from the UI.

Goals

  • Reconciliation tick runs every `reconcileIntervalMs` (default 30000).
  • For each `running` thread, refresh the GitHub issue state in one batched call.
  • Issue `closed` or carrying a terminal label (configurable list) → cancel the pipeline.
  • Cancellation uses the same SIGHUP→SIGKILL escalation as `killAllAndWait`.

Non-Goals

  • Auto-restart when an issue is reopened.
  • Hot-reload of `terminalLabels` (treated as static config until Hot-reload WORKFLOW.md changes #71).
  • Reconciliation across multiple repos in one tick (single-repo per tick is fine for v1).

User Stories

  • As a teammate who decides to close an issue mid-pipeline, I want shipcode to notice within a poll interval and cancel the run, so we do not waste API tokens or land an unwanted PR.
    Acceptance:
    • Closing the issue mid-pipeline cancels within `reconcileIntervalMs`.
    • Adding a `wontfix` label cancels.
    • Worktree is cleaned up post-cancel.
    • Reason logged and surfaced in UI as `cancelled: issue terminal`.

Functional Requirements

  1. New scheduler loop in `packages/pipeline/src/` (or extend existing `IssueGroupScheduler`) ticking every `reconcileIntervalMs`.
  2. Tick batches running threads by repo, calls `gh issue list --json number,state,labels --search ""` (or per-issue `gh issue view`).
  3. For each running thread whose issue is `closed` or has any label in `terminalLabels`, invoke pipeline `cancel(threadId)`.
  4. Worktree cleanup proceeds via existing `WorktreeManager.remove(path, branch)`.
  5. API failure during reconcile must not crash; log and retry next tick.

Non-Functional Requirements

  • Tick must complete in <2s for 20 running threads.
  • API rate limits must not be exceeded — coalesce per-repo into one query when possible.

Success Criteria

  • Unit test with fake timers and a mocked GH API: closing the issue triggers cancel within one tick.
  • Label-based cancel: `wontfix` label triggers cancel; non-terminal labels do not.
  • API-failure test: reconcile error does not crash; running threads survive.
  • Manual: close a real GH issue mid-pipeline; verify cancel within 30s and worktree cleaned.

Out of Scope

Dependencies

Verification Plan

  • tests: new `packages/pipeline/src/reconciliation-loop.test.ts` covering tick scheduling, state cancel, label cancel, API failure resilience.
  • manual: start a pipeline on a test issue; close the issue from a different terminal; verify pipeline cancels within reconcile interval.

Risks & Open Questions

  • Per-issue `gh issue view` calls vs one batched search query — confirm GH API supports batched fetch by issue numbers; fall back to per-issue if not.
  • False positive cancel if a label is added mid-PR-feedback for triage purposes — make the terminal-label list opt-in and conservative by default.
  • Race between reconcile tick and a phase that is about to finish — existing pipeline.cancel must be idempotent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request
    No fields configured for Feature.

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions