Compass server ownership layer — amendment: one canonical type family
Tracker: SEA
Extends #995 (merged). This record is a sibling amendment to
docs/designs/server/compass-server-ownership-layer/design.md(merged in #995). The merged record is frozen; per the frozen-record convention a later change ADDS a record. This amendment reconciles #995’s forge-shaped proto type family to the single canonicalcompass.v1family defined bydocs/designs/server/compass-issue-model/design.md(#1018).
Problem / Intent
Section titled “Problem / Intent”The #995 Decision 5 proto block adds a forge-shaped family to the wire:
message Issue (#995 design.md:361-372, fields repo, number, title, body, state, url, ForgeAuthor author, forge_author_login, labels, updated_at_unix_ms),
ForgeAuthor (:385-390, itself flagged “the pre-OQ-1 draft, not a frozen
contract”), IssueComment (:392-401), PullRequest (:403-417, carrying
ForgeChecksSummary checks), and ForgeChecksSummary/ForgeCheck
(:421-431) — all slated for agent_gateway.proto by task T1 (:1625-1635).
Matt’s type-architecture ruling (2026-07-31, ratified as DL-069,
DECISIONS.md:127) supersedes that family: Compass owns a single canonical
compass.v1 Issue/PullRequest type pair; “the server translates raw forge
data into these types at ingestion, the raw forge shape is never a proto/wire
type” (DL-069). The #1018 record names this amendment as the
reconciliation site:
“(PR #995’s earlier sketch of forge-proxy proto messages under these names in
agent_gateway.proto— #995 design.md:361-372, :403-417, :1625 — is superseded by this model and reconciled in a sibling amendment to that record; this record does not depend on any forge proto message existing.)” — #1018 design.md:105-108
This amendment is that sibling. It removes the second, forge-shaped wire
family from #995’s plan and points every surface that would have carried it at
the canonical compass.v1 types. Nothing here is a retrofit of shipped wire
surface: the #995 T1 proto is not yet built — grep of the live proto
tree (proto/compass/v1/, compass repo) finds no ForgeCall, ForgeAuthor,
or RelayForgeCall symbol, and no go/internal/forge package exists. The
amendment edits the #995 plan’s remaining work, not deployed protocol.
Approach
Section titled “Approach”What this amendment asserts
Section titled “What this amendment asserts”The forge domain messages (Issue / IssueComment / PullRequest /
ForgeChecksSummary / ForgeCheck) and ForgeAuthor do not go on the
wire. Three layers, one type family:
-
Ingestion boundary — internal Go value types, unchanged. The forge Provider layer stays exactly as #995 T2a specifies it: “New package
go/internal/forge: theProviderandServiceinterfaces verbatim from Decision 3, the value types (Author,Issue,Comment,PullRequest,Checks,CreateIssue,CreatePR,IssueFilter)” (#995 design.md:1659-1661). These are server-internal Go structs at the ingestion boundary — precisely the “raw forge payload … server-internal Go data that exists only inside the ingestion translation” #1018 describes (design.md:42-46). T2a survives verbatim; only what its Service layer emits across the wire changes. -
The wire — the canonical
compass.v1family only. What crosses any proto boundary is #1018’s canonical family, defined incompass.proto(packagecompass.v1), notagent_gateway.proto:Issue(#1018 design.md:169-197) — “The board unit: a Compass Issue — the forge issue’s fields PLUS the Compass agent attribution PLUS the Compass machinery, translated from raw forge data at server ingestion” (:165-168).PullRequest(:202-218), carryingChecksSummary checks = 13and the full review state.ChecksSummary/Check(:222-232) — replaces #995’sForgeChecksSummary/ForgeCheck(:421-431); field-for-field the same shape (head_sha,state,checks/name,state,url,required), now Compass-owned.AgentAttribution(:137-144:agent_handle,owner_handle,bool verified) — replaces #995’sForgeAuthor(:385-390). This also RESOLVES #995’s OQ-1 field-set gate on that message:agent_account_idandsession_idare deleted (both flagged for likely deletion at #995 design.md:387-389), and theverifiedbool #995 OQ-1 contemplated is in, “set by the server’s forge-login cross-check at ingestion (#995 OQ-1)” (#1018 design.md:140-143).
-
Delivery to the UI — the projection +
SubscribeEvents. The canonicalIssue“ridesSubscribeEventsResponseas a new oneof variant” (Issue issue = 16, #1018 design.md:284-298), computed by the server projection composing the existinggo/internal/boardrecorded-state pattern (:349-358, :371-384). Lifecycle mutation is the soleUpdateIssueStateRPC (:487-496). None of that is this amendment’s surface — it is #1018’s, cited here as the replacement read path the UI uses.
The #995 T1 add-list, item by item
Section titled “The #995 T1 add-list, item by item”The #995 T1 brief (design.md:1625-1635) is the authoritative enumeration of everything #995 adds to the proto tree. Disposition of each item under this amendment:
| # | T1 item | Disposition | Why |
|---|---|---|---|
| 1 | Forge RPC on AgentGateway (#995 :303-306) |
SURVIVES | The agent-facing tool carrier. Its envelope types are unchanged; only result payload types reconcile. The read ops survive answered from the projection/store (OQ-A ruled option 3, §Resolved decisions). |
| 2 | ForgeCallRequest (#995 :310-323) |
SURVIVES | Carries call_id + a oneof of the op requests below — no forge domain type in it. |
| 3 | ForgeCallResult (#995 :325-337) |
RECONCILED | The envelope survives; its oneof result arms retype: Issue issue → compass.v1.Issue, ListIssuesResponse issues (internally retyped, item 11), PullRequest pull_request → compass.v1.PullRequest, and the two IssueComment arms (issue_comment, pr_comment) retype to the ruled CommentRef (§Resolved decisions OQ-B). |
| 4 | ForgeCallError (#995 :343-347) |
SURVIVES | Scalars only (code, message, retry_after_ms). |
| 5 | message Issue (#995 :361-372) |
REMOVED | Replaced by compass.v1.Issue (#1018 :169-197). Every #995 field has a canonical home: repo/number/title/body/url/labels map directly; state → forge_state (:181); author → agent (AgentAttribution, :183-184); forge_author_login → forge_account (:185-186); updated_at_unix_ms is dropped (no canonical counterpart — the projection’s stream ordering supersedes a per-artifact timestamp; flag at fold if a consumer needs it). |
| 6 | message IssueComment (#995 :392-401) |
REMOVED | Canonical Issue deliberately carries no comment list, and #1018’s Comment (:277-281) exists only inside a PR review thread — there is no standalone canonical issue-comment message. The two carriers that embedded IssueComment take the ruled CommentRef (§Resolved decisions OQ-B). |
| 7 | message PullRequest (#995 :403-417) |
REMOVED | Replaced by compass.v1.PullRequest (#1018 :202-218); same field mapping as item 5, plus checks → ChecksSummary and the added ChangedStats/Review/ReviewThread surface the canonical type carries. |
| 8 | message ForgeAuthor (#995 :385-390) |
REMOVED | Replaced by compass.v1.AgentAttribution (#1018 :137-144). Resolves #995 OQ-1’s field-set gate as described above. |
| 9 | ForgeChecksSummary/ForgeCheck (#995 :421-431) |
REMOVED | Replaced by compass.v1.ChecksSummary/Check (#1018 :222-232), field-for-field identical. |
| 10 | The seven op requests (#995 :433-473): CreateIssueRequest, CommentOnIssueRequest, GetIssueRequest, ListIssuesRequest, CreatePullRequestRequest, CommentOnPullRequestRequest, GetPullRequestRequest |
SURVIVE | Confirmed per-request this run: every field in all seven is a scalar (string/uint64/uint32/bool/repeated string) — no forge domain type appears in any request shape (#995 :433-473). Read-op requests (GetIssueRequest, ListIssuesRequest, GetPullRequestRequest) survive (OQ-A ruled option 3 — answered from the projection/store, §Resolved decisions). One width note: #995 keeps uint64 issue/pull numbers in these requests (:444-446, :470-472) while the canonical result types carry uint32 number (#1018 :178); narrow the surviving request fields to uint32 to match the canonical width — free now, buf-breaking after the surface ships. |
| 11 | ListIssuesResponse (#995 :454-456) |
RECONCILED | repeated Issue issues = 1 retypes to repeated compass.v1.Issue; the read op is answered from the projection/store (OQ-A ruled option 3). |
| 12 | SubscribeForgeRequest/Response, UnsubscribeForgeRequest/Response, ForgeArtifactKind (#995 :1027-1043) |
SURVIVE | All scalar/enum fields (repo, kind, number, subscription_id); no domain type. (ForgeArtifactKind relocates to the new forge.proto leaf per DL-182 — see row 13; the subscribe/unsubscribe requests stay in agent_gateway.proto.) |
| 13 | ForgeNotification/ForgeNotificationKind (#995 :1191-1212) |
RECONCILED + RELOCATED (DL-182) | The message and kind enum survive but move to the new leaf forge.proto (with ForgeArtifactKind + CommentRef) to break the notification-placement cycle. Three arms reconcile: ForgeChecksSummary checks = 9 → compass.v1.ChecksSummary; IssueComment comment = 8 → the ruled CommentRef (§Resolved decisions OQ-B); and string provider = 2 (#995 :1193) is retyped to compass.v1.ForgeRef forge = 2 — the shipped decision (forge.proto ForgeRef forge = 2), so the notification path carries the one compass.v1 forge-identity vocabulary rather than a second bare-string one. |
| 14 | runner.proto: RelayForgeCall + RelayForgeCallRequest/Response (#995 :485-493) |
SURVIVES | Pure wrapper: ForgeCallRequest call / ForgeCallResult result by reference; reconciliation is inherited from items 2-3. |
| 15 | SessionsResponse.forge_notification = 7 (#995 :1633) |
SURVIVES | Carries ForgeNotification, reconciled internally (item 13). |
| 16 | agent.proto: AgentControl.forge_notification = 9 (#995 :1633-1635) |
SURVIVES | Same carrier logic as item 15. |
| 17 | gen-fence grep extension (#995 :1636-1639) |
RECONCILED | The extension is still required for the surviving internal family (ForgeCall|RelayForgeCall|ForgeNotification|ForgeArtifactKind), but it MUST NOT fence the canonical types: Issue/PullRequest/ChecksSummary/Check/AgentAttribution are PUBLIC compass.proto symbols that legitimately generate into the public trees. See Global Constraints for the cross-file import this creates. |
Net: the carrier surface survives (RPCs, envelopes, requests,
subscriptions, notification kinds, relay leg) and the domain payload family
is deleted — six messages removed, four carrier arms retyped to compass.v1.
Multi-forge request addressing (a reconciliation consequence). The
surviving op requests and SubscribeForgeRequest address artifacts by
(repo, number) with no provider/host (#995 :433-473, :1027-1043), while the
canonical result types — and now the notification path (row 13) — carry
ForgeRef (#1018 :146-160, DL-091:
repo-only coordinates collide under multiple connected forges). #995 even
names subscriptions “(provider, repo, kind, number)” (:944-945) though its
wire request carries no provider. Ruled (Matt, 2026-07-31, §Resolved
decisions): request addressing stays single-forge for a GitHub-first v1 (repo
unambiguous); adding an optional ForgeRef/provider to the op requests and
SubscribeForgeRequest is a named additive follow-up, free to add later
without a buf-breaking change.
Two structural consequences: a cross-file import and a new leaf file
Section titled “Two structural consequences: a cross-file import and a new leaf file”(1) The carrier imports the canonical family. The #995 record placed the
domain messages in agent_gateway.proto (internal-only). The canonical family
lives in compass.proto — public, package compass.v1 (#1018
design.md:99-101). Retyping the result arms therefore makes
agent_gateway.proto import compass/v1/compass.proto. This edge is already
present transitively (agent_gateway.proto pulls agent.proto which imports
compass.proto, agent.proto:25); making it direct changes no gen wiring the
lanes don’t already handle. The two internal gen lanes treat the canonical
types differently — the TS lane regenerates them via --include-imports, the Go
lane M-redirects them to the public go/gen package — and the gen-fence must
keep the canonical symbols unfenced.
(2) A new internal leaf forge.proto (DL-182). The types
AgentControl.forge_notification reaches (ForgeNotification/
ForgeNotificationKind, ForgeArtifactKind, CommentRef) do NOT live in
agent_gateway.proto as #995 T1 first directed — that placement is a circular
import, since agent_gateway.proto already imports agent.proto and
agent.proto would then have to import back for the notification type. They
move into a new internal-only leaf proto/compass/v1/forge.proto that imports
only compass.proto; agent.proto, agent_gateway.proto, and runner.proto
all import the leaf without a cycle. This adds one internal file to all three
gen lanes (buf.gen.yaml exclude + both internal-lane inputs) and to the
gen-fence symbol set. Wire contract, field numbers, and generated names are
unchanged — only the source file each type lives in moves. Detail in Global
Constraints below.
Alternatives considered
Section titled “Alternatives considered”-
Keep #995’s forge-proxy family as a “translation-source” proto — i.e. ship both families and define the ingestion translation as a proto→proto mapping (
agent_gateway.Issue→compass.v1.Issue). Rejected: DL-069 rules “the raw forge shape is never a proto/wire type” (DECISIONS.md:127), and #1018 is explicit that the raw forge payload “is server-internal Go data that exists only inside the ingestion translation; it is never exposed, not to the UI and not as a separatecompass.v1message” (#1018 design.md:42-46). A second wire family is exactly the wart being removed — and it would force permanent double-maintenance of every field. -
Delete the
ForgeRPC family entirely and route agent forge access through some other surface. Rejected: the carrier is ratified independently of the payload types — DL-049 (“Forge tools ride the existingAgentGatewaysocket as a siblingForgeCall*family relayed byRelayForgeCall”, DECISIONS.md:100) governs how agent forge calls travel, and nothing in DL-069 touches it. The write path (create/comment) has no substitute surface at all; only the read ops had a candidate alternative (OQ-A, now ruled — read ops survive on the gateway, answered from the store). -
Rename rather than remove — keep the forge-shaped
Issue/PullRequestmessages under new names (ForgeIssue,ForgePullRequest) as internal-only wire types. Rejected: DL-069’s wart is a parallel family for the same board artifact — two messages (ForgeIssueandcompass.v1.Issue) for one issue, forcing permanent double-maintenance of every field. Renaming keeps that duplication, and it would re-open #995 OQ-1 that #1018’sAgentAttributionalready closes (#1018 design.md:137-144). (This is NOT an objection to a message carrying forge fields as such — the canonical types themselves are forge fields plus Compass attribution plus machinery; the ruledCommentRefis the same pattern and duplicates no canonical type, see §Resolved decisions OQ-B.)
Global Constraints
Section titled “Global Constraints”Every task below inherits these; they restate the gates that bind the amended proto work, confirmed against the live tree this run.
- No AI tool, agent-product, or persona names anywhere in the record, the proto comments, or code comments. Describe behavior directly.
- Cite ledger ids in proto comments. The reconciled result arms and the
ingestion translation carry
DL-069(and this amendment’s row) in their comments, mirroring how #1018’s proto sketch cites DL-033/DL-091 (#1018 design.md:114-117). - buf breaking stays clean by construction. None of the #995 T1 surface
exists on
mainyet — the live proto tree has noForgeCall,ForgeAuthor, orRelayForgeCallsymbol (grep ofproto/compass/v1/, this run) — so the amended add-list is still a purely additive change and passes thebreakingtask (buf breaking … --against origin/main, #995 design.md:1490-1491) with no new exemption. Nothing is ever removed from a shipped wire surface. - Files touched (amending #995 T1’s list, design.md:1625-1639):
the NEW leaf
proto/compass/v1/forge.proto(ForgeNotification/ForgeNotificationKind,ForgeArtifactKind,CommentRef; imports onlycompass.proto— DL-182),proto/compass/v1/agent_gateway.proto(carrier family + reconciled arms; imports the leaf),proto/compass/v1/runner.proto(RelayForgeCall,SessionsResponse.forge_notification = 7; imports the leaf),proto/compass/v1/agent.proto(AgentControl.forge_notification = 9; imports the leaf), thegen-fencegrep inproto/moon.yml, and the three gen templates the new leaf is wired into —buf.gen.yaml(added toexclude_paths),buf.gen.internal-go.yaml(added topaths+M-maps), andbuf.gen.agent-ts.yaml(added topaths). Note a line-drift against #995’s cite: the gen-fence grep script sits atproto/moon.yml:151(taskgen-fenceopens at :121), not :141 as #995 recorded — confirmed by reading the live file this run. - The gen-fence extension fences the carrier family plus the internal-only
requests. Extend the grep with the unanchored
ForgeCall|RelayForgeCall| ForgeNotification|ForgeArtifactKindfamily exactly as #995 T1 specifies (design.md:1636-1639) — but the canonicalIssue/PullRequest/ChecksSummary/Check/AgentAttributionsymbols are PUBLICcompass.prototypes that must generate into the public trees and MUST NOT be added to the fence. Those four patterns are safe not because of word-bounding (word-bounding guards substring collisions likeSessionFramevsAgentSessionFrame,proto/moon.yml:138-150) but because none of them prefixes a public symbol —ForgeRef/ForgeProvidershare only theForgestem, no full-pattern match. The surviving internal-only requests are NOT matched by any of those four family patterns (CreateIssueRequest,CommentOnIssueRequest, theGet*/List*requests,ListIssuesResponse,SubscribeForge*/UnsubscribeForge*, andCommentRef); this gap existed in #995 too. Decided (A1): the fence IS extended with these word-boundable names (none collides with a public symbol), not left as an accepted gap —CommentRefin particular MUST be fenced, because DL-182 relocates it into the internal-onlyforge.protoleaf, so it is a now-internal symbol that would otherwise leak onto the public gen surface. - Cross-file import — already satisfiable, handled per-lane. Retyping the
result arms makes
agent_gateway.protoimportcompass/v1/compass.proto. This is not a new dependency:agent_gateway.protoalready transitively pullscompass.protovia itsagent.protoimport (agent.proto:25), and both internal gen lanes already handle it. Per lane: the TS lane (buf.gen.agent-ts.yaml→packages/compass-agent/src/gen) runs--include-imports, so the canonical types already regenerate into the agent gen tree (compass_pb.tsis present today); the Go lane (buf.gen.internal-go.yaml→go/internal/gen; #995 design.md:1506-1508) deliberately M-mapscompass.prototo the PUBLICgo/genpackage and MUST NOT gain--include-imports— its header warns that doing so triggers a duplicate-registration init panic. A1 confirms this and thatbuf.gen.yaml’sexclude_pathsstill excludes every internal file. The placement correction (DL-182) adds one more internal file to keep in that fence: the newproto/compass/v1/forge.protoleaf is added tobuf.gen.yaml’sexclude_paths, and to thepathsinputs of both internal lanes (buf.gen.internal-go.yamlwith itsM-maps →compassv1internal, andbuf.gen.agent-ts.yaml). forge.proto imports onlycompass.proto, so it introduces no new cross-package edge beyond the one described here. - Enum sentinel rule. Any surviving new enum (
ForgeArtifactKind,ForgeNotificationKind) keeps its_UNSPECIFIED = 0sentinel: theENUM_ZERO_VALUE_SUFFIXexemption coverscomms.protoonly (#995 design.md:1516-1520). - Design-PR gates. This amendment ships as a design PR: it MUST carry its
DECISIONS.md delta in the same PR (
tools/design-ledger-gate), aSpec-impact:line in the PR body or adocs/specs/touch (tools/spec-impact-gate), and pass rootmarkdownlint. The spawning design-owner applies the ledger rows proposed in §Ledger delta.
This amendment edits #995’s not-yet-executed task briefs, not shipped code. Each task below names the #995 task it amends and carries the exact symbol deltas. Execution order: A1 → A2 → A3 (A2/A3 consume A1’s regenerated types). OQ-A, OQ-B, and multi-forge addressing are ruled (§Resolved decisions); the task briefs below reflect those rulings.
A1 — Proto: amend the T1 add-list to the reconciled surface
Section titled “A1 — Proto: amend the T1 add-list to the reconciled surface”Amends #995 T1 (design.md:1621-1651). Placement corrected (Matt, 2026-08-05): #995 T1
and this record’s original draft directed ForgeNotification into
agent_gateway.proto while also adding AgentControl.forge_notification to
agent.proto. That is a circular import — agent_gateway.proto already
imports agent.proto (for AgentFrame/AgentControl), so a back-reference
from agent.proto to a type defined in agent_gateway.proto is a cycle buf
rejects (buf lint: compass/v1/agent.proto: detected cyclic import while importing "compass/v1/agent_gateway.proto"). Every other AgentControl
payload type is defined in agent.proto; forge is the first agent_gateway.proto
call family that also needs an AgentControl notification variant, which is why
the defect surfaced only here. The fix is a new internal-only leaf
proto/compass/v1/forge.proto that imports only compass/v1/compass.proto
and holds every type reachable from AgentControl.forge_notification
(ForgeNotification, ForgeNotificationKind, ForgeArtifactKind, and the
CommentRef reference); agent.proto, agent_gateway.proto, and runner.proto
all import the leaf without a cycle. The wire contract, field numbers, and
generated Go/TS names are identical to the shapes below — only the source file
each type lives in changes (DL-182).
Add to proto/compass/v1/forge.proto (new leaf): ForgeNotification /
ForgeNotificationKind, ForgeArtifactKind, and CommentRef. Add to
proto/compass/v1/agent_gateway.proto: the Forge RPC;
ForgeCallRequest/ForgeCallResult/ForgeCallError; the seven operation
requests and ListIssuesResponse; SubscribeForgeRequest/Response,
UnsubscribeForgeRequest/Response. Do NOT add the six domain
messages (Issue, IssueComment, PullRequest, ForgeAuthor,
ForgeChecksSummary, ForgeCheck) — instead import
compass/v1/compass.proto and retype the carrier arms:
Interfaces:
ForgeCallResult.resultoneof:compass.v1.Issue issue = 2(create_issue / get_issue),ListIssuesResponse issues = 4,compass.v1.PullRequest pull_request = 5(create_pull_request / get_pull_request); the two formerIssueCommentarms (issue_comment = 3,pr_comment = 6) take the ruledCommentRef(§Resolved decisions OQ-B; defined in theforge.protoleaf) — a write ack setsurl+comment_id.ListIssuesResponse { repeated compass.v1.Issue issues = 1; }ForgeNotification(inforge.proto):compass.v1.ChecksSummary checks = 9;comment = 8takes the same ruledCommentRef, withbody+forge_account+ (for a Compass-agent commenter)agentset. Itsforgefield is acompass.v1.ForgeRef(one forge-identity vocabulary, §item 13), not a barestring provider.runner.proto:rpc RelayForgeCall(RelayForgeCallRequest) returns (RelayForgeCallResponse)+SessionsResponse.forge_notification = 7;agent.proto:AgentControl.forge_notification = 9(its typeForgeNotificationresolved through theforge.protoimport) — field numbers verbatim from #995 T1 (design.md:1631-1635).gen-fencegrep (proto/moon.yml) extended with unanchoredForgeCall|RelayForgeCall|ForgeNotification|ForgeArtifactKindplus the word-boundable internal-only requests (CommentRef, the op requests,SubscribeForge*/UnsubscribeForge*); canonicalcompass.v1symbols NOT fenced. The newforge.protois wired into all three gen lanes: the publicbuf.gen.yamlexclude, thebuf.gen.internal-go.yamlinputs +M-maps (→compassv1internal), and thebuf.gen.agent-ts.yamlinputs.- Read ops survive (OQ-A ruled option 3):
GetIssueRequest,ListIssuesRequest,GetPullRequestRequest,ListIssuesResponse, and theget_issue/list_issues/get_pull_requestoneof members + theissuesresult arm all stay; the read path is answered from the projection/store (see A2).
Test cycle: unchanged from #995 T1 (design.md:1648-1651): direnv exec . moon run compass-proto:ci — lint, breaking (additive → clean), drift,
gen-fence (now also proving the canonical types did NOT get fenced and DID
generate into the internal lanes).
A2 — Go: the ingestion translation replaces proto-mirroring in the Service layer
Section titled “A2 — Go: the ingestion translation replaces proto-mirroring in the Service layer”Amends #995 T2a’s Service emission surface and T3’s ForgeCaller seam
(design.md:1653-1670, :1921-1923). The go/internal/forge value types
(Author, Issue, Comment, PullRequest, Checks, CreateIssue,
CreatePR, IssueFilter) and the Provider interface survive verbatim as
internal Go types. What changes: the Service layer’s wire-facing methods
return canonical types, and the forge→canonical mapping is one exported,
tested translation site — the ingestion translation #1018 mandates
(design.md:42-46, :371-384).
Interfaces:
package forge
// Translate is the single forge→canonical mapping site (DL-069): every field// of the internal value types lands in the canonical compass.v1 shapes here// and nowhere else. Owner-header parse results land in AgentAttribution;// verified is set by the forge-login cross-check (#995 OQ-1 / #1018 ruling).func TranslateIssue(in Issue, attr *compassv1.AgentAttribution) *compassv1.Issuefunc TranslatePullRequest(in PullRequest, attr *compassv1.AgentAttribution) *compassv1.PullRequestfunc TranslateChecks(in Checks) *compassv1.ChecksSummaryThe ForgeCaller seam (#995 :1921-1923) retypes accordingly, e.g.:
GetPullRequest(ctx context.Context, account store.AccountID, req *compassv1internal.GetPullRequestRequest) (*compassv1.PullRequest, error)(compassv1 = the public generated package for compass.proto;
compassv1internal keeps carrying the internal envelopes.) Subscribe/
Unsubscribe signatures are unchanged (scalar payloads).
Read-path population contract (OQ-A/F1). TranslateIssue above maps a
forge value type to a forge-SUBSET canonical Issue — it cannot populate the
server-side id or lifecycle machinery (state/priority/assignee/prs/tracker),
which have no source in a live forge read. Under OQ-A’s ruled option 3,
the read ops do NOT call TranslateIssue directly: the Service answers
get_issue/list_issues/get_pull_request from its projection/store (a
fully-populated canonical Issue, id set), composing a forge fetch +
TranslateIssue only for an artifact the store does not track (subset
Issue, id empty ⇒ not addressable by UpdateIssueState). That read path
takes the store handle, not just the forge value type. The population contract
is stated here, not left implicit.
Test cycle: T2a’s RED-first table tests extend with translation
round-trips: every internal value-type field asserted present in the
canonical output; owner-header parse → AgentAttribution including the
unverified default.
A3 — Agent + notification surfaces consume the canonical types
Section titled “A3 — Agent + notification surfaces consume the canonical types”Amends #995’s agent-side task (forge.ts tool family, design.md:2293-2349)
and the notification path (NotifyForgeAccount/DetectChanges,
design.md:2084-2089, :2254). No structural change: the same seams carry the
retyped payloads generated by A1.
Interfaces:
RunnerTransport.forge(req: ForgeCallRequest): Promise<ForgeCallResult>— unchanged signature; the result arms now deserialize to the generated canonicalIssue/PullRequestTS types (from the internal agent gen lane, which now emitscompass_pb.tsvia the A1 import).func (h *Hub) NotifyForgeAccount(ctx context.Context, account store.AccountID, n *compassv1internal.ForgeNotification) (delivered int, err error)— unchanged;ForgeNotification’s retyped arms ride through.func DetectChanges(prev Snapshot, now Snapshot) []*compassv1internal.ForgeNotification— unchanged signature; emits OQ-B comment refs andcompass.v1.ChecksSummary.
Test cycle: the #995 T9 E2E (design.md:2369-2372) runs unchanged as the
smoke: subscribe → fabricated upstream comment → notification reaches the
agent’s control lane → chat ping lands.
- A1 — amend T1: carrier family added, six domain messages NOT added,
result arms retyped to
compass.v1, gen-fence extended (carrier-only),compass-proto:cigreen. - A2 — amend T2a/T3:
TranslateIssue/TranslatePullRequest/TranslateChecksas the sole mapping site;ForgeCallerretyped; translation round-trip tests green. - A3 — amend agent/notification briefs: retyped payloads through unchanged seams; T9 E2E green.
- OQ-A (read ops answered from projection/store), OQ-B (single
CommentRefmirroringIssueauthorship), and multi-forge addressing (single-forge v1) ruled by Matt (2026-07-31); folded into the briefs above.
Ledger delta
Section titled “Ledger delta”Proposed DECISIONS.md rows — pure append under Comms & tools (the #995
forge rows’ home, DECISIONS.md:99-103). On current main (after #1035
renumbered the duplicate DL-071 to DL-091) the highest allocated id is DL-091
(DECISIONS.md:129), DL-082..089 are reserved for #1021 (RIG-1570, in flight),
and DL-056..066 is an older unexplained gap. The next free id above the
reserved band is DL-092, which this amendment proposes. (The single-writer
owns the final id; only the id cell changes.) The placement-correction row
DL-182 was allocated later, at correction time: after the design corpus
migrated into this repo (#179), it takes the next free id above the whole
reserved band (the compass-ux-foundation record #186 reserves DL-148..160), so
it clears every declared block — ledger ids are non-sequential by convention,
grouped by theme not issued in order.
| ID | Decision | Status | Record |
|---|---|---|---|
| DL-092 | #995’s forge-shaped proto domain family (Issue/IssueComment/PullRequest/ForgeAuthor/ForgeChecksSummary/ForgeCheck in agent_gateway.proto) is not built: per DL-069 the forge Provider layer’s value types stay internal Go data at the ingestion boundary, the ForgeCall* carrier family survives with its result arms retyped to the canonical compass.v1 types, and AgentAttribution supersedes ForgeAuthor (closing #995 OQ-1’s field-set gate) |
Active (Matt, 2026-07-31) | ownership amendment §What this amendment asserts |
| DL-182 | The forge notification/carrier types that AgentControl.forge_notification reaches (ForgeNotification/ForgeNotificationKind, ForgeArtifactKind, CommentRef) live in a new internal-only leaf proto/compass/v1/forge.proto (imports only compass.proto), NOT in agent_gateway.proto as #995 T1 / this record’s original §A1 add-list directed: that placement is a circular import (agent_gateway.proto already imports agent.proto, so agent.proto referencing an agent_gateway.proto type is a cycle buf rejects). The ForgeCall* carrier + op requests stay in agent_gateway.proto; the leaf is imported by agent.proto/agent_gateway.proto/runner.proto acyclically. Wire contract, field numbers, and generated names unchanged — placement only |
Active (Matt, 2026-08-05) | ownership amendment §A1 |
No existing #995 row flips: DL-048 (ownership layer), DL-049 (ForgeCall*
carrier on the gateway socket), DL-050 (owner header), DL-051 (go/internal/ forge provider), DL-052 (server-only credential), DL-053 (subscriptions),
DL-054 (notifications v1), and DL-055 (ownership index) all govern surfaces
this amendment keeps — none of them ledgered the domain-message shapes, which
lived only in #995’s Decision-5 proto block (design.md:356-431). Matt’s OQ-A
ruling keeps the read ops (answered from the projection/store), so DL-049’s
“sibling ForgeCall* family” wording stands unchanged and no narrowing row is
needed.
Resolved decisions (Matt, 2026-07-31)
Section titled “Resolved decisions (Matt, 2026-07-31)”OQ-A (RULED — option 3) — the Forge read ops survive, answered from the
projection/store. The read ops (get_issue/list_issues/get_pull_request)
stay on the AgentGateway lane (the egress-sealed agent holds no server token
and cannot subscribe to CompassService.SubscribeEvents; DL-076,
DECISIONS.md:109), and the server answers them from its projection/store —
returning a fully-populated canonical Issue/PullRequest (server-side id
set, lifecycle machinery present) for a tracked artifact, composing a live
forge fetch + TranslateIssue only for an artifact the store does not track
(subset shape, id empty ⇒ not addressable by UpdateIssueState). This
dissolves the critic-F1 population gap (the canonical Issue REQUIRES a
server-side id, #1018:170-173, plus machinery a live forge PROXY read cannot
populate — DL-055’s ownership index only covers Compass-authored artifacts),
changes NO wire surface (only the op’s server-side semantics), and spends no
forge rate budget on artifacts the server already tracks (a proxy
get_pull_request would otherwise spend several forge API calls per op filling
reviews/threads/checks, #1018 PullRequest :202-218, against the shared 5000/hr
budget). A2’s read path gains a store-join step (its signature takes the store
handle, not just the forge value type); the population contract is stated in
A2, not left implicit.
OQ-B (RULED — one CommentRef, authorship mirroring Issue) — the
IssueComment replacement. #995 used IssueComment in two carriers: the
ForgeCallResult arm for the two comment writes (#995 design.md:329, :332)
and ForgeNotification.comment = 8 (:1199-1200). Both take ONE message:
message CommentRef { string url = 1; // the forge comment permalink uint64 comment_id = 2; // the forge comment id string body = 3; // the comment text (unset on a write ack) string forge_account = 4; // the commenter's forge login; always set on a notification compass.v1.AgentAttribution agent = 5; // set only when the commenter is a Compass agent; unset for a human}The write-result arms set url + comment_id (a write tool needs only an
acknowledgement + link); the notification arm sets body + forge_account +
(when the commenter is a Compass agent) agent. This is deliberately the same
authorship pattern the canonical Issue already uses — forge_account always
set, agent unset for a non-Compass (human) author (#1018 Issue :183-186) —
so a comment’s authorship reads identically to an issue’s, one grain down. It
does NOT reintroduce the DL-069 wart: that wart is a parallel
Issue/PullRequest family (two messages for one board artifact forcing
double-maintenance), and CommentRef duplicates no canonical type — #1018’s
Comment (:277-281) exists only as a member INSIDE a PR review thread, there
is no standalone canonical issue-comment message for CommentRef to shadow. A
carrier that adds Compass attribution to a forge comment is the same shape the
canonical types themselves are (forge fields + AgentAttribution + Compass
machinery), not a second copy of an existing one.
Multi-forge request addressing (RULED — single-forge v1). The surviving
op requests and SubscribeForgeRequest address artifacts by (repo, number)
with no provider/host, while the canonical RESULT types carry ForgeRef
(DL-091: repo-only coordinates collide under multiple connected forges).
Request addressing stays single-forge for a GitHub-first v1 (repo unambiguous);
adding an optional ForgeRef/provider to the op requests and
SubscribeForgeRequest is a named additive follow-up, free to add later
without a buf-breaking change.
Assumption 1 (non-load-bearing) — ledger id. The dispatch brief named “next id DL-072+”, but DECISIONS.md already allocates through DL-091 (DL-072..081, DL-090, DL-091; #1035’s renumber took DL-091), so this record proposes DL-092 as described in §Ledger delta. The single-writer owns the final id; only the id cell changes.
Assumption 2 (non-load-bearing) — updated_at_unix_ms is dropped. #995’s
Issue.updated_at_unix_ms (design.md:371) and
PullRequest.updated_at_unix_ms (:416) have no counterpart on the canonical
types (#1018 design.md:169-218 carries no timestamps). Assumed intentional:
the projection’s stream ordering and SubscribeEventsResponse.at_unix_ms
supersede per-artifact freshness stamps. If an agent-side tool needs it,
it is an additive canonical field later — not a reason to keep a forge wire
type.
Assumption 3 (non-load-bearing) — gen-fence cite drift. #995 cites the
gen-fence grep at proto/moon.yml:141; the live file has the script at
proto/moon.yml:151 (read this run). Treated as ordinary line drift, not a
semantic change.