@mrciphersmith/keryx 0.3.44 → 0.3.46

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.
@@ -53,28 +53,28 @@ these go.
53
53
 
54
54
  ## Bulk import of overlay reviewers
55
55
 
56
- When the overlays already exist as `review-vantage-*` skill packages (for
57
- example an overlay skill tree with `review-vantage-*` packages), do not recreate them one by one.
56
+ When the reviewers already exist as skill packages in a tree someone else maintains, import
57
+ them instead of recreating them one by one. Dry-run first, then the same command without it:
58
58
 
59
59
  ```bash
60
- keryx skills import --from <overlay-home> --module review
61
- # alias: keryx review import --from <overlay-home>
60
+ keryx review import --from <overlay-home> --only 'review-acme-*' --dry-run
61
+ keryx review import --from <overlay-home> --only 'review-acme-*'
62
+ keryx review reviewers # every imported name must be listed here
63
+ keryx skills remove review/<name> # undo one
62
64
  ```
63
65
 
64
- That copies every `review-vantage-*` package into
65
- `.metaproject/project-skills/review/`, stamps Origin on each SKILL.md, and is
66
- the same discovery `review-orchestrator` uses. Generic copies of keryx
67
- reviewers (`review-logic`, `review-frontend`, …) are skipped on purpose —
68
- those would shadow the bundled engine.
66
+ `keryx review import` is the review-shaped spelling of `keryx skills import --module review`:
67
+ same importer, module implied. A tree of several packages is refused, with the candidate list,
68
+ until `--only <glob>` names the ones you want: `review-orchestrator` dispatches every package in
69
+ module `review`, and one with no path gate is dispatched on every round. Import reviewers only —
70
+ an orchestrator or facade, a deprecated alias, a report generator, or anything with no review
71
+ contract does not belong there. One package directory passed as `--from` needs no `--only`.
69
72
 
70
- One overlay package:
71
-
72
- ```bash
73
- keryx review import --from <overlay-home>/skills/review-vantage-frontend
74
- ```
75
-
76
- Then `keryx review reviewers`. Until that list shows the names, they are not
77
- wired.
73
+ Read `SKILL.detail.md` beside this file before running an import. It is the worked example, with
74
+ real output: the refusals, what each run writes, how `flags`, `paths`, `pathsSource`,
75
+ `stackRequires`, `unresolvedRules` and `drift` are derived, rule collisions, undo with
76
+ `keryx skills remove`, refresh, the per-clone note, and the `review-learning.config.json` shape.
77
+ Until `keryx review reviewers` shows the names, they are not wired.
78
78
 
79
79
  ---
80
80
 
@@ -221,8 +221,8 @@ source file differs from what was imported. It does **not** mean the reviewer is
221
221
 
222
222
  Re-read the source, diff it against what the skill encodes, and then decide per
223
223
  change: fold it in, or record in the skill why this project deliberately differs.
224
- Re-run Step 2's command with the same `--name` to re-record the hash once the
225
- skill matches the source again.
224
+ For a reviewer written here, re-run Step 2's command with the same `--name` to re-record the hash once it matches the source again.
225
+ For an imported package, `keryx skills update review/<name>` re-reads its Origin and overwrites the SKILL.md — see `SKILL.detail.md`.
226
226
 
227
227
  A deliberate divergence that is written down is a decision. The same divergence
228
228
  undocumented is drift that will be silently "fixed" by whoever refreshes next.
@@ -239,7 +239,7 @@ undocumented is drift that will be silently "fixed" by whoever refreshes next.
239
239
  | Change a reviewer keryx ships | NO | edit `src/gdskills/bundled/skills/review/` and open a PR |
240
240
  | Update a skill from review findings | NO | `entity-skill-learner`, `keryx skills learn` |
241
241
  | Decide which reviewers a round dispatches | NO | `review-orchestrator` |
242
- | Import a tree of overlay reviewers | YES — `keryx skills import --from <dir> --module review` (`keryx review import` alias) | — |
242
+ | Import a tree of overlay reviewers | YES — `keryx review import --from <dir> --only '<glob>'`, dry-run first (`SKILL.detail.md`) | — |
243
243
  | Import a non-review SKILL.md / GitHub URL | NO | `entity-skill-creator` / `keryx skills import` |
244
244
 
245
245
  ---
@@ -32,7 +32,7 @@ keryx rules distill
32
32
  - `.metaproject/rules/entrypoints/index.md`
33
33
  - `.metaproject/rules/entrypoints/*.md`
34
34
  - `.metaproject/project-skills/entrypoints/*/SKILL.md`
35
- - compact `AGENTS.md` and `CLAUDE.md` still point to `.metaproject/index.md`
35
+ - the `.metaproject/index.md` block sits where `agentEntrypoints.root` says: `CLAUDE.local.md` and a regenerated `AGENTS.override.md` under scope `local` (the team files are rewritten without it), the team file itself under scope `shared`
36
36
  4. If the command changed root entrypoints, check that only global/personal or
37
37
  highest-priority always-on instructions remain there.
38
38
  5. Run focused verification:
@@ -51,7 +51,7 @@ Stop and re-read this skill if you are thinking:
51
51
  | Rationalization | Rebuttal |
52
52
  |---|---|
53
53
  | "`keryx rules distill` exited 0, so the split is correct." | Exit 0 says the command ran, not that it split well. It happily writes an empty rule, a rule titled after a heading it could not classify, and two rules holding the same paragraph. Open every file listed in step 3 before reporting anything. |
54
- | "The root entrypoint is short now, so the job is done." | Shortness is a side effect, not the goal. A root that no longer points at `.metaproject/index.md`, or that lost an always-on instruction into a rule nothing loads, is a regression that looks like success on a line count. |
54
+ | "The root entrypoint is short now, so the job is done." | Shortness is a side effect, not the goal. An entrypoint set from which no file points at `.metaproject/index.md` any more, or a root that lost an always-on instruction into a rule nothing loads, is a regression that looks like success on a line count. |
55
55
  | "This section read like a project rule, so I moved it out of the root." | Global, personal and highest-priority always-on instructions stay in the root by design. If you had to guess which kind a section was, it belongs in the ambiguous list for a human — not silently in a rule file. |
56
56
  | "The project has no flow, so I can skip verification." | `keryx flow check` is not conditional either — with no flows it reports `All flows are consistent.` and exits 0. `keryx rules sync` runs every time; skipping it ships a rules tree whose index and files disagree. |
57
57
  | "`keryx rules distill` rewrote the root files, so I don't need to read them." | The rewrite is exactly what needs checking. Reading the post-distill `AGENTS.md` and `CLAUDE.md` is the only way to see what was carried out of them. |
@@ -63,7 +63,7 @@ Before reporting, all of these must hold:
63
63
  - `keryx rules sync` exits 0.
64
64
  - `.metaproject/rules/entrypoints/index.md` exists and names every file written under `.metaproject/rules/entrypoints/`.
65
65
  - Every generated rule and project-skill file is non-empty and was read, not just listed.
66
- - `AGENTS.md` and `CLAUDE.md` both still reference `.metaproject/index.md`.
66
+ - Claude and Codex still reach `.metaproject/index.md`: the managed block is in `CLAUDE.local.md` and `AGENTS.override.md` (scope `local`; Codex `skip` writes none) or in the shared `CLAUDE.md` / `AGENTS.md` — never in both.
67
67
  - `keryx flow check` exits 0. It takes no id and checks every flow package in the project, so a distill that corrupted one reports here even though the distill never touched flows.
68
68
  - Every section you could not confidently classify appears in the ambiguous list of the Output Contract. An empty ambiguous list on a large entrypoint is a claim, and it must be a true one.
69
69
 
@@ -85,7 +85,7 @@ Present diff preview:
85
85
  - NEVER remove existing entries without explanation
86
86
  - NEVER add entries that duplicate what's already there
87
87
  - Keep CLAUDE.md files under 100 lines
88
- - Prefer project-level for project-specific things
88
+ - Prefer project-level for project-specific things; never write inside the `<!-- keryx:index -->` markers (in `CLAUDE.local.md` by default, `CLAUDE.md` under shared scope) — keryx regenerates that block
89
89
 
90
90
  ## Red Flags
91
91
 
@@ -181,13 +181,13 @@ Flags:
181
181
  #### A3. useEffect Misuse
182
182
 
183
183
  - [ ] `useEffect` must not contain business logic
184
- - [ ] `useEffect` must not call `store.init()`, `store.load()`, or `store.onMount()` — lifecycle initialization is the parent store's responsibility
184
+ - [ ] `useEffect` must not fetch data or run business logic through the store: no `store.load()` / `store.loadX()`, and no `init()` of a child store that the parent store owns. The lifecycle bridge `useEffect(() => { store.onMount(); return () => store.onUnmount(); }, [store])` is correct and is not flagged (D6) — it hands the mount event to the store, which decides what to load
185
185
  - [ ] `useEffect` must not contain API calls or IO
186
186
  - [ ] `useEffect` used to synchronize component state with store state is a sign the component should be an `observer` instead
187
187
  - [ ] Every `useEffect` that creates a subscription, timer, or listener MUST return a cleanup function
188
188
 
189
189
  Flags:
190
- - `useEffect` calls `store.init()` or `store.loadX()` — **major**
190
+ - `useEffect` calls `store.loadX()`, or a child store's `init()` that the parent store owns — **major** (`store.onMount()` / `store.onUnmount()` in the bridge form is not this)
191
191
  - `useEffect` contains `fetch()` or IO — **major** (API in component)
192
192
  - `useEffect` contains business logic / state derivation — **major**
193
193
  - `useEffect` creates a subscription/timer/listener with no cleanup return — **major** (memory leak)
@@ -350,13 +350,13 @@ Flags:
350
350
 
351
351
  #### B10. Lifecycle Initialization and Disposal
352
352
 
353
- - [ ] `init()` / `onMount()` of a child store is called from the **parent store's** `init()` / `onMount()`, NOT from a component `useEffect`
353
+ - [ ] `init()` / `onMount()` of a **child** store is called from the **parent store's** `init()` / `onMount()`, not from a component `useEffect`. The component's own store is different: its `onMount()` / `onUnmount()` are called from the component through the lifecycle bridge (D6), and that is correct
354
354
  - [ ] Components must NOT trigger store data loading via `useEffect` (e.g., `useEffect(() => { store.load() }, [])`)
355
355
  - [ ] Parent store's `onUnmount()` / `dispose()` MUST call `childStore.dispose()` to prevent stale-state updates
356
356
  - [ ] Store `dispose()` MUST set `this.disposed = true` before canceling async work
357
357
 
358
358
  Flags:
359
- - Component `useEffect` calls `store.loadX()`, `store.init()`, or `store.onMount()` — **major**
359
+ - Component `useEffect` calls `store.loadX()`, or a child store's `init()` / `onMount()` that the parent store owns — **major**. Do not flag the bridge `useEffect(() => { store.onMount(); return () => store.onUnmount(); }, [store])`
360
360
  - Parent store does not call `child.dispose()` in `onUnmount()` / `dispose()` — **major**
361
361
  - Missing `disposed` flag in store with async operations — **major**
362
362
 
