Wire the workflow runner into TCA and the CLI (063 B3) - #744
Conversation
Power B2's engine on for CLI callers: `prowl workflow run / status / done / cancel`, the reducer-owned `WorkflowRunsFeature` with one ordered effect executor per run, the live boundaries (idle wait on the #733 evidence rules, typed lines, profile launch with child-only `PROWL_WORKFLOW_*` carriers, per-activation watchdogs, notify, close), admission preflight, the `done` rendezvous that answers only after the output is persisted, the dispatch-complete interception, the restart scan, and the versioned payload, schema, docs, contract, and skill. Found live and fixed here: a pane holding a pending dispatch record is refused at admission (`DISPATCH_PENDING`) and ends an idle wait as attention instead of looping; `status` reports only the activation `done` can address; the B2 watchdog re-arms a grace whose expiry saw activity instead of going silent when a launched agent's first detector `working` arrives after its hook `turn-ended`. Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Spell an activation's completion command only to the verified caller pane that owns it (`run` no longer leaks a launch role's token to the launcher; a manual or forced `done` gets none), fence a run's effect queue whenever a transition revokes the in-flight invocation or ends the run so queued injections stop typing after a cancel (and abandon the record an ignored injection opened), refuse `agents dispatch-complete` for the activations of runs that already ended, keep the #733 baseline for the whole idle wait and let an exact `needs-input` outrank the detector (`WorkflowRoleWaitPolicy`), reserve a launched pane until the reducer binds it, answer a self-initiated `run` only once its activation record exists, and refuse duplicate request ids with `REQUEST_CONFLICT`. Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 1 (neighboring
|
…rvations Fence only the effects that act on a pane or the worktree so a later cancel never drops a delivered step's dispatch completion, evaluate the liveness guard inside `deliverLine` on the same main-actor turn as the insertion for every typed line, drop a native action's result when the run left meanwhile, prune pane reservations against every run that ever bound the pane, and keep a cancelled waiter's request id in flight until the reducer answers. Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 25 findings (0 P0, 4 P1, 1 P2), all accepted and fixed; round-1 fixes verified by the reviewer.
|
…rministic fence tests
- `close` is a revocable effect: a cancel that beats a queued close keeps the
pane, and the live boundary refuses to close a pane another active run has
bound since (the run id travels with the close). The unconfirmed close is
recorded as decision W7.
- The executor consults the fence right before a record is issued and as the
last main-actor operation before a native action starts; the machine's cancel
logs an action that is still running, whose result is discarded.
- `FencingQueue` raises the real queue's fence on the n-th staleness check and
resumes the test through a continuation; the fake terminal evaluates the
guard the reducer supplied. No `Task.sleep` left in the reducer tests, and
the `send { $0.x == y }` closures now assert.
- Spurious `await`s on synchronous main-actor closures removed from the
executor.
Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 3 (verification)Round-2 fixes verified by the reviewer; 4 new findings (3 P1, 1 P2), all accepted and fixed.
|
|
Fixed in 1dede20 (workflow suites: 105 passed; |
…on logs, lazy scan clock
- The close boundary closes a pane only when the run is its most recent
binder, whatever that binder's status: a later run that took the pane and
ended keeps it (`WorkflowRunsFeature.State.latestBinder(of:)`, W7 amended).
- The machine no longer guesses at cancel whether a native action is running;
the executor logs what happened ("not started; the run had moved on" from
the pre-start guard, "finished / failed after the run moved on" after a
late return). The action runner is injected (`workflowActionExecutor`) so a
test can hold an action open across a cancel.
- `markInterruptedRuns` reads the clock only for a record it marks: the eager
`@Dependency(\.date)` read on every `repositoriesChanged` failed the
AppFeature suites in CI (no clock override there).
Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 4 (verification)Round-3 fixes verified by the reviewer (including the fence-before-enqueue ordering that keeps a completing batch's
Also fixed on the way: the full |
|
Fixed in 5215cd5. The CI failures on every earlier push (the same 9 |
… at the batch check - Pane ownership no longer derives from `run.startedAt` (neither monotonic nor total): the reducer records `paneOwners[surfaceID] = runID` when a run is admitted and when a launch is taken up, and the close boundary compares run ids only. - A `.runAction` the executor skips at the batch check writes the same "not started; the run had moved on" line to the run log as the pre-start guard does. Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 5 (verification)Round-4 fixes verified by the reviewer; 2 new findings (1 P1, 1 P2), both accepted and fixed.
|
|
Fixed in 41e0db0 (workflow + AppFeature suites: 135 passed, 0 warnings in the changed files). Round 6 (verification) requested. |
…a run ever owned A relaunch drops the old pane from the role's binding before the replacement is taken up, so pruning reservations against the runs' current bindings kept that pane reserved for as long as it lived and refused it to later runs as PANE_BUSY. Admission now prunes against `paneOwners.keys`. Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 6 (verification)Round-5 fixes verified by the reviewer; 1 new finding (P1), accepted and fixed.
|
|
Fixed in 8bd3fd3 (affected suites: 69 passed, 0 warnings in the changed files). Round 7 (verification) requested. |
…runing The pruning rule moves into `WorkflowPaneReservations.pending(for:isLive:)` (every pane a run ever owned, from `paneOwners`), and the relaunch test reserves the first launch pane and asserts the reservation is gone after the relaunch while the pane still lives. The record carries the final test figures and the full-suite note. Claude-Session: https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z
Adversarial review — round 7 (verification)Round-6 fix verified by the reviewer: no P0/P1 remains. 1 P2, accepted and fixed.
Review summary for the PR: 7 rounds with the neighboring |
Summary
063 B3 — Workflow Runner Wiring. Powers B2's engine on for CLI callers:
prowl workflow run / status / done / cancel, the reducer-ownedWorkflowRunsFeature, the live boundaries (idle wait, typed lines, profile launch with child-onlyPROWL_WORKFLOW_*, per-activation watchdogs, notify/close), admission preflight, and thedonerendezvous. Record:docs-ai/063-agent-workflows/008-b3-runner-wiring.md(decisions W1–W6, Delivered, Verification).WorkflowRunsFeature): one FIFO executor per run performs B2's effects in machine order (instruction file exists before the pointer is typed;run.jsonwrites never overtake each other); idle waits and watchdogs are cancellable observers; late/stale.launchedabandons its record and closes the pane; successful launches remember their profile under B2's digest key; a restart scan marks leftover runsinterruptedwhen repositories load.donerendezvous (W1): the reducer answers only when the addressed activation leavespersisting—delivered/provisionalsucceed; persist failure →WORKFLOW_FAILED; cancel/skip/ended →STEP_NOT_EXPECTING; a disconnected client getsREQUEST_CANCELLEDwhile the run continues.WORKFLOW_INVALID/WORKFLOW_DISABLED, source rules, bindings in role order (current/pick/launchwith--roleoverride → remembered → suggestion → Recommended), one run per pane (PANE_BUSY), pending record refusal (DISPATCH_PENDING), inputs/skips, frozen launch plans, layout + initial record before the reply.--run --stepmanual,ROLE_MISMATCHunless--force;agents dispatch-completefrom a workflow pane →WORKFLOW_DELIVERY_REQUIRED.AgentProfileLaunchPlan.attachingWorkflowswaps in the kickoff prompt and carriesPROWL_WORKFLOW_TOKEN/_RUN/_ROLEasPROWL_LAUNCH_WORKFLOW_<n>carriers theenvline unsets for the child (verified from inside the child: only the threePROWL_WORKFLOW_*variables, noPROWL_DISPATCH_*/PROWL_LAUNCH_*).prowl.cli.workflow.v1gainsrun/status/cancel(run object) anddone(run+delivery); executable schema, text renderers,docs/components/cli.md,docs-ai/013-prowl-cli/contracts/workflow.md, theprowl-cliskill recipe.Found live and fixed in this PR
dispatch-completestill held its launch record; the self-initiated activation failedroleBusy, the machine fell back to the idle wait, and the run ended in an injection attention whilestatusstill advertised awaitingactivation. Now: admission refuses such panes withDISPATCH_PENDING, the idle wait ends as adispatchPendingattention instead of spinning, andstatusreports only the activationdonecan address.turn_grace/idle_graceexpiry that had seen activity cleared the flag and scheduled nothing; a freshly launched agent whose first detectorworkingarrives after its hookturn-endedleft the watchdog silent for good (no nudge, no attention). The expiry now re-arms the same grace (heuristicidle_gracestill re-arms from the detector's next idle level); regression test + updated B2 pins.WorkflowRunEvent.roleUnavailable(blocked / gone / no agent / foreign pending record during the idle wait → the injection-failed attentions).Verification
make check,make build-cli,make test-cli-unit(233),make test-cli-smoke,make test-cli-integration(110),make build-app(0 warnings),make test(full app suite, 0 failures), workflow app suites (121 + 77).b3-reviewend to end from a launched Claude Code pane — self-initiated brief,done -by caller ancestry, reviewer launched with the protocol block,agents wait --dispatchreceipt on the activation, two CLI: dispatch a new task into an existing agent pane (re-dispatch) #733 re-dispatches (author, then reviewer),untilonclean,notify,close;WORKFLOW_DELIVERY_REQUIREDrefusal; a deliberately provisional delivery (delivery.state: provisional,statusattention with the H14 actions,cancel); restart interruption (statusfromrun.json:interrupted);b3-idlewatchdog nudge after the fix.Notes
status+cancelonly until the panel ships.display_nameof acurrentrole is the detected agent token (the app'sDetectedAgent.displayName); C1 owns display polish.https://claude.ai/code/session_01MxJN8e4UKvCgzpqmh4PL7Z