Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/plan-anvil/references/codex-0.152-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ For Codex 0.152, `SubagentStart` may inject `additionalContext`, but `continue:
## Fail-closed rule

When the active Codex runtime cannot provide a lifecycle behavior required by the PlanAnvil contract, preserve deterministic evidence and stop with a runtime prerequisite blocker. Do not weaken path, source-immutability, approval, or recovery guarantees to make the run pass.

## Recovery delivery — source-verified Codex CLI 0.153.4

The [official hooks contract](https://developers.openai.com/codex/hooks/) distinguishes stateless compaction events from context-delivery events. `PostCompact` may emit universal control/advisory fields, but its output is not model-visible additional context. PlanAnvil must supply recovery pointers through `SessionStart` matching `source=compact` after compaction; the existing startup/resume/clear matchers remain supported.

This is a product rule, not a test-only substitution. The PostCompact recovery command reports readiness without embedding the next-action target. The SessionStart command supplies the pointer and asks the model to read canonical files and reconcile Git before continuing. Hooks never substitute for those checks. The pinned `rust-v0.153.4` sources are `codex-rs/hooks/src/events/compact.rs` and `codex-rs/core/src/hook_runtime.rs`.
29 changes: 29 additions & 0 deletions .agents/skills/plan-anvil/tests/test_hooks_and_edge_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,35 @@ def test_compaction_accepts_schema_and_git_valid_checkpoint(self) -> None:
self.assertIn("Validated checkpoint", message)
self.assertIn(str(checkpoint_path.resolve()), message)

def test_compact_recovery_uses_session_start_not_postcompact_context(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = self._active_repo(Path(directory), mode="PLAN_EXECUTION")
self._write_valid_checkpoint(repo)
target = "evidence/instruction-map.json"
before = (repo / ".pursue/runs/run/state.json").read_bytes()
advisory = self._hook("plan-anvil-recovery.py", repo,
{"cwd": str(repo), "hook_event_name": "PostCompact", "trigger": "auto"})
self.assertEqual(set(advisory), {"continue", "systemMessage"})
self.assertTrue(advisory["continue"])
self.assertNotIn(target, json.dumps(advisory))
self.assertIn("SessionStart(source=compact)", advisory["systemMessage"])
recovery = self._hook("plan-anvil-recovery.py", repo,
{"cwd": str(repo), "hook_event_name": "SessionStart", "source": "compact"})
output = recovery["hookSpecificOutput"]
self.assertEqual(output["hookEventName"], "SessionStart")
self.assertIn(target, output["additionalContext"])
self.assertIn("Validated checkpoint", output["additionalContext"])
self.assertEqual((repo / ".pursue/runs/run/state.json").read_bytes(), before)

def test_postcompact_reports_invalid_checkpoint_without_unsupported_fields(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = self._active_repo(Path(directory))
output = self._hook("plan-anvil-recovery.py", repo,
{"cwd": str(repo), "hook_event_name": "PostCompact", "trigger": "auto"})
self.assertEqual(set(output), {"continue", "systemMessage"})
self.assertIn("invalid", output["systemMessage"])
self.assertNotIn("hookSpecificOutput", output)

def test_detached_head_with_multiple_containing_branches_is_ambiguous(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repo = init_repo(Path(directory) / "repo")
Expand Down
2 changes: 1 addition & 1 deletion .codex/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{
"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/plan-anvil-recovery.py\"",
"commandWindows": "py -3 -c \"import pathlib,runpy,subprocess;r=pathlib.Path(subprocess.check_output(['git','rev-parse','--show-toplevel'],text=True).strip());runpy.run_path(str(r/'.codex/hooks/plan-anvil-recovery.py'),run_name='__main__')\"",
"statusMessage": "Restoring PlanAnvil recovery pointer",
"statusMessage": "Checking PlanAnvil post-compaction recovery readiness",
"timeout": 30,
"type": "command"
}
Expand Down
26 changes: 24 additions & 2 deletions .codex/hooks/plan-anvil-recovery.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import json

from plan_anvil_checkpoint import validate_checkpoint_for_run
from plan_anvil_hooklib import (
active_run_for_event,
Expand All @@ -9,11 +11,24 @@
)


def emit_recovery(event_name: str, message: str) -> None:
if event_name == "PostCompact":
# Codex 0.153.4 PostCompact accepts universal output, not additionalContext.
# Keep this advisory free of recovery targets; SessionStart(source=compact)
# is the model-context delivery boundary, before the continuation request.
print(json.dumps({
"continue": True,
"systemMessage": message,
}, sort_keys=True))
else:
context(event_name, message)


def main() -> int:
event = read_event()
event_name = str(event.get("hook_event_name") or "SessionStart")
if event_has_ambiguous_active_runs(event):
context(
emit_recovery(
event_name,
"Multiple active PlanAnvil runs match this worktree. Set PLANANVIL_RUN_ID to the intended run before recovery or write-capable work.",
)
Expand All @@ -36,7 +51,14 @@ def main() -> int:
f"next action is {next_action.get('type')} targeting {next_action.get('target')}. "
f"{checkpoint_text}"
)
context(event_name, message)
if event_name == "PostCompact":
readiness = "valid" if checkpoint.ok else "invalid; repair required before continuing"
message = (
f"PlanAnvil checkpoint is {readiness}. "
"SessionStart(source=compact) supplies the recovery pointer; "
"canonical files and Git remain authoritative."
)
emit_recovery(event_name, message)
return 0


Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/plananvil-codex-qualification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,23 @@ jobs:
echo "exit_code=${rc}" >> "${GITHUB_OUTPUT}"
exit 0

- name: Pack and verify complete sanitized evidence
id: package_evidence
if: always() && env.QUALIFICATION_ARTIFACT != ''
shell: bash
run: |
set -euo pipefail
cd "${QUALIFICATION_REPO}"
python3 tools/qualification_artifact.py \
--root "${QUALIFICATION_ARTIFACT}" \
--output "${QUALIFICATION_ARTIFACT}.zip"

- name: Upload sanitized capability evidence
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: plananvil-codex-evidence-${{ github.run_id }}
path: ${{ env.QUALIFICATION_ARTIFACT }}
path: ${{ env.QUALIFICATION_ARTIFACT }}.zip
if-no-files-found: error
retention-days: 14

Expand All @@ -361,3 +372,4 @@ jobs:
run: |
set -euo pipefail
test "${{ steps.qualify.outputs.exit_code }}" = "0"
test "${{ steps.package_evidence.outcome }}" = "success"
2 changes: 1 addition & 1 deletion capabilities/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{"evidence_directory":"capabilities/C07","expected_behavior":"The Git guard rejects the configured unsafe-command corpus","id":"C07","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"},
{"evidence_directory":"capabilities/C08","expected_behavior":"PreCompact can stop compaction","id":"C08","required":true,"result":"BLOCKED","source":"DOCUMENTED"},
{"evidence_directory":"capabilities/C09","expected_behavior":"Compaction is allowed after checkpoint creation without a permanent loop","id":"C09","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"},
{"evidence_directory":"capabilities/C10","expected_behavior":"PostCompact and SessionStart can provide recovery context","id":"C10","required":true,"result":"BLOCKED","source":"DOCUMENTED"},
{"evidence_directory":"capabilities/C10","expected_behavior":"SessionStart supplies recovery context at startup/resume and source=compact; PostCompact is advisory","id":"C10","required":true,"result":"BLOCKED","source":"DOCUMENTED"},
{"evidence_directory":"capabilities/C11","expected_behavior":"Project instructions follow documented directory scope and precedence","id":"C11","required":true,"result":"BLOCKED","source":"DOCUMENTED"},
{"evidence_directory":"capabilities/C12","expected_behavior":"project_doc_max_bytes can truncate automatic instruction loading","id":"C12","required":true,"result":"BLOCKED","source":"DOCUMENTED"},
{"evidence_directory":"capabilities/C13","expected_behavior":"SubagentStart can add context but continue false does not stop subagent startup","id":"C13","required":true,"result":"BLOCKED","source":"DOCUMENTED"},
Expand Down
2 changes: 1 addition & 1 deletion docs/CODEX_CAPABILITY_BASELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Do not commit session transcripts, credentials, private paths, unrelated Git dat
| C07 | The Git guard rejects the configured unsafe-command corpus | CONTRACT_DEFINED | BLOCKED | Git postconditions remain mandatory |
| C08 | `PreCompact` can stop compaction | DOCUMENTED | BLOCKED | Delay only until a valid checkpoint exists |
| C09 | Compaction is allowed after checkpoint creation without a permanent stop loop | CONTRACT_DEFINED | BLOCKED | Checkpoint, allow, recover and reconcile |
| C10 | `PostCompact` and `SessionStart` can provide recovery context | DOCUMENTED | BLOCKED | Inject only a recovery pointer |
| C10 | `SessionStart` supplies recovery context at startup/resume and after compaction (`source=compact`); `PostCompact` remains advisory | DOCUMENTED | BLOCKED | Inject only a recovery pointer through the supported context event |
| C11 | Project instructions follow documented directory scope and precedence | DOCUMENTED | BLOCKED | Explicitly map affected instructions |
| C12 | `project_doc_max_bytes` can truncate automatic instruction loading | DOCUMENTED | BLOCKED | Read, size and hash complete files explicitly |
| C13 | `SubagentStart` can add context but `continue: false` does not stop subagent startup | DOCUMENTED | BLOCKED | Context/audit only; qualify ephemeral-first with a controlled project-scoped non-ephemeral fallback when the recognized ephemeral parent-thread blocker occurs |
Expand Down
2 changes: 2 additions & 0 deletions docs/CODEX_QUALIFICATION_EXECUTION_AUDIT_2026-09-05.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Regression: enter the real compatibility context, seed the same declared project

### C10 configuration provenance

**Superseded assumption:** this historical audit correctly identified root-checkout declaration precedence, but incorrectly assumed that PostCompact itself could deliver model context. The subsequent source review in `CODEX_RECOVERY_DELIVERY_AUDIT_2026-09-05.md` establishes SessionStart(source=compact) as the supported recovery channel. The current fixture narrows that matcher instead of removing SessionStart. The following paragraphs describe the earlier patch, not the active delivery contract.

The previous PostCompact probe removed SessionStart only from a linked planning worktree. Codex still loaded the primary checkout's declarations. The same proof could reach the model through SessionStart, invalidating attribution to PostCompact.

Repair: independent source repositories, planning worktrees and random proofs for the two trials. Prepare the second root checkout's hook selection before the fixture commit, product bootstrap and checkpoint. Verify root/local declarations agree and preserve both source and planning state during the probe. Keep the actual product recovery and compaction scripts; never synthesize a live hook event in the qualification runtime.
Expand Down
52 changes: 52 additions & 0 deletions docs/CODEX_RECOVERY_DELIVERY_AUDIT_2026-09-05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Recovery delivery and evidence audit — 2026-09-05

## Scope and authority

Baseline input: PlanAnvil `57909a810b3ceb9deeac8550fd510dc4df28a951`; qualification [run #22](https://github.com/KeyffMS/PlanAnvil/actions/runs/33978465210). C13 was reproduced using its allowed project-scoped fallback and its runtime is unchanged. C09 timed out and C10 lacked exact echo in the isolated after-compaction probe. The previous audit's assumption that PostCompact was a model-context channel was incorrect.

The implementation specification gives current official Codex documentation priority. This is a product/output-contract correction, not a waiver of capability requirements. No offline test here is evidence that the live capability is reproduced.

## Source-verified recovery channel

The [official hooks reference](https://developers.openai.com/codex/hooks/) explicitly limits PostCompact output to common fields and ignores plain stdout. SessionStart matches `source`; `source=compact` runs before the immediate next model request, including compaction in the middle of a turn.

Pinned Codex source at `rust-v0.153.4`:

| File | Boundary |
|---|---|
| [hooks/src/events/compact.rs](https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/hooks/src/events/compact.rs) | StatelessHookOutcome and parsing have no additional-context return channel. |
| [core/src/hook_runtime.rs](https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/core/src/hook_runtime.rs) | run_post_compact_hooks emits events/control only; pending SessionStart uses record_additional_contexts. |
| [hooks/src/events/session_start.rs](https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/hooks/src/events/session_start.rs) | SessionStart source matching and additionalContext interpretation. |
| [config/src/loader/mod.rs](https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/config/src/loader/mod.rs) | Linked-worktree hook declarations come from the corresponding primary checkout. |

Product behavior: PostCompact reports checkpoint readiness through universal `systemMessage`, with `continue=true`, without including the next-action target. SessionStart supplies the existing pointer/context. This does not change canonical state, checkpoint validation, source ownership or the generator/executor separation.

C10 tests startup and after-compaction recovery in independent real product fixtures with different opaque targets. The second fixture narrows SessionStart to `^compact$` in the primary checkout BEFORE commit/start/checkpoint. It must observe PreCompact, PostCompact and subsequently SessionStart(source=compact), with no ordinary startup record. PostCompact output must contain no additional context. A matching product-emitted target, exact model echo, allowed tool usage, checkpoint validity and source/planning immutability are all required. The existing two acceptance assertion strings are unchanged.

## C09/C10 observation and timeout boundary

Only the C09/C10 process invocations opt into the new observed runner. It retains event counts, the last 128 structural events, exact allowlisted command labels, category-only error counts, and process exit/cleanup status. It never retains raw stdout/stderr, command output, unknown command text, prompts or thread IDs. Lines are size-bounded; malformed/oversized diagnostics are counted.

On timeout, terminate the owned process group on POSIX or process tree on Windows, wait for it and drain bounded reader threads before proceeding. A cleanup/reader failure blocks the probe. This prevents a CLI launcher timeout from silently leaving its ordinary descendants running during the next capability. It does not claim to control a process which deliberately escapes its owned group/tree.

C09 retains a bounded structural hook sequence as well. The 900-second timeout, compaction threshold, two-cycle/continuation/checkpoint requirements and rejection of incomplete results remain. We have not proved the cause of the remaining C09 timeout and do not claim to have fixed its runtime completion. C08's intentional negative-path handling and C13's runner are unchanged.

C10 compares the real product's emitted target against an outer-generated digest, then checks the structured model payload both before and after sanitization. Persisted value-flow diagnostics contain booleans/counts/lengths and fixed classification labels, not proof values or their hashes. Wrong-format echo is diagnosed but not accepted.

## Complete, limited evidence upload

The workflow builds a deterministic inner ZIP from validated staged evidence. Packaging requires exactly C01-C16 manifests plus top-level summary/index/guide. Each listed file, including the three hidden fixture files, is hash-checked; unlisted files and symlinks are rejected. ZIP verification independently checks per-capability manifests and the archive manifest, with path and size limits. Removing a hidden member and rewriting only the outer manifest still fails.

Only the verified archive is uploaded, not the runner workspace or an unrestricted set of dotfiles. Packaging failure also fails the selected gate. The downloaded Actions artifact contains this inner evidence ZIP. No secret-bearing runtime files are added to the allowlist.

## Regression strategy and handoff

Executable tests cover real product hook output at PostCompact and SessionStart(source=compact), the installer/Git/start/checkpoint path, root-source matcher isolation, exact echo acceptance, proof-safe diagnostic comparison, actual process timeout/descendant cleanup, partial event retention, malformed/oversized streams and strict archive verification. Existing product and qualification suites remain mandatory. The offline lifecycle driver only treats documented SessionStart output as model context; it does not pretend that arbitrary PostCompact JSON is injectable.

The existing hosted OS/Python matrix runs `test_qualification_*.py` in addition to core product tests. Qualification results remain BLOCKED until new live evidence is produced. Use the existing workflow on main with `mode=recovery`; that selects C09/C10/C13 through the same runtimes as full and cannot claim a C01-C16 release pass. Do not rerun full until the targeted results are understood.

## Completion review — 2026-09-06

The interrupted upload was recovered and reviewed in PR #32. Unexpected diagnostic reader failures now set a fail-closed flag rather than disappearing in a daemon thread; deeply nested JSON is counted as malformed input without losing later events. Executable tests verify both reader and cleanup failures reject otherwise positive output. The temporary source-snapshot workflow is removed before merge. No live Codex workflow was started during this completion review.

The Actions artifact now contains an inner verified evidence ZIP. Extract that ZIP before reading qualification-summary.json and the C01-C16 evidence directories. The required hidden fixture files are inside the verified ZIP; unrelated runner dotfiles are never uploaded.
3 changes: 3 additions & 0 deletions docs/IMPLEMENTATION_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,15 @@ After compaction:

```text
POST_COMPACT_EVENT
→ SESSION_START(source=compact)
→ RECOVERY_POINTER
→ READ_MANIFEST_STATE_LOCAL_STATE_CHECKPOINT_PROFILES
→ RECONCILE_WITH_GIT
→ CONTINUE_OR_STOP
```

For Codex CLI 0.153.4, `PostCompact` is a stateless notification/control event; it MUST NOT be relied upon to inject `additionalContext`. The product's `SessionStart` handler matching `source=compact` supplies the recovery pointer to the immediate continuation. Startup/resume recovery uses the same handler with the corresponding source. PostCompact may emit a universal advisory `systemMessage`, without embedding canonical state or the next-action target.

Conversation and hook context are never the source of truth.

## 19. Status model
Expand Down
Loading