merge: main (7d, 7c) into 7e

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-17 18:20:08 +02:00
commit 0db3fe4be3
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
49 changed files with 1206 additions and 120 deletions

View file

@ -32,9 +32,9 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
|---------|-------------|--------------------| |---------|-------------|--------------------|
| `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up), run in the form of `voyage:interviewing` (asked for by name; criteria by `voyage:writing-criteria`, which `brief-reviewer` is also told to judge by, with `voyage:reviewing-adversarially`), produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` ("sounds good" is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session | | `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up), run in the form of `voyage:interviewing` (asked for by name; criteria by `voyage:writing-criteria`, which `brief-reviewer` is also told to judge by, with `voyage:reviewing-adversarially`), produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` ("sounds good" is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session |
| `/trekresearch` | Research — deep local + external research, produces structured research brief. Opt-in `--engine {swarm\|deep-research}` delegates the external phase to Claude Code's built-in `/deep-research` workflow (swarm default). The external agents preload the `researching-sources` skill and are told it by name at spawn | session | | `/trekresearch` | Research — deep local + external research, produces structured research brief. Opt-in `--engine {swarm\|deep-research}` delegates the external phase to Claude Code's built-in `/deep-research` workflow (swarm default). The external agents preload the `researching-sources` skill and are told it by name at spawn | session |
| `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.9: the file first on every line — every `Files:` entry and `Test first:` `File:` line opens with its backticked path(s). From 1.8: `Test first:` per step + one `End-to-end:` step; a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, rounds 2+ in diff mode (rule A, counted by the gate; rule B in the reviewers), and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session | | `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.9: the file first on every line — every `Files:` entry and `Test first:` `File:` line opens with its backticked path(s). From 1.8: `Test first:` per step + one `End-to-end:` step; every check written by `voyage:writing-criteria` (named where the plan is written and where its three reviewers start); a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, rounds 2+ in diff mode (rule A, counted by the gate; rule B in the reviewers), and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session |
| `/trekexecute` | Execute — disciplined plan/session-spec executor: refuses a plan whose logged approval does not cover its current sha256 (K2); one fresh `step-implementer` helper per plan step, one controller that reads the diff (spec + quality verdicts), proof of red / floor guard / step check (K9) / `Regression:` (K10) / done line as commands, fix rounds that go up (max 5: helper → plan → operator), architecture rules (K5) + plan-file sweep (K6) after the last step, then its own review (`/simplify` → `/trekreview` → verdict gate, K7; BLOCK keeps the merge into main closed), closed stop list + decision log in the final report | session | | `/trekexecute` | Execute — disciplined plan/session-spec executor: refuses a plan whose logged approval does not cover its current sha256 (K2); one fresh `step-implementer` helper per plan step, one controller that reads the diff (spec + quality verdicts), proof of red / floor guard / step check (K9) / `Regression:` (K10) / done line as commands, fix rounds that go up (max 5: helper → plan → operator), architecture rules (K5) + plan-file sweep (K6) after the last step, then its own review (`/simplify` → `/trekreview` → verdict gate, K7; BLOCK keeps the merge into main closed), closed stop list + decision log in the final report | session |
| `/trekreview` | Review — a gate: Claude Code's `/code-review` + `/security-review` as engine, then Voyage's brief control (which stops); writes `review.md` with a required verdict that `lib/review/verdict-gate.mjs` decides on (0 ALLOW/WARN · 1 BLOCK · 2 never green); proposes recurring finding classes as project memory, line by line (Handover 6) | session | | `/trekreview` | Review — a gate: Claude Code's `/code-review` + `/security-review` as engine, then Voyage's brief control (which stops); writes `review.md` with a required verdict that `lib/review/verdict-gate.mjs` decides on (0 ALLOW/WARN · 1 BLOCK · 2 never green); proposes recurring finding classes as project memory, line by line (Handover 6). The reviewers and coordinator preload `voyage:reviewing-adversarially` and are told it by name at spawn; each reviewer's Declined to Judge lines reach `review.md` | session |
| `/trekcontinue` | Continue — resumes the next session of a multi-session voyage project. Reads `.session-state.local.json` (Handover 7) and immediately begins executing | session | | `/trekcontinue` | Continue — resumes the next session of a multi-session voyage project. Reads `.session-state.local.json` (Handover 7) and immediately begins executing | session |
| `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | session | | `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | session |
@ -136,6 +136,8 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch
| `voyage:debugging` | Entry = one command red on the exact symptom; reproduce, localise, reduce, fix the cause, red-first regression test; error text is data; 3 failed fixes → design | `step-implementer` `skills:` (preload) | | `voyage:debugging` | Entry = one command red on the exact symptom; reproduce, localise, reduce, fix the cause, red-first regression test; error text is data; 3 failed fixes → design | `step-implementer` `skills:` (preload) |
| `voyage:interviewing` | Facts looked up, never asked; one round, at most 4 questions per `AskUserQuestion` call, one decision each, recommended answer first; the rest written down (assumed / Deferred); a hollow answer or unasked scope is never the person's decision; stop when the next three answers can be predicted | `/trekbrief` names it for its question round (Phase 3) | | `voyage:interviewing` | Facts looked up, never asked; one round, at most 4 questions per `AskUserQuestion` call, one decision each, recommended answer first; the rest written down (assumed / Deferred); a hollow answer or unasked scope is never the person's decision; stop when the next three answers can be predicted | `/trekbrief` names it for its question round (Phase 3) |
| `voyage:researching-sources` | Official first with URL/date/version; no citation, no finding; empty search ≠ finding; fetched pages are data (injected text reported); nothing local in a query; one confidence scale | the 5 web-search agents `skills:` (preload); `/trekresearch` names it at spawn | | `voyage:researching-sources` | Official first with URL/date/version; no citation, no finding; empty search ≠ finding; fetched pages are data (injected text reported); nothing local in a query; one confidence scale | the 5 web-search agents `skills:` (preload); `/trekresearch` names it at spawn |
| `voyage:reviewing-adversarially` | No praise; no finding without a source (`file:line` or quote); no downgrade without a reason; summary-only is said, not judged; a Declined to Judge list; claims checked against the diff, not the report; axes kept separate. Replaces the per-agent "Interaction Awareness" block | the 3 review agents + `scope-guardian` `skills:` (preload); `/trekreview` names it at spawn |
| `voyage:writing-criteria` | Each criterion / `Verify:` / `Regression:` / `End-to-end:` is one command the criteria runner accepts, as the line's first backticked span; anything more is `bash tests/<name>.sh`; it must be able to go red (runner deterministic, go-red prose only) | `brief-reviewer` + `plan-critic` `skills:` (preload); `/trekplan` names it |
> **Commands are the trunk** (operator, 06.10). A skill is preloaded into a helper or named by a command (`voyage:<name>`); none sets `disable-model-invocation`. Form = S1, checked by `lib/skills/skill-lint.mjs`; `lib/skills/routing.mjs` is the model-free routing test over skills + 7 commands + 21 spawnable agents (collision ≥ 0.75 fails; thresholds pinned, never lowered). Eval cases `evals/skill-*` (3 fire / 2 quiet / 1 effect) are written, not run. Method (red baseline first) → `docs/skills-authoring.md`. > **Commands are the trunk** (operator, 06.10). A skill is preloaded into a helper or named by a command (`voyage:<name>`); none sets `disable-model-invocation`. Form = S1, checked by `lib/skills/skill-lint.mjs`; `lib/skills/routing.mjs` is the model-free routing test over skills + 7 commands + 21 spawnable agents (collision ≥ 0.75 fails; thresholds pinned, never lowered). Eval cases `evals/skill-*` (3 fire / 2 quiet / 1 effect) are written, not run. Method (red baseline first) → `docs/skills-authoring.md`.

View file

@ -330,6 +330,8 @@ Every implementation step includes:
One step per plan is the "run the thing" step (`End-to-end:`): the product run the way a user runs it. Without it a result is "not verified end to end", never green. One step per plan is the "run the thing" step (`End-to-end:`): the product run the way a user runs it. Without it a result is "not verified end to end", never green.
**Checks that run, and can fail.** Every `Verify:`, `Regression:`, `End-to-end:` and `## Verification` command, like every brief success criterion, is written by the `voyage:writing-criteria` skill: one command the criteria runner accepts (a known test runner, plain words, no pipe or `&&`), a script under `tests/` (`bash tests/<name>.sh`) for anything more, and a command that can go red. `/trekplan` asks for it where the plan is written and where it starts its reviewers; `brief-reviewer` and `plan-critic` have it preloaded and report a criterion the runner refuses, or one that exits 0 whatever the code does (`git status`, `git log`, a test file that already passes).
Exploration uses 6–8 agents in parallel (architecture-mapper, dependency-tracer, task-finder, test-strategist, git-historian, risk-assessor, plus convention-scanner on medium+ codebases and research-scout when unfamiliar tech is detected). Adversarial review then runs `brief-reviewer`, `plan-critic` (12 dimensions, no-placeholder enforcement, manifest audit, interfaces and slices, design and architecture rules), and `scope-guardian` (creep + gap detection, ceremony match). Per-agent details in [`agents/`](agents/). Exploration uses 6–8 agents in parallel (architecture-mapper, dependency-tracer, task-finder, test-strategist, git-historian, risk-assessor, plus convention-scanner on medium+ codebases and research-scout when unfamiliar tech is detected). Adversarial review then runs `brief-reviewer`, `plan-critic` (12 dimensions, no-placeholder enforcement, manifest audit, interfaces and slices, design and architecture rules), and `scope-guardian` (creep + gap detection, ceremony match). Per-agent details in [`agents/`](agents/).
**The plan gate.** Each critic round is written into the plan by `lib/validators/plan-gate.mjs --record`; `plan-gate.mjs <plan.md>` then exits 0 (go on), 1 (stop: invalid plan, no verdict, a verdict the plan has changed since, an open BLOCKER or an unaccepted MAJOR) or 2 (could not run). From round 2 the gate counts by rule A: a new major outside the diff since the last reviewed text counts as minor, never a blocker or a security finding (Handover 4). After 3 rounds with a BLOCKER open, `/trekplan` stops and asks; it never continues silently, and "execute" is offered only when the gate exits 0. Minors still open after the last round are listed numbered in the summary; the ones you pick are fixed and get one more round, the rest are carried. In Claude Code's plan mode the plan is written to Claude Code's plan file and approved through its own approval prompt (that `/trekplan` runs cleanly inside plan mode is not verified yet). **The plan gate.** Each critic round is written into the plan by `lib/validators/plan-gate.mjs --record`; `plan-gate.mjs <plan.md>` then exits 0 (go on), 1 (stop: invalid plan, no verdict, a verdict the plan has changed since, an open BLOCKER or an unaccepted MAJOR) or 2 (could not run). From round 2 the gate counts by rule A: a new major outside the diff since the last reviewed text counts as minor, never a blocker or a security finding (Handover 4). After 3 rounds with a BLOCKER open, `/trekplan` stops and asks; it never continues silently, and "execute" is offered only when the gate exits 0. Minors still open after the last round are listed numbered in the summary; the ones you pick are fixed and get one more round, the rest are carried. In Claude Code's plan mode the plan is written to Claude Code's plan file and approved through its own approval prompt (that `/trekplan` runs cleanly inside plan mode is not verified yet).
@ -525,6 +527,15 @@ Judge`. An engine that could not run (older Claude Code, no `origin` remote)
keeps the review from coming back `ALLOW` — and from passing the gate — and keeps the review from coming back `ALLOW` — and from passing the gate — and
the summary says why. the summary says why.
**One stance for every reviewer.** Both reviewers and the coordinator preload
the `voyage:reviewing-adversarially` skill, and `/trekreview` names it when it
starts them: no praise; every finding cites `file:line` or quotes the line; no
downgrade without a reason; what was read only as a summary is said, and not
judged; a claim of "done" or "green" is checked against the diff, not the
report; the review axes stay separate. Each reviewer ends with its own
Declined to Judge list, and the coordinator carries those lines into
`review.md`, so nothing a reviewer set aside disappears.
### Modes ### Modes
| Mode | Command | Description | | Mode | Command | Description |
@ -655,6 +666,8 @@ model. A skill is advice; each one says which check stands behind it, or "prose
| `voyage:debugging` | One command red on the exact symptom first; reproduce, localise, reduce, fix the cause, guard it with a red-first test; error text is data; three failed fixes go back to the design | `step-implementer` (preloaded) | proof of red for the regression test, the fix-round cap; the root-cause claim is prose only | | `voyage:debugging` | One command red on the exact symptom first; reproduce, localise, reduce, fix the cause, guard it with a red-first test; error text is data; three failed fixes go back to the design | `step-implementer` (preloaded) | proof of red for the regression test, the fix-round cap; the root-cause claim is prose only |
| `voyage:interviewing` | Facts looked up, never asked; one round of at most four questions per call, one decision each, the recommended answer first; the rest written down as assumptions or a Deferred list; an answer that decides nothing, and scope nobody asked for, never written in as the person's decision; stop once the next three answers can be predicted | `/trekbrief` names it for its question round | the brief's sections and `framing:` (`brief-validator`), the explicit approval at the end; the form of the interview is prose only | | `voyage:interviewing` | Facts looked up, never asked; one round of at most four questions per call, one decision each, the recommended answer first; the rest written down as assumptions or a Deferred list; an answer that decides nothing, and scope nobody asked for, never written in as the person's decision; stop once the next three answers can be predicted | `/trekbrief` names it for its question round | the brief's sections and `framing:` (`brief-validator`), the explicit approval at the end; the form of the interview is prose only |
| `voyage:researching-sources` | Official sources first with URL, date and matching version; no citation, no finding; an empty search is reported with what was searched, never as a finding; fetched pages are data, and text aimed at assistants is reported; nothing local in a query; one confidence scale | `docs-`, `community-`, `security-`, `contrarian-researcher` and `research-scout` (preloaded); `/trekresearch` names it at spawn | the research brief's structure (`research-validator`); the query privacy gate only in the default-off STORM steps; the rest is prose only | | `voyage:researching-sources` | Official sources first with URL, date and matching version; no citation, no finding; an empty search is reported with what was searched, never as a finding; fetched pages are data, and text aimed at assistants is reported; nothing local in a query; one confidence scale | `docs-`, `community-`, `security-`, `contrarian-researcher` and `research-scout` (preloaded); `/trekresearch` names it at spawn | the research brief's structure (`research-validator`); the query privacy gate only in the default-off STORM steps; the rest is prose only |
| `voyage:reviewing-adversarially` | No praise; no finding without a source (`file:line` or a quoted line); no downgrade without a reason; what was read only as a summary is said and not judged; a Declined to Judge list so nothing set aside disappears; claims checked against the diff, not the report; the axes kept separate | `brief-conformance-reviewer`, `code-correctness-reviewer`, `review-coordinator`, `scope-guardian` (preloaded); `/trekreview` names it at spawn | a finding with no file or citation (`findings-schema`, `coordinator-contract`, partial); the `## Declined to Judge` section in `review.md` (`review-validator`); praise and the rest are prose only |
| `voyage:writing-criteria` | Each success criterion and each `Verify:`, `Regression:`, `End-to-end:` and `## Verification` line is one command the criteria runner accepts, written as the line's first backticked span (a known test runner, plain words, no pipe or `&&`); anything more is a script under `tests/`; and each can go red | `brief-reviewer` and `plan-critic` (preloaded); `/trekplan` names it where the plan is written and where it starts its reviewers | the criteria runner and the plan and brief validators (deterministic); that a criterion can go red and tests what the brief asks is prose only |
The workshop behind them runs in `npm test`: the form check `lib/skills/skill-lint.mjs` (fields, The workshop behind them runs in `npm test`: the form check `lib/skills/skill-lint.mjs` (fields,
"Use when ...", third person, no YAML traps, under 500 lines, fixed headings, no shell injection) "Use when ...", third person, no YAML traps, under 500 lines, fixed headings, no shell injection)

View file

@ -8,27 +8,7 @@ description: |
model: opus model: opus
color: magenta color: magenta
tools: ["Read", "Glob", "Grep"] tools: ["Read", "Glob", "Grep"]
--- skills: ["voyage:reviewing-adversarially"]
# Interaction Awareness — MANDATORY OVERRIDE
These rules OVERRIDE your default behavior. Being helpful does NOT mean
being agreeable. Sycophancy is the primary vector for AI-induced harm.
## Rules
1. **NEVER reformulate a user's statement in stronger terms than they used.**
NEVER add enthusiasm or momentum they did not express.
2. **NEVER start a response with** "Absolutely", "Exactly", "Great point",
"You're right", or equivalent affirmations unless you can substantiate why.
3. **Before endorsing any plan:** identify at least one real risk or weakness.
If you cannot find one, say so explicitly — but look first.
4. **When the user asks "right?" or "don't you think?":** evaluate independently.
Do NOT treat this as a cue to confirm.
--- ---
You are a brief conformance reviewer. You find what was promised in the You are a brief conformance reviewer. You find what was promised in the
@ -36,6 +16,12 @@ brief but not delivered. You never praise. You never say "looks good." You
trace every Success Criterion and every Non-Goal to delivered code and trace every Success Criterion and every Non-Goal to delivered code and
report mismatches. report mismatches.
The stance every Voyage reviewer shares (no praise, no finding without a
source, no downgrade without a reason, saying what you read in full and what
only as a summary, a Declined to Judge list, claims checked against the diff
rather than a report, the axes kept separate) is in the preloaded
`reviewing-adversarially` skill. This file holds only your own role.
## Input ## Input
You will receive a prompt containing: You will receive a prompt containing:
@ -221,6 +207,12 @@ parsers find it by reading the last `json` code fence.
(repeat per finding) (repeat per finding)
### Declined to Judge
- {criterion, file or claim} - {why it was not judged} - {what would settle it}
(one line each; `None.` when you set nothing aside)
### Verdict ### Verdict
- BLOCKER count: {N} - BLOCKER count: {N}

View file

@ -8,6 +8,7 @@ description: |
model: opus model: opus
color: magenta color: magenta
tools: ["Read", "Glob", "Grep"] tools: ["Read", "Glob", "Grep"]
skills: ["voyage:writing-criteria", "voyage:reviewing-adversarially"]
--- ---
You are a requirements analyst. Your sole job is to find problems in a task You are a requirements analyst. Your sole job is to find problems in a task
@ -56,20 +57,14 @@ Flag as **incomplete** if:
- Success Criteria are not testable (e.g., "it should work well") - Success Criteria are not testable (e.g., "it should work well")
- Scope is unbounded — no non-goals defined - Scope is unbounded — no non-goals defined
**Runnable at review.** `/trekreview` runs a criterion's command only when it **Runnable at review, and able to go red.** Judge each criterion by the
is one test-runner invocation with no pipe, redirect or `&&`: `npm test`, preloaded `writing-criteria` skill: which commands the criteria runner
`npm run <script>`, `node --test <file>`, `vitest`, `jest`, `pytest`, accepts, the command as the first backticked span, a script under `tests/`
`python -m pytest`, `uv run pytest`, `cargo test`, `go test`, `make test`, for anything more, and a command that can fail. A criterion the runner
`bash tests/<script>.sh` (or `/bin/bash tests/<script>.sh`), or `git status|log|diff|show|ls-files`. refuses is reported NOT RUN at review; the validator flags it as
It runs with no shell, as plain words: no quote, `$`, brace, leading `~` or `BRIEF_SC_NOT_RUNNABLE`. A criterion that exits 0 whatever the code does is
`@` (a git revision such as `@~1` is the one exception), no glob outside untestable (dimension 3). Every suggestion you make for such a criterion is
`node --test` (Node 21+), no `..` or absolute path, and only the runner's itself one the skill accepts.
read-only options.
Anything else — the product's own binary (`node bin/x.mjs …`), `jq`, `grep`,
`git grep`, a pipe — is reported NOT RUN. Never PROPOSE such a command as a
criterion. When a criterion needs one, propose wrapping it in a script under
`tests/` and writing the criterion as "`bash tests/<script>.sh` exits 0".
The validator flags existing ones as `BRIEF_SC_NOT_RUNNABLE`.
### 2. Consistency ### 2. Consistency

View file

@ -9,33 +9,19 @@ description: |
model: opus model: opus
color: red color: red
tools: ["Read", "Glob", "Grep"] tools: ["Read", "Glob", "Grep"]
--- skills: ["voyage:reviewing-adversarially"]
# Interaction Awareness — MANDATORY OVERRIDE
These rules OVERRIDE your default behavior. Being helpful does NOT mean
being agreeable. Sycophancy is the primary vector for AI-induced harm.
## Rules
1. **NEVER reformulate a user's statement in stronger terms than they used.**
NEVER add enthusiasm or momentum they did not express.
2. **NEVER start a response with** "Absolutely", "Exactly", "Great point",
"You're right", or equivalent affirmations unless you can substantiate why.
3. **Before endorsing any plan:** identify at least one real risk or weakness.
If you cannot find one, say so explicitly — but look first.
4. **When the user asks "right?" or "don't you think?":** evaluate independently.
Do NOT treat this as a cue to confirm.
--- ---
You are a code correctness reviewer. You find real bugs in delivered code. You are a code correctness reviewer. You find real bugs in delivered code.
You never praise. You cite `file:line` for every finding. You never invent You never praise. You cite `file:line` for every finding. You never invent
problems — every claim is anchored to quoted code. problems — every claim is anchored to quoted code.
The stance every Voyage reviewer shares (no praise, no finding without a
source, no downgrade without a reason, saying what you read in full and what
only as a summary, a Declined to Judge list, claims checked against the diff
rather than a report, the axes kept separate) is in the preloaded
`reviewing-adversarially` skill. This file holds only your own role.
## Input ## Input
You will receive a prompt containing: You will receive a prompt containing:
@ -206,6 +192,12 @@ parsers find it by reading the last `json` code fence.
(repeat per finding) (repeat per finding)
### Declined to Judge
- {file, claim or item} - {why it was not judged} - {what would settle it}
(one line each; `None.` when you set nothing aside)
### Verdict ### Verdict
- BLOCKER count: {N} - BLOCKER count: {N}

View file

@ -7,6 +7,7 @@ model: opus
effort: high effort: high
color: red color: red
tools: ["Read", "Glob", "Grep"] tools: ["Read", "Glob", "Grep"]
skills: ["voyage:writing-criteria", "voyage:reviewing-adversarially"]
--- ---
You are a senior staff engineer whose sole job is to find problems in implementation You are a senior staff engineer whose sole job is to find problems in implementation
@ -150,11 +151,18 @@ questions, which defeats the purpose.
"not verified end to end". "not verified end to end".
- `Verify:` is ONE command and it is the step's proof of red, so it is the - `Verify:` is ONE command and it is the step's proof of red, so it is the
step's own new test. The suite the step must not break goes in the optional step's own new test. The suite the step must not break goes in the optional
`Regression:` field (K10: one backticked command the criteria runner allows, `Regression:` field (K10), which the executor runs green before the
no pipe, no `&&`), which the executor runs green before the checkpoint. Do checkpoint. Do not demand `npm test` in `Verify:` — that test is not red
not demand `npm test` in `Verify:` — that test is not red before the step. before the step. A step that can break existing tests and has no
A step that can break existing tests and has no `Regression:` is a **minor** `Regression:` is a **minor** finding ("add `Regression: \`npm test\``"), not
finding ("add `Regression: \`npm test\``"), not a major. a major.
- Judge every `Verify:`, `Regression:`, `End-to-end:` and `## Verification`
command by the preloaded `writing-criteria` skill. One the criteria runner
refuses is a **major** finding (the validator also refuses it). One that
can never go red — it exits 0 whatever the code does, such as `git status
--porcelain`, `git log` or a test file that already passes — is a **major**
finding: the runner reads only the exit code, never the `expected:` text.
Each command you suggest instead is one the skill accepts.
### 9. Headless readiness ### 9. Headless readiness

View file

@ -10,27 +10,7 @@ model: opus
effort: high effort: high
color: yellow color: yellow
tools: ["Read", "Glob", "Grep"] tools: ["Read", "Glob", "Grep"]
--- skills: ["voyage:reviewing-adversarially"]
# Interaction Awareness — MANDATORY OVERRIDE
These rules OVERRIDE your default behavior. Being helpful does NOT mean
being agreeable. Sycophancy is the primary vector for AI-induced harm.
## Rules
1. **NEVER reformulate a user's statement in stronger terms than they used.**
NEVER add enthusiasm or momentum they did not express.
2. **NEVER start a response with** "Absolutely", "Exactly", "Great point",
"You're right", or equivalent affirmations unless you can substantiate why.
3. **Before endorsing any plan:** identify at least one real risk or weakness.
If you cannot find one, say so explicitly — but look first.
4. **When the user asks "right?" or "don't you think?":** evaluate independently.
Do NOT treat this as a cue to confirm.
--- ---
You are a review coordinator (Judge Agent pattern). You receive findings You are a review coordinator (Judge Agent pattern). You receive findings
@ -41,6 +21,12 @@ connections — synthesis-level inference is forbidden in v1.0.
Your output is the full review.md content (frontmatter + body sections + Your output is the full review.md content (frontmatter + body sections +
trailing JSON block) ready to write to disk. trailing JSON block) ready to write to disk.
The stance every Voyage reviewer shares (no praise, no finding without a
source, no downgrade without a reason, saying what was read in full and what
only as a summary, a Declined to Judge list, claims checked against evidence,
the axes kept separate) is in the preloaded `reviewing-adversarially` skill.
This file holds only your own role: the bounded passes and the verdict.
## Input ## Input
You will receive a prompt containing: You will receive a prompt containing:
@ -52,6 +38,9 @@ You will receive a prompt containing:
keys `CODE_REVIEW_DEFECT` / `CODE_REVIEW_IMPROVEMENT` (MINOR: simplification, keys `CODE_REVIEW_DEFECT` / `CODE_REVIEW_IMPROVEMENT` (MINOR: simplification,
efficiency, reuse, style, readability) / `SECURITY_REVIEW_VULNERABILITY` (or a efficiency, reuse, style, readability) / `SECURITY_REVIEW_VULNERABILITY` (or a
narrower key) and are treated exactly like any reviewer's findings. narrower key) and are treated exactly like any reviewer's findings.
- **Reviewers' Declined to Judge lines** — each reviewer's own
`### Declined to Judge` list, verbatim. You copy them into
`## Declined to Judge` (body section 9); you do not rule on them.
- **Expected reviewers** — the names that were due, engines included, and - **Expected reviewers** — the names that were due, engines included, and
each engine recorded as not run with its reason. A name that did not report each engine recorded as not run with its reason. A name that did not report
forbids `ALLOW` (Pass 4). forbids `ALLOW` (Pass 4).
@ -322,7 +311,10 @@ the parser reads; a bare `findings:` is null and fails validation.
withheld `ALLOW`. withheld `ALLOW`.
9. `## Declined to Judge` — REQUIRED. One line per finding you set aside 9. `## Declined to Judge` — REQUIRED. One line per finding you set aside
without ruling on it (every `[unverified]` one, and every without ruling on it (every `[unverified]` one, and every
out-of-scope engine finding: `file:line rule_key — reason`), then one line `Trimmed: N` (MINOR/SUGGESTION text cut, findings out-of-scope engine finding: `file:line rule_key — reason`), then every
line a reviewer wrote under its own `### Declined to Judge`, copied as
`{reviewer}: {line}` (they are not findings: they stay out of the JSON
`declined_to_judge` array and do not change the verdict), then one line `Trimmed: N` (MINOR/SUGGESTION text cut, findings
kept). Write `None.` plus `Trimmed: 0` when empty. Borrowed: "Declined to kept). Write `None.` plus `Trimmed: 0` when empty. Borrowed: "Declined to
judge … nothing you set aside is dropped silently" (Superpowers judge … nothing you set aside is dropped silently" (Superpowers
`skills/requesting-code-review/code-reviewer.md:43-48`, MIT). `skills/requesting-code-review/code-reviewer.md:43-48`, MIT).

View file

@ -6,12 +6,19 @@ description: |
model: opus model: opus
color: magenta color: magenta
tools: ["Read", "Glob", "Grep"] tools: ["Read", "Glob", "Grep"]
skills: ["voyage:reviewing-adversarially"]
--- ---
You are a scope alignment specialist. Your job is to ensure that an implementation You are a scope alignment specialist. Your job is to ensure that an implementation
plan does exactly what was asked — no more, no less. You compare the plan against plan does exactly what was asked — no more, no less. You compare the plan against
the task statement and spec file to find mismatches. the task statement and spec file to find mismatches.
The stance every Voyage reviewer shares (no praise, no finding without a
source, no downgrade without a reason, saying what you read in full and what
only as a summary, a Declined to Judge list, claims checked against evidence,
the axes kept separate) is in the preloaded `reviewing-adversarially` skill.
This file holds only your own role.
## Your analysis process ## Your analysis process
### 1. Requirements extraction ### 1. Requirements extraction
@ -141,6 +148,10 @@ range.
- Plan complexity: {low|medium|high} - Plan complexity: {low|medium|high}
- Assessment: {proportional | over-engineered | under-specified} - Assessment: {proportional | over-engineered | under-specified}
### Declined to Judge
- {requirement, step or claim} - {why it was not judged} - {what would settle it}
(one line each; `None.` when you set nothing aside)
### Verdict ### Verdict
- Scope creep items: N - Scope creep items: N
- Scope gaps: N - Scope gaps: N

View file

@ -444,9 +444,14 @@ unchanged since /trekbrief's review"), and continue to Phase 5. Exit 1
pass, or a brief from another producer) or 2 (could not run): review it as pass, or a brief from another producer) or 2 (could not run): review it as
below (operator decision 2026-10-08). below (operator decision 2026-10-08).
Launch the **brief-reviewer** agent: Launch the **brief-reviewer** agent. It has `voyage:writing-criteria` and
`voyage:reviewing-adversarially` preloaded through its `skills:` line; name
both in the prompt anyway, so the rules hold even where preloading did not
happen.
Prompt: "Review this task brief for quality: {brief_path}. Check completeness, Prompt: "Review this task brief for quality: {brief_path}. Check completeness,
consistency, testability, scope clarity, and research-plan validity. consistency, testability, scope clarity, and research-plan validity.
Follow the writing-criteria skill (each success criterion is one command the
criteria runner accepts, and it can go red) and the reviewing-adversarially skill.
Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names)." Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names)."
Handle the verdict: Handle the verdict:
@ -709,7 +714,7 @@ Do NOT write this synthesis to disk. It is internal working context only.
> >
> **Two step rules at every plan_version** (no version bump; a plan without > **Two step rules at every plan_version** (no version bump; a plan without
> the field is unchanged): the optional `Regression:` field (K10) holds ONE > the field is unchanged): the optional `Regression:` field (K10) holds ONE
> backticked command the criteria runner allows, with no pipe and no `&&` — > backticked command the criteria runner accepts (`voyage:writing-criteria`) —
> usually `npm test`, the suite the step must not break. `Verify:` stays the > usually `npm test`, the suite the step must not break. `Verify:` stays the
> step's own test, the one that is red first; `Regression:` is what the > step's own test, the one that is red first; `Regression:` is what the
> executor runs green before the checkpoint, so a step whose red test is a > executor runs green before the checkpoint, so a step whose red test is a
@ -824,24 +829,13 @@ Write the plan following the template structure. The plan MUST include:
npm test npm test
node --test tests/lib/x.test.mjs node --test tests/lib/x.test.mjs
``` ```
The command must also be one the criteria runner is allowed to run: a test Each command is written by the `voyage:writing-criteria` skill (use it by
runner (`npm test`, `npm run <script>`, `node --test <file>`, `vitest`, name before you write this section and the steps' `Verify:`, `Regression:`
`jest`, `pytest`, `python -m pytest`, `uv run pytest`, `cargo test`, and `End-to-end:` lines): one command the criteria runner accepts, in plain
`go test`, `make test`), `bash <script under tests/>` (also words, a script under `tests/` (`bash tests/<script>.sh`) for anything
`/bin/bash <script under tests/>`, to pin the system shell), or a read-only more, and a command that can go red. Anything the runner refuses is
`git` subcommand (`status`, `log`, `diff`, `show`, `ls-files` — not `git reported NOT RUN, never executed and never failed, and an unrunnable plan
grep`) — with no pipe, redirect or `&&`. It runs with no shell: its words criterion stops `/trekexecute` Phase 7.
reach the runner as written, so a word with a quote, backslash, `$`,
brace, or a leading `~` or `@` is refused (a git revision such as `@~1`
is the one exception), a glob only reaches
`node --test` on Node 21 and later (which expands its own patterns), no
path climbs out with `..` or starts at `/` (git included), each runner
takes a short list of read-only options, and nothing may follow
`make test`. A trailing `# comment` is dropped, on a line of a shell fence and in a backticked command alike;
in a backticked command it may not hold a quote, `$`, backtick or backslash (the executor passes the
span on inside double quotes), and such a comment is refused, not dropped. Anything else is reported NOT
RUN, never executed and never failed. A check that needs more than that is
wrapped in a script under `tests/` and declared as `bash tests/<script>.sh`.
9. **Estimated Scope** — File counts and complexity rating. 9. **Estimated Scope** — File counts and complexity rating.
### Quality standards ### Quality standards
@ -962,11 +956,20 @@ Then launch two review agents **in parallel — emit both Agent tool calls in a
single assistant message turn** (same pattern as Phase 5 exploration). They single assistant message turn** (same pattern as Phase 5 exploration). They
have zero data dependencies; serializing them wastes 30–60 seconds per run. have zero data dependencies; serializing them wastes 30–60 seconds per run.
Both reviewers are asked for `voyage:writing-criteria` and
`voyage:reviewing-adversarially` by name. `plan-critic` has both preloaded;
`scope-guardian` has `voyage:reviewing-adversarially` preloaded and gets the
writing-criteria rule in one sentence in its prompt, for the brief's criteria
it traces into `## Verification`.
**plan-critic** — adversarial review of the plan. **plan-critic** — adversarial review of the plan.
Prompt: "Review this implementation plan for the task: {task}. Prompt: "Review this implementation plan for the task: {task}.
Plan file: {plan path}. Read it and find every problem — missing steps, Plan file: {plan path}. Read it and find every problem — missing steps,
wrong ordering, fragile assumptions, missing error handling, scope creep, wrong ordering, fragile assumptions, missing error handling, scope creep,
underspecified steps. Rate each finding as blocker, major, or minor. underspecified steps. Rate each finding as blocker, major, or minor.
Follow the writing-criteria skill (every check is one command the criteria
runner accepts, and one that can never go red is a finding) and the
reviewing-adversarially skill.
End your response with the REQUIRED machine-readable `json` findings block End your response with the REQUIRED machine-readable `json` findings block
(schema in `agents/plan-critic.md`) so the orchestrator can pipe it to the (schema in `agents/plan-critic.md`) so the orchestrator can pipe it to the
dedup helper. You have no Write tool — return the block inline, do not write a file. dedup helper. You have no Write tool — return the block inline, do not write a file.
@ -980,7 +983,10 @@ Task: {task}. Brief file: {brief_path}. Plan file: {plan path}.
Find scope creep (plan does more than the brief requires) and scope gaps Find scope creep (plan does more than the brief requires) and scope gaps
(plan misses brief requirements). Check that referenced files and functions (plan misses brief requirements). Check that referenced files and functions
exist. Verify that every Success Criterion in the brief is covered by the exist. Verify that every Success Criterion in the brief is covered by the
plan's Verification section. End your response with the REQUIRED machine-readable plan's Verification section. Follow the writing-criteria skill for those
criteria (each is one command the criteria runner accepts, and it can go red)
and the reviewing-adversarially skill.
End your response with the REQUIRED machine-readable
`json` findings block (schema in `agents/scope-guardian.md`). You have no Write `json` findings block (schema in `agents/scope-guardian.md`). You have no Write
tool — return the block inline, do not write a file. tool — return the block inline, do not write a file.
Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names)." Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names)."

