Skip to content
Merged
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: 1 addition & 1 deletion docs/designs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ check enforces the mechanical half. Full rationale:
| DL-210 | Forge integration testing adds two live-contract tiers above the DL-174 hermetic pyramid: (1) a hermetic golden-fixture replay leg (committed `go/internal/forge/testdata/` fixtures replayed through the stub RoundTripper, an untagged Go test riding moon affected-detection on every forge-affected PR + the main/nightly sweep, no `ci.yml` project enumeration) and (2) a `//go:build livegithub` live-credentials oracle (same scenarios against a throwaway `RigelBuild/compass-forge-testbed` + a Linear test team under test-only bot PATs ≠ the `server_only` production secrets), REQUIRED on forge-affected PRs + nightly, covering GitHub and Linear co-equally (the GitHub-only F1 reviewer≠author 422→APPROVE headline; Linear's PR/review family is `ErrUnsupported`, not a scenario). This is a NEW secret-bearing per-PR CI step — a standalone decision, NOT an amendment: D2's full-stack deterministic tier (`compass-dogfood-e2e/design.md:815-830`) stays secret-free and unchanged; a forward pointer next to D2 records the carve-out. Fork-PR secret safety: the same-repo-head-guarded step goes vacuously green on fork heads (GitHub withholds secrets there); accepted, with the push-to-main run + a maintainer same-repo re-push convention as the catch (0 forks today) | Active (Matt, 2026-08-18) | [forge integration testing §Approach](server/compass-forge-integration-testing/design.md#approach) |
| DL-226 | A message owed to an OFFLINE channel agent member is never silently stranded: the server wakes the member by RESUMING its most recent session (a system-authorized internal sibling of the public resume path, via a new `LatestSessionForAccount` read; fresh start only for a never-started agent; per-agent singleflight + not-live pre-check as cost control — agent-triggered starts are a core product capability, and `StartAgentSession`'s adminOnly PUBLIC door is untouched). Durability split: a mentioned member outside the sweep set (unsubscribed, non-home, non-mandatory) gets a durable owed-mention row `(agent_account_id, message_id)` written at the settle edge before the wake and swept subscription-independently on session start AS A STEER (D5 mention→steer preserved; steer-only precedence intact), cleared on the frozen message_id ack; a subscribed/home/mandatory member gets no owed row — the D2 cursor sweep is its durable backstop and the wake (both the mention arm and the plain-deliver arm) is pure latency. Broadcast mentions (`@everyone`/`@agents`) wake too; N-start amplification accepted. The residual pre-settle window is accepted for MVP (RIG-2490). Amends D5/OQ-3's offline clause by citation | Active (Matt, 2026-08-21); residual pre-settle window closed by DL-240 (RIG-2490, 2026-08-23) | [mention offline redelivery §Decisions](server/compass-mention-offline-redelivery.md#decisions-ruled-by-matt-2026-08-21) |
| DL-240 | The RIG-1641 OQ-5 residual pre-settle mention-loss window (a mention to an offline out-of-sweep-set member, lost when a crash or bus overrun drops `MessagePosted` before the settle-edge `routeMentions` pass) is closed by a per-message delivery marker, NOT a high-water cursor: a nullable `messages.mentions_routed_at BIGINT` (unix ms) set by the consumer after the settle-edge pass, with a recovery scan at consumer start + the `Lagged()` overrun re-subscribe reading `WHERE mentions_routed_at IS NULL` (skipping message_ids in `c.held`), replaying the ONE shared `routeMentions` body, then marking. The killed cursor could advance past a LOWER in-flight seq (`messages.seq` is `BIGSERIAL` assigned at INSERT but the bus event publishes post-commit, `comms.go:361-375`) whose crash-dropped event then stranded the mention forever — a new silent loss the marker cannot express. Seed-forward on first deploy (no historical backfill; compass pre-live). Extends DL-226's no-loss invariant from the settle edge back to the message-post commit; DL-226's wake+owed-mention mechanism is unchanged | Active (Matt, 2026-08-23) | [mention offline pre-settle closure §Resolved decisions](server/compass-mention-offline-redelivery-pre-settle-closure.md#resolved-decisions) |
| DL-241 | The agent forge native toolset is ten single-purpose tools, one per `ForgeCallRequest` arm (`forge_get_issue`, `forge_get_pull_request`, `forge_list_issues`, `forge_comment_on_issue`, `forge_comment_on_pull_request`, `forge_submit_review`, `forge_create_issue`, `forge_create_pull_request`, `forge_subscribe`, `forge_unsubscribe`), each a native `AgentTool` over a thin `ForgeBroker` on the `RunnerTransport.forge()` seam; `forge_subscribe`/`forge_unsubscribe` ship the complete surface now but return the server's in-band `unimplemented` until the poll-driver lane lands the `agent_forge_subscriptions` writer (DL-163). Multi-forge is exposed: every tool takes an optional forge selector (`forge_provider` + optional `forge_host`, unset = the configured default GitHub forge, DL-202) so an agent targets Linear (issues-only, `repo` = team key, DL-051; PR/review arms return in-band `unimplemented`) as well as GitHub. Mirrors DL-212's comms-toolset count row; the tool-count claim is load-bearing for future toolset-refresh rows | Active (Matt, 2026-08-24) | [forge tools §Tool set and shape](agent/compass-agent-forge-tools/design.md#tool-set-and-shape--ten-native-tools-one-per-arm) |
| DL-241 | The agent forge native toolset is twelve single-purpose tools, one per `ForgeCallRequest` arm (`forge_get_issue`, `forge_get_pull_request`, `forge_list_issues`, `forge_comment_on_issue`, `forge_comment_on_pull_request`, `forge_submit_review`, `forge_create_issue`, `forge_create_pull_request`, `forge_transition_issue_state`, `forge_transition_pull_request_state`, `forge_subscribe`, `forge_unsubscribe`), each a native `AgentTool` over a thin `ForgeBroker` on the `RunnerTransport.forge()` seam. Multi-forge is exposed: every tool takes an optional forge selector (`forge_provider` + optional `forge_host`, unset = the configured default GitHub forge, DL-202) so an agent targets Linear (issues-only, `repo` = team key, DL-051; PR/review arms return in-band `unimplemented`) as well as GitHub. Mirrors DL-212's comms-toolset count row; the tool-count claim is load-bearing for future toolset-refresh rows. Amended 2026-09-13: the count was ten at ratification; the transition pair arrived with DL-342 (RIG-3331) and the original row was not refreshed. The ratified caveat that `forge_subscribe`/`forge_unsubscribe` return in-band `unimplemented` until the DL-163 writer lands is also retired — both arms persist (`EnsureAgentForgeSubscription` / `DeleteAgentForgeSubscription`) and `forge_subscribe` returns a real subscription id | Active (Matt, 2026-08-24; amended Matt, 2026-09-13) | [forge tools §Tool set and shape](agent/compass-agent-forge-tools/design.md#tool-set-and-shape--twelve-native-tools-one-per-arm) |
| DL-291 | Agents and client UIs address channels and topics by NAME, not id — request-input channel/topic fields on the agent tool surface are name-typed, resolved viewer-scoped at the service edge (`ChannelByNameForViewer`: miss ≡ invisible ≡ merged `not_found`; a multi-hit errors `invalid_argument`, never auto-picks); response/stored/event fields stay id-typed. Extends DL-269/DL-270 from accounts to channels + topics. | Active (Matt, 2026-08-30) | [peer-DM §Ledger delta](agent/compass-agent-peer-dm/design.md#ledger-delta) |
| DL-292 | Steer/deliver control ops denormalize the source channel name + topic name (server-resolved at wrap in `deliverOp`/`steerOp`, the `from_handle` pattern; a name-resolve miss degrades like a handle miss, never blocks delivery); the agent renders `Channel <name> › topic <name>:` and must name both on every post — `comms_post_message` has NO home-channel default and never auto-picks a reply target. The dogfood reply-mis-routing fix (RIG-2956 T0). Proto: `DeliverControl.channel_name = 5`, `SteerControl.topic_name = 4`/`channel_name = 5`. | Active (Matt, 2026-08-30) | [peer-DM §Ledger delta](agent/compass-agent-peer-dm/design.md#ledger-delta) |
| DL-293 | Creating a topic requires `create_topic: true` on the post, in every channel including DMs (`PostMessageRequest.create_topic = 6`; `TopicRef.Create` gates the store name path) — amends DL-098's topic get-or-create clause (the Zulip threading model stays live; only auto-mint-on-miss is gated). A gated miss errors in-band `not_found`, never mints and never drops the message. Escape hatch (Matt): may revert for DMs only if it proves a blocker. | Active (Matt, 2026-08-30) | [peer-DM §Ledger delta](agent/compass-agent-peer-dm/design.md#ledger-delta) |
Expand Down
62 changes: 33 additions & 29 deletions docs/designs/agent/compass-agent-forge-tools/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Status: Active

Design for the TypeScript agent-side leg of the forge surface: a `forge()`
method on the frozen `RunnerTransport` seam plus a native tool set (`forge.ts`)
that exposes all ten `ForgeCallRequest` arms to the containerized first-party
that exposes all twelve `ForgeCallRequest` arms to the containerized first-party
agent, one tool per arm (the two subscription arms ship now but return the
server's in-band `unimplemented` until the `agent_forge_subscriptions` store
writer lands), with a
Expand Down Expand Up @@ -49,7 +49,7 @@ But the agent cannot reach any of it: `RunnerTransport`
(`packages/compass-agent/src/transport/index.ts`) exposes `comms()` and
`lifecycle()` and no `forge()`, and no `forge.ts` exists (comms ships five
tools, lifecycle two, forge zero). This record designs the missing TS leg —
the transport method, the `ForgeBroker`, the ten native tools (the two
the transport method, the `ForgeBroker`, the twelve native tools (the two
subscription tools built now over the stub arms) with their multi-forge
selector and per-tool prompt guidance, and the `cli.ts` wiring — and nothing
else.
Expand All @@ -58,7 +58,7 @@ else.

### Decisions (pre-resolved by Matt — not reopened below)

1. **Full surface: all ten `ForgeCallRequest` arms become tools.** Every arm
1. **Full surface: all twelve `ForgeCallRequest` arms become tools.** Every arm
the wire carries ships as an agent tool in this record — the eight arms
`ExecuteForgeCallAsAccount` dispatches for real PLUS `subscribe`/
`unsubscribe`. No partial MVP subset, no hidden arms.
Expand All @@ -67,7 +67,9 @@ else.
precedent. Names: `forge_get_issue`, `forge_get_pull_request`,
`forge_comment_on_issue`, `forge_comment_on_pull_request`,
`forge_submit_review`, `forge_create_issue`, `forge_create_pull_request`,
`forge_list_issues`, `forge_subscribe`, `forge_unsubscribe`.
`forge_list_issues`, `forge_transition_issue_state`,
`forge_transition_pull_request_state`, `forge_subscribe`,
`forge_unsubscribe`.
3. **`forge_subscribe`/`forge_unsubscribe` ship NOW, returning the server's
in-band `unimplemented` until the writer lands.** The backend arms are
`CodeUnimplemented` stubs (`go/server/forge.go`) pending the
Expand All @@ -79,6 +81,12 @@ else.
`unimplemented`, with the per-tool guidance saying "not yet wired") until
the writer lands, at which point they light up with no contract rework and
no re-registration.
**Landed since (2026-09-13): the writer is in, so this caveat is spent.**
`subscribeForge` calls `store.EnsureAgentForgeSubscription` and returns a
real subscription id; `unsubscribeForge` calls
`store.DeleteAgentForgeSubscription` (both in `go/server/forge.go`). The
two arms are no longer `CodeUnimplemented`, and the per-tool "not yet
wired" guidance no longer describes them.
4. **V1 is multi-forge: the `ForgeRef` selector is exposed on every tool.**
The wave needs GitHub AND Linear (it files Linear issues and opens GitHub
PRs), and the substrate resolves a per-provider coordinate end to end
Expand Down Expand Up @@ -118,7 +126,7 @@ else.
- **The proto carrier and its generated TS types exist.**
`proto/compass/v1/agent_gateway.proto` `rpc Forge(ForgeCallRequest) returns
(ForgeCallResult)` on `AgentGateway`; `ForgeCallRequest` carries `call_id`,
the ten-arm `call` oneof (arms 2–11), an optional `ForgeRef forge` (unset =
the twelve-arm `call` oneof (arms 2–11 plus 14–15), an optional `ForgeRef forge` (unset =
configured default GitHub forge, DL-202), and `client_request_id` (create
arms only, DL-206). `ForgeCallResult` retypes the domain arms to canonical
`compass.v1.Issue`/`PullRequest` (DL-069/DL-092) plus `CommentRef`/`ReviewRef`
Expand Down Expand Up @@ -162,9 +170,9 @@ else.
customTools→state.tools→`#withNatives` path. Forge adds one broker and one
spread to that list.

### Tool set and shape — ten native tools, one per arm
### Tool set and shape — twelve native tools, one per arm

All ten tools live in one new `packages/compass-agent/src/forge.ts`,
All twelve tools live in one new `packages/compass-agent/src/forge.ts`,
authored as OMP `AgentTool`s with arktype parameters, closing over a
`ForgeBroker`. Approval levels follow the comms precedent (`comms.ts`:
mutations `approval: "write"`, reads `approval: "read"`; the container runs
Expand Down Expand Up @@ -509,17 +517,14 @@ returns its result; `idempotencyKey` is nonce-prefixed, stable per broker,
distinct across brokers. `direnv exec . moon run compass-agent:test` red
first, then green; biome clean.

### T2 — Agent: `createForgeTools` (ten tools) + `cli.ts` wiring + prompt guidance
### T2 — Agent: `createForgeTools` (twelve tools) + `cli.ts` wiring + prompt guidance

In `packages/compass-agent/src/forge.ts` (same file, tool half):

```ts
/**
* The native forge tool set. Ten tools, one per `ForgeCallRequest` arm. Two
* of them — `forge_subscribe`/`forge_unsubscribe` — return the server's
* in-band `unimplemented` until the poll-driver lane lands the
* `agent_forge_subscriptions` writer; they ship now so the surface is stable
* (Matt's build-all ruling). Every tool takes an optional forge selector
* The native forge tool set. Twelve tools, one per `ForgeCallRequest` arm.
* Every tool takes an optional forge selector
* (`provider` + `host`) so a call targets Linear as well as the default
* GitHub forge. Wired into the container entrypoint by `cli.ts main()`:
* merged into the session's `customTools` and registered as `#withNatives`
Expand All @@ -546,7 +551,7 @@ const forgeSelector = type({
});
// EVERY tool schema below also spreads `...forgeSelector` (elided in each body
// for brevity — the eight non-subscription sketches show only their arm-specific
// fields). An implementer adds `...forgeSelector,` to all ten object literals.
// fields). An implementer adds `...forgeSelector,` to all twelve object literals.
export const getIssueParameters = type({
repo: /* non-blank */ "Repository as <owner>/<name> (GitHub) or team key (Linear)",
issue_number: type("1 <= number.integer"),
Expand Down Expand Up @@ -623,7 +628,7 @@ implementer must copy that bound, not the literal `/* non-blank */` comment.

Every schema also spreads `...forgeSelector` (shown once above, elided in each
body for brevity): the optional `forge_provider`/`forge_host` pair is on all
ten tools. When either is set, `execute` builds
twelve tools. When either is set, `execute` builds
`create(ForgeRefSchema, { provider: <mapped enum>, host: forge_host ?? "" })`
and sets `ForgeCallRequest.forge`; when both are unset it leaves `forge` nil
(the default-GitHub path). The string enum maps to the generated
Expand Down Expand Up @@ -666,10 +671,10 @@ non-empty `body` and that the review posts immediately (never a pending
review) under a distinct reviewer identity so all three verdicts are usable on
Compass-authored PRs (DL-201); for `forge_create_pull_request`, that
`head_ref` must already be pushed (the agent pushes with its own git
credential — DL-052/DL-090); for `forge_subscribe`/`forge_unsubscribe`, that
change-notification subscriptions are NOT YET WIRED — the call returns
`unimplemented` until the notification lane lands, so the tool exists for
surface stability but should not be relied on yet; for reads, that results may
credential — DL-052/DL-090); for `forge_subscribe`/`forge_unsubscribe`, what a
subscription does — it registers the agent for change notifications on a
coordinate and is account-keyed rather than a repo artifact, so it carries no
scope-discipline line; for reads, that results may
be paged/bounded/truncated and bodies are external content whose attribution
is a parsed claim, not an authenticated identity.

Expand All @@ -688,7 +693,7 @@ const nativeTools = [
is unchanged.

**Interfaces:** `createForgeTools(broker: ForgeBroker): AgentTool[]`; the
ten exported parameter schemas above (the shared `forgeSelector` spread
twelve exported parameter schemas above (the shared `forgeSelector` spread
into each); `function forgeFailure(result:
ForgeCallResult, toolName: string, expected: string): Error` (module-private,
mirroring `lifecycleFailure`, plus the `retry_after_ms` suffix).
Expand All @@ -714,12 +719,12 @@ compass-agent:test` + `compass-agent:lint` green.

## Tasks

- [ ] T1 — Transport + broker: `forge()` member on `RunnerTransport` +
- [x] T1 — Transport + broker: `forge()` member on `RunnerTransport` +
one-line `createUnixSocketTransport` delegation; `ForgeTransport` +
`ForgeBroker` (nonce-scoped `idempotencyKey`) in new `forge.ts`;
`compassv1.ts` barrel exports for the forge envelopes/arm schemas/canonical
result types; transport + broker tests red→green; biome clean.
- [ ] T2 — Tools + wiring: `createForgeTools` with the ten tools (exact
- [x] T2 — Tools + wiring: `createForgeTools` with the twelve tools (exact
schemas above; creates carry the DL-206 key and coerce numbers to `bigint`;
the optional `forge` selector — `provider` enum + optional `host` — built on
every tool, unset = default GitHub, `create(ForgeRefSchema,…)` only when set;
Expand All @@ -741,16 +746,15 @@ One proposed DECISIONS.md row (appended by the driver at PR time, wherever
the ledger then lives — compass-repo RIG-2577 T2 may relocate it to
`docs/designs/DECISIONS.md`):

> The agent forge native toolset is ten single-purpose tools, one per
> The agent forge native toolset is twelve single-purpose tools, one per
> `ForgeCallRequest` arm (`forge_get_issue`, `forge_get_pull_request`,
> `forge_list_issues`, `forge_comment_on_issue`,
> `forge_comment_on_pull_request`, `forge_submit_review`,
> `forge_create_issue`, `forge_create_pull_request`, `forge_subscribe`,
> `forge_unsubscribe`), each a native `AgentTool` over a thin `ForgeBroker`
> on the `RunnerTransport.forge()` seam; `forge_subscribe`/`forge_unsubscribe`
> ship the complete surface but return the server's in-band `unimplemented`
> until the poll-driver lane lands the `agent_forge_subscriptions` writer
> (DL-163). Multi-forge is exposed: every tool takes an optional forge
> `forge_create_issue`, `forge_create_pull_request`,
> `forge_transition_issue_state`, `forge_transition_pull_request_state`,
> `forge_subscribe`, `forge_unsubscribe`), each a native `AgentTool` over a
> thin `ForgeBroker` on the `RunnerTransport.forge()` seam. Multi-forge is
> exposed: every tool takes an optional forge
> selector (`provider` + optional `host`, unset = the configured default
> GitHub forge, DL-202) so an agent targets Linear (issues-only, `repo` = team
> key, DL-051) as well as GitHub; PR/review arms on a non-GitHub provider
Expand Down
Loading
Loading