Skip to content

human-rule: an answered design routes to ai:reject, same send-back as a rejection - #224

Merged
thedavidmeister merged 9 commits into
mainfrom
2026-08-06-design-answer-routes-to-reject
Aug 7, 2026
Merged

human-rule: an answered design routes to ai:reject, same send-back as a rejection#224
thedavidmeister merged 9 commits into
mainfrom
2026-08-06-design-answer-routes-to-reject

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The ruling (verbatim)

me answering the question should immediately go back to the producer for rework, same as a rejection

i said delete, migrate everything away and remove thoroughly

What this does

human:design is 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 state reject --rework does: ai:reject, every other ai:* cleared (ai:design included), the trusted Rework note @<sha>: … work order posted in the same call at the same head-sha anchor. The producer picks it up through the ai:reject label exactly as it picks up any reject. --park is removed from the CLI and the MCP schema — there is no parked spelling; a question still open is already the ai:design state. The sha-pinned 👤 human comment 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 to ai: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

design leaves the issue vocabulary's label space: it validates, requires --rework like reject, posts the pinned Ruled …: 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)

  • the Delegation ordered/executed discriminant and design_delegation;
  • the human-rule can park a PR but cannot delegate it — the rework note has no tool #111 clearing-by-execution arm in verdict_plan — no AI transition removes a human:* label at all any more; the migrations are the only movers;
  • the rework-ruling worklist routing, NextAction::ReworkRuling, the human_work_order signal and the humanWorkOrder row field — replaced, in this PR, by nextAction: 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;
  • the pr_human_sacred narrowing — the predicate survives as sacred label ∨ native review ∨ ruling-at-head; with the narrowing gone, PR_PARKED_HUMAN_LABELS / pr_absolutely_parked became byte-identical twins of PR_SACRED_LABELS / has_human_override and were collapsed into them;
  • the PR_SACRED_LABELS entry, HUMAN_DECISION_LABELS and the lane special-case, the humanDesign count and the HUMAN-DESIGN dash section, and the label_meta registration. Residue still wearing the label classifies as whatever it otherwise is (un-vetted at the bottom) until the migration moves it;
  • the delegated-design paragraphs in campaign-prompt.txt / review-prompt.txt, with prompt-conformance tests now asserting the deleted spellings are gone.

Migration — migrate-design, run once and then REMOVED

