# v1.7.7 — Restricted Late-Phase Window for Goal-Stack Hypothesis Test

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

> **Plan version:** v2 (post outside-voice review, 2026-05-09). v1 reviewed in parallel by `senior-code-reviewer` agent + `codex exec -m gpt-5.5 -c model_reasoning_effort=high`. Consolidated 16 revisions: **4 P0** (`run.mjs` made import-safe BEFORE exports — codex caught that `main()` ran unconditionally and would spawn hippo subprocesses on every test import; preflight regex tightened from `\b` to non-digit boundary; C2 sanity band recalibrated for the N=4 lattice; goalposts hard-stop + cumulative-evidence retraction clause), **8 P1** (cross-version comparability lock; tie-degeneracy guard + paired-diff histogram; exact-enumeration sensitivity check at near-threshold; cliff humility; preflight runs analyzer + slice-math tests; inline starvation warning at C2 sanity; reproducibility blob refs; v1.8 pre-commit constraints), **4 P2** (test coverage gaps; name-shadow comment; shell-quirk warning; time-budget realism).

**Goal:** Test whether the goal-stack mechanism produces a ≥10pp drop in late-phase trap-hit-rate when the late-phase metric is narrowed from "last third (7 traps)" to "last 4 trap encounters". v1.7.6 confirmed budget reduction does not discriminate; this plan acts on the pre-committed v1.7.7 escalation. Verdict is DEMONSTRATED, NOT_SUPPORTED, or SANITY_FAIL — published with measured numbers + CIs in every case.

**Architecture:** Three logical pieces, all built on v1.7.5/v1.7.6 infrastructure (do not rebuild). (1) **Workload knob** — `--restrict-late-to <int>` flag on `run.mjs`. When set, late slice = last N trap encounters; early/mid re-split to "first ceil((total-N)/2)" / "remainder" so the three slices stay disjoint and total to all trap tasks. Default `null` preserves current chronological-third behavior. `buildOutput()` records the value in result JSON for audit. (2) **C2 sanity preflight** — same C2-before-C3 design from v1.7.6 P0-2: run C2 only at `--budget 2000 --restrict-late-to 4 --n-seeds 20 --eval-strict`, mechanically check C2 late mean ∈ [0.04, 0.24] AND lower-CI > 0; on FAIL, STOP, do NOT collect C3, retarget to v1.8 adversarial categories. (3) **Hypothesis run + decision** — only if C2 sanity passes. Run C0, C1, C3 at the same workload; analyzer emits verdict (SUPPORTED / NOT_SUPPORTED / SANITY_FAIL / HOOK_FAIL) per pre-registered rule. All three verdict templates committed BEFORE the run.

**Tech Stack:** Node 22.5+ (built-in `node:sqlite`), vitest, the v1.7.5 multi-seed harness (`run.mjs`, `aggregate.mjs`, `traps.mjs`), `pairedPermutationCI` (10k Monte Carlo resamples), `hippo` CLI subprocess via `execSync`. No new dependencies. The starvation-guard re-enable is **deferred to a later release** (see "Scope decisions" below) to keep the v1.7.7 narrative tight on the metric switch.

---

## Pre-investigated facts (anchors — do not re-investigate)

**v1.7.5 / v1.7.6 shipped infrastructure (REUSE, do not rebuild):**
- `--budget` flag end-to-end through `run.mjs` → `simulate()` → `adapter.recall(query, budget)`. Default 2000. **For v1.7.7 budget stays at default — budget is NOT the knob.**
- `benchmarks/sequential-learning/run.mjs::simulate()` accepts `opts.useGoalStack`, `opts.evalStrict`, `opts.budget`. v1.7.7 adds `opts.restrictLateTo`.
- `benchmarks/sequential-learning/run.mjs::hitRateByPhase(results)` (lines 314–329). With 25 trap tasks: third = ceil(25/3) = 9. early=slice(0,9), mid=slice(9,18), late=slice(18) — i.e. **late = last 7 trap encounters**, not 8. The v1.7.5 prereg's "late = last 8" was documentation drift; v1.7.7 corrects to actual `last 7` when restrictLateTo is null.
- `benchmarks/sequential-learning/aggregate.mjs` — `mean`, `stdDev`, `ciHalfWidth95` (returns 0 for n<5; n=20 uses t=2.093), `aggregatePhases`, `pairedPermutationCI` (10k Monte Carlo). Reuse unchanged.
- `benchmarks/sequential-learning/traps.mjs::generateTasks(seed)` — seeded category-to-slot variance. Always produces 25 trap encounters per 50-task seed.
- `benchmarks/sequential-learning/analyze-v1.7.5.mjs` — analyzer template. Single-seed schema confirmed (no per-task `results` in JSON; phase data lives at `j.conditions[cn].phases.{early,mid,late}` and per-seed at `j.conditions[cn].seeds[i].phases.late`). v1.7.6 plan post-review extracted `computeVerdict` into a pure function — adopt same pattern for v1.7.7.
- Hypothesis seeds: `Math.imul(0x9E3779B9, (1000 + i) >>> 0) >>> 0` for i in 0..19. **Reused identically — same paired structure as v1.7.5/v1.7.6.**
- v1.7.6 plan post-review (commit `c670ac9`): hardened mechanical preflight script (P0-4), C2-before-C3 sanity (P0-2), reproducibility-anchor capture (P1-7), no-soft-frame NOT_SUPPORTED template (P0-7), Monte Carlo wording (P1-2). **Adopt verbatim where applicable.**

**v1.7.6 result (the calibration finding to forward-point from):**
- `docs/evals/2026-05-09-v1.7.6-calibration-result.md` — B*=NULL across 5 budgets × 10 seeds; lateMean=0% on every run with default chronological-third late slice. "Floor effect is structural to the 50-task workload, not budget-tunable."
- v1.7.6 CHANGELOG and README pre-committed: "v1.7.7 will sweep `--restrict-late-to last-4`."
- v1.7.6 plan TODOS line: "Re-enable starvation guard in `calibrate.mjs` with correct schema → v1.7.7+." (See "Scope decisions" — deferred.)

**Public-surface status of the −10pp claim (post-v1.7.6, unchanged):**
- The −10pp magnitude is **untested on a discriminating workload**. v1.7.5 saturated, v1.7.6 confirmed budget reduction doesn't bite.
- Locations to walk in claim inventory: `CHANGELOG.md` v1.7.5 + v1.7.6 entries, `README.md` "What's new" sections, `docs/plans/2026-04-29-b3-dlpfc-depth.md` (B3 stretch), `TODOS.md` (the v1.7.7 line itself plus the v1.7.7+ starvation-guard line), `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` (forward-pointer block), `docs/evals/2026-05-09-v1.7.6-calibration-result.md` (forward-pointer block), `RESEARCH.md` (verify).

**Slice math (Option A — locked):**
- Total trap tasks per seed = 25 (deterministic from `traps.mjs`).
- When `restrictLateTo` is `null` (default): late = last 7 (`slice(18)`), early = first 9 (`slice(0,9)`), mid = middle 9 (`slice(9,18)`). Backward-compatible.
- When `restrictLateTo = N` with `N ∈ [1, 25]`: late = last N (`slice(25-N)`), early = first `ceil((25-N)/2)`, mid = remainder.
  - N=4 (the v1.7.7 hypothesis workload): early = first 11 (positions 1-11), mid = 10 (positions 12-21), late = 4 (positions 22-25). Total 25 ✓.
  - N=8 (close to v1.7.5 default): early = ceil(17/2)=9, mid = 8, late = 8. Slightly different from chronological-third (which gives early=9, mid=9, late=7), but disjoint and exhaustive.
- Edge cases: N=0 → late slice empty, return 0; N>25 → late = all traps (early=mid=empty); N<0 → reject in parseArgs with parseInt-NaN-style error.

**Time budget (extrapolated from v1.7.6, post-review P2-4 raised):**
- Task 1 (flag plumbing + tests + import-safety wrap): ~40 min code work.
- Task 2 (pre-reg + claim inventory + v1.8 pre-commit constraints): ~40 min doc work.
- Task 3 (analyzer + verdict tests + tie-degeneracy guard): ~30 min code work.
- Task 4 (C2 preflight + reproducibility blob refs): ~6-8 min subprocess + ~5 min doc work.
- Task 5 (hypothesis run if C2 passes): ~25-35 min subprocess. Realistic per-condition: C0 ~30s; C1 ~30s; C2 ~6-8 min (~20s/seed × 20 seeds at default budget 2000); C3 ~20-30 min (3 hippo subprocess calls per trap-encounter — `pushGoal` + `recall` + `completeGoal` × 25 trap encounters × 20 seeds).
- Task 6 (consequences): ~30 min doc work.
- Task 7 (ship): ~15 min.
- **Total wall time: 2-3 hours; subprocess work: ~35-50 min if C2 passes, ~8-10 min if C2 fails.**

---

## Scope decisions (locked before any code touches)

**1. Slice math: Option A (locked).** Late = last N. Early/mid re-split to "first ceil((total-N)/2)" / "remainder". Disjoint, exhaustive, deterministic. Backward-compatible when `restrictLateTo` is null.

