Skip to content

Confirm gates and interactive handoff-then-resume

State: open/active · Genre: analysis

Use case: human-in-the-loop-gates RFC mapping: UC9 Source: cross-repo survey of sksizer/* (2026-07-05), companion to the workflow-constellation RFC (PR #682).

Human gates are hand-rolled with stdin [Y/n] prompts, TTY sniffing, and — because flows nest — bespoke env-var suppression protocols to decide which nesting level owns the one prompt. Richer versions build pause-and-resume machinery by hand: mpsc channels with timeouts and stale-reply draining, terminal handoff with resume-after semantics, and recoverable pending-interaction state.

pr_prompt.sh: end-of-run ‘Open all N PRs? [y/N]’ gate with TTY detection and a PR_PROMPT_SUPPRESS env protocol so only the outermost of nested flows prompts once, the parent re-scraping child log files to reassemble the URL list — vendored across ~8 repos.

Where: scripts/lib/pr_prompt.sh · scripts/bring_up_to_date_all.sh

Pain: The scripts had to invent ‘who owns the interactive gate’ semantics that a workflow library defines once.

Agent confirmation gate: destructive tools pause the loop, emit ConfirmationNeeded with a UUID, and await a decision on an mpsc channel with 5-minute timeout, stale-call_id draining, channel-closed handling, and synthetic rejection payloads; Tauri layer persists pending confirmations so a reloaded UI can recover them. ~500 lines of tests just for the gate mechanics.

Where: crates/vaultpolish-agent/src/agent.rs · src-tauri/src/commands/chat.rs

Pain: Every concurrency edge case (timeout, stale reply, closed channel) is the author’s responsibility.

InteractiveTask: first-class terminal handoff mid-workflow (gather → interactive discuss → interactive critique → commit), with banner logic and resume-after-exit semantics in the runner.

Where: python/darkfactory/commands/discuss/operation.py · python/darkfactory/runner.py

Pain: Interactive handoff-then-resume is a bespoke task type with its own prompt-resolution and banner logic.

Commit-message [Y/n/e(dit)] gate mid-pipeline whose abort path bails after files were already copied, stranding the destination working tree mutated but uncommitted.

Where: src/sync_cmd.rs

Pain: What happens to already-executed prior steps on rejection is undefined because the gate isn’t a modeled node.

AskUserQuestion consent gates threaded through task-work (planning-status override, dirty-main, resume-or-start-over) plus batched one-shot approval gates in import-planning and friends, each with —auto/—dry-run escape hatches re-specified per skill.

Where: plugin/skills/task-work/SKILL.md · plugin/skills/import-planning/SKILL.md

Pain: Each skill re-describes its approval-gate contract and non-interactive fallback in prose.

UC9’s confirm-gate/interactive-handoff step. Evidence-driven requirements: gate ownership must compose across nested flows (the suppression protocols exist only because it doesn’t today); pause-with-timeout needs durable pending-interaction state so a restarted process/UI can recover the question (polish); and rejection semantics must address already-executed steps (monosync). Deferred/asynchronous human resolution (agent-dashboard’s conflict-skip ‘for manual resolution’ with nothing tracking unresolved items) is the same gate stretched across runs.

Builds on: 03, 06 Introduces: confirm(), interactive handoff invoker

A typed gate instead of a bare read -p the dry-run path cannot see, plus handoff-then-resume:

import { confirm } from "flowline/workflow";
import { agent, cliInvoker } from "flowline/llm";
const prGate = confirm("open-pr", {
message: (ctx) => `Create PR for ${ctx.read(Branch).name}?`,
onDecline: "halt", // a typed outcome in result.steps, not a dropped prompt
});
// Interactive handoff-then-resume (the RFC's spawnClaude variant): the step hands
// the terminal to the user; the workflow resumes when the session ends.
const r = await agent({ prompt: fixPrompt, invoker: cliInvoker({ handoff: true }) });

confirm() is implied by the RFC’s UC9 (“L1+”) but has no sketch there — this names it. The handoff invoker rides the cliInvoker variant the RFC already specifies.


← Back to Notes