Skip to content

Historical field note

This design/spec note is retained for provenance only. It is not a current operating contract; the premises or implementation path it describes have been superseded or never shipped.

Linear Bridge Spec — Review Notes

Captured 2026-05-28. Reviewer: codex exec (codex-cli 0.134.0). A parallel architect-subagent review converged on the same load-bearing findings (1 BLOCKER + 15 MAJOR vs. codex's 4 BLOCKER + 7 MAJOR + 1 MINOR). v2 of linear-bridge-spec.md was rewritten against this feedback. See the "Revision history" section in the spec for the mapping of findings → spec changes.


Architectural Review

  1. BLOCKER: TrackerBackend is not a behavior-preserving Beads retrofit.
    Spec §7 says "BeadsBridgeService and LinearBridgeService each implement this protocol" and that the Beads retrofit is "behavior-preserving" (docs/linear-bridge-spec.md, docs/linear-bridge-spec.md). Reality: beads_bridge_service.py is only a narrow bd subprocess wrapper with cli_path() and run() (beads_bridge_service.py, beads_bridge_service.py). The real bridge is embedded in ControlPlane: registration at services.py, polling at services.py, import at services.py, writeback at services.py. A protocol around BeadsBridgeService would miss most actual behavior.

  2. BLOCKER: the proposed protocol is under-shaped for existing Beads semantics.
    Spec §7 makes create_issue, attach_pr, and update_state first-class backend methods (docs/linear-bridge-spec.md, docs/linear-bridge-spec.md, docs/linear-bridge-spec.md). Current Beads-backed imports only bind tasks whose metadata origin is {"type":"beads"} and has a bead_id (services.py). Normal Hermes-created tasks are hermes_interaction tasks (services.py) and create_task may only add repository execution metadata, not create a bead (services.py). So "beads-backed tenant task creation" is not an existing behavior to route through the same create_issue path.

  3. BLOCKER: outbox retry/backoff claims are false.
    Spec §15.2 says "The outbox already handles retries with exponential backoff and a max-attempt limit" and permanent failures become dead_letter (docs/linear-bridge-spec.md). Actual schema has only status, attempts, created_at, processed_at; no next_attempt_at, max_attempts, last_error column, or dead-letter state (schema.sql). mark_outbox_failed sets status to failed, so it will not be picked up by list_outbox(status="pending") (task_lifecycle.py, task_lifecycle.py). Worse, Beads writeback failures are swallowed: _run_bd_for_task returns False on exceptions (services.py), _sync_beads_transition_ledger ignores that return (services.py), and the drain marks the row processed if no exception escaped (services.py).

  4. BLOCKER: cp.secrets.upsert does not exist, and secret dereference is underspecified.
    Spec §10 calls cp.secrets.upsert(...) (docs/linear-bridge-spec.md). SecretsService exposes create_secret, rotate_secret, request_secret, and reveal_secret, not upsert (secrets_service.py, secrets_service.py, secrets_service.py, secrets_service.py). The table also has name TEXT NOT NULL UNIQUE (schema.sql), so repeated auto-register using create would fail unless the design explicitly does get-or-rotate. Also, bridge-side plaintext access cannot simply "dereference via secrets_service"; reveal_secret requires an audit handle and accessor agent (secrets_service.py).

  5. MAJOR: auto-register is not race-safe enough for replicas or env drift.
    Spec §10 says linear_workspaces.name UNIQUE makes the loser’s ON CONFLICT DO UPDATE a no-op (docs/linear-bridge-spec.md). Existing Beads auto-register does the opposite: ON CONFLICT(name) DO UPDATE overwrites path/source/project/capabilities/interval/metadata (services.py). If Linear mirrors that pattern, divergent env between replicas becomes last-writer-wins. If it truly no-ops, secret rotation and config changes are ignored. The spec needs a config hash, explicit drift finding, and a defined rotation path.

  6. MAJOR: poll/import idempotency is overclaimed.
    Spec §6 says UNIQUE(source, external_id) gives idempotent re-import (docs/linear-bridge-spec.md). The index exists (schema.sql), but current import_project_item does SELECT, then create_task, then INSERT project_items (services.py, services.py, services.py). In two replicas, that can create duplicate tasks before one project item insert loses. Linear polling must use a single transaction/upsert, not the existing helper as-is.

  7. MAJOR: state_map is too flat for Linear.
    Spec §9.1 caches resolve_state_map(...) -> Dict[str, str] in linear_workspaces.state_map (docs/linear-bridge-spec.md) and §17 assumes lazy refresh on unknown state (docs/linear-bridge-spec.md). Linear workflow state UUIDs are per-team, so a flat {"Done": uuid} collides across teams. Lazy refresh also does not catch a renamed state whose old UUID still exists but now means something else operationally. Store per-team mappings keyed by team_id, include Linear state type/name/id, and refresh predictably, not only on unknown names.

  8. MAJOR: auth requirements do not match current API behavior.
    Spec §11 says workspace management requires admin and runtime calls require agent (docs/linear-bridge-spec.md). Current scope routing has no /bridge special case, so non-GET bridge routes fall through to generic write (api.py, api.py). Existing Beads bridge route handlers do not take a principal or enforce tenant/admin in-handler (api.py). Linear needs explicit scope mapping and tenant checks, especially for workspace/secret binding.

  9. MAJOR: plugin changes are incomplete and partly point to the wrong file.
    Spec §13 says "plugin/manifest.py gains provides_tools" (docs/linear-bridge-spec.md). Actual provides_tools lives in plugin.yaml, while manifest.py defines TOOLS. Adding tools also requires schemas in schemas.py, and possibly body shaping in init.py.

  10. MAJOR: phasing hides required dependencies.
    Spec §19 puts lifecycle comments in L3 and TrackerBackend in L4 (docs/linear-bridge-spec.md, docs/linear-bridge-spec.md). But L3 depends on an outbox redesign, backend dispatch, secret plaintext access, idempotency, and API auth choices. L1 also needs migrations in both SQLite and Postgres; current SQLite migrations are private _ensure_column calls (store.py), while Postgres uses packaged schema plus public ensure_column (store_postgres.py, store_postgres.py).

  11. MINOR: Beads drift and ledger details are not quite accurate.
    Spec §3 names beads.export_drift.jsonl_only_ready (docs/linear-bridge-spec.md); current open finding is beads.export_drift.ready_mismatch, while jsonl_only_ready is only resolved as legacy/old state (services.py, services.py). Spec §9.2 examples use mac-ledger v1: ...; actual Beads comments are pipe-delimited with task=, event=, actor=, and fingerprint fields (services.py, services.py).

What Is Right

integration_findings is the actual table name (schema.sql), and record_integration_finding uses it directly (services.py). project_items really has UNIQUE(source, external_id) (schema.sql). Secrets are Fernet-encrypted at rest (secrets_service.py). Dolt sync is disabled by default in _sync_beads_database (services.py).

Missing entirely: a tenant deletion story, Linear workspace/secret cleanup, Linear-specific observability taxonomy and metrics, explicit API token/scopes for bridge operations, and a migration ordering plan that keeps SQLite and Postgres aligned.