**2. Workload knob: window restriction only.** Budget stays at default 2000 (v1.7.6 confirmed budget isn't the knob). `--use-goal-stack` toggles between C2 and C3. No new workload axes introduced.

**3. Starvation-guard re-enable: DEFERRED.** v1.7.6 plan named this as a "v1.7.7+" task. v1.7.7 ships ONE workload knob change to keep the eval narrative tight; folding in a new schema change to `buildOutput()` would muddy the C2 preflight investigation if it FAILed. Tracked for v1.7.8 or later.

**4. Calibration step: SKIPPED.** v1.7.6 ran a budget calibration. v1.7.7's metric switch has no parameter to sweep — `last-4` is pre-committed in v1.7.6 escalation prose. The C2 sanity preflight (Task 4) replaces calibration as the discrimination check.

**5. Default backward-compat.** Existing callers running `node run.mjs --adapter hippo --n-seeds 20` without `--restrict-late-to` see ZERO behavior change. New `restrictLateTo` field appears in JSON output as `null` for backward-compat (or omitted entirely — plan picks "always present, value can be null" for schema stability).

**6. `run.mjs` made import-safe BEFORE adding exports (post-review P0-1).** `benchmarks/sequential-learning/run.mjs:591` currently calls `main().catch(...)` unconditionally at module load. Adding any `export` to this file would cause every test that imports `hitRateByPhase` or `parseRestrictLateTo` to spawn the full benchmark (50 hippo subprocesses) as an import side-effect, breaking the test suite. Task 1 wraps `main()` in an `invokedAsScript` guard (same pattern `calibrate.mjs` already uses) BEFORE adding any exports. This is the first concrete edit — verified by smoke-test before any other code change.

**7. C2 sanity gate replaces the inherited [4%, 24%] band with an N=4 lattice-aware rule (post-review P0-3).** At N=4 with 20 paired seeds, the lateMean lattice is `k/(20×4) = k/80 ∈ {0, 1.25%, 2.5%, ..., 100%}`. The inherited band `[4%, 24%]` was calibrated to the README 14% headline measured at full-late (last 7 of 25, chronological-third), not at the N=4 lattice. v1.7.7 replaces it with a stronger discrimination check: **`mean ∈ [0.05, 0.50]` AND `≥3 distinct seeds non-zero`**. The `≥3 seeds non-zero` requirement rules out the "lone outlier" failure mode where a single seed-hit could pass the inherited band by accident. The lattice math is documented in the prereg.

---

## Risk register

| Risk | Mitigation |
|------|------------|
| **Goalposts optics on the metric switch (post-review P0-4 — hard-stop added)** | (a) v1.7.6 plan P2-4 pre-committed `--restrict-late-to last-4` as the v1.7.7 escalation BEFORE running v1.7.6 calibration. (b) The decision rule (Δ≥10pp AND lower-CI>0) is unchanged from v1.7.5/v1.7.6. (c) All three verdict templates pre-committed BEFORE eval. (d) Pre-reg framing reworded from "Why this metric switch is not p-hacking" to **"Why this metric switch is a pre-committed adaptive follow-up"** (per codex review — absolute "not p-hacking" framing was too strong for an adaptive sequence over knobs). (e) Pre-reg includes a **HARD-STOP CLAUSE**: if v1.7.7 returns NOT_SUPPORTED, the −10pp magnitude is RETRACTED from CHANGELOG/README headlines; v1.8 ships as a *new hypothesis*, not a continuation. |
| **Import-side-effect breaks tests (post-review P0-1)** | `run.mjs` calls `main()` unconditionally; adding exports without wrapping would spawn hippo subprocesses on every vitest import. Task 1 Step 1 wraps `main()` in an `invokedAsScript` guard FIRST, smoke-tests the import as no-op, then proceeds to add exports. |
| **Tie-degeneracy at N=4 lattice (post-review P1-2)** | At N=4 with heavy ties (likely if C2 saturates near 0%), `pairedPermutationCI` may emit `[0,0]` and the verdict reads as null without surfacing degeneracy. Mitigation: `computeVerdict` accepts a `tiePass` flag; degenerate when `>14/20 paired diffs are exactly 0`. On `tiePass=false`, return `NOT_SUPPORTED` with `degenerate: true` in JSON summary. Result doc emits paired-diff histogram (5-bucket 2-row markdown table). |
| **Cliff-edge cases at Δ near 10pp (post-review P1-3 + P1-4)** | Pre-committed sensitivity check: if `|Δ - 0.10| < 0.02` OR `|ciLow| < 0.02`, ALSO run exact enumeration over 2^20 ≈ 1M sign-flip combinations. If exact and Monte Carlo disagree on verdict, result doc reports both and treats as borderline. NOT_SUPPORTED template has a "within-1pp-of-threshold" branch for honest reporting. |
| **Cross-version comparability of early/mid (post-review P1-1)** | At N=4: early=11/mid=10/late=4. At N=null (chronological-third): early=9/mid=9/late=7. Early/mid memory-load differs across versions. Pre-reg locks reporting scope: "v1.7.7 reports late-phase only as comparable; early/mid/learns are informational and NOT cross-N comparable." |
| **C2 floor persists at last-4** (still saturates at 0% even on the restricted window) | C2 sanity preflight (Task 4) catches this BEFORE C3 runs. SANITY_FAIL → STOP, retarget v1.8 to adversarial categories per pre-reg. C3 not collected on SANITY_FAIL — no goal-stack data leak. |
| **Discrete bounded distribution at N=4** (per-seed late rate ∈ {0, 0.25, 0.5, 0.75, 1.0}) | Documented in pre-reg. 10k Monte Carlo permutation CI handles bounded discrete distributions; not affected by ties. Tie distribution reported alongside the CI in the result doc. |
| **C2 sanity passes but C3 also saturates at 0% (Δ=0pp)** | NOT_SUPPORTED template fires mechanically. Reports the measured Δ ± CI. The mechanism is shipped from v1.7.4; the magnitude isn't demonstrated. No further escalation. |
| **Pre-registration enforcement bypass** | Hardened mechanical preflight script (verbatim from v1.7.6 P0-4, paths adjusted): verifies prereg + claim inventory + analyzer commits reachable from HEAD; verifies prereg pins the metric value (`last-4`); verifies no stale eval result dirs; verifies clean working tree. STOP on any failure. |
| **Reproducibility drift across runs** | Task 4 Step 0 captures `hippo --version`, `which hippo` (resolved path), `git rev-parse HEAD`, `node --version` into the result doc (post-review P1-7). |
| **CHANGELOG soft-frame guard** | NOT_SUPPORTED template forbidden from hedging "on this benchmark at this metric"; v1.7.6 P0-7 fix retained. |
| **Confirmation bias** | Pre-registered decision rule + 3 verdict templates + claim inventory committed BEFORE C2 preflight runs. Analyzer emits verdict mechanically via `computeVerdict()` pure function with 5 fixture tests. |
| **Slice-math regression breaking v1.7.5 / v1.7.6 callers** | Default `restrictLateTo=null` preserves current chronological-third behavior. Tests in Task 1 explicitly verify default-mode output matches pre-v1.7.7 hitRateByPhase. |
| **Subprocess timing creep** | Document expected timing in result doc per v1.7.6 P2-1. C3 with pushGoal/completeGoal at restrictLateTo=4 may be ~15-20 min for 20 seeds. |

---

## Pre-registration commit ordering (CRITICAL — same hardening as v1.7.6 P0-4)

The hypothesis eval **MUST NOT RUN** before the pre-reg + claim inventory + analyzer are committed. Order:

1. Task 1 — flag plumbing + tests → commit
2. Task 2 — pre-reg + claim inventory + forward-pointer to v1.7.6 result → commit
3. Task 3 — analyzer + computeVerdict tests → commit
4. Task 4 — preflight script (mechanical) → run C2 → C2 sanity check → commit C2 results + sanity verdict
5. Task 5 — if C2 passes: run C0/C1/C3 → analyze → write result.md → commit
6. Task 6 — apply consequences (CHANGELOG/README/RESEARCH/TODOS + forward-pointers) → commit
7. Task 7 — `/self-review` → `/review` → `/ship-check` → `/publish-repo`

### Mechanical preflight script (Task 4 Step 1 — adapted from v1.7.6)

```bash
#!/bin/bash
set -euo pipefail

# (a) Required files exist on disk and are committed and reachable from HEAD.
for f in docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md \
         docs/evals/2026-05-09-v1.7.7-claim-inventory.md \
         benchmarks/sequential-learning/analyze-v1.7.7.mjs; do
  test -f "$f" || { echo "STOP: $f missing on disk"; exit 1; }
  sha=$(git log -n1 --pretty=%H -- "$f" 2>/dev/null || true)
  [ -n "$sha" ] || { echo "STOP: $f has no commit"; exit 1; }
  git merge-base --is-ancestor "$sha" HEAD || { echo "STOP: $f commit unreachable from HEAD"; exit 1; }
done

# (b) Pre-reg pins the workload knob value verbatim. Post-review P0-2: tightened
# regex with non-digit/end-of-line trailing assertion so `--restrict-late-to 14`
# (or 24, 34, ...) does NOT silently pass.
grep -qE "(--restrict-late-to[[:space:]]+4|last-4|last 4)([^0-9]|$)" \
  docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md \
  || { echo "STOP: prereg does not pin --restrict-late-to 4"; exit 1; }

# (c) No eval result dirs already exist (would imply earlier silent run).
for d in v1.7.7-eval-C0-none v1.7.7-eval-C1-static v1.7.7-eval-C2-hippo-base v1.7.7-eval-C3-hippo-goalstack; do
  test ! -e "benchmarks/sequential-learning/results/$d" \
    || { echo "STOP: $d already exists; eval would not be a fresh run"; exit 1; }
done

# (d) Working tree clean (allow pre-existing untracked).
[ -z "$(git status --porcelain | grep -v 'settings.local\|stateful-frontier')" ] \
  || { echo "STOP: working tree dirty"; exit 1; }

# (e) Post-review P1-5: analyzer + slice-math tests pass before eval runs. A
# commit that broke `computeVerdict` or `hitRateByPhase` would otherwise pass
# preflight on the strength of file-existence alone.
npx vitest run tests/sl-analyze-v1.7.7.test.ts tests/sl-restrict-late-to.test.ts --reporter=basic \
  || { echo "STOP: analyzer or slice-math tests failing"; exit 1; }

echo "PREFLIGHT OK: prereg pinned --restrict-late-to 4, no stale eval dirs, clean tree, analyzer + slice tests pass."
```

---

## Task 1: Make `run.mjs` import-safe + add `--restrict-late-to <int>` flag + plumb through `hitRateByPhase`

**Why first:** All downstream tasks (preflight, analyzer, eval runs) depend on the new flag being mechanically reliable. TDD-friendly: pure-function slice math.

**Critical pre-change (post-review P0-1):** `benchmarks/sequential-learning/run.mjs:591` currently calls `main().catch(...)` unconditionally. Adding any `export` to this file without first wrapping `main()` in an `invokedAsScript` guard would cause every test that imports `hitRateByPhase` or `parseRestrictLateTo` to spawn the full benchmark (50 hippo subprocesses) as an import side-effect. **Step 1 below is the import-safety wrap; it MUST land before any export is added.**

**Files:**
- Modify: `benchmarks/sequential-learning/run.mjs:589-594` (wrap `main()` call in `invokedAsScript` guard — Step 1)
- Modify: `benchmarks/sequential-learning/run.mjs:35-89` (parseArgs + opts)
- Modify: `benchmarks/sequential-learning/run.mjs:180-285` (simulate signature)
- Modify: `benchmarks/sequential-learning/run.mjs:309-329` (hitRateByPhase signature, `export`)
- Modify: `benchmarks/sequential-learning/run.mjs:393-450` (buildOutput captures restrict_late_to)
- Modify: `benchmarks/sequential-learning/run.mjs:478-510` (runAdapter call site passes opts.restrictLateTo)
- Test: `tests/sl-restrict-late-to.test.ts` (NEW)

**Step 0 (POST-REVIEW P0-1, MUST RUN BEFORE ALL OTHER STEPS): Wrap `main()` in `invokedAsScript` guard**

Edit `benchmarks/sequential-learning/run.mjs:589-594`:

```javascript
// BEFORE
main().catch((err) => {
  console.error('Benchmark failed:', err);
  process.exit(1);
});

// AFTER
// v1.7.7 P0-1 -- run.mjs is now import-safe. main() only runs when invoked as a
// script (matches `calibrate.mjs::invokedAsScript` pattern). Tests can import
// `hitRateByPhase` and `parseRestrictLateTo` without spawning hippo subprocesses.
const invokedAsScript =
  process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (invokedAsScript) {
  main().catch((err) => {
    console.error('Benchmark failed:', err);
    process.exit(1);
  });
}
```

`fileURLToPath` is already imported at line 22.

**Step 0.5 (verify import-safety):**

```bash
node -e "import('./benchmarks/sequential-learning/run.mjs').then(m => console.log('imported clean:', Object.keys(m).join(',')))"
```

Expected: prints "imported clean: ..." with no benchmark output, no subprocess spawn, no crash. If subprocess spawns or `Benchmark failed:` appears: STOP, the guard is wrong.

**Step 0.6 (commit import-safety BEFORE adding exports):**

```bash
git add benchmarks/sequential-learning/run.mjs
git commit -m "$(cat <<'EOF'
refactor(bench): make run.mjs import-safe (P0 prereq for v1.7.7 exports)

Wraps main().catch(...) in an invokedAsScript guard so future exports
from this file (hitRateByPhase, parseRestrictLateTo) do not spawn the
benchmark on test import. Matches calibrate.mjs's existing pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

This is its own atomic commit so the import-safety change is isolated and bisectable.

**Step 1: Write the failing test**

```typescript
// tests/sl-restrict-late-to.test.ts
import { describe, it, expect } from 'vitest';
import { hitRateByPhase, parseRestrictLateTo } from '../benchmarks/sequential-learning/run.mjs';

// Synthetic 25-trap result fixture. Hit pattern: alternate hit/miss for clarity.
function buildTrapResults(hitPattern: boolean[]): Array<{trapCategory: string|null, trapHit: boolean}> {
  return hitPattern.map((hit, i) => ({ trapCategory: 'cat_' + i, trapHit: hit }));
}

describe('v1.7.7 hitRateByPhase with restrictLateTo', () => {
  it('default (no restrictLateTo) preserves chronological-third behavior', () => {
    // 25 traps: third=9, early=slice(0,9), mid=slice(9,18), late=slice(18) = last 7
    const results = buildTrapResults(Array(25).fill(true));
    const phases = hitRateByPhase(results);
    expect(phases.early).toBe(1.0);
    expect(phases.mid).toBe(1.0);
    expect(phases.late).toBe(1.0);
  });

  it('restrictLateTo=4 puts last 4 traps in late, first 11 in early, middle 10 in mid', () => {
    // Build pattern: first 11 all hit, middle 10 half hit, last 4 none hit.
    const pattern = [
      ...Array(11).fill(true),  // early all hit -> 100%
      ...Array(5).fill(true), ...Array(5).fill(false),  // mid 50%
      ...Array(4).fill(false),  // late all miss -> 0%
    ];
    expect(pattern.length).toBe(25);
    const results = buildTrapResults(pattern);
    const phases = hitRateByPhase(results, 4);
    expect(phases.early).toBe(1.0);
    expect(phases.mid).toBe(0.5);
    expect(phases.late).toBe(0.0);
  });

  it('restrictLateTo=4 with all late traps hit returns late=1.0', () => {
    const pattern = [
      ...Array(11).fill(false),  // early all miss
      ...Array(10).fill(false),  // mid all miss
      ...Array(4).fill(true),    // late all hit
    ];
    const results = buildTrapResults(pattern);
    const phases = hitRateByPhase(results, 4);
    expect(phases.late).toBe(1.0);
    expect(phases.early).toBe(0.0);
    expect(phases.mid).toBe(0.0);
  });

  it('restrictLateTo=0 returns late=0 (empty slice)', () => {
    const results = buildTrapResults(Array(25).fill(true));
    const phases = hitRateByPhase(results, 0);
    expect(phases.late).toBe(0);
  });

  it('restrictLateTo greater than total returns all traps as late', () => {
    const results = buildTrapResults(Array(25).fill(true));
    const phases = hitRateByPhase(results, 30);
    expect(phases.late).toBe(1.0);
    expect(phases.early).toBe(0);  // empty slice
    expect(phases.mid).toBe(0);
  });

  it('parseRestrictLateTo rejects negative integers', () => {
    expect(() => parseRestrictLateTo('-1')).toThrow();
  });

  it('parseRestrictLateTo rejects non-numeric input', () => {
    expect(() => parseRestrictLateTo('abc')).toThrow();
  });

  it('parseRestrictLateTo accepts 0 and positive integers', () => {
    expect(parseRestrictLateTo('0')).toBe(0);
    expect(parseRestrictLateTo('4')).toBe(4);
    expect(parseRestrictLateTo('25')).toBe(25);
  });

  // Post-review P2-1 — additional boundary coverage
  it('restrictLateTo=1 puts only the last trap in late', () => {
    const pattern = [...Array(24).fill(false), true]; // last one hit
    const results = buildTrapResults(pattern);
    const phases = hitRateByPhase(results, 1);
    expect(phases.late).toBe(1.0);
    // n-N=24 so early=ceil(24/2)=12, mid=12. Both all-miss.
    expect(phases.early).toBe(0.0);
    expect(phases.mid).toBe(0.0);
  });

  it('restrictLateTo=25 puts all traps in late, early/mid empty -> 0', () => {
    const results = buildTrapResults(Array(25).fill(true));
    const phases = hitRateByPhase(results, 25);
    expect(phases.late).toBe(1.0);
    expect(phases.early).toBe(0); // empty slice
    expect(phases.mid).toBe(0);
  });

  it('parseRestrictLateTo rejects floats and signed numbers', () => {
    expect(() => parseRestrictLateTo('4.5')).toThrow();
    expect(() => parseRestrictLateTo('+4')).toThrow();
    expect(() => parseRestrictLateTo('-1.5')).toThrow();
  });
});
```

**Step 2: Run the failing test**

Run: `npx vitest run tests/sl-restrict-late-to.test.ts`
Expected: FAIL — `hitRateByPhase` doesn't accept the second arg yet; `parseRestrictLateTo` doesn't exist.

**Step 3: Update `hitRateByPhase` signature in run.mjs**

Edit `benchmarks/sequential-learning/run.mjs:309-329`:

```javascript
/**
 * Split trap results into phases and compute hit rate for each.
 *
 * v1.7.7: when `restrictLateTo` is null (default), late = last third of traps
 * (chronological third). When `restrictLateTo = N` (positive integer), late =
 * last N traps; early/mid re-split to "first ceil((total-N)/2)" / "remainder"
 * so the three slices stay disjoint and exhaustive.
 *
 * Post-review P2-2 -- `tests/agent-eval.test.ts` defines a local namesake
 * `hitRateByPhase`. If you import this module function from elsewhere, alias
 * to avoid shadowing the local in that test file.
 *
 * @param {Array<{trapCategory: string|null, trapHit: boolean}>} results
 * @param {number|null} [restrictLateTo=null] - Optional override for late slice size.
 * @returns {{early: number, mid: number, late: number}}
 */
export function hitRateByPhase(results, restrictLateTo = null) {
  const trapTasks = results.filter((r) => r.trapCategory !== null);
  const n = trapTasks.length;

  const rate = (slice) => {
    if (slice.length === 0) return 0;
    return slice.filter((r) => r.trapHit).length / slice.length;
  };

  if (restrictLateTo === null) {
    // Default: chronological third (v1.7.0..v1.7.6 behavior).
    const third = Math.ceil(n / 3);
    return {
      early: rate(trapTasks.slice(0, third)),
      mid: rate(trapTasks.slice(third, third * 2)),
      late: rate(trapTasks.slice(third * 2)),
    };
  }

  // v1.7.7: late = last N. early = first ceil((n-N)/2). mid = remainder.
  const N = Math.max(0, Math.min(restrictLateTo, n));
  const earlyEnd = Math.ceil((n - N) / 2);
  const midEnd = n - N;
  return {
    early: rate(trapTasks.slice(0, earlyEnd)),
    mid: rate(trapTasks.slice(earlyEnd, midEnd)),
    late: rate(trapTasks.slice(midEnd)),
  };
}
```

Note: `hitRateByPhase` is now `export`ed for the test. Verify no other callers in the codebase break (it was previously module-private).

**Step 4: Add `parseRestrictLateTo` helper + export**

Add near `parseArgs`:

```javascript
/**
 * Parse and validate the --restrict-late-to flag value. Returns null if input
 * is the literal string 'null' (programmatic CLI authors). Otherwise expects a
 * non-negative integer; throws otherwise.
 *
 * @param {string} raw
 * @returns {number}
 */
export function parseRestrictLateTo(raw) {
  if (typeof raw !== 'string') {
    throw new Error(`--restrict-late-to expects a string; got ${typeof raw}`);
  }
  if (!/^\d+$/.test(raw)) {
    throw new Error(`--restrict-late-to expects a non-negative integer; got "${raw}"`);
  }
  return parseInt(raw, 10);
}
```

**Step 5: Wire `--restrict-late-to` into parseArgs**

Edit `parseArgs()` opts default and the arg loop:

```javascript
function parseArgs() {
  const args = process.argv.slice(2);
  const opts = {
    adapter: 'all',
    output: join(__dirname, 'results'),
    useGoalStack: false,
    evalStrict: false,
    seed: undefined,
    nSeeds: 1,
    budget: 2000,
    restrictLateTo: null, // v1.7.7 -- null preserves chronological-third behavior.
  };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--adapter' && args[i + 1]) {
      opts.adapter = args[++i];
    } else if (args[i] === '--output' && args[i + 1]) {
      opts.output = args[++i];
    } else if (args[i] === '--use-goal-stack') {
      opts.useGoalStack = true;
    } else if (args[i] === '--eval-strict') {
      opts.evalStrict = true;
    } else if (args[i] === '--seed' && args[i + 1]) {
      opts.seed = parseInt(args[++i], 10);
    } else if (args[i] === '--n-seeds' && args[i + 1]) {
      opts.nSeeds = parseInt(args[++i], 10);
    } else if (args[i] === '--budget' && args[i + 1]) {
      opts.budget = parseInt(args[++i], 10);
    } else if (args[i] === '--restrict-late-to' && args[i + 1]) {
      // v1.7.7 -- narrow the late-phase metric to last N trap encounters.
      opts.restrictLateTo = parseRestrictLateTo(args[++i]);
    } else if (args[i] === '--help' || args[i] === '-h') {
      console.log(/* ...help text... */);
      process.exit(0);
    }
  }

  return opts;
}
```

Update `--help` output to document `--restrict-late-to <int>`:

```
  --restrict-late-to <int>  v1.7.7 -- narrow the late-phase metric to the last
                            <int> trap encounters (early/mid re-split to keep
                            the three slices disjoint). Default: chronological
                            third (~last 7 of 25). Use --restrict-late-to 4 to
                            reproduce the v1.7.7 hypothesis workload.
