@mrciphersmith/keryx 0.3.114 → 0.3.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cli.js +2917 -2387
- package/dist/core.js +59 -15
- package/package.json +1 -1
- package/src/gdskills/bundled/install-manifest.json +1 -0
- package/src/gdskills/bundled/skills/review/review-layout/SKILL.md +2 -2
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +39 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +5 -6
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +9 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +4 -0
- package/src/gdskills/bundled/skills/review/review-pr-contract/SKILL.md +191 -0
- package/src/gdskills/bundled/skills/review/review-testing-practices/SKILL.md +5 -5
- package/src/gdskills/contracts/subagent-dispatch.schema.json +4 -0
package/dist/core.js
CHANGED
|
@@ -1078,6 +1078,11 @@ var init_catalog = __esm(() => {
|
|
|
1078
1078
|
"Anchor class_scope on the callers that break, never on the changed line.",
|
|
1079
1079
|
"Say when the radius could not answer \u2014 a non-code change has an empty one."
|
|
1080
1080
|
]),
|
|
1081
|
+
skill("review-pr-contract", "review", ["recommended", "full"], "Compare what the PR body claims (checklist boxes, stated changes) with the diff, and look for documentation outside the diff that the change made stale.", [
|
|
1082
|
+
"Work every checklist box in pr-context.md as a row: quoted box, state, quoted diff line, expectation.",
|
|
1083
|
+
"File a finding only where a quoted box or claim disagrees with a quoted diff or document line.",
|
|
1084
|
+
"On a shell host stay read-only and answer NEEDS_CONTEXT with named paths instead of guessing a document."
|
|
1085
|
+
]),
|
|
1081
1086
|
skill("review-core-boundaries", "review", ["recommended", "full"], "Review shared/core module coupling, public API stability, and dependency minimization.", [
|
|
1082
1087
|
"Identify core/shared surfaces changed by the task.",
|
|
1083
1088
|
"Check if feature-specific logic leaked into shared modules.",
|
|
@@ -9695,8 +9700,9 @@ var init_skill_length_ceilings = __esm(() => {
|
|
|
9695
9700
|
"review/review-jev-scenarios": 187,
|
|
9696
9701
|
"review/review-layout": 238,
|
|
9697
9702
|
"review/review-logic": 370,
|
|
9698
|
-
"review/review-orchestrator":
|
|
9703
|
+
"review/review-orchestrator": 1615,
|
|
9699
9704
|
"review/review-performance": 359,
|
|
9705
|
+
"review/review-pr-contract": 191,
|
|
9700
9706
|
"review/review-pr-feedback": 895,
|
|
9701
9707
|
"review/review-regression": 214,
|
|
9702
9708
|
"review/review-security-code": 364,
|
|
@@ -28072,6 +28078,7 @@ var NOT_ROSTER = new Set([
|
|
|
28072
28078
|
"review-orchestrator",
|
|
28073
28079
|
"review-verifier",
|
|
28074
28080
|
"review-regression",
|
|
28081
|
+
"review-pr-contract",
|
|
28075
28082
|
"review-pr-feedback",
|
|
28076
28083
|
"review-greptile",
|
|
28077
28084
|
"review-strict"
|
|
@@ -28125,6 +28132,10 @@ async function readSkill(root, relativePath) {
|
|
|
28125
28132
|
}) : undefined;
|
|
28126
28133
|
}
|
|
28127
28134
|
var REGRESSION = "review-regression";
|
|
28135
|
+
var PR_CONTRACT = "review-pr-contract";
|
|
28136
|
+
function dispatchKnowsRole(dispatch, role) {
|
|
28137
|
+
return Array.isArray(dispatch.roles) && dispatch.roles.includes(role);
|
|
28138
|
+
}
|
|
28128
28139
|
async function requiredReviewers(root, mode, files, options = {}) {
|
|
28129
28140
|
const inventory = await collectReviewers(root);
|
|
28130
28141
|
const stack = await detectProjectStack(root);
|
|
@@ -28180,6 +28191,12 @@ async function requiredReviewers(root, mode, files, options = {}) {
|
|
|
28180
28191
|
else
|
|
28181
28192
|
excluded.push({ reviewer: REGRESSION, reason: "no blast radius computed for the package: there is no scope B to review" });
|
|
28182
28193
|
}
|
|
28194
|
+
if (inventory.bundled.some((r) => r.name === PR_CONTRACT)) {
|
|
28195
|
+
if (options.prContext === true)
|
|
28196
|
+
required.push(PR_CONTRACT);
|
|
28197
|
+
else
|
|
28198
|
+
excluded.push({ reviewer: PR_CONTRACT, reason: "no pr-context.md in the package: there is no PR body checklist to compare with the diff" });
|
|
28199
|
+
}
|
|
28183
28200
|
return { mode, required: required.sort(), excluded, filesKnown };
|
|
28184
28201
|
}
|
|
28185
28202
|
function isAttendedRun(env = process.env) {
|
|
@@ -28206,6 +28223,9 @@ async function packageHasBlastRadius(packageDir, name = "blast-radius.json") {
|
|
|
28206
28223
|
return false;
|
|
28207
28224
|
}
|
|
28208
28225
|
}
|
|
28226
|
+
function packageHasPrContext(packageDir, name = "pr-context.md") {
|
|
28227
|
+
return existsSync6(path46.join(packageDir, name));
|
|
28228
|
+
}
|
|
28209
28229
|
async function coverageErrors(dispatch, options) {
|
|
28210
28230
|
const record = dispatch && typeof dispatch === "object" && !Array.isArray(dispatch) ? dispatch : undefined;
|
|
28211
28231
|
return [...await reviewerSetErrors(dispatch, options), ...record === undefined ? [] : mutationErrors(record, options)];
|
|
@@ -28226,7 +28246,10 @@ async function reviewerSetErrors(dispatch, options) {
|
|
|
28226
28246
|
}
|
|
28227
28247
|
if (record.mode === "diff" && options.files === undefined)
|
|
28228
28248
|
return ["diff mode needs the scope-A file list: pass `--scope <scope.json>` to `keryx review start|ingest`, or run mode all"];
|
|
28229
|
-
const need = await requiredReviewers(options.root, record.mode, options.files, {
|
|
28249
|
+
const need = await requiredReviewers(options.root, record.mode, options.files, {
|
|
28250
|
+
blastRadius: options.blastRadius,
|
|
28251
|
+
prContext: options.prContext === true && dispatchKnowsRole(record, PR_CONTRACT)
|
|
28252
|
+
});
|
|
28230
28253
|
const selected = new Set(Array.isArray(record.selected) ? record.selected.filter(text) : []);
|
|
28231
28254
|
const missing = need.required.filter((name) => !selected.has(name));
|
|
28232
28255
|
if (!missing.length)
|
|
@@ -28657,6 +28680,7 @@ function dispatchErrors(value) {
|
|
|
28657
28680
|
import { existsSync as existsSync8 } from "fs";
|
|
28658
28681
|
import { cp, mkdir as mkdir9, mkdtemp, readFile as readFile23, readdir as readdir10, realpath as realpath12, rm as rm3, symlink, writeFile as writeFile6 } from "fs/promises";
|
|
28659
28682
|
import path49 from "path";
|
|
28683
|
+
var MUTANT_KINDS = ["behavior", "presentation"];
|
|
28660
28684
|
var MUTANTS_SPEC = "mutants.json";
|
|
28661
28685
|
var EVIDENCE_DIR = "research-evidence";
|
|
28662
28686
|
var SKIP_COPY = new Set([".git", "node_modules"]);
|
|
@@ -28676,9 +28700,39 @@ function parseMutantSpec(raw) {
|
|
|
28676
28700
|
ids.add(id);
|
|
28677
28701
|
if (m.anchor === m.replacement)
|
|
28678
28702
|
throw new Error(`mutant ${id}: replacement equals the anchor, so nothing changes`);
|
|
28679
|
-
|
|
28703
|
+
if (m.kind !== undefined && !MUTANT_KINDS.includes(m.kind))
|
|
28704
|
+
throw new Error(`mutant ${id}: "kind" must be one of ${MUTANT_KINDS.join(", ")}`);
|
|
28705
|
+
return {
|
|
28706
|
+
id,
|
|
28707
|
+
file: m.file,
|
|
28708
|
+
anchor: m.anchor,
|
|
28709
|
+
replacement: m.replacement,
|
|
28710
|
+
note: typeof m.note === "string" ? m.note : undefined,
|
|
28711
|
+
...m.kind === undefined ? {} : { kind: m.kind }
|
|
28712
|
+
};
|
|
28680
28713
|
});
|
|
28681
28714
|
}
|
|
28715
|
+
async function doneMutantIds(packageDir) {
|
|
28716
|
+
const dir = path49.join(packageDir, EVIDENCE_DIR);
|
|
28717
|
+
const done = new Set;
|
|
28718
|
+
if (!existsSync8(dir))
|
|
28719
|
+
return done;
|
|
28720
|
+
for (const name of (await readdir10(dir)).filter((n) => /^mutation-\d+\.json$/.test(n))) {
|
|
28721
|
+
try {
|
|
28722
|
+
const record = JSON.parse(await readFile23(path49.join(dir, name), "utf8"));
|
|
28723
|
+
for (const m of record.mutants)
|
|
28724
|
+
if (m.status !== "invalid")
|
|
28725
|
+
done.add(m.id);
|
|
28726
|
+
} catch {
|
|
28727
|
+
continue;
|
|
28728
|
+
}
|
|
28729
|
+
}
|
|
28730
|
+
return done;
|
|
28731
|
+
}
|
|
28732
|
+
async function unjudgedMutants(packageDir, specs) {
|
|
28733
|
+
const done = await doneMutantIds(packageDir);
|
|
28734
|
+
return specs.filter((s) => !done.has(s.id));
|
|
28735
|
+
}
|
|
28682
28736
|
async function pendingMutants(packageDir) {
|
|
28683
28737
|
const dir = path49.join(packageDir, EVIDENCE_DIR);
|
|
28684
28738
|
const specFile = path49.join(dir, MUTANTS_SPEC);
|
|
@@ -28693,18 +28747,7 @@ async function pendingMutants(packageDir) {
|
|
|
28693
28747
|
} catch {
|
|
28694
28748
|
return [];
|
|
28695
28749
|
}
|
|
28696
|
-
|
|
28697
|
-
for (const name of (await readdir10(dir)).filter((n) => /^mutation-\d+\.json$/.test(n))) {
|
|
28698
|
-
try {
|
|
28699
|
-
const record = JSON.parse(await readFile23(path49.join(dir, name), "utf8"));
|
|
28700
|
-
for (const m of record.mutants)
|
|
28701
|
-
if (m.status !== "invalid")
|
|
28702
|
-
done.add(m.id);
|
|
28703
|
-
} catch {
|
|
28704
|
-
continue;
|
|
28705
|
-
}
|
|
28706
|
-
}
|
|
28707
|
-
return specs.filter((s) => !done.has(s.id));
|
|
28750
|
+
return unjudgedMutants(packageDir, specs);
|
|
28708
28751
|
}
|
|
28709
28752
|
|
|
28710
28753
|
// src/review/research.ts
|
|
@@ -28768,6 +28811,7 @@ async function packageResearchErrors(packageDir, artifacts, findings, options =
|
|
|
28768
28811
|
root: projectRootOfPackage(packageDir),
|
|
28769
28812
|
files: await packageScopeFiles(packageDir),
|
|
28770
28813
|
blastRadius: await packageHasBlastRadius(packageDir),
|
|
28814
|
+
prContext: packageHasPrContext(packageDir),
|
|
28771
28815
|
pendingMutants: (await pendingMutants(packageDir)).length
|
|
28772
28816
|
});
|
|
28773
28817
|
const completeness = a.reviewerDispatch === undefined || options.coverage === false || options.completeness === false || dispatch.length ? [] : await coverageGateErrors(packageDir, ledger);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.116",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -465,6 +465,7 @@
|
|
|
465
465
|
"src/gdskills/bundled/skills/review/review-clean-code/**",
|
|
466
466
|
"src/gdskills/bundled/skills/review/review-highload/**",
|
|
467
467
|
"src/gdskills/bundled/skills/review/review-regression/**",
|
|
468
|
+
"src/gdskills/bundled/skills/review/review-pr-contract/**",
|
|
468
469
|
"src/gdskills/bundled/skills/review/review-core-boundaries/**",
|
|
469
470
|
"src/gdskills/bundled/skills/review/review-flow-graph/**",
|
|
470
471
|
"src/gdskills/bundled/skills/review/review-layout/**",
|
|
@@ -234,5 +234,5 @@ Report done only once all of these hold:
|
|
|
234
234
|
- No finding invents a severity: each lands under **Severity (canonical)** in
|
|
235
235
|
`review-orchestrator/SKILL.md`.
|
|
236
236
|
- The reply is the `REVIEW_RESULT` the Orchestrated Review Contract asks for, with
|
|
237
|
-
finding ids prefixed `LY-`, or `NEEDS_CONTEXT` naming the
|
|
238
|
-
|
|
237
|
+
finding ids prefixed `LY-`, or `NEEDS_CONTEXT` naming the measurement that was missing.
|
|
238
|
+
Measurements supplied as `context_files` count as evidence when they name the command and the viewport.
|
|
@@ -697,6 +697,11 @@ each proposed mutant to `<review-dir>/research-evidence/mutants.json`:
|
|
|
697
697
|
`{ "mutants": [{ "id", "file", "anchor", "replacement" }] }`. `anchor` is exact text that
|
|
698
698
|
occurs once in `file`; `"runnable": false` at the top level says the suite cannot run here.
|
|
699
699
|
|
|
700
|
+
The testing reviewer's contract is `mutant_candidates` for **every changed branch**, including
|
|
701
|
+
code downstream of the diff that the diff makes reachable differently. The main session writes
|
|
702
|
+
that list to `mutants.json` unchanged. It may not narrow, drop or merge candidates to fit a
|
|
703
|
+
budget: a smaller pass is a cap (below), stated, never a smaller list.
|
|
704
|
+
|
|
700
705
|
Run `keryx review mutate --test "<the nearest suite command>" --package <review-dir>`. It
|
|
701
706
|
applies each mutant in a scratch copy of the worktree (never in the worktree), refuses to
|
|
702
707
|
start when the baseline is red, and writes the table to `research-evidence/mutation-<n>.md`
|
|
@@ -708,6 +713,12 @@ A survivor is evidence for the finding that names it.
|
|
|
708
713
|
`dispatch.mutationWaiver = { operatorQuote, reason }` carries the operator's own words.
|
|
709
714
|
`keryx review run --autonomous` is unchanged: it records `Not run: mutation pass` as before.
|
|
710
715
|
|
|
716
|
+
`keryx review mutate --cap <n>` runs the first `n` candidates and records `cap` and `not_reached`
|
|
717
|
+
in `mutation-<n>.json`. The table then prints `Not run: N candidates` (and the report must carry
|
|
718
|
+
that line, see `keryx review report-parity`). A candidate the cap left out is still pending, so it
|
|
719
|
+
keeps blocking the coverage gate until a later pass runs it; a cap moves work to the next pass, it
|
|
720
|
+
never closes it.
|
|
721
|
+
|
|
711
722
|
### Severity audit — `keryx review severity-audit`
|
|
712
723
|
|
|
713
724
|
A reviewer that argues a finding from a mutant writes `mutant:<id>` in the finding's `evidence`.
|
|
@@ -718,6 +729,34 @@ run executed stays minor (a major is capped at minor) and gets the label `unprov
|
|
|
718
729
|
never deletes a finding and never touches a blocker; it is a separate pass from the verifier,
|
|
719
730
|
which can only delete. `--dry-run` prints the changes without writing `findings.json`.
|
|
720
731
|
|
|
732
|
+
Each mutant in `mutants.json` carries a `kind`: `behavior` (the default, and what a record written
|
|
733
|
+
before the field reads as) or `presentation` (it changes only what a reader sees: a label, a
|
|
734
|
+
column, copy, spacing). A behavior survivor raises as above. When the only survivors a finding
|
|
735
|
+
cites are `presentation`, the severity stays, and the finding gets
|
|
736
|
+
`severity_audit {held: {severity, mutant, kind: "presentation", evidence, reason}}` instead of
|
|
737
|
+
`{from, to}`. A held note is not a raise and not a deletion; it is the record that a survivor was
|
|
738
|
+
seen and did not move the severity, so the report must say so.
|
|
739
|
+
|
|
740
|
+
`keryx review report-parity <review-dir>` prints what the round left behind as Markdown to paste
|
|
741
|
+
into `report.md`: the `Not run: N candidates` line, each held note, and each follow-up still open
|
|
742
|
+
as `Not verified: <text>`. The same lines show in `keryx review status`, in the shell
|
|
743
|
+
`/review parity` view and in the sidebar. `keryx review complete` warns, and does not refuse, when
|
|
744
|
+
any of them exists and the report names none.
|
|
745
|
+
|
|
746
|
+
### Layout measurements on a shell host
|
|
747
|
+
|
|
748
|
+
A shell reviewer cannot start a browser. Before dispatching `review-layout` the main session runs
|
|
749
|
+
the browser measurements itself (Playwright, or Storybook with its test runner) for the changed
|
|
750
|
+
components and passes the output files as `context_files` in the **first** dispatch. A measurement
|
|
751
|
+
counts as evidence for `review-layout` when it names the command and the viewport it ran at; one
|
|
752
|
+
without them is context, not evidence.
|
|
753
|
+
|
|
754
|
+
`src/review/retry-plan.ts` treats `NEEDS_CONTEXT` as one refill: the first one re-dispatches the
|
|
755
|
+
role with what it asked for. A second `NEEDS_CONTEXT` from the same role that covered no new slice and
|
|
756
|
+
asks nothing new is `notRun`; the reason is `layout: no engine` for `review-layout` and `no engine` for any other
|
|
757
|
+
role when its questions ask for a browser or engine and name no route without one, else `no progress after one
|
|
758
|
+
context refill`, and the round goes on. A second one that covered a new slice or asked a new question still gets its refill.
|
|
759
|
+
|
|
721
760
|
### Pull request context — `keryx review context-collect`
|
|
722
761
|
|
|
723
762
|
A pull-request review starts with `keryx review context-collect --repo <o/r> --pr <n> --package <review-dir>`.
|
|
@@ -87,6 +87,8 @@ A reviewer on this host cannot open a path: its file tool reads inside its worki
|
|
|
87
87
|
3. **Check before dispatch.** `keryx review dispatch-check --payload <file> --manifest <dir>/manifest.json` for the wave; exit 1 means a payload names no slice, is over the wave ceiling (dispatch fewer slices, then the next wave), or breaks `reviewer-input.schema.json`. Fix it; do not dispatch around it.
|
|
88
88
|
4. **One batch.** Dispatch the whole wave — up to 10 reviewers — in one message, not one at a time. Give each reviewer `max_rounds` of at least 40 (the default); a reviewer that spends its rounds reading the skill and the schema returns nothing. **Incomplete means the next wave while it makes progress, Not run when it stalls.** Every reviewer result lists the `slices` it covered, in its `REVIEW_RESULT` object, as ids (`"slices": ["slice-01", "slice-02"]`); an object with an `id` and an optional `coverage` note (`{"id": "slice-01", "coverage": "read in full"}`) is read the same way. An object with a `status` counts only when it is DONE, COMPLETE, COVERED or OK (any case); a slice marked NOT_READ, INCOMPLETE, SKIPPED or PARTIAL is not covered, so give a slice you did not read no `status` of that kind and leave it out. An entry with no id counts for nothing. **One driver file per dispatch.** Write `drivers/<reviewer>.json` next to `raw/` with `{"reviewer": "<name>", "slices": [<ids dispatched>], "result": "raw/<reviewer>.txt", "definition": "<path of the reviewer's SKILL.md>"}`: `ledger build` takes the reviewer's rule file from `definition` (a `skill_path` there, or inside a `payload` object, is read too) and the slices it was handed from `slices`. Only reviewer replies belong in `raw/`; a verification payload or a note filed there is reported as set aside and left in place, but it still costs a step. On `INCOMPLETE`, `BLOCKED` or `NEEDS_CONTEXT` (never accepted as a finished scope), and on a `DONE` that left slices open, `keryx review retry-plan --manifest <dir>/manifest.json --result <raw/reviewer.txt>` decides and hands out the next wave of only the slices still uncovered (`--next-wave` plans one without a result). A result that covered a new slice spends no attempt; two results in a row with nothing new, or six failed rounds, print the `- **Not run:** …` line to paste into the report, and a result with nothing new first has its open slices cut in half. Dispatch each wave, save its reply, and run `retry-plan` again until it says `accepted`. Only the operator can accept a smaller set than that, with their own quote. An unfinished reviewer is never counted as a clean pass. **Persist every result.** Pass each reviewer `save_result_to: .metaproject/data/review/raw/<reviewer>.txt`: the host writes its full final text there, so `review complete` has raw evidence even after the transcript is cut. Build `research.json` with `keryx review ledger build <input-dir> --package <review-dir>` — never by hand: it reads the saved results, `scope-files.json` and the retry state, marks a reviewer complete only when its result is finished and its payloads cover the scope, and prints each gap with the exact next command and the waves left (a `retry-plan --next-wave` call, or a `--dispositions` entry for a raw finding). Run `keryx review ingest --research <review-dir>/research.json` once it prints READY (exit 0). A refused `review complete` names what is missing: fix that with a command and run it again; it is never `blocked`. `--head` wants a SHA (`git rev-parse HEAD`), not `HEAD`.
|
|
89
89
|
5. **Never print a scope or a slice.** The tool output of one call is cut at about 20 KB and `keryx review scope` of a large PR prints hundreds of KB. Run it as `keryx review scope --ref <base> --json > .metaproject/data/review/scope.json` and read the counts from the file (`keryx ctx read`, or `jq`); take `<base>` from `git symbolic-ref refs/remotes/origin/HEAD`, not a guessed `origin/main`. Create `.metaproject/data/review/` first (`mkdir -p`) — a missing directory is a command to run, not a blocker. **Machine artifacts by direct write.** Save slices, payloads and results by redirection or `keryx ctx run --raw -- <command>`; the summarized form of `keryx ctx run` drops the bytes a later step parses.
|
|
90
|
+
6. **Write mutants whole.** A testing reviewer's `mutant_candidates` cover every changed branch, code downstream of the diff included. Write them to `research-evidence/mutants.json` unchanged; you may not narrow them, and a candidate no pass has run still blocks the coverage gate (`SKILL.detail.md`, mutation pass).
|
|
91
|
+
7. **Measure before `review-layout`.** Run the browser measurements (Playwright/Storybook) yourself and pass them as `context_files` in its first dispatch; they count as evidence when the command and viewport are named. A second `NEEDS_CONTEXT` from one role is Not run (`layout: no engine`), not another refill (`SKILL.detail.md`).
|
|
90
92
|
|
|
91
93
|
## Control
|
|
92
94
|
|
|
@@ -871,6 +873,7 @@ If the repository has local convention docs such as `CLAUDE.md`, `AGENTS.md`,
|
|
|
871
873
|
| `src/core/**`, `core/**`, `shared/**`, `foundation/**` | `review-core-boundaries` |
|
|
872
874
|
| `src/core/flow/**`, `src/graph/**`, `src/shared/flow/**` | `review-flow-graph` |
|
|
873
875
|
| A `.tsx`/`.jsx`/`.css`/`.scss` hunk that adds or changes a sizing or spacing utility on a rendered element — `w-*`, `min-w-*`, `max-w-*`, `flex-*`, `basis-*`, `grid-cols-*`, `truncate`, `line-clamp-*`, `overflow-*`, `p*`/`m*`/`gap-*` and their logical `ps-*`/`pe-*`/`ms-*`/`me-*` forms, or an inline `style` carrying a dimension | `review-layout` |
|
|
876
|
+
| `pr-context.md` is in the package | `review-pr-contract` |
|
|
874
877
|
|
|
875
878
|
These convention reviewers are additive: keep the generic reviewers selected by normal detection,
|
|
876
879
|
then add the matching convention pass. Deduplicate reviewer names before dispatch.
|
|
@@ -1057,6 +1060,7 @@ Skipped reviewers:
|
|
|
1057
1060
|
| `--core-boundaries` | `review-core-boundaries` |
|
|
1058
1061
|
| `--flow-graph` | `review-flow-graph` |
|
|
1059
1062
|
| `--layout` | `review-layout` |
|
|
1063
|
+
| `--pr-contract` | `review-pr-contract` |
|
|
1060
1064
|
| `--all` | all reviewers above (including `review-clean-code`, `review-highload`, applicable legacy/profile reviewers, and project convention reviewers when local convention docs exist) |
|
|
1061
1065
|
| `--verify` | `review-verifier`, AFTER all others; checks the consolidated findings by running something. Delete-only. |
|
|
1062
1066
|
| (auto) | detected from diff file extensions — see Auto-detection table |
|
|
@@ -1113,12 +1117,7 @@ Dispatch selected reviewers in parallel when independent — this engine dispatc
|
|
|
1113
1117
|
3. Wave C - **verification**: `review-verifier` over the consolidated findings, when blockers/majors
|
|
1114
1118
|
exist, `--verify` is set, or the PR is high-risk. See below.
|
|
1115
1119
|
|
|
1116
|
-
**Execution belongs to both B and C, and they execute for opposite reasons.**
|
|
1117
|
-
Wave C runs a command to *decide the fate of a finding that exists*; it is
|
|
1118
|
-
delete-only and can produce nothing. Wave B runs commands to *make findings* —
|
|
1119
|
-
`review-testing-practices` deletes each gate the diff added and records whether
|
|
1120
|
-
the suite goes red, and `review-layout` measures rendered geometry in a real
|
|
1121
|
-
engine.
|
|
1120
|
+
**Execution belongs to both B and C, and they execute for opposite reasons.** Wave C runs a command to *decide the fate of a finding that exists*; it is delete-only and can produce nothing. Wave B runs commands to *make findings* — `review-testing-practices` deletes each gate the diff added and records whether the suite goes red, and `review-layout` measures rendered geometry in a real engine.
|
|
1122
1121
|
|
|
1123
1122
|
The consequence is directional and easy to get wrong: **a surviving mutation is
|
|
1124
1123
|
not something Wave C can hand you.** If Wave B skips its mutation pass, that
|
|
@@ -184,6 +184,15 @@
|
|
|
184
184
|
"type": "string"
|
|
185
185
|
}
|
|
186
186
|
},
|
|
187
|
+
"follow_ups": {
|
|
188
|
+
"title": "Follow-ups handed back to the parent",
|
|
189
|
+
"description": "Work the reviewer could not do itself and leaves to the parent. Each entry becomes a research obligation `<raw>:fu<N>` (N counts from 1, in this order; a `Parent follow-up:` line after the REVIEW_RESULT block is appended) with status `unresolved`. It closes only through a disposition (`finding`, `refuted`, `out-of-scope` or `narrowed` with a \"what remains\" text); until then the ledger is not reconciled and the report lists it as not verified.",
|
|
190
|
+
"type": "array",
|
|
191
|
+
"items": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"minLength": 1
|
|
194
|
+
}
|
|
195
|
+
},
|
|
187
196
|
"slices": {
|
|
188
197
|
"title": "Slices this result covered",
|
|
189
198
|
"description": "The ids of the dispatched slices the reviewer read. An entry is an id string, or an object with an `id`, an optional `coverage` note and an optional `status`. `keryx review retry-plan` and `ledger build` read both. An object counts as covered only when its `status` is absent or one of DONE, COMPLETE, COMPLETED, COVERED, OK (any case); any other status (NOT_READ, INCOMPLETE, SKIPPED, PARTIAL, …) leaves the slice uncovered and is named in the plan reason or the ledger gap. An entry with no id counts for nothing.",
|
|
@@ -89,6 +89,10 @@
|
|
|
89
89
|
"type": "boolean",
|
|
90
90
|
"description": "Run this dispatch on the caller's own model. Present in place of `provider`/`model`, never alongside them, when `keryx review tier` could not resolve a concrete model."
|
|
91
91
|
},
|
|
92
|
+
"routed": {
|
|
93
|
+
"type": "boolean",
|
|
94
|
+
"description": "True when the provider/model were set by the routing table's `review` category (`keryx review tier` emits it on a routed pick) rather than tier-ranked discovery. Additive to `tier_resolution`, which still records how the tier resolved a model; the two questions are independent."
|
|
95
|
+
},
|
|
92
96
|
"tier_resolution": {
|
|
93
97
|
"type": "string",
|
|
94
98
|
"enum": [
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review-pr-contract
|
|
3
|
+
model_tier: standard
|
|
4
|
+
description: |
|
|
5
|
+
Use when a review package carries pr-context.md and the claims the author made there
|
|
6
|
+
need checking against the diff: a checklist box ("Documentation needed", "Tests
|
|
7
|
+
updated", "Breaking change") that the diff contradicts, and documentation outside the
|
|
8
|
+
diff that still describes the behaviour the change altered. Dispatched by
|
|
9
|
+
review-orchestrator for --pr-contract, --all, or whenever pr-context.md is in the
|
|
10
|
+
package.
|
|
11
|
+
NOT for: whether the code is correct (review-logic), whether the tests can fail
|
|
12
|
+
(review-testing-practices), or comments that the diff itself adds (review-jev-comments).
|
|
13
|
+
triggers:
|
|
14
|
+
- "pr contract"
|
|
15
|
+
- "review pr contract"
|
|
16
|
+
- "pr checklist"
|
|
17
|
+
- "documentation needed"
|
|
18
|
+
metadata:
|
|
19
|
+
author: "MrCipherSmith"
|
|
20
|
+
version: "1.0.0"
|
|
21
|
+
category: "review"
|
|
22
|
+
flags: "--pr-contract"
|
|
23
|
+
compatible_harnesses: "cursor,codex,zed,opencode,claude"
|
|
24
|
+
origin: "authored"
|
|
25
|
+
license: "MIT"
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Review — PR Contract
|
|
29
|
+
|
|
30
|
+
Reviewer for what the pull request says about itself.
|
|
31
|
+
|
|
32
|
+
This lane exists because nobody else reads the PR body. `review-logic` reads the code,
|
|
33
|
+
`review-testing-practices` reads the tests, and a retrospective review of a merged
|
|
34
|
+
frontend PR missed a documentation gap that sat in plain view: the body left
|
|
35
|
+
**Documentation needed** unticked while the diff added a user-visible table row, and
|
|
36
|
+
the page that describes that table lives outside the diff. The box and the diff
|
|
37
|
+
disagreed, and no reviewer held both.
|
|
38
|
+
|
|
39
|
+
A finding here is a disagreement between two things you can quote: a line of
|
|
40
|
+
`pr-context.md` and a line of the diff (or of a document). Nothing else is filed.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What you are given
|
|
45
|
+
|
|
46
|
+
- `pr-context.md` — the PR body, its checklist, the review comments and the round-1
|
|
47
|
+
intent, written by `keryx review context-collect`. Read it before the diff.
|
|
48
|
+
- the diff slices the orchestrator supplies, as text in the task or as `context_files`;
|
|
49
|
+
- documentation the orchestrator chose to supply with them.
|
|
50
|
+
|
|
51
|
+
## The checklist form
|
|
52
|
+
|
|
53
|
+
Work each checklist box and each stated claim of the PR body as one row. A row has four
|
|
54
|
+
cells, and a row with an empty cell is not a finding:
|
|
55
|
+
|
|
56
|
+
| Box or claim (quoted) | State | What the diff shows (`file:line`, quoted) | Expectation |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `Documentation needed` | unticked | `src/admin/ExamplePage.tsx:41` adds a user-visible row label | A user-visible string or row is added, so the box should be ticked and a document should describe it |
|
|
59
|
+
| `Tests updated` | ticked | no `*.test.*`, `*.spec.*` or `*.stories.*` file is in the diff | A ticked box names a test change; the diff has none |
|
|
60
|
+
|
|
61
|
+
Boxes this lane checks, and the diff signal that contradicts each:
|
|
62
|
+
|
|
63
|
+
- **Documentation** unticked, while the diff adds or changes a user-visible string, table
|
|
64
|
+
column or row, route, menu entry, command flag, config key, or error message.
|
|
65
|
+
- **Tests updated** ticked, while no test, spec or story file is in the diff; or unticked
|
|
66
|
+
while one is.
|
|
67
|
+
- **Breaking change** unticked, while the diff removes or renames an exported symbol, a
|
|
68
|
+
route, a flag, a schema field or a persisted key; or ticked with no such change.
|
|
69
|
+
- **Screenshots / visual evidence** ticked, while `pr-context.md` links none.
|
|
70
|
+
- Any other box or claim: quote it and apply the same test — what would the diff contain
|
|
71
|
+
if the box were true?
|
|
72
|
+
|
|
73
|
+
A box that is not applicable (marked `N/A`) is read as the author's statement; contradict
|
|
74
|
+
it only with a quoted diff line.
|
|
75
|
+
|
|
76
|
+
## Documentation outside the diff
|
|
77
|
+
|
|
78
|
+
When the diff changes behaviour a reader sees, look for documentation that describes it
|
|
79
|
+
and was not touched:
|
|
80
|
+
|
|
81
|
+
- `keryx gdgraph find <symbol>` and `keryx gdgraph affected <file>` — what points at the
|
|
82
|
+
changed file; a `.md` or docs-site file in the answer is a candidate.
|
|
83
|
+
- `keryx wiki ask "<the changed behaviour>"` and `keryx wiki query` — the wiki pages that
|
|
84
|
+
name it.
|
|
85
|
+
- `keryx ctx rg "<the old label or flag>" docs README.md` — the string the change
|
|
86
|
+
replaced, wherever it is still quoted.
|
|
87
|
+
|
|
88
|
+
Name the document, the line that describes the old behaviour, and the diff line that
|
|
89
|
+
moved it. A document that is merely related is not a finding.
|
|
90
|
+
|
|
91
|
+
### On a shell host
|
|
92
|
+
|
|
93
|
+
A reviewer on a shell host is read-only: its file tool reads inside its working directory
|
|
94
|
+
and it runs no `keryx` command. Work from `pr-context.md` and the slices you were
|
|
95
|
+
supplied. If answering needs a document you were not given, do not guess it: reply
|
|
96
|
+
`STATUS: NEEDS_CONTEXT` and name the paths (or the symbol and the wiki query) the
|
|
97
|
+
orchestrator should supply. Rows you could settle from what you hold are still reported.
|
|
98
|
+
|
|
99
|
+
## Severity
|
|
100
|
+
|
|
101
|
+
A disagreement between the body and the diff names no runtime trigger, so it is `minor`:
|
|
102
|
+
the code is correct; the cost is a reader who trusts the box. A document that describes
|
|
103
|
+
behaviour the diff now contradicts, where following it gives a wrong result, names its
|
|
104
|
+
trigger and outcome and may be `major`. A box you cannot settle from what you hold is
|
|
105
|
+
`info`, marked unverified, with the paths that would settle it.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Class scope — required for `blocker` and `major`
|
|
110
|
+
|
|
111
|
+
Every `blocker` and `major` finding must carry `class_scope`: **every** site that holds
|
|
112
|
+
the shape you found, and **how you enumerated them** — the search you ran, or the guard
|
|
113
|
+
that derives the set.
|
|
114
|
+
|
|
115
|
+
A finding anchored to one `file:line` is a claim about one site. A document gap usually
|
|
116
|
+
has siblings: the README, the docs-site page and the in-app help each quote the same
|
|
117
|
+
label.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
class_scope:
|
|
121
|
+
sites: ["docs/admin/query-log.md:12", "README.md:88"]
|
|
122
|
+
enumeration_method: "keryx ctx rg for the old column label across docs and README; 2 documents still use it"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
"I checked the others" is not an enumeration method. A single-entry `sites` list is a
|
|
126
|
+
claim that the class has exactly one member — make it deliberately, because
|
|
127
|
+
`review-finding.schema.json` accepts it and the next round tests it.
|
|
128
|
+
|
|
129
|
+
`minor` and `info` may omit it.
|
|
130
|
+
|
|
131
|
+
```markdown
|
|
132
|
+
### [F-NNN] Title
|
|
133
|
+
|
|
134
|
+
- **Severity**: blocker | major | minor | info
|
|
135
|
+
- **File**: path/to/file:line
|
|
136
|
+
- **Problem**: the box or claim, quoted, and the diff or document line that contradicts it
|
|
137
|
+
- **Why it matters**: who trusts the box or the document, and what they get wrong
|
|
138
|
+
- **Fix**: tick the box and add the document, or correct the claim
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Severity comes from **Severity (canonical)** in `review-orchestrator/SKILL.md`; no table
|
|
142
|
+
here. When dispatched, follow the `reviewer-input.schema.json` payload and return a
|
|
143
|
+
`REVIEW_RESULT` object compatible with
|
|
144
|
+
`.metaproject/skills/gdskills/review/review-orchestrator/reviewer-finding.schema.json`,
|
|
145
|
+
finding ids prefixed `PC-`, then a short summary; `NEEDS_CONTEXT` rather than a guess.
|
|
146
|
+
|
|
147
|
+
### Shared laws (every reviewer)
|
|
148
|
+
|
|
149
|
+
1. **A claim of runtime harm with no reproducible path is `info`.** If you cannot name the input, call, or condition that reaches the code, you have an observation, not a finding. Report it as `info` and say what would settle it.
|
|
150
|
+
2. **Never flag the theoretical.** The path you describe must exist in the code under review. Do not report a safe API because it could be misused, or a pattern because it is often wrong elsewhere.
|
|
151
|
+
3. **One finding per class, not one per occurrence.** When the same shape appears at several sites, report it once and list every site. Ten findings that are one finding hide the other nine problems.
|
|
152
|
+
|
|
153
|
+
Severity levels are defined once, in `review-orchestrator/SKILL.md` → **Severity (canonical)**. This reviewer does not restate them: `blocker` is the four merge-blocking shapes named there and nothing else, and the `major`/`minor` boundary is the trigger-and-outcome test.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Red Flags
|
|
158
|
+
|
|
159
|
+
| Rationalization | Why it is wrong |
|
|
160
|
+
|----------------|-----------------|
|
|
161
|
+
| "The box is unticked, so documentation is missing." | The box is the claim under review, not the evidence. Quote the diff line that adds the user-visible string; with no such line there is nothing to file. |
|
|
162
|
+
| "There is probably a docs page for this." | Probably is not a path. Find the page with `gdgraph`, `wiki ask` or `ctx rg`, or — on a shell host — return `NEEDS_CONTEXT` naming what you need. |
|
|
163
|
+
| "`Tests updated` is ticked and the author is trustworthy." | Trust is not what this lane checks. If no test file is in the diff, the row is a disagreement and gets filed. |
|
|
164
|
+
| "I could not open the docs, so I assume they are current." | An unread document is unverified, not clean. Say which paths were not read. |
|
|
165
|
+
| "The PR body is long, I checked the main boxes." | Every box is a row. A box you skipped is a box the next round finds. |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Verification
|
|
170
|
+
|
|
171
|
+
Report done only once all of these hold:
|
|
172
|
+
|
|
173
|
+
- Every checklist box in `pr-context.md` appears as a row in the checklist form, ticked or not, with the state quoted.
|
|
174
|
+
- Every finding quotes both sides: the box or claim, and the diff or document line that contradicts it.
|
|
175
|
+
- Every document named was found by a command or was supplied; none is inferred.
|
|
176
|
+
- On a shell host, each document you needed and did not hold is named in a `NEEDS_CONTEXT` reply.
|
|
177
|
+
- Every `blocker` and `major` carries `class_scope` with `sites` and an `enumeration_method` naming the search.
|
|
178
|
+
- The reply is the `REVIEW_RESULT` the Orchestrated Review Contract asks for, or `NEEDS_CONTEXT` naming the context that was missing — never a guess in its place.
|
|
179
|
+
|
|
180
|
+
<!-- keryx:layers-block:start -->
|
|
181
|
+
## Project layers: graph, tests, wiki, memory
|
|
182
|
+
|
|
183
|
+
Your payload may carry a `layers` block for your files: the files the graph says are affected, symbol neighbours, related tests, the wiki pages that describe the files and the memory entries for their module. It is bounded, and `truncated` names what was cut and the command that fetches the rest. When the diff alone does not settle a question, ask the project. Each command below is read-only and calls no model.
|
|
184
|
+
|
|
185
|
+
- `keryx gdgraph affected <file>`: who depends on the file. Use it before you call a change contained, and whenever a changed export, signature or behaviour may have callers outside your files.
|
|
186
|
+
- `keryx test related <file>`: the tests that cover the file. Use it before you say a behaviour is untested, and to judge whether a test would fail if the code broke.
|
|
187
|
+
- `keryx wiki ask "<q>"`: what the project wiki says a module is for and which rules bind it. Use it when the intent of the code is unclear and the diff does not state it.
|
|
188
|
+
- `keryx memory search "<q>"`: recorded lessons, decisions and known mistakes. Use it before you report a pattern the project may already have decided on, and when a failure looks like one it has seen.
|
|
189
|
+
|
|
190
|
+
In a review package, `keryx gdgraph affected <file> --data-dir <package>/layers/graph` reads the graph built at the PR head; the tracked graph may be from another commit. A finding still rests on the code you read: what a layer says is a lead, not evidence. If a layer is empty or a command fails, say so in the report instead of guessing.
|
|
191
|
+
<!-- keryx:layers-block:end -->
|
|
@@ -85,14 +85,14 @@ this pass the class is unreachable for the whole round.
|
|
|
85
85
|
|
|
86
86
|
### Bounds
|
|
87
87
|
|
|
88
|
-
-
|
|
88
|
+
- Run mutants one at a time on gates inside **scope A**, reverting each before the
|
|
89
89
|
next. Never leave a mutation in the tree.
|
|
90
90
|
- Run the **nearest** suite, not the full one. This is not a `test:mutation`
|
|
91
91
|
sweep — those are slow, opt-in, and answer a different question.
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
reads as a clean sweep.
|
|
92
|
+
- Propose, do not cap: `mutant_candidates` in your `REVIEW_RESULT` names a mutant (`id`, `file`, `anchor`, `replacement`,
|
|
93
|
+
optional `kind`: `behavior` (default) or `presentation`, for copy or styling only) for **every changed branch**, code downstream of the diff included.
|
|
94
|
+
Run what your budget allows, previous-round gates first, and say how many you did not reach:
|
|
95
|
+
the main session writes your list to `mutants.json` unchanged and runs the rest. Silence reads as a clean sweep.
|
|
96
96
|
- If you cannot run the suite at all, say so and skip the pass. Do **not** report
|
|
97
97
|
a mutation you did not run — a predicted result is `info` at best, and this
|
|
98
98
|
reviewer has no use for one.
|
|
@@ -145,6 +145,10 @@
|
|
|
145
145
|
"inherit": {
|
|
146
146
|
"description": "Adaptive: the host picks the model. `keryx review tier` writes it — in place of `provider`/`model`, never alongside them — whenever it does not name a model other than the session's: no session was named by the caller (flags or KERYX_SESSION_PROVIDER/KERYX_SESSION_MODEL), the catalogue or the session model could not be ranked, or the tier resolved to the session's own model. The host resolves the accompanying `tier` against the models its own dispatch tool offers: `standard` on its session model, `deep` on its most capable class, `light` on its lighter class — each falling back to the session model when the host offers no choice. The alternative, an empty or stale `provider`/`model` pair, is schema-invalid or pins a model the runner may not have.",
|
|
147
147
|
"type": "boolean"
|
|
148
|
+
},
|
|
149
|
+
"routed": {
|
|
150
|
+
"description": "True when the provider/model were set by the routing table's `review` category (`keryx review tier` emits it on a routed pick) rather than tier-ranked discovery. Additive to `tier_resolution`, which still records how the tier resolved a model; the two questions are independent.",
|
|
151
|
+
"type": "boolean"
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
},
|