diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 87bda7b4..77fd2ead 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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, read the next ai:design PR and check the raised question against the issue and the code it is about, read the next FSM leak and locate which of three defects left the PR in no modeled state, then rule on a close-candidate, send work back as needs-work, rule a design question, or keep an issue open — plus an observation command that forces a run, watches it, measures what its context cost and reads the retained trace corpus for what a run still hand-rolls. Every input comes from pr-review-report, and the reads rule through it what they can articulate.", - "version": "0.23.0", + "version": "0.24.0", "author": { "name": "Rain Open Source Software Ltd" }, "keywords": [ "rainlanguage", diff --git a/plugins/human-fsm/.claude-plugin/plugin.json b/plugins/human-fsm/.claude-plugin/plugin.json index 127d6cec..fbcd094c 100644 --- a/plugins/human-fsm/.claude-plugin/plugin.json +++ b/plugins/human-fsm/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "human-fsm", "displayName": "Human FSM transitions", - "version": "0.23.0", + "version": "0.24.0", "description": "Slash commands for the human's side of the rainlanguage issue→PR pipeline finite state machine: the reads that precede a ruling, the transitions that make it, and /observe-run — force a run, watch it, measure its context, and read the retained trace corpus for what a run still hand-rolls. Every input arrives from pr-review-report — each ruling as one subcommand, /nr, /ncc, /ndd and /nm as typed MCP reads — and the guards (vocabulary, mandatory note, provenance anchor, stranded-flag refusal, subject type, terminal-is-moot, idempotence) live in that binary, not here.", "author": { "name": "Rain Open Source Software Ltd", diff --git a/plugins/human-fsm/README.md b/plugins/human-fsm/README.md index 0eeb70a2..d5c290b5 100644 --- a/plugins/human-fsm/README.md +++ b/plugins/human-fsm/README.md @@ -11,9 +11,9 @@ JSON-RPC frame, a Python filter over the response, and two raw `gh` calls. | Command | The call it invokes | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/nr [1-3]` | `next_ready` + `pr_context` + `pr_checkout` + `clone_release` + `human_rule` (MCP) and the `audit` skill — the next `ai:ready` PR, and the vetter's verdict checked against its diff, its issue and its source. Sends back what it can articulate; the merge is the human's | -| `/ncc [1-3]` | `next_close_candidate` + `close_candidate_context` + `pr_context` (MCP) — the next `ai:close-candidate` flag, and the producer's reason checked against the issue as filed and the code it claims about. Writes no GitHub state | +| `/ncc [1-3]` | `next_close_candidate` + `close_candidate_context` + `pr_context` (MCP) — the next `ai:close-candidate` flag, and the producer's reason checked against the issue as filed and the code it claims about. Writes no GitHub state — there is no typed `reject`, so the one exit it can articulate is handed over as `/close-candidate reject` | | `/ndd [1-3]` | `next_design` + `pr_context` + `pr_checkout` + `clone_release` + `human_rule` (MCP) and the `audit` skill — the next `ai:design` PR, and the raised question checked against its issue, its diff and its source: genuine (presented with its option space, for the human to answer), already answered or misrouted (ruled here, reported in a few lines) | -| `/nm [1-3]` | `next_leak` + `pr_context` + `pr_checkout` + `clone_release` (MCP) and the `audit` skill — the next FSM-conformance leak, and which of three places the defect is in: the PR's state record, the machine's vocabulary, or the classifier. Writes no GitHub state | +| `/nm [1-3]` | `next_leak` + `pr_context` + `pr_checkout` + `clone_release` + `human_rule` (MCP) and the `audit` skill — the next FSM-conformance leak, and which of three places the defect is in: the PR's state record, the machine's vocabulary, or the classifier. Sends back what it can articulate; the close is the human's | | `/close-candidate uphold "…"` | `human-close` — rule, retire `ai:close-candidate`, close. Issue **or** PR, resolved by lookup | | `/close-candidate reject "…"` | `record-close-candidate-verdict … reject` — drop the flag, back to the producer (issue-only) | | `/needs-work "…"` | `human-rule … needs-work --rework` / `human-rule-issue … needs-work --rework` — the send-back: `ai:needs-work` (PR) plus the trusted `Rework note` work order, one call, pinned to the head sha or the issue. `--rework` is REQUIRED on either subject; there is no parked needs-work | @@ -29,21 +29,25 @@ inbox: the merge queue, the flag queue, the design questions, and the leaks. The rest are the rulings. They differ in how they reach the binary, and the difference is the point. -A read that can articulate the ruling takes it: `/nr` and `/ndd` carry +A read that can articulate the ruling takes it: `/nr`, `/ndd` and `/nm` carry `human_rule` and send back what they can put into words, so what reaches the -human is the merge, or the design question no source they can read settles. +human is the merge, the close, or the design question no source they can read +settles. `/ncc` is the exception and says so in its own file: there is no typed +`reject`, and `human_rule_issue` refuses `needs-work` on a live flag because it +would strand it, so the reject it can articulate is handed over rather than +taken. The rulings shell out to a `pr-review-report` subcommand. The reads call **MCP tools** — served by the `fsm` server this plugin ships in its own manifest — and -**no shell at all**. `/nr` is granted `next_ready`, `pr_context`, `pr_checkout` -and `clone_release`, plus `Skill` and `Read`, which it needs because it puts the -PR's source on disk and audits it. `/ndd` is granted the same shape with -`next_design` at its head, because a design question is a claim about code on a -PR and weighing its options means reading the tree. `/nm` is granted the same -shape with `next_leak` at its head: locating a leak sometimes turns on what the -code actually did, so the tree has to be reachable — though most leaks are -located from the trusted comments and the labels, and the lens is the exception -rather than a step. `/ncc` is granted `next_close_candidate`, +**no shell at all**. `/nr` is granted `next_ready`, `pr_context`, `pr_checkout`, +`clone_release` and `human_rule`, plus `Skill` and `Read`, which it needs +because it puts the PR's source on disk and audits it. `/ndd` is granted the +same shape with `next_design` at its head, because a design question is a claim +about code on a PR and weighing its options means reading the tree. `/nm` is +granted the same shape with `next_leak` at its head: locating a leak sometimes +turns on what the code actually did, so the tree has to be reachable — though +most leaks are located from the trusted comments and the labels, and the lens is +the exception rather than a step. `/ncc` is granted `next_close_candidate`, `close_candidate_context` and `pr_context`, and those three only: a flag has no diff and no tree to check out, so there is nothing for `Skill` or `Read` to reach, and a grant a command cannot use is surface it cannot account for. None diff --git a/plugins/human-fsm/commands/ncc.md b/plugins/human-fsm/commands/ncc.md index b9a38859..1baeeff3 100644 --- a/plugins/human-fsm/commands/ncc.md +++ b/plugins/human-fsm/commands/ncc.md @@ -220,6 +220,23 @@ issue, reassembled by hand, is a decision whose inputs nobody can audit. If a tool is unavailable, say so and stop — the answer is to connect the plugin's MCP server, not to work around it. +## If you can articulate it, send it BACK — not forward + +**Anything you can put into words against the flag is a reject.** Do not write +it up for the human to reach the same answer; the words are the reject's note. + +Two exits stay theirs, and both for the same reason — they destroy or freeze +work: `uphold`, which closes somebody's issue, and `keep-open`, which forbids +the producer from ever flagging it again. + +**Blocked today:** there is no typed tool for `reject`. The server exposes +`human_close` (uphold) and `human_rule_issue`, and the latter refuses +`needs-work` on a live flag because it would strand it. So the reject is handed +to the human as `/close-candidate reject ` — the one exit this +command cannot take itself, and a gap worth closing rather than living with. + +## Typed reads + The grant is three typed reads and nothing else. All three are reads; this command writes no GitHub state. That list is a **declaration, not a sandbox** — measured on Claude Code 2.1.220, a command granting only `Read` still ran a @@ -293,7 +310,7 @@ The rulings are the existing ones and this command invents none: - `/design ` — the flag raises a question a human has to settle rather than a claim anyone can check. -**This command does not rule and does not close.** It is the read that precedes -the human's word. +**This command does not close** — destroying work is the human's word. It DOES +reject what it can articulate against, once a typed reject exists. Names collide across plugins; `/human-fsm:ncc` disambiguates. diff --git a/plugins/human-fsm/commands/nm.md b/plugins/human-fsm/commands/nm.md index 63430c89..0a973e3c 100644 --- a/plugins/human-fsm/commands/nm.md +++ b/plugins/human-fsm/commands/nm.md @@ -1,7 +1,7 @@ --- description: The next unmodelled PR — an FSM leak the lane classifier buckets into no modeled state — located as a defect in exactly one of three places, against an independent read of the PR, its trusted comments, and the classifier's own rule. argument-hint: [1-3] -allowed-tools: mcp__plugin_human-fsm_fsm__next_leak, mcp__plugin_human-fsm_fsm__pr_context, mcp__plugin_human-fsm_fsm__pr_checkout, mcp__plugin_human-fsm_fsm__clone_release, Skill, Read +allowed-tools: mcp__plugin_human-fsm_fsm__next_leak, mcp__plugin_human-fsm_fsm__pr_context, mcp__plugin_human-fsm_fsm__pr_checkout, mcp__plugin_human-fsm_fsm__clone_release, mcp__plugin_human-fsm_fsm__human_rule, Skill, Read --- Arguments: `$ARGUMENTS` @@ -22,21 +22,17 @@ deliverable of this command is naming which: - **The PR's state record is wrong.** It belongs in an existing state and the label went missing or was hand-mangled. The finding is the state it belongs - in, and the ONE command that files it there, written out in full so the human - can type it: - - `/human-fsm:needs-work ` — work is owed on it; the note - is the work order, and the producer is the next mover. - - `/human-fsm:design ` — the note is the ANSWER to a - question the PR raises, which is itself producer work. - - `/human-fsm:close-candidate uphold ` — the PR is - finished or should be destroyed. This is the close: it runs `human-close`, - which resolves PR-or-issue by lookup, posts the ruling, closes the subject, - and retires any pending flag. It does NOT need an `ai:close-candidate` flag - to already exist, which matters here because a leaked PR by definition - carries no `ai:*` label at all. - - One command, because a state reached by a sequence of hand edits is a state - nothing can audit, which is how this PR got here. + in, and you FILE it there in one typed call rather than naming a command for + someone else to run: + - `human_rule` — `needs-work` when work is owed, `design` when the note is the + ANSWER to a question the PR raises. Both land `ai:needs-work`; the verb + records which ruled, and `rework` is what the producer executes. + - The close stays the human's, because destroying work is theirs to order: + `/human-fsm:close-candidate uphold `. It needs no + existing flag, which matters because a leaked PR carries no `ai:*` at all. + + One call, because a state reached by hand edits is a state nothing can audit — + which is how this PR got here. The commonest cause is a label the classifier NO LONGER RECOGNISES, and there are three cases worth telling apart before you diagnose one: @@ -64,10 +60,11 @@ deliverable of this command is naming which: extra steps. - **The classifier is wrong.** The PR IS in a modeled condition and the machinery fails to see it. The fix is the classifier, never the instance: - hand-patching the PR would clear the box while leaving the defect armed for - the next PR shaped like it. The finding names the defect precisely enough to - file, and it is filed as an issue on the pipeline repo — by the human, since - this command writes nothing. + re-filing the PR would clear the box while leaving the defect armed for the + next PR shaped like it. So the classifier finding goes to the human, stated + precisely enough to file as an issue on the pipeline repo — and it does NOT + substitute for the send-back where this PR also has work owed on it. The + structural fix and the send-back are both true at once. Naming a state and its consuming transition is the whole job. A leak "processed" with a plausible label and no diagnosis is the machine's account of itself @@ -155,16 +152,18 @@ checkouts are how this box filled its disk. If `pr_checkout` ERRORED there is nothing to release: re-call it ONCE, and if it fails again present the read without that half and say so in as many words. -**7. Present the location, not a disposition.** Print every field of the row. -Then say which of the three places the defect is in and the finding that follows -from it — the state and its ONE filing command, or the missing state and its -consuming transition for the design path, or the classifier defect stated -precisely enough to file. Then say what you checked to conclude it: the note, -the labels, the rule, and the code where you read any. A leak whose location you -cannot determine is a complete and correct outcome STATED AS SUCH — say what you -read and what would decide it, rather than defaulting to the nearest plausible -label, because a guessed re-filing is indistinguishable from a located one and -wrong in the way nothing downstream can detect. +**7. Locate it, act on it, and report both.** Print every field of the row. Then +say which of the three places the defect is in and the finding that follows from +it — the state, or the missing state and its consuming transition, or the +classifier defect stated precisely enough to file. Then TAKE the send-back per +**If you can articulate it** below, and report that you took it. A located leak +you handed onward is a leak that is still leaking. Then say what you checked to +conclude it: the note, the labels, the rule, and the code where you read any. A +leak whose location you cannot determine is a complete and correct outcome +STATED AS SUCH — say what you read and what would decide it, rather than +defaulting to the nearest plausible label, because a guessed re-filing is +indistinguishable from a located one and wrong in the way nothing downstream can +detect. ## Why this is a second opinion and not a dispatcher @@ -194,9 +193,18 @@ typed: - `/human-fsm:keep-open ` — protects an ISSUE from being re-flagged; not a move on a PR. -**This command does not rule, does not label, and does not file** — it is the -read that precedes the human's word, and on this queue the human's word is a -diagnosis. +## If you can articulate it, send it BACK — not forward + +**Anything you can put into words about why this PR is not merged is a +send-back.** Rule it with `human_rule`; the words you were about to write for +the human ARE the work order, and they go in `rework`. + +Diagnose first anyway — a leak located in the wrong place is a defect left +armed. Then act. The structural finding and the send-back are both true at once, +never alternatives. + +Not yours to move: `keep-open` on an issue, `close-candidate reject` on a +flagged subject. ## Typed reads, and no shell at all @@ -207,12 +215,17 @@ reassembled by hand outside the machine, is the same defect it is diagnosing. If a tool is unavailable, say so and stop — the answer is to connect the plugin's MCP server, not to work around it. -The grant is four typed calls plus `Skill` and `Read`, and `Read` applies to the -`pr_checkout` tree and nothing else. All four typed calls are reads except -`clone_release`, which disposes of what this command itself created and writes -no GitHub state. That list is a **declaration, not a sandbox**: measured on -Claude Code 2.1.220, a command granting only `Read` still ran a `Bash` call with -no permission denial. So the prohibition above is the thing that actually binds, +The grant is five typed calls plus `Skill` and `Read`, and `Read` applies to the +`pr_checkout` tree and nothing else. Four are reads; `clone_release` disposes of +what this command itself created and writes no GitHub state. + +The fifth, `human_rule`, is the send-back and the only call that writes GitHub +state. Typed for the same reason as every other input: its guards — mandatory +work order, head-sha anchor, clearing every other `ai:*` — live in the binary, +and a state reached by hand edits is the unauditable record this queue exists to +diagnose. That list is a **declaration, not a sandbox**: measured on Claude Code +2.1.220, a command granting only `Read` still ran a `Bash` call with no +permission denial. So the prohibition above is the thing that actually binds, which is why it is written here rather than assumed of the frontmatter — and why nothing in this file is fenced as a shell line, because what a reader copies out of a command is what the command showed them.