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 oflinear-bridge-spec.mdwas rewritten against this feedback. See the "Revision history" section in the spec for the mapping of findings → spec changes.
Architectural Review¶
-
BLOCKER:
TrackerBackendis not a behavior-preserving Beads retrofit.
Spec §7 says "BeadsBridgeServiceandLinearBridgeServiceeach 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 narrowbdsubprocess wrapper withcli_path()andrun()(beads_bridge_service.py, beads_bridge_service.py). The real bridge is embedded inControlPlane: registration at services.py, polling at services.py, import at services.py, writeback at services.py. A protocol aroundBeadsBridgeServicewould miss most actual behavior. -
BLOCKER: the proposed protocol is under-shaped for existing Beads semantics.
Spec §7 makescreate_issue,attach_pr, andupdate_statefirst-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 abead_id(services.py). Normal Hermes-created tasks arehermes_interactiontasks (services.py) andcreate_taskmay 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 samecreate_issuepath. -
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 becomedead_letter(docs/linear-bridge-spec.md). Actual schema has onlystatus,attempts,created_at,processed_at; nonext_attempt_at,max_attempts,last_errorcolumn, or dead-letter state (schema.sql).mark_outbox_failedsets status tofailed, so it will not be picked up bylist_outbox(status="pending")(task_lifecycle.py, task_lifecycle.py). Worse, Beads writeback failures are swallowed:_run_bd_for_taskreturnsFalseon exceptions (services.py),_sync_beads_transition_ledgerignores that return (services.py), and the drain marks the row processed if no exception escaped (services.py). -
BLOCKER:
cp.secrets.upsertdoes not exist, and secret dereference is underspecified.
Spec §10 callscp.secrets.upsert(...)(docs/linear-bridge-spec.md).SecretsServiceexposescreate_secret,rotate_secret,request_secret, andreveal_secret, notupsert(secrets_service.py, secrets_service.py, secrets_service.py, secrets_service.py). The table also hasname 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_secretrequires an audit handle and accessor agent (secrets_service.py). -
MAJOR: auto-register is not race-safe enough for replicas or env drift.
Spec §10 sayslinear_workspaces.name UNIQUEmakes the loser’sON CONFLICT DO UPDATEa no-op (docs/linear-bridge-spec.md). Existing Beads auto-register does the opposite:ON CONFLICT(name) DO UPDATEoverwrites 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. -
MAJOR: poll/import idempotency is overclaimed.
Spec §6 saysUNIQUE(source, external_id)gives idempotent re-import (docs/linear-bridge-spec.md). The index exists (schema.sql), but currentimport_project_itemdoesSELECT, thencreate_task, thenINSERT 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. -
MAJOR:
state_mapis too flat for Linear.
Spec §9.1 cachesresolve_state_map(...) -> Dict[str, str]inlinear_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 byteam_id, include Linear state type/name/id, and refresh predictably, not only on unknown names. -
MAJOR: auth requirements do not match current API behavior.
Spec §11 says workspace management requiresadminand runtime calls requireagent(docs/linear-bridge-spec.md). Current scope routing has no/bridgespecial case, so non-GET bridge routes fall through to genericwrite(api.py, api.py). Existing Beads bridge route handlers do not take aprincipalor enforce tenant/admin in-handler (api.py). Linear needs explicit scope mapping and tenant checks, especially for workspace/secret binding. -
MAJOR: plugin changes are incomplete and partly point to the wrong file.
Spec §13 says "plugin/manifest.pygainsprovides_tools" (docs/linear-bridge-spec.md). Actualprovides_toolslives in plugin.yaml, while manifest.py definesTOOLS. Adding tools also requires schemas in schemas.py, and possibly body shaping in init.py. -
MAJOR: phasing hides required dependencies.
Spec §19 puts lifecycle comments in L3 andTrackerBackendin 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_columncalls (store.py), while Postgres uses packaged schema plus publicensure_column(store_postgres.py, store_postgres.py). -
MINOR: Beads drift and ledger details are not quite accurate.
Spec §3 namesbeads.export_drift.jsonl_only_ready(docs/linear-bridge-spec.md); current open finding isbeads.export_drift.ready_mismatch, whilejsonl_only_readyis only resolved as legacy/old state (services.py, services.py). Spec §9.2 examples usemac-ledger v1: ...; actual Beads comments are pipe-delimited withtask=,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.