# v1.7.6 — Discriminating Workload Variant 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-07). v1 reviewed in parallel by `senior-code-reviewer` agent + `codex exec -m gpt-5.5 -c model_reasoning_effort=high`. Consolidated 22 revisions: 7 P0 (calibration N bumped 5→10 to clear `ciHalfWidth95` n<5 floor; **C2 sanity preflight inserted between C2 and C3 so SANITY_FAIL never collects goal-stack data**; calibration script schema fix to read `phases.late` not `seeds[0].phases.late`; hardened mechanical preflight script replaces `git log | grep`; explicit `simulate()` opts.budget diff; observable budget-delta test; soft-frame removed from NOT_SUPPORTED template), 10 P1 (Monte Carlo wording fix; calibration seed offset 100→10000 for non-collision; `computeVerdict` extracted + 5 fixture tests; starvation guard; within-band drift soft-warning; SANITY_FAIL causality fix; reproducibility logging; win32 smoke; "why budget" pre-reg paragraph; boundary tests), 5 P2 (time budget realism; drift soft-warning; README inventory grep; v1.7.7+ escalation pre-named; v1.7.5 forward-pointer in Task 4 not Task 7).

**Goal:** Re-run the v1.7.5 4-condition × 20-seed paired eval on a workload variant where C2 (hippo-base) lands inside the pre-registered sanity band [4%, 24%] late-phase trap-hit-rate, leaving headroom for the goal-stack mechanism to demonstrate (or fail to demonstrate) the −10pp lift hypothesis. Apply the v1.7.5 decision rule mechanically and update CHANGELOG / README / RESEARCH.md / TODOS.md per the verdict (DEMONSTRATED, NOT SUPPORTED, or SANITY_FAIL).

**Architecture:** Three logical pieces, all built on v1.7.5 infrastructure (do not rebuild). (1) **Budget plumbing** — `interface.mjs` `recall(query, budget?)`, `run.mjs` `--budget` flag, `hippo.mjs` honors it. The hardcoded `--budget 2000` in `hippo.mjs:84` is the single non-controllable variable blocking the calibration sweep. (2) **Calibration step** — new `benchmarks/sequential-learning/calibrate.mjs` runs C2 only across budget candidates {200, 400, 600, 800, 1000} with N=5 calibration seeds (range 100..104, distinct from hypothesis seeds 0..19), picks B* mechanically per a pre-registered rule (largest budget where C2 late ∈ [4%, 24%] AND lower-CI > 0%). (3) **Hypothesis run** — same v1.7.5 harness at `--budget B*` for 4 conditions × 20 seeds. Pre-registration document, claim inventory, and both report templates committed BEFORE the hypothesis run. New analyzer `analyze-v1.7.6.mjs` emits the verdict mechanically.

**Tech Stack:** Node 22.5+ (built-in `node:sqlite`, `node:crypto`), vitest, the v1.7.5 multi-seed harness (`run.mjs`, `aggregate.mjs`, `traps.mjs`), `pairedPermutationCI` (10k resamples), `hippo` CLI subprocess via `execSync`. No new dependencies.

---

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

**v1.7.5 shipped infrastructure (REUSE, do not rebuild):**
- `benchmarks/sequential-learning/adapters/interface.mjs` — `pushGoal/completeGoal` optional hooks present.
- `benchmarks/sequential-learning/adapters/hippo.mjs:84` — currently hardcodes `--budget 2000` in the `hippo recall` invocation. **This is the bug to fix in Task 1.**
- `benchmarks/sequential-learning/run.mjs` — `--seed`, `--n-seeds`, `--use-goal-stack`, `--eval-strict` flags present. **No `--budget` flag yet.** `simulate()` calls `adapter.recall(task.recallQuery)` without a budget arg.
- `benchmarks/sequential-learning/aggregate.mjs` — `mean`, `stdDev`, `ciHalfWidth95` (n<5 → 0), `aggregatePhases`, `pairedPermutationCI` (10k resamples). Reuse unchanged.
- `benchmarks/sequential-learning/traps.mjs::generateTasks(seed)` — seeded category-to-slot assignment within phase shape groups (EML, ELL, EM, ML). 25 trap encounters per seed.
- `benchmarks/sequential-learning/analyze-v1.7.5.mjs` — applies the v1.7.5 decision rule. Used as template for `analyze-v1.7.6.mjs`.
- Pre-registration convention: `docs/evals/YYYY-MM-DD-vX.Y.Z-goal-stack-eval-prereg.md` + `claim-inventory.md` + `result.md`.
- Seed derivation: `(Math.imul(0x9E3779B9, (1000 + i) >>> 0)) >>> 0` for i in 0..19. Calibration seeds: same formula with i in 100..104 (distinct hash space, avoids correlated mulberry32 streams between calibration and hypothesis runs).

**v1.7.5 result (the floor effect):**
- `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` — C2 = 0.0%, C3 = 0.0%, ΔLate = 0pp. Sanity band [4%, 24%] FAIL. STOP. Floor diagnosis: `--budget 2000` × 50 tasks × 25 trap encounters fully suppresses late-phase traps regardless of goal-stack. Reducing `--budget` is the cleanest knob to put the workload back into the discriminating zone.

**Current hippo CLI surface (verified 2026-05-07):**
- `hippo recall "<query>" --json --budget <N>` — `--budget` parameter is honored end-to-end through `src/cli.ts::cmdRecall`. No CLI changes needed in `src/`.
- `hippo goal push <name>` — prints `g_<16hex>` id on stdout (already integrated by v1.7.5 hooks).
- `hippo goal complete <id> --outcome <0..1>` — already integrated.

**Public-surface status of the −10pp claim (post-v1.7.5):**
- The −10pp magnitude is currently **untested on a discriminating workload**. v1.7.5 explicitly framed itself as "mechanism shipped, hypothesis untested."
- Locations referencing the claim: `CHANGELOG.md` v1.7.5 entry (says STOPPED per sanity gate), `README.md` "What's new in v1.7.5" (same framing), `docs/plans/2026-04-29-b3-dlpfc-depth.md` (B3 stretch, header note already pointing to v1.7.5 result), `TODOS.md:144` (the v1.7.6+ followup line itself), `docs/evals/2026-05-07-v1.7.5-*` (frozen).
- v1.7.6 claim inventory (Task 4) re-walks these and adds any new locations.

**Time budget (extrapolated from v1.7.5 timing, post-review correction):**
- Calibration: 10 seeds × 5 budgets × Node startup (~200ms) × 25 trap encounters × ~150–300ms per `hippo recall` subprocess ≈ 10–15 min total.
- Hypothesis run: 4 conditions × 20 seeds. C0/C1 fast (~30s each). C2 ≈ 6–8 min. C3 ≈ 12–15 min (3 subprocesses per trap: pushGoal + recall + completeGoal). Total ≈ 20–25 min.
- C2 sanity preflight (post-review P0-2): ~6–8 min before C3 starts; if SANITY_FAIL, C3 never runs (saves 12–15 min).
- **Total wall time: 30–45 min realistic, 60 min worst case** if calibration drifts and re-sweep needed at finer granularity. Earlier estimate of "25 min" was optimistic — Node subprocess startup dominates at small budgets.

---

## Risk register

| Risk | Mitigation |
|------|------------|
| **Calibration overfit (N=5 seeds → N=20 hypothesis)** | Calibration uses seed range 100..104; hypothesis uses 0..19. Hash-derived, non-overlapping. Pre-registered both ranges. |
| **Determinism trap (single-run claim)** | Already fixed in v1.7.5 — multi-seed harness with permutation CI. Reused unchanged. |
| **Confounding via budget** (smaller budget changes both retrieval surface AND goal-stack effect) | 4-condition design isolates (C2 − C3) at fixed B*; budget is constant across the comparison. Goal-stack is the only variable between C2 and C3. |
| **Subprocess overhead at small budgets** | Smaller budget means fewer rows ranked, marginally faster. Document timing in calibration result doc. Not a blocker. |
| **Confirmation bias** | Pre-registered decision rule + both report templates + claim inventory committed BEFORE hypothesis run. Analyzer emits verdict mechanically (no human judgment in the verdict path). |
| **Goalposts optics** ("you kept tuning until you got a number") | (a) v1.7.5 STOP was honest — sanity gate fired publicly. (b) v1.7.6 calibration is itself pre-registered with a mechanical B* selection rule. (c) Workload variant fixes a known floor effect on a published spec, not a fishing expedition. (d) The pre-reg doc names the calibration step in the audit trail BEFORE running it. |
| **No budget candidate qualifies** (all C2 means outside [4%, 24%] in calibration) | Pre-reg includes an escalation branch: STOP, document calibration result, escalate to adversarial-categories OR window-restriction in v1.7.7+. Do NOT silently move to a different knob without a second pre-reg. |
| **Calibration B* picks but hypothesis run drifts out of band at N=20** | Sanity gate re-fires on hypothesis run. STOP per pre-reg. Document drift, ship calibration + sanity finding only. Equivalent to v1.7.5 STOP outcome. |
| **Adapter contract regression from budget plumbing** | Task 1 has unit tests covering: (a) recall signature backward compat (calling with no budget still works); (b) hippo.mjs honors the budget when passed (asserted by observable result-count delta at extreme budgets, not by signature alone); (c) baseline/static adapters ignore the budget arg cleanly. |
| **B\* candidate produces near-zero recall surface (starvation)** *(post-review P1-4)* | Calibration script logs per-seed avg `len(results)` returned by hippo. If mean < 1 across all seeds at a candidate budget, mark candidate "starved" and exclude from B* selection. Documented in calibration result doc. |
| **Within-band calibration overfit not caught by sanity gate** *(post-review P1-5)* | Sanity gate detects only out-of-band drift. Within-band overfit (e.g., calibration C2=18%, hypothesis C2=22%, both in band but Δ shifts) is NOT caught by the gate. Mitigation: result doc reports calibration C2 vs hypothesis C2 side-by-side; \|drift\| > 5pp triggers a soft-warning paragraph in the result doc EVEN ON SUPPORTED. |
| **Reproducibility drift across runs** *(post-review P1-7)* | Task 6 Step 0 captures `hippo --version`, `which hippo` (resolved path), `git rev-parse HEAD`, `node --version`, `npx vitest --version` into the result doc. Pinning these makes the run reproducible 6 months later. |
| **Windows shell-quoting fragility in `hippoExec`** *(post-review P1-8)* | Task 1 Step 10 win32 smoke explicitly verifies the calibration subprocess invocation produces parseable JSON under cmd.exe. If smoke fails, pin shell handling explicitly before continuing. |
| **`pairedPermutationCI` is Monte Carlo, not exact** *(post-review P1-2)* | Plan + pre-reg use the precise wording "10k Monte Carlo resamples of paired sign-flip permutation". For n=20, full enumeration is 2^20 ≈ 1M; precommit exact enumeration as a follow-up sensitivity check if needed. |

---

## Pre-registration commit ordering (CRITICAL — mechanically enforced, post-review P0-4)

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

1. Task 1 — budget plumbing (code + tests) → commit
2. Task 2 — calibration script (code + tests) → commit
3. Task 3 — run calibration → write `calibration-result.md` → commit
4. Task 4 — write pre-reg + claim inventory → commit (and append forward-pointer to v1.7.5 result doc per P2-5)
5. Task 5 — write `analyze-v1.7.6.mjs` + verdict tests → commit
6. Task 6 — preflight check (mechanical, see below) → run C2 → C2 sanity check → run C3 → analyze → write `result.md` → commit
7. Task 7 — apply consequences (CHANGELOG / README / RESEARCH.md / TODOS.md) → commit
8. Task 8 — `/self-review` → `/review` → `/ship-check` → `/publish-repo`

### Mechanical preflight script (Task 6 Step 1 — replaces the polite `git log | grep`)

```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-07-v1.7.6-goal-stack-eval-prereg.md \
         docs/evals/2026-05-07-v1.7.6-claim-inventory.md \
         benchmarks/sequential-learning/analyze-v1.7.6.mjs \
         benchmarks/sequential-learning/results/v1.7.6-calibration/calibration-result.json; 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 calibration B* value verbatim.
B_STAR=$(node -e "const j=JSON.parse(require('fs').readFileSync('benchmarks/sequential-learning/results/v1.7.6-calibration/calibration-result.json'));console.log(j.bStar.budget)")
[ "$B_STAR" != "null" ] && [ -n "$B_STAR" ] || { echo "STOP: calibration B* = null; SANITY_FAIL escalation required"; exit 1; }
grep -qE "B\\\\* = ${B_STAR}\\b|--budget ${B_STAR}\\b|budget ${B_STAR}\\b" docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md \
  || { echo "STOP: prereg does not pin B*=${B_STAR}"; exit 1; }

# (c) No eval result dirs already exist (would imply earlier silent run).
for d in v1.7.6-eval-C0-none v1.7.6-eval-C1-static v1.7.6-eval-C2-hippo-base v1.7.6-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.
[ -z "$(git status --porcelain)" ] || { echo "STOP: working tree dirty"; exit 1; }

echo "PREFLIGHT OK: B*=${B_STAR}, prereg pinned, no stale eval dirs, clean tree."
```

This script is the gate. If it fails, STOP — do not proceed under any speed directive. Pasting `--no-verify` style overrides on git commits remains forbidden per CLAUDE.md.

If at any step a sanity gate fires, STOP and document. Do not silently re-run with different parameters. Re-running requires a pre-reg amendment commit.

---

## Task 1: Plumb `--budget` through `run.mjs` → `adapter.recall(query, budget?)`

**Why first:** The hardcoded `--budget 2000` in `hippo.mjs:84` is the single non-controllable variable blocking calibration. This is a 30-line surgical change with backward-compat tests. Once shipped, calibration becomes a one-line invocation per budget candidate.

**Files:**
- Modify: `benchmarks/sequential-learning/adapters/interface.mjs:32-50` — extend `recall` JSDoc signature with optional `budget` param
- Modify: `benchmarks/sequential-learning/adapters/hippo.mjs:82-108` — honor `budget` arg, default to 2000 for backward compat
- Modify: `benchmarks/sequential-learning/adapters/baseline.mjs` — add `budget` param to `recall` (no-op, just signature compat)
- Modify: `benchmarks/sequential-learning/adapters/static.mjs` — add `budget` param to `recall` (no-op, just signature compat)
- Modify: `benchmarks/sequential-learning/run.mjs:35-89` — add `--budget` flag to `parseArgs`; default 2000
- Modify: `benchmarks/sequential-learning/run.mjs:222-225` — pass `opts.budget` into `adapter.recall(task.recallQuery, opts.budget)`
- Modify: `benchmarks/sequential-learning/run.mjs:180-190` — extend `simulate()` opts to accept `budget`
- Test: `tests/sl-adapter-budget.test.ts` (NEW)

**Step 1: Write the failing test (real harness, real CLI subprocess)**

```typescript
// tests/sl-adapter-budget.test.ts
import { describe, it, expect } from 'vitest';
import hippoAdapter from '../benchmarks/sequential-learning/adapters/hippo.mjs';
import baselineAdapter from '../benchmarks/sequential-learning/adapters/baseline.mjs';
import staticAdapter from '../benchmarks/sequential-learning/adapters/static.mjs';

describe('sequential-learning adapter recall budget', () => {
  it('hippo adapter honors the budget arg with OBSERVABLE result-count delta at extreme budgets (post-review P0-6)', async () => {
    await hippoAdapter.init();
    try {
      // Seed with many memories so an extreme-low budget collapses the result set.
      for (let i = 0; i < 30; i++) {
        await hippoAdapter.store(`lesson ${i}: do not commit broken code, item ${i}`, [
          'lesson',
          `lesson_${i}`,
        ]);
      }
      const wide = await hippoAdapter.recall('do not commit broken code', 2000);
      const tiny = await hippoAdapter.recall('do not commit broken code', 1);
      // Budget=1 must collapse the result set well below budget=2000. We assert
      // an OBSERVABLE delta (not just <=), so the test cannot pass if --budget
      // is silently ignored. If hippo's --budget enforcement is strict, tiny=0;
      // if lenient (returns at least 1 hit), it MUST be strictly fewer than wide.
      expect(wide.length).toBeGreaterThanOrEqual(3); // sanity: 30 memories, query matches
      expect(tiny.length).toBeLessThan(wide.length); // STRICT inequality, not <=
    } finally {
      await hippoAdapter.cleanup();
    }
  }, 60_000);

  it('hippo adapter defaults to 2000 when budget arg is omitted (backward compat)', async () => {
    await hippoAdapter.init();
    try {
      await hippoAdapter.store('compat lesson', ['compat']);
      // Single-arg signature must not throw.
      const results = await hippoAdapter.recall('compat lesson');
      expect(Array.isArray(results)).toBe(true);
    } finally {
      await hippoAdapter.cleanup();
    }
  }, 30_000);

  it('baseline adapter ignores the budget arg cleanly', async () => {
    await baselineAdapter.init();
    try {
      const r1 = await baselineAdapter.recall('anything', 100);
      const r2 = await baselineAdapter.recall('anything');
      // Baseline always returns []; budget arg must not throw.
      expect(r1).toEqual([]);
      expect(r2).toEqual([]);
    } finally {
      await baselineAdapter.cleanup();
    }
  });

  it('static adapter ignores the budget arg cleanly', async () => {
    await staticAdapter.init();
    try {
      const r1 = await staticAdapter.recall('overwrite production', 100);
      const r2 = await staticAdapter.recall('overwrite production');
      // Static returns the full pre-loaded lesson set; budget arg must not throw.
      expect(Array.isArray(r1)).toBe(true);
      expect(Array.isArray(r2)).toBe(true);
    } finally {
      await staticAdapter.cleanup();
    }
  });
});
```

