E3 — Delivery Implementation Plan

For agentic workers: implement this plan task by task. Do not import the old harness/ package or copy its code. The old repository is evidence for behavior and incidents, not source.

Goal: Deliver satyrn-engine deliver --repo REPO [--timeout SECONDS] CONTRACT -- COMMAND [ARG ...]: run one synchronous command in a detached worktree pinned to the repository’s exact HEAD, then emit one exact receipt without modifying the caller’s working tree, index, branch, or HEAD. A successful changed tree also publishes one reviewable candidate ref; every other handled attempt publishes none.

Architecture: delivery.py owns the delivery state machine and its frozen receipt. The existing check() seam still parses the contract and performs the E1 repository-path check. A small internal Git boundary invokes real Git; it is not injectable, and its real Git subprocess behavior is exercised by the marked integration tier. The command runs without a shell in a new POSIX process group. The worktree is removed before the candidate ref is created atomically. cli.py owns the literal -- split and renders the receipt as the only stdout record.

Tech stack: Python 3.14 standard library, Git, pytest, pytest-cov, ruff, pyrefly, and the existing Sphinx documentation stack. No new runtime dependency.

Spec: docs/superpowers/specs/2026-08-18-e3-delivery-design.md

Correction — 2026-08-19: The accepted plan originally prescribed direct pipe forwarding, an engine-owned empty hooks directory, and setting the cleanup guard only after confirmed worktree registration. Review replaced those steps with a temporary-file spool and synchronous process-group teardown, core.hooksPath=/dev/null plus core.fsmonitor=false on every engine-owned Git call, and an uncertain guard set before the mutating git worktree add. These changes prevent descendant-held descriptor blocking, suppress publication and fsmonitor hooks for the full operation, and cover interruption after Git registers a worktree but before Python sees the return. Production still exposes no injected Git runner or failure hook; marked integration tests exercise real Git and process behavior, while default-tier failure tests monkeypatch private production functions only for destructive or non-portable abnormal branches. The current tasks below are normative.

Phase-size decision

Keep E3 as one phase, implemented in six reviewable tasks below. Although the Git and process details are substantial, every task enforces one user-visible promise: one attempt always becomes a receipt while the caller’s checkout remains untouched, and only a successful changed tree also becomes a candidate ref.

Split or defer work immediately if implementation requires any of the following: a platform-neutral process abstraction, a persistent supervisor, model or Pi knowledge, writable-path policy, validation policy, a second public command, or a generalized candidate-management API. Those are not supporting details of E3; they belong to E4–E6 or a separately proposed phase.

Global constraints

  • The supported implementation and evidence target is POSIX, a normal non-bare repository, and a synchronous trusted command. Windows packaging is E6.

  • Worktree isolation is not a security sandbox. Do not add a container, namespace, filesystem interposer, or claim about adversarial commands.

  • The command is invoked once, with its exact argv, shell=False, DEVNULL stdin, the isolated worktree as cwd, and a new POSIX session/process group.

  • On timeout, signal the process group with SIGTERM, allow five seconds, then signal any remaining group with SIGKILL and reap the direct child before Git cleanup.

  • candidate-created, discarded, and refused remain the only receipt outcomes. The specific code, not a fourth outcome, carries the cause.

  • All handled non-candidate results after CLI acceptance exit NO_CANDIDATE = 8; inherited E1 refusals keep their existing values.

  • The candidate ref is refs/satyrn/candidates/<contract-id>/head; ids are rejected, never normalized. Publication is create-if-absent with git update-ref --no-deref REF COMMIT "".

  • The default test tier starts no process and opens no network connection. Real Git and command behavior live only in @pytest.mark.integration tests.

  • A refusal test always has a sibling success. Integration tests use temporary local repositories and the production CLI/library seams, never a fake Git runner or injected failure backend.

  • The earlier local-ai-pi repository is consulted at immutable revision 8588ba4, especially harness/candidate.py, harness/processes.py, the two-repository rewrite, and HARVEST-INDEX.md. No implementation is copied.


Task 1: Freeze the public receipt and CLI boundary

