1198 lines
57 KiB
Markdown
1198 lines
57 KiB
Markdown
# 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 1–3 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 0–4, 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.
|
||
|
||
#### 4.1.3 TaskLink
|
||
|
||
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.
|
||
|
||
#### 4.2.3 Link `status`
|
||
|
||
- `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.
|
||
|
||
## 6. Task Link Lifecycle
|
||
|
||
### 6.1 Link State Machine
|
||
|
||
```
|
||
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.
|
||
|
||
### 6.2 Link Creation Paths
|
||
|
||
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.
|
||
|
||
### 6.4 Unlink and External Deletion Semantics
|
||
|
||
- 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.4–9.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 1–3 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.
|
||
|
||
### 12.3 Link Surface (REQUIRED)
|
||
|
||
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).
|
||
|
||
### 12.4 External-Side Surface (RECOMMENDED)
|
||
|
||
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 1–2)
|
||
|
||
- 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)
|
||
|
||
### 16.4 RECOMMENDED Extensions (Not REQUIRED for Conformance)
|
||
|
||
- 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 4–14.
|
||
|
||
### 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 |
|
||
| 17–18 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.
|