```

**Step 6: Plumb `opts.restrictLateTo` into `simulate()` → `hitRateByPhase`**

Edit `simulate(adapter, tasks, opts = {})` body. Find where `hitRateByPhase` is called (currently at the bottom of `simulate` it's not — `hitRateByPhase` is called in `runAdapter` after `simulate` returns). Verify with `grep -n "hitRateByPhase" benchmarks/sequential-learning/run.mjs`.

Looking at `runAdapter` around line 482-508:

```javascript
seedRuns.push({
  seed: seed ?? null,
  results,
  overall: trapHitRate(results),
  phases: hitRateByPhase(results),  // <-- line ~505
  hookFailures,
});
```

Change to:

```javascript
seedRuns.push({
  seed: seed ?? null,
  results,
  overall: trapHitRate(results),
  phases: hitRateByPhase(results, opts.restrictLateTo),
  hookFailures,
});
```

(`opts` here = `parseArgs()` output, so `opts.restrictLateTo` is available.)

The `simulate()` call site does NOT need to change — `hitRateByPhase` is called from `runAdapter`, not `simulate`. Verify with grep.

**Step 7: Capture `restrict_late_to` in `buildOutput()`**

Edit `buildOutput(conditionResults)` (around line 393):

```javascript
function buildOutput(conditionResults, opts = {}) {
  const tasks = generateTasks();
  const trapEncounters = tasks.filter((t) => t.trapCategory !== null).length;

  const conditions = {};
  // ... (existing per-condition logic unchanged)

  return {
    benchmark: 'hippo-sequential-learning',
    version: '1.7.7',  // bump for audit
    timestamp: new Date().toISOString(),
    conditions,
    tasks: tasks.length,
    traps: TRAP_CATEGORIES.length,
    trap_encounters: trapEncounters,
    // v1.7.7 -- audit field. null preserves chronological-third behavior.
    restrict_late_to: opts.restrictLateTo ?? null,
  };
}
```

Update the call site (around line 581):

```javascript
const output = buildOutput(conditionResults, opts);
```

**Step 8: Run the test**

Run: `npx vitest run tests/sl-restrict-late-to.test.ts`
Expected: PASS — all 11 tests pass (8 original + 3 boundary coverage from post-review P2-1).

**Step 9: Run the full vitest suite**

Run: `npx vitest run`
Expected: 1459 + 11 = 1470 tests pass, 0 failures. Confirm no v1.7.6 budget-test or v1.7.5 multi-seed test broke from the `hitRateByPhase` signature change.

**Step 10: Smoke-test the new flag end-to-end**

Run:
```bash
node benchmarks/sequential-learning/run.mjs --adapter hippo --n-seeds 1 --budget 2000 --restrict-late-to 4 --output /tmp/v1.7.7-smoke/
```

Expected: completes without error. Inspect output JSON:
```bash
cat /tmp/v1.7.7-smoke/benchmark-*.json | python -c "import json,sys; j=json.load(sys.stdin); print('restrict_late_to:', j.get('restrict_late_to')); print('phases:', j['conditions']['hippo']['phases'])"
```
Expected: `restrict_late_to: 4` AND late rate computed over last 4 traps (per-seed; may be 0 or non-zero depending on seed luck).

Also confirm default behavior unchanged:
```bash
node benchmarks/sequential-learning/run.mjs --adapter hippo --n-seeds 1 --budget 2000 --output /tmp/v1.7.7-default-smoke/
cat /tmp/v1.7.7-default-smoke/benchmark-*.json | python -c "import json,sys; j=json.load(sys.stdin); print('restrict_late_to:', j.get('restrict_late_to'))"
```
Expected: `restrict_late_to: None` (Python null).

**Step 11: Commit**

```bash
git add benchmarks/sequential-learning/run.mjs tests/sl-restrict-late-to.test.ts
git commit -m "$(cat <<'EOF'
feat(bench): --restrict-late-to <int> flag for v1.7.7 narrow-window eval

When set, late = last N trap encounters; early/mid re-split to keep the
three slices disjoint and exhaustive. Default null preserves chronological-
third behavior (v1.7.0..v1.7.6 backward-compat). buildOutput captures the
value as `restrict_late_to` for audit. Adds parseRestrictLateTo helper
that rejects non-numeric / negative input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 2: Pre-registration document + claim inventory + v1.7.6 forward-pointer

**CRITICAL — must be committed BEFORE Task 4 runs.**

**Files:**
- Create: `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md`
- Create: `docs/evals/2026-05-09-v1.7.7-claim-inventory.md`
- Modify: `docs/evals/2026-05-09-v1.7.6-calibration-result.md` (append forward-pointer)

**Step 1: Write `2026-05-09-v1.7.7-goal-stack-eval-prereg.md`**

```markdown
# v1.7.7 Goal-Stack Eval — Pre-Registration

**Pre-registered:** 2026-05-09, BEFORE running the eval.
**Author:** Keith So
**Mechanism under test:** B3 dlPFC goal-stack boost (`applyGoalStackBoost`), shipped v1.7.4.
**Workload variant:** default budget (2000) with `--restrict-late-to 4` — late-phase metric narrowed from "last third (7 of 25)" to "last 4 of 25 trap encounters". Single-line workload tweak preserving the metric definition shift as the only knob.
**Public-surface status of the claim:** the −10pp magnitude was untestable in v1.7.5 (floor effect on full late = last 7) and untested in v1.7.6 (budget reduction confirmed not discriminating). v1.7.7 tests on the restricted late window; result is "supported / not supported", published with measured numbers + CIs either way.

## Hypothesis

H₁: Activating the dlPFC goal-stack boost on top of base hippo reduces late-phase trap-hit-rate by ≥10 percentage points on the v1.7.7 restricted-late workload (`--budget 2000 --restrict-late-to 4`), with 95% CI lower bound > 0pp.

H₀: Activation produces no detectable improvement (95% CI for Δ overlaps 0).

## Why this metric switch is a pre-committed adaptive follow-up (post-review P0-4)

The escalation was pre-committed in the v1.7.6 plan (commit `c670ac9`, "Plan v2 — Why budget reduction, not the other 4 v1.7.5 candidates"):

> Pre-committed escalation order:
> - If v1.7.6 SANITY_FAILs → v1.7.7 will be `--restrict-late-to last-4` (deterministic, single-line workload tweak).
> - If v1.7.7 also fails → v1.8 explores adversarial trap categories (requires new lesson authoring).
> - LongMemEval R@5 corpus is a separate eval line, not a substitute.

v1.7.6 SANITY_FAILed on calibration. v1.7.7 acts on the pre-committed next step. The metric value `4` was named in the v1.7.6 escalation prose BEFORE running the v1.7.6 calibration — so this is not a result-conditional choice.

**This is a pre-committed adaptive follow-up, not p-hacking.** The chain (v1.7.5 → v1.7.6 → v1.7.7) is an adaptive sequence over workload knobs, with each individual knob pre-registered before its eval ran. The decision rule (Δ ≥ 10pp AND lower-CI > 0pp) is unchanged from v1.7.5 and v1.7.6. The seed range (0..19) is unchanged. The four-condition design is unchanged. The metric switch is the only knob in v1.7.7. The C2 sanity band changes because the N=4 lattice required recalibration (see "Sanity gate" below).

## Hard-stop clause (post-review P0-4 — retraction at NOT_SUPPORTED)

If v1.7.7 returns NOT_SUPPORTED, **the −10pp magnitude is RETRACTED from CHANGELOG / README headlines**. v1.8 (adversarial trap categories) ships as a *new hypothesis*, not a continuation of this one. The cumulative evidence after v1.7.5 saturated, v1.7.6 found B*=NULL, and v1.7.7 found Δ < 10pp is sufficient to retire the magnitude claim. The mechanism (B3 dlPFC goal-stack boost) remains shipped from v1.7.4 with no specific magnitude attached — that does not change.

## Reporting scope (post-review P1-1 — cross-version comparability lock)

v1.7.7 reports **late-phase only as the comparable metric**. Early/mid are recorded for audit but are NOT comparable to v1.7.5/v1.7.6 due to the slice re-split:

- v1.7.5 chronological-third: early=9 / mid=9 / late=7 trap encounters.
- v1.7.7 restrict-late-to=4: early=11 / mid=10 / late=4 trap encounters.

Early-phase memory load is 22% larger at N=4 than at the default. **Do not cross-cite early/mid across versions.** "Learns?" boolean is also informational only.

## Protocol

- Benchmark: `benchmarks/sequential-learning` (v1.7.5 multi-seed harness).
- **Workload knob:** `--budget 2000` (default), `--restrict-late-to 4`.
- **Conditions (4):** C0 none, C1 static, C2 hippo-base (`--use-goal-stack` OFF), C3 hippo+goalstack (`--use-goal-stack` ON).
- **Hypothesis seeds: 20.** Hash-derived in `run.mjs` (`Math.imul(0x9E3779B9, 1000+i) >>> 0` for i in 0..19) — same as v1.7.5/v1.7.6.
- **Eval mode:** `--eval-strict`.
- **Primary metric:** late-phase trap-hit-rate over the LAST 4 trap encounters per seed. Slice math (Option A from the plan): late = trapTasks.slice(21..25), early = trapTasks.slice(0..11), mid = trapTasks.slice(11..21). Disjoint and exhaustive.
- **Top-5 cap interaction:** unchanged from v1.7.5 — a task is "trap hit" iff `isRecalled(top5, category) === false`.

## Statistical method (PRE-LOCKED, unchanged primary + new sensitivity check)

- Estimand: mean late-phase Δ in pp = `mean_late(C2) − mean_late(C3)` over 20 paired seeds (positive Δ = goal-stack helped).
- **Primary CI:** 10,000 Monte Carlo resamples of paired sign-flip permutation (`pairedPermutationCI` in `aggregate.mjs`). NOT t-test — phase rates are bounded discrete.
- **Sensitivity check (POST-REVIEW P1-3, PRE-COMMITTED):** If `|Δ - 0.10| < 0.02` OR `|ciLow| < 0.02`, ALSO run **exact enumeration over 2^20 ≈ 1M sign-flip combinations** (computationally cheap; preserves cliff-edge analysis). If exact and Monte Carlo disagree on the verdict, the result doc reports BOTH and treats the result as borderline.
- **Tie-degeneracy guard (POST-REVIEW P1-2, PRE-COMMITTED):** Compute `tiePass = (count of zero paired diffs) / 20 ≤ 0.70`. If `tiePass=false` (>14 of 20 paired diffs are zero), `computeVerdict` returns `NOT_SUPPORTED` with `degenerate: true` in the structured JSON summary. The result doc flags this with: "CI is degenerate due to ties; permutation test underpowered at N=4 lattice."
- **Discrete-distribution caveat:** at N=4 trap encounters per seed, per-seed late rate ∈ {0, 0.25, 0.5, 0.75, 1.0}. Aggregate `lateMean = k/(20×4) = k/80 ∈ {0, 1.25%, 2.5%, ..., 100%}`. Result doc reports a 2-row markdown histogram (rows: C2, C3; columns: 0%, 25%, 50%, 75%, 100%; cells: count of 20 seeds at that lattice value).

## Decision rule (mechanical)

Compute Δ and 95% permutation CI on Δ.

- **Δ ≥ 0.10 AND lower-CI > 0** → **HYPOTHESIS SUPPORTED.** Ship "SUPPORTED" template citing the measured Δ, CI, and per-condition phase rates.
- **Δ < 0.10 OR CI overlaps 0** → **HYPOTHESIS NOT SUPPORTED.** Ship "NOT SUPPORTED" template. The −10pp magnitude was not demonstrated; the mechanism is shipped (v1.7.4); the hypothesis is unsupported.
- **Δ < 0 with upper-CI < 0** = goal-stack measurably HARMS performance → flag for v1.8 design review.

## Stop conditions (mid-run)

- **C2 sanity gate (post-v1.7.6 P0-2 + post-review P0-3 recalibrated for N=4 lattice):** after C2 completes, apply this stricter rule: `mean ∈ [0.05, 0.50]` AND `≥3 distinct seeds non-zero`. If FAIL, **STOP**. Do NOT run C3. Apply SANITY_FAIL template. Retarget to v1.8 adversarial categories per pre-committed escalation. C3 (goal-stack ON) is NOT collected on SANITY_FAIL — no goal-stack data leak.

  *Lattice math:* per-seed late rate at N=4 lives on `{0, 0.25, 0.5, 0.75, 1.0}`. Aggregate over 20 seeds: `lateMean = k/(20×4) = k/80`. The inherited `[4%, 24%]` band (calibrated from a README 14% headline measured at full-late=last 7) is replaced because (a) the band is meaningless on the N=4 lattice (the 4% lower bound corresponds to ~3 hits across all 80 seed-slots, easily satisfied by a single high-rate seed); (b) `[0.05, 0.50]` is a wider headroom band that ensures discrimination room for a ≥10pp drop; (c) the `≥3 distinct seeds non-zero` requirement rules out the lone-outlier failure mode.

- **Hook failure gate (post-C3):** if `C3.hookFailures.push > 0` OR `.complete > 0`, run is contaminated. STOP, fix root cause, restart C3 with fresh result dir.
- **Seed crash policy:** drop the seed across ALL conditions (paired); replacement seed indexed 1020+. No cherry-picking.
- **Outlier policy:** any single seed >3σ from the rest in either condition → investigate and document in result doc.

## Pre-committed v1.8 escalation constraints (POST-REVIEW P1-8)

If v1.7.7 returns SANITY_FAIL or NOT_SUPPORTED, v1.8 ships adversarial trap categories with these PRE-COMMITTED constraints (named here BEFORE running v1.7.7):

- **At least 3 new trap categories** beyond the existing 10 (TRAP_CATEGORIES in `traps.mjs`).
- **Lessons must use vocabulary that has <40% Jaccard overlap with v1.7.5 lessons** (BM25 should NOT trivially match).
- **Target C2 band: `mean ∈ [0.05, 0.50]` AND `≥3 distinct seeds non-zero`** (same rule as v1.7.7 — pre-committed discrimination check, not a README-headline match).
- **Same C2-before-C3 sanity preflight rule** (no goal-stack data collected on SANITY_FAIL).
- **Categories must be authored BEFORE running C3** (no choosing-after-seeing-results).
- **All 4 conditions × 20 seeds**, hash-derived as before.

This pre-commitment exists so v1.8 cannot be retroactively justified by v1.7.7's results.

## Pre-committed report templates

### Template "HYPOTHESIS SUPPORTED" (use IFF the decision rule selects it)

> **Goal-stack lift on sequential-learning benchmark — HYPOTHESIS SUPPORTED.** v1.7.7 restricted-late workload (`--budget 2000 --restrict-late-to 4`), 20 paired seeds, 10k Monte Carlo permutation CI. Late-phase trap-hit-rate dropped by Δ = {DELTA}pp (95% CI [{CI_LOW}pp, {CI_HIGH}pp]). Goal-stack ON: late mean = {C3_LATE} (CI ±{C3_CI}pp). Goal-stack OFF: late mean = {C2_LATE} (CI ±{C2_CI}pp). Sanity anchors — none: {C0_LATE}, static: {C1_LATE}. C2 sanity preflight: {C2_PREFLIGHT_PASS_FAIL} (C2 late = {C2_LATE}, in band [4%, 24%], lower-CI {C2_LOWER_CI}pp > 0). Pre-registration: docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md.

### Template "HYPOTHESIS NOT SUPPORTED" (use IFF the decision rule selects it — NO SOFT FRAMING + cumulative evidence + cliff humility)

> **Goal-stack lift on sequential-learning benchmark — HYPOTHESIS NOT SUPPORTED.** v1.7.7 restricted-late workload (`--budget 2000 --restrict-late-to 4`), 20 paired seeds, 10k Monte Carlo permutation CI {plus exact enumeration if `|Δ - 0.10| < 0.02` or `|ciLow| < 0.02`}. Late-phase Δ = {DELTA}pp (95% CI [{CI_LOW}pp, {CI_HIGH}pp]) does not meet the pre-registered ≥10pp threshold with lower-CI bound above zero. Goal-stack ON: late mean = {C3_LATE} (CI ±{C3_CI}pp). Goal-stack OFF: late mean = {C2_LATE} (CI ±{C2_CI}pp). The −10pp magnitude is not demonstrated. Tie-degeneracy: {tiePass=true|false}. Paired-diff histogram and exact-enumeration result (if applicable) in the result doc.
>
> **{include if `|Δ - 10pp| < 1pp`:}** Δ landed within 1pp of the threshold ({DELTA}pp vs 10pp). Per the pre-reg cliff rule, treated as null. Result is on the threshold; readers should weight the discrete-distribution sample noise accordingly.
>
> **Cumulative evidence (post-review P0-4):** This is the third pre-registered workload variant in which the −10pp magnitude was not demonstrated (v1.7.5: SANITY_FAIL on full-late saturation; v1.7.6: B*=NULL across 5 budgets × 10 seeds; v1.7.7: Δ={DELTA}pp on `--restrict-late-to 4`). Per the v1.7.7 prereg's hard-stop clause, the magnitude is now retracted from public-surface claims; the goal-stack mechanism remains shipped from v1.7.4 with no specific magnitude attached. v1.8 (adversarial trap categories) ships as a new hypothesis, not a continuation of this one.
>
> Pre-registration: docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md.

### Template "SANITY_FAIL" (use IFF C2 preflight fails)

> **Goal-stack lift on sequential-learning benchmark — eval STOPPED per pre-registered C2 sanity gate.** v1.7.7 restricted-late workload (`--budget 2000 --restrict-late-to 4`), 20 seeds. C2 hippo-base late mean = {C2_LATE} ({k of 20 seeds non-zero}), failing the pre-registered N=4 lattice gate (`mean ∈ [0.05, 0.50]` AND `≥3 distinct seeds non-zero`). Floor effect persists at last-4 — narrowing the metric did not produce a discriminating workload. **C3 (goal-stack ON) was NOT collected** — no goal-stack data was produced under SANITY_FAIL. Hypothesis remains untested on a benchmark variant. Per pre-committed escalation (v1.7.6 plan P2-4 + v1.7.7 prereg): v1.8 will explore adversarial trap categories with the constraints named in the v1.7.7 prereg's "Pre-committed v1.8 escalation constraints" section. Calibration: docs/evals/2026-05-09-v1.7.6-calibration-result.md. Pre-registration: docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md.

## Audit trail

- Plan: `docs/plans/2026-05-09-v1.7.7-restricted-late-window.md`
- Code: branch `feat/v1.7.7-restricted-late-window` commits to be filled in.
- Outside-voice review on plan: senior-code-reviewer + codex (gpt-5.5 thinking high) — to run before /full-power execute.
- v1.7.6 forward-pointer: `docs/evals/2026-05-09-v1.7.6-calibration-result.md` (Update appended).
- v1.7.5 forward-pointer (already present): `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md`.
- Claim inventory: `docs/evals/2026-05-09-v1.7.7-claim-inventory.md` (committed alongside this prereg).
```