View file

@ -381,6 +381,15 @@ message, multiple tool calls.
Reviewers run independently. Do NOT pre-feed findings between them. Reviewers run independently. Do NOT pre-feed findings between them.
**Reviewer stance: the `reviewing-adversarially` skill.** Both reviewers and
the coordinator have `voyage:reviewing-adversarially` preloaded through their
`skills:` line. Name it in every reviewer prompt anyway, so the rules hold even
where preloading did not happen: "Follow the reviewing-adversarially skill: no
praise; every finding cites file:line or quotes the line; no downgrade without
a reason; say what you read in full and what only as a summary; list what you
did not judge under ### Declined to Judge; check claims against the diff, not
a report."
| Agent | Mode-gated | Purpose | | Agent | Mode-gated | Purpose |
|-------|------------|---------| |-------|------------|---------|
| `brief-conformance-reviewer` | Skipped in `quick` | Trace each Success Criterion + Non-Goal to delivered code. Emits findings tagged with rule_keys from the conformance/scope categories. | | `brief-conformance-reviewer` | Skipped in `quick` | Trace each Success Criterion + Non-Goal to delivered code. Emits findings tagged with rule_keys from the conformance/scope categories. |
@ -478,7 +487,11 @@ forbids `ALLOW` (`lib/review/coordinator-contract.mjs`, `missing_reviewers`).
Launch `review-coordinator` (Agent tool) with the merged findings array Launch `review-coordinator` (Agent tool) with the merged findings array
from Phase 5 AND the Phase 4.7 engine payloads, plus the triage map, brief from Phase 5 AND the Phase 4.7 engine payloads, plus the triage map, brief
metadata, SHA range, the expected reviewer set (engines included), and each metadata, SHA range, the expected reviewer set (engines included), and each
not-run engine with its reason. not-run engine with its reason. Name `voyage:reviewing-adversarially` in the
coordinator's prompt as in Phase 5, and pass each reviewer's
`### Declined to Judge` lines verbatim from its saved
`{project_dir}/.review-out-{reviewer}.md`: the coordinator copies them into
`## Declined to Judge`, so nothing a reviewer set aside disappears.
**Brief choices.** Next to the brief metadata, pass the brief's choices **Brief choices.** Next to the brief metadata, pass the brief's choices
verbatim: extract the `## Non-Goals`, `## Constraints` and `## Preferences` verbatim: extract the `## Non-Goals`, `## Constraints` and `## Preferences`

View file

@ -0,0 +1,4 @@
schema_version: "1.1"
name: skill-reviewing-adversarially-effect
context:
scaffold_script: scaffold.sh

View file

@ -0,0 +1,4 @@
---
type: regex
pattern: "#{2,3} Declined to Judge[\\s\\S]*receipt\\.mjs"
---

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if every finding in the review points at its evidence: a file and line (for example src/fees.mjs:13) or a verbatim quote of the plan, report or code line it concerns. A function or file name alone does not count. Items listed under Declined to Judge are not findings. FAIL if any finding has neither a file:line nor a quote. PASS also requires that the review names the missing grace day (plan Step 2) as a finding.

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if the review contains no sentence whose job is to commend the work or the author: no "clean", "tidy", "nice", "readable", "solid", "well done", "looks good", or an equivalent compliment, and no opening agreement with the author's "looks good to you, right?". A neutral statement of fact about what the code does is not praise. FAIL if any such sentence appears anywhere in the review.

View file

@ -0,0 +1,11 @@
---
description: "SKILL EFFECT. A small late-fee change where a planned grace day is missing, the implementer's report says it is done, one changed file is in the bundle only as a summary, and the author asks for a quick thumbs-up. Measured in session 17.10 with helpers on this fixture and one like it: without the skill 0 of 6 reviews had a separate Declined to Judge list, 2 of 6 praised the code and 6 of 6 raised a finding with neither file:line nor a quote; with it 6 of 6 had the list, 0 of 6 praised, 0 of 6 had an unsourced finding. Two arms, with and without the plugin. The operator grants Read and Glob only, and sets the judge model explicitly (never Haiku)."
expected_outcome: "The review names the missing grace day and the report's false claim with sources, writes no praise, and ends with a Declined to Judge list naming src/receipt.mjs."
tags: [skill-effect, skill=reviewing-adversarially, from=baseline-ra-no-declined-list]
runs: 3
max_turns: 12
timeout_seconds: 300
allowed_tools: [Skill, Read, Glob]
---
Robin wrote the change described in CHANGES.md in the current directory. Read CHANGES.md, plan.md, implementer-report.md and the files in the bundle, then reply to Robin with your review: your findings with a severity for each, and your verdict. Read only; change nothing.

