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
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ metadata:
# acme rule selector

Resolve to `selector: none` when the `ACME_RULES` environment variable is
`off`. Otherwise resolve to these three lines:
`off`. Otherwise resolve to these four lines:

selector: acme-rules select --format {format}
verdicts: acme-rules verdicts --selection {selection} --verdicts {verdicts}
fence: acme-verdicts
check: acme-rules check --base {base}
15 changes: 14 additions & 1 deletion lib/skills/__tests__/fixtures/rule-selector/pack/skills.jsonc
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
{
"version": 2,
"pipelines": {
"feature": [
"mattstack:stage-provision",
"mattstack:stage-plan",
"mattstack:stage-gates",
"mattstack:stage-evidence",
"mattstack:stage-implement",
"mattstack:stage-self-review",
"mattstack:stage-ship",
"mattstack:stage-watch-ci"
]
},
"bindings": {
"mattstack:review/rule-selector": { "rule-selector": "acme:acme-rule-selector" },
"mattstack:self-review/rule-selector": { "rule-selector": "acme:acme-rule-selector" },
"mattstack:receive-review/rule-selector": { "rule-selector": "acme:acme-rule-selector" }
"mattstack:receive-review/rule-selector": { "rule-selector": "acme:acme-rule-selector" },
"mattstack:stage-implement/rule-selector": { "rule-selector": "acme:acme-rule-selector" }
}
}
3 changes: 2 additions & 1 deletion lib/skills/__tests__/fixtures/rule-selector/pack/stubs.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"verbs": {
"review": { "engine": "review", "description": "Review a teammate's MR" },
"self-review": { "engine": "self-review", "description": "Self-review the current branch" },
"receive-review": { "engine": "receive-review", "description": "Act on review feedback on your MR" }
"receive-review": { "engine": "receive-review", "description": "Act on review feedback on your MR" },
"work": { "engine": "work", "pipeline": "feature", "description": "Run a unit of work end to end" }
}
}
65 changes: 62 additions & 3 deletions lib/skills/__tests__/rule-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ const seam = (verb: string) => `<!-- part: prop:${verb}/rule-selector.rule-selec
const SELECT = /^#+ Select the rules$/m;
const FAILS_TO_FINDINGS = /^#+ Failed rules become findings$/m;

type Compiled = Record<(typeof ENGINES)[number], string>;
const CHECK_LINE = "check: acme-rules check --base {base}";
const edge = (a: string, b: string) => new RegExp(`\\b${a} -> ${b}\\b`);

type Compiled = Record<(typeof ENGINES)[number] | "stage-implement", string>;

let root = "";
let seq = 0;

function writeBindings(pack: string, bindings: Record<string, Record<string, string>>): void {
writeFileSync(join(pack, "pack", "skills.jsonc"), JSON.stringify({ version: 2, bindings }, null, 2));
const file = join(pack, "pack", "skills.jsonc");
const manifest = JSON.parse(readFileSync(file, "utf8"));
writeFileSync(file, JSON.stringify({ ...manifest, bindings }, null, 2));
}

/** A scratch copy of the fixture pack, `shape` applied, and a mattstack home that points at this repo's real plugin. */
Expand All @@ -48,7 +53,7 @@ async function compilePack(shape?: (pack: string) => void): Promise<{ pack: stri
const side = existsSync(join(pack, "skills", verb)) ? "skills" : "attachments";
return readFileSync(join(pack, side, verb, "SKILL.md"), "utf8");
};
return { pack, verbs: Object.fromEntries(ENGINES.map((v) => [v, read(v)])) as Compiled };
return { pack, verbs: Object.fromEntries([...ENGINES, "stage-implement"].map((v) => [v, read(v)])) as Compiled };
}

describe("rule selector slot", () => {
Expand Down Expand Up @@ -138,6 +143,42 @@ describe("rule selector slot", () => {
expect(body).not.toMatch(/\bcode_changes_answer -> dispatch_adjudicator\b/);
expect(body).not.toMatch(/\bcheck_verdicts\b/);
});

test("the contract takes an optional check line and stays rule-selector@1", () => {
for (const verb of ENGINES) {
const body = compiled.verbs[verb];
expect(body).toContain(CHECK_LINE);
expect(body).toMatch(/optional fourth line,\s+`check: <command template>`/);
expect(body).toMatch(/The\s+review verbs never run it/);
}
});

test("stage-implement inlines the fill under its rule-selector prop", () => {
const body = compiled.verbs["stage-implement"];
expect(body).toContain(CHECK_LINE);
expect(body).toContain("<!-- part: prop:stage-implement/rule-selector.rule-selector ");
expect(body).toMatch(/^#+ Check the work against the team's rules$/m);
});

test("stage-implement graph runs the check between committing and recording the commits", () => {
const body = compiled.verbs["stage-implement"];
expect(body).toMatch(edge("next_move", "check_bound"));
expect(body).toMatch(edge("stop_implement_push", "check_bound"));
expect(body).toMatch(edge("check_bound", "check_rules"));
expect(body).toMatch(edge("check_rules", "check_result"));
expect(body).toMatch(edge("check_result", "fix_rule_breaks"));
expect(body).toMatch(edge("fix_rule_breaks", "git_log_shas"));
expect(body).not.toMatch(edge("next_move", "git_log_shas"));
const into = [...body.matchAll(/^\s*(\w+) -> git_log_shas\b/gm)].map((m) => m[1]).sort();
expect(into).toEqual(["check_bound", "check_result", "check_result", "fix_rule_breaks"]);
});

test("stage-implement: a failed check never fails the stage", () => {
const body = compiled.verbs["stage-implement"];
expect(body).not.toMatch(edge("check_result", "run_stage_fail"));
expect(body).not.toMatch(edge("fix_rule_breaks", "run_stage_fail"));
expect(body).toMatch(/check_result -> git_log_shas \[label="non-zero exit/);
});
});

test("unbound: no fill text, bodies still present", async () => {
Expand All @@ -149,6 +190,24 @@ describe("rule selector slot", () => {
}
});

test("unbound stage-implement keeps the check step and its skip edge", async () => {
const unbound = await compilePack((pack) => writeBindings(pack, {}));
const body = unbound.verbs["stage-implement"];
expect(body).not.toContain("acme-rules");
expect(body).toMatch(/check_bound -> git_log_shas \[label="no: unbound, selector: none, or no check: line"\]/);
});

test("a fill without a check line compiles and leaves only the skip edge to take", async () => {
const noCheck = await compilePack((pack) => {
const fill = join(pack, "attachments", "acme-rule-selector", "SKILL.md");
writeFileSync(fill, readFileSync(fill, "utf8").replace(`${CHECK_LINE}\n`, "").replace("four lines", "three lines"));
});
const body = noCheck.verbs["stage-implement"];
expect(body).toContain(FILL_LINE);
expect(body).not.toContain(CHECK_LINE);
expect(body).toMatch(/check_bound -> git_log_shas \[label="no: unbound, selector: none, or no check: line"\]/);
});

test("a fill providing another contract is refused", async () => {
const { compileArgs } = stagePack((pack) =>
writeBindings(pack, { "mattstack:review/rule-selector": { "rule-selector": "acme:acme-wrong-contract" } }),
Expand Down
2 changes: 1 addition & 1 deletion plugins/mattstack/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mattstack",
"version": "0.32.32",
"version": "0.32.33",
"description": "The mattstack skills pack: public doors subagent-review-loop, model-tiering, editing-skills, creating-a-pack, extending-a-pack, getting-current-time, process-digraphs, work-progress, wrap-up, the pack's own compiled shepherdr verb (domain-unbound), and the three writing-style presets (sparse, conversational, structured), plus the compile-native engines (work orchestrator, pipeline stages, review verbs, forge adapters, shepherdr) and attachments (execution strategies, account pool, review include bodies, review-posting, parameterized-skill primitive) that rt skills compile inlines into team verbs at compile time -- reached by name, not by slash menu. Also ships an mcp tools reference read by path, not inlined.",
"author": {
"name": "Matthew Goodwin"
Expand Down
3 changes: 2 additions & 1 deletion plugins/mattstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ running that one step outside a full pipeline.
- **mattstack:stage-evidence** -- capture the before-state per the evidence
plan, before implementation.
- **mattstack:stage-implement** -- do the implementation under the TDD
floor (no props; pure methodology).
floor, then run the bound rule selector's optional `check:` once
(`rule-selector` prop).
- **mattstack:stage-self-review** -- self-review checkpoint between
implementation and ship.
- **mattstack:stage-ship** -- publish the unit of work: push, open the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
node: fix_rule_breaks
---

# Fix the rules the change breaks, then commit

Fix each rule the check showed the change breaking. A fix that changes
behavior is test-first, as in the rest of this stage. Commit the fixes on
the branch, inside the worktree. A rule you judge does not apply, or a
fix you cannot make without leaving the plan, stays as it is: name the
rule and the line in one line when the stage returns to the orchestrator.
Then record the commits; there is no second check.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ flowchart TB
commit_incrementally[Commit incrementally on branch, inside worktree]
next_move{Next move once committed?}
stop_implement_push[STOP: implement never pushes; ship owns the push]:::stop
check_bound{Does the rule selector resolve to a check line?}
check_rules
check_result{What did the check report?}
fix_rule_breaks
git_log_shas[/git log --format=%h <branch-point>..HEAD/]
run_field_set_commits[/"run_field_set {key: commits, value: <shas>, stage: implement}"/]
run_stage_fail[/"run_stage {action: fail, stage: implement, reason}"/]
Expand All @@ -54,9 +58,16 @@ flowchart TB
superpowers_chain --> commit_incrementally
superpowers_chain -->|a failure the approach cannot get past| run_stage_fail
commit_incrementally --> next_move
next_move -->|record the commits| git_log_shas
next_move -->|check the work, then record the commits| check_bound
next_move -->|tempted to push| stop_implement_push
stop_implement_push --> git_log_shas
stop_implement_push --> check_bound
check_bound -->|no: unbound, selector: none, or no check: line| git_log_shas
check_bound -->|yes| check_rules
check_rules --> check_result
check_result -->|exit 0, nothing the change breaks| git_log_shas
check_result -->|exit 0, rules the change breaks| fix_rule_breaks
check_result -->|non-zero exit: noted for the return| git_log_shas
fix_rule_breaks --> git_log_shas
git_log_shas --> run_field_set_commits
run_field_set_commits --> implement_done
run_stage_fail --> stage_failed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
node: check_rules
props:
rule-selector:
merge: replace
default: ""
contract: "rule-selector@1"
---

# Check the work against the team's rules

The pack's rule selector is the text after the italic line at the end of
this section; an unbound slot leaves it empty. Resolve it now, on this
run, since it may consult settings or the environment. It resolves to the
single line `selector: none`, or to `selector:`, `verdicts:` and `fence:`
lines with an optional fourth, `check: <command template>`. Only the
`check:` line matters here. Skip this step (the `no` edge of Does the rule
selector resolve to a check line?) when the text is empty, resolves to
`selector: none`, or has no `check:` line.

With a `check:` line:

1. Find the branch point the way self-review does: `git merge-base
origin/HEAD HEAD`, or the default branch by name when `origin/HEAD` is
unset.
2. Replace `{base}` in the command with that sha and run it once on Bash,
in the worktree, with nothing on stdin. Capture its stdout and its exit
status.
3. Read the stdout. It is written for you: the rules that apply to this
change and the lines worth a look. Decide, rule by rule, whether the
change breaks it.

An exit other than 0 is one observation, never a failure of the stage:
quote the exit status and the first line of stderr in one line when the
stage returns to the orchestrator, fix nothing on its account, and record
the commits.

Run it once. The check never runs again in this stage. Rule verdicts come
later only where the pack's self-review runs the self-review verb with
this same selector bound; the default self-review stage reads the diff
without the rules.

*If a rule below asks for a move this skill's graph marks STOP, take the off-script edge instead.*

{{props.rule-selector}}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ shapes.
- `fence: <info string>`, the tag of the fenced code block the reviewer
returns its verdict set in.

An optional fourth line, `check: <command template>`, holds the
placeholder `{base}`. The implement stage runs it in the worktree with
nothing on stdin, `{base}` replaced by the branch point, and reads its
stdout as the rules that apply and the lines worth a look; exit 0 means
it ran, and any other exit is one observation that never blocks. The
review verbs never run it: a fill with a `check:` line is still the
three-line shape here. The line is additive, so the contract stays
`rule-selector@1`, and a fill without it behaves as before.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

**The selection** is the JSON the selector prints. This skill reads only
its `rules` list: each rule has an `id`, a `severity` (`blocking` or
`advisory`) and a `summary`.
Expand Down
2 changes: 1 addition & 1 deletion plugins/mattstack/plugin/skills/extending-a-pack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ metadata:
```