@@ -468,7 +468,7 @@ const store = useLocalObservable(() => props.store ?? new ComponentStore(props.s
468
468
 
469
469
  #### D6. Store Lifecycle Bridge via useEffect
470
470
 
471
- When a project routes store lifecycle through `useEffect` as a documented pattern:
471
+ The component tells its store that it mounted and unmounted; the store decides what that means:
472
472
 
473
473
  ```typescript
474
474
  useEffect(() => {
@@ -477,11 +477,11 @@ useEffect(() => {
477
477
  }, [store]);
478
478
  ```
479
479
 
480
- This IS the correct lifecycle bridge in projects that use this pattern — do NOT flag as an A3 violation. The A3 rule ("useEffect must not call store.init()") targets implicit data-loading, not explicit lifecycle hooks.
480
+ This is the correct lifecycle bridge — do not flag it under A3 or B10. Those rules target a data fetch or business logic placed in the effect, and a component reaching into a child store the parent store owns; they do not target the lifecycle hooks themselves. What `onMount()` does (load, subscribe, initialise child stores) is store code and is reviewed as store code.
481
481
 
482
482
  Distinction:
483
483
  - `useEffect(() => { store.onMount(); return () => store.onUnmount(); }, [store])` — correct lifecycle bridge — **do not flag**
484
- - `useEffect(() => { store.loadData(); }, [])` — data trigger in component — **major** (A3 violation regardless of project conventions)
484
+ - `useEffect(() => { store.loadData(); }, [])` or `useEffect(() => { store.childStore.init(); }, [])` — data trigger, or a child store's lifecycle the parent store owns, run from the component — **major** (A3 / B10, regardless of project conventions)
485
485
 
486
486
  #### D7. JSX and Naming Conventions
487
487
 
@@ -14,16 +14,99 @@ them from the reviewer's prose.
14
14
 
15
15
  | Field | Use |
16
16
  |---|---|
17
- | `flags` | A flag the user passed that is in this list selects the reviewer **explicitly** — it is then never path-gated, like any flag-selected reviewer. `--all` selects every project reviewer. |
17
+ | `flags` | The reviewer's selection flags: `metadata.flags` from its frontmatter when it has at least one non-empty entry — then it takes precedence, even when none of its entries turns out to be a flag or it holds only `--all`, and the reviewer has no flags — otherwise the flags its description names. An empty `metadata.flags` (`[]`, `''`) leaves the description's flags in force. Already normalised (lower-case, `--` prefixed), so compare a passed flag with them as they are. A flag the user passed that is in this list selects the reviewer. Whether that selection is also exempt from the path gate depends on `familyFlags`. `--all` selects every project reviewer. |
18
+ | `flagWarnings` | One line per `metadata.flags` entry that was dropped because it is not a flag after normalising, plus one more when that left the reviewer with no flags. A list of only `--all` leaves no flags and no warning. Empty in the ordinary case. A dropped entry selects nothing: do not match a passed flag against the raw spelling in the reviewer's frontmatter. Dispatch the reviewer as its other fields say, and name the warnings once in the report — a reviewer its author meant to be flag-selectable is not. Never a finding against the code. |
19
+ | `familyFlags` | The subset of `flags` that at least one other project reviewer also carries. A passed flag listed here is a **family flag**: it selects this reviewer, and the reviewer stays path-gated. A passed flag that is in `flags` and not here is unique to this reviewer: the selection is **explicit** and never path-gated, like any flag-selected bundled reviewer. |
18
20
  | `paths` | Its path triggers for the **path gate**. No file in scope A matches → `Skipped reviewers`, reason `no-matching-paths`. |
19
- | `pathsSource` | `metadata` (declared `metadata.paths`) or `description` (globs read out of its description). `none` means there is nothing to gate on: **dispatch it** — ambiguity includes. |
21
+ | `pathsSource` | `metadata` (declared `metadata.paths`) or `description` (globs read out of its description, and the literal file paths such as `src/utils/column-zone.ts` listed beside them; a cited `.md` / `.mdc` document and a `.metaproject/` path are not triggers). A description with no glob gives `none` even when it names a file: a literal path counts only beside a glob. `none` means there is nothing to gate on: **dispatch it** — ambiguity includes. |
20
22
  | `stackRequires` | Apply stack scoping exactly as for a bundled reviewer carrying `metadata.stack_requires`. |
21
- | `unresolvedRules` | Rules it cites that the project does not have. Dispatch it anyway, tell it in the prompt which rules are absent, and name them once in the report with the fix (`keryx review import --from <overlay>` copies them). Never a finding against the code. |
23
+ | `unresolvedRules` | Rules it cites that the project does not have. Dispatch it anyway, tell it in the prompt which rules are absent, and name them once in the report with the fix (`keryx review import --from <overlay> --only '<glob>'` copies them). Never a finding against the code. |
24
+ | `shadowedRules` | `[{ ref, resolved }]`: rules it cites by one path and must read from another. The project holds its own version of the rule at `resolved` — the reference's slot under `.metaproject/rules/project/`, the whole reference kept (`core/x.mdc` → `.metaproject/rules/project/core/x.mdc`) — which answers the reference before the file `ref` names. In the dispatch prompt, tell the reviewer to read `resolved` in place of `ref` — see "Dispatching a project reviewer" below. Not a defect and not reported as one. |
25
+ | `unresolvedReferences` | `[{ ref, reason }]`: other references in its text that will not hold. `missing` — a backticked `skills/…` or `rules/…` path ending `.md` / `.json` with no file under `.metaproject/`; `non-portable` — a rule cited by an absolute or `~` path, true on one machine at most. Dispatch it anyway, tell it in the prompt which references are absent, and name them once in the report. Never a finding against the code. |
26
+
27
+ `metadata.paths`, `metadata.flags` and `metadata.stack_requires` are read by
28
+ one frontmatter reader that supports a YAML subset (BOM and CRLF accepted,
29
+ trailing `# comments` dropped, `paths` and `flags` as a flow, block or
30
+ comma-separated list, `stack_requires` as a string, only keys directly under
31
+ `metadata:`); a shape outside it reads as not declared.
32
+ `reviewer-skill-creator`'s `SKILL.detail.md` lists the subset. The inventory
33
+ has already applied it — use the fields, not the raw frontmatter.
22
34
 
23
35
  A description saying another entry point dispatches it ("Dispatched by
24
36
  vantage-review …") is its author's routing note, not a restriction: this
25
37
  orchestrator dispatches it through the fields above.
26
38
 
39
+ ### Family flags — why a shared flag does not lift the path gate
40
+
41
+ An overlay usually gives every reviewer it ships the same umbrella flag beside
42
+ the reviewer's own: eight reviewers all carrying `--acme`, each also carrying
43
+ `--acme-styling`, `--acme-testing` and so on. Read as explicit, `--acme` would
44
+ dispatch all eight against a diff that touches one of their path sets — the
45
+ waste the path gate exists to stop, brought back by a flag.
46
+
47
+ So the two cases are told apart by how many project reviewers carry the flag,
48
+ and the inventory reports the answer per reviewer in `familyFlags` rather than
49
+ leaving the orchestrator to count:
50
+
51
+ | Passed flag | In this reviewer's `familyFlags`? | Result |
52
+ |---|---|---|
53
+ | `--acme` | yes (shared) | selected, then path-gated on its `paths` |
54
+ | `--acme-styling` | no (unique to it) | selected explicitly, not path-gated |
55
+ | both | — | the unique flag wins: not path-gated |
56
+
57
+ A family member gated out goes to `Skipped reviewers` with reason
58
+ `no-matching-paths` like any other gated reviewer. `pathsSource: none` still
59
+ means dispatch. An inventory that has no `familyFlags` field comes from a keryx
60
+ that predates it: treat every passed flag as explicit, which is the earlier
61
+ behaviour, and say so once in the report.
62
+
63
+ ### Dispatching a project reviewer — by its package, not by its name
64
+
65
+ Each `project` entry carries `path`, the directory its package is registered
66
+ at. Dispatch it with the absolute path of `<path>/SKILL.md` as `skill_path` in
67
+ the reviewer input, and state in the prompt that this file is the reviewer's
68
+ definition and overrides the built-in definition of any agent type with the
69
+ same name. `skill_path` in the reviewer input is set for project reviewers
70
+ only, and it carries that override. A bundled reviewer's input never has it:
71
+ when one runs through the `general-purpose` fallback, the prompt text names its
72
+ skill file and `review_context.review_plan.dispatch_plan` records the path, but nothing is
73
+ being overridden, so the input field stays out. This holds on every branch of "Agent Runtime Compatibility" in
74
+ SKILL.md: when a same-named agent type exists, dispatch it **with** the path
75
+ and the override sentence; when none exists, dispatch `general-purpose` with
76
+ the same two things.
77
+
78
+ The reason is a name collision nobody chose. A runtime can ship an agent type
79
+ whose name equals a project reviewer's — an earlier copy of the same reviewer,
80
+ or an unrelated one. Dispatched by name alone, the round runs that built-in
81
+ prompt and files its findings under the project reviewer's name, and nothing in
82
+ the report shows the project's own text was never read.
83
+
84
+ If `<path>/SKILL.md` does not exist, the reviewer is `BLOCKED`: record the
85
+ missing path, continue with the independent reviewers, and do not run the
86
+ same-named agent type or any other reviewer in its place. A substituted
87
+ reviewer reads as coverage the round did not have.
88
+
89
+ **Shadowed rules go into the same prompt.** For every entry in the reviewer's
90
+ `shadowedRules`, add one line to the dispatch: "where your text names `<ref>`,
91
+ read `<absolute path of resolved>` instead" — for a reviewer citing
92
+ `core/error-handling.mdc`, that is
93
+ `<project root>/.metaproject/rules/project/core/error-handling.mdc`. Take the
94
+ path from `resolved`; do not build it from the rule's filename, because the slot
95
+ keeps the reference's directory and a file lying directly in `rules/project/`
96
+ answers only a literal `project/<name>.mdc` citation. An import keeps an
97
+ overlay's rule in that slot whenever a file already answers the reference with
98
+ other content, or the name is one keryx also ships under `rules/core`, because
99
+ `keryx init`, `keryx update` and the legacy-profile `keryx skills install`
100
+ overwrite `rules/core` with keryx's own rules (the manifest form with
101
+ `--target keryx-shell` skips an existing file it did not record unless
102
+ `--force`; see "Skills present only when installed"). The reviewer's text was written against the overlay and
103
+ still says `core/<name>.mdc`, so followed literally it reads keryx's generic
104
+ rule of the same name and reviews against the wrong standard — silently, since
105
+ both files exist. The inventory computes the project-first order and reports
106
+ it; no loader applies it. This line in the dispatch is the only place it takes
107
+ effect, and a project reviewer run outside this orchestrator reads what its
108
+ text names.
109
+
27
110
  ### `drift` — the source moved, the reviewer did not
28
111
 
29
112
  A project reviewer built from an external file — a rules file, a review profile,
@@ -32,7 +115,7 @@ import. `keryx review reviewers` re-reads the source and reports:
32
115
 
33
116
  | `drift` | Meaning | What to do this round |
34
117
  |---|---|---|
35
- | `none` | No external source; written here | Nothing |
118
+ | `none` | No origin recorded; written here. The text row reads `(no recorded origin)` | Nothing |
36
119
  | `clean` | Source matches the import | Nothing |
37
120
  | `changed` | Source has moved on since import | Dispatch it, and say so in the report |
38
121
  | `missing` | Source can no longer be read | Dispatch it, and say so in the report |
@@ -44,6 +127,127 @@ suppressing it would trade real coverage for tidiness. Record the drift in
44
127
  profile is due a re-read. Never file it as a finding against the code under
45
128
  review: it is a fact about the review, not about the diff.
46
129
 
130
+ ## Skills present only when installed
131
+
132
+ SKILL.md names five skills that the `minimal` and `recommended` profiles do not
133
+ install — they are in the `full` profile only (`src/gdskills/catalog.ts`):
134
+
135
+ | Skill | Reached through |
136
+ |---|---|
137
+ | `review-pr-feedback` | "A round never answers a pull request another skill is answering" |
138
+ | `code-ai-review` | `--code-ai`, `--legacy-profiles` |
139
+ | `code-learned-review` | `--learned`, `--legacy-profiles` |
140
+ | `code-style-review` | `--code-style`, `--legacy-profiles` |
141
+ | `code-mobx-store-review` | `--mobx-store`, `--legacy-profiles` |
142
+
143
+ Whether one is **present** is read from `keryx review reviewers --json`, and
144
+ the answer depends on the inventory's `bundledSource`, which says which tree
145
+ the `bundled` half was read from. Do not infer presence from the flag table,
146
+ which describes what keryx ships, not what this project has.
147
+
148
+ | `bundledSource` | What the `bundled` half is | Is a listed skill installed here? |
149
+ |---|---|---|
150
+ | `"project"` | this project's `.metaproject/skills/gdskills/review` | yes; one that is not listed is not installed |
151
+ | `"package"` | the review skills the keryx package itself ships | the list cannot say |
152
+ | `"not-found"` | empty — neither tree could be located (the command exits 1) | the list cannot say |
153
+
154
+ `"package"` means the project has no `.metaproject/skills/gdskills/review`
155
+ directory at all — a worktree or clone whose `.metaproject/` holds only data,
156
+ for instance. The inventory then lists every review skill in the package, all
157
+ five above included, whatever profile the project would install, and each
158
+ entry's `path` is relative to the keryx package (`src/gdskills/bundled/…`), not
159
+ to the project. Nothing is installed in this project in that state, so a name
160
+ being in the list is not evidence that it is, and a name missing from it is not
161
+ what `not-installed` means either. Decide each of the five by what can actually
162
+ be dispatched: if the runtime has an agent type of that exact name ("Agent
163
+ Runtime Compatibility" in SKILL.md), the skill is usable and is dispatched as
164
+ any other reviewer; if it does not, record `not-installed` with the command
165
+ below, because the project holds no skill file to hand a `general-purpose`
166
+ agent. Say once in the report that the bundled half came from the package, so
167
+ nobody reads the round's reviewer list as this project's install.
168
+
169
+ `"not-found"` is a failed inventory, not an empty install: report it and do not
170
+ record any reviewer as `not-installed` on its strength.
171
+
172
+ When one is absent:
173
+
174
+ - **A legacy reviewer a flag asked for** is not dispatched and not replaced.
175
+ Record it in `Skipped reviewers` with reason `not-installed` and the command
176
+ below. `--legacy-profiles` and `--all` dispatch the legacy reviewers that are
177
+ installed and record the others the same way. Leave an absent reviewer out of
178
+ the `Optional legacy/profile reviewers` preview: offering a flag that cannot
179
+ run is worse than not offering it.
180
+ - **`review-pr-feedback`** changes nothing about a round. No caller can declare
181
+ that it owns a pull request's reply, so this orchestrator owns the reply as it
182
+ does by default. If the user wants existing pull-request comments interpreted
183
+ and answered, say the skill is not installed and give the command; do not
184
+ improvise its workflow from this file.
185
+
186
+ Adding one. The commands are `keryx skills install` in its two forms
187
+ (`src/commands/skills.ts`):
188
+
189
+ ```bash
190
+ # everything the full profile holds, including all five
191
+ keryx skills install --profile full
192
+
193
+ # manifest installer: the generic review module only — review-pr-feedback,
194
+ # code-ai-review, code-learned-review and code-style-review among its skills
195
+ keryx skills install --profile core --target keryx-shell
196
+
197
+ # manifest installer: add the MobX component to a profile — code-mobx-store-review
198
+ keryx skills install --profile <manifest-profile> --with capability:mobx --target keryx-shell
199
+ ```
200
+
201
+ What those flags do and do not reach:
202
+
203
+ - `--target keryx-shell` is what writes under `.metaproject/skills/gdskills/`,
204
+ where the inventory looks. Without it the manifest installer targets `claude`
205
+ and writes `.claude/skills/`.
206
+ - `--with <id>` is repeatable and takes a **component** id from
207
+ `install-manifest.json` (`capability:mobx`, `framework:react`, …). It has no
208
+ single-skill form: the smallest unit the manifest installs is a module.
209
+ `review-pr-feedback` and the three `code-*` profile reviewers live in module
210
+ `review-core-skills`, which belongs to no component and arrives through a
211
+ profile that lists it (`core`, `full`).
212
+ - Add `--dry-run` first to read the plan. A file already on disk that the
213
+ manifest installer did not write is left alone unless `--force` is passed.
214
+
215
+ What to expect from the exit code:
216
+
217
+ - **The manifest-installer forms can exit 1 after installing what was missing.**
218
+ `applyInstall` (`src/gdskills/manifest/apply.ts`) writes every planned file
219
+ whose destination is free, skips every planned file that already exists and
220
+ is not recorded in the manifest installer's install-state (or has drifted from
221
+ its recorded hash), and returns `ok: false` when it skipped anything;
222
+ `src/commands/skills.ts` turns that into exit 1. The legacy installer, which
223
+ `keryx init` uses, records no such state, so on a project it set up the output
224
+ is a `Wrote N file(s)` line for the missing skills, then a `Skipped (existing
225
+ file not recorded in install-state…)` list naming the skills already there,
226
+ then exit 1. Running the command again gives the same skip list and the same
227
+ exit code.
228
+ - **Judge the outcome by the inventory, not the exit code.** Run
229
+ `keryx review reviewers --json`: with `bundledSource: "project"`, a skill in
230
+ the `bundled` half is installed. (`"package"` after an install means the
231
+ install did not write under `.metaproject/skills/gdskills/review` — check
232
+ `--target`.) If it is there, continue. If it is not, read the `Skipped` and `Errors` lines
233
+ for the skill's own path before doing anything else. `--force` makes the
234
+ installer overwrite the skipped files with the bundled copies, local edits
235
+ included, so it is the user's call, not a default retry.
236
+ - **`keryx skills install --profile full` does not behave this way.** With no
237
+ manifest flag it takes the legacy route (`installGdskills`,
238
+ `src/gdskills/install.ts`), which copies each bundled skill directory over the
239
+ installed one without consulting install-state, and rewrites the project's
240
+ skills catalog and gdskills module manifest for the `full` profile. It sets
241
+ exit 1 only when `.metaproject/` is absent; a destination it will not write
242
+ through (a symlink, a non-directory) is listed under `Warnings` with exit 0.
243
+ - **The dry-run's `Apply this plan:` hint is not the whole command.** It prints
244
+ only `--profile` and `--target`, so after a dry-run with
245
+ `--with capability:mobx` the hinted command installs the profile without
246
+ `code-mobx-store-review`. Re-type the command you dry-ran, minus `--dry-run`.
247
+
248
+ Run `keryx review reviewers --json` again afterwards and dispatch from what it
249
+ returns.
250
+
47
251
  ## CLI-engine reviewers — dispatched as a command, not a sub-agent
48
252
 
49
253
  `review-jev-rules` (flow 330), `review-jev-risk` and `review-jev-scenarios`
@@ -383,7 +383,7 @@ human or a bot has already reviewed pull request `#A`, and someone wants those
383
383
  comments interpreted, checked against the code, fixed and answered. Under its
384
384
  `--fix` mode it dispatches `flow-orchestrator`, which opens a **second** pull
385
385
  request `#B` carrying the fix, based on `#A`'s own head branch, and dispatches
386
- **this** orchestrator on every round against `#B`.
386
+ **this** orchestrator on every round against `#B`. `review-pr-feedback` is present only when installed — it is in the `full` profile only. When it is absent no caller of this kind exists, so the default below holds and this orchestrator owns the reply; do not hand a request to it, say it is not installed and name the install command (`SKILL.detail.md`, "Skills present only when installed").
387
387
 
388
388
  `#B` is its own conversation. Collect and reply on it exactly as always: someone
389
389
  reviewing the fix deserves an answer from the run that made the fix, and its
@@ -1000,7 +1000,7 @@ Three bounds keep it from deleting coverage:
1000
1000
 
1001
1001
  - **Only path-triggered reviewers.** A reviewer selected by an explicit flag, or
1002
1002
  one whose scope is the whole change rather than a path set — `review-logic`,
1003
- `review-regression`, `review-verifier` — is never path-gated.
1003
+ `review-regression`, `review-verifier` — is never path-gated. One exception, for project reviewers only: a flag that more than one project reviewer carries is a **family flag** (the inventory lists it in that reviewer's `familyFlags`). It selects the whole family, and each member stays path-gated — `--<family>` over a diff touching one member's paths dispatches that member, not all of them.
1004
1004
  - **Scope A only.** Scope B is the blast radius; it is *supposed* to name files the
1005
1005
  diff never touched.
1006
1006
  - **Ambiguity includes.** If you cannot decide whether a path matches, dispatch.
@@ -1024,7 +1024,7 @@ keryx review reviewers --json
1024
1024
 
1025
1025
  It returns two halves. `bundled` is the installed gdskills review tree — the set
1026
1026
  this project's install profile actually produced, which is not everything keryx
1027
- ships. `project` is every project-skill under module `review`, living at
1027
+ ships — when `bundledSource` is `"project"`; when it is `"package"` the project has no installed review tree and the list is the keryx package's own (`SKILL.detail.md`, "Skills present only when installed"). `project` is every project-skill under module `review`, living at
1028
1028
  `.metaproject/project-skills/review/<name>/` so that it sits beside
1029
1029
  `.metaproject/skills/gdskills/review/<name>/` and needs no other marking.
1030
1030
 
@@ -1044,10 +1044,10 @@ Two things it does NOT get:
1044
1044
  the origin.
1045
1045
 
1046
1046
  **Select them with the fields the inventory returns, not by reading prose.**
1047
- Each `project` entry carries `flags` (a passed flag selects it explicitly),
1047
+ Each `project` entry carries `flags` (a passed flag selects it) and `familyFlags` (the subset of `flags` another project reviewer also carries: a passed flag listed there selects it but leaves it path-gated; a passed flag in `flags` and not in `familyFlags` is unique to it, so it is explicit and ungated), `flagWarnings` (declared `metadata.flags` entries that were dropped as not being flags: they select nothing; name them once in the report),
1048
1048
  `paths` + `pathsSource` (the path gate; `none` → dispatch), `stackRequires`
1049
1049
  (stack scoping), `unresolvedRules` (dispatch, name the missing rules once in the
1050
- report) and `drift` (dispatch, name it once in the report). What each means and
1050
+ report), `shadowedRules` (the dispatch prompt names each `resolved` file as the one to read in place of the `ref` the reviewer's own text spells), `unresolvedReferences` (dispatch, name them once in the report) and `drift` (dispatch, name it once in the report). What each means and
1051
1051
  what to do with it: `SKILL.detail.md` beside this file, section "Project-local
1052
1052
  reviewers". A description saying another entry point dispatches it is its
1053
1053
  author's routing note, not a restriction.
@@ -1085,7 +1085,7 @@ When `review.jev.select` is on, before Wave A/B dispatch: run `keryx review jev-
1085
1085
 
1086
1086
  ## Legacy/Profile Reviewer Auto-Detection
1087
1087
 
1088
- Legacy/profile reviewers are specialized review profiles that predate the review-domain `review-*` naming. They are still valid and must be shown separately from generic and convention reviewers so the user can opt in deliberately.
1088
+ Legacy/profile reviewers are specialized review profiles that predate the review-domain `review-*` naming. They are still valid and must be shown separately from generic and convention reviewers so the user can opt in deliberately. **They are present only when installed:** `code-ai-review`, `code-learned-review`, `code-style-review` and `code-mobx-store-review` are in the `full` profile only, so in any other profile the flags below (`--code-ai`, `--learned`, `--code-style`, `--mobx-store`, `--legacy-profiles`) name reviewers that may not exist. When the inventory's `bundledSource` is `"project"`, a legacy reviewer missing from the `bundled` half of `keryx review reviewers --json` is absent (when it is `"package"` the half is the keryx package's own list and says nothing about this project — decide as `SKILL.detail.md`, "Skills present only when installed", describes): leave it out of the `Optional legacy/profile reviewers` preview, and when a flag asked for it, do not dispatch a substitute — record it in `Skipped reviewers` with reason `not-installed` and the command that adds it (`keryx skills install --profile full`, or the narrower manifest form in `SKILL.detail.md`, "Skills present only when installed"). `--legacy-profiles` and `--all` dispatch the legacy reviewers that are installed and skip the rest the same way.
1089
1089
 
1090
1090
  | Trigger | Reviewers appended |
1091
1091
  |---|---|
@@ -1289,18 +1289,18 @@ Before dispatching a reviewer through a platform-native sub-agent mechanism, ver
1289
1289
 
1290
1290
  Runtime rules:
1291
1291
  - If the exact reviewer agent type exists, dispatch that reviewer directly.
1292
- - If the exact reviewer agent type does not exist but `skills/<reviewer>/SKILL.md` exists, dispatch `general-purpose` and include the reviewer name, skill path, bounded review context, and required `REVIEW_RESULT` schema in the prompt.
1292
+ - If the exact reviewer agent type does not exist but `skills/<reviewer>/SKILL.md` exists, dispatch `general-purpose` and include the reviewer name, the path of that skill file, bounded review context, and required `REVIEW_RESULT` schema in the prompt. For a bundled reviewer that path goes in the prompt text and in `dispatch_plan`, not in the input's `skill_path` field, which is for project reviewers only (the project-reviewer rule below).
1293
1293
  - If neither the agent type nor the skill file exists, do not silently substitute another reviewer. Mark that reviewer as `BLOCKED`, include the missing agent/skill name, and continue only with independent reviewers.
1294
+ - **A project reviewer is dispatched by its package, on every branch above.** For each entry in the `project` half of `keryx review reviewers --json`, put the absolute path of its registered `<path>/SKILL.md` in the dispatch (`skill_path`) and say in the prompt that this file is the reviewer's definition and overrides the built-in definition of any agent type with the same name. A runtime may ship an agent type called exactly what the project called its reviewer, carrying an older or unrelated prompt; without the path the round runs that prompt and reports it under the project reviewer's name. If the file is missing, mark the reviewer `BLOCKED` and name the missing path — do not fall back to the same-named agent type or to any other reviewer. For every entry in that reviewer's `shadowedRules`, the prompt also says which file to read: "where your text names `<ref>`, read `<resolved>` instead" (the absolute path of `resolved`). The project keeps its own version of that rule beside the one keryx ships under the same name, the reviewer's text still spells the shipped path, and nothing but this sentence in the dispatch makes it read the project's — see `SKILL.detail.md`, "Dispatching a project reviewer".
1294
1295
  - Record the chosen runtime per reviewer in `review_context.review_plan.dispatch_plan`.
1295
- - The user-facing progress line must be explicit: "Running `<reviewer>` via `general-purpose` fallback because native agent type is unavailable."
1296
-
1297
- Do not use vague fallback messages such as "running through available agent types" without naming which reviewers used fallback and why.
1296
+ - The user-facing progress line must be explicit: "Running `<reviewer>` via `general-purpose` fallback because native agent type is unavailable." Do not use vague fallback messages such as "running through available agent types" without naming which reviewers used fallback and why.
1298
1297
 
1299
1298
  Pass each sub-reviewer a payload matching `.metaproject/skills/gdskills/review/review-orchestrator/reviewer-input.schema.json`:
1300
1299
 
1301
1300
  ```yaml
1302
1301
  review_context: <bounded context pack>
1303
1302
  reviewer: <skill-name>
1303
+ skill_path: <absolute path of <path>/SKILL.md — project reviewers only; omit for a bundled reviewer>
1304
1304
  scope_mode: diff | path
1305
1305
  context_doc: <path or empty>
1306
1306
  issue_url: <url or empty>
@@ -17,6 +17,13 @@
17
17
  "reviewer": {
18
18
  "type": "string"
19
19
  },
20
+ "skill_path": {
21
+ "type": [
22
+ "string",
23
+ "null"
24
+ ],
25
+ "description": "Absolute path of the reviewer's registered `<path>/SKILL.md`, set for every reviewer from the `project` half of `keryx review reviewers --json` and omitted for a bundled one. The file is the reviewer's definition: it overrides the built-in definition of a same-named agent type, whichever runtime branch dispatches it. A path that does not exist makes the reviewer BLOCKED, never substituted. Optional, and like the rest of this schema it is read by the dispatching agent — no keryx command validates a dispatch against it."
26
+ },
20
27
  "scope_mode": {
21
28
  "type": "string",
22
29
  "enum": [