human-rule: an answered design routes to ai:reject, same send-back as a rejection - #224
Conversation
… a rejection human:design is DELETED from the FSM (#219). A design ruling IS its answer, the answer is producer work, and the machine state is the ONE reject state: - PR side: design + --rework lands exactly what reject + --rework lands — ai:reject, every other ai:* cleared, the trusted Rework note posted at the same head-sha anchor in the same call. --park is removed from the CLI and the MCP schema; the pinned 👤 human comment still records the verb. - Issue side: design leaves the vocabulary's label space — it validates, posts the pinned ruling comment (+ work order) and writes NO label; the issue stays in the producer backlog. - Deleted machinery: the Delegation ordered/executed discriminant and design_delegation, the #111 clearing-by-execution arm in verdict_plan, the rework-ruling worklist routing and humanWorkOrder row field, the pr_human_sacred narrowing, the PR_SACRED_LABELS entry (and the now-equal PR_PARKED_HUMAN_LABELS/pr_absolutely_parked pair), HUMAN_DECISION_LABELS and the lane special-case, the humanDesign count and dash section, and the label_meta registration. - Vocabularies are verb->target tables now (HUMAN_PR_RULINGS / HUMAN_ISSUE_RULINGS): both PR verbs -> ai:reject; issue design -> no label. - migrate-design [--apply]: one-shot over both subject kinds, on the migrate-reject precedent. PRs -> ai:reject with the live order at head kept, else the ruling's own note posted as the order; issues -> label stripped, the ruling comment stays the record. Nothing keeps the label. - README FSM, CLAUDE.md invariants, plugin docs and both cron prompts describe the new routing; tests moved with the behavior. Closes #219 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkthroughHuman design rulings now require producer rework. PRs use ChangesHuman ruling flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
QAAdversarial mutation pass over the changed behavior (
12/12 killed; no survivors, so no tests needed strengthening beyond those written with the change. |
The rs-static gate runs the rainix pre-commit bundle and denofmt reflows prose line-wrapping in CLAUDE.md, README.md and design.md — content unchanged. The plugin-version gate requires any change under plugins/human-fsm to bump plugin.json, with the marketplace entry in lockstep: 0.11.0 -> 0.12.0. Refs #219 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n flight Three open PRs bump human-fsm from 0.11.0 and two of them claimed 0.12.0. `/plugin` detects an update by comparing version STRINGS, so the second of a duplicate pair to land serves new content under a version installers have already seen — the stale-content bug the version-hygiene gate exists to catch. Claims are now distinct and monotonic in merge order: #224 (0.12.0), #227 (0.13.0), this (0.14.0). This PR merges last of the three; landing it earlier means re-bumping past whatever main then carries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ject Review of the live population found the note-as-order fallback cannot fire on it. All six open human:design PRs (rain.erc4626.words#233 #166, raindex#2778 #2721 #2720, cyclo.site#412) predate the marker-emitting ruling transition: each carries the label alone, with no trusted 👤 human comment and no Rework note, so no order is recoverable for any of them. Mapping those to ai:reject would assert an answer nobody gave AND land them in the producer's queue as orderless rejects — a state campaign-prompt.txt defines as inert ('a reject-labelled PR WITHOUT any trusted note is PARKED for a human'), so six unanswered design questions would go neither answered nor asked. So migrate_design_pr_plan maps by whether the ANSWER is recoverable: - Answered (a trusted order live at head, or a trusted ruling whose note can be posted as one) -> ai:reject + that order, as before. - Unanswered (neither) -> ai:design, the machine's own state for 'a human must answer this', which is the /nd queue. MigrateOrder loses its Unrecoverable arm: it now describes only where a recoverable order comes from, and recoverability decides the TARGET. Also: - strands_flag_error states the reason that always holds. A comment-only design ruling writes no label and strands nothing, so the message leads with the anchor rationale (it would pin to close-candidate @<at>, the record of judging the flag, while answering something else) and keeps the stranding note for rulings that do write a label. - last_human_ruling_note only carries a SEND-BACK verb's note forward. A torn human-close leaves a trusted close ruling on an OPEN subject, and its note is a close reason, not an order to hand a producer. - CLAUDE.md states that issue-side design writes no label. Refs #219 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…irst A surviving mutant (rsplit_once -> split_once) found the gap: a human ruling at a terminal can carry a context line above the `Ruled …` line, and every `Word: ` in it sits before the verb. Reading from the first `: ` takes that context as the verb, finds it is not a send-back, and silently discards an answer the migration could have carried forward. Refs #219 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA — mutation pass 2 (the recoverability mapping)Applied to the committed change at
M18 was a real gap, not an equivalent mutant. The prelude is not guaranteed to be one line: a human ruling written at a terminal can carry context above the 22/22 killed across both passes (12 in pass 1, 10 here). |
A migration is an execution vehicle, not permanent machinery (#221 (comment)). migrate-design has run: six PRs moved to ai:design, zero human:design subjects remain org-wide across both subject kinds, and the label definition is swept from every repo that could be edited. So the subcommand comes out of the merged tree. Deleted: the Cmd variant and its dispatch arm, migrate_design_mode, migrate_design_pr_plan, migrate_design_issue_carries, migrate_design_search, MigrateDesignPlan, MigrateOrder, last_human_ruling_note, SEND_BACK_RULINGS, DELETED_DESIGN_LABEL, and the tests that existed only to exercise them — 678 lines. No migrate_design identifier survives; the only remaining mention is the parse test that pins the subcommand as GONE. DELETED_DESIGN_LABEL was removed compiler-first rather than re-literalised. Following the warnings surfaced rework_note_anchor as newly dead: it parsed the work-order pin for design_delegation (deleted earlier in this PR) and then for the migration, and nothing in the binary reads a pin back now — the PRODUCER does, from the emitted text. So the parser goes and its test is re-pointed at what is still a live contract: that rework_note_comment emits in exactly the shape the producer's read expects. Kept, because they pin the FSM rather than the migration: human:design does not classify into any lane, parks nothing, blocks no producer flag, has no label_meta row, and issue-side design writes no label. README/CLAUDE.md prose describing the migration as available machinery is gone; the transcripts stay in the PR body as the once-off's record. Refs #219 Refs #221 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA — mutation pass 3 (the
|
| # | Mutant (what it resurrects) | Result | Killed by |
|---|---|---|---|
| R1 | human:design back in PR_SACRED_LABELS — the label parks the vetter again |
KILLED (3) | sacredness_is_authorship_and_currency; a_verdict_neither_refuses_on_nor_clears_the_deleted_design_label; worklist_has_no_design_routing_and_the_sacred_labels_still_park |
| R2 | the label buckets into the human-decisions lane again | KILLED (4) | classify_lane_maps_every_state_by_precedence; lanes_doc_emits_every_state_with_the_right_members; every_pr_ruling_label_lands_in_a_modeled_lane; verdict_currency_is_fetched_only_where_it_can_change_the_lane |
| R3 | the label_meta colour/description row comes back |
KILLED | every_human_label_carries_the_orgs_live_colour_and_description |
| R4 | issue-side design writes a label again |
KILLED (4) | human_ruling_vocabulary_is_the_tables; a_human_ruling_on_an_issue_is_sacred_whichever_label_it_wears; close_candidate_plan_respects_state_human_and_dedup; every_human_label_carries_the_orgs_live_colour_and_description |
| R5 | the PR design verb targets human:design again |
KILLED (4) | an_answered_design_lands_the_pr_exactly_where_a_reject_does; human_ruling_vocabulary_is_the_tables; every_pr_ruling_label_lands_in_a_modeled_lane; a_pr_ruling_on_a_live_flag_disposes_it_structurally |
| R6 | the label blocks a producer close-candidate flag again | KILLED (2) | a_human_ruling_on_an_issue_is_sacred_whichever_label_it_wears; close_candidate_plan_respects_state_human_and_dedup |
| R7 | the migrate-design subcommand reappears in Cmd |
KILLED (compile) | non-exhaustive match — the dispatch arm is gone, so a resurrected variant cannot compile |
7/7 killed, no survivors. The tests that kill R1–R6 are exactly the ones kept because they pin the FSM (the state is deleted) rather than the migration — which is the evidence that removing the migration did not weaken the deletion it performed.
Across the whole PR: 29/29 mutants killed (12 core + 10 mapping + 7 removal), one survivor found and fixed with a new discriminating test along the way.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@campaign-prompt.txt`:
- Line 45: The producer prompt’s reject-work-order discovery must use the
FSM/report fleet state rather than raw GitHub queries. Update the reject
discovery instructions in the campaign prompt to consume reject rows from the
report’s state-load/worklist output and verify trust-bearing comments through
trusted-comments; if those rows are unavailable, extend the report subcommand.
Remove the `gh search prs` pipeline-input guidance and preserve the existing
rework/park handling.
In `@plugins/human-fsm/commands/design.md`:
- Line 2: Align the /design issue fallback with the refusal emitted by
HumanRulePlan::NotAnIssue, using the actual “is not a pull request” contract so
routing reaches human-rule-issue. Update plugins/human-fsm/commands/design.md:2,
README.md:300, and plugins/human-fsm/README.md:18 consistently, retaining the
issue route only where this fallback works.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: bc4951bd-79e4-4e07-a06f-5145d838d0b5
📒 Files selected for processing (11)
.claude-plugin/marketplace.jsonCLAUDE.mdREADME.mdcampaign-prompt.txtcampaign-run.shplugins/human-fsm/.claude-plugin/plugin.jsonplugins/human-fsm/README.mdplugins/human-fsm/commands/design.mdplugins/human-fsm/commands/reject.mdpr-review-report-rs/src/main.rsreview-prompt.txt
Deleting NextAction::ReworkRuling takes out the one typed expression of "a human sent this PR back", leaving campaign-prompt.txt step 3 to find rejects with a raw `gh search prs --label ai:reject` in a repo whose contract is that the prompts route ALL GitHub I/O through pr-review-report. A second read of the fleet is a second answer free to disagree with the one state-load just handed over, which is the failure mode state-load exists to remove. ai:reject is ONE state whoever ruled it, but it is not one ACTION. A reject the tool can read a TRUSTED instruction for is the producer's work order — NextAction::ReworkReject, `rework-reject`. A reject with nothing trusted behind it is parked for a human, and stays parked-skip: the label says the PR was sent back and nothing trusted says what for, so a rework is a guess. The instruction has three spellings because three transitions write the one state: the vetter's own `Reviewed <sha>: reject` verdict, the human's `Rework note`, and the human's `👤 human` ruling. All three are read through trusted_comments, which filters by AUTHOR before any marker — so a spoofed note from another account produces no work-order row. Reading only the two human spellings would park two thirds of the live reject fleet: of 12 sampled open ai:reject PRs, 8 carry only a vetter verdict. pr_close_verdict_line generalises to pr_verdict_line(body, verdict) so there is one parse of a `Reviewed <sha>: <verdict>` line rather than two, and the verdict is matched on the WORD of that line — the prose under it is model-authored, so a verdict that talks about a reject is not one. rework-reject is ranked ahead of every derived route (it is the one route a person wrote about this exact PR) and behind the arms that park, is in ACTIONABLE_ACTIONS so a work order is enumerated rather than counted, and is in ALL_ACTIONS so byAction states its count as zero rather than absent. flag_migration_leads_the_action_rank is strengthened in place: the unknown-action rank is asserted against parked-skip's own rank, and every action a row can carry must be ranked explicitly. ReworkRuling had no rank arm and silently sorted last. Closes #229 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial mutation pass — the #229 addition (
|
| # | Mutation | Result | Killed by |
|---|---|---|---|
| 1 | Collapse the two reject shapes into one row — RejectState::Parked => return NextAction::ReworkReject |
KILLED | a_reject_with_an_instruction_and_one_without_are_different_actions, worklist_row_reject_without_a_trusted_instruction_is_parked |
| 2 | Accept an untrusted note as a work order — reject_instruction reads raw comment bodies instead of trusted_comments, so the author filter is gone |
KILLED | worklist_row_reject_without_a_trusted_instruction_is_parked (the spoofed-author fixtures) |
| 3 | Drop the zero-count emission — "rework-reject" removed from ALL_ACTIONS |
KILLED | the_histogram_states_every_action_including_the_zeroes, only_the_actions_that_name_work_are_enumerated |
| 4 | Delete the reject arm from next_action entirely |
KILLED | a_reject_with_an_instruction_and_one_without_are_different_actions, a_reject_work_order_outranks_every_derived_route, worklist_row_reject_with_a_trusted_instruction_is_the_work_order, worklist_row_reject_without_a_trusted_instruction_is_parked |
| 5 | pr_verdict_line matches the verdict as a substring of the line tail instead of its first word |
KILLED | worklist_row_reject_without_a_trusted_instruction_is_parked (the close — … so a reject would be wrong here fixture) |
| 6 | reject_state ignores the ai:reject label — any PR with a trusted comment becomes a reject |
KILLED | worklist_has_no_design_routing_and_the_sacred_labels_still_park, the_shape_signal_and_the_migration_flag_diverge_only_at_a_confirmed_head, worklist_row_red_prodpin_is_flag_migration, worklist_row_deploy_done_must_be_head_scoped, +6 more |
| 7 | "rework-reject" => 1 removed from action_rank — the action sinks to the parked rank |
KILLED | flag_migration_leads_the_action_rank (the every-action-is-ranked loop added here) |
| 8 | "rework-reject" removed from ACTIONABLE_ACTIONS — counted but never enumerated |
KILLED | the_histogram_states_every_action_including_the_zeroes |
| 9 | Note-less reject falls through to the CI classifier — else { RejectState::NotRejected } |
KILLED | worklist_row_reject_without_a_trusted_instruction_is_parked |
| 10 | The pr_close_verdict_line → pr_verdict_line refactor re-pointed at "reject" |
KILLED | cc_row_classifies_pr_subjects_through_the_same_gate |
Mutants 1–3 are the three the change is most exposed on, and each is killed by a test written for exactly that property rather than incidentally. Mutant 10 exists because generalising pr_close_verdict_line into pr_verdict_line(body, verdict) is the one edit here that touches behavior the reject work has no interest in — the close-candidate queue's own reader — and it is still guarded.
Mutant 6's wide kill-set is the useful signal in it: dropping the label check makes almost every fixture in the worklist suite classify as a reject, so the label really is what puts a PR in the state, and the trusted comments only say what the state means.
Restore integrity
baseline sha256: ad63ce21d5e07818faca026bc3b1ed22be01a6d71203d823f425327851457d5a
final sha256: ad63ce21d5e07818faca026bc3b1ed22be01a6d71203d823f425327851457d5a
git status --porcelain: (empty)
Re-verification: PR 224's own guarantees still hold at the new head
The addition inserts an arm into next_action directly beneath the two that park, so the ordering this PR already relied on is exactly what a new arm could quietly break. Five of 224's own behaviors re-mutated at 02421fb, same protocol (assert-applied, whole unit suite, sha256-verified restore):
| Mutation of 224's behavior | Result | Killed by |
|---|---|---|
The sacred human:* label no longer parks in worklist_row |
KILLED | the sacred-label worklist pins |
The human_parked arm deleted from next_action |
KILLED | human_override_parks_over_stale_ai_label_ci_and_deploy, a_parking_state_still_beats_a_reject_work_order |
design writes human:design again instead of ai:reject |
KILLED | an_answered_design_lands_the_pr_exactly_where_a_reject_does, human_ruling_vocabulary_is_the_tables, every_pr_ruling_label_lands_in_a_modeled_lane, a_pr_ruling_on_a_live_flag_disposes_it_structurally |
A 👤 human ruling at the head is no longer sacred |
KILLED | a_human_ruling_at_the_head_parks_the_pr_against_every_ai_write, pr_context_hands_the_vetter_the_humans_ruling |
The modeled ai:* state-label park arm deleted |
KILLED | modeled_state_label_short_circuits_to_parked, a_parking_state_still_beats_a_reject_work_order |
5/5 killed. The two bolded entries are the point of the exercise: the new a_parking_state_still_beats_a_reject_work_order now stands as a second guard on both park arms, so the reject row cannot be reached by a PR a human or a modeled state has parked. 224's PASS-2 mutants are not re-run because the code they mutated (migrate-design) was removed by 8b6a149 — there is nothing left to mutate.
15/15 killed across both passes at this head, tree clean and main.rs byte-identical to its committed state afterwards.
…d row too ALL_ACTIONS and ACTIONABLE_ACTIONS were hand-maintained beside NextAction and had drifted BOTH ways at once. They named `deploy`, a retired spelling no variant can produce, so `byAction` reported a permanent phantom zero. And they omitted `flag-migration` — the action action_rank puts FIRST — so a migration row was never stated in the histogram (breaking the zeroes-stated contract, which exists precisely so an absent class is never something a reader has to infer) and never appeared in fleet.actionable, so the producer was never handed one and those PRs were silently skipped. Fixing the contents alone would leave the CLASS: a copied vocabulary drifts from its source, which is the lesson #130 already paid for. So both lists are DERIVED from NextAction::ALL — ALL_ACTIONS is its names, ACTIONABLE_ACTIONS is that set filtered by NextAction::names_work rather than a second hand list — and action_rank now resolves the string to a variant and asks it, instead of mirroring the order in a parallel match. Membership is compiler-enforced: rank() and names_work() are exhaustive, so a variant added to the enum cannot compile until somebody decides where it sorts and whether it is work. the_action_vocabulary_is_the_enum pins it structurally, iterating every variant rather than the lists — a test driven by the lists cannot see what the lists are missing — and asserts the converse too, that nothing is stated which no variant produces. campaign-prompt.txt step 3d had the same defect as step 3's reject query, one search plus a fetch PER PR to rebuild a classification state-load already computed. It now consumes the `conflict-3d` row, which carries the mergeState the step branches on. `baseRefName` is the one detail the row does not carry, so the step fetches that alone, for the PRs it actually works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removing a query from a prompt is not the same as forbidding it, and the repo already carries prompt-conformance tests for exactly that reason. This asserts on the STEPS: the executable searches are gone as instructions AND present as prohibitions, step 2a offers the two rows both steps now depend on, and step 3d still names `baseRefName` as the one field the row does not carry — without that last clause the step could lose the ability to name its base branch at all and nothing would notice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial mutation pass — combined, at
|
| Mutation | Killed by |
|---|---|
Collapse the two reject shapes (Parked ⇒ ReworkReject) |
a_reject_with_an_instruction_and_one_without_are_different_actions, worklist_row_reject_without_a_trusted_instruction_is_parked |
| Untrusted note becomes a work order (raw bodies, no author filter) | worklist_row_reject_without_a_trusted_instruction_is_parked |
| Reject arm deleted entirely | 4 tests |
| Verdict matched as a substring of the line tail | worklist_row_reject_without_a_trusted_instruction_is_parked |
reject_state ignores the ai:reject label |
10 tests |
| Note-less reject falls through to CI | worklist_row_reject_without_a_trusted_instruction_is_parked |
pr_close_verdict_line re-pointed at "reject" |
cc_row_classifies_pr_subjects_through_the_same_gate |
B — the enum-derived action vocabulary (6)
| Mutation | Killed by |
|---|---|
A variant retired from the vocabulary list, enum untouched (FlagMigration dropped from NextAction::ALL) |
the_action_vocabulary_is_the_enum, the_histogram_states_every_action_including_the_zeroes, only_the_actions_that_name_work_are_enumerated, flag_migration_leads_the_action_rank |
A retired spelling outlives its variant (as_str returns deploy) |
the_action_vocabulary_is_the_enum, the_histogram_states_every_action_including_the_zeroes, +2 |
flag-migration dropped from the actionable set |
the_action_vocabulary_is_the_enum, only_the_actions_that_name_work_are_enumerated |
A non-work action classified as work (green-ready ⇒ true) |
only_the_actions_that_name_work_are_enumerated |
rework-reject classified as not-work |
the_histogram_states_every_action_including_the_zeroes |
action_rank stops asking the enum |
flag_migration_leads_the_action_rank |
The first three are the coordinator-named cases, and they are the point of deriving the lists: the first two cannot even be written as a source edit any more — dropping a variant from ALL or misspelling as_str is the only way left to express the drift, and both are caught. the_action_vocabulary_is_the_enum kills all three, which is what "pinned structurally" has to mean.
C — prompt conformance (2)
| Mutation | Killed by |
|---|---|
Step 3's raw gh search prs --label ai:reject reinstated |
the_reject_and_conflict_steps_consume_typed_rows_not_a_search |
Step 3d's gh pr view --json mergeStateStatus,baseRefName reinstated |
the_reject_and_conflict_steps_consume_typed_rows_not_a_search |
D — PR 224's own guarantees, re-verified (5)
| Mutation | Killed by |
|---|---|
Sacred human:* label no longer parks in worklist_row |
the sacred-label worklist pins |
human_parked arm deleted from next_action |
human_override_parks_over_stale_ai_label_ci_and_deploy, a_parking_state_still_beats_a_reject_work_order |
design writes human:design again |
an_answered_design_lands_the_pr_exactly_where_a_reject_does, human_ruling_vocabulary_is_the_tables, +2 |
A 👤 human ruling at the head no longer sacred |
a_human_ruling_at_the_head_parks_the_pr_against_every_ai_write, pr_context_hands_the_vetter_the_humans_ruling |
Modeled ai:* state-label park arm deleted |
modeled_state_label_short_circuits_to_parked, a_parking_state_still_beats_a_reject_work_order |
The new arm sits directly beneath both park arms, so those two are the ones a new arm could quietly break; the bolded entries show the new test now stands as a second guard on each. 224's PASS-2 mutants are not re-runnable — 8b6a149 deleted the migrate-design code they targeted.
Restore integrity
main.rs 9d892c80562a2f600e869393c47132e8fbf29c97e4680114d72b7e026cc34c3e (== baseline)
campaign-prompt.txt 1eff9755181550a97b0ceb980e343d3b78dae5acf9011015f0653cc09046ec63 (== baseline)
git status --porcelain: (empty)
…ranch Two independent DELETIONS of an FSM state landed on the same regions: #221/#225 deleted `ai:blocked-deploy`, this branch deletes `human:design`. Every conflict was that symmetry, and every one takes BOTH deletions rather than a side. - classify_lane's lanes fixture now carries residue of both deleted labels, so the un-vetted bucket gains a member from each: count 3 -> 4, members [1, 2, 8, 12]. Neither deleted state emits a lane cell. - the domination case keeps this branch's subject (the retired `human:reject` is the human-decisions lane's one remaining state, not the deleted `human:design`) over main's, and main's spelling of the deploy label: a bare literal, because #225 deleted the constant and re-declaring one to name a state that does not exist would be the deletion undone. - README's lane list takes main's producer-blocked bullet whole and this branch's human-decisions bullet whole. next_action auto-merged correctly and is the interaction worth stating: main removed the deleted label from the modeled-state park arm, this branch added the reject arm directly beneath it, and the result keeps both — a note-less `ai:reject` still classifies parked-skip and nothing parks on the deleted deploy label. The derived action vocabulary needed no edit at all: #225 added and retired no NextAction variant, and the exhaustive rank()/names_work() matches would have refused to compile if it had. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed f66244d: approve — read the diff independently at this head. The ruling is structural, not conventional: |
Adversarial mutation pass — re-run against merged main, at
|
| Section | Mutants | Result |
|---|---|---|
| A — the #229 reject behavior | 7 | 7 killed |
| B — the enum-derived action vocabulary | 6 | 6 killed |
| C — prompt conformance (both steps) | 2 | 2 killed |
| D — PR 224's own guarantees | 5 | 5 killed |
| E — the merge with #225 | 1 | 1 killed |
E — the merge
| Mutation | Killed by |
|---|---|
The deleted ai:blocked-deploy parks again in next_action's modeled-state arm |
modeled_state_label_short_circuits_to_parked |
That arm is exactly where the two branches met: #225 removed the deleted label from it, this branch added the reject arm directly beneath it. The mutant re-introduces the park that #225 deleted, and #225's own test kills it — so the merge did not quietly restore the state, and the reject arm's placement did not disturb the arm above it.
Two kills in section D are worth re-reading in that light: 224-park-arm-deleted-from-next-action and 224-modeled-state-label-no-longer-parks are both killed by a_parking_state_still_beats_a_reject_work_order as well as by their own tests. The new arm sits under both park arms, and that test is now a second guard on each.
Nothing in section B needed touching for the merge: #225 added and retired no NextAction variant, so the derived vocabulary was unaffected — and the exhaustive rank() / names_work() matches would have refused to compile if it had been.
Restore integrity
main.rs 41fe3d736f647b5ae99bc5ade6fb5b551aecac9cf628435ec5e8a912ab356c57 (== baseline)
campaign-prompt.txt b0c8411981b339c054a2b57451401d16e00b3cc1537fd8f994fc13db99dc530d (== baseline)
git status --porcelain: (empty)
…) into /nm Both deletions land under a PR whose whole subject is "which PRs are in no modeled state", so the interactions are the merge, not a side effect of it. CONFLICT SHAPE: two files, both the plugin version JSONs, both trivial — main carries #224's 0.12.0 and this branch 0.13.0, and mine is the superset (it names /nm) and is already ahead. main.rs auto-merged CLEAN, which is exactly the case that has bitten twice today, so the real work was hunting what the markers could not show. WHAT THE SUITE CAUGHT THAT THE MARKERS DID NOT: - The drift witness in `a_row_carries_the_leaks_own_evidence` asserted a `human:design` PR classifies into the human-decisions lane. #224 deleted that state, so it now classifies as a LEAK and the assertion failed on the merge commit. Repointed at `human:reject` — the lane's one surviving state, and still the live instance of the hazard the field exists for (a `human:*` label carries no `ai:*` prefix, which is what the old population wrongly admitted). - `is_leak_candidate` referenced `RETIRED_BLOCKED_DEPLOY_LABEL`, deleted by #225: a compile error, fixed by dropping the row. WHAT ONLY READING #225's OWN COMMIT CAUGHT — and no test would have: `fdf2ee0` ruled that a PR hand-wearing a DELETED `ai:*` string lands `un-vetted`, not `leak`, because the vetter absorbs it and the verdict that judges it strips the dead label. That ruling was justified by the caller-side `ai_state_label` partition — the exact thing this PR replaced with a classifier-derived population. Under the merge as it auto-resolved, such a PR became a leak again and silently overturned a ruling that landed hours ago. The `lanes_doc` fixture cannot catch it (it feeds `producer_commented` directly), so this is added as a conjunct on `is_leak_candidate` with the ruling cited, plus an assertion over the registry below. It cannot reintroduce the defect this predicate exists to fix: that defect was `human:*` labels parked in a real lane, and the classifier arm is what excludes those. THE THREE PIECES OF COVERAGE THE HUMAN CALLED FOR, so none of the above is re-verified by hand at the next merge: 1. `every_lane_is_reachable_from_some_label_set` — one witness per `Lane`, driven off a new `Lane::ALL`, so an arm going unreachable behind a precedence change is a failure and a lane added later is covered on the day it is added. 2. `DELETED_LABELS` + `a_deleted_label_models_nothing_anywhere` — one property over a registry, replacing the two bespoke per-deletion tests #219 and #221 each shipped. For every deleted label: the ordinary unmodelled fallthrough, dominates nothing, occupies no lane cell (checked across `Lane::ALL`), and no `counts` key names it. The next deletion adds a row of DATA. 3. `lanes_doc_emits_every_state_with_the_right_members` now derives every total from per-row expectations instead of literals. That assertion is the one that read `3` on two branches and was wrong at `4` once both landed, with nothing pointing at it; an expectation beside its input cannot go stale that way. nm.md, the README and the doc comments now state the post-deletion truth: a RETIRED label stays bucketed and never leaks; a deleted `ai:*` label is absorbed as un-vetted; a deleted `human:*` label DOES leak, because the vetter's sweep is `ai:*`-scoped and nothing else claims it. The classifier rule nm.md tells the human to check is now its three real clauses rather than the `ai:*`-only paraphrase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…) into /nd Conflicts, and how each resolved: - `.claude-plugin/marketplace.json` + `plugins/human-fsm/.claude-plugin/plugin.json` — both sides bumped the plugin. OURS: 0.14.0 stays, distinct from and above main's 0.12.0 (#224) and the 0.13.0 #227 still claims, and the description keeps the third read. - `plugins/human-fsm/README.md` — the whole command table. MAIN's table taken wholesale, because #224 rewrote the `/design` row and resolving toward ours would restore the `human:design` / `--park` prose #219 deleted; the `/nd` row is spliced back into it. Both intents, main's row verbatim. Everything else auto-merged. Main's deletions stand: nothing here reintroduces `human:design` or `ai:blocked-deploy` in any spelling — every remaining mention in the tree is main's own assertion that the deleted state is inert. nd.md needed no correction against merged main: it describes the post-#219 exit (`ai:reject` with the answer as the trusted work order, one call at the head anchor, no parked spelling), and `human-rule … design --rework` now does exactly that. What was aspirational when written is now what the code does. Coverage rather than hand-verification, per the standing call — two hand-written totals near the merged region are now DERIVED from their fixtures: - the withheld-overflow expectation is `raised - ND_MAX_WITHHELD` off a fixture sized from the cap, not a literal `2`; - the row and envelope digit reserves count their numeric fields from the emitted document (`numeric_fields`) instead of a number typed in a comment. That is the exact shape that survives review and breaks on a merge: a count key added on one branch and a reserve typed on another are each correct alone and conflict with nothing when both land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main is 31 commits ahead of this branch's base and every one of the five PRs that landed in between touches what this renames, so main wins on structure and this branch wins on the spelling. Where a hunk carried both, the merged code takes both: 224's verb->target tables and typed worklist row, 223's state descriptors, 225's deleted `ai:blocked-deploy`, and 226/227's `/nd` and `/nm`, all spelled `needs-work`. The rename had to reach code that did not exist when this branch was written: - `NextAction::ReworkReject` -> `ReworkNeedsWork`, string `rework-reject` -> `rework-needs-work`. The action vocabulary is DERIVED from the enum (#224), so `ALL_ACTIONS`, `ACTIONABLE_ACTIONS` and `action_rank` follow from the variant. - `RejectState` -> `NeedsWorkState` with `NotRejected` -> `NotNeedsWork`, `reject_state` -> `needs_work_state`, `reject_instruction` -> `needs_work_instruction`, and the `PrSignals.reject` field -> `needs_work` — 224's typed row that replaced the producer prompt's raw label search. - The producer prompt's step 3, which 224 rewrote around that typed row. - review-prompt's #219 sentence, `design.md`, and 226/227's `nd.md` / `nm.md`. Deliberately NOT renamed, because a blind sweep would have corrupted them: the close-candidate FLAG verdicts (`CC_VERDICTS`, `cc_verdict_plan`, `cc_gate`, `CcGate::RejectedStillFlagged`, `/close-candidate <n> reject`) judge a flag and write no state; the "refuse" sense (`ResultRejected`, the rate-limit `rejected`, a non-fast-forward push); `reworked-reject`, a subcommand that is GONE; and `counts.humanReject`, which is both a kept series and a `histFold` key. History continuity is measured, not assumed. `STATE_NEEDS_WORK.hist_fold` keeps `"reject"` alongside `humanReject`/`relink`, and against the committed `human-queue-history.jsonl` (232 rollup lines) those three keys carry 218 samples each, newest 2026-08-07T11:31:03Z, while `needsWork` has none — so the whole drawn series comes through the fold. The 14 lines that fold nothing are the contiguous oldest prefix, predating the state itself. The history file is not rewritten; `the_needs_work_series_draws_every_spelling_the_state_has_had` pins this structurally first so it cannot pass vacuously in the flake sandbox. Three things the merge markers did not cover and the suite did: - `every_shipped_command_carries_its_frontmatter` — the command set is compared SORTED, and `needs-work` sorts between `nd` and `nm` where `reject` sorted first. Carrying the old order across would have been green-looking nonsense. - `a_cc_verdict_carries_the_machines_citation_read_under_the_vetters_note` — the close-candidate comment's expected string had been swept into `needs-work`; the CC verb is not this state. - `the_vetter_prompt_makes_the_note_say_what_it_read` — same, on a prompt bullet about rejecting a FLAG. Counts re-derived from merged reality rather than carried across: 22 tools declared, the shipped command set is 8 files, and no count assertion was retyped. Gates at this commit: `cargo test` 1052+3+9+47+8+7+4 passed 0 failed; CI-exact `rainix-rs-static` at rainix 53e96a7 with the changed file touched first; `pre-commit run --all-files` all hooks passed; `nix build .#pr-review-report`. Note for the record: this branch renames `migrate-reject` to `migrate-needs-work` as a permanent subcommand. The live labels were already migrated before this PR reached that step, so the verb is spent — under the standing ruling that a migration is an execution vehicle, it should come out, which is a separate change from this merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… drain `migrate-needs-work` was the #133 one-shot: move every open PR still carrying the RETIRED PR-side `human:needs-work` onto `ai:needs-work`. It has run. Across `rainlanguage` + `cyclofinance` + `S01-Issuer` the open-PR population on that label is now ZERO (the two remaining subjects are closed PRs), which is the same residue-zero bar #225 held `migrate-blocked-deploy` to. Per the standing ruling on #221 a migration is an execution vehicle rather than permanent machinery, so the verb comes out with the state it drained — exactly as `migrate-blocked-deploy` (#225) and `migrate-design` (#224) did, each leaving only a test that the subcommand no longer parses. There is no separate `migrate-reject`: this branch had renamed that verb to `migrate-needs-work`, so both spellings go with this one deletion. Done compiler-first — the `Cmd` variant, its dispatch arm, the plan/mode machinery and its tests, then `RETIRED_HUMAN_NEEDS_WORK_LABEL` itself, following every rustc error rather than re-literalising anything to keep the tree compiling. That cascade is larger than the subcommand, because the retired label was the last thing holding the PR-side sacred-label machinery up: - `PR_SACRED_LABELS` had one entry, so it and `has_human_override` are gone. A PR's human-sacredness is now exactly what #133/#219 said it should be: a native review, or a `👤 human` ruling pinned to the current head. No label. - `PrSignals.human_parked` and `next_action`'s parking arm — #225's list names this one explicitly. Parking is now a modeled `ai:*` state, not a namespace. - `STATE_HUMAN_NEEDS_WORK`, its `classify_lane` arm, its daily-review section, and `Lane::HumanDecisions` — the lane held that one state, so it drained to nothing and came out with it. - `DesignHit::HumanRuled` and `/nd`'s `humanRuled` count, whose only producer was `has_human_override`. `aiDesign`'s stated partition loses that term. - The worklist row's `humanOverride` key, for the same reason. Nothing outside the binary read it. `human:needs-work` is NOT added to `DELETED_STATE_LABELS`, deliberately. That registry claims no transition can write the string again, and on an ISSUE that is false: `human-rule-issue needs-work` still writes it and 21 open issues carry it today. Only the PR-side state was removed, so the lane-arithmetic test names the string locally and says why. What the compiler could NOT see, and the suite caught: fourteen tests built their human-sacred witness from the string literal `"human:needs-work"` rather than the constant, so they compiled clean and then failed — the verdict/scope/ lens/convention gates, the MCP refusal, `producer_state_plan`, the state-load fixture's 36-PR cohort, and the golden descriptor and lane rows. Each now uses a form that is still a human decision (a native review), or asserts the new truth. The last grep sweep then found what neither reached: prose in README.md, CLAUDE.md, `keep-open.md` and `nm.md`, plus the subcommand's row in CLAUDE.md's table — seven documentation sites with no compiler or test coverage at all. Counts re-derived from the merged table rather than carried across: `STATE_DESCRIPTORS` 12 -> 11, `REVIEW_LANE_SECTIONS` 8 -> 7, `Lane::ALL` 6 -> 5. Gates: `cargo test` 1047+3+9+47+8+7+4 passed 0 failed; CI-exact `rainix-rs-static` at rainix 53e96a7 with the changed file touched first; `pre-commit run --all-files`; `nix build .#pr-review-report`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ruling (verbatim)
What this does
human:designis deleted from the FSM — the label, on both subject types, and every special case that existed only to serve it. A design ruling IS its answer, the answer IS producer work, and the machine's next mover is the producer from the moment it is written.PR side — one send-back state, whichever verb ruled
human-rule <owner/repo> <pr> design "<answer>" --rework "<order>"now lands the PR in exactly the statereject --reworkdoes:ai:reject, every otherai:*cleared (ai:designincluded), the trustedRework note @<sha>: …work order posted in the same call at the same head-sha anchor. The producer picks it up through theai:rejectlabel exactly as it picks up any reject.--parkis removed from the CLI and the MCP schema — there is no parked spelling; a question still open is already theai:designstate. The sha-pinned👤 humancomment still records the ruling word (Ruled <sha>: design — …), so the record keeps the distinction the label machinery no longer carries.The vocabularies are now verb→target tables (
HUMAN_PR_RULINGS/HUMAN_ISSUE_RULINGS): both PR verbs map toai:reject; the map is not one-to-one in either direction, which is why the label-array-derives-the-verbs shape had to go.Issue side — comment-only
designleaves the issue vocabulary's label space: it validates, requires--reworklike reject, posts the pinnedRuled …: design — <answer>comment plus the work order, and writes NO label. The issue stays in (or returns to) the producer backlog and is worked per the ruling comment.Deleted machinery (semantic removal, read-verified)
Delegationordered/executed discriminant anddesign_delegation;verdict_plan— no AI transition removes ahuman:*label at all any more; the migrations are the only movers;rework-rulingworklist routing,NextAction::ReworkRuling, thehuman_work_ordersignal and thehumanWorkOrderrow field — replaced, in this PR, bynextAction: rework-reject: see "Also here: the typed reject work order (worklist has no row for a reject work order, so the producer prompt discovers it with raw gh search #229)" below;pr_human_sacrednarrowing — the predicate survives assacred label ∨ native review ∨ ruling-at-head; with the narrowing gone,PR_PARKED_HUMAN_LABELS/pr_absolutely_parkedbecame byte-identical twins ofPR_SACRED_LABELS/has_human_overrideand were collapsed into them;PR_SACRED_LABELSentry,HUMAN_DECISION_LABELSand the lane special-case, thehumanDesigncount and the HUMAN-DESIGN dash section, and thelabel_metaregistration. Residue still wearing the label classifies as whatever it otherwise is (un-vetted at the bottom) until the migration moves it;campaign-prompt.txt/review-prompt.txt, with prompt-conformance tests now asserting the deleted spellings are gone.Migration —
migrate-design, run once and then REMOVEDOn the
migrate-rejectprecedent (#133): a one-shot over a fixed, shrinking population, report-by-default,--applygates the write. Every open subject carryinghuman:designmigrates; nothing keeps the label.The target is decided by whether the ruling's ANSWER is recoverable, not by one blanket mapping:
👤 humanruling whose note can be posted as one →ai:reject+ that order, the state an answered design ruling produces today. Fail-safe order:ai:rejecton first, order posted next,human:designoff last, so a mid-sequence failure leaves the PR more parked than it started and still in the search population.ai:design, the machine's own state for "a human must answer this", which is the/ndqueue (/nd: next ai:design PR to rule on — next_design tool + command #226). An unanswered hold is an unanswered question; sending it toai:rejectwould assert an answer nobody gave, and land it in the producer's queue as an orderless reject — a statecampaign-prompt.txtdefines as inert ("a reject-labelled PR WITHOUT any trusted note is PARKED for a human"), so the question would go neither answered nor asked.An earlier draft of this PR claimed the note-as-order fallback always fires because "a note is mandatory on every ruling". That is false for the real population and is corrected above:
human:designpredates the marker-emitting ruling transition, so all six live residue PRs record their hold as untrusted prose — no trusted👤 humancomment, noRework note— andlast_human_ruling_notereads nothing. Every one of them is Unanswered.Dry-run transcript
Apply transcript
Post-run verification
Queried independently of the migration's own report, across all three configured orgs (
rainlanguage,cyclofinance,S01-Issuer), both subject kinds:human:designopen subjects: 0. The population is empty, so deleting the sacred-label protection strands nobody — which is what makes this PR safe to merge.rain.erc4626.words#233,#166,raindex#2778,#2721,#2720,cyclo.site#412— every one carries exactlyai:designand nothing else (no staleai:*, no leftoverhuman:*).Six PRs that were invisible holds are now six questions in the
/ndqueue, which is where a human answers them.The subcommand is gone from the merged tree
A migration is an execution vehicle, not permanent machinery (ruling). It has executed, so the final commit removes it: the
Cmdvariant and dispatch arm,migrate_design_mode,migrate_design_pr_plan,migrate_design_issue_carries,migrate_design_search,MigrateDesignPlan,MigrateOrder,last_human_ruling_note,SEND_BACK_RULINGS,DELETED_DESIGN_LABEL, and every test that existed only to exercise them — 766 deletions. Nomigrate_design/migrate-designidentifier survives; the sole remaining mention is the parse test pinning the subcommand as gone.DELETED_DESIGN_LABELwas removed compiler-first, not re-literalised. Following the resulting warnings surfacedrework_note_anchoras newly dead: it parsed the work-order pin fordesign_delegation(deleted earlier in this PR) and then for the migration, and nothing in the binary reads a pin back any more — the producer does, from the emitted text. So the parser goes, and its test is re-pointed at what is still a live contract: thatrework_note_commentemitsRework note @<head>: …in exactly the shape the producer's read expects.Kept, because they pin the FSM rather than the migration:
human:designdoes not classify into any lane, parks nothing, blocks no producer flag, has nolabel_metarow, and issue-sidedesignwrites no label.Label-definition sweep
The label is deleted at the source too, as PR 225 did for
ai:blocked-deploy. Population was already zero, so nothing was destroyed.rainlanguage,cyclofinance,S01-Issuer)human:designrainlanguage/rain.webapp,HTTP 403: Repository was archived so is read-onlyRepos swept:
issue-pr-cron,raindex,rain.erc4626.words,rain.flare,rainlang,rainix,rain.vats,rain.solver,rain.math.float,rain.strategies,rain.math.fixedpoint,rain.metadata,rain.subgraph.docker,dotrain,sushiswap,cyclofinance/cyclo.site,cyclofinance/cyclo.sol.rain.webappis a known-archived repo already outside the pipeline's scope; its read-only label list is inert.Docs
README FSM: the
hdesignnode and bothready --> hdesignedges are gone;ready --> reject : human-rule design --reworkanddesign --> reject : human answers · same send-back as a rejectionreplace them; the--rework/--parkprose follows. CLAUDE.md invariants,plugins/human-fsm/commands/design.md,reject.md, the plugin README and both cron prompts describe the new routing.Also here: the typed reject work order (#229)
The deletion above takes
NextAction::ReworkRulingout — the one typed expression of "a human sent this PR back". Nothing else inworklistcould say it, so after this PR an answered design question would have been discoverable only throughcampaign-prompt.txtstep 3's rawgh search prs --author … --state open --label ai:reject, in a repo whose contract is that producer and vetter prompts route ALL GitHub I/O throughpr-review-report. The PR that removes the typed path supplies the replacement rather than shipping the gap.nextAction: rework-rejectai:rejectis ONE state whoever ruled it (#133/#219) — but it is not one action, and the prompt has always said so. A reject the tool can read a trusted instruction for is the producer's work order; a reject with nothing trusted behind it is "PARKED for a human, open NO new PR". Those are two rows now, and they cannot collapse:ai:reject+ a trusted instructionrework-reject— rework that same branch per the instructionai:reject, nothing trusted behind itparked-skip— parked for a humanThe instruction has three spellings, because three transitions write the one state: the vetter's own
Reviewed <sha>: reject — …verdict, the human'sRework note, and the human's👤 humanruling. All three are read back throughtrusted_comments, which filters by author before it looks at any marker — the same shared predicate the prompt already names, not a second notion of trust. Every marker is public body text a third party can post, so the same words from another account produce norework-rejectrow.Reading only the two human spellings was the tempting narrow reading, and it is wrong on the real population: of 12 sampled open
ai:rejectPRs across the three configured orgs, 8 carry only a vetter reject verdict — noRework note, no👤 human. A two-marker predicate would have parked two thirds of the live reject fleet on a human who has nothing to do, which is the opposite of "a vetter verdict and a human ruling … ask you for exactly the same thing".The verdict match is
pr_verdict_lineon the verdict WORD of aReviewedline, never a substring of the body: the prose under a verdict line is model-authored, soclose — superseded, so a reject would be wrong herenames the word without being one.pr_close_verdict_lineis now that same reader at"close", so there is one parse of a verdict line rather than two.Priority and reporting
rework-rejectis decided after the arms that park (a sacredhuman:*label, a modeled human-gatedai:*state — a reject beside one is a contradictory hand-state and fail-safes to parked) and ahead of every derived route includingflag-migration: it is the one route a person WROTE about this exact PR, where the deploy-shaped and CI-derived routes are inferred from signals nobody wrote for it. It isaction_rank1,ACTIONABLE_ACTIONS(a work order names work, so it is enumerated infleet.actionable, not merely counted), andALL_ACTIONS— sobyActionstates its count as zero when empty, per the existing convention. The query it replaced always answered; an absent key would be the tool declining to say whether anything is sent back, which is exactly the question that sends a run back to GitHub.flag_migration_leads_the_action_rankis strengthened in place rather than duplicated: it now asserts the unknown-action rank againstparked-skip's OWN rank, and that every action a row can carry is ranked explicitly.ReworkRulinghad noaction_rankarm at all and silently sorted last — the drift that test is named for, on the variant this one replaces.campaign-prompt.txtstep 3The raw
gh search prs --author {{ASSIGNEE}} --state open --label ai:reject --json repository,numberis gone. Step 3 now consumes the typed row and says why the label alone cannot answer the question (it does not tell the two shapes apart), keeping the surrounding prose intact: one reject state whoever ruled it, the👤 humancomment as the tell that a human wrote this one, design answers arriving as the same send-back, and the two exits — rework and close. Step 2a'snextActionenumeration gainsrework-reject, andparked-skip's gloss gains the note-less reject.Verification of this addition
cargo test(nix rust-shell, rainix53e96a7d): 1088 tests, 0 failures (1010 unit + 78 across the integration binaries).rainix-rs-static(cargo fmt --all -- --check,cargo clippy --all-targets --all-features -- -D warnings -D clippy::all) withmain.rstouched first so a warm target dir cannot hide a lint: exit 0, no warnings.pre-commit run --all-files: exit 0, all 11 hooks pass (denofmtincluded — it reflowed the README paragraph, and that reflow is in the commit).nix build .#pr-review-report: exit 0.Two defects in the same code, fixed here rather than filed
Both were found while doing #229 and both live in code this PR already edits.
ALL_ACTIONS/ACTIONABLE_ACTIONShad drifted from the enum — and it was liveThe two string lists were hand-maintained beside
NextAction, and they were wrong in both directions at once:deploy, a retired spelling no variant can produce — sobyActionreported a permanent phantom zero for a class that cannot exist.flag-migration, whichaction_rankputs first. So a migration row was never stated in the histogram — breaking the zeroes-stated contract, which exists precisely so an absent class is never something a reader has to infer — and never appeared infleet.actionable, so the producer was never handed a migration PR as work. Those PRs were silently skipped.Fixing the contents alone would have left the class, which is the drift #130 already paid for once. So the vocabulary is now derived from the enum:
NextAction::ALLis the variants in dispatch order;ALL_ACTIONSis its names, computed in aconstblock.ACTIONABLE_ACTIONSis that set filtered byNextAction::names_work— not a second hand list.action_rankresolves the string to a variant viaNextAction::from_strand asks it forrank(), instead of mirroring the order in a parallelmatch. That parallel match is exactly how the retiredrework-rulingcame to have no rank at all and sort as parked while the classifier went on emitting it.Membership is compiler-enforced:
rank()andnames_work()are exhaustive, so a variant added to the enum cannot compile until somebody decides where it sorts and whether it is work.the_action_vocabulary_is_the_enumpins the rest structurally — it iterates every variant rather than the lists, because a test driven by the lists cannot see what the lists are missing, and it asserts the converse too (nothing is stated that no variant produces).Step 3d found its conflicts with a search plus a fetch per PR
The same defect as step 3's reject query, and cheaper to fix because
conflict-3dalready existed. Step 3d rangh search prs --author {{ASSIGNEE}} …and then agh pr view --json mergeStateStatus,baseRefNameper PR, to rebuild a classificationstate-loadhad already computed. It now consumes theconflict-3drow, which carries themergeStatethe step branches on, with the ban stated rather than the query merely deleted.One field is genuinely not on the row:
baseRefName, the branch to merge in. The step fetches that alone, for the PRs it actually works — which is step 2a's existing rule ("fetch a single PR directly ONLY for a detail the row does not carry"), not an exception to it.Both prompt edits are pinned by
the_reject_and_conflict_steps_consume_typed_rows_not_a_search, asserted both ways round: the query text must be gone as an instruction and present as a prohibition, because a rule that is merely deleted is one the next edit reinvents.Against live data
worklistover the real fleet (150 open PRs, 3 orgs):deployis gone from the output;flag-migrationis stated. The fleet happens to hold no migration row at this moment, sofleet.actionablecarries none to show — but the class is now stated at zero instead of being absent, and a migration row would be enumerated (and would lead the list, being rank 0). That is asserted directly rather than left to the live fleet's mood:only_the_actions_that_name_work_are_enumeratednow requiresflag-migrationto head the actionable list.The reject split is not theoretical on this data. 79 fleet rows carry
ai:reject: 58 classify asrework-reject, 21 asparked-skip. Collapsing them would have mis-driven one group or the other. And the typed set is exactly the raw query's set minusrainlanguage/rain.webapp#354— an archived repo the fleet withholds (#206), i.e. a PR the old query was handing the producer that it cannot push to.Merged with main after #225 landed
#225 merged first and left this branch CONFLICTING. Main was merged in (never rebased over the review history), and the branch is
MERGEABLEagain.Every conflict was the same symmetry — two independent deletions of an FSM state touching the same regions: #221/#225 deleted
ai:blocked-deploy, this PR deleteshuman:design. Each was resolved by taking both deletions rather than a side:classify_lane's lanes fixture now carries residue of both deleted labels, so the un-vetted bucket gains a member from each: count3 → 4, members[1, 2, 8, 12]. Neither deleted state emits a lane cell. (The count line was outside the conflict markers — both sides said3, each having deleted one state; with both deletions it is4. That is the kind of line a clean auto-merge gets wrong.)human:rejectis the human-decisions lane's one remaining state, not the deletedhuman:design— and main's spelling of the deploy label: a bare literal, not a constant, because ai:blocked-deploy: migrated as a once-off, state deleted #225 deleted the constant and re-declaring one to name a state that does not exist would be the deletion undone.producer-blockedbullet whole and this branch'shuman-decisionsbullet whole.campaign-prompt.txtauto-merged with no conflict.The interaction worth stating is
next_action, which auto-merged: main removed the deleted label from the modeled-state park arm, this branch added the reject arm directly beneath it, and the result keeps both. Re-verified explicitly after merging — a note-lessai:rejectstill classifiesparked-skip, and nothing parks on the deleted deploy label (modeled_state_label_short_circuits_to_parked,worklist_row_reject_without_a_trusted_instruction_is_parked,a_parking_state_still_beats_a_reject_work_order,the_action_vocabulary_is_the_enumall green).The compiler surfaced nothing, which is the correct result and was worth checking: #225 added and retired no
NextActionvariant, so the derivedALL_ACTIONS/ACTIONABLE_ACTIONSneeded no edit and picked up no deploy-era action. Had it changed the variant set, the exhaustiverank()/names_work()matches would have refused to compile rather than letting the arrays drift — which is the whole reason they are exhaustive. NoRETIRED_BLOCKED_DEPLOY_LABELreference survives anywhere in the tree.Verification
cargo test(nix rust-shell): 1088 tests, 0 failures — 1010 unit + 78 across the integration binaries.cargo fmt --checkclean; CI-exact clippy (-D warnings -D clippy::all) clean on every changed line (the one remaining hit is a pre-existingcollapsible_matchon an untouched line, flagged only by a clippy newer than the rainix pin).Judgment calls (spec gaps resolved)
ai:design, notai:reject— see the migration section. This is what keeps six unanswered questions from becoming inert orderless rejects, and it is the one place this PR departs from a literal reading of "PRs →ai:reject".last_human_ruling_noteonly carries a SEND-BACK verb's note forward (design/reject). A tornhuman-closeleaves a trusted close ruling on an open subject, and its note is a close reason — handing "duplicate of pr-review-report: constrained Zoltu-deploy subcommand so the producer can deploy the sanctioned way only #40" to a producer as a work order is the failure that guard prevents. Zero exposure on today's data; guarded because it is cheap.designon an issue carrying a live close-candidate flag stays refused (the flag's own question must be answered first); the refusal still names every legal move.human-close's shared plan keeps its empty-target spelling (Some("")) — a close supersedes every standinghuman:*, deliberately distinct from the comment-onlydesign(None), which supersedes nothing.is_producer_backlogkeeps its namespace-prefix exclusion: residue issues sit out untilmigrate-designstrips the label; a live design ruling writes no label, so it never leaves the backlog at all.QA
an_answered_design_lands_the_pr_exactly_where_a_reject_does,an_issue_design_ruling_writes_no_label_and_moves_none,a_ruling_delegates_and_nothing_parks,sacredness_is_authorship_and_currency,a_verdict_neither_refuses_on_nor_clears_the_deleted_design_label,worklist_has_no_design_routing_and_the_sacred_labels_still_park,migrate_design_moves_ordered_and_parked_prs_alike_and_keeps_a_live_order,the_migrations_order_text_is_the_last_trusted_rulings_note,migrate_design_searches_both_subject_kinds_for_the_deleted_label,human_ruling_vocabulary_is_the_tables, plus the--parkparse refusal inhuman_transition_subcommands_present— each fails on base (verified structurally: they name symbols base does not have —HUMAN_PR_RULINGS,MigrateDesignPlan, the 5-arghuman_rule_steps, the 2-argruling_work— or assert the inverse of base behavior, e.g. base parses--parkand bucketshuman:designinto the human-decisions lane).git status+ green pristine re-run). PASS 1 over the core change, 12 mutants, 12 killed (design→human:design→an_answered_design_lands_the_pr_exactly_where_a_reject_does; work order never posted→the_ruling_and_the_work_order_stay_two_records_and_the_ruling_leads; bare design parks→a_ruling_delegates_and_nothing_parks; comment-only ruling writes a label→an_issue_design_ruling_writes_no_label_and_moves_none; residue label parks the vetter→sacredness_is_authorship_and_currency; PR ruling clears no ai:*→a_human_reject_ruling_lands_the_pr_in_the_one_reject_state; +6 more). PASS 2 over the recoverability mapping, 10 mutants: unanswered→ai:reject killed bymigrate_design_asks_an_unanswered_ruling_as_a_question; live-order branch dropped killed bymigrate_design_sends_back_an_answered_ruling_and_keeps_a_live_order; note-fallback dropped killed by both; answered→unanswered compile-killed; close-reason-as-order killed bymigrate_design_asks_an_unanswered_ruling_as_a_question+the_migrations_order_text_is_the_last_trusted_rulings_note; migration touches non-carriers, order-from-first-ruling, design-writes-human-design, residue-parks-again all killed. ONE SURVIVOR —rsplit_once(": ")→split_once(": ")on the verb parse — was a real gap: a ruling carrying a context line aboveRuled …has an earlierWord:, so the first-colon parse takes the context as the verb and silently discards a recoverable answer. Killed by a new discriminating test inthe_migrations_order_text_is_the_last_trusted_rulings_note; re-run confirms KILLED. 22/22 across both passes.migrate-rejectprecedent, never from the implementation under test.human:design(both subject types + every special case), the PR-side design→ai:reject+order one-call send-back with--parkremoved, the issue-side comment-only ruling,migrate-designover both populations with note-as-order fallback, README FSM re-edging, and plugin/prompt doc updates; covered: all of them (the downstream rain-org-health dash box is explicitly out of scope per the issue).a_reject_with_an_instruction_and_one_without_are_different_actions,a_reject_work_order_outranks_every_derived_route,a_parking_state_still_beats_a_reject_work_order,worklist_row_reject_with_a_trusted_instruction_is_the_work_order,worklist_row_reject_without_a_trusted_instruction_is_parked,worklist_row_trusted_notes_without_the_label_are_not_a_reject, plus therework-rejectassertions folded intothe_histogram_states_every_action_including_the_zeroesand the every-action-is-ranked loop added toflag_migration_leads_the_action_rank. Each fails on base structurally — base has noRejectState, noNextAction::ReworkRejectand norework-rejectstring, so base cannot compile or cannot satisfy them.main.rsasserted equal to baseline after every restore; cleangit status; pristine baseline re-run green). 10/10 KILLED — including the three that matter most: collapsing the two reject shapes into one row (RejectState::Parked => ReworkReject) killed bya_reject_with_an_instruction_and_one_without_are_different_actions+worklist_row_reject_without_a_trusted_instruction_is_parked; accepting an untrusted note as a work order (reject_instructionreading raw comment bodies instead oftrusted_comments) killed byworklist_row_reject_without_a_trusted_instruction_is_parked; and dropping the zero-count emission (rework-rejectout ofALL_ACTIONS) killed bythe_histogram_states_every_action_including_the_zeroes+only_the_actions_that_name_work_are_enumerated. Full table in a PR comment.campaign-prompt.txtstep 3 as it stood before this PR — "a PR labelledai:reject… whose PR ALSO carries a TRUSTED rework instruction … is YOUR WORK ORDER" and "a reject-labelled PR WITHOUT any trusted note is PARKED for a human" — plus its own next sentence, "a vetter verdict and a human ruling both writeai:reject, they ask you for exactly the same thing". Expected classifications are derived from that prose and checked against the LIVE population (12 sampled openai:rejectPRs), not from the implementation: 8 of the 12 carry only a vetter verdict, which is what rules out the narrower two-marker reading of the instruction.NextActionvariant for a reject carrying a trusted rework instruction, the note-less reject classifying differently, both wired throughworklist/state-loadwith thebyActioncount stated as zero rather than absent, the rawgh search prsin step 3 replaced by the typed row, and pins on the two shapes + the spoof; covered: all of them, plus theaction_rankgap that madeReworkRulingsort as parked.the_action_vocabulary_is_the_enum(iterates everyNextActionvariant, asserts each is inALL_ACTIONSand is enumerated as work iffnames_work, plus the converse that nothing is stated which no variant produces), the every-variant rank agreement folded intoflag_migration_leads_the_action_rank, theflag-migration-heads-the-actionable-list assertion added toonly_the_actions_that_name_work_are_enumerated, the histogram test re-driven off the enum instead of offALL_ACTIONS, andthe_reject_and_conflict_steps_consume_typed_rows_not_a_searchfor both prompt steps. Each fails on base: base has noNextAction::ALL,from_str,rankornames_work, and base's prompt carries the two queries the prompt test asserts are absent-as-instruction and present-as-prohibition.f66244d, 21/21 KILLED (the pre-merge run was 20/20; a headline in an earlier PR comment miscounted it as 22 — the per-section tables there were always correct), bothmain.rsandcampaign-prompt.txtrestored byte-identical (sha256 asserted per mutant). Coverage of the coordinator-named cases: a variant retired from the vocabulary list without touching the enum (vocabulary-list-forgets-a-variant) killed bythe_action_vocabulary_is_the_enum+ 3 more; a retired spelling outliving its variant (retired-spelling-outlives-its-variant,as_strreturningdeploy) killed by the same test + the histogram;flag-migrationdropped from the actionable set killed bythe_action_vocabulary_is_the_enum+only_the_actions_that_name_work_are_enumerated. Plus both prompt queries reinstated, each killed by the conformance test. Full table in a PR comment.byActioncontract ascampaign-prompt.txtstep 2 states it — "a count for EVERY action, zeroes stated, so an absent class is never something you have to infer" — checked against the enum rather than against the lists, and against the live fleet (150 PRs) whereflag-migrationwas absent from the histogram entirely anddeploywas stated as a permanent zero.NextActionin both directions, with the consequence that migration rows are neither counted nor handed over as work, and (b) step 3d re-deriving a typed classification with a search plus a per-PR fetch; covered: both, fixed at the class (lists derived from the enum, membership compiler-enforced) rather than by correcting the contents, with the one field the row genuinely lacks (baseRefName) named rather than left as a reason to keep the query.Closes #219
Closes #229
Refs:
🤖 Generated with Claude Code
Summary by CodeRabbit
human:designstate.