opensymphony 3.0.0

A Rust implementation of the OpenAI Symphony orchestration design
# 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.

<!-- BEGIN OPENSYMPHONY MANAGED MEMORY SYNC -->

## 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

<!-- END OPENSYMPHONY MANAGED MEMORY SYNC -->