Multi-tracker federation¶
TL;DR¶
| Component | Purpose |
|---|---|
FederationConfig | Typed view over bernstein.yaml: federation. |
LinkDetector (+ 3 defaults) | Find cross-tracker refs in ticket bodies, fields, comments. |
FederationBuilder | Walks every adapter once and builds the per-run graph. |
FederatedTicketGraph | Small in-memory graph rendered as agent-run context. |
FederationDispatcher | Reads/comments/transitions with per-role allow-list and audit. |
CrossTrackerAuditRecord | JSONL row recording every cross-tracker action. |
Vendor tracker AI is tenant-locked. The federation layer lets one Bernstein orchestrator instance pull tickets from Linear, GitHub Projects, Jira, Notion, etc. in a single run, detect cross-references, and dispatch an agent that can act across the joined surface, with every cross-tracker action recorded in the audit log.
Configuration¶
Add a federation block to bernstein.yaml:
federation:
linked_trackers:
- linear
- github_projects
- notion
link_detectors:
- url
- custom_field
- comment_mention
cross_tracker_dispatch:
allow:
backend:
- github_projects
reviewer: "*"
Field reference:
| Key | Type | Meaning |
|---|---|---|
linked_trackers | list of strings | Adapter names participating in this federation. |
link_detectors | list of strings | Detector names to enable. Defaults to all three. |
cross_tracker_dispatch.allow | mapping role -> trackers | Per-role write allow-list. "*" grants every tracker. |
A role missing from the allow-list is deny-all for cross-tracker writes. Reads are never gated, but they still produce an audit entry so cross-tracker information flow remains traceable.
Worked example: Linear + GitHub Projects + Notion¶
Three adapters are configured. A Linear ticket links to a GitHub Projects issue in its body, which in turn carries an External-Link custom field pointing at a Notion page.
from pathlib import Path
from bernstein.core.orchestration.federation import (
FederationBuilder,
FederationConfig,
FederationDispatcher,
write_audit_record,
)
builder = FederationBuilder(adapters=[linear, github_projects, notion])
graph = builder.build()
print(graph.render_context())
Rendered output:
federation:
- node: github_projects:42
-> notion:abc (custom_field: https://notion.so/p/abc)
- node: linear:LIN-1
-> github_projects:42 (url: https://github.com/acme/repo/issues/42)
- node: notion:abc
The agent run consumes the rendered block as a single context message. Tool calls into the dispatcher are namespaced by tracker:
cfg = FederationConfig.from_dict(yaml_block)
sink = []
dispatcher = FederationDispatcher(
graph=graph,
adapters={
"linear": linear,
"github_projects": github_projects,
"notion": notion,
},
config=cfg,
role="backend",
audit_sink=lambda record: write_audit_record(record, Path(".sdd")),
)
dispatcher.comment(
"github_projects", "42", "linked from LIN-1", from_tracker="linear"
)
The dispatcher routes the comment to the correct adapter, records the link_kind derived from the graph (here url), and appends one JSONL row to .sdd/lineage/cross-tracker-audit.jsonl.
Default detectors¶
| Detector | Triggers on | Notes |
|---|---|---|
URLDetector | tracker_uri_base prefix found in body or comments. | Skips URLs that point at the source tracker. |
CustomFieldDetector | Custom-field keys named like External-Link, links. | Case-insensitive on field names. |
CommentMentionDetector | Prefix-style ids (JIRA-1234, LIN-456) and optionally bare #1234. | Bare-hash mode requires hash_default_tracker. |
Detectors are side-effect-free, may be enabled or disabled per project, and additional detectors can be passed to FederationBuilder(detectors=...) without touching the core code path.
Audit record shape¶
Every cross-tracker action emits one CrossTrackerAuditRecord:
| Field | Description |
|---|---|
event | One of cross_tracker_read, cross_tracker_read_miss, cross_tracker_comment, cross_tracker_transition, cross_tracker_write_blocked. |
timestamp | Wall-clock seconds. |
role | Agent role driving the dispatcher. |
tracker_name_from | Tracker context the agent was reasoning from. |
tracker_name_to | Adapter that received the call. |
ticket_id_from | Source ticket id, if any. |
ticket_id_to | Destination ticket id. |
link_kind | Detector kind that justified the action, joined with + if multiple. |
action | read, comment, or transition:<state>. |
detail | Free-form note (e.g. "adapter read-only"). |
Records land in .sdd/lineage/cross-tracker-audit.jsonl by default and can be tailed alongside the lineage merge audit, which uses the same field naming convention.
Permission boundary¶
The dispatcher consults FederationConfig.is_allowed(role, tracker) before any write. A blocked attempt raises FederationPermissionError and produces no audit record, since no action took place. A write that reaches a read-only adapter raises TrackerReadOnlyError and emits a cross_tracker_write_blocked audit row with detail="adapter read-only". The federation test suite exercises both paths and a chained three-tracker setup.
Out of scope (v1)¶
- Auto-discovery of which trackers a team uses.
- Cross-tracker ticket creation.
- Bi-directional link sync.
- Cross-tenant federation across organisations.