**Step 2: Write `2026-05-09-v1.7.7-claim-inventory.md`**

```markdown
# v1.7.7 Goal-Stack Claim Inventory

**Pre-eval inventory.** Captures every location in the repo (excluding worktrees, `node_modules`, `results/`, and the v1.7.7 docs themselves) that mentions the −10pp goal-stack lift / "10pp" / "goal-stack" claim, plus prior-release framing that needs an update per the v1.7.7 verdict.

Generated: 2026-05-09, BEFORE running the v1.7.7 hypothesis eval.

## Step 0: Re-grep before populating (post-review v1.7.6 P2-3 retained)

```bash
git grep -n "10pp\|78%\|14%\|sequential-learning\|goal-stack" \
  README.md RESEARCH.md CHANGELOG.md TODOS.md \
  docs/plans/2026-04-29-b3-dlpfc-depth.md
```

Every line hit must appear as a row in the inventory table below.

## Locations referencing the −10pp goal-stack hypothesis (v1.7.7-relevant)

| Path | Line | Context | Action if SUPPORTED | Action if NOT_SUPPORTED | Action if SANITY_FAIL |
|------|------|---------|---------------------|-------------------------|-----------------------|
| `CHANGELOG.md` | v1.7.5 + v1.7.6 entries | "STOPPED per sanity gate" / "B*=NULL, hypothesis remains untested" | Add v1.7.7 entry citing measured Δ; v1.7.5/v1.7.6 lines stay. | Add v1.7.7 entry citing measured Δ < 10pp; v1.7.5/v1.7.6 stay. | Add v1.7.7 entry citing C2 sanity fail; v1.7.5/v1.7.6 stay. |
| `README.md` | "What's new in v1.7.5" + "What's new in v1.7.6" | Same framing as CHANGELOG | Add "What's new in v1.7.7" with SUPPORTED. | Add "What's new in v1.7.7" with NOT_SUPPORTED. | Add "What's new in v1.7.7" with SANITY_FAIL. |
| `RESEARCH.md` | dlPFC / B3 section (verify exact line via grep) | (TBD; v1.7.6 found no matches — re-verify for v1.7.7) | Add measured ΔLate ± CI sentence. | Update to "magnitude not demonstrated, mechanism shipped." | Update to "discriminating workload sweep deferred to v1.8 (adversarial categories)." |
| `docs/plans/2026-04-29-b3-dlpfc-depth.md` | header note | Already points to v1.7.5 result + v1.7.6 calibration | Append "v1.7.7: SUPPORTED, see result doc." | Append "v1.7.7: NOT_SUPPORTED, see result doc." | Append "v1.7.7: SANITY_FAIL on restricted-late window, see result doc." |
| `TODOS.md` line ~144 | "−10pp goal-stack hypothesis test → v1.7.7" | Open v1.7.7 line (current) | Mark `[x]` with link to v1.7.7 result. | Mark `[x]` with link; add note "magnitude not demonstrated." | Keep `[ ]`, retarget to v1.8 (adversarial categories); add note. |
| `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` | "Update 2026-05-09" forward-pointer block | Currently points only to v1.7.6 calibration. Append v1.7.7 verdict line. | "Update 2026-05-09 — v1.7.7 ran on `--restrict-late-to 4`: SUPPORTED, see ..." | Same with NOT_SUPPORTED. | Same with SANITY_FAIL. |
| `docs/evals/2026-05-09-v1.7.6-calibration-result.md` | "Pre-committed escalation" section | Names v1.7.7 + v1.8 escalations | Append "v1.7.7 ran: SUPPORTED, see ..." | Append "v1.7.7 ran: NOT_SUPPORTED, see ..." | Append "v1.7.7 ran: SANITY_FAIL, v1.8 retargeted to adversarial categories." |

## Locations with "10pp" strings UNRELATED (no action — same as v1.7.5/v1.7.6 inventories)

`docs/plans/2026-04-21-model-aware-hippo.md`, `docs/plans/2026-04-21-phase-a-decision.md`, `evals/README.md`. Re-verify with `git grep "10pp"` before claim sweep.

## Locations with "goal-stack" strings describing the MECHANISM (no retraction needed)

Mechanism descriptions in CHANGELOG / RESEARCH / README do not depend on the −10pp magnitude. v1.7.7 result either supports or fails to support the magnitude on the restricted-late workload; the mechanism remains shipped either way.

## Result-doc update procedure (post-decision)

After the analyzer emits a verdict in `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-result.md`, walk this table and apply the column matching the verdict. Commit each file group separately for diff clarity.

## Worktree exclusions

`./.claude/worktrees/agent-*/...` paths are local development scratch and not part of the public surface. Excluded from claim sweep.
```

