feat(webhooks): fire on proposal open/merge/reject (Phase 5a)
The firing half — proposal lifecycle events now deliver GraphQL-native
webhooks. Verified end to end against a live daemon: an agent REST
propose delivers a signed POST whose body is the subscription's stored
query executed against the ProposalEvent payload.
- service: an EventSink seam (service/events.go). Propose emits
PROPOSAL_OPENED for a new proposal, mergeProposal emits PROPOSAL_MERGED
(the single merge point — both auto-merge and the human approve reach
it), Reject emits PROPOSAL_REJECTED. Nil-safe; a Service with no sink
emits nothing.
- graph.NewProposalEvent builds the *model.ProposalEvent payload from a
service.Proposal (reusing the existing service→graph→model mapping).
- cmd webhookEventSink: proposal events happen in the service layer,
which has none of core-go's request context, so the sink enqueues a
dowork task onto the webhook queue. The task runs in the queue's worker
context (server+database+config, from WithQueues), adds the owner's
INTERNAL auth, and calls Schedule — which renders each subscriber's
query and delivers it Ed25519-signed. Fire-and-forget off the write
path: a webhook never blocks or fails a proposal write.
Phase 5a (webhooks) is complete: DB, the authn→AuthContext bridge, the
GraphQL surface, the core-go server wiring, and firing.
feat(service): write plane — Propose, Merge, ListProposals (Phase 3)
The service-layer orchestration for spec-zqb: the plane where the merge
model and the proposal state machine first run under real proposals
rather than on paper. Primitives (db proposal CRUD/merge, gitx
branch/commit/merge, authn provenance, core policy matcher) already
existed and were unit-tested; this composes them.
- Propose: the write plane, identical for REST and MCP. Row-first open
(branch name derives from the serial id), branch cut, and a
provenance-stamped commit — the agent authors, the owner commits, and
the X-Agent-Session / X-Agent-Base trailers carry the rest into a
plain git log. Adds to an existing proposal via ProposalID against its
fixed base. Frontmatter/schema/id validation at propose time, mirroring
the update hook the in-process agent write bypasses. Returns
{proposal, url}.
- Auto-merge policy: a proposal whose every changed path matches the
space's .spec.yml auto_merge lands immediately with ApprovalPolicy,
best-effort — a stale or mixed-path proposal falls back to human review
rather than failing the write.
- Merge / Reject: the state machine. Merge does the already-merged
ancestry check before gitx's If-Match staleness (design: the two need
different tests), the owner-signed merge commit, and the atomic row +
document-registry flip. ErrStale / ErrAlreadyMerged / ErrForbidden map
the 409/403 boundary.
- ListProposals / GetProposal / ProposalURL: the space-scoped read the
graph Proposals port declared and left nil, plus the stable
<origin>/~owner/space/p/<id> link.
- db.ListProposalsBySpace: the per-space, per-state listing.
Tested end to end against Postgres: open, auto-merge, mixed-path
fallback, stale-base 409, human merge, reject, add-to-existing, drifted
base. Surfaces (graph wiring, spec_propose, REST api/) fan out next.