Files:

  • Modify: src/satyrn_engine/exits.py

  • Modify: src/satyrn_engine/cli.py

  • Modify: tests/test_exits.py

  • Create: src/satyrn_engine/delivery.py

  • Create: tests/test_delivery.py

  • Create: tests/fixtures/delivery/receipt-ok.json

  • Create: tests/fixtures/delivery/receipt-repo-dirty.json

  • Create: tests/fixtures/delivery/receipt-command-failed.json

  • Create: tests/fixtures/delivery/receipt-candidate-exists.json

  • Create: tests/fixtures/delivery/receipt-cleanup-failed.json

Produces:

  • ExitCode.NO_CANDIDATE = 8, without renumbering any existing member.

  • A frozen DeliveryReceipt with the exact fields and nullability from the spec, plus one deterministic compact-JSON renderer.

  • Closed DeliveryCode and DeliveryOutcome enums, a typed JSON payload, and complete code-to-outcome and code-to-exit mappings. outcome is derived, rather than accepted independently by the receipt constructor.

  • Pure timeout validation accepting finite positive values only.

  • A CLI parser that requires a literal --, preserves every command token after it, and reports missing separator/command as argparse USAGE = 2 without producing a receipt.

Steps:

  1. Add failing default-tier tests for the complete exit table, all five exact receipt fixtures, valid/default timeout, invalid timeout values, a missing separator, an empty command, and exact argv preservation.

  2. Add NO_CANDIDATE and implement only the frozen receipt plus pure parsing helpers. Do not start Git or a command from these tests.

  3. Make main() dispatch shape explicit while keeping delivery execution unwired until Task 2. Existing check and protocol behavior must remain byte-for-byte compatible.

  4. Run the default suite, ruff, and pyrefly. Record one success fixture and one refusal fixture by name.

Evidence: parsing deliver --repo . contract.yaml -- tool --flag returns the command tuple ("tool", "--flag"); parsing without -- fails with exit 2. The committed OK and COMMAND_FAILED receipt bytes match the renderer.

Commit: feat(delivery): freeze the E3 receipt and CLI boundary


Task 2: Refuse before creating a worktree

Files:

  • Modify: src/satyrn_engine/check.py

  • Modify: src/satyrn_engine/delivery.py

  • Modify: src/satyrn_engine/cli.py

  • Modify: tests/test_check.py

  • Create: tests/test_integration_delivery.py

Produces:

  • deliver(repo, contract_path, command, timeout) -> DeliveryReceipt as the library seam.

  • Contract-first E1 refusals with contract_id populated whenever parsing succeeded, including REPO_UNAVAILABLE.

  • Git-root resolution, exact HEAD^{commit} capture, full dirty-state refusal, candidate-id validation, and early existing-ref refusal.

  • A sanitized engine/command environment that removes repository-local routing variables reported by git rev-parse --local-env-vars, plus GIT_NAMESPACE, while preserving ordinary configuration and PATH.

Steps:

  1. Add a sibling test showing check() retains the parsed contract on a repository-path refusal; this is required by the receipt’s nullability rule.

  2. Add integration helpers that initialize a temporary local Git repository, create one base commit, invoke the real CLI, and parse exactly one stdout receipt.

  3. Add failing integration pairs for a clean repository versus dirty tracked, deleted, and untracked state; repository root versus a subdirectory; ordinary root versus linked worktree root; valid id versus / and Git-invalid ids; absent versus existing ordinary, symbolic, and dangling-symbolic refs.

  4. Implement preflight using byte-oriented Git output and --no-optional-locks status --porcelain=v1 -z --untracked-files=all --ignore-submodules=none. The input repository shown in the receipt is an absolute normalized path without resolving its symlink spelling; Git operations use the canonical root.

  5. Wire the CLI to print exactly one receipt and return the receipt’s exit code.

  6. Run the default suite plus the focused integration tests. Confirm every refusal leaves the source status and HEAD unchanged and creates no engine-owned worktree.

Evidence: one clean-root invocation reaches the later delivery boundary; REPO_DIRTY and INVALID_CANDIDATE_ID refuse before worktree registration and match their receipt fields exactly.

Commit: feat(delivery): refuse unsafe repositories and candidate ids


Task 3: Run one isolated attempt and create one candidate