**Step 3: Append v1.7.6 forward-pointer**

Edit `docs/evals/2026-05-09-v1.7.6-calibration-result.md` — append at the end:

```markdown

## Update 2026-05-09 (forward pointer to v1.7.7)

The "Pre-committed escalation" section above named v1.7.7 → `--restrict-late-to last-4` as the next workload knob. v1.7.7 acts on that escalation:

- Plan: `docs/plans/2026-05-09-v1.7.7-restricted-late-window.md`
- Pre-registration: `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md`
- Claim inventory: `docs/evals/2026-05-09-v1.7.7-claim-inventory.md`
- Result (post-eval): `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-result.md`

Verdict will be one of SUPPORTED / NOT_SUPPORTED / SANITY_FAIL per the pre-registered decision rule.
```

**Step 4: Commit pre-reg + claim inventory + forward-pointer**

```bash
git add docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md \
        docs/evals/2026-05-09-v1.7.7-claim-inventory.md \
        docs/evals/2026-05-09-v1.7.6-calibration-result.md
git commit -m "$(cat <<'EOF'
docs(eval): v1.7.7 pre-registration + claim inventory + v1.7.6 forward-pointer (PRE-eval)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 3: Hypothesis-eval analyzer + verdict tests

**Files:**
- Create: `benchmarks/sequential-learning/analyze-v1.7.7.mjs` (template from `analyze-v1.7.5.mjs`)
- Create: `tests/sl-analyze-v1.7.7.test.ts`

**Step 1: Copy `analyze-v1.7.5.mjs` and rename**

```bash
cp benchmarks/sequential-learning/analyze-v1.7.5.mjs \
   benchmarks/sequential-learning/analyze-v1.7.7.mjs
```

**Step 2: Update result paths and version references**

Edit the new file:
- All 4 `loadLatestJson` calls: replace `v1.7.5-eval-` with `v1.7.7-eval-`.
- Header comment: update from "v1.7.5" to "v1.7.7" and the prereg filename to `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md`.
- `Run:` line: `2026-05-09`.
- `Pre-registration:` line: `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md`.
- `Claim inventory:` line: `docs/evals/2026-05-09-v1.7.7-claim-inventory.md`.
- The 4 result-paths near the bottom: `results/v1.7.7-eval-{C0-none,C1-static,C2-hippo-base,C3-hippo-goalstack}/benchmark-*.json`.

**Step 3: Refactor verdict computation into a pure exported `computeVerdict`**

Add near the top of the file (after imports):

```javascript
/**
 * Pure verdict computation. No I/O. Testable in isolation.
 *
 * Post-review P1-2: `tiePass` flag handles tie-degeneracy at N=4 lattice.
 * When >14 of 20 paired diffs are exactly 0 (`tiePass=false`), the permutation
 * CI is underpowered; verdict is mechanically NOT_SUPPORTED with `degenerate`
 * flag in the structured JSON summary. Caller is responsible for computing
 * `tiePass` from the paired-diff array and passing it in.
 *
 * @param {{c2Late:number, c3Late:number, delta:number, ciLow:number, ciHigh:number,
 *          hookFailures:{push:number, complete:number}|null,
 *          sanityPass:boolean,
 *          tiePass:boolean}} input
 * @returns {'SUPPORTED' | 'NOT_SUPPORTED' | 'SANITY_FAIL' | 'HOOK_FAIL'}
 */
export function computeVerdict({ c2Late, c3Late, delta, ciLow, ciHigh, hookFailures, sanityPass, tiePass }) {
  if (!sanityPass) return 'SANITY_FAIL';
  if ((hookFailures?.push ?? 0) > 0 || (hookFailures?.complete ?? 0) > 0) return 'HOOK_FAIL';
  if (tiePass === false) return 'NOT_SUPPORTED'; // degenerate, surface in result doc
  if (delta >= 0.10 && ciLow > 0) return 'SUPPORTED';
  return 'NOT_SUPPORTED';
}
```

Replace the inline verdict-derivation logic in the analyzer body with a single call to `computeVerdict({...})`. The analyzer's main body is responsible for:

1. Computing `tiePass` from the paired-diff array: `const zeroDiffCount = pairedDiffs.filter(d => d === 0).length; const tiePass = zeroDiffCount / pairedDiffs.length <= 0.70;`
2. Building the paired-diff histogram (5-bucket: 0%, 25%, 50%, 75%, 100%) for both C2 and C3.
3. Calling `computeVerdict` and emitting `verdict`, `degenerate`, and `tiePass` in the JSON summary.
4. If `|delta - 0.10| < 0.02` OR `|ciLow| < 0.02`, ALSO run exact enumeration (post-review P1-3 sensitivity check) — see "exact-enumeration helper" sub-step below.

### Exact-enumeration helper (sub-step for P1-3 sensitivity)

Add to `analyze-v1.7.7.mjs`:

```javascript
/**
 * Exact paired sign-flip enumeration over 2^n combinations. For n=20 this is
 * 2^20 ≈ 1M, computable in <1s. Returns the same {ciLow, ciHigh, deltaMean}
 * shape as `pairedPermutationCI` for direct comparison. Used only when the
 * Monte Carlo result is within 2pp of a decision boundary (post-review P1-3).
 *
 * @param {number[]} c2 - per-seed C2 late rates
 * @param {number[]} c3 - per-seed C3 late rates
 * @param {number} alpha - 0.05 for 95% CI
 * @returns {{deltaMean:number, ciLow:number, ciHigh:number, exact:true}}
 */
export function exactPairedPermutationCI(c2, c3, alpha = 0.05) {
  if (c2.length !== c3.length) throw new Error('c2/c3 length mismatch');
  const n = c2.length;
  if (n > 22) throw new Error(`exact enumeration impractical for n=${n} (2^n > 4M)`);
  const diffs = c2.map((v, i) => v - c3[i]);
  const observed = diffs.reduce((a, b) => a + b, 0) / n;
  const allMeans = [];
  const total = 1 << n; // 2^n
  for (let mask = 0; mask < total; mask++) {
    let sum = 0;
    for (let i = 0; i < n; i++) {
      sum += (mask & (1 << i)) ? -diffs[i] : diffs[i];
    }
    allMeans.push(sum / n);
  }
  allMeans.sort((a, b) => a - b);
  const meanOfNull = allMeans.reduce((a, b) => a + b, 0) / total;
  const loIdx = Math.floor(total * (alpha / 2));
  const hiIdx = Math.ceil(total * (1 - alpha / 2)) - 1;
  return {
    deltaMean: observed,
    ciLow: observed + (allMeans[loIdx] - meanOfNull),
    ciHigh: observed + (allMeans[hiIdx] - meanOfNull),
    exact: true,
  };
}
```

If exact enumeration's verdict differs from Monte Carlo's, the result doc reports both and treats the result as borderline (per pre-reg "Statistical method").

**Step 4: Write `tests/sl-analyze-v1.7.7.test.ts`**

```typescript
// tests/sl-analyze-v1.7.7.test.ts
import { describe, it, expect } from 'vitest';
import { computeVerdict } from '../benchmarks/sequential-learning/analyze-v1.7.7.mjs';

describe('v1.7.7 computeVerdict', () => {
  it('returns SUPPORTED when delta >= 0.10 AND ciLow > 0 AND sanity passes AND tiePass', () => {
    expect(computeVerdict({
      c2Late: 0.18, c3Late: 0.05, delta: 0.13,
      ciLow: 0.04, ciHigh: 0.22,
      hookFailures: { push: 0, complete: 0 }, sanityPass: true,
      tiePass: true,
    })).toBe('SUPPORTED');
  });

  it('returns NOT_SUPPORTED when delta < 0.10', () => {
    expect(computeVerdict({
      c2Late: 0.18, c3Late: 0.13, delta: 0.05,
      ciLow: 0.01, ciHigh: 0.09,
      hookFailures: { push: 0, complete: 0 }, sanityPass: true,
      tiePass: true,
    })).toBe('NOT_SUPPORTED');
  });

  it('returns NOT_SUPPORTED when ciLow <= 0 even if delta >= 0.10', () => {
    expect(computeVerdict({
      c2Late: 0.18, c3Late: 0.05, delta: 0.13,
      ciLow: -0.02, ciHigh: 0.28,
      hookFailures: { push: 0, complete: 0 }, sanityPass: true,
      tiePass: true,
    })).toBe('NOT_SUPPORTED');
  });

  it('returns SANITY_FAIL when sanityPass is false (overrides everything)', () => {
    expect(computeVerdict({
      c2Late: 0.0, c3Late: 0.0, delta: 0.0,
      ciLow: 0.0, ciHigh: 0.0,
      hookFailures: { push: 0, complete: 0 }, sanityPass: false,
      tiePass: true,
    })).toBe('SANITY_FAIL');
  });

  it('returns HOOK_FAIL when sanity passes but hook failures > 0', () => {
    expect(computeVerdict({
      c2Late: 0.18, c3Late: 0.05, delta: 0.13,
      ciLow: 0.04, ciHigh: 0.22,
      hookFailures: { push: 1, complete: 0 }, sanityPass: true,
      tiePass: true,
    })).toBe('HOOK_FAIL');
  });

  // Post-review P1-2 — tie-degeneracy guard
  it('returns NOT_SUPPORTED when tiePass=false even if delta >= 0.10 AND ciLow > 0', () => {
    expect(computeVerdict({
      c2Late: 0.18, c3Late: 0.05, delta: 0.13,
      ciLow: 0.04, ciHigh: 0.22,
      hookFailures: { push: 0, complete: 0 }, sanityPass: true,
      tiePass: false, // degenerate
    })).toBe('NOT_SUPPORTED');
  });
});
```

**Step 5: Run vitest**

Run: `npx vitest run tests/sl-analyze-v1.7.7.test.ts`
Expected: 6 tests pass (5 original + 1 tie-degenerate from post-review P1-2).

Run: `npx vitest run`
Expected: 1470 + 6 = 1476 tests pass, 0 failures.

**Step 6: Smoke-test the analyzer (without real data)**

Run: `node benchmarks/sequential-learning/analyze-v1.7.7.mjs 2>&1 | head -5`
Expected: ENOENT or readdir error mentioning `v1.7.7-eval-*` directories. Confirms the analyzer loads, parses, and fails on missing eval data — NOT on a syntax/import error.

**Step 7: Commit**

```bash
git add benchmarks/sequential-learning/analyze-v1.7.7.mjs \
        tests/sl-analyze-v1.7.7.test.ts