`provides` is the prop's contract when it has one (`criteria` takes
`review-criteria@1`, `rule-selector` takes `rule-selector@1`); a `rules` prop has none, so any `provides` binds.
`review-criteria@1`, `rule-selector` takes `rule-selector@1`, whose optional `check:` line `stage-implement` runs); a `rules` prop has none, so any `provides` binds.

The body: the rule, its reason, and the decision it changes. The stage
keeps its own flow; the fill carries only what the team adds.
Expand Down
6 changes: 4 additions & 2 deletions plugins/mattstack/plugin/skills/extending-a-pack/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ block. One fill may bind to any number of blocks.
| MR preflight, fast checks, commit shape, description shape, labels | `stage-ship` and `ship`: the step blocks below, `domain-rules` |
| CI job names, retry rules, triage | `stage-watch-ci` and `watch-ci`: `classify-triage-report`, `domain-rules`, `forge` |
| what reviewers check | `review`, `self-review`, `receive-review`: `criteria` |
| which written rules a review is checked against, one verdict per rule | `review`, `self-review`, `receive-review`: `rule-selector` |
| which written rules a review is checked against, one verdict per rule; the check implement runs before self-review | `review`, `self-review`, `receive-review`, `stage-implement`: `rule-selector` |
| how to answer review threads | `receive-review`: `reply-rules` |
| fan-out rules for parallel agents | `shepherdr`: the root's `rules`, or a step block below |

Expand All @@ -94,7 +94,7 @@ default. The other props:
| `work/sub-agent-tiering` | `tiering` | replace | `model-tiering@1` | nothing |
| `review/criteria`, `self-review/criteria`, `receive-review/criteria` | `criteria` | replace | `review-criteria@1` | nothing |
| `review/reviewer`, `self-review/reviewer`, `receive-review/reviewer` | `reviewer` | replace | `reviewer-dispatch@1` | nothing |
| `review/rule-selector`, `self-review/rule-selector`, `receive-review/rule-selector` | `rule-selector` | replace | `rule-selector@1` | empty (no rule selection) |
| `review/rule-selector`, `self-review/rule-selector`, `receive-review/rule-selector`, `stage-implement/rule-selector` | `rule-selector` | replace | `rule-selector@1` | empty (no rule selection) |
| `receive-review/reply-rules` | `reply-rules` | replace | `reply-rules@1` | nothing |
| `shepherdr` | `tiering` | replace | `model-tiering@1` | `mattstack:model-tiering` |
| `shepherdr` | `strategy` | replace | `execution-strategy@1` | `mattstack:execution-strategy` |
Expand All @@ -103,6 +103,8 @@ default. The other props:
A `rule-selector` fill resolves to `selector: none`, or three lines: `selector:`
(a command with `{format}`), `verdicts:` (a command with `{selection}` and
`{verdicts}`) and `fence:` (the info string of the reviewer's verdict block).
It may add an optional fourth line, `check: <command template>` with `{base}`,
which only `stage-implement` runs; still `rule-selector@1`, since it is additive.

A `shepherdr` door compiles with nothing bound; bind one of its three
replace props only to swap in the pack's own fill.
Expand Down
Loading
Loading