Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "human-fsm",
"source": "./plugins/human-fsm",
"description": "Slash commands for the human's side of the issue→PR pipeline FSM: read the next ai:ready PR and check the vetter's verdict against the diff and the issue it closes, read the next ai:close-candidate flag and check the producer's reason against the issue and the code it claims about, then rule on a close-candidate, reject, raise a design question, or keep an issue open. Every input comes from pr-review-report; none writes GitHub state itself.",
"version": "0.9.3",
"version": "0.10.0",
"author": { "name": "Rain Open Source Software Ltd" },
"keywords": [
"rainlanguage",
Expand Down
31 changes: 21 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ transition functions:
| `sol-toolchain-audit <trace>` | the CHECK on the rule above (#203): every Solidity check the run ran, against the toolchain `sol-toolchain` named for that checkout — `matched` / `skew` / `unasked` / `unmatchable`, plus the audit's own two blind spots (an invocation naming no checkout, one entering the working directory's flake without naming it). Read out of the run's OWN trace, where the answer and the `nix develop` that followed are both recorded, so it costs no network read and cannot stop a run. Exit 3 on skew or unasked; `campaign-run.sh` appends it to the run log |
| `unvetted_close_candidates` (MCP) | the vetter's second state-load: which producer close-candidate flags need judging this run |
| `record_close_candidate_verdict` (MCP) | the vetter's issue write: uphold (queued for the human) or reject (strips the flag → producer's queue) |
| `human-rule <owner/repo> <n> <ruling> "<note>"` | the HUMAN's PR ruling: `human:<ruling>` + a head-sha-pinned `👤 human` comment (supersedes any prior human ruling) |
| `human-rule-issue <owner/repo> <n> <ruling> "<note>"` | the HUMAN's issue ruling: adds `keep-open`; pinned to the live close-candidate flag, or to the issue as filed |
| `human-rule <owner/repo> <n> <ruling> "<note>"` | the HUMAN's PR ruling: label + a head-sha-pinned `👤 human` comment (supersedes any prior human ruling). Park-or-delegate is chosen HERE (#111) and belongs to two verbs only: `reject` REQUIRES `--rework "<order>"` / `--rework-file <path>`, which emits the trusted `Rework note` work order in the same call; `design` takes exactly one of `--rework` / `--park` (the explicit pure park); a bare reject/design refuses, and `close-candidate` refuses both flags — the verb is its own disposition, and its refusal names `human-close` |
| `human-rule-issue <owner/repo> <n> <ruling> "<note>"` | the HUMAN's issue ruling: adds `keep-open`; pinned to the live close-candidate flag, or to the issue as filed. `reject` / `design` take the same disposition flags on the same terms; `close-candidate` / `keep-open` refuse them |
| `human-close <owner/repo> <n> "<note>"` | the HUMAN's TERMINAL edge on either subject: rule `close-candidate`, retire the pending `ai:close-candidate`, close — ONE transition (#94) |
| `record-close-candidate-verdict <owner/repo> <n> <v> …` | the vetter's flag verdict, also as a subcommand — `human-rule-issue`'s stranded-flag refusal names it, and a terminal has no MCP |
| `require-qa-block` | the QA-GUIDE §8 gate on PR-open: refuses a `gh pr create` whose body lacks the evidence block. Wired as a PreToolUse `Bash` hook, so it binds every session — including the ones with no MCP surface, which is the only population still reaching for `gh pr create` now `open_pr` exists |
Expand Down Expand Up @@ -304,10 +304,19 @@ it finds is a different PR's code.

## Invariants

- **Human decisions are sacred.** A `human:*` label, a native `APPROVED` /
`CHANGES_REQUESTED` review, OR a `👤 human` ruling comment pinned to the
CURRENT head is never overwritten by the vetter — `--record-verdict` refuses
(exit 3), closing the TOCTOU race.
- **Human decisions protect AUTHORSHIP, and a ruling is an INPUT (#111).** No AI
actor ever writes a `human:*` label, and none removes one as an OVERRIDE of
the human: a native `APPROVED`/`CHANGES_REQUESTED` review, a `👤 human` ruling
pinned to the CURRENT head, an absolutely-parking label
(`human:close-candidate`, retired `human:reject`), or an un-executed
`human:design` is never overwritten by the vetter — `--record-verdict` refuses
(exit 3), closing the TOCTOU race. But absolute parking was the ruled-out
overreaction: a ruling is an input the machine EXECUTES. A delegated
`human:design` (its trusted `Rework note` pinned at head) is the producer's
work order; once executed (the push moves the head past the pin) the PR is
un-vetted and the verdict that re-judges it clears the spent label —
clearing-by-execution, the completion of what the human asked, not an
override. Pure parking survives only as the explicit `--park` spelling.
- **A reject is ONE state, and the ruler rides on the comment (#133).**
`ai:reject` and `human:reject` demanded the same move from the same actor, so
they are one state: `ai:reject`, whoever ruled. The label says what the work
Expand All @@ -321,10 +330,12 @@ it finds is a different PR's code.
`pr_context.humanComments`. `reworked-reject` is retired: its timestamp
comparison proved only that SOME commit post-dated the label event, and what
actually protects the objection is a stateless re-vet that can read it. So
`human:*` now means one thing absolutely — sacred, never written or cleared by
an AI actor — with no carve-out. `human:reject` survives only as a RETIRED
label on PRs the migration has not moved; it stays sacred and stays bucketed
until `migrate-reject` does.
`human:*` means one thing — AUTHORSHIP-protected: never written by an AI
actor, never removed as an override. (#111 narrowed this from the absolute
parking #133 briefly enshrined: an EXECUTED `human:design` delegation is
cleared by the re-vet as the ruling's completion.) `human:reject` survives
only as a RETIRED label on PRs the migration has not moved; it stays sacred
and stays bucketed until `migrate-reject` does.
- **A verdict accounts for every file the PR changes.** Scope coverage was the
one thing `record_verdict` took on trust, and a verdict formed without a
changed file in view is indistinguishable from a diligent one:
Expand Down
66 changes: 51 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,18 @@ stateDiagram-v2
unvetted --> infradown : infra-down · environment is impeding the work
infradown --> unvetted : next tick · 4h later, from scratch

%% human decisions are sacred — the vetter never re-verdicts these
%% a human REJECT is not one of them: it writes the same ai:reject the vetter writes, and the
%% sha-pinned 👤 human comment is what records that a human ruled (#133).
ready --> reject : human-rule reject + Rework note
ready --> hdesign : human-rule design
%% human decisions protect AUTHORSHIP (#111): no AI actor writes a human:* label, none
%% removes one as an override. A ruling is an INPUT the machine executes — park is the
%% explicit minority spelling, never the default — and clearing-by-execution is modeled.
%% a human REJECT writes the same ai:reject the vetter writes, with the work order in the
%% same call; the sha-pinned 👤 human comment records that a human ruled (#133).
ready --> reject : human-rule reject --rework · ruling + work order, one call
ready --> hdesign : human-rule design --rework · delegated work order
ready --> hdesign : human-rule design --park · explicit park
ready --> hclose : human-rule close-candidate
hdesign --> [*] : human rules
hclose --> [*] : human-close · retires the flag too
hdesign --> unvetted : producer executes the order → push · the re-vet clears the spent label
hdesign --> [*] : parked · exit is the human superseding their own ruling
hclose --> [*] : human-close · the human's terminal edge, retires the flag too

design --> [*] : human design ruling
close --> [*] : human-close (a PR) · retires the flag too
Expand All @@ -107,12 +111,37 @@ and `human:keep-open` appeared in the binary only as strings it **read and
refused on**, so the one actor whose decisions everything else treats as sacred
was also the only one improvising raw `gh issue edit --add-label`.

| Transition | The move it makes |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `human-rule <owner/repo> <pr> <ruling> "<note>"` | PR ruling — `reject` / `design` / `close-candidate`, pinned to the **head sha** |
| `human-rule-issue <owner/repo> <issue> <ruling> "<…>"` | issue ruling — those three plus `keep-open`, pinned to the **live flag** or to the **issue as filed** |
| `human-close <owner/repo> <n> "<note>"` | the **terminal** edge, on either subject: rule, retire the pending flag, close — one transition |
| `record-close-candidate-verdict <owner/repo> <issue>` | the vetter's flag verdict, now reachable from a terminal too (the refusal above names it) |
| Transition | The move it makes |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `human-rule <owner/repo> <pr> <ruling> "<note>"` | PR ruling — `reject` / `design` / `close-candidate`, pinned to the **head sha**; park-or-delegate is chosen HERE (#111), and the flags that choose it are `reject`'s and `design`'s alone: `reject` REQUIRES `--rework "<order>"`, `design` takes exactly one of `--rework` / `--park` |
| `human-rule-issue <owner/repo> <issue> <ruling> "<…>"` | issue ruling — those three plus `keep-open`, pinned to the **live flag** or to the **issue as filed**; `reject` / `design` carry the same disposition flags, `close-candidate` / `keep-open` refuse them |
| `human-close <owner/repo> <n> "<note>"` | the **terminal** edge, on either subject: rule, retire the pending flag, close — one transition |
| `record-close-candidate-verdict <owner/repo> <issue>` | the vetter's flag verdict, now reachable from a terminal too (the refusal above names it) |

**A ruling can delegate, not only park (#111).** The ruling and the work order
are two records with their own shapes — the ruling is provenance (_a human
decided X, at anchor Y_; pinned, historical, true forever), the work order is an
instruction to the producer, spent once acted on — and ONE call emits both:
`--rework "<order>"` posts a trusted `Rework note @<anchor>: …` comment beside
the ruling, in the **exact prefix form** the producer's
`trusted-comments --marker 'Rework note'` verification accepts, pinned to the
same anchor as the ruling so the two go stale together. The tool owns the
prefix, so the failure the issue measured — a hand-typed `Rework Note` silently
parking a PR meant to be delegated — is unconstructible. `reject` **requires**
the order (a reject IS a send-back; one not worth reworking is a
close-candidate, not a park); `design` takes exactly one of `--rework` /
`--park`; and a bare call to either **refuses** rather than parking by accident
— pure parking is an explicit spelling, never the default meaning of a ruling.
The other two verbs take neither flag and say so: `close-candidate` is a decided
close whose refusal names `human-close` as its consumer, and `keep-open` is a
standing constraint — the verb is its own disposition, so a second spelling of
it would only be a way to mean something else by accident. What the human
namespace protects is **authorship only**: no AI actor writes a `human:*` label,
and none removes one as an override — but a ruling is an input the machine
executes, so the producer's state-load picks a delegated `human:design` up as a
work order (`worklist` routes it `rework-ruling`), the push moves the head, and
the vetter's next verdict clears the spent label as the **completion** of the
ruling through the ordinary rework → un-vetted → re-vet flow.

The vocabularies are not a second list: they **are** `HUMAN_DECISION_LABELS`
(PRs) and `HUMAN_RULING_LABELS` (issues), the same constants every AI transition
Expand Down Expand Up @@ -149,8 +178,15 @@ that has already happened:
no state left to move out of, so nothing is written and the exit is 0;
- **re-ruling supersedes** rather than refuses. The human owns this namespace
and may correct a mis-click, so the old `human:*` is removed and the new one
added — since #133 the **only** sanctioned removal of a `human:*` label, and
sanctioned because the actor removing it wrote it;
added — one of the **two RUNTIME** removals of a `human:*` label, sanctioned
because the actor removing it wrote it. The other is #111's
clearing-by-execution: the verdict that re-judges an **executed** delegation
clears the spent `human:design`, sanctioned because it completes what the
human asked rather than overriding it. There is a third remover, and it is a
MIGRATION rather than a transition of the running FSM: `migrate-reject` moves
the PRs still carrying the `human:reject` #133 retired onto `ai:reject` — a
one-shot over a fixed, shrinking population, which is why it is not one of the
two paths a live ruling can take;
- **a ruling that would strand a live flag is refused** (exit 4). This is the
one from #86. On `rainlanguage/rain.erc4626.words#93` a hand-applied
`human:reject` sat on an issue whose producer close-candidate flag had not
Expand Down
Loading
Loading