**Step 2: Run the failing test**

Run: `npx vitest run tests/sl-adapter-budget.test.ts`
Expected: FAIL — `hippo adapter honors the budget arg when provided` fails because `recall` ignores its second arg today.

**Step 3: Update `interface.mjs` JSDoc**

Edit `benchmarks/sequential-learning/adapters/interface.mjs` line 38:

```javascript
 * @property {(query: string, budget?: number) => Promise<RecallResult[]>} recall - Retrieve memories relevant to a query (return top-5). v1.7.6: optional `budget` controls token budget for retrieval; adapter MAY ignore.
```

**Step 4: Update `hippo.mjs` to honor budget**

Edit `benchmarks/sequential-learning/adapters/hippo.mjs:82-84`:

```javascript
  async recall(query, budget = 2000) {
    const escaped = query.replace(/"/g, '\\"');
    const raw = hippoExec(this._storeDir, `recall "${escaped}" --json --budget ${budget}`);
```

**Step 5: Update `baseline.mjs` and `static.mjs` signatures (no-op honor)**

Edit `baseline.mjs` `recall` method to accept `(query, _budget = undefined)` and ignore the second arg.
Edit `static.mjs` `recall` method to accept `(query, _budget = undefined)` and ignore the second arg.

**Step 6: Add `--budget` flag to `run.mjs::parseArgs`**

Edit `benchmarks/sequential-learning/run.mjs:35-89`. Add to `opts`:

```javascript
    budget: 2000,
```

Add to the arg loop:

```javascript
    } else if (args[i] === '--budget' && args[i + 1]) {
      // v1.7.6 -- recall budget for the discriminating-workload sweep.
      opts.budget = parseInt(args[++i], 10);
```

Update the `--help` block to document `--budget <int>` (default 2000, accepted by adapters that honor token budgets; baseline/static ignore).

**Step 7: Plumb `opts.budget` into `simulate()` (post-review P0-5 — exact diff)**

Two literal edits, no hand-waving:

(a) `run.mjs` `simulate()` body (around line 222–225, where `adapter.recall` is called):

```javascript
// BEFORE
const recalled = await adapter.recall(task.recallQuery);

// AFTER
const budget = opts.budget ?? 2000;
const recalled = await adapter.recall(task.recallQuery, budget);
```

(b) `run.mjs` `runAdapter` call site (lines 482–486 — the `simulate()` invocation in the seed loop):

```javascript
// BEFORE
const out = await simulate(adapter, tasks, {
  useGoalStack: opts.useGoalStack,
  evalStrict: opts.evalStrict,
});

// AFTER
const out = await simulate(adapter, tasks, {
  useGoalStack: opts.useGoalStack,
  evalStrict: opts.evalStrict,
  budget: opts.budget,
});
```

No other call sites of `simulate()` exist in `run.mjs`. Verify with `grep -n "simulate(" benchmarks/sequential-learning/run.mjs`.

**Step 8: Run the test**

Run: `npx vitest run tests/sl-adapter-budget.test.ts`
Expected: PASS — all 4 tests pass.

**Step 9: Run the full vitest suite to confirm no regression**

Run: `npx vitest run`
Expected: 1445+4 = 1449 tests pass (4 new from this task), 0 failures.

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

Run: `node benchmarks/sequential-learning/run.mjs --adapter hippo --n-seeds 1 --budget 500 --output /tmp/v1.7.6-budget-smoke/`
Expected: completes without error; the resulting JSON shows `phase_aggregate.late.mean` is some value (not crashing). This is a sanity smoke, not a measurement run.

**Step 11: Commit**

```bash
git add benchmarks/sequential-learning/adapters/interface.mjs \
        benchmarks/sequential-learning/adapters/hippo.mjs \
        benchmarks/sequential-learning/adapters/baseline.mjs \
        benchmarks/sequential-learning/adapters/static.mjs \
        benchmarks/sequential-learning/run.mjs \
        tests/sl-adapter-budget.test.ts
git commit -m "feat(bench): plumb --budget flag through run.mjs and adapter recall()"
```

---

## Task 2: Calibration script

**Why a separate script (not a flag on `run.mjs`):** Calibration runs C2 only across multiple budgets and emits a different result shape (per-budget table, not per-condition). Mixing into `run.mjs` would risk polluting the hypothesis-run code path with a calibration-only branch. A separate `calibrate.mjs` keeps the hypothesis runner's contract immutable.

**Files:**
- Create: `benchmarks/sequential-learning/calibrate.mjs`
- Test: `tests/sl-calibrate.test.ts` (NEW — unit-tests on the budget-selection rule, NOT the full subprocess sweep)

**Step 1: Write the failing test**

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

import { mulberry32 } from '../benchmarks/sequential-learning/aggregate.mjs';

describe('v1.7.6 calibration B* selection', () => {
  it('picks the LARGEST budget where C2 late mean ∈ [0.04, 0.24] AND lower-CI > 0', () => {
    const candidates = [
      { budget: 200, lateMean: 0.30, lateCI: 0.05 }, // mean too high
      { budget: 400, lateMean: 0.20, lateCI: 0.05 }, // in band, lower-CI=0.15 > 0 ✓
      { budget: 600, lateMean: 0.10, lateCI: 0.05 }, // in band, lower-CI=0.05 > 0 ✓
      { budget: 800, lateMean: 0.05, lateCI: 0.06 }, // in band, lower-CI=-0.01 < 0 ✗
      { budget: 1000, lateMean: 0.02, lateCI: 0.01 }, // too low ✗
    ];
    const bStar = selectBStar(candidates);
    expect(bStar.budget).toBe(600); // largest passing
    expect(bStar.reason).toMatch(/in band/i);
  });

  it('returns null when no candidate qualifies', () => {
    const candidates = [
      { budget: 200, lateMean: 0.40, lateCI: 0.05 },
      { budget: 400, lateMean: 0.30, lateCI: 0.05 },
      { budget: 800, lateMean: 0.02, lateCI: 0.01 },
    ];
    const bStar = selectBStar(candidates);
    expect(bStar.budget).toBeNull();
    expect(bStar.reason).toMatch(/no candidate/i);
  });

  it('handles empty input', () => {
    expect(selectBStar([]).budget).toBeNull();
  });

  it('rejects candidate with mean exactly at boundary lower-CI=0', () => {
    // mean=0.04 with CI=0.04 → lower-CI = 0.0, NOT > 0, so reject.
    const candidates = [
      { budget: 400, lateMean: 0.04, lateCI: 0.04 },
    ];
    expect(selectBStar(candidates).budget).toBeNull();
  });

  // Post-review P1-10 — boundary positive case
  it('accepts candidate at lower band boundary with positive lower-CI', () => {
    // mean=0.04 with CI=0.039 → lower-CI = 0.001 > 0, accept.
    const candidates = [
      { budget: 400, lateMean: 0.04, lateCI: 0.039 },
    ];
    expect(selectBStar(candidates).budget).toBe(400);
  });

  // Post-review P1-10 — boundary negative case (mean below band)
  it('rejects candidate just below band even with zero CI', () => {
    // mean=0.0399 < BAND_LOW=0.04 → reject regardless of CI.
    const candidates = [
      { budget: 400, lateMean: 0.0399, lateCI: 0.0 },
    ];
    expect(selectBStar(candidates).budget).toBeNull();
  });

  // Post-review P1-1 — calibration vs hypothesis seed-stream non-collision
  it('calibration and hypothesis mulberry32 streams are pairwise distinct in first 50 draws', () => {
    // Hypothesis seeds: 1000 + i for i in 0..19
    const hypoSeeds = Array.from({ length: 20 }, (_, i) =>
      (Math.imul(0x9E3779B9, (1000 + i) >>> 0)) >>> 0,
    );
    // Calibration seeds (post-review P1-1 — bumped offset 100 → 10000): 1000 + 10000 + i for i in 0..9
    const calibSeeds = Array.from({ length: 10 }, (_, i) =>
      (Math.imul(0x9E3779B9, (1000 + 10000 + i) >>> 0)) >>> 0,
    );
    const allSeeds = [...hypoSeeds, ...calibSeeds];
    const draws = allSeeds.map((s) => {
      const rng = mulberry32(s);
      return Array.from({ length: 50 }, () => rng());
    });
    // Pairwise: no two seed streams produce identical first 50 draws.
    for (let i = 0; i < draws.length; i++) {
      for (let j = i + 1; j < draws.length; j++) {
        const same = draws[i].every((v, k) => v === draws[j][k]);
        expect(same).toBe(false);
      }
    }
  });
});
```

**Step 2: Run test to verify it fails**

Run: `npx vitest run tests/sl-calibrate.test.ts`
Expected: FAIL — `calibrate.mjs` does not exist yet.

**Step 3: Write `calibrate.mjs`**

Create `benchmarks/sequential-learning/calibrate.mjs`:

```javascript
#!/usr/bin/env node
/**
 * v1.7.6 -- Calibration sweep for the discriminating-workload variant.
 *
 * Runs C2 (hippo-base, no goal-stack) at a fixed list of budget candidates
 * with N=10 calibration seeds (post-review P0-1 -- ciHalfWidth95 returns 0
 * for n<5; n=10 gives a usable t-band). Calibration seeds: 1000+10000+i
 * for i in 0..9 (post-review P1-1 -- offset bumped from 100 to 10000 for
 * larger input distance vs hypothesis seeds 1000+i for i in 0..19).
 *
 * Picks B* mechanically per the pre-registered selection rule:
 *   B* = LARGEST budget where (mean late ∈ [0.04, 0.24]) AND (lower-CI > 0)
 *        AND mean(len(results)) >= 1 across calibration seeds (P1-4
 *        starvation guard).
 *
 * Outputs:
 *   - stdout: markdown table + B* announcement
 *   - <output>/calibration-result.json: structured per-budget rows
 *
 * Usage:
 *   node benchmarks/sequential-learning/calibrate.mjs \
 *     --budgets 200,400,600,800,1000 \
 *     --n-seeds 10 \
 *     --output benchmarks/sequential-learning/results/v1.7.6-calibration/
 */

