Skip to content

Gate approvals advance builders silently: notify the spawning architect from the porch approve path #1194

Description

@amrmelsayed

Problem

The sidebar's Approve Gate button (and the gate toast, palette, and bare porch approve in a terminal) writes porch state directly. The spawning architect is never told, so its mental model of the builder goes stale: builders advance phases without the architect's knowledge, and the architect's review at the next gate starts from confusion instead of context.

All approval surfaces funnel through one place today: runPorchApprove in apps/vscode/src/commands/approve.ts shells out to porch approve <id> <gate> --a-human-explicitly-approved-this, and terminal users invoke the same CLI.

Design (v2 — post team review, decision by Amr 2026-07-18)

Treat this as an information problem, not an authority problem. Gates stay human gates; the architect is never inserted into the execution path. Emit a notification from inside porch approve immediately after the gate write succeeds, so every surface (sidebar, toast, palette, CLI, future mobile) inherits it with zero per-surface work.

Settled scope

  1. All four gates (spec-approval, plan-approval, dev-approval, pr), uniformly. No per-gate policy. Self-adjusting: in the architect-driven path, pre-approved spec/plan frontmatter makes porch skip those phases, so notifications only fire on paths where the architect genuinely didn't know. Already-approved gates return early, so no double-fire.

  2. Typed gate-event frame ships in v1 (decision: Amr). The notification carries machine-parseable metadata alongside the human-readable banner, typed in @cluesmith/codev-types (wire contract only). This is the first system-typed bus frame and the seed of the structured activity events web + mobile need. Draft shape (field set open for team feedback, see below):

    interface GateEventMeta {
      kind: 'gate-event';
      gate: 'spec-approval' | 'plan-approval' | 'dev-approval' | 'pr';
      action: 'approved';
      projectId: string;          // "1191"
      builderId: string;          // "builder-pir-1191"
      workspacePath: string;
      source?: 'sidebar' | 'toast' | 'cli';
      routedTo: string;           // architect name the event was delivered to
      fallback?: boolean;         // true when rerouted to main
      artifactPath?: string;      // e.g. "codev/plans/1191-....md"
      timestamp: string;
    }
  3. Transport: HTTP via the existing shared clienttower-client.sendMessagePOST /api/send (packages/core/src/tower-client.ts), extended to carry the meta field. Forced by the typed frame: afx send --raw is plain PTY text and cannot carry metadata. This also sidesteps the one confirmed subprocess failure mode: afx send sets from = detectCurrentBuilderId() ?? 'architect' (send.ts:293), so a fallback-to-main notification sent from a builder-worktree cwd would be builder-classified and rejected by the spoofing check (verified; see comment thread). The HTTP call uses an explicit system/porch sender type, so the builder spoofing check is never implicated and consumers can key behavior off sender type rather than parsing a banner.

  4. Routing: the actual spawning architect, not main. spawned_by_architect from global.db (the Multi-architect routing: builder→architect messages misrouted to 'main' when worktree has its own state.db #774 affinity data), read-only lookup. Fallback to main only when the column is NULL (shrinking legacy concern) or the named architect is no longer registered — stated in the message body AND logged, never silently dropped. Drop-with-log only when no architect is registered at all.

  5. Strictly best-effort: short timeout; Tower down or send failure prints a non-fatal note and never blocks or fails the approval.

  6. Human-readable banner stays one glanceable block, e.g.:

    ### [PORCH GATE EVENT | builder-pir-1191] ###
    plan-approval APPROVED by human (sidebar). Builder advancing to implement.
    Artifact: codev/plans/1191-recently-closed-sort.md
    ###
    
  7. porch/notify.ts doc-comment MUST be rewritten in the same PR. It currently documents the deliberate removal of architect-bound gate notifications ("architect can't act on a gate, therefore noise"). This change reverses that decision with a new rationale (awareness for the next gate, not actionability); the comment must encode the reversal or a future maintainer re-removes the feature citing the old text.

  8. Independence from the builder wake-up: the existing builder wake-up in approve() is suppressed when approve runs from inside the builder worktree (calledFromBuilderWorktree guard). The architect notification targets a different terminal and must fire regardless — the two calls stay independent.

  9. Architect-side convention (role-doc addition): fire-and-forget awareness, not a request. Read it, update your notebook and expectations, do not reply, do not touch porch state. Post-hoc concerns about the approved artifact go to the human or the issue. One rule across all four gates, keyed off the system sender type.

Explicitly out of scope (deferred, unanimous)

  • Gate rejections: no funnel exists (rejections never call porch approve; the human types feedback straight to the builder). Separate follow-up issue to be filed so the gap stays visible.
  • "Request architect review" companion affordance: a different, interactive feature with exactly the latency/dead-terminal coupling this proposal avoids. Demand-gated follow-up.

Alternative considered and rejected: mediated approval

Routing the button through the spawning architect (button sends a request; architect runs porch approve) was rejected: approval latency tied to architect responsiveness, stalls on dead/busy architect terminals, a new dropped-message failure mode, no path for CLI-only workspaces, and it converts a decision the human already made into something an AI can second-guess.

Remaining questions for team feedback (before spawning)

  1. Frame schema: is the GateEventMeta field set above right? Missing fields (e.g. phase the builder advances to, issue number, protocol)? Should action anticipate values beyond 'approved' now, or stay minimal until a reject verb exists?
  2. Envelope placement: does meta belong on the MessageFrame itself (every message can optionally carry typed metadata — the general bus pattern) or as a gate-event-specific field on the /api/send body? MessageFrame-level seems right for the activity-feed trajectory, but it touches the shared type.
  3. Sender type naming: 'system' vs 'porch' as the sender type for spoofing-exemption and role-doc keying. Precedent-setting for future system emitters.
  4. --source attribution: worth plumbing a --source sidebar|toast|cli flag through runPorchApprove, or derive nothing and drop the field from v1?

Touch points

  • packages/codev/src/commands/porch/ (approve path): emit after successful gate write; rewrite notify.ts header comment
  • packages/types: GateEventMeta (wire contract)
  • packages/core/src/tower-client.ts + Tower /api/send route: carry + route meta, system sender type
  • Architect role doc: the no-reply handling convention
  • apps/vscode/src/commands/approve.ts: optional --source flag only; no behavior change

History

Original body proposed the same funnel-at-porch approve approach with transport and scope as open questions; see the comment thread for the three architect reviews (mobile, demos, reviewer), the synthesis, and the verified send.ts:293 sender-classification correction that settled transport.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cross-cuttingTouches multiple areas — needs coordinated handling

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions