From a8f52bcb16b88264285e65c07dd3be2343cc9cf0 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:57:04 +0200 Subject: [PATCH 01/36] Harden CI and add release candidate checks --- .github/workflows/plananvil-tests.yml | 52 ++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/plananvil-tests.yml b/.github/workflows/plananvil-tests.yml index edc1ea9..4916bf5 100644 --- a/.github/workflows/plananvil-tests.yml +++ b/.github/workflows/plananvil-tests.yml @@ -9,8 +9,17 @@ on: permissions: contents: read +concurrency: + group: plananvil-tests-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHONDONTWRITEBYTECODE: "1" + jobs: - test: + core: + name: core (${{ matrix.os }}, py${{ matrix.python-version }}) + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -20,12 +29,13 @@ jobs: - windows-latest python-version: - "3.11" + - "3.14" runs-on: ${{ matrix.os }} steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python-version }} - name: Configure Git fixture identity @@ -35,6 +45,38 @@ jobs: git config --global commit.gpgsign false git config --global protocol.file.allow always - name: Compile deterministic scripts and hooks - run: python -m compileall -q .agents/skills/plan-anvil .codex/hooks - - name: Run unit and integration tests + run: python -m compileall -q .agents/skills/plan-anvil .codex/hooks tools tests + - name: Run core unit and integration tests run: python -m unittest discover -s .agents/skills/plan-anvil/tests -v + - name: Check patch whitespace + run: git diff --check + + distribution: + name: distribution and release candidate + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.11" + - name: Configure Git fixture identity + run: | + git config --global user.name "PlanAnvil CI" + git config --global user.email "plananvil-ci@example.invalid" + git config --global commit.gpgsign false + git config --global protocol.file.allow always + - name: Run distribution and release tests + run: python -m unittest discover -s tests -v + - name: Validate prepared C01-C16 evidence templates + run: | + python tools/prepare_capabilities.py --target "$RUNNER_TEMP/plananvil-capabilities" --force + python tools/validate_capabilities.py --root "$RUNNER_TEMP/plananvil-capabilities" + - name: Validate release candidate metadata + run: python tools/release_check.py --candidate + - name: Build deterministic release archive + run: python tools/build_release.py --output "$RUNNER_TEMP/plananvil-dist" + - name: Check patch whitespace + run: git diff --check From 2c247fc6f8f1165e9bf67e7a936199117f32eb65 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:57:46 +0200 Subject: [PATCH 02/36] Document distribution and remaining release gates --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d031591..4cbd229 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,43 @@ It **generates and validates a plan but never executes it**. Product implementat ## Status -The deterministic generator core, schemas, templates, tests, optional planning agents, and defense-in-depth hooks are implemented. +**Distribution version:** 0.2.0. -Release status is **candidate**, not production-ready. The Codex capability matrix in `docs/CODEX_CAPABILITY_BASELINE.md` remains a release gate until the required tests have committed sanitized `REPRODUCED` evidence for the current Codex version, model, operating systems, permission modes, and project-trust modes. +The deterministic generator core, schemas, templates, tests, optional planning agents, defense-in-depth hooks, repository installer/upgrader/uninstaller, release tooling, and a deterministic C01-C16 live-qualification template archive/materializer are implemented. + +Release status is **candidate**, not production-ready. Deterministic CI is green, but production publication remains gated on two external steps: + +1. protect `main` with required PR/CI checks (tracked by issue #6); +2. execute the prepared C01-C16 packages in an authenticated current Codex sandbox and commit required `REPRODUCED` evidence (tracked by issue #7). + +The capability contract is defined in `docs/CODEX_CAPABILITY_BASELINE.md`. Deterministic tests and prepared fixtures do not substitute for live Codex evidence. + +## Install into another repository + +From a PlanAnvil checkout or extracted release archive: + +```text +python tools/plananvil_dist.py install --target /path/to/repository +``` + +The installer: + +- copies the PlanAnvil skill and project-scoped agent/hook files; +- preserves an existing `AGENTS.md` unchanged; +- conservatively merges compatible `[agents]` settings instead of replacing `.codex/config.toml`; +- structurally merges only PlanAnvil entries into `.codex/hooks.json` while preserving unrelated hooks; +- records file ownership/hashes in `.plananvil/installation.json`; +- fails closed on unmanaged conflicts or locally modified managed files. + +Verify, upgrade, or uninstall: + +```text +python tools/plananvil_dist.py verify --target /path/to/repository +python tools/plananvil_dist.py --source /path/to/new/release upgrade --target /path/to/repository +python tools/plananvil_dist.py uninstall --target /path/to/repository +``` + +See `docs/INSTALLATION.md` for the ownership and conflict contract. ## Use @@ -44,31 +78,41 @@ python .agents/skills/plan-anvil/scripts/plan_anvil.py start \ --goal "Add validation that rejects an empty display name." ``` -The bootstrap controller stops after isolation, profiling, run scaffolding, and durable bootstrap evidence. The skill then performs evidence-based analysis and plan authoring before deterministic validation, blind review, and the planning-only commit gate. - ## Safety boundary PlanAnvil does not modify application code or tests, execute generated stages, deploy, migrate, restart services, switch a live environment, use destructive Git cleanup, or push or merge the base branch. The retained planning worktree is the durable control root. Machine-specific paths remain only in ignored local files; committed artifacts use repository-relative paths and Git identity. -Project-scoped `.codex` agents and hooks are optional. Hooks require project trust and remain defense in depth; mandatory filesystem and Git postconditions apply in every hook mode. +Project-scoped `.codex` agents and hooks remain defense in depth; mandatory filesystem and Git postconditions apply in every hook mode. ## Requirements - Python 3.11 or newer; - Git 2.30 or newer; - no elevated privileges; -- no third-party Python packages for the deterministic core; -- no network access for local validation. +- no third-party Python packages for deterministic core/distribution tooling; +- no network access for local validation or installation. + +CI tests Python 3.11 and the current upper supported interpreter on Ubuntu, macOS, and Windows. Parser/preflight tests enforce the Git 2.30 minimum; live Codex C16 remains responsible for the permission-mode Git capability matrix. ## Validation ```text python -m unittest discover -s .agents/skills/plan-anvil/tests -v -python -m compileall -q .agents/skills/plan-anvil .codex/hooks +python -m unittest discover -s tests -v +python -m compileall -q .agents/skills/plan-anvil .codex/hooks tools tests +python tools/release_check.py --candidate ``` +## Release and live qualification + +- `docs/RELEASE.md` — deterministic gates, tag workflow and publication contract +- `docs/CODEX_SANDBOX_RUNBOOK.md` — exact remaining C01-C16 sandbox sequence +- `capabilities/templates.tar.gz.b64` + `tools/prepare_capabilities.py` — deterministic prepared C01-C16 fixtures/prompts/config/assertions/results/hashes + +A production tag is rejected by `.github/workflows/release.yml` until every required capability is `REPRODUCED`. + ## Documentation - `docs/IMPLEMENTATION_SPEC.md` — authoritative product and implementation contract @@ -77,8 +121,11 @@ python -m compileall -q .agents/skills/plan-anvil .codex/hooks - `docs/RECOVERY_AND_VALIDATION.md` — crash recovery, checkpoint, schema and path-safety guarantees - `docs/OPENAI_COMPLIANCE.md` — Codex compatibility decisions - `docs/CODEX_CAPABILITY_BASELINE.md` — reproducible capability release gate -- `docs/EXAMPLES.md` — expected decisions and output shapes -- `capabilities/README.md` — evidence-package workflow +- `docs/CODEX_CAPABILITY_QUALIFICATION_2026-08-28.md` — latest qualification audit +- `docs/INSTALLATION.md` — install/upgrade/uninstall contract +- `docs/TROUBLESHOOTING.md` — operational recovery guidance +- `docs/RELEASE.md` — release workflow +- `docs/CODEX_SANDBOX_RUNBOOK.md` — remaining live qualification procedure ## Author From e39244c750cac35429736ea4c1ce49966151ee76 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:58:21 +0200 Subject: [PATCH 03/36] Prepare 0.2.0 release candidate changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cc915..fcf6924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,29 @@ All notable changes to PlanAnvil are documented here. ## [Unreleased] +No queued changes. + +## [0.2.0] - 2026-08-28 + +### Added + +- standard-library repository distribution manager with install, verify, upgrade, status and uninstall operations; +- transactional rollback, ownership/hash state, conservative Codex config merging, and structural hook merging; +- distribution tests covering clean repositories, existing `AGENTS.md`, existing `.codex/config.toml`, unrelated hooks, upgrade conflicts and uninstall conflicts; +- deterministic release archive builder, candidate/production release gate, and tag-driven GitHub Release workflow; +- deterministic C01-C16 qualification template archive containing fixture, prompt, config, expected result, current sanitized BLOCKED result, evaluation and SHA-256 manifests; +- capability evidence materializer/validator/rehash tools and a live Codex sandbox runbook; +- installation, troubleshooting, and release documentation. + ### Changed - refresh the Codex capability baseline to 2.2 against current 2026-08-28 official documentation; - replace legacy agent concurrency/depth configuration with `agents.enabled` and `agents.max_concurrent_threads_per_session`; - enforce flat direct-child execution topology in the generated contract instead of relying on undocumented `agents.max_depth` behavior; -- record the 2026-08-28 C01–C16 qualification attempt and its live Codex runtime blocker. +- record the 2026-08-28 C01–C16 qualification attempt and its live Codex runtime blocker; +- pin GitHub Actions to immutable SHAs and Node-24-based checkout/setup-python releases; +- expand CI across Python 3.11 and the current upper supported interpreter on Ubuntu, macOS and Windows; +- split distribution/release-candidate validation into a stable named CI check. ### Fixed @@ -19,6 +36,10 @@ All notable changes to PlanAnvil are documented here. - make the Git-hook probe fixture emit explicit hook diagnostics while preserving fail-closed classification for unrelated commit failures; - synchronize golden blind-review fixtures and dependent comparison hashes with the required independent `plan-anvil-reviewer` author role. +### Release status + +0.2.0 is code-complete as a release candidate. Production publication remains blocked until protected-`main` administration is enabled and required C01-C16 live Codex evidence is committed as `REPRODUCED`. + ## [0.1.0] - 2026-07-12 ### Added @@ -39,4 +60,4 @@ All notable changes to PlanAnvil are documented here. ### Release status -The deterministic core is implemented. Production readiness remains gated on reproduced Codex capability evidence defined in `docs/CODEX_CAPABILITY_BASELINE.md`. +The deterministic core was implemented. Production readiness remained gated on reproduced Codex capability evidence. From d850d66ab71fa77bf0ea3f7c1c05e6ff9c3fa0c5 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:58:33 +0200 Subject: [PATCH 04/36] Document capability template materialization --- capabilities/README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/capabilities/README.md b/capabilities/README.md index 5469389..0eb8bfd 100644 --- a/capabilities/README.md +++ b/capabilities/README.md @@ -2,7 +2,14 @@ This directory stores sanitized, reproducible evidence for the release gate in `docs/CODEX_CAPABILITY_BASELINE.md`. -A capability is not `REPRODUCED` until its directory contains: +`capabilities/templates.tar.gz.b64` contains deterministic prepared C01-C16 packages. Before a live qualification run, materialize them on the evidence branch: + +```text +python tools/prepare_capabilities.py --force +python tools/validate_capabilities.py +``` + +Each materialized package contains: ```text CXX/ @@ -17,6 +24,15 @@ CXX/ └── hashes.json ``` -Record the exact Codex version, model slug, operating system, permission mode, project-trust mode, fixture commit, setup, cleanup, expected behavior, sanitized actual behavior, and evaluation. Remove usernames, home directories, private repository URLs, credentials, session identifiers, transcripts, unrelated Git databases, and proprietary source. +The prepared result is `BLOCKED`: it documents the fixture and the lack of live runtime evidence. `REPRODUCED` requires a real current Codex run with exact Codex version, model slug, OS, permission mode, project-trust mode, fixture commit, sanitized observations, evaluation, and SHA-256 integrity. + +After editing one capability: + +```text +python tools/rehash_capability.py C01 +python tools/validate_capabilities.py +``` + +Use `docs/CODEX_SANDBOX_RUNBOOK.md` for the full sequence. The materializer is path-traversal safe and writes only under `capabilities/`. -The repository ships deterministic contract tests and evidence scaffolding, but does not label live Codex behavior as reproduced without these packages. +Do not commit session transcripts, credentials, usernames, home directories, temporary absolute paths, private repository URLs, session identifiers, unrelated Git databases, or proprietary source. Keep only the minimal structural event/decision data required to evaluate `expected.json`. From 53b8870efa088c690e85e236f42c98827fdcce64 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:58:53 +0200 Subject: [PATCH 05/36] Mark live qualification templates ready --- capabilities/index.json | 149 +++++----------------------------------- 1 file changed, 19 insertions(+), 130 deletions(-) diff --git a/capabilities/index.json b/capabilities/index.json index 4d02c09..56c6e83 100644 --- a/capabilities/index.json +++ b/capabilities/index.json @@ -1,140 +1,29 @@ { "baseline_version": "2.2", "capabilities": [ - { - "evidence_directory": "capabilities/C01", - "expected_behavior": "Repository skills are discovered from .agents/skills", - "id": "C01", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C02", - "expected_behavior": "allow_implicit_invocation false disables implicit invocation while explicit invocation remains available", - "id": "C02", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C03", - "expected_behavior": "Generated execution contracts require a flat direct-child topology without relying on a Codex nesting-depth setting", - "id": "C03", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - }, - { - "evidence_directory": "capabilities/C04", - "expected_behavior": "Codex subagent workflows use current agents enablement and concurrency settings; PlanAnvil does not require nested descendants", - "id": "C04", - "required": false, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C05", - "expected_behavior": "Required reviewer handoffs use explicit immutable files and hashes", - "id": "C05", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - }, - { - "evidence_directory": "capabilities/C06", - "expected_behavior": "PreToolUse covers supported local function-tool paths but not every equivalent path", - "id": "C06", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C07", - "expected_behavior": "The Git guard rejects the configured unsafe-command corpus", - "id": "C07", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - }, - { - "evidence_directory": "capabilities/C08", - "expected_behavior": "PreCompact can stop compaction", - "id": "C08", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C09", - "expected_behavior": "Compaction is allowed after checkpoint creation without a permanent loop", - "id": "C09", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - }, - { - "evidence_directory": "capabilities/C10", - "expected_behavior": "PostCompact and SessionStart can provide recovery context", - "id": "C10", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C11", - "expected_behavior": "Project instructions follow documented directory scope and precedence", - "id": "C11", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C12", - "expected_behavior": "project_doc_max_bytes can truncate automatic instruction loading", - "id": "C12", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C13", - "expected_behavior": "SubagentStart can add context but continue false does not stop subagent startup", - "id": "C13", - "required": true, - "result": "BLOCKED", - "source": "DOCUMENTED" - }, - { - "evidence_directory": "capabilities/C14", - "expected_behavior": "Planning isolation preserves source branch, SHA, index, and files", - "id": "C14", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - }, - { - "evidence_directory": "capabilities/C15", - "expected_behavior": "Blind review is immutable and detects seeded contract defects", - "id": "C15", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - }, - { - "evidence_directory": "capabilities/C16", - "expected_behavior": "Git probe reports refs, branches, worktrees, index, commits, and cleanup", - "id": "C16", - "required": true, - "result": "BLOCKED", - "source": "CONTRACT_DEFINED" - } + {"evidence_directory":"capabilities/C01","expected_behavior":"Repository skills are discovered from .agents/skills","id":"C01","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C02","expected_behavior":"allow_implicit_invocation false disables implicit invocation while explicit invocation remains available","id":"C02","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C03","expected_behavior":"Generated execution contracts require a flat direct-child topology without relying on a Codex nesting-depth setting","id":"C03","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"}, + {"evidence_directory":"capabilities/C04","expected_behavior":"Codex subagent workflows use current agents enablement and concurrency settings; PlanAnvil does not require nested descendants","id":"C04","required":false,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C05","expected_behavior":"Required reviewer handoffs use explicit immutable files and hashes","id":"C05","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"}, + {"evidence_directory":"capabilities/C06","expected_behavior":"PreToolUse covers supported local function-tool paths but not every equivalent path","id":"C06","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C07","expected_behavior":"The Git guard rejects the configured unsafe-command corpus","id":"C07","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"}, + {"evidence_directory":"capabilities/C08","expected_behavior":"PreCompact can stop compaction","id":"C08","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C09","expected_behavior":"Compaction is allowed after checkpoint creation without a permanent loop","id":"C09","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"}, + {"evidence_directory":"capabilities/C10","expected_behavior":"PostCompact and SessionStart can provide recovery context","id":"C10","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C11","expected_behavior":"Project instructions follow documented directory scope and precedence","id":"C11","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C12","expected_behavior":"project_doc_max_bytes can truncate automatic instruction loading","id":"C12","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C13","expected_behavior":"SubagentStart can add context but continue false does not stop subagent startup","id":"C13","required":true,"result":"BLOCKED","source":"DOCUMENTED"}, + {"evidence_directory":"capabilities/C14","expected_behavior":"Planning isolation preserves source branch, SHA, index, and files","id":"C14","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"}, + {"evidence_directory":"capabilities/C15","expected_behavior":"Blind review is immutable and detects seeded contract defects","id":"C15","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"}, + {"evidence_directory":"capabilities/C16","expected_behavior":"Git probe reports refs, branches, worktrees, index, commits, and cleanup","id":"C16","required":true,"result":"BLOCKED","source":"CONTRACT_DEFINED"} ], + "evidence_package_state": "TEMPLATE_ARCHIVE_READY", "generated_at": "2026-08-28", "qualification_attempt": { "date": "2026-08-28", - "deterministic_ci_commit": "acb1dd664d039ed6d8b3069b6e7c24be7646e559", - "deterministic_ci_run": 24, + "deterministic_ci_commit": "0e1091fc4168671215ebcd89c92db702ea33ccdd", + "deterministic_ci_run": 26, "deterministic_ci_result": "PASS", "live_codex_result": "BLOCKED", "blocker": "No authenticated Codex runtime or codex executable is available in the qualification execution environment." From ebfdc1fb47a9e5dfbb37b35a1be8d57dd5b16b45 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:59:01 +0200 Subject: [PATCH 06/36] Add distribution release version --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.2.0 From fb19c06766448879257970c119205b1f07e546cf Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:59:11 +0200 Subject: [PATCH 07/36] Define distribution ownership manifest --- distribution/manifest.json | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 distribution/manifest.json diff --git a/distribution/manifest.json b/distribution/manifest.json new file mode 100644 index 0000000..bbb8e6f --- /dev/null +++ b/distribution/manifest.json @@ -0,0 +1,29 @@ +{ + "agents_config": { + "enabled": true, + "max_concurrent_threads_per_session": 4 + }, + "copy_roots": [ + ".agents/skills/plan-anvil", + ".codex/agents/plan-anvil-profiler.toml", + ".codex/agents/plan-anvil-reviewer.toml", + ".codex/hooks/plan-anvil-agent-context.py", + ".codex/hooks/plan-anvil-compaction.py", + ".codex/hooks/plan-anvil-guard.py", + ".codex/hooks/plan-anvil-recovery.py", + ".codex/hooks/plan_anvil_checkpoint.py", + ".codex/hooks/plan_anvil_hooklib.py" + ], + "format_version": 1, + "hooks_source": ".codex/hooks.json", + "product_version": "0.2.0", + "release_files": [ + "VERSION", + "LICENSE", + "README.md", + "distribution/manifest.json", + "tools/plananvil_dist.py", + "docs/INSTALLATION.md", + "docs/TROUBLESHOOTING.md" + ] +} From 870b7f440bc8ca317cd423171865cef0dcb8aae1 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:59:24 +0200 Subject: [PATCH 08/36] Add gated release workflow --- .github/workflows/release.yml | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..863b652 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: PlanAnvil release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +env: + PYTHONDONTWRITEBYTECODE: "1" + +jobs: + release: + name: validate and publish release + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out tagged source + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.11" + - name: Configure Git fixture identity + run: | + git config --global user.name "PlanAnvil Release CI" + git config --global user.email "plananvil-release@example.invalid" + git config --global commit.gpgsign false + git config --global protocol.file.allow always + - name: Compile deterministic code + run: python -m compileall -q .agents/skills/plan-anvil .codex/hooks tools tests + - name: Run core tests + run: python -m unittest discover -s .agents/skills/plan-anvil/tests -v + - name: Run distribution tests + run: python -m unittest discover -s tests -v + - name: Validate live capability evidence + run: python tools/validate_capabilities.py + - name: Enforce production release gate + run: python tools/release_check.py --tag "${GITHUB_REF_NAME}" + - name: Build release archive + run: python tools/build_release.py --output dist + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="$(cat VERSION)" + gh release create "v${VERSION}" \ + "dist/plananvil-${VERSION}.zip" \ + "dist/SHA256SUMS" \ + --verify-tag \ + --title "PlanAnvil ${VERSION}" \ + --notes-file dist/release-notes.md From 12f508e3673b359de7ca50f4352d8900a2351a04 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:01:02 +0200 Subject: [PATCH 09/36] Add deterministic C01-C16 template archive --- capabilities/templates.tar.gz.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.tar.gz.b64 diff --git a/capabilities/templates.tar.gz.b64 b/capabilities/templates.tar.gz.b64 new file mode 100644 index 0000000..b960bcf --- /dev/null +++ b/capabilities/templates.tar.gz.b64 @@ -0,0 +1 @@ +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 From e099d297bb294bcf584678e1a38f783c8e4ebcd1 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:01:31 +0200 Subject: [PATCH 10/36] Document install upgrade and uninstall contract --- docs/INSTALLATION.md | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/INSTALLATION.md diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..c3691ae --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,75 @@ +# Installing PlanAnvil + +PlanAnvil 0.2.0 ships a standard-library distribution manager. It installs only the repository skill and the optional project-scoped Codex agents/hooks required by PlanAnvil. It does not edit product code, product tests, `AGENTS.md`, or unrelated Codex configuration. + +## Requirements + +- Python 3.11 or newer; +- Git 2.30 or newer; +- a Git repository with a clean worktree for normal install/upgrade/uninstall operations. + +## Install from a checkout or release archive + +From the PlanAnvil source/release root: + +```text +python tools/plananvil_dist.py install --target /path/to/repository +``` + +Review the generated changes and commit them in the target repository. The installation records ownership in `.plananvil/installation.json` so later upgrades and uninstall operations can distinguish PlanAnvil-owned files from pre-existing files. + +Verify at any time: + +```text +python tools/plananvil_dist.py verify --target /path/to/repository +python tools/plananvil_dist.py status --target /path/to/repository +``` + +## Existing `.codex/config.toml` + +The installer never replaces the file. It handles `[agents]` conservatively: + +- existing compatible `enabled = true` is preserved; +- existing positive `max_concurrent_threads_per_session` or legacy `max_threads` is preserved; +- missing required keys are inserted inside a clearly marked PlanAnvil-managed block; +- a conflicting `enabled = false` or malformed concurrency value blocks installation instead of being overwritten. + +Uninstall removes only the exact managed block recorded in installation state. + +## Existing `.codex/hooks.json` + +The installer parses JSON, preserves unrelated hook events/entries, and adds only the exact PlanAnvil hook entries from the release. A different unmanaged hook that already references `.codex/hooks/plan-anvil-*` is treated as a conflict and blocks installation. + +Uninstall removes only the exact PlanAnvil entries recorded in installation state. Other hooks and other top-level JSON fields remain untouched. + +## Existing `AGENTS.md` + +PlanAnvil does not modify it. Codex instruction discovery remains the target repository's responsibility. The PlanAnvil skill explicitly reads applicable instructions during a run and records their complete hashes/scope in its instruction map. + +## Upgrade + +Use a newer PlanAnvil checkout or extracted release archive: + +```text +python tools/plananvil_dist.py --source /path/to/new/plananvil upgrade --target /path/to/repository +``` + +Upgrade replaces a PlanAnvil-owned file only when its current hash still matches the hash recorded by the prior installation. Locally modified managed files cause a fail-closed conflict. Pre-existing identical files that were adopted rather than owned are never overwritten with different content. + +## Uninstall + +From any PlanAnvil checkout/release containing the distribution tool: + +```text +python tools/plananvil_dist.py uninstall --target /path/to/repository +``` + +The command removes only unmodified PlanAnvil-owned files, the exact managed config block, exact PlanAnvil hook entries, and `.plananvil/installation.json`. It preserves adopted files and unrelated target-repository content. + +## Dirty repositories + +Normal operations require a clean worktree. `--allow-dirty` exists for controlled recovery/test scenarios, but it should not be the default installation workflow because it makes review of PlanAnvil's changes harder. + +## Release archive integrity + +Official release automation produces `plananvil-.zip` and `SHA256SUMS`. Verify the archive checksum before extracting it, then use the installer above. From 6bc265224618885b598ea6c2981ac9a2086f52f2 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:01:40 +0200 Subject: [PATCH 11/36] Add distribution troubleshooting guide --- docs/TROUBLESHOOTING.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/TROUBLESHOOTING.md diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..296957d --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,29 @@ +# Troubleshooting + +## Installer reports a conflicting file + +PlanAnvil refuses to overwrite an unmanaged file at a path it owns. Compare the target file with the release payload. Rename/remove it deliberately, or keep the current file and do not install that PlanAnvil release. + +## Installer reports modified managed files during upgrade/uninstall + +The file no longer matches `.plananvil/installation.json`. Preserve/review the local modification first. PlanAnvil intentionally does not overwrite or delete it automatically. + +## Existing `[agents]` configuration conflicts + +PlanAnvil requires agents to be enabled for its reviewer/profiler workflow. If the repository explicitly sets `enabled = false`, decide at repository-policy level whether PlanAnvil agents are permitted; the installer will not flip the setting automatically. + +## Existing hooks conflict + +A hook referencing `.codex/hooks/plan-anvil-*` but differing from the release is ambiguous ownership. Reconcile or remove the old PlanAnvil hook entry before installing/upgrading. + +## `verify` reports a hash mismatch + +A managed or adopted payload file changed after installation. Compare it with the release/source version and either restore it or intentionally reconcile it before upgrade/uninstall. + +## Capability gate is `BLOCKED` + +That is expected until the live Codex sandbox is available. Deterministic CI, distribution tests, and capability package validation do not substitute for live `REPRODUCED` evidence. Follow `docs/CODEX_SANDBOX_RUNBOOK.md` once an authenticated Codex runtime is available. + +## GitHub release workflow refuses to publish + +`tools/release_check.py` is intentionally fail-closed. A stable tag can publish only when every capability marked `required` in `capabilities/index.json` is `REPRODUCED`, all evidence packages validate, the tag matches `VERSION`, and the distribution manifest is complete. From 215410a2218d80c09f0485fd60ba5f60520e12b6 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:01:52 +0200 Subject: [PATCH 12/36] Document production release gates --- docs/RELEASE.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/RELEASE.md diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..434b5c6 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,43 @@ +# Release process + +PlanAnvil release engineering is split into deterministic readiness and live Codex qualification. + +## Deterministic readiness + +The normal CI workflow verifies: + +1. core compile + unit/integration suite on Ubuntu, macOS, and Windows; +2. Python 3.11 (minimum) and the current tested upper interpreter; +3. distribution install/verify/upgrade/uninstall behavior; +4. deterministic materialization plus structural/SHA-256 integrity of the prepared C01-C16 template archive; +5. release-candidate metadata and deterministic archive construction. + +Run locally: + +```text +python -m unittest discover -s .agents/skills/plan-anvil/tests -v +python -m unittest discover -s tests -v +python tools/release_check.py --candidate +python tools/build_release.py --output dist +``` + +## Live Codex gate + +Before a production tag, execute `docs/CODEX_SANDBOX_RUNBOOK.md`. Required entries in `capabilities/index.json` must be changed from `BLOCKED` to `REPRODUCED` only after their complete sanitized evidence package is committed and `python tools/validate_capabilities.py` passes. + +## Publish + +`VERSION` is the canonical distribution/release version. The deterministic artifact generator version is recorded independently in generated artifacts for compatibility. A production tag must be exactly `v`. + +After live qualification is merged and `main` is green: + +```text +git tag -s v0.2.0 -m "PlanAnvil 0.2.0" +git push origin v0.2.0 +``` + +`.github/workflows/release.yml` reruns the deterministic gates, requires all release-gating capabilities to be `REPRODUCED`, builds a deterministic ZIP + checksum, and creates the GitHub Release. It cannot publish while capability evidence remains blocked. + +## Repository administration prerequisite + +Before production release, protect `main` as tracked in issue #6: PR-only changes, required CI, up-to-date branch, conversation resolution, and no force push/delete. From 85e9ba155f13bc43b50cb0b20d65971855f74b59 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:02:06 +0200 Subject: [PATCH 13/36] Add live Codex sandbox qualification runbook --- docs/CODEX_SANDBOX_RUNBOOK.md | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/CODEX_SANDBOX_RUNBOOK.md diff --git a/docs/CODEX_SANDBOX_RUNBOOK.md b/docs/CODEX_SANDBOX_RUNBOOK.md new file mode 100644 index 0000000..2c07a7f --- /dev/null +++ b/docs/CODEX_SANDBOX_RUNBOOK.md @@ -0,0 +1,56 @@ +# Codex sandbox qualification runbook + +This is the remaining live-runtime step after deterministic/release hardening is complete. + +## Sandbox prerequisites + +- authenticated current Codex CLI/runtime; +- exact Codex version recorded before the first test; +- model slug recorded; +- operating system recorded; +- permission mode and project-trust mode recorded; +- Git available and target fixture repositories disposable; +- no real credentials, private repository URLs, personal paths, or proprietary source in fixtures/evidence. + +Start the evidence branch by running: + +```text +codex --version +python tools/prepare_capabilities.py --force +python tools/validate_capabilities.py +``` + +## Per-capability sequence + +For C01 through C16 in order: + +1. read `capabilities/CXX/README.md`; +2. instantiate the files described by `fixture/README.md` in a disposable Git repository; +3. apply the capability-specific configuration described in `config/README.md`; +4. execute the prompt in `prompt.txt` using the invocation recorded in `run-command.txt`; +5. capture only the structural observations required by `expected.json`; +6. sanitize the result into `actual.sanitized.json`; +7. set `evaluation.json` to `REPRODUCED`, `FAILED`, or `BLOCKED` based on the assertions; +8. when `REPRODUCED`, fill exact runtime metadata (`codex_version`, `model`, `os`, `permission_mode`, `project_trust`); +9. update the same result in `capabilities/index.json`; +10. run `python tools/rehash_capability.py CXX`; +11. run `python tools/validate_capabilities.py` before continuing. + +C04 is informational/non-gating in baseline 2.2 but should still be observed if the sandbox exposes nested-subagent behavior relevant to future architecture. + +## Sanitization + +Do not commit session transcripts. Retain the minimum event/decision structure needed to evaluate assertions. Replace usernames, home directories, temporary absolute paths, repository URLs, IDs, credentials, and session identifiers with stable placeholders or hashes. + +The validator rejects obvious token/private-path patterns in `actual.sanitized.json`. + +## Final gate + +When all required capabilities are `REPRODUCED`: + +```text +python tools/validate_capabilities.py +python tools/release_check.py +``` + +Both must pass on a clean tree. Then merge the evidence PR, confirm protected `main` is green, and create the signed `v0.2.0` tag described in `docs/RELEASE.md`. From 70b333d2263e6fb1755667a67d07530c093ee651 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:02:25 +0200 Subject: [PATCH 14/36] Add safe capability template materializer --- tools/prepare_capabilities.py | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tools/prepare_capabilities.py diff --git a/tools/prepare_capabilities.py b/tools/prepare_capabilities.py new file mode 100644 index 0000000..ee37b1e --- /dev/null +++ b/tools/prepare_capabilities.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import argparse +import base64 +import io +import shutil +import tarfile +from pathlib import Path, PurePosixPath + +ARCHIVE = Path('capabilities/templates.tar.gz.b64') + + +def _safe_member(name: str) -> PurePosixPath: + path = PurePosixPath(name) + if path.is_absolute() or '..' in path.parts or not path.parts or path.parts[0] != 'capabilities': + raise ValueError(f'unsafe capability template path: {name}') + return path + + +def materialize(source_root: Path, target_root: Path, *, force: bool = False) -> list[str]: + source_root = source_root.resolve() + target_root = target_root.resolve() + raw = base64.b64decode((source_root / ARCHIVE).read_text(encoding='ascii')) + written: list[str] = [] + with tarfile.open(fileobj=io.BytesIO(raw), mode='r:gz') as tar: + members = [m for m in tar.getmembers() if m.isfile()] + for member in members: + rel = _safe_member(member.name) + target = target_root.joinpath(*rel.parts) + target.parent.mkdir(parents=True, exist_ok=True) + payload = tar.extractfile(member) + if payload is None: + raise ValueError(f'missing template payload: {member.name}') + data = payload.read() + if target.exists() and not force and target.read_bytes() != data: + raise FileExistsError(f'refusing to overwrite existing capability evidence: {target}') + target.write_bytes(data) + written.append(rel.as_posix()) + for rel in (Path('capabilities/index.json'), Path('capabilities/README.md')): + source = source_root / rel + target = target_root / rel + target.parent.mkdir(parents=True, exist_ok=True) + if source.resolve() != target.resolve(): + shutil.copyfile(source, target) + written.append(rel.as_posix()) + return sorted(set(written)) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description='Materialize prepared PlanAnvil C01-C16 live-evidence templates') + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1], help='PlanAnvil source root') + parser.add_argument('--target', type=Path, help='Target root; defaults to the source root') + parser.add_argument('--force', action='store_true', help='Replace existing placeholder/template evidence files') + args = parser.parse_args(argv) + root = args.root.resolve() + target = (args.target or root).resolve() + written = materialize(root, target, force=args.force) + print(f'Materialized {len(written)} capability template files under {target}.') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From fe4b0d0394dec90782b6cbd733581cccf7f7474e Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:02:51 +0200 Subject: [PATCH 15/36] Add capability evidence validator --- tools/validate_capabilities.py | 164 +++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tools/validate_capabilities.py diff --git a/tools/validate_capabilities.py b/tools/validate_capabilities.py new file mode 100644 index 0000000..86cdd47 --- /dev/null +++ b/tools/validate_capabilities.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + +REQUIRED_FILES = { + 'README.md', + 'prompt.txt', + 'run-command.txt', + 'expected.json', + 'actual.sanitized.json', + 'evaluation.json', + 'hashes.json', +} +VALID_RESULTS = {'NOT_RUN', 'REPRODUCED', 'FAILED', 'BLOCKED'} +SENSITIVE_PATTERNS = [ + re.compile(r'\b(?:sk|gh[pousr])_[A-Za-z0-9_-]{16,}\b'), + re.compile(r'(?i)\b(?:password|api[_-]?key|access[_-]?token)\b\s*[:=]\s*["\']?[^\s"\']{8,}'), + re.compile(r'(?]+'), + re.compile(r'(?i)(?]+'), +] + + +class CapabilityError(RuntimeError): + pass + + +def load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError) as exc: + raise CapabilityError(f'{path}: invalid or unreadable JSON: {exc}') from exc + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def package_files(directory: Path) -> list[Path]: + result = [] + for path in directory.rglob('*'): + if path.is_file() and path.name != 'hashes.json' and '__pycache__' not in path.parts: + result.append(path) + return sorted(result) + + +def _relative_set(paths: list[Path], root: Path) -> set[str]: + return {path.relative_to(root).as_posix() for path in paths} + + +def validate_one(root: Path, capability_id: str, expected_index_result: str | None = None) -> list[str]: + errors: list[str] = [] + directory = root / 'capabilities' / capability_id + if not directory.is_dir(): + return [f'{capability_id}: evidence directory is missing'] + names = {path.name for path in directory.iterdir() if path.is_file()} + missing = sorted(REQUIRED_FILES - names) + if missing: + errors.append(f'{capability_id}: missing files: {", ".join(missing)}') + fixture = directory / 'fixture' + config = directory / 'config' + if not fixture.is_dir() or not any(path.is_file() for path in fixture.rglob('*')): + errors.append(f'{capability_id}: fixture/ must contain at least one file') + if not config.is_dir() or not any(path.is_file() for path in config.rglob('*')): + errors.append(f'{capability_id}: config/ must contain at least one file') + if errors: + return errors + + expected = load_json(directory / 'expected.json') + actual = load_json(directory / 'actual.sanitized.json') + evaluation = load_json(directory / 'evaluation.json') + hashes = load_json(directory / 'hashes.json') + for label, value in [('expected', expected), ('actual', actual), ('evaluation', evaluation), ('hashes', hashes)]: + if not isinstance(value, dict): + errors.append(f'{capability_id}: {label}.json must contain an object') + + for label, value in [('expected', expected), ('actual', actual), ('evaluation', evaluation)]: + if isinstance(value, dict) and value.get('capability_id') != capability_id: + errors.append(f'{capability_id}: {label}.json capability_id mismatch') + + result = evaluation.get('result') if isinstance(evaluation, dict) else None + if result not in VALID_RESULTS: + errors.append(f'{capability_id}: evaluation result is invalid: {result!r}') + if expected_index_result is not None and result != expected_index_result: + errors.append(f'{capability_id}: index result {expected_index_result} != evaluation result {result}') + if isinstance(actual, dict) and actual.get('result') != result: + errors.append(f'{capability_id}: actual result does not match evaluation') + + if result == 'REPRODUCED': + env = actual.get('environment') if isinstance(actual, dict) else None + required_env = ('codex_version', 'model', 'os', 'permission_mode', 'project_trust') + if not isinstance(env, dict) or any(not isinstance(env.get(key), str) or not env.get(key).strip() for key in required_env): + errors.append(f'{capability_id}: REPRODUCED evidence lacks complete runtime metadata') + if evaluation.get('expected_met') is not True: + errors.append(f'{capability_id}: REPRODUCED evaluation must set expected_met=true') + if actual.get('blocker'): + errors.append(f'{capability_id}: REPRODUCED actual evidence cannot contain a blocker') + elif result == 'BLOCKED': + if not isinstance(actual.get('blocker'), str) or not actual.get('blocker').strip(): + errors.append(f'{capability_id}: BLOCKED actual evidence requires a blocker') + + text = (directory / 'actual.sanitized.json').read_text(encoding='utf-8') + for pattern in SENSITIVE_PATTERNS: + if pattern.search(text): + errors.append(f'{capability_id}: actual.sanitized.json contains sensitive/private-looking data') + break + + files = hashes.get('files') if isinstance(hashes, dict) else None + if hashes.get('algorithm') != 'sha256' or not isinstance(files, dict): + errors.append(f'{capability_id}: hashes.json must use sha256 and contain a files object') + else: + actual_files = package_files(directory) + actual_set = _relative_set(actual_files, directory) + hash_set = set(files) + if actual_set != hash_set: + errors.append( + f'{capability_id}: hashes.json file set mismatch; missing={sorted(actual_set-hash_set)}, extra={sorted(hash_set-actual_set)}' + ) + for path in actual_files: + rel = path.relative_to(directory).as_posix() + if files.get(rel) != sha256(path): + errors.append(f'{capability_id}: hash mismatch for {rel}') + return errors + + +def validate_all(root: Path) -> list[str]: + index = load_json(root / 'capabilities/index.json') + if not isinstance(index, dict) or not isinstance(index.get('capabilities'), list): + return ['capabilities/index.json is invalid'] + errors: list[str] = [] + seen: set[str] = set() + for item in index['capabilities']: + if not isinstance(item, dict) or not isinstance(item.get('id'), str): + errors.append('capabilities/index.json contains an invalid capability record') + continue + capability_id = item['id'] + seen.add(capability_id) + errors.extend(validate_one(root, capability_id, item.get('result'))) + expected_ids = {f'C{i:02d}' for i in range(1, 17)} + if seen != expected_ids: + errors.append(f'capability index IDs mismatch: expected {sorted(expected_ids)}, got {sorted(seen)}') + return errors + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description='Validate PlanAnvil capability evidence packages') + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1]) + args = parser.parse_args(argv) + errors = validate_all(args.root.resolve()) + if errors: + for error in errors: + print(f'ERROR: {error}', file=sys.stderr) + return 2 + print('All PlanAnvil capability evidence packages are structurally valid and hash-consistent.') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From 028dbc8f20a8d3313071e1a6da6aea227d6aef67 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:03:05 +0200 Subject: [PATCH 16/36] Add capability hash tool --- tools/rehash_capability.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tools/rehash_capability.py diff --git a/tools/rehash_capability.py b/tools/rehash_capability.py new file mode 100644 index 0000000..0f1c320 --- /dev/null +++ b/tools/rehash_capability.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def rehash(directory: Path) -> None: + files = {} + for path in sorted(directory.rglob('*')): + if path.is_file() and path.name != 'hashes.json' and '__pycache__' not in path.parts: + files[path.relative_to(directory).as_posix()] = sha256(path) + payload = {'schema_version': '1.0', 'algorithm': 'sha256', 'files': files} + (directory / 'hashes.json').write_text(json.dumps(payload, indent=2, sort_keys=True) + '\n', encoding='utf-8') + + +def main() -> int: + parser = argparse.ArgumentParser(description='Recompute PlanAnvil capability evidence hashes') + parser.add_argument('capability', nargs='?', help='C01..C16; omit with --all') + parser.add_argument('--all', action='store_true') + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1]) + args = parser.parse_args() + root = args.root.resolve() + if args.all: + ids = [f'C{i:02d}' for i in range(1, 17)] + elif args.capability: + ids = [args.capability.upper()] + else: + parser.error('provide a capability ID or --all') + for cid in ids: + directory = root / 'capabilities' / cid + if not directory.is_dir(): + parser.error(f'missing capability directory: {cid}') + rehash(directory) + print(f'rehashed {cid}') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From 46249727f36027b67d15fca6f060136376d86b86 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:03:20 +0200 Subject: [PATCH 17/36] Add candidate and production release gate --- tools/release_check.py | 87 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tools/release_check.py diff --git a/tools/release_check.py b/tools/release_check.py new file mode 100644 index 0000000..b027d5c --- /dev/null +++ b/tools/release_check.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import argparse +import json +import re +import sys +import tempfile +from pathlib import Path + +from prepare_capabilities import materialize +from validate_capabilities import validate_all + +SEMVER = re.compile(r'^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$') + + +def release_blockers(root: Path, *, require_reproduced: bool = True, tag: str | None = None) -> list[str]: + blockers: list[str] = [] + version_path = root / 'VERSION' + if not version_path.is_file(): + return ['VERSION is missing'] + version = version_path.read_text(encoding='utf-8').strip() + if not SEMVER.fullmatch(version): + blockers.append(f'VERSION is not semver: {version!r}') + + manifest_path = root / 'distribution/manifest.json' + try: + manifest = json.loads(manifest_path.read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError) as exc: + blockers.append(f'distribution manifest is invalid: {exc}') + manifest = {} + if manifest.get('product_version') != version: + blockers.append('distribution manifest product_version does not match VERSION') + + changelog = (root / 'CHANGELOG.md').read_text(encoding='utf-8') if (root / 'CHANGELOG.md').is_file() else '' + if f'## [{version}]' not in changelog: + blockers.append(f'CHANGELOG.md has no section for {version}') + + if tag is not None and tag != f'v{version}': + blockers.append(f'tag {tag!r} does not match VERSION v{version}') + + if require_reproduced: + blockers.extend(validate_all(root)) + else: + try: + with tempfile.TemporaryDirectory() as tmp: + prepared = Path(tmp) / 'prepared' + materialize(root, prepared, force=True) + blockers.extend(validate_all(prepared)) + except Exception as exc: + blockers.append(f'capability template materialization failed: {exc}') + + try: + index = json.loads((root / 'capabilities/index.json').read_text(encoding='utf-8')) + except (OSError, json.JSONDecodeError) as exc: + blockers.append(f'capabilities/index.json is invalid: {exc}') + index = {} + if require_reproduced: + for item in index.get('capabilities', []): + if item.get('required') and item.get('result') != 'REPRODUCED': + blockers.append(f"{item.get('id')}: required capability is {item.get('result')}, not REPRODUCED") + + required_release_files = manifest.get('release_files', []) if isinstance(manifest, dict) else [] + for raw in required_release_files: + path = root / raw + if not path.is_file(): + blockers.append(f'release file is missing: {raw}') + return blockers + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description='Validate PlanAnvil release readiness') + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument('--candidate', action='store_true', help='validate candidate structure without requiring live REPRODUCED evidence') + parser.add_argument('--tag') + args = parser.parse_args(argv) + blockers = release_blockers(args.root.resolve(), require_reproduced=not args.candidate, tag=args.tag) + if blockers: + for blocker in blockers: + print(f'BLOCKER: {blocker}', file=sys.stderr) + return 2 + mode = 'candidate' if args.candidate else 'release' + print(f'PlanAnvil {mode} checks passed.') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From 99762f99dd3853cef53f8f68c69e8af6c9d3f993 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:03:41 +0200 Subject: [PATCH 18/36] Add deterministic release archive builder --- tools/build_release.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tools/build_release.py diff --git a/tools/build_release.py b/tools/build_release.py new file mode 100644 index 0000000..746d4b8 --- /dev/null +++ b/tools/build_release.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import zipfile +from pathlib import Path + +EPOCH = (1980, 1, 1, 0, 0, 0) + + +def load_manifest(root: Path) -> dict: + return json.loads((root / 'distribution/manifest.json').read_text(encoding='utf-8')) + + +def release_paths(root: Path, manifest: dict) -> list[Path]: + result: set[Path] = set() + for raw in manifest.get('release_files', []): + path = root / raw + if not path.is_file(): + raise RuntimeError(f'missing release file: {raw}') + result.add(path) + for raw in manifest.get('copy_roots', []): + path = root / raw + if path.is_file(): + result.add(path) + elif path.is_dir(): + for child in path.rglob('*'): + if child.is_file() and '__pycache__' not in child.parts and child.suffix not in {'.pyc', '.pyo'}: + result.add(child) + else: + raise RuntimeError(f'missing release payload: {raw}') + hook_source = root / manifest['hooks_source'] + if not hook_source.is_file(): + raise RuntimeError('hooks source is missing') + result.add(hook_source) + return sorted(result, key=lambda p: p.relative_to(root).as_posix()) + + +def changelog_notes(root: Path, version: str) -> str: + text = (root / 'CHANGELOG.md').read_text(encoding='utf-8') + marker = f'## [{version}]' + start = text.find(marker) + if start < 0: + raise RuntimeError(f'CHANGELOG.md has no {version} section') + next_start = text.find('\n## [', start + len(marker)) + section = text[start: next_start if next_start >= 0 else len(text)].strip() + return f'# PlanAnvil {version}\n\n{section}\n' + + +def build(root: Path, output: Path) -> dict[str, str]: + manifest = load_manifest(root) + version = (root / 'VERSION').read_text(encoding='utf-8').strip() + if manifest.get('product_version') != version: + raise RuntimeError('manifest version does not match VERSION') + output.mkdir(parents=True, exist_ok=True) + archive = output / f'plananvil-{version}.zip' + with zipfile.ZipFile(archive, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: + for path in release_paths(root, manifest): + rel = path.relative_to(root).as_posix() + info = zipfile.ZipInfo(rel, date_time=EPOCH) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + zf.writestr(info, path.read_bytes()) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + sums = output / 'SHA256SUMS' + sums.write_text(f'{digest} {archive.name}\n', encoding='utf-8') + notes = output / 'release-notes.md' + notes.write_text(changelog_notes(root, version), encoding='utf-8') + return {'archive': str(archive), 'sha256': digest, 'checksums': str(sums), 'notes': str(notes)} + + +def main() -> int: + parser = argparse.ArgumentParser(description='Build deterministic PlanAnvil release archive') + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument('--output', type=Path, default=Path('dist')) + args = parser.parse_args() + result = build(args.root.resolve(), args.output.resolve()) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From 8e5153319d2a9767bff988d0612814346a177c17 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:05:25 +0200 Subject: [PATCH 19/36] Add transactional PlanAnvil distribution manager --- tools/plananvil_dist.py | 549 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 tools/plananvil_dist.py diff --git a/tools/plananvil_dist.py b/tools/plananvil_dist.py new file mode 100644 index 0000000..a27922f --- /dev/null +++ b/tools/plananvil_dist.py @@ -0,0 +1,549 @@ +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + +STATE_REL = Path('.plananvil/installation.json') +MANIFEST_REL = Path('distribution/manifest.json') +CONFIG_REL = Path('.codex/config.toml') +HOOKS_REL = Path('.codex/hooks.json') +BEGIN_CONFIG = '# BEGIN PLANANVIL MANAGED AGENT SETTINGS' +END_CONFIG = '# END PLANANVIL MANAGED AGENT SETTINGS' +HOOK_TOKEN = '.codex/hooks/plan-anvil-' + + +class DistError(RuntimeError): + pass + + +def utcnow() -> str: + return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z') + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def json_bytes(value: Any) -> bytes: + return (json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False) + '\n').encode('utf-8') + + +def write_bytes(path: Path, data: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name(f'.{path.name}.plananvil-tmp') + tmp.write_bytes(data) + os.replace(tmp, path) + + +def load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding='utf-8')) + except FileNotFoundError as exc: + raise DistError(f'missing required file: {path}') from exc + except json.JSONDecodeError as exc: + raise DistError(f'invalid JSON in {path}: {exc}') from exc + + +def safe_rel(raw: str) -> Path: + path = Path(raw) + if path.is_absolute() or not raw or any(part in {'', '.', '..'} for part in path.parts): + raise DistError(f'unsafe relative path: {raw!r}') + return path + + +def target_path(root: Path, rel: Path) -> Path: + candidate = (root / rel).resolve(strict=False) + try: + candidate.relative_to(root.resolve()) + except ValueError as exc: + raise DistError(f'target path escapes repository: {rel}') from exc + return candidate + + +def git(cwd: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + result = subprocess.run(['git', *args], cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if check and result.returncode != 0: + raise DistError(f"git {' '.join(args)} failed: {result.stderr.strip()}") + return result + + +def git_root(path: Path) -> Path: + return Path(git(path, 'rev-parse', '--show-toplevel').stdout.strip()).resolve() + + +def require_clean(root: Path, allow_dirty: bool) -> None: + if allow_dirty: + return + if git(root, 'status', '--porcelain', '--untracked-files=all').stdout.strip(): + raise DistError('target repository is not clean; commit/stash changes or pass --allow-dirty') + + +def source_commit(source: Path) -> str | None: + result = git(source, 'rev-parse', 'HEAD', check=False) + value = result.stdout.strip() + return value if result.returncode == 0 and re.fullmatch(r'[0-9a-f]{40,64}', value) else None + + +@dataclass +class Snapshot: + existed: bool + data: bytes | None + + +class Transaction: + def __init__(self) -> None: + self.snapshots: dict[Path, Snapshot] = {} + + def capture(self, path: Path) -> None: + if path in self.snapshots: + return + if path.exists(): + if path.is_dir(): + raise DistError(f'expected file, found directory: {path}') + self.snapshots[path] = Snapshot(True, path.read_bytes()) + else: + self.snapshots[path] = Snapshot(False, None) + + def rollback(self) -> None: + for path, snap in reversed(list(self.snapshots.items())): + try: + if snap.existed: + write_bytes(path, snap.data or b'') + elif path.exists(): + path.unlink() + except OSError: + pass + + +def load_manifest(source: Path) -> dict[str, Any]: + value = load_json(source / MANIFEST_REL) + if not isinstance(value, dict) or value.get('format_version') != 1: + raise DistError('unsupported distribution manifest') + version = value.get('product_version') + if not isinstance(version, str) or not re.fullmatch(r'\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?', version): + raise DistError('invalid product_version') + roots = value.get('copy_roots') + if not isinstance(roots, list) or not roots or not all(isinstance(item, str) for item in roots): + raise DistError('copy_roots must be a non-empty string list') + for raw in roots: + safe_rel(raw) + if not isinstance(value.get('hooks_source'), str): + raise DistError('hooks_source is required') + safe_rel(value['hooks_source']) + return value + + +def iter_payload(source: Path, manifest: dict[str, Any]) -> Iterable[tuple[Path, Path]]: + seen: set[Path] = set() + for raw in manifest['copy_roots']: + rel = safe_rel(raw) + src = source / rel + if not src.exists(): + raise DistError(f'distribution payload missing: {rel}') + candidates = [src] if src.is_file() else sorted(p for p in src.rglob('*') if p.is_file()) + for path in candidates: + if path.is_symlink(): + raise DistError(f'symlink is not allowed in payload: {path.relative_to(source)}') + child = path.relative_to(source) + if '__pycache__' in child.parts or path.suffix in {'.pyc', '.pyo'} or child in seen: + continue + seen.add(child) + yield child, path + + +def desired_hooks(source: Path, manifest: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: + value = load_json(source / safe_rel(manifest['hooks_source'])) + hooks = value.get('hooks') if isinstance(value, dict) else None + if not isinstance(hooks, dict): + raise DistError('hooks source must contain an object named hooks') + result: dict[str, list[dict[str, Any]]] = {} + for event, entries in hooks.items(): + if not isinstance(event, str) or not isinstance(entries, list) or not all(isinstance(entry, dict) for entry in entries): + raise DistError('invalid hooks source structure') + if any(HOOK_TOKEN not in json.dumps(entry, sort_keys=True) for entry in entries): + raise DistError(f'non-PlanAnvil entry found in hooks source: {event}') + result[event] = entries + return result + + +def load_target_hooks(path: Path) -> dict[str, Any]: + if not path.exists(): + return {'hooks': {}} + value = load_json(path) + if not isinstance(value, dict) or not isinstance(value.get('hooks', {}), dict): + raise DistError('.codex/hooks.json must contain a hooks object') + value.setdefault('hooks', {}) + for event, entries in value['hooks'].items(): + if not isinstance(event, str) or not isinstance(entries, list) or not all(isinstance(entry, dict) for entry in entries): + raise DistError('.codex/hooks.json has invalid entries') + return value + + +def merge_hooks(target: dict[str, Any], desired: dict[str, list[dict[str, Any]]], previous: dict[str, list[dict[str, Any]]] | None = None) -> tuple[dict[str, Any], dict[str, list[dict[str, Any]]]]: + previous = previous or {} + hooks = target['hooks'] + managed: dict[str, list[dict[str, Any]]] = {} + for event, wanted in desired.items(): + current = hooks.setdefault(event, []) + for old in previous.get(event, []): + if old in current and old not in wanted: + current.remove(old) + for entry in current: + if HOOK_TOKEN in json.dumps(entry, sort_keys=True) and entry not in wanted: + raise DistError(f'conflicting unmanaged PlanAnvil hook for {event}') + for entry in wanted: + if entry not in current: + current.append(entry) + managed[event] = list(wanted) + return target, managed + + +SECTION_RE = re.compile(r'^\s*\[([^\]]+)\]\s*(?:#.*)?$') + + +def find_section(lines: list[str], section: str) -> tuple[int, int] | None: + start = None + for idx, line in enumerate(lines): + match = SECTION_RE.match(line) + if not match: + continue + name = match.group(1).strip() + if start is None and name == section: + start = idx + elif start is not None: + return start, idx + return (start, len(lines)) if start is not None else None + + +def section_key(lines: list[str], start: int, end: int, key: str) -> str | None: + pattern = re.compile(rf'^\s*{re.escape(key)}\s*=\s*(.*?)\s*(?:#.*)?$') + for idx in range(start + 1, end): + match = pattern.match(lines[idx]) + if match: + return match.group(1).strip() + return None + + +def strip_managed_block(text: str, expected: str | None = None) -> str: + if BEGIN_CONFIG not in text and END_CONFIG not in text: + return text + if expected is not None and expected not in text: + raise DistError('managed .codex/config.toml block was modified') + pattern = re.compile(rf'(?:\n)?{re.escape(BEGIN_CONFIG)}\n.*?{re.escape(END_CONFIG)}\n?', re.DOTALL) + updated, count = pattern.subn('\n', text, count=1) + if count != 1: + raise DistError('invalid PlanAnvil managed config block') + return updated.lstrip('\n') if not updated.strip() else updated + + +def plan_config(current: str, desired: dict[str, Any], previous_block: str | None = None) -> tuple[str, str | None]: + base = strip_managed_block(current, previous_block) if previous_block else current + lines = base.splitlines() + section = find_section(lines, 'agents') + enabled_wanted = bool(desired.get('enabled', True)) + threads_wanted = int(desired.get('max_concurrent_threads_per_session', 4)) + if threads_wanted < 1: + raise DistError('desired agent concurrency must be positive') + + if section is None: + block = (f'{BEGIN_CONFIG}\n[agents]\n' + f"enabled = {'true' if enabled_wanted else 'false'}\n" + f'max_concurrent_threads_per_session = {threads_wanted}\n{END_CONFIG}\n') + text = base + if text and not text.endswith('\n'): + text += '\n' + if text.strip(): + text += '\n' + return text + block, block + + start, end = section + missing: list[str] = [] + enabled = section_key(lines, start, end, 'enabled') + if enabled is None: + missing.append(f"enabled = {'true' if enabled_wanted else 'false'}") + elif enabled.lower() not in {'true', 'false'} or (enabled.lower() == 'true') != enabled_wanted: + raise DistError('existing [agents].enabled conflicts with PlanAnvil') + + threads = section_key(lines, start, end, 'max_concurrent_threads_per_session') + legacy = section_key(lines, start, end, 'max_threads') + value = threads if threads is not None else legacy + if value is None: + missing.append(f'max_concurrent_threads_per_session = {threads_wanted}') + else: + try: + if int(value) < 1: + raise ValueError + except ValueError as exc: + raise DistError('existing agent concurrency is not a positive integer') from exc + + if not missing: + return base, None + block = BEGIN_CONFIG + '\n' + '\n'.join(missing) + '\n' + END_CONFIG + '\n' + insertion = start + 1 + new_lines = lines[:insertion] + [BEGIN_CONFIG, *missing, END_CONFIG] + lines[insertion:] + return '\n'.join(new_lines) + '\n', block + + +def read_state(root: Path) -> dict[str, Any] | None: + path = root / STATE_REL + if not path.exists(): + return None + value = load_json(path) + if not isinstance(value, dict) or value.get('format_version') != 1: + raise DistError('unsupported installation state') + return value + + +def plan_payload(source: Path, root: Path, manifest: dict[str, Any], previous: dict[str, Any] | None) -> tuple[list[tuple[Path, Path, str, bool]], dict[str, dict[str, Any]]]: + previous_files = previous.get('files', {}) if previous else {} + plan: list[tuple[Path, Path, str, bool]] = [] + state_files: dict[str, dict[str, Any]] = {} + for rel, src in iter_payload(source, manifest): + dest = target_path(root, rel) + digest = sha256_file(src) + old = previous_files.get(rel.as_posix()) + owned = True + if dest.exists(): + if dest.is_dir(): + raise DistError(f'payload destination is a directory: {rel}') + current = sha256_file(dest) + if old: + if old.get('owned') and current != old.get('sha256'): + raise DistError(f'managed file was modified locally: {rel}') + if not old.get('owned'): + if current != digest: + raise DistError(f'adopted file conflicts with upgrade payload: {rel}') + owned = False + elif current == digest: + owned = False + else: + raise DistError(f'target contains a conflicting file: {rel}') + plan.append((rel, src, digest, owned)) + state_files[rel.as_posix()] = {'sha256': digest, 'owned': owned} + return plan, state_files + + +def install_or_upgrade(source: Path, target: Path, upgrade: bool, allow_dirty: bool) -> dict[str, Any]: + source = source.resolve() + root = git_root(target.resolve()) + require_clean(root, allow_dirty) + manifest = load_manifest(source) + previous = read_state(root) + if upgrade and previous is None: + raise DistError('PlanAnvil is not installed; run install first') + if not upgrade and previous is not None: + raise DistError('PlanAnvil is already installed; run upgrade instead') + + payload, state_files = plan_payload(source, root, manifest, previous) + config_path = root / CONFIG_REL + current_config = config_path.read_text(encoding='utf-8') if config_path.exists() else '' + old_block = previous.get('config', {}).get('managed_block') if previous else None + config_text, block = plan_config(current_config, manifest.get('agents_config', {}), old_block) + hooks_path = root / HOOKS_REL + hooks, managed_hooks = merge_hooks( + load_target_hooks(hooks_path), desired_hooks(source, manifest), + previous.get('hooks', {}).get('entries', {}) if previous else None, + ) + state = { + 'format_version': 1, + 'product_version': manifest['product_version'], + 'source_commit': source_commit(source), + 'installed_at': previous.get('installed_at') if previous else utcnow(), + 'updated_at': utcnow(), + 'files': state_files, + 'config': {'managed_block': block, 'file_created_by_plananvil': not config_path.exists()}, + 'hooks': {'entries': managed_hooks, 'file_created_by_plananvil': not hooks_path.exists()}, + } + tx = Transaction() + state_path = root / STATE_REL + try: + for rel, src, _digest, owned in payload: + dest = target_path(root, rel) + if not owned and dest.exists(): + continue + tx.capture(dest) + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) + if config_text != current_config: + tx.capture(config_path) + write_bytes(config_path, config_text.encode('utf-8')) + hooks_data = json_bytes(hooks) + if not hooks_path.exists() or hooks_path.read_bytes() != hooks_data: + tx.capture(hooks_path) + write_bytes(hooks_path, hooks_data) + tx.capture(state_path) + write_bytes(state_path, json_bytes(state)) + except Exception: + tx.rollback() + raise + return state + + +def verify_installation(target: Path) -> dict[str, Any]: + root = git_root(target.resolve()) + state = read_state(root) + if state is None: + raise DistError('PlanAnvil is not installed') + problems: list[str] = [] + for raw, entry in state.get('files', {}).items(): + path = target_path(root, safe_rel(raw)) + if not path.is_file(): + problems.append(f'missing file: {raw}') + elif sha256_file(path) != entry.get('sha256'): + problems.append(f'hash mismatch: {raw}') + block = state.get('config', {}).get('managed_block') + if block: + path = root / CONFIG_REL + if not path.exists() or block not in path.read_text(encoding='utf-8'): + problems.append('managed .codex/config.toml block missing or changed') + entries = state.get('hooks', {}).get('entries', {}) + if entries: + path = root / HOOKS_REL + if not path.exists(): + problems.append('.codex/hooks.json missing') + else: + hooks = load_target_hooks(path)['hooks'] + for event, managed in entries.items(): + for entry in managed: + if entry not in hooks.get(event, []): + problems.append(f'managed hook missing: {event}') + return {'ok': not problems, 'product_version': state.get('product_version'), 'problems': problems} + + +def preflight_uninstall(root: Path, state: dict[str, Any]) -> None: + for raw, entry in state.get('files', {}).items(): + if not entry.get('owned'): + continue + path = target_path(root, safe_rel(raw)) + if path.exists() and (path.is_dir() or sha256_file(path) != entry.get('sha256')): + raise DistError(f'managed file was modified locally; refusing to remove: {raw}') + block = state.get('config', {}).get('managed_block') + path = root / CONFIG_REL + if block and path.exists() and block not in path.read_text(encoding='utf-8'): + raise DistError('managed .codex/config.toml block was modified') + hook_entries = state.get('hooks', {}).get('entries', {}) + hook_path = root / HOOKS_REL + if hook_entries and hook_path.exists(): + current_hooks = load_target_hooks(hook_path)['hooks'] + for event, managed in hook_entries.items(): + for entry in managed: + if entry not in current_hooks.get(event, []) and any( + HOOK_TOKEN in json.dumps(candidate, sort_keys=True) + for candidate in current_hooks.get(event, []) + ): + raise DistError(f'managed hook for {event} was modified') + + +def uninstall(target: Path, allow_dirty: bool) -> dict[str, Any]: + root = git_root(target.resolve()) + require_clean(root, allow_dirty) + state = read_state(root) + if state is None: + raise DistError('PlanAnvil is not installed') + preflight_uninstall(root, state) + tx = Transaction() + removed: list[str] = [] + state_path = root / STATE_REL + config_path = root / CONFIG_REL + hooks_path = root / HOOKS_REL + try: + for raw, entry in state.get('files', {}).items(): + if not entry.get('owned'): + continue + path = target_path(root, safe_rel(raw)) + if path.exists(): + tx.capture(path) + path.unlink() + removed.append(raw) + block = state.get('config', {}).get('managed_block') + if block and config_path.exists(): + tx.capture(config_path) + updated = strip_managed_block(config_path.read_text(encoding='utf-8'), block) + if updated.strip(): + write_bytes(config_path, updated.encode('utf-8')) + else: + config_path.unlink() + entries = state.get('hooks', {}).get('entries', {}) + if entries and hooks_path.exists(): + tx.capture(hooks_path) + value = load_target_hooks(hooks_path) + for event, managed in entries.items(): + current = value['hooks'].get(event, []) + for entry in managed: + if entry in current: + current.remove(entry) + if not current: + value['hooks'].pop(event, None) + if value['hooks'] or any(key != 'hooks' for key in value): + write_bytes(hooks_path, json_bytes(value)) + else: + hooks_path.unlink() + tx.capture(state_path) + state_path.unlink() + except Exception: + tx.rollback() + raise + for directory in sorted({(root / safe_rel(raw)).parent for raw in removed} | {state_path.parent}, key=lambda p: len(p.parts), reverse=True): + current = directory + while current != root: + try: + current.rmdir() + except OSError: + break + current = current.parent + return {'ok': True, 'removed_files': sorted(removed)} + + +def parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description='PlanAnvil repository distribution manager') + p.add_argument('--source', type=Path, default=Path(__file__).resolve().parents[1]) + sub = p.add_subparsers(dest='command', required=True) + for name in ('install', 'upgrade', 'uninstall'): + cmd = sub.add_parser(name) + cmd.add_argument('--target', type=Path, default=Path.cwd()) + cmd.add_argument('--allow-dirty', action='store_true') + for name in ('verify', 'status'): + cmd = sub.add_parser(name) + cmd.add_argument('--target', type=Path, default=Path.cwd()) + return p + + +def main(argv: list[str] | None = None) -> int: + args = parser().parse_args(argv) + try: + if args.command == 'install': + result = install_or_upgrade(args.source, args.target, False, args.allow_dirty) + elif args.command == 'upgrade': + result = install_or_upgrade(args.source, args.target, True, args.allow_dirty) + elif args.command == 'uninstall': + result = uninstall(args.target, args.allow_dirty) + elif args.command == 'verify': + result = verify_installation(args.target) + if not result['ok']: + print(json.dumps(result, indent=2, sort_keys=True)) + return 2 + elif args.command == 'status': + root = git_root(args.target.resolve()) + state = read_state(root) + result = {'installed': state is not None, 'state': state} + else: + raise AssertionError(args.command) + except DistError as exc: + print(f'PlanAnvil distribution error: {exc}', file=sys.stderr) + return 2 + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From a798d0ff7f4106b69543b2cd1b130c70414107fe Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:06:02 +0200 Subject: [PATCH 20/36] Add distribution lifecycle regression tests --- tests/test_distribution.py | 158 +++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/test_distribution.py diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..7b43522 --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import json +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'tools')) + +import plananvil_dist + + +def run(*args: str, cwd: Path) -> subprocess.CompletedProcess[str]: + result = subprocess.run(args, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if result.returncode != 0: + raise AssertionError(f"command failed: {' '.join(args)}\nstdout={result.stdout}\nstderr={result.stderr}") + return result + + +def init_repo(path: Path, *, config: str | None = None, hooks: dict | None = None) -> Path: + path.mkdir(parents=True) + run('git', 'init', '-b', 'main', cwd=path) + run('git', 'config', 'user.name', 'PlanAnvil Test', cwd=path) + run('git', 'config', 'user.email', 'plananvil-test@example.invalid', cwd=path) + (path / 'README.md').write_text('# Fixture\n', encoding='utf-8') + (path / 'AGENTS.md').write_text('USER_OWNED_AGENTS\n', encoding='utf-8') + if config is not None: + p = path / '.codex/config.toml' + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(config, encoding='utf-8') + if hooks is not None: + p = path / '.codex/hooks.json' + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(hooks, indent=2, sort_keys=True) + '\n', encoding='utf-8') + run('git', 'add', '-A', cwd=path) + run('git', 'commit', '-m', 'fixture', cwd=path) + return path + + +def commit_all(repo: Path, message: str) -> None: + run('git', 'add', '-A', cwd=repo) + run('git', 'commit', '-m', message, cwd=repo) + + +def synthetic_source(source: Path, version: str) -> Path: + source.mkdir(parents=True) + manifest = json.loads((ROOT / 'distribution/manifest.json').read_text(encoding='utf-8')) + for raw in manifest['copy_roots'] + [manifest['hooks_source']]: + src = ROOT / raw + dest = source / raw + if src.is_dir(): + shutil.copytree(src, dest) + else: + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) + (source / 'distribution').mkdir(parents=True, exist_ok=True) + manifest['product_version'] = version + (source / 'distribution/manifest.json').write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n', encoding='utf-8') + return source + + +class DistributionTests(unittest.TestCase): + def test_install_verify_and_uninstall_clean_repo(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + repo = init_repo(Path(tmp) / 'repo') + original_agents = (repo / 'AGENTS.md').read_bytes() + state = plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + self.assertEqual(state['product_version'], '0.2.0') + self.assertTrue((repo / '.agents/skills/plan-anvil/SKILL.md').is_file()) + self.assertTrue((repo / '.plananvil/installation.json').is_file()) + self.assertTrue(plananvil_dist.verify_installation(repo)['ok']) + self.assertEqual((repo / 'AGENTS.md').read_bytes(), original_agents) + commit_all(repo, 'install plananvil') + result = plananvil_dist.uninstall(repo, False) + self.assertTrue(result['ok']) + self.assertFalse((repo / '.agents/skills/plan-anvil/SKILL.md').exists()) + self.assertFalse((repo / '.plananvil/installation.json').exists()) + self.assertEqual((repo / 'AGENTS.md').read_bytes(), original_agents) + + def test_existing_config_and_hooks_are_preserved(self) -> None: + config = '[agents]\nenabled = true\nmax_concurrent_threads_per_session = 2\n\n[other]\nvalue = "keep"\n' + custom_hook = { + 'metadata': {'owner': 'fixture'}, + 'hooks': { + 'PreToolUse': [ + {'matcher': '^Custom$', 'hooks': [{'type': 'command', 'command': 'echo custom'}]} + ] + }, + } + with tempfile.TemporaryDirectory() as tmp: + repo = init_repo(Path(tmp) / 'repo', config=config, hooks=custom_hook) + plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + self.assertEqual((repo / '.codex/config.toml').read_text(encoding='utf-8'), config) + merged = json.loads((repo / '.codex/hooks.json').read_text(encoding='utf-8')) + self.assertEqual(merged['metadata'], {'owner': 'fixture'}) + self.assertIn(custom_hook['hooks']['PreToolUse'][0], merged['hooks']['PreToolUse']) + commit_all(repo, 'install plananvil') + plananvil_dist.uninstall(repo, False) + final_hooks = json.loads((repo / '.codex/hooks.json').read_text(encoding='utf-8')) + self.assertEqual(final_hooks, custom_hook) + self.assertEqual((repo / '.codex/config.toml').read_text(encoding='utf-8'), config) + + def test_conflicting_disabled_agents_fails_without_changes(self) -> None: + config = '[agents]\nenabled = false\n' + with tempfile.TemporaryDirectory() as tmp: + repo = init_repo(Path(tmp) / 'repo', config=config) + before = run('git', 'status', '--porcelain', cwd=repo).stdout + with self.assertRaises(plananvil_dist.DistError): + plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + self.assertEqual((repo / '.codex/config.toml').read_text(encoding='utf-8'), config) + self.assertEqual(run('git', 'status', '--porcelain', cwd=repo).stdout, before) + + def test_upgrade_replaces_only_unmodified_owned_payload(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + repo = init_repo(root / 'repo') + plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + commit_all(repo, 'install plananvil') + source = synthetic_source(root / 'source', '0.2.1') + skill = source / '.agents/skills/plan-anvil/SKILL.md' + skill.write_text(skill.read_text(encoding='utf-8') + '\n\n', encoding='utf-8') + state = plananvil_dist.install_or_upgrade(source, repo, True, False) + self.assertEqual(state['product_version'], '0.2.1') + self.assertIn('upgrade fixture', (repo / '.agents/skills/plan-anvil/SKILL.md').read_text(encoding='utf-8')) + self.assertTrue(plananvil_dist.verify_installation(repo)['ok']) + + def test_upgrade_refuses_locally_modified_managed_file(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + repo = init_repo(root / 'repo') + plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + commit_all(repo, 'install plananvil') + path = repo / '.agents/skills/plan-anvil/SKILL.md' + path.write_text(path.read_text(encoding='utf-8') + '\nLOCAL CHANGE\n', encoding='utf-8') + source = synthetic_source(root / 'source', '0.2.1') + with self.assertRaises(plananvil_dist.DistError): + plananvil_dist.install_or_upgrade(source, repo, True, True) + self.assertIn('LOCAL CHANGE', path.read_text(encoding='utf-8')) + + def test_uninstall_refuses_locally_modified_managed_file(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + repo = init_repo(Path(tmp) / 'repo') + plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + commit_all(repo, 'install plananvil') + path = repo / '.codex/hooks/plan-anvil-guard.py' + path.write_text(path.read_text(encoding='utf-8') + '\n# local\n', encoding='utf-8') + with self.assertRaises(plananvil_dist.DistError): + plananvil_dist.uninstall(repo, True) + self.assertTrue(path.exists()) + self.assertTrue((repo / '.plananvil/installation.json').exists()) + + +if __name__ == '__main__': + unittest.main() From 88afc406588c5854d517965f57d56eb469c62c0a Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:06:14 +0200 Subject: [PATCH 21/36] Add release engineering tests --- tests/test_release_engineering.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_release_engineering.py diff --git a/tests/test_release_engineering.py b/tests/test_release_engineering.py new file mode 100644 index 0000000..afb3a9b --- /dev/null +++ b/tests/test_release_engineering.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import tempfile +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'tools')) + +import build_release +import prepare_capabilities +import release_check +import validate_capabilities + + +class ReleaseEngineeringTests(unittest.TestCase): + def test_capability_templates_materialize_complete_hash_consistent_packages(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + prepared = Path(tmp) / 'prepared' + prepare_capabilities.materialize(ROOT, prepared, force=True) + self.assertEqual(validate_capabilities.validate_all(prepared), []) + + def test_candidate_release_metadata_passes_but_live_gate_remains_closed(self) -> None: + candidate = release_check.release_blockers(ROOT, require_reproduced=False) + self.assertEqual(candidate, []) + strict = release_check.release_blockers(ROOT, require_reproduced=True) + self.assertTrue(any('required capability' in item for item in strict), strict) + + def test_release_archive_is_deterministic(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + first = Path(tmp) / 'first' + second = Path(tmp) / 'second' + a = build_release.build(ROOT, first) + b = build_release.build(ROOT, second) + self.assertEqual(Path(a['archive']).read_bytes(), Path(b['archive']).read_bytes()) + self.assertEqual(a['sha256'], b['sha256']) + self.assertTrue((first / 'SHA256SUMS').is_file()) + self.assertTrue((first / 'release-notes.md').is_file()) + + +if __name__ == '__main__': + unittest.main() From db979d09210c8786a5358826aea8d0d7cb497ce1 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:10:07 +0200 Subject: [PATCH 22/36] Make capability template transport chunk-safe --- tools/prepare_capabilities.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/prepare_capabilities.py b/tools/prepare_capabilities.py index ee37b1e..3e29402 100644 --- a/tools/prepare_capabilities.py +++ b/tools/prepare_capabilities.py @@ -7,7 +7,7 @@ import tarfile from pathlib import Path, PurePosixPath -ARCHIVE = Path('capabilities/templates.tar.gz.b64') +PART_GLOB = 'templates.part*' def _safe_member(name: str) -> PurePosixPath: @@ -20,7 +20,12 @@ def _safe_member(name: str) -> PurePosixPath: def materialize(source_root: Path, target_root: Path, *, force: bool = False) -> list[str]: source_root = source_root.resolve() target_root = target_root.resolve() - raw = base64.b64decode((source_root / ARCHIVE).read_text(encoding='ascii')) + part_dir = source_root / 'capabilities' + parts = sorted(part_dir.glob(PART_GLOB)) + if not parts: + raise FileNotFoundError('capability template parts are missing') + encoded = ''.join(part.read_text(encoding='ascii').strip() for part in parts) + raw = base64.b64decode(encoded, validate=True) written: list[str] = [] with tarfile.open(fileobj=io.BytesIO(raw), mode='r:gz') as tar: members = [m for m in tar.getmembers() if m.isfile()] @@ -36,6 +41,8 @@ def materialize(source_root: Path, target_root: Path, *, force: bool = False) -> raise FileExistsError(f'refusing to overwrite existing capability evidence: {target}') target.write_bytes(data) written.append(rel.as_posix()) + # The index and package guide are tracked outside the archive and are needed + # when materializing into a disposable validation/sandbox root. for rel in (Path('capabilities/index.json'), Path('capabilities/README.md')): source = source_root / rel target = target_root / rel From f65a9b7d3a27b12432d9258c1d9ee44df27e58ca Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:10:39 +0200 Subject: [PATCH 23/36] Document chunked capability templates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cbd229..8e12301 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ python tools/release_check.py --candidate - `docs/RELEASE.md` — deterministic gates, tag workflow and publication contract - `docs/CODEX_SANDBOX_RUNBOOK.md` — exact remaining C01-C16 sandbox sequence -- `capabilities/templates.tar.gz.b64` + `tools/prepare_capabilities.py` — deterministic prepared C01-C16 fixtures/prompts/config/assertions/results/hashes +- `capabilities/templates.part*` + `tools/prepare_capabilities.py` — deterministic prepared C01-C16 fixtures/prompts/config/assertions/results/hashes A production tag is rejected by `.github/workflows/release.yml` until every required capability is `REPRODUCED`. From d5185c1f75f443d7ac29f20b02b67ff6d500d762 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:10:53 +0200 Subject: [PATCH 24/36] Document chunked capability template archive --- capabilities/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capabilities/README.md b/capabilities/README.md index 0eb8bfd..6575c7c 100644 --- a/capabilities/README.md +++ b/capabilities/README.md @@ -2,7 +2,7 @@ This directory stores sanitized, reproducible evidence for the release gate in `docs/CODEX_CAPABILITY_BASELINE.md`. -`capabilities/templates.tar.gz.b64` contains deterministic prepared C01-C16 packages. Before a live qualification run, materialize them on the evidence branch: +`capabilities/templates.part*` contains deterministic prepared C01-C16 packages. Before a live qualification run, materialize them on the evidence branch: ```text python tools/prepare_capabilities.py --force From f19af6ca38ba8d76f60db1b80eb018252df4fac0 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:11:11 +0200 Subject: [PATCH 25/36] Replace truncated capability archive with chunked parts --- capabilities/templates.tar.gz.b64 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 capabilities/templates.tar.gz.b64 diff --git a/capabilities/templates.tar.gz.b64 b/capabilities/templates.tar.gz.b64 deleted file mode 100644 index b960bcf..0000000 --- a/capabilities/templates.tar.gz.b64 +++ /dev/null @@ -1 +0,0 @@ -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 From c6c085c42c41df970fc29c1d3097ab36b5a966ce Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:11:38 +0200 Subject: [PATCH 26/36] Add capability template part 01 --- capabilities/templates.part01 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part01 diff --git a/capabilities/templates.part01 b/capabilities/templates.part01 new file mode 100644 index 0000000..62e8a37 --- /dev/null +++ b/capabilities/templates.part01 @@ -0,0 +1 @@ +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 From 59ea17ee5fe0f3ba45e97627546dc948346cfdd4 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:12:01 +0200 Subject: [PATCH 27/36] Add capability template part 02 --- capabilities/templates.part02 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part02 diff --git a/capabilities/templates.part02 b/capabilities/templates.part02 new file mode 100644 index 0000000..5a773d7 --- /dev/null +++ b/capabilities/templates.part02 @@ -0,0 +1 @@ +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 From 5063cbc00f847ef7a132dce05e8965885cfb9835 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:12:28 +0200 Subject: [PATCH 28/36] Add capability template part 03 --- capabilities/templates.part03 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part03 diff --git a/capabilities/templates.part03 b/capabilities/templates.part03 new file mode 100644 index 0000000..8c1a9c3 --- /dev/null +++ b/capabilities/templates.part03 @@ -0,0 +1 @@ +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 From 71809fcd060434f1e0dab8b4f52230d5f9684df4 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:12:52 +0200 Subject: [PATCH 29/36] Add capability template part 04 --- capabilities/templates.part04 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part04 diff --git a/capabilities/templates.part04 b/capabilities/templates.part04 new file mode 100644 index 0000000..7499c06 --- /dev/null +++ b/capabilities/templates.part04 @@ -0,0 +1 @@ +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 From 3b3b32abaea15f1e6562555b411c26d6dcacad8a Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:13:21 +0200 Subject: [PATCH 30/36] Add capability template part 05 --- capabilities/templates.part05 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part05 diff --git a/capabilities/templates.part05 b/capabilities/templates.part05 new file mode 100644 index 0000000..e4da080 --- /dev/null +++ b/capabilities/templates.part05 @@ -0,0 +1 @@ +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 From 4ac0a3670304e35ed199e0fddf4c19cd5bf9eb30 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:13:40 +0200 Subject: [PATCH 31/36] Add capability template part 06 --- capabilities/templates.part06 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part06 diff --git a/capabilities/templates.part06 b/capabilities/templates.part06 new file mode 100644 index 0000000..c41fcdf --- /dev/null +++ b/capabilities/templates.part06 @@ -0,0 +1 @@ +rxSPft8s6OSz0jYN9R1G/g8l/m+z/wby/zT+t83/1vIv9xL/V6pt/rfF/x/M/yt7/neJHIyXSzJVP3WcR6fBz1nhUbcC+8Wgvy8uJr2zekLzuhS9GtQ2lHcPetahravk8sZv/8w1eskisLEMtcnornmWK03VNcn8Id/Fs51GBzc6+J50sByEDt4a/w+F/23+/+H8f/vjf2Xjfw/M/tsD/2u3438Z0+R/QPuv8b4v/uZT/Q+F/G/8/nP7foZgb//uV+X/2wf+6bf2vm/9nwPX/G/jfH5fuEazqmrq1muohmY8I8N11X8rq6O+V6L6iM74Zqtv4FmdJH9mlyn3nYtiOJbwx+Z9P4Hz+ji0Cln0eJbqTJDfuH7/tYsH0HprVl46rA2ZUeKbC49LqQVxPnp/wqNQxjP1Ml80beDuZzrvzLHfiwe7D8eAdvrQDG1i25H8f/K+9Yf2nxf8b0P5v/O+K/z1g3/Gw8v92vDjuR6x7jwF38r9ebPn/VLP/B0ncKceTqqh/nY252/91tNhU3fz78gCAv454Pv0z9/F1yLW/9uH+Vw7uv658/X8dzZca+a+bw0anj/86Wnbrtkh7qPp/L/yvbfzvXu3/j+J/LWmPGGoAWgwAUhRb4+kqsIovCSeENsWEQDJrHdBAysSTPSCtktLiTv5XEohSlNaUkjbJ+1RDxaroijK6gLACyRDGhEkFgd6L6BM/nLwoES3t4n+zcM4VEZ0IKLi0RZAVLhvrkVLxhiRXR2PyxhvjTBJWKRWjSi47y6XZwf8aBZyrhVCiqVvbfAahtFLFIxczC11UkSpBrhSsUypkRapSsFEJJYS6lf8NLEWULQWLOlDKQmbvg7PGWOvBFbQObKGcogGTwQZRTDYGkFvFKad28r++cA2liYQ8v4OkucldjfiL3CrOFzKoncq2CJM0kdOoi0FPKkhPErO8nvkV86F7m0IrkCaARUOeRErWaZ9D5Apxf7E2UCj8TlwwEQwEbVSNhiyTQKAs6Wb8ly9b7lIydTGaAZXKMkiI2QZtlTYhW0nSaUg2BhlCbQdZXCabuMGz++pDAG+N//vgf/0N/G/b/z1I2sH/uke7mKCTw+ZsGg/8kfJ/KPxvk/+B/D+N/22ppZZaaqmlllpqqaWWWmqppZZaaqmlllpqqaWWWmqppZZaaqmlllr6ktP/B7hMANkA+AIA From 9aa8d53f565584210714731ef0e195b5ab5ac85a Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:14:43 +0200 Subject: [PATCH 32/36] Restore complete capability template part 06 --- capabilities/templates.part06 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capabilities/templates.part06 b/capabilities/templates.part06 index c41fcdf..dbed0fb 100644 --- a/capabilities/templates.part06 +++ b/capabilities/templates.part06 @@ -1 +1 @@ -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 From c7f186eec6160d7c494253f56a0c27123a5236cc Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:15:13 +0200 Subject: [PATCH 33/36] Add capability template part 07 --- capabilities/templates.part07 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part07 diff --git a/capabilities/templates.part07 b/capabilities/templates.part07 new file mode 100644 index 0000000..e0ffb88 --- /dev/null +++ b/capabilities/templates.part07 @@ -0,0 +1 @@ +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 From b6fdfb1566368996c1038bcfd893536ac9f30d6f Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:15:36 +0200 Subject: [PATCH 34/36] Add capability template part 08 --- capabilities/templates.part08 | 1 + 1 file changed, 1 insertion(+) create mode 100644 capabilities/templates.part08 diff --git a/capabilities/templates.part08 b/capabilities/templates.part08 new file mode 100644 index 0000000..0b8ada9 --- /dev/null +++ b/capabilities/templates.part08 @@ -0,0 +1 @@ +v3td/7+J/+3dJTyJ7xmgpe+nE9U6UZ9V6WZNnGlRLe3JeN7dd/q+j+PbOVNYA8/na3n++0230hnMfs7TXycP5yzXCLMVLpyXur/Dgkf5YtZBwpcelVVmL1YemjXWN553/PE5Teonl5hit1jEN3axqHYiw/bDkeEPd5Yd+ECyJf/74H+lafF/92r/N/53Hf93j87ig5L/vgWOa73vOwjcGf/XXF//tbLxH0PJ/8ubuno90qYKdxfttx7pXIWCNehpAtbttaOzJHfLtXRpIiwt8urFq1t+6jHQVbzaIuuXpP8PJf5vk/8B7f+Piv+rAhTUGGLJBApsEckGhSXEoIhSlo6KlwBWZSz8u88OQy7ORRKy3M3/mmiEFuQlWbC6YEwKKwMsLGsMa1wAD4VNRQKZQCtT2VWLArlQrD6i3sX/JkqFM1dCihAFxpy8Bn6QCtai09ZEL13gcoeEQUgng0skSiCnDT8+7OB/JckQkgkqhRgLBR2M5QEWZdY1LrIJFUaVwQvFrZFdDgGUdzqQjVxqk2/lf2PRFj1yAdHHEgxI/iQhRdI5+lCSp8A5G61FsaRksraQzAjFI+TLgMs38r8gg3YquhSdcVzFZIvSEkROgh8oJcoIznlbMoZEUPHXEpQpWXktcvBb/O8V86ErPr85I0HF4ITTGaKv0YOd8AFL9I6zKpYLkTnLSFz2VLjs2mernOSK38IABy+9TxmVMwEzSEWx1LDQmGVK2dgUMqWkiubelC0qwy9SYN1hZFCniC3+77Xxfx/8r1Xb/G/b/zlI2sH/2ke/gwk62TNn06jfTyT/h8L/NvtvoPlf43/b/G8t/24v/K/Z5n9lW/8dzP/rOv63+kg3/LAstYnVKCxm49++MOC3R0b74YhtiDpUrfzYWxUEnE3nbFRcnC+XtHoH8jV6dr55SvOMynwZgoZlZrUFaQl5PuwJ2If1KOcVTbs6GWveeODGA9+XB3aflgfeGv8Phf9t87/h/H/7439d438PzP47FP63yf+A9l/jfxv/u5T/Q+F/G/8/nP7foZgb//uV+X/2wf+6bf2vm/9nwPX/G/jfH5fuEazqmrq1muohmY8I8N11X8rq6O+V6L6iM74Zqtv4FmdJH9mlyn3nYtiOJbwx+Z9P4Hz+ji0Cln0eJbqTJDfuH7/tYsH0HprVl46rA2ZUeKbC49LqQVxPnp/wqNQxjP1Ml80beDuZzrvzLHfiwe7D8eAdvrQDG1i25H8f/K+9Yf2nxf8b0P5v/O+K/z1g3/Gw8v92vDjuR6x7jwF38r9ebPn/VLP/B0ncKceTqqh/nY252/91tNhU3fz78gCAv454Pv0z9/F1yLW/9uH+Vw7uv658/X8dzZca+a+bw0anj/86Wnbrtkh7qPp/L/yvbfzvXu3/j+J/LWmPGGoAWgwAUhRb4+kqsIovCSeENsWEQDJrHdBAysSTPSCtktLiTv5XEohSlNaUkjbJ+1RDxaroijK6gLACyRDGhEkFgd6L6BM/nLwoES3t4n+zcM4VEZ0IKLi0RZAVLhvrkVLxhiRXR2PyxhvjTBJWKRWjSi47y6XZwf8aBZyrhVCiqVvbfAahtFLFIxczC11UkSpBrhSsUypkRapSsFEJJYS6lf8NLEWULQWLOlDKQmbvg7PGWOvBFbQObKGcogGTwQZRTDYGkFvFKad28r++cA2liYQ8v4OkucldjfiL3CrOFzKoncq2CJM0kdOoi0FPKkhPErO8nvkV86F7m0IrkCaARUOeRErWaZ9D5Apxf7E2UCj8TlwwEQwEbVSNhiyTQKAs6Wb8ly9b7lIydTGaAZXKMkiI2QZtlTYhW0nSaUg2BhlCbQdZXCabuMGz++pDAG+N//vgf/0N/G/b/z1I2sH/uke7mKCTw+ZsGg/8kfJ/KPxvk/+B/D+N/22ppZZaaqmlllpqqaWWWmqppZZaaqmlllpqqaWWWmqppZZaaqmlllr6ktP/B7hMANkA+AIA From 492f8a872c152095481891c1b78eaa3279e5c005 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:18:58 +0200 Subject: [PATCH 35/36] Cover monorepo install and transactional rollback --- tests/test_distribution.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_distribution.py b/tests/test_distribution.py index 7b43522..b9ec15e 100644 --- a/tests/test_distribution.py +++ b/tests/test_distribution.py @@ -7,6 +7,7 @@ import tempfile import unittest from pathlib import Path +from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / 'tools')) @@ -153,6 +154,39 @@ def test_uninstall_refuses_locally_modified_managed_file(self) -> None: self.assertTrue(path.exists()) self.assertTrue((repo / '.plananvil/installation.json').exists()) + def test_nested_target_installs_at_monorepo_git_root(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + repo = init_repo(Path(tmp) / 'repo') + nested = repo / 'packages/app' + nested.mkdir(parents=True) + (nested / 'README.md').write_text('# App\n', encoding='utf-8') + commit_all(repo, 'add monorepo package') + plananvil_dist.install_or_upgrade(ROOT, nested, False, False) + self.assertTrue((repo / '.agents/skills/plan-anvil/SKILL.md').is_file()) + self.assertTrue((repo / '.plananvil/installation.json').is_file()) + self.assertFalse((nested / '.plananvil/installation.json').exists()) + self.assertTrue(plananvil_dist.verify_installation(nested)['ok']) + + def test_failed_payload_copy_rolls_back_partial_install(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + repo = init_repo(Path(tmp) / 'repo') + real_copy = plananvil_dist.shutil.copy2 + calls = {'count': 0} + + def fail_on_second_copy(src, dst, *args, **kwargs): + calls['count'] += 1 + if calls['count'] == 2: + raise OSError('injected copy failure') + return real_copy(src, dst, *args, **kwargs) + + with patch.object(plananvil_dist.shutil, 'copy2', side_effect=fail_on_second_copy): + with self.assertRaises(OSError): + plananvil_dist.install_or_upgrade(ROOT, repo, False, False) + self.assertFalse((repo / '.agents/skills/plan-anvil').exists()) + self.assertFalse((repo / '.plananvil/installation.json').exists()) + self.assertFalse((repo / '.codex/hooks/plan-anvil-guard.py').exists()) + self.assertEqual(run('git', 'status', '--porcelain', cwd=repo).stdout, '') + if __name__ == '__main__': unittest.main() From 5584a756592481401c86faf0436983e178aaba39 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:21:43 +0200 Subject: [PATCH 36/36] Restore created directories on transaction rollback --- tools/plananvil_dist.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tools/plananvil_dist.py b/tools/plananvil_dist.py index a27922f..d6f5f4d 100644 --- a/tools/plananvil_dist.py +++ b/tools/plananvil_dist.py @@ -101,12 +101,22 @@ class Snapshot: class Transaction: - def __init__(self) -> None: + def __init__(self, root: Path) -> None: + self.root = root.resolve() self.snapshots: dict[Path, Snapshot] = {} + self.created_dirs: list[Path] = [] def capture(self, path: Path) -> None: if path in self.snapshots: return + cursor = path.parent + missing: list[Path] = [] + while cursor != self.root and not cursor.exists(): + missing.append(cursor) + cursor = cursor.parent + for directory in reversed(missing): + if directory not in self.created_dirs: + self.created_dirs.append(directory) if path.exists(): if path.is_dir(): raise DistError(f'expected file, found directory: {path}') @@ -123,6 +133,11 @@ def rollback(self) -> None: path.unlink() except OSError: pass + for directory in sorted(set(self.created_dirs), key=lambda item: len(item.parts), reverse=True): + try: + directory.rmdir() + except OSError: + pass def load_manifest(source: Path) -> dict[str, Any]: @@ -364,7 +379,7 @@ def install_or_upgrade(source: Path, target: Path, upgrade: bool, allow_dirty: b 'config': {'managed_block': block, 'file_created_by_plananvil': not config_path.exists()}, 'hooks': {'entries': managed_hooks, 'file_created_by_plananvil': not hooks_path.exists()}, } - tx = Transaction() + tx = Transaction(root) state_path = root / STATE_REL try: for rel, src, _digest, owned in payload: @@ -451,7 +466,7 @@ def uninstall(target: Path, allow_dirty: bool) -> dict[str, Any]: if state is None: raise DistError('PlanAnvil is not installed') preflight_uninstall(root, state) - tx = Transaction() + tx = Transaction(root) removed: list[str] = [] state_path = root / STATE_REL config_path = root / CONFIG_REL