Skip to content

docs(design): forge state-transition write op record (RIG-3331) - #981

Merged
trunk-io[bot] merged 3 commits into
mainfrom
compass-forge/rig-3331-state-transition-design
Sep 7, 2026
Merged

docs(design): forge state-transition write op record (RIG-3331)#981
trunk-io[bot] merged 3 commits into
mainfrom
compass-forge/rig-3331-state-transition-design

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What

Design record for the forge state-transition write op — an agent setting
an issue/PR's state on both GitHub and Linear, through the same attribution
chokepoint every other forge write rides. Today forgeService has no such
arm and forge.Provider has no such method, so every STATE event reaching
the notify router is human- or externally-actored.

This push freezes the record (Status: Active) and lands its ledger rows.

Both Open Questions ruled (Matt, 2026-09-07)

OQ-1 — actor carrier: the consumable memo. The acting agent's identity
travels in a durable, tenant-scoped forge_state_transitions memo, written
after a successful transition at the chokepoint and consumed on match at the
notify lane; the synthetic-event alternative is rejected as inventing an
event the forge never sent. Because RIG-3326's frozen text describes the
actor as stamped onto the emitted event, RIG-3326 is the side whose text
bends
: its STATE arm resolves the actor through a memo lookup at the
actor-resolution seam. Identical suppression outcome, different mechanism.
Consequence: T5 has a subject, T4's memo half is in scope, DL-343 carries it.

OQ-2 — Linear default state: default when unambiguous, reject when not.
With workflow_state empty, resolve the sole candidate of the target type
and use it; on two or more candidates fail invalid_argument naming them and
requiring an explicit workflow_state. Both originally-offered options were
rejected — a positional tie-break because it guesses silently and would start
guessing years later with no test watching, a mandatory workflow_state
because it forces a provider-aware state fetch into every callsite and splits
GitHub/Linear ergonomics.

OQ-2 was also ill-posed as filed, and saying so is part of the freeze. It
was surfaced as a product-behavior call about "silently choosing a
human-visible board column" — a premise never measured against the target
board. Measured at freeze, the Rigel team has eight workflow states of which
exactly one is completed ("Done"), one unstarted ("Todo") and one
backlog ("Backlog"), so there was no column choice to make. The ruling
converts OQ-2 from a product call into a fail-loud guard against future
drift; because no current board reproduces the ambiguous case, the rejection
arm's only coverage is a fixture (added to T3).

Dependency direction

