From 87f36bfd085f46ceb50ca88878a922c8caaa7e3e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:08:13 -0700 Subject: [PATCH 01/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20PM-too?= =?UTF-8?q?l=20projection=20contract=20(structured=20work=20items)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes the Telegram bridge's proven shape to two-way projection of tasks/status/assignees between the Commonly board and external PM tools. Provider is a plugin behind a four-verb driver; identity maps explicit; claims never project. Sibling of ADR-025 (messages). Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/adr/ADR-027-pm-tool-projection-contract.md diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md new file mode 100644 index 000000000..9353bc7e9 --- /dev/null +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -0,0 +1,116 @@ +# ADR-027: PM-tool projection contract — structured work items across tool boundaries + +**Status:** Proposed (2026-08-31, Wren; commissioned by Sam). Acknowledged +unknowns: the first provider (decided by next week's interview answers, not +by this ADR), per-provider sync transport (webhook vs poll), and custom-status +fidelity limits. Ratifying this ADR does not settle those. + +**Scope boundary:** this ADR governs how STRUCTURED work items (tasks, their +status, assignees, provenance) project two-way between a Commonly pod and an +external PM surface. It is the sibling of ADR-025, which governs the same +boundary for MESSAGES (channel routing); a reader designing a channel bridge +wants ADR-025, a reader syncing a board wants this one. It does not govern +the attention gate (ADR-017/018), and it uses — not changes — the Installable +taxonomy (ADR-001) and the task board (`/api/v1/tasks`). + +## Context + +Teams do not arrive tool-less. The adoption wedge is the opposite of rip-and- +replace: agents plug into the PM surface a team already runs, and Commonly is +the ledger above the tools — the place where agent and human work is one +board, whatever surface each participant happens to look at. Evidence this +is the wedge and not a hunch: a biomed team's first question was whether +agents could work their existing PM software "same or better" (operator +outreach note, 2026-08-31); the Dock "Company Brain" cadence makes the same +demand from the ops side; and ACP convergence (Lody/Bloome) says the +ecosystem is standardizing agent↔tool seams now. + +The Telegram bridge (#1282–#1290, ADR-025) already proved the shape on the +message side, and its two hardest lessons transfer whole: + +- **Attribution is the security boundary.** Relaying a message as the wrong + identity is impersonation (#1289); the same applies to a task edit. +- **The mapping table IS the router.** relayMap on the integration, not + heuristics at read time (ADR-025 D3). + +**Portability is the other half of adoption safety.** A team that brings its +board wants to know it can leave, and that its agents are not hostage to our +runtime: the same agent identity and memory runs BYO on a laptop or hosted +(ADR-023), and one-command migration between the two is a stated goal of this +track. A projection that can be turned off without data loss and an agent +that can walk between runtimes are the same promise at two layers: adopting +Commonly is never a one-way door. + +## Decision + +**D1 — Projection is a pod-scoped kernel object; the provider is a plugin.** +`Projection { podId, provider, externalRef, fieldMap, identityMap, status }`. +`provider` selects a driver behind one interface (D8); adding Notion, Linear, +or Paperclip is one adapter file (design rule 6), never a schema change. The +first provider is a configuration of this contract, not its architecture. + +**D2 — Transport is kernel; judgment is agent (inherits ADR-025 D2).** Sync +is deterministic kernel code: field mapping, echo suppression, provenance +stamping. Agents act ON the board (claim, complete, comment) and their acts +project like anyone else's; they never carry the sync bytes, so a hung seat +never stalls the projection. + +**D3 — The projection map is the ledger.** Per item: `{ taskId, externalId, +lastSyncedAt, lastSyncedHash, origin }` on the Projection row. Every sync +decision reads this map; nothing is matched by title or heuristics. Echo +suppression = skip when the inbound hash equals lastSyncedHash (the +relayMap lesson, applied to rows instead of messages). + +**D4 — Identity maps are explicit; unmapped actors annotate, never author.** +`identityMap: externalUserId ↔ { commonlyUserId | agentUserId }`, curated by +the projection's installer. An edit by an unmapped external actor lands as a +provenance annotation ("changed in Linear by J. Ortiz") on a system-attributed +change — it is NEVER written as a mapped user, and there is no fuzzy match +by display name or email (the #1289 rule for rows). Agent identities project +outward as real assignees where the tool supports them, else as a tagged +marker in the item body. + +**D5 — One canonical item schema; lossy maps are declared, not discovered.** +Canonical: `{ title, description, status, assignee, labels, links }` with +status ∈ Task's own enum. Each driver declares its status map both +directions; a state with no mapping parks the item with an explicit marker +and syncs the rest — silent drops and silent coercions are both defects. +Round-trip invariant: project out then in with no external edit = no change. + +**D6 — Conflicts resolve per field, newest wins, provenance kept.** Compare +at field granularity using updatedAt on each side; the loser's value is +recorded in the item's provenance trail, not discarded. No merge dialogs in +v1; the trail is the appeal. + +**D7 — Claims do not project; assignees do.** ADR-018 claims are attention +leases, not assignments. Outward we project `assignee` only; an external +assignee change maps to Commonly `assignee` and never creates or breaks a +claim. Tools with no agent-assignee concept get D4's marker. + +**D8 — The driver interface is four verbs**, mirroring CAP's shape: +`pull(since)`, `push(changes)`, `mapIdentity(actor)`, `verify()` (health + +scope check). Everything provider-specific lives behind these; the kernel +sync loop is provider-blind. Webhook vs poll is a driver property declared +by `verify()`, not a kernel branch. + +## Consequences + +- The interview answers pick provider #1 by filling in one driver — the + contract, map storage, sync loop, and provenance UI are shared. +- The task board becomes the canonical store for projected items; external + boards are views. Teams who leave keep everything (portability promise). +- The Connectors surface grows a "Boards" section beside channels — same + card anatomy, same gate model (per-pod projection instead of per-pod + relay); design follows the connectors-v2 spec patterns. +- Provenance UI: every synced item shows its origin chain; this is new + shell work and gates GA, not the first driver. + +## Alternatives rejected + +- **Per-tool bespoke integrations** — three tools deep you have three + schemas and no ledger; the wedge inverts into maintenance. +- **Agent-as-transport** (an agent that "watches Notion") — ADR-025 D2's + rejection, same reasons, plus rate limits bind to a seat's cadence. +- **One-time import/migration** — answers the demo, not the wedge; teams + live in both tools during the entire adoption window, which is exactly + when sync must be trustworthy. From e7b325c002cac5bd7c6934641313f1afb7d3240c Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:09:19 -0700 Subject: [PATCH 02/18] =?UTF-8?q?docs(adr):=20ADR-027=20D3=20=E2=80=94=20p?= =?UTF-8?q?rovenance=20is=20the=20loop=20breaker,=20not=20the=20hash=20(Ve?= =?UTF-8?q?ra's=20mutable-items=20criterion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 9353bc7e9..6597ef8c8 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -55,11 +55,20 @@ stamping. Agents act ON the board (claim, complete, comment) and their acts project like anyone else's; they never carry the sync bytes, so a hung seat never stalls the projection. -**D3 — The projection map is the ledger.** Per item: `{ taskId, externalId, -lastSyncedAt, lastSyncedHash, origin }` on the Projection row. Every sync -decision reads this map; nothing is matched by title or heuristics. Echo -suppression = skip when the inbound hash equals lastSyncedHash (the -relayMap lesson, applied to rows instead of messages). +**D3 — The projection map is the ledger, and provenance is the loop +breaker.** Per item: `{ taskId, externalId, lastSyncedAt, lastSyncedHash, +origin }` on the Projection row; nothing is matched by title or heuristics. +Messages are append-only but work items are MUTABLE — a duplicated message +is noise, a looped status write is silent corruption — so echo suppression +cannot rest on content hashes alone (a provider that reformats on write +changes the hash and the loop survives). Two layers, and the second is the +invariant: (a) skip when the inbound hash equals lastSyncedHash; (b) every +outbound write carries the projection's own provenance identity, and **an +inbound edit whose actor is that identity is dropped, unconditionally** — +stated as an invariant with a mutation test (delete the drop and the +round-trip test must catch the loop). Drivers must expose the acting +identity on inbound events for exactly this check; a provider that cannot +is not integrable under this contract. **D4 — Identity maps are explicit; unmapped actors annotate, never author.** `identityMap: externalUserId ↔ { commonlyUserId | agentUserId }`, curated by From c34b011b5cf57ff16807b3391b6f0ef77eee8c41 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:10:15 -0700 Subject: [PATCH 03/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20named?= =?UTF-8?q?=20field-class=20conflict=20rules=20(D6)=20+=20declared-scope?= =?UTF-8?q?=20enforcement=20(new=20D8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 6597ef8c8..a553e58b4 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -86,21 +86,40 @@ directions; a state with no mapping parks the item with an explicit marker and syncs the rest — silent drops and silent coercions are both defects. Round-trip invariant: project out then in with no external edit = no change. -**D6 — Conflicts resolve per field, newest wins, provenance kept.** Compare -at field granularity using updatedAt on each side; the loser's value is -recorded in the item's provenance trail, not discarded. No merge dialogs in -v1; the trail is the appeal. +**D6 — Conflicts resolve per FIELD CLASS, and the classes are named here** +so the first adapter author does not decide them by accident (Vera 61303). +Three classes, three rules: +- *Content* (title, description, labels): newest-wins per field, loser's + value kept in the provenance trail. No merge dialogs in v1; the trail is + the appeal. +- *Coordination* (status, assignee): newest-wins, BUT a write that would + regress a terminal state (done → in-progress) requires the inbound side's + actor to be mapped (D4) — an anonymous regression parks with a marker + instead of applying. +- *Existence* (create/delete/archive): creation propagates; deletion NEVER + propagates automatically in either direction — the counterpart archives + with a provenance marker. A sync that can delete someone's work item on + the other side of a mapping bug is unrecoverable; archive is. **D7 — Claims do not project; assignees do.** ADR-018 claims are attention leases, not assignments. Outward we project `assignee` only; an external assignee change maps to Commonly `assignee` and never creates or breaks a claim. Tools with no agent-assignee concept get D4's marker. -**D8 — The driver interface is four verbs**, mirroring CAP's shape: +**D8 — Declared scope, enforced by the kernel, not inherited from the +token.** Provider tokens are routinely over-broad (a Notion integration +token grants the workspace). The Projection declares its exact external +scope (`externalRef`: one database / one project / one board) at install, +the kernel refuses to read or write outside it regardless of what the token +allows, and `verify()` reports the token's actual grant so the Connectors +surface can show "token exceeds declared scope" as a warning state. Scope +widening is a new install decision, never a drift. + +**D9 — The driver interface is four verbs**, mirroring CAP's shape: `pull(since)`, `push(changes)`, `mapIdentity(actor)`, `verify()` (health + -scope check). Everything provider-specific lives behind these; the kernel -sync loop is provider-blind. Webhook vs poll is a driver property declared -by `verify()`, not a kernel branch. +scope-grant report, per D8). Everything provider-specific lives behind +these; the kernel sync loop is provider-blind. Webhook vs poll is a driver +property declared by `verify()`, not a kernel branch. ## Consequences From 57d1862ca68e16851fde58c25e3326849c8fb8fc Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 20:11:05 -0700 Subject: [PATCH 04/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20portab?= =?UTF-8?q?ility=20claim=20carries=20its=20equality=20test=20(Vera=2061304?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index a553e58b4..ef67e988a 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -36,10 +36,18 @@ message side, and its two hardest lessons transfer whole: **Portability is the other half of adoption safety.** A team that brings its board wants to know it can leave, and that its agents are not hostage to our runtime: the same agent identity and memory runs BYO on a laptop or hosted -(ADR-023), and one-command migration between the two is a stated goal of this -track. A projection that can be turned off without data loss and an agent -that can walk between runtimes are the same promise at two layers: adopting -Commonly is never a one-way door. +(ADR-023), and one-command migration between the two is a stated goal of +this track — stated with its test, since teams will hold us to it (Vera +61304). ADR-026 supplies the machinery: identity and memory survive by +rule 8, and moving a seat is release + rebind (ADR-026 D3) plus a +credential mint. The equality claim that makes "migration" true: after the +move, the agent's User row id, memory head revision, pod memberships, and +display identity are IDENTICAL — the only rows that changed are the +machine binding and the runtime credential. The acceptance test snapshots +those four before, migrates, and diffs; anything else changing fails it. +A projection that can be turned off without data loss and an agent that can +walk between runtimes are the same promise at two layers: adopting Commonly +is never a one-way door. ## Decision From 48921f3dc8d6ed468f6654af928c989bad255cfd Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:43:13 -0700 Subject: [PATCH 05/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20Notion?= =?UTF-8?q?-first=20working=20assumption=20+=20ACP=20binding=20statement?= =?UTF-8?q?=20(Sam=2061475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index ef67e988a..4e161772f 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -1,9 +1,13 @@ # ADR-027: PM-tool projection contract — structured work items across tool boundaries **Status:** Proposed (2026-08-31, Wren; commissioned by Sam). Acknowledged -unknowns: the first provider (decided by next week's interview answers, not -by this ADR), per-provider sync transport (webhook vs poll), and custom-status -fidelity limits. Ratifying this ADR does not settle those. +unknowns: per-provider sync transport (webhook vs poll) and custom-status +fidelity limits — ratifying this ADR does not settle those. The first +provider is a WORKING ASSUMPTION, not a decision of this ADR: **Notion +first, Linear second** (Sam, 2026-08-31, ship-and-measure; interviews +dropped as the gate). The contract stays provider-agnostic by construction +(D1/D9), so a veto of that assumption costs one plugin, never this +architecture. **Scope boundary:** this ADR governs how STRUCTURED work items (tasks, their status, assignees, provenance) project two-way between a Commonly pod and an @@ -129,6 +133,18 @@ scope-grant report, per D8). Everything provider-specific lives behind these; the kernel sync loop is provider-blind. Webhook vs poll is a driver property declared by `verify()`, not a kernel branch. +**D9 is transport-agnostic, and ACP is a supported binding.** The ecosystem +is converging on ACP for agent↔tool session transport (operator strategy +note, 2026-08-30; multiple independent adoptions). The four verbs carry no +provider SDK assumptions: a driver may be implemented over an ACP +connection exactly as over a REST client — we already run an ACP-family +adapter in production (the acpx path, ADR-005 lineage), so this is a +compatibility statement, not an aspiration. The strategic read is stated +here so the ADR carries it: ACP commoditizes the transport; what it does +NOT carry — identity, memory, membership, and this contract's provenance +ledger — is the half Commonly holds. This ADR is deliberately a +specification of that half. + ## Consequences - The interview answers pick provider #1 by filling in one driver — the From a39f322e716ba0686056e97c885c029952f2c043 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:44:48 -0700 Subject: [PATCH 06/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20resolv?= =?UTF-8?q?able-actor=20D3=20with=20outbound-only=20fallback;=20D6=20lastS?= =?UTF-8?q?yncedAt=20algorithm;=20Notion=20attribution=20unknown=20(Vera?= =?UTF-8?q?=2061310/61313/61315/61322/61478)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 4e161772f..12fc6c9d9 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -2,7 +2,12 @@ **Status:** Proposed (2026-08-31, Wren; commissioned by Sam). Acknowledged unknowns: per-provider sync transport (webhook vs poll) and custom-status -fidelity limits — ratifying this ADR does not settle those. The first +fidelity limits, and **Notion's editor-attribution granularity** — D3/D6 +need a resolvable actor per change, Notion may expose it only page-level; +this must be confirmed against the live API before the adapter is built, +and if page-level is the truth, D6's anonymous-regression parking is +per-page on provider #1 and the adapter doc must say so. Ratifying this +ADR does not settle any of these. The first provider is a WORKING ASSUMPTION, not a decision of this ADR: **Notion first, Linear second** (Sam, 2026-08-31, ship-and-measure; interviews dropped as the gate). The contract stays provider-agnostic by construction @@ -78,9 +83,13 @@ invariant: (a) skip when the inbound hash equals lastSyncedHash; (b) every outbound write carries the projection's own provenance identity, and **an inbound edit whose actor is that identity is dropped, unconditionally** — stated as an invariant with a mutation test (delete the drop and the -round-trip test must catch the loop). Drivers must expose the acting -identity on inbound events for exactly this check; a provider that cannot -is not integrable under this contract. +round-trip test must catch the loop). Drivers must make the acting identity RESOLVABLE for this check — on the +event itself or via one follow-up read (Notion exposes last_edited_by on a +read, not the webhook; that satisfies the invariant). A provider where the +actor cannot be resolved at all falls back to **outbound-only projection** +— no inbound edits means no loop to break — rather than being excluded; +two-way sync is gated on resolvability, the contract is not (Vera +61313/61315). **D4 — Identity maps are explicit; unmapped actors annotate, never author.** `identityMap: externalUserId ↔ { commonlyUserId | agentUserId }`, curated by @@ -100,7 +109,17 @@ Round-trip invariant: project out then in with no external edit = no change. **D6 — Conflicts resolve per FIELD CLASS, and the classes are named here** so the first adapter author does not decide them by accident (Vera 61303). -Three classes, three rules: +"Newest wins" is the user-facing phrasing, NOT the algorithm — two systems +with unsynced clocks cannot be compared by timestamp, or a few seconds of +skew silently makes one side always win (Vera 61310/61322). The algorithm +decides WHO changed against `lastSyncedAt`: if only one side changed since +the last sync, that side wins with no cross-clock comparison — which is +nearly every case. Both changed = a real conflict, resolved by the class +rule below with a tiebreak that is not a timestamp: **the Commonly value +stands** (the board is the ledger; external boards are views — see +Consequences) **and the external value is parked in the provenance trail +with a conflict marker**, surfaced on the item. Three classes, three rules +for that both-changed case: - *Content* (title, description, labels): newest-wins per field, loser's value kept in the provenance trail. No merge dialogs in v1; the trail is the appeal. From 5356976cd66f5a3ee3623e6ce6f99837bb5ce6ac Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:30:35 -0700 Subject: [PATCH 07/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20staged?= =?UTF-8?q?:=20v1=20outbound-only,=20two-way=20documented=20as=20phase=202?= =?UTF-8?q?=20(GTM=20convergence)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 12fc6c9d9..db3a2d95f 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -164,6 +164,29 @@ NOT carry — identity, memory, membership, and this contract's provenance ledger — is the half Commonly holds. This ADR is deliberately a specification of that half. +## Staging (added at ratification, GTM convergence 2026-08-31) + +Five independent seats converged on the same must-not-build for v1: two-way +sync. This ADR adopts that as its staging, not as a scope cut: + +- **Phase 1 — outbound-only.** The ledger projects OUT: tasks, status, + assignees, provenance markers appear in the external tool; nothing is read + back except `verify()`. One command, zero settings. This is D3's + unresolvable-actor fallback promoted to the default: no inbound edits, no + loop to break, no conflict resolution, no inbound identity mapping, and + the Notion attribution unknown does not gate shipping. +- **Phase 2 — two-way.** Everything D3–D7 specifies for inbound (resolvable + actor, field-class conflict rules, lastSyncedAt algorithm, parking) is + DOCUMENTED NOW and built only when outbound-only measurably fails a real + team — the trigger is a team telling us the external board is where they + edit, with the specific edit that got lost. The contract is written so + phase 2 adds a capability to the same Projection row; it does not migrate + it. + +Phase 1 makes the wedge sentence honest: "your board, visible where your +team already looks" — reconciliation in someone else's product is a phase-2 +promise we make only when asked to keep it. + ## Consequences - The interview answers pick provider #1 by filling in one driver — the From e80bc0df0907d08b4a520bc16c928023c1146203 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:31:32 -0700 Subject: [PATCH 08/18] =?UTF-8?q?docs(adr):=20ADR-027=20D6=20=E2=80=94=20r?= =?UTF-8?q?ewrite=20coherently=20around=20the=20lastSyncedAt=20algorithm;?= =?UTF-8?q?=20no=20residual=20newest-wins=20(Vera=2061536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../ADR-027-pm-tool-projection-contract.md | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index db3a2d95f..a393cd981 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -107,30 +107,27 @@ directions; a state with no mapping parks the item with an explicit marker and syncs the rest — silent drops and silent coercions are both defects. Round-trip invariant: project out then in with no external edit = no change. -**D6 — Conflicts resolve per FIELD CLASS, and the classes are named here** -so the first adapter author does not decide them by accident (Vera 61303). -"Newest wins" is the user-facing phrasing, NOT the algorithm — two systems -with unsynced clocks cannot be compared by timestamp, or a few seconds of -skew silently makes one side always win (Vera 61310/61322). The algorithm -decides WHO changed against `lastSyncedAt`: if only one side changed since -the last sync, that side wins with no cross-clock comparison — which is -nearly every case. Both changed = a real conflict, resolved by the class -rule below with a tiebreak that is not a timestamp: **the Commonly value -stands** (the board is the ledger; external boards are views — see -Consequences) **and the external value is parked in the provenance trail -with a conflict marker**, surfaced on the item. Three classes, three rules -for that both-changed case: -- *Content* (title, description, labels): newest-wins per field, loser's - value kept in the provenance trail. No merge dialogs in v1; the trail is - the appeal. -- *Coordination* (status, assignee): newest-wins, BUT a write that would - regress a terminal state (done → in-progress) requires the inbound side's - actor to be mapped (D4) — an anonymous regression parks with a marker - instead of applying. -- *Existence* (create/delete/archive): creation propagates; deletion NEVER - propagates automatically in either direction — the counterpart archives - with a provenance marker. A sync that can delete someone's work item on - the other side of a mapping bug is unrecoverable; archive is. +**D6 — Conflict resolution (phase 2; designed now, built on the phase-2 +trigger).** "Newest wins" is user-facing phrasing, NOT the algorithm — two +systems with unsynced clocks are never compared by timestamp (a few seconds +of skew silently makes one side always win). The algorithm decides WHO +changed against `lastSyncedAt`: + +- **One side changed since the last sync → that side wins**, no timestamp + comparison. This is nearly every write. Two exceptions when the changed + side is external: a terminal-state regression (done → in-progress) whose + actor is unmapped (D4) parks with a marker instead of applying; and + deletion NEVER propagates (below). +- **Both sides changed since the last sync → a real conflict**, and the + tiebreak is not a timestamp: **the Commonly value stands** for every + field class (the board is the ledger; external boards are views — see + Consequences), and the external value is parked in the item's provenance + trail with a surfaced conflict marker. The trail is the appeal; no merge + dialogs. +- **Existence is not a field**: creation propagates both ways; deletion + auto-propagates in NEITHER direction — the counterpart archives with a + provenance marker. A delete across a mapping bug is unrecoverable; an + archive is not. **D7 — Claims do not project; assignees do.** ADR-018 claims are attention leases, not assignments. Outward we project `assignee` only; an external From a660253bc2a68a2e1b706e8528d37d2ce9b3c9e0 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:32:39 -0700 Subject: [PATCH 09/18] =?UTF-8?q?docs(adr):=20ADR-027=20staging=20?= =?UTF-8?q?=E2=80=94=20name=20what=20phase=201=20KEEPS:=20declared=20scope?= =?UTF-8?q?,=20assignee=20attribution,=20never-delete-outward=20(Vera=2061?= =?UTF-8?q?540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index a393cd981..8bbb2ea81 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -172,6 +172,15 @@ sync. This ADR adopts that as its staging, not as a scope cut: unresolvable-actor fallback promoted to the default: no inbound edits, no loop to break, no conflict resolution, no inbound identity mapping, and the Notion attribution unknown does not gate shipping. + **Outbound-only is not rule-free — three decisions bind in phase 1:** + D8's declared scope (the kernel writes only inside the declared + database/project, whatever the token grants); D4's agent-assignee + projection (real assignee where the tool supports it, tagged marker where + not — attribution rules do not wait for two-way); and the existence rule's + outward half: **a task deleted or archived in Commonly never deletes the + external row — it archives it with a marker.** Deletion is the only + irreversible operation this contract touches, and it is exactly the rule + a reader would misfile under phase-2 machinery; it is phase 1. - **Phase 2 — two-way.** Everything D3–D7 specifies for inbound (resolvable actor, field-class conflict rules, lastSyncedAt algorithm, parking) is DOCUMENTED NOW and built only when outbound-only measurably fails a real From f9aae9f348c64c0405bf3e37c2cbd63fca561b13 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:32:32 -0700 Subject: [PATCH 10/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20attent?= =?UTF-8?q?ion-routing=20doctrine=20+=20link-back=20acceptance=20rule=20(S?= =?UTF-8?q?am=2061552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 8bbb2ea81..fcd8415cd 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -27,7 +27,11 @@ taxonomy (ADR-001) and the task board (`/api/v1/tasks`). Teams do not arrive tool-less. The adoption wedge is the opposite of rip-and- replace: agents plug into the PM surface a team already runs, and Commonly is the ledger above the tools — the place where agent and human work is one -board, whatever surface each participant happens to look at. Evidence this +board, whatever surface each participant happens to look at. The ratified +doctrine (operator strategy note, 2026-08-31): **we route attention, we do +not compete for it.** External tools remain the human attention layer; the +kernel owns what deserves attention (ADR-018) and the ledger owns the +decision moment. Evidence this is the wedge and not a hunch: a biomed team's first question was whether agents could work their existing PM software "same or better" (operator outreach note, 2026-08-31); the Dock "Company Brain" cadence makes the same @@ -181,6 +185,13 @@ sync. This ADR adopts that as its staging, not as a scope cut: external row — it archives it with a marker.** Deletion is the only irreversible operation this contract touches, and it is exactly the rule a reader would misfile under phase-2 machinery; it is phase 1. + **Acceptance rule (doctrine carve-out, ratified 2026-08-31): every + projected item carries a link back to its ledger view in Commonly.** The + decision moment — gate approvals, claim conflicts, the digest — is the + one surface the ledger keeps, because it needs agent identity, claim + state, and the decision trail the external tool cannot render. Outbound- + only makes this trivially satisfiable: project out, link back; a + projected item with no backlink is a failing acceptance test. - **Phase 2 — two-way.** Everything D3–D7 specifies for inbound (resolvable actor, field-class conflict rules, lastSyncedAt algorithm, parking) is DOCUMENTED NOW and built only when outbound-only measurably fails a real From 925c2c47150523b0ad733bc5c94eeaaf4a561711 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:33:58 -0700 Subject: [PATCH 11/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20backli?= =?UTF-8?q?nk=20is=20authenticated-route-only;=20pod=20names=20outward=20a?= =?UTF-8?q?re=20opt-in=20(Vera=2061560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index fcd8415cd..e6739b53e 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -191,7 +191,14 @@ sync. This ADR adopts that as its staging, not as a scope cut: one surface the ledger keeps, because it needs agent identity, claim state, and the decision trail the external tool cannot render. Outbound- only makes this trivially satisfiable: project out, link back; a - projected item with no backlink is a failing acceptance test. + projected item with no backlink is a failing acceptance test. Two + constraints on the link itself (Vera 61560): it points at the normal + authenticated route — never a signed/capability URL, so a leaked external + board leaks no access; and the visible link text is a deliberate + disclosure decision — the default carries no pod name ("Open in + Commonly"), and putting the pod name in outward-facing text is a + per-projection opt-in, because the external tool's audience is not the + pod's membership. - **Phase 2 — two-way.** Everything D3–D7 specifies for inbound (resolvable actor, field-class conflict rules, lastSyncedAt algorithm, parking) is DOCUMENTED NOW and built only when outbound-only measurably fails a real From 711c705c6ad7d84bc9fc4771212bda1546b7c8c2 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:54:04 -0700 Subject: [PATCH 12/18] =?UTF-8?q?docs(adr):=20ADR-027=20=E2=80=94=20harden?= =?UTF-8?q?ed=20outbound=20send=20contract=20(D9)=20+=20credential/schema/?= =?UTF-8?q?health=20properties=20(D8),=20per=20the=20connector=20patterns?= =?UTF-8?q?=20study?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index e6739b53e..e2bb16b0a 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -153,6 +153,16 @@ scope-grant report, per D8). Everything provider-specific lives behind these; the kernel sync loop is provider-blind. Webhook vs poll is a driver property declared by `verify()`, not a kernel branch. +**D9's outbound half is the hardened send contract, built once in the +SDK.** Every `push()` send carries an idempotency key (retries never +double-post) and resolves through the three-way error taxonomy: throttled → +sleep the provider's retry-after and retry within budget; definitive +rejection → fail fast into the projection's status; ambiguous → verify then +retry. Adapter authors implement provider calls, never delivery policy — +the policy ships once as the SDK's normalized outbound message (pattern +per the operator engineering study, 2026-08-31; openly licensed sources +only, ideas not code). + **D9 is transport-agnostic, and ACP is a supported binding.** The ecosystem is converging on ACP for agent↔tool session transport (operator strategy note, 2026-08-30; multiple independent adoptions). The four verbs carry no From 1c307b12f50f6715c4cef9d91fe77fededa3426e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:54:25 -0700 Subject: [PATCH 13/18] =?UTF-8?q?docs(adr):=20ADR-027=20D8=20=E2=80=94=20c?= =?UTF-8?q?redential=20store=20reference,=20schema-at-persistence,=20healt?= =?UTF-8?q?h=20surfacing=20(study=20fold,=20missed=20anchor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index e2bb16b0a..56d340373 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -145,7 +145,15 @@ scope (`externalRef`: one database / one project / one board) at install, the kernel refuses to read or write outside it regardless of what the token allows, and `verify()` reports the token's actual grant so the Connectors surface can show "token exceeds declared scope" as a warning state. Scope -widening is a new install decision, never a drift. +widening is a new install decision, never a drift. Two further +hardening properties bind here (same study): provider credentials are held +in the encrypted credential store behind a `credentialId` — never inline in +the Projection row, never returned by any API (ADR-025 D6 shape); and the +Projection's config schema is enforced at the persistence boundary with +unknown keys rejected, while runtime state (the projection map, cursors) +lives in its own store with its own lifecycle, never inside config. +`verify()` failures write the projection's `status`/`errorMessage`, which +the Connectors surface renders — a broken projection is never silent. **D9 — The driver interface is four verbs**, mirroring CAP's shape: `pull(since)`, `push(changes)`, `mapIdentity(actor)`, `verify()` (health + From 4749322dec5437921023dc54fd0862e59aca6b4b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:56:11 -0700 Subject: [PATCH 14/18] =?UTF-8?q?docs(adr):=20ADR-027=20D9=20=E2=80=94=20i?= =?UTF-8?q?dempotency=20key=20is=20event-derived,=20never=20per-attempt=20?= =?UTF-8?q?(Vera=2061614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 56d340373..87ec20aa0 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -162,8 +162,9 @@ these; the kernel sync loop is provider-blind. Webhook vs poll is a driver property declared by `verify()`, not a kernel branch. **D9's outbound half is the hardened send contract, built once in the -SDK.** Every `push()` send carries an idempotency key (retries never -double-post) and resolves through the three-way error taxonomy: throttled → +SDK.** Every `push()` send carries an idempotency key **derived from the item +change (the event), never the attempt** — a per-attempt key turns every +retry into a new provider message (retries never double-post) and resolves through the three-way error taxonomy: throttled → sleep the provider's retry-after and retry within budget; definitive rejection → fail fast into the projection's status; ambiguous → verify then retry. Adapter authors implement provider calls, never delivery policy — From 9ae1fc47f4ec4394e3fbc869e8c0f79394300af0 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:01:18 -0700 Subject: [PATCH 15/18] =?UTF-8?q?docs(adr):=20ADR-027=20D5=20=E2=80=94=20p?= =?UTF-8?q?ointer-not-payload=20projection=20rule=20(QM/Lody=20build-level?= =?UTF-8?q?=20study)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 87ec20aa0..30988168f 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -104,8 +104,18 @@ by display name or email (the #1289 rule for rows). Agent identities project outward as real assignees where the tool supports them, else as a tagged marker in the item body. -**D5 — One canonical item schema; lossy maps are declared, not discovered.** -Canonical: `{ title, description, status, assignee, labels, links }` with +**D5 — One canonical item schema; lossy maps are declared, not discovered. +And the projected record is a pointer, never the payload** (build-level +study, 2026-08-31; openly licensed sources, ideas not code): outward we +project `{ title, status, assignee, awaiting-decision, backlink }` plus +counts and a stable content hash — description bodies, attachments, and +work artifacts stay in the ledger and are served on demand behind the +backlink. This is what keeps a projection an attention surface rather than +a data pipe, keeps the one-database scope (D8) small, and makes the +link-back acceptance rule cheap — the item links to the ledger without +shipping the ledger. Rendering a projected row must never require opening +the underlying document. Canonical (full, for the phase-2 inbound map): +`{ title, description, status, assignee, labels, links }` with status ∈ Task's own enum. Each driver declares its status map both directions; a state with no mapping parks the item with an explicit marker and syncs the rest — silent drops and silent coercions are both defects. From 11436df227d357febff6c7fd2fec26d412a59230 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:02:35 -0700 Subject: [PATCH 16/18] =?UTF-8?q?docs(adr):=20ADR-027=20D5=20=E2=80=94=20r?= =?UTF-8?q?ound-trip=20invariant=20scoped=20to=20the=20projected=20field?= =?UTF-8?q?=20set=20(Vera=2061646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 30988168f..1b35e5bd4 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -119,7 +119,12 @@ the underlying document. Canonical (full, for the phase-2 inbound map): status ∈ Task's own enum. Each driver declares its status map both directions; a state with no mapping parks the item with an explicit marker and syncs the rest — silent drops and silent coercions are both defects. -Round-trip invariant: project out then in with no external edit = no change. +Round-trip invariant, scoped to the PROJECTED field set (Vera 61646 — +unprojected fields like description cannot round-trip by construction): +for every field the projection carries outward, project out then in with +no external edit = no change. A driver is conformant when the projected +set round-trips exactly and the unprojected set is untouched on both +sides. **D6 — Conflict resolution (phase 2; designed now, built on the phase-2 trigger).** "Newest wins" is user-facing phrasing, NOT the algorithm — two From e9972d01721a2c2eb8b312592f269523b2b10f87 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:03:21 -0700 Subject: [PATCH 17/18] =?UTF-8?q?docs(adr):=20ADR-027=20D5=20=E2=80=94=20c?= =?UTF-8?q?ontent=20hash=20keyed=20per=20projection,=20never=20a=20bare=20?= =?UTF-8?q?digest=20(Vera=2061647)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 1b35e5bd4..1df1ebb39 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -108,7 +108,11 @@ marker in the item body. And the projected record is a pointer, never the payload** (build-level study, 2026-08-31; openly licensed sources, ideas not code): outward we project `{ title, status, assignee, awaiting-decision, backlink }` plus -counts and a stable content hash — description bodies, attachments, and +counts and a stable content hash **keyed per projection** (HMAC with a +per-projection secret, never a bare digest — the hash sits in a database +the whole external workspace reads, and over tiny input spaces like +status/assignee an unsalted hash is a lookup table away from disclosing +exactly what this rule keeps in the ledger; Vera 61647) — description bodies, attachments, and work artifacts stay in the ledger and are served on demand behind the backlink. This is what keeps a projection an attention surface rather than a data pipe, keeps the one-database scope (D8) small, and makes the From 8e1641e0bc6ba69b12e75202e592797bc6b2652d Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 31 Aug 2026 04:04:11 -0700 Subject: [PATCH 18/18] =?UTF-8?q?docs(adr):=20ADR-027=20D5=20=E2=80=94=20a?= =?UTF-8?q?waiting=20signal=20is=20ADR-028's=20primitive,=20projected=20no?= =?UTF-8?q?t=20redefined=20(Vera=2061648)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/adr/ADR-027-pm-tool-projection-contract.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/adr/ADR-027-pm-tool-projection-contract.md b/docs/adr/ADR-027-pm-tool-projection-contract.md index 1df1ebb39..9b62bc8ec 100644 --- a/docs/adr/ADR-027-pm-tool-projection-contract.md +++ b/docs/adr/ADR-027-pm-tool-projection-contract.md @@ -107,7 +107,11 @@ marker in the item body. **D5 — One canonical item schema; lossy maps are declared, not discovered. And the projected record is a pointer, never the payload** (build-level study, 2026-08-31; openly licensed sources, ideas not code): outward we -project `{ title, status, assignee, awaiting-decision, backlink }` plus +project `{ title, status, assignee, awaitingSince, backlink }` — `awaitingSince` +is the kernel's needs-a-human signal, ONE concept defined where the ledger +defines it (ADR-028; the study's awaitingUserSince). This ADR projects +that field and never redefines it; if ADR-028 lands a different name, this +line follows it — plus counts and a stable content hash **keyed per projection** (HMAC with a per-projection secret, never a bare digest — the hash sits in a database the whole external workspace reads, and over tiny input spaces like