import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { mean, ciHalfWidth95 } from './aggregate.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));

// ---------------------------------------------------------------------------
// B* selection rule (pre-registered in
// docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md).
// ---------------------------------------------------------------------------

const BAND_LOW = 0.04;
const BAND_HIGH = 0.24;

/**
 * @param {Array<{budget:number, lateMean:number, lateCI:number, starved?:boolean}>} candidates
 * @returns {{budget:number|null, reason:string}}
 *
 * Post-review P1-4 -- starvation guard: if a candidate is flagged `starved`
 * (mean recall surface < 1 across calibration seeds), exclude before applying
 * the band rule.
 */
export function selectBStar(candidates) {
  if (!candidates || candidates.length === 0) {
    return { budget: null, reason: 'No candidates supplied.' };
  }
  // Sort descending by budget; pick the first that qualifies.
  const sorted = [...candidates].sort((a, b) => b.budget - a.budget);
  for (const c of sorted) {
    if (c.starved === true) continue; // P1-4 starvation exclusion
    const lowerCI = c.lateMean - c.lateCI;
    if (
      c.lateMean >= BAND_LOW &&
      c.lateMean <= BAND_HIGH &&
      lowerCI > 0
    ) {
      return {
        budget: c.budget,
        reason: `largest budget in band [${BAND_LOW}, ${BAND_HIGH}] with lower-CI ${lowerCI.toFixed(4)} > 0 (not starved)`,
      };
    }
  }
  return {
    budget: null,
    reason: `No candidate satisfies (mean ∈ [${BAND_LOW}, ${BAND_HIGH}]) AND (lower-CI > 0) AND (not starved).`,
  };
}

// ---------------------------------------------------------------------------
// CLI driver (no-op when imported from tests)
// ---------------------------------------------------------------------------

function parseArgs() {
  const args = process.argv.slice(2);
  const opts = {
    budgets: [200, 400, 600, 800, 1000],
    nSeeds: 10, // post-review P0-1 -- bumped from 5 to 10 for usable CI.
    output: join(__dirname, 'results', 'v1.7.6-calibration'),
  };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--budgets' && args[i + 1]) {
      opts.budgets = args[++i].split(',').map((s) => parseInt(s, 10));
    } else if (args[i] === '--n-seeds' && args[i + 1]) {
      opts.nSeeds = parseInt(args[++i], 10);
    } else if (args[i] === '--output' && args[i + 1]) {
      opts.output = args[++i];
    }
  }
  return opts;
}

function deriveCalibrationSeeds(n) {
  // Post-review P1-1 -- offset bumped from 100 to 10000 for larger input
  // distance vs hypothesis seeds (1000+i for i in 0..19). Pairwise non-collision
  // verified by tests/sl-calibrate.test.ts.
  return Array.from({ length: n }, (_, i) =>
    (Math.imul(0x9E3779B9, (1000 + 10000 + i) >>> 0)) >>> 0,
  );
}

function runOneBudget(budget, seeds, outputBase) {
  const outDir = join(outputBase, `budget-${budget}`);
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });

  // Post-review P0-3 -- run.mjs sets `seedRuns: null` for single-seed runs
  // (run.mjs:548 -- `seedList.length > 1 ? seedRuns : null`). The single-seed
  // late rate lives at j.conditions[condName].phases.late, NOT at
  // j.conditions[condName].seeds[0].phases.late. Reading the wrong path
  // crashes silently (`Cannot read properties of null`).
  //
  // We invoke run.mjs once per calibration seed via --seed <hash>.
  const seedRates = [];
  const seedResultCounts = []; // post-review P1-4 -- starvation guard.
  for (const seed of seeds) {
    const seedDir = join(outDir, `seed-${seed}`);
    if (!existsSync(seedDir)) mkdirSync(seedDir, { recursive: true });
    execSync(
      `node "${join(__dirname, 'run.mjs')}" --adapter hippo --seed ${seed} --budget ${budget} --output "${seedDir}"`,
      { stdio: 'inherit' },
    );
    // Read the resulting JSON and extract late-phase rate for this single seed.
    const files = readdirSync(seedDir).filter((f) => f.endsWith('.json'));
    if (files.length === 0) throw new Error(`No JSON output in ${seedDir}`);
    files.sort();
    const j = JSON.parse(readFileSync(join(seedDir, files[files.length - 1]), 'utf-8'));
    const condName = Object.keys(j.conditions)[0];
    // Single-seed path -- read directly from `phases.late`.
    const lateRate = j.conditions[condName].phases.late;
    if (typeof lateRate !== 'number') {
      throw new Error(
        `runOneBudget: phases.late not numeric for ${seedDir} (got ${typeof lateRate}). ` +
        `Schema may have changed; verify run.mjs:539-550 output shape.`,
      );
    }
    seedRates.push(lateRate);

    // Starvation guard -- count avg results returned per trap encounter.
    // results[].memoryRecalled is true iff recall returned a matching memory;
    // we approximate avg recall surface from `results.length` of trap tasks
    // (which is fixed at ~25) vs how many had memoryRecalled. Real per-trap
    // result-count is not surfaced in run.mjs JSON; use a simpler heuristic:
    // if zero traps had memoryRecalled across the whole seed, the budget
    // is starving the BM25 ranker.
    const trapResults = (j.conditions[condName].results ?? []).filter(
      (r) => r.trapCategory !== null,
    );
    const recalledCount = trapResults.filter((r) => r.memoryRecalled).length;
    seedResultCounts.push(recalledCount);
  }
  const starvationFlag =
    seedResultCounts.length > 0 &&
    seedResultCounts.reduce((a, b) => a + b, 0) / seedResultCounts.length < 1;
  return {
    budget,
    seedRates,
    seedResultCounts,
    starved: starvationFlag,
    lateMean: mean(seedRates),
    lateCI: ciHalfWidth95(seedRates),
  };
}

async function main() {
  const opts = parseArgs();
  if (!existsSync(opts.output)) mkdirSync(opts.output, { recursive: true });

  const seeds = deriveCalibrationSeeds(opts.nSeeds);
  console.log(`Calibration seeds (n=${opts.nSeeds}): ${seeds.join(', ')}`);
  console.log(`Budget candidates: ${opts.budgets.join(', ')}`);
  console.log('');

  const rows = [];
  for (const budget of opts.budgets) {
    console.log(`--- budget=${budget} ---`);
    const row = runOneBudget(budget, seeds, opts.output);
    rows.push(row);
    console.log(
      `  late mean=${(row.lateMean * 100).toFixed(2)}% ±${(row.lateCI * 100).toFixed(2)}pp ` +
      `(seeds: ${row.seedRates.map((r) => (r * 100).toFixed(1) + '%').join(', ')})`,
    );
  }

  const bStar = selectBStar(rows);
  console.log('');
  console.log('# Calibration Summary');
  console.log('| Budget | Late mean | ±95% CI | Lower-CI | In band? |');
  console.log('|--------|-----------|---------|----------|----------|');
  for (const r of rows) {
    const lowerCI = r.lateMean - r.lateCI;
    const inBand =
      r.lateMean >= BAND_LOW && r.lateMean <= BAND_HIGH && lowerCI > 0
        ? 'YES'
        : 'no';
    console.log(
      `| ${r.budget} | ${(r.lateMean * 100).toFixed(2)}% | ±${(r.lateCI * 100).toFixed(2)}pp | ${(lowerCI * 100).toFixed(2)}pp | ${inBand} |`,
    );
  }
  console.log('');
  console.log(`B* = ${bStar.budget ?? 'NONE'} (${bStar.reason})`);

  // Write structured JSON
  writeFileSync(
    join(opts.output, 'calibration-result.json'),
    JSON.stringify({ rows, bStar, seeds, candidates: opts.budgets }, null, 2),
  );
}

// Only run main() when invoked as a script. When imported (by tests), skip.
const invokedAsScript =
  process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (invokedAsScript) {
  main().catch((err) => {
    console.error(err);
    process.exit(1);
  });
}
```

**Step 4: Run the test**

Run: `npx vitest run tests/sl-calibrate.test.ts`
Expected: PASS — all 4 unit tests pass on `selectBStar`.

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

Run: `npx vitest run`
Expected: 1453 tests pass (4 from Task 1 + 4 from Task 2), 0 failures.

**Step 6: Commit**

```bash
git add benchmarks/sequential-learning/calibrate.mjs tests/sl-calibrate.test.ts
git commit -m "feat(bench): v1.7.6 calibration script with mechanical B* selection rule"
```

---

## Task 3: Run calibration → pick B* → write `calibration-result.md`

**Files:**
- Run: `node benchmarks/sequential-learning/calibrate.mjs --budgets 200,400,600,800,1000 --n-seeds 5 --output benchmarks/sequential-learning/results/v1.7.6-calibration/`
- Create: `docs/evals/2026-05-07-v1.7.6-calibration-result.md`

**Step 1: Verify hippo CLI is on PATH**

Run: `hippo --version`
Expected: `1.7.5` (or newer if local install). If missing: `npm link` from repo root.

**Step 2: Run calibration**

Run:

```bash
node benchmarks/sequential-learning/calibrate.mjs \
  --budgets 200,400,600,800,1000 \
  --n-seeds 10 \
  --output benchmarks/sequential-learning/results/v1.7.6-calibration/
```

Note: `--n-seeds 10` per post-review P0-1 (n<5 has zero-CI floor; n=10 gives usable t-band).

Expected runtime: 10–15 min. Stdout shows per-budget table and `B* = <value>`. Includes a `starved` column (post-review P1-4) flagging candidates where mean recall surface < 1 across seeds.

**Step 3: Capture the calibration result**

Read `benchmarks/sequential-learning/results/v1.7.6-calibration/calibration-result.json` to confirm structure.

**Step 4: Write `docs/evals/2026-05-07-v1.7.6-calibration-result.md`**

```markdown
# v1.7.6 Calibration — Result

**Run:** 2026-05-07
**Pre-registered selection rule:** B* = LARGEST budget where (mean late ∈ [4%, 24%]) AND (lower-CI > 0) AND (not starved).
**Calibration seeds (n=10, post-review P0-1):** {paste from stdout}, hash-derived from `1000 + 10000 + i` for i in 0..9 (post-review P1-1 — offset bumped from 100 to 10000 for non-collision with hypothesis seeds `1000 + i` for i in 0..19).
**Candidates:** 200, 400, 600, 800, 1000.

## Reproducibility anchors

- `hippo --version` = {paste output}
- `which hippo` = {paste output}
- `git rev-parse HEAD` = {paste sha}
- `node --version` = {paste output}
- Wall time = {minutes:seconds}

## Per-budget late-phase trap rate (C2 only)

| Budget | Late mean | ±95% CI | Lower-CI | Avg recall surface | Starved? | In band? |
|--------|-----------|---------|----------|--------------------|----------|----------|
| {paste from stdout} |

## B* selected

**B* = {value}** ({reason from stdout}).

## Notes

- Seed range `1000 + 10000 + i` for i in 0..9 is hash-derived to avoid correlated mulberry32 streams with the hypothesis run (seeds `1000 + i` for i in 0..19). Pairwise non-collision verified by `tests/sl-calibrate.test.ts` (post-review P1-1).
- The full hypothesis run (Task 6) re-runs C2 at B* with the hypothesis seeds 0..19; sanity gate re-fires AT THE C2 STAGE (before C3 runs — post-review P0-2) if drift moves C2 out of band.
- Within-band drift (calibration C2 vs hypothesis C2 differ by >5pp but both stay in band) is documented in the result doc as a soft warning per post-review P1-5, even on SUPPORTED.
- If `B* = NONE`, the workload knob (budget reduction) failed. Escalation: pre-reg pre-commits v1.7.7 to `--restrict-late-to last-4` (post-review P2-4); adversarial categories deferred to v1.8.
```

**Step 5: Commit**

```bash
git add benchmarks/sequential-learning/results/v1.7.6-calibration/ \
        docs/evals/2026-05-07-v1.7.6-calibration-result.md
git commit -m "docs(eval): v1.7.6 calibration result, B*={value} selected"
```

---

## Task 4: Pre-registration document + claim inventory

**CRITICAL — these documents MUST be committed BEFORE Task 6 runs.** No hypothesis eval before pre-reg lands.

**Files:**
- Create: `docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md`
- Create: `docs/evals/2026-05-07-v1.7.6-claim-inventory.md`

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

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

**Pre-registered:** 2026-05-07, BEFORE running the hypothesis eval.
**Author:** Keith So
**Mechanism under test:** B3 dlPFC goal-stack boost (`applyGoalStackBoost`), shipped v1.7.4.
**Workload variant:** discriminating budget B* = {VALUE_FROM_TASK_3}, picked by mechanical selection rule documented in `docs/evals/2026-05-07-v1.7.6-calibration-result.md` and `benchmarks/sequential-learning/calibrate.mjs::selectBStar`.
**Public-surface status of the claim:** the −10pp hypothesis was not testable in v1.7.5 due to floor effect (C2 = 0.0% late). v1.7.6 re-tests on the calibrated workload; 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.6 discriminating workload (`--budget B*`), with 95% CI lower bound > 0pp.

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

## Protocol

- Benchmark: `benchmarks/sequential-learning` (v1.7.5 multi-seed harness, unchanged).
- **Workload knob (NEW for v1.7.6):** `--budget {B*}` (selected by Task 3 calibration).
- **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). Distinct from calibration seeds (1000+100+i for i in 0..4).
- **Eval mode:** `--eval-strict`.
- **Primary metric:** late-phase trap-hit-rate (existing `hitRateByPhase`, last 8 of 25 trap encounters).
- **Top-5 cap interaction:** unchanged from v1.7.5.

## Statistical method (PRE-LOCKED, unchanged from v1.7.5 — wording corrected post-review P1-2)

- Estimand: mean late-phase Δ in pp = `mean_late(C2) − mean_late(C3)` over 20 paired seeds.
- CI: 10,000 Monte Carlo resamples of paired sign-flip permutation (`pairedPermutationCI` in `aggregate.mjs`). For n=20, full enumeration would be 2^20 ≈ 1M sign-flip combinations; 10k Monte Carlo draws are sufficient for stable percentile bounds. NOT t-test — phase rates are bounded discrete.
- For ties heavy at 0% (last-8 of bounded discrete trap encounters), Monte Carlo permutation may produce wide CI; result doc reports tie distribution alongside the CI for transparency.

## Decision rule (mechanical)

Compute Δ and 95% permutation CI on Δ.

- **Δ ≥ 0.10 AND lower-CI > 0** → **HYPOTHESIS SUPPORTED.** Ship "SUPPORTED" template.
- **Δ < 0.10 OR CI overlaps 0** → **HYPOTHESIS NOT SUPPORTED.** Ship "NOT SUPPORTED" template citing measured Δ and CI.
- **Δ < 0 with upper-CI < 0** = goal-stack measurably HARMS performance. Flag for v1.8 design review.

## Stop conditions (mid-run)

- **Sanity gate** (re-fires at hypothesis-run scale): if C2 late mean at N=20 is outside [0.04, 0.24], STOP. Write `result.md` with SANITY_FAIL verdict; document drift; do NOT silently re-calibrate. Re-calibration requires a pre-reg amendment commit.
- **Hook failure gate:** if `C3.hookFailures.push > 0` OR `.complete > 0`, STOP. Run contaminated.
- **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.

## Calibration → hypothesis link

The B* picked by Task 3 calibration is locked into this pre-reg's `--budget` parameter. Calibration uses seed input range `1000 + 10000 + i` for i in 0..9 (10 seeds, post-review P0-1 + P1-1); hypothesis uses `1000 + i` for i in 0..19 (20 seeds). Hash-derived from `0x9E3779B9` so the two ranges are deterministic, reproducible, and non-overlapping (verified pairwise distinct by unit test).

The sanity gate re-fires AT THE C2 STAGE of the hypothesis run (post-review P0-2). If drift moves C2 out of band, STOP — C3 is NOT collected. This avoids leaking goal-stack directional data on a "stopped" eval.

**Within-band overfit caveat (post-review P1-5):** the sanity gate detects only out-of-band drift. If calibration C2 = 18% and hypothesis C2 = 22% (both in band but drift > 5pp), the result doc must report both side-by-side and flag the drift as a soft warning even on SUPPORTED.

## Why budget reduction, not the other 4 workload variants (post-review P1-9)

v1.7.5 result lists 5 candidate workload variants. This pre-reg picks budget reduction first because:

1. **Single CLI knob.** No new code surface; `--budget` already exists end-to-end (Task 1 lifts the hardcoded 2000).
2. **Monotone effect.** Smaller budget = harder retrieval. No semantic change to traps or metric.
3. **No C3 exposure during workload selection.** Calibration runs C2 only; goal-stack is never invoked at the calibration stage.
4. **Doesn't require new lesson authoring.** Adversarial categories would need new trap text; that's v1.8 scope.
5. **Doesn't redefine the metric.** Restricting the late-phase window changes what "late" means; that's v1.7.7 fallback.

Pre-committed escalation order (post-review P2-4):
- 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 + harness changes).
- LongMemEval R@5 corpus is a separate eval line, not a substitute.

## 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.6 discriminating workload (`--budget {B*}`), 20 paired seeds, exact 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}. Pre-registration: docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md. Calibration: docs/evals/2026-05-07-v1.7.6-calibration-result.md.

### Template "HYPOTHESIS NOT SUPPORTED" (use IFF the decision rule selects it — post-review P0-7 soft-frame removed)

> **Goal-stack lift on sequential-learning benchmark — HYPOTHESIS NOT SUPPORTED.** v1.7.6 discriminating workload (`--budget {B*}`), 20 paired seeds, 10k Monte Carlo permutation CI. 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. The mechanism remains shipped (v1.7.4); the hypothesis is unsupported on the v1.7.6 discriminating workload. Pre-registration: docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md.

### Template "SANITY_FAIL" (use IFF C2 drifts out of band at the C2 sanity preflight — post-review P1-6 causality fix)

> **Goal-stack lift on sequential-learning benchmark — eval STOPPED per pre-registered sanity gate.** v1.7.6 calibration picked B* = {B*} on 10 seeds (C2 late = {CALIB_C2}%, in band [4%, 24%]). At C2 sanity preflight on the hypothesis seed range (20 seeds), C2 late = {HYPO_C2}%, outside the band. Drift consistent with calibration overfit or seed-sample variance; root cause not investigated this round. **C3 (goal-stack ON) was NOT collected** — no goal-stack data was produced under SANITY_FAIL. Hypothesis remains untested on this workload knob. Per pre-committed escalation: v1.7.7 will be `--restrict-late-to last-4`. Calibration: docs/evals/2026-05-07-v1.7.6-calibration-result.md. Pre-registration: docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md.

## Audit trail

- Plan: `docs/plans/2026-05-07-v1.7.6-discriminating-workload.md`
- Calibration script: `benchmarks/sequential-learning/calibrate.mjs`
- Calibration result: `docs/evals/2026-05-07-v1.7.6-calibration-result.md`
- Hypothesis analyzer: `benchmarks/sequential-learning/analyze-v1.7.6.mjs`
- Outside-voice review: TBD before /full-power execute (`/plan-eng-review` + `/codex`).
- Claim inventory: `docs/evals/2026-05-07-v1.7.6-claim-inventory.md` (committed alongside this prereg).
```

**Step 1.5: Pre-populate inventory by greppping README + claim-bearing files (post-review P2-3)**

```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. Specifically check for the README "Agent eval benchmark" section (or whatever the heading is) — it carries the public-claim surface and must be in the inventory by name.

**Step 2: Write `2026-05-07-v1.7.6-claim-inventory.md`**

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

**Pre-eval inventory.** Captures every location in the repo (excluding worktrees, `node_modules`, `results/`, and the v1.7.6 docs themselves) that mentions a `−10pp` / "10pp" / "goal-stack lift" string, plus any v1.7.5 STOP-framed text that needs updating per the v1.7.6 verdict.

Generated: 2026-05-07, BEFORE running the v1.7.6 hypothesis eval.

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

| Path | Line | Context | Action if SUPPORTED | Action if NOT SUPPORTED | Action if SANITY_FAIL |
|------|------|---------|---------------------|-------------------------|-----------------------|
| `CHANGELOG.md` | v1.7.5 entry | "STOPPED per pre-registered sanity gate due to floor effect" | Add v1.7.6 entry citing measured Δ; v1.7.5 line stays. | Add v1.7.6 entry citing measured Δ < 10pp; v1.7.5 stays. | Add v1.7.6 entry citing calibration drift; v1.7.5 stays. |
| `README.md` | "What's new in v1.7.5" | "Eval ran but stopped per pre-registered sanity gate" | Add "What's new in v1.7.6" section citing supported result. | Add "What's new in v1.7.6" with NOT SUPPORTED result. | Add "What's new in v1.7.6" with SANITY_FAIL result. |
| `RESEARCH.md` | dlPFC / B3 section | (verify exact line in claim-inventory pass) | Add measured ΔLate ± CI sentence. | Update to "magnitude not demonstrated, mechanism shipped." | Update to "discriminating workload sweep deferred to v1.7.7+." |
| `docs/plans/2026-04-29-b3-dlpfc-depth.md` | header note | Already points to v1.7.5 result | Append "v1.7.6: SUPPORTED, see result doc" | Append "v1.7.6: NOT SUPPORTED, see result doc" | Append "v1.7.6: SANITY_FAIL, see result doc" |
| `TODOS.md` | "B3 follow-up: −10pp goal-stack hypothesis test" line ~144 | Open v1.7.6+ followup | Mark `[x]` with link to v1.7.6 result. | Mark `[x]` with link; add note "magnitude not demonstrated." | Keep `[ ]`, retarget to v1.7.7+; add note. |
| `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` | "Future eval design" section | Already names v1.7.6 candidates | Add header note "v1.7.6 ran: SUPPORTED, see 2026-05-07-v1.7.6-...-result.md" | Same | Same |

## Locations with "10pp" strings UNRELATED (no action)

Same as v1.7.5 inventory — `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.6 result either supports or fails to support the magnitude on the v1.7.6 workload; the mechanism remains shipped either way.

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

After the analyzer emits a verdict in `docs/evals/2026-05-07-v1.7.6-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 forward-pointer to v1.7.5 result doc (post-review P2-5)**

This step lives in Task 4 (not Task 7) so the calibration is documented as a pre-committed next step, not a post-hoc rescue. Append to `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md`:

```markdown
## Update 2026-05-07 (forward pointer to v1.7.6)

