From a32a996e69cc646ed6b3394504345cabaa2777d8 Mon Sep 17 00:00:00 2001 From: Svrnty Date: Thu, 4 Jun 2026 14:59:18 -0400 Subject: [PATCH] CC: Cover Codex retention in CTO validator --- tools/validate_cto_child.py | 160 ++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/tools/validate_cto_child.py b/tools/validate_cto_child.py index 23436bf..cdb9ba2 100644 --- a/tools/validate_cto_child.py +++ b/tools/validate_cto_child.py @@ -111,6 +111,14 @@ REQUIRED_FILES = [ ".sot/03-PROTOCOLS/CTO-CASE-SPARK-ENDPOINT-CONFIG-PRD.md", ".sot/03-PROTOCOLS/CTO-CASE-SPARK-ENDPOINT-CONFIG-ISSUES.md", ".sot/03-PROTOCOLS/CTO-CASE-AGENT-PROTOCOL-BLOCKER.md", + ".sot/03-PROTOCOLS/CTO-CODEX-RETENTION-DRY-RUN-PACKET.md", + ".sot/03-PROTOCOLS/CTO-CODEX-RETENTION-POLICY-PACKET.md", + ".sot/03-PROTOCOLS/CTO-CODEX-RETENTION-ARCHIVE-EXECUTOR-PACKET.md", + "tools/report_codex_retention_pressure.py", + "tools/plan_codex_retention_policy.py", + "tools/archive_codex_inactive_threads.py", + "tools/probe_codex_native_retention.py", + "tools/codex_ephemeral_exec.py", ] REQUIRED_BRIEF_PHRASES = [ @@ -1656,6 +1664,105 @@ REQUIRED_CORE_ROUTE_ADMISSION_GUARD_CLOSEOUT_PHRASES = [ "candidate-only until the guard passes", ] +REQUIRED_CODEX_RETENTION_ISSUE_IDS = [ + "CTO-WORK-093", + "CTO-WORK-094", + "CTO-WORK-095", + "CTO-WORK-096", +] + +REQUIRED_CODEX_RETENTION_DRY_RUN_PHRASES = [ + "Local planning SOT only. Not a Core Protocol. Not active Core authority.", + "Codex retention pressure is measurable without reading raw transcripts or mutating `~/.codex`.", + "metadata-only JSON", + "It does not read transcript bodies, update SQLite, delete files, vacuum databases, archive threads, mutate Core, start Runtime, read secrets, change Codex config, or claim product readiness.", + "Prevention default: use `codex exec --ephemeral` for disposable non-interactive worker runs.", + "Blocked without explicit operator approval:", + "direct `threads.archived` updates", + "session JSONL deletion", + "logs table deletion", + "SQLite vacuum or checkpoint", + "raw transcript import into Core", + "broad cleanup of `~/.codex`", +] + +REQUIRED_CODEX_RETENTION_POLICY_PHRASES = [ + "Local planning SOT only. Not a Core Protocol. Not active Core authority.", + "Codex retention cleanup now has a backup-first, approval-gated policy plan.", + "`python3 tools/plan_codex_retention_policy.py` emits metadata-only JSON.", + "`python3 tools/probe_codex_native_retention.py` checks installed Codex CLI help, feature flags, and local version cache.", + "native cleanup/archive/retention command advertised by installed CLI: false", + "prevention flag advertised: `codex exec --ephemeral`", + "Phase 1: backup `state_5.sqlite`, `logs_2.sqlite`, WAL, and SHM files.", + "Phase 2: archive-only candidate threads by DB flag only after explicit approval.", + "Phase 3: delete archived session JSONL only after separate destructive approval.", + "`python3 tools/codex_ephemeral_exec.py` builds disposable worker commands as `codex exec --ephemeral`.", + "The helper is prevention only. It does not archive threads, delete JSONL, truncate logs, checkpoint, vacuum, read transcript bodies, or mutate Core.", + "Blocked without explicit operator approval:", + "updating `threads.archived`", + "deleting session JSONL", + "SQLite checkpoint or vacuum", + "raw transcript read/import", +] + +REQUIRED_CODEX_RETENTION_ARCHIVE_PHRASES = [ + "Local planning SOT only. Not a Core Protocol. Not active Core authority.", + "Codex retention cleanup now has a guarded archive-only executor.", + "Default mode is dry-run.", + "Mutation requires an exact approval token.", + "It does not delete session JSONL, truncate logs, checkpoint, vacuum, read transcript bodies, or import transcripts into Core.", + "python3 tools/archive_codex_inactive_threads.py --check", + "python3 tools/probe_codex_native_retention.py --check", + "I approve CTO-WORK-095 archive-only Codex threads older than 7 days.", + "candidate selection reads only `id`, `rollout_path`, `updated_at`, `archived`, and file size", + "mutation is limited to `threads.archived=1` and `archived_at`", + "Still blocked without separate approval:", + "delete archived session JSONL", + "run SQLite checkpoint or vacuum", + "read raw transcript bodies", +] + +REQUIRED_CODEX_RETENTION_TOOL_PHRASES = { + "tools/report_codex_retention_pressure.py": [ + "metadata_only", + "raw_transcript_bodies_read", + "mutation_performed", + "blocked_without_approval", + ], + "tools/plan_codex_retention_policy.py": [ + "metadata_only", + "raw_transcript_bodies_read", + "raw_thread_text_fields_read", + "mutation_performed", + "approval_boundaries", + "false_effects", + ], + "tools/archive_codex_inactive_threads.py": [ + "approval_token_invalid", + "backup_codex_state", + "raw_transcript_bodies_read", + "session_jsonl_deleted", + "sqlite_checkpoint_or_vacuum", + "false_effects", + ], + "tools/probe_codex_native_retention.py": [ + "metadata_only", + "mutation_performed", + "native_retention_cleanup_command_advertised", + "ephemeral_prevention_advertised", + "false_effects", + ], + "tools/codex_ephemeral_exec.py": [ + "codex", + "exec", + "--ephemeral", + "--print-command", + "dangerously-bypass-approvals-and-sandbox", + "session_persistence_requested", + "false_effects", + ], +} + def workboard_status(text: str, issue_id: str) -> str | None: pattern = rf"- id: {re.escape(issue_id)}\n(?: .+\n)*? status: ([^\n]+)" @@ -2758,6 +2865,45 @@ def main() -> int: if phrase not in text: errors.append(f"missing_provider_decision_record_phrase:{phrase}") + codex_retention_dry_run = ROOT / ".sot/03-PROTOCOLS/CTO-CODEX-RETENTION-DRY-RUN-PACKET.md" + if codex_retention_dry_run.is_file(): + text = codex_retention_dry_run.read_text(encoding="utf-8") + for phrase in REQUIRED_CODEX_RETENTION_DRY_RUN_PHRASES: + checked.append(f"codex_retention_dry_run_phrase:{phrase}") + if phrase not in text: + errors.append(f"missing_codex_retention_dry_run_phrase:{phrase}") + if "source: CTO-WORK-093" not in text: + errors.append("codex_retention_dry_run_missing_source") + + codex_retention_policy = ROOT / ".sot/03-PROTOCOLS/CTO-CODEX-RETENTION-POLICY-PACKET.md" + if codex_retention_policy.is_file(): + text = codex_retention_policy.read_text(encoding="utf-8") + for phrase in REQUIRED_CODEX_RETENTION_POLICY_PHRASES: + checked.append(f"codex_retention_policy_phrase:{phrase}") + if phrase not in text: + errors.append(f"missing_codex_retention_policy_phrase:{phrase}") + if "source: CTO-WORK-094" not in text: + errors.append("codex_retention_policy_missing_source") + + codex_retention_archive = ROOT / ".sot/03-PROTOCOLS/CTO-CODEX-RETENTION-ARCHIVE-EXECUTOR-PACKET.md" + if codex_retention_archive.is_file(): + text = codex_retention_archive.read_text(encoding="utf-8") + for phrase in REQUIRED_CODEX_RETENTION_ARCHIVE_PHRASES: + checked.append(f"codex_retention_archive_phrase:{phrase}") + if phrase not in text: + errors.append(f"missing_codex_retention_archive_phrase:{phrase}") + if "source: CTO-WORK-095" not in text: + errors.append("codex_retention_archive_missing_source") + + for rel, phrases in REQUIRED_CODEX_RETENTION_TOOL_PHRASES.items(): + path = ROOT / rel + if path.is_file(): + text = path.read_text(encoding="utf-8") + for phrase in phrases: + checked.append(f"codex_retention_tool_phrase:{rel}:{phrase}") + if phrase not in text: + errors.append(f"missing_codex_retention_tool_phrase:{rel}:{phrase}") + board = ROOT / "WORKBOARD.yaml" if board.is_file(): text = board.read_text(encoding="utf-8") @@ -2825,6 +2971,10 @@ def main() -> int: checked.append(f"workboard_id:{issue_id}") if issue_id not in text: errors.append(f"missing_workboard_id:{issue_id}") + for issue_id in REQUIRED_CODEX_RETENTION_ISSUE_IDS: + checked.append(f"workboard_id:{issue_id}") + if issue_id not in text: + errors.append(f"missing_workboard_id:{issue_id}") expected_statuses = { "CTO-WORK-001": "validated", "CTO-WORK-002": "validated", @@ -2915,6 +3065,10 @@ def main() -> int: "CTO-WORK-090": "validated", "CTO-WORK-091": "validated", "CTO-WORK-092": "validated", + "CTO-WORK-093": "validated", + "CTO-WORK-094": "validated", + "CTO-WORK-095": "validated", + "CTO-WORK-096": "validated", } for issue_id, expected in expected_statuses.items(): checked.append(f"workboard_status:{issue_id}:{expected}") @@ -3009,6 +3163,12 @@ def main() -> int: errors.append("workboard_missing_core_route_admission_guard_source") if "CTO-CORE-ROUTE-ADMISSION-GUARD-CLOSEOUT.md" not in text: errors.append("workboard_missing_core_route_admission_guard_closeout_source") + if "CTO-CODEX-RETENTION-DRY-RUN-PACKET.md" not in text: + errors.append("workboard_missing_codex_retention_dry_run_source") + if "CTO-CODEX-RETENTION-POLICY-PACKET.md" not in text: + errors.append("workboard_missing_codex_retention_policy_source") + if "CTO-CODEX-RETENTION-ARCHIVE-EXECUTOR-PACKET.md" not in text: + errors.append("workboard_missing_codex_retention_archive_source") payload = { "ok": not errors,