Files:

  • Modify: src/satyrn_engine/delivery.py

  • Modify: tests/test_integration_delivery.py

Produces:

  • A unique temporary parent verified outside every registered worktree, with a detached linked worktree at the captured base commit.

  • One exact command invocation whose stdout/stderr are spooled outside engine memory and forwarded in chunks to stderr while stdout remains receipt-only.

  • POSIX process-group timeout teardown.

  • Explicit typed states for uncertain Git registration and process cleanup, with a minimal process protocol and an immutable teardown result.

  • Staging, explicit tree/commit creation, deterministic changed-path extraction, worktree removal, and create-if-absent candidate publication.

Steps:

  1. Add a successful fixture command that modifies, adds, and deletes files. Assert the candidate commit has exactly BASE as parent, contains the three changes, is reachable by the expected ref, and leaves the caller’s tree, index, branch, and HEAD unchanged.

  2. Add sibling failure fixtures for no change, nonzero exit, missing executable, timeout, and a command that moves the isolated HEAD. Assert exact codes, no candidate ref, and no registered worktree.

  3. Add the timeout descendant proof: the command starts a same-process-group child that would write a delayed sentinel, then exceeds the timeout. Assert COMMAND_TIMEOUT, absence of the sentinel after its delay, and cleanup.

  4. Implement registration-aware cleanup. Set the cleanup guard to uncertain immediately before the mutating worktree-add call, refine it after Git’s registration query, and clear it only after Git confirms absence. This covers interruption after Git mutates shared metadata but before Python sees the return. Do not use a temporary-directory context manager that deletes a retained or uncertain registered worktree after cleanup failure.

  5. Create the commit with write-tree and commit-tree, explicit parent, fixed engine identity, disabled signing, and a message containing candidate id and base SHA. Derive final paths from diff-tree -z, not pre-commit filesystem observation.

  6. Remove the worktree before publishing the ref. If command or Git handling fails, route through the same cleanup and apply CLEANUP_FAILED precedence.

  7. Run the focused integration tests repeatedly to demonstrate that no delayed process writes after timeout.

Evidence: the success receipt names a readable candidate commit/ref and three canonical paths; the timeout receipt names no candidate and its delayed sentinel never appears.

Commit: feat(delivery): create candidates in isolated worktrees


Task 4: Prove atomic publication and cleanup precedence

Files:

  • Modify: src/satyrn_engine/delivery.py

  • Modify: tests/test_integration_delivery.py

Produces:

  • Race-safe create-if-absent publication with diagnostic re-read after a failed compare-and-swap.

  • Correct differentiation of CANDIDATE_EXISTS from GIT_FAILED without claiming retry safety.

  • Visible retained-path reporting when a locked worktree cannot be removed.

  • Unexpected-exception cleanup through the same registered-worktree guard.

Steps:

  1. Add two real CLI processes synchronized inside their attempt commands so both target the same absent candidate id. Assert exactly one OK, exactly one CANDIDATE_EXISTS, one published ref, and an unchanged caller checkout.

  2. Add an ancestor-ref collision whose sibling candidate id succeeds. Assert the collision is GIT_FAILED, not mislabeled as candidate existence.

  3. Add a command that locks its own worktree before returning. Assert CLEANUP_FAILED supersedes the pending result, reports the retained path, and publishes no candidate. The test must explicitly unlock/remove/prune in its own teardown.

  4. Exercise a real failure after worktree registration and confirm the common cleanup path. Do not add an injected Git runner solely to manufacture the failure.

  5. Implement update-ref --no-deref with an empty expected-old object id. After failure, inspect symbolic and ordinary state; an existing ref is CANDIDATE_EXISTS, every absent or unreadable result is GIT_FAILED.

  6. Run focused integration tests several times so the race proof is not an ordering accident.

Evidence: the two-process fixture always produces one winner and one loser; the locked-worktree fixture retains a real path and proves result precedence.

Commit: fix(delivery): make publication and cleanup outcomes atomic


Task 5: Close the supported Git and environment edges

Files:

  • Modify: src/satyrn_engine/delivery.py

  • Modify: tests/test_integration_delivery.py

Produces:

  • Canonical UTF-8 byte ordering for receipt paths and refusal of non-UTF-8 candidate paths before publication.

  • Engine Git operations that cannot be redirected by inherited local Git routing variables or execute repository, reference-transaction, or external fsmonitor hooks.

  • Engine Git operations that ignore inherited author and committer dates while the trusted command continues to receive them.

  • Preserved ordinary Git configuration and filters.

Steps:

  1. Add a UTF-8 success fixture whose path ordering differs between locale/text sorting and raw UTF-8 byte sorting. Assert the specified byte order.

  2. On POSIX, create a non-UTF-8 path through a trusted fixture command. Assert GIT_FAILED, no publication, and cleanup; pair it with the UTF-8 success.

  3. Configure a hook sentinel and a harmless repository filter. Assert the hook never fires during engine Git operations while the filter still participates successfully in checkout/staging.

  4. Invoke delivery with inherited GIT_DIR, GIT_WORK_TREE, and GIT_NAMESPACE pointing elsewhere. Assert Git still discovers the isolated worktree and publishes in the intended repository.

  5. Confirm all Git subprocesses use the canonical cwd split from the spec and byte-oriented capture where paths are involved.

Evidence: the UTF-8 candidate succeeds in byte order; the non-UTF-8 sibling refuses without a ref; the hook sentinel remains absent while the filter-backed candidate succeeds.

Commit: fix(delivery): harden Git environment and path handling


Task 6: Document the behavior and record the phase

Files:

  • Modify: README.md

  • Modify: ROADMAP.md

  • Modify: CLAUDE.md

  • Modify: docs/architecture.md

  • Modify: docs/usage.md

  • Modify: docs/glossary.md

  • Modify: docs/contributing.md

  • Modify: docs/index.md

  • Modify: docs/superpowers/index.md

  • Modify: pyproject.toml

  • Modify: src/satyrn_engine/__init__.py

Produces:

  • Contributor-facing documentation of why the design uses a separate worktree, detached HEAD, a pinned base commit, one synchronous attempt, and a dedicated non-branch ref.

  • An explicit trust boundary: worktree isolation is not a sandbox; unrestricted absolute paths and shared Git mutation remain trusted until E4/E5 constrain the model tool surface.

  • Honest prior-art framing: worktree isolation exists elsewhere; E3 applies it to single-attempt safety and a dedicated candidate-ref namespace.

  • Correct provenance instructions pointing at local-ai-pi@8588ba4; a visible correction in CLAUDE.md for its stale commit/guard-copy wording.

  • candidate, receipt, and worktree isolation definitions added to the concept budget and glossary only now that E3 implements them.

  • Integration-marker help that describes local Git/process requirements rather than claiming every integration test needs network access.

Steps:

  1. Add CLI usage examples for candidate creation, inspection with git show, and explicit deletion with git update-ref -d. Do not document merge or automatic apply behavior that E3 does not have.

  2. Add an architecture section explaining the snapshot-isolation analogy and each design choice’s reason. Cite the old evidence and verified external prior art; do not copy unverified claims from a PR comment.

  3. Update the roadmap and SDD record with the commands and named success/failure fixtures actually run. Preserve corrections rather than rewriting them away.

  4. Correct stale repository instructions before they can direct a later worker to the wrong provenance or claim the guards were already copied.

  5. Run the full default suite, the E3 integration file, ruff, pyrefly, strict Sphinx build, combined statement/branch coverage, and git diff --check.

  6. Review the final diff against the phase-size decision. Move anything about Pi, validation, mutation policy, model quality, candidate management, or Windows to its named later phase.

Evidence: one documented success command creates a candidate ref and one documented failure command emits a stable receipt; strict docs and all code checks pass from a clean checkout.

Commit: docs: explain and record E3 delivery


Completion commands

uv run pytest
uv run pytest -m integration tests/test_integration_delivery.py -v
uv run pytest -m "" --cov
uv run ruff check .
uv run pyrefly check
uv run --group docs sphinx-build -W -b html docs docs/_build/html
git diff --check

The final report names the exact candidate-created fixture, at least one discarded fixture, at least one refused fixture, the two-process race result, and the process-group descendant sentinel result. It does not claim model improvement, sandboxing, validation, Windows support, or packaging.