RIG-3331 is the prerequisite; RIG-3326 (#913) is the consumer. #913's own
body says its STATE arm "stays interim-open until RIG-3331 lands" — a
dispatch-ordering dependency. An earlier sequencing of mine had this arrow
backwards and gated the prerequisite behind its own consumer; Matt corrected
it. Landing T5 is what closes #913's interim-open arm.

Plan

Nine unconditional slices (all contingency tags cleared by the rulings):
proto arms → Provider widening with all four implementors together (or the
package is red) → GitHub PATCH → Linear issueUpdate + workflow-state
resolution → server arms + memo → actor-resolution seam → tools → live-oracle
sweep → ledger.

Ledger impact

Ledger-impact: APPENDS DL-342 (the op: portable {open, closed} core +
per-provider refinements, fail-loud in-band on every mismatch including the
multi-candidate default, ErrUnsupported on the Linear PR half, and not
F3-deduped/DL-055-recorded — because a transition's coordinate row is a
write-once authorship fact whose client_request_id backs the create's F3
memo, so recording through that path would overwrite it) and DL-343 (the
memo mechanism and the RIG-3326 contract).

Ids re-verified next-free at freeze, against main and every open design
PR: main's highest is DL-337 (304 rows), #913 holds DL-338/339, #900 holds
DL-340, #932 holds DL-341. This check decays as main advances — it was re-run
here because main moved between the two pushes, and an earlier draft of the
numbering paragraph did collide (proposing DL-340, already claimed by #900).

Verification

moon ci green — 18 actions, 0 failed. design-ledger-gate OK (306 rows,
128 record headers valid); root:markdownlint 0 errors across 203 files.
Design-record-only change: no Go, proto or SQL touched.

Spec-impact: none

Refs RIG-3331

Design record for the forge state-transition write op: an agent sets an
issue/PR state on both GitHub and Linear through the same attribution
chokepoint every other forge write rides.

Portable {open, closed} core plus per-provider refinements (close_reason on
GitHub, workflow_state on Linear), fail-loud invalid_argument on a
refinement/provider mismatch, ErrUnsupported on the Linear PR half.
Transitions mutate an existing coordinate, so they are neither F3-deduped nor
DL-055-recorded, following the comment-arm precedent.

The load-bearing contract is actor attribution: the emitted STATE event must
carry the acting agent so RIG-3326's suppression arm can key on the real
transition actor rather than the DL-055 author-row proxy, which would wrongly
eat a human-close notification to the author.

Two load-bearing Open Questions are held for Matt: the actor-carrier mechanism
(OQ-1, a cross-record contract with RIG-3326) and the Linear default
workflow-state rule (OQ-2, a product-behavior call).

Refs RIG-3331

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@linear-code

linear-code Bot commented Sep 7, 2026

Copy link
Copy Markdown

RIG-3331

@trunk-io

trunk-io Bot commented Sep 7, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-forge-rig-3331-state.compass-eng-docs.pages.dev

Deployed from compass-forge/rig-3331-state-transition-design at 1d99dd8.

Changed pages:

…d (RIG-3331)

Adversarial review returned 4 high, 5 medium, 3 low. Fixes, each verified
against the cited source:

Two fabricated citations removed (rule://planning-evidence). The record named
translateAttribution as an existing symbol - it does not exist anywhere in the
tree; the real path is stripBodyToRef plus the detect-path attribution
assignment. It also quoted 'deliberately never imports the store' as if from
the ingest package; the actual term of art is the no-store rule. Both
underlying claims were true, only the citations invented.

The F3/DL-055 argument reached the right conclusion from the wrong premise.
The comment arm is excluded because a comment is unrepresentable in the store
index; a transition targets a coordinate that IS representable and often
already occupied. The record inverted the risk as 'no row exists at the
coordinate at all'. The real hazard is a row that DOES exist:
RecordAuthoredArtifact's ON CONFLICT DO UPDATE overwrites client_request_id,
which backs the F3 memo through a unique partial index, so routing a
transition through record would destroy the original create's idempotency
memo. Conclusion kept, reasoning replaced, hazard named.

T1 was red by construction: it widened Provider while extending only the fake,
but four compile-time satisfaction assertions live in that package, so the
GitHub and Linear ones break immediately and T2/T3 (the repair) come after.
T1 now lands the interface with all four implementors.

The transition memo omitted tenant_id. Its sibling forge_authored_artifacts is
tenant-keyed and defended by a two-tenants-same-coordinate test, so without it
one tenant's memo could attribute another's STATE event. The RLS catalog test
audits tables that carry the column, so an omission is invisible to it - the
column now ships with three explicit obligations instead of a reliance on the
guard.

Three tasks silently depended on unruled Open Questions while the checklist
presented nine unconditional slices. T3 is OQ-2-contingent, T4's memo half and
all of T5 are OQ-1-contingent, each now tagged with what changes under the
alternative ruling.

Also: ruled the ambiguous-workflow-state-name case (Linear does not enforce
name uniqueness within a team) rather than leaving an implementer to invent a
rule on the write path; specified the workflow-state cache separately from the
invalidation-free teamIDs cache, since humans rename and reorder states;
named the webhook/sweep double-emission and cursor-advance races; stated the
memo read's tenant context; pinned the table to 0001_init.sql per the
collapsed-migration convention; corrected the DL enumeration to include PR
#932's DL-341; resolved the T8 contradiction (the rows ship in the freeze
push, because DL-343's substance is what OQ-1 asks Matt to rule, and stamping
'Active (Matt)' now would attribute a decision he has not made); and flipped
Status to Draft.

Refs RIG-3331

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka marked this pull request as ready for review September 7, 2026 18:50
Both load-bearing Open Questions are ruled, so the record flips Draft ->
Active and its ledger rows land in this push.

- OQ-1: the actor carrier is the consumable `forge_state_transitions` memo,
  not a synthetic STATE event. RIG-3326 is the side whose text bends -- its
  STATE arm resolves the actor through a memo lookup rather than off the
  event body. T5 keeps its subject and T4's memo half is in scope.
- OQ-2: Linear default-state resolution defaults only where the target is
  unambiguous and fails `invalid_argument` naming the candidates otherwise,
  replacing the positional tie-break. A mandatory `workflow_state` was also
  rejected: it forces a provider-aware state fetch into every callsite and
  splits GitHub/Linear ergonomics. Measured at freeze, the Rigel team has
  exactly one `completed`, one `unstarted` and one `backlog` state, so the
  original "silently picks a board column" premise had no choice to make --
  the rejection arm is a guard against future drift, covered by fixture
  because no current board reproduces it.

All nine task slices are now unconditional; T3 gains the multi-candidate
rejection fixture. DL-342/DL-343 appended with the real ruling date, ids
re-verified next-free against main (highest DL-337) and every open design
PR (#913 338/339, #900 340, #932 341).

Refs RIG-3331

Co-authored-by: Matt Wilkinson <matt@rigel.build>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants