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 @@ -210,6 +210,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 the RIG-3331 state-transition op's stamped acting agent for STATE (NOT the author-row proxy — a proxy would eat a human's close of the agent's own issue). 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) |

## Agent roles & prompts

Expand Down Expand Up @@ -248,6 +249,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 (`githubapp_webhook.go:280`, `linearagent/data_event.go:177`, `ingest/notify_detect.go:328`) from `forge.Author.OwnerHandle` (`forge/owner.go:26,:162`). 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)` (`0001_init.sql:158`) — 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