feat(trekresearch): wire Phase 5 scope marker so the cap hook enforces
pre-agent-cap.mjs (S78) enforces the Phase 5 loop bound only while a scope
marker exists for the calling session. Nothing wrote that marker, so the hook
shipped correct but latent. Phase 5 now writes it at loop start and removes it
on all three exits.
- Write: ${CLAUDE_PLUGIN_DATA}/trekresearch-loop-scope/<session_id>.json with
{runId, startedAt}, keyed by CLAUDE_CODE_SESSION_ID. Verified 2026-08-12 that
this equals the session_id on the hook's PreToolUse payload.
- runId must be the same --run-id the ledger is counted under; a mismatched id
counts zero turns and enforces nothing.
- Fail-soft on write: the hook is defence in depth, research-loop-cap.mjs stays
the gate. Report and continue. The reverse (skipping the budget gate because
a marker exists) stays forbidden.
- Removal on every exit, load-bearing on the exhausted one: the hook keeps
denying WebSearch/WebFetch/Task while the marker is there, and Phase 6 spawns
agents. Crash is covered by the hook TTL, not by cleanup - stated as such
rather than claiming cleanup covers it.
- Marker written in Phase 5, not Phase 4.5: 4.5 mines already-retrieved Phase-4
results and spends no loop turns, so scoping there widens the window for
nothing. Pinned by a test.
Six pins in tests/lib/doc-consistency.test.mjs derive the directory name from
SCOPE_DIRNAME in the hook and the payload fields from marker.runId/startedAt,
so drift in either direction fails. hooks/scripts/pre-agent-cap.mjs untouched.
Verified end-to-end with the snippets as shipped: marker written -> hook allows
under budget, denies 8/8 at budget, allows again after removal; removal is
idempotent; unset CLAUDE_PLUGIN_DATA takes the fail-soft branch.
Suite 937 (935/0/2, baseline 931 + 6).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R77nGJjZ1hqjAQQHefFdnc
This commit is contained in:
parent
fce11a1178
commit
cef3e7fa24
3 changed files with 170 additions and 10 deletions
|
|
@ -445,6 +445,51 @@ own append-only ledger — it never asks this prose how many turns it has used.
|
|||
The loop is **default-off**: `research-loop-cap.mjs` grants a budget of 0
|
||||
unless `VOYAGE_STORM_ENABLED=1`. Doing nothing leaves the mechanism off.
|
||||
|
||||
### Loop scope marker
|
||||
|
||||
`hooks/scripts/pre-agent-cap.mjs` (PreToolUse on `WebSearch|WebFetch|Task`)
|
||||
enforces the same bound in the harness rather than trusting this prose — but it
|
||||
enforces **only** for a session that carries a scope marker, and allows
|
||||
unconditionally for every session that does not. That is what keeps a globally
|
||||
wired PreToolUse hook from denying tool calls in unrelated sessions. Write the
|
||||
marker once, immediately before the first turn:
|
||||
|
||||
```bash
|
||||
# Arms the PreToolUse cap for THIS session only.
|
||||
# CLAUDE_CODE_SESSION_ID is the same id the hook reads as `session_id`.
|
||||
DATA="${CLAUDE_PLUGIN_DATA:-}"
|
||||
case "$DATA" in /*) SCOPE_DIR="$DATA/trekresearch-loop-scope" ;; *) SCOPE_DIR="" ;; esac
|
||||
if [ -n "$SCOPE_DIR" ] && mkdir -p "$SCOPE_DIR" 2>/dev/null; then
|
||||
printf '{"runId":"%s","startedAt":"%s"}\n' "{run_id}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
> "$SCOPE_DIR/${CLAUDE_CODE_SESSION_ID}.json"
|
||||
else
|
||||
echo "[voyage] scope marker not written — harness cap stays inert for this run"
|
||||
fi
|
||||
```
|
||||
|
||||
`runId` MUST be the same `{run_id}` passed to `research-loop-cap.mjs --run-id`.
|
||||
The hook counts ledger lines carrying that id, so a marker written with any
|
||||
other id counts zero turns and enforces nothing.
|
||||
|
||||
**A failed marker write is not a reason to stop.** The hook is defence in
|
||||
depth; `research-loop-cap.mjs` is the gate and stays correct on its own. Report
|
||||
the failure to the operator and run the loop. The reverse — skipping the budget
|
||||
gate because a marker exists — is never allowed.
|
||||
|
||||
**Removal belongs to every exit below, especially the exhausted one.** The hook
|
||||
denies `WebSearch`/`WebFetch`/`Task` once the budget is spent, and it keeps
|
||||
denying for as long as the marker is there — including Phase 6, which spawns
|
||||
agents. A marker that outlives the loop turns a bound on this loop into a brick
|
||||
on the rest of the session. Cleanup covers the three exits and nothing else: a
|
||||
crashed session runs no cleanup at all, and is covered instead by the hook's
|
||||
TTL (default 6h, `VOYAGE_CAP_SCOPE_TTL_MS`), which auto-resets a stale marker.
|
||||
|
||||
```bash
|
||||
# Removal — idempotent, safe to repeat.
|
||||
[ -n "${CLAUDE_PLUGIN_DATA:-}" ] && \
|
||||
rm -f "${CLAUDE_PLUGIN_DATA}/trekresearch-loop-scope/${CLAUDE_CODE_SESSION_ID}.json"
|
||||
```
|
||||
|
||||
### Per-turn protocol
|
||||
|
||||
Each turn targets exactly one under-illuminated dimension, and runs two gates
|
||||
|
|
@ -475,16 +520,18 @@ next under-illuminated dimension, or exit.
|
|||
### Exits (all three, always one of them)
|
||||
|
||||
1. **Converged** — the dimension carries findings with citations and no
|
||||
remaining contradiction. Stop turning on it. This is the normal exit.
|
||||
remaining contradiction. Stop turning on it. This is the normal exit. Once
|
||||
the last dimension has converged, remove the scope marker.
|
||||
2. **Cap exhausted** — `research-loop-cap.mjs` denies the turn. Print the
|
||||
exhaustion **visibly** to the operator, never silently:
|
||||
`Loop bound reached for dimension {dimension} after {N} turns — remaining
|
||||
gaps are carried into the brief as open questions.` A silent cap is
|
||||
indistinguishable from convergence, and that confusion is exactly what this
|
||||
phase exists to prevent.
|
||||
3. **Operator stop** — the operator interrupts. Carry whatever has been
|
||||
gathered into Phase 6 and record the remaining gaps as open questions. Do
|
||||
not re-enter the loop after a stop.
|
||||
phase exists to prevent. Then remove the scope marker — leaving it here is
|
||||
what would block Phase 6.
|
||||
3. **Operator stop** — the operator interrupts. Remove the scope marker first,
|
||||
then carry whatever has been gathered into Phase 6 and record the remaining
|
||||
gaps as open questions. Do not re-enter the loop after a stop.
|
||||
|
||||
### When the loop does not apply
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue