fix(trekresearch): gate Phase 4.5 on VOYAGE_STORM_ENABLED, not effort alone
CLAUDE.md claimed both STORM phases go inert when the flag is unset. Only Phase 5 did: the flag check lives in research-loop-cap.mjs, and Phase 4.5 never invokes the cap — it was gated on effort: high alone. At high effort with the flag unset, discovery still mined Phase-4 output and mutated the dimension list, so `dimensions` diverged from `dimensions_baseline` and the decline branch was unreachable for half the mechanism. The code was the deviator, so the guard is fixed rather than the claim: the Phase 4.5 skip-guard now names both conditions, with the reason inline. Three surfaces scoped the flag to "the loop" and are corrected with it (README, docs/command-modes, docs/architecture), plus the orchestrator phase map. CLAUDE.md's claim is now true, but its stated MECHANISM was not — Phase 4.5's inertness comes from its own guard, not from the cap module — so that sentence is corrected too. New doc-consistency pin: the flag must be documented as gating both phases on all four reference surfaces. Review finding 00a3af1a. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vPSXe88qp5aqWUqbDNWoF
This commit is contained in:
parent
4c4457f6e9
commit
2e352a7dbb
8 changed files with 55 additions and 9 deletions
|
|
@ -27,8 +27,9 @@ Full flag reference for each command (modes, `--gates`, `--profile`, breaking ch
|
|||
> **STORM bounded loop — default-off, env-gated.** `/trekresearch` Phase 4.5
|
||||
> (dimension discovery, under the existing `maxDimensions: 8` ceiling) and
|
||||
> Phase 5 (bounded multi-turn follow-up) run only at `effort: high` **and** only
|
||||
> when `VOYAGE_STORM_ENABLED=1`; unset, `lib/util/research-loop-cap.mjs` grants
|
||||
> a budget of 0 and both are inert. `TREKRESEARCH_MAX_CONV_TURNS` (default `3`,
|
||||
> when `VOYAGE_STORM_ENABLED=1`; unset, both are inert — Phase 5 because
|
||||
> `lib/util/research-loop-cap.mjs` grants a budget of 0, Phase 4.5 because its
|
||||
> skip-guard reads the flag directly (it never calls the cap). `TREKRESEARCH_MAX_CONV_TURNS` (default `3`,
|
||||
> invalid values fall back to `3`) sets turns per dimension; the budget is that
|
||||
> × `maxDimensions`. `VOYAGE_DISABLE_CAP_HOOK=1` switches off
|
||||
> `hooks/scripts/pre-agent-cap.mjs`, the `PreToolUse` gate that enforces the
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ them costs turns:
|
|||
|
||||
| Env-var | Default | Behavior |
|
||||
|---------|---------|----------|
|
||||
| `VOYAGE_STORM_ENABLED` | _(unset — default-off)_ | `=1` gives the loop a non-zero turn budget. Unset, the budget is 0 and the loop is inert. |
|
||||
| `VOYAGE_STORM_ENABLED` | _(unset — default-off)_ | `=1` gives the Phase 5 loop a non-zero turn budget and lets Phase 4.5 discovery run. Unset, the budget is 0 and **both** phases are inert. |
|
||||
| `TREKRESEARCH_MAX_CONV_TURNS` | `3` | Turns per under-illuminated dimension. Invalid values fall back to `3`, never to unbounded. |
|
||||
| `VOYAGE_DISABLE_CAP_HOOK` | _(unset)_ | `=1` disables the `PreToolUse` hook that enforces the turn budget. |
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ tools: ["Read", "Glob", "Grep", "Write", "Edit", "Bash"]
|
|||
Orchestrator Phase 2 = Command Phase 4 (Parallel research — same
|
||||
command phase; the orchestrator
|
||||
splits selection from launch)
|
||||
(no orchestrator phase)= Command Phase 4.5 (Dimension discovery,
|
||||
high effort only — v5.10)
|
||||
(no orchestrator phase)= Command Phase 4.5 (Dimension discovery, high
|
||||
effort AND VOYAGE_STORM_ENABLED
|
||||
=1 only — v5.10)
|
||||
Orchestrator Phase 3 = Command Phase 5 (Targeted follow-ups; bounded
|
||||
conversation loop at high effort)
|
||||
Orchestrator Phase 4 = Command Phase 6 (Triangulation)
|
||||
|
|
|
|||
|
|
@ -393,7 +393,15 @@ other agents — the value of Gemini is independence.
|
|||
|
||||
## Phase 4.5 — Dimension discovery
|
||||
|
||||
**Skip this phase entirely unless `phase_signal_result.effort == 'high'`.**
|
||||
**Skip this phase entirely unless `phase_signal_result.effort == 'high'` AND
|
||||
`VOYAGE_STORM_ENABLED=1`.** Both conditions, never either.
|
||||
|
||||
This phase never invokes `research-loop-cap.mjs`, so the cap's own flag check
|
||||
does not cover it — the flag has to be read here. Gating on effort alone would
|
||||
leave discovery mutating the dimension list at `effort: high` with the flag
|
||||
unset, making `dimensions` diverge from `dimensions_baseline` and putting the
|
||||
decline branch out of reach for half the mechanism. Doing nothing must leave
|
||||
**both** STORM phases inert.
|
||||
|
||||
Phase 4 retrieves more than the interview knew to ask for. This phase mines
|
||||
that surplus: findings that were **retrieved but unintegrated** — material an
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Imported from `CLAUDE.md` via pointer.
|
|||
- `lib/stats/event-emit.mjs` — single-source stats event emitter for autonomy-gate transitions and main-merge-gate (v3.4.0)
|
||||
- `lib/validators/{brief,research,plan,progress,session-state}-validator.mjs` — schema validators with CLI shims (`node lib/validators/X.mjs --json <path>`)
|
||||
- `lib/validators/architecture-discovery.mjs` — drift-WARN external-contract discovery for `architecture/overview.md`
|
||||
- `lib/util/research-loop-cap.mjs` — stateful, **default-off** turn budget for the `/trekresearch` bounded conversation loop. `allowTurn()` derives the used-turn count from its own append-only JSONL ledger; it never asks the caller how many turns it has spent, because a cap that does is not a cap. Budget = `TREKRESEARCH_MAX_CONV_TURNS` (default `3`, invalid values fall back to `3`) × `maxDimensions` (8, `settings.json:16`). Grants 0 unless `VOYAGE_STORM_ENABLED=1`; missing `CLAUDE_PLUGIN_DATA` denies (fail-closed — the opposite of `event-emit.mjs`, which is telemetry and must never block). CLI shim: `node lib/util/research-loop-cap.mjs --run-id ID --dimension D --effort E`
|
||||
- `lib/util/research-loop-cap.mjs` — stateful, **default-off** turn budget for the `/trekresearch` bounded conversation loop. `allowTurn()` derives the used-turn count from its own append-only JSONL ledger; it never asks the caller how many turns it has spent, because a cap that does is not a cap. Budget = `TREKRESEARCH_MAX_CONV_TURNS` (default `3`, invalid values fall back to `3`) × `maxDimensions` (8, `settings.json:16`). Grants 0 unless `VOYAGE_STORM_ENABLED=1`; missing `CLAUDE_PLUGIN_DATA` denies (fail-closed — the opposite of `event-emit.mjs`, which is telemetry and must never block). The same flag is the second condition on Phase 4.5's skip-guard, which does not call this module: unset, **both** STORM phases are inert. CLI shim: `node lib/util/research-loop-cap.mjs --run-id ID --dimension D --effort E`
|
||||
- `lib/validators/query-privacy-gate.mjs` — gates **every** outbound research query before it leaves the machine; the hard-block tier (secret-shaped strings) is not operator-overridable, so a query that trips it must be reformulated rather than forced through. CLI shim: `node lib/validators/query-privacy-gate.mjs "<query>"`
|
||||
|
||||
Wiring points (replaces previous prose-grep instructions):
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ They run only at `effort: high` (resolved from the brief's `phase_signals`).
|
|||
|
||||
| Env-var | Default | Behavior |
|
||||
|---------|---------|----------|
|
||||
| `VOYAGE_STORM_ENABLED` | _(unset — default-off)_ | `=1` grants the loop a non-zero turn budget. Unset, `research-loop-cap.mjs` grants 0 turns and the loop is inert: doing nothing keeps the mechanism off. |
|
||||
| `VOYAGE_STORM_ENABLED` | _(unset — default-off)_ | `=1` grants the Phase 5 loop a non-zero turn budget and is the second condition on Phase 4.5's skip-guard. Unset, `research-loop-cap.mjs` grants 0 turns and **both** phases are inert: doing nothing keeps the whole mechanism off. |
|
||||
| `TREKRESEARCH_MAX_CONV_TURNS` | `3` | Max turns per under-illuminated dimension. Budget = this × `maxDimensions` (8, `settings.json:16`). Empty, non-numeric, zero, or negative values fall back to `3` — never to unbounded. |
|
||||
| `VOYAGE_DISABLE_CAP_HOOK` | _(unset)_ | `=1` disables `hooks/scripts/pre-agent-cap.mjs`, the `PreToolUse` enforcement of the turn budget. The cap primitive still applies; only the second gate is switched off. |
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ test('trekresearch — Phase 4.5 exists between Phase 4 and Phase 5 with the eff
|
|||
const slice = text.slice(p45, p5);
|
||||
assert.match(
|
||||
slice,
|
||||
/\*\*Skip this phase entirely unless `phase_signal_result\.effort == 'high'`\.\*\*/,
|
||||
/\*\*Skip this phase entirely unless `phase_signal_result\.effort == 'high'`/,
|
||||
'Phase 4.5 must carry the bolded skip-guard in the Phase 3.5 form',
|
||||
);
|
||||
assert.match(slice, /query-privacy-gate\.mjs/,
|
||||
|
|
@ -191,6 +191,25 @@ test('trekresearch — Phase 4.5 exists between Phase 4 and Phase 5 with the eff
|
|||
'Phase 4.5 must augment under the existing maxDimensions ceiling, not raise it');
|
||||
});
|
||||
|
||||
// Phase 4.5 never invokes research-loop-cap.mjs, so the cap's own flag check
|
||||
// does not reach it. Gating on effort alone means unsetting VOYAGE_STORM_ENABLED
|
||||
// leaves half the mechanism live and the decline branch unreachable — while
|
||||
// CLAUDE.md claims both phases go inert. The guard has to name both conditions.
|
||||
test('trekresearch — Phase 4.5 skip-guard is gated on VOYAGE_STORM_ENABLED as well as effort', () => {
|
||||
const text = read();
|
||||
const p45 = text.indexOf('## Phase 4.5 —');
|
||||
const p5 = text.indexOf('## Phase 5 —');
|
||||
const slice = text.slice(p45, p5);
|
||||
|
||||
const guard = slice.slice(0, slice.indexOf('\n\n', slice.indexOf('**Skip this phase')));
|
||||
assert.match(guard, /VOYAGE_STORM_ENABLED/,
|
||||
'the Phase 4.5 skip-guard must name VOYAGE_STORM_ENABLED, not effort alone');
|
||||
assert.match(guard, /\bAND\b|\*\*and\*\*/,
|
||||
'the guard must be a conjunction — both conditions, not either');
|
||||
assert.match(slice, /decline/i,
|
||||
'Phase 4.5 must say why the flag gates it: the decline branch has to stay reachable');
|
||||
});
|
||||
|
||||
test('trekresearch — Independence hard rule carries an explicit Phase 4.5 amendment', () => {
|
||||
const text = read();
|
||||
const rulesIdx = text.indexOf('## Hard rules');
|
||||
|
|
|
|||
|
|
@ -1148,6 +1148,23 @@ test('STORM: VOYAGE_STORM_ENABLED is documented WITH its default-off contract',
|
|||
}
|
||||
});
|
||||
|
||||
// The flag gates BOTH STORM phases, not just the Phase 5 loop. A surface that
|
||||
// scopes it to "the loop" tells an operator that unsetting it still leaves
|
||||
// Phase 4.5 discovery running — which was true until the guard was fixed, and
|
||||
// is the half-off state the decline branch cannot survive.
|
||||
test('STORM: VOYAGE_STORM_ENABLED is documented as gating BOTH phases, not the loop alone', () => {
|
||||
for (const f of STORM_SURFACES) {
|
||||
const t = read(f);
|
||||
const i = t.indexOf('VOYAGE_STORM_ENABLED');
|
||||
const window = t.slice(Math.max(0, i - 400), i + 400);
|
||||
assert.match(
|
||||
window,
|
||||
/4\.5|discovery|dimension discovery|both/i,
|
||||
`${f}: VOYAGE_STORM_ENABLED must be documented as gating Phase 4.5 too, not only the Phase 5 loop`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('STORM: README research-dimension prose stays at the existing 3–8 ceiling', () => {
|
||||
// Phase 4.5 discovers dimensions UNDER settings.json:16's maxDimensions: 8.
|
||||
// Rewriting this prose upward would raise a ceiling the brief asked us to hold.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue