merge: wave 3 order 7c, skill writing-criteria (decision 55)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
01ab61e451
26 changed files with 605 additions and 53 deletions
|
|
@ -32,7 +32,7 @@ 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) 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 |
|
||||
| `/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 |
|
||||
| `/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 |
|
||||
|
|
@ -136,6 +136,7 @@ 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: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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -328,6 +328,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.
|
||||
|
||||
**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/).
|
||||
|
||||
**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).
|
||||
|
|
@ -662,6 +664,7 @@ 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: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,
|
||||
"Use when ...", third person, no YAML traps, under 500 lines, fixed headings, no shell injection)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ description: |
|
|||
model: opus
|
||||
color: magenta
|
||||
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
|
||||
|
|
@ -56,20 +57,14 @@ Flag as **incomplete** if:
|
|||
- Success Criteria are not testable (e.g., "it should work well")
|
||||
- Scope is unbounded — no non-goals defined
|
||||
|
||||
**Runnable at review.** `/trekreview` runs a criterion's command only when it
|
||||
is one test-runner invocation with no pipe, redirect or `&&`: `npm test`,
|
||||
`npm run <script>`, `node --test <file>`, `vitest`, `jest`, `pytest`,
|
||||
`python -m pytest`, `uv run pytest`, `cargo test`, `go test`, `make test`,
|
||||
`bash tests/<script>.sh` (or `/bin/bash tests/<script>.sh`), or `git status|log|diff|show|ls-files`.
|
||||
It runs with no shell, as plain words: no quote, `$`, brace, leading `~` or
|
||||
`@` (a git revision such as `@~1` is the one exception), no glob outside
|
||||
`node --test` (Node 21+), no `..` or absolute path, and only the runner's
|
||||
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`.
|
||||
**Runnable at review, and able to go red.** Judge each criterion by the
|
||||
preloaded `writing-criteria` skill: which commands the criteria runner
|
||||
accepts, the command as the first backticked span, a script under `tests/`
|
||||
for anything more, and a command that can fail. A criterion the runner
|
||||
refuses is reported NOT RUN at review; the validator flags it as
|
||||
`BRIEF_SC_NOT_RUNNABLE`. A criterion that exits 0 whatever the code does is
|
||||
untestable (dimension 3). Every suggestion you make for such a criterion is
|
||||
itself one the skill accepts.
|
||||
|
||||
### 2. Consistency
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ model: opus
|
|||
effort: high
|
||||
color: red
|
||||
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
|
||||
|
|
@ -150,11 +151,18 @@ questions, which defeats the purpose.
|
|||
"not verified end to end".
|
||||
- `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
|
||||
`Regression:` field (K10: one backticked command the criteria runner allows,
|
||||
no pipe, no `&&`), which the executor runs green before the checkpoint. Do
|
||||
not demand `npm test` in `Verify:` — that test is not red before the step.
|
||||
A step that can break existing tests and has no `Regression:` is a **minor**
|
||||
finding ("add `Regression: \`npm test\``"), not a major.
|
||||
`Regression:` field (K10), which the executor runs green before the
|
||||
checkpoint. Do not demand `npm test` in `Verify:` — that test is not red
|
||||
before the step. A step that can break existing tests and has no
|
||||
`Regression:` is a **minor** finding ("add `Regression: \`npm test\``"), not
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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,
|
||||
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)."
|
||||
|
||||
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
|
||||
> 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
|
||||
> 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
|
||||
|
|
@ -824,24 +829,13 @@ Write the plan following the template structure. The plan MUST include:
|
|||
npm test
|
||||
node --test tests/lib/x.test.mjs
|
||||
```
|
||||
The command must also be one the criteria runner is allowed to run: a test
|
||||
runner (`npm test`, `npm run <script>`, `node --test <file>`, `vitest`,
|
||||
`jest`, `pytest`, `python -m pytest`, `uv run pytest`, `cargo test`,
|
||||
`go test`, `make test`), `bash <script under tests/>` (also
|
||||
`/bin/bash <script under tests/>`, to pin the system shell), or a read-only
|
||||
`git` subcommand (`status`, `log`, `diff`, `show`, `ls-files` — not `git
|
||||
grep`) — with no pipe, redirect or `&&`. It runs with no shell: its words
|
||||
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`.
|
||||
Each command is written by the `voyage:writing-criteria` skill (use it by
|
||||
name before you write this section and the steps' `Verify:`, `Regression:`
|
||||
and `End-to-end:` lines): one command the criteria runner accepts, in plain
|
||||
words, a script under `tests/` (`bash tests/<script>.sh`) for anything
|
||||
more, and a command that can go red. Anything the runner refuses is
|
||||
reported NOT RUN, never executed and never failed, and an unrunnable plan
|
||||
criterion stops `/trekexecute` Phase 7.
|
||||
9. **Estimated Scope** — File counts and complexity rating.
|
||||
|
||||
### 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
|
||||
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.
|
||||
Prompt: "Review this implementation plan for the task: {task}.
|
||||
Plan file: {plan path}. Read it and find every problem — missing steps,
|
||||
wrong ordering, fragile assumptions, missing error handling, scope creep,
|
||||
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
|
||||
(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.
|
||||
|
|
@ -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
|
||||
(plan misses brief requirements). Check that referenced files and functions
|
||||
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
|
||||
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)."
|
||||
|
|
|
|||
4
evals/skill-writing-criteria-effect/case.yaml
Normal file
4
evals/skill-writing-criteria-effect/case.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
schema_version: "1.1"
|
||||
name: skill-writing-criteria-effect
|
||||
context:
|
||||
scaffold_script: scaffold.sh
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: regex
|
||||
pattern: '&&'
|
||||
match: not_contains
|
||||
---
|
||||
34
evals/skill-writing-criteria-effect/prompt.md
Normal file
34
evals/skill-writing-criteria-effect/prompt.md
Normal 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.
|
||||
60
evals/skill-writing-criteria-effect/scaffold.sh
Executable file
60
evals/skill-writing-criteria-effect/scaffold.sh
Executable 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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: tool_used
|
||||
tool: Skill
|
||||
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
|
||||
---
|
||||
10
evals/skill-writing-criteria-fires-1/prompt.md
Normal file
10
evals/skill-writing-criteria-fires-1/prompt.md
Normal 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.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: tool_used
|
||||
tool: Skill
|
||||
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
|
||||
---
|
||||
10
evals/skill-writing-criteria-fires-2/prompt.md
Normal file
10
evals/skill-writing-criteria-fires-2/prompt.md
Normal 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.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: tool_used
|
||||
tool: Skill
|
||||
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
|
||||
---
|
||||
10
evals/skill-writing-criteria-fires-3/prompt.md
Normal file
10
evals/skill-writing-criteria-fires-3/prompt.md
Normal 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?
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
type: tool_used
|
||||
tool: Skill
|
||||
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
|
||||
min: 0
|
||||
max: 0
|
||||
arm: both
|
||||
---
|
||||
10
evals/skill-writing-criteria-quiet-1/prompt.md
Normal file
10
evals/skill-writing-criteria-quiet-1/prompt.md
Normal 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.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
type: tool_used
|
||||
tool: Skill
|
||||
input_match: '"skill"\s*:\s*"(?:voyage:)?writing-criteria"'
|
||||
min: 0
|
||||
max: 0
|
||||
arm: both
|
||||
---
|
||||
10
evals/skill-writing-criteria-quiet-2/prompt.md
Normal file
10
evals/skill-writing-criteria-quiet-2/prompt.md
Normal 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.
|
||||
119
skills/writing-criteria/SKILL.md
Normal file
119
skills/writing-criteria/SKILL.md
Normal 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`).
|
||||
|
|
@ -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
|
||||
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
|
||||
break, quoted verbatim), and `Regression:` (ONE backticked command the criteria
|
||||
runner allows, no pipe and no `&&` — usually `npm test` — that the executor
|
||||
runs green before the checkpoint, on a step that can break existing tests;
|
||||
`Verify:` stays the step's own red-first test). A field that is present is
|
||||
break, quoted verbatim), and `Regression:` (ONE backticked command — usually
|
||||
`npm test` — that the executor runs green before the checkpoint, on a step that
|
||||
can break existing tests; `Verify:` stays the step's own red-first test).
|
||||
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`,
|
||||
then the specifics.
|
||||
|
||||
|
|
@ -118,7 +120,7 @@ Several test files share one `File:` line the same way:
|
|||
- Verifies: {what the test checks}
|
||||
- Pattern: `path/to/existing-test.ts` *(follow this style)*
|
||||
- **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}
|
||||
- **Checkpoint:** `git add {the step's Files and its test files} && git commit -m "{conventional commit message}"`
|
||||
- **Manifest:**
|
||||
|
|
@ -143,7 +145,7 @@ Several test files share one `File:` line the same way:
|
|||
- File: `path/to/test.ts` *(existing | new)*
|
||||
- Verifies: {what the test checks}
|
||||
- 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}`
|
||||
- **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}"`
|
||||
|
|
|
|||
|
|
@ -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., "No TypeScript errors: `npm run typecheck` exits 0"}
|
||||
|
||||
One command per criterion, from the runner's allowlist (`npm test`, `npm run <script>`,
|
||||
`node --test`, `pytest`, `bash tests/<script>.sh`, …), with no pipe or `&&`,
|
||||
in plain words (it runs with no shell: no quote, `$` or glob outside `node --test`).
|
||||
Anything else is wrapped in a script under `tests/`; otherwise the review
|
||||
Each command is written by the `voyage:writing-criteria` skill: the first
|
||||
backticked span on the line, one command the criteria runner accepts, a script
|
||||
under `tests/` for anything more, and able to go red. Otherwise the review
|
||||
reports the criterion as NOT RUN.
|
||||
|
||||
Do NOT write vague criteria:
|
||||
|
|
|
|||
2
tests/fixtures/jsonl-schemas.md
vendored
2
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -21,7 +21,7 @@
|
|||
|-----------|--------|-------------|----------|---------------|-----|
|
||||
| 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:1137-1161 | 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 |
|
||||
| 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 (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) |
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
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 @');
|
||||
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`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
227
tests/lib/skills-criteria-prose-pins.test.mjs
Normal file
227
tests/lib/skills-criteria-prose-pins.test.mjs
Normal 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)');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue