126 lines
7.4 KiB
Python
126 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the persistent continuous-execution checkpoint.
|
|
|
|
This is intentionally read-only. It cannot start a model/agent turn; it gives
|
|
an external scheduler or the next resumed turn a deterministic continuation
|
|
decision without changing task state.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CHECKPOINT = ROOT / "docs/current/continuation-state.json"
|
|
TASK_STATE = ROOT / "docs/current/task-state.json"
|
|
MODULE_BATCH = ROOT / "docs/current/module-batch-manifest.json"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--quiet", action="store_true")
|
|
parser.add_argument("--json", action="store_true", help="emit a machine-readable result")
|
|
args = parser.parse_args()
|
|
checkpoint = json.loads(CHECKPOINT.read_text(encoding="utf-8"))
|
|
task_state = json.loads(TASK_STATE.read_text(encoding="utf-8"))
|
|
active = task_state.get("active_task")
|
|
counts = task_state.get("counts", {})
|
|
errors: list[str] = []
|
|
if checkpoint.get("mode") != "continuous":
|
|
errors.append("continuation mode is not continuous")
|
|
if checkpoint.get("scope") != "full-project":
|
|
errors.append("continuation scope is not full-project")
|
|
allowed_scopes = {"all-task-cards-all-modules-all-required-y-gates"}
|
|
if checkpoint.get("implementationStrategy", {}).get("mode") == "feature-first-deferred-validation":
|
|
allowed_scopes.add("all-business-workflows-and-feature-routes")
|
|
if checkpoint.get("completionScope") not in allowed_scopes:
|
|
errors.append("completion scope is not the full project")
|
|
if checkpoint.get("noPartialCompletion") is not True:
|
|
errors.append("noPartialCompletion is not true")
|
|
if checkpoint.get("autoAdvance") is not True:
|
|
errors.append("autoAdvance is not true")
|
|
if checkpoint.get("mustContinue") is not True:
|
|
errors.append("mustContinue is not true")
|
|
if checkpoint.get("noFinalBeforeTerminate") is not True:
|
|
errors.append("noFinalBeforeTerminate is not true")
|
|
if checkpoint.get("phaseTransitionPolicy") != "immediate":
|
|
errors.append("phase transition policy is not immediate")
|
|
if checkpoint.get("confirmationPolicy") != "never_for_in_scope":
|
|
errors.append("confirmation policy is not never_for_in_scope")
|
|
if checkpoint.get("pausePolicy") != "never_between_phases":
|
|
errors.append("pause policy allows a phase pause")
|
|
if checkpoint.get("waitingAllowed") is not False:
|
|
errors.append("waitingAllowed must be false")
|
|
if checkpoint.get("awaitConfirmation") is not False:
|
|
errors.append("awaitConfirmation must be false")
|
|
if not isinstance(checkpoint.get("automaticActions"), list) or not checkpoint.get("automaticActions"):
|
|
errors.append("automaticActions is empty")
|
|
required_actions = {"source edits", "native builds", "asset sync", "context rotation", "ledger sync", "task claim", "single retry after timeout", "recovery-card creation"}
|
|
if not required_actions.issubset(set(checkpoint.get("automaticActions", []))):
|
|
errors.append("automaticActions is missing required in-scope actions")
|
|
allowed_pauses = {"missing credentials", "irreversible destructive action outside the requested repository scope", "new architecture decision that cannot be inferred from source"}
|
|
if set(checkpoint.get("pauseOnlyFor", [])) != allowed_pauses:
|
|
errors.append("pauseOnlyFor does not match the declared exception boundary")
|
|
if not checkpoint.get("termination"):
|
|
errors.append("termination policy is empty")
|
|
batch_policy = checkpoint.get("batchPolicy")
|
|
if not isinstance(batch_policy, dict):
|
|
errors.append("batchPolicy is missing")
|
|
else:
|
|
if batch_policy.get("mode") != "module+tier+family":
|
|
errors.append("batchPolicy mode is not module+tier+family")
|
|
if int(batch_policy.get("minRecords", 0)) < 2:
|
|
errors.append("batchPolicy minRecords must be at least 2")
|
|
if int(batch_policy.get("defaultRecords", 0)) < int(batch_policy.get("minRecords", 0)):
|
|
errors.append("batchPolicy defaultRecords is below minRecords")
|
|
if int(batch_policy.get("maxRecords", 0)) < int(batch_policy.get("defaultRecords", 0)):
|
|
errors.append("batchPolicy maxRecords is below defaultRecords")
|
|
if batch_policy.get("manifest") != "docs/current/module-batch-manifest.json":
|
|
errors.append("batchPolicy manifest is not the canonical module batch manifest")
|
|
if checkpoint.get("batchManifest") != "docs/current/module-batch-manifest.json":
|
|
errors.append("batchManifest is not the canonical module batch manifest")
|
|
if active and not MODULE_BATCH.exists():
|
|
errors.append("module-batch-manifest.json is missing for active task")
|
|
elif active and MODULE_BATCH.exists():
|
|
try:
|
|
batch = json.loads(MODULE_BATCH.read_text(encoding="utf-8"))
|
|
if batch.get("taskId") not in (None, active):
|
|
errors.append("module batch manifest taskId does not match active task")
|
|
if batch.get("status") == "ready":
|
|
selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {}
|
|
min_records = int(batch_policy.get("minRecords", 2)) if isinstance(batch_policy, dict) else 2
|
|
if int(selected.get("recordCount", 0)) < min_records:
|
|
if batch.get("singletonFallback") is not True:
|
|
errors.append("module batch manifest has fewer records than batchPolicy minRecords without singletonFallback")
|
|
if checkpoint.get("batchId") not in (None, batch.get("batchId")):
|
|
errors.append("continuation batchId does not match module batch manifest")
|
|
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
errors.append("module-batch-manifest.json is invalid")
|
|
if checkpoint.get("activeTask") != active:
|
|
errors.append(f"active task mismatch: checkpoint={checkpoint.get('activeTask')} ledger={active}")
|
|
if counts.get("in_progress", 0) != 1:
|
|
errors.append(f"expected exactly one in_progress task, got {counts.get('in_progress', 0)}")
|
|
phases = {"LOAD_STATE", "VALIDATE", "PREPARE_CONTEXT", "READ_MANIFEST", "IMPLEMENT_BATCH", "RECORD_RESULT", "ROTATE_CONTEXT", "SYNC_STATE", "PREPARE_NEXT", "CONTINUE"}
|
|
if checkpoint.get("phase") not in phases:
|
|
errors.append(f"invalid continuation phase: {checkpoint.get('phase')}")
|
|
if errors:
|
|
if args.json:
|
|
print(json.dumps({"schema": "freecad-web-continuation-check/v1", "ok": False, "activeTask": active, "errors": errors}, ensure_ascii=False))
|
|
return 1
|
|
if not args.quiet:
|
|
for error in errors:
|
|
print(f"CONTINUATION_STATE_INVALID: {error}")
|
|
return 1
|
|
if args.json:
|
|
print(json.dumps({"schema": "freecad-web-continuation-check/v1", "ok": True, "activeTask": active, "phase": checkpoint.get("phase"), "mustContinue": True, "noFinalBeforeTerminate": True, "resumeCommand": checkpoint.get("resumeCommand", "")}, ensure_ascii=False))
|
|
elif not args.quiet:
|
|
print(f"CONTINUATION_STATE_READY: active={active} phase={checkpoint.get('phase')} mustContinue=true noFinalBeforeTerminate=true")
|
|
print(f"RESUME_COMMAND: {checkpoint.get('resumeCommand', '')}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|