<!-- source: openspec/changes/automate-spec-to-build-loop/proposal.md -->

---
title: Automate the change-first spec→build loop
audience:
  - founder
  - architect
  - executor
---

# Automate the change-first spec→build loop

> **Affects capability spec:** `openspec/current/forge-build-pipeline.md`, `openspec/current/linear-as-agent-control-plane.md`

## Why

`forge-on-change-merge.yml` already dispatches Forge for changes that carry a `RAP-id` in their commit (RAP-1202 / RAP-1207). When Pavel authors a change folder without a pre-existing issue the workflow exits with `scaffold-stage PR — no impl issue yet` and the rest — issue creation, labeling, dispatch — is manual. This gap breaks the change-first authoring flow and forces context-switching out of the editor. The fix is bounded: an explicit opt-in marker (`> **Auto-build:** true`) gates automation so unmarked scaffolds, research, and spec-only changes are never touched.

## What Changes

- **Linear write primitives lifted to `core/linear-client.ts`** — `createIssue`, `ensureLabels`, `createRelation`, `findExistingIssueForChange` extracted from `packages/forge/src/commands/audit/*` and exposed as raw-GraphQL helpers (`@linear/sdk` remains `tools/`-only per `linear-as-agent-control-plane` spec).
- **New `forge open-issue <slug>` command** — deterministic, no LLM in the hot path; reads the change folder, detects shape (simple vs. epic), creates the appropriate Linear work item(s), and emits `--json` / `--dry-run` output.
- **Simple-change shape** — flat `tasks.md` checklist → single issue in `Todo` + `auto-build` label, dispatched immediately via `gh workflow run` (fail-loud on non-zero exit; no silent swallow).
- **Epic-change shape** — `## Phase N` headers → parent epic + one sub-issue per buildable phase (`isBuildablePhase()` predicate: ≥1 actionable `- [ ]`, no title substring-matching), contiguous wave numbering, `blockedBy` chain, parked in `Backlog` for manual release.
- **`readAutoBuildMarker` reader** — blockquote-line parser tolerant of varying/absent metadata blocks; three fixture tests; returns `false` for anything not matching `> **Auto-build:** true` exactly.
- **Dedup / claim model (Layer A + B2)** — slug-keyed `concurrency.group` in the workflow (Layer A) serializes same-slug runs; `change/<slug>` Linear label acts as the idempotency claim (Layer B2, no migration required); `auto_build_claims` DB table deferred to a later upgrade.
- **`forge-on-change-merge.yml` flip** — drop `head -1` so multi-add merges loop over all changed slugs; add slug-keyed concurrency group; replace the no-RAP-id early-exit branch with `forge open-issue` invocation gated on marker presence.
- **`audit/*` refactor** — existing audit commands repointed onto the new `core/linear-client.ts` primitives; no behaviour change.
- **Capability-spec amendment** — `forge-build-pipeline.md` and `linear-as-agent-control-plane.md` updated to document the new trigger path, marker contract, and dedup layers.

## Impact

- Spec files touched: `openspec/current/forge-build-pipeline.md`, `openspec/current/linear-as-agent-control-plane.md`
- Packages affected: `packages/forge` (new command + core client), `.github/workflows/forge-on-change-merge.yml`
- Downstream surfaces: Linear workspace (issue / label / relation writes), Forge build queue (dispatch via `gh workflow run`)

## Out of scope

- Conductor cron as a dispatch fallback — explicitly rejected; `gh workflow run` is the sole guarantee for simple changes.
- `ideas` table as claim host — no `change_slug` column; `ideas.slug ≠ change slug` (D-IT-2); not touched.
- `auto_build_claims` DB table (Layer B1) — deferred; B2 label claim is the shipping default.
- Studio substrate write-back — scoped to Phase 5 after B1/B2 decision is locked in design.md.
- Any LLM-assisted issue drafting in the hot path — `forge open-issue` is fully deterministic.
- Changes to the issue-first trigger path (RAP-1207) — already shipped; untouched here.
