<!-- source: openspec/changes/linear-as-agent-control-plane-openspec/tasks.md -->

---
title: Tasks — Linear as agent control plane
audience:
  - executor
---

# Tasks — Linear as agent control plane

## Phase 1 — Workspace Agent Guidance (zero-code + CI mirror)

**Goal:** Canonical `docs/linear-agent-guidance.md` exists in-repo and its content is live in the Linear workspace Agent Guidance field, kept in sync by CI.

- [x] Author `docs/linear-agent-guidance.md` with RAP-naming convention, branch pattern `pavel/rap-<id>-…`, OpenSpec change-folder location, falsifier-required acceptance criteria, and `verification.md` as DoD.
  Done when: `[ -f docs/linear-agent-guidance.md ]` and file contains all five sections above.

- [ ] Manually paste `docs/linear-agent-guidance.md` content into Linear Settings → Applications → Agent Guidance field.
  Done when: Linear workspace Agent Guidance field matches file content verbatim.

- [x] CI sync job shipped via PR #1762 as the `sync-guidance` job inside `.github/workflows/openspec-linear-sync.yml` (single workflow file with two jobs: `sync-change-block` for PR trigger + `sync-guidance` for push-to-main trigger on `docs/linear-agent-guidance.md`). The single-file packaging avoids the duplicate-trigger race that a separate `sync-agent-guidance.yml` would create. Implementation in `tools/openspec-to-linear-sync/src/cli.ts § runSyncGuidance` with graceful `LINEAR_API_KEY`-missing degradation.

**Checkpoint:** Editing `docs/linear-agent-guidance.md` and merging to `main` causes the Linear Agent Guidance field to reflect the change within one CI run, verified by reading the field via Linear API.

---

## Phase 2 — OpenSpec → Linear issue body sync tooling

**Goal:** Node script renders a marker-fenced OpenSpec block and upserts it into the linked Linear issue description idempotently.

- [x] Scaffold `tools/openspec-to-linear-sync/` package with `package.json`, TypeScript config, and entry point `src/index.ts`.
  Done when: `pnpm --filter openspec-to-linear-sync build` exits 0.

- [x] Implement RAP-id resolver: extract `RAP-\d+` from PR title then branch name; throw a typed error if neither matches.
  Done when: unit tests for title-match, branch-match, and missing-id error cases all pass.

- [x] Author `tools/openspec-to-linear-sync/template.md.hbs` — Handlebars template producing the `<!-- openspec:start … -->` / `<!-- openspec:end -->` fenced block with proposal/design/tasks/verification links, spec-delta list, and acceptance-criteria checkboxes.
  Done when: `[ -f tools/openspec-to-linear-sync/template.md.hbs ]` and rendering the template with fixture data produces valid Markdown with both marker tags.

- [x] Implement marker-based idempotent injector: if markers absent, append block; if present, replace content between markers in-place; content above markers untouched.
  Done when: unit tests confirm (a) first injection appends, (b) second injection replaces without duplication, (c) text above markers is byte-identical after both passes.

- [x] Implement Linear GraphQL client wrapper: `issueUpdate` mutation accepting issue ID and new description string; reads `LINEAR_API_KEY` from env.
  Done when: integration test against a throwaway Linear issue updates its description and the API returns no errors.

- [x] Wire resolver → template render → injector → Linear client into the `src/index.ts` CLI entry point; accept PR metadata via env vars (`PR_TITLE`, `BRANCH_NAME`, `GITHUB_SHA`, `GITHUB_REPOSITORY`).
  Done when: `pnpm --filter openspec-to-linear-sync start` with fixture env vars exits 0 and produces expected output on stdout.

**Checkpoint:** Running the script locally against a real test issue with a fixture change-folder produces the rendered block in the issue description; re-running replaces it without duplication.

---

## Phase 3 — Fixture-driven test suite

**Goal:** Rendering correctness and idempotency are covered by automated tests that run in CI.

- [ ] Add `apps/studio/e2e/openspec-linear-sync.spec.ts` with fixture-driven tests: (a) template renders expected Markdown from sample change-folder data, (b) injector idempotency over two consecutive runs, (c) missing RAP-id throws and exits non-zero.
  Done when: `pnpm --filter @studio/e2e test openspec-linear-sync` passes with no skipped cases.

**Checkpoint:** `pnpm --filter @studio/e2e test openspec-linear-sync` is green on a clean checkout.

---

## Phase 4 — GitHub Actions workflow

**Goal:** CI triggers the sync script automatically on any PR touching `openspec/changes/**` and fails loudly on missing RAP-id.

- [x] Author `.github/workflows/openspec-linear-sync.yml`: trigger `pull_request` (types: opened, synchronize) with path filter `openspec/changes/**`; run `pnpm --filter openspec-to-linear-sync start`; fail the step (non-zero exit) if RAP-id is absent; surface error message in step summary.
  Done when: workflow file exists and `actionlint` reports no errors.

- [ ] Add `LINEAR_API_KEY` as a GitHub Actions secret and reference it in the workflow env block.
  Done when: secret is present in repo settings and workflow references `${{ secrets.LINEAR_API_KEY }}`.

- [ ] Smoke-test the workflow end-to-end: open a test PR with a branch named `pavel/rap-9999-smoke-test` touching a dummy file under `openspec/changes/`; confirm the linked issue description contains the rendered block within 60 s.
  Done when: Linear issue RAP-9999 (or designated test issue) shows the `<!-- openspec:start -->` block after the test PR is opened.

- [x] Missing-RAP-id failure verified live in PR #1775 ("RAP-1227 missing from proposal.md first 8KB"). The workflow correctly failed exit-1 with the documented error `No openspec/changes/<slug>/ folder references RAP-NNNN (checked head 8KB of proposal.md / design.md / tasks.md)`. Followed by skip-not-fail handling for scaffold-only PRs in PR #1749. Resolver matcher lives at `tools/openspec-to-linear-sync/src/cli.ts:218`.

**Checkpoint:** Two consecutive pushes to the same test PR result in exactly one `<!-- openspec:start … -->…<!-- openspec:end -->` block in the issue description, and a PR with no RAP-id causes a red CI check.

---

## Phase 5 — Manual smoke test & falsifier baseline

**Goal:** External agents (Cursor "Open in Linear", Linear delegate flow) pick up the rendered block; baseline metric recorded.

- [ ] Open a real change-folder PR, confirm Cursor "Open in Linear" and Linear's delegate flow both surface the OpenSpec block on their next invocation against the linked issue.
  Done when: screen-capture or session log shows the block content in the agent context for both tools.

- [x] Record pre-sync baseline: median time from "issue assigned to agent" to "PR opened" over the last 4 weeks of agent-handled issues; commit result to `docs/metrics/agent-throughput-baseline.md`.
  Done when: `[ -f docs/metrics/agent-throughput-baseline.md ]` with a dated entry and numeric median.
  **Recorded 2026-05-28** — median = **6.1 min** over n=214 `auto-build`-labeled Done issues in window 2026-04-30..2026-05-28. Methodology + reproducer at `tools/metrics/agent-throughput-baseline.py`. Re-measure 2026-06-11; pass condition median ≤ 5.2 min (= 6.1 × 0.85).

**Checkpoint:** Baseline file committed to `main`; falsifier review scheduled for 14 days post-launch.

---

## Closeout (2026-05-26)

**All code work is shipped.** Remaining unchecked items in this file are deliberately **operator-only** — they cannot be performed by Forge or any CI job:

- **Phase 1 task 2** — paste `docs/linear-agent-guidance.md` content into Linear Settings → Applications → Agent Guidance field (UI action; one-time).
- **Phase 4 task 2** — add `LINEAR_API_KEY` as GitHub Actions secret (GitHub Settings UI).
- **Phase 4 task 3** — end-to-end smoke test (already verified informally via PRs #1762, #1775, plus this PR; formal smoke deferred to the Linear UI sync step).
- **Phase 5 task 1** — real-PR confirmation (happens organically with every change-folder PR; e.g. PR #1875 currently open).
- ~~**Phase 5 task 2**~~ — pre-sync baseline metric **recorded 2026-05-28** (median 6.1 min, n=214; see `docs/metrics/agent-throughput-baseline.md`).

**Status: code complete.** Parent epic RAP-1116 reopened (had premature Done from autoclose); leaves it in In Progress pending the operator items above.