The "Future eval design" section above named budget reduction as candidate #1. v1.7.6 acts on that candidate via pre-registered calibration: see `docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md` and `docs/evals/2026-05-07-v1.7.6-calibration-result.md`. Hypothesis re-test runs at calibrated `--budget B*` on the 20-seed range; verdict published in `docs/evals/2026-05-07-v1.7.6-goal-stack-eval-result.md` after Task 6.
```

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

```bash
git add docs/evals/2026-05-07-v1.7.6-goal-stack-eval-prereg.md \
        docs/evals/2026-05-07-v1.7.6-claim-inventory.md \
        docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md
git commit -m "docs(eval): v1.7.6 pre-registration + claim inventory + v1.7.5 forward-pointer (PRE-eval)"
```

---

## Task 5: Hypothesis-eval analyzer + verdict tests (post-review P1-3)

**Files:**
- Create: `benchmarks/sequential-learning/analyze-v1.7.6.mjs` (template from `analyze-v1.7.5.mjs`)
- Refactor: extract pure `computeVerdict({c2Late, c3Late, delta, ciLow, ciHigh, hookFailures, sanityPass})` into the analyzer
- Test: `tests/sl-analyze-v1.7.6.test.ts` (NEW — 4 fixture cases, one per verdict string)

**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.6.mjs
```

**Step 2: Edit constants and result paths**

Edit the new file:
- Replace `v1.7.5-eval-` with `v1.7.6-eval-` in the four `loadLatestJson` calls.
- Replace `2026-05-07` references in print statements with `2026-05-07` (same date or update to actual eval date).
- Replace `docs/evals/2026-05-07-v1.7.5-` with `docs/evals/2026-05-07-v1.7.6-` in the printed prereg/inventory paths.
- Update the `decision` variable strings:
  - Keep `'STOP — sanity gate failed (C2 late outside [4%, 24%])'` → emit `SANITY_FAIL` for the structured JSON summary.
  - Keep `HYPOTHESIS SUPPORTED` and `HYPOTHESIS NOT SUPPORTED` paths.
- Add to the structured JSON summary at the bottom: a top-level `verdict` field with one of `SUPPORTED | NOT_SUPPORTED | SANITY_FAIL | HOOK_FAIL` for downstream Task 7 scripting.

Diff sketch (illustrative — apply via Edit, not patch-apply):

```javascript
// At the bottom of the file:
let verdict;
if (!sanityPass) verdict = 'SANITY_FAIL';
else if (!hookFailuresOk) verdict = 'HOOK_FAIL';
else if (SUPPORTED) verdict = 'SUPPORTED';
else verdict = 'NOT_SUPPORTED';

const summary = {
  verdict,
  decision,
  sanityPass,
  hookFailuresOk,
  conditions: rows,
  pairedDelta: { mean: delta, ciLow, ciHigh },
  hookFailures: e3.hookFailures,
};
process.stderr.write('\n--- JSON SUMMARY ---\n' + JSON.stringify(summary, null, 2) + '\n');
```

**Step 3: Refactor verdict computation into a pure exported function**

Extract from the analyzer body into the top of the module:

```javascript
/**
 * Pure verdict computation. No I/O. Testable in isolation.
 * @param {{c2Late:number, c3Late:number, delta:number, ciLow:number, ciHigh:number,
 *          hookFailures:{push:number, complete:number}|null,
 *          sanityPass:boolean}} input
 * @returns {'SUPPORTED' | 'NOT_SUPPORTED' | 'SANITY_FAIL' | 'HOOK_FAIL'}
 */
export function computeVerdict({ c2Late, c3Late, delta, ciLow, ciHigh, hookFailures, sanityPass }) {
  if (!sanityPass) return 'SANITY_FAIL';
  if ((hookFailures?.push ?? 0) > 0 || (hookFailures?.complete ?? 0) > 0) return 'HOOK_FAIL';
  if (delta >= 0.10 && ciLow > 0) return 'SUPPORTED';
  return 'NOT_SUPPORTED';
}
```

The analyzer's bottom-of-file `verdict` derivation calls `computeVerdict({...})` instead of recomputing inline.

**Step 4: Write the verdict-emission test**

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

describe('v1.7.6 computeVerdict', () => {
  it('returns SUPPORTED when delta >= 0.10 AND ciLow > 0 AND sanity passes', () => {
    expect(computeVerdict({
      c2Late: 0.18, c3Late: 0.05, delta: 0.13,
      ciLow: 0.04, ciHigh: 0.22,
      hookFailures: { push: 0, complete: 0 }, sanityPass: 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,
    })).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,
    })).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,
    })).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,
    })).toBe('HOOK_FAIL');
  });
});
```

**Step 5: Run vitest**

Run: `npx vitest run tests/sl-analyze-v1.7.6.test.ts`
Expected: 5 tests pass.

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

Run: `node benchmarks/sequential-learning/analyze-v1.7.6.mjs`
Expected: fails with `ENOENT` or `readdirSync` error because the `v1.7.6-eval-*` result dirs don't exist yet. This is fine — the analyzer is only run in Task 6.

**Step 7: Commit**

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

---

## Task 6: Run hypothesis eval + apply decision rule

**Files:**
- Run: `node benchmarks/sequential-learning/run.mjs ...` (4 invocations, one per condition)
- Run: `node benchmarks/sequential-learning/analyze-v1.7.6.mjs > docs/evals/2026-05-07-v1.7.6-goal-stack-eval-result.md`
- Create: `docs/evals/2026-05-07-v1.7.6-goal-stack-eval-result.md`

**Step 0: Reproducibility logging (post-review P1-7)**

Capture into a temp file that will be appended to the result doc in Step 8:

```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 start = $(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > /tmp/v1.7.6-eval-anchors.md
cat /tmp/v1.7.6-eval-anchors.md
```

**Step 1: Mechanical preflight (post-review P0-4 — replaces the `git log | grep` polite check)**

Run the hardened preflight script from the "Pre-registration commit ordering" section above. STOP on any failure. Specifically: verifies prereg + inventory + analyzer + calibration JSON are committed and reachable from HEAD; verifies the prereg pins the calibration B* value; verifies no eval result dirs already exist; verifies clean working tree.

```bash
bash scripts/v1.7.6-preflight.sh  # OR inline the preflight script from the plan
```

Expected: `PREFLIGHT OK: B*=<value>, prereg pinned, no stale eval dirs, clean tree.` Any other output → STOP.

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

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter none --n-seeds 20 --eval-strict --budget {B*} \
  --output benchmarks/sequential-learning/results/v1.7.6-eval-C0-none/
```

Expected runtime: ~30s (no subprocess). C0 ignores `--budget` (baseline adapter has no recall).

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

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter static --n-seeds 20 --eval-strict --budget {B*} \
  --output benchmarks/sequential-learning/results/v1.7.6-eval-C1-static/
```

Expected runtime: ~30s.

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

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter hippo --n-seeds 20 --eval-strict --budget {B*} \
  --output benchmarks/sequential-learning/results/v1.7.6-eval-C2-hippo-base/
```

Expected runtime: 6–8 min.

**Step 4.5: C2 SANITY PREFLIGHT (post-review P0-2, CRITICAL — must run BEFORE C3)**

Mechanically verify C2 late mean lands inside [0.04, 0.24] AND lower-CI > 0 BEFORE collecting any C3 data. If sanity fails, C3 is NOT collected — goal-stack data will not be produced, ensuring a "stopped" eval cannot leak directional information.

```bash
node -e "
  const fs = require('node:fs');
  const path = require('node:path');
  const { mean, ciHalfWidth95 } = require('./benchmarks/sequential-learning/aggregate.mjs');
  const dir = 'benchmarks/sequential-learning/results/v1.7.6-eval-C2-hippo-base';
  const files = fs.readdirSync(dir).filter(f => f.endsWith('.json')).sort();
  const j = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf-8'));
  const condName = Object.keys(j.conditions)[0];
  const lateRates = j.conditions[condName].seedRuns.map(r => r.phases.late);
  const m = mean(lateRates);
  const ci = ciHalfWidth95(lateRates);
  const lowerCI = m - ci;
  const inBand = m >= 0.04 && m <= 0.24 && lowerCI > 0;
  console.log('C2 late: mean=' + (m*100).toFixed(2) + '%, CI=±' + (ci*100).toFixed(2) + 'pp, lower-CI=' + (lowerCI*100).toFixed(2) + 'pp');
  console.log('Sanity gate: ' + (inBand ? 'PASS' : 'FAIL') + ' (band [4%, 24%], lower-CI > 0)');
  if (!inBand) {
    console.error('STOP: C2 sanity FAIL. C3 will NOT be collected. Apply Task 7 SANITY_FAIL branch.');
    process.exit(1);
  }
" || { echo "C2 sanity FAILED — STOP. Skip Step 5 (C3). Apply SANITY_FAIL branch in Task 7."; exit 1; }
```

If sanity FAILS:
- Skip Step 5 entirely (do NOT collect C3).
- Skip Step 6 (analyzer needs all 4 conditions).
- Jump to Step 8 with verdict pre-determined as `SANITY_FAIL`.
- The result doc must explicitly state "C3 not collected; goal-stack data not produced this run."
- Apply Task 7 Branch 7-C (SANITY_FAIL).

If sanity PASSES → continue to Step 5.

**Step 5: Run condition C3 (hippo + goal-stack) — only if C2 sanity passed**

```bash
node benchmarks/sequential-learning/run.mjs \
  --adapter hippo --n-seeds 20 --use-goal-stack --eval-strict --budget {B*} \
  --output benchmarks/sequential-learning/results/v1.7.6-eval-C3-hippo-goalstack/
```

Expected runtime: 12–15 min.

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

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

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

**Step 7: Read the verdict mechanically**

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

Expected: one of `"verdict": "SUPPORTED"`, `"NOT_SUPPORTED"`, `"SANITY_FAIL"`, or `"HOOK_FAIL"`.

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

**Step 8: Augment the result doc**

The analyzer emits the table + decision + raw artifacts list. Edit `docs/evals/2026-05-07-v1.7.6-goal-stack-eval-result.md` to add:

- **Reproducibility anchors block** from Step 0 (`/tmp/v1.7.6-eval-anchors.md`) — paste at the top, after the verdict line.
- B* citation (link to calibration result).
- Hook-failure counts (from the JSON summary).
- Sanity-gate status (PASS/FAIL/SKIPPED-on-SANITY_FAIL).
- **Calibration vs hypothesis C2 side-by-side (post-review P1-5).** Always report:
  - calibration C2 mean (from `calibration-result.json`)
  - hypothesis C2 mean (from this run)
  - drift = |calibration_C2 − hypothesis_C2| in pp
  - if drift > 5pp AND verdict ≠ SANITY_FAIL: include a soft-warning paragraph: "Within-band drift of {drift}pp between calibration and hypothesis C2 means; the verdict is preserved but readers should weight the small calibration N (10 seeds) accordingly."
- For SANITY_FAIL: state explicitly "C3 not collected. Goal-stack data not produced this run."
- For SUPPORTED / NOT_SUPPORTED: paste the matching template from the pre-reg with placeholders filled in.
- Investigation notes (timing deviations, anomalous seeds, tie-distribution on late-phase rates if many at 0% or 100%).

**Step 9: Commit eval results + result doc**

```bash
git add benchmarks/sequential-learning/results/v1.7.6-eval-C0-none/ \
        benchmarks/sequential-learning/results/v1.7.6-eval-C1-static/ \
        benchmarks/sequential-learning/results/v1.7.6-eval-C2-hippo-base/ \
        benchmarks/sequential-learning/results/v1.7.6-eval-C3-hippo-goalstack/ \
        docs/evals/2026-05-07-v1.7.6-goal-stack-eval-result.md
git commit -m "docs(eval): v1.7.6 hypothesis eval result, verdict={VERDICT}"
```

---

## Task 7: Apply consequences (CHANGELOG / README / RESEARCH.md / TODOS.md)

**Branch on the verdict from Task 6.** Each branch walks the claim inventory (Task 4) and applies the matching column.

### Branch 7-A: Verdict = SUPPORTED

**Files to edit:**
- `CHANGELOG.md` — add v1.7.6 entry citing measured Δ ± CI, B*, seed count.
- `README.md` — add "What's new in v1.7.6" with SUPPORTED template paste.
- `RESEARCH.md` — update dlPFC / B3 section with measured ΔLate ± CI.
- `docs/plans/2026-04-29-b3-dlpfc-depth.md` — append header note "v1.7.6: SUPPORTED, see docs/evals/2026-05-07-v1.7.6-goal-stack-eval-result.md".
- `TODOS.md:144` — mark `[x]` and link to result.
- `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` — append "Update 2026-05-07: v1.7.6 ran on discriminating workload — SUPPORTED, see result doc."

### Branch 7-B: Verdict = NOT_SUPPORTED

**Files to edit:**
- `CHANGELOG.md` — add v1.7.6 entry citing measured Δ < 10pp; magnitude not demonstrated; mechanism shipped.
- `README.md` — add "What's new in v1.7.6" with NOT SUPPORTED template paste.
- `RESEARCH.md` — update to "magnitude not demonstrated on this workload; mechanism shipped."
- `docs/plans/2026-04-29-b3-dlpfc-depth.md` — append "v1.7.6: NOT SUPPORTED, see result doc."
- `TODOS.md:144` — mark `[x]` with note "magnitude not demonstrated; mechanism shipped."
- `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` — append "Update 2026-05-07: v1.7.6 ran on discriminating workload — NOT SUPPORTED, see result doc."

### Branch 7-C: Verdict = SANITY_FAIL

**Files to edit:**
- `CHANGELOG.md` — add v1.7.6 entry: calibration ran, B* = {B*}; hypothesis run drifted out of band; no claim made.
- `README.md` — add "What's new in v1.7.6" with SANITY_FAIL template paste; magnitude still untested.
- `RESEARCH.md` — update "discriminating workload sweep deferred to v1.7.7+ (adversarial categories or window restriction)."
- `docs/plans/2026-04-29-b3-dlpfc-depth.md` — append "v1.7.6: SANITY_FAIL on calibration drift; see result doc."
- `TODOS.md:144` — keep `[ ]`, retarget to v1.7.7+, add note "v1.7.6 budget knob inconclusive due to calibration drift; escalate to adversarial categories or window restriction."
- `docs/evals/2026-05-07-v1.7.5-goal-stack-eval-result.md` — append "Update 2026-05-07: v1.7.6 budget knob inconclusive; escalation deferred."

**Step 1: Identify the verdict**

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

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

**Step 3: Verify no orphan claims**

```bash
git grep -n "10pp\|goal-stack lift\|−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: 1453 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
git commit -m "docs: apply v1.7.6 verdict={VERDICT} consequences across CHANGELOG/README/RESEARCH/TODOS"
```

---

## Task 8: 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.5 → 1.7.6 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.

**Step 5: Capture session state**

```bash
hippo capture --stdin <<< "v1.7.6 shipped. Verdict: {VERDICT}. {one-line summary of what was learned}."
```

**Step 6: Final TODOS sweep**

Verify `TODOS.md` reflects:
- v1.7.6 line closed (or retargeted to v1.7.7+ if SANITY_FAIL).
- v1.8.0 vlPFC line still open and unchanged.
- Any new follow-ups (e.g., adversarial-categories knob if SANITY_FAIL) 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). Per CLAUDE.md "Outside Voice" rule, dispatch outside voice on this plan BEFORE any code touches.

**Reviewers:**
- `/plan-eng-review` — in-house architecture critique
- `/codex` — cross-model adversarial review (gpt-5.5 thinking high if available, else gpt-5.4)

**Brief both reviewers on:**
- Plan file path: `docs/plans/2026-05-07-v1.7.6-discriminating-workload.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`, `benchmarks/sequential-learning/run.mjs`, `benchmarks/sequential-learning/aggregate.mjs`, `benchmarks/sequential-learning/traps.mjs`.
- Cap report length at ~600 words each.
- Look for: (a) goalposts-shifting concerns on the calibration→hypothesis seed split; (b) overfit risk on B* picked from N=5; (c) missing mitigations in the risk register; (d) statistical pitfalls in the permutation CI usage; (e) drift between v1.7.5 frame and v1.7.6 frame; (f) any contract drift in the budget plumbing; (g) test coverage holes; (h) honest-reporting weaknesses in the report templates.

**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. Zero new statistical machinery.
- ✅ Calibration script is small (~150 lines); uses `selectBStar` as the only new pure function (TDD-friendly).
- ✅ Budget plumbing is a single signature change + 4 adapter updates; backward-compat tested.
- ✅ Pre-registration commit ordering enforced before hypothesis run.
- ✅ Three explicit verdict branches in Task 7; no implicit "we'll figure it out" drift.
- ✅ Outside voice runs against the plan before code touches.

---

## 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 calibration + hypothesis run together are ~30 min and all in-session.