On the migrate-reject precedent (#133): a one-shot over a fixed, shrinking population, report-by-default, --apply gates the write. Every open subject carrying human:design migrates; nothing keeps the label.

The target is decided by whether the ruling's ANSWER is recoverable, not by one blanket mapping:

  • Answered — a trusted work order pinned at the head, or a trusted 👤 human ruling whose note can be posted as one → ai:reject + that order, the state an answered design ruling produces today. Fail-safe order: ai:reject on first, order posted next, human:design off last, so a mid-sequence failure leaves the PR more parked than it started and still in the search population.
  • Unanswered — neither → ai:design, the machine's own state for "a human must answer this", which is the /nd queue (/nd: next ai:design PR to rule on — next_design tool + command #226). An unanswered hold is an unanswered question; sending it to ai:reject would assert an answer nobody gave, and land it in the producer's queue as an orderless reject — a state campaign-prompt.txt defines as inert ("a reject-labelled PR WITHOUT any trusted note is PARKED for a human"), so the question would go neither answered nor asked.
  • Issues → the label is stripped; the ruling comment stays the record and the issue re-enters the producer backlog.

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:design predates the marker-emitting ruling transition, so all six live residue PRs record their hold as untrusted prose — no trusted 👤 human comment, no Rework note — and last_human_ruling_note reads nothing. Every one of them is Unanswered.

Dry-run transcript

6 open PR(s) carry the deleted human:design (report only; pass --apply to write)
  [report] cyclofinance/cyclo.site#412 -> add ai:design , remove human:design  [no recoverable answer — asked as a question, for /nd]
  [report] rainlanguage/rain.erc4626.words#233 -> add ai:design , remove human:design  [no recoverable answer — asked as a question, for /nd]
  [report] rainlanguage/rain.erc4626.words#166 -> add ai:design , remove human:design  [no recoverable answer — asked as a question, for /nd]
  [report] rainlanguage/raindex#2778 -> add ai:design , remove human:design  [no recoverable answer — asked as a question, for /nd]
  [report] rainlanguage/raindex#2721 -> add ai:design , remove human:design  [no recoverable answer — asked as a question, for /nd]
  [report] rainlanguage/raindex#2720 -> add ai:design , remove human:design  [no recoverable answer — asked as a question, for /nd]
0 open issue(s) carry the deleted human:design (report only; pass --apply to write)
would migrate: 6 subject(s), 6 PR(s) asked as questions (ai:design, no recoverable answer)

Apply transcript

6 open PR(s) carry the deleted human:design — answered ones to ai:reject + work order, unanswered ones to ai:design
  cyclofinance/cyclo.site#412 -> ai:design  [no recoverable answer — asked as a question, for /nd]
  rainlanguage/rain.erc4626.words#233 -> ai:design  [no recoverable answer — asked as a question, for /nd]
  rainlanguage/rain.erc4626.words#166 -> ai:design  [no recoverable answer — asked as a question, for /nd]
  rainlanguage/raindex#2778 -> ai:design  [no recoverable answer — asked as a question, for /nd]
  rainlanguage/raindex#2721 -> ai:design  [no recoverable answer — asked as a question, for /nd]
  rainlanguage/raindex#2720 -> ai:design  [no recoverable answer — asked as a question, for /nd]
0 open issue(s) carry the deleted human:design — stripping the label (the ruling comment stays the record)
migrated: 6 subject(s), 6 PR(s) asked as questions (ai:design, no recoverable answer)

Post-run verification

Queried independently of the migration's own report, across all three configured orgs (rainlanguage, cyclofinance, S01-Issuer), both subject kinds:

  • human:design open subjects: 0. The population is empty, so deleting the sacred-label protection strands nobody — which is what makes this PR safe to merge.
  • Each migrated PR's live label set, spot-checked: rain.erc4626.words#233, #166, raindex#2778, #2721, #2720, cyclo.site#412 — every one carries exactly ai:design and nothing else (no stale ai:*, no leftover human:*).

Six PRs that were invisible holds are now six questions in the /nd queue, 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 Cmd variant 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. No migrate_design / migrate-design identifier survives; the sole remaining mention is the parse test pinning the subcommand as gone.

DELETED_DESIGN_LABEL was removed compiler-first, not re-literalised. Following the resulting 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 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: that rework_note_comment emits Rework note @<head>: … 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.

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.

Repos scanned (rainlanguage, cyclofinance, S01-Issuer) 172
Repos that defined human:design 18
Deleted successfully 17
Could not be deleted 1rainlanguage/rain.webapp, HTTP 403: Repository was archived so is read-only
Post-sweep re-scan of those 18 1 still defines it — the archived repo, unchangeable by anyone
Open subjects carrying the label, org-wide, both kinds 0

Repos 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.webapp is a known-archived repo already outside the pipeline's scope; its read-only label list is inert.

Docs

README FSM: the hdesign node and both ready --> hdesign edges are gone; ready --> reject : human-rule design --rework and design --> reject : human answers · same send-back as a rejection replace them; the --rework/--park prose 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::ReworkRuling out — the one typed expression of "a human sent this PR back". Nothing else in worklist could say it, so after this PR an answered design question would have been discoverable only through campaign-prompt.txt step 3's raw gh search prs --author … --state open --label ai:reject, in a repo whose contract is that producer and vetter prompts route ALL GitHub I/O through pr-review-report. The PR that removes the typed path supplies the replacement rather than shipping the gap.

nextAction: rework-reject

ai:reject is 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:

shape row
ai:reject + a trusted instruction rework-reject — rework that same branch per the instruction
ai:reject, nothing trusted behind it parked-skip — parked for a human

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 back through trusted_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 no rework-reject row.

Reading only the two human spellings was the tempting narrow reading, and it is wrong on the real population: of 12 sampled open ai:reject PRs across the three configured orgs, 8 carry only a vetter reject verdict — no Rework 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_line on the verdict WORD of a Reviewed line, never a substring of the body: the prose under a verdict line is model-authored, so close — superseded, so a reject would be wrong here names the word without being one. pr_close_verdict_line is now that same reader at "close", so there is one parse of a verdict line rather than two.

Priority and reporting

rework-reject is decided after the arms that park (a sacred human:* label, a modeled human-gated ai:* state — a reject beside one is a contradictory hand-state and fail-safes to parked) and ahead of every derived route including flag-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 is action_rank 1, ACTIONABLE_ACTIONS (a work order names work, so it is enumerated in fleet.actionable, not merely counted), and ALL_ACTIONS — so byAction states 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_rank is strengthened in place rather than duplicated: it now asserts the unknown-action rank against parked-skip's OWN rank, and that every action a row can carry is ranked explicitly. ReworkRuling had no action_rank arm at all and silently sorted last — the drift that test is named for, on the variant this one replaces.

campaign-prompt.txt step 3

The raw gh search prs --author {{ASSIGNEE}} --state open --label ai:reject --json repository,number is 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 👤 human comment 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's nextAction enumeration gains rework-reject, and parked-skip's gloss gains the note-less reject.

Verification of this addition

  • cargo test (nix rust-shell, rainix 53e96a7d): 1088 tests, 0 failures (1010 unit + 78 across the integration binaries).
  • CI-exact static — rainix-rs-static (cargo fmt --all -- --check, cargo clippy --all-targets --all-features -- -D warnings -D clippy::all) with main.rs touched first so a warm target dir cannot hide a lint: exit 0, no warnings.
  • The full rainix hook bundle at the pinned SHA, pre-commit run --all-files: exit 0, all 11 hooks pass (denofmt included — it reflowed the README paragraph, and that reflow is in the commit).
  • nix build .#pr-review-report: exit 0.
  • Adversarial mutation pass against the committed change — table in a PR comment. Required mutants included: collapsing the two reject shapes into one row, accepting an untrusted note as a work order, and dropping the zero-count emission. File restored byte-identical after every mutant (sha256 asserted).

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_ACTIONS had drifted from the enum — and it was live

The two string lists were hand-maintained beside NextAction, and they were wrong in both directions at once:

  • They named deploy, a retired spelling no variant can produce — so byAction reported a permanent phantom zero for a class that cannot exist.
  • They omitted flag-migration, which 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 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::ALL is the variants in dispatch order; ALL_ACTIONS is its names, computed in a const block.
  • ACTIONABLE_ACTIONS is that set filtered by NextAction::names_work — not a second hand list.
  • action_rank resolves the string to a variant via NextAction::from_str and asks it for rank(), instead of mirroring the order in a parallel match. That parallel match is exactly how the retired rework-ruling came to have no rank at all and sort as parked while the classifier went on emitting it.

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 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-3d already existed. Step 3d ran gh search prs --author {{ASSIGNEE}} … and then a gh pr view --json mergeStateStatus,baseRefName per PR, to rebuild a classification state-load had already computed. It now consumes the conflict-3d row, which carries the mergeState the 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

worklist over the real fleet (150 open PRs, 3 orgs):

flag-migration 0      <- previously ABSENT from the histogram entirely
rework-reject  58
needs-3b       0
conflict-3d    2
coderabbit-3e  1
screenshot-3c  1
green-ready    26
wait           0
parked-skip    62

deploy is gone from the output; flag-migration is stated. The fleet happens to hold no migration row at this moment, so fleet.actionable carries 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_enumerated now requires flag-migration to head the actionable list.

The reject split is not theoretical on this data. 79 fleet rows carry ai:reject: 58 classify as rework-reject, 21 as parked-skip. Collapsing them would have mis-driven one group or the other. And the typed set is exactly the raw query's set minus rainlanguage/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 MERGEABLE again.

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 deletes human: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: count 3 → 4, members [1, 2, 8, 12]. Neither deleted state emits a lane cell. (The count line was outside the conflict markers — both sides said 3, each having deleted one state; with both deletions it is 4. That is the kind of line a clean auto-merge gets wrong.)
  • 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 — 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.
  • README's lane list takes main's producer-blocked bullet whole and this branch's human-decisions bullet whole.

campaign-prompt.txt auto-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-less ai:reject still classifies parked-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_enum all green).

