Measured 2026-09-18: nine ordinary shapes of a plan's `## Verification`
section parsed to zero criteria - an untagged fence, a ```text fence, a
markdown table, `## Verification (acceptance)`, `## Verification:`,
`### Verification`, an unclosed fence earlier in the document. Every one came
out as `0 of 0`, NOT OK, exit 1, and Phase 7 then forbade `result: completed`
without anyone being told that the FORMAT, not the code, was the problem.
"The section is empty" and "I cannot read this format" are different facts.
Three changes, one hole:
- The runner reports `NO_CRITERIA` with a source line (`plan.md:NN`) when the
section is there and nothing in it parsed, and names the two forms it does
read. Same for a brief's `## Success Criteria`, so the evidence block the
conformance reviewer gets says which of the two it is looking at rather than
showing an empty table.
- Phase 7 says it out loud instead of failing silently: report the source line
and the two forms, and say that the plan is what failed there, not the run.
- `/trekplan` now pins what it produces to what the runner reads: the heading
is exactly `## Verification`, the criteria are a bullet whose first
backticked span is the command or a shell-tagged fence, and the command must
be one the allowlist runs. A doc-consistency test holds the writer and the
reader together, so a runner that learns a new form must update the source.
Honest about the round trip: the two round-trip tests were GREEN on arrival -
the template already writes the bullet form the runner reads. What was missing
was not the format but the PIN: `/trekplan` mandated neither the heading string
nor the format, so a plan could satisfy the command's own instructions and
still parse to nothing. The tests now hold that.
Red first: 5 of the 7 new tests failed before the change (3 NO_CRITERIA, 2
doc-consistency); the 2 round-trip tests are guards, and said so above.
Suite 1161 (1159/0/2). Gate unchanged: defects 0 of 7, intact, exit 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A denylist in front of /bin/sh is whack-a-mole. Measured 2026-09-18, end to
end through both screens: 5 of 11 named evasions ran with real effect - a
`command` prefix reached git, an escaped `rm` inside a shell fence deleted a
directory, `find -delete` deleted a file, `>|` and `tee` wrote outside the
working tree, a python one-liner deleted the whole tree - and 19 of 28 got
past the refusal list on its own. Every quoting, aliasing and indirection form
of the shell is another mole.
So the screen is now an ALLOWLIST. A criterion runs only when its first word
is a known test runner (npm test, npm run <script package.json declares>,
node --test, vitest, jest, pytest, python -m pytest, uv run pytest,
cargo test, go test, make test, bash <script under tests/>, a read-only git
subcommand) AND the command carries no shell operator and no newline.
Everything else is NOT RUN with the reason said out loud: never run, and never
reported as a failure either - an absent measurement is not a finding. That
also closes the smaller hole in the same file: a bare word a sentence merely
names (`whoami`, `login`, `package.json`) is no longer executed, because it is
not a runner.
REFUSED_BY_POLICY is gone with the list that produced it; a command outside
the allowlist is `unrunnable`, which in plan mode still fells the run and in
brief mode is reported to the reviewer as an absent measurement.
What the allowlist deliberately does NOT do, said in the file and in the
reviewer's rubric: it is not a sandbox. `npm test`, `npm run <script>` and
`make test` run whatever the repo's own package.json/Makefile says they run,
including a script that pushes - that is the repo's responsibility. And it
rejects honest commands too: an env prefix, a project's own binary, anything
piped. A check that needs one of those is declared through
`bash tests/<script>.sh`, the documented way in.
Red first: 6 of the new tests fail against the previous runner (measured with
an always-allow shim so the module still loads), including the end-to-end one
where the canary directory was deleted and files were written outside the
tree. The fixtures move from `true`/`false` to two allowlisted shell fixtures,
because `false` is no longer a runner - the fail case must still be a real
non-zero exit, not an unrun criterion.
Suite 1148 (1146/0/2).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/trekreview passed --cwd and trekexecute Phase 7 did not, so the same criterion
could resolve two ways in the two phases (PM checkpoint 2026-09-18, MINOR).
It stopped being cosmetic with the refusal list: --cwd is the boundary a write
is measured against, so leaving it unset silently moves that boundary to
whatever the process cwd happens to be. Both phases now resolve
CRITERIA_CWD="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
- one form, with a fallback, so a repo-less checkout falls back instead of
passing an empty --cwd. Pinned in tests/lib/doc-consistency.test.mjs.
Red first: the new pin failed on both phases before this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/trekreview now runs the commands a BRIEF declares, and a brief is an artifact
that can arrive from outside the repo. Measured 2026-09-18 on 6cafb4c: the
executor denylist stopped a download piped into a shell, but the remote-writing
git subcommand and a recursive delete of a path both RAN. The denylist screens
catastrophe (root deletion, fork bombs, mkfs); it was never meant to screen an
artifact under review.
A second screen, in the runner and ahead of the denylist, refuses four classes:
- a remote-writing git subcommand. The subcommand is found by walking git's own
options (`-C`, `-c`, `--git-dir`, ... take a value), so `git status` and
`git log` still run and `git -C sub push` does not.
- a recursive delete: any `rm` carrying `-r`/`-rf`/`--recursive`. A plain
`rm build/artifact.txt` still runs.
- a download piped straight into a shell (also caught by the denylist; pinned
here so the runner does not depend on another file for it).
- a write outside the working tree. `/dev/null`-class devices are fine, and so
is anything under the working tree; `~/...`, an absolute path elsewhere, and
a target carrying an unexpanded `$VAR` are refused - the runner cannot know
where a variable points, and guessing is how a screen stops screening.
A refusal is its own outcome, REFUSED_BY_POLICY: the command never reaches a
shell, and `summary.ok` is false in both plan and brief mode. For the reviewer,
REFUSED is like NOT RUN - the absence of a measurement, never on its own a
finding - and the rubric and the evidence block both say so.
Chosen deliberately, and it is stricter than today's habit: writing scratch to
/tmp is refused too. The repo's own example plan does `> /tmp/out`. Verification
output belongs in the working tree; exempting the whole system temp dir would
have made the rule unstatable, since a working tree created under /tmp then
contains its own escape hatch.
NOT covered, stated rather than implied:
- other writing git subcommands (tag, remote, config, gc) - only push is listed
- writes through a wrapper: `sh -c '...'`, `xargs`, `find -exec`, a Makefile
target, a script the criterion invokes. The screen reads the command it is
given, not what that command goes on to do.
- `>` inside a quoted string reads as a redirect, so a criterion echoing a
literal `>` is refused. Fail-closed, on purpose.
- the whole surface still runs with the invoking process's permissions; this is
a refusal list, not a sandbox.
The denylist-layer test now uses a stand-in command with a screen double: the
refusal list catches a recursive delete first, so naming one there would have
stopped exercising the denylist layer at all.
Red first: the 6 new tests failed before this change (`refuseCommand` did not
exist), and the fixture brief's four writes ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A trekplan's `## Verification` section is where the brief's success criteria
land. Phase 7 opened with "**Skip for trekplans.**", and only the multi-session
wave path (Phase 2.6 Step 3) ran master verification. A plan executed in ONE
session therefore reported `completed` without ever running the criteria it was
measured against — the executor's own belief was the only evidence.
The check now exists as code, not as an instruction:
- `lib/verification/criteria-runner.mjs` parses the criteria an artifact
DECLARES (a plan's `## Verification`, a brief's `## Success Criteria`), runs
each command, and returns a verdict built from exit codes. Fail-closed
throughout: a placeholder, a prose-only criterion, or an unavailable screen
is `unrunnable`/`blocked`, never `passed`. A plan with no `## Verification`
section exits 1 — a plan that promises no end-to-end check cannot be reported
as verified.
- Every command is screened through the plugin's own PreToolUse denylist
(`hooks/scripts/pre-bash-executor.mjs`) before it reaches a shell. Chose
invoking that hook over its documented stdin protocol rather than copying its
rules, because a command spawned from node never passes through the Bash tool
and so the hook cannot fire by itself — this keeps exactly one denylist.
- Phase 7 is now "Exit / verification check": session specs run the exit
condition, trekplans run the criteria runner. Phase 4's entry-condition skip
for trekplans stands — a plan carries no entry condition; the exit side is
not symmetrical.
- A failing criterion FELLS the run: `plan_verification.status != "passed"`
forbids `result: completed`. That is clause 2 of the stop-signal contract,
now enforced on the single-session path too.
Red first: `tests/lib/criteria-runner.test.mjs` (26 tests) against two committed
fixture plans, one of which declares a criterion that fails on purpose. The
doc pin in `tests/lib/doc-consistency.test.mjs` guards the wiring — a capability
no phase calls is the same defect wearing a lib/ file; verified red against the
pre-fix Phase 7 (skip present, runner absent, no fell-the-run clause).
Suite: 1108 (1106/0/2), up 27 from 1081.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to 63e78c5. Adversarial review found that the fix had reintroduced,
in a smaller form, the exact defect the order was filed about.
1. The rewritten Check 2 block opened with
`git ls-files --error-unmatch "$PLAN_PATH"` while the assignment lived in
the prose above it ("Set PLAN_PATH to the plan path, then run:"). Nothing
upstream sets it — Check 2 is Phase 2.55, and REPO_ROOT/WORKTREE_DIR are
only created in Phase 2.6 Step 1, which runs after. An agent copying the
block verbatim would have run it with PLAN_PATH empty. The note was prose,
the code was what ran. The block now carries
`PLAN_PATH="{plan-path}"` as its first line, the same placeholder idiom
the rest of the file uses (`BRANCH_NAME="trek/{slug}/session-{N}"`).
The three Check 2 tests could not have caught this: they injected
PLAN_PATH through the environment, supplying what the doc has to supply
itself. They now substitute `{plan-path}` the way an agent does, assert
the placeholder is present, and pass PLAN_PATH="" in the environment so a
block that fails to assign it goes red. Control run with the assignment
line stripped: `fatal: empty string is not a valid pathspec` -> exit 1.
2. Both copied blocks contained an em-dash (one in an `echo` string, one in a
comment). Shell that bash 3.2 executes stays ASCII — a multibyte char
under `set -u` has crashed it before. Replaced with plain `-`; the prose
outside the fences keeps its em-dashes. New test asserts both blocks are
ASCII-clean, with a known-positive proving the detector fires on an
em-dash.
Suite 1022 (1020/0/2) -> 1023 (1021/0/2). No version bump, no release.
63e78c5 stays valid in history: the order archive and two coord messages
point at it.
Co-Authored-By: Claude <claude-opus-5>
Two independent defects took down a real voyage on macOS today
(llm-ingestion-okf, wave 1, zero steps executed). Both paths led to the same
end state: the worktree had no plan.
1. Phase 2.6 Step 2a' derived the project relpath with
`realpath --relative-to`, which is GNU coreutils only. On BSD realpath
(macOS default) the substitution fails silently: PROJECT_REL ends up
empty, `mkdir -p "$wt/"` and `cp ... "$wt//"` both succeed, and
brief.md/plan.md land at the worktree root where no child session looks.
The portable form existed only as a prose note saying an operator "may
substitute" it — the note was prose, the code was what ran. The block now
uses `python3 -c os.path.relpath` (stdlib), resolves both operands with
bare realpath (identical on BSD and GNU, and correct across the macOS
/var -> /private/var symlink), and aborts loudly when the relpath is
empty or escapes the repo instead of dropping files at the root. The
coreutils note is deleted rather than extended: two forms means the next
reader picks, and that pick is what failed.
Measured on this machine 2026-08-31 (Intel Mac, no coreutils):
`realpath --relative-to=...` -> `realpath: illegal option -- -`;
bare `realpath /Users/ktg/.claude` -> correct path, exit 0.
2. Phase 2.55 Check 2 ran `git add {plan-path}` unconditionally. When the
project directory is gitignored (`.claude/projects/` is tool-managed and
local-only — normal, not exotic) the add refuses and the plan never
reaches HEAD. Check 2 now probes with `git check-ignore` and branches:
tracked -> nothing; untracked -> add + commit as before; ignored -> step
aside and let Step 2a' be the delivery path. `git add -f` was rejected as
the fix: forcing operator-local artifacts into history publishes them to
whatever remote the repo pushes to. A `check-ignore` exit code other than
0 or 1 is fatal and stops execution — a probe that failed is not a probe
that answered "not ignored".
Also fixed in the same block: `[ -d research ] && cp -r ...` as the last
statement of the loop body made a project without research/ exit the wave
non-zero.
TDD, Iron Law: tests/commands/trekexecute-parallel-portability.test.mjs
extracts both shell blocks from commands/trekexecute.md by grep-able anchor
and executes them, so the tests bind to what an agent actually copies. The
assertions check FILE PLACEMENT, not exit status — the whole defect is that
the broken form exits 0. Controls, all present before the fix: a
known-positive BSD-realpath stub (bare path resolves, GNU flag rejected with
`illegal option`), a negative control running the old GNU form under that
stub and asserting plan.md lands at the worktree root, a known-positive
Check 2 arm where a non-ignored plan is still added and committed, and a
stubbed fatal `git check-ignore` (128). Red first: 7 failed, 2 passed (the
two controls). Green after: 9/9.
Suite 1013 (1011/0/2) -> 1022 (1020/0/2). No version bump, no release.
Order: 20260831T214411Z-941965142-from-.claude (from .claude).
Co-Authored-By: Claude <claude-opus-5>
S2 of the 2.1.181 upgrade. Schemas verified verbatim against the official
slash-commands and hooks docs before editing (a first-pass camelCase
'disallowedTools' claim was caught and corrected to kebab-case against the doc).
- CC-14 (SHIP): migrate all 7 hooks in hooks/hooks.json to exec-form
{command:"node", args:["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/X.mjs"]}. Doc
recommends exec-form whenever a hook references a path placeholder; protects
consumers installing under a path with spaces. ${CLAUDE_PLUGIN_ROOT}
interpolates inside args (verified). hooks-json-stop-wired test made
form-agnostic (normalizes command+args to one invocation string).
- CC-11 (SHIP): add `disallowed-tools: Agent, TeamCreate` to trekexecute
frontmatter, enforcing its documented "No Agent tool, no TeamCreate" rule.
allowed-tools grants auto-approval but does NOT remove tools from the pool,
so the prior omission left Agent callable; disallowed-tools removes it.
trekexecute is the only command with a documented exclusion.
- CC-15 (DECIDE: keep universal): re-affirm F2 deferral. pre-bash/pre-write
executors stay universal -- session-agnostic safety (rm -rf /, ~/.ssh, .env)
that narrowing to execute-only would only weaken. Header comments corrected.
- CC-10 (DECIDE: design note, no code): no blanket Agent(model:opus) deny rule
-- would break balanced/economy profiles; any model-enforcement must be
profile-aware, deferred into W2. Folded into open question #3.
Matrix updated with S2 resolutions section. Tests 578 pass / 0 fail / 2 skip;
claude plugin validate passes (only pre-existing root-CLAUDE.md warning).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
Slash-command-parseren matcher !`...` selv inne i ```bash markdown-fences,
som gjorde at Phase 8 NEXT-SESSION-PROMPT-template eksekverte ved skill-load
med literale {project_dir}/{next_session_brief_path}/{next_session_label}/
{status}-strenger som argv. Det ga ENOENT på .session-state.local.json.tmp
og blokkerte hele /trekexecute skill-loadet.
Fjern !`...`-wrapperen og merk blokken eksplisitt som runtime-template.
Pattern matcher nå konvensjonen brukt andre steder i samme fil
(linje 202-208) der ```bash brukes for orkestrator-instruksjon uten
auto-eksekvering.
Wave 0 av v4.1-execute — pre-requisite for å låse opp /trekexecute
skill-invokasjon mot .claude/projects/2026-05-08-voyage-v4.1-modellprofiler/
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>