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).
The need
Section titled “The need”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.
Evidence
Section titled “Evidence”rust-fluent-git
Section titled “rust-fluent-git”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.
polish
Section titled “polish”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.
DarkFactory
Section titled “DarkFactory”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.
monosync-private
Section titled “monosync-private”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.
Flowline fit
Section titled “Flowline fit”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.
Proposed API
Section titled “Proposed API”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.