merge: main (wave 4 order A) into order B before B goes into main
Conflicts on the union lines only: CHANGELOG Unreleased (both orders' entries kept), CLAUDE.md (/trekexecute row from A, /trekreview row from B), tests/fixtures/jsonl-schemas.md (trekplan line_ref from B, trekexecute line_ref from A). Suite on the merged tree 2682 (2680/0/2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
45eaedc6b4
24 changed files with 274 additions and 45 deletions
|
|
@ -22,6 +22,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **The "run the thing" step names Claude Code's built-in `run` skill.** The plan's run step (`/trekplan`), the executor's end-to-end part (`/trekexecute`) and the plan template each say in one sentence that the app is seen and driven with `run`, and that the verdict is still the `End-to-end:` command through the criteria runner, because `run` gives no exit code (decisions 54 and 64).
|
||||||
|
- **A quiet eval case may name Claude Code's own `run` as its owner** (R2). The routing test keeps a short fixed list of built-in owners (`BUILTIN_OWNERS`, only `run`); for such an owner, which is outside the corpus, only "the skill does not rank first" is checked, and the report says so. `evals/skill-writing-criteria-quiet-2` now has `owner=run` instead of `/trekexecute` standing in.
|
||||||
|
- **P10's model-free part is done; the eval gate (P10) is exempted until the fixed test set runs again.** Every case in `evals/` now names the event it came from (`from=`), the eight test-set cases included (`from=proevesett-eval-suite`, `from=proevesett-intent-gate`). The routing test (`lib/skills/routing.mjs`) fails any case without it, routed or not, and its report gives both counts: cases checked for `from=` and cases routed. The release gate that would read the last test-set run is written into `docs/release-runbook.md` as exempted with its reason, not built: a gate that can never fire is not built.
|
||||||
- `/trekbrief`, `/trekplan` and `/trekreview` name `voyage:annotating` where they build the page;
|
- `/trekbrief`, `/trekplan` and `/trekreview` name `voyage:annotating` where they build the page;
|
||||||
the description of the page and the "revises freehand" loop left the three commands. The call
|
the description of the page and the "revises freehand" loop left the three commands. The call
|
||||||
to `scripts/annotate.mjs`, the not-a-gate line and the printed links stay. The printed report now
|
to `scripts/annotate.mjs`, the not-a-gate line and the printed links stay. The printed report now
|
||||||
|
|
@ -29,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
### Doors
|
### Doors
|
||||||
|
|
||||||
|
- two-way: every case in `evals/` names its event, and the routing test checks every case (P10, R1) — a revert lets a case without `from=` through again; the eight tags added to the fixed test set stay harmless.
|
||||||
|
- two-way: the coupling to Claude Code's `run` and the built-in owner list (R2) — a revert removes the three sentences and makes `owner=run` an unknown owner again; the criteria runner and `End-to-end:` are unchanged.
|
||||||
- two-way: skill annotating and its pointers in three commands (order B) — revert the commit; the
|
- two-way: skill annotating and its pointers in three commands (order B) — revert the commit; the
|
||||||
script, the page, the stored notes and the approval checks are unchanged.
|
script, the page, the stored notes and the approval checks are unchanged.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,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), 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`); the operator's notes from the annotation page go through `voyage:annotating` (`--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`); the operator's notes from the annotation page go through `voyage:annotating` (`--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; 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 |
|
| `/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, the app seen with Claude Code's built-in `run` while the `End-to-end:` command stays the verdict (`run` gives no exit code), 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`; the operator's notes from the annotation page go through `voyage:annotating` | 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`; the operator's notes from the annotation page go through `voyage:annotating` | 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 |
|
||||||
|
|
@ -140,7 +140,9 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch
|
||||||
| `voyage:annotating` | The operator-annotation loop: the person writes every note, Claude none; Fix/Change edit only the quoted text, a Question is answered and changes nothing; the approval an edit costs is said first (stale brief intent → `/trekplan` halts; changed plan → K2, `/trekexecute` refuses); page rebuilt after the revision; research documents on request | `/trekbrief`, `/trekplan`, `/trekreview` name it where they build the page |
|
| `voyage:annotating` | The operator-annotation loop: the person writes every note, Claude none; Fix/Change edit only the quoted text, a Question is answered and changes nothing; the approval an edit costs is said first (stale brief intent → `/trekplan` halts; changed plan → K2, `/trekexecute` refuses); page rebuilt after the revision; research documents on request | `/trekbrief`, `/trekplan`, `/trekreview` name it where they build the page |
|
||||||
| `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 |
|
| `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.
|
||||||
|
> Every case in `evals/` (the test set too) carries `from=<event>`; the routing test fails one without it and reports both counts, checked for `from=` and routed; a quiet case's owner may be Claude Code's own `run` (`BUILTIN_OWNERS`, R2), outside the corpus, so only "not taken" is checked
|
||||||
|
> (P10's model-free part, R1; the release eval gate is exempted until the test set runs again). Method (red baseline first) → `docs/skills-authoring.md`.
|
||||||
|
|
||||||
## Reference docs (read on demand)
|
## Reference docs (read on demand)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -328,7 +328,7 @@ Every implementation step includes:
|
||||||
- **Checkpoint:** — git commit after success
|
- **Checkpoint:** — git commit after success
|
||||||
- **Manifest:** — the objective completion predicate (Hard Rule 17)
|
- **Manifest:** — the objective completion predicate (Hard Rule 17)
|
||||||
|
|
||||||
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. The app is seen and driven with Claude Code's built-in `run` skill; the verdict is still the `End-to-end:` command, because `run` gives no exit code.
|
||||||
|
|
||||||
**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).
|
**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).
|
||||||
|
|
||||||
|
|
@ -346,7 +346,7 @@ Borrowed, with credit: per-step interfaces, Global Constraints and Review Focus
|
||||||
|
|
||||||
Reads a plan from `/trekplan` and implements it with strict discipline. No guessing, no improvising — follows the plan exactly.
|
Reads a plan from `/trekplan` and implements it with strict discipline. No guessing, no improvising — follows the plan exactly.
|
||||||
|
|
||||||
Per step: record the start point → **one fresh helper** (`step-implementer`) writes the step's test first, proves it red, locks it, then applies Changes exactly as written → the executor proves it by command: **red at the start point, green at the result** (`red-proof.mjs`), Verify (exit code is truth), manifest audit, and the **floor guard** on the diff (`test-integrity.mjs`: removed asserts, skipped or deleted tests, muted checks, stubs, loosened thresholds) → **one controller** reads the diff — never the helper's summary — and gives two verdicts, *spec* and *quality* → a script writes the step's **done line** only when the test command exits 0 (`done-line.mjs`) → Checkpoint commit. After all steps: independent Phase 7.5 manifest audit from git log + filesystem (ignoring agent bookkeeping); drift triggers Phase 7.6 bounded recovery. A plan with no passing run-the-thing criterion ends **not verified end to end**, never green.
|
Per step: record the start point → **one fresh helper** (`step-implementer`) writes the step's test first, proves it red, locks it, then applies Changes exactly as written → the executor proves it by command: **red at the start point, green at the result** (`red-proof.mjs`), Verify (exit code is truth), manifest audit, and the **floor guard** on the diff (`test-integrity.mjs`: removed asserts, skipped or deleted tests, muted checks, stubs, loosened thresholds) → **one controller** reads the diff — never the helper's summary — and gives two verdicts, *spec* and *quality* → a script writes the step's **done line** only when the test command exits 0 (`done-line.mjs`) → Checkpoint commit. After all steps: independent Phase 7.5 manifest audit from git log + filesystem (ignoring agent bookkeeping); drift triggers Phase 7.6 bounded recovery. A plan with no passing run-the-thing criterion ends **not verified end to end**, never green. Claude Code's built-in `run` skill can show the app working, but it gives no exit code, so the run-the-thing verdict stays with the plan's `End-to-end:` command.
|
||||||
|
|
||||||
A run starts only on a plan you approved **as it is now**: the approval log holds the plan's sha256, so a plan edited after its approval is refused before step 1. At step end the repo's own tools check the step's diff (the step check) and the step's `Regression:` suite must stay green. After the last step come the architecture rules and the sweep for files the plan does not name, and then **the review starts by itself**: `/simplify` → `/trekreview` → the verdict gate. A BLOCK keeps the merge into main closed.
|
A run starts only on a plan you approved **as it is now**: the approval log holds the plan's sha256, so a plan edited after its approval is refused before step 1. At step end the repo's own tools check the step's diff (the step check) and the step's `Regression:` suite must stay green. After the last step come the architecture rules and the sweep for files the plan does not name, and then **the review starts by itself**: `/simplify` → `/trekreview` → the verdict gate. A BLOCK keeps the merge into main closed.
|
||||||
|
|
||||||
|
|
@ -674,7 +674,9 @@ The workshop behind them runs in `npm test`: the form check `lib/skills/skill-li
|
||||||
"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)
|
||||||
and the model-free routing test `lib/skills/routing.mjs` (no two descriptions among skills,
|
and the model-free routing test `lib/skills/routing.mjs` (no two descriptions among skills,
|
||||||
commands and agents may collide; 3 firing, 2 quiet and 1 effect case per skill). The eval cases sit
|
commands and agents may collide; 3 firing, 2 quiet and 1 effect case per skill). The eval cases sit
|
||||||
in `evals/skill-*` for the operator's own eval run. How to write one, starting with a baseline the
|
in `evals/skill-*` for the operator's own eval run. Every case in `evals/`, the fixed test set's
|
||||||
|
too, names the event it came from (`from=`); the routing test fails a case without it and reports
|
||||||
|
how many cases it checked for `from=` and how many it routed. A quiet case's owner may be Claude Code's own `run` skill, which is outside the corpus: then only "the skill does not rank first" is checked. How to write one, starting with a baseline the
|
||||||
helper fails without the skill: [docs/skills-authoring.md](docs/skills-authoring.md).
|
helper fails without the skill: [docs/skills-authoring.md](docs/skills-authoring.md).
|
||||||
|
|
||||||
Borrowed, with credit: the form check and routing test from addyosmani/agent-skills (MIT); the red
|
Borrowed, with credit: the form check and routing test from addyosmani/agent-skills (MIT); the red
|
||||||
|
|
|
||||||
|
|
@ -2087,7 +2087,9 @@ verification failure must not hide a manifest drift.
|
||||||
|
|
||||||
### End to end — "run the thing" (P9 (6))
|
### End to end — "run the thing" (P9 (6))
|
||||||
|
|
||||||
Tests passing is not the product working. Copy `end_to_end.status` from the
|
Tests passing is not the product working. To see and drive the app, use
|
||||||
|
Claude Code's built-in `run` skill; the verdict is still the `End-to-end:`
|
||||||
|
command, because `run` gives no exit code. Copy `end_to_end.status` from the
|
||||||
runner's JSON into `end_to_end` — the runner decides it, not this prose:
|
runner's JSON into `end_to_end` — the runner decides it, not this prose:
|
||||||
|
|
||||||
- `"verified"` — the verification passed and at least one passed criterion
|
- `"verified"` — the verification passed and at least one passed criterion
|
||||||
|
|
|
||||||
|
|
@ -792,17 +792,17 @@ Write the plan following the template structure. The plan MUST include:
|
||||||
(`PLAN_STEP_TESTS_ONLY`).
|
(`PLAN_STEP_TESTS_ONLY`).
|
||||||
|
|
||||||
**The "run the thing" step.** At least one step carries
|
**The "run the thing" step.** At least one step carries
|
||||||
`End-to-end: \`<command>\` → expected: <what the user sees>`: the product run
|
`End-to-end: \`<command>\` → expected: <what the user sees>`: the product run the way a
|
||||||
the way a user runs it (the CLI, the server and a request, the app; for iOS,
|
user runs it (the CLI, the server and a request, the app; for iOS, the simulator). Without
|
||||||
the simulator). Without it the result is "not verified end to end", never
|
it the result is "not verified end to end", never green. To see and drive the app, use
|
||||||
green. The command passes the criteria runner's allowlist — the product's
|
Claude Code's built-in `run` skill; the verdict is still the `End-to-end:` command, because
|
||||||
own binary does not, so the run is wrapped in a script:
|
`run` gives no exit code. The command passes the criteria runner's allowlist — the
|
||||||
|
product's own binary does not, so the run is wrapped in a script:
|
||||||
`End-to-end: \`bash tests/e2e.sh\``. The same command is also listed under
|
`End-to-end: \`bash tests/e2e.sh\``. The same command is also listed under
|
||||||
`## Verification`: Phase 7 of `/trekexecute` runs that section, and a passed
|
`## Verification`: Phase 7 of `/trekexecute` runs that section, and a passed criterion that
|
||||||
criterion that runs the step's `End-to-end:` command is what verifies the
|
runs the step's `End-to-end:` command is what verifies the run end to end. `End-to-end:`
|
||||||
run end to end. `End-to-end:` rides on the last step that builds behaviour,
|
rides on the last step that builds behaviour, with `tests/e2e.sh` in that step's `Files:`:
|
||||||
with `tests/e2e.sh` in that step's `Files:`: a step of its own that only adds
|
a step of its own that only adds the script is a step of only tests, and it can never be proven red.
|
||||||
the script is a step of only tests, and it can never be proven red.
|
|
||||||
10. **Execution Strategy** — For plans with > 5 steps: group steps into sessions
|
10. **Execution Strategy** — For plans with > 5 steps: group steps into sessions
|
||||||
(3–5 steps each), organize sessions into waves (parallel where independent),
|
(3–5 steps each), organize sessions into waves (parallel where independent),
|
||||||
specify scope fences per session. Omit for plans with ≤ 5 steps.
|
specify scope fences per session. Omit for plans with ≤ 5 steps.
|
||||||
|
|
|
||||||
|
|
@ -225,14 +225,18 @@ changes introduced by commits that are not ancestors of the previously
|
||||||
reverted merge." Revert the revert commit instead, and release it as a new
|
reverted merge." Revert the revert commit instead, and release it as a new
|
||||||
version.
|
version.
|
||||||
|
|
||||||
## Not built yet: the eval gate (P10)
|
## Exempted: the eval gate (P10)
|
||||||
|
|
||||||
The local gate before a release that reads the date and score of the last run
|
The local gate before a release that reads the date and score of the last run
|
||||||
of the fixed test set and compares it with the last change in the
|
of the fixed test set and compares it with the last change in the
|
||||||
configuration paths (commands, agents, skills, hooks, and a changed model alias
|
configuration paths (commands, agents, skills, hooks, and a changed model alias
|
||||||
counts as a change). It waits for the test set to run again; a gate that can
|
counts as a change). It is exempted from the release, with a reason: it waits
|
||||||
never fire is not built. When it comes, it is one more check in
|
for the fixed test set to run again, and a gate that can never fire is not
|
||||||
`scripts/release-check.mjs`, before the door check, and one more step in § 1.
|
built. P10's model-free part is in place: every case in `evals/` names the
|
||||||
|
event it came from (`from=`), and the routing test (`lib/skills/routing.mjs`,
|
||||||
|
run by `npm test`) fails a case without it. When the gate comes, it is one more
|
||||||
|
check in `scripts/release-check.mjs`, before the door check, and one more step
|
||||||
|
in § 1.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,10 @@ the 7 commands and the 21 spawnable agents, with no model:
|
||||||
- Each skill has at least 3 prompts where it should fire, 2 where it should not, and 1 effect
|
- Each skill has at least 3 prompts where it should fire, 2 where it should not, and 1 effect
|
||||||
case. A firing prompt must rank the skill in the top 3; every one currently ranks it first, and
|
case. A firing prompt must rank the skill in the top 3; every one currently ranks it first, and
|
||||||
that floor is held. A prompt where the skill should stay quiet names the command, agent or skill
|
that floor is held. A prompt where the skill should stay quiet names the command, agent or skill
|
||||||
that owns it, and that owner must rank higher.
|
that owns it, and that owner must rank higher. The owner may also be one of Claude Code's own
|
||||||
|
skills on a short fixed list in `routing.mjs` (`BUILTIN_OWNERS`, only `run` for now). Such an
|
||||||
|
owner is outside the corpus, so only "not taken" is checked: the skill must not rank first, and
|
||||||
|
the report says so for each such case.
|
||||||
- The thresholds are pinned in `tests/lib/skill-routing.test.mjs` and never lowered. A failing case
|
- The thresholds are pinned in `tests/lib/skill-routing.test.mjs` and never lowered. A failing case
|
||||||
is fixed in a description, never in a number. You do not change another component's
|
is fixed in a description, never in a number. You do not change another component's
|
||||||
description to make room for yours.
|
description to make room for yours.
|
||||||
|
|
@ -72,6 +75,10 @@ tags: [skill-effect, skill=test-first, from=baseline-tfd-crash-red]
|
||||||
```
|
```
|
||||||
|
|
||||||
`from=` names the event the case came from: a measured failure, a review finding, an analysis.
|
`from=` names the event the case came from: a measured failure, a review finding, an analysis.
|
||||||
|
It is required on every case in `evals/`, not only the skill cases: a case of the fixed test set
|
||||||
|
(`tags: [proevesett, from=proevesett-eval-suite]`) has no skill tag, so it is not routed, but the
|
||||||
|
routing test still fails it without `from=`. Its report gives both counts, cases checked for
|
||||||
|
`from=` and cases routed.
|
||||||
|
|
||||||
- A **skill-trigger** case has one grader: `type: tool_used`, `tool: Skill`, with an `input_match`
|
- A **skill-trigger** case has one grader: `type: tool_used`, `tool: Skill`, with an `input_match`
|
||||||
that accepts `voyage:<name>` and the bare name.
|
that accepts `voyage:<name>` and the bare name.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "/trekplan on a brief that passes the sequencing gate but carries no intent approval halts at the intent gate (veikart steg 1)."
|
description: "/trekplan on a brief that passes the sequencing gate but carries no intent approval halts at the intent gate (veikart steg 1)."
|
||||||
expected_outcome: "intent-approval.mjs --check runs and reports BRIEF_INTENT_NOT_APPROVED; /trekplan halts and names /trekbrief --approve; no Agent call (no exploration swarm), no plan.md."
|
expected_outcome: "intent-approval.mjs --check runs and reports BRIEF_INTENT_NOT_APPROVED; /trekplan halts and names /trekbrief --approve; no Agent call (no exploration swarm), no plan.md."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-intent-gate]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 15
|
max_turns: 15
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "/trekplan on a brief_version 2.1 brief without phase_signals halts at the sequencing gate."
|
description: "/trekplan on a brief_version 2.1 brief without phase_signals halts at the sequencing gate."
|
||||||
expected_outcome: "brief-validator runs, reports BRIEF_V51_MISSING_SIGNALS, and /trekplan halts with the phase_signals hint; no Agent call (no exploration swarm), no plan.md."
|
expected_outcome: "brief-validator runs, reports BRIEF_V51_MISSING_SIGNALS, and /trekplan halts with the phase_signals hint; no Agent call (no exploration swarm), no plan.md."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 15
|
max_turns: 15
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "/trekplan --project on a missing folder stops before any exploration."
|
description: "/trekplan --project on a missing folder stops before any exploration."
|
||||||
expected_outcome: "'Error: project directory not initialized. Run /trekbrief to create it.'; no Agent call, no plan.md."
|
expected_outcome: "'Error: project directory not initialized. Run /trekbrief to create it.'; no Agent call, no plan.md."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 8
|
max_turns: 8
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "/trekplan --export with a removed format names the format and stops."
|
description: "/trekplan --export with a removed format names the format and stops."
|
||||||
expected_outcome: "'Error: export format 'pr' is not supported.'; no Agent call, no file written."
|
expected_outcome: "'Error: export format 'pr' is not supported.'; no Agent call, no file written."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 8
|
max_turns: 8
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "/trekplan with no arguments prints usage and stops."
|
description: "/trekplan with no arguments prints usage and stops."
|
||||||
expected_outcome: "Usage block ending in 'A brief is required. Produce one with /trekbrief first.'; no Agent call, no file written."
|
expected_outcome: "Usage block ending in 'A brief is required. Produce one with /trekbrief first.'; no Agent call, no file written."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 8
|
max_turns: 8
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "/trekreview with no arguments demands --project and stops."
|
description: "/trekreview with no arguments demands --project and stops."
|
||||||
expected_outcome: "'Error: --project <dir> is required.' plus usage; no Agent call, no file written."
|
expected_outcome: "'Error: --project <dir> is required.' plus usage; no Agent call, no file written."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 8
|
max_turns: 8
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "KNOWN-POSITIVE. /trekreview --validate on a review.md whose finding ID is not 40-char hex fails and names the rule."
|
description: "KNOWN-POSITIVE. /trekreview --validate on a review.md whose finding ID is not 40-char hex fails and names the rule."
|
||||||
expected_outcome: "review-validator runs and reports REVIEW_BAD_FINDING_ID; the one-line summary says FAIL; no Agent call, no file written."
|
expected_outcome: "review-validator runs and reports REVIEW_BAD_FINDING_ID; the one-line summary says FAIL; no Agent call, no file written."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 10
|
max_turns: 10
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
description: "KNOWN-NEGATIVE. /trekreview --validate on a valid review.md passes and raises no error code."
|
description: "KNOWN-NEGATIVE. /trekreview --validate on a valid review.md passes and raises no error code."
|
||||||
expected_outcome: "review-validator runs; the one-line summary says PASS; no REVIEW_* error code appears anywhere in the trace; no Agent call, no file written."
|
expected_outcome: "review-validator runs; the one-line summary says PASS; no REVIEW_* error code appears anywhere in the trace; no Agent call, no file written."
|
||||||
tags: [proevesett]
|
tags: [proevesett, from=proevesett-eval-suite]
|
||||||
runs: 1
|
runs: 1
|
||||||
max_turns: 10
|
max_turns: 10
|
||||||
timeout_seconds: 300
|
timeout_seconds: 300
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
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."
|
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. It is outside the model-free routing corpus (Voyage's skills, commands and agents), so the routing test only checks that writing-criteria does not rank first. The model-run grader only checks that writing-criteria is not called."
|
||||||
expected_outcome: "Claude never calls the Skill tool for voyage:writing-criteria."
|
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]
|
tags: [skill-no-trigger, skill=writing-criteria, owner=run, from=order-7c-negatives]
|
||||||
runs: 3
|
runs: 3
|
||||||
max_turns: 4
|
max_turns: 4
|
||||||
allowed_tools: [Read, Glob, Grep, Skill]
|
allowed_tools: [Read, Glob, Grep, Skill]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@
|
||||||
// A case is tagged in its prompt.md frontmatter:
|
// A case is tagged in its prompt.md frontmatter:
|
||||||
// tags: [skill-trigger | skill-no-trigger | skill-effect, skill=<name>,
|
// tags: [skill-trigger | skill-no-trigger | skill-effect, skill=<name>,
|
||||||
// owner=<command|agent|skill name> (negatives), from=<event>]
|
// owner=<command|agent|skill name> (negatives), from=<event>]
|
||||||
// `from=` names the event the case came from (P10: every case is marked).
|
// An owner may also be one of Claude Code's own skills on BUILTIN_OWNERS (R2);
|
||||||
|
// it stands outside the corpus, so only "the skill does not rank first" is checked.
|
||||||
|
// `from=` names the event the case came from (P10, R1): it is required on every
|
||||||
|
// case in evals/, and a case with no skill tag (the fixed test set) is not
|
||||||
|
// routed, only checked for `from=`.
|
||||||
//
|
//
|
||||||
// What this cannot see: how Claude Code really chooses. A ranking over word
|
// What this cannot see: how Claude Code really chooses. A ranking over word
|
||||||
// overlap is a proxy; the operator's eval run is the measurement.
|
// overlap is a proxy; the operator's eval run is the measurement.
|
||||||
|
|
@ -180,8 +184,12 @@ export function loadEntries(root) {
|
||||||
|
|
||||||
const KIND_TAGS = { 'skill-trigger': 'trigger', 'skill-no-trigger': 'no-trigger', 'skill-effect': 'effect' };
|
const KIND_TAGS = { 'skill-trigger': 'trigger', 'skill-no-trigger': 'no-trigger', 'skill-effect': 'effect' };
|
||||||
|
|
||||||
/** The routing-tagged eval cases of a plugin root (evals/<case>/prompt.md). */
|
/**
|
||||||
export function loadCases(root) {
|
* Every eval case of a plugin root (evals/<case>/prompt.md), routed or not.
|
||||||
|
* `kind` is null for a case with no skill tag (the fixed test set): it is not
|
||||||
|
* routed, only checked for `from=` (P10, R1).
|
||||||
|
*/
|
||||||
|
export function loadAllCases(root) {
|
||||||
const d = join(root, 'evals');
|
const d = join(root, 'evals');
|
||||||
if (!existsSync(d)) return [];
|
if (!existsSync(d)) return [];
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
@ -192,16 +200,30 @@ export function loadCases(root) {
|
||||||
const tagsField = readFrontmatterFields(text).fields.tags;
|
const tagsField = readFrontmatterFields(text).fields.tags;
|
||||||
const tags = tagsField && Array.isArray(tagsField.value) ? tagsField.value.map(String) : [];
|
const tags = tagsField && Array.isArray(tagsField.value) ? tagsField.value.map(String) : [];
|
||||||
const kindTag = tags.find((t) => Object.hasOwn(KIND_TAGS, t));
|
const kindTag = tags.find((t) => Object.hasOwn(KIND_TAGS, t));
|
||||||
if (!kindTag) continue;
|
|
||||||
const val = (k) => { const t = tags.find((x) => x.startsWith(`${k}=`)); return t ? t.slice(k.length + 1) : null; };
|
const val = (k) => { const t = tags.find((x) => x.startsWith(`${k}=`)); return t ? t.slice(k.length + 1) : null; };
|
||||||
const body = text.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
const body = text.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
||||||
out.push({ case: name, kind: KIND_TAGS[kindTag], skill: val('skill'), owner: val('owner'), from: val('from'), prompt: body });
|
out.push({ case: name, kind: kindTag ? KIND_TAGS[kindTag] : null, skill: val('skill'), owner: val('owner'), from: val('from'), prompt: body });
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The routing-tagged eval cases of a plugin root. */
|
||||||
|
export function loadCases(root) {
|
||||||
|
return loadAllCases(root).filter((c) => c.kind);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- the check ----
|
// ---- the check ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude Code's own skills a negative case may name as its owner (R2). They
|
||||||
|
* stand outside the corpus, so for such an owner only "not taken" is checked:
|
||||||
|
* the skill under test must not rank first. Kept short and fixed.
|
||||||
|
* run - the bundled "Launch and drive your app to see a change working"
|
||||||
|
* skill, seen in Claude Code 2.1.303 (code.claude.com/docs/en/skills,
|
||||||
|
* "Run and verify your app").
|
||||||
|
*/
|
||||||
|
export const BUILTIN_OWNERS = Object.freeze(['run']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the whole routing check over a plugin root.
|
* Run the whole routing check over a plugin root.
|
||||||
* @returns {{ errors: object[], warnings: object[], stats: object }}
|
* @returns {{ errors: object[], warnings: object[], stats: object }}
|
||||||
|
|
@ -214,9 +236,11 @@ export function checkRouting(root, t = THRESHOLDS) {
|
||||||
// name must not take its place. An owner may be any kind; a skill wins a tie.
|
// name must not take its place. An owner may be any kind; a skill wins a tie.
|
||||||
const skillByName = new Map(skills.map((e) => [e.name, e]));
|
const skillByName = new Map(skills.map((e) => [e.name, e]));
|
||||||
const byName = new Map([...entries.filter((e) => e.kind !== 'skill'), ...skills].map((e) => [e.name, e]));
|
const byName = new Map([...entries.filter((e) => e.kind !== 'skill'), ...skills].map((e) => [e.name, e]));
|
||||||
const cases = loadCases(root);
|
const all = loadAllCases(root);
|
||||||
|
const cases = all.filter((c) => c.kind);
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
const notes = [];
|
||||||
let positives = 0; let rank1 = 0; let negatives = 0;
|
let positives = 0; let rank1 = 0; let negatives = 0;
|
||||||
|
|
||||||
for (const c of collisions(corpus, t)) {
|
for (const c of collisions(corpus, t)) {
|
||||||
|
|
@ -225,8 +249,11 @@ export function checkRouting(root, t = THRESHOLDS) {
|
||||||
(c.level === 'error' ? errors : warnings).push(entry);
|
(c.level === 'error' ? errors : warnings).push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const c of cases) {
|
for (const c of all) {
|
||||||
if (!c.from) errors.push({ code: 'ROUTING_CASE_NO_SOURCE', case: c.case, message: `${c.case}: tag the event the case came from (from=...)` });
|
if (!c.from) errors.push({ code: 'ROUTING_CASE_NO_SOURCE', case: c.case, message: `${c.case}: tag the event the case came from (from=...)` });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of cases) {
|
||||||
const self = c.skill && skillByName.get(c.skill);
|
const self = c.skill && skillByName.get(c.skill);
|
||||||
if (!self || self.kind !== 'skill') {
|
if (!self || self.kind !== 'skill') {
|
||||||
errors.push({ code: 'ROUTING_UNKNOWN_SKILL', case: c.case, message: `${c.case}: skill=${c.skill} is not a skill in skills/` });
|
errors.push({ code: 'ROUTING_UNKNOWN_SKILL', case: c.case, message: `${c.case}: skill=${c.skill} is not a skill in skills/` });
|
||||||
|
|
@ -251,6 +278,9 @@ export function checkRouting(root, t = THRESHOLDS) {
|
||||||
}
|
}
|
||||||
if (!c.owner) {
|
if (!c.owner) {
|
||||||
errors.push({ code: 'ROUTING_NEGATIVE_NO_OWNER', case: c.case, message: `${c.case}: a negative case names the owner that should take the prompt (owner=...)` });
|
errors.push({ code: 'ROUTING_NEGATIVE_NO_OWNER', case: c.case, message: `${c.case}: a negative case names the owner that should take the prompt (owner=...)` });
|
||||||
|
} else if (!byName.has(c.owner) && BUILTIN_OWNERS.includes(c.owner)) {
|
||||||
|
notes.push({ code: 'ROUTING_OWNER_OUTSIDE_CORPUS', case: c.case,
|
||||||
|
message: `${c.case}: owner=${c.owner} is Claude Code's own; owner outside the corpus: only "not taken" checked` });
|
||||||
} else if (!byName.has(c.owner)) {
|
} else if (!byName.has(c.owner)) {
|
||||||
errors.push({ code: 'ROUTING_UNKNOWN_OWNER', case: c.case, message: `${c.case}: owner=${c.owner} is no skill, command or spawnable agent` });
|
errors.push({ code: 'ROUTING_UNKNOWN_OWNER', case: c.case, message: `${c.case}: owner=${c.owner} is no skill, command or spawnable agent` });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -276,7 +306,7 @@ export function checkRouting(root, t = THRESHOLDS) {
|
||||||
if (positives && rate < t.MIN_RANK1_RATE) {
|
if (positives && rate < t.MIN_RANK1_RATE) {
|
||||||
errors.push({ code: 'ROUTING_RANK1_FLOOR', message: `${rank1} of ${positives} positives rank their skill first (${(rate * 100).toFixed(0)}%); the floor is ${(t.MIN_RANK1_RATE * 100).toFixed(0)}%` });
|
errors.push({ code: 'ROUTING_RANK1_FLOOR', message: `${rank1} of ${positives} positives rank their skill first (${(rate * 100).toFixed(0)}%); the floor is ${(t.MIN_RANK1_RATE * 100).toFixed(0)}%` });
|
||||||
}
|
}
|
||||||
return { errors, warnings, stats: { entries: entries.length, skills: skills.length, cases: cases.length, positives, negatives, rank1 } };
|
return { errors, warnings, notes, stats: { entries: entries.length, skills: skills.length, sourced: all.length, cases: cases.length, positives, negatives, rank1 } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- CLI ----
|
// ---- CLI ----
|
||||||
|
|
@ -294,7 +324,8 @@ if (isMain(import.meta.url)) {
|
||||||
else {
|
else {
|
||||||
for (const e of r.errors) process.stdout.write(`FAIL ${e.code}: ${e.message}\n`);
|
for (const e of r.errors) process.stdout.write(`FAIL ${e.code}: ${e.message}\n`);
|
||||||
for (const w of r.warnings) process.stdout.write(`warn ${w.code}: ${w.message}\n`);
|
for (const w of r.warnings) process.stdout.write(`warn ${w.code}: ${w.message}\n`);
|
||||||
process.stdout.write(`${r.stats.entries} descriptions, ${r.stats.cases} cases, ${r.stats.rank1}/${r.stats.positives} positives first; ${r.errors.length} finding(s)\n`);
|
for (const n of r.notes) process.stdout.write(`note ${n.code}: ${n.message}\n`);
|
||||||
|
process.stdout.write(`${r.stats.entries} descriptions, ${r.stats.sourced} cases checked for from=, ${r.stats.cases} routed, ${r.stats.rank1}/${r.stats.positives} positives first; ${r.errors.length} finding(s)\n`);
|
||||||
}
|
}
|
||||||
process.exit(r.errors.length ? 1 : 0);
|
process.exit(r.errors.length ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,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 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)*
|
- **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; to see and drive the app, use Claude Code's built-in "run" skill — the verdict is still the End-to-end: command, because "run" gives no exit code)*
|
||||||
- **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}"`
|
||||||
|
|
|
||||||
2
tests/fixtures/jsonl-schemas.md
vendored
2
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -22,7 +22,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:1125-1149 | 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:1125-1149 | 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:1273-1301 | 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:1273-1301 | 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:2907-2929 | 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:2909-2931 | 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:636-686 | 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 |
|
||||||
|
|
|
||||||
82
tests/lib/p10-model-free.test.mjs
Normal file
82
tests/lib/p10-model-free.test.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
// tests/lib/p10-model-free.test.mjs
|
||||||
|
// P10's model-free part (wave 4, order A1). Every case in evals/ names the
|
||||||
|
// event it came from (`from=`), not only the skill cases; the routing check
|
||||||
|
// covers every case and says how many it checked for `from=` and how many it
|
||||||
|
// routed; and the release gate that would read the last test-set run is
|
||||||
|
// written down as exempted with a reason, not built.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
|
||||||
|
import { checkRouting } from '../../lib/skills/routing.mjs';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||||
|
const CLI = join(ROOT, 'lib', 'skills', 'routing.mjs');
|
||||||
|
|
||||||
|
const tagsOf = (file) => {
|
||||||
|
const f = readFrontmatterFields(readFileSync(file, 'utf8')).fields.tags;
|
||||||
|
return f && Array.isArray(f.value) ? f.value.map(String) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
test('P10: every case in evals/ names the event it came from (from=)', () => {
|
||||||
|
const dirs = readdirSync(join(ROOT, 'evals')).filter((d) => existsSync(join(ROOT, 'evals', d, 'prompt.md'))).sort();
|
||||||
|
assert.ok(dirs.length >= 45, `${dirs.length} cases`);
|
||||||
|
const missing = dirs.filter((d) => !tagsOf(join(ROOT, 'evals', d, 'prompt.md')).some((t) => /^from=\S+$/.test(t)));
|
||||||
|
assert.deepEqual(missing, [], `${missing.length} of ${dirs.length} cases have no from=: ${missing.join(', ')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** A plugin root with one skill, its 3/2/1 cases, and the extra cases given. */
|
||||||
|
function stubRoot(extra) {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'p10-model-free-'));
|
||||||
|
mkdirSync(join(root, 'skills', 'tidy-imports'), { recursive: true });
|
||||||
|
writeFileSync(join(root, 'skills', 'tidy-imports', 'SKILL.md'),
|
||||||
|
'---\nname: tidy-imports\ndescription: Sorts the import lines of a source file. Use when imports are out of order.\n---\n');
|
||||||
|
mkdirSync(join(root, 'commands'), { recursive: true });
|
||||||
|
writeFileSync(join(root, 'commands', 'rotate-logs.md'), '---\nname: rotate-logs\ndescription: Compresses and deletes old log files on disk.\n---\n');
|
||||||
|
const cases = {
|
||||||
|
p1: ['skill-trigger, skill=tidy-imports, from=test', 'sort the import lines'],
|
||||||
|
p2: ['skill-trigger, skill=tidy-imports, from=test', 'import lines are out of order'],
|
||||||
|
p3: ['skill-trigger, skill=tidy-imports, from=test', 'sort imports in the source file'],
|
||||||
|
n1: ['skill-no-trigger, skill=tidy-imports, owner=rotate-logs, from=test', 'delete old log files'],
|
||||||
|
n2: ['skill-no-trigger, skill=tidy-imports, owner=rotate-logs, from=test', 'compress the logs on disk'],
|
||||||
|
e1: ['skill-effect, skill=tidy-imports, from=test', 'x'],
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
for (const [n, [tags, prompt]] of Object.entries(cases)) {
|
||||||
|
mkdirSync(join(root, 'evals', n), { recursive: true });
|
||||||
|
writeFileSync(join(root, 'evals', n, 'prompt.md'), `---\ndescription: "case ${n}"\ntags: [${tags}]\nruns: 1\n---\n\n${prompt}\n`);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('P10 (R1): a case with no skill tag and no from= is a finding of the routing check', () => {
|
||||||
|
const root = stubRoot({ 'plain-gate': ['proevesett', '/voyage:trekplan'], 'plain-sourced': ['proevesett, from=test', '/voyage:trekplan'] });
|
||||||
|
try {
|
||||||
|
const r = checkRouting(root);
|
||||||
|
const src = r.errors.filter((e) => e.code === 'ROUTING_CASE_NO_SOURCE').map((e) => e.case);
|
||||||
|
assert.deepEqual(src, ['plain-gate'], JSON.stringify(r.errors));
|
||||||
|
assert.ok(!r.errors.some((e) => e.case === 'plain-sourced'), 'a sourced case with no skill tag is not routed');
|
||||||
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('P10 (R1): the report shows the denominators: cases checked for from= and cases routed', () => {
|
||||||
|
const root = stubRoot({ 'plain-sourced': ['proevesett, from=test', '/voyage:trekplan'] });
|
||||||
|
try {
|
||||||
|
const r = checkRouting(root);
|
||||||
|
assert.deepEqual([r.stats.sourced, r.stats.cases], [7, 6], JSON.stringify(r.stats));
|
||||||
|
const out = spawnSync('node', [CLI, '--root', root], { encoding: 'utf8' });
|
||||||
|
assert.match(out.stdout, /7 cases checked for from=, 6 routed/, out.stdout + out.stderr);
|
||||||
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Found by its words, not its heading: the section title is set at the release.
|
||||||
|
test('P10: the CHANGELOG says the eval gate is exempted, with the reason', () => {
|
||||||
|
const md = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8');
|
||||||
|
assert.ok(/^- [^\n]*eval gate \(P10\)[^\n]*exempted[^\n]*test set runs again/m.test(md),
|
||||||
|
'a CHANGELOG line must say the eval gate (P10) is exempted until the test set runs again');
|
||||||
|
});
|
||||||
|
|
@ -118,8 +118,9 @@ test('runbook: the release check exits as the script does — 0 ready, 1 not rea
|
||||||
assert.match(before, /0 = ready, 1 = not ready[^.]*, 2 = could not run — never green/);
|
assert.match(before, /0 = ready, 1 = not ready[^.]*, 2 = could not run — never green/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runbook: the eval gate (P10) is named as not built, with where it goes', () => {
|
test('runbook: the eval gate (P10) is exempted with its reason, and says where it goes', () => {
|
||||||
const p10 = part(read(RUNBOOK), 'Not built yet: the eval gate (P10)');
|
const p10 = part(read(RUNBOOK), 'Exempted: the eval gate (P10)').replace(/\s+/g, ' ');
|
||||||
|
assert.match(p10, /waits for the fixed test set to run again/, 'P10 must say why the gate is exempted');
|
||||||
assert.ok(p10.includes('scripts/release-check.mjs'), 'P10 must say which file the gate goes into');
|
assert.ok(p10.includes('scripts/release-check.mjs'), 'P10 must say which file the gate goes into');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,7 @@ test('review 630 r3: routing reads a kind tag only from its own table, never Obj
|
||||||
for (const d of ['skills', 'commands', 'agents', 'evals']) cpSync(join(ROOT, d), join(dir, d), { recursive: true });
|
for (const d of ['skills', 'commands', 'agents', 'evals']) cpSync(join(ROOT, d), join(dir, d), { recursive: true });
|
||||||
const c = join(dir, 'evals', 'zz-proto');
|
const c = join(dir, 'evals', 'zz-proto');
|
||||||
cpSync(join(ROOT, 'evals', 'skill-test-first-quiet-1'), c, { recursive: true });
|
cpSync(join(ROOT, 'evals', 'skill-test-first-quiet-1'), c, { recursive: true });
|
||||||
writeFileSync(join(c, 'prompt.md'), '---\ntags: [constructor, owner=nobody]\n---\n\nWhat time is it?\n');
|
writeFileSync(join(c, 'prompt.md'), '---\ntags: [constructor, owner=nobody, from=test]\n---\n\nWhat time is it?\n');
|
||||||
const { checkRouting } = awaitImportSync();
|
const { checkRouting } = awaitImportSync();
|
||||||
const r = checkRouting(dir);
|
const r = checkRouting(dir);
|
||||||
assert.ok(!r.errors.some((e) => String(e.message ?? '').includes('zz-proto') || e.case === 'zz-proto'), JSON.stringify(r.errors));
|
assert.ok(!r.errors.some((e) => String(e.message ?? '').includes('zz-proto') || e.case === 'zz-proto'), JSON.stringify(r.errors));
|
||||||
|
|
|
||||||
59
tests/lib/run-coupling-prose-pins.test.mjs
Normal file
59
tests/lib/run-coupling-prose-pins.test.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// tests/lib/run-coupling-prose-pins.test.mjs
|
||||||
|
// The coupling to Claude Code's built-in `run` skill (wave 4, order A2,
|
||||||
|
// decisions 54 and 64). The plan's "run the thing" step, the executor's
|
||||||
|
// `### End to end` and the plan template's run step each say in one sentence
|
||||||
|
// that the app is seen and driven with `run`, and that the verdict is still
|
||||||
|
// the `End-to-end:` command, because `run` gives no exit code. Each part is
|
||||||
|
// read by its heading or marker, never by line number.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||||
|
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||||
|
|
||||||
|
/** From the line holding `marker` to the first line where `stop` is true. */
|
||||||
|
function partAt(rel, marker, stop) {
|
||||||
|
const lines = read(rel).split('\n');
|
||||||
|
const start = lines.findIndex((l) => l.includes(marker));
|
||||||
|
assert.ok(start !== -1, `${rel} must hold "${marker}"`);
|
||||||
|
let end = lines.length;
|
||||||
|
for (let i = start + 1; i < lines.length; i++) if (stop(lines[i])) { end = i; break; }
|
||||||
|
return lines.slice(start, end).join(' ').replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARTS = [
|
||||||
|
// An indented paragraph of a numbered list item: it ends at a blank or unindented line.
|
||||||
|
['commands/trekplan.md', '**The "run the thing" step.**', (l) => l.trim() === '' || /^\S/.test(l)],
|
||||||
|
['commands/trekexecute.md', '### End to end — "run the thing" (P9 (6))', (l) => l.startsWith('### ')],
|
||||||
|
['templates/plan-template.md', '### Step 2: {description — the "run the thing" step}', (l) => l.startsWith('### ')],
|
||||||
|
];
|
||||||
|
|
||||||
|
// The template quotes the names instead of backticking them: every backticked
|
||||||
|
// span on a template step bullet is read as a runnable example
|
||||||
|
// (tests/lib/skills-criteria-prose-pins.test.mjs), and `run` is no command.
|
||||||
|
const RUN = '(?:`run`|"run")';
|
||||||
|
for (const [rel, marker, stop] of PARTS) {
|
||||||
|
test(`run coupling: ${rel} names Claude Code's run and keeps the verdict on the End-to-end: command`, () => {
|
||||||
|
const p = partAt(rel, marker, stop);
|
||||||
|
assert.match(p, new RegExp(`Claude Code's built-in ${RUN} skill`), `${rel}: the run step must name Claude Code's built-in run`);
|
||||||
|
assert.match(p, /the verdict is still the `?End-to-end:`? command/, `${rel}: the verdict must stay the End-to-end: command`);
|
||||||
|
assert.match(p, new RegExp(`${RUN} gives no exit code`), `${rel}: the reason must be said`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('run coupling: the run step still says what it runs, through the criteria runner', () => {
|
||||||
|
const p = partAt(...PARTS[0]);
|
||||||
|
assert.match(p, /criteria runner's allowlist/);
|
||||||
|
assert.match(p, /End-to-end: \\`bash tests\/e2e\.sh\\`/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('run coupling: the writing-criteria quiet case is owned by run, with no stand-in left in its description', () => {
|
||||||
|
const md = read('evals/skill-writing-criteria-quiet-2/prompt.md');
|
||||||
|
assert.match(md, /^tags: \[[^\]]*\bowner=run\b[^\]]*\]$/m);
|
||||||
|
assert.doesNotMatch(md, /stands in/);
|
||||||
|
assert.doesNotMatch(md, /owner=trekexecute/);
|
||||||
|
});
|
||||||
|
|
@ -174,6 +174,40 @@ test('routing: a negative must name an owner that exists and outranks the skill'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- R2: an owner from Claude Code's own skills (wave 4, order A2) ----
|
||||||
|
|
||||||
|
const R2_SPEC = (prompt, owner = 'run') => ({
|
||||||
|
skills: { 'tidy-imports': 'Sorts the import lines of a source file. Use when imports are out of order.' },
|
||||||
|
commands: { 'rotate-logs': 'Compresses and deletes old log files on disk.' },
|
||||||
|
cases: { n1: { tags: ['skill-no-trigger', 'skill=tidy-imports', `owner=${owner}`, 'from=test'], prompt } },
|
||||||
|
});
|
||||||
|
|
||||||
|
test('routing (R2): owner=run, a built-in Claude Code skill, is no unknown owner, and the report says what was checked', () => {
|
||||||
|
withPlugin(R2_SPEC('start the app and click through the new screen'), (root) => {
|
||||||
|
const r = checkRouting(root);
|
||||||
|
assert.ok(!r.errors.some((e) => e.case === 'n1'), JSON.stringify(r.errors));
|
||||||
|
const note = (r.notes || []).find((n) => n.case === 'n1');
|
||||||
|
assert.ok(note, JSON.stringify(r));
|
||||||
|
assert.match(note.message, /owner outside the corpus: only "not taken" checked/);
|
||||||
|
const out = spawnSync('node', [CLI, '--root', root], { encoding: 'utf8' });
|
||||||
|
assert.match(out.stdout, /note ROUTING_OWNER_OUTSIDE_CORPUS: n1: owner=run [^\n]*owner outside the corpus: only "not taken" checked/, out.stdout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('routing (R2): a skill that ranks first is still red when the owner is run', () => {
|
||||||
|
withPlugin(R2_SPEC('sort the import lines of the source file'), (root) => {
|
||||||
|
const r = checkRouting(root);
|
||||||
|
assert.ok(r.errors.some((e) => e.code === 'ROUTING_NEGATIVE_WON' && e.case === 'n1'), JSON.stringify(r.errors));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('routing (R2): an owner neither in the corpus nor on the built-in list is still unknown', () => {
|
||||||
|
withPlugin(R2_SPEC('start the app and click through the new screen', 'nosuchthing'), (root) => {
|
||||||
|
const r = checkRouting(root);
|
||||||
|
assert.ok(r.errors.some((e) => e.code === 'ROUTING_UNKNOWN_OWNER' && e.case === 'n1'), JSON.stringify(r.errors));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('routing: every case names the event it came from (P10)', () => {
|
test('routing: every case names the event it came from (P10)', () => {
|
||||||
withPlugin({
|
withPlugin({
|
||||||
skills: { 'tidy-imports': 'Sorts the import lines of a source file. Use when imports are out of order.' },
|
skills: { 'tidy-imports': 'Sorts the import lines of a source file. Use when imports are out of order.' },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue