diff --git a/MIGRATIONS.md b/MIGRATIONS.md index dc97ef4..bd24d37 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -139,9 +139,10 @@ Using a fresh ChatGPT conversation and only the documented beginner **Create → 2. require the Plus-or-higher plan check, selected/authenticated/authorized `@GitHub`, correct repository ID, privacy check, reversible CRUD/readback, cleanup, and `Operational memory: READY` receipt; 3. install the repository-ID bootloader supplied by activation; 4. create a small piece of genuine durable project state through normal conversation; -5. start another fresh conversation and recover that state from the repository; -6. rename the private repository; -7. start another fresh conversation and verify the same repository ID resolves to the renamed repository, the state is recovered, and a verified write succeeds without changing the bootloader. +5. for the exact `main` commit created by that durable-state write, verify the derived copy's **Operational-memory protocol validation** workflow starts from a `push` event on `main` and completes successfully with `RESULT: PASS`; a source-repository run or manual dispatch is not a substitute; +6. start another fresh conversation and recover that state from the repository; +7. rename the private repository; +8. start another fresh conversation and verify the same repository ID resolves to the renamed repository, the state is recovered, and a verified write succeeds without changing the bootloader. Failure or user confusion is release evidence. Fix the smallest root cause, return the public template to development, and, when the existing acceptance authorization remains in force, re-enter with a new candidate after corrective work. diff --git a/SETUP.md b/SETUP.md index 4c39e6a..41bb9c1 100644 --- a/SETUP.md +++ b/SETUP.md @@ -73,7 +73,9 @@ Then it should say **One final step** and tell you exactly where to install the ChatGPT supplies the completed block with your repository ID already filled in. Its compact form is: + > Operational Memory: I use GitHub repository ID `` for durable operational memory. When prior durable state could affect the task, or this conversation creates/changes clear future-governing state, use `@GitHub`, resolve this ID to its current repository, and follow `START_HERE.md`. Do not claim retrieval or persistence unless GitHub actions actually ran and writes were verified. If the ID cannot resolve or a write cannot be verified, say so; never guess another repository. + With a current 10-digit GitHub repository ID, this is about **487 characters**. You copy the completed block as-is; you do not need to understand, remember, or type the numeric ID yourself. @@ -157,7 +159,7 @@ The rest of this file is optional. Use it when setup is blocked, when you want s GitHub template copies do **not** inherit this public repository's branch-protection/ruleset configuration. Protection is optional hardening you may configure separately; see **Optional `main` protection** below. -The template includes an advisory GitHub Actions validator. In a derived working copy it does **not** run on every direct operational-memory write; it runs for pull requests or when manually dispatched. It is not a required status check. +The template includes an advisory GitHub Actions validator. In a derived working copy it runs after every push to canonical `main` (including normal Operational Memory writes), for pull requests, or when manually dispatched. It is not a required status check and does not replace immediate write/readback verification. Having many unrelated repositories connected does not make setup ambiguous. The activation URL selects one exact repository. ChatGPT captures that repository's numeric GitHub repository ID for future routing. If that exact repository later cannot be resolved or accessed, ChatGPT should stop rather than select another repository. diff --git a/tools/validate_protocol.py b/tools/validate_protocol.py index 607353a..e1d7b38 100755 --- a/tools/validate_protocol.py +++ b/tools/validate_protocol.py @@ -219,6 +219,77 @@ def check_removed_or_ambiguous_product_wording() -> None: ) +def check_validation_workflow(manifest: dict) -> None: + validation = manifest.get("validation", {}) or {} + workflow_rel = str(validation.get("workflow", "")).strip() + if not workflow_rel: + error("PROTOCOL.yaml validation.workflow is missing") + return + + workflow_path = require_file(workflow_rel, "validation workflow") + if validation.get("runs_on_pull_request") is not True: + error("PROTOCOL.yaml validation.runs_on_pull_request must be true") + if validation.get("runs_on_main_push") is not True: + error("PROTOCOL.yaml validation.runs_on_main_push must be true") + if not workflow_path.is_file(): + return + + workflow_text = read_text(workflow_path) + on_match = re.search( + r"(?ms)^on:\s*\n(?P.*?)(?=^[^\s#])", + workflow_text, + ) + if not on_match: + error(f"{workflow_rel} is missing a readable top-level on: trigger block") + return + + trigger_block = on_match.group("body") + if not re.search(r"(?m)^ pull_request:\s*$", trigger_block): + error(f"{workflow_rel} must run on pull_request") + + push_match = re.search( + r"(?ms)^ push:\s*\n(?P(?: {4,}.*\n?)*)", + trigger_block, + ) + if not push_match: + error(f"{workflow_rel} must run on push to canonical main") + return + + push_block = push_match.group("body") + if not re.search(r"(?m)^ branches:\s*$", push_block): + error(f"{workflow_rel} push trigger must declare branches") + if not re.search(r"(?m)^ - main\s*$", push_block): + error(f"{workflow_rel} push trigger must include canonical main") + + +def check_documented_bootloader(manifest: dict) -> None: + bootloader = str(manifest.get("custom_instruction_template", "")).strip() + if not bootloader: + error("PROTOCOL.yaml custom_instruction_template is missing") + return + + setup_rel = str((manifest.get("human_docs", {}) or {}).get("setup", "SETUP.md")) + setup_path = require_file(setup_rel, "setup documentation") + if not setup_path.is_file(): + return + + setup_text = read_text(setup_path) + match = re.search( + r"\s*\n>\s*(?P[^\n]+)\n", + setup_text, + ) + if not match: + error(f"{setup_rel} is missing the marked canonical bootloader documentation block") + return + + documented = match.group("text").strip() + if documented != bootloader: + error( + f"{setup_rel} documented bootloader does not match " + "PROTOCOL.yaml custom_instruction_template" + ) + + def load_manifest() -> dict: manifest_path = require_file("PROTOCOL.yaml", "protocol manifest") if not manifest_path.is_file(): @@ -263,6 +334,8 @@ def main() -> int: check_legacy_project_naming() check_removed_or_ambiguous_product_wording() + check_validation_workflow(manifest) + check_documented_bootloader(manifest) version = str(manifest.get("protocol_version", "")).strip() if not version: @@ -496,4 +569,4 @@ def print_results(version: str = "unknown") -> None: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file