Bug-fix-and-maintenance release. No new features; 9 specific items from the v2.40 R+D experiment (proposal_v2_40.md on the master branch) ship together.
Fixed
Hook resolution order inverted (#1 in the proposal)
UserPromptSubmit, PreToolUse, and PreCompact hook bodies used to check if [ -f task_plan.md ] at the project root FIRST. .planning/.active_plan was consulted only for attestation lookup, never for content lookup. When both a root task_plan.md and a slug-mode plan existed, the root plan silently won and the user's pinned slug plan was bypassed. v2.40 inverts the resolution chain across all three hooks: $PLAN_ID env -> .planning/.active_plan -> newest .planning/<slug>/ by mtime -> root fallback.
.active_plan corruption guards (#2, #3)
.active_plan content is now validated against ^[A-Za-z0-9_][A-Za-z0-9._-]*$. Whitespace-only content, path traversal (../escape), and leading-dot dotfile names fall through cleanly to newest-mtime resolution instead of producing weird path lookups. Dead .active_plan targets (pointing at a deleted plan dir) likewise fall through.
check-complete.sh honors slug-mode (#4)
The script previously defaulted to ./task_plan.md when invoked without arguments. Any caller running in pure-slug-mode saw "No task_plan.md found" even with an active slug plan. The Stop hook in SKILL.md passed the resolved path explicitly, so this was silent. v2.40 wires the script into resolve-plan-dir.sh when no path argument is passed.
Pi extension git push false-positive (#5)
runtime.ts isDangerousBashCommand used substring matching against a flat list including the literal string "git push". Every benign git push origin <branch> fired the warning, training users to ignore it. v2.40 replaces the substring list with a DANGEROUS_BASH_PATTERNS regex array. Only destructive variants now trigger: git push --force / -f / --mirror / +ref, rm -rf, sudo, chmod 777 / chmod a+rwx, git reset --hard, git clean -fd, shell fork bombs, and dd ... of=/dev/[sh]d[a-z].
Performance
mtime-keyed SHA-256 attestation cache (#6)
Every UserPromptSubmit and PreToolUse fire used to run a fresh sha256sum on task_plan.md to compare against the stored attestation. On Windows Git Bash this is ~800ms per fire dominated by bash spawn and disk I/O. v2.40 caches the result under ${TMPDIR:-/tmp}/pwf-sha/<key> keyed by the absolute plan-file path, storing mtime and the hash. On the next fire, if mtime is unchanged, the cached hash is reused without re-running sha256sum. The cache is per-system, transient, and invalidated automatically by any plan edit.
KV-cache hygiene on injected progress.md tail (#7)
The auto-injection feature is most valuable when the model's prefix cache stays warm across turns. The previous injection embedded the literal tail -20 progress.md, including sub-second timestamps and timezone-suffix forms that change every fire. Those bytes mid-prefix prevented cache reuse. v2.40 pipes the tail through sed -E to normalize T<HH:MM:SS>(.<frac>)?Z and T<HH:MM:SS>(.<frac>)?(+|-)HH:MM to a stable form. The model still sees recent progress structure; only the volatile sub-fields are collapsed.
Portability + races
resolve-plan-dir.sh portable mtime (#19)
The old date -r FILE +%s || stat -c '%Y' FILE || echo 0 chain silently fell to 0 on systems lacking GNU coreutils. When mtime resolves to 0 for every dir, newest-by-mtime resolution becomes order-dependent rather than recency-based. v2.40 extends the chain to: GNU stat -c '%Y', BSD stat -f '%m', date -r FILE +%s, python3 -c os.stat, python -c os.stat, perl -e (stat $f)[9], then 0. Covers GNU + BSD + macOS + Windows Git Bash + Alpine + WSL natively; python/perl fallbacks cover the rest.
attest-plan.sh concurrent-write protection (#20)
Concurrent legacy-mode attestations (two sessions in the same cwd with no PLAN_ID) used to race on a non-atomic > .plan-attestation redirect, occasionally producing a truncated file that the hook then read as the expected hash and threw a false [PLAN TAMPERED]. v2.40 writes to a .plan-attestation.tmp.<pid> and renames into place, with flock -w 5 around the rename when flock is available. Test added that spawns 8 concurrent attestations and asserts no corruption.
Verification
130 pass / 2 pre-existing Windows exec-bit failures (test_script_permissions, unchanged from v2.39.0 and unrelated to this release). +20 new tests vs v2.39.0:
tests/test_resolve_plan_dir.py(+5): corruption + dead-target + invalid-slug-scan coveragetests/test_check_complete_resolver.py(+5): resolver wire-up coveragetests/test_plan_attestation.py(+1): concurrent-writer testtests/test_pi_extension_capabilities.py(+1): word-boundary contract testtests/test_hook_body_v240.py(+8): hook-body behavioral tests covering slug-beats-root, legacy-root, silent no-plan, corrupt-.active_planfall-through, SHA cache population, tamper-still-blocks, progress-timestamp normalization, PreToolUse injection
Changed
Version bumped to 2.40.0 across 14 SKILL.md variants + plugin.json + marketplace.json + CITATION.cff via scripts/bump-version.py. .continue, .gemini, .pi, .kiro lag intentionally.
Not changed (deliberate)
- No brainstorm-before-plan gate (#8 in the proposal). Deferred. Changes user-facing workflow shape and deserves its own focused cycle.
- No new sidecar files (
decisions.md,lessons.md,await_approval.md, dispatch queue, checkpoints). All deferred to v2.41 or v3.0. - No refactor of the canonical hook body into a dedicated script (#17). The inline pattern is preserved; the new logic ships within the same single-line idiom. Acknowledged as maintenance debt and tracked for v2.41.
Compatibility
- v1.x users (root
task_plan.mdonly, no.planning/dir): no behavior change. - v2.36+ slug-mode users: slug plans now correctly take precedence over any leftover root file.
- v2.37+ attestation users: tamper detection is unchanged in semantics, faster on warm cache.
- v2.38+ delimiter users:
===BEGIN PLAN DATA===framing preserved. - Pi adapter users:
git push origin <branch>no longer triggers false warnings; destructive variants still do.