Workflow gates: iw-cli owns where they fire, kanon owns what they check #421
No reviewers
Labels
No labels
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli!421
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/workflow-gates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
Release 0.9.0. It adds a hook type that lets a workflow plugin decide whether an
iw-cli command may proceed, fires it at two points, gives it a cheap self-check verb,
and fixes a stale-attention bug in
phase-startthat predates all of it.The stale-attention fix
phase-startmovedstatustoimplementingand leftneeds_attentionandactivityexactly as the previous phase left them. A card that ended a phase with the flag raised
and
activity: waitingkept both across the start of the next phase.That matters because of what reads those fields.
PendingActresolves a card to a laneby what the writers published, and it treats
activity: "working"as "an agent holds theball" — that case returns
None, i.e. nothing is waiting on the human. With the twofields stale, the card still resolved to
ReviewInWorktree, so the dashboard kept askinga human to come and resume work an agent had already resumed.
phase-startnow states who holds the ball as part of beginning the phase:activity: working,needs_attention: false.The gate mechanism
The split is deliberate: iw-cli owns where a gate fires, the plugin owns what it checks.
WorkflowGateis a trait with one method — given aGateContext, return aList[CheckResult], the same result type the doctor already speaks. iw-cli knows noworkflow rule. It does not know what an analysis is, what a task file is, or what any of
ag/wf/dx require. It only knows that at this point in this command, something may want a say.
The reason for the split is where the definitions live. Workflow rules live in kanon, not
here. If iw-cli owned the checks, every new invariant kanon wants to enforce would need an
iw-cli release, a version bump and a reinstall — for a rule that is entirely a kanon
concept. With the gate returning results rather than answering a fixed question, kanon adds
an invariant by editing a hook file in its own repo, and nothing here moves.
requireArtifactandrequireFileare conveniences for the two invariants we expect mostoften. They are explicitly not a ceiling: the trait hands back a function, so a gate can
express a check the helpers do not cover without touching iw-cli.
Two design points worth naming:
GateContextcarries afileExists: os.Path => Booleancapabilityrather than a filesystem, the way
CiPresencetakeslistInHead. A gate stays a functionof its arguments, and a test supplies a fake.
being the thing the user asked to run, so third-party hook code that fails to answer costs
its own answer and nothing more. Otherwise one stale plugin hook stops every command it
attaches to.
Evaluation reduces the results: one
Erroranywhere blocks, warnings report withoutblocking,
Skipis ignored, andSuccessstays silent unless--verboseasks.Where the gates fire
phase-pr— asked before the push, deliberately. A blocked gate must leave nobranch on the forge and burn no CI.
phase-start— asked before the branch is cut. Note the cardphase-startis aboutto write says nothing yet, so a gate here can only speak for invariants
phase-startdoesnot itself control — never for the fields the line below it writes.
iw gate check <point>A gate you can only trigger by attempting the real action answers at the worst possible
moment: you find out you owe a task file at the point where you wanted a PR.
iw gate check phase-prasks the same gates thatphase-prruns, while the work can still be done cheaply.For the two answers to agree they must come from the same gates, which is what the next
section is about.
The
iw-runchangeHook files attach by exact command name —
*.hook-phase-pr.scalaloads forphase-pr.Left alone,
iw gate check phase-prwould have run as the commandgate, found none ofphase-pr's hook files, and cheerfully reported all clear on gates it never loaded. Aself-check that always says yes is worse than no self-check.
hook_command_nameresolves the name discovery uses: the gate point forgate check <point>, the command's own name otherwise. It is applied at all four discovery sites(shared, project and plugin hooks of a shared command; project hooks of a plugin command).
The same reasoning drove a fix on the verb side:
iw gate check --issue-id TEST-100usedto exit 0 reporting all clear, because the positional filter dropped flags but kept their
values, so
TEST-100took the place of the point. A run naming no point now prints usageand fails.
Version
VERSIONgoes to 0.9.0, and kanon's gate hooks declare// REQUIRES: iw-cli >= 0.9.0astheir floor — 0.9.0 is the release carrying
WorkflowGateand the gate points. Aninstallation below the floor skips the hook with a warning rather than failing the command.
Tests
./mill core.test— 2317 tests, green../mill dashboard.test— green (it sharesReviewStateJson, which moved into core so a gate could be handed the card its worktreepublished).
bats test/gate-hooks.bats— 10 tests, green.WorkflowGatesTest— the pure evaluator: blocking, warnings, skips, gate order, athrowing gate, and both helpers.
WorkflowGateOutputTest— rendering, including verbose.GateCommandHarnessTest— the verb: verdicts, the reported hints, the context it builds,and the argument-parsing failures.
PhaseStartHarnessTest/PhasePrHarnessTest— that a blocked gate cuts no branch andpushes nothing, that a warning lets the command through, and that with no gates installed
the output is byte-for-byte what it was.
test/gate-hooks.bats— drivesexecute_commandwith a stubscala-clithat reports thecommand line the launcher built, over all four discovery sites. Reverting any one of them
fails these tests.
What is NOT covered — please read
Be aware of the honest gaps:
LiveHookOps.workflowGatesis untested. It is a one-lineHookDiscovery.collectValues[WorkflowGate].HookDiscoveryTestcoverscollectValuesFromforCheckandSessionAction, never forWorkflowGate. Reflectionover a real
WorkflowGatevalue in a real hook file has not been exercised by a test.The bats suite proves file selection, not execution. It asserts which hook files
the launcher puts on the command line. It stubs
scala-cli, so it never shows that agate object inside those files is instantiated and asked.
Nothing was verified end to end. No kanon hook shipping a
WorkflowGateexists yet,so the complete path — a real
*.hook-phase-pr.scala→ discovery → a realphase-prblocked — has never once been executed. Every gate-point test injects gates through
FakeHookOps. Points 1 and 2 above are exactly the seam that path crosses, so if thisis broken in practice, that is where it will be. It wants a smoke test against kanon's
hooks before we lean on it.
The stale-attention fix was verified at the write, not at the read. Tests assert what
phase-startwrites intoreview-state.json. I did not observe a running dashboardmoving the card out of the waiting lane. The
PendingActbehaviour it relies on ispre-existing and separately tested, so the inference is sound, but it is an inference.
Also note the last commit is mine: the new tests tripped
DisableSyntax(var,throw) onthe pre-push hook. I did not bypass the hook; I rewrote the captures to the conventions the
surrounding suites already use (
AtomicReference,sys.error), which folded six identicalblocks into one helper.