Skip to content

feat: refusal as a first-class terminal outcome - #13

Merged
hmanoor merged 5 commits into
mainfrom
feat/refusal-first-class
Jul 31, 2026
Merged

feat: refusal as a first-class terminal outcome#13
hmanoor merged 5 commits into
mainfrom
feat/refusal-first-class

Conversation

@hmanoor

@hmanoor hmanoor commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Backports the refusal work from the desktop build. Two defects with one root cause: a structured refusal was computed and then thrown away.

1. Structure lost at the boundary

The acceptance gate already produces the right objects — {check, expected, message, severity} — and the escalation path joined them into an English sentence, so the UI and the next planner turn got prose they couldn't act on.

2. No terminal state, hence a re-raise loop

Escalations surfaced as ordinary phase gates with Approve/Deny. "Approve" cannot satisfy "this needs a human to fix it", so the gate re-evaluated, refused again and re-raised — observed ~70 times against an auto-approving harness.

What this adds

src/shared/refusal.ts:

  • retryable typed as the literal falsenothing can construct a retryable refusal, so it can't be fed back into a retry loop by accident
  • a deterministic intentId (project:phase:reason), which makes a repeat refusal recognisable as the same closed intent
  • details keeps the violation objects intact
  • bestAttempt preserves the least-broken artifact across the refusal
  • premiseForRetry names what would have to change for another attempt to be meaningful — so a retry with no new information is structurally unjustified, catching the wall-clock and token burn that a spend cap can't see

PhaseGateManager.requestApproval takes an optional refusal, dedupes by intentId, and logs a repeat instead of re-publishing. Resolving the gate clears it.

Credit

The shape was sharpened in a public conversation with the author of Agent Pump, who hit the identical failure against a wallet boundary: an agent that was financially safe yet hit INSUFFICIENT_BALANCE 46 times, because the failure returned as a retryable tool error and nothing forced it to acknowledge that the original intent was dead. Thanks to them — the retryable: false + closed-intent + changed-premise shape is theirs.

Follow-ups (not in this PR)

  • UI: offer a third terminal verb ("cannot complete; human action required") instead of Approve/Deny. The structured refusal now rides in the approval.required payload, so the renderer has what it needs.
  • Feed formatRefusalForPrompt into the remediation task so a retrying agent reads structure and a closed intent rather than prose.
  • Persist closed intents across restart (in-memory today; a restart re-opens the intent, which fails in the safe direction).

Test plan

  • 18 contract tests, each pinning a property that caused a real incident
  • Full suite green on the desktop build (355 files / 6,485 tests), tsc --strict clean
  • Port verified as curated, not a blind sync: phase-gates.ts confirmed a clean superset before copying, and sensei.ts had the edit applied to this repo's version so its last_reopened_at handling is preserved

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

All contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

@hmanoor

hmanoor commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

I have read the CLA Document and I hereby sign the CLA

@hmanoor

hmanoor commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

recheck

@hmanoor

hmanoor commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

recheck

@hmanoor

hmanoor commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'

@hmanoor
hmanoor merged commit b9ee819 into main Jul 31, 2026
4 of 6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant