SOW — Linear as Core Shared Workspace

Authored 2026-07-28 · Owner: Akash · Executor: any Claude session (this doc is self-contained — a fresh session can complete it cold) · Architecture per Nick Saraev's agent-workflow videos (brain video_notes 2547 + 2552)
The decision (Akash, 2026-07-28): Linear becomes the front-of-house shared human+AI workspace — capture, board, agent dispatch visibility, QA, comments. The Postgres brain stays the system of record; Linear is the operating surface. Two-way sync, never a data fork. Linear workspace exists: team "Stpierre", business trial active.

1 · Where the system stands today (all LIVE unless noted)

ComponentStateWhere
Board columns on public.tasksLIVE flow_status (inbox/next/doing/waiting/done), agent_ready, waiting_on, executor (agent/akash/collins), verdict (keep/park/kill), triage_reason/lockedPostgres brain
Dispatch pipeLIVE trigger → task_dispatch_outbox → n8n reader (5-min cron) → taskos_eventsn8n wf ut9OwshB5eSbeCgI
Agent runnerLIVE every 30 min, ≤3 events/run, kill-gate first, consumes task_activity context as binding brief, model-tiered subagents, parks at waiting — never self-marks done, never runs gated writesscheduled task agent-board-runner; spec pair: 01-Projects/St_Pierre_Digital/agent-board-rail/runner.md = ~/.claude/scheduled-tasks/agent-board-runner/SKILL.md (edit BOTH together)
Kanban v2 boardLIVE = the board Collins seesteam.stpierre.ai/builderpipeline + preview stpierre-builder-kanban-v2.pages.dev; renderer 01-Projects/St_Pierre_Digital/builder-kanban-v2/render_board.py; 06:45 chain re-renders daily
Morning QA surfaceLIVE QA queue + decisions + human lanes + aging; three moves per cardstpierre-morning-qa.pages.dev; 01-Projects/St_Pierre_Digital/morning-qa/; CLIs ~/bin/verdict (approve/redo/kill) + capture context <id> "..."
CaptureLIVE host CLI ~/bin/capture; iOS n8n webhook wkkBq03HfcscTD4n ACTIVE (capture + context routes, X-Capture-Secret header, secret in vault).codex/references/capture/README.md (has iPhone Shortcut recipe + Mac Shortcut click-path — Akash's 2-min setup still pending)
Card activity trailLIVE public.task_activity (plan/progress/context/verdict rows)brain; rendered on both boards
EvalsLIVE canon retry loop (check_and_iterate.py + gap checks v2 at warn severity) · akash-scorecard (8×0–2, any-zero fails) · 80/20 kill gate · Build Contract gate.codex/references/creative-canon/ · .codex/references/evals/
Backlog stateAPPLIED 1,226 keep / 605 park / 9 soft-kills / 1,777 legacy executor-labeled (labels only, NOT queued) / 190 stale parked as archive candidates / 15 PROPOSED-KILL cards in QA laneundo files: 01-Projects/St_Pierre_Digital/builder-kanban-v2/undo/
Cost routing + learned-rules + contract gatesLIVE in AGENTS.md + .codex/references/cost-routing.mdledgers 91839–44
Slack waiting-on-Akash pingGATED n8n wf VRx1w1HUvcfjMj2X inactive — needs Akash to vault a working Slack tokenops_alerts → 09:00 briefing covers the gap
Linear scaffoldingBUILDING NOW (background agent): inactive n8n Linear-webhook receiver + linear_sync.py (bootstrap/webhook/push/comment/import functions) + activation runbook01-Projects/St_Pierre_Digital/linear-workspace/ — check its README for final ids

Ledger trail for the whole build: change_ledger 91805–91854 (+ scaffolding rows after). Every write has reverse_how or an undo file.

2 · Target architecture — Linear front-of-house, brain as record

Flow: Capture (Linear hotkey C / mobile app / our CLIs) → issue in team Stpierre → status Next + label agent-ready → Linear webhook → n8n receiver → upsert public.tasks → existing outbox/dispatch/runner (unchanged) → runner mirrors back: plan/progress as Linear comments, state moves (Doing → Waiting), label waiting-on-akash → Akash QAs in Linear (or Morning QA page) → approve/redo/kill flows back through the sync.

Mapping contract:
LinearBrain (public.tasks)
Workflow states Inbox / Next / Doing / Waiting / Doneflow_status inbox / next / doing / waiting / done
Label agent-readyagent_ready=true
Label waiting-on-akashflow_status='waiting' + waiting_on='akash'
Labels human-only / collinsexecutor='akash' / 'collins'
Issue identifier (e.g. STP-42)source='linear', source_ref=identifier
Commentstask_activity rows (context/plan/progress mirror both ways)
Issue descriptiondescription (DoD) + triage_reason

Anti-loop rule: the sync skips webhook events authored by the API-key user that match its own last write. Brain remains authoritative on verdicts and ledger; Linear is authoritative on nothing — it is a view + input surface.

3 · AKASH — your manual steps (the only human-gated part)

On the settings page you screenshotted (Settings → Administration → API):
  1. Leave "API key creation: All members" as-is. Nothing to change on this page itself.
  2. Do NOT create an OAuth application — that's Phase 2 (a named agent identity like "Nick OS"); we start with your profile like Nick does ("I'm cheap" mode).
  3. Do NOT click New webhook manually — the sync creates it via API with a signing secret already vaulted. (Manual fallback exists in the linear-workspace README if API creation hits a permission wall.)
  4. The key itself is NOT made on this page: click "security & access settings" (linked in the Member API keys paragraph, or sidebar Personal → Security & access) → Personal API keys → New API key.
  5. Label it stpierre-agent-rail. If asked for scopes/teams: full read/write, team Stpierre (or all).
  6. Copy the key (starts lin_api_) and paste it into any Claude session. Tokenmaster auto-vaults it as service='linear', scope='stpierre', label='api-key'. That paste is the trigger for the whole activation runbook below.
Also still pending from before (unrelated to Linear): the 2-minute Mac Shortcut for capture (recipe in .codex/references/capture/README.md) — though once Linear is live, Linear's own C hotkey + mobile app may make it redundant. Decide after Phase 3.

4 · Execution phases (any session can run these in order)

#PhaseWhatGate
P0Key in vaultAkash pastes the Linear API key (step 3 above) → verify with brain.get_credential(service='linear', scope='stpierre', label='api-key')Akash
P1Bootstrap workspacepython3 linear_sync.py bootstrap — creates the 5 states + 4 labels on team Stpierre, idempotent. Verify by listing states via API.auto once P0
P2Webhook liveActivate the inactive n8n Linear receiver, then linear_sync.py create-webhook. Test: create a throwaway Linear issue, move it to Next + agent-ready, confirm the tasks row appears, delete both.auto once P1
P3Import curated cardslinear_sync.py import-curated — ONLY board cards (source='builder-board-v1' OR flow_status set, verdict≠kill, ≈90). NEVER the 1,857 backlog — Linear stays curated; the lake stays in Postgres.auto once P2
P4Runner mirrorRunner posts plan/progress/waiting as Linear comments + state changes (spec: "Linear mirror" section of agent-board-rail.md). Edit runner.md AND the scheduled-task SKILL.md together — they are a pair.1 approval
P5QA in LinearAkash's approve/redo/kill via moving cards / labels in Linear flows back through the webhook (verdict semantics preserved: approve→Next+agent-ready, kill→cancel, redo→Next + comment as context). Morning QA page stays as the batched daily view until Linear views fully replace it — retire/merge is Akash's call per the reporting-surfaces rule.Akash tries it
P6Capture cutover decisionCompare: Linear C-hotkey/mobile/Action-button vs our CLI+webhook. Keep both or retire ours. Also decide: point the 06:45 kanban render at Linear data or retire the HTML board (Collins's view!).Akash
P7Agent identity (optional)OAuth app w/ agent capability → a named agent actor ("SP Agent") instead of acting as Akash. Nick-parity vanity; do only if attribution matters.Akash

5 · Session learnings a fresh session must know (hard-won today)

6 · Link registry

Definition of done for this SOW: P0–P5 complete and verified (a Linear card moved to Next by Akash produces agent work that returns as a Linear comment + Waiting state, and his approve in Linear re-queues it) · the curated ~90 cards live in Linear · no orphaned QA items · capture + board retirement decisions (P6) explicitly made by Akash, not defaulted.