paperclip/docs/specs/external-task-protocol.md

57 KiB
Raw Permalink Blame History

Paperclip External Task Protocol Specification

Status: Draft v1 (provider-agnostic)

Purpose: Define the protocol by which Paperclip interoperates with external task managers (Linear, Jira, Asana, Notion, Trello, GitHub Issues, and others) so that external tasks can drive Paperclip agent work and Paperclip work is visible in the external tool, without requiring an external user account for every Paperclip agent.

Normative Language

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.

Implementation-defined means the behavior is part of the implementation contract, but this specification does not prescribe one universal policy. Implementations MUST document the selected behavior.

Terminology in this document:

  • Provider — an external task manager product (Linear, Jira, Asana, Notion, Trello, GitHub Issues, ...).
  • Connector — an implementation of this protocol for one provider, hosted in the Paperclip plugin runtime.
  • External task — a task record that lives in the provider.
  • Paperclip issue — the native Paperclip task record.
  • Host — the Paperclip control plane: companies, agents, issues, checkout, heartbeat runs, execution workspaces, budgets, approvals, blockers, issue documents, and work products.

1. Problem Statement

Teams already run their work in external task managers. Paperclip runs companies of agents. Today these worlds only meet through manual copy/paste: a human reads a Linear issue, rewrites it as a Paperclip issue, and later rewrites the outcome back into Linear.

This protocol defines the contract that lets an external task manager act as a task source and collaboration surface for a Paperclip company, while Paperclip remains the execution and governance control plane.

The protocol solves four interoperability problems:

  • It defines one normalized task model so each connector translates provider payloads once, at the edge, instead of leaking provider-specific shapes into agent prompts and core logic.
  • It defines how an external task becomes (or links to) exactly one Paperclip issue, with idempotent sync state that survives retries, webhook replays, and restarts.
  • It defines how external assignment routes to Paperclip agents without creating a provider user per agent.
  • It defines which writes flow back to the provider, under what policy, and with what provenance, so sync is predictable rather than chatty or destructive.

Important boundary (inherited from OpenAI Symphony, see Section 3.3):

  • The connector is a tracker reader, sync engine, and routing layer.
  • Provider writes (state transitions, comments, links) are explicit, policy-bound connector operations — never hidden side effects of core host logic.
  • A successful agent run can end at a workflow-defined handoff state (for example review), not necessarily the provider's Done.

2. Goals and Non-Goals

2.1 Goals

  • Let an external task manager act as a task source: active external tasks create or wake Paperclip issues under configured policy.
  • Keep one normalized external-task model across all providers, with provider-specific data carried in an envelope, not in the core model.
  • Route external assignment intent to Paperclip agents through deterministic, configurable rules.
  • Reuse Paperclip's existing execution semantics — checkout as claim, heartbeat runs, execution workspaces, retries and recovery — as a Symphony-compatible orchestration profile.
  • Make outbound writes explicit, policy-bound, idempotent, and attributable.
  • Detect conflicts instead of silently overwriting either side.
  • Give operators observability: link state, sync health, cursors, queues, retries, and conflicts.
  • Keep connectors implementable as plugins, without private host internals.

2.2 Non-Goals

  • Replacing the Paperclip board with a clone of any provider's UI.
  • Real-time field-level collaborative editing between Paperclip and a provider.
  • A generic workflow engine or a universal schema covering every provider feature. Provider features beyond the normalized model stay in the provider envelope.
  • Creating a provider user account per Paperclip agent (supported as an OPTIONAL mapping, never required).
  • Letting external systems bypass Paperclip governance: checkout, budgets, approvals, blockers, execution policies, and company boundaries remain host-owned.
  • Migrating data: this is a sync protocol, not an importer/exporter for full historical fidelity.

3. System Overview

3.1 Main Components

  1. Task Source Adapter

    • Provider client owned by the connector.
    • Fetches candidate external tasks and task states.
    • Normalizes provider payloads into the ExternalTask model.
    • Performs provider writes when policy allows.
  2. Task Link Store

    • Persists the 1:1 association between an external task and a Paperclip issue.
    • Owns sync state: snapshots, fingerprints, cursors, status.
    • MUST be stored in plugin state/entities first; MAY be promoted to core schema once at least two connectors prove the model (see Section 16.4).
  3. Inbound Sync Engine

    • Consumes webhooks and poll results.
    • Creates/updates Paperclip issues and imports comments with provenance.
    • Runs scheduled reconciliation to repair drift.
  4. Routing Engine

    • Resolves external assignment intent into a Paperclip assignee using the ordered rule pipeline in Section 8.
  5. Execution Bridge

    • Maps tracker-driven work onto host execution: issue checkout (claim), heartbeat runs, execution workspaces, retry/recovery. Defined as the Symphony profile in Section 9.
  6. Outbound Write Engine

    • Applies the write policy in Section 10: progress comments, status projection, backlinks, artifact/PR links.
  7. Conflict Queue

    • Records detected conflicts and exposes resolution actions (Section 11).
  8. Health Surface

    • Operator-visible connector status: last webhook, last poll, last reconcile, cursor positions, queue depth, retry counts, current errors (Section 12).

3.2 Protocol Layers

The protocol is layered. A connector MAY implement lower layers without higher ones.

  1. Layer 1 — Task Source Adapter (REQUIRED)

    • Normalized read access to provider tasks. Sufficient for browse/import UX.
  2. Layer 2 — Task Link (REQUIRED)

    • Durable external-task ↔ Paperclip-issue association with sync state.
  3. Layer 3 — Execution Orchestration, Symphony Profile (RECOMMENDED)

    • Active external states drive Paperclip issue creation/wake; Paperclip checkout/run/retry semantics are projected back as claim state.
  4. Layer 4 — Write and Sync Policy (RECOMMENDED)

    • Policy-bound outbound writes and two-way comment/status sync.

A connector that implements Layers 13 for read-driven execution is a Symphony-compatible task source (Section 9.6).

3.3 Relationship to OpenAI Symphony

OpenAI Symphony specifies a service in which an issue tracker drives isolated, autonomous coding agent runs: normalized tracker issues, active/terminal state mapping, claim/run/retry/release lifecycle, per-issue workspaces, repository-owned workflow policy (WORKFLOW.md), and operator-visible observability.

This protocol deliberately defers to Symphony's semantics wherever Symphony solves the same problem, and maps them onto existing host primitives:

Symphony concept This protocol
Tracker client + normalized issue Task Source Adapter + ExternalTask (Section 5)
Active / terminal states Connector state mapping (Section 7.2)
Claim Paperclip issue checkout + execution lock (Section 9.2)
Running map / live session Heartbeat runs + issue liveness state (Section 9.3)
Retry queued Host retry/recovery + scheduled wakes (Section 9.3)
Released Issue terminal/review/blocked, or external task no longer active (Section 9.3)
Per-issue workspace Execution workspace / worktree (Section 9.4)
WORKFLOW.md Workflow policy document (Section 9.5)
Tracker writes via agent tools, not orchestrator Outbound Write Engine + connector tools (Section 10.1)
Status surface Issue/run/connector health UI (Section 12)

Where Symphony is intentionally narrow — single repository, coding agents only, one tracker, no multi-tenant control plane — this protocol extends it with company scoping, multi-agent routing, governance gates, issue documents/work products, and two-way sync policy. Section 9.6 defines what a connector MUST do to claim Symphony-profile conformance. Appendix B gives a section-by-section crosswalk.

3.4 Core vs Connector Boundary

The host MUST own:

  • Company scoping and authorization of every mutation.
  • Issue checkout, execution locks, heartbeat invocation, and execution workspace lifecycle.
  • Budget, approval, blocker, and execution-policy enforcement.
  • Issue documents and work products.
  • The normalized protocol types, once validated (shared SDK package).

Connectors MUST own:

  • Provider authentication and webhook verification.
  • Provider polling, pagination, and cursor management.
  • Provider-specific state mapping and the provider envelope.
  • All provider writes (comments, transitions, fields, links).
  • Provider-specific setup, settings, and issue-detail UI.

A connector MUST perform all Paperclip issue/comment/status mutations through the capability-gated plugin host clients (for example, ctx.issues) with plugin attribution. It MUST use ctx.entities or ctx.state for connector-owned link and cursor data, and secret references for provider credentials. A connector MUST NOT write host tables directly.

4. Core Domain Model

Field names use camelCase, matching Paperclip API conventions. (Symphony uses snake_case; the crosswalk in Appendix B maps equivalent fields.)

4.1 Entities

4.1.1 ExternalTask

Normalized external task record produced by the Task Source Adapter. This is the only shape the host, routing engine, and prompt/task-packet rendering may depend on.

Fields:

  • providerKey (string)
    • Stable provider identifier: linear, jira, asana, notion, trello, github_issues.
  • externalId (string)
    • Stable provider-internal ID. Used for lookups and link keys.
  • externalKey (string or null)
    • Human-readable key where the provider has one (example: ABC-123). Null for providers without keys (Trello, Notion).
  • title (string)
  • description (string or null)
    • Normalized to Markdown on a best-effort basis; lossy conversions MUST be noted in the envelope.
  • state (object)
    • name (string) — provider state/lane name.
    • category (enum) — normalized category: backlog, active, review, blocked, terminal (Section 7.2).
  • priority (integer or null)
    • Normalized 04, lower is more urgent. Non-mappable provider scales become null with the raw value preserved in the envelope.
  • assignees (list of external actor refs)
  • labels (list of strings)
    • Trimmed, lowercased.
  • blockedBy (list of external task refs: {externalId, externalKey, state})
    • Derived from provider relations where supported; empty otherwise.
  • url (string or null)
  • branchName (string or null)
    • Provider-supplied branch metadata if available.
  • dueAt, createdAt, updatedAt (timestamps or null)
  • revision (string or null)
    • Provider revision/version/etag when available; used for conflict detection.
  • envelope (object)
    • Provider-specific payload (Section 4.1.2). Opaque to the host.

4.1.2 ProviderEnvelope

Provider-specific data that does not generalize. Carried alongside the normalized model, persisted with the task link, surfaced in provider-specific UI, and available to connector tools. The host core MUST NOT branch on envelope contents.

Representative envelope content per provider is listed in Appendix A.

The durable 1:1 association between one external task and one Paperclip issue.

Fields:

  • id (string)
  • providerKey (string)
  • connectorInstanceId (string)
    • One installed connector configuration (a provider MAY be installed multiple times per company, e.g. two Jira sites).
  • companyId, projectId, goalId, issueId (strings; issueId is the linked Paperclip issue)
  • handoffHistory (list of {issueId, endedAt, cause}; empty for a link that has never moved)
    • Prior Paperclip issue associations retained when reopened work is handed to a follow-up.
  • externalWorkspaceId, externalProjectId (strings or null)
    • Provider container coordinates (team/project/board/database).
  • externalTaskId, externalKey, externalUrl
  • originSide (enum: paperclip, external, manual_link)
    • Which side created the pairing.
  • syncMode (enum, Section 4.2.1)
  • runMode (enum, Section 4.2.2)
  • fieldPolicy (map field → owner, Section 4.1.4)
  • baseSnapshot (object)
    • Last agreed projected state of both sides; the three-way merge base for conflict detection.
  • lastExternalRevision (string or null)
  • lastPaperclipFingerprint (string or null)
    • Hash of the last Paperclip-side state this connector projected outbound.
  • lastInboundAt, lastOutboundAt, lastReconcileAt (timestamps or null)
  • status (enum, Section 4.2.3)
  • statusDetail (string or null)
    • Human-readable explanation for conflict, error, paused.

Invariants:

  • A (connectorInstanceId, externalTaskId) pair MUST map to at most one active link.
  • An issueId MUST appear in at most one active link per connector instance, and SHOULD appear in at most one active link overall; cross-connector double-linking is implementation-defined and MUST be surfaced in UI when allowed.
  • Every link MUST belong to exactly one companyId, resolved at link creation, before any mutation on either side.

4.1.4 FieldOwnershipPolicy

Per-field ownership controlling sync direction and conflict classification.

  • Owners: external, paperclip, shared.
  • Coverable fields (minimum): title, description, state, priority, labels, assignee, dueAt.
  • Default policy (RECOMMENDED): title/description/priority/labels/dueAt owned by external for links with originSide=external (and by paperclip when originSide=paperclip); state is shared and mediated by Section 7.4 and Section 10.3; assignee is never directly synced — it flows through routing (Section 8).
  • Writes from the non-owning side to an owned field MUST NOT be applied silently; they produce a conflict entry (Section 11) or are dropped per policy, and the choice MUST be visible in link health.

