Skip to content

Commit 40edfd6

Browse files
th3vib3coderclaude
andcommitted
phase9-wave2-t24-round50: pin Wave 4 trigger claim with pre-handoff fixture
Round 50 adversarial closure on top of seq 064. The ledger claim "heartbeat / loop-iteration / handoff / pre-compact / manual snapshot triggers" was only pinned at the writer level for heartbeat + pre-compact. Round 50 adds valid-pre-handoff.json fixture, registers it in the schema test, and pins three writer regressions: pre-handoff end-to-end, loop-iteration end-to-end (budget decrement 20 -> 19), and mismatched-objectiveRecord fail-closed guard. No new runtime surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2ded5e9 commit 40edfd6

4 files changed

Lines changed: 158 additions & 1 deletion

File tree

environment/tests/control/resume-snapshot.test.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,96 @@ test('writeObjectiveResumeSnapshot treats active pointer as lease authority and
141141
await rm(projectRoot, { recursive: true, force: true });
142142
}
143143
});
144+
145+
test('writeObjectiveResumeSnapshot pins the Wave 4 pre-handoff trigger claim made in ledger seq 064', async () => {
146+
const projectRoot = await mkdtemp(path.join(tmpdir(), 'vre-resume-snapshot-prehandoff-'));
147+
try {
148+
const objectiveRecord = await readFixture('objective', 'valid-active.json');
149+
await activateObjective(projectRoot, objectiveRecord, {
150+
sessionId: 'sess-resume-snapshot-prehandoff'
151+
});
152+
153+
const result = await writeObjectiveResumeSnapshot(projectRoot, objectiveRecord.objectiveId, {
154+
writtenReason: 'pre-handoff',
155+
writtenAt: '2026-04-23T11:30:00Z',
156+
notes: 'Pre-handoff checkpoint before opening cross-role consultation.'
157+
});
158+
159+
assert.equal(result.snapshot.schemaVersion, 'phase9.resume-snapshot.v1');
160+
assert.equal(result.snapshot.writtenReason, 'pre-handoff');
161+
assert.equal(result.snapshot.objectiveStatusAtSnapshot, 'active');
162+
assert.equal(result.snapshot.reasoningMode, 'rule-only');
163+
164+
const freshRead = await readResumeSnapshot(projectRoot, objectiveRecord.objectiveId);
165+
assert.equal(freshRead.exists, true);
166+
assert.equal(freshRead.validationError, null);
167+
assert.equal(freshRead.snapshot.writtenReason, 'pre-handoff');
168+
} finally {
169+
await rm(projectRoot, { recursive: true, force: true });
170+
}
171+
});
172+
173+
test('writeObjectiveResumeSnapshot pins the Wave 4 loop-iteration trigger claim made in ledger seq 064', async () => {
174+
const projectRoot = await mkdtemp(path.join(tmpdir(), 'vre-resume-snapshot-loopiter-'));
175+
try {
176+
const objectiveRecord = await readFixture('objective', 'valid-active.json');
177+
await activateObjective(projectRoot, objectiveRecord, {
178+
sessionId: 'sess-resume-snapshot-loopiter'
179+
});
180+
await appendObjectiveEvent(
181+
projectRoot,
182+
objectiveRecord.objectiveId,
183+
'loop-iteration',
184+
{
185+
iteration: 1,
186+
status: 'completed'
187+
},
188+
'2026-04-23T12:00:00Z'
189+
);
190+
191+
const result = await writeObjectiveResumeSnapshot(projectRoot, objectiveRecord.objectiveId, {
192+
writtenReason: 'loop-iteration',
193+
writtenAt: '2026-04-23T12:00:30Z'
194+
});
195+
196+
assert.equal(result.snapshot.writtenReason, 'loop-iteration');
197+
// One loop-iteration event has been appended; maxIterations=20, so maxIterationsLeft=19.
198+
assert.equal(result.snapshot.budgetRemaining.maxIterationsLeft, 19);
199+
200+
const freshRead = await readResumeSnapshot(projectRoot, objectiveRecord.objectiveId);
201+
assert.equal(freshRead.exists, true);
202+
assert.equal(freshRead.validationError, null);
203+
assert.equal(freshRead.snapshot.writtenReason, 'loop-iteration');
204+
} finally {
205+
await rm(projectRoot, { recursive: true, force: true });
206+
}
207+
});
208+
209+
test('writeObjectiveResumeSnapshot fails closed when injected objectiveRecord does not match requested objectiveId', async () => {
210+
const projectRoot = await mkdtemp(path.join(tmpdir(), 'vre-resume-snapshot-mismatch-'));
211+
try {
212+
const objectiveRecord = await readFixture('objective', 'valid-active.json');
213+
await activateObjective(projectRoot, objectiveRecord, {
214+
sessionId: 'sess-resume-snapshot-mismatch'
215+
});
216+
217+
const rogueRecord = {
218+
...objectiveRecord,
219+
objectiveId: 'OBJ-9999-99-99-rogue'
220+
};
221+
222+
await assert.rejects(
223+
writeObjectiveResumeSnapshot(projectRoot, objectiveRecord.objectiveId, {
224+
writtenReason: 'manual',
225+
objectiveRecord: rogueRecord
226+
}),
227+
/does not match requested snapshot objective/
228+
);
229+
230+
// The guard must fire before any snapshot write, so the snapshot file must not exist.
231+
const freshRead = await readResumeSnapshot(projectRoot, objectiveRecord.objectiveId);
232+
assert.equal(freshRead.exists, false);
233+
} finally {
234+
await rm(projectRoot, { recursive: true, force: true });
235+
}
236+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"schemaVersion": "phase9.resume-snapshot.v1",
3+
"writtenAt": "2026-04-23T11:30:00Z",
4+
"writtenReason": "pre-handoff",
5+
"objectiveId": "OBJ-2026-04-22-001",
6+
"objectiveStatusAtSnapshot": "active",
7+
"runtimeMode": "unattended-batch",
8+
"reasoningMode": "rule-only",
9+
"wakePolicySnapshot": {
10+
"wakeOwner": "windows-task-scheduler",
11+
"wakeSourceId": "TASK-OBJ-2026-04-22-001",
12+
"heartbeatIntervalSeconds": 1800,
13+
"leaseTtlSeconds": 900,
14+
"duplicateWakePolicy": "no-op"
15+
},
16+
"budgetRemaining": {
17+
"maxWallSecondsLeft": 14200,
18+
"maxIterationsLeft": 10,
19+
"costCeilingLeft": 17.00
20+
},
21+
"queueVisibility": {
22+
"queuePath": ".vibe-science-environment/objectives/OBJ-2026-04-22-001/queue.json",
23+
"queueCursor": "cursor-3",
24+
"pendingCount": 0,
25+
"runningCount": 0,
26+
"lastTaskId": "TASK-003"
27+
},
28+
"stageCursor": {
29+
"current": "analysis",
30+
"stageStatus": "active",
31+
"lastCompleteStage": "orientation"
32+
},
33+
"nextAction": {
34+
"kind": "enqueue-task",
35+
"params": {
36+
"stageId": "analysis",
37+
"blockedByHandoffId": "H-0008"
38+
}
39+
},
40+
"openBlockers": [],
41+
"openHandoffs": [
42+
"H-0007",
43+
"H-0008"
44+
],
45+
"wakeLease": {
46+
"wakeId": "WAKE-2026-04-23-001",
47+
"leaseAcquiredAt": "2026-04-23T11:25:00Z",
48+
"leaseExpiresAt": "2026-04-23T11:40:00Z",
49+
"acquiredBy": "scheduler",
50+
"previousWakeId": "WAKE-2026-04-22-001"
51+
},
52+
"kernelFingerprint": {
53+
"lastClaimId": "CLM-002",
54+
"lastCitationCheckId": "CIT-005",
55+
"lastR2VerdictId": "R2-001",
56+
"lastObserverAlertId": null,
57+
"lastGateCheckId": "GATE-003",
58+
"lastPatternId": null,
59+
"takenAt": "2026-04-23T11:29:45Z"
60+
},
61+
"notes": "Pre-handoff checkpoint before opening H-0008 cross-role consultation."
62+
}

environment/tests/schemas/phase9-resume-snapshot.schema.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { expectFixtureValidity } from './phase9-schema-fixture-helper.js';
55
for (const fixturePath of [
66
'environment/tests/fixtures/phase9/resume-snapshot/valid-mid-loop.json',
77
'environment/tests/fixtures/phase9/resume-snapshot/valid-pre-stop.json',
8-
'environment/tests/fixtures/phase9/resume-snapshot/valid-heartbeat.json'
8+
'environment/tests/fixtures/phase9/resume-snapshot/valid-heartbeat.json',
9+
'environment/tests/fixtures/phase9/resume-snapshot/valid-pre-handoff.json'
910
]) {
1011
test(`phase9-resume-snapshot.schema accepts ${fixturePath}`, async () => {
1112
await expectFixtureValidity({

phase9-vre-feature-ledger.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ Companion files:
127127
| 062 | 2026-04-23 | 2 | W2-OBJECTIVE-CLI | Land the T2.3 objective lifecycle CLI surface: structured JSON for lifecycle success/errors, explicit wake-policy validation, `objective status --json`, `objective resume`, blocker resolution, and snapshot repair without mutating immutable objective fields | `bin/vre`, `environment/objectives/cli.js`, `environment/tests/cli/objective-cli.test.js`, `environment/tests/control/objective-lock.test.js`, `environment/tests/cli/bin-vre-phase9-stubs.test.js`, `environment/tests/control/capability-handshake.test.js`, `environment/tests/fixtures/phase9/capability-handshake/valid-full.json`, `environment/tests/fixtures/phase9/capability-handshake/valid-degraded-no-kernel.json`, `environment/tests/ci/phase9-surface-index.js`, `environment/tests/ci/phase9-surface-index.test.js`, `environment/tests/ci/validate-runtime-contracts.js`, `environment/tests/ci/validate-counts.js`, `package.json`, `phase9-vre-surface-index.json` | none | `node --test environment/tests/cli/objective-cli.test.js` (7/7 pass); `node --test environment/tests/control/objective-lock.test.js` (9/9 pass); `node --test environment/tests/control/capability-handshake.test.js` (7/7 pass); `node --test environment/tests/schemas/phase9-capability-handshake.schema.test.js` (13/13 pass); adversarial live attack matrix A1-A5 (wake-policy fail-closed, interactive manual default, pause/status summary, divergent snapshot repair, `BLOCKER.flag` blocker-resolve ordering) all defended; `npm run test:phase9` (157 pass, 0 fail, 1 skipped) | verified | Wave 2 `T2.3`. `objective start` now fails closed with structured `E_WAKE_POLICY_REQUIRED` for unattended modes without explicit wake policy while interactive mode uses an explicit manual/no-op wake policy. `objective status --json` reports the canonical objective summary (status, runtime/reasoning mode, wake lease health, capability summary, iteration/wall-clock budget, blocker/stop signals, and artifact pointers). `objective pause` and `objective stop` now write resume snapshots; `objective resume` reactivates paused/blocked objectives, writes `blocker-resolve` before removing `BLOCKER.flag`, supports `--repair-snapshot`, appends `state-repair(repairedLayer=snapshot)`, and preserves immutable `objective.reasoningMode`. Lifecycle error branches now return structured JSON payloads through `ObjectiveCliError`, closing the UX consistency follow-up noted in seq 061 for the T2.3-owned commands. `objective doctor` remains a Wave 0 stub by design; no stale-lock reclaim, reusable reconciliation engine, or plugin startup loader is introduced here. Same-pass bookkeeping cleanup: the seq 061 notes now correctly say Round 46 instead of the stale Round 44 label. |
128128
| 063 | 2026-04-23 | 2 | W2-OBJECTIVE-RESUME-STRUCTURED-ERROR-FIX | Fix the missing `await` before `withLock` in `resumeObjectiveCommand` so that resume error branches actually reach `coerceObjectiveCliError` and become structured JSON through `ObjectiveCliError`, closing the contradiction between the seq 062 claim and the real post-T2.3 runtime | `environment/objectives/cli.js`, `environment/tests/cli/objective-cli.test.js` | none | `node --test environment/tests/cli/objective-cli.test.js` (9/9 pass; +2 new regression tests pinning `resume-when-active` -> `E_OBJECTIVE_STATE_INVALID` and `resume-after-stop` -> `E_ACTIVE_OBJECTIVE_POINTER_MISSING`); `npm run test:phase9` (159 pass, 0 fail, 1 skipped); `npm run validate` (green, all 12 validators); live repro pre-fix: `objective resume` of an active objective emitted plain-text stderr `vre: objective resume: Cannot resume objective in status active`; live repro post-fix: same scenario emits `{ok:false, code:"E_OBJECTIVE_STATE_INVALID", transition:"resume", status:"active"}` via `writeJsonPayload` | verified | Round 48 adversarial closure on top of seq 062. Seq 062 claimed that "Lifecycle error branches now return structured JSON payloads through ObjectiveCliError, closing the UX consistency follow-up noted in seq 061 for the T2.3-owned commands". Adversarial re-review confirmed that the claim held for `objective start`, `objective pause`, `objective stop`, and `objective status`, but NOT for `objective resume`: two resume error branches fell through the dispatcher `ObjectiveCliError` catcher (`bin/vre:1117`) and emitted plain-text stderr via the generic handler (`bin/vre:1128`). Root cause: `resumeObjectiveCommand` used `return withLock(...)` instead of `return await withLock(...)` at `environment/objectives/cli.js:659`, so the outer `try/catch` exited synchronously before the inner `withLock` promise rejected, and raw `Error` objects (not `ObjectiveCliError`) bubbled up to `runObjectiveResume`. Fix: one-line `return` -> `return await` change, plus a code comment explaining the semantics so a future refactor cannot regress it silently. Known follow-up, NOT closed here: the canonical resume-snapshot writer currently lives in `cli.js` (full `phase9.resume-snapshot.v1` field set) rather than in `environment/objectives/resume-snapshot.js` as the `T2.4` plan prescribes; this is a soft scope-creep that `T2.4` will refactor when it lands, without introducing a new writer. Also deliberately NOT closed here: seq 061 was edited in-place to say Round 46 instead of Round 44; this deviates from the strict append-only rule, is transparently disclosed in seq 062 and in this row, and is accepted as a one-time bookkeeping exception, not as a pattern. |
129129
| 064 | 2026-04-23 | 2 | W2-RESUME-SNAPSHOT-WRITER | Refactor the canonical Phase 9 resume snapshot writer out of the objective CLI into a reviewed `environment/objectives/resume-snapshot.js` API that Wave 4 can call after loop iterations, heartbeat no-ops, blocker transitions, handoffs, pause/stop, pre-compact, and manual snapshot commands | `environment/objectives/resume-snapshot.js`, `environment/objectives/cli.js`, `environment/objectives/store.js`, `environment/tests/control/resume-snapshot.test.js`, `environment/tests/cli/objective-cli.test.js`, `environment/tests/fixtures/phase9/resume-snapshot/valid-heartbeat.json`, `environment/tests/fixtures/phase9/resume-snapshot/invalid-missing-queue-visibility.json`, `environment/tests/fixtures/phase9/resume-snapshot/invalid-missing-next-action.json`, `environment/tests/fixtures/phase9/resume-snapshot/invalid-missing-open-handoffs.json`, `environment/tests/schemas/phase9-resume-snapshot.schema.test.js`, `environment/tests/ci/phase9-surface-index.js`, `environment/tests/ci/phase9-surface-index.test.js`, `environment/tests/ci/validate-runtime-contracts.js`, `environment/tests/ci/validate-counts.js`, `package.json`, `phase9-vre-surface-index.json` | none | `node --test environment/tests/control/resume-snapshot.test.js` (2/2 pass); `node --test environment/tests/cli/objective-cli.test.js` (10/10 pass; +1 stale kernel-fingerprint resume block regression); `node --test environment/tests/schemas/phase9-resume-snapshot.schema.test.js` (10/10 pass; +1 valid heartbeat fixture and +3 required-field negative fixtures); `node --test environment/tests/ci/phase9-surface-index.test.js` (2/2 pass); `npm run build:surface-index` (31 total surfaces, including `resume-snapshot`); `npm run test:phase9` (166 pass, 0 fail, 1 skipped); `npm run validate` (green, all 12 validators) | verified | Wave 2 `T2.4`. This is a refactor-not-reimplementation of the writer that T2.3 temporarily hosted in `environment/objectives/cli.js`: `writeResumeSnapshot`, `writeObjectiveResumeSnapshot`, `readResumeSnapshot`, `detectSnapshotDivergence`, `appendObjectiveEvent`, and `readBlockerFlag` now live in `environment/objectives/resume-snapshot.js` and validate through the reviewed schema host fallback. The new API writes the full `phase9.resume-snapshot.v1` required field set, mirrors `activePointer.currentWakeLease` without making the snapshot the lease authority, preserves immutable objective fields such as `reasoningMode`, and gives Wave 4 a direct callable surface for heartbeat / loop-iteration / handoff / pre-compact / manual snapshot triggers. `objective pause`, `objective stop`, `objective resume --repair-snapshot`, and `objective status --json` now consume the shared module instead of carrying a private writer. Required T2.4 fixture coverage is expanded with `valid-heartbeat`, missing `queueVisibility`, missing `nextAction`, and missing `openHandoffs`; stale/invalid kernel fingerprint now blocks `objective resume` through structured `E_RESUME_SNAPSHOT_INVALID` while leaving the paused objective state untouched. No plugin startup loader, unattended runtime loop, stale-lock reclaim, or separate active pointer source is introduced here; those remain owned by later Wave 2 / Wave 4 tasks. |
130+
| 065 | 2026-04-23 | 2 | W2-RESUME-SNAPSHOT-WRITER-WAVE4-TRIGGER-COVERAGE | Pin the Wave 4 trigger claim made in seq 064 (`gives Wave 4 a direct callable surface for heartbeat / loop-iteration / handoff / pre-compact / manual snapshot triggers`) with dedicated writer-level regression tests and a valid `pre-handoff` fixture, closing the coverage gap where `pre-handoff` and `loop-iteration` trigger reasons were enumerated in the schema but not exercised through `writeObjectiveResumeSnapshot` | `environment/objectives/resume-snapshot.js`, `environment/tests/control/resume-snapshot.test.js`, `environment/tests/schemas/phase9-resume-snapshot.schema.test.js`, `environment/tests/fixtures/phase9/resume-snapshot/valid-pre-handoff.json` | none | `node --test environment/tests/control/resume-snapshot.test.js` (5/5 pass; +3 new regression tests pinning `writtenReason: pre-handoff` end-to-end, `writtenReason: loop-iteration` end-to-end, and `writeObjectiveResumeSnapshot` fail-closed on mismatched injected `objectiveRecord`); `node --test environment/tests/schemas/phase9-resume-snapshot.schema.test.js` (11/11 pass; +1 `valid-pre-handoff.json` fixture); `npm run test:phase9` (170 pass, 0 fail, 1 skipped); `npm run validate` (green, all 12 validators); explicit `npm run check:phase9-ledger -- --changed-file=...` (green on full T2.4 + Round 50 delta) | verified | Round 50 adversarial closure on top of seq 064. Seq 064 claim language (`heartbeat / loop-iteration / handoff / pre-compact / manual snapshot triggers`) had only two of the five triggers exercised at the writer level (`heartbeat` via `resume-snapshot.test.js` test 1 and `pre-compact` via test 2). `operator-pause`, `pre-stop`, and `manual` were pinned transitively through the CLI tests (`objective-cli.test.js` pause/stop/resume --repair-snapshot). `loop-iteration` existed only as a schema fixture (`valid-mid-loop.json`), with no writer-level invocation, and `pre-handoff` had neither fixture nor test. This is the exact same class of gap Round 48 closed for `objective resume`: a ledger claim made transparently but not actually pinned by a test. Round 50 adds (a) `valid-pre-handoff.json` with an enum-valid `nextAction.kind=enqueue-task` carrying an explicit `blockedByHandoffId` param, registered in the schema test; (b) a writer-level `pre-handoff` trigger test that asserts schema-valid output AND a clean round-trip through `readResumeSnapshot`; (c) a writer-level `loop-iteration` trigger test that also asserts the event counter decrements `budgetRemaining.maxIterationsLeft` (20 -> 19) after `appendObjectiveEvent(kind=loop-iteration)`; and (d) a writer-level regression test for the `writeObjectiveResumeSnapshot` defensive guard at `resume-snapshot.js:334-338` that throws when a caller injects an `objectiveRecord` whose `objectiveId` does not match the `objectiveId` argument, with a post-rejection assertion that `resume-snapshot.json` was never written. No runtime surface added; this is pure coverage hardening. |

0 commit comments

Comments
 (0)