TDD: remote judgment — unblock gate parks from a phone (draft) - #241
TDD: remote judgment — unblock gate parks from a phone (draft)#241itsHabib wants to merge 1 commit into
Conversation
Design for hardening + extending the shipped escalate-serve ingress into a daily-use remote stamp: confirm modal with a serve-signed token, thread post-back with the pinned merge command, persist-before-ack durability, heartbeat so the card says when the tunnel is down, then written why, model-judge request, and batch — with batch gated on two open gate races. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review This is a design review, not a code review — the only file is
|
|
Claude finished @itsHabib's task in 2m 46s —— View job Design Review —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2be5e4f0be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| The card for `esc_y` is 40 min old; meanwhile the PR was re-gated and re-parked as | ||
| `esc_w`. Operator approves `esc_y`. Step 4: `gate next -json` no longer lists | ||
| `esc_y` → `ErrNotParked` → thread: "☑️ This park was superseded by esc_w — open | ||
| the newer card." No gate write. (Backstop if the inbox is stale: gate's own | ||
| `requireOpenEscalation` refuses under the lock.) |
There was a problem hiding this comment.
Block remote stamps on the subject-scoped gate races
When the same PR re-parks after serve's gate next -json lookup but before the old resolve completes, requireOpenEscalation checks only the old run, so the stale judgment can still land and its later act can hide the genuinely newer subject-level park. Therefore the claimed backstop here is not valid for the race documented in FOLLOWUPS (1)/(2), and restricting only P3 batch does not make the committed P0/P1 single-stamp path satisfy FR4; either close those races before rollout or explicitly weaken the safety claim and acceptance gate.
Useful? React with 👍 / 👎.
| Intent `accepted` is on disk. On restart, serve replays: same path as step 4. | ||
| If the first attempt had actually reached gate before the kill, replay gets | ||
| `judgment_duplicate` → intent `done`, thread gets "☑️ already resolved" (the | ||
| thread may have missed the original ✅ — acceptable, and it prints the command | ||
| again on a pass via `gate next -json`). |
There was a problem hiding this comment.
Preserve recovery context before invoking gate
If serve is killed after gate resolve records a pass but before Slack delivery or the done marker, replay cannot perform the behavior claimed here: the escalation has left gate next -json's parked list, while the persisted intent contains only the escalation id and no run id or merge command with which to join ready_to_merge[]. The replay therefore stops at ErrNotParked and can only report “already resolved,” losing the pinned merge command; persist the resolved run/result before invoking gate or provide a gate read seam that recovers them from the escalation.
Useful? React with 👍 / 👎.
| An accepted `view_submission` is appended to `<escalate-state>/intents.jsonl` | ||
| *before* the 200 ack; `process` marks it done after delivery; on start serve | ||
| replays undone intents (gate's guards make replay safe: a second apply is | ||
| `judgment_duplicate`). This is non-authoritative transport state — the | ||
| decision of record is still gate's log. Closes the open FOLLOWUPS item. |
There was a problem hiding this comment.
Make the intent append durable before acknowledging
An append that merely returns before the 200 response is not sufficient to close the referenced HARD-crash follow-up: its bytes may still be only in the OS page cache, and a power loss can also leave a torn final JSONL record. Specify an fsync/fdatasync durability boundary before ack plus defined torn-tail recovery; otherwise an acknowledged decision can still disappear despite the stated persist-before-ack guarantee.
Useful? React with 👍 / 👎.
| ### 4.3 Confirm modal with a serve-signed, short-lived, user-bound token — **decided; reviewers weigh the binding** | ||
|
|
||
| | | | | ||
| |---|---| | ||
| | Choice | Tap → `views.open` confirm modal. `private_metadata` = `base64(json{esc, decision, uid, channel, ts, grant, exp}) . hmac_sha256(serve_key, …)`; `exp` = now+10 min. On `view_submission`: Slack sig → token sig + exp → `uid == payload.user.id` → allowlist → proceed. | |
There was a problem hiding this comment.
Retain a way to update the source card after modal submit
A Slack view_submission does not carry the original button interaction's response_url, and the proposed signed metadata contains only esc, decision, user, channel, timestamp, grant, and expiry. Consequently the later response_url replace_original shown in §3/§7.1 cannot be performed after the modal is submitted, leaving the original buttons and status intact; either carry the response URL in the signed token/intent or use chat.update with the already-bound channel and message timestamp.
Useful? React with 👍 / 👎.
| - **Q3 — May the phone trigger a model judgment?** `gate judge -auto` writes a | ||
| real one-shot judgment. Remote-triggering it is still "replaying an operator | ||
| decision" (the operator decided to ask the judge) but the *verdict* is the | ||
| model's. Options: (a) allow, it's the same command the operator would type; | ||
| (b) advisory-only — needs a dry mode gate doesn't have; (c) drop P2b. |
There was a problem hiding this comment.
Resolve the model-judge authority contradiction before P2b
gate judge -auto immediately writes the escalation's one-shot judgment and may emit the would_merge action; there is no subsequent gate resolve or human pass/block stamp. Thus option (a) cannot also satisfy FR6's promise that the model verdict “still goes through the human stamp” or the document's claim that the phone only transports a human decision. P2b must use an advisory-only mode followed by an explicit confirmed human resolve, be dropped, or explicitly redefine the authority model rather than treating the Ask tap as equivalent.
Useful? React with 👍 / 👎.
|
Scope note from the PR-sweep session (engineering owner): most of phase 1 already exists in production on this machine — |
|
Live failure from the first real remote approval — design input for this TDD. Tonight the operator approved a park from Slack (workbench#242, run The run had been opened under a T1 grant while the change (it edits gate itself) reduces to T3 — even though a live operator-minted T3 grant for the same repo existed the whole time ( Three requirements this implies:
Also worth covering: the card should surface a terminal outcome back to the thread ("merged |

Summary
Technical Design Document at
docs/features/remote-judgment/spec.md. Design review, not code review — nothing executable changes here.The headline finding while grounding this: the Slack-button → ngrok →
escalate serve→gate resolvepath already ships (#130/#140/#150, evidence files underdocs/features/escalation-plane/). So this TDD is a gap-close + extension on one binary, not a new component — and it deliberately avoids the word "executor" (taken by thegate executorGitHub-App merge bridge).Decisions that need a call (§4, §10)
private_metadata— is the binding(esc, decision, uid, channel, ts, grant, exp)enough, and is the layer order (Slack sig → token → allowlist → grant) right?container.message_tsoff the interaction payload so flare stores nothing new — does Amendment 3 survive cleanly?actnot linearized). Agree a batch over that is unsafe?cmdJudge'srequireOpenEscalationin P0 (the "second writer" revisit trigger is met by intent replay)?gate judge -autoat all (P2b)?Rollout (§9)
Commitment boundary is P0 (no-regret hardening) + P1 (confirm modal) → validation gate (§11: ≥10 real phone stamps, zero double-apply, zero off-grant, one induced outage, one induced SIGKILL). P2a/P2b/P3/P4 are dossier stubs until the gate passes.
Dossier: project
workbench, phasesremote-judgment-p0…remote-judgment-p4; tasks materialized for P0 + P1 only.🤖 Generated with Claude Code