# Linear and Tools
## 1. Boundary
OpenSymphony uses Linear in two different ways:
- the Rust orchestrator reads Linear through the internal `opensymphony_linear`
module
- the coding agent reads and writes Linear through the repo-local GraphQL skill
assets copied into the target repository
Scheduler correctness must never depend on agent-side ticket writes succeeding.
## 2. Orchestrator read adapter
The internal `opensymphony_linear` module is the only tracker adapter used by
the daemon.
It is responsible for:
- fetching active candidates for the configured project
- refreshing current state for already-running issues
- reading terminal issues for startup cleanup
- normalizing GraphQL payloads into stable domain models
- serving gateway task graph reads with Linear-native parent, child, and
blocker relationships
- loading gateway task graph issue details through project-scoped, paged Linear
reads rather than per-identifier GraphQL lookups
Current workflow contract:
- `tracker.kind` must be `linear`
- `tracker.project_slug` stores Linear `Project.slugId` and remains the
compatibility selector for legacy workflow files
- central configurations may also set `tracker.project_id` to the immutable
Linear `Project.id`; when present, the orchestrator resolves that provider
project first and uses its returned `slugId` for the existing project-scoped
queries. A legacy-compatible configuration with a distinct non-empty
`project_slug` falls back to that slug if the ID lookup returns no project;
a typed central configuration normally keeps the provider ID and slug
aligned, so an unresolved ID fails startup before polling. A resolved
project without a `slugId` always fails startup.
- `LINEAR_API_KEY` must be available when Linear mode is enabled
- if `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET` are both present,
`opensymphony run` mints a Linear OAuth client-credentials access token at
startup, uses it for the scheduler's Linear tracker client, and exposes it to
workers as `LINEAR_API_KEY`
- the scheduler's Linear tracker client remains mandatory for `opensymphony run`;
the gateway task graph reader is optional and, when unavailable, causes only
the task graph endpoint to return `503`
Local scheduler polling keeps the 5s worker/snapshot tick, but Linear reads are
cadenced separately so a busy local workstation does not burn through the shared
Linear API quota:
- running issue state and managed repository labels are refreshed with the
lightweight by-ID state query every 30s, so a binding mutation fences the
claimed generation without waiting for the hourly full-detail pass; nested
label pages are walked per issue before the binding is resolved, and archived
children are included so parent neutrality is preserved during reconciliation
- dispatch discovery uses a lightweight active-issue summary query every 60s
- terminal cleanup reads run at startup and then every 5 minutes
- full-detail active issue reads run at startup, for selected dispatches, and
then hourly
Multi-project central routing keeps the configured project identities as
parallel vectors rather than collapsing them into one scalar selector:
- `tracker.project_ids` and `tracker.project_slugs` remain index-aligned; each
provider project ID is resolved to its current `slugId` before a project-
scoped query, while legacy slug-only entries use their configured slug
directly
- active candidates, lightweight active summaries, terminal cleanup reads,
running-issue state refreshes, and project issue scans iterate every
configured project and combine their paginated results before the scheduler
or gateway consumes them
- running-issue state refreshes perform a bounded unscoped ID lookup for any
tracked issue omitted by those project filters, so moving an issue out of
the configured project set cannot leave stale execution state behind
- state-only issue snapshots carry Linear's project ID and slug; active
reconciliation uses that identity to recompute repository routing and
releases an issue immediately when it moves outside the configured project
set; project identity drift also supersedes an active worker even when the
selected repository remains unchanged so its project-scoped grant and
conversation are not reused across projects
- the scalar `tracker.project_id`/`tracker.project_slug` fields remain the
compatibility view for the first configured project; they do not limit a
central multi-project poll to that one project
- gateway task-graph snapshots use the same combined project-scoped read and
retain only requested nodes and relationships present in that snapshot;
identifier lookups remain the bounded fallback for tracked issues that moved
outside the configured project set
The lightweight dispatch query returns summary-shaped scheduler data only.
Selected candidates must be reloaded through bounded issue-by-identifier
full-detail lookups before workspace creation, prompt construction, or worker
launch. Detail refreshes are issued in bounded batches sized to the currently
available worker capacity, so blocked or stale candidates do not force a full
project rescan for every batch. A repository supersession removes the stopped
generation's workspace before the replacement claim is materialized.
Restart recovery backfills a missing binding in a legacy run manifest from the
current configured legacy repository before comparing generations for drift.
When Linear returns a rate-limit error with retry metadata longer than the
client's short retry backoff, the Linear client returns the error immediately
instead of sleeping inside the request. The inline retry boundary is the lower
of `tracker.retry_policy.max_backoff` and 30 seconds; longer reset windows are
handed to the scheduler. The scheduler records one shared Linear cooldown,
skips Linear reads until it expires, and continues draining worker updates and
publishing snapshots.
Important normalization rules:
- `blocked_by` is derived from `inverseRelations` entries whose relation type is
`blocks`; gateway task graph responses filter these IDs to nodes present in
the returned project snapshot so clients do not receive dangling graph edges
- `state_kind` is derived from Linear's stable workflow-state `type`; clients and
caches must not infer categories from mutable display names such as
"Human Review"
- `branch_name` comes from Linear `Issue.branchName` when present and is carried
through tracker normalization so run-detail clients can show the same branch
known to the scheduler
- `pr_urls` retains qualifying URLs from the 32 most recently updated Linear
attachments containing `/pull/`, fetched in the issue query without historical
attachment pagination. When more exist, hydration logs the truncation. The explicit
`sourceType` must be GitHub or API (including links created by the agent-side
GraphQL helper), and the URL must be an HTTPS pull-request URL in the
`<owner>/<repo>/pull/<number>` shape. This accepts both github.com and
GitHub Enterprise authorities; non-PR URL attachments are ignored for Run
Detail PR metadata. `pr_url` remains the first qualifying URL as a
compatibility projection for consumers that only display one pull request.
- `parent_id` comes from `parent.id`
- `parent` retains the parent identifier when Linear returns it, and gateway
task graph nodes use that identifier as the client-facing `parent_id`; the
gateway clears `parent_id` when that parent is outside the returned project
snapshot so clients do not receive dangling hierarchy edges
- `sub_issues` comes from `children.nodes`
- gateway task graph `children` are filtered to nodes present in the returned
project snapshot
- `state` remains the workflow-facing state name string used by
`WORKFLOW.md`
- gateway task graph `root_ids` are the returned node identifiers whose Linear
parent is absent or outside the returned node set; clients must not infer
tracker hierarchy from fixture data or local fallbacks
- `repository_binding` is derived from managed `repo:<alias>` labels and the
central repository inventory; project associations validate the binding but
never provide a default. Parents keep this field unresolved and repository
aliases are propagated through planning and conversion without storing
remotes or workspace paths in Linear. Conversion removes stale managed
`repo:*` labels before adding the current binding, while preserving unrelated
labels; the converter-managed `area:*` namespace is replaced in the same
operation so stale area labels do not accumulate. Label connections are
paged before replacement, both for project snapshots and mapped issue
lookups. When the final managed binding is removed from an existing issue,
the conversion mutation explicitly sends an empty label list. Strict
project-set packages must declare a non-empty alias inventory. If a publish
mapping points to an issue outside the paged project snapshot, conversion
fetches that issue's labels before replacing the managed subset so unrelated
labels are not lost. Lightweight running-state snapshots also include a
current child-presence bit, allowing the scheduler to neutralize a running
task immediately when it becomes a parent. The planning-state and mapped
issue detail queries also inspect existing child presence before the
converter adds a repository label, so a stale Linear child cannot turn a
formerly terminal issue into a repository-bound parent. Gateway task-graph
and run-detail blockers retain the typed repository-binding diagnostic
instead of collapsing every routing failure into a dependency message.
## 3. Agent-side Linear access
OpenSymphony 1.0.0 is GraphQL-only for agent-side Linear work.
Every initialized repository receives:
- `.agents/skills/linear/SKILL.md`
- `.agents/skills/linear/scripts/linear_graphql.py`
- `.agents/skills/linear/queries/*.graphql`
- `.agents/skills/linear/references/*.md`
Later, `opensymphony update` refreshes the template-managed `.agents/skills/`
tree in place for an existing target repo without rerunning the full bootstrap
flow.
The agent path is intentionally simple:
1. require `LINEAR_API_KEY`
2. choose a checked-in query file
3. pass variables as JSON
4. inspect the returned JSON
When `opensymphony run` minted a Linear OAuth client-credentials token, spawned
workers receive that value as an environment overlay. The target repo's shell
startup files do not need to be changed for those workers.
Example:
```bash
python3 .agents/skills/linear/scripts/linear_graphql.py \
--query-file .agents/skills/linear/queries/issue_by_key.graphql \
--variables '{"key":"COE-123"}'
```
## 4. Supported GraphQL workflows
The checked-in query assets cover the current repository-supported write and
inspection paths:
- issue create and follow-up issue creation
- issue body and metadata updates
- issue lookup by key or ID
- issue detail reads
- team workflow-state lookup
- issue transitions
- comment create and update
- issue relation creation
- GitHub PR attachment
- plain URL attachment
- project lookup by slug
- project overview/content updates
- project status create, update, and assignment
- upload bootstrapping through `fileUpload`
- schema introspection for mutation names and input shapes
If a new mutation is needed, prefer adding a checked-in query file and updating
the skill references instead of improvising large inline GraphQL strings in
prompts.
## 5. Why GraphQL-only
OpenSymphony previously carried a custom Linear bridge layer for agent-side
writes. That indirection is gone in 1.0.0.
The GraphQL-only design keeps the system smaller and easier to reason about:
- no extra local bridge process
- no duplicated tool contract to maintain
- no ambiguity about which Linear surface the agent should use
- full access to Linear capabilities without waiting for a narrower wrapper
## 6. Failure model
The expected behavior is:
- missing `LINEAR_API_KEY` is a real blocker for Linear operations
- GraphQL write failures do not change scheduler correctness
- the orchestrator continues to reconcile issue state from its own read adapter
- target-repo skills must treat a top-level GraphQL `errors` array as failure
- `opensymphony linear archive` is an operator command, not an agent-side write;
it refuses to archive issues without fresh captured memory unless `--force`
is supplied
## 7. Repository ownership
The relevant ownership boundaries are:
- `crates/opensymphony-linear/`
- orchestrator-side GraphQL adapter module tree
- `crates/opensymphony-workflow/`
- workflow validation module tree for Linear-related config
- `.agents/skills/linear/` in the template repo
- agent-side GraphQL helper, query files, and references
OpenSymphony intentionally does not ship a second agent-side Linear server.
## 8. Validation
Before merging Linear-related changes:
- run `cargo test`
- run `cargo test --test init`
- run `cargo test --test update`
- initialize a sample repo with `opensymphony init`
- confirm the copied `.agents/skills/linear/` tree includes scripts, queries,
and references
- update the same sample repo with `opensymphony update` and confirm changed or
new template-managed Linear skill files sync cleanly
- smoke-test the helper with `queries/viewer.graphql`
## 9. Migration note
OpenSymphony 1.0.0 removed workflow-owned Linear bridge configuration.
If an older repository still contains `openhands.mcp`, remove that block and
use the repo-local Linear GraphQL helper assets with `LINEAR_API_KEY` instead.
## Current model
- COE-254 contributed: PR #6: COE-254: bootstrap tracker, workspace, and orchestration core
- COE-263 contributed: PR #35: COE-263: Implement workspace manager and lifecycle hooks (merge `2693eea`)
- COE-264 contributed: PR #33: COE-264: Linear read adapter and issue normalization (merge `45cca3c`)
- COE-267 contributed: PR #83: Add memory init and mapped docs sync
- COE-268 contributed: PR #43: Implement orchestrator scheduler retries and reconciliation (merge `2ad73ad`)
- COE-270 contributed: PR #39: COE-270: add deterministic workspace context artifacts (merge `3a90eea`)
## Important invariants
- Preserve the behavior described in the recent captured changes unless current code and tests show it has changed.
- Use capsule source refs to inspect the original PR or Linear issue when context is ambiguous.
## Operational flow
- No generated diagram requested for this sync.
## Known gotchas
- No area-specific gotchas were inferred from the selected memory.
## Recent changes
- COE-254: Tracker, Workspaces, and Orchestration
- COE-263: Workspace manager and lifecycle hooks
- COE-264: Linear read adapter and issue normalization
- COE-267: Linear MCP write surface
- COE-268: Orchestrator scheduler, retries, and reconciliation
- COE-270: Repository harness and generated context artifacts
- COE-277: Implement hierarchy-aware task selection
- COE-401: Web App Entry And Deployment Modes
- COE-407: Browser Transport And Remote Stream Protocols
- COE-419: Hosted Auth Placeholders And Web Parity
- COE-473: Desktop task graph dependency and run detail parity
- COE-486: Harness Interrupt Contract And Run Diagnostics
- COE-487: Desktop Run Detail TUI Parity
- COE-488: Lazy Desktop Launcher Command
- COE-489: OpenHands Agent-Server Interrupt Adapter
- COE-490: Codex App-Server Turn Interrupt Adapter
- COE-491: Desktop Run Detail Action Wiring And Cleanup
- COE-492: Merging Supersedes Human Review Polling
- COE-493: Desktop Operations Integration Hardening
- COE-504: Linear Polling And Rate-Limit Recovery
## Source refs
- COE-254
- COE-263
- COE-264
- COE-267
- COE-268
- COE-270
- COE-277
- COE-401
- COE-407
- COE-419
- COE-473
- COE-486
- COE-487
- COE-488
- COE-489
- COE-490
- COE-491
- COE-492
- COE-493
- COE-504