voyage/tests/hooks
Kjell Tore Guttormsen 6dafdf2a2a fix(research-loop-cap): resolve the data root in code so the loop can run
CLAUDE_PLUGIN_DATA is empty in the Bash tool's process env, and the Phase 5
bash snippet is the cap's only caller. resolveLedgerPath() returned null there
and allowTurn() failed closed, so the budget gate denied turn 1 of every real
run: the loop this delivery exists to bound could never spend a turn, and the
pre-registered measurement could not be run at all.

resolveDataRoot() is now the single root for everything the loop writes --
CLAUDE_PLUGIN_DATA when the harness sets it, ~/.claude/voyage when it does
not. Three consumers resolve through it, which is the point: the cap ledger,
the PreToolUse hook's scope-marker lookup, and the command's bash snippets.
A writer and a reader that resolved the root separately are what made the
enforcement hook allow unconditionally in every real run while CLAUDE.md and
docs/architecture.md called it enforcing.

Same root cause, same commit:
- Marker write and remove now share ONE absolute-path guard and one root; the
  write requires a non-empty CLAUDE_CODE_SESSION_ID before composing the path
  (unset, the marker was named `.json`, which no lookup matches and no TTL
  sweep cleans up).
- The per-turn gates resolve VOYAGE_ROOT with a plugin-cache fallback and
  reserve exit 2 for "gate could not run". Interpolating an empty
  ${CLAUDE_PLUGIN_ROOT} ran `node /lib/...` -> exit 1, which the contract read
  as "privacy gate says no" -- an unsatisfiable rewrite loop no query could
  clear.

Two now-unreachable deny branches are removed rather than left as dead safety
claims (allowTurn's no_plugin_data_dir; the hook's uncountable-ledger deny).
The fail-closed stance stays where it is still real: a ledger that cannot be
WRITTEN denies the turn.

Verified end-to-end through the real bash snippets and the real hook with both
variables stripped and HOME sandboxed: marker written under the fallback root,
8 turns spent, 9th denied, hook exits 2, and exits 0 again after removal.
Note: the fallback exit-2 branch fires against the installed v5.9.1 cache,
which predates lib/util/research-loop-cap.mjs -- correct behaviour, and it
clears when the plugin is reinstalled.

Review findings 2670c10a, fbd6d534, 93550dfb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vPSXe88qp5aqWUqbDNWoF
2026-08-12 22:26:46 +02:00
..
agent-cap.test.mjs fix(research-loop-cap): resolve the data root in code so the loop can run 2026-08-12 22:26:46 +02:00
bash-guard.test.mjs fix(hooks): close the bypasses anchoring opened in the destructive-command rule 2026-08-12 22:03:22 +02:00
hooks-json-stop-wired.test.mjs chore(voyage): W3 hardening (S2) — exec-form hooks, enforced disallowed-tools, F2 decision 2026-06-18 12:03:11 +02:00
otel-export-otlp.test.mjs feat(voyage): add lib/exporters/otlp-format.mjs — OTLP/JSON enum-integer SC #13 2026-05-09 09:32:29 +02:00
otel-export-textfile.test.mjs feat(trekresearch): emit and export the five STORM measurement fields 2026-08-09 15:01:15 +02:00
otel-export-token-capture.test.mjs feat(observability): opt-in token capture in Stop hook (VOYAGE_TOKEN_METER) 2026-06-26 14:42:58 +02:00
otel-export-validators.test.mjs fix(exporters): allowlist trekresearch engine field and pin schema fixture agreement 2026-08-09 13:04:47 +02:00
otel-export.test.mjs feat(voyage): add hooks/scripts/otel-export.mjs — Stop-hook orchestration SC #14, opt-in via VOYAGE_EXPORT_MODE 2026-05-09 09:44:13 +02:00
path-guard.test.mjs test(ultraplan-local): add path-guard + bash-guard baseline hook tests (SC8 baseline) 2026-05-04 08:55:49 +02:00
post-compact-flush.test.mjs feat(ultraplan-local): add PostCompact rehydrate hook to re-inject session-state after compaction 2026-05-04 07:57:42 +02:00
worktree-guard.test.mjs feat(ultraplan-local): harden Phase 2.6 wave executor (11 sub-changes for plugin-in-monorepo + gitignored-state topology) 2026-05-04 07:49:45 +02:00