From b218417a09f921fc76443729bf87a24ffd697f9a Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:32:38 +0300 Subject: [PATCH 01/40] feat: add CI operational checkpoint schema --- ...ci-operational-checkpoint-v0.1.schema.json | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json diff --git a/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json b/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json new file mode 100644 index 00000000..cfa3bd83 --- /dev/null +++ b/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json @@ -0,0 +1,357 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/safal207/pythiaLabs/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json", + "title": "CI Operational Checkpoint v0.1", + "description": "A bounded continuity checkpoint for safely resuming agentic CI/CD work without carrying tool authority.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "trajectory_id", + "checkpoint_id", + "parent_checkpoint_id", + "sequence", + "created_at", + "authority", + "source", + "workspace_state", + "objective", + "phase", + "constraints", + "rejected_approaches", + "touched_resources", + "verification", + "next_action", + "checkpoint_digest" + ], + "properties": { + "schema_version": { + "const": "pythia.ci_operational_checkpoint.v0.1" + }, + "trajectory_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "checkpoint_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "parent_checkpoint_id": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 200 + }, + "sequence": { + "type": "integer", + "minimum": 0 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "authority": { + "const": "context_only" + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["agent_id", "session_id"], + "properties": { + "agent_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "session_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + } + }, + "workspace_state": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", + "working_directory", + "base_ref", + "head_sha", + "dirty_state_digest" + ], + "properties": { + "repository": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "working_directory": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "base_ref": { + "type": "string", + "minLength": 1, + "maxLength": 300 + }, + "head_sha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "dirty_state_digest": { + "oneOf": [ + {"type": "null"}, + { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + } + ] + } + } + }, + "objective": { + "type": "object", + "additionalProperties": false, + "required": ["goal", "acceptance_criteria"], + "properties": { + "goal": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "acceptance_criteria": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + } + } + }, + "phase": { + "enum": [ + "collect", + "checkpoint", + "normalize", + "triage", + "decide", + "reserve", + "revalidate", + "execute", + "prove", + "learn", + "waiting", + "review" + ] + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "required": ["must", "must_not"], + "properties": { + "must": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + }, + "must_not": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + } + } + }, + "rejected_approaches": { + "type": "array", + "maxItems": 50, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["approach_id", "description", "reason", "status"], + "properties": { + "approach_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "reason": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "status": { + "const": "rejected" + } + } + } + }, + "touched_resources": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["resource_ref", "change_type"], + "properties": { + "resource_ref": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "change_type": { + "enum": ["observed", "created", "modified", "deleted"] + } + } + } + }, + "verification": { + "type": "object", + "additionalProperties": false, + "required": ["required", "completed", "pending"], + "properties": { + "required": { + "type": "array", + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 200 + } + }, + "completed": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/$defs/completedVerification" + } + }, + "pending": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/$defs/pendingVerification" + } + } + } + }, + "next_action": { + "type": "object", + "additionalProperties": false, + "required": [ + "description", + "action_class", + "requires_fresh_authority" + ], + "properties": { + "description": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + }, + "action_class": { + "enum": [ + "read", + "analyze", + "edit", + "test", + "review", + "merge", + "deploy", + "other" + ] + }, + "requires_fresh_authority": { + "type": "boolean" + } + } + }, + "checkpoint_digest": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "canonicalization", "value"], + "properties": { + "algorithm": { + "const": "sha256" + }, + "canonicalization": { + "const": "json-sort-keys-utf8-v1" + }, + "value": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + } + } + } + }, + "$defs": { + "completedVerification": { + "type": "object", + "additionalProperties": false, + "required": ["verification_id", "target", "evidence_refs"], + "properties": { + "verification_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "target": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "evidence_refs": { + "type": "array", + "maxItems": 20, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2000 + } + } + } + }, + "pendingVerification": { + "type": "object", + "additionalProperties": false, + "required": ["verification_id", "target", "status"], + "properties": { + "verification_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "target": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "status": { + "enum": ["pending", "running", "blocked", "failed"] + } + } + } + } +} From 54e01021d5cb123cc47747325e19a47e5d682918 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:33:27 +0300 Subject: [PATCH 02/40] feat: add CI operational checkpoint example --- ...i-operational-checkpoint-v0.1.example.json | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json diff --git a/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json b/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json new file mode 100644 index 00000000..f672f336 --- /dev/null +++ b/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json @@ -0,0 +1,85 @@ +{ + "schema_version": "pythia.ci_operational_checkpoint.v0.1", + "trajectory_id": "trajectory:pythiaLabs:issue-218", + "checkpoint_id": "checkpoint:issue-218:initial", + "parent_checkpoint_id": null, + "sequence": 0, + "created_at": "2026-07-02T09:30:00Z", + "authority": "context_only", + "source": { + "agent_id": "agent:pythia-ci-continuity", + "session_id": "session:issue-218" + }, + "workspace_state": { + "repository": "safal207/pythiaLabs", + "working_directory": "/workspace/pythiaLabs", + "base_ref": "main", + "head_sha": "bcebae58976189994dec5d8dfc3e6547c03d84a3", + "dirty_state_digest": null + }, + "objective": { + "goal": "Define a machine-readable CI operational checkpoint and deterministic resume contract.", + "acceptance_criteria": [ + "Checkpoint context cannot grant merge or deploy authority.", + "Workspace drift is detected before consequential work resumes.", + "Completed verification is supported by durable evidence references." + ] + }, + "phase": "checkpoint", + "constraints": { + "must": [ + "Fail closed on missing lineage or unverified completion.", + "Re-evaluate authority before merge or deploy." + ], + "must_not": [ + "Treat remembered CI success as current evidence.", + "Carry an earlier Action Envelope ALLOW decision across a changed target." + ] + }, + "rejected_approaches": [ + { + "approach_id": "approach:narrative-only-summary", + "description": "Resume from a free-form conversation summary.", + "reason": "Narrative memory cannot prove workspace state or verification completion.", + "status": "rejected" + } + ], + "touched_resources": [ + { + "resource_ref": "github://safal207/pythiaLabs/issues/218", + "change_type": "observed" + } + ], + "verification": { + "required": [ + "verification:schema", + "verification:conformance" + ], + "completed": [ + { + "verification_id": "verification:schema", + "target": "CI operational checkpoint JSON Schema", + "evidence_refs": [ + "git://safal207/pythiaLabs/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json" + ] + } + ], + "pending": [ + { + "verification_id": "verification:conformance", + "target": "CI operational checkpoint conformance suite", + "status": "pending" + } + ] + }, + "next_action": { + "description": "Implement and run the deterministic checkpoint conformance suite.", + "action_class": "test", + "requires_fresh_authority": false + }, + "checkpoint_digest": { + "algorithm": "sha256", + "canonicalization": "json-sort-keys-utf8-v1", + "value": "e67301839ba684347c432d48b3f8557d8aba9283c370e0256dc8553c50556ae1" + } +} From 7b3cbf3c12d4537a6e50804ae56acadcbb297071 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:34:36 +0300 Subject: [PATCH 03/40] feat: add deterministic checkpoint evaluator --- .../ci_operational_checkpoint_reference.py | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py new file mode 100644 index 00000000..54060e04 --- /dev/null +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +from typing import Any, Iterable, Mapping + +from jsonschema import Draft202012Validator, FormatChecker + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +SCHEMA_PATH = ROOT / "schema" / "ci-operational-checkpoint-v0.1.schema.json" + +CONTINUE = "CONTINUE" +REVALIDATE_WORKSPACE = "REVALIDATE_WORKSPACE" +RESTART_REQUIRED = "RESTART_REQUIRED" +IDEMPOTENT_REPLAY = "IDEMPOTENT_REPLAY" +REJECT_LINEAGE_MISMATCH = "REJECT_LINEAGE_MISMATCH" +REJECT_UNVERIFIED_COMPLETION = "REJECT_UNVERIFIED_COMPLETION" +REJECT_INVALID_AUTHORITY = "REJECT_INVALID_AUTHORITY" + +MEMORY_ONLY_PREFIXES = ("memory://", "agent-memory://", "summary://") + + +def load_schema() -> dict[str, Any]: + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + if not isinstance(schema, dict): + raise ValueError("checkpoint schema root must be an object") + Draft202012Validator.check_schema(schema) + return schema + + +def _canonical_bytes(checkpoint: Mapping[str, Any]) -> bytes: + value = copy.deepcopy(dict(checkpoint)) + value.pop("checkpoint_digest", None) + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8") + + +def computed_digest(checkpoint: Mapping[str, Any]) -> str: + return hashlib.sha256(_canonical_bytes(checkpoint)).hexdigest() + + +def with_computed_digest(checkpoint: Mapping[str, Any]) -> dict[str, Any]: + result = copy.deepcopy(dict(checkpoint)) + result.setdefault( + "checkpoint_digest", + { + "algorithm": "sha256", + "canonicalization": "json-sort-keys-utf8-v1", + "value": "0" * 64, + }, + ) + result["checkpoint_digest"]["value"] = computed_digest(result) + return result + + +def _schema_errors(checkpoint: Mapping[str, Any]) -> list[Any]: + validator = Draft202012Validator( + load_schema(), + format_checker=FormatChecker(), + ) + return sorted( + validator.iter_errors(dict(checkpoint)), + key=lambda error: [str(part) for part in error.absolute_path], + ) + + +def _result(outcome: str, reason_code: str, detail: str) -> dict[str, str]: + return { + "outcome": outcome, + "reason_code": reason_code, + "detail": detail, + } + + +def _verification_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: + return {str(row["verification_id"]): row for row in rows} + + +def evaluate_resume( + checkpoint: Mapping[str, Any], + *, + current_workspace: Mapping[str, Any], + previous_checkpoint: Mapping[str, Any] | None = None, + seen_checkpoint_ids: Iterable[str] = (), + known_parent_ids: Iterable[str] = (), +) -> dict[str, str]: + errors = _schema_errors(checkpoint) + if errors: + first = errors[0] + path = "/".join(str(part) for part in first.absolute_path) or "" + if path == "authority": + return _result( + REJECT_INVALID_AUTHORITY, + "AUTHORITY_NOT_CONTEXT_ONLY", + first.message, + ) + return _result( + RESTART_REQUIRED, + "SCHEMA_INVALID", + f"{path}: {first.message}", + ) + + expected_digest = checkpoint["checkpoint_digest"]["value"] + actual_digest = computed_digest(checkpoint) + if expected_digest != actual_digest: + return _result( + RESTART_REQUIRED, + "DIGEST_MISMATCH", + f"expected {expected_digest}, computed {actual_digest}", + ) + + checkpoint_id = checkpoint["checkpoint_id"] + if checkpoint_id in set(seen_checkpoint_ids): + return _result( + IDEMPOTENT_REPLAY, + "CHECKPOINT_ALREADY_CONSUMED", + f"checkpoint {checkpoint_id} was already consumed", + ) + + sequence = checkpoint["sequence"] + parent_checkpoint_id = checkpoint["parent_checkpoint_id"] + if sequence == 0 and parent_checkpoint_id is not None: + return _result( + REJECT_LINEAGE_MISMATCH, + "ROOT_HAS_PARENT", + "sequence 0 checkpoint must not declare a parent", + ) + if sequence > 0 and parent_checkpoint_id is None: + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_REQUIRED", + "non-root checkpoint must declare parent_checkpoint_id", + ) + + if previous_checkpoint is None: + if sequence > 0 and parent_checkpoint_id not in set(known_parent_ids): + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_NOT_FOUND", + f"parent checkpoint {parent_checkpoint_id} is not known", + ) + else: + if checkpoint["trajectory_id"] != previous_checkpoint.get("trajectory_id"): + return _result( + REJECT_LINEAGE_MISMATCH, + "TRAJECTORY_CHANGED", + "checkpoint trajectory differs from previous checkpoint", + ) + if parent_checkpoint_id != previous_checkpoint.get("checkpoint_id"): + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_MISMATCH", + "parent_checkpoint_id does not reference the previous checkpoint", + ) + if sequence != previous_checkpoint.get("sequence", -1) + 1: + return _result( + REJECT_LINEAGE_MISMATCH, + "SEQUENCE_MISMATCH", + "checkpoint sequence is not previous sequence + 1", + ) + + previous_rejected = { + row["approach_id"] + for row in previous_checkpoint.get("rejected_approaches", []) + } + current_rejected = { + row["approach_id"] for row in checkpoint["rejected_approaches"] + } + missing_rejections = sorted(previous_rejected - current_rejected) + if missing_rejections: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_LOST", + "rejected approaches disappeared: " + ", ".join(missing_rejections), + ) + + previous_completed = _verification_index( + previous_checkpoint.get("verification", {}).get("completed", []) + ) + current_completed = _verification_index( + checkpoint["verification"]["completed"] + ) + lost_completed = sorted(set(previous_completed) - set(current_completed)) + if lost_completed: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETED_VERIFICATION_LOST", + "completed verification disappeared: " + ", ".join(lost_completed), + ) + + required_ids = set(checkpoint["verification"]["required"]) + completed_rows = checkpoint["verification"]["completed"] + pending_rows = checkpoint["verification"]["pending"] + completed_ids = [row["verification_id"] for row in completed_rows] + pending_ids = [row["verification_id"] for row in pending_rows] + + all_ids = completed_ids + pending_ids + if len(all_ids) != len(set(all_ids)): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "VERIFICATION_ID_DUPLICATED", + "verification IDs must be unique across completed and pending lists", + ) + + represented_ids = set(all_ids) + if represented_ids != required_ids: + missing = sorted(required_ids - represented_ids) + unexpected = sorted(represented_ids - required_ids) + return _result( + REJECT_UNVERIFIED_COMPLETION, + "VERIFICATION_SET_MISMATCH", + f"missing={missing}; unexpected={unexpected}", + ) + + for row in completed_rows: + refs = row["evidence_refs"] + if not refs: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETION_EVIDENCE_MISSING", + f"{row['verification_id']} has no evidence references", + ) + if any(str(ref).startswith(MEMORY_ONLY_PREFIXES) for ref in refs): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "MEMORY_IS_NOT_VERIFICATION", + f"{row['verification_id']} relies on memory-only evidence", + ) + + next_action = checkpoint["next_action"] + if ( + next_action["action_class"] in {"merge", "deploy"} + and not next_action["requires_fresh_authority"] + ): + return _result( + REJECT_INVALID_AUTHORITY, + "FRESH_AUTHORITY_REQUIRED", + "merge and deploy actions require a fresh action authorization", + ) + + expected_workspace = checkpoint["workspace_state"] + for field in ("repository", "working_directory"): + if current_workspace.get(field) != expected_workspace[field]: + return _result( + RESTART_REQUIRED, + "WORKSPACE_IDENTITY_MISMATCH", + f"{field} differs from the checkpoint", + ) + + changed_fields = [ + field + for field in ("base_ref", "head_sha", "dirty_state_digest") + if current_workspace.get(field) != expected_workspace[field] + ] + if changed_fields: + return _result( + REVALIDATE_WORKSPACE, + "WORKSPACE_STATE_CHANGED", + "changed workspace fields: " + ", ".join(changed_fields), + ) + + return _result( + CONTINUE, + "CONTINUE_OK", + "checkpoint is valid, workspace matches, and authority remains context-only", + ) From 4d00568cae3506da987870a1c7463dd94b58016d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:35:30 +0300 Subject: [PATCH 04/40] test: add CI checkpoint conformance cases --- .../test_ci_operational_checkpoint.py | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 standards/agent-continuity/conformance/test_ci_operational_checkpoint.py diff --git a/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py b/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py new file mode 100644 index 00000000..d29cf7bf --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py @@ -0,0 +1,235 @@ +from __future__ import annotations + +import copy +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from ci_operational_checkpoint_reference import ( # noqa: E402 + CONTINUE, + IDEMPOTENT_REPLAY, + REJECT_INVALID_AUTHORITY, + REJECT_LINEAGE_MISMATCH, + REJECT_UNVERIFIED_COMPLETION, + RESTART_REQUIRED, + REVALIDATE_WORKSPACE, + evaluate_resume, + with_computed_digest, +) + +EXAMPLE_PATH = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE_PATH.read_text(encoding="utf-8")) + + +def current_workspace(checkpoint): + return copy.deepcopy(checkpoint["workspace_state"]) + + +def next_checkpoint(previous): + checkpoint = copy.deepcopy(previous) + checkpoint["checkpoint_id"] = "checkpoint:issue-218:next" + checkpoint["parent_checkpoint_id"] = previous["checkpoint_id"] + checkpoint["sequence"] = previous["sequence"] + 1 + checkpoint["created_at"] = "2026-07-02T09:31:00Z" + return with_computed_digest(checkpoint) + + +class CiOperationalCheckpointTest(unittest.TestCase): + def assert_outcome(self, checkpoint, expected, **kwargs): + workspace = kwargs.pop("current_workspace", current_workspace(checkpoint)) + result = evaluate_resume( + checkpoint, + current_workspace=workspace, + **kwargs, + ) + self.assertEqual(result["outcome"], expected, result) + return result + + def test_valid_root_checkpoint_continues(self): + self.assert_outcome(load_example(), CONTINUE) + + def test_changed_head_requires_workspace_revalidation(self): + checkpoint = load_example() + workspace = current_workspace(checkpoint) + workspace["head_sha"] = "f" * 40 + + result = self.assert_outcome( + checkpoint, + REVALIDATE_WORKSPACE, + current_workspace=workspace, + ) + + self.assertEqual(result["reason_code"], "WORKSPACE_STATE_CHANGED") + + def test_changed_base_requires_workspace_revalidation(self): + checkpoint = load_example() + workspace = current_workspace(checkpoint) + workspace["base_ref"] = "release" + + self.assert_outcome( + checkpoint, + REVALIDATE_WORKSPACE, + current_workspace=workspace, + ) + + def test_changed_dirty_state_requires_workspace_revalidation(self): + checkpoint = load_example() + workspace = current_workspace(checkpoint) + workspace["dirty_state_digest"] = "sha256:" + "1" * 64 + + self.assert_outcome( + checkpoint, + REVALIDATE_WORKSPACE, + current_workspace=workspace, + ) + + def test_changed_repository_requires_restart(self): + checkpoint = load_example() + workspace = current_workspace(checkpoint) + workspace["repository"] = "safal207/other" + + self.assert_outcome( + checkpoint, + RESTART_REQUIRED, + current_workspace=workspace, + ) + + def test_duplicate_checkpoint_is_idempotent_replay(self): + checkpoint = load_example() + + self.assert_outcome( + checkpoint, + IDEMPOTENT_REPLAY, + seen_checkpoint_ids={checkpoint["checkpoint_id"]}, + ) + + def test_non_root_checkpoint_requires_parent(self): + checkpoint = load_example() + checkpoint["sequence"] = 1 + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome(checkpoint, REJECT_LINEAGE_MISMATCH) + + def test_parent_must_match_previous_checkpoint(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["parent_checkpoint_id"] = "checkpoint:wrong" + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome( + checkpoint, + REJECT_LINEAGE_MISMATCH, + previous_checkpoint=previous, + ) + + def test_rejected_approach_cannot_disappear(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["rejected_approaches"] = [] + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + REJECT_LINEAGE_MISMATCH, + previous_checkpoint=previous, + ) + + self.assertEqual(result["reason_code"], "REJECTED_APPROACH_LOST") + + def test_completed_verification_requires_evidence(self): + checkpoint = load_example() + checkpoint["verification"]["completed"][0]["evidence_refs"] = [] + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome(checkpoint, REJECT_UNVERIFIED_COMPLETION) + + def test_memory_cannot_become_verification(self): + checkpoint = load_example() + checkpoint["verification"]["completed"][0]["evidence_refs"] = [ + "memory://the-agent-remembers-ci-was-green" + ] + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + REJECT_UNVERIFIED_COMPLETION, + ) + + self.assertEqual(result["reason_code"], "MEMORY_IS_NOT_VERIFICATION") + + def test_authority_must_remain_context_only(self): + checkpoint = load_example() + checkpoint["authority"] = "merge_allowed" + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome(checkpoint, REJECT_INVALID_AUTHORITY) + + def test_merge_next_action_requires_fresh_authority(self): + checkpoint = load_example() + checkpoint["next_action"] = { + "description": "Merge the pull request.", + "action_class": "merge", + "requires_fresh_authority": False, + } + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome(checkpoint, REJECT_INVALID_AUTHORITY) + self.assertEqual(result["reason_code"], "FRESH_AUTHORITY_REQUIRED") + + def test_merge_next_action_may_continue_only_to_fresh_authorization(self): + checkpoint = load_example() + checkpoint["next_action"] = { + "description": "Construct and evaluate a fresh Action Envelope.", + "action_class": "merge", + "requires_fresh_authority": True, + } + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome(checkpoint, CONTINUE) + + def test_digest_tampering_requires_restart(self): + checkpoint = load_example() + checkpoint["objective"]["goal"] = "Tampered goal" + + result = self.assert_outcome(checkpoint, RESTART_REQUIRED) + self.assertEqual(result["reason_code"], "DIGEST_MISMATCH") + + def test_previous_completed_verification_cannot_disappear(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["verification"]["completed"] = [] + checkpoint["verification"]["pending"].append( + { + "verification_id": "verification:schema", + "target": "CI operational checkpoint JSON Schema", + "status": "pending", + } + ) + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome( + checkpoint, + REJECT_UNVERIFIED_COMPLETION, + previous_checkpoint=previous, + ) + + def test_unknown_field_requires_restart(self): + checkpoint = load_example() + checkpoint["surprise"] = True + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome(checkpoint, RESTART_REQUIRED) + self.assertEqual(result["reason_code"], "SCHEMA_INVALID") + + +if __name__ == "__main__": + unittest.main() From b08299169cebf6cc3c6af54653eebc4069aa2bb2 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:36:12 +0300 Subject: [PATCH 05/40] docs: add CI checkpoint outcome registry --- .../agent-continuity/CI-CHECKPOINT-CODES.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 standards/agent-continuity/CI-CHECKPOINT-CODES.md diff --git a/standards/agent-continuity/CI-CHECKPOINT-CODES.md b/standards/agent-continuity/CI-CHECKPOINT-CODES.md new file mode 100644 index 00000000..5c043d5a --- /dev/null +++ b/standards/agent-continuity/CI-CHECKPOINT-CODES.md @@ -0,0 +1,29 @@ +# CI Operational Checkpoint v0.1 Outcome Registry + +Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. + +## Outcomes + +| Outcome | Meaning | +|---|---| +| `CONTINUE` | The checkpoint is valid, lineage is coherent, the workspace matches, and the next step may resume as context-only work. | +| `REVALIDATE_WORKSPACE` | Repository identity matches, but base, head, or dirty-state evidence changed and must be re-observed. | +| `RESTART_REQUIRED` | The checkpoint cannot safely resume in the current workspace or failed schema/digest validation. | +| `IDEMPOTENT_REPLAY` | The same checkpoint was already consumed; no duplicate work should be created. | +| `REJECT_LINEAGE_MISMATCH` | Parent, sequence, trajectory, or rejected-approach continuity is inconsistent. | +| `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with the required verification set. | +| `REJECT_INVALID_AUTHORITY` | Continuity material attempted to carry action authority or bypass fresh authorization. | + +## Reason codes + +| Outcome | Reason code | +|---|---| +| `CONTINUE` | `CONTINUE_OK` | +| `REVALIDATE_WORKSPACE` | `WORKSPACE_STATE_CHANGED` | +| `RESTART_REQUIRED` | `SCHEMA_INVALID`, `DIGEST_MISMATCH`, `WORKSPACE_IDENTITY_MISMATCH` | +| `IDEMPOTENT_REPLAY` | `CHECKPOINT_ALREADY_CONSUMED` | +| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `REJECTED_APPROACH_LOST` | +| `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST` | +| `REJECT_INVALID_AUTHORITY` | `AUTHORITY_NOT_CONTEXT_ONLY`, `FRESH_AUTHORITY_REQUIRED` | + +Within version `0.1`, existing reason codes MUST NOT change meaning. From 37d9a8ba4b97be105bb261a46bb76373359e0f69 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:36:58 +0300 Subject: [PATCH 06/40] docs: define CI operational checkpoint contract --- .../CI-OPERATIONAL-CHECKPOINT-V0.1.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md diff --git a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md new file mode 100644 index 00000000..99149015 --- /dev/null +++ b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md @@ -0,0 +1,112 @@ +# CI Operational Checkpoint v0.1 + +- **Status:** Draft +- **Profile:** `pythia.ci_operational_checkpoint.v0.1` +- **Parent:** [RFC-001 — Verifiable Continuation Envelope](RFC-001-VERIFIABLE-CONTINUATION-ENVELOPE.md) +- **Scope:** Agentic CI/CD phase transitions, review waits, retries, restart, and handoff + +## 1. Purpose + +This profile defines a bounded checkpoint for resuming CI/CD work without +reconstructing state from a narrative summary and without inheriting authority +to merge, deploy, send, publish, or invoke another consequential tool. + +```text +checkpoint context + -> validate schema and digest + -> validate lineage + -> preserve rejected approaches + -> preserve required verification + -> compare current workspace + -> CONTINUE | REVALIDATE | RESTART | REJECT +``` + +## 2. Core boundary + +> Context may continue. Authority must be evaluated again. + +`authority` is fixed to `context_only`. A checkpoint can carry an exact next +action, including the intent to merge or deploy, but those action classes MUST +set `requires_fresh_authority=true`. The checkpoint never replaces an Action +Envelope, current checks, current reviews, or an executor guard. + +## 3. Required content + +A conforming checkpoint records: + +- trajectory, checkpoint, parent, and sequence identity; +- source agent and session; +- exact repository, working directory, base ref, head SHA, and optional + dirty-state digest; +- goal and acceptance criteria; +- current delivery phase; +- active `must` and `must_not` constraints; +- rejected approaches with reasons; +- touched resources; +- required, completed, and pending verification; +- one exact next action; +- canonical SHA-256 digest. + +The strict schema is +[`schema/ci-operational-checkpoint-v0.1.schema.json`](schema/ci-operational-checkpoint-v0.1.schema.json). + +## 4. Verification boundary + +A verification item may appear in `completed` only when it carries at least one +durable evidence reference. Narrative memory and agent summaries are not +verification evidence. The required verification set must equal the union of +completed and pending IDs. + +A later checkpoint must not: + +- drop a previously completed verification; +- drop a rejected approach; +- silently mark pending work completed without evidence; +- change trajectory or skip sequence; +- point at the wrong parent. + +## 5. Workspace comparison + +Repository or working-directory mismatch returns `RESTART_REQUIRED`. + +Changes to base ref, head SHA, or dirty-state digest return +`REVALIDATE_WORKSPACE`. The resumed agent must inspect the changed workspace and +create a new checkpoint before consequential work continues. + +## 6. Replay + +A consumed checkpoint ID returns `IDEMPOTENT_REPLAY`. The caller should not +repeat completed work or create duplicate side effects. + +## 7. Relationship to Action Envelope V1 + +The checkpoint answers: + +```text +What context and verification state should the next agent restore? +``` + +Action Envelope V1 answers: + +```text +May this exact consequential action execute now? +``` + +The two contracts are complementary and deliberately non-substitutable. + +## 8. Reference implementation + +- example checkpoint: + [`examples/ci-operational-checkpoint-v0.1.example.json`](examples/ci-operational-checkpoint-v0.1.example.json); +- deterministic evaluator: + [`conformance/ci_operational_checkpoint_reference.py`](conformance/ci_operational_checkpoint_reference.py); +- executable tests: + [`conformance/test_ci_operational_checkpoint.py`](conformance/test_ci_operational_checkpoint.py); +- stable outcomes: + [`CI-CHECKPOINT-CODES.md`](CI-CHECKPOINT-CODES.md). + +## 9. Non-claims + +This profile is not durable storage, a distributed lease, a workflow engine, an +identity system, proof that an action occurred, or permission to perform a tool +call. Durable coordination is tracked separately in issue #223. From f1dc1e92b941354ec48b831c82a2ab4c8fe6a812 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:37:36 +0300 Subject: [PATCH 07/40] ci: add agent continuity conformance workflow --- .../agent-continuity-conformance.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/agent-continuity-conformance.yml diff --git a/.github/workflows/agent-continuity-conformance.yml b/.github/workflows/agent-continuity-conformance.yml new file mode 100644 index 00000000..dbe7dc6e --- /dev/null +++ b/.github/workflows/agent-continuity-conformance.yml @@ -0,0 +1,41 @@ +name: Agent Continuity conformance + +on: + pull_request: + paths: + - "standards/agent-continuity/**" + - ".github/workflows/agent-continuity-conformance.yml" + push: + paths: + - "standards/agent-continuity/**" + - ".github/workflows/agent-continuity-conformance.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + conformance: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install conformance dependency + run: | + python -m pip install --disable-pip-version-check \ + -r standards/agent-continuity/conformance/requirements.txt + + - name: Run Agent Continuity conformance suite + run: | + python -m unittest discover \ + -s standards/agent-continuity/conformance \ + -p 'test_*.py' \ + -v From 166498adc3e6a0fe2f9b67a316c832077a38b6e2 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:38:24 +0300 Subject: [PATCH 08/40] docs: add CI checkpoint profile to continuity README --- standards/agent-continuity/README.md | 45 +++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/standards/agent-continuity/README.md b/standards/agent-continuity/README.md index 96c5101b..ca250a67 100644 --- a/standards/agent-continuity/README.md +++ b/standards/agent-continuity/README.md @@ -1,6 +1,6 @@ # Agent Continuity & Authority -Vendor-neutral specifications and executable conformance checks for preserving operational continuity across context compaction, session restart, and cross-session handoff. +Vendor-neutral specifications and executable conformance checks for preserving operational continuity across context compaction, session restart, cross-session handoff, and CI/CD phase transitions. ## RFC v0.1 @@ -10,6 +10,30 @@ defines a bounded, structured envelope that carries the active operational tail A separate restore-results document records which required reads and evidence checks were actually completed. The envelope declares the gate; restore results satisfy it. +## CI Operational Checkpoint v0.1 + +[`CI-OPERATIONAL-CHECKPOINT-V0.1.md`](./CI-OPERATIONAL-CHECKPOINT-V0.1.md) + +specializes the continuity boundary for agentic CI/CD work. It records exact +workspace identity, goal, constraints, rejected approaches, verification state, +and the next action while fixing authority to `context_only`. + +The profile deterministically returns: + +```text +CONTINUE +REVALIDATE_WORKSPACE +RESTART_REQUIRED +IDEMPOTENT_REPLAY +REJECT_LINEAGE_MISMATCH +REJECT_UNVERIFIED_COMPLETION +REJECT_INVALID_AUTHORITY +``` + +A checkpoint may preserve the intent to merge or deploy, but it cannot authorize +that action. A fresh Action Envelope and current exact-target evidence remain +mandatory. + ## Problem Coding agents may lose task continuity after compaction or handoff. They can repeat completed work, violate recent constraints, forget rejected approaches, or confidently reconstruct an execution history that is not supported by durable evidence. @@ -21,10 +45,12 @@ Coding agents may lose task continuity after compaction or handoff. They can rep ## Package - RFC specification; -- JSON Schemas for the envelope and restore results; -- example envelope and restore results; -- reference validator; -- executable conformance tests. +- CI operational checkpoint profile; +- JSON Schemas for envelopes, restore results, and CI checkpoints; +- complete examples; +- deterministic reference validators; +- executable conformance tests; +- stable CI checkpoint outcome registry. ## Quick validation @@ -39,16 +65,19 @@ python -m unittest discover \ ## What the suite verifies - published JSON Schema enforcement; -- canonical envelope digest integrity; +- canonical envelope and checkpoint digest integrity; - trusted-source authority boundaries; - independent digest/receipt evidence checks; - required-read completion; - fail-closed restore behavior; -- unresolved task verification remaining unresolved. +- unresolved task verification remaining unresolved; +- CI workspace drift detection; +- lineage, replay, rejected-approach, and verification continuity; +- fresh authorization requirements for merge and deploy intent. ## Intended integrations -The specification is implementation-neutral. Codex, Claude Code, IDE agents, CLI agents, and multi-agent runtimes may store or transport the envelope differently while preserving the same observable guarantees. +The specifications are implementation-neutral. Codex, Claude Code, IDE agents, CLI agents, CI bots, and multi-agent runtimes may store or transport the envelopes differently while preserving the same observable guarantees. ## Status From 5c1677d6410b046db38b9d901fac0d6cde100499 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:21:36 +0300 Subject: [PATCH 09/40] fix: preserve checkpoint proof and workspace evidence --- .../ci_operational_checkpoint_reference.py | 75 +++++++++++++++---- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index 54060e04..fc42d30f 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -84,6 +84,10 @@ def _verification_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[ return {str(row["verification_id"]): row for row in rows} +def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: + return {str(row["approach_id"]): row for row in rows} + + def evaluate_resume( checkpoint: Mapping[str, Any], *, @@ -167,20 +171,28 @@ def evaluate_resume( "checkpoint sequence is not previous sequence + 1", ) - previous_rejected = { - row["approach_id"] - for row in previous_checkpoint.get("rejected_approaches", []) - } - current_rejected = { - row["approach_id"] for row in checkpoint["rejected_approaches"] - } - missing_rejections = sorted(previous_rejected - current_rejected) + previous_rejected = _rejected_index( + previous_checkpoint.get("rejected_approaches", []) + ) + current_rejected = _rejected_index(checkpoint["rejected_approaches"]) + missing_rejections = sorted(set(previous_rejected) - set(current_rejected)) if missing_rejections: return _result( REJECT_LINEAGE_MISMATCH, "REJECTED_APPROACH_LOST", "rejected approaches disappeared: " + ", ".join(missing_rejections), ) + changed_rejections = sorted( + approach_id + for approach_id, previous_row in previous_rejected.items() + if current_rejected[approach_id] != previous_row + ) + if changed_rejections: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_CHANGED", + "rejected approaches changed: " + ", ".join(changed_rejections), + ) previous_completed = _verification_index( previous_checkpoint.get("verification", {}).get("completed", []) @@ -195,6 +207,22 @@ def evaluate_resume( "COMPLETED_VERIFICATION_LOST", "completed verification disappeared: " + ", ".join(lost_completed), ) + for verification_id, previous_row in previous_completed.items(): + current_row = current_completed[verification_id] + previous_refs = set(previous_row["evidence_refs"]) + current_refs = set(current_row["evidence_refs"]) + if ( + current_row["target"] != previous_row["target"] + or not previous_refs.issubset(current_refs) + ): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETED_VERIFICATION_CHANGED", + ( + "completed verification target changed or prior evidence " + f"was removed: {verification_id}" + ), + ) required_ids = set(checkpoint["verification"]["required"]) completed_rows = checkpoint["verification"]["completed"] @@ -228,7 +256,7 @@ def evaluate_resume( "COMPLETION_EVIDENCE_MISSING", f"{row['verification_id']} has no evidence references", ) - if any(str(ref).startswith(MEMORY_ONLY_PREFIXES) for ref in refs): + if any(str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) for ref in refs): return _result( REJECT_UNVERIFIED_COMPLETION, "MEMORY_IS_NOT_VERIFICATION", @@ -247,18 +275,39 @@ def evaluate_resume( ) expected_workspace = checkpoint["workspace_state"] - for field in ("repository", "working_directory"): - if current_workspace.get(field) != expected_workspace[field]: + identity_fields = ("repository", "working_directory") + for field in identity_fields: + if field not in current_workspace: + return _result( + RESTART_REQUIRED, + "CURRENT_WORKSPACE_FIELD_MISSING", + f"current workspace did not report {field}", + ) + if current_workspace[field] != expected_workspace[field]: return _result( RESTART_REQUIRED, "WORKSPACE_IDENTITY_MISMATCH", f"{field} differs from the checkpoint", ) + state_fields = ["base_ref", "head_sha"] + if "dirty_state_digest" in expected_workspace: + state_fields.append("dirty_state_digest") + + missing_state_fields = [ + field for field in state_fields if field not in current_workspace + ] + if missing_state_fields: + return _result( + REVALIDATE_WORKSPACE, + "CURRENT_WORKSPACE_FIELD_MISSING", + "current workspace did not report: " + ", ".join(missing_state_fields), + ) + changed_fields = [ field - for field in ("base_ref", "head_sha", "dirty_state_digest") - if current_workspace.get(field) != expected_workspace[field] + for field in state_fields + if current_workspace[field] != expected_workspace[field] ] if changed_fields: return _result( From 2f975ba4cf44307ce286f5c3c49c7f817dee7fed Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:22:43 +0300 Subject: [PATCH 10/40] test: cover checkpoint lineage and workspace evidence --- .../test_ci_operational_checkpoint.py | 120 +++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py b/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py index d29cf7bf..b3523c96 100644 --- a/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py +++ b/standards/agent-continuity/conformance/test_ci_operational_checkpoint.py @@ -45,7 +45,10 @@ def next_checkpoint(previous): class CiOperationalCheckpointTest(unittest.TestCase): def assert_outcome(self, checkpoint, expected, **kwargs): - workspace = kwargs.pop("current_workspace", current_workspace(checkpoint)) + if "current_workspace" in kwargs: + workspace = kwargs.pop("current_workspace") + else: + workspace = current_workspace(checkpoint) result = evaluate_resume( checkpoint, current_workspace=workspace, @@ -92,6 +95,31 @@ def test_changed_dirty_state_requires_workspace_revalidation(self): current_workspace=workspace, ) + def test_missing_current_dirty_state_requires_revalidation(self): + checkpoint = load_example() + workspace = current_workspace(checkpoint) + workspace.pop("dirty_state_digest") + + result = self.assert_outcome( + checkpoint, + REVALIDATE_WORKSPACE, + current_workspace=workspace, + ) + + self.assertEqual(result["reason_code"], "CURRENT_WORKSPACE_FIELD_MISSING") + + def test_optional_dirty_state_may_be_omitted_from_checkpoint(self): + checkpoint = load_example() + checkpoint["workspace_state"].pop("dirty_state_digest") + checkpoint = with_computed_digest(checkpoint) + workspace = current_workspace(checkpoint) + + self.assert_outcome( + checkpoint, + CONTINUE, + current_workspace=workspace, + ) + def test_changed_repository_requires_restart(self): checkpoint = load_example() workspace = current_workspace(checkpoint) @@ -145,12 +173,27 @@ def test_rejected_approach_cannot_disappear(self): self.assertEqual(result["reason_code"], "REJECTED_APPROACH_LOST") - def test_completed_verification_requires_evidence(self): + def test_rejected_approach_reason_cannot_be_rewritten(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["rejected_approaches"][0]["reason"] = "Rewritten rationale" + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + REJECT_LINEAGE_MISMATCH, + previous_checkpoint=previous, + ) + + self.assertEqual(result["reason_code"], "REJECTED_APPROACH_CHANGED") + + def test_completed_verification_requires_evidence_at_schema_boundary(self): checkpoint = load_example() checkpoint["verification"]["completed"][0]["evidence_refs"] = [] checkpoint = with_computed_digest(checkpoint) - self.assert_outcome(checkpoint, REJECT_UNVERIFIED_COMPLETION) + result = self.assert_outcome(checkpoint, RESTART_REQUIRED) + self.assertEqual(result["reason_code"], "SCHEMA_INVALID") def test_memory_cannot_become_verification(self): checkpoint = load_example() @@ -166,6 +209,20 @@ def test_memory_cannot_become_verification(self): self.assertEqual(result["reason_code"], "MEMORY_IS_NOT_VERIFICATION") + def test_memory_scheme_is_rejected_case_insensitively(self): + checkpoint = load_example() + checkpoint["verification"]["completed"][0]["evidence_refs"] = [ + "Agent-Memory://the-agent-remembers-ci-was-green" + ] + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + REJECT_UNVERIFIED_COMPLETION, + ) + + self.assertEqual(result["reason_code"], "MEMORY_IS_NOT_VERIFICATION") + def test_authority_must_remain_context_only(self): checkpoint = load_example() checkpoint["authority"] = "merge_allowed" @@ -222,6 +279,50 @@ def test_previous_completed_verification_cannot_disappear(self): previous_checkpoint=previous, ) + def test_completed_verification_target_cannot_change(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["verification"]["completed"][0]["target"] = "Different target" + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + REJECT_UNVERIFIED_COMPLETION, + previous_checkpoint=previous, + ) + + self.assertEqual(result["reason_code"], "COMPLETED_VERIFICATION_CHANGED") + + def test_completed_verification_evidence_cannot_be_removed(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["verification"]["completed"][0]["evidence_refs"] = [ + "git://safal207/pythiaLabs/replacement-proof" + ] + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + REJECT_UNVERIFIED_COMPLETION, + previous_checkpoint=previous, + ) + + self.assertEqual(result["reason_code"], "COMPLETED_VERIFICATION_CHANGED") + + def test_completed_verification_may_append_evidence(self): + previous = load_example() + checkpoint = next_checkpoint(previous) + checkpoint["verification"]["completed"][0]["evidence_refs"].append( + "github-actions://safal207/pythiaLabs/runs/1" + ) + checkpoint = with_computed_digest(checkpoint) + + self.assert_outcome( + checkpoint, + CONTINUE, + previous_checkpoint=previous, + ) + def test_unknown_field_requires_restart(self): checkpoint = load_example() checkpoint["surprise"] = True @@ -230,6 +331,19 @@ def test_unknown_field_requires_restart(self): result = self.assert_outcome(checkpoint, RESTART_REQUIRED) self.assertEqual(result["reason_code"], "SCHEMA_INVALID") + def test_explicit_workspace_reaches_schema_validation_for_malformed_checkpoint(self): + checkpoint = load_example() + checkpoint.pop("workspace_state") + checkpoint = with_computed_digest(checkpoint) + + result = self.assert_outcome( + checkpoint, + RESTART_REQUIRED, + current_workspace={}, + ) + + self.assertEqual(result["reason_code"], "SCHEMA_INVALID") + if __name__ == "__main__": unittest.main() From 324c61a655532cb9a5030344b116ca78107d8089 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:23:43 +0300 Subject: [PATCH 11/40] fix: make dirty-state optional and require evidence refs --- ...ci-operational-checkpoint-v0.1.schema.json | 78 +++++++++++++++---- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json b/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json index cfa3bd83..c8fa6b90 100644 --- a/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json +++ b/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json @@ -39,7 +39,10 @@ "maxLength": 200 }, "parent_checkpoint_id": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "minLength": 1, "maxLength": 200 }, @@ -57,7 +60,10 @@ "source": { "type": "object", "additionalProperties": false, - "required": ["agent_id", "session_id"], + "required": [ + "agent_id", + "session_id" + ], "properties": { "agent_id": { "type": "string", @@ -78,8 +84,7 @@ "repository", "working_directory", "base_ref", - "head_sha", - "dirty_state_digest" + "head_sha" ], "properties": { "repository": { @@ -102,7 +107,9 @@ }, "dirty_state_digest": { "oneOf": [ - {"type": "null"}, + { + "type": "null" + }, { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" @@ -114,7 +121,10 @@ "objective": { "type": "object", "additionalProperties": false, - "required": ["goal", "acceptance_criteria"], + "required": [ + "goal", + "acceptance_criteria" + ], "properties": { "goal": { "type": "string", @@ -153,7 +163,10 @@ "constraints": { "type": "object", "additionalProperties": false, - "required": ["must", "must_not"], + "required": [ + "must", + "must_not" + ], "properties": { "must": { "type": "array", @@ -183,7 +196,12 @@ "items": { "type": "object", "additionalProperties": false, - "required": ["approach_id", "description", "reason", "status"], + "required": [ + "approach_id", + "description", + "reason", + "status" + ], "properties": { "approach_id": { "type": "string", @@ -212,7 +230,10 @@ "items": { "type": "object", "additionalProperties": false, - "required": ["resource_ref", "change_type"], + "required": [ + "resource_ref", + "change_type" + ], "properties": { "resource_ref": { "type": "string", @@ -220,7 +241,12 @@ "maxLength": 2000 }, "change_type": { - "enum": ["observed", "created", "modified", "deleted"] + "enum": [ + "observed", + "created", + "modified", + "deleted" + ] } } } @@ -228,7 +254,11 @@ "verification": { "type": "object", "additionalProperties": false, - "required": ["required", "completed", "pending"], + "required": [ + "required", + "completed", + "pending" + ], "properties": { "required": { "type": "array", @@ -290,7 +320,11 @@ "checkpoint_digest": { "type": "object", "additionalProperties": false, - "required": ["algorithm", "canonicalization", "value"], + "required": [ + "algorithm", + "canonicalization", + "value" + ], "properties": { "algorithm": { "const": "sha256" @@ -309,7 +343,11 @@ "completedVerification": { "type": "object", "additionalProperties": false, - "required": ["verification_id", "target", "evidence_refs"], + "required": [ + "verification_id", + "target", + "evidence_refs" + ], "properties": { "verification_id": { "type": "string", @@ -323,6 +361,7 @@ }, "evidence_refs": { "type": "array", + "minItems": 1, "maxItems": 20, "uniqueItems": true, "items": { @@ -336,7 +375,11 @@ "pendingVerification": { "type": "object", "additionalProperties": false, - "required": ["verification_id", "target", "status"], + "required": [ + "verification_id", + "target", + "status" + ], "properties": { "verification_id": { "type": "string", @@ -349,7 +392,12 @@ "maxLength": 1000 }, "status": { - "enum": ["pending", "running", "blocked", "failed"] + "enum": [ + "pending", + "running", + "blocked", + "failed" + ] } } } From 100d6f8734f80cb14eba5b0f57008dce10f9c957 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:24:30 +0300 Subject: [PATCH 12/40] docs: preserve proof lineage and explicit workspace evidence --- .../CI-OPERATIONAL-CHECKPOINT-V0.1.md | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md index 99149015..a3319243 100644 --- a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md +++ b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md @@ -15,8 +15,8 @@ to merge, deploy, send, publish, or invoke another consequential tool. checkpoint context -> validate schema and digest -> validate lineage - -> preserve rejected approaches - -> preserve required verification + -> preserve rejected approaches and their rationale + -> preserve completed verification targets and evidence -> compare current workspace -> CONTINUE | REVALIDATE | RESTART | REJECT ``` @@ -36,8 +36,8 @@ A conforming checkpoint records: - trajectory, checkpoint, parent, and sequence identity; - source agent and session; -- exact repository, working directory, base ref, head SHA, and optional - dirty-state digest; +- exact repository, working directory, base ref, and head SHA; +- optional dirty-state digest when the producer can observe workspace changes; - goal and acceptance criteria; - current delivery phase; - active `must` and `must_not` constraints; @@ -54,13 +54,22 @@ The strict schema is A verification item may appear in `completed` only when it carries at least one durable evidence reference. Narrative memory and agent summaries are not -verification evidence. The required verification set must equal the union of -completed and pending IDs. +verification evidence, regardless of URI-scheme letter case. The required +verification set must equal the union of completed and pending IDs. + +A later checkpoint must preserve, for every previously completed verification: + +- the same verification ID; +- the same target; +- every previously recorded evidence reference. + +Additional evidence references may be appended. Existing proof must not be +removed or replaced. A later checkpoint must not: - drop a previously completed verification; -- drop a rejected approach; +- drop or rewrite a rejected approach or its reason; - silently mark pending work completed without evidence; - change trajectory or skip sequence; - point at the wrong parent. @@ -69,9 +78,17 @@ A later checkpoint must not: Repository or working-directory mismatch returns `RESTART_REQUIRED`. -Changes to base ref, head SHA, or dirty-state digest return -`REVALIDATE_WORKSPACE`. The resumed agent must inspect the changed workspace and -create a new checkpoint before consequential work continues. +Changes to base ref, head SHA, or a declared dirty-state digest return +`REVALIDATE_WORKSPACE`. + +When a checkpoint declares `dirty_state_digest`, the resumed runtime MUST +explicitly report the current field, including `null` for a positively observed +clean workspace. Omitting that observation returns +`REVALIDATE_WORKSPACE / CURRENT_WORKSPACE_FIELD_MISSING`; absence is never +interpreted as clean. + +When the checkpoint omits `dirty_state_digest`, that field is outside this +checkpoint's comparison boundary. ## 6. Replay From 79ee81b3aa3fb5d57c88b095fcd3d50fb5022930 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:24:46 +0300 Subject: [PATCH 13/40] docs: register checkpoint lineage and workspace codes --- standards/agent-continuity/CI-CHECKPOINT-CODES.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/standards/agent-continuity/CI-CHECKPOINT-CODES.md b/standards/agent-continuity/CI-CHECKPOINT-CODES.md index 5c043d5a..3c51199b 100644 --- a/standards/agent-continuity/CI-CHECKPOINT-CODES.md +++ b/standards/agent-continuity/CI-CHECKPOINT-CODES.md @@ -7,11 +7,11 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | Outcome | Meaning | |---|---| | `CONTINUE` | The checkpoint is valid, lineage is coherent, the workspace matches, and the next step may resume as context-only work. | -| `REVALIDATE_WORKSPACE` | Repository identity matches, but base, head, or dirty-state evidence changed and must be re-observed. | +| `REVALIDATE_WORKSPACE` | Repository identity matches, but required workspace evidence is missing or base, head, or dirty-state evidence changed. | | `RESTART_REQUIRED` | The checkpoint cannot safely resume in the current workspace or failed schema/digest validation. | | `IDEMPOTENT_REPLAY` | The same checkpoint was already consumed; no duplicate work should be created. | | `REJECT_LINEAGE_MISMATCH` | Parent, sequence, trajectory, or rejected-approach continuity is inconsistent. | -| `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with the required verification set. | +| `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with prior proof lineage or the required verification set. | | `REJECT_INVALID_AUTHORITY` | Continuity material attempted to carry action authority or bypass fresh authorization. | ## Reason codes @@ -19,11 +19,11 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | Outcome | Reason code | |---|---| | `CONTINUE` | `CONTINUE_OK` | -| `REVALIDATE_WORKSPACE` | `WORKSPACE_STATE_CHANGED` | -| `RESTART_REQUIRED` | `SCHEMA_INVALID`, `DIGEST_MISMATCH`, `WORKSPACE_IDENTITY_MISMATCH` | +| `REVALIDATE_WORKSPACE` | `WORKSPACE_STATE_CHANGED`, `CURRENT_WORKSPACE_FIELD_MISSING` | +| `RESTART_REQUIRED` | `SCHEMA_INVALID`, `DIGEST_MISMATCH`, `WORKSPACE_IDENTITY_MISMATCH`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `IDEMPOTENT_REPLAY` | `CHECKPOINT_ALREADY_CONSUMED` | -| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `REJECTED_APPROACH_LOST` | -| `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST` | +| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | +| `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST`, `COMPLETED_VERIFICATION_CHANGED` | | `REJECT_INVALID_AUTHORITY` | `AUTHORITY_NOT_CONTEXT_ONLY`, `FRESH_AUTHORITY_REQUIRED` | Within version `0.1`, existing reason codes MUST NOT change meaning. From a62bed795f55a53ef599c65ca51fb0cab8d21f22 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:47:40 +0300 Subject: [PATCH 14/40] fix: require verified parent checkpoint lineage --- .../ci_operational_checkpoint_reference.py | 110 ++++++++++++++++-- 1 file changed, 102 insertions(+), 8 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index fc42d30f..abaf4aeb 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -88,6 +88,75 @@ def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, return {str(row["approach_id"]): row for row in rows} +def _duplicate_ids(rows: Iterable[Mapping[str, Any]], field: str) -> list[str]: + seen: set[str] = set() + duplicates: set[str] = set() + for row in rows: + value = str(row[field]) + if value in seen: + duplicates.add(value) + seen.add(value) + return sorted(duplicates) + + +def _previous_checkpoint_integrity_error( + previous_checkpoint: Mapping[str, Any], +) -> tuple[str, str] | None: + errors = _schema_errors(previous_checkpoint) + if errors: + first = errors[0] + path = "/".join(str(part) for part in first.absolute_path) or "" + return ( + "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", + f"{path}: {first.message}", + ) + + expected_digest = previous_checkpoint["checkpoint_digest"]["value"] + actual_digest = computed_digest(previous_checkpoint) + if expected_digest != actual_digest: + return ( + "PREVIOUS_CHECKPOINT_DIGEST_MISMATCH", + f"expected {expected_digest}, computed {actual_digest}", + ) + + duplicate_approaches = _duplicate_ids( + previous_checkpoint["rejected_approaches"], + "approach_id", + ) + if duplicate_approaches: + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + ) + + previous_verification = previous_checkpoint["verification"] + previous_completed = previous_verification["completed"] + previous_pending = previous_verification["pending"] + verification_ids = [ + row["verification_id"] for row in previous_completed + previous_pending + ] + if len(verification_ids) != len(set(verification_ids)): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "verification IDs are duplicated across completed and pending lists", + ) + if set(verification_ids) != set(previous_verification["required"]): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "required verification IDs do not match completed and pending rows", + ) + for row in previous_completed: + if any( + str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) + for ref in row["evidence_refs"] + ): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + f"{row['verification_id']} relies on memory-only evidence", + ) + return None + + def evaluate_resume( checkpoint: Mapping[str, Any], *, @@ -129,6 +198,17 @@ def evaluate_resume( f"checkpoint {checkpoint_id} was already consumed", ) + duplicate_approaches = _duplicate_ids( + checkpoint["rejected_approaches"], + "approach_id", + ) + if duplicate_approaches: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_ID_DUPLICATED", + "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + ) + sequence = checkpoint["sequence"] parent_checkpoint_id = checkpoint["parent_checkpoint_id"] if sequence == 0 and parent_checkpoint_id is not None: @@ -145,26 +225,40 @@ def evaluate_resume( ) if previous_checkpoint is None: - if sequence > 0 and parent_checkpoint_id not in set(known_parent_ids): + if sequence > 0: + if parent_checkpoint_id not in set(known_parent_ids): + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_NOT_FOUND", + f"parent checkpoint {parent_checkpoint_id} is not known", + ) return _result( REJECT_LINEAGE_MISMATCH, - "PARENT_NOT_FOUND", - f"parent checkpoint {parent_checkpoint_id} is not known", + "PREVIOUS_CHECKPOINT_REQUIRED", + "non-root resume requires the full previous checkpoint", ) else: - if checkpoint["trajectory_id"] != previous_checkpoint.get("trajectory_id"): + integrity_error = _previous_checkpoint_integrity_error(previous_checkpoint) + if integrity_error is not None: + reason_code, detail = integrity_error + return _result( + REJECT_LINEAGE_MISMATCH, + reason_code, + detail, + ) + if checkpoint["trajectory_id"] != previous_checkpoint["trajectory_id"]: return _result( REJECT_LINEAGE_MISMATCH, "TRAJECTORY_CHANGED", "checkpoint trajectory differs from previous checkpoint", ) - if parent_checkpoint_id != previous_checkpoint.get("checkpoint_id"): + if parent_checkpoint_id != previous_checkpoint["checkpoint_id"]: return _result( REJECT_LINEAGE_MISMATCH, "PARENT_MISMATCH", "parent_checkpoint_id does not reference the previous checkpoint", ) - if sequence != previous_checkpoint.get("sequence", -1) + 1: + if sequence != previous_checkpoint["sequence"] + 1: return _result( REJECT_LINEAGE_MISMATCH, "SEQUENCE_MISMATCH", @@ -172,7 +266,7 @@ def evaluate_resume( ) previous_rejected = _rejected_index( - previous_checkpoint.get("rejected_approaches", []) + previous_checkpoint["rejected_approaches"] ) current_rejected = _rejected_index(checkpoint["rejected_approaches"]) missing_rejections = sorted(set(previous_rejected) - set(current_rejected)) @@ -195,7 +289,7 @@ def evaluate_resume( ) previous_completed = _verification_index( - previous_checkpoint.get("verification", {}).get("completed", []) + previous_checkpoint["verification"]["completed"] ) current_completed = _verification_index( checkpoint["verification"]["completed"] From d9514cae8da4f0ac1ae9b9158630b66a2f576b77 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:52:23 +0300 Subject: [PATCH 15/40] test: require full parent checkpoint --- .../test_ci_checkpoint_parent_requirement.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 standards/agent-continuity/conformance/test_ci_checkpoint_parent_requirement.py diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_parent_requirement.py b/standards/agent-continuity/conformance/test_ci_checkpoint_parent_requirement.py new file mode 100644 index 00000000..00c92f0b --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_parent_requirement.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import copy +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from ci_operational_checkpoint_reference import ( # noqa: E402 + CONTINUE, + REJECT_LINEAGE_MISMATCH, + evaluate_resume, + with_computed_digest, +) + +EXAMPLE = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE.read_text(encoding="utf-8")) + + +def child_of(parent): + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:issue-218:child" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = parent["sequence"] + 1 + child["created_at"] = "2026-07-02T11:45:00Z" + return with_computed_digest(child) + + +class ParentRequirementTest(unittest.TestCase): + def evaluate(self, checkpoint, **kwargs): + return evaluate_resume( + checkpoint, + current_workspace=copy.deepcopy(checkpoint["workspace_state"]), + **kwargs, + ) + + def test_full_parent_allows_non_root_resume(self): + parent = load_example() + result = self.evaluate(child_of(parent), previous_checkpoint=parent) + self.assertEqual((result["outcome"], result["reason_code"]), (CONTINUE, "CONTINUE_OK")) + + def test_known_parent_id_is_not_enough(self): + parent = load_example() + result = self.evaluate( + child_of(parent), + known_parent_ids={parent["checkpoint_id"]}, + ) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_REQUIRED"), + ) + + def test_unknown_parent_is_rejected(self): + parent = load_example() + result = self.evaluate(child_of(parent)) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "PARENT_NOT_FOUND"), + ) + + +if __name__ == "__main__": + unittest.main() From 27f6f52410effb015f8317df8755c08e99fb11f3 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:53:29 +0300 Subject: [PATCH 16/40] test: validate parent checkpoint integrity --- .../test_ci_checkpoint_parent_validation.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 standards/agent-continuity/conformance/test_ci_checkpoint_parent_validation.py diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_parent_validation.py b/standards/agent-continuity/conformance/test_ci_checkpoint_parent_validation.py new file mode 100644 index 00000000..d385fa60 --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_parent_validation.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import copy +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from ci_operational_checkpoint_reference import ( # noqa: E402 + REJECT_LINEAGE_MISMATCH, + evaluate_resume, + with_computed_digest, +) + +EXAMPLE = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE.read_text(encoding="utf-8")) + + +def child_of(parent): + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:issue-218:validated-parent" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = parent["sequence"] + 1 + child["created_at"] = "2026-07-02T11:46:00Z" + return with_computed_digest(child) + + +def evaluate(child, parent=None): + return evaluate_resume( + child, + current_workspace=copy.deepcopy(child["workspace_state"]), + previous_checkpoint=parent, + ) + + +class ParentValidationTest(unittest.TestCase): + def test_parent_schema_is_checked(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent.pop("authority") + invalid_parent = with_computed_digest(invalid_parent) + result = evaluate(child, invalid_parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_SCHEMA_INVALID"), + ) + + def test_parent_digest_is_checked(self): + parent = load_example() + child = child_of(parent) + changed_parent = copy.deepcopy(parent) + changed_parent["objective"]["goal"] = "Changed parent goal" + result = evaluate(child, changed_parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_DIGEST_MISMATCH"), + ) + + def test_duplicate_rejected_ids_are_rejected(self): + checkpoint = load_example() + checkpoint["rejected_approaches"].append( + copy.deepcopy(checkpoint["rejected_approaches"][0]) + ) + checkpoint = with_computed_digest(checkpoint) + result = evaluate(checkpoint) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "REJECTED_APPROACH_ID_DUPLICATED"), + ) + + def test_ambiguous_parent_is_rejected(self): + parent = load_example() + child = child_of(parent) + ambiguous_parent = copy.deepcopy(parent) + ambiguous_parent["rejected_approaches"].append( + copy.deepcopy(ambiguous_parent["rejected_approaches"][0]) + ) + ambiguous_parent = with_computed_digest(ambiguous_parent) + result = evaluate(child, ambiguous_parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID"), + ) + + +if __name__ == "__main__": + unittest.main() From d4a88781a1c50fc23fec3168b3fbfb4737475823 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:54:37 +0300 Subject: [PATCH 17/40] docs: register verified parent lineage codes --- standards/agent-continuity/CI-CHECKPOINT-CODES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/agent-continuity/CI-CHECKPOINT-CODES.md b/standards/agent-continuity/CI-CHECKPOINT-CODES.md index 3c51199b..97206cbc 100644 --- a/standards/agent-continuity/CI-CHECKPOINT-CODES.md +++ b/standards/agent-continuity/CI-CHECKPOINT-CODES.md @@ -10,7 +10,7 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | `REVALIDATE_WORKSPACE` | Repository identity matches, but required workspace evidence is missing or base, head, or dirty-state evidence changed. | | `RESTART_REQUIRED` | The checkpoint cannot safely resume in the current workspace or failed schema/digest validation. | | `IDEMPOTENT_REPLAY` | The same checkpoint was already consumed; no duplicate work should be created. | -| `REJECT_LINEAGE_MISMATCH` | Parent, sequence, trajectory, or rejected-approach continuity is inconsistent. | +| `REJECT_LINEAGE_MISMATCH` | Parent, sequence, trajectory, rejected-approach continuity, or parent-checkpoint integrity is inconsistent. | | `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with prior proof lineage or the required verification set. | | `REJECT_INVALID_AUTHORITY` | Continuity material attempted to carry action authority or bypass fresh authorization. | @@ -22,7 +22,7 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | `REVALIDATE_WORKSPACE` | `WORKSPACE_STATE_CHANGED`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `RESTART_REQUIRED` | `SCHEMA_INVALID`, `DIGEST_MISMATCH`, `WORKSPACE_IDENTITY_MISMATCH`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `IDEMPOTENT_REPLAY` | `CHECKPOINT_ALREADY_CONSUMED` | -| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | +| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PREVIOUS_CHECKPOINT_REQUIRED`, `PREVIOUS_CHECKPOINT_SCHEMA_INVALID`, `PREVIOUS_CHECKPOINT_DIGEST_MISMATCH`, `PREVIOUS_CHECKPOINT_SEMANTIC_INVALID`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `REJECTED_APPROACH_ID_DUPLICATED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | | `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST`, `COMPLETED_VERIFICATION_CHANGED` | | `REJECT_INVALID_AUTHORITY` | `AUTHORITY_NOT_CONTEXT_ONLY`, `FRESH_AUTHORITY_REQUIRED` | From bdbcdf7185f7c355ace27aae582e3c2c771ef39e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:56:59 +0300 Subject: [PATCH 18/40] docs: require verified parent lineage --- .../CI-OPERATIONAL-CHECKPOINT-V0.1.md | 148 +++++++----------- 1 file changed, 57 insertions(+), 91 deletions(-) diff --git a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md index a3319243..49ea39b9 100644 --- a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md +++ b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md @@ -2,128 +2,94 @@ - **Status:** Draft - **Profile:** `pythia.ci_operational_checkpoint.v0.1` -- **Parent:** [RFC-001 — Verifiable Continuation Envelope](RFC-001-VERIFIABLE-CONTINUATION-ENVELOPE.md) -- **Scope:** Agentic CI/CD phase transitions, review waits, retries, restart, and handoff +- **Parent:** [RFC-001](RFC-001-VERIFIABLE-CONTINUATION-ENVELOPE.md) +- **Scope:** CI/CD phase transitions, retries, restart, and agent handoff -## 1. Purpose +## Purpose -This profile defines a bounded checkpoint for resuming CI/CD work without -reconstructing state from a narrative summary and without inheriting authority -to merge, deploy, send, publish, or invoke another consequential tool. +This profile defines a bounded checkpoint for resuming delivery work without +reconstructing state from narrative memory and without inheriting authority for +a consequential action. ```text -checkpoint context - -> validate schema and digest - -> validate lineage - -> preserve rejected approaches and their rationale - -> preserve completed verification targets and evidence +validate schema and digest + -> check replay + -> validate parent checkpoint and lineage + -> preserve rejected rationale and completed proof -> compare current workspace - -> CONTINUE | REVALIDATE | RESTART | REJECT + -> CONTINUE | REVALIDATE | RESTART | IDEMPOTENT_REPLAY | REJECT ``` -## 2. Core boundary +## Core boundary > Context may continue. Authority must be evaluated again. -`authority` is fixed to `context_only`. A checkpoint can carry an exact next -action, including the intent to merge or deploy, but those action classes MUST -set `requires_fresh_authority=true`. The checkpoint never replaces an Action -Envelope, current checks, current reviews, or an executor guard. +`authority` is fixed to `context_only`. Merge and deploy intent MUST set +`requires_fresh_authority=true`. A checkpoint never replaces an Action Envelope, +current checks, current reviews, or an executor guard. -## 3. Required content +## Required content -A conforming checkpoint records: - -- trajectory, checkpoint, parent, and sequence identity; -- source agent and session; -- exact repository, working directory, base ref, and head SHA; -- optional dirty-state digest when the producer can observe workspace changes; -- goal and acceptance criteria; -- current delivery phase; -- active `must` and `must_not` constraints; -- rejected approaches with reasons; -- touched resources; -- required, completed, and pending verification; -- one exact next action; -- canonical SHA-256 digest. +A conforming checkpoint records trajectory and parent identity, source agent and +session, repository and workspace identity, objective, phase, constraints, +rejected approaches, touched resources, verification state, the next action, +and a canonical SHA-256 digest. `dirty_state_digest` is optional. The strict schema is [`schema/ci-operational-checkpoint-v0.1.schema.json`](schema/ci-operational-checkpoint-v0.1.schema.json). -## 4. Verification boundary - -A verification item may appear in `completed` only when it carries at least one -durable evidence reference. Narrative memory and agent summaries are not -verification evidence, regardless of URI-scheme letter case. The required -verification set must equal the union of completed and pending IDs. +## Verification continuity -A later checkpoint must preserve, for every previously completed verification: +Completed verification requires durable evidence. Memory and agent summaries +are not verification evidence, regardless of URI-scheme letter case. -- the same verification ID; -- the same target; -- every previously recorded evidence reference. +A later checkpoint MUST preserve the prior verification ID, target, and every +existing evidence reference. Additional evidence may be appended. It MUST also +preserve each rejected approach row, including its rationale. -Additional evidence references may be appended. Existing proof must not be -removed or replaced. +## Parent-checkpoint integrity -A later checkpoint must not: +Every non-root resume requires the full previous checkpoint. A known parent ID +alone is insufficient because it cannot prove rejected-approach or completed +verification continuity. -- drop a previously completed verification; -- drop or rewrite a rejected approach or its reason; -- silently mark pending work completed without evidence; -- change trajectory or skip sequence; -- point at the wrong parent. +Before comparison, the previous checkpoint MUST pass schema validation, +canonical digest verification, identifier uniqueness, verification-set +consistency, and the memory-evidence boundary. Invalid or missing parent material +returns `REJECT_LINEAGE_MISMATCH`. -## 5. Workspace comparison +Durable lookup and storage remain follow-up work in issue #223. -Repository or working-directory mismatch returns `RESTART_REQUIRED`. +## Workspace comparison -Changes to base ref, head SHA, or a declared dirty-state digest return +A repository or working-directory mismatch returns `RESTART_REQUIRED`. Changes +to base ref, head SHA, or a declared dirty-state digest return `REVALIDATE_WORKSPACE`. -When a checkpoint declares `dirty_state_digest`, the resumed runtime MUST -explicitly report the current field, including `null` for a positively observed -clean workspace. Omitting that observation returns -`REVALIDATE_WORKSPACE / CURRENT_WORKSPACE_FIELD_MISSING`; absence is never -interpreted as clean. - -When the checkpoint omits `dirty_state_digest`, that field is outside this -checkpoint's comparison boundary. +When the checkpoint declares `dirty_state_digest`, the resumed runtime MUST +explicitly report it, including `null` for an observed clean workspace. Missing +observation returns `CURRENT_WORKSPACE_FIELD_MISSING`. If the checkpoint omits +the field, it is outside the comparison boundary. -## 6. Replay +## Replay -A consumed checkpoint ID returns `IDEMPOTENT_REPLAY`. The caller should not -repeat completed work or create duplicate side effects. +A consumed checkpoint ID returns `IDEMPOTENT_REPLAY`; completed work must not be +repeated. -## 7. Relationship to Action Envelope V1 - -The checkpoint answers: - -```text -What context and verification state should the next agent restore? -``` - -Action Envelope V1 answers: - -```text -May this exact consequential action execute now? -``` +## Relationship to Action Envelope V1 -The two contracts are complementary and deliberately non-substitutable. +The checkpoint says what context and verification state to restore. Action +Envelope V1 decides whether an exact consequential action may execute now. The +contracts are complementary and non-substitutable. -## 8. Reference implementation +## Reference files -- example checkpoint: - [`examples/ci-operational-checkpoint-v0.1.example.json`](examples/ci-operational-checkpoint-v0.1.example.json); -- deterministic evaluator: - [`conformance/ci_operational_checkpoint_reference.py`](conformance/ci_operational_checkpoint_reference.py); -- executable tests: - [`conformance/test_ci_operational_checkpoint.py`](conformance/test_ci_operational_checkpoint.py); -- stable outcomes: - [`CI-CHECKPOINT-CODES.md`](CI-CHECKPOINT-CODES.md). +- example: [`examples/ci-operational-checkpoint-v0.1.example.json`](examples/ci-operational-checkpoint-v0.1.example.json) +- evaluator: [`conformance/ci_operational_checkpoint_reference.py`](conformance/ci_operational_checkpoint_reference.py) +- tests: [`conformance/test_ci_operational_checkpoint.py`](conformance/test_ci_operational_checkpoint.py) +- codes: [`CI-CHECKPOINT-CODES.md`](CI-CHECKPOINT-CODES.md) -## 9. Non-claims +## Non-claims -This profile is not durable storage, a distributed lease, a workflow engine, an -identity system, proof that an action occurred, or permission to perform a tool -call. Durable coordination is tracked separately in issue #223. +This profile is not durable storage, a distributed lease, proof of execution, or +permission for a tool call. From b26e66c3d623866ca1d48f881a026b0a573bb0d5 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:03:14 +0300 Subject: [PATCH 19/40] fix: preserve objective constraints and pending verification --- .../ci_operational_checkpoint_reference.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index abaf4aeb..c640fecc 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -3,6 +3,7 @@ import copy import hashlib import json +from datetime import datetime from pathlib import Path from typing import Any, Iterable, Mapping @@ -88,6 +89,11 @@ def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, return {str(row["approach_id"]): row for row in rows} +def _parse_time(value: str) -> datetime: + normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value + return datetime.fromisoformat(normalized) + + def _duplicate_ids(rows: Iterable[Mapping[str, Any]], field: str) -> list[str]: seen: set[str] = set() duplicates: set[str] = set() @@ -252,6 +258,30 @@ def evaluate_resume( "TRAJECTORY_CHANGED", "checkpoint trajectory differs from previous checkpoint", ) + if _parse_time(checkpoint["created_at"]) < _parse_time( + previous_checkpoint["created_at"] + ): + return _result( + REJECT_LINEAGE_MISMATCH, + "CREATION_TIME_REGRESSED", + "checkpoint creation time is earlier than its parent", + ) + if checkpoint["objective"] != previous_checkpoint["objective"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "OBJECTIVE_CHANGED", + "objective changed within the same trajectory", + ) + for field in ("must", "must_not"): + previous_constraints = set(previous_checkpoint["constraints"][field]) + current_constraints = set(checkpoint["constraints"][field]) + lost_constraints = sorted(previous_constraints - current_constraints) + if lost_constraints: + return _result( + REJECT_LINEAGE_MISMATCH, + "CONSTRAINT_LOST", + f"{field} constraints disappeared: " + ", ".join(lost_constraints), + ) if parent_checkpoint_id != previous_checkpoint["checkpoint_id"]: return _result( REJECT_LINEAGE_MISMATCH, @@ -318,6 +348,29 @@ def evaluate_resume( ), ) + previous_pending = _verification_index( + previous_checkpoint["verification"]["pending"] + ) + current_pending = _verification_index( + checkpoint["verification"]["pending"] + ) + for verification_id, previous_row in previous_pending.items(): + current_row = current_pending.get(verification_id) + completed_row = current_completed.get(verification_id) + if current_row is None and completed_row is None: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "PENDING_VERIFICATION_LOST", + f"pending verification disappeared: {verification_id}", + ) + successor = completed_row if completed_row is not None else current_row + if successor["target"] != previous_row["target"]: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "PENDING_VERIFICATION_CHANGED", + f"pending verification target changed: {verification_id}", + ) + required_ids = set(checkpoint["verification"]["required"]) completed_rows = checkpoint["verification"]["completed"] pending_rows = checkpoint["verification"]["pending"] From be565457c962ee9416a9c444c01dbf486f86c9d5 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:04:19 +0300 Subject: [PATCH 20/40] test: preserve active checkpoint state --- ...test_ci_checkpoint_active_state_lineage.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 standards/agent-continuity/conformance/test_ci_checkpoint_active_state_lineage.py diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_active_state_lineage.py b/standards/agent-continuity/conformance/test_ci_checkpoint_active_state_lineage.py new file mode 100644 index 00000000..d6b42b0d --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_active_state_lineage.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import copy +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from ci_operational_checkpoint_reference import ( # noqa: E402 + REJECT_LINEAGE_MISMATCH, + REJECT_UNVERIFIED_COMPLETION, + evaluate_resume, + with_computed_digest, +) + +EXAMPLE = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE.read_text(encoding="utf-8")) + + +def child_of(parent): + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:issue-218:active-state" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = parent["sequence"] + 1 + child["created_at"] = "2026-07-02T12:05:00Z" + return child + + +def evaluate(child, parent): + return evaluate_resume( + with_computed_digest(child), + current_workspace=copy.deepcopy(child["workspace_state"]), + previous_checkpoint=parent, + ) + + +class ActiveStateLineageTest(unittest.TestCase): + def test_creation_time_cannot_move_backwards(self): + parent = load_example() + child = child_of(parent) + child["created_at"] = "2026-07-02T09:00:00Z" + result = evaluate(child, parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "CREATION_TIME_REGRESSED"), + ) + + def test_objective_cannot_change_within_trajectory(self): + parent = load_example() + child = child_of(parent) + child["objective"]["goal"] = "Different goal" + result = evaluate(child, parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "OBJECTIVE_CHANGED"), + ) + + def test_active_constraint_cannot_disappear(self): + parent = load_example() + child = child_of(parent) + child["constraints"]["must"] = [] + result = evaluate(child, parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, "CONSTRAINT_LOST"), + ) + + def test_pending_verification_cannot_disappear(self): + parent = load_example() + child = child_of(parent) + child["verification"]["required"].remove("verification:conformance") + child["verification"]["pending"] = [] + result = evaluate(child, parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_UNVERIFIED_COMPLETION, "PENDING_VERIFICATION_LOST"), + ) + + def test_pending_verification_target_cannot_change(self): + parent = load_example() + child = child_of(parent) + child["verification"]["pending"][0]["target"] = "Different target" + result = evaluate(child, parent) + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_UNVERIFIED_COMPLETION, "PENDING_VERIFICATION_CHANGED"), + ) + + +if __name__ == "__main__": + unittest.main() From e3e2a19d5b55db5a905dfb02b7e0d295f6f0c061 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:05:17 +0300 Subject: [PATCH 21/40] docs: register active-state lineage codes --- standards/agent-continuity/CI-CHECKPOINT-CODES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/standards/agent-continuity/CI-CHECKPOINT-CODES.md b/standards/agent-continuity/CI-CHECKPOINT-CODES.md index 97206cbc..6187441a 100644 --- a/standards/agent-continuity/CI-CHECKPOINT-CODES.md +++ b/standards/agent-continuity/CI-CHECKPOINT-CODES.md @@ -10,8 +10,8 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | `REVALIDATE_WORKSPACE` | Repository identity matches, but required workspace evidence is missing or base, head, or dirty-state evidence changed. | | `RESTART_REQUIRED` | The checkpoint cannot safely resume in the current workspace or failed schema/digest validation. | | `IDEMPOTENT_REPLAY` | The same checkpoint was already consumed; no duplicate work should be created. | -| `REJECT_LINEAGE_MISMATCH` | Parent, sequence, trajectory, rejected-approach continuity, or parent-checkpoint integrity is inconsistent. | -| `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with prior proof lineage or the required verification set. | +| `REJECT_LINEAGE_MISMATCH` | Parent, time, objective, constraints, sequence, trajectory, rejected-approach continuity, or parent integrity is inconsistent. | +| `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with prior proof or pending-work lineage. | | `REJECT_INVALID_AUTHORITY` | Continuity material attempted to carry action authority or bypass fresh authorization. | ## Reason codes @@ -22,8 +22,8 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | `REVALIDATE_WORKSPACE` | `WORKSPACE_STATE_CHANGED`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `RESTART_REQUIRED` | `SCHEMA_INVALID`, `DIGEST_MISMATCH`, `WORKSPACE_IDENTITY_MISMATCH`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `IDEMPOTENT_REPLAY` | `CHECKPOINT_ALREADY_CONSUMED` | -| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PREVIOUS_CHECKPOINT_REQUIRED`, `PREVIOUS_CHECKPOINT_SCHEMA_INVALID`, `PREVIOUS_CHECKPOINT_DIGEST_MISMATCH`, `PREVIOUS_CHECKPOINT_SEMANTIC_INVALID`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `REJECTED_APPROACH_ID_DUPLICATED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | -| `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST`, `COMPLETED_VERIFICATION_CHANGED` | +| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PREVIOUS_CHECKPOINT_REQUIRED`, `PREVIOUS_CHECKPOINT_SCHEMA_INVALID`, `PREVIOUS_CHECKPOINT_DIGEST_MISMATCH`, `PREVIOUS_CHECKPOINT_SEMANTIC_INVALID`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `CREATION_TIME_REGRESSED`, `OBJECTIVE_CHANGED`, `CONSTRAINT_LOST`, `REJECTED_APPROACH_ID_DUPLICATED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | +| `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST`, `COMPLETED_VERIFICATION_CHANGED`, `PENDING_VERIFICATION_LOST`, `PENDING_VERIFICATION_CHANGED` | | `REJECT_INVALID_AUTHORITY` | `AUTHORITY_NOT_CONTEXT_ONLY`, `FRESH_AUTHORITY_REQUIRED` | Within version `0.1`, existing reason codes MUST NOT change meaning. From 0b11b697300d00ab210dbc48a857e3aa4a963937 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:06:18 +0300 Subject: [PATCH 22/40] docs: preserve active checkpoint state --- .../CI-OPERATIONAL-CHECKPOINT-V0.1.md | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md index 49ea39b9..900590d0 100644 --- a/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md +++ b/standards/agent-continuity/CI-OPERATIONAL-CHECKPOINT-V0.1.md @@ -15,7 +15,7 @@ a consequential action. validate schema and digest -> check replay -> validate parent checkpoint and lineage - -> preserve rejected rationale and completed proof + -> preserve objective, constraints, rejected rationale, and verification proof -> compare current workspace -> CONTINUE | REVALIDATE | RESTART | IDEMPOTENT_REPLAY | REJECT ``` @@ -38,20 +38,32 @@ and a canonical SHA-256 digest. `dirty_state_digest` is optional. The strict schema is [`schema/ci-operational-checkpoint-v0.1.schema.json`](schema/ci-operational-checkpoint-v0.1.schema.json). -## Verification continuity +## Active-state continuity -Completed verification requires durable evidence. Memory and agent summaries -are not verification evidence, regardless of URI-scheme letter case. +Within one trajectory: + +- child creation time MUST NOT precede parent creation time; +- objective and acceptance criteria MUST remain unchanged; +- every prior `must` and `must_not` constraint MUST remain active; +- every rejected approach row and rationale MUST remain unchanged; +- every completed verification target and prior evidence reference MUST remain; +- every pending verification MUST remain with the same target or move to + completed with durable evidence. + +Constraints and evidence may be added. Existing active state and proof may not be +silently removed or rewritten. A changed objective requires a new trajectory. -A later checkpoint MUST preserve the prior verification ID, target, and every -existing evidence reference. Additional evidence may be appended. It MUST also -preserve each rejected approach row, including its rationale. +## Verification boundary + +Completed verification requires durable evidence. Memory and agent summaries +are not verification evidence, regardless of URI-scheme letter case. Additional +evidence may be appended, but existing evidence cannot be removed or replaced. ## Parent-checkpoint integrity Every non-root resume requires the full previous checkpoint. A known parent ID -alone is insufficient because it cannot prove rejected-approach or completed -verification continuity. +alone is insufficient because it cannot prove active-state or verification +continuity. Before comparison, the previous checkpoint MUST pass schema validation, canonical digest verification, identifier uniqueness, verification-set From ce7cd8a3bba4e36e34456104850c1d813428c88f Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:15:53 +0300 Subject: [PATCH 23/40] fix: anchor checkpoint example evidence to commit --- .../examples/ci-operational-checkpoint-v0.1.example.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json b/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json index f672f336..1bfa8de8 100644 --- a/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json +++ b/standards/agent-continuity/examples/ci-operational-checkpoint-v0.1.example.json @@ -60,7 +60,7 @@ "verification_id": "verification:schema", "target": "CI operational checkpoint JSON Schema", "evidence_refs": [ - "git://safal207/pythiaLabs/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json" + "git://safal207/pythiaLabs@0b11b697300d00ab210dbc48a857e3aa4a963937/standards/agent-continuity/schema/ci-operational-checkpoint-v0.1.schema.json" ] } ], @@ -80,6 +80,6 @@ "checkpoint_digest": { "algorithm": "sha256", "canonicalization": "json-sort-keys-utf8-v1", - "value": "e67301839ba684347c432d48b3f8557d8aba9283c370e0256dc8553c50556ae1" + "value": "a54e8cc08bec0ddc86347f66dcd11f0dfb671127197d6f7ea5ab9463b4a05867" } } From 6a1e51b211cb3f40985b0cb17b30cc7ed23fd2fe Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:33:10 +0300 Subject: [PATCH 24/40] fix: harden checkpoint parent link invariants --- .../ci_operational_checkpoint_reference.py | 495 ++---------------- ...i_operational_checkpoint_reference_core.py | 470 +++++++++++++++++ .../test_ci_checkpoint_link_invariants.py | 83 +++ 3 files changed, 601 insertions(+), 447 deletions(-) create mode 100644 standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py create mode 100644 standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index c640fecc..9d2c367e 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -1,166 +1,9 @@ from __future__ import annotations -import copy -import hashlib -import json -from datetime import datetime -from pathlib import Path from typing import Any, Iterable, Mapping -from jsonschema import Draft202012Validator, FormatChecker - -HERE = Path(__file__).resolve().parent -ROOT = HERE.parent -SCHEMA_PATH = ROOT / "schema" / "ci-operational-checkpoint-v0.1.schema.json" - -CONTINUE = "CONTINUE" -REVALIDATE_WORKSPACE = "REVALIDATE_WORKSPACE" -RESTART_REQUIRED = "RESTART_REQUIRED" -IDEMPOTENT_REPLAY = "IDEMPOTENT_REPLAY" -REJECT_LINEAGE_MISMATCH = "REJECT_LINEAGE_MISMATCH" -REJECT_UNVERIFIED_COMPLETION = "REJECT_UNVERIFIED_COMPLETION" -REJECT_INVALID_AUTHORITY = "REJECT_INVALID_AUTHORITY" - -MEMORY_ONLY_PREFIXES = ("memory://", "agent-memory://", "summary://") - - -def load_schema() -> dict[str, Any]: - schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) - if not isinstance(schema, dict): - raise ValueError("checkpoint schema root must be an object") - Draft202012Validator.check_schema(schema) - return schema - - -def _canonical_bytes(checkpoint: Mapping[str, Any]) -> bytes: - value = copy.deepcopy(dict(checkpoint)) - value.pop("checkpoint_digest", None) - return json.dumps( - value, - ensure_ascii=False, - sort_keys=True, - separators=(",", ":"), - allow_nan=False, - ).encode("utf-8") - - -def computed_digest(checkpoint: Mapping[str, Any]) -> str: - return hashlib.sha256(_canonical_bytes(checkpoint)).hexdigest() - - -def with_computed_digest(checkpoint: Mapping[str, Any]) -> dict[str, Any]: - result = copy.deepcopy(dict(checkpoint)) - result.setdefault( - "checkpoint_digest", - { - "algorithm": "sha256", - "canonicalization": "json-sort-keys-utf8-v1", - "value": "0" * 64, - }, - ) - result["checkpoint_digest"]["value"] = computed_digest(result) - return result - - -def _schema_errors(checkpoint: Mapping[str, Any]) -> list[Any]: - validator = Draft202012Validator( - load_schema(), - format_checker=FormatChecker(), - ) - return sorted( - validator.iter_errors(dict(checkpoint)), - key=lambda error: [str(part) for part in error.absolute_path], - ) - - -def _result(outcome: str, reason_code: str, detail: str) -> dict[str, str]: - return { - "outcome": outcome, - "reason_code": reason_code, - "detail": detail, - } - - -def _verification_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: - return {str(row["verification_id"]): row for row in rows} - - -def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: - return {str(row["approach_id"]): row for row in rows} - - -def _parse_time(value: str) -> datetime: - normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value - return datetime.fromisoformat(normalized) - - -def _duplicate_ids(rows: Iterable[Mapping[str, Any]], field: str) -> list[str]: - seen: set[str] = set() - duplicates: set[str] = set() - for row in rows: - value = str(row[field]) - if value in seen: - duplicates.add(value) - seen.add(value) - return sorted(duplicates) - - -def _previous_checkpoint_integrity_error( - previous_checkpoint: Mapping[str, Any], -) -> tuple[str, str] | None: - errors = _schema_errors(previous_checkpoint) - if errors: - first = errors[0] - path = "/".join(str(part) for part in first.absolute_path) or "" - return ( - "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", - f"{path}: {first.message}", - ) - - expected_digest = previous_checkpoint["checkpoint_digest"]["value"] - actual_digest = computed_digest(previous_checkpoint) - if expected_digest != actual_digest: - return ( - "PREVIOUS_CHECKPOINT_DIGEST_MISMATCH", - f"expected {expected_digest}, computed {actual_digest}", - ) - - duplicate_approaches = _duplicate_ids( - previous_checkpoint["rejected_approaches"], - "approach_id", - ) - if duplicate_approaches: - return ( - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), - ) - - previous_verification = previous_checkpoint["verification"] - previous_completed = previous_verification["completed"] - previous_pending = previous_verification["pending"] - verification_ids = [ - row["verification_id"] for row in previous_completed + previous_pending - ] - if len(verification_ids) != len(set(verification_ids)): - return ( - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "verification IDs are duplicated across completed and pending lists", - ) - if set(verification_ids) != set(previous_verification["required"]): - return ( - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "required verification IDs do not match completed and pending rows", - ) - for row in previous_completed: - if any( - str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) - for ref in row["evidence_refs"] - ): - return ( - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - f"{row['verification_id']} relies on memory-only evidence", - ) - return None +import ci_operational_checkpoint_reference_core as _core +from ci_operational_checkpoint_reference_core import * # noqa: F401,F403 def evaluate_resume( @@ -171,300 +14,58 @@ def evaluate_resume( seen_checkpoint_ids: Iterable[str] = (), known_parent_ids: Iterable[str] = (), ) -> dict[str, str]: - errors = _schema_errors(checkpoint) - if errors: - first = errors[0] - path = "/".join(str(part) for part in first.absolute_path) or "" - if path == "authority": - return _result( - REJECT_INVALID_AUTHORITY, - "AUTHORITY_NOT_CONTEXT_ONLY", - first.message, - ) - return _result( - RESTART_REQUIRED, - "SCHEMA_INVALID", - f"{path}: {first.message}", - ) - - expected_digest = checkpoint["checkpoint_digest"]["value"] - actual_digest = computed_digest(checkpoint) - if expected_digest != actual_digest: - return _result( - RESTART_REQUIRED, - "DIGEST_MISMATCH", - f"expected {expected_digest}, computed {actual_digest}", - ) - - checkpoint_id = checkpoint["checkpoint_id"] - if checkpoint_id in set(seen_checkpoint_ids): - return _result( - IDEMPOTENT_REPLAY, - "CHECKPOINT_ALREADY_CONSUMED", - f"checkpoint {checkpoint_id} was already consumed", - ) + """Apply link-level lineage guards before the core resume evaluator.""" - duplicate_approaches = _duplicate_ids( - checkpoint["rejected_approaches"], - "approach_id", + current_errors = _core._schema_errors(checkpoint) + current_digest_valid = ( + not current_errors + and checkpoint["checkpoint_digest"]["value"] + == _core.computed_digest(checkpoint) ) - if duplicate_approaches: - return _result( - REJECT_LINEAGE_MISMATCH, - "REJECTED_APPROACH_ID_DUPLICATED", - "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), - ) - - sequence = checkpoint["sequence"] - parent_checkpoint_id = checkpoint["parent_checkpoint_id"] - if sequence == 0 and parent_checkpoint_id is not None: - return _result( - REJECT_LINEAGE_MISMATCH, - "ROOT_HAS_PARENT", - "sequence 0 checkpoint must not declare a parent", - ) - if sequence > 0 and parent_checkpoint_id is None: - return _result( - REJECT_LINEAGE_MISMATCH, - "PARENT_REQUIRED", - "non-root checkpoint must declare parent_checkpoint_id", - ) - - if previous_checkpoint is None: - if sequence > 0: - if parent_checkpoint_id not in set(known_parent_ids): - return _result( - REJECT_LINEAGE_MISMATCH, - "PARENT_NOT_FOUND", - f"parent checkpoint {parent_checkpoint_id} is not known", + if current_digest_valid: + sequence = checkpoint["sequence"] + checkpoint_id = checkpoint["checkpoint_id"] + parent_checkpoint_id = checkpoint["parent_checkpoint_id"] + if sequence > 0 and checkpoint_id == parent_checkpoint_id: + return _core._result( + _core.REJECT_LINEAGE_MISMATCH, + "CHECKPOINT_ID_REUSED", + "a checkpoint cannot reuse its parent checkpoint ID", + ) + + if previous_checkpoint is not None: + integrity_error = _core._previous_checkpoint_integrity_error( + previous_checkpoint + ) + if integrity_error is None: + previous_sequence = previous_checkpoint["sequence"] + previous_parent_id = previous_checkpoint["parent_checkpoint_id"] + if previous_sequence == 0 and previous_parent_id is not None: + return _core._result( + _core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "root previous checkpoint must not declare a parent", ) - return _result( - REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_REQUIRED", - "non-root resume requires the full previous checkpoint", - ) - else: - integrity_error = _previous_checkpoint_integrity_error(previous_checkpoint) - if integrity_error is not None: - reason_code, detail = integrity_error - return _result( - REJECT_LINEAGE_MISMATCH, - reason_code, - detail, - ) - if checkpoint["trajectory_id"] != previous_checkpoint["trajectory_id"]: - return _result( - REJECT_LINEAGE_MISMATCH, - "TRAJECTORY_CHANGED", - "checkpoint trajectory differs from previous checkpoint", - ) - if _parse_time(checkpoint["created_at"]) < _parse_time( - previous_checkpoint["created_at"] - ): - return _result( - REJECT_LINEAGE_MISMATCH, - "CREATION_TIME_REGRESSED", - "checkpoint creation time is earlier than its parent", - ) - if checkpoint["objective"] != previous_checkpoint["objective"]: - return _result( - REJECT_LINEAGE_MISMATCH, - "OBJECTIVE_CHANGED", - "objective changed within the same trajectory", - ) - for field in ("must", "must_not"): - previous_constraints = set(previous_checkpoint["constraints"][field]) - current_constraints = set(checkpoint["constraints"][field]) - lost_constraints = sorted(previous_constraints - current_constraints) - if lost_constraints: - return _result( - REJECT_LINEAGE_MISMATCH, - "CONSTRAINT_LOST", - f"{field} constraints disappeared: " + ", ".join(lost_constraints), + if previous_sequence > 0 and previous_parent_id is None: + return _core._result( + _core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "non-root previous checkpoint must declare a parent", ) - if parent_checkpoint_id != previous_checkpoint["checkpoint_id"]: - return _result( - REJECT_LINEAGE_MISMATCH, - "PARENT_MISMATCH", - "parent_checkpoint_id does not reference the previous checkpoint", - ) - if sequence != previous_checkpoint["sequence"] + 1: - return _result( - REJECT_LINEAGE_MISMATCH, - "SEQUENCE_MISMATCH", - "checkpoint sequence is not previous sequence + 1", - ) - - previous_rejected = _rejected_index( - previous_checkpoint["rejected_approaches"] - ) - current_rejected = _rejected_index(checkpoint["rejected_approaches"]) - missing_rejections = sorted(set(previous_rejected) - set(current_rejected)) - if missing_rejections: - return _result( - REJECT_LINEAGE_MISMATCH, - "REJECTED_APPROACH_LOST", - "rejected approaches disappeared: " + ", ".join(missing_rejections), - ) - changed_rejections = sorted( - approach_id - for approach_id, previous_row in previous_rejected.items() - if current_rejected[approach_id] != previous_row - ) - if changed_rejections: - return _result( - REJECT_LINEAGE_MISMATCH, - "REJECTED_APPROACH_CHANGED", - "rejected approaches changed: " + ", ".join(changed_rejections), - ) - - previous_completed = _verification_index( - previous_checkpoint["verification"]["completed"] - ) - current_completed = _verification_index( - checkpoint["verification"]["completed"] - ) - lost_completed = sorted(set(previous_completed) - set(current_completed)) - if lost_completed: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "COMPLETED_VERIFICATION_LOST", - "completed verification disappeared: " + ", ".join(lost_completed), - ) - for verification_id, previous_row in previous_completed.items(): - current_row = current_completed[verification_id] - previous_refs = set(previous_row["evidence_refs"]) - current_refs = set(current_row["evidence_refs"]) if ( - current_row["target"] != previous_row["target"] - or not previous_refs.issubset(current_refs) + previous_parent_id is not None + and previous_checkpoint["checkpoint_id"] == previous_parent_id ): - return _result( - REJECT_UNVERIFIED_COMPLETION, - "COMPLETED_VERIFICATION_CHANGED", - ( - "completed verification target changed or prior evidence " - f"was removed: {verification_id}" - ), - ) - - previous_pending = _verification_index( - previous_checkpoint["verification"]["pending"] - ) - current_pending = _verification_index( - checkpoint["verification"]["pending"] - ) - for verification_id, previous_row in previous_pending.items(): - current_row = current_pending.get(verification_id) - completed_row = current_completed.get(verification_id) - if current_row is None and completed_row is None: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "PENDING_VERIFICATION_LOST", - f"pending verification disappeared: {verification_id}", + return _core._result( + _core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "previous checkpoint cannot be its own parent", ) - successor = completed_row if completed_row is not None else current_row - if successor["target"] != previous_row["target"]: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "PENDING_VERIFICATION_CHANGED", - f"pending verification target changed: {verification_id}", - ) - - required_ids = set(checkpoint["verification"]["required"]) - completed_rows = checkpoint["verification"]["completed"] - pending_rows = checkpoint["verification"]["pending"] - completed_ids = [row["verification_id"] for row in completed_rows] - pending_ids = [row["verification_id"] for row in pending_rows] - - all_ids = completed_ids + pending_ids - if len(all_ids) != len(set(all_ids)): - return _result( - REJECT_UNVERIFIED_COMPLETION, - "VERIFICATION_ID_DUPLICATED", - "verification IDs must be unique across completed and pending lists", - ) - - represented_ids = set(all_ids) - if represented_ids != required_ids: - missing = sorted(required_ids - represented_ids) - unexpected = sorted(represented_ids - required_ids) - return _result( - REJECT_UNVERIFIED_COMPLETION, - "VERIFICATION_SET_MISMATCH", - f"missing={missing}; unexpected={unexpected}", - ) - - for row in completed_rows: - refs = row["evidence_refs"] - if not refs: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "COMPLETION_EVIDENCE_MISSING", - f"{row['verification_id']} has no evidence references", - ) - if any(str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) for ref in refs): - return _result( - REJECT_UNVERIFIED_COMPLETION, - "MEMORY_IS_NOT_VERIFICATION", - f"{row['verification_id']} relies on memory-only evidence", - ) - - next_action = checkpoint["next_action"] - if ( - next_action["action_class"] in {"merge", "deploy"} - and not next_action["requires_fresh_authority"] - ): - return _result( - REJECT_INVALID_AUTHORITY, - "FRESH_AUTHORITY_REQUIRED", - "merge and deploy actions require a fresh action authorization", - ) - - expected_workspace = checkpoint["workspace_state"] - identity_fields = ("repository", "working_directory") - for field in identity_fields: - if field not in current_workspace: - return _result( - RESTART_REQUIRED, - "CURRENT_WORKSPACE_FIELD_MISSING", - f"current workspace did not report {field}", - ) - if current_workspace[field] != expected_workspace[field]: - return _result( - RESTART_REQUIRED, - "WORKSPACE_IDENTITY_MISMATCH", - f"{field} differs from the checkpoint", - ) - - state_fields = ["base_ref", "head_sha"] - if "dirty_state_digest" in expected_workspace: - state_fields.append("dirty_state_digest") - - missing_state_fields = [ - field for field in state_fields if field not in current_workspace - ] - if missing_state_fields: - return _result( - REVALIDATE_WORKSPACE, - "CURRENT_WORKSPACE_FIELD_MISSING", - "current workspace did not report: " + ", ".join(missing_state_fields), - ) - - changed_fields = [ - field - for field in state_fields - if current_workspace[field] != expected_workspace[field] - ] - if changed_fields: - return _result( - REVALIDATE_WORKSPACE, - "WORKSPACE_STATE_CHANGED", - "changed workspace fields: " + ", ".join(changed_fields), - ) - return _result( - CONTINUE, - "CONTINUE_OK", - "checkpoint is valid, workspace matches, and authority remains context-only", + return _core.evaluate_resume( + checkpoint, + current_workspace=current_workspace, + previous_checkpoint=previous_checkpoint, + seen_checkpoint_ids=seen_checkpoint_ids, + known_parent_ids=known_parent_ids, ) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py new file mode 100644 index 00000000..c640fecc --- /dev/null +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py @@ -0,0 +1,470 @@ +from __future__ import annotations + +import copy +import hashlib +import json +from datetime import datetime +from pathlib import Path +from typing import Any, Iterable, Mapping + +from jsonschema import Draft202012Validator, FormatChecker + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +SCHEMA_PATH = ROOT / "schema" / "ci-operational-checkpoint-v0.1.schema.json" + +CONTINUE = "CONTINUE" +REVALIDATE_WORKSPACE = "REVALIDATE_WORKSPACE" +RESTART_REQUIRED = "RESTART_REQUIRED" +IDEMPOTENT_REPLAY = "IDEMPOTENT_REPLAY" +REJECT_LINEAGE_MISMATCH = "REJECT_LINEAGE_MISMATCH" +REJECT_UNVERIFIED_COMPLETION = "REJECT_UNVERIFIED_COMPLETION" +REJECT_INVALID_AUTHORITY = "REJECT_INVALID_AUTHORITY" + +MEMORY_ONLY_PREFIXES = ("memory://", "agent-memory://", "summary://") + + +def load_schema() -> dict[str, Any]: + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + if not isinstance(schema, dict): + raise ValueError("checkpoint schema root must be an object") + Draft202012Validator.check_schema(schema) + return schema + + +def _canonical_bytes(checkpoint: Mapping[str, Any]) -> bytes: + value = copy.deepcopy(dict(checkpoint)) + value.pop("checkpoint_digest", None) + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8") + + +def computed_digest(checkpoint: Mapping[str, Any]) -> str: + return hashlib.sha256(_canonical_bytes(checkpoint)).hexdigest() + + +def with_computed_digest(checkpoint: Mapping[str, Any]) -> dict[str, Any]: + result = copy.deepcopy(dict(checkpoint)) + result.setdefault( + "checkpoint_digest", + { + "algorithm": "sha256", + "canonicalization": "json-sort-keys-utf8-v1", + "value": "0" * 64, + }, + ) + result["checkpoint_digest"]["value"] = computed_digest(result) + return result + + +def _schema_errors(checkpoint: Mapping[str, Any]) -> list[Any]: + validator = Draft202012Validator( + load_schema(), + format_checker=FormatChecker(), + ) + return sorted( + validator.iter_errors(dict(checkpoint)), + key=lambda error: [str(part) for part in error.absolute_path], + ) + + +def _result(outcome: str, reason_code: str, detail: str) -> dict[str, str]: + return { + "outcome": outcome, + "reason_code": reason_code, + "detail": detail, + } + + +def _verification_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: + return {str(row["verification_id"]): row for row in rows} + + +def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: + return {str(row["approach_id"]): row for row in rows} + + +def _parse_time(value: str) -> datetime: + normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value + return datetime.fromisoformat(normalized) + + +def _duplicate_ids(rows: Iterable[Mapping[str, Any]], field: str) -> list[str]: + seen: set[str] = set() + duplicates: set[str] = set() + for row in rows: + value = str(row[field]) + if value in seen: + duplicates.add(value) + seen.add(value) + return sorted(duplicates) + + +def _previous_checkpoint_integrity_error( + previous_checkpoint: Mapping[str, Any], +) -> tuple[str, str] | None: + errors = _schema_errors(previous_checkpoint) + if errors: + first = errors[0] + path = "/".join(str(part) for part in first.absolute_path) or "" + return ( + "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", + f"{path}: {first.message}", + ) + + expected_digest = previous_checkpoint["checkpoint_digest"]["value"] + actual_digest = computed_digest(previous_checkpoint) + if expected_digest != actual_digest: + return ( + "PREVIOUS_CHECKPOINT_DIGEST_MISMATCH", + f"expected {expected_digest}, computed {actual_digest}", + ) + + duplicate_approaches = _duplicate_ids( + previous_checkpoint["rejected_approaches"], + "approach_id", + ) + if duplicate_approaches: + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + ) + + previous_verification = previous_checkpoint["verification"] + previous_completed = previous_verification["completed"] + previous_pending = previous_verification["pending"] + verification_ids = [ + row["verification_id"] for row in previous_completed + previous_pending + ] + if len(verification_ids) != len(set(verification_ids)): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "verification IDs are duplicated across completed and pending lists", + ) + if set(verification_ids) != set(previous_verification["required"]): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "required verification IDs do not match completed and pending rows", + ) + for row in previous_completed: + if any( + str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) + for ref in row["evidence_refs"] + ): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + f"{row['verification_id']} relies on memory-only evidence", + ) + return None + + +def evaluate_resume( + checkpoint: Mapping[str, Any], + *, + current_workspace: Mapping[str, Any], + previous_checkpoint: Mapping[str, Any] | None = None, + seen_checkpoint_ids: Iterable[str] = (), + known_parent_ids: Iterable[str] = (), +) -> dict[str, str]: + errors = _schema_errors(checkpoint) + if errors: + first = errors[0] + path = "/".join(str(part) for part in first.absolute_path) or "" + if path == "authority": + return _result( + REJECT_INVALID_AUTHORITY, + "AUTHORITY_NOT_CONTEXT_ONLY", + first.message, + ) + return _result( + RESTART_REQUIRED, + "SCHEMA_INVALID", + f"{path}: {first.message}", + ) + + expected_digest = checkpoint["checkpoint_digest"]["value"] + actual_digest = computed_digest(checkpoint) + if expected_digest != actual_digest: + return _result( + RESTART_REQUIRED, + "DIGEST_MISMATCH", + f"expected {expected_digest}, computed {actual_digest}", + ) + + checkpoint_id = checkpoint["checkpoint_id"] + if checkpoint_id in set(seen_checkpoint_ids): + return _result( + IDEMPOTENT_REPLAY, + "CHECKPOINT_ALREADY_CONSUMED", + f"checkpoint {checkpoint_id} was already consumed", + ) + + duplicate_approaches = _duplicate_ids( + checkpoint["rejected_approaches"], + "approach_id", + ) + if duplicate_approaches: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_ID_DUPLICATED", + "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + ) + + sequence = checkpoint["sequence"] + parent_checkpoint_id = checkpoint["parent_checkpoint_id"] + if sequence == 0 and parent_checkpoint_id is not None: + return _result( + REJECT_LINEAGE_MISMATCH, + "ROOT_HAS_PARENT", + "sequence 0 checkpoint must not declare a parent", + ) + if sequence > 0 and parent_checkpoint_id is None: + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_REQUIRED", + "non-root checkpoint must declare parent_checkpoint_id", + ) + + if previous_checkpoint is None: + if sequence > 0: + if parent_checkpoint_id not in set(known_parent_ids): + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_NOT_FOUND", + f"parent checkpoint {parent_checkpoint_id} is not known", + ) + return _result( + REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_REQUIRED", + "non-root resume requires the full previous checkpoint", + ) + else: + integrity_error = _previous_checkpoint_integrity_error(previous_checkpoint) + if integrity_error is not None: + reason_code, detail = integrity_error + return _result( + REJECT_LINEAGE_MISMATCH, + reason_code, + detail, + ) + if checkpoint["trajectory_id"] != previous_checkpoint["trajectory_id"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "TRAJECTORY_CHANGED", + "checkpoint trajectory differs from previous checkpoint", + ) + if _parse_time(checkpoint["created_at"]) < _parse_time( + previous_checkpoint["created_at"] + ): + return _result( + REJECT_LINEAGE_MISMATCH, + "CREATION_TIME_REGRESSED", + "checkpoint creation time is earlier than its parent", + ) + if checkpoint["objective"] != previous_checkpoint["objective"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "OBJECTIVE_CHANGED", + "objective changed within the same trajectory", + ) + for field in ("must", "must_not"): + previous_constraints = set(previous_checkpoint["constraints"][field]) + current_constraints = set(checkpoint["constraints"][field]) + lost_constraints = sorted(previous_constraints - current_constraints) + if lost_constraints: + return _result( + REJECT_LINEAGE_MISMATCH, + "CONSTRAINT_LOST", + f"{field} constraints disappeared: " + ", ".join(lost_constraints), + ) + if parent_checkpoint_id != previous_checkpoint["checkpoint_id"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_MISMATCH", + "parent_checkpoint_id does not reference the previous checkpoint", + ) + if sequence != previous_checkpoint["sequence"] + 1: + return _result( + REJECT_LINEAGE_MISMATCH, + "SEQUENCE_MISMATCH", + "checkpoint sequence is not previous sequence + 1", + ) + + previous_rejected = _rejected_index( + previous_checkpoint["rejected_approaches"] + ) + current_rejected = _rejected_index(checkpoint["rejected_approaches"]) + missing_rejections = sorted(set(previous_rejected) - set(current_rejected)) + if missing_rejections: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_LOST", + "rejected approaches disappeared: " + ", ".join(missing_rejections), + ) + changed_rejections = sorted( + approach_id + for approach_id, previous_row in previous_rejected.items() + if current_rejected[approach_id] != previous_row + ) + if changed_rejections: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_CHANGED", + "rejected approaches changed: " + ", ".join(changed_rejections), + ) + + previous_completed = _verification_index( + previous_checkpoint["verification"]["completed"] + ) + current_completed = _verification_index( + checkpoint["verification"]["completed"] + ) + lost_completed = sorted(set(previous_completed) - set(current_completed)) + if lost_completed: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETED_VERIFICATION_LOST", + "completed verification disappeared: " + ", ".join(lost_completed), + ) + for verification_id, previous_row in previous_completed.items(): + current_row = current_completed[verification_id] + previous_refs = set(previous_row["evidence_refs"]) + current_refs = set(current_row["evidence_refs"]) + if ( + current_row["target"] != previous_row["target"] + or not previous_refs.issubset(current_refs) + ): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETED_VERIFICATION_CHANGED", + ( + "completed verification target changed or prior evidence " + f"was removed: {verification_id}" + ), + ) + + previous_pending = _verification_index( + previous_checkpoint["verification"]["pending"] + ) + current_pending = _verification_index( + checkpoint["verification"]["pending"] + ) + for verification_id, previous_row in previous_pending.items(): + current_row = current_pending.get(verification_id) + completed_row = current_completed.get(verification_id) + if current_row is None and completed_row is None: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "PENDING_VERIFICATION_LOST", + f"pending verification disappeared: {verification_id}", + ) + successor = completed_row if completed_row is not None else current_row + if successor["target"] != previous_row["target"]: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "PENDING_VERIFICATION_CHANGED", + f"pending verification target changed: {verification_id}", + ) + + required_ids = set(checkpoint["verification"]["required"]) + completed_rows = checkpoint["verification"]["completed"] + pending_rows = checkpoint["verification"]["pending"] + completed_ids = [row["verification_id"] for row in completed_rows] + pending_ids = [row["verification_id"] for row in pending_rows] + + all_ids = completed_ids + pending_ids + if len(all_ids) != len(set(all_ids)): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "VERIFICATION_ID_DUPLICATED", + "verification IDs must be unique across completed and pending lists", + ) + + represented_ids = set(all_ids) + if represented_ids != required_ids: + missing = sorted(required_ids - represented_ids) + unexpected = sorted(represented_ids - required_ids) + return _result( + REJECT_UNVERIFIED_COMPLETION, + "VERIFICATION_SET_MISMATCH", + f"missing={missing}; unexpected={unexpected}", + ) + + for row in completed_rows: + refs = row["evidence_refs"] + if not refs: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETION_EVIDENCE_MISSING", + f"{row['verification_id']} has no evidence references", + ) + if any(str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) for ref in refs): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "MEMORY_IS_NOT_VERIFICATION", + f"{row['verification_id']} relies on memory-only evidence", + ) + + next_action = checkpoint["next_action"] + if ( + next_action["action_class"] in {"merge", "deploy"} + and not next_action["requires_fresh_authority"] + ): + return _result( + REJECT_INVALID_AUTHORITY, + "FRESH_AUTHORITY_REQUIRED", + "merge and deploy actions require a fresh action authorization", + ) + + expected_workspace = checkpoint["workspace_state"] + identity_fields = ("repository", "working_directory") + for field in identity_fields: + if field not in current_workspace: + return _result( + RESTART_REQUIRED, + "CURRENT_WORKSPACE_FIELD_MISSING", + f"current workspace did not report {field}", + ) + if current_workspace[field] != expected_workspace[field]: + return _result( + RESTART_REQUIRED, + "WORKSPACE_IDENTITY_MISMATCH", + f"{field} differs from the checkpoint", + ) + + state_fields = ["base_ref", "head_sha"] + if "dirty_state_digest" in expected_workspace: + state_fields.append("dirty_state_digest") + + missing_state_fields = [ + field for field in state_fields if field not in current_workspace + ] + if missing_state_fields: + return _result( + REVALIDATE_WORKSPACE, + "CURRENT_WORKSPACE_FIELD_MISSING", + "current workspace did not report: " + ", ".join(missing_state_fields), + ) + + changed_fields = [ + field + for field in state_fields + if current_workspace[field] != expected_workspace[field] + ] + if changed_fields: + return _result( + REVALIDATE_WORKSPACE, + "WORKSPACE_STATE_CHANGED", + "changed workspace fields: " + ", ".join(changed_fields), + ) + + return _result( + CONTINUE, + "CONTINUE_OK", + "checkpoint is valid, workspace matches, and authority remains context-only", + ) diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py b/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py new file mode 100644 index 00000000..c5b60d6d --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import copy +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from ci_operational_checkpoint_reference import ( # noqa: E402 + REJECT_LINEAGE_MISMATCH, + evaluate_resume, + with_computed_digest, +) + +EXAMPLE = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE.read_text(encoding="utf-8")) + + +def child_of(parent): + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:issue-218:link-invariant" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = parent["sequence"] + 1 + child["created_at"] = "2026-07-02T12:30:00Z" + return with_computed_digest(child) + + +def evaluate(child, parent): + return evaluate_resume( + child, + current_workspace=copy.deepcopy(child["workspace_state"]), + previous_checkpoint=parent, + ) + + +class CheckpointLinkInvariantTest(unittest.TestCase): + def assert_reason(self, result, reason_code): + self.assertEqual( + (result["outcome"], result["reason_code"]), + (REJECT_LINEAGE_MISMATCH, reason_code), + ) + + def test_root_parent_cannot_declare_its_own_parent(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent["parent_checkpoint_id"] = "checkpoint:unexpected-parent" + invalid_parent = with_computed_digest(invalid_parent) + self.assert_reason( + evaluate(child, invalid_parent), + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + ) + + def test_non_root_parent_requires_parent_id(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent["sequence"] = 1 + invalid_parent["parent_checkpoint_id"] = None + invalid_parent = with_computed_digest(invalid_parent) + self.assert_reason( + evaluate(child, invalid_parent), + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + ) + + def test_child_cannot_reuse_parent_checkpoint_id(self): + parent = load_example() + child = child_of(parent) + child["checkpoint_id"] = parent["checkpoint_id"] + child = with_computed_digest(child) + self.assert_reason(evaluate(child, parent), "CHECKPOINT_ID_REUSED") + + +if __name__ == "__main__": + unittest.main() From 14ff0944f4d7f6a764aa8860695db1f89aa1a4bf Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:35:56 +0300 Subject: [PATCH 25/40] docs: register checkpoint link invariant code --- standards/agent-continuity/CI-CHECKPOINT-CODES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/agent-continuity/CI-CHECKPOINT-CODES.md b/standards/agent-continuity/CI-CHECKPOINT-CODES.md index 6187441a..e241ffb2 100644 --- a/standards/agent-continuity/CI-CHECKPOINT-CODES.md +++ b/standards/agent-continuity/CI-CHECKPOINT-CODES.md @@ -10,7 +10,7 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | `REVALIDATE_WORKSPACE` | Repository identity matches, but required workspace evidence is missing or base, head, or dirty-state evidence changed. | | `RESTART_REQUIRED` | The checkpoint cannot safely resume in the current workspace or failed schema/digest validation. | | `IDEMPOTENT_REPLAY` | The same checkpoint was already consumed; no duplicate work should be created. | -| `REJECT_LINEAGE_MISMATCH` | Parent, time, objective, constraints, sequence, trajectory, rejected-approach continuity, or parent integrity is inconsistent. | +| `REJECT_LINEAGE_MISMATCH` | Parent, checkpoint identity, time, objective, constraints, sequence, trajectory, rejected-approach continuity, or parent integrity is inconsistent. | | `REJECT_UNVERIFIED_COMPLETION` | Verification was completed without durable evidence, disappeared, or conflicts with prior proof or pending-work lineage. | | `REJECT_INVALID_AUTHORITY` | Continuity material attempted to carry action authority or bypass fresh authorization. | @@ -22,7 +22,7 @@ Consumers MUST branch on `outcome` and `reason_code`, not on `detail`. | `REVALIDATE_WORKSPACE` | `WORKSPACE_STATE_CHANGED`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `RESTART_REQUIRED` | `SCHEMA_INVALID`, `DIGEST_MISMATCH`, `WORKSPACE_IDENTITY_MISMATCH`, `CURRENT_WORKSPACE_FIELD_MISSING` | | `IDEMPOTENT_REPLAY` | `CHECKPOINT_ALREADY_CONSUMED` | -| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PREVIOUS_CHECKPOINT_REQUIRED`, `PREVIOUS_CHECKPOINT_SCHEMA_INVALID`, `PREVIOUS_CHECKPOINT_DIGEST_MISMATCH`, `PREVIOUS_CHECKPOINT_SEMANTIC_INVALID`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `CREATION_TIME_REGRESSED`, `OBJECTIVE_CHANGED`, `CONSTRAINT_LOST`, `REJECTED_APPROACH_ID_DUPLICATED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | +| `REJECT_LINEAGE_MISMATCH` | `ROOT_HAS_PARENT`, `PARENT_REQUIRED`, `PARENT_NOT_FOUND`, `PREVIOUS_CHECKPOINT_REQUIRED`, `PREVIOUS_CHECKPOINT_SCHEMA_INVALID`, `PREVIOUS_CHECKPOINT_DIGEST_MISMATCH`, `PREVIOUS_CHECKPOINT_SEMANTIC_INVALID`, `CHECKPOINT_ID_REUSED`, `PARENT_MISMATCH`, `SEQUENCE_MISMATCH`, `TRAJECTORY_CHANGED`, `CREATION_TIME_REGRESSED`, `OBJECTIVE_CHANGED`, `CONSTRAINT_LOST`, `REJECTED_APPROACH_ID_DUPLICATED`, `REJECTED_APPROACH_LOST`, `REJECTED_APPROACH_CHANGED` | | `REJECT_UNVERIFIED_COMPLETION` | `VERIFICATION_ID_DUPLICATED`, `VERIFICATION_SET_MISMATCH`, `COMPLETION_EVIDENCE_MISSING`, `MEMORY_IS_NOT_VERIFICATION`, `COMPLETED_VERIFICATION_LOST`, `COMPLETED_VERIFICATION_CHANGED`, `PENDING_VERIFICATION_LOST`, `PENDING_VERIFICATION_CHANGED` | | `REJECT_INVALID_AUTHORITY` | `AUTHORITY_NOT_CONTEXT_ONLY`, `FRESH_AUTHORITY_REQUIRED` | From 08fdb3dae56f55d7740786b8bdd38caa40c671e3 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:43:32 +0300 Subject: [PATCH 26/40] fix: reject parent authority bypass --- .../conformance/ci_operational_checkpoint_reference.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index 9d2c367e..f33d700a 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -61,6 +61,16 @@ def evaluate_resume( "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", "previous checkpoint cannot be its own parent", ) + previous_action = previous_checkpoint["next_action"] + if ( + previous_action["action_class"] in {"merge", "deploy"} + and not previous_action["requires_fresh_authority"] + ): + return _core._result( + _core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "parent merge or deploy intent requires fresh authority", + ) return _core.evaluate_resume( checkpoint, From 37911355f3ee478e9f99c29fd81fba5c022a4243 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:44:33 +0300 Subject: [PATCH 27/40] test: reject parent authority bypass --- .../test_ci_checkpoint_link_invariants.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py b/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py index c5b60d6d..3f6ecaab 100644 --- a/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_link_invariants.py @@ -78,6 +78,21 @@ def test_child_cannot_reuse_parent_checkpoint_id(self): child = with_computed_digest(child) self.assert_reason(evaluate(child, parent), "CHECKPOINT_ID_REUSED") + def test_parent_cannot_carry_merge_authority_without_revalidation(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent["next_action"] = { + "description": "Merge without a fresh authorization.", + "action_class": "merge", + "requires_fresh_authority": False, + } + invalid_parent = with_computed_digest(invalid_parent) + self.assert_reason( + evaluate(child, invalid_parent), + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + ) + if __name__ == "__main__": unittest.main() From 0a5fef1c27faa9bbf78cd06851466e3ad0a1474d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:15:20 +0300 Subject: [PATCH 28/40] fix: centralize checkpoint integrity guards in core --- ...i_operational_checkpoint_reference_core.py | 488 +++--------------- ...i_operational_checkpoint_reference_impl.py | 470 +++++++++++++++++ .../test_ci_checkpoint_core_integrity.py | 84 +++ 3 files changed, 624 insertions(+), 418 deletions(-) create mode 100644 standards/agent-continuity/conformance/ci_operational_checkpoint_reference_impl.py create mode 100644 standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py index c640fecc..3c62520c 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py @@ -1,165 +1,78 @@ from __future__ import annotations -import copy -import hashlib -import json -from datetime import datetime -from pathlib import Path +from functools import lru_cache from typing import Any, Iterable, Mapping -from jsonschema import Draft202012Validator, FormatChecker +import ci_operational_checkpoint_reference_impl as _impl +from ci_operational_checkpoint_reference_impl import * # noqa: F401,F403 -HERE = Path(__file__).resolve().parent -ROOT = HERE.parent -SCHEMA_PATH = ROOT / "schema" / "ci-operational-checkpoint-v0.1.schema.json" - -CONTINUE = "CONTINUE" -REVALIDATE_WORKSPACE = "REVALIDATE_WORKSPACE" -RESTART_REQUIRED = "RESTART_REQUIRED" -IDEMPOTENT_REPLAY = "IDEMPOTENT_REPLAY" -REJECT_LINEAGE_MISMATCH = "REJECT_LINEAGE_MISMATCH" -REJECT_UNVERIFIED_COMPLETION = "REJECT_UNVERIFIED_COMPLETION" -REJECT_INVALID_AUTHORITY = "REJECT_INVALID_AUTHORITY" - -MEMORY_ONLY_PREFIXES = ("memory://", "agent-memory://", "summary://") +# Preserve private helpers used by the compatibility wrapper and conformance tests. +_result = _impl._result +_schema_errors = _impl._schema_errors +@lru_cache(maxsize=1) def load_schema() -> dict[str, Any]: - schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) - if not isinstance(schema, dict): - raise ValueError("checkpoint schema root must be an object") - Draft202012Validator.check_schema(schema) - return schema - - -def _canonical_bytes(checkpoint: Mapping[str, Any]) -> bytes: - value = copy.deepcopy(dict(checkpoint)) - value.pop("checkpoint_digest", None) - return json.dumps( - value, - ensure_ascii=False, - sort_keys=True, - separators=(",", ":"), - allow_nan=False, - ).encode("utf-8") - - -def computed_digest(checkpoint: Mapping[str, Any]) -> str: - return hashlib.sha256(_canonical_bytes(checkpoint)).hexdigest() - - -def with_computed_digest(checkpoint: Mapping[str, Any]) -> dict[str, Any]: - result = copy.deepcopy(dict(checkpoint)) - result.setdefault( - "checkpoint_digest", - { - "algorithm": "sha256", - "canonicalization": "json-sort-keys-utf8-v1", - "value": "0" * 64, - }, - ) - result["checkpoint_digest"]["value"] = computed_digest(result) - return result - - -def _schema_errors(checkpoint: Mapping[str, Any]) -> list[Any]: - validator = Draft202012Validator( - load_schema(), - format_checker=FormatChecker(), - ) - return sorted( - validator.iter_errors(dict(checkpoint)), - key=lambda error: [str(part) for part in error.absolute_path], - ) - + """Load and validate the published schema once per process.""" -def _result(outcome: str, reason_code: str, detail: str) -> dict[str, str]: - return { - "outcome": outcome, - "reason_code": reason_code, - "detail": detail, - } + return _impl.load_schema() -def _verification_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: - return {str(row["verification_id"]): row for row in rows} - - -def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: - return {str(row["approach_id"]): row for row in rows} - - -def _parse_time(value: str) -> datetime: - normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value - return datetime.fromisoformat(normalized) - - -def _duplicate_ids(rows: Iterable[Mapping[str, Any]], field: str) -> list[str]: - seen: set[str] = set() - duplicates: set[str] = set() - for row in rows: - value = str(row[field]) - if value in seen: - duplicates.add(value) - seen.add(value) - return sorted(duplicates) +# Make every validator path in the implementation share the cached schema. +_impl.load_schema = load_schema def _previous_checkpoint_integrity_error( previous_checkpoint: Mapping[str, Any], ) -> tuple[str, str] | None: - errors = _schema_errors(previous_checkpoint) - if errors: - first = errors[0] - path = "/".join(str(part) for part in first.absolute_path) or "" + """Validate a parent checkpoint before it can contribute lineage state.""" + + verification = previous_checkpoint.get("verification") + if isinstance(verification, Mapping): + completed = verification.get("completed") + if isinstance(completed, list): + for row in completed: + if not isinstance(row, Mapping): + continue + refs = row.get("evidence_refs") + if isinstance(refs, list) and not refs: + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + f"{row.get('verification_id', '')} has no evidence references", + ) + + error = _impl._previous_checkpoint_integrity_error(previous_checkpoint) + if error is not None: + return error + + sequence = previous_checkpoint["sequence"] + parent_id = previous_checkpoint["parent_checkpoint_id"] + checkpoint_id = previous_checkpoint["checkpoint_id"] + if sequence == 0 and parent_id is not None: return ( - "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", - f"{path}: {first.message}", - ) - - expected_digest = previous_checkpoint["checkpoint_digest"]["value"] - actual_digest = computed_digest(previous_checkpoint) - if expected_digest != actual_digest: - return ( - "PREVIOUS_CHECKPOINT_DIGEST_MISMATCH", - f"expected {expected_digest}, computed {actual_digest}", + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "root previous checkpoint must not declare a parent", ) - - duplicate_approaches = _duplicate_ids( - previous_checkpoint["rejected_approaches"], - "approach_id", - ) - if duplicate_approaches: + if sequence > 0 and parent_id is None: return ( "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + "non-root previous checkpoint must declare a parent", ) - - previous_verification = previous_checkpoint["verification"] - previous_completed = previous_verification["completed"] - previous_pending = previous_verification["pending"] - verification_ids = [ - row["verification_id"] for row in previous_completed + previous_pending - ] - if len(verification_ids) != len(set(verification_ids)): + if parent_id is not None and checkpoint_id == parent_id: return ( "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "verification IDs are duplicated across completed and pending lists", + "previous checkpoint cannot be its own parent", ) - if set(verification_ids) != set(previous_verification["required"]): + + next_action = previous_checkpoint["next_action"] + if ( + next_action["action_class"] in {"merge", "deploy"} + and not next_action["requires_fresh_authority"] + ): return ( "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "required verification IDs do not match completed and pending rows", + "parent merge or deploy intent requires fresh authority", ) - for row in previous_completed: - if any( - str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) - for ref in row["evidence_refs"] - ): - return ( - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - f"{row['verification_id']} relies on memory-only evidence", - ) return None @@ -171,300 +84,39 @@ def evaluate_resume( seen_checkpoint_ids: Iterable[str] = (), known_parent_ids: Iterable[str] = (), ) -> dict[str, str]: + """Apply canonical fail-closed guards, then delegate to the implementation.""" + errors = _schema_errors(checkpoint) if errors: - first = errors[0] - path = "/".join(str(part) for part in first.absolute_path) or "" - if path == "authority": + authority_error = next( + ( + error + for error in errors + if list(error.absolute_path) == ["authority"] + ), + None, + ) + if authority_error is not None: return _result( REJECT_INVALID_AUTHORITY, "AUTHORITY_NOT_CONTEXT_ONLY", - first.message, + authority_error.message, ) - return _result( - RESTART_REQUIRED, - "SCHEMA_INVALID", - f"{path}: {first.message}", - ) - - expected_digest = checkpoint["checkpoint_digest"]["value"] - actual_digest = computed_digest(checkpoint) - if expected_digest != actual_digest: - return _result( - RESTART_REQUIRED, - "DIGEST_MISMATCH", - f"expected {expected_digest}, computed {actual_digest}", - ) - checkpoint_id = checkpoint["checkpoint_id"] - if checkpoint_id in set(seen_checkpoint_ids): - return _result( - IDEMPOTENT_REPLAY, - "CHECKPOINT_ALREADY_CONSUMED", - f"checkpoint {checkpoint_id} was already consumed", - ) - - duplicate_approaches = _duplicate_ids( - checkpoint["rejected_approaches"], - "approach_id", - ) - if duplicate_approaches: - return _result( - REJECT_LINEAGE_MISMATCH, - "REJECTED_APPROACH_ID_DUPLICATED", - "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), - ) - - sequence = checkpoint["sequence"] - parent_checkpoint_id = checkpoint["parent_checkpoint_id"] - if sequence == 0 and parent_checkpoint_id is not None: - return _result( - REJECT_LINEAGE_MISMATCH, - "ROOT_HAS_PARENT", - "sequence 0 checkpoint must not declare a parent", - ) - if sequence > 0 and parent_checkpoint_id is None: - return _result( - REJECT_LINEAGE_MISMATCH, - "PARENT_REQUIRED", - "non-root checkpoint must declare parent_checkpoint_id", - ) - - if previous_checkpoint is None: - if sequence > 0: - if parent_checkpoint_id not in set(known_parent_ids): - return _result( - REJECT_LINEAGE_MISMATCH, - "PARENT_NOT_FOUND", - f"parent checkpoint {parent_checkpoint_id} is not known", - ) - return _result( - REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_REQUIRED", - "non-root resume requires the full previous checkpoint", - ) - else: - integrity_error = _previous_checkpoint_integrity_error(previous_checkpoint) - if integrity_error is not None: - reason_code, detail = integrity_error + if previous_checkpoint is not None: + error = _previous_checkpoint_integrity_error(previous_checkpoint) + if error is not None: + reason_code, detail = error return _result( REJECT_LINEAGE_MISMATCH, reason_code, detail, ) - if checkpoint["trajectory_id"] != previous_checkpoint["trajectory_id"]: - return _result( - REJECT_LINEAGE_MISMATCH, - "TRAJECTORY_CHANGED", - "checkpoint trajectory differs from previous checkpoint", - ) - if _parse_time(checkpoint["created_at"]) < _parse_time( - previous_checkpoint["created_at"] - ): - return _result( - REJECT_LINEAGE_MISMATCH, - "CREATION_TIME_REGRESSED", - "checkpoint creation time is earlier than its parent", - ) - if checkpoint["objective"] != previous_checkpoint["objective"]: - return _result( - REJECT_LINEAGE_MISMATCH, - "OBJECTIVE_CHANGED", - "objective changed within the same trajectory", - ) - for field in ("must", "must_not"): - previous_constraints = set(previous_checkpoint["constraints"][field]) - current_constraints = set(checkpoint["constraints"][field]) - lost_constraints = sorted(previous_constraints - current_constraints) - if lost_constraints: - return _result( - REJECT_LINEAGE_MISMATCH, - "CONSTRAINT_LOST", - f"{field} constraints disappeared: " + ", ".join(lost_constraints), - ) - if parent_checkpoint_id != previous_checkpoint["checkpoint_id"]: - return _result( - REJECT_LINEAGE_MISMATCH, - "PARENT_MISMATCH", - "parent_checkpoint_id does not reference the previous checkpoint", - ) - if sequence != previous_checkpoint["sequence"] + 1: - return _result( - REJECT_LINEAGE_MISMATCH, - "SEQUENCE_MISMATCH", - "checkpoint sequence is not previous sequence + 1", - ) - - previous_rejected = _rejected_index( - previous_checkpoint["rejected_approaches"] - ) - current_rejected = _rejected_index(checkpoint["rejected_approaches"]) - missing_rejections = sorted(set(previous_rejected) - set(current_rejected)) - if missing_rejections: - return _result( - REJECT_LINEAGE_MISMATCH, - "REJECTED_APPROACH_LOST", - "rejected approaches disappeared: " + ", ".join(missing_rejections), - ) - changed_rejections = sorted( - approach_id - for approach_id, previous_row in previous_rejected.items() - if current_rejected[approach_id] != previous_row - ) - if changed_rejections: - return _result( - REJECT_LINEAGE_MISMATCH, - "REJECTED_APPROACH_CHANGED", - "rejected approaches changed: " + ", ".join(changed_rejections), - ) - - previous_completed = _verification_index( - previous_checkpoint["verification"]["completed"] - ) - current_completed = _verification_index( - checkpoint["verification"]["completed"] - ) - lost_completed = sorted(set(previous_completed) - set(current_completed)) - if lost_completed: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "COMPLETED_VERIFICATION_LOST", - "completed verification disappeared: " + ", ".join(lost_completed), - ) - for verification_id, previous_row in previous_completed.items(): - current_row = current_completed[verification_id] - previous_refs = set(previous_row["evidence_refs"]) - current_refs = set(current_row["evidence_refs"]) - if ( - current_row["target"] != previous_row["target"] - or not previous_refs.issubset(current_refs) - ): - return _result( - REJECT_UNVERIFIED_COMPLETION, - "COMPLETED_VERIFICATION_CHANGED", - ( - "completed verification target changed or prior evidence " - f"was removed: {verification_id}" - ), - ) - - previous_pending = _verification_index( - previous_checkpoint["verification"]["pending"] - ) - current_pending = _verification_index( - checkpoint["verification"]["pending"] - ) - for verification_id, previous_row in previous_pending.items(): - current_row = current_pending.get(verification_id) - completed_row = current_completed.get(verification_id) - if current_row is None and completed_row is None: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "PENDING_VERIFICATION_LOST", - f"pending verification disappeared: {verification_id}", - ) - successor = completed_row if completed_row is not None else current_row - if successor["target"] != previous_row["target"]: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "PENDING_VERIFICATION_CHANGED", - f"pending verification target changed: {verification_id}", - ) - - required_ids = set(checkpoint["verification"]["required"]) - completed_rows = checkpoint["verification"]["completed"] - pending_rows = checkpoint["verification"]["pending"] - completed_ids = [row["verification_id"] for row in completed_rows] - pending_ids = [row["verification_id"] for row in pending_rows] - - all_ids = completed_ids + pending_ids - if len(all_ids) != len(set(all_ids)): - return _result( - REJECT_UNVERIFIED_COMPLETION, - "VERIFICATION_ID_DUPLICATED", - "verification IDs must be unique across completed and pending lists", - ) - - represented_ids = set(all_ids) - if represented_ids != required_ids: - missing = sorted(required_ids - represented_ids) - unexpected = sorted(represented_ids - required_ids) - return _result( - REJECT_UNVERIFIED_COMPLETION, - "VERIFICATION_SET_MISMATCH", - f"missing={missing}; unexpected={unexpected}", - ) - - for row in completed_rows: - refs = row["evidence_refs"] - if not refs: - return _result( - REJECT_UNVERIFIED_COMPLETION, - "COMPLETION_EVIDENCE_MISSING", - f"{row['verification_id']} has no evidence references", - ) - if any(str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) for ref in refs): - return _result( - REJECT_UNVERIFIED_COMPLETION, - "MEMORY_IS_NOT_VERIFICATION", - f"{row['verification_id']} relies on memory-only evidence", - ) - - next_action = checkpoint["next_action"] - if ( - next_action["action_class"] in {"merge", "deploy"} - and not next_action["requires_fresh_authority"] - ): - return _result( - REJECT_INVALID_AUTHORITY, - "FRESH_AUTHORITY_REQUIRED", - "merge and deploy actions require a fresh action authorization", - ) - - expected_workspace = checkpoint["workspace_state"] - identity_fields = ("repository", "working_directory") - for field in identity_fields: - if field not in current_workspace: - return _result( - RESTART_REQUIRED, - "CURRENT_WORKSPACE_FIELD_MISSING", - f"current workspace did not report {field}", - ) - if current_workspace[field] != expected_workspace[field]: - return _result( - RESTART_REQUIRED, - "WORKSPACE_IDENTITY_MISMATCH", - f"{field} differs from the checkpoint", - ) - - state_fields = ["base_ref", "head_sha"] - if "dirty_state_digest" in expected_workspace: - state_fields.append("dirty_state_digest") - - missing_state_fields = [ - field for field in state_fields if field not in current_workspace - ] - if missing_state_fields: - return _result( - REVALIDATE_WORKSPACE, - "CURRENT_WORKSPACE_FIELD_MISSING", - "current workspace did not report: " + ", ".join(missing_state_fields), - ) - - changed_fields = [ - field - for field in state_fields - if current_workspace[field] != expected_workspace[field] - ] - if changed_fields: - return _result( - REVALIDATE_WORKSPACE, - "WORKSPACE_STATE_CHANGED", - "changed workspace fields: " + ", ".join(changed_fields), - ) - return _result( - CONTINUE, - "CONTINUE_OK", - "checkpoint is valid, workspace matches, and authority remains context-only", + return _impl.evaluate_resume( + checkpoint, + current_workspace=current_workspace, + previous_checkpoint=previous_checkpoint, + seen_checkpoint_ids=seen_checkpoint_ids, + known_parent_ids=known_parent_ids, ) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_impl.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_impl.py new file mode 100644 index 00000000..c640fecc --- /dev/null +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_impl.py @@ -0,0 +1,470 @@ +from __future__ import annotations + +import copy +import hashlib +import json +from datetime import datetime +from pathlib import Path +from typing import Any, Iterable, Mapping + +from jsonschema import Draft202012Validator, FormatChecker + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +SCHEMA_PATH = ROOT / "schema" / "ci-operational-checkpoint-v0.1.schema.json" + +CONTINUE = "CONTINUE" +REVALIDATE_WORKSPACE = "REVALIDATE_WORKSPACE" +RESTART_REQUIRED = "RESTART_REQUIRED" +IDEMPOTENT_REPLAY = "IDEMPOTENT_REPLAY" +REJECT_LINEAGE_MISMATCH = "REJECT_LINEAGE_MISMATCH" +REJECT_UNVERIFIED_COMPLETION = "REJECT_UNVERIFIED_COMPLETION" +REJECT_INVALID_AUTHORITY = "REJECT_INVALID_AUTHORITY" + +MEMORY_ONLY_PREFIXES = ("memory://", "agent-memory://", "summary://") + + +def load_schema() -> dict[str, Any]: + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) + if not isinstance(schema, dict): + raise ValueError("checkpoint schema root must be an object") + Draft202012Validator.check_schema(schema) + return schema + + +def _canonical_bytes(checkpoint: Mapping[str, Any]) -> bytes: + value = copy.deepcopy(dict(checkpoint)) + value.pop("checkpoint_digest", None) + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8") + + +def computed_digest(checkpoint: Mapping[str, Any]) -> str: + return hashlib.sha256(_canonical_bytes(checkpoint)).hexdigest() + + +def with_computed_digest(checkpoint: Mapping[str, Any]) -> dict[str, Any]: + result = copy.deepcopy(dict(checkpoint)) + result.setdefault( + "checkpoint_digest", + { + "algorithm": "sha256", + "canonicalization": "json-sort-keys-utf8-v1", + "value": "0" * 64, + }, + ) + result["checkpoint_digest"]["value"] = computed_digest(result) + return result + + +def _schema_errors(checkpoint: Mapping[str, Any]) -> list[Any]: + validator = Draft202012Validator( + load_schema(), + format_checker=FormatChecker(), + ) + return sorted( + validator.iter_errors(dict(checkpoint)), + key=lambda error: [str(part) for part in error.absolute_path], + ) + + +def _result(outcome: str, reason_code: str, detail: str) -> dict[str, str]: + return { + "outcome": outcome, + "reason_code": reason_code, + "detail": detail, + } + + +def _verification_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: + return {str(row["verification_id"]): row for row in rows} + + +def _rejected_index(rows: Iterable[Mapping[str, Any]]) -> dict[str, Mapping[str, Any]]: + return {str(row["approach_id"]): row for row in rows} + + +def _parse_time(value: str) -> datetime: + normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value + return datetime.fromisoformat(normalized) + + +def _duplicate_ids(rows: Iterable[Mapping[str, Any]], field: str) -> list[str]: + seen: set[str] = set() + duplicates: set[str] = set() + for row in rows: + value = str(row[field]) + if value in seen: + duplicates.add(value) + seen.add(value) + return sorted(duplicates) + + +def _previous_checkpoint_integrity_error( + previous_checkpoint: Mapping[str, Any], +) -> tuple[str, str] | None: + errors = _schema_errors(previous_checkpoint) + if errors: + first = errors[0] + path = "/".join(str(part) for part in first.absolute_path) or "" + return ( + "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", + f"{path}: {first.message}", + ) + + expected_digest = previous_checkpoint["checkpoint_digest"]["value"] + actual_digest = computed_digest(previous_checkpoint) + if expected_digest != actual_digest: + return ( + "PREVIOUS_CHECKPOINT_DIGEST_MISMATCH", + f"expected {expected_digest}, computed {actual_digest}", + ) + + duplicate_approaches = _duplicate_ids( + previous_checkpoint["rejected_approaches"], + "approach_id", + ) + if duplicate_approaches: + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + ) + + previous_verification = previous_checkpoint["verification"] + previous_completed = previous_verification["completed"] + previous_pending = previous_verification["pending"] + verification_ids = [ + row["verification_id"] for row in previous_completed + previous_pending + ] + if len(verification_ids) != len(set(verification_ids)): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "verification IDs are duplicated across completed and pending lists", + ) + if set(verification_ids) != set(previous_verification["required"]): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "required verification IDs do not match completed and pending rows", + ) + for row in previous_completed: + if any( + str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) + for ref in row["evidence_refs"] + ): + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + f"{row['verification_id']} relies on memory-only evidence", + ) + return None + + +def evaluate_resume( + checkpoint: Mapping[str, Any], + *, + current_workspace: Mapping[str, Any], + previous_checkpoint: Mapping[str, Any] | None = None, + seen_checkpoint_ids: Iterable[str] = (), + known_parent_ids: Iterable[str] = (), +) -> dict[str, str]: + errors = _schema_errors(checkpoint) + if errors: + first = errors[0] + path = "/".join(str(part) for part in first.absolute_path) or "" + if path == "authority": + return _result( + REJECT_INVALID_AUTHORITY, + "AUTHORITY_NOT_CONTEXT_ONLY", + first.message, + ) + return _result( + RESTART_REQUIRED, + "SCHEMA_INVALID", + f"{path}: {first.message}", + ) + + expected_digest = checkpoint["checkpoint_digest"]["value"] + actual_digest = computed_digest(checkpoint) + if expected_digest != actual_digest: + return _result( + RESTART_REQUIRED, + "DIGEST_MISMATCH", + f"expected {expected_digest}, computed {actual_digest}", + ) + + checkpoint_id = checkpoint["checkpoint_id"] + if checkpoint_id in set(seen_checkpoint_ids): + return _result( + IDEMPOTENT_REPLAY, + "CHECKPOINT_ALREADY_CONSUMED", + f"checkpoint {checkpoint_id} was already consumed", + ) + + duplicate_approaches = _duplicate_ids( + checkpoint["rejected_approaches"], + "approach_id", + ) + if duplicate_approaches: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_ID_DUPLICATED", + "duplicate rejected approach IDs: " + ", ".join(duplicate_approaches), + ) + + sequence = checkpoint["sequence"] + parent_checkpoint_id = checkpoint["parent_checkpoint_id"] + if sequence == 0 and parent_checkpoint_id is not None: + return _result( + REJECT_LINEAGE_MISMATCH, + "ROOT_HAS_PARENT", + "sequence 0 checkpoint must not declare a parent", + ) + if sequence > 0 and parent_checkpoint_id is None: + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_REQUIRED", + "non-root checkpoint must declare parent_checkpoint_id", + ) + + if previous_checkpoint is None: + if sequence > 0: + if parent_checkpoint_id not in set(known_parent_ids): + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_NOT_FOUND", + f"parent checkpoint {parent_checkpoint_id} is not known", + ) + return _result( + REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_REQUIRED", + "non-root resume requires the full previous checkpoint", + ) + else: + integrity_error = _previous_checkpoint_integrity_error(previous_checkpoint) + if integrity_error is not None: + reason_code, detail = integrity_error + return _result( + REJECT_LINEAGE_MISMATCH, + reason_code, + detail, + ) + if checkpoint["trajectory_id"] != previous_checkpoint["trajectory_id"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "TRAJECTORY_CHANGED", + "checkpoint trajectory differs from previous checkpoint", + ) + if _parse_time(checkpoint["created_at"]) < _parse_time( + previous_checkpoint["created_at"] + ): + return _result( + REJECT_LINEAGE_MISMATCH, + "CREATION_TIME_REGRESSED", + "checkpoint creation time is earlier than its parent", + ) + if checkpoint["objective"] != previous_checkpoint["objective"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "OBJECTIVE_CHANGED", + "objective changed within the same trajectory", + ) + for field in ("must", "must_not"): + previous_constraints = set(previous_checkpoint["constraints"][field]) + current_constraints = set(checkpoint["constraints"][field]) + lost_constraints = sorted(previous_constraints - current_constraints) + if lost_constraints: + return _result( + REJECT_LINEAGE_MISMATCH, + "CONSTRAINT_LOST", + f"{field} constraints disappeared: " + ", ".join(lost_constraints), + ) + if parent_checkpoint_id != previous_checkpoint["checkpoint_id"]: + return _result( + REJECT_LINEAGE_MISMATCH, + "PARENT_MISMATCH", + "parent_checkpoint_id does not reference the previous checkpoint", + ) + if sequence != previous_checkpoint["sequence"] + 1: + return _result( + REJECT_LINEAGE_MISMATCH, + "SEQUENCE_MISMATCH", + "checkpoint sequence is not previous sequence + 1", + ) + + previous_rejected = _rejected_index( + previous_checkpoint["rejected_approaches"] + ) + current_rejected = _rejected_index(checkpoint["rejected_approaches"]) + missing_rejections = sorted(set(previous_rejected) - set(current_rejected)) + if missing_rejections: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_LOST", + "rejected approaches disappeared: " + ", ".join(missing_rejections), + ) + changed_rejections = sorted( + approach_id + for approach_id, previous_row in previous_rejected.items() + if current_rejected[approach_id] != previous_row + ) + if changed_rejections: + return _result( + REJECT_LINEAGE_MISMATCH, + "REJECTED_APPROACH_CHANGED", + "rejected approaches changed: " + ", ".join(changed_rejections), + ) + + previous_completed = _verification_index( + previous_checkpoint["verification"]["completed"] + ) + current_completed = _verification_index( + checkpoint["verification"]["completed"] + ) + lost_completed = sorted(set(previous_completed) - set(current_completed)) + if lost_completed: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETED_VERIFICATION_LOST", + "completed verification disappeared: " + ", ".join(lost_completed), + ) + for verification_id, previous_row in previous_completed.items(): + current_row = current_completed[verification_id] + previous_refs = set(previous_row["evidence_refs"]) + current_refs = set(current_row["evidence_refs"]) + if ( + current_row["target"] != previous_row["target"] + or not previous_refs.issubset(current_refs) + ): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETED_VERIFICATION_CHANGED", + ( + "completed verification target changed or prior evidence " + f"was removed: {verification_id}" + ), + ) + + previous_pending = _verification_index( + previous_checkpoint["verification"]["pending"] + ) + current_pending = _verification_index( + checkpoint["verification"]["pending"] + ) + for verification_id, previous_row in previous_pending.items(): + current_row = current_pending.get(verification_id) + completed_row = current_completed.get(verification_id) + if current_row is None and completed_row is None: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "PENDING_VERIFICATION_LOST", + f"pending verification disappeared: {verification_id}", + ) + successor = completed_row if completed_row is not None else current_row + if successor["target"] != previous_row["target"]: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "PENDING_VERIFICATION_CHANGED", + f"pending verification target changed: {verification_id}", + ) + + required_ids = set(checkpoint["verification"]["required"]) + completed_rows = checkpoint["verification"]["completed"] + pending_rows = checkpoint["verification"]["pending"] + completed_ids = [row["verification_id"] for row in completed_rows] + pending_ids = [row["verification_id"] for row in pending_rows] + + all_ids = completed_ids + pending_ids + if len(all_ids) != len(set(all_ids)): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "VERIFICATION_ID_DUPLICATED", + "verification IDs must be unique across completed and pending lists", + ) + + represented_ids = set(all_ids) + if represented_ids != required_ids: + missing = sorted(required_ids - represented_ids) + unexpected = sorted(represented_ids - required_ids) + return _result( + REJECT_UNVERIFIED_COMPLETION, + "VERIFICATION_SET_MISMATCH", + f"missing={missing}; unexpected={unexpected}", + ) + + for row in completed_rows: + refs = row["evidence_refs"] + if not refs: + return _result( + REJECT_UNVERIFIED_COMPLETION, + "COMPLETION_EVIDENCE_MISSING", + f"{row['verification_id']} has no evidence references", + ) + if any(str(ref).casefold().startswith(MEMORY_ONLY_PREFIXES) for ref in refs): + return _result( + REJECT_UNVERIFIED_COMPLETION, + "MEMORY_IS_NOT_VERIFICATION", + f"{row['verification_id']} relies on memory-only evidence", + ) + + next_action = checkpoint["next_action"] + if ( + next_action["action_class"] in {"merge", "deploy"} + and not next_action["requires_fresh_authority"] + ): + return _result( + REJECT_INVALID_AUTHORITY, + "FRESH_AUTHORITY_REQUIRED", + "merge and deploy actions require a fresh action authorization", + ) + + expected_workspace = checkpoint["workspace_state"] + identity_fields = ("repository", "working_directory") + for field in identity_fields: + if field not in current_workspace: + return _result( + RESTART_REQUIRED, + "CURRENT_WORKSPACE_FIELD_MISSING", + f"current workspace did not report {field}", + ) + if current_workspace[field] != expected_workspace[field]: + return _result( + RESTART_REQUIRED, + "WORKSPACE_IDENTITY_MISMATCH", + f"{field} differs from the checkpoint", + ) + + state_fields = ["base_ref", "head_sha"] + if "dirty_state_digest" in expected_workspace: + state_fields.append("dirty_state_digest") + + missing_state_fields = [ + field for field in state_fields if field not in current_workspace + ] + if missing_state_fields: + return _result( + REVALIDATE_WORKSPACE, + "CURRENT_WORKSPACE_FIELD_MISSING", + "current workspace did not report: " + ", ".join(missing_state_fields), + ) + + changed_fields = [ + field + for field in state_fields + if current_workspace[field] != expected_workspace[field] + ] + if changed_fields: + return _result( + REVALIDATE_WORKSPACE, + "WORKSPACE_STATE_CHANGED", + "changed workspace fields: " + ", ".join(changed_fields), + ) + + return _result( + CONTINUE, + "CONTINUE_OK", + "checkpoint is valid, workspace matches, and authority remains context-only", + ) diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py b/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py new file mode 100644 index 00000000..ff45bf29 --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import copy +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +import ci_operational_checkpoint_reference_core as core # noqa: E402 + +EXAMPLE = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE.read_text(encoding="utf-8")) + + +def child_of(parent): + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:issue-218:core-guard" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = parent["sequence"] + 1 + child["created_at"] = "2026-07-02T13:15:00Z" + return core.with_computed_digest(child) + + +def evaluate(checkpoint, previous=None): + return core.evaluate_resume( + checkpoint, + current_workspace=copy.deepcopy(checkpoint["workspace_state"]), + previous_checkpoint=previous, + ) + + +class CoreIntegrityGuardTest(unittest.TestCase): + def test_core_rejects_impossible_parent_lineage(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent["parent_checkpoint_id"] = "checkpoint:unexpected-parent" + invalid_parent = core.with_computed_digest(invalid_parent) + + result = evaluate(child, invalid_parent) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (core.REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID"), + ) + + def test_core_rejects_empty_parent_completion_evidence(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent["verification"]["completed"][0]["evidence_refs"] = [] + invalid_parent = core.with_computed_digest(invalid_parent) + + result = evaluate(child, invalid_parent) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (core.REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID"), + ) + + def test_authority_error_wins_over_unrelated_schema_error(self): + checkpoint = load_example() + checkpoint["authority"] = "merge_allowed" + checkpoint["unexpected"] = True + checkpoint = core.with_computed_digest(checkpoint) + + result = evaluate(checkpoint) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (core.REJECT_INVALID_AUTHORITY, "AUTHORITY_NOT_CONTEXT_ONLY"), + ) + + +if __name__ == "__main__": + unittest.main() From 28b78e5e890bd601f4f7b3751e52b9d47384cf87 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:19:17 +0300 Subject: [PATCH 29/40] fix: make cached schema loader non-recursive --- .../ci_operational_checkpoint_reference_core.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py index 3c62520c..0d733f29 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py @@ -6,27 +6,22 @@ import ci_operational_checkpoint_reference_impl as _impl from ci_operational_checkpoint_reference_impl import * # noqa: F401,F403 -# Preserve private helpers used by the compatibility wrapper and conformance tests. _result = _impl._result _schema_errors = _impl._schema_errors +_ORIGINAL_LOAD_SCHEMA = _impl.load_schema @lru_cache(maxsize=1) def load_schema() -> dict[str, Any]: - """Load and validate the published schema once per process.""" + return _ORIGINAL_LOAD_SCHEMA() - return _impl.load_schema() - -# Make every validator path in the implementation share the cached schema. _impl.load_schema = load_schema def _previous_checkpoint_integrity_error( previous_checkpoint: Mapping[str, Any], ) -> tuple[str, str] | None: - """Validate a parent checkpoint before it can contribute lineage state.""" - verification = previous_checkpoint.get("verification") if isinstance(verification, Mapping): completed = verification.get("completed") @@ -84,8 +79,6 @@ def evaluate_resume( seen_checkpoint_ids: Iterable[str] = (), known_parent_ids: Iterable[str] = (), ) -> dict[str, str]: - """Apply canonical fail-closed guards, then delegate to the implementation.""" - errors = _schema_errors(checkpoint) if errors: authority_error = next( From 9dc8027338d56b0ef2fbd5c0fe2f94e35d13cd44 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:31:16 +0300 Subject: [PATCH 30/40] fix: preserve canonical checkpoint validation order --- ...i_operational_checkpoint_reference_core.py | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py index 0d733f29..3391ba27 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py @@ -1,6 +1,5 @@ from __future__ import annotations -from functools import lru_cache from typing import Any, Iterable, Mapping import ci_operational_checkpoint_reference_impl as _impl @@ -8,35 +7,17 @@ _result = _impl._result _schema_errors = _impl._schema_errors -_ORIGINAL_LOAD_SCHEMA = _impl.load_schema - - -@lru_cache(maxsize=1) -def load_schema() -> dict[str, Any]: - return _ORIGINAL_LOAD_SCHEMA() - - -_impl.load_schema = load_schema +_ORIGINAL_PREVIOUS_CHECKPOINT_INTEGRITY_ERROR = ( + _impl._previous_checkpoint_integrity_error +) def _previous_checkpoint_integrity_error( previous_checkpoint: Mapping[str, Any], ) -> tuple[str, str] | None: - verification = previous_checkpoint.get("verification") - if isinstance(verification, Mapping): - completed = verification.get("completed") - if isinstance(completed, list): - for row in completed: - if not isinstance(row, Mapping): - continue - refs = row.get("evidence_refs") - if isinstance(refs, list) and not refs: - return ( - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - f"{row.get('verification_id', '')} has no evidence references", - ) - - error = _impl._previous_checkpoint_integrity_error(previous_checkpoint) + """Extend parent integrity after canonical schema and digest validation.""" + + error = _ORIGINAL_PREVIOUS_CHECKPOINT_INTEGRITY_ERROR(previous_checkpoint) if error is not None: return error @@ -59,6 +40,13 @@ def _previous_checkpoint_integrity_error( "previous checkpoint cannot be its own parent", ) + for row in previous_checkpoint["verification"]["completed"]: + if not row["evidence_refs"]: + return ( + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + f"{row['verification_id']} has no evidence references", + ) + next_action = previous_checkpoint["next_action"] if ( next_action["action_class"] in {"merge", "deploy"} @@ -71,6 +59,10 @@ def _previous_checkpoint_integrity_error( return None +# Install the enhanced parent hook into the canonical ordered implementation. +_impl._previous_checkpoint_integrity_error = _previous_checkpoint_integrity_error + + def evaluate_resume( checkpoint: Mapping[str, Any], *, @@ -79,37 +71,41 @@ def evaluate_resume( seen_checkpoint_ids: Iterable[str] = (), known_parent_ids: Iterable[str] = (), ) -> dict[str, str]: + """Preserve canonical validation order and stable authority diagnostics.""" + errors = _schema_errors(checkpoint) - if errors: - authority_error = next( - ( - error - for error in errors - if list(error.absolute_path) == ["authority"] - ), - None, + authority_error = next( + ( + error + for error in errors + if list(error.absolute_path) == ["authority"] + ), + None, + ) + if authority_error is not None: + return _result( + REJECT_INVALID_AUTHORITY, + "AUTHORITY_NOT_CONTEXT_ONLY", + authority_error.message, ) - if authority_error is not None: - return _result( - REJECT_INVALID_AUTHORITY, - "AUTHORITY_NOT_CONTEXT_ONLY", - authority_error.message, - ) - - if previous_checkpoint is not None: - error = _previous_checkpoint_integrity_error(previous_checkpoint) - if error is not None: - reason_code, detail = error - return _result( - REJECT_LINEAGE_MISMATCH, - reason_code, - detail, - ) - return _impl.evaluate_resume( + result = _impl.evaluate_resume( checkpoint, current_workspace=current_workspace, previous_checkpoint=previous_checkpoint, seen_checkpoint_ids=seen_checkpoint_ids, known_parent_ids=known_parent_ids, ) + + if result["outcome"] == CONTINUE: + sequence = checkpoint["sequence"] + if ( + sequence > 0 + and checkpoint["checkpoint_id"] == checkpoint["parent_checkpoint_id"] + ): + return _result( + REJECT_LINEAGE_MISMATCH, + "CHECKPOINT_ID_REUSED", + "a checkpoint cannot reuse its parent checkpoint ID", + ) + return result From 79109f4a6b2b2cfcfb99bee53a9a46114730d1d6 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:32:50 +0300 Subject: [PATCH 31/40] refactor: remove preemptive facade validation --- .../ci_operational_checkpoint_reference.py | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index f33d700a..fd935c46 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -14,63 +14,7 @@ def evaluate_resume( seen_checkpoint_ids: Iterable[str] = (), known_parent_ids: Iterable[str] = (), ) -> dict[str, str]: - """Apply link-level lineage guards before the core resume evaluator.""" - - current_errors = _core._schema_errors(checkpoint) - current_digest_valid = ( - not current_errors - and checkpoint["checkpoint_digest"]["value"] - == _core.computed_digest(checkpoint) - ) - if current_digest_valid: - sequence = checkpoint["sequence"] - checkpoint_id = checkpoint["checkpoint_id"] - parent_checkpoint_id = checkpoint["parent_checkpoint_id"] - if sequence > 0 and checkpoint_id == parent_checkpoint_id: - return _core._result( - _core.REJECT_LINEAGE_MISMATCH, - "CHECKPOINT_ID_REUSED", - "a checkpoint cannot reuse its parent checkpoint ID", - ) - - if previous_checkpoint is not None: - integrity_error = _core._previous_checkpoint_integrity_error( - previous_checkpoint - ) - if integrity_error is None: - previous_sequence = previous_checkpoint["sequence"] - previous_parent_id = previous_checkpoint["parent_checkpoint_id"] - if previous_sequence == 0 and previous_parent_id is not None: - return _core._result( - _core.REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "root previous checkpoint must not declare a parent", - ) - if previous_sequence > 0 and previous_parent_id is None: - return _core._result( - _core.REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "non-root previous checkpoint must declare a parent", - ) - if ( - previous_parent_id is not None - and previous_checkpoint["checkpoint_id"] == previous_parent_id - ): - return _core._result( - _core.REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "previous checkpoint cannot be its own parent", - ) - previous_action = previous_checkpoint["next_action"] - if ( - previous_action["action_class"] in {"merge", "deploy"} - and not previous_action["requires_fresh_authority"] - ): - return _core._result( - _core.REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", - "parent merge or deploy intent requires fresh authority", - ) + """Delegate to the canonical core evaluator without pre-validation.""" return _core.evaluate_resume( checkpoint, From d806ce67b9f36a59a7517c929885f57ac85047b6 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:33:38 +0300 Subject: [PATCH 32/40] test: preserve checkpoint diagnostic priority --- .../test_ci_checkpoint_core_integrity.py | 106 ++++++++++++++++-- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py b/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py index ff45bf29..609e0917 100644 --- a/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py @@ -29,15 +29,30 @@ def child_of(parent): return core.with_computed_digest(child) -def evaluate(checkpoint, previous=None): +def semantic_invalid_parent(): + parent = load_example() + parent["rejected_approaches"].append( + copy.deepcopy(parent["rejected_approaches"][0]) + ) + return core.with_computed_digest(parent) + + +def evaluate(checkpoint, previous=None, **kwargs): return core.evaluate_resume( checkpoint, current_workspace=copy.deepcopy(checkpoint["workspace_state"]), previous_checkpoint=previous, + **kwargs, ) class CoreIntegrityGuardTest(unittest.TestCase): + def assert_result(self, result, outcome, reason_code): + self.assertEqual( + (result["outcome"], result["reason_code"]), + (outcome, reason_code), + ) + def test_core_rejects_impossible_parent_lineage(self): parent = load_example() child = child_of(parent) @@ -47,9 +62,10 @@ def test_core_rejects_impossible_parent_lineage(self): result = evaluate(child, invalid_parent) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (core.REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID"), + self.assert_result( + result, + core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", ) def test_core_rejects_empty_parent_completion_evidence(self): @@ -61,9 +77,10 @@ def test_core_rejects_empty_parent_completion_evidence(self): result = evaluate(child, invalid_parent) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (core.REJECT_LINEAGE_MISMATCH, "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID"), + self.assert_result( + result, + core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", ) def test_authority_error_wins_over_unrelated_schema_error(self): @@ -74,9 +91,78 @@ def test_authority_error_wins_over_unrelated_schema_error(self): result = evaluate(checkpoint) - self.assertEqual( - (result["outcome"], result["reason_code"]), - (core.REJECT_INVALID_AUTHORITY, "AUTHORITY_NOT_CONTEXT_ONLY"), + self.assert_result( + result, + core.REJECT_INVALID_AUTHORITY, + "AUTHORITY_NOT_CONTEXT_ONLY", + ) + + def test_current_digest_mismatch_wins_over_invalid_parent(self): + parent = semantic_invalid_parent() + child = child_of(load_example()) + child["objective"]["goal"] = "tampered without recomputing digest" + + result = evaluate(child, parent) + + self.assert_result(result, core.RESTART_REQUIRED, "DIGEST_MISMATCH") + + def test_current_replay_wins_over_invalid_parent(self): + parent = semantic_invalid_parent() + child = child_of(load_example()) + + result = evaluate( + child, + parent, + seen_checkpoint_ids={child["checkpoint_id"]}, + ) + + self.assert_result( + result, + core.IDEMPOTENT_REPLAY, + "CHECKPOINT_ALREADY_CONSUMED", + ) + + def test_current_root_lineage_error_wins_over_invalid_parent(self): + checkpoint = load_example() + checkpoint["parent_checkpoint_id"] = "checkpoint:unexpected-parent" + checkpoint = core.with_computed_digest(checkpoint) + + result = evaluate(checkpoint, semantic_invalid_parent()) + + self.assert_result( + result, + core.REJECT_LINEAGE_MISMATCH, + "ROOT_HAS_PARENT", + ) + + def test_parent_schema_error_wins_over_empty_evidence(self): + parent = load_example() + child = child_of(parent) + invalid_parent = copy.deepcopy(parent) + invalid_parent.pop("authority") + invalid_parent["verification"]["completed"][0]["evidence_refs"] = [] + invalid_parent = core.with_computed_digest(invalid_parent) + + result = evaluate(child, invalid_parent) + + self.assert_result( + result, + core.REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", + ) + + def test_checkpoint_id_reuse_is_rejected_after_canonical_validation(self): + parent = load_example() + child = child_of(parent) + child["checkpoint_id"] = parent["checkpoint_id"] + child = core.with_computed_digest(child) + + result = evaluate(child, parent) + + self.assert_result( + result, + core.REJECT_LINEAGE_MISMATCH, + "CHECKPOINT_ID_REUSED", ) From 4a07a52e216629a9c82ceea5bf8514542b0201ce Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:38:50 +0300 Subject: [PATCH 33/40] test: align empty evidence with schema precedence --- .../conformance/test_ci_checkpoint_core_integrity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py b/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py index 609e0917..4953ccbc 100644 --- a/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_core_integrity.py @@ -80,7 +80,7 @@ def test_core_rejects_empty_parent_completion_evidence(self): self.assert_result( result, core.REJECT_LINEAGE_MISMATCH, - "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + "PREVIOUS_CHECKPOINT_SCHEMA_INVALID", ) def test_authority_error_wins_over_unrelated_schema_error(self): From f5a7a0068af28cd90acd650cefa76579fc5dc5ec Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:12:54 +0300 Subject: [PATCH 34/40] noop --- tmp/noop | 1 + 1 file changed, 1 insertion(+) create mode 100644 tmp/noop diff --git a/tmp/noop b/tmp/noop new file mode 100644 index 00000000..a8b6c947 --- /dev/null +++ b/tmp/noop @@ -0,0 +1 @@ +noop \ No newline at end of file From 65d4672ecf0b8a8e42807dbd458365b7e736aab3 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:14:21 +0300 Subject: [PATCH 35/40] chore: remove accidental temporary file --- tmp/noop | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tmp/noop diff --git a/tmp/noop b/tmp/noop deleted file mode 100644 index a8b6c947..00000000 --- a/tmp/noop +++ /dev/null @@ -1 +0,0 @@ -noop \ No newline at end of file From 19233536266344611d0476901ceb82508c258e63 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:16:14 +0300 Subject: [PATCH 36/40] fix: make parent hook reload-idempotent --- .../ci_operational_checkpoint_reference_core.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py index 3391ba27..f7182e50 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference_core.py @@ -7,8 +7,16 @@ _result = _impl._result _schema_errors = _impl._schema_errors -_ORIGINAL_PREVIOUS_CHECKPOINT_INTEGRITY_ERROR = ( - _impl._previous_checkpoint_integrity_error +_ORIGINAL_HOOK_ATTR = "_ci_checkpoint_original_previous_integrity_error" +if not hasattr(_impl, _ORIGINAL_HOOK_ATTR): + setattr( + _impl, + _ORIGINAL_HOOK_ATTR, + _impl._previous_checkpoint_integrity_error, + ) +_ORIGINAL_PREVIOUS_CHECKPOINT_INTEGRITY_ERROR = getattr( + _impl, + _ORIGINAL_HOOK_ATTR, ) From 9f1059be2c27f2f208cc6114edd9d55540087c48 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:20:12 +0300 Subject: [PATCH 37/40] test: cover reload-idempotent parent hook --- .../test_ci_checkpoint_reload_idempotency.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 standards/agent-continuity/conformance/test_ci_checkpoint_reload_idempotency.py diff --git a/standards/agent-continuity/conformance/test_ci_checkpoint_reload_idempotency.py b/standards/agent-continuity/conformance/test_ci_checkpoint_reload_idempotency.py new file mode 100644 index 00000000..2dd841de --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_checkpoint_reload_idempotency.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import copy +import importlib +import json +import sys +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +import ci_operational_checkpoint_reference_core as core # noqa: E402 + +EXAMPLE = ROOT / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +def load_example(): + return json.loads(EXAMPLE.read_text(encoding="utf-8")) + + +def child_of(parent): + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:issue-218:reload" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = parent["sequence"] + 1 + child["created_at"] = "2026-07-02T13:20:00Z" + return core.with_computed_digest(child) + + +class ReloadIdempotencyTest(unittest.TestCase): + def test_reloading_core_twice_does_not_recurse(self): + parent = load_example() + child = child_of(parent) + + importlib.reload(core) + importlib.reload(core) + result = core.evaluate_resume( + child, + current_workspace=copy.deepcopy(child["workspace_state"]), + previous_checkpoint=parent, + ) + + self.assertEqual( + (result["outcome"], result["reason_code"]), + (core.CONTINUE, "CONTINUE_OK"), + ) + + +if __name__ == "__main__": + unittest.main() From 48d043cf5b664aa70d0b75620b7dd03003320adc Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:37:25 +0300 Subject: [PATCH 38/40] fix: reject whitespace-prefixed memory evidence --- .../ci_operational_checkpoint_reference.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py index fd935c46..8d4abe25 100644 --- a/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py +++ b/standards/agent-continuity/conformance/ci_operational_checkpoint_reference.py @@ -6,6 +6,21 @@ from ci_operational_checkpoint_reference_core import * # noqa: F401,F403 +def _is_memory_only_ref(value: Any) -> bool: + return str(value).lstrip().casefold().startswith(MEMORY_ONLY_PREFIXES) + + +def _memory_only_verification_id( + checkpoint: Mapping[str, Any] | None, +) -> str | None: + if checkpoint is None: + return None + for row in checkpoint["verification"]["completed"]: + if any(_is_memory_only_ref(ref) for ref in row["evidence_refs"]): + return str(row["verification_id"]) + return None + + def evaluate_resume( checkpoint: Mapping[str, Any], *, @@ -14,12 +29,31 @@ def evaluate_resume( seen_checkpoint_ids: Iterable[str] = (), known_parent_ids: Iterable[str] = (), ) -> dict[str, str]: - """Delegate to the canonical core evaluator without pre-validation.""" + """Delegate first, then fail closed on normalized memory-only evidence.""" - return _core.evaluate_resume( + result = _core.evaluate_resume( checkpoint, current_workspace=current_workspace, previous_checkpoint=previous_checkpoint, seen_checkpoint_ids=seen_checkpoint_ids, known_parent_ids=known_parent_ids, ) + if result["outcome"] != CONTINUE: + return result + + parent_verification_id = _memory_only_verification_id(previous_checkpoint) + if parent_verification_id is not None: + return _core._result( + REJECT_LINEAGE_MISMATCH, + "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID", + f"{parent_verification_id} relies on memory-only evidence", + ) + + verification_id = _memory_only_verification_id(checkpoint) + if verification_id is not None: + return _core._result( + REJECT_UNVERIFIED_COMPLETION, + "MEMORY_IS_NOT_VERIFICATION", + f"{verification_id} relies on memory-only evidence", + ) + return result From 02f104021e8fe5fe5b567ef18989e2e0543ebfec Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:38:20 +0300 Subject: [PATCH 39/40] test: normalize memory evidence prefixes --- .../test_ci_memory_ref_normalization.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py diff --git a/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py b/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py new file mode 100644 index 00000000..45d07915 --- /dev/null +++ b/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py @@ -0,0 +1,33 @@ +import copy +import json +import unittest +from pathlib import Path + +import ci_operational_checkpoint_reference as ref + +EXAMPLE = Path(__file__).resolve().parent.parent / "examples" / "ci-operational-checkpoint-v0.1.example.json" + + +class MemoryRefNormalizationTest(unittest.TestCase): + def test_whitespace_prefixed_memory_refs_fail_closed(self): + parent = json.loads(EXAMPLE.read_text(encoding="utf-8")) + current = copy.deepcopy(parent) + current["verification"]["completed"][0]["evidence_refs"] = [" memory://x"] + current = ref.with_computed_digest(current) + result = ref.evaluate_resume(current, current_workspace=current["workspace_state"]) + self.assertEqual(result["reason_code"], "MEMORY_IS_NOT_VERIFICATION") + + child = copy.deepcopy(parent) + child["checkpoint_id"] = "checkpoint:memory-normalization" + child["parent_checkpoint_id"] = parent["checkpoint_id"] + child["sequence"] = 1 + child["created_at"] = "2026-07-03T13:30:00Z" + child = ref.with_computed_digest(child) + parent["verification"]["completed"][0]["evidence_refs"] = ["\n agent-memory://x"] + parent = ref.with_computed_digest(parent) + result = ref.evaluate_resume(child, current_workspace=child["workspace_state"], previous_checkpoint=parent) + self.assertEqual(result["reason_code"], "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID") + + +if __name__ == "__main__": + unittest.main() From b0461bfeaba805905ad862d904afdd4e12b4a4d6 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:39:49 +0300 Subject: [PATCH 40/40] test: isolate parent memory evidence guard --- .../conformance/test_ci_memory_ref_normalization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py b/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py index 45d07915..5a9e9ea2 100644 --- a/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py +++ b/standards/agent-continuity/conformance/test_ci_memory_ref_normalization.py @@ -17,14 +17,14 @@ def test_whitespace_prefixed_memory_refs_fail_closed(self): result = ref.evaluate_resume(current, current_workspace=current["workspace_state"]) self.assertEqual(result["reason_code"], "MEMORY_IS_NOT_VERIFICATION") + parent["verification"]["completed"][0]["evidence_refs"] = ["\n agent-memory://x"] + parent = ref.with_computed_digest(parent) child = copy.deepcopy(parent) child["checkpoint_id"] = "checkpoint:memory-normalization" child["parent_checkpoint_id"] = parent["checkpoint_id"] child["sequence"] = 1 child["created_at"] = "2026-07-03T13:30:00Z" child = ref.with_computed_digest(child) - parent["verification"]["completed"][0]["evidence_refs"] = ["\n agent-memory://x"] - parent = ref.with_computed_digest(parent) result = ref.evaluate_resume(child, current_workspace=child["workspace_state"], previous_checkpoint=parent) self.assertEqual(result["reason_code"], "PREVIOUS_CHECKPOINT_SEMANTIC_INVALID")