4.1.5 RoutingRule

One rule in the connector's ordered routing pipeline (Section 8).

Fields:

  • order (integer)
  • kind (enum: default_route, custom_field, label, state_lane, comment_command, user_mapping)
  • match (object, kind-specific: field id + value pattern, label pattern, lane/state name, external user id)
  • target (object)
    • agentId (string) or routingAlias (string, e.g. triage, qa, resolved against company configuration).
  • enabled (boolean)

4.1.6 ExternalActor

Provenance identity for inbound content.

Fields:

  • providerKey
  • externalUserId (string)
  • displayName (string)
  • mappedUserId (string or null) — Paperclip user, if mapped.
  • mappedAgentId (string or null) — Paperclip agent, if explicitly mapped (OPTIONAL feature).
  • isConnectorServiceAccount (boolean)
    • True when the actor is the connector's own provider identity. REQUIRED for echo suppression (Section 10.4).

4.1.7 SyncCursor

Per-connector-instance incremental sync state.

Fields:

  • scope (string) — e.g. tasks:<externalProjectId>, comments:<externalTaskId>.
  • cursor (string) — provider-opaque pagination/delta token or high-water-mark timestamp.
  • updatedAt (timestamp)

Cursors MUST be persisted transactionally with the effects of the page they describe, so a crash between "apply page" and "save cursor" re-applies idempotently rather than skipping.

4.1.8 ConnectorHealth

Operator-visible runtime state per connector instance.

Fields:

  • lastWebhookAt, lastPollAt, lastReconcileAt (timestamps or null)
  • webhookVerified (boolean)
  • queueDepth (integer) — pending inbound events.
  • retryCount (integer) — currently scheduled retries.
  • conflictCount (integer) — open conflict entries.
  • lastError (object or null: {category, message, at})
  • status (enum: healthy, degraded, error, paused)

4.2 Enumerations

4.2.1 syncMode

Per-link data flow direction:

  • import_only — external → Paperclip only.
  • export_only — Paperclip → external only.
  • bidirectional — both directions under fieldPolicy.
  • observer — read and display external state; mutate neither side.
  • disabled — link retained, no sync.

4.2.2 runMode

Per-link execution posture:

  • paperclip_controlled — Paperclip governance decides when agents run; external state changes inform but do not command execution.
  • symphony_compatible — active external states create/wake Paperclip work automatically per Section 9; the external tracker effectively drives dispatch.
  • external_observer — no execution coupling; link exists for visibility only.
  • pending — link created, first sync not yet completed.
  • healthy — last sync succeeded, no open conflicts.
  • conflict — one or more unresolved conflict entries.
  • error — last sync attempt failed; retry scheduled or exhausted.
  • paused — operator suspended sync.
  • unlinked — association severed; record retained for audit.

4.3 Stable Identifiers and Normalization Rules

  • External Task ID — use for provider lookups and link keys. Never display-only keys.
  • External Key — use for human-readable logs, comments, and UI.
  • Link key(connectorInstanceId, externalTaskId).
  • Idempotency key for inbound effects — (connectorInstanceId, externalTaskId, eventId) where the provider supplies event IDs, else a content hash of the normalized change. Replayed webhooks MUST NOT duplicate issues or comments.
  • Idempotency key for outbound effects — (linkId, effectKind, paperclipSourceId) (e.g. the Paperclip comment ID being mirrored). Retried jobs MUST NOT double-post.
  • State names compare after trim + lowercase.
  • Labels normalize to trimmed, lowercased strings.
  • Timestamps normalize to ISO-8601 UTC.

5. Task Source Adapter Contract

5.1 REQUIRED Operations

A connector MUST implement:

  1. listCandidateTasks(scope, cursor) -> {tasks: ExternalTask[], cursor}

    • Return tasks in configured containers (team/project/board/database), filtered to configured states where the provider supports server-side filtering, paginated.
  2. getTask(externalTaskId) -> ExternalTask

  3. getTaskStates(externalTaskIds) -> {externalTaskId -> state}

    • Batched state refresh for reconciliation (Section 7.1.3) and run-eligibility checks (Section 9.3).
  4. listComments(externalTaskId, cursor) -> {comments: ExternalComment[], cursor}

    • Each comment carries externalCommentId, ExternalActor, body (Markdown best-effort), timestamps.
  5. verifyWebhook(request) -> VerifiedEvent | reject

    • Validate provider signatures/secrets where the provider supports them (Section 14.2). For providers without signed webhooks, the connector MUST treat webhook payloads as untrusted hints and confirm by API read-back before applying effects.

5.2 OPTIONAL Write Operations and Capability Declaration

Write operations are OPTIONAL per connector and gated by policy (Section 10):

  • createTask(projection) -> ExternalTask
  • updateTaskState(externalTaskId, targetStateName)
  • postComment(externalTaskId, body, attribution) -> externalCommentId
  • setFields(externalTaskId, partial fields per fieldPolicy)
  • setBacklink(externalTaskId, paperclipUrl)
    • Via link field, custom field, or pinned comment — provider-dependent.

A connector MUST declare a static capability set (readable by host UI) enumerating which operations it implements, which state categories it can map, whether webhooks are signed, and which routing rule kinds it supports. Host UX MUST degrade gracefully (hide actions, explain gaps) based on declared capabilities.

5.3 Normalization Requirements

  • Adapter output MUST match Section 4.1.1. Provider payload fields with no normalized home go in the envelope.
  • State category mapping (Section 7.2) is part of adapter configuration, not code, wherever the provider has user-defined workflows (Jira, Linear, Trello lists, Notion selects).
  • Rich text MUST convert to Markdown best-effort; the envelope SHOULD retain the source format reference for lossless round-trips where the connector supports them.
  • Person references in body text SHOULD be converted to plain display names; raw provider mention syntax MUST NOT leak into Paperclip issue bodies where it could be misparsed as Paperclip mentions.

5.4 Error Handling Contract

RECOMMENDED error categories:

  • provider_auth (expired/invalid credentials)
  • provider_rate_limited (with retry-after when available)
  • provider_request (transport failures)
  • provider_status (non-2xx)
  • provider_payload (unparseable/unknown payload)
  • cursor_invalid (provider rejected or lost the cursor)
  • webhook_signature (verification failure)

Engine behavior on adapter errors:

  • Candidate fetch failure: log, mark health degraded, skip this tick; never tear down existing links or active work.
  • State refresh failure during reconciliation: keep current link state; do not release claims or stop runs on missing data (matches Symphony's reconciliation posture).
  • cursor_invalid: fall back to a bounded full re-list with idempotent re-apply; never wipe and re-import.
  • provider_rate_limited: back off honoring provider guidance; webhook intake continues to queue.
create                         -> pending
pending  -- first sync ok      -> healthy
healthy  -- sync failure       -> error
error    -- retry ok           -> healthy
healthy  -- conflict detected  -> conflict
conflict -- resolved           -> healthy
any state -- operator pause    -> paused (resume returns to previous state)
any state -- unlink            -> unlinked (terminal; record retained)

Transitions MUST be recorded with timestamps and causes in the link record or its audit trail.

  1. Import (originSide=external)

    • An external task is selected (manually, or automatically under runMode=symphony_compatible state mapping) and a Paperclip issue is created from its projection.
    • The created issue MUST carry: source attribution (provider, external key, URL), the normalized description, and routing-resolved assignee (Section 8).
  2. Export (originSide=paperclip)

    • A Paperclip issue is projected into the provider via createTask.
    • REQUIRED only for connectors declaring write capability.
  3. Manual link (originSide=manual_link)

    • Operator pairs an existing external task with an existing Paperclip issue.
    • The connector MUST compute an initial baseSnapshot from both sides and surface immediate divergence as conflicts rather than picking a winner.

6.3 Deduplication

Before creating an issue or external task, the connector MUST check, in order:

  1. Active link with the same (connectorInstanceId, externalTaskId).
  2. Paperclip backlink already present on the external task (custom field/comment marker).
  3. External URL/key reference already present on a Paperclip issue in the same company.

Title-similarity matching MAY be used to warn in import UX; it MUST NOT silently merge.

  • Unlink severs sync but MUST NOT delete either side's record.
  • When an external task is deleted/archived: the connector MUST NOT delete or cancel the Paperclip issue. Default behavior: mark the link conflict (statusDetail: external task deleted), comment on the Paperclip issue, and let the assignee/operator decide. import_only links with no Paperclip-side activity MAY auto-unlink.
  • When a Paperclip issue is cancelled/deleted: outbound policy decides whether to comment and/or transition the external task; the connector MUST NOT delete the external task unless an operator explicitly invokes a delete capability.

7. Inbound Synchronization (External → Paperclip)

7.1 Event Channels

A connector MUST implement at least one push or pull channel, and MUST implement reconciliation.

7.1.1 Webhooks

Webhooks are RECOMMENDED where the provider offers them. They are verified per Section 5.1(5). Events are queued, deduplicated by idempotency key, and applied in per-task order. Webhooks are treated as hints: on any doubt, re-read via the adapter.

7.1.2 Polling

Polling uses cursor-based incremental listing on a configured cadence. It is REQUIRED when webhooks are unavailable or unverified, and RECOMMENDED as a backstop even with webhooks.

7.1.3 Reconciliation

Reconciliation is REQUIRED. A scheduled job compares linked tasks' current provider state against baseSnapshot and the linked issue. It repairs missed events, detects deletions or archivals, and refreshes health timestamps. Reconciliation MUST be rate-bounded and incremental (cursor or rolling window). It MUST NOT re-read every link on every run for large installations.

7.2 State Mapping

Each connector instance MUST carry an explicit mapping from provider states/lanes to the normalized categories:

  • backlog — visible, not requesting execution.
  • active — requesting execution (Symphony's "active states").
  • review — provider-side human review/handoff.
  • blocked — provider-side blocked indication, where representable.
  • terminal — done/cancelled/archived.

Mapping rules:

  • Mapping is configuration, validated at setup against live provider workflow metadata where available.
  • Unmapped states MUST be treated as backlog (inert) and reported in health, never guessed.
  • Category transitions drive issue effects per Section 7.4 and run effects per Section 9.3.

7.3 Comment Import and Provenance

  • Imported comments MUST carry ExternalActor provenance and a deep link to the external comment where the provider supports it.
  • Imported comment content is untrusted input: it MUST be clearly attributed in the issue thread, MUST NOT be interpreted as Paperclip system/agent instructions, and mention-like syntax MUST NOT trigger Paperclip mention semantics except via the strict command path (Section 8.2).
  • Whether an imported comment wakes the issue assignee is link policy (wakeOnExternalComment, default true for symphony_compatible, false for observer).
  • Comments authored by the connector's own service account MUST be suppressed on import (Section 10.4).

7.4 External State Transitions Against Paperclip Work

When the external task changes category:

  • backlog -> active: create or wake per runMode (Section 9.3).
  • active -> backlog or active -> blocked: the connector SHOULD surface this on the issue; under symphony_compatible it MUST make the issue ineligible for new tracker-driven dispatch and MUST request stop of a tracker-driven run only when the plugin host declares an issue-scoped run-stop capability (Section 9.3). Without that capability, it records a health warning and lets the active run finish. It MUST NOT pause the whole agent because that could stop unrelated work. Paperclip-native obligations (approvals in flight, blockers) are unaffected.
  • * -> terminal while the Paperclip issue is active (checked out, running, in review, or carrying unresolved blockers/approvals): default is a conflict, not silent closure. The issue gets a comment naming the external actor and transition; the link enters conflict until an agent or operator resolves it. Auto-close MAY be enabled per link policy only when no active run, no pending review/approval, and no blockers exist.
  • terminal -> active (reopen): if the linked issue is terminal, policy chooses between creating a follow-up Paperclip issue linked to the same external task (RECOMMENDED default) or reopening, subject to host rules for resuming closed issues. The connector MUST create or find the follow-up with a stable idempotency key while the existing link remains active. After the follow-up is durably identified, it retargets that same TaskLink record to the follow-up and appends the old issueId to handoffHistory in one record update. On the current plugin SDK, this is one ctx.entities.upsert of the entity keyed by the external task, not an unlink plus a second entity write; inbound processing is already serialized per task (Section 7.1.1). A crash before the upsert leaves the old link active, and a retry reuses the idempotent follow-up. A crash after it leaves the retargeted link active. This preserves the link-key invariant in Section 4.1.3 without requiring a multi-record transaction, while handoffHistory retains the original association for audit.

External transitions MUST NOT directly set Paperclip issue status; they translate into host-level requests that respect checkout, approvals, blockers, budget stops, and execution policy.

8. Agent Routing (Assignment Without External Agent Users)

External assignment intent resolves to a Paperclip assignee through an ordered pipeline. The first matching enabled rule wins. Routing runs at import, and again whenever external assignment intent changes (assignee change, label/field change, lane move, command comment).

8.1 Routing Pipeline

  1. comment_command — strict slash command in an external comment (Section 8.2).
  2. custom_field — provider custom field naming an agent (example: Paperclip Agent = CodexCoder).
  3. label — namespaced label (example: pc:agent/codexcoder, pc:route/qa).
  4. state_lane — provider state/lane mapped to a route (example: Trello list Paperclip: QA).
  5. user_mapping — explicit external-user → agent mapping table (OPTIONAL; for teams that choose to create provider users for agents).
  6. default_route — connector/project default (example: all imported tasks go to a triage agent or the CTO agent).

Requirements:

  • Routing MUST resolve before the issue is created; every imported issue has an assignee.
  • If no rule matches and no default route exists, the connector MUST assign the configured triage fallback and post a visible "unresolved routing" comment on both sides (external side only if write policy allows). Import MUST NOT be dropped silently.
  • Routing targets are validated against the live company agent registry at apply time; routes to missing/paused agents fall through to triage with a health entry.
  • Re-routing an issue that is checked out MUST NOT force-reassign; it posts a handoff request comment for the current owner (host reassignment rules apply).

8.2 Strict Comment Commands

Comment commands give external users explicit control without new UI in the provider.

  • Syntax: line-anchored /paperclip <verb> [args]. Minimum verbs: assign <agent>, status, pause, resume. Connectors MAY add verbs; all verbs MUST be listed in capability metadata.
  • Parsing MUST be deterministic connector code. An LLM MUST NOT infer intent from free text.
  • The connector MUST reply (externally, if writes allowed; on the Paperclip issue otherwise) with the command outcome, including rejections (unknown agent, not permitted).
  • Command authorization is implementation-defined but MUST be documented (e.g. any provider member vs. mapped users only).

8.3 Identity Model

  • Paperclip agents MUST NOT be required to exist as provider users.
  • Outbound writes use one connector service account per connector instance (Section 10.4), with agent attribution carried in message content ("CodexCoder via Paperclip").
  • user_mapping rules MAY map specific provider users to Paperclip users (for assign it back to me flows) and to agents, but every such mapping is explicit configuration.

9. Execution Orchestration: Symphony Profile

This section defines how tracker-driven execution maps onto host primitives. It applies to links with runMode=symphony_compatible; paperclip_controlled links use only Sections 9.49.5 advice.

9.1 Dispatch Model

Symphony's poll-tick loop becomes, in Paperclip:

  1. Inbound sync (webhook/poll/reconcile) maintains link + normalized state.
  2. For each link whose external category is active, the connector first moves a linked backlog issue to todo through the host issue client, subject to normal host transition rules. A backlog issue is not wakeable and MUST NOT receive a wake request directly.
  3. When the linked issue is todo, not blocked, and not awaiting approval, the connector wakes the assignee through ctx.issues.requestWakeup.
  4. Host heartbeat scheduling — not the connector — decides actual run start, respecting company concurrency, budget, and execution policy. The connector MUST NOT spawn agent processes itself.

Bounded concurrency, dispatch ordering by priority, and per-issue serialization are host responsibilities; the connector's job ends at "make the issue actionable and wake the right agent."

9.2 Claim Semantics

  • Paperclip issue checkout is the claim. One agent owns an issue at a time; checkout conflicts (409) mean the issue is already claimed.
  • The connector MUST treat checkout state as authoritative and MUST NOT maintain a parallel claim registry for linked issues.
  • Claim state SHOULD be projected outbound (Section 10.3) so external users can see that an agent has picked the task up.

9.3 Run Lifecycle, Retry, Release

Host-side equivalents of Symphony's run-attempt machine:

  • Running — an active heartbeat run exists for the issue's assignee on this issue.
  • RetryQueued — host recovery/scheduled wake exists (failed run recovery, blocked-resume, scheduled continuation).
  • Released — issue reached terminal/review/blocked state, or the external task left active.

Profile requirements:

  • When the external task leaves active during tracker-driven work, the connector MUST mark the link state accordingly and stop issuing new connector wake requests. If the plugin host declares an issue-scoped run-stop capability, the connector MUST use it for the active issue run. It MUST NOT kill processes directly or approximate issue-scoped stop by pausing the assigned agent.
  • The current plugin SDK does not expose an issue-scoped run-stop operation. On that host version, a connector MUST record issue_run_stop_unsupported, let the active run finish, and MUST NOT claim full Symphony profile conformance. Layers 1, 2, and 4 remain implementable.
  • Failed runs follow host retry/recovery; the connector MUST NOT re-wake an issue in a tight loop (wake requests for the same link MUST be debounced, RECOMMENDED minimum 60s).
  • A successful run that ends at in_review/handoff is a valid terminal outcome for the tracker-driven cycle (mirrors Symphony's Human Review boundary); the connector projects review, it does not force terminal.

9.4 Workspaces

Per-issue isolation is provided by host execution workspaces/worktrees keyed by issue. Links and follow-up issues that must share a checkout use host workspace-inheritance (inheritExecutionWorkspaceFromIssueId); the connector never manages filesystem workspaces.

9.5 Workflow Policy

Symphony's repository-owned WORKFLOW.md maps to a layered policy lookup for rendering the task packet (Section 9.5.1) and run guidance:

  1. Paperclip project workflow document (RECOMMENDED MVP form).
  2. Repository WORKFLOW.md discovered in the project workspace (OPTIONAL, code projects).
  3. Connector instance defaults.

Precedence and safety:

  • Workflow policy is versioned project policy. It MAY shape prompts, validation steps, and handoff targets.
  • Workflow policy MUST NOT override host system/company/agent instructions, governance gates, or this protocol's invariants. It is additive guidance, lowest precedence.

9.5.1 Task Packet Rendering

Tracker-originated work MUST reach the agent as a rendered task packet, not raw provider text:

  • Inputs: normalized ExternalTask, link metadata (backlink URL, sync/run mode), workflow policy, attempt/continuation context.
  • The packet MUST mark external content as externally-authored untrusted input.
  • The packet MUST include the external key/URL so agents can reference the source task in comments and PRs.
  • Rendering MUST be strict: missing required variables fail the dispatch with a health entry rather than emitting a partial prompt.

9.6 Symphony Profile Conformance

A connector MAY claim "Symphony-compatible task source" when:

  • Layers 13 are implemented.
  • Active/terminal state mapping is explicit configuration (Section 7.2).
  • Active external tasks create/wake issues without manual import, under operator-enabled policy.
  • Checkout-as-claim is respected (Section 9.2) and external de-activation stops tracker-driven dispatch (Section 9.3).
  • The host exposes an issue-scoped run-stop capability to plugins, and external de-activation uses it when a tracker-driven run is active (Section 9.3).
  • Per-issue execution uses host workspaces (Section 9.4).
  • Workflow policy lookup is implemented for at least one source (Section 9.5).
  • Claim/run/retry/release state is observable per link (Section 12).

10. Outbound Synchronization and Write Policy (Paperclip → External)

10.1 Write Boundary

All provider writes flow through the connector's Outbound Write Engine or connector-provided agent tools. Host core logic MUST NOT write to providers. Agent tools that write externally are connector tools with the same policy checks as engine writes. (This is Symphony's tracker-writes boundary, kept.)

Every outbound write MUST be:

  • Policy-checked against the link's syncMode and fieldPolicy and the write toggles below.
  • Idempotent under the outbound idempotency key (Section 4.3).
  • Attributed (Section 10.4) and audit-logged (Section 14.5).

10.2 Write Policy Toggles and Defaults

Per link (with connector-instance defaults):

Toggle Default Meaning
postBacklink on Paperclip URL on the external task (field or pinned comment).
postProgressComments on Concise milestone comments: claimed, plan ready, PR opened, review requested, done.
postArtifactLinks on PR/work-product/document links when produced.
mirrorAgentComments off Full Paperclip comment thread mirrored externally.
postTranscripts off Run logs/transcripts externally. SHOULD remain off; transcripts may contain sensitive context.
projectStatus on Status projection per Section 10.3.
mirrorExternalComments on (import side) External comments imported per Section 7.3.

Progress comments MUST be concise and milestone-based, not per-heartbeat chatter. Connectors SHOULD batch/debounce outbound comments (RECOMMENDED minimum interval 5 minutes per link except for claim/done/review milestones).

10.3 Status Projection

Paperclip issue status projects to provider states through the same mapping table as Section 7.2, inverted, with these rules:

  • Projection only moves the external task between states the operator mapped; unmapped Paperclip statuses project as comments, not transitions.
  • in_review projects to the mapped review state where one exists — the Symphony handoff pattern — otherwise stays in active with a review-requested comment.
  • done projects to the mapped terminal state only when fieldPolicy.state permits Paperclip-side closure; otherwise it posts a completion comment and leaves the transition to external users.
  • Claim/run state (claimed by which agent, running, retrying, released) SHOULD be projected into a custom field or status comment where the provider allows, so external users see liveness.

10.4 Attribution and Echo Suppression

  • Outbound writes use the connector instance's service account. Message bodies MUST carry agent attribution and the Paperclip issue link.
  • Inbound processing MUST drop events authored by the connector's own service account (isConnectorServiceAccount) and events matching a recently-issued outbound idempotency key, preventing echo loops with providers that obscure authorship.
  • Mirrored content MUST be marked so a second connector instance never re-mirrors it (loop prevention across instances): a stable marker (hidden metadata or footer convention) is REQUIRED on all mirrored comments.

11. Conflict Detection and Resolution

11.1 Detection

Three-way comparison per synced field: baseSnapshot vs current external value vs current Paperclip value.

  • Changed on one side only → propagate per fieldPolicy (or queue conflict if the changed side is not the owner).
  • Changed on both sides since base → conflict entry.
  • Structural conflicts (Section 6.4 deletion, Section 7.4 terminal-while-active, routing failure) are first-class conflict kinds.

11.2 Conflict Entry

Fields: linkId, kind (field, deletion, closure, routing, policy), field (when applicable), baseValue, externalValue, paperclipValue, detectedAt, actors (both sides where known), status (open, resolved, dismissed), resolution.

11.3 Resolution Actions

Exposed in the conflict queue UI and as agent-invocable connector tools:

  • keep_paperclip (push Paperclip value outbound)
  • keep_external (apply external value inbound)
  • merge_manual (operator/agent supplies the merged value)
  • unlink
  • dismiss (acknowledge without change; updates baseSnapshot to current values)

Resolving a conflict MUST update baseSnapshot so the same divergence is not re-detected. Open conflicts MUST NOT block unrelated fields from syncing.

12. Observability and Health

12.1 Logging Conventions

Connector log entries MUST carry: connectorInstanceId, providerKey, linkId (when bound), externalTaskId/externalKey, issueId/issue identifier, event kind, idempotency key, and outcome. Plugin activity logging is the REQUIRED sink; additional sinks are implementation-defined.

12.2 Health Surface (REQUIRED)

Per connector instance, operators MUST be able to see ConnectorHealth (Section 4.1.8) plus:

  • cursor positions and lag estimates,
  • inbound queue depth and oldest pending event age,
  • scheduled retries with next-due times,
  • open conflicts (count + queue link),
  • last 50 sync errors with categories.

Per linked issue (issue-detail integration), users MUST be able to see: external key/title/URL and current external state; sync and run mode; field ownership; the routing rule that selected the assignee; last inbound/outbound/reconcile times; current claim/run/retry/release state; and actions (open external, resync now, pause/resume, unlink, resolve conflict).

On the provider side, a linked task SHOULD show: the Paperclip backlink, current Paperclip owner/status (field or comment), and concise progress per Section 10.2.

13. Failure Model and Recovery

13.1 Failure Classes

  1. Provider outage / transport failure — retry with exponential backoff and jitter; health degraded; links untouched.
  2. Credential expiry — health error, operator notification path REQUIRED; no link mutations.
  3. Webhook loss / delivery gaps — repaired by polling backstop and reconciliation; this is why Section 7.1.3 is REQUIRED.
  4. Cursor loss/corruption — bounded re-list with idempotent re-apply (Section 5.4).
  5. Rate limiting — honor provider guidance; shed reconciliation load before shedding webhook processing; never drop queued inbound events on rate limits.
  6. Partial apply crash — idempotency keys + transactional cursor persistence (Section 4.1.7) make re-apply safe.
  7. Poison event — an event that repeatedly fails application is parked with a health entry after a bounded retry count (RECOMMENDED 5); it MUST NOT block the per-task queue forever.
  8. Provider schema drift — unknown payload shapes degrade to provider_payload errors with the raw payload preserved for diagnosis; known-good fields continue to apply.

13.2 Restart Recovery

Connector restart recovery is cursor- and link-store-driven: re-verify webhook registration, resume cursors, run one reconciliation pass. In-memory queue contents may be lost; reconciliation repairs the gap. No durable orchestrator state beyond the link store and cursors is REQUIRED.

13.3 Operator Intervention Points

Operators MUST be able to: pause/resume a connector instance; pause/resume a single link; force resync of a link; replay a parked event; rotate credentials; and unlink. All interventions are audit-logged.

14. Security and Governance

14.1 Company Boundary

Every connector instance binds to exactly one company. Every inbound effect MUST resolve its target company from the link/connector instance before any mutation, and MUST NOT cross companies. Multi-company installations are separate connector instances with separate credentials and state.

14.2 Credentials and Webhooks

  • Provider tokens/OAuth grants MUST be stored as secret references, never inline configuration or plugin state.
  • Webhook endpoints MUST verify provider signatures where offered; unverifiable webhook payloads are hints requiring API read-back (Section 5.1(5)).
  • Webhook endpoints MUST reject payloads that do not match the connector instance's registered provider workspace.

14.3 Untrusted Content and Commands

  • All external content (titles, descriptions, comments, field values, workflow files) is untrusted input with provenance, per Sections 7.3 and 9.5.1.
  • Comment commands are parsed deterministically (Section 8.2); free-text external content MUST NOT trigger privileged actions.
  • Workflow policy is lowest-precedence guidance (Section 9.5) — it cannot grant capabilities, alter governance, or override system instructions.

14.4 Governance Invariants

No connector path may bypass:

  • issue checkout/claim exclusivity,
  • budget hard-stops and pause/cancel,
  • approval gates and execution-policy stages,
  • blocker semantics,
  • company boundaries.

External signals translate into host-level requests subject to all of the above (Section 7.4). Destructive cascade is prohibited: external deletion/archival never deletes Paperclip work (Section 6.4).

14.5 Audit

Every mutation on either side carries attribution: connector instance, link, triggering event idempotency key, and acting identity (external actor inbound; agent + connector service account outbound). Audit entries are queryable per link.

15. Reference Flows (Language-Agnostic)

15.1 Import One External Task

on import_request(externalTaskId):
  task = adapter.getTask(externalTaskId)
  if dedupe_hit(task): surface_existing_link(); stop
  company, project, goal = resolve_target(connector_instance)
  assignee = routing.resolve(task)            # Section 8; triage fallback guaranteed
  packet  = render_projection(task)           # normalized fields only
  issue   = host.create_issue(company, project, goal, assignee, packet,
                              source_attribution(task))
  link    = links.create(task, issue, originSide=external,
                         baseSnapshot=snapshot(task, issue))
  if policy.postBacklink: adapter.setBacklink(task.externalId, issue.url)
  health.record(inbound_ok)

15.2 Inbound Comment

on provider_comment_event(evt):
  evt = verify_or_readback(evt)
  if evt.actor.isConnectorServiceAccount or echo_match(evt): drop
  if not idempotency.first_time(evt): drop
  link = links.by_external(evt.externalTaskId) or stop
  if command = parse_strict_command(evt.body):    # Section 8.2
      execute_and_reply(command); record; stop
  host.post_issue_comment(link.issueId, attributed(evt))   # provenance, untrusted
  if link.policy.wakeOnExternalComment: host.wake_assignee(link.issueId)

15.3 Tracker-Driven Execution Cycle (Symphony Profile)

on external_state_change(task) where link.runMode == symphony_compatible:
  category = state_mapping(task.state)
  issue = host.issue(link.issueId)
  if category == active and issue.status in {done, cancelled}:
      if reopen_policy == follow_up:
          issue = host.create_or_get_follow_up(
              issue, inherit_workspace=true, idempotency_key=stable_key(task))
          link = links.retarget(link, issue)  # one entity upsert: new issueId + handoffHistory
      else:
          issue = host.request_resume(issue)                    # normal host resume rules
  if category == active and issue.status == backlog:
      issue = host.set_status(issue.id, todo)                   # backlog is not wakeable
  if category == active and issue_dispatchable(issue):
      host.wake_assignee(issue.id)                              # debounced
  if category in {backlog, blocked, terminal} and tracker_driven_run_active(link):
      if host.capabilities.issueScopedRunStop:
          host.request_issue_run_stop(link.issueId)
      else:
          health.record(issue_run_stop_unsupported)             # let this run finish
      if category == terminal and issue_active(link.issueId):
          conflicts.open(link, kind=closure)      # Section 7.4

on issue_status_change(issue) where linked(issue):
  outbound.project_status(link, issue.status)     # Section 10.3, policy-gated
  outbound.post_milestone_comment_if_due(link, issue)

15.4 Reconciliation Tick

every reconcile_interval per connector_instance:
  for link in links.window(cursor):               # rate-bounded slice
    state = adapter.getTaskStates([link.externalTaskId])
    if missing(state): conflicts.open(link, kind=deletion); continue
    diff = three_way(link.baseSnapshot, state, host.issue(link.issueId))
    apply_owned_changes(diff)                      # Section 11.1
    queue_conflicts(diff)
    link.lastReconcileAt = now
  health.record(reconcile_ok, cursor)

16. Conformance Checklist (Definition of Done)

16.1 REQUIRED for Core Conformance (Layers 12)

  • Task Source Adapter with listCandidateTasks, getTask, getTaskStates, listComments, webhook verification or documented poll-only posture
  • Static capability declaration consumed by host UX
  • Normalized ExternalTask output with provider envelope separation
  • Task link store with the Section 4.1.3 invariants and Section 6.1 state machine
  • Idempotent inbound apply (event idempotency keys + transactional cursors)
  • Deduplicated link creation (Section 6.3)
  • External deletion handled non-destructively (Section 6.4)
  • Explicit state-category mapping with unmapped-state safety (Section 7.2)
  • Comment import with provenance and untrusted-content handling (Section 7.3)
  • Routing pipeline with guaranteed triage fallback (Section 8)
  • Reconciliation job (Section 7.1.3)
  • Health and link surfaces (Sections 12.2, 12.3)
  • Secret-ref credentials, company-boundary enforcement, audit attribution (Section 14)

16.2 REQUIRED for Symphony Profile Conformance (Layer 3)

  • runMode=symphony_compatible honoring Section 9.1 dispatch (wake, never spawn)
  • Checkout-as-claim with no parallel claim registry (Section 9.2)
  • External de-activation stops tracker-driven dispatch and uses an issue-scoped host run-stop capability for an active issue run (Section 9.3)
  • Debounced wake requests (Section 9.3)
  • Task packet rendering with strict variables and untrusted marking (Section 9.5.1)
  • Workflow policy lookup from at least one source with lowest-precedence guarantee (Section 9.5)
  • Claim/run/retry/release state visible per link (Section 12.3)

16.3 REQUIRED for Write Conformance (Layer 4)

  • Outbound writes only via Outbound Write Engine / connector tools (Section 10.1)
  • Policy toggles with Section 10.2 defaults, including transcripts off
  • Idempotent outbound effects
  • Status projection rules including review-handoff and gated closure (Section 10.3)
  • Service-account attribution, echo suppression, and cross-instance loop markers (Section 10.4)
  • Three-way conflict detection with resolution actions and base-snapshot advance (Section 11)
  • External-side claim/run state projection into provider custom fields
  • user_mapping routing and "assign back to me" flows
  • Lossless rich-text round-trip per provider
  • Comment-command verb extensions (e.g. /paperclip plan, /paperclip qa)
  • Promotion of task links from plugin state to core schema once two providers validate the model

Appendix A. Provider Profiles

Each profile lists container coordinates, envelope content, state-mapping notes, and known write constraints. Profiles are informative; the normative contract is Sections 414.

A.1 Linear

  • Containers: team, project, cycle. Envelope: issue key, workflow state id/type, labels with ids, branch name, relations, estimates, cycle/project ids.
  • State mapping: Linear workflow state type (backlog, unstarted, started, completed, canceled) gives a reliable default category mapping; per-team overrides supported.
  • Webhooks: signed; verification REQUIRED. API: GraphQL; pagination REQUIRED; keep queries isolated (schema drift).
  • Notes: closest provider to the Symphony reference; RECOMMENDED first connector.

A.2 Jira

  • Containers: site/cloud id, project key. Envelope: issue type, workflow transition ids, status category, components, epic/parent links, sprints, custom fields by id.
  • State mapping: map via Jira status categories (To Do, In Progress, Done) plus named-status overrides; transitions require transition ids, so updateTaskState resolves transitions at apply time.
  • Webhooks: available (signed via secret on Cloud); polling backstop RECOMMENDED.
  • Notes: per-project workflow variance is the main mapping risk; setup MUST validate against live workflow metadata.

A.3 Asana

  • Containers: workspace, project, section. Envelope: section memberships, custom fields, followers, multi-project memberships.
  • State mapping: Asana has completion plus sections; map sections→categories per project; completion maps to terminal.
  • Webhooks: handshake + HMAC signatures; verification REQUIRED.
  • Notes: multi-project membership means one task can match several containers — the link binds to the connector instance's configured container.

A.4 Notion

  • Containers: database id. Envelope: property schema snapshot, select/status property values, relation properties, block-content references.
  • State mapping: a designated status/select property maps to categories; databases without one are import/browse only (no Symphony profile).
  • Webhooks: limited; polling with last_edited_time high-water mark is the primary channel.
  • Notes: rich-text conversion is lossy; envelope SHOULD retain block references for backlinks.

A.5 Trello

  • Containers: board, list. Envelope: list id, checklists, members, power-up metadata, card position.
  • State mapping: lists are lanes; map lists→categories (Paperclip: QA style lanes also drive state_lane routing).
  • Webhooks: available with callback verification; signatures via content digest.
  • Notes: no native blockers/priority; both normalize to null/empty with envelope passthrough.

A.6 GitHub Issues

  • Containers: repository (and optionally Projects v2). Envelope: labels, milestones, assignees, linked PR references, reactions.
  • State mapping: open/closed plus label conventions or Projects v2 status field for richer categories.
  • Webhooks: HMAC-signed; verification REQUIRED.
  • Notes: PR linkage is first-class here; postArtifactLinks SHOULD use native cross-references.

Appendix B. Symphony Spec Crosswalk

Symphony SPEC.md section This document
1 Problem Statement 1
2 Goals / Non-Goals 2
3 System Overview 3
4 Core Domain Model (Issue, Run Attempt, Retry Entry, Runtime State) 4 (ExternalTask, TaskLink, host run state via 9.3)
5 Workflow Specification (WORKFLOW.md) 9.5 (layered workflow policy)
6 Configuration Specification connector instance configuration (3.4, 5.2, 7.2, 10.2)
7 Orchestration State Machine 6.1 (link), 9.3 (claim/run/retry/release on host primitives)
8 Polling, Scheduling, Reconciliation 7.1, 9.1, 15.4
9 Workspace Management and Safety 9.4 (host execution workspaces)
10 Agent Runner Protocol host heartbeat runtime (out of scope here; see 9.1 boundary)
11 Issue Tracker Integration Contract 5 (Task Source Adapter contract)
11.5 Tracker Writes boundary 10.1
12 Prompt Construction 9.5.1 (task packet rendering)
13 Logging, Status, Observability 12
14 Failure Model and Recovery 13
15 Security and Operational Safety 14
16 Reference Algorithms 15
1718 Test Matrix / Implementation Checklist 16
Appendix A SSH Worker not applicable (host owns execution substrate)

Differences are deliberate: Symphony assumes one tracker, one repo, and coding agents only, with the orchestrator as a standalone daemon. This protocol assumes a multi-tenant control plane that already owns claims, runs, workspaces, and governance — so the connector's scope shrinks to normalization, linking, routing, sync policy, and observability, and Symphony's orchestrator/runner sections collapse onto existing host primitives.