Skip to content

Retire ai:blocked-deploy: deploys never block merges under the split lifecycle - #168

Merged
thedavidmeister merged 11 commits into
mainfrom
2026-07-31-issue-162-retire-blocked-deploy
Aug 5, 2026
Merged

thedavidmeister merged 11 commits into
mainfrom
2026-07-31-issue-162-retire-blocked-deploy

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes #162

The ruling, honored

"i don't hold the keys, it's all on github actions, the concern is more that deploys shouldn't be blocking merges at all under the new release workflows with deploy/abstract repo classification split."

Under the split lifecycle no merge waits on a deploy anywhere — deploy repos freeze per-tag records and publish on rainix-tag-release, the on-chain deploy is a decoupled manual dispatch (rainlanguage/rainix#282), library repos never deploy. So ai:blocked-deploy names a wait that no longer exists: a PR that would earn it is evidence of exactly one thing — its repo has not migrated. This PR makes that the machine's reading end to end, composing with #164's typed---blocked-by clearance: blocked-deploy collapses into the ONE blocked state whose clearance the vetter already automates.

The retirement, by the established precedent

flag-blocked-deploy is retired the way ai:relink's writer was (#135/#139) and the way every retirement here has been shaped (#108/#133):

  • The writer dies. The dispatcher arm no longer transitions anything: it refuses unconditionally (exit 2, --dry-run included, no fetch, no write) with a message naming the replacement verbatim — pr-review-report flag-blocked-on <owner/repo> <n> "<why>" --blocked-by <owner/repo#n of the repo's migration issue/PR>, filing the migration issue first if none exists. The subcommand deliberately still parses (unlike flag-blocked-infra, which is gone from the surface): a clap unknown-subcommand error names neither the why nor the repair, and the refusal is where the old spelling learns the new move.
  • The label constant survives as RETIRED (RETIRED_BLOCKED_DEPLOY_LABEL) for residue accounting only, on the kept-while-nonzero contract every retirement has used. PRODUCER_STATE_LABELS is now exactly ["ai:design", "ai:blocked-on"].
  • The prompts teach the replacement, not just the absence: campaign-prompt 3b (iv) is now the MIGRATION case (find the repo's lifecycle-migration issue/PR; file it via gh issue create — the one sanctioned issue creation — shaped as the audit skill's lifecycle-split finding; flag-blocked-on with the migration as the typed dep), and the prompt states "There is no blocked-deploy state and no producer deploy any more".

Residue plan (the 13 live PRs) — eyes-on, never auto-migrated

Per the issue's ruling this PR migrates nothing automatically:

The deploy-gate readers, enumerated (issue item 5)

Every place the binary or prompts read the deploy gate, and what happened to each:

# Reader Disposition
1 REDEPLOY_MARKER ("REQUIRES redeploy at land" body marker) Re-pointed: detection kept (residue + legacy repos), semantics now "repo not migrated"; the producer never writes it again (prompt step 6)
2 requires_redeploy() — the substring match over body + trusted comments Re-pointed: same predicate, now shape evidence; still title-blind and author-trust-filtered
3 deploy_pin_red() — red prod-pin / testProdDeploy* check names Re-pointed: the definition of a legacy-shaped repo made executable
4 deploy_confirmed_at_head() Re-pointed: no longer clears a merge gate; suppresses the migration flag for a head the old choreography already resolved (still head-scoped, short-sha aware)
5 next_action()NextAction::Deploy Re-pointedNextAction::FlagMigration ("flag-migration"): the producer's route is the blocked-on-migration flag, never deploy choreography
6 action_rank("deploy") => 0 Re-pointed"flag-migration" => 0 (new test pins the rank so a one-sided rename cannot silently sink it)
7 DeployGate enum + deploy_gate() ReplacedLegacyDeploySignal / legacy_deploy_signal(): none | repo-not-migrated, deliberately head-BLIND (repo shape, not choreography state), never a gate
8 next_ready_row "deployGate" JSON key (human MCP row) Replaced"legacyDeploySignal" — a key named "gate" whose values stopped gating would teach the old model
9 MCP next_ready tool description ("any deploy-before-merge gate") Re-pointed to the legacy signal wording
10 worklist_row markers requiresRedeploy / deployDoneAtHead Kept (keys unchanged for row consumers), comments re-pointed
11 Cmd::Deploy / deploy_mode (the manual-sol-artifacts dispatch wrapper) Kept as a manual tool — deploys still exist, decoupled from merges; every pipeline route to it is gone (prompts, next_action) and its docs now say so. Not ruled by #162, so not removed; flagging here for visibility
12 campaign-prompt step 2 worklist vocabulary (deployflag-migration), step 2z RED/GREEN bullets + WAITING-DEPLOY report vocabulary, step 3b intro + (i) env bullet + (iv) DEPLOY→MIGRATION, COMMUNICATION routing table, step 6 conventions (no marker on new PRs), step 7 scope (producer deploys nothing; gh issue create sanctioned solely for a missing migration issue) Re-pointed / retired, pinned by a new prompt-conformance test
13 review-prompt "a prod pin awaiting its deploy" Re-pointed: a red prod-pin is incidental because the repo has not migrated (the producer routes it to the migration); pinned by a new prompt-conformance test
14 README: FSM diagram (bdeploy state + its three edges + producer deploy self-edge), next_ready section's deploy-gate paragraph + bullet, lanes doc, modeled-states paragraph Re-pointed / removed, with the retirement noted in place
15 plugins/human-fsm/commands/nr.md — the deploy-before-merge gate bullet + the "not a plain merge" presentation paragraph Re-pointed: the signal is repo health, never a gate; plugin version bumped 0.6.0 → 0.7.0 in lockstep (marketplace + manifest)

Composition with #164

  • Clearance selects on the exact ai:blocked-on label; the retired residue is never touched by it (different label, no typed refs) — pinned in blocked_deploy_residue_is_producer_blocked_not_the_vetters_to_clear and by the existing live_blocked_on_flag fixture where a Blocked-deploy: note never matches.
  • The replacement flag the refusal and the prompts teach is ai:blocked-on sits with the vetter: typed dependency refs, automated clearance on dep merge, human polling removed #164's exact spelling, so a migration dep clears automatically the run after the migration lands and the PR re-enters vetting fresh.

No VET_PROTOCOL bump

Verified: what vetting MEANS is unchanged — no new mandatory gate, no verdict-vocabulary change; the vetter's only edit is the re-pointed incidental-red example. VET_PROTOCOL stays 4.

Dash follow-up (out of scope)

The rain-org-health follow-up — retire the BLOCKED-DEPLOY box and fold blockedDeploy history, the #147/#151 shape — is deliberately NOT in this PR. It is gated on the residue count reaching zero (the kept-while-nonzero contract this PR preserves), and the emission keeps serving the key it reads until then.

QA

  • Discriminating tests: retired_flag_blocked_deploy_refuses_naming_the_replacement, the_retired_labels_are_no_longer_destinations, deploy_trigger_routes_to_flag_migration_even_when_green, worklist_row_red_prodpin_is_flag_migration, worklist_row_requires_redeploy_green_is_flag_migration, the_legacy_deploy_signal_is_read_from_the_body_not_the_title, a_deploy_confirmation_does_not_erase_the_repo_shape_signal, blocked_deploy_residue_is_producer_blocked_not_the_vetters_to_clear (the ai:ready-dominance leg), the_retired_blocked_deploy_count_keeps_its_key_and_counts_the_residue, flag_migration_leads_the_action_rank, the_producer_prompt_routes_deploy_shaped_blocks_to_the_migration, the_vetter_prompt_reads_a_red_prod_pin_as_migration_evidence_not_a_deploy_wait — each fails on base (on base the writer transitions instead of refusing, PRODUCER_STATE_LABELS has 3 members, the classifier answers deploy, the signal type does not exist, and both prompts teach the deploy choreography; verified by construction against the pre-change code paths and by the mutation battery below, which reverts each behavior piecewise and watches the suite go red).
  • Mutations applied: run from the committed baseline, one at a time, restored after each — next_action residue-park clause dropped → modeled_state_label_short_circuits_to_parked FAILED; !deploy_done_at_head suppression dropped → deploy_trigger_routes_to_flag_migration_even_when_green FAILED; legacy_deploy_signal stuck at None and, separately, blinded to red prod-pins → signal test FAILED both times; refusal stripped of the flag-blocked-on replacement → refusal test FAILED; residue check deleted from classify_lane → residue-lane test FAILED; emission's blockedDeploy key re-pointed at the wrong bucket → kept-key test FAILED; as_str drifted back to "deploy" → worklist test FAILED; action_rank keyed on the old spelling → rank test FAILED; prompt re-taught WAITING-DEPLOY / 3b(iv) reverted to DEPLOY → producer-prompt test FAILED both times; review-prompt reverted to "awaiting its deploy" → vetter-prompt test FAILED. 11/11 killed, tree clean after battery.
  • Oracle: the human's verbatim ruling in issue Retire ai:blocked-deploy: deploys never block merges under the split lifecycle — collapse into typed ai:blocked-on refs to the repo's migration #162 plus the repo's own retirement precedents (Producer should exit and log an error when infra is down — and blocked-infra should not be a state #108 blocked-infra, ai:reject and human:reject are one state: same owner, same action, split only by who ruled #133 human:reject, ai:relink is a reject with a specific note — and the producer has no tool to execute it #135/retire the ai:relink verdict, and give the producer the linkage repair it never had #139 ai:relink) and ai:blocked-on sits with the vetter: typed dependency refs, automated clearance on dep merge, human polling removed #164's clearance contract — expected behaviors derived from those records, not from the implementation under test.
  • Category check: issue asks (1) stop producing the state, (2) replace with typed blocked-on migration refs, (3) eyes-on triage of the 13 residue PRs, (4) dash follow-up after zero, (5) re-point requires-redeploy detection; covered (1), (2), (5) in code+prompts, (3) as the preserved-visibility machinery the human pass runs on (the pass itself is the human's, by the issue's own ruling), (4) explicitly deferred and named above as gated on residue zero.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added migration-aware handling for legacy deployment markers and production-pin issues.
    • Added clearer routing to repository lifecycle migration work, including migration status and hand-off guidance.
    • Legacy deployment indicators remain visible for manual review without blocking merges.
    • Deployment actions are now handled manually rather than as merge gates.
  • Bug Fixes

    • Infrastructure outages no longer modify affected pull requests.
    • CI failures caused by known environmental or migration conditions are handled more accurately.
  • Documentation

    • Updated pipeline, review, and human-queue guidance to reflect the revised release workflow.

thedavidmeister and others added 4 commits July 31, 2026 11:44
…lifecycle

flag-blocked-deploy is removed from the producer's vocabulary the way
ai:relink's writer was (#135/#139): the subcommand still parses but refuses,
naming the replacement (flag-blocked-on --blocked-by <the repo's migration
ref>), and the label constant survives as RETIRED for residue accounting only.
The queue emission keeps the blockedDeploy key while the count can be non-zero;
the 13 live PRs stay visible for the human's eyes-on triage, parked (unlike
the #108 residue) so no automatic pass re-derives them.

The deploy-gate reading is re-pointed end to end: NextAction::Deploy becomes
FlagMigration, DeployGate becomes LegacyDeploySignal (none/repo-not-migrated,
head-blind, never a merge gate), and the prompts stop teaching the deploy
choreography — a deploy-shaped block is a repo-migration dependency, flagged
ai:blocked-on with the migration issue/PR as its typed dep, filed if none
exists.

Closes #162

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 31, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA evidence for this PR, run on the branch as pushed.

Full suite (nix develop .#rust -c cargo test): every suite green at c620fb7 (the feature commit) — 600+ unit tests plus the run_timings / run_usage / usage_gate_skip integration suites, exit 0. Subsequent commits are cargo-fmt only, denofmt-on-markdown only, and one new test (flag_migration_leads_the_action_rank, run green in isolation).

Static gate, CI's exact spelling: nix develop github:rainlanguage/rainix/53e96a7d#rust-shell -c rainix-rs-static && pre-commit run --all-files — clean (the denofmt reflow it produced on README.md/nr.md is committed as its own commit, same as c904a98 on main).

Mutation battery — 12 mutants, applied one at a time to the committed baseline, each restored before the next; every one killed by the named test, tree clean afterwards:

Mutant Killing test Result
next_action residue-park clause dropped (blocked-deploy stops parking) modeled_state_label_short_circuits_to_parked KILLED
!deploy_done_at_head suppression dropped (confirmed head still flags) deploy_trigger_routes_to_flag_migration_even_when_green KILLED
legacy_deploy_signal stuck at None the_legacy_deploy_signal_is_read_from_the_body_not_the_title KILLED
legacy_deploy_signal blinded to red prod-pins the_legacy_deploy_signal_is_read_from_the_body_not_the_title KILLED
refusal stripped of the flag-blocked-on replacement retired_flag_blocked_deploy_refuses_naming_the_replacement KILLED
residue check deleted from classify_lane blocked_deploy_residue_is_producer_blocked_not_the_vetters_to_clear KILLED
emission's blockedDeploy re-pointed at the wrong bucket the_retired_blocked_deploy_count_keeps_its_key_and_counts_the_residue KILLED
NextAction::as_str drifted back to "deploy" worklist_row_red_prodpin_is_flag_migration KILLED
action_rank keyed on the old "deploy" spelling flag_migration_leads_the_action_rank KILLED
campaign-prompt re-teaches WAITING-DEPLOY the_producer_prompt_routes_deploy_shaped_blocks_to_the_migration KILLED
campaign-prompt 3b(iv) reverted to (iv) DEPLOY the_producer_prompt_routes_deploy_shaped_blocks_to_the_migration KILLED
review-prompt reverted to "a prod pin awaiting its deploy" the_vetter_prompt_reads_a_red_prod_pin_as_migration_evidence_not_a_deploy_wait KILLED

Clearance interplay with #164 (a blocked-deploy PR is not the vetter's to clear): the state-load selects the clearance path on the exact ai:blocked-on label; the residue lane test pins blocked-deploy to producer-blocked (never vet-lifecycle), and the pre-existing the_live_flag_is_the_most_recent_blocked_on_comment fixture already proves a Blocked-deploy: note never reads as a live blocked-on flag.

No VET_PROTOCOL bump: verified unchanged at 4 — no new mandatory gate, no verdict-vocabulary change; the vetter edit is a re-pointed prose example only.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 134fb450-8a44-4ec3-b211-c121f2d656f1

📥 Commits

Reviewing files that changed from the base of the PR and between c54db14 and 8793a16.

📒 Files selected for processing (6)
  • .claude-plugin/marketplace.json
  • README.md
  • campaign-prompt.txt
  • plugins/human-fsm/.claude-plugin/plugin.json
  • pr-review-report-rs/src/main.rs
  • review-prompt.txt

Walkthrough

The pipeline retires deploy-before-merge blocking. It reports repository migration evidence through legacyDeploySignal, routes affected PRs to typed flag-migration handling, preserves retired residue for manual triage, and updates prompts and documentation.

Changes

Deploy lifecycle migration

Layer / File(s) Summary
Retire deploy state and command
pr-review-report-rs/src/main.rs
The producer removes deploy-blocked writes. The retired command remains parseable and refuses without state changes.
Derive migration signal and route actions
pr-review-report-rs/src/main.rs
Worklists expose legacyDeploySignal. FSM routing, dispatch, ranking, confirmations, and tests now use flag-migration.
Preserve residue and lane behavior
pr-review-report-rs/src/main.rs
Retired residue remains visible, counted, and producer-blocked. Lane classification and verdict fetching use the updated semantics.
Update workflow contracts and documentation
README.md, campaign-prompt.txt, plugins/human-fsm/commands/nr.md, review-prompt.txt, .claude-plugin/marketplace.json, plugins/human-fsm/.claude-plugin/plugin.json
Prompts and documentation describe typed migration dependencies, non-gating legacy signals, prohibited producer deploy actions, and plugin version 0.7.0.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Worklist
  participant LegacyDeploySignal
  participant FSM
  participant MigrationIssue
  Worklist->>LegacyDeploySignal: derive repository migration evidence
  LegacyDeploySignal->>FSM: report repo-not-migrated
  FSM->>MigrationIssue: route flag-migration with typed dependency
  MigrationIssue-->>FSM: current-head confirmation suppresses routing
Loading

Possibly related issues

  • rainlanguage/issue-pr-cron#161: Introduces the typed ai:blocked-on state and its dependency-clearance workflow used by this migration routing.

Possibly related PRs

Suggested labels: ai:ready

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states that ai:blocked-deploy is retired because deploys no longer block merges.
Linked Issues check ✅ Passed The changes stop producing ai:blocked-deploy, route legacy deploy blocks to typed migration dependencies, preserve residue, and update related readers and documentation.
Out of Scope Changes check ✅ Passed The code, prompts, documentation, plugin updates, and version changes support the linked issue objectives without unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-07-31-issue-162-retire-blocked-deploy

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.

@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: 3

🤖 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`:
- Around line 69-70: Update the producer prompt’s transition guidance to remove
raw GitHub and git mutation commands, including gh pr/issue operations and git
push. Require all GitHub input, output, and state transitions to use the tested
pr-review-report subcommands, while preserving the existing prohibitions on
merge, close, deploy, force-push, and reset operations.
- Line 46: Make lifecycle-migration issue resolution atomic in the migration
path around the “find or FILE it” step before flag-blocked-on: use a stable
repository-specific marker with an idempotent create-or-find operation, or
serialize lookup and creation, so concurrent workers reuse one migration
issue/PR. Ensure all dependency flags reference the same canonical migration
issue and preserve the existing behavior when an open migration issue already
exists.

In `@pr-review-report-rs/src/main.rs`:
- Line 7546: Update the dominated closure in human_queue_mode so its
producer-label exclusion also treats RETIRED_BLOCKED_DEPLOY_LABEL as dominated,
matching the explicit handling already used by next_action alongside
PRODUCER_STATE_LABELS. Preserve the existing behavior for all labels in
PRODUCER_STATE_LABELS and ensure PRs carrying ai:ready with the retired
blocked-deploy residue do not trigger the extra ready_vetted_at_head fetch.
🪄 Autofix (Beta)

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: ab03a7da-f77a-4c3d-9d6e-f51a5a5c81f3

📥 Commits

Reviewing files that changed from the base of the PR and between dbea070 and aebfc83.

📒 Files selected for processing (7)
  • .claude-plugin/marketplace.json
  • README.md
  • campaign-prompt.txt
  • plugins/human-fsm/.claude-plugin/plugin.json
  • plugins/human-fsm/commands/nr.md
  • pr-review-report-rs/src/main.rs
  • review-prompt.txt

Comment thread campaign-prompt.txt
Comment thread campaign-prompt.txt Outdated
Comment thread pr-review-report-rs/src/main.rs
thedavidmeister and others added 2 commits August 2, 2026 13:26
Retiring `ai:blocked-deploy` took it out of `PRODUCER_STATE_LABELS`, and
`human_queue_mode`'s `dominated` closure derived "this PR is excluded from
the ai:ready verdict-currency fetch" from that same array — so a PR carrying
the residue label alongside a stale `ai:ready` started paying a `gh pr view`
whose result nothing reads. `classify_lane` returns `ProducerBlocked` at the
residue check before the `ai:ready` branch ever consults
`ready_vetted_at_head`, and `lanes_doc` is that value's only reader, so the
lane, the counts and the emitted JSON were all unaffected: the regression was
one wasted fetch per residue PR.

The closure was a hand-maintained mirror of `classify_lane`'s precedence
order, and it had already drifted the same way once — `ai:blocked-infra` left
`PRODUCER_STATE_LABELS` at #108 while staying ahead of `ai:ready`, and nothing
was added here for it either. So the mirror goes rather than gaining a third
entry: `needs_verdict_currency` asks `classify_lane` whether the answer moves
the PR at all, comparing `Some(false)` (what a fetch finding a stale verdict
yields) against `None` (literally what the lookup yields when no fetch was
made). It names no label, so every future retirement carries itself, and it
subsumes the separate `ai:ready` presence check the loop carried.

`verdict_currency_is_fetched_only_where_it_can_change_the_lane` asserts the
fetch is made for `ai:ready` and for `ai:ready` + `ai:design` (which does not
dominate), and refused for every state returning ahead of that branch —
`human:design`, `human:close-candidate`, `human:reject`, `ai:blocked-deploy`,
`ai:blocked-on`, `ai:blocked-infra` — each paired with the assertion that
skipping the fetch leaves `classify_lane`'s answer identical. Mutating the
function back to the `PRODUCER_STATE_LABELS`-derived form fails it on
`ai:blocked-deploy` and `ai:blocked-infra`; the rest of the suite stays green
under that mutant, which is why this needed a test of its own.

Co-Authored-By: Claude <noreply@anthropic.com>
Three conflicts, all resolved as unions rather than by picking a side:

marketplace.json / plugin.json: description text only. main's is a
superset (it documents the close-candidate reader and /ncc); this branch
had no stake in either sentence, so main's wins.

campaign-prompt.txt step 3b: main added the 3-item run budget while this
branch renamed the deploy nextAction to flag-migration. Kept main's
budget sentence carrying this branch's rename.

campaign-prompt.txt steps 6-7: this branch retires the producer deploy
path (#162), which is its whole purpose, so its side stands — but it was
written before main replaced gh pr create with mcp__fsm__open_pr, so
that change is grafted into the surviving step 7.

cargo test: 789 passed, 0 failed.

Co-Authored-By: Claude <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer

Producer note: conflicts with main resolved at head 24f1252; the PR is
mergeable again. All three were resolved as unions, not by picking a side:

nix develop .#rust -c cargo test: 789 passed, 0 failed.

One thing for you, not a blocker. Both sides independently bumped the
human-fsm plugin to 0.7.0, so git merged that line without a conflict and the
merge now ships both changes under one version. plugin-version-lockstep
only checks that the listing and the manifest agree, which they do, so nothing
goes red — but per this repo's own note that "/plugin compares version
strings, so a stale listing serves stale content silently", you may want
0.8.0 here. I did not bump it: picking the version slot is your call, and
nothing about the merge forced it.

Both this branch and main independently bumped human-fsm to 0.7.0, so
the merge took that line without a conflict and the version stopped
moving relative to main — which is exactly what the gate checks. main
shipped 0.7.0 with different content, so this branch's release is 0.8.0.

plugin-version-lockstep: ok human-fsm 0.8.0.

Co-Authored-By: Claude <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer

Correction to my previous note: the version slot was not yours to decide after all — CI settled it. The a plugin change bumps its version gate went red at 24f1252:

plugins/human-fsm changed but plugins/human-fsm/.claude-plugin/plugin.json is still 0.7.0. Bump it (and the marketplace entry to match)

Both sides had independently bumped to 0.7.0, so the merge took that line cleanly and the version stopped moving relative to main — which is exactly what the gate measures. Since main shipped 0.7.0 with different content, this branch's release is 0.8.0; both files bumped at 35abfd5, and plugin-version-lockstep reports ok human-fsm 0.8.0 locally.

thedavidmeister and others added 3 commits August 5, 2026 11:04
…y has

`legacy_deploy_signal`'s doc claimed the human's shape signal and the
producer's `next_action` "can never be answered differently". False since
the #162 re-point: `next_action` gates on three inputs
(`has_deploy_trigger && !deploy_done_at_head`), this signal reads two. On a
head carrying a trusted deploy-confirmed note they DO answer differently —
the `/nr` row says `repo-not-migrated` while `next_action` declines to flag.

The BEHAVIOUR is right: repo shape and this-run action are different
questions, and head-blindness is correct for a signal that reports shape.
Only the claim is wrong, so only the claim moves — both sides read the same
two SHAPE signals and can never disagree about whether the repo is
legacy-shaped; they diverge on exactly one further input, deliberately.

Same drift fixed in `worklist_row`'s marker comment, which inherited the
identical parity claim.

Two further comment claims corrected while auditing the re-point:

- `next_action`'s "so it leads" omitted the two gates that actually lead
  (human override, then the modeled-state park) — the deploy-shaped signal
  leads the CI-DERIVED cases, not the whole classifier.
- `deploy_confirmed_at_head`'s "the SHORT sha still counts" is true only of
  a 12-char prefix; the match is fixed at 12, so a 7-char abbreviation does
  not count. Said so.

And the relationship is now PINNED, not just argued: a new test runs both
sides over the same fixture matrix — both spellings of the shape evidence,
both ways of not being it — asserts they agree on repo shape every time, and
asserts the single deliberate divergence at a deploy-confirmed head.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ol-push rewrite

Merge base was 92 commits behind main, 74 of them touching main.rs. Seven
conflict hunks, every one resolved as the UNION — main's edits survive and
this PR's substance is unchanged.

pr-review-report-rs/src/main.rs (1 hunk, the `next_ready` MCP tool):
main added `"narrows": "limit"`, this PR rewrote the description to the
retirement's `legacyDeploySignal` wording. Kept both.

.claude-plugin/marketplace.json + plugins/human-fsm/.claude-plugin/plugin.json:
version bumps on both sides (0.8.0 here, 0.9.2 on main). Resolved to 0.9.3 in
BOTH files — strictly greater than main's, and the two stay in lockstep for
`plugin-version-lockstep`, while the version-hygiene `bump` gate sees a change
against a 0.9.2 base.

campaign-prompt.txt (4 hunks):
- steps 1/2/2z — main's rewrite wins whole (the `preflight`-asserted
  environment, `state-load`'s grouped read, `fleet.approved`). Only the
  worklist vocabulary is this PR's: `deploy` becomes `flag-migration`.
- 3b (i)'s two bullets — main's transient-flake bullet (`mcp__fsm__push`), this
  PR's infra bullet (a red prod-pin is the MIGRATION case, not the DEPLOY one).
- 3b (iii)/(iv) + 3c — main's push-is-a-tool (iii) and its rewritten 3c (which
  now routes off `worklist` rows rather than a second path list); this PR's
  (iv) MIGRATION replaces main's (iv) DEPLOY.
- steps 6/7 — main's clone-release and push/open_pr tool language, with this
  PR's deploy-retirement sentences spliced in: no redeploy marker, no producer
  deploy in any form, and the lifecycle-migration `gh issue create` carve-out.

Two things the textual merge got wrong and a reader would not see:

- `human_queue_doc` grew from 10 to 13 parameters on main (#165's
  `Option<&[OpenIssue]>`, plus `archived_prs` and `now_ms`). This PR's
  blocked-deploy kept-while-nonzero test still called the 10-arg form and the
  test binary did not compile. Updated to the real signature: no open-issue
  population (`None` — the test asserts a count, not an age), no archived PRs,
  and the module's `DOC_NOW_MS`.
- three step references still counted a producer deploy as a work item after
  this PR removed the deploy path: the run budget's "a deploy you dispatch",
  step 6(c)'s "3b(iv) deploy dispatched", and step 9's
  "rework/conflict/deploy work". A prompt that both forbids every deploy and
  budgets for one is a contradiction the retirement has to close, so each now
  names the migration flag that replaced it.

Verified on the merged tree, not assumed: no surviving `deploy_gate` /
`DeployGate` / `NextAction::Deploy` / `deployGate`; `PRODUCER_STATE_LABELS` is
still `[&str; 2]`; `next_action` still orders human-override → modeled-state
park (including the retired label) → `FlagMigration` → the CI classifier;
`classify_lane` still files the residue producer-blocked; `blockedDeploy` is
still emitted kept-while-nonzero; `VET_PROTOCOL` stays at main's 4 (this PR
does not bump it); `PrSignals` gained no field needing population.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed true

Third instance of the parity claim the code comments carried: the README's
`legacyDeploySignal` bullet said the producer's "flags this blocked on the
migration" and the human's "sees a repo that needs migrating" "cannot be
answered differently". They can — on a head whose deploy the old choreography
confirmed, `next_action` declines to flag while the row still reports
`repo-not-migrated`, because only the producer reads `deploy_confirmed_at_head`.

The row is right and the sentence was wrong, so the sentence moves: the two
share the SAME two shape predicates and can never disagree about whether the
repo is legacy-shaped, and the one input they do not share is named, along with
why the row is head-blind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Review finding fixed, and main merged in

Head is now 8793a16, three commits on top of 35abfd5: the comment finding
(db5703c), the merge (bafa4a8), and the README instance of the same finding
(8793a16).

1. The review finding — legacy_deploy_signal's doc comment

The claim was false as written. next_action gates on
s.has_deploy_trigger && !s.deploy_done_at_head — three inputs.
legacy_deploy_signal reads two and has no deploy_confirmed_at_head term. On a
PR whose current head carries a trusted deploy-confirmed note they do answer
differently: the /nr row says repo-not-migrated while next_action returns
GreenReady. The wording is inherited from the DeployGate version, where both
sides read the same three inputs and the parity claim was true; the #162
re-point dropped an input from one side and kept the claim.

The behaviour is correct — repo shape and this-run action are different
questions, and head-blindness is right for a signal that reports shape — so only
the claim moved.

Before

/// PURE: the legacy deploy signal, composed from the two shape signals above. The producer's
/// `next_action` reads the same two, so "the producer flags this blocked on the migration" and
/// "the human sees a repo that needs migrating" can never be answered differently. Deliberately
/// head-BLIND (no [`deploy_confirmed_at_head`] input): a deploy-confirmed note resolved one head's
/// pins under the old choreography, but the REPO is still legacy-shaped, and repo shape is what
/// this reports.

After

/// PURE: the legacy deploy signal, composed from the two shape signals above. The producer's
/// `next_action` reads the SAME two, so the two sides can never disagree about the question this
/// one answers: whether the REPO is legacy-SHAPED. They diverge on exactly one further input, and
/// deliberately — `next_action` also consults [`deploy_confirmed_at_head`] and declines to flag a
/// head the old choreography already resolved, while this stays head-BLIND: that note resolved one
/// head's pins, the repo is legacy-shaped either way, and repo shape is what this reports.

2. The rest of the #162 re-point, audited

Every claim checked, and whether it held.

Site Claim Verdict
legacy_deploy_signal "can never be answered differently" FALSE — fixed (above)
worklist_row's marker comment same parity claim, second copy: "the producer's 'flag this blocked on the migration' and the human's 'this repo needs migrating' cannot disagree" FALSE — fixed. Now: they cannot disagree about whether the repo is legacy-SHAPED, and deployDoneAtHead is named as the producer's own extra input
README legacyDeploySignal bullet same parity claim, third copy FALSE — fixed in 8793a16. This one is the human-facing copy, and the wrap across markdown lines is why a grep for the sentence missed it first time
next_action's doc "a deploy-shaped signal … so it leads" FALSE as an ordering claim — fixed. Human override and the modeled-state park both precede it; the deploy-shaped signal leads the CI-DERIVED cases. The sentence was rewritten by this PR, so it is this PR's to get right
deploy_confirmed_at_head "a note that embedded the SHORT sha still counts" Imprecise — tightened. head_short is head[..12], so a 12-char prefix counts and a 7-char abbreviation does not. Now says so; the existing test already pinned 12
REDEPLOY_MARKER "no new PR is flagged with it (the producer prompt no longer teaches it)" Holds. campaign-prompt.txt names the marker only as a signal to DETECT and route; nothing instructs writing one
REDEPLOY_MARKER "it gates nothing" Holds on the reading its own em-dash gives it ("no merge waits on a deploy"). It does still route the producer to FlagMigration; that is an action, not a merge gate
REDEPLOY_MARKER "reads those two and never title" Holds — body + trusted_comments, pinned by the_legacy_deploy_signal_is_read_from_the_body_not_the_title
requires_redeploy "untrusted authors are filtered by trusted_comments" Holds, pinned by the spoofed-author case in the same test
deploy_pin_red "can only fire while the PR is red" Holdsci == Ci::Red && guards it
deploy_confirmed_at_head "now suppresses the MIGRATION flag" / "an empty head can never read as confirmed" Holds — early return false on an empty head
LegacyDeploySignal enum "the signal never blocks anything" Holds — the only consumer is the /nr row's JSON field
NextAction::FlagMigration "no code fix can green a prod-pin there" Holds — no code path deploys
RETIRED_BLOCKED_DEPLOY_LABEL "residue STILL PARKS, unlike ai:blocked-infra" Holdsnext_action consults it, RETIRED_STATE_LABEL is not consulted
RETIRED_BLOCKED_DEPLOY_LABEL "classify_lane keeps it visible (producer-blocked)" Holds, pinned
state_noun "ai:blocked-deploy deliberately absent; no transition writes that state" Holds — no arm, and flag-blocked-deploy refuses
worklist_row's other marker keys requiresRedeploy / deployDoneAtHead / uiTouch descriptions Hold
next_ready_row's legacyDeploySignal comment "never a merge gate" Holds
MCP next_ready tool description "never a merge gate — #162" Holds
/nr command doc (plugins/human-fsm/commands/nr.md) the legacy-signal section Holds — it makes no parity claim

3. The invariant is now pinned, not only argued

the_shape_signal_and_the_migration_flag_diverge_only_at_a_confirmed_head runs
both sides over the same fixture matrix — both spellings of the shape evidence
(the marker, a red prod-pin) and both ways of not being it (a plain green PR, a
red PR whose red is not a pin) — asserts the two agree on repo shape on every
one, and then asserts the single deliberate divergence at a deploy-confirmed
head. A future edit that gives one side a third input breaks it.

Head-blindness alone was already pinned
(a_deploy_confirmation_does_not_erase_the_repo_shape_signal); what was not
pinned anywhere was the two sides agreeing on the same fixture.

4. The merge — seven conflicts, all resolved as unions

Merge base was 92 commits behind main, 74 of them touching main.rs.

pr-review-report-rs/src/main.rs (1) — the next_ready MCP tool entry.
main added "narrows": "limit"; this PR rewrote the description to the
legacyDeploySignal wording. Kept both.

.claude-plugin/marketplace.json + plugins/human-fsm/.claude-plugin/plugin.json (2)
— version bumps on both sides (0.8.0 here, 0.9.2 on main). Resolved to
0.9.3 in both, so plugin-version-lockstep sees them agree and the
version-hygiene bump gate sees a change against a 0.9.2 base.

campaign-prompt.txt (4)

  • steps 1/2/2z — main's rewrite whole (the preflight-asserted environment,
    state-load's grouped read, fleet.approved). The only thing taken from this
    side is the worklist vocabulary: deployflag-migration.
  • 3b (i)'s two bullets — main's transient-flake bullet (now mcp__fsm__push),
    this PR's infra bullet (a red prod-pin is the MIGRATION case, not the DEPLOY
    one).
  • 3b (iii)/(iv) + 3c — main's push-is-a-tool (iii) and its rewritten 3c (which
    routes off worklist rows instead of a second path list); this PR's (iv)
    MIGRATION in place of main's (iv) DEPLOY.
  • steps 6/7 — main's clone-release and push/open_pr tool language, with this
    PR's deploy-retirement sentences spliced in: no redeploy marker, no producer
    deploy in any form, and the lifecycle-migration gh issue create carve-out.

Nothing had to be dropped: no pair of changes contradicted.

5. Two things the textual merge got wrong

Both found by reading and building the merged tree, not by trusting a clean
textual merge.

  • human_queue_doc grew from 10 to 13 parameters on main (refresh-human-queue: emit uncovered-issues age stats (median/oldest days) beside counts — recording half of rain-org-health#140 #165's
    Option<&[OpenIssue]>, plus archived_prs and now_ms). This PR's
    the_retired_blocked_deploy_count_keeps_its_key_and_counts_the_residue still
    called the 10-arg form, and the test binary did not compile. Updated to the
    real signature — None for the open-issue population (the test asserts a
    count, not an age), no archived PRs, and the module's DOC_NOW_MS.
  • Three step references still counted a producer deploy as a work item after
    this PR removed the deploy path: the run budget's "a deploy you dispatch",
    step 6(c)'s "3b(iv) deploy dispatched", and step 9's
    "rework/conflict/deploy work". A prompt that both forbids every deploy (step 7)
    and budgets for one is a contradiction the retirement has to close, so each now
    names the migration flag that replaced it.

6. Semantic checks on the merged tree

  • Zero surviving deploy_gate / DeployGate / NextAction::Deploy /
    "deployGate" anywhere in the repo.
  • PRODUCER_STATE_LABELS is [&str; 2]; the drift-catching
    len() == 2 assertion still holds.
  • next_action's order survives: human override → modeled-state park (including
    RETIRED_BLOCKED_DEPLOY_LABEL) → FlagMigration → the CI classifier.
  • classify_lane still files the residue producer-blocked, and ai:blocked-on sits with the vetter: typed dependency refs, automated clearance on dep merge, human polling removed #164's
    clearance still never selects it.
  • blockedDeploy is still emitted kept-while-nonzero, counting via
    RETIRED_BLOCKED_DEPLOY_LABEL.
  • VET_PROTOCOL stays at main's 4 — not bumped here.
  • PrSignals gained no field on main, so nothing new to populate in
    worklist_row.
  • Withhold archived-repo rows from every org-wide enumeration, with a stated count #209's every_org_wide_enumeration_withholds_archived_repos passes: nothing
    here introduces or moves an org-wide enumeration.
  • The 13 live ai:blocked-deploy PRs are untouched — that is the ruling.

7. Verification

Everything through nix, from the merged tree.

  • cargo build — clean.
  • cargo test1049 passed, 0 failed, 0 ignored (971 + 3 + 9 + 47 + 8 + 7 + 4
    across the seven binaries).
  • rainix-rs-static (rustfmt + clippy, CI's exact invocation at rainix
    53e96a7), after touching main.rs so a warm target could not mask a lint
    — clean, zero warnings.
  • pre-commit run --all-files11 hooks, all Passed (deadnix, denofmt, nil,
    nixfmt, no-consumer-prettier, prettier-rainix, rustfmt, shellcheck, statix,
    taplo, yamlfmt).

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed 8793a16: ready

Verified before merge: 20 SUCCESS / 1 skipped, MERGEABLE/CLEAN, 0 unresolved threads, 1049 tests, clippy and pre-commit clean. I read the diff, the consumeSentinel-adjacent guard chain, and the merged source rather than relaying the report.

The retirement is the human's ruling made structural: under the split lifecycle no merge waits on a deploy, so a PR that would earn ai:blocked-deploy is evidence its REPO has not migrated — the block is real, its object was wrong. The writer refuses unconditionally and the subcommand still PARSES on purpose, because a clap unknown-subcommand error names neither the reason nor the repair while the refusal spells out flag-blocked-on … --blocked-by <migration ref>. The refusal is unconditional BY CONSTRUCTION, not by a check: Cmd::FlagBlockedDeploy { slug, pr, .. } destructures dry_run away entirely, so there is no path on which a plan is previewed or a write attempted.

The re-point is the substance, not the deletion. Every deploy-gate reader is kept and re-aimed at repo SHAPE — REDEPLOY_MARKER and deploy_pin_red become the definition of a legacy-shaped repo made executable, DeployGate becomes LegacyDeploySignal{None, RepoNotMigrated}, NextAction::Deploy becomes FlagMigration, and the deployGate JSON key was RENAMED rather than left as a key called "gate" whose values stopped gating. I confirmed zero surviving references to deploy_gate, DeployGate, NextAction::Deploy or "deployGate" at this head.

It composes with #164 rather than colliding: the residue keeps ai:blocked-deploy, which the clearance never selects (it reads ai:blocked-on typed refs), and next_action PARKS the residue — deliberately opposite to #108's blocked-infra — because re-deriving those PRs from CI would perform exactly the auto-migration the ruling forbids. I re-counted the residue live: 13, untouched by this PR (raindex#2779, rain.dia#57/#61, rain.erc4626.words#252, rain.flare#160, rainlang#534/#536/#537, rain.math.float#237/#244/#245/#253/#256).

On the send-back finding: the false parity claim existed in THREE places, not the one I named — the legacy_deploy_signal doc, worklist_row's marker comment verbatim, and README's human-facing bullet, the last only visible in the post-merge README diff because the sentence wraps across markdown lines and a phrase grep misses it. All three now state it accurately: both sides read the same two SHAPE predicates and can never disagree about whether the repo is legacy-shaped; they diverge on exactly one further input, deliberately, because next_action also consults deploy_confirmed_at_head and declines to flag a head the old choreography resolved. Two further stale claims were corrected inside sentences this PR had itself rewritten — next_action saying the deploy signal "leads" when states park first, and deploy_confirmed_at_head saying a SHORT sha still counts when the prefix is fixed at 12.

The added test pins what nothing pinned: head-blindness was already covered, the two sides AGREEING was not. It runs both functions over both spellings of the shape evidence and both ways of not being it, asserting agreement each time before asserting the single divergence — and supplies "files": [] so the screenshot gate reads "no UI" rather than "unknown" and no fixture can route to 3c instead of the case under test.

The base merge caught two things reading alone would not: human_queue_doc grew 10 to 13 parameters on main (#165), so this PR's blocked-deploy test did not COMPILE — a textual union that only builds because someone built it; and three prompt sites still budgeted for a producer deploy while step 7 forbids every deploy, a contradiction the retirement has to close.

One caveat recorded rather than hidden: CodeRabbit's green check reads Review rate limited, so no bot review stands behind it and the zero unresolved threads is vacuous here — unlike #166, where its review did land and found four real defects. The human read above is what this merge rests on.

@thedavidmeister
thedavidmeister merged commit 95b9b7d into main Aug 5, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Retire ai:blocked-deploy: deploys never block merges under the split lifecycle — collapse into typed ai:blocked-on refs to the repo's migration

1 participant