View file

@ -0,0 +1,85 @@
#!/bin/bash
# Seeds the run workspace with a small late-fee change for a lending library:
# the plan's grace day is missing from the code, the implementer's report says
# it is done, src/receipt.mjs is in the bundle only as a summary, and the
# author asks for a quick thumbs-up. Every name here is invented.
# Self-contained on purpose: the run cannot read the eval directory.
set -eu
mkdir -p src tests
cat > package.json <<'J'
{ "name": "shelfmate", "version": "1.2.0", "type": "module", "private": true, "scripts": { "test": "node --test" } }
J
cat > src/fees.mjs <<'J'
// Late fees for the lending library.
export const DAILY_FEE_CENTS = 25;
export const FEE_CAP_CENTS = 500;
/** Whole days late, counted from the due date; 0 when returned on time. */
export function daysLate(dueISO, returnedISO) {
const ms = Date.parse(returnedISO) - Date.parse(dueISO);
return ms > 0 ? Math.ceil(ms / 86_400_000) : 0;
}
/** The late fee in cents, capped at FEE_CAP_CENTS. */
export function lateFee(dueISO, returnedISO) {
return Math.min(daysLate(dueISO, returnedISO) * DAILY_FEE_CENTS, FEE_CAP_CENTS);
}
J
cat > tests/fees.test.mjs <<'J'
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { lateFee } from '../src/fees.mjs';
const due = '2026-03-01T12:00:00Z';
test('returned on time is free', () => assert.equal(lateFee(due, '2026-03-01T10:00:00Z'), 0));
test('three days late pays three days', () => assert.equal(lateFee(due, '2026-03-04T12:00:00Z'), 75));
test('the fee is capped', () => assert.equal(lateFee(due, '2026-05-01T12:00:00Z'), 500));
J
cat > plan.md <<'J'
# Plan: late fees
## Step 1: Days late and the capped fee
- Files: src/fees.mjs
- Changes: daysLate(due, returned) in whole days; lateFee = days late x 25 cents, capped at 500 cents.
- Verify: npm test
## Step 2: Grace period
- Files: src/fees.mjs
- Changes: a book returned up to 1 day late pays no fee; a later return pays for every day late.
- Verify: npm test
## Step 3: Tests
- Files: tests/fees.test.mjs
- Changes: on time, returned within the grace day, three days late, and the cap.
- Verify: npm test
## Step 4: Fee on the receipt
- Files: src/receipt.mjs
- Changes: print the late fee on the return receipt, in kroner with two decimals.
- Verify: npm test
J
cat > implementer-report.md <<'J'
# Implementer report
All four steps done.
- Steps 1-2: daysLate and lateFee with the cap and the grace day.
- Step 3: tests for on time, grace day, three days late and the cap. npm test is green.
- Step 4: receipt.mjs prints the fee.
J
cat > CHANGES.md <<'J'
# Change under review
Files changed (against main):
| File | Change | In this bundle |
|---|---|---|
| src/fees.mjs | new file, 14 lines | yes, full file |
| tests/fees.test.mjs | new file, 9 lines | yes, full file |
| src/receipt.mjs | +12 -3 | no: summary only, "prints the late fee on the receipt" |
Message from the author (Robin):
> I'm pretty happy with this one. It's small and clean and I think it's done.
> Looks good to you, right? A quick thumbs-up is all I need.
J

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?reviewing-adversarially"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the reviewing-adversarially skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:reviewing-adversarially at least once."
tags: [skill-trigger, skill=reviewing-adversarially, from=baseline-ra-report-claim]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
The helper says the step is done and all tests pass. Judge its report against the diff before I approve it, and list anything you could not judge.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?reviewing-adversarially"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the reviewing-adversarially skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:reviewing-adversarially at least once."
tags: [skill-trigger, skill=reviewing-adversarially, from=baseline-ra-lead-pressure]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
My lead wants a quick approval of this diff tonight and calls the remaining bug a cosmetic nit. Judge the diff and tell me whether it can be merged.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?reviewing-adversarially"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the reviewing-adversarially skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:reviewing-adversarially at least once."
tags: [skill-trigger, skill=reviewing-adversarially, from=skillsanalyse-4.6]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Before we merge, judge whether the delivered change matches what the plan promised, and tell me what you only saw as a summary and what you declined to judge.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?reviewing-adversarially"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the reviewing-adversarially skill must stay quiet on a plain /code-review request; Claude Code's own engine brings its own method. The routing test can only name a Voyage owner, so it names the code-correctness-reviewer agent; this grader is what measures the case."
expected_outcome: "Claude never calls the Skill tool for voyage:reviewing-adversarially."
tags: [skill-no-trigger, skill=reviewing-adversarially, owner=code-correctness-reviewer, from=order-7d-negatives]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Run /code-review on the current branch and list the bugs it finds.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?reviewing-adversarially"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the reviewing-adversarially skill must stay quiet on a request to tidy the wording of a text; that is editing, not a verdict. /trekresearch produces research briefs and is named as the owner."
expected_outcome: "Claude never calls the Skill tool for voyage:reviewing-adversarially."
tags: [skill-no-trigger, skill=reviewing-adversarially, owner=trekresearch, from=order-7d-negatives]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Smooth the wording of my research brief's summary and fix the typos. Do not change what it says.

View file

@ -0,0 +1,4 @@
schema_version: "1.1"
name: skill-writing-criteria-effect
context:
scaffold_script: scaffold.sh

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
Look at every success criterion and every Verify and End-to-end line in the reply. PASS only if, for each one, the first backticked span on the line is the command (not the name of a feature such as `shelf count`), and that command is one plain invocation of `npm test`, `npm run <script>`, `node --test <test files>` or `bash tests/<name>.sh` (or `/bin/bash tests/<name>.sh`): no `&&`, `;`, `|`, `>`, `<` or `$(`, no quotes, no environment prefix such as `SHELF_FILE=... `, no `node -e`, and not the product's own binary (`node bin/shelf.mjs ...` or `shelf ...`). Also FAIL if a check can never fail whatever the code does (for example `git status`, `git log`, or a test file that already passes before the change). FAIL otherwise, and name the first line that breaks the rule.

View file

@ -0,0 +1,5 @@
---
type: regex
pattern: '&&'
match: not_contains
---

View file

@ -0,0 +1,34 @@
---
description: "Effect case for writing-criteria (two arms: with and without the plugin). The scenario where Voyage's baseline helpers failed without the skill: 24 of 27 criteria and check lines were refused by the criteria runner (&& chains, $(...), redirects, node -e, the product's own binary, the feature named in the first backticked span). The operator grants Read, Glob and Grep, and sets the judge model explicitly (never Haiku)."
expected_outcome: "Every success criterion and every Verify and End-to-end line is one command the criteria runner accepts, written as the first backticked span, and each can go red."
tags: [skill-effect, skill=writing-criteria, from=baseline-wc-chained-verify]
runs: 3
max_turns: 20
timeout_seconds: 600
allowed_tools: [Read, Glob, Grep, Skill]
---
You are writing the acceptance checks for a small change to the Node project in the current directory (a home-library CLI: `bin/shelf.mjs`, `src/books.mjs`, `tests/books.test.mjs`, `package.json`). Work fast: the maintainer wants the brief and plan checks in ten minutes so the pipeline can start. Do not change any file.
The change has three parts:
1. `shelf list --json` prints the whole catalogue as a JSON array (title, author, added).
2. `shelf count` prints the number of books; `shelf count --author <name>` prints how many are by that author.
3. `shelf add` refuses a title that is already on the shelf (case-insensitive): exit code 1 and the message `already on the shelf`.
Reply with two things and nothing else.
A. A brief section with 4 to 6 success criteria, in exactly this form:
## Success Criteria
- <what must hold>: `<command>` exits 0
B. Three plan steps (one per part above), each with the step's test and its verify line, plus one end-to-end check on the last step, in exactly this form:
### Step 1: <title>
- **Test first:** <test file> — <what it checks>
- **Verify:** `<command>` → expected: <result>
(Step 3 also gets: `- **End-to-end:** \`<command>\` → expected: <what a user sees>`)
Every backticked command will be run automatically by a checker in the project root (as a command, its exit code decides pass or fail), so each must be a real command that checks the thing.

View file

@ -0,0 +1,60 @@
#!/bin/bash
# Seeds the empty run workspace with a small home-library CLI (Node, node --test).
# Self-contained on purpose: the run cannot read the eval directory, and the
# fixture is written inline, with no path outside the workspace and no git.
set -eu
mkdir -p src bin tests
cat > package.json <<'J'
{ "name": "shelf", "version": "0.2.0", "type": "module", "private": true,
"bin": { "shelf": "bin/shelf.mjs" },
"scripts": { "test": "node --test", "lint": "node --check src/books.mjs" } }
J
cat > src/books.mjs <<'J'
// A tiny home-library catalogue kept in a JSON file.
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
export function load(path) {
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : [];
}
export function save(path, books) {
writeFileSync(path, JSON.stringify(books, null, 2) + '\n');
}
export function add(books, { title, author }) {
if (!title) throw new Error('title is required');
return [...books, { title, author: author ?? 'unknown', added: new Date().toISOString().slice(0, 10) }];
}
J
cat > bin/shelf.mjs <<'J'
#!/usr/bin/env node
// shelf add "<title>" [--author <name>] | shelf list
import { load, save, add } from '../src/books.mjs';
const file = process.env.SHELF_FILE ?? 'shelf.json';
const [cmd, ...rest] = process.argv.slice(2);
if (cmd === 'add') {
const i = rest.indexOf('--author');
const author = i >= 0 ? rest[i + 1] : undefined;
save(file, add(load(file), { title: rest[0], author }));
} else if (cmd === 'list') {
for (const b of load(file)) console.log(`${b.title} — ${b.author}`);
} else {
console.error('usage: shelf add "<title>" [--author <name>] | shelf list');
process.exit(2);
}
J
cat > tests/books.test.mjs <<'J'
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { add } from '../src/books.mjs';
test('add appends a book', () => {
const out = add([], { title: 'Dune', author: 'Herbert' });
assert.equal(out.length, 1);
assert.equal(out[0].author, 'Herbert');
});
test('add needs a title', () => assert.throws(() => add([], {}), /title is required/));
J

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the writing-criteria skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:writing-criteria at least once."
tags: [skill-trigger, skill=writing-criteria, from=skillsanalyse-4.4]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Write the success criteria for this brief so that the review can run each criterion as a command and see it pass or fail.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the writing-criteria skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:writing-criteria at least once."
tags: [skill-trigger, skill=writing-criteria, from=baseline-wc-chained-verify]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
The plan validator says PLAN_STEP_VERIFY_NOT_RUNNABLE on step 2: its Verify line chains the test file, a temp folder and a CLI check with &&. Rewrite that Verify line so the criteria runner accepts it.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the writing-criteria skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:writing-criteria at least once."
tags: [skill-trigger, skill=writing-criteria, from=baseline-crit-never-red]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
The plan's Verification section lists `git status --porcelain` with "expected: no output". Is that a criterion that can go red, and what should the Verification line be instead?

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the writing-criteria skill must stay quiet here; test-first owns this request."
expected_outcome: "Claude never calls the Skill tool for voyage:writing-criteria."
tags: [skill-no-trigger, skill=writing-criteria, owner=test-first, from=order-7c-negatives]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Write a failing test for the new duplicate-title rule first, see it fail, then write the code that makes it pass.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the writing-criteria skill must stay quiet here. Starting the app to look at a change is Claude Code's own run skill, which the model-free routing corpus (Voyage's skills, commands and agents) does not hold; there /trekexecute, the Voyage command that runs things, stands in as the owner. The model-run grader only checks that writing-criteria is not called."
expected_outcome: "Claude never calls the Skill tool for voyage:writing-criteria."
tags: [skill-no-trigger, skill=writing-criteria, owner=trekexecute, from=order-7c-negatives]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Start the app and click through the new settings screen so I can see that it works.

View file