git commit -m "$(cat <<'EOF'
feat(bench): v1.7.7 hypothesis-eval analyzer + computeVerdict tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 4: C2 sanity preflight + apply gate

**Files:**
- Run: `node benchmarks/sequential-learning/run.mjs ...` (1 invocation, C2 only)
- Read: `benchmarks/sequential-learning/results/v1.7.7-eval-C2-hippo-base/`

**Step 0: Reproducibility logging (post-review P1-7 + P2-3 shell warning)**

**Run from `bash` (Git Bash on Windows), not PowerShell.** `which hippo` and the heredoc syntax assume bash.

Capture into a temp file that will be appended to the result doc later. Includes blob refs for `run.mjs`, `aggregate.mjs`, and `analyze-v1.7.7.mjs` so the eval is reproducible at the file-content level even if commit history is later rewritten:

```bash
{
  echo "## Reproducibility anchors"
  echo ""
  echo "- hippo --version = $(hippo --version 2>&1)"
  echo "- which hippo = $(which hippo 2>&1)"
  echo "- git rev-parse HEAD = $(git rev-parse HEAD)"
  echo "- node --version = $(node --version)"
  echo "- npx vitest --version = $(npx vitest --version 2>&1 | head -1)"
  echo "- run.mjs blob = $(git rev-parse HEAD:benchmarks/sequential-learning/run.mjs)"
  echo "- aggregate.mjs blob = $(git rev-parse HEAD:benchmarks/sequential-learning/aggregate.mjs)"
  echo "- analyze-v1.7.7.mjs blob = $(git rev-parse HEAD:benchmarks/sequential-learning/analyze-v1.7.7.mjs)"
  echo "- workload knob = --restrict-late-to 4"
  echo "- budget = 2000 (default)"
  echo "- exact command (per condition) = node benchmarks/sequential-learning/run.mjs --adapter <NAME> --n-seeds 20 --eval-strict --budget 2000 --restrict-late-to 4 [--use-goal-stack for C3] --output benchmarks/sequential-learning/results/v1.7.7-eval-<NAME>/"
  echo "- run start = $(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > /tmp/v1.7.7-eval-anchors.md
cat /tmp/v1.7.7-eval-anchors.md
```

**Step 1: Mechanical preflight (post-review v1.7.6 P0-4)**

Run the hardened preflight script from the "Pre-registration commit ordering" section above. STOP on any failure. Specifically verifies prereg + claim inventory + analyzer are committed and reachable from HEAD; verifies prereg pins `--restrict-late-to 4`; verifies no stale eval result dirs; verifies clean working tree.

```bash
bash <<'EOF'
set -euo pipefail
for f in docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md \
         docs/evals/2026-05-09-v1.7.7-claim-inventory.md \
         benchmarks/sequential-learning/analyze-v1.7.7.mjs; do
  test -f "$f" || { echo "STOP: $f missing on disk"; exit 1; }
  sha=$(git log -n1 --pretty=%H -- "$f" 2>/dev/null || true)
  [ -n "$sha" ] || { echo "STOP: $f has no commit"; exit 1; }
  git merge-base --is-ancestor "$sha" HEAD || { echo "STOP: $f commit unreachable from HEAD"; exit 1; }
done
grep -qE "restrict-late-to.*4\b|--restrict-late-to 4|last 4|last-4" docs/evals/2026-05-09-v1.7.7-goal-stack-eval-prereg.md \
  || { echo "STOP: prereg does not pin --restrict-late-to 4"; exit 1; }
for d in v1.7.7-eval-C0-none v1.7.7-eval-C1-static v1.7.7-eval-C2-hippo-base v1.7.7-eval-C3-hippo-goalstack; do
  test ! -e "benchmarks/sequential-learning/results/$d" \
    || { echo "STOP: $d already exists; eval would not be a fresh run"; exit 1; }
done
[ -z "$(git status --porcelain | grep -v 'settings.local\|stateful-frontier')" ] || { echo "STOP: working tree dirty"; exit 1; }
echo "PREFLIGHT OK: prereg pinned --restrict-late-to 4, no stale eval dirs, clean tree."
EOF
```

Expected: `PREFLIGHT OK ...`. STOP on any other output.

**Step 2: Run condition C2 (hippo-base, no goal-stack)**

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter hippo --n-seeds 20 --eval-strict \
  --budget 2000 --restrict-late-to 4 \
  --output benchmarks/sequential-learning/results/v1.7.7-eval-C2-hippo-base/
```

Expected runtime: ~6-8 min.

**Step 3: C2 sanity check (post-review P0-3 — N=4 lattice gate + P1-6 starvation warning)**

```bash
node --import tsx/esm -e "
  import('node:fs').then(async ({readFileSync, readdirSync}) => {
    const path = await import('node:path');
    const dir = 'benchmarks/sequential-learning/results/v1.7.7-eval-C2-hippo-base';
    const files = readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'latest.json').sort();
    const j = JSON.parse(readFileSync(path.join(dir, files[files.length - 1]), 'utf-8'));
    const condName = Object.keys(j.conditions)[0];
    const seeds = j.conditions[condName].seeds;
    const lateRates = seeds.map(r => r.phases.late);
    const m = lateRates.reduce((a,b)=>a+b,0)/lateRates.length;
    const distinctNonZero = new Set(lateRates.filter(r => r > 0)).size; // distinct lattice values, not seed count
    const seedNonZeroCount = lateRates.filter(r => r > 0).length;
    // POST-REVIEW P0-3: N=4 lattice-aware gate.
    const inBand = m >= 0.05 && m <= 0.50 && seedNonZeroCount >= 3;
    console.log('C2 late: mean=' + (m*100).toFixed(2) + '%, seeds non-zero=' + seedNonZeroCount + '/20, distinct lattice values non-zero=' + distinctNonZero);
    console.log('Sanity gate (POST-REVIEW P0-3): ' + (inBand ? 'PASS' : 'FAIL') + ' (mean ∈ [5%, 50%] AND ≥3 seeds non-zero)');
    console.log('Per-seed late rates:', lateRates.map(r => (r*100).toFixed(0) + '%').join(', '));
    // POST-REVIEW P1-6: inline starvation warning -- if all 3 phases are zero across all 20 seeds, the adapter may be starved.
    const allPhasesZero = seeds.every(s => s.phases.late === 0 && s.phases.mid === 0 && s.phases.early === 0);
    if (allPhasesZero) {
      console.error('WARN: all phases zero across all seeds — possible adapter starvation, not just late-phase floor. Investigate before proceeding.');
    }
    if (!inBand) {
      console.error('STOP: C2 sanity FAIL. C3 will NOT be collected. Apply Task 6 SANITY_FAIL branch.');
      process.exit(1);
    }
  });
" || { echo "C2 sanity FAILED — STOP. Skip Task 5 (C3 + analyzer). Apply SANITY_FAIL branch in Task 6."; exit 1; }
```

If C2 sanity FAILS:
- Skip Task 5 entirely (do NOT collect C0/C1/C3).
- Skip the analyzer run (no full 4-condition data).
- Jump to Task 6 with verdict pre-determined as `SANITY_FAIL`.
- The result doc must explicitly state "C0, C1, C3 not collected; goal-stack data not produced this run."

**Step 4: Commit C2 results regardless of pass/fail**

```bash
# Note: results/ is .gitignored, so this commit captures only the analysis script run + reproducibility anchors that may be tracked. The per-seed JSONs stay local.
# We commit a minimal C2-result snapshot doc if SANITY_FAIL; otherwise keep going to Task 5.
```

If sanity PASSED → continue to Task 5.
If sanity FAILED → write a short SANITY_FAIL result doc inline as part of Task 6 (no separate commit yet; consequences and result-doc land together).

---

## Task 5: Hypothesis run (only if C2 sanity passed) + apply decision rule

**Files:**
- Run: 3 invocations (C0, C1, C3)
- Run: `node benchmarks/sequential-learning/analyze-v1.7.7.mjs > docs/evals/2026-05-09-v1.7.7-goal-stack-eval-result.md 2>/tmp/v1.7.7-eval-summary.json`

**Step 1: Run condition C0 (baseline, no memory)**

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter none --n-seeds 20 --eval-strict \
  --budget 2000 --restrict-late-to 4 \
  --output benchmarks/sequential-learning/results/v1.7.7-eval-C0-none/
```

Expected runtime: ~30s (no subprocess).

**Step 2: Run condition C1 (static memory, all lessons pre-loaded)**

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter static --n-seeds 20 --eval-strict \
  --budget 2000 --restrict-late-to 4 \
  --output benchmarks/sequential-learning/results/v1.7.7-eval-C1-static/
```

Expected runtime: ~30s.

**Step 3: Run condition C3 (hippo + goal-stack)**

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter hippo --n-seeds 20 --use-goal-stack --eval-strict \
  --budget 2000 --restrict-late-to 4 \
  --output benchmarks/sequential-learning/results/v1.7.7-eval-C3-hippo-goalstack/
```

Expected runtime: ~12-15 min.

**Step 4: Run analyzer + capture result**

```bash
node benchmarks/sequential-learning/analyze-v1.7.7.mjs \
  > docs/evals/2026-05-09-v1.7.7-goal-stack-eval-result.md \
  2>/tmp/v1.7.7-eval-summary.json
```

Expected: stdout is the markdown result document; stderr contains the structured JSON summary with the `verdict` field.

**Step 5: Read the verdict mechanically**

```bash
grep -E '"verdict":' /tmp/v1.7.7-eval-summary.json
```

Expected: one of `"verdict": "SUPPORTED"`, `"NOT_SUPPORTED"`, `"SANITY_FAIL"` (if C2 drifted at full-N analysis), or `"HOOK_FAIL"`.

If `HOOK_FAIL`: STOP. Eval contaminated. Investigate `hook_failures` in the C3 JSON, fix root cause, re-run from Step 3. Do NOT proceed to Task 6.

**Step 6: Augment the result doc**

Edit `docs/evals/2026-05-09-v1.7.7-goal-stack-eval-result.md` to add (after the analyzer-emitted Decision section):

- **Reproducibility anchors block** from Task 4 Step 0 (`/tmp/v1.7.7-eval-anchors.md`) — paste at the top, after the verdict line. Add `run end` and `wall-time` rows. Includes blob refs for `run.mjs`, `aggregate.mjs`, `analyze-v1.7.7.mjs` per post-review P1-7.
- restrict_late_to + budget + seed range citations.
- Hook-failure counts.
- **Paired-diff histogram (POST-REVIEW P2-4 — exact format).** Render as a 2-row markdown table:

  ```markdown
  ## Paired-diff histogram (per-seed late rates, N=4 lattice)

  | Condition | 0% | 25% | 50% | 75% | 100% |
  |-----------|----|-----|-----|-----|------|
  | C2 hippo-base     | {count} | {count} | {count} | {count} | {count} |
  | C3 hippo+goalstack | {count} | {count} | {count} | {count} | {count} |

  **Tie analysis:** {zeroDiffCount} of 20 paired diffs (C2−C3) are exactly 0; tiePass = {true|false} (threshold: ≤14/20 zeros).
  ```

