Class: Pikuri::Workspace::Confirmer

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/workspace/confirmer.rb,
lib/pikuri/workspace/confirmer/terminal.rb

Overview

Port for resolving a "may I do this?" confirmation to a decision — currently Code::Bash, Write, and the agent tool's delegation gate. Subclass and implement #ask.

The one job: resolve a Request to a decision

A Confirmer takes a semantic Request and returns Approved or Rejected. That is all: it carries no tool policy and never classifies the request's content (no filesystem reads, no command parsing, no notion of "passive"). Two shipped implementations:

  • Terminal — the human chrome: shows the request and reads the answer. In confirmer/terminal.rb so this seam file stays free of rendering deps. Written for pikuri's own single-threaded bin/ scripts and best read as the worked example — a TUI or web host implements this seam itself.
  • AutoApprove — the headless stance: ignores the request and blanket-approves (+--yolo+ / dev-container). Request-independent, so it encodes no policy either.

Sharing is per implementation, and the two shipped ones differ: AutoApprove is P_stateless and shares freely, while Terminal is P_one_agent because concurrent agents fight over one human's keystrokes. A confirmer written for a multi-agent host owns that queueing — and owns naming which agent is asking, which is why the queue can't usefully live down here.

Content classification ("is this bash command provably passive, skip the prompt?") deliberately does not live here — it's a tool concern (Code::Bash takes a separate passive_detector: predicate, falling through to this confirmer only for commands it can't pre-approve). Keeping them apart lets one stateless chrome serve every tool.

The return type: Approved / Rejected

#ask returns Approved (the possibly-edited Request#detail to act on) or Rejected (an optional decline reason); callers pattern-match:

case confirmer.ask(request: request)
in Confirmer::Approved(new_request_detail:) then ...act on it...
in Confirmer::Rejected(reason:)             then ...steer with it...
end

#confirm? is the derived boolean convenience for gates (Write, Code::ExitPlanMode) needing neither the payload nor the reason. Two orthogonal axes ride the richer return:

  1. Editing (opt-in via Request#editable) — when detail is the verbatim actionable payload (the agent task, where detail == task), a confirmer may let the human rewrite it — "human as author, not approver". Bash's "$ <command>" is a decorated display string, so it stays non-editable.
  2. Reason (universal) — any decline can carry typed steering ("use the other library") back to the model as the next observation.

A +:once+/+:always+ scope knob was rejected: it manages decision fatigue, a distinct axis, and the long-term answer to fatigue is making confirmations rare (sandboxing, --yolo, the passive_detector skip), not smarter approval modes. (An agentic destructive-or-not classifier is a deferred v2.)

Seam discipline

Tools take a Confirmer via constructor and invoke #ask with a semantic Requestwhat is asked, never how it looks. All presentation belongs to the confirmer: color, the answer cue, parsing, the edit affordance, and — security-relevant — neutralizing hostile bytes in LLM-supplied text. The chrome-independent half (escape control bytes, flag bidi/zero-width/homoglyph spoofs) is the shared Sanitizer; the medium-specific half stays with the renderer (a terminal prints sanitized text; a web client HTML-escapes). Tools never call +gets+/+puts+ directly — keep IO at the seam so a TUI/web client plugs in without touching tools.

Defined Under Namespace

Classes: Approved, AutoApprove, Change, Rejected, Request, Terminal

Constant Summary collapse

AUTO_APPROVE =

Shared stateless AutoApprove singleton, reusable across tools, sub-agents, and hosts' yolo modes. (No TERMINAL singleton: Terminal is a demo chrome, and real front-ends bring their own.)

AutoApprove.new

Instance Method Summary collapse

Instance Method Details

#ask(request:) ⇒ Approved, Rejected

Parameters:

  • request (Request)

    semantic content composed by the calling tool. The confirmer renders it (escaping for its medium), poses the question, and parses the answer.

Returns:

Raises:

  • (NotImplementedError)

    in the abstract base



123
124
125
# File 'lib/pikuri/workspace/confirmer.rb', line 123

def ask(request:)
  raise NotImplementedError, "#{self.class}#ask must be implemented"
end

#blocks_on_human?Boolean

Whether #ask can actually stop and wait for a person — the posture Trifecta reads when deciding whether a gated tool reaches the approver seat rather than counting as autonomous egress.

true on the base class: a confirmer exists to ask, so a subclass that doesn't must say so, and one that forgets is credited with a gate it does not have only if it also forgets to be AutoApprove. Override to false for any request-independent auto-answer.

This is what makes --no-confirm re-harden a wiring's verdict on its own, with nothing in the detector special-casing the flag.

Returns:

  • (Boolean)


149
# File 'lib/pikuri/workspace/confirmer.rb', line 149

def blocks_on_human? = true

#confirm?(request:) ⇒ Boolean

Boolean convenience over #ask for gates needing neither the edited payload nor the reason.

Parameters:

Returns:

  • (Boolean)

    true iff approved



132
133
134
# File 'lib/pikuri/workspace/confirmer.rb', line 132

def confirm?(request:)
  ask(request:).is_a?(Approved)
end