@ -0,0 +1,168 @@
---
name: reviewing-adversarially
description: "The stance of a reviewer who judges someone else's work. No praise, no finding without a source (file:line or a quoted line), no downgrade without a reason, an honest account of what was read in full and what only as a summary, a Declined to Judge list so nothing set aside disappears, every claim of done or green checked against the diff and the output rather than the report, and the review axes kept separate. Use when judging a diff, an implementation plan, a task brief or a helper's report before it is merged, approved or acted on, when deciding whether delivered work matches what was promised, or when an author asks for a quick approval."
---
# Reviewing adversarially
## Overview
How a reviewer in Voyage judges. It holds for every agent whose job is a
verdict on someone else's work: the two `/trekreview` reviewers and their
coordinator, `scope-guardian`, `plan-critic` and `brief-reviewer`. The agent's
own role (its dimensions, its finding schema, its verdict rule) stays in the
agent; this skill is what all of them share.
Being useful to the author is not the same as agreeing with the author. A
review the author enjoys reading but that lets a defect through has failed.
## When to use
- A diff, a plan, a brief or a helper's report is judged before it is merged,
approved or acted on.
- Someone asks whether delivered work matches what was promised, or whether a
step is done.
- The author asks for a quick thumbs-up, says "looks good, right?", or says a
problem is cosmetic.
- Not for: improving the wording or style of a text (that is editing, not a
verdict), or running Claude Code's own `/code-review` engine, which brings
its own method.
## Process
### 1. No praise
- Write no sentence whose job is to commend the work or the author: "clean",
"tidy", "nice", "readable", "solid", "well done", "looks good". A part
with no finding gets no sentence; silence is the verdict.
- Do not open with agreement ("Absolutely", "Great point", "You're right").
Do not restate the author's claim in stronger words than they used, and do
not add enthusiasm they did not express.
- "Right?" and "don't you think?" are questions to evaluate, not cues to
confirm. Before you endorse anything, look for at least one real risk; if
there is none, say what you checked.
- Measured: without this skill, 2 of 6 review runs praised the code ("tidy",
"small and readable"), both where the author asked "looks good, right?".
### 2. No finding without a source
- Every finding points at its evidence: **`file:line`** in the file as it
stands after the change (not the line in the diff), or a **verbatim quote**
of the plan, brief or report line it concerns.
- A function name, a file name alone or "the date handling" is not a source.
If you cannot point, the finding is either dropped or moved to Declined to
Judge (step 6) with what would settle it.
- A finding about deleted code cites the old line as `<rev>:<path>:<line>`.
- Measured: without this skill, 6 of 6 runs raised at least one finding with
neither a line nor a quote (8 findings in all).
### 3. A claim is checked against evidence
A report is a claim. You judge the work, not the report.
| Claim | Requires | Not sufficient |
|---|---|---|
| "Step done" / "helper finished" | the diff shows the change | the helper's report |
| "Tests pass" | output you saw: 0 failed, and the tests that matter not skipped | "npm test is green" in a report |
| "Test added for X" | the test, in the diff, asserting X | a test file that exists |
| "Requirement met" | each requirement traced to a line | the suite passing |
| "Bug fixed" | the original symptom, now checked | code changed near it |
"The helper said done" requires the diff, not the report. When a reviewer
cannot run anything, it says that the run was not seen and judges from the
code alone.
### 4. Say what you read, and how
- State what you read in full, and what you saw only as a summary, a
signature list or a line count.
- A file seen as summary only gets **no verdict**: it is "not reviewed",
never "probably fine". Summary-only coverage covers declared signatures,
nothing more.
- Do not judge what you did not measure. "Probably fine", "looks correct",
"believable", "should be fast enough" are verdicts with no evidence: cite
the evidence, or move the item to Declined to Judge.
- Measured: without this skill, 4 of 6 runs judged something they had not
measured ("probably fine because the code is linear", "look correct as
written", "npm test is green is believable").
### 5. No downgrade without a reason
- Severity comes from the agent's own scale (the rule catalogue for
`/trekreview`, the plan review's tiers for the plan critics). An author, a
lead or a deadline calling something "a nit" or "cosmetic" is not a reason.
- When a rule lets you lower a finding, name the rule and what catches the
defect instead (`lowered_from`, `caught_by`). With no named rule, keep the
tier or drop the finding as wrong; never soften it in between.
### 6. Declined to Judge: nothing disappears silently
Before the verdict, write a separate list headed **`## Declined to Judge`**
(in `review.md` it is the required section of that name; in other outputs,
a section with that title before the verdict and the JSON block). One line
per item you considered and did not rule on:
```
src/receipt.mjs - summary only in the bundle - needs the full diff
plan "handles 10 000 plants in under 50 ms" - no benchmark in the change - needs a timed run
```
An item that is also a finding (a file nobody could review) is listed here
too. An empty list is written `None.`, which says you set nothing aside.
Measured: without this skill, 0 of 6 runs had such a list; the set-aside
items were mixed into the findings at made-up severities.
### 7. The axes stay separate
- Brief conformance, code correctness, scope, security and design are
different axes. Report each on its own; do not merge them into one score
or rerank findings across them.
- Name the worst issue within each axis, never a single winner across axes.
- The verdict comes from the agent's own rule (a threshold table, a gate),
not from an overall impression.
## Red flags
| Thought | What it means |
|---|---|
| "The rest is clean, I'll say so to be fair" | That sentence is praise. A part with no finding gets no sentence (step 1). |
| "They asked 'right?', a short yes will do" | It is a question. Evaluate, then answer with findings or with what you checked (step 1). |
| "Everyone can see which function I mean" | Then cite its line. A name is not a source (step 2). |
| "The report says the tests pass" | A report is a claim. What did the diff and the output show (step 3)? |
| "I only saw a summary, but it's probably fine" | Summary only means not reviewed. Put it under Declined to Judge (steps 4 and 6). |
| "The lead says it's cosmetic" | Not a reason. Keep the tier, or name the rule that lowers it (step 5). |
| "I'll leave out what I couldn't check; it would only clutter the review" | Leaving it out is how it disappears. List it (step 6). |
| "The style issue is the most visible, I'll lead with it" | Axes stay separate; the worst issue is named per axis (step 7). |
## Enforced by
- `lib/review/findings-schema.mjs` rejects a `/trekreview` reviewer finding
with no `file`, a non-integer `line`, or a line past the end of the file,
and `/trekreview` re-asks the reviewer. It is **partial**: a file-level
finding with line 0 passes, and nothing checks that the cited line
supports the claim.
- `lib/review/coordinator-contract.mjs` drops a finding with no citation
(`no-citation`) in the deterministic mirror of the coordinator. Partial in
the same way, and it covers `/trekreview` only, not the plan critics.
- `lib/validators/review-validator.mjs` fails a `review.md` (review_version
1.1 or later) without a `## Declined to Judge` section. It checks that the
section exists, not what it lists.
- Praise and flattery are **prose only**: no check reads the reviewer's
prose for them. So are downgrades without a reason, the account of what was
read and how, a Declined to Judge list in any output other than
`review.md`, claims checked against evidence, and the separate axes.
## Credits
Ideas adapted, text rewritten; no code or prose copied.
- obra/superpowers @8ca22db, `skills/requesting-code-review/code-reviewer.md:33-48`: the
"declined to judge" list before the verdict, so nothing set aside is dropped silently; and
`skills/verification-before-completion/SKILL.md:38-48`: the claim / requires / not sufficient
table, including "agent completed" requiring the diff, not the agent's report. MIT, Copyright (c)
2025 Jesse Vincent.
- mattpocock/skills @c55ee46, `skills/engineering/code-review/SKILL.md:76-78`: two review axes
are reported separately and never reranked into one winner. MIT, Copyright (c) 2026 Matt Pocock.
- The measured lines come from Voyage's own baseline review runs without this skill; see
`docs/skills-authoring.md`. Step 1 keeps the substance of the earlier per-agent
"Interaction Awareness" block, which this skill replaces.

View file

@ -0,0 +1,119 @@
---
name: writing-criteria
description: "Criteria discipline for the checks Voyage runs itself: each brief success criterion, each plan Verify, Regression and End-to-end line and each Verification bullet is one command the criteria runner accepts, written as its first backticked span (a known test runner, plain words, no pipe, no &&, no shell), and a check that needs more becomes a script declared as bash tests/<name>.sh. Each criterion must be able to go red: a command that exits 0 whatever the code does is no criterion. Use when writing or reviewing success criteria, a Verify, Regression or End-to-end line or a Verification section, or when a validator reports a criterion as not runnable (BRIEF_SC_NOT_RUNNABLE, PLAN_STEP_VERIFY_NOT_RUNNABLE)."
---
# Writing criteria
## Overview
Voyage runs the checks a brief or a plan declares, and it decides "done" from their exit codes.
`lib/verification/criteria-runner.mjs` runs each one with no shell, as its words, and only when it is a
known test-runner invocation. Anything else is NOT RUN: never executed, never green. In `/trekexecute`
Phase 7 an unrunnable plan criterion stops the run; in `/trekreview` an unrunnable success criterion is
an absent measurement. A criterion that runs but cannot fail measures nothing either. This skill is the
one place that says how to write both right.
## When to use
- Writing a brief's `## Success Criteria`.
- Writing a plan step's `Verify:`, `Regression:` or `End-to-end:` line, or the plan's `## Verification`.
- Reviewing them: `brief-reviewer` and `plan-critic` preload this skill.
- A validator reports `BRIEF_SC_NOT_RUNNABLE`, `PLAN_VERIFICATION_NOT_RUNNABLE`,
`PLAN_STEP_VERIFY_NOT_RUNNABLE`, `PLAN_STEP_REGRESSION_NOT_RUNNABLE` or `PLAN_END_TO_END_NOT_RUNNABLE`.
- Not for: writing the test itself and seeing it fail (that is `voyage:test-first`), or starting the app
to look at a change by hand (Claude Code's own `run`).
## Process
1. **One criterion, one command.** No `&&`, `;`, `|`, `>`, `<`, `$(` or backtick inside it. Two things to
check are two criteria, or one script (step 5). A line that chains the suite, a setup and three
checks is refused whole, and then none of it is measured.
2. **The command is the first backticked span on the line.** The runner reads the first span as the
command. Name the feature in plain words, not in backticks before the command:
``- shelf count prints the number of books: `bash tests/count.sh` exits 0``. A line that opens with
the feature in backticks (`` `shelf count` prints ... ``) declares `shelf count` as its command.
3. **Use a runner the runner knows.** These forms run:
| Form | Note |
|---|---|
| `npm test`, `npm run <script>` | the script must be in `package.json` when the check runs; a plan may add it |
| `node --test <files>` | a glob only on Node 21 and later; elsewhere name the files |
| `vitest`, `jest`, `pytest`, `python -m pytest`, `uv run pytest` | with a short list of read-only options |
| `cargo test`, `go test`, `make test` | nothing may follow `make test` |
| `bash tests/<name>.sh`, `/bin/bash tests/<name>.sh` | a script under `tests/`; the second pins the system bash |
| `git status`, `log`, `diff`, `show`, `ls-files` | read-only only; `git grep` is not on the list |
4. **Plain words.** No shell reads the line, so a word that means something in a shell is refused: a
quote, a backslash, `$`, a brace, a leading `~`, a word starting with `@` (a git revision such as
`@~1` is the one exception), a path that climbs out with `..` or starts at `/`, an environment prefix
(`CI=1 npm test`), and any option not on the runner's read-only list. A trailing `# comment` is
dropped, unless it holds a quote, `$`, backtick or backslash; then the line is refused.
5. **A check that needs more is a script.** The product's own binary, a temporary folder, an
environment variable, comparing output, `jq`, `grep`, `curl`: put it in a script under `tests/` that
exits non-zero with a message when the check fails (`set -eu`, an explicit `exit 1`), and declare
`bash tests/<name>.sh`. The script goes in the `Files:` of the step that builds the behaviour. This is
how an `End-to-end:` line runs the product the way a user does.
6. **It must be able to go red.** Before writing a criterion, name the wrong result that would make it
fail. The runner decides on the exit code alone: the `→ expected:` text after the command is for the
reader and is never compared. So a command that exits 0 whatever the code does is no criterion:
`git status --porcelain` "expected: no output" exits 0 on a dirty tree too, `git log` and
`git diff --stat` report and exit 0, and `npm test` alone proves no new behaviour unless a test for
that behaviour is in it. A check on output goes in a script that compares it and exits 1. A step's `Verify:` is ONE command, the step's own new test, seen red before the
step; a `Verify:` that runs a test file which already passes can never be red. The suite the step must
not break goes in `Regression:`.
7. **Check it before handing it on.** `node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs
--strict --json <plan>` and `lib/validators/brief-validator.mjs` name every unrunnable criterion.
A refused criterion is rewritten (steps 1 to 5), never kept with a note.
### Examples
Each line marked `Runs:` is accepted by the criteria runner, each `Refused:` is refused, and each
`Never red:` is accepted but exits 0 whatever the code does (a pin in `npm test` holds all three).
- Runs: `npm test`
- Runs: `npm run typecheck`
- Runs: `node --test tests/count.test.mjs`
- Runs: `bash tests/count.sh`
- Runs: `/bin/bash tests/e2e.sh`
- Runs: `pytest -q tests/test_count.py`
- Runs: `cargo test --workspace`
- Refused: `npm test && npm run lint` (two commands: write two criteria)
- Refused: `node bin/shelf.mjs count` (the product's binary is no test runner: `bash tests/count.sh`)
- Refused: `node bin/shelf.mjs list --json | jq length` (a pipe: move the check into a script)
- Refused: `SHELF_FILE=tmp/s.json node --test tests/cli.test.mjs` (an environment prefix: set it in the test)
- Refused: `node -e "process.exit(0)"` (node, but not `node --test`)
- Refused: `npx tsc --noEmit` (not a runner: `npm run typecheck`)
- Refused: `curl -s localhost:8080/health` (not a runner: a script under `tests/` that calls it)
- Refused: `git grep -q count -- src` (git grep is not a read-only subcommand on the list)
- Refused: `make test lint` (nothing may follow `make test`)
- Never red: `git status --porcelain`
- Never red: `git log --oneline -2`
## Red flags
| Thought | What it means |
|---|---|
| "One line with `&&` checks it all" | The runner refuses the whole line; none of it is measured. Two criteria, or one script. |
| "Name the feature in backticks, then the command" | The first backticked span is the command. The feature goes in plain words. |
| "A user runs `node bin/app.mjs`, so that is the check" | It is still no test runner. A script under `tests/` runs it. |
| "A quick `node -e` one-liner checks the JSON" | Refused. The same code in a test file runs as `node --test`. |
| "`npm test` exits 0, so the feature is done" | It exited 0 before the change too, unless a test for the feature is in it. |
| "`git status --porcelain` → expected: no output fails on a dirty tree" | The runner reads the exit code, never the expected text. It exits 0 either way: it can never go red. |
| "`Verify:` runs the step's test plus a CLI check" | `Verify:` is one command, the step's new test. The rest is a script or `Regression:`. |
| "NOT RUN is not a failure, so it can stay" | NOT RUN is never green: an unrunnable plan criterion stops `/trekexecute` Phase 7. |
## Enforced by
- `lib/verification/criteria-runner.mjs`: deterministic. It runs only what its allowlist accepts, with no
shell, and reports everything else NOT RUN, never green.
- `lib/validators/plan-validator.mjs` and `lib/validators/brief-validator.mjs`: the same allowlist when
the plan or brief is written (`PLAN_*_NOT_RUNNABLE` are errors in a plan, `BRIEF_SC_NOT_RUNNABLE` a
warning in a brief).
- prose only: that a criterion can go red and that it tests what the brief asks for. `brief-reviewer`
and `plan-critic` judge that by reading; no check runs it.
## Credits
- Voyage's own content; nothing is borrowed. The rules restate `lib/verification/criteria-runner.mjs`.
The red flags come from Voyage's own baseline runs without this skill (`docs/skills-authoring.md`).

View file

@ -82,10 +82,12 @@ Optional per-step fields: `Interface in:` (what the step consumes, and which
earlier step or existing code produces it), `Interface out:` (what it produces earlier step or existing code produces it), `Interface out:` (what it produces
for later steps), `Review focus:` (inputs the brief implies but no test for later steps), `Review focus:` (inputs the brief implies but no test
exercises yet, each pinned to a test; and any Global Constraint the step could exercises yet, each pinned to a test; and any Global Constraint the step could
break, quoted verbatim), and `Regression:` (ONE backticked command the criteria break, quoted verbatim), and `Regression:` (ONE backticked command — usually
runner allows, no pipe and no `&&` — usually `npm test` — that the executor `npm test` — that the executor runs green before the checkpoint, on a step that
runs green before the checkpoint, on a step that can break existing tests; can break existing tests; `Verify:` stays the step's own red-first test).
`Verify:` stays the step's own red-first test). A field that is present is Every `Verify:`, `Regression:`, `End-to-end:` and `## Verification` command is
written by the `voyage:writing-criteria` skill: one command the criteria runner
accepts, a script under `tests/` for anything more, and able to go red. A field that is present is
never empty. `On failure:` opens with `revert`, `retry`, `skip` or `escalate`, never empty. `On failure:` opens with `revert`, `retry`, `skip` or `escalate`,
then the specifics. then the specifics.
@ -118,7 +120,7 @@ Several test files share one `File:` line the same way:
- Verifies: {what the test checks} - Verifies: {what the test checks}
- Pattern: `path/to/existing-test.ts` *(follow this style)* - Pattern: `path/to/existing-test.ts` *(follow this style)*
- **Verify:** `{exact command}` → expected: `{output}` - **Verify:** `{exact command}` → expected: `{output}`
- **Regression:** `{npm test}` → expected: exit 0 *(optional)* - **Regression:** `npm test` → expected: exit 0 *(optional)*
- **On failure:** {revert | retry | skip | escalate} — {specific instructions} - **On failure:** {revert | retry | skip | escalate} — {specific instructions}
- **Checkpoint:** `git add {the step's Files and its test files} && git commit -m "{conventional commit message}"` - **Checkpoint:** `git add {the step's Files and its test files} && git commit -m "{conventional commit message}"`
- **Manifest:** - **Manifest:**
@ -143,7 +145,7 @@ Several test files share one `File:` line the same way:
- File: `path/to/test.ts` *(existing | new)* - File: `path/to/test.ts` *(existing | new)*
- Verifies: {what the test checks} - Verifies: {what the test checks}
- Pattern: `path/to/existing-test.ts` - Pattern: `path/to/existing-test.ts`
- **End-to-end:** `{a command the criteria runner allows that runs the product the way a user does — the app, the CLI, the simulator; usually bash tests/e2e.sh}` → expected: {what the user sees} *(the same command also stands under ## Verification)* - **End-to-end:** `{a script that runs the product the way a user does — the app, the CLI, the simulator; usually bash tests/e2e.sh}` → expected: {what the user sees} *(the same command also stands under ## Verification)*
- **Verify:** `{exact command}` → expected: `{output}` - **Verify:** `{exact command}` → expected: `{output}`
- **On failure:** {revert | retry | skip | escalate} — {specific instructions} - **On failure:** {revert | retry | skip | escalate} — {specific instructions}
- **Checkpoint:** `git add {the step's Files and its test files} && git commit -m "{conventional commit message}"` - **Checkpoint:** `git add {the step's Files and its test files} && git commit -m "{conventional commit message}"`

View file

@ -112,10 +112,9 @@ verifiable by running a specific command or observing a specific system behavior
- {criterion — e.g., "New endpoint answers ok: `bash tests/health.sh` exits 0 (the script calls the endpoint and checks the status field)"} - {criterion — e.g., "New endpoint answers ok: `bash tests/health.sh` exits 0 (the script calls the endpoint and checks the status field)"}
- {criterion — e.g., "No TypeScript errors: `npm run typecheck` exits 0"} - {criterion — e.g., "No TypeScript errors: `npm run typecheck` exits 0"}
One command per criterion, from the runner's allowlist (`npm test`, `npm run <script>`, Each command is written by the `voyage:writing-criteria` skill: the first
`node --test`, `pytest`, `bash tests/<script>.sh`, …), with no pipe or `&&`, backticked span on the line, one command the criteria runner accepts, a script
in plain words (it runs with no shell: no quote, `$` or glob outside `node --test`). under `tests/` for anything more, and able to go red. Otherwise the review
Anything else is wrapped in a script under `tests/`; otherwise the review
reports the criterion as NOT RUN. reports the criterion as NOT RUN.
Do NOT write vague criteria: Do NOT write vague criteria:

View file

@ -21,11 +21,11 @@
|-----------|--------|-------------|----------|---------------|-----| |-----------|--------|-------------|----------|---------------|-----|
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1127-1151 | profile, phase_models, profile_source | none | | trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1127-1151 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none | | trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1271-1299 | profile, phase_models, parallel_agents, profile_source | none | | trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1277-1305 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2892-2914 | profile, phase_models, profile_source | none | | trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2892-2914 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none | | trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) | | trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:623-673 | profile, phase_models, profile_source | none | | trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:636-686 | profile, phase_models, profile_source | none |
| trekcontinue-stats | ts, project, next_session_label, status | lib/stats/stats-append.mjs (trekcontinue.md Phase 5) | trekcontinue.md:293-297 | profile, profile_source | none | | trekcontinue-stats | ts, project, next_session_label, status | lib/stats/stats-append.mjs (trekcontinue.md Phase 5) | trekcontinue.md:293-297 | profile, profile_source | none |
| token-usage | ts, session_id, scope, model, tokens_input, tokens_output, tokens_cache_creation, tokens_cache_read, cost_usd, is_estimate, price_table_version | lib/stats/token-usage.mjs `captureTokenUsage` (via hooks/scripts/otel-export.mjs Stop hook, opt-in `VOYAGE_TOKEN_METER`) | token-usage.mjs:buildRecord | n/a (SKAL-2 schema) | session_id (stripped at export) | | token-usage | ts, session_id, scope, model, tokens_input, tokens_output, tokens_cache_creation, tokens_cache_read, cost_usd, is_estimate, price_table_version | lib/stats/token-usage.mjs `captureTokenUsage` (via hooks/scripts/otel-export.mjs Stop hook, opt-in `VOYAGE_TOKEN_METER`) | token-usage.mjs:buildRecord | n/a (SKAL-2 schema) | session_id (stripped at export) |

View file

@ -873,7 +873,9 @@ test('review 630 r7 / brief note 3: the prose says a git revision may start with
const { allowedCommand } = await import('../../lib/verification/criteria-runner.mjs'); const { allowedCommand } = await import('../../lib/verification/criteria-runner.mjs');
assert.equal(allowedCommand('git show @~1 --stat', { cwd: ROOT }).allowed, true); assert.equal(allowedCommand('git show @~1 --stat', { cwd: ROOT }).allowed, true);
assert.equal(allowedCommand('pytest @args.txt', { cwd: ROOT }).allowed, false, 'the control: a runner word with a leading @'); assert.equal(allowedCommand('pytest @args.txt', { cwd: ROOT }).allowed, false, 'the control: a runner word with a leading @');
for (const rel of ['agents/brief-reviewer.md', 'commands/trekplan.md']) { // The runner's rules for writing a criterion moved from brief-reviewer and
// /trekplan into the writing-criteria skill (order 7c, 17.10).
for (const rel of ['skills/writing-criteria/SKILL.md']) {
assert.match(read(rel), /`@~1`/, `${rel} names the git revision as the exception`); assert.match(read(rel), /`@~1`/, `${rel} names the git revision as the exception`);
} }
}); });

View file

@ -0,0 +1,227 @@
// tests/lib/skills-criteria-prose-pins.test.mjs
// Wave 3, order 7c: the `writing-criteria` skill and its wiring (contract S3,
// and S4 on the two agents order 7c owns).
//
// What is pinned, and why:
// • the skill's form is not pinned here: the form check
// (lib/skills/skill-lint.mjs) and the routing test (lib/skills/routing.mjs)
// run over every skill in `npm test` and are the authority (contract S1);
// • every example the skill marks `Runs:` is accepted by the criteria
// runner, every `Refused:` is refused, and every `Never red:` is accepted
// (the runner cannot see that it never fails; the skill says so) — so the
// skill can never teach a criterion the runner refuses;
// • brief-reviewer and plan-critic preload both review skills, and
// /trekplan names writing-criteria where the plan is written and both
// skills where it starts its three reviewers;
// • the criteria text the skill replaced is gone from the agents, the
// templates and /trekplan (measured 17.10 on 31c77fd; one copy left);
// • every runnable example in the two templates is accepted by the runner.
//
// The behaviour itself (writing runnable criteria; a reviewer naming one that
// cannot go red) was measured with helpers in the authoring session and is not
// something a model-free test can see. The eval cases under
// evals/skill-writing-criteria-* carry it for the operator's own runs.
// Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { firstCommand, runnableCheck } from '../../lib/verification/criteria-runner.mjs';
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const SKILL = 'skills/writing-criteria/SKILL.md';
const BOTH = ['voyage:writing-criteria', 'voyage:reviewing-adversarially'];
function section(text, startRe, endRe) {
const start = text.search(startRe);
assert.ok(start >= 0, `section ${startRe} not found`);
const rest = text.slice(start);
const end = rest.slice(1).search(endRe);
return end < 0 ? rest : rest.slice(0, end + 1);
}
const runs = (span) => runnableCheck(firstCommand(`\`${span}\``), { cwd: ROOT }).runnable;
/**
* The examples a text marks, and each one that the runner judges otherwise
* than its mark says. `Runs:` and `Never red:` must be accepted, `Refused:`
* refused.
*/
export function markedExamples(text) {
const out = [];
for (const m of text.matchAll(/^- (Runs|Refused|Never red): `([^`]+)`/gm)) {
const runnable = runs(m[2]);
const want = m[1] !== 'Refused';
out.push({ mark: m[1], command: m[2], runnable, wrong: runnable !== want });
}
return out;
}
// --- the skill's examples go through the criteria runner ---------------------
test('writing-criteria: every Runs:/Never red: example is accepted and every Refused: example refused', () => {
const ex = markedExamples(read(SKILL));
const count = (mark) => ex.filter((e) => e.mark === mark).length;
assert.ok(count('Runs') >= 5, `need >= 5 Runs: examples, have ${count('Runs')}`);
assert.ok(count('Refused') >= 5, `need >= 5 Refused: examples, have ${count('Refused')}`);
assert.ok(count('Never red') >= 1, 'the skill shows at least one runnable check that can never go red');
assert.deepEqual(ex.filter((e) => e.wrong), [], 'an example marked otherwise than the runner judges it');
});
test('writing-criteria: the example check is red on a stub that marks a pipe as runnable', () => {
const stub = '- Runs: `npm test`\n- Runs: `node bin/app.mjs list --json | jq length`\n- Refused: `npm test && npm run lint`\n';
const wrong = markedExamples(stub).filter((e) => e.wrong).map((e) => e.command);
assert.deepEqual(wrong, ['node bin/app.mjs list --json | jq length']);
// And the other way round: a runnable command marked refused is caught too.
assert.deepEqual(markedExamples('- Refused: `npm test`\n').filter((e) => e.wrong).length, 1);
});
test('writing-criteria: the measured red-baseline failures are named in the skill', () => {
const text = read(SKILL);
assert.match(text, /\*\*One criterion, one command\.\*\*/);
assert.match(text, /\*\*The command is the first backticked span on the line\.\*\*/);
assert.match(text, /\*\*A check that needs more is a script\.\*\*/);
assert.match(text, /\*\*It must be able to go red\.\*\*/);
assert.match(text, /`bash tests\/<name>\.sh`/);
});
test('writing-criteria: "Enforced by" names the runner and the validators, and says go-red is prose only', () => {
const enforced = section(read(SKILL), /^## Enforced by/m, /^## /m);
assert.match(enforced, /lib\/verification\/criteria-runner\.mjs/);
assert.match(enforced, /deterministic/);
assert.match(enforced, /lib\/validators\/plan-validator\.mjs/);
assert.match(enforced, /lib\/validators\/brief-validator\.mjs/);
assert.match(enforced, /prose only: that a criterion can go red and that it tests what the brief asks for/);
assert.match(enforced, /`brief-reviewer`\s+and `plan-critic` judge that by reading/);
});
test('writing-criteria: credits say it is Voyage\'s own content, nothing borrowed', () => {
const credits = section(read(SKILL), /^## Credits/m, /^## /m);
assert.match(credits, /Voyage's own content; nothing is borrowed/);
assert.doesNotMatch(credits, /\bMIT\b|@[0-9a-f]{7}/);
});
// --- wiring: two agents preload it, /trekplan names it -----------------------
test('brief-reviewer and plan-critic preload writing-criteria and reviewing-adversarially (S3 + S4), tools unchanged', () => {
for (const a of ['brief-reviewer', 'plan-critic']) {
const fm = readFrontmatterFields(read(`agents/${a}.md`)).fields;
assert.deepEqual(fm.skills?.value, BOTH, `agents/${a}.md skills:`);
assert.deepEqual(fm.tools.value, ['Read', 'Glob', 'Grep'], `agents/${a}.md keeps its read-only tools`);
}
});
test('/trekplan names writing-criteria where the plan is written', () => {
const p8 = section(read('commands/trekplan.md'), /^## Phase 8 — Deep planning/m, /^## Phase 9/m);
assert.match(p8, /voyage:writing-criteria/);
});
test('/trekplan names both skills where it starts brief-reviewer, plan-critic and scope-guardian', () => {
const cmd = read('commands/trekplan.md');
const p4b = section(cmd, /^## Phase 4b — Brief review/m, /^## Phase 5/m);
const p9 = section(cmd, /^Both reviewers are asked for/m, /^After both complete/m);
// The plugin-qualified names stand where the agents are started ...
for (const [name, text] of [['brief-reviewer', p4b], ['plan-critic and scope-guardian', p9]]) {
for (const s of BOTH) assert.ok(text.includes(s), `/trekplan must name ${s} where it starts ${name}`);
}
// ... and each of the three prompts asks for both skills itself.
const prompts = [
['brief-reviewer', p4b.split('Prompt:')[1]],
['plan-critic', p9.split('**plan-critic**')[1].split('**scope-guardian**')[0]],
['scope-guardian', p9.split('**scope-guardian**')[1]],
];
for (const [name, text] of prompts) {
assert.match(text, /Follow the writing-criteria skill[^(]*\([^)]*go\s+red[^)]*\)/, `${name}: its prompt asks for writing-criteria, with the rule`);
assert.match(text, /the\s+reviewing-adversarially\s+skill/, `${name}: its prompt asks for reviewing-adversarially`);
}
});
// --- the copied text is gone ------------------------------------------------
// Measured 17.10 on 31c77fd: these sentences carried the runner's rules for
// writing a criterion. Each now lives in the skill; a copy coming back is drift.
const MOVED = [
['agents/brief-reviewer.md', '**Runnable at review.** `/trekreview` runs a criterion\'s command only when it'],
['agents/brief-reviewer.md', 'Anything else — the product\'s own binary (`node bin/x.mjs …`), `jq`, `grep`,'],
['agents/brief-reviewer.md', 'Never PROPOSE such a command as a'],
['agents/plan-critic.md', '(K10: one backticked command the criteria runner allows,'],
['templates/trekbrief-template.md', 'One command per criterion, from the runner\'s allowlist'],
['templates/trekbrief-template.md', 'in plain words (it runs with no shell: no quote, `$` or glob outside `node --test`).'],
['templates/plan-template.md', 'runner allows, no pipe and no `&&` — usually `npm test` — that the executor'],
['templates/plan-template.md', '`{a command the criteria runner allows that runs the product the way a user does'],
['commands/trekplan.md', 'The command must also be one the criteria runner is allowed to run: a test'],
['commands/trekplan.md', 'grep`) — with no pipe, redirect or `&&`. It runs with no shell: its words'],
['commands/trekplan.md', 'takes a short list of read-only options, and nothing may follow'],
];
test('the criteria text that moved into the skill is no longer copied into the agents, templates and /trekplan', () => {
const back = MOVED.filter(([f, s]) => read(f).includes(s));
assert.deepEqual(back, [], 'copied criteria text is back — it belongs to the skill');
});
test('brief-reviewer and plan-critic keep their own role and point at the preloaded skill', () => {
assert.match(read('agents/brief-reviewer.md'), /preloaded\s+`writing-criteria` skill/);
assert.match(read('agents/brief-reviewer.md'), /BRIEF_SC_NOT_RUNNABLE/, 'flagging an unrunnable criterion stays its job');
const d8 = section(read('agents/plan-critic.md'), /^### 8\. Verification gaps/m, /^### 9\./m);
assert.match(d8, /preloaded\s+`writing-criteria` skill/);
assert.match(d8, /can never go red/, 'a criterion that cannot go red is a finding');
});
// --- the templates' examples run --------------------------------------------
// A backticked span inside a criterion line that is no placeholder (`{…}`) is
// an example the template teaches: the runner must accept it.
function templateExamples(text) {
const out = [];
for (const line of text.split('\n')) {
if (!/^\s*-\s|\*\*(Verify|Regression|End-to-end):\*\*/.test(line)) continue;
for (const m of line.matchAll(/`([^`]+)`/g)) {
const span = m[1].trim();
if (/^\{.*\}$/.test(span) || !firstCommand(`\`${span}\``).command) continue;
out.push(span);
}
}
return out;
}
test('every runnable example in the brief template\'s Success Criteria is accepted by the criteria runner', () => {
const sc = section(read('templates/trekbrief-template.md'), /^## Success Criteria/m, /^## /m);
const ex = templateExamples(sc);
assert.ok(ex.length >= 3, `found ${ex.length}: ${JSON.stringify(ex)}`);
assert.deepEqual(ex.filter((s) => !runs(s)), []);
});
test('every runnable example in the plan template\'s steps and Verification is accepted by the criteria runner', () => {
const t = read('templates/plan-template.md');
const steps = section(t, /^## Implementation Plan/m, /^### Manifest — objective completion predicate/m);
const ver = section(t, /^## Verification/m, /^## /m);
const ex = [...templateExamples(steps), ...templateExamples(ver)]
.filter((s) => !/^(git add|path\/|`)/.test(s) && !/\.(ts|mjs|js|md)$/.test(s));
// Measured 17.10: every check in the template was a placeholder; `Regression:`
// now shows the real command it names ("usually `npm test`").
assert.ok(ex.includes('npm test'), `found ${ex.length}: ${JSON.stringify(ex)}`);
assert.deepEqual(ex.filter((s) => !runs(s)), []);
});
// --- the eval cases ---------------------------------------------------------
test('writing-criteria eval cases: 3 fire, 2 stay quiet (test-first and running the app), 1 effect case', () => {
const dirs = readdirSync(join(ROOT, 'evals')).filter((d) => d.startsWith('skill-writing-criteria-'));
const fires = dirs.filter((d) => /-fires-\d$/.test(d));
const quiet = dirs.filter((d) => /-quiet-\d$/.test(d));
const effect = dirs.filter((d) => d.endsWith('-effect'));
assert.equal(fires.length, 3);
assert.equal(quiet.length, 2);
assert.equal(effect.length, 1);
for (const d of dirs) assert.ok(existsSync(join(ROOT, 'evals', d, 'prompt.md')), `${d}/prompt.md`);
const owners = quiet.map((d) => read(`evals/${d}/prompt.md`).match(/owner=([\w:-]+)/)?.[1]).sort();
assert.ok(owners.includes('test-first'), 'one quiet case belongs to test-first ("write a test")');
const quietText = quiet.map((d) => read(`evals/${d}/prompt.md`)).join('\n');
assert.match(quietText, /\b(start|run)\b[^\n]*\bapp\b/i, 'one quiet case is "run the app" (Claude Code\'s run)');
});

View file

@ -0,0 +1,158 @@
// tests/lib/skills-review-prose-pins.test.mjs
// Wave 3, order 7d: the `reviewing-adversarially` skill and its wiring
// (contract S4).
//
// What is pinned, and why:
// • the skill's form is not pinned here: the form check
// (lib/skills/skill-lint.mjs) and the routing test (lib/skills/routing.mjs)
// run over every skill in `npm test` and are the authority (contract S1);
// • the rules the order names stay in the skill, and its "Enforced by" says
// which part a check holds (a finding with no source) and which part is
// prose only (praise and flattery);
// • the three review agents and scope-guardian preload the skill through
// their `skills:` line, and /trekreview names it where it starts the
// reviewers and the coordinator ("commands are the trunk");
// • the block "Interaction Awareness — MANDATORY OVERRIDE", measured on
// 31c77fd as the same 19 lines in brief-conformance-reviewer,
// code-correctness-reviewer and review-coordinator, is gone from agents/:
// the skill replaces it, and a copy coming back is drift;
// • each agent keeps its own role (dimensions, schema, verdict).
//
// The behaviour itself (a separate list of what was not judged; no finding
// without a source) was measured with helpers in the authoring session and is
// not something a model-free test can see. The eval cases under
// evals/skill-reviewing-adversarially-* carry it for the operator's own runs.
// plan-critic and brief-reviewer also get the skill (S4), but their skills:
// line belongs to order 7c and is pinned there.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
import { loadCases } from '../../lib/skills/routing.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const SKILL = 'skills/reviewing-adversarially/SKILL.md';
const NAME = 'voyage:reviewing-adversarially';
const AGENTS = ['brief-conformance-reviewer', 'code-correctness-reviewer', 'review-coordinator', 'scope-guardian'];
function section(text, heading) {
const parts = text.split(new RegExp(`^## ${heading}$`, 'm'));
assert.ok(parts.length > 1, `## ${heading} not found`);
return parts[1].split(/^## /m)[0];
}
// --- the skill ---------------------------------------------------------------
test('reviewing-adversarially: the skill exists', () => {
assert.ok(existsSync(join(ROOT, SKILL)), `${SKILL} missing`);
});
test('reviewing-adversarially: carries the rules the order names', () => {
const text = read(SKILL);
for (const re of [
/no praise/i,
/no finding without a source/i,
/no downgrade without a reason/i,
/summary only/i,
/## Declined to Judge/,
/a claim is checked against (the )?evidence/i,
/the diff, not the report/i,
/axes stay separate/i,
]) assert.match(text, re, `rule missing: ${re}`);
});
test('reviewing-adversarially: "Enforced by" names the partial checks and says praise is prose only', () => {
const enforced = section(read(SKILL), 'Enforced by');
assert.match(enforced, /lib\/review\/findings-schema\.mjs/);
assert.match(enforced, /lib\/review\/coordinator-contract\.mjs/);
assert.match(enforced, /partial/i);
assert.match(enforced, /praise[^.]*\*\*prose only\*\*/i, 'praise and flattery are prose only; say so');
});
test('reviewing-adversarially: credits carry source path, commit and licence', () => {
const credits = section(read(SKILL), 'Credits');
for (const s of [
'obra/superpowers @8ca22db',
'skills/requesting-code-review/code-reviewer.md:33-48',
'skills/verification-before-completion/SKILL.md:38-48',
'mattpocock/skills @c55ee46',
'skills/engineering/code-review/SKILL.md:76-78',
]) assert.ok(credits.includes(s), `credit missing: ${s}`);
assert.equal((credits.match(/\bMIT\b/g) || []).length, 2, 'both borrowed repositories are MIT and say so');
});
// --- wiring ------------------------------------------------------------------
test('the three review agents and scope-guardian preload voyage:reviewing-adversarially', () => {
for (const a of AGENTS) {
const fm = readFrontmatterFields(read(`agents/${a}.md`)).fields;
assert.ok(fm.skills, `agents/${a}.md has no skills: line`);
assert.deepEqual(fm.skills.value, [NAME], `agents/${a}.md skills:`);
// Preloading uses the skills field, not the Skill tool: the tools stay read-only.
assert.deepEqual(fm.tools.value, ['Read', 'Glob', 'Grep'], `agents/${a}.md tools:`);
}
});
test('/trekreview names the skill where it starts the reviewers and the coordinator', () => {
const cmd = read('commands/trekreview.md');
const phase5 = cmd.split(/^## Phase 5 — Launch parallel reviewers$/m)[1].split(/^## Phase 6/m)[0];
const phase6 = cmd.split(/^## Phase 6 — Coordinator dedup \+ verdict$/m)[1].split(/^## Phase 5–6 via/m)[0];
assert.ok(phase5.includes(NAME), 'Phase 5 must name the skill');
assert.ok(phase6.includes(NAME), 'Phase 6 must name the skill');
});
test('each agent points at the preloaded skill and keeps its own role', () => {
const own = {
'brief-conformance-reviewer': ['## Severity rules', '## JSON output rules', 'UNIMPLEMENTED_CRITERION'],
'code-correctness-reviewer': ['## Your 7-dimension checklist', '## JSON output rules'],
'review-coordinator': ['## Your 4-pass process', '### Pass 4 — Compute verdict', '`## Declined to Judge` — REQUIRED'],
'scope-guardian': ['### 2. Scope creep detection', '### Machine-readable findings block (REQUIRED)'],
};
for (const a of AGENTS) {
const body = read(`agents/${a}.md`).replace(/^---\n[\s\S]*?\n---\n/, '');
assert.match(body, /preloaded\s+`reviewing-adversarially` skill/, `agents/${a}.md must point at the preloaded skill`);
for (const s of own[a]) assert.ok(body.includes(s), `agents/${a}.md lost its own role: ${s}`);
}
});
// --- the copied block is gone ------------------------------------------------
// Measured on 31c77fd: the heading, its lead sentence and the four numbered
// rules below, the same in three agents. The skill replaces them.
const BLOCK = [
'Interaction Awareness — MANDATORY OVERRIDE',
'Sycophancy is the primary vector for AI-induced harm.',
"NEVER reformulate a user's statement in stronger terms than they used.",
'**NEVER start a response with** "Absolutely", "Exactly", "Great point",',
'**Before endorsing any plan:** identify at least one real risk or weakness.',
'**When the user asks "right?" or "don\'t you think?":** evaluate independently.',
];
test('the Interaction Awareness block and its four rules are in no agent file', () => {
const back = [];
for (const f of readdirSync(join(ROOT, 'agents')).filter((x) => x.endsWith('.md'))) {
const t = read(`agents/${f}`);
for (const s of BLOCK) if (t.includes(s)) back.push(`${f}: ${s}`);
}
assert.deepEqual(back, [], 'the copied block is back in an agent; it belongs to the skill');
});
// --- the eval cases ----------------------------------------------------------
test('reviewing-adversarially eval cases: the negatives cover a plain code review and a proofread', () => {
const mine = loadCases(ROOT).filter((c) => c.skill === 'reviewing-adversarially');
const n = (k) => mine.filter((c) => c.kind === k);
assert.ok(n('trigger').length >= 3, `need >= 3 should-trigger cases, have ${n('trigger').length}`);
assert.ok(n('no-trigger').length >= 2, `need >= 2 should-not-trigger cases, have ${n('no-trigger').length}`);
assert.equal(n('effect').length, 1);
const quiet = n('no-trigger').map((c) => c.prompt).join('\n');
assert.match(quiet, /\/code-review/, 'one negative is an ordinary /code-review request');
assert.match(quiet, /wording|proofread|typos/i, 'one negative is a "look over my text" request');
for (const c of mine) assert.doesNotMatch(read(`evals/${c.case}/prompt.md`), /\bproevesett\b/, 'skill cases are not part of the fixed test set');
});