The compiler surfaced nothing, which is the correct result and was worth checking: #225 added and retired no NextAction variant, so the derived ALL_ACTIONS / ACTIONABLE_ACTIONS needed no edit and picked up no deploy-era action. Had it changed the variant set, the exhaustive rank() / names_work() matches would have refused to compile rather than letting the arrays drift — which is the whole reason they are exhaustive. No RETIRED_BLOCKED_DEPLOY_LABEL reference survives anywhere in the tree.

Verification

  • cargo test (nix rust-shell): 1088 tests, 0 failures — 1010 unit + 78 across the integration binaries.
  • cargo fmt --check clean; CI-exact clippy (-D warnings -D clippy::all) clean on every changed line (the one remaining hit is a pre-existing collapsible_match on an untouched line, flagged only by a clippy newer than the rainix pin).
  • Adversarial mutation pass over the changed behavior — the mutant → killing-test tables are in the PR comments (12/12 on the original change, 10/10 on the recoverability mapping after one survivor was killed by a new discriminating test). All mutations restored byte-identical.

Judgment calls (spec gaps resolved)

  • Executed-shaped residue (order pinned only to a superseded head) is migrated with the ruling's note as a fresh order at the current head — "live work order at head if one exists, else the ruling's own note" taken literally.
  • A ruling with no recoverable answer maps to ai:design, not ai: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_note only carries a SEND-BACK verb's note forward (design / reject). A torn human-close leaves 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.
  • design on 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 standing human:*, deliberately distinct from the comment-only design (None), which supersedes nothing.
  • is_producer_backlog keeps its namespace-prefix exclusion: residue issues sit out until migrate-design strips the label; a live design ruling writes no label, so it never leaves the backlog at all.

QA

  • Discriminating tests: 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 --park parse refusal in human_transition_subcommands_present — each fails on base (verified structurally: they name symbols base does not have — HUMAN_PR_RULINGS, MigrateDesignPlan, the 5-arg human_rule_steps, the 2-arg ruling_work — or assert the inverse of base behavior, e.g. base parses --park and buckets human:design into the human-decisions lane).
  • Mutations applied: two passes, each applied to committed work, whole unit suite run per mutant, file restored byte-identical (verified by clean git status + green pristine re-run). PASS 1 over the core change, 12 mutants, 12 killed (design→human:designan_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 by migrate_design_asks_an_unanswered_ruling_as_a_question; live-order branch dropped killed by migrate_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 by migrate_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 above Ruled … has an earlier Word: , so the first-colon parse takes the context as the verb and silently discards a recoverable answer. Killed by a new discriminating test in the_migrations_order_text_is_the_last_trusted_rulings_note; re-run confirms KILLED. 22/22 across both passes.
  • Oracle: the verbatim human ruling on RULED: an answered design question routes straight to producer rework, same as a rejection #219 ("answering the question should immediately go back to the producer for rework, same as a rejection" / "delete, migrate everything away and remove thoroughly") plus the existing reject-path behavior (ai:reject and human:reject are one state: same owner, same action, split only by who ruled #133) as the reference state a design ruling must be indistinguishable from — expected label sets, anchors and step orders are derived from the ruling and the migrate-reject precedent, never from the implementation under test.
  • Category check: issue requires deletion of human:design (both subject types + every special case), the PR-side design→ai:reject+order one-call send-back with --park removed, the issue-side comment-only ruling, migrate-design over 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).
  • (worklist has no row for a reject work order, so the producer prompt discovers it with raw gh search #229) Discriminating tests: 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 the rework-reject assertions folded into the_histogram_states_every_action_including_the_zeroes and the every-action-is-ranked loop added to flag_migration_leads_the_action_rank. Each fails on base structurally — base has no RejectState, no NextAction::ReworkReject and no rework-reject string, so base cannot compile or cannot satisfy them.
  • (worklist has no row for a reject work order, so the producer prompt discovers it with raw gh search #229) Mutations applied: 10 mutants against the COMMITTED change, whole unit suite per mutant, file restored byte-identical each time (sha256 of main.rs asserted equal to baseline after every restore; clean git 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 by a_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_instruction reading raw comment bodies instead of trusted_comments) killed by worklist_row_reject_without_a_trusted_instruction_is_parked; and dropping the zero-count emission (rework-reject out of ALL_ACTIONS) killed by the_histogram_states_every_action_including_the_zeroes + only_the_actions_that_name_work_are_enumerated. Full table in a PR comment.
  • (worklist has no row for a reject work order, so the producer prompt discovers it with raw gh search #229) Oracle: campaign-prompt.txt step 3 as it stood before this PR — "a PR labelled ai: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 write ai:reject, they ask you for exactly the same thing". Expected classifications are derived from that prose and checked against the LIVE population (12 sampled open ai:reject PRs), 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.
  • (worklist has no row for a reject work order, so the producer prompt discovers it with raw gh search #229) Category check: issue worklist has no row for a reject work order, so the producer prompt discovers it with raw gh search #229 requires a NextAction variant for a reject carrying a trusted rework instruction, the note-less reject classifying differently, both wired through worklist/state-load with the byAction count stated as zero rather than absent, the raw gh search prs in step 3 replaced by the typed row, and pins on the two shapes + the spoof; covered: all of them, plus the action_rank gap that made ReworkRuling sort as parked.
  • (#229b) Discriminating tests for the two defects: the_action_vocabulary_is_the_enum (iterates every NextAction variant, asserts each is in ALL_ACTIONS and is enumerated as work iff names_work, plus the converse that nothing is stated which no variant produces), the every-variant rank agreement folded into flag_migration_leads_the_action_rank, the flag-migration-heads-the-actionable-list assertion added to only_the_actions_that_name_work_are_enumerated, the histogram test re-driven off the enum instead of off ALL_ACTIONS, and the_reject_and_conflict_steps_consume_typed_rows_not_a_search for both prompt steps. Each fails on base: base has no NextAction::ALL, from_str, rank or names_work, and base's prompt carries the two queries the prompt test asserts are absent-as-instruction and present-as-prohibition.
  • (#229b) Mutations: 21 against committed work at the merge commit 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), both main.rs and campaign-prompt.txt restored 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 by the_action_vocabulary_is_the_enum + 3 more; a retired spelling outliving its variant (retired-spelling-outlives-its-variant, as_str returning deploy) killed by the same test + the histogram; flag-migration dropped from the actionable set killed by the_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.
  • (#229b) Oracle: the byAction contract as campaign-prompt.txt step 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) where flag-migration was absent from the histogram entirely and deploy was stated as a permanent zero.
  • (#229b) Category check: the defects are (a) both action lists drifting from NextAction in 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

  • New Features
    • Human design rulings now require rework and route pull requests through a unified rejection workflow.
    • Updated design and ruling guidance clarifies rework instructions, vetting, and label behavior.
  • Removed
    • Removed support for parking design requests and the human:design state.
  • Documentation
    • Updated command guidance, workflow descriptions, and queue documentation to reflect the revised ruling process.
  • Chores
    • Updated the plugin version to 0.12.0.

… 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>
@thedavidmeister thedavidmeister self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Human design rulings now require producer rework. PRs use ai:reject; issues record comment-only work orders. The human:design, parking, and separate rework-ruling paths are removed from the documented workflow.

Changes

Human ruling flow

Layer / File(s) Summary
Ruling contracts and FSM semantics
CLAUDE.md, README.md
Human design rulings require rework and use the shared reject state. Parking, human:design, and related queue behavior are removed.
Design command rework behavior
plugins/human-fsm/commands/design.md, plugins/human-fsm/commands/reject.md, plugins/human-fsm/README.md, .claude-plugin/marketplace.json, plugins/human-fsm/.claude-plugin/plugin.json
The /design command requires an answer and rework order. PRs use ai:reject; issues record comment-only rulings. The plugin version changes to 0.12.0.
Worklist and vetting integration
campaign-prompt.txt, campaign-run.sh, review-prompt.txt
The separate rework-ruling and human:design workflows are removed. Stale rulings return to vetting after rework, and design candidates use ai:design.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • rainlanguage/issue-pr-cron#220 — Builds on the same ai:reject and rework semantics for a design workflow.

Possibly related PRs

Suggested labels: ai:ready

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The objectives match issue #219, but the supplied summaries show only documentation and version changes, so the required FSM implementation and migration cannot be verified. Provide the implementation, migration, and test changes, or evidence that these requirements were completed in the reviewed diff.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The documented changes and plugin version updates directly support the human:design removal and updated routing described in issue #219.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: answered design rulings now route to ai:reject with the same send-back behavior as rejection.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-06-design-answer-routes-to-reject

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA

Adversarial mutation pass over the changed behavior (adversarial-mutation-test method: commit the real work first, mutate one behavior at a time, run the whole unit suite, restore byte-identical — restore verified with a clean git status and a green pristine re-run of the full suite).

# Mutant (what it breaks) Result Killed by
M1 an answered design writes human:design again (HUMAN_PR_RULINGS design target flipped) 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
M2 the work-order comment is silently never posted (with_rework_note drops the note) KILLED (2) the_ruling_and_the_work_order_stay_two_records_and_the_ruling_leads; an_issue_design_ruling_writes_no_label_and_moves_none
M3 migrate-design touches PRs NOT carrying the label (carry guard inverted) KILLED (1) migrate_design_moves_ordered_and_parked_prs_alike_and_keeps_a_live_order
M4 migrate-design re-posts an order over one already live at head (LiveAtHead arm dead) KILLED (1) migrate_design_moves_ordered_and_parked_prs_alike_and_keeps_a_live_order
M5 a bare design parks by default again (ruling_work design/None → Bare) KILLED (2) a_ruling_delegates_and_nothing_parks; a_human_ruling_validates_or_reaches_no_effect
M6 the comment-only issue ruling still writes a label (writes_label ignored) KILLED (2) an_issue_design_ruling_writes_no_label_and_moves_none; the_write_order_never_leaves_an_unrecorded_or_unruled_subject
M7 the residue human:design label parks the vetter again (pr_human_sacred re-widened) KILLED (2) sacredness_is_authorship_and_currency; a_verdict_neither_refuses_on_nor_clears_the_deleted_design_label
M8 a PR ruling clears no ai:* (one-state sweep dropped) KILLED (3) a_human_reject_ruling_lands_the_pr_in_the_one_reject_state; an_answered_design_lands_the_pr_exactly_where_a_reject_does; a_pr_ruling_on_a_live_flag_disposes_it_structurally
M9 the migration takes the FIRST ruling's note, not the human's latest (.rev() dropped) KILLED (1) the_migrations_order_text_is_the_last_trusted_rulings_note
M10 the migration searches the wrong label (population key swapped) KILLED (1) migrate_design_searches_both_subject_kinds_for_the_deleted_label
M11 the comment-only issue design supersedes every human:* (park-discriminant inverted: a standing keep-open would be swept) KILLED (1) an_issue_design_ruling_writes_no_label_and_moves_none
M12 the issue-side design grows a label write (HUMAN_ISSUE_RULINGS design → Some("human:design")) 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

12/12 killed; no survivors, so no tests needed strengthening beyond those written with the change. --park as a CLI spelling is additionally refused at parse (pinned by human_transition_subcommands_present), and the prompt-conformance test the_marker_is_the_prompts_exact_spelling now asserts the producer prompt contains neither rework-ruling nor the deleted label.

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>
thedavidmeister added a commit that referenced this pull request Aug 7, 2026
…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>
thedavidmeister and others added 2 commits August 7, 2026 07:11
…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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA — mutation pass 2 (the recoverability mapping)

Applied to the committed change at 65d8a02/1faec92, whole unit suite per mutant, file restored byte-identical (driver asserts the restore and the pristine suite re-runs green).

# Mutant (what it breaks) Result Killed by
M13 an unanswered ruling maps back to ai:reject (the blocker's shape) KILLED migrate_design_asks_an_unanswered_ruling_as_a_question
M14 the live-order-at-head branch never fires KILLED migrate_design_sends_back_an_answered_ruling_and_keeps_a_live_order
M15 the note-as-order fallback never fires KILLED (2) migrate_design_asks_an_unanswered_ruling_as_a_question; migrate_design_sends_back_an_answered_ruling_and_keeps_a_live_order
M16 an answered ruling is reported as unanswered KILLED (compile) type error — Answered/Unanswered carry different fields, so the confusion cannot be written
M17 a close reason is taken as a work order (SEND_BACK_RULINGS guard bypassed) KILLED (2) migrate_design_asks_an_unanswered_ruling_as_a_question; the_migrations_order_text_is_the_last_trusted_rulings_note
M18 the ruling verb is read from the FIRST ": " instead of the last SURVIVED → fixed → KILLED the_migrations_order_text_is_the_last_trusted_rulings_note (new case)
M19 the migration touches PRs not carrying the label KILLED (2) migrate_design_asks_an_unanswered_ruling_as_a_question; migrate_design_sends_back_an_answered_ruling_and_keeps_a_live_order
M20 the migration takes the FIRST ruling's note, not the human's latest KILLED the_migrations_order_text_is_the_last_trusted_rulings_note
M21 an answered design writes human:design again (regression anchor) KILLED (4) human_ruling_vocabulary_is_the_tables; an_answered_design_lands_the_pr_exactly_where_a_reject_does; +2
M22 the residue label parks the vetter again (regression anchor) KILLED (2) sacredness_is_authorship_and_currency; a_verdict_neither_refuses_on_nor_clears_the_deleted_design_label

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 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 — turning an Answered PR into an Unanswered one. A discriminating case (a Context: see the thread line above a real Ruled <sha>: design — …) was added to the_migrations_order_text_is_the_last_trusted_rulings_note, asserted both at the parser and through migrate_design_pr_plan; re-running M18 against it now reports KILLED.

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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA — mutation pass 3 (the migrate-design removal)

Every mutant resurrects what the removal deleted — the state or the subcommand. Applied to the committed removal at 8b6a149, whole unit suite per mutant, file restored byte-identical (driver asserts the restore).

# 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between eed61cf and 8b6a149.

📒 Files selected for processing (11)
  • .claude-plugin/marketplace.json
  • CLAUDE.md
  • README.md
  • campaign-prompt.txt
  • campaign-run.sh
  • plugins/human-fsm/.claude-plugin/plugin.json
  • plugins/human-fsm/README.md
  • plugins/human-fsm/commands/design.md
  • plugins/human-fsm/commands/reject.md
  • pr-review-report-rs/src/main.rs
  • review-prompt.txt

Comment thread campaign-prompt.txt Outdated
Comment thread plugins/human-fsm/commands/design.md
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>
@thedavidmeister

thedavidmeister commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial mutation pass — the #229 addition (nextAction: rework-reject)

Run against the committed change at 02421fb. Each mutant: applied with an assertion that it actually changed the file (a no-op edit reporting "survived" is the failure mode this guards), whole unit suite run, then restored and the restore proved byte-identical by sha256sum — not by eye. Baseline suite green before the first mutant and the tree clean after the last.

10 mutants, 10 killed, 0 survivors.

# Mutation Result Killed by
1 Collapse the two reject shapes into one rowRejectState::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 orderreject_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_linepr_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.

thedavidmeister and others added 2 commits August 7, 2026 09:33
…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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Adversarial mutation pass — combined, at 8f1794c

Supersedes the 15-mutant run posted above: everything re-run at the new head so the earlier kills are shown to still hold beside the new ones, plus the vocabulary and prompt work. Same protocol throughout — apply, assert the file actually changed (a no-op edit reporting "survived" is the failure this guards), run the whole unit suite, restore, and prove the restore byte-identical by sha256sum on both mutated files.

22 mutants, 22 killed, 0 survivors. Baseline green before the first and after the last.

A — the #229 reject behavior (7)

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>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed f66244d: approve — read the diff independently at this head. The ruling is structural, not conventional: HUMAN_PR_RULINGS maps BOTH reject and design to ai:reject, so an answered design question has no other target to land in. Issue-side design maps to None — the pinned ruling comment is the whole record and the issue stays the producer's subject. --park is unconstructible: the only two survivors in source are a test asserting the CLI refuses it and a test asserting the refusal message never names it. human:design deletion is semantic — Delegation, design_delegation, PR_PARKED_HUMAN_LABELS, HUMAN_DECISION_LABELS, NextAction::ReworkRuling, the #111 clearing arm and the label_meta row all gone; the 53 surviving strings are tests pinning the deleted behaviour and comments explaining the absence. The migration executed (6 PRs → ai:design, zero residue org-wide) and its vehicle was then removed per the once-off ruling. Folded fixes: the typed rework-reject row replaces the producer's raw gh search (live split 58 work-orders / 21 parked, and the typed set correctly excludes an archived repo the old query was handing the producer), and the action vocabulary is now DERIVED from NextAction with exhaustive rank()/names_work(), retiring a phantom deploy action and restoring flag-migration — which ranked first yet appeared in neither list. Merge-main resolution verified by hand: zero occurrences of RETIRED_BLOCKED_DEPLOY_LABEL, the blockedDeploy emission or the migration machinery, so 225's deletion survived intact including in the relocated counts block, while 224's own side is fully present. 1088 tests, 22/22 mutants, 0 unresolved threads.
Rulings-conformance: checked against CLAUDE.md and every ruling stated for this work. (1) 'me answering the question should immediately go back to the producer for rework, same as a rejection' — one verb→target table, both land ai:reject with the work order in the same call. (2) 'i said delete, migrate everything away and remove thoroughly' — human:design deleted on both subject types, no park spelling survives, migration executed to zero residue, label definitions swept. (3) 'a subcommand? … once off' — migrate-design removed once spent; only a test asserting it no longer parses remains. (4) 'the prs are open, trying to get them right' — both defects found in review were folded into this PR rather than filed as follow-ups. (5) removal-is-semantic-not-grep — deletion done compiler-first, read-verified. (6) comments describe CURRENT behavior only. The artifact obeys each.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Adversarial mutation pass — re-run against merged main, at f66244d

Correction to the comment above first: it is headed "22 mutants, 22 killed". The run was 20 mutants, 20 killed — the per-section tables in it (7 + 6 + 2 + 5) are correct and complete; only my arithmetic in the headline was wrong. No result changes; the count does.

This run is at the merge commit with main (#225, ai:blocked-deploy deleted). Same protocol: apply, assert the file actually changed, run the whole unit suite, restore, prove the restore byte-identical by sha256sum on both mutated files.

21 mutants, 21 killed, 0 survivors — the 20 above, re-run so their kills are shown to hold against merged main, plus one for the merge itself.

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)

@thedavidmeister
thedavidmeister merged commit 171efa6 into main Aug 7, 2026
21 checks passed
thedavidmeister added a commit that referenced this pull request Aug 7, 2026
…) 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>
thedavidmeister added a commit that referenced this pull request Aug 7, 2026
…) 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>
thedavidmeister pushed a commit that referenced this pull request Aug 7, 2026
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>
thedavidmeister pushed a commit that referenced this pull request Aug 7, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant