diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 637bf38..4e1d6cc 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -35,6 +35,21 @@ "judgement", "github" ] + }, + { + "name": "rain-repo-conventions", + "source": "./plugins/rain-repo-conventions", + "description": "The standing constraints on any agent doing work in a rainlanguage repo, shipped once so a brief stops restating them. One skill, grouped by what KIND of rule each entry is: rules that are chosen and never expire (clone and scratch isolation for parallel agents, the irreversible acts reserved to the human, assignment, never departing from an agreed spec, never reporting a run you did not watch finish, keeping every wait bounded), facts that are true of the box and the harness rather than of the work (the `## QA` gate on `gh pr create` and its literal-path argument, force-backgrounded builds, `jq` only inside the devshell, a `pgrep` search that finds itself), and workarounds that name the defect they route around and what would retire them (the soldeer bump sequence, the generated pre-commit config). A wrong fact carried as belief is worse than no entry, so each is stated so a reader can check it — and it states properties, never cases.", + "version": "0.1.0", + "author": { "name": "Rain Open Source Software Ltd" }, + "keywords": [ + "rainlanguage", + "conventions", + "agents", + "tooling", + "github", + "foundry" + ] } ] } diff --git a/README.md b/README.md index d444fa8..4a67dfb 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,53 @@ and a quietly worse vetter. An autonomous pipeline's behaviour is a pure function of its committed code and prompts, and a rule worth remembering is a rule worth shipping. +### The standing conventions as a skill + +The third thing this marketplace publishes is for **neither** actor in +particular: `rain-repo-conventions` is ONE skill holding what is true around the +work in any rainlanguage repo — the isolation a parallel agent needs, the +irreversible acts reserved to the human, the gate a `gh pr create` passes +through, what makes a wait terminate, and the environment facts whose shape a +run breaks on. + +``` +/plugin marketplace add rainlanguage/issue-pr-cron +/plugin install rain-repo-conventions@issue-pr-cron +``` + +**A brief is the wrong place for something that never varies.** These +constraints were hand-copied into agent brief after agent brief, which pays to +restate an invariant every time it is needed and offers a fresh chance to +restate it wrongly or omit one — the same argument that moved the worker rules +out of the improvised dispatch prompt and into +[`campaign-worker-prompt.txt`](#briefing-a-dispatched-worker--rules-not-state). +A `--agents` brief reaches only agents the cron itself dispatches; a marketplace +skill reaches every session on any box that installed it, which is where the +hand-copying was happening. + +**It is grouped by the KIND of rule, and the grouping is the content.** _Rules_ +are chosen and do not expire. _Facts_ are true of the environment rather than of +the work — each stated so a reader can check it, because the day the box changes +the entry is simply wrong, and a stale fact carried as belief is worse than no +entry. _Workarounds_ route around a defect elsewhere and each names what would +retire it. An agent that cannot tell which it is holding argues with a fact, +verifies a prohibition, or treats a piece of debt as permanent. + +**The split disciplines the entries themselves.** `pgrep -f` matching the +searching process's own command line is a FACT — checkable, and about the box. +"Every wait carries a maximum iteration count and is never keyed on a pattern +its own command line contains" is a RULE. Written as one entry they collapsed +into a ban on the whole `until ; do sleep; done` shape, which is stronger +than the fact underneath it: a bounded loop terminates correctly, so the ban +forbade something safe while naming neither thing that actually strands a wait. +A rule that overreaches its fact is the failure this grouping is built to catch. + +**It states properties, never cases**, for the reasons the section above gives. +It is also not a gate: the `## QA` block's substance stays in +[QA-GUIDE.md](QA-GUIDE.md) where `require-qa-block` and the vetter enforce it, +and this records only that the gate exists and what shape it refuses — one fact, +one surface. Where it and a repo's own `CLAUDE.md` disagree, the repo wins. + ### The vetter's transitions as an MCP surface For the producer, routing through the tool is enforced by the **prompt**, and a @@ -2287,24 +2334,25 @@ and evidence that answers a narrower question than the issue asked. ## Files (tracked here) -| File | Purpose | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `campaign-run.sh` | Durable runner (built as the `campaign-run` flake package): `flock` single-run lock, `DISABLED` kill-switch, `timeout`, invokes `claude --print` with the prompt + settings, logs to `campaign.log` (+ per-run JSONL traces in `runs/`). Nix builds its PATH; it sets none itself. | -| `campaign-prompt.txt` | The campaign instructions fed to the model. | -| `campaign-worker-prompt.txt` | The standing brief every DISPATCHED worker starts with. `campaign-run.sh` wraps it into the `pr-worker` subagent type with `jq` and passes it as `--agents`, so the harness loads it straight into each dispatched agent and the main loop pays none of those bytes. See [Briefing a dispatched worker](#briefing-a-dispatched-worker--rules-not-state). | -| `campaign-settings.json` | Tool allow/deny list passed via `--settings` (the permission guardrails). | -| `review-run.sh` | Vetting runner (same hardened pattern as `campaign-run.sh`): vets open PRs on the MCP surface, logs to `review.log`. Its one GitHub write is `record_verdict`. Kill-switch `review-DISABLED`. | -| `review-prompt.txt` | The AI-vetting instructions fed to the model: the judgement gates only — every `gh` recipe is a tool schema instead. | -| `review-auditor-prompt.txt` | The standing brief every DISPATCHED auditor starts with. `review-run.sh` wraps it into the `pr-auditor` subagent type with `jq` and passes it as `--agents`, with a `tools` list that is the READ half of the vetter's surface and no write at all. See [Fanning the audit out](#fanning-the-audit-out--and-keeping-the-verdict). | -| `review-settings.json` | Tool allow/deny for the vetter: the eight `mcp__fsm__*` tools + `Read`/`Glob`/`Grep`/`Skill`/`Task`/`ToolSearch`, **Bash denied outright**. `Task` is the dispatch tool the audit fan-out needs; every write tool stays denied, and a session deny reaches inside a dispatched agent too. | -| `review-mcp.json` | The vetter's MCP config: one stdio server, `pr-review-report mcp`, named `fsm` (so its tools are `mcp__fsm__*`). | -| `campaign-mcp.json` | MCP config for the producer's clone-lifecycle surface: one stdio server, `pr-review-report mcp --profile producer`, named `fsm`. Additive — the producer keeps its Bash. | -| `cron.env.example` | Template for deployment-specific values (PR assignee, work dir, models, run caps). Copy to `cron.env` (gitignored) and edit. | -| `pr-review-report.sh` | Thin wrapper (flake package `pr-review-report-sh`) over the binary. Reports every open PR by its pipeline stage (approved / AI-vetted / needs-producer-fix (red) / needs-work / close / unreviewed / pending / draft — a conflicted ready PR reports as needs-work, the state it is owed), reading `ai:*`/`human:*` labels + GitHub approvals, as clickable URLs. | -| `hooks/` | The two bash PreToolUse guards that close deny-list bypasses. See [PreToolUse guards](#pretooluse-guards--what-a-prompt-cannot-hold). | -| `.claude-plugin/` | The marketplace listing this repo publishes. Its version must match the plugin manifest's — `pr-review-report plugin-version-lockstep` is the gate. | -| `plugins/human-fsm/` | The human's slash commands as a Claude Code plugin. Prompts only: every guard is in the binary. See [The human's slash commands](#the-humans-slash-commands). | -| `plugins/vetter-judgement/` | The machine vetter's judgement as a Claude Code skill — properties, never cases — loaded on demand at verdict time rather than carried in `review-prompt.txt` on every run. See [The vetter's judgement as a skill](#the-vetters-judgement-as-a-skill). | +| File | Purpose | +| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `campaign-run.sh` | Durable runner (built as the `campaign-run` flake package): `flock` single-run lock, `DISABLED` kill-switch, `timeout`, invokes `claude --print` with the prompt + settings, logs to `campaign.log` (+ per-run JSONL traces in `runs/`). Nix builds its PATH; it sets none itself. | +| `campaign-prompt.txt` | The campaign instructions fed to the model. | +| `campaign-worker-prompt.txt` | The standing brief every DISPATCHED worker starts with. `campaign-run.sh` wraps it into the `pr-worker` subagent type with `jq` and passes it as `--agents`, so the harness loads it straight into each dispatched agent and the main loop pays none of those bytes. See [Briefing a dispatched worker](#briefing-a-dispatched-worker--rules-not-state). | +| `campaign-settings.json` | Tool allow/deny list passed via `--settings` (the permission guardrails). | +| `review-run.sh` | Vetting runner (same hardened pattern as `campaign-run.sh`): vets open PRs on the MCP surface, logs to `review.log`. Its one GitHub write is `record_verdict`. Kill-switch `review-DISABLED`. | +| `review-prompt.txt` | The AI-vetting instructions fed to the model: the judgement gates only — every `gh` recipe is a tool schema instead. | +| `review-auditor-prompt.txt` | The standing brief every DISPATCHED auditor starts with. `review-run.sh` wraps it into the `pr-auditor` subagent type with `jq` and passes it as `--agents`, with a `tools` list that is the READ half of the vetter's surface and no write at all. See [Fanning the audit out](#fanning-the-audit-out--and-keeping-the-verdict). | +| `review-settings.json` | Tool allow/deny for the vetter: the eight `mcp__fsm__*` tools + `Read`/`Glob`/`Grep`/`Skill`/`Task`/`ToolSearch`, **Bash denied outright**. `Task` is the dispatch tool the audit fan-out needs; every write tool stays denied, and a session deny reaches inside a dispatched agent too. | +| `review-mcp.json` | The vetter's MCP config: one stdio server, `pr-review-report mcp`, named `fsm` (so its tools are `mcp__fsm__*`). | +| `campaign-mcp.json` | MCP config for the producer's clone-lifecycle surface: one stdio server, `pr-review-report mcp --profile producer`, named `fsm`. Additive — the producer keeps its Bash. | +| `cron.env.example` | Template for deployment-specific values (PR assignee, work dir, models, run caps). Copy to `cron.env` (gitignored) and edit. | +| `pr-review-report.sh` | Thin wrapper (flake package `pr-review-report-sh`) over the binary. Reports every open PR by its pipeline stage (approved / AI-vetted / needs-producer-fix (red) / needs-work / close / unreviewed / pending / draft — a conflicted ready PR reports as needs-work, the state it is owed), reading `ai:*`/`human:*` labels + GitHub approvals, as clickable URLs. | +| `hooks/` | The two bash PreToolUse guards that close deny-list bypasses. See [PreToolUse guards](#pretooluse-guards--what-a-prompt-cannot-hold). | +| `.claude-plugin/` | The marketplace listing this repo publishes. Its version must match the plugin manifest's — `pr-review-report plugin-version-lockstep` is the gate. | +| `plugins/human-fsm/` | The human's slash commands as a Claude Code plugin. Prompts only: every guard is in the binary. See [The human's slash commands](#the-humans-slash-commands). | +| `plugins/vetter-judgement/` | The machine vetter's judgement as a Claude Code skill — properties, never cases — loaded on demand at verdict time rather than carried in `review-prompt.txt` on every run. See [The vetter's judgement as a skill](#the-vetters-judgement-as-a-skill). | +| `plugins/rain-repo-conventions/` | What is true around the work in ANY rainlanguage repo, as a Claude Code skill — grouped into rules that do not expire, environment facts to check, and workarounds that name what would retire them. See [The standing conventions as a skill](#the-standing-conventions-as-a-skill). | ## Briefing a dispatched worker — rules, not state diff --git a/plugins/rain-repo-conventions/.claude-plugin/plugin.json b/plugins/rain-repo-conventions/.claude-plugin/plugin.json new file mode 100644 index 0000000..f326f52 --- /dev/null +++ b/plugins/rain-repo-conventions/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "rain-repo-conventions", + "displayName": "Rain repo conventions", + "version": "0.1.0", + "description": "The standing constraints on any agent doing work in a rainlanguage repo, shipped once so a brief stops restating them. One skill, grouped by what KIND of rule each entry is: rules that are chosen and never expire (clone and scratch isolation for parallel agents, the irreversible acts reserved to the human, assignment, never departing from an agreed spec, never reporting a run you did not watch finish, keeping every wait bounded), facts that are true of the box and the harness rather than of the work (the `## QA` gate on `gh pr create` and its literal-path argument, force-backgrounded builds, `jq` only inside the devshell, a `pgrep` search that finds itself), and workarounds that name the defect they route around and what would retire them (the soldeer bump sequence, the generated pre-commit config). A wrong fact carried as belief is worse than no entry, so each is stated so a reader can check it — and it states properties, never cases.", + "author": { + "name": "Rain Open Source Software Ltd", + "url": "https://github.com/rainlanguage" + }, + "homepage": "https://github.com/rainlanguage/issue-pr-cron", + "repository": "https://github.com/rainlanguage/issue-pr-cron", + "license": "LicenseRef-DCL-1.0", + "keywords": [ + "rainlanguage", + "conventions", + "agents", + "tooling", + "github", + "foundry" + ], + "skills": "./skills/" +} diff --git a/plugins/rain-repo-conventions/README.md b/plugins/rain-repo-conventions/README.md new file mode 100644 index 0000000..5e15ff0 --- /dev/null +++ b/plugins/rain-repo-conventions/README.md @@ -0,0 +1,41 @@ +# rain-repo-conventions — what is true around the work + +One skill. Every agent working in a rainlanguage repo is bound by the same +standing constraints, and until they were shipped somewhere they were +hand-copied into brief after brief — which is a per-brief cost paid to restate +something that never varies, and a per-brief opportunity to restate it wrongly +or leave one out. + +It is grouped by the KIND of rule each entry is, because that is what decides +what a reader does with it: + +| Section | What the group is | +| --------------- | ------------------------------------------------------------------------------------------------------------ | +| **Rules** | chosen, and they do not expire — nothing about the box lifts one | +| **Facts** | true of the environment rather than of the work — stated so you can check one, wrong the day the box changes | +| **Workarounds** | a route around a defect elsewhere, each naming what would retire it | + +The split is not decoration. A rule is argued with, a fact is verified, and a +workaround is a piece of debt with an owner — an agent that cannot tell which it +is holding treats a stale environment fact as a prohibition, or a prohibition as +something to route around. + +**It states properties, never cases.** No PR numbers, no dates, no incidents. An +instruction that needs a worked example to be obeyed is underspecified; a cited +case rots when its subject changes state, and its context cost is paid on every +load. The one anecdote the source list carried — a shared scratch path that +nearly put one repo's test numbers in another repo's PR body — is here as the +mechanism instead, which is the half that is actually applicable. + +**It is not a gate and it does not replace one.** The `## QA` block's content is +[QA-GUIDE.md](https://github.com/rainlanguage/issue-pr-cron/blob/main/QA-GUIDE.md)'s, +enforced by `pr-review-report require-qa-block` and judged by the vetter; this +records only that the gate is there and what shape it refuses. Where this file +and a repo's own `CLAUDE.md` disagree, the repo wins. + +## Install + +``` +/plugin marketplace add rainlanguage/issue-pr-cron +/plugin install rain-repo-conventions@issue-pr-cron +``` diff --git a/plugins/rain-repo-conventions/skills/rain-repo-conventions/SKILL.md b/plugins/rain-repo-conventions/skills/rain-repo-conventions/SKILL.md new file mode 100644 index 0000000..75ce40a --- /dev/null +++ b/plugins/rain-repo-conventions/skills/rain-repo-conventions/SKILL.md @@ -0,0 +1,122 @@ +--- +name: rain-repo-conventions +description: The standing constraints on any agent doing work in a rainlanguage repo, so a brief does not have to restate them — what is never done, what is true of the box and the harness rather than of the work, and what is a route around a defect that should be fixed. Covers clone and scratch isolation for parallel agents, the irreversible acts reserved to the human, the `## QA` gate every `gh pr create` passes through, the force-backgrounding of long builds and how to read one to completion, what makes a wait terminate, and the soldeer bump sequence. Invoke once per session, before the first clone or the first write. Triggers on "work this issue", "open a PR", "clone the repo to work in", "bump a dependency", "run the test suite", "wait for CI", "CI is red", "report the result". +version: 0.1.0 +--- + +# Rain repo conventions + +These bind every agent working in a rainlanguage repo, whether or not its brief +restates them, and they are why a brief does not have to. Nothing here is a +pipeline transition, a verdict, or a tool: it is what is true around the work. + +**Where this and a repo's own `CLAUDE.md` disagree, the repo wins** — it is +closer to the code and it is versioned with it. Where this and the brief you +were given disagree, **say so and stop**; picking one silently is the failure +either way round. + +Three groups, and the group tells you what kind of rule you are reading: + +- **Rules** are chosen. Nothing about the box lifts one and none of them + expires. +- **Facts** are true of the environment, not of the work. Each is stated so you + can check it — when the environment changes the entry is wrong, and a wrong + fact carried as belief is worse than no entry at all. +- **Workarounds** route around a defect somewhere else. Each names what would + retire it. Fixing the defect and deleting the entry is always the better move. + +## Rules + +- **One fresh clone per agent invocation, at a path no other agent is using.** + Never a git worktree: worktrees share one repository — its refs, its config + and its object store — so two agents in two worktrees are one agent with two + prompts. A distinct clone path is the only isolation the filesystem actually + enforces, and the failure it prevents is silent cross-contamination rather + than an error anyone sees. +- **Scratch files and logs go under a path scoped to the repo being worked, + never a shared session scratchpad.** Parallel agents are handed the same + scratchpad, so an unqualified filename is one another agent is also writing: + what you read back is another repo's numbers, and you will report them as this + repo's. A count, a log or a diff read out of a shared path is evidence for + nothing. +- **Never merge a PR, delete a branch, create a tag, dispatch a deploy workflow + or broadcast a transaction. Never force-push, in any spelling.** Every one of + them either destroys work the next agent cannot recover or moves state a human + then has to live with. Performing one is taking a decision, not doing the work + you were asked for. +- **Assign every PR and issue you open to `thedavidmeister`.** An unassigned + subject has no inbox: it is found only by someone already looking for it. +- **Never depart from an agreed spec without asking first — including to turn a + red CI green.** A red that reflects reality is information, and the damage in + papering over it is that the information is destroyed rather than read. Ask, + and say what the red actually reports. +- **Never report a result you did not watch finish.** "Green" requires the + pass/fail line in front of you — not an exit code you inferred, not a run you + started and left, not a suite that was green on the last head. A claim about a + run you did not read to its end is fabricated whatever the run goes on to do. +- **A wait is bounded, and never keyed on a pattern its own command line + contains.** Every loop carries a maximum iteration count and says what it last + saw when it reaches it, because an unbounded loop whose condition never + arrives does not fail — it runs on past the turn that started it, invisible + and unattended, and they accumulate. The self-matching pattern below is the + other way one never terminates. Prefer neither shape: poll once in the + foreground and move on, or read the backgrounded output file. A loop is the + last resort, not the default. + +## Facts + +- **`gh pr create` passes through a PreToolUse gate that reads the body before + anything is created.** The body must carry a literal `## QA` heading and these + four literal lines: + + ``` + ## QA + - Discriminating tests: + - Mutations applied: + - Oracle: + - Category check: + ``` + + An honest `n/a` **with its reason** satisfies a line the change genuinely + cannot have; an absent line does not. The gate checks the block is PRESENT — + what the four lines have to say, and whether their claims hold, is the + producer QA guide's business and the vetter's, not this file's. + +- **Push the branch before `gh pr create`.** The create is where the gate reads + the body, and a create against a branch the remote does not have either fails + outright or drags an interactive push prompt into a session that cannot answer + one. +- **That gate reads a command line, not a shell.** `--body-file` must be a + literal absolute path: a `$VAR` or a `~` in the argument is never expanded, + because the gate resolves quoting and nothing else. This one is not a defect + awaiting a fix — a guard that expanded shell variables would have to BE a + shell, with every bypass that implies. +- **A long `nix`, `forge` or `cargo` command may be force-backgrounded by a + hook.** The call then returns immediately with an output file path in place of + the command's output. Poll that file with `Read` until the command's own + completion line is in it: the return is not the result, and neither is the + last line of a file still being written. +- **`jq` is on PATH only inside the flake devshell.** Outside one it is simply + absent, and a pipeline through it fails in a way that reads like the `gh` call + failing. `gh --jq` takes the same expressions and needs nothing on PATH. +- **`pgrep -f ` matches the searching process's own command line.** The + pattern sits in the argv of the shell doing the looking, so the search finds + itself and goes on finding itself after the process it is watching is gone. A + wait keyed on it can never be satisfied in the direction it needs. + +## Workarounds + +- **Bump a soldeer dependency with `soldeer update`, never `soldeer install`.** + `install` resolves from the lock rather than from the bumped manifest and dies + in the remappings step, with an error that names the remapping instead of the + resolution that produced it. `update` then **appends** the new remapping + without pruning the one it replaces: delete the stale line by hand, and check + what still imports through it — a versioned import prefix is deliberate, so a + leftover remapping keeps compiling and pins the old version silently. _Retired + when a bump resolves from the manifest and rewrites the remapping in place._ +- **Never commit a `.pre-commit-config.yaml`.** Entering a rainix devshell drops + one in the repo root — a symlink into the nix store, rewritten on every entry. + It is output, not source, and it looks authored because it arrives untracked + next to real files. Much of the org already `.gitignore`s it and the rest does + not. _Retired per repo by that `.gitignore` line, which is the fix worth + making the moment you meet a repo without it._