Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/designs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ check enforces the mechanical half. Full rationale:
| DL-309 | Real-App webhook validation is a LIVE TUNNEL ROUND-TRIP in the livegithub tier: a smee.io-style tunnel receiver gives the oracle a public ingress, the real GitHub App and real Linear app webhooks register against the tunnel, and one real delivery per provider is asserted end to end through the mounted handlers (`NewGitHubWebhookHandler`/`NewLinearWebhookHandler`) with fail-closed signature verify — chosen over capture-and-replay, accepting the live-ingress machinery in CI; the one-time registration + PEM-rotation runbook stays | Active (Matt, 2026-08-31) | [forge app credentials](server/compass-forge-app-credentials/design.md) |
| DL-324 | The forge live-test Linear credential is app-actor ONLY — DL-308's retain-one-Linear-user-credential carve-out is dropped (Matt's RIG-3096 pivot): the `livegithub` oracle's Linear legs gate on a single env var, `LINEAR_FORGE` (`go/internal/forge/livegithub_test.go:65`, consumed by `requireLinear` :91-99), minted as an app-actor token per CI run by `tools/forge-linear-token/index.ts` via the client_credentials grant from the `LINEAR_FORGE_CLIENT_ID`/`_SECRET` Actions secrets (`.github/workflows/ci.yml:1002-1004`) — with NO `LINEAR_FORGE_USER_TOKEN` (zero code hits) and no human→app delegation-setup step in CI. Supersedes DL-308's Linear-user-cred clause ONLY — DL-308's GitHub App-installation-token-mint clause stays live, so DL-308 itself remains Active | Active (Matt, 2026-09-02) | [forge app credentials](server/compass-forge-app-credentials/design.md) |
| DL-340 | The forge Linear provider gains an outbound SELF-DELEGATE write path: `forge.CreateIssue` carries optional `DelegateSelf bool` + `Assignee string` (Linear-only; GitHub ignores `DelegateSelf`), and `Linear.CreateIssue` sets `input["delegateId"]` to its OWN app-user id (resolved via a `viewer { id app }` sibling of the existing `actorAttribution` probe, and set ONLY when the probe confirms the actor is app-capable — a non-app principal's `viewer.id` is never delegated to) and `input["assigneeId"]` when set — degrade-on-probe-failure (create without delegate + warn, never fail). The outbound shape ALWAYS sets BOTH slots (Matt, 2026-09-05): a human `assigneeId` alongside the app `delegateId`, never delegate-only, since Linear's UI offers no delegate-without-assignee; the `Assignee` field stays typed optional (empty = unset) but the policy forbids the empty case on a self-delegated create, and the assignee UUID is caller-supplied, never defaulted in the provider. A live probe (2026-09-05, RIG-3302) confirmed an `actor=app` client-credentials token scoped `read,write,app:assignable` MAY self-delegate on `issueCreate` (`success: true`), and the assignee slot is INDEPENDENT (delegate-only leaves `assignee` null; a human assignee + app delegate coexist in one create) — so the self-delegate path is ratified over the `agentSessionCreateOnIssue` fallback, and the oracle mint scope moves to `read,write,app:assignable` (`tools/forge-linear-token/index.ts`). Outbound half only (app token, no user credential — DL-324 stays live); the inbound delegation round-trip is Record B | Active (Matt, 2026-09-05) | [forge self-delegate](server/compass-forge-self-delegate/design.md) |
| DL-338 | Forge notifications are self-origin-suppressed at the notify-router fan-out (`go/internal/ingest/notify_router.go` step 6): a COMMENT / REVIEW / OPENED / STATE notification is skipped when the event actor's OWNER-QUALIFIED Compass handle (owner-handle + agent-handle — never account ids, and never a bare agent handle, which is unique only per `(tenant_id, owner_user_id, handle)`) equals the subscriber's. Actor source per kind: `CommentRef.Agent` for COMMENT/REVIEW, the DL-055 ownership row's recorded author for OPENED, and — for STATE — the acting agent resolved from RIG-3331's consumable `forge_state_transitions` memo, keyed on the event's forge coordinate and applied state (NOT the author-row proxy, which would eat a human's close of the agent's own issue, and NOT a field on the event: per RIG-3331 OQ-1 the actor never rides the provider-parsed struct). Fails OPEN on any unresolved / unqualified / ambiguous / faulted identity and on a nil resolver; CHECKS (an agent's own CI results) and UPDATE are NEVER suppressed (CHECKS an invariant with its own test). A suppressed notification advances the subscriber's `delivered_revision` (the one router-side advance, a narrow amendment to W3) ONLY when the subscriber was already caught up to the prior cursor — CAS-guarded (`AND delivered_revision = $prior`) — so the reconcile sweep neither resurrects the suppressed notification nor masks a prior undelivered real notification (an unconditional advance would permanently mask it) — and ARTIFACT-scope subscriptions ONLY: a suppressed OPENED dispatch to a CONTAINER-scope subscriber skips the dispatch and never advances, since an artifact revision written into a container cursor poisons the container sweep. The Linear agent-session surface gets a source-tagged drop-before-enqueue seam, implementation Record-B-gated (RIG-3271). STATE suppression is interim-open (fail-open, delivers) until RIG-3331 stamps the real actor. | Active (Matt, 2026-09-05) | [forge self-delegate suppression §matrix](server/compass-forge-self-delegate-suppression/design.md#actor-identity-by-change-kind--the-suppresskeep-matrix) |
| DL-342 | The forge state-transition write op is a parallel-arm pair (`TransitionIssueState` / `TransitionPullRequestState` on `forge.Provider`, `transition_issue_state` / `transition_pull_request_state` on `ForgeCallRequest`) over a portable `{open, closed}` core plus per-provider refinements carried in one `TransitionState` input (`close_reason` for GitHub issues, `workflow_state` for Linear). Every mismatch fails LOUD and in-band as `invalid_argument` before any provider call: a refinement the target provider cannot express, an unknown or ambiguous Linear state name, a named state whose type contradicts the portable target, and — per the OQ-2 ruling — a default resolution finding MORE THAN ONE candidate state, which names the candidates and demands an explicit `workflow_state` rather than taking a positional guess. Defaulting happens only where the target is unambiguous (default-close resolves against `completed`, never `canceled`). The Linear PR half returns `ErrUnsupported` (Linear is issues-only). Transitions are NOT F3-deduped and NOT DL-055-recorded — not the comment arm's reason (a comment has no representable coordinate) but because a transition targets a coordinate whose row is a write-once authorship fact whose `client_request_id` backs the create's F3 memo, so routing a transition through the record path would overwrite it. Amends DL-241's tool count by citation (twelve tools, rule unchanged). | Active (Matt, 2026-09-07) | [forge state transition](server/compass-forge-state-transition/design.md) |
| DL-343 | Forge transition actor attribution rides a CONSUMABLE `forge_state_transitions` memo, never a parsed-text or author-row proxy: written after a successful transition at the same attribution chokepoint every forge write rides, then consumed on match at the notify lane so the provider-echoed STATE event resolves to the acting agent. Tenant-scoped like its `forge_authored_artifacts` sibling, and fail-open on a miss (a late webhook loses attribution; it never blocks or misattributes). This is the contract surface the RIG-3326 self-origin suppression record's STATE arm keys on, and it settles that cross-record question in the memo's favour: RIG-3326's frozen text describes the actor as stamped onto the emitted event, so its STATE arm bends to a memo lookup at the actor-resolution seam. The rejected alternative was a synthetic STATE event emitted at the chokepoint — equivalent for the suppression outcome, but it invents an event the forge never sent. Landing the seam is what closes RIG-3326's interim-open STATE arm. | Active (Matt, 2026-09-07) | [forge state transition](server/compass-forge-state-transition/design.md) |

