ADR-001: Mainark owns the task graph (exit Linear)
Status: Phase 1 merged; Phase 2 complete (SQLite backend; Beads prior stands); Phase 3 automated slice green — human soak + default flip are Phase 4
Date: 2026-07-20
Deciders: Mainark maintainers
Context: Linear remains the default (TASK_BACKEND=linear). Opt into local SoT with TASK_BACKEND=sqlite. See backend spike and task-provider.md.
1. Decision (architectural)
Mainark will stop treating Linear as the source of truth for engineering work items.
End state
| Concern | Owner |
|---|---|
| Issue graph (id, title, body, deps, epic plans, human work status) | Mainark (local-first backend) |
| Code, review, merge | GitHub |
| Agent execution, worktrees, review loops, stacked PRs | Mainark |
| Linear APIs | None |
Hard requirement: Mainark must be usable completely offline for issue CRUD, implement / plan / queue, and orchestrator dispatch against an already-local DB. Network may still be needed for GitHub PR operations and agent model calls; issue storage and task-graph queries must not require SaaS.
This is not “replace a PM tool.” It is deciding that the orchestrator owns the task graph.
2. Non-goals (do not build)
- A Linear clone (cycles, roadmaps, assignees as first-class product, SLA, rich projects UI).
- Fulreach product-DB issues as the eng workflow SoT (unless issues must bind to tenants — separate product bet).
- GitHub Issues as the orchestrator’s primary graph (acceptable as optional mirror later; not SoT).
- A second plan store in target repos (
.mainark/plans/) — see anti-patterns. - Linear webhook parity (Mainark already polls; local events suffice).
- Cancelling Linear before an offline end-to-end epic has succeeded on the new default backend.
3. Linear touchpoint inventory
Classification:
- required — capability Mainark must keep (any backend).
- replaceable — same job, new local API / backend.
- obsolete — delete when Linear is gone (no local equivalent needed).
3.1 Integrations
| Location | Direction | Class | Notes |
|---|---|---|---|
src/integrations/linear/client.ts | in/out | replaceable | GraphQL hub |
src/integrations/linear/reconcile.ts | ingress (+ reverse egress) | replaceable | Label poll → SQLite; reverse-sync drift |
src/integrations/linear/statusSync.ts | egress | obsolete* | Board mirroring; replace with local work_status / pipeline status only |
src/integrations/linear/constants.ts | config | obsolete* | Linear board names/labels |
src/types/linear.ts | types | replaceable | Become backend-neutral issue types |
*Obsolete as Linear concepts; human pause/resume must still exist locally.
3.2 CLI
| Location | Direction | Class | Notes |
|---|---|---|---|
src/cli/linearApi.ts | in/out | replaceable | Second client; must collapse into TaskProvider |
src/cli/start.ts | egress + identity | replaceable | Create/link issue, In Progress, branch rename |
src/cli/publish.ts | egress + identity | replaceable | PR title, Linear comment/state → PR comment / local status |
src/cli/implementEntry.ts + parallel/epic paths | ingress | required | Needs issue body, deps, plans |
src/cli/epicPlan.ts | parse | required | Parser stays; storage moves |
src/cli/epicQueue.ts / epicStatus.ts | ingress | required | Children + blockers |
src/cli/naming.ts | identity | required | Stable display ids; initially preserve FUL-n; allocation depends on backend |
src/cli/issueManifest.ts | local | replaceable | Drop linearUuid / linearUrl when unused |
3.3 Orchestrator / pipeline
| Location | Direction | Class | Notes |
|---|---|---|---|
src/reconciliation/loop.ts | ingress via provider | required | Must call TaskProvider, not Linear |
src/pipeline/runPipeline.ts | egress | replaceable / obsolete | Drop Linear comments; keep pipeline |
src/pipeline/reviewLoop.ts | egress | obsolete* | Linear comments → GitHub PR |
src/pipeline/failureRouter.ts | egress | replaceable | Local failed / paused |
src/orchestrator/dispatcher.ts | — | required | Dispatch from local pending queue |
src/integrations/github/merge.ts / reconcile.ts | egress today | replaceable | Update local status, not Linear Done/Cancelled |
src/recovery/startup.ts | egress | replaceable | Local failed |
src/integrations/slack/notifier.ts | display | replaceable | Link to Mainark UI / issue id, not linearUrl |
3.4 Data & UI
| Location | Class | Notes |
|---|---|---|
Local task store / task repository (issues table + 003_linear_metadata.sql) | mixed | Task repository required (today mirrors Linear; may become graph SoT later); Linear display cols obsolete |
depends_on | required | Already local shape; stop sourcing only from Linear |
Epic ## Mainark plan | required | Must live in local store (field or attached markdown), not Linear description |
.mainark/epics.json | required | Active epic watch list (target repo) |
Web /issues Linear chrome | replaceable / obsolete | Keep pipeline UI; drop Linear links/metadata |
3.5 Agent / docs / env
| Location | Class | Notes |
|---|---|---|
.cursor/skills/spec-to-linear/ | replaceable | Becomes spec → Mainark issues |
.cursor/rules/mainark-parallel.mdc | replaceable | Point at local SoT |
docs/parallel-agents/linear-setup.md | replaceable | Retitle / rewrite after cutover |
LINEAR_*, TASK_BACKEND | replaceable | LINEAR_* removed at end; TASK_BACKEND stays |
| Fulreach CI → Linear comments | replaceable | Outside this repo; must move to PR comments in lockstep |
3.6 Current abstraction (insufficient)
// src/tasks/provider.ts today — too narrow
interface TaskProvider {
backend: "linear" | "beads";
reconcile(): Promise<LinearReconcileResult>;
}
Only src/reconciliation/loop.ts uses it. CLI and pipeline still hard-import Linear. BeadsTaskProvider throws. No Linear replacement work until §5 is implemented and parity-proven (§7).
4. Backend evaluation (criteria before picking a winner)
Candidates:
- SQLite — extend Mainark’s existing orchestrator DB
- Beads (
bd, Dolt-backed) — existingTASK_BACKEND=beadsstub - Git-backed Markdown — files + conventions as SoT
4.1 Criteria (must score explicitly)
| Criterion | What “good” means for Mainark |
|---|---|
| Offline | Full CRUD + ready/claim + epic plan read with no network |
| Concurrency | Safe multi-agent / CLI + orchestrator without corrupt graph (claim atomicity) |
| Backup / portability | Dump, restore, move machine; clear disaster story |
| Agent ergonomics | Easy for Cursor/agents: show body, deps, ready work, create linked tasks |
| Epic plans | Store and parse ## Mainark plan (or equivalent) without a second system |
| Dependency graph | First-class blockers; topo sort for parallel/stacks |
| Orchestrator fit | Maps cleanly to pipeline status, pause/resume, reconcile tick |
| Identity | Support stable FUL-n (or documented mapping) for branches/worktrees/CI |
| Ops cost | Dependencies, daemons, merge conflicts, learning curve |
| Ownership | Mainark controls schema and migration; no mandatory third-party SoT |
4.2 Provisional scores (design-time; validate with spike)
Scores: H / M / L relative to Mainark needs. Not a final decision.
| Criterion | SQLite (extend) | Beads | Markdown SoT |
|---|---|---|---|
| Offline | H — already local | H — local Dolt / embedded | H |
| Concurrency | M–H — single-writer SQLite is fine for solo + orchestrator; need explicit claim | H — bd update --claim, ready queue, server mode for multi-writer | L — merge conflicts, no atomic claim |
| Backup / portability | H — copy DB file; already in Mainark ops | H — bd export / bd backup; Dolt remotes | H — git |
| Agent ergonomics | M — need CLI/MCP (mainark issue …) | H — designed for agents (bd ready --json, dep types, remember/prime) | H for reading; L for structured mutations |
| Epic plans | H — plan_md column or blob; parser unchanged | M — parent-child + description possible; need convention for Mainark plan format | H — natural markdown |
| Dependency graph | M–H — depends_on exists; ready = query | H — blocks / parent-child / discovered-from; bd ready | L — prose or fragile frontmatter |
| Orchestrator fit | H — issues table + pipeline already coupled | M — must map Beads status ↔ pipeline; dual concepts risk | L — poll/parse files awkwardly |
Identity (FUL-n) | H — sequence table | M — native bd-* ids; need alias/FUL-n mapping or accept rename cost | M — filename conventions |
| Ops cost | H — zero new runtime | M — bd + Dolt; another moving part | H — no daemon |
| Ownership | H — Mainark schema | L–M — graph lives in Beads/Dolt product; Mainark becomes client again | H |
4.3 How to read this (important)
- Prior: SQLite is favored on ownership and orchestrator-fit grounds. Mainark already owns the task repository schema; making Beads the graph SoT would introduce a second embedded DB the orchestrator must reason about. That tradeoff has not disappeared.
- The spike (or design review) exists to falsify that prior, not to treat SQLite vs Beads as a coin-flip. Beads still deserves evaluation because it scores highest on agent ergonomics and ready/claim semantics — but the burden of proof is on Beads to win without Mainark becoming a client of another product’s data model.
- Prefer a cheap design review first. A half-day read of Beads’ CLI/data model (IDs, deps, claim, epic/plan storage, multi-writer) may resolve the ownership question without building two working backends. Escalate to dual coded
TaskProviderslices only if ownership is still contested after that review. - Markdown as SoT remains attractive for AI reading / export, but weak as the runtime graph. Prefer markdown as a view over a structured backend.
4.4 Phase 2 spike acceptance criteria (both candidates)
Whichever path is spiked must document:
| Criterion | Must show |
|---|---|
| Concurrent claims | Safe claim under CLI + orchestrator without double-pickup |
| Atomic state transitions | work_status / pipeline transitions do not tear or race |
| Durability / backup | Dump, restore, move-machine story; what is lost if the process dies mid-claim |
| Event logging | Append-only issue_events (or equivalent) for status/claim/failure |
| Offline operation | Full Phase 3 slice with no Linear (and no remote sync required) |
| Implementation complexity | Ops surface, deps, concepts leaked into Mainark domain types |
| Identity | Stable display ids for branches/worktrees/CI |
| Ownership | Mainark domain types remain authoritative behind TaskProvider |
4.5 Decision gate (backend)
Pick SoT only after:
- TaskProvider methods in §5 exist behind Linear (no behavior change).
- Beads ownership question resolved via design review, and only then an optional coded spike if still ambiguous.
“Still unclear” (operational): docs/CLI inspection alone cannot answer (a) display-id mapping for branches/CI and (b) atomic claim without leaking Beads concepts into Mainark domain types. - Phase 3 parity checklist (§7) passes on the candidate default.
- Offline gate:
mainark implement/plan/ issue CRUD with network disabled for Linear.
Until then: TASK_BACKEND=linear remains default; docs say backend deferred (SQLite prior stands).
5. TaskProvider design (must cover every workflow)
Expand beyond reconcile(). All CLI and pipeline Linear calls eventually go through this interface (or a thin facade used by both). Types should not be named Linear*.
5.1 Domain model (minimal — not a Linear clone)
Issue
id // stable display identifier; initially preserve FUL-n;
// allocation mechanism determined by the selected backend
title
description // agent-facing body
dependsOn[] // blocker ids (execution / stack)
parentId? // epic → child
planMd? // epic-only: ## Mainark plan body (or full description section)
workStatus // todo | in_progress | paused | done (human control plane)
labels[] // e.g. orchestrator-managed
// pipeline fields stay on local task store even if graph SoT is another backend:
pipelineStatus, pr*, reviewCycle, failure*, tokens*, ...
IssueEvent (append-only; required before Phase 2 coding)
id, issueId, at, kind, detail
// kinds: created | updated | claimed | work_status | pipeline_status |
// failed | paused | resumed | pr_opened | merged | ...
// Purpose: agents debug their own execution history without Linear comments
Comments are not required on the issue backend; use GitHub PR (and Slack) for human narrative. Agent-facing history is IssueEvent, not a comment stream.
5.2 Provider API (parity surface)
type TaskBackend = "linear" | "sqlite" | "beads"; // markdown is export, not a backend enum value
interface TaskProvider {
backend: TaskBackend;
// Identity & CRUD
getIssue(id: string): Promise<Issue | null>;
createIssue(input: CreateIssueInput): Promise<Issue>;
updateIssue(id: string, patch: UpdateIssuePatch): Promise<Issue>;
// Graph
setDependsOn(id: string, blockerIds: string[]): Promise<void>;
listReady(filter?: ReadyFilter): Promise<Issue[]>; // unblocked + workStatus eligible
listChildren(epicId: string): Promise<Issue[]>;
// Epic plans
getPlan(epicId: string): Promise<string | null>; // raw ## Mainark plan markdown
setPlan(epicId: string, planMd: string): Promise<void>;
// Human control (replaces Linear board moves)
setWorkStatus(id: string, status: WorkStatus): Promise<void>;
// Orchestrator
reconcile(): Promise<ReconcileResult>; // ingest/sync into local task store; no-op or local-only when SoT is local
claim(id: string): Promise<boolean>; // atomic start; false if lost race
// Optional migration
importSnapshot?(snapshot: IssueSnapshot[]): Promise<void>;
}
5.3 Workflow → API mapping (coverage checklist)
| Workflow today | Provider methods required |
|---|---|
mainark start create/link | createIssue / getIssue, setWorkStatus(in_progress) |
mainark publish | getIssue; status → local/PR (no Linear comment) |
mainark implement leaf / queue | listReady or getIssue + deps |
| Parallel bootstrap / topo | getIssue, dependsOn |
mainark plan / --resume | getPlan, listChildren |
mainark queue / next | getPlan / children + workStatus |
| Spec → tickets (skill) | createIssue, setDependsOn, setPlan |
| Orchestrator pickup | reconcile + claim / pending queue in task store |
| Pause / resume (was Linear Backlog ↔ Todo) | setWorkStatus(paused|todo) |
| Failure / merge terminal | pipeline repo + setWorkStatus(done) as needed |
| Agent implement body | getIssue → write .mainark/implement-*.md |
Gate: Do not switch default backend until every row has an automated or manual parity test on the new backend while Linear remains available as rollback (TASK_BACKEND=linear).
5.4 Local task store vs graph SoT
Clarify in code:
- Graph SoT — TaskProvider backend (Linear today; SQLite favored later)
- Local task store — Mainark’s
issues(+issue_events) repository: pipeline fields, and eventually possibly the full graph
Today the store mirrors Linear. After migration it may be the SoT. The neutral name “task store / task repository” covers both eras.
Do not require Linear (or Beads remotes) for task-store updates once a local backend is default.
6. Migration plan (incremental, rollback-friendly)
Each phase leaves Mainark working. No phase deletes Linear until §6.5.
Phase 0 — Export & conventions (docs only / one-shot scripts OK)
- Export open Linear issues: id, title, body, state, labels, blockedBy, epic plan sections.
- Freeze display-id rule: initially preserve
FUL-nin branches/CI; allocation mechanism is determined later by the selected backend. - Record offline requirement in this ADR (done).
- Rollback: N/A.
Phase 1 — Widen TaskProvider; Linear still only backend
- Implement §5 API with
LinearTaskProvider. - Route CLI + pipeline through provider (behavior unchanged).
- Collapse
linearApi.tsusages behind the provider. - Rollback: revert PR; Linear unchanged.
- Exit criteria: all workflows in §5.3 call provider; no new direct GraphQL from CLI/pipeline.
Phase 2 — Backend spike; Linear remains default
- Step A (preferred): half-day Beads design review against §4.1 / §4.4 (IDs, claim, deps, plans, multi-writer, ownership). Document go/no-go.
Escalate only if still unclear: display-id mapping for branches/CI, or atomic claim without leaking Beads concepts into Mainark domain types, cannot be answered from docs/CLI inspection alone. - Step B (only if still ambiguous): implement the same narrow TaskProvider workflow slice for SQLite and/or Beads behind a flag so comparison is apples-to-apples.
- Spike acceptance (§4.4): concurrent claims, atomic state transitions, durability/backup, event logging, offline operation, implementation complexity.
- Default read path still Linear.
- Rollback:
TASK_BACKEND=linear. - Exit criteria: written decision (SQLite prior stands, or Beads falsified it) + optional slice demo; Linear default green.
- Practical note: after Phase 1 ships, use Mainark for a week or two before deciding whether a Beads spike is needed at all — experience often clarifies more than further design.
Phase 3 — Prove feature parity (explicit exit criteria)
Run the checklist below on the chosen candidate with TASK_BACKEND=<candidate>. Fix gaps in provider or task store — not by special-casing Linear.
Rollback: default remains Linear.
Exit criteria: every row signed off; offline gate passed.
| Capability | Pass condition |
|---|---|
| Create issue | Stable display id allocated; title/body persist offline |
| Edit issue | Title / description / work_status update without Linear |
| Dependency graph | Blockers set; topo / parallel bootstrap correct |
| Epic plan | ## Mainark plan parse; mainark plan / --resume |
| Queue | Ready queue returns unblocked work |
| Implement | Agent body from local store; worktree created |
| Publish | PR titled/linked; local status updated |
| Review | Review loop + local status; no Linear comments required |
| Resume paused work | Pause / resume without a board UI |
| Offline execution | Airplane mode: issue-graph CRUD + implement path |
| Worktree lifecycle | Create / reuse / clean worktree for issue id |
| Event history | issue_events (or equivalent) records claim/status/failure |
| Backup | Documented dump/restore for the chosen backend |
Phase 4 — Switch default backend
- Default
TASK_BACKEND→ chosen local backend. - Keep Linear provider for emergency rollback for one release window.
- Rollback: flip env back to
linear.
Phase 5 — Remove runtime Linear dependency
- Delete runtime Linear client/statusSync usage and
LINEAR_*requirements. - Rewrite skill → spec-to-mainark.
- Coordinate Fulreach CI: Linear comments → PR comments.
- Keep migration helpers/docs that still mention Linear until Phase 6.
- Rollback: only via git revert / reinstall old version (expect pain — do not rush).
Phase 6 — Cleanup docs/migration shims + cancel subscription
- Remove migration code/docs that assume Linear is SoT.
- Cancel Linear subscription only after one full epic offline: create → parallel implement → publish → review/merge path.
7. Parity checklist (before default switch)
Canonical copy lives under Phase 3 above. Summary:
- Create issue
- Edit issue
- Dependency graph
- Epic plan
- Queue
- Implement
- Publish
- Review
- Resume paused work
- Offline execution
- Worktree lifecycle
- Event history
- Backup / restore documented
8. Risks
| Risk | Mitigation |
|---|---|
| Switching SoT before provider covers CLI | Phase 1 gate |
| Losing pause/resume with board sync | workStatus + CLI/UI before Phase 5 |
| Epic plans stranded in Linear descriptions | Export in Phase 0; getPlan/setPlan before cutting ingress |
Display id (FUL-n) vs backend-native ids | Spike ID strategy; do not surprise CI/branch naming |
| Dual Linear clients drift | Collapse in Phase 1 |
| Fulreach CI still posts to Linear | Track as explicit dependency of Phase 5 |
| Choosing SQLite only “because it exists” | §4 criteria + concurrency/backup/events in spike |
| Beads ownership boundary | SQLite prior stands; design review / spike exists to falsify it, not to toss a coin |
9. Consequences
Positive
- Offline task graph; no subscription for IDs.
- Single owner for deps + epic plans + dispatch.
- Cleaner architecture: GitHub for code, Mainark for work graph.
Negative / costs
- Build/maintain issue CRUD and human control plane.
- Migration + Fulreach CI coordination.
- Temporary dual-backend complexity (Phases 1–4).
Neutral
- Historical
FUL-*in code/docs remain archaeology. - Markdown implement files remain generated views.
10. Immediate next steps
- Soak with
TASK_BACKEND=sqliteon real tickets (create → implement → publish) for a week or two. - When Phase 3 human checklist is signed off, Phase 4: flip default to
sqlite. - Phase 5–6: remove Linear runtime + cancel subscription.
- Beads remains a non-SoT stub (backend spike).
References
- Parallel agents overview
- Linear setup (current SoT docs — to be superseded)
- Anti-patterns
- Pipeline state mapper
src/tasks/provider.ts— current narrow seam- Beads: https://github.com/steveyegge/beads (evaluate against §4; do not assume unfit)