- **Exact-enumeration result (POST-REVIEW P1-3, only if `|delta - 0.10| < 0.02` OR `|ciLow| < 0.02`):**

  ```markdown
  ## Exact-enumeration sensitivity check (triggered: {condition})

  Δ = {delta_pp} (95% exact CI [{exact_ciLow_pp}, {exact_ciHigh_pp}]).
  Monte Carlo verdict: {SUPPORTED|NOT_SUPPORTED}. Exact verdict: {SUPPORTED|NOT_SUPPORTED}.
  Verdicts agree: {yes|no}.
  ```
  If verdicts disagree, the result doc treats the result as borderline and reports both prominently.

- C2 sanity preflight result (PASS / FAIL / PASS-with-starvation-warn).
- For SUPPORTED / NOT_SUPPORTED: paste the matching template from the pre-reg with placeholders filled in.
- Investigation notes: timing, anomalous seeds, calibration-vs-hypothesis-C2 drift if any.

**Step 7: Commit results + result doc**

```bash
git add docs/evals/2026-05-09-v1.7.7-goal-stack-eval-result.md
git commit -m "$(cat <<'EOF'
docs(eval): v1.7.7 hypothesis eval result, verdict={VERDICT}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 6: Apply consequences (CHANGELOG / README / RESEARCH / TODOS)

Branch on the verdict from Task 4 (SANITY_FAIL) or Task 5 (SUPPORTED / NOT_SUPPORTED). Walk the claim inventory and apply the matching column.

### Branch 6-A: Verdict = SUPPORTED

- `CHANGELOG.md`: add v1.7.7 entry citing measured Δ ± CI, restrict-late-to=4, seed count.
- `README.md`: add "What's new in v1.7.7" with SUPPORTED template paste.
- `RESEARCH.md`: update dlPFC/B3 section with measured ΔLate ± CI (verify via grep first).
- `docs/plans/2026-04-29-b3-dlpfc-depth.md`: append "v1.7.7: SUPPORTED, see result doc."
- `TODOS.md` line ~144: mark `[x]` and link to result.
- `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md`: append "Update 2026-05-09 — v1.7.7 ran on `--restrict-late-to 4`: SUPPORTED, see result doc."
- `docs/evals/2026-05-09-v1.7.6-calibration-result.md`: append "v1.7.7 ran: SUPPORTED, see result doc."

### Branch 6-B: Verdict = NOT_SUPPORTED

- `CHANGELOG.md`: add v1.7.7 entry citing measured Δ < 10pp; magnitude not demonstrated; mechanism shipped. **No soft-frame.**
- `README.md`: add "What's new in v1.7.7" with NOT_SUPPORTED template paste.
- `RESEARCH.md`: update to "magnitude not demonstrated on the v1.7.7 restricted-late workload; mechanism shipped."
- `docs/plans/2026-04-29-b3-dlpfc-depth.md`: append "v1.7.7: NOT_SUPPORTED, see result doc."
- `TODOS.md`: mark `[x]` with note "magnitude not demonstrated; mechanism shipped."
- v1.7.5 result doc + v1.7.6 calibration doc: append matching forward-pointer.

### Branch 6-C: Verdict = SANITY_FAIL

- `CHANGELOG.md`: add v1.7.7 entry citing C2 sanity fail; C3 not collected; v1.8 retargeted to adversarial categories.
- `README.md`: add "What's new in v1.7.7" with SANITY_FAIL template paste.
- `RESEARCH.md`: update "discriminating workload sweep deferred to v1.8 (adversarial categories)."
- `docs/plans/2026-04-29-b3-dlpfc-depth.md`: append "v1.7.7: SANITY_FAIL on restricted-late window, see result doc."
- `TODOS.md`: keep `[ ]` on the goal-stack hypothesis line, retarget to v1.8, add note. Add new TODO line for "v1.8 — adversarial trap categories" with concrete spec stub.
- v1.7.5 + v1.7.6 forward-pointers: append SANITY_FAIL note.

**Step 1: Identify the verdict**

```bash
grep -E '"verdict":' /tmp/v1.7.7-eval-summary.json
```

(or, if SANITY_FAIL fired in Task 4, the verdict is pre-determined.)

**Step 2: Apply the matching branch above** by editing each file in the table. Surgical edits — do NOT touch unrelated content.

**Step 3: Verify no orphan claims**

```bash
git grep -n "10pp\|goal-stack lift\|−10pp\|-10pp" -- ':!docs/evals/' ':!docs/plans/'
```

Expected: every match is either (a) honest measured-numbers prose per the verdict, or (b) explicitly marked unrelated in the claim inventory.

**Step 4: Run vitest one more time to confirm tests still pass after doc edits**

Run: `npx vitest run`
Expected: 1472 tests pass (no regressions from doc-only changes).

**Step 5: Commit consequences**

```bash
git add CHANGELOG.md README.md RESEARCH.md \
        docs/plans/2026-04-29-b3-dlpfc-depth.md \
        TODOS.md \
        docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md \
        docs/evals/2026-05-09-v1.7.6-calibration-result.md
git commit -m "$(cat <<'EOF'
docs: apply v1.7.7 verdict={VERDICT} consequences

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 7: Ship phase

**Step 1: Run `/self-review`**

Walk every file changed on this branch. Flag any leftover debug code, hardcoded paths, comment-only edits, or off-brief changes. Apply fixes if any.

**Step 2: Run `/review`**

Pre-landing PR-style code review on the branch. Address findings (or defer with note).

**Step 3: Run `/ship-check`**

Get the four-question audit: achievement summary, ship verdict, significance rating, due-diligence checklist.

**Step 4: Run `/publish-repo`**

Bump 1.7.6 → 1.7.7 across all 5 manifests (`package.json`, `openclaw.plugin.json`, `extensions/openclaw-plugin/package.json`, `extensions/openclaw-plugin/openclaw.plugin.json`, `src/version.ts`). Update `package-lock.json` via `npm install`. Build, test, commit, npm publish, tag, push, GitHub Release, post-publish doc audit.

The merge to master goes via `git merge --no-ff feat/v1.7.7-restricted-late-window` per v1.7.6 precedent.

**Step 5: Capture session state**

```bash
hippo remember "v1.7.7 SHIPPED: verdict={VERDICT} on --restrict-late-to 4 workload (default budget, 20 paired seeds, 10k Monte Carlo permutation CI). {one-line summary of the measured numbers + decision}." \
  --tag hippo --tag v1.7.7 --tag eval-result
```

**Step 6: Final TODOS sweep**

Verify `TODOS.md` reflects:
- v1.7.7 line closed (or retargeted to v1.8 adversarial categories if SANITY_FAIL).
- v1.7.7+ "re-enable starvation guard" line still open.
- v1.8.0 vlPFC line still open and unchanged.
- Any new follow-ups added to TODOS with target version.

---

## Outside-voice gate (run BEFORE Task 1 implementation)

This plan is contract-shaped (public claim hangs on the eval result, third release in a row testing the same hypothesis on different workload knobs). Per CLAUDE.md "Outside Voice" rule, dispatch outside voice on this plan BEFORE any code touches.

**Reviewers:**
- `/plan-eng-review` — in-house architecture critique via `senior-code-reviewer` agent.
- `/codex` — cross-model adversarial review (`codex exec -m gpt-5.5 -c model_reasoning_effort=high`, fall back to gpt-5.4 with same settings if gpt-5.5 unavailable, per global memory).

**Brief both reviewers on:**
- Plan file path: `docs/plans/2026-05-09-v1.7.7-restricted-late-window.md`
- Constraint files: `CLAUDE.md`, `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-prereg.md`, `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md`, `docs/evals/2026-05-09-v1.7.6-calibration-result.md`, `benchmarks/sequential-learning/run.mjs`, `benchmarks/sequential-learning/aggregate.mjs`, `benchmarks/sequential-learning/traps.mjs`, `benchmarks/sequential-learning/calibrate.mjs` (post-v1.7.6 bug-fix shape).
- Cap report length at ~600 words each.
- Look for: (a) **goalposts-shifting concerns** on the metric switch — is the pre-committed-escalation framing sufficient, or does it still smell like p-hacking? (b) **slice-math edge cases** in `hitRateByPhase` Option A — does N=4 produce surprising early/mid splits that affect interpretability? (c) **ties at N=4** — discrete bounded distribution at 5 possible values; permutation CI behavior at n=20 with heavy ties? (d) **C2 sanity preflight scope** — should it ALSO check C3 doesn't already saturate? (e) **decision-rule cliff** at Δ=0.099 vs 0.101 — same as v1.7.6 review caught. (f) **honest-reporting weaknesses** in the three pre-committed templates. (g) **starvation-guard deferral** — is deferring it acceptable or does it leave a known-broken contract in the codebase? (h) **claim-inventory holes** — anything missed?

**Consolidate revisions into a single blob** (each item: section reference, one-sentence issue, concrete fix). Apply only after explicit "apply consolidated" from Keith.

---

## DRY / YAGNI / TDD checklist

- ✅ Reuses v1.7.5 multi-seed harness, paired permutation CI, category-to-slot variance, eval-strict mode, goal-stack hooks, `--budget` flag, `--use-goal-stack` flag, `analyze-v1.7.5` template, `computeVerdict` extraction pattern, hardened mechanical preflight, C2-before-C3 sanity, reproducibility anchors, no-soft-frame template guard.
- ✅ One new flag (`--restrict-late-to`), one new analyzer file (template-derived), one new pre-reg + claim inventory + result doc.
- ✅ Slice math is pure-function TDD-friendly with 8 unit tests covering edge cases.
- ✅ `computeVerdict` is pure-function TDD-friendly with 5 fixture tests covering all 4 verdict branches.
- ✅ Pre-registration commit ordering enforced via mechanical preflight script.
- ✅ Three explicit verdict branches in Task 6; no implicit "we'll figure it out" drift.
- ✅ Outside voice runs against the plan before code touches.
- ✅ Starvation-guard re-enable deferred (not folded in) to keep eval narrative tight.

---

## Plan-execution choice

After this plan is reviewed by outside voice and revisions applied, two execution paths:

**1. Subagent-Driven (this session)** — fresh subagent per task slice, code review between tasks, fast iteration. Pick this if Keith wants to ship today.

**2. Parallel Session (separate)** — open new Claude Code session with `superpowers:executing-plans`, batch execution with checkpoints. Pick this if Keith wants to deploy the eval long-running and check back later.

Recommended: Subagent-Driven, since C2 preflight + (conditional) C3 hypothesis run together are ~30-40 min and all in-session — same workflow as v1.7.5 / v1.7.6.