Expand Down Expand Up @@ -261,6 +262,7 @@ check enforces the mechanical half. Full rationale:
| DL-127 | The Compass UI shell adopts `@solidjs/router` in `HashRouter` mode (Matt's library ruling) emitting the frozen `#/channel/<channelId>/topic/<topicId>` route shape for a client-only SPA in the Wails v3 webview (DL-110); and the URL becomes the source of truth for view + routed selection — the store derives `View` and routed memos from the matched route (route drives store, not store drives route), with `MemoryRouter` for deterministic tests | Active (Matt, 2026-08-03) | [shell routing §A2](ui/compass-shell-routing/design.md#a2--source-of-truth-url-drives-the-store-decision) |
| DL-128 | The Compass UI adopts `@tanstack/solid-query` + `@connectrpc/connect-query-core` for server-state: one app-lifetime `QueryClient` created beside the store singleton in `index.tsx` and provided via the SolidJS `QueryClientProvider` (Solid adapter, never React); server-state reads move off the store onto query hooks over the generated Connect clients, the store keeping only client/UI state | Active (Matt, 2026-08-03) | [query layer §Approach](ui/compass-ui-query-layer/design.md#approach) |
| DL-186 | Pre-dogfood proto wire-compat is stripped: all `reserved` markers removed across compass/v1, live fields densely renumbered (no holes), and the `buf breaking` CI gate removed (re-armed at GA / first pinned client). Reclaims AgentAttribution field numbers 2,3 (DL-094's forever-reservation) — DL-094's attribution-as-display-fact semantics are UNCHANGED (owner resolved on AgentAccount.owner_user_id, not per-artifact); only its wire-number reservation is dropped, safe because zero clients are deployed and nothing on disk is proto-encoded. | Active (Matt, 2026-08-07) | [attribution simplification §Approach](server/compass-attribution-simplification/design.md#approach) |
| DL-339 | `compass.v1.AgentAttribution` regains `owner_handle` (field 2 — the slot DL-186's wire-compat strip freed), populated at the three parse sites that already produce it and discard it (`stripBodyToRef` in `go/internal/forge/githubapp_webhook.go`, `linearCommentRef` in `go/internal/linearagent/data_event.go`, `detectArtifact` in `go/internal/ingest/notify_detect.go`) from `forge.Author.OwnerHandle` (`Author` in `go/internal/forge/owner.go`). AMENDS — does not reverse — the "owner is an account property, resolved server-side, never restated per artifact" clause DL-094 wrote and DL-186 carried forward: that clause was scoped to attribution as a DISPLAY fact (the board renders a bare `@handle`), and its "resolved server-side" escape hatch does not survive an identity-MATCHING consumer, because the only server-side key available for a comment actor is the bare agent handle, which is unique only per `(tenant_id, owner_user_id, handle)` (`go/internal/store/migrations/0001_init.sql`) — the exact ambiguity owner-qualification exists to resolve. Neither DL-094 nor DL-186 weighed a matching consumer; DL-338's self-origin suppression is the first, and without the owner half its COMMENT/REVIEW arm is structurally inert. Attribution stays a display fact, not a trust claim (DL-094's core semantics unchanged) — it now carries both halves of the identity it already parsed. `forge.ForgeEvent` additionally gains an internal `Actor *AgentAttribution` carrying the same owner-qualified attribution for kinds with no `CommentRef` (STATE, populated by RIG-3331). DL-186 stays Active: its wire-compat-strip clause is untouched | Active (Matt, 2026-09-05) | [forge self-delegate suppression §owner-carriage](server/compass-forge-self-delegate-suppression/design.md#why-owner-qualified-handles-not-account-ids-matts-hard-rule) |
| DL-193 | The Compass UI's live agent roster is a reactive JOIN, not a `GetRoster`-as-roster replace: durable identity + tree come from the already-live `SubscribeComms` accounts (the single identity source — `RosterEntry` lacks `homeChannelId`/`kind` the UI requires), while ephemeral presence + activity come from a new `CommsState.presence` map seeded by one `GetRoster(scope: OWNER)` per snapshot boundary and tailed by the `AgentPresenceChanged` event the stream driver currently drops; `STUB_AGENTS` is demoted from live render source to the offline-store seed behind the store's reactive `agents` accessor (the `STUB_ISSUES` seam pattern), never rendered on a live connection | Active (Matt, 2026-08-16) | [live roster §Resolved decisions](ui/compass-live-roster/design.md#resolved-decisions) |
| DL-194 | The presence→dot projection is the total 4-state mapping `WORKING→working, IDLE→idle, WAITING→waiting, OFFLINE→stopped` plus a defensive `UNSPECIFIED→undefined` (unreachable on the `GetRoster` path, which defaults every agent absent from its in-memory presence source to `OFFLINE`; the client `joinAgents` mirrors this at its own seam, mapping a presence-map miss — a snapshot-boundary race or a post-snapshot `accountChanged` arrival — to `stopped` rather than the components' `?? "idle"` fallback, so the absent→stopped invariant holds end-to-end); the 4-state enum cannot distinguish a deliberately-stopped agent from a never-started one, so a freshly-seeded-but-unstarted agent renders the "stopped" dot — accepted as a known day-one cost, with the terminated/never-started split owed to the deferred `AgentSessionStatus` lane, which is also the only source for the remaining four `AgentState` values (`done/paused/error/disconnected`) | Active (Matt, 2026-08-16) | [live roster §Resolved decisions](ui/compass-live-roster/design.md#resolved-decisions) |
| DL-195 | `Agent.role`/`model`/`cwd` are optional view-model fields with no server source: a live agent renders without them (the role pip is render-gated on a present, non-worker role — never derived from tree position, since "has children" is not "supervisor"), and `terminals` is `[]` for live agents until a terminal stream exists | Active (Matt, 2026-08-16) | [live roster §Resolved decisions](ui/compass-live-roster/design.md#resolved-decisions) |
Expand Down
Loading
Loading