Skip to content

Phase 2, wave 3 ​

Fast repeats ​

The supplied wave 2 release trace measured 24.4s total / 21.1s write on an unchanged repository, with per-branch tree, commit, history, PR and viewer reads. Successful writes now store a receipt containing the verified commit/tree OIDs, input identity, final file diffs and canonical PR state. A fresh paginated GraphQL inventory reads heads, commit metadata and associated open PRs, including title, body, author, head/base, labels and native auto-merge state. The inventory also checks the default head and detects obsolete marked heads outside the current prefix. All heads are inventoried so prefix changes and legacy names cannot hide obsolete work.

A matching receipt skips branch verification, tool execution and PR reconciliation. A mixed run also skips matching branches individually. Changed heads, plans, policies, PR edits/closure, missing PRs, ambiguous PRs, truncated connections and obsolete heads fall back to ordinary reconciliation or fail closed. Behind mode invalidates on base changes. Conflicts mode retains an unchanged manifest on an older base; tool-generated branches regenerate whenever the base changes so changed lockfiles or source imports cannot be overlooked. Copied trailers never establish a receipt. Only verified writes or complete ownership/tree validation can establish one. Remaining REST reads revalidate cached validators; writes still use atomic branch/base leases.

Offline evidence: verified_receipts_reduce_warm_write_to_one_inventory_and_detect_changes uses one query, zero mutations, 200ms simulated elapsed with 200ms request latency. It covers head, body, title, labels, merge state, closure, base and plan changes, obsolete heads and pagination. This is synthetic evidence, not a live speed claim. The first upgraded run must establish receipts. No installation token is configured in this implementation session, so no live -vv run was made.

Wave 3 live fixes ​

The supplied steady trace measured 22.146s total / 19.943s write and 134 network requests. The cache was inspected read-only: all 16 open-PR receipts existed under the branch-prefix key with the current base. Their PR head/base fields contained the full REST objects (including repository metadata), while the inventory reconstructed only ref/SHA/repository identity. Those objects could never compare equal. The synthetic server had returned minimal REST objects, masking the mismatch. Receipt state now explicitly selects the same fields from both APIs; tests include extra REST fields.

Seven manually closed targets also remained planned until full PR-history inventory. Version 3 receipts retain their verified closed PR IDs, target intent and body. The same inventory query checks those nodes for closed/unmerged state, author, repository and unchanged marker/body. Reopening, merging, deletion, ownership/body edits or a new target invalidates the proof. The first upgraded run performs full verification and seeds v3 receipts, including existing branches that were not created in this run. No cache migration or live cache write was performed during implementation.

Inventory cleanup detection excludes the default branch and tips whose author/committer/signature already rule out ownership. A foreign actor's branch does not force repeated verification; plausible obsolete owned tips still fall back to full history inspection and leased cleanup.

After publishing a head, PR reconciliation accepts the published commit or polls the exact previously verified head. Six waits of 100/200/400/800/1600/3200ms allow seven fresh reads. An unfamiliar head fails immediately; exhaustion reports propagation timeout rather than claiming the head moved. Every read rechecks ownership and closure. An owner closure during publication or obsolete-PR inspection produces skipped with a closed-by-owner reason and preserves the PR number. It does not fail the repository. Policy/limit/closed-target skips are non-errors; transport/API failures, unexpected heads, exhausted propagation, ownership changes and protected histories still fail the repository.

Offline measurement: the complete engine creates once, reopens its persistent SQLite cache in a fresh engine, then makes exactly four requests: repository metadata, default-branch head, npm lookup and one inventory query. Both open and closed cases take 600ms simulated total at 200ms/request, with metadata/head reads overlapping lookup. The write inventory alone takes 200ms, zero mutations. Tests also cover receipt adoption, foreign obsolete tips, pagination, actual rebases, propagation timeout and closure while polling. These are deterministic offline measurements, not new live timings.

For the supplied sandbox state after v3 receipts are seeded, an unchanged run needs exactly one write-phase POST /graphql query (one refs page plus the seven closed PR nodes), with no per-branch ref/commit/tree/compare/pull requests, PR-history/branch-list/viewer requests, release-note reads or mutations. The supplied lookup phase made 24 network requests and 22 fresh cache hits: one repository-head GraphQL query, five GitHub release-list GETs, three GitHub tag-ref GETs, one Node catalog GET, one Cargo sparse GET and thirteen Docker manifest HEADs. With the same freshness and credential conditions, that projects to 25 total network requests, ten to GitHub. A reusable credential-scoped branch hint replaces the one repository-head query with two conditional REST reads (repository metadata and branches/main), giving 26 total, eleven to GitHub. Expired catalogs, changed dependencies/base/PRs, additional refs pages, authentication retries or another actor's concurrent edits change those counts.

Pin wording ​

Pin-only commits and titles use <commitPrefix> pin dependencies. Pin rows stay in the table, while both release-note requests and unavailable-note notices are omitted for those rows. Mixed updates can still show notes for their version updates. A wording change refreshes the branch/PR without changing closed-target suppression.

SWE-109 ​

The cache's sibling <cache-stem>.worktrees directory holds one bare partial clone per repository. Initialization requests blob:none; only a changed base needs an explicit shallow fetch. A repository lock protects shared clone operations. Each branch gets a separate temporary tree and Git index. Only manifests, lockfiles, mise version files, Rust/Go source needed for resolution, and other files explicitly edited by the plan are checked out. Blob hashes must agree with Git and the API extraction before edits are applied. Symlinks, special files, unexpected generated changes, oversized files and non-UTF-8 output fail the branch. The source tree is never pushed or committed with Git.

Tools are installed into the runner's own tool store: PATCHULA_TOOLS_DIR, or <cache>.tools beside the SQLite cache (for example .patchula-cache.tools). It is separate from the operator's mise installs and persists across runs, so mount it as a volume in containers. Every successful tool run records its tool@version with a timestamp in patchula-usage.json. patchula tools install prewarms every pinned default (Rust excepted, which runs through rustup), for example in an image build, since some downloads are slow (the static PHP host served about 44 KB/s when measured). patchula tools prune --days 30 uninstalls versions no run has used for that long; pinned defaults are never pruned. In CI, mise-action caches the same task-scoped toolchains the real-tool tests install.

Tools run through mise --no-config exec with an explicit tool version. Only version strings or version-table entries for relevant tools are read from ancestor repository mise.toml / .mise.toml files. Repository environment blocks, hooks and unrelated tools are not loaded. Defaults live in crates/update/src/lockfiles/defaults.toml: node 24.21.0, Bun 1.4.3, Rust 1.99.0, Go 1.27.2, Terraform 1.14.7, aube 2.8.0, Yarn Classic 1.22.22 and Yarn Berry 4.18.1. Rust uses its pinned rustup toolchain behind mise while keeping Cargo's configuration and credentials home isolated. Go is also pinned in this repository's mise config for offline fixture tests.

The commands are:

text
bun install --cwd <root> --lockfile-only --ignore-scripts
aube -C <root> install --lockfile-only --ignore-scripts
npm install --prefix <root> --package-lock-only --ignore-scripts --no-audit --no-fund
yarn install --ignore-scripts                            # Yarn Classic 1
yarn install --mode=update-lockfile                      # Yarn Berry 2+
cargo update --manifest-path <root>/Cargo.toml -p <crate>@<old> --precise <target>
go -C <root> get <module>@<target>
go -C <root> mod tidy       # lockfiles.tidyGo=true
go -C <root> mod download   # otherwise
terraform -chdir=<root> providers lock
uv lock --directory <root> --upgrade-package <package>   # maintenance: --upgrade
poetry lock --no-interaction --no-plugins --directory <root>   # maintenance: --regenerate

uv and Poetry resolve against Python from mise: the nearest .python-version, then a repository pin, then the runner default (3.14.8). uv never downloads its own interpreter (UV_PYTHON_DOWNLOADS=never), Poetry runs without plugins and without creating a virtualenv. Poetry 2's poetry lock keeps existing pins and re-resolves changed constraints. A targeted upgrade or manifest edit that leaves the lock unchanged fails the branch, and the requested version must be the one the regenerated lock records.

pnpm-lock.yaml, package-lock.json and npm-shrinkwrap.json are regenerated by aube, which reads and writes the committed format in place. aube is told the committed lockfile name, so a second lockfile in the same directory cannot redirect it, and a branch that gains an aube-lock.yaml fails. The lockfiles.npmEngine setting selects "aube" (the default) or native "npm" for npm lockfiles only; pnpm always uses aube. Native npm also regenerates the npm lockfiles aube 2.8.0 cannot keep valid: format v1 locks, which aube cannot read, and trees whose manifests declare file: or link: dependencies, whose entries aube drops so npm ci rejects the lock (aube#1796). aube 2.8.1 fixed that, and only v1 locks still go to native npm; see the lockfiles guide. Yarn's version comes from the packageManager field of package.json, then the repository pin, then the format of the committed lockfile. Yarn Berry has no --ignore-scripts flag and runs --mode=update-lockfile, which updates the lockfile without linking node_modules, so nodeLinker: pnp projects are supported. Yarn reads registries, cache keys and catalogs from .yarnrc.yml, so the checkout includes it. Yarn runs with YARN_IGNORE_PATH=1 on a scratch copy without yarnPath, plugins and ${VAR} values: a committed Yarn release and plugins are repository code, and interpolations name credentials the runner never passes. The committed file is restored before the tree is collected. A catalog edit in pnpm-workspace.yaml or .yarnrc.yml counts as a dependency change for the unchanged-lockfile rule.

Cargo toolchain-only changes use cargo update --workspace; maintenance uses untargeted cargo update. Both npm and Bun locks are regenerated when both are present. Maintenance refreshes a lockfile within the manifest ranges: aube update --lockfile-only --no-save, npm update --package-lock-only, a regenerated Bun lock, and Yarn Classic install --ignore-scripts. Terraform regenerates provider selections from the manifest constraints without initializing or accessing a backend. Missing module initialization, unavailable providers or other tool errors are branch failures.

Every process starts from an environment allowlist with an isolated HOME, npm config, Cargo home and Go paths. Git alone receives a host-scoped temporary HTTP Basic header in its process environment for installation-token access. It is absent from remote URLs, argv, stored Git config and every ecosystem process. Git errors redact the header, encoded credentials and token. Parent tokens, cloud credentials, Git helpers and tool configuration are not inherited. Yarn keeps its own cache and global folder inside the isolated HOME and never receives registry credentials. Each command has a 180s deadline; timeout kills the process group on Unix. Output is drained with bounded buffers and retained in diagnostics only on failure (at most 4KiB stdout plus 4KiB stderr). Successful scratch trees are removed, as are failed scratch trees. Only the partial clone and tool cache remain. No API commit/ref is created until every tool for that branch succeeds and the selected manifest/lock resolution still matches its requested targets. Lock additions and deletions join the manifest in the same verified API commit.

Text Bun locks, npm package locks/shrinkwrap, pnpm locks, Yarn Classic and Berry locks, Cargo, Go, Terraform, uv.lock and poetry.lock are covered. A lockfile that does not change after a manifest edit that changes recorded dependencies is a branch failure, so a tool that reports "up to date" without re-resolving cannot publish a stale lock; an edit outside the recorded dependencies cannot affect the lock and is not a failure. Binary bun.lockb remains unsupported. The retained --allow-lockfile-pending flag is an explicit manifest-only testing escape hatch for that format; supported formats always require successful tooling.

lockfiles.refresh=true maintains <prs.branch>lock-file-maintenance (default patchula/lock-file-maintenance). It groups all enabled ecosystems, uses default labels/merge policy, and reports the configured lockfile group and changed files. Maintenance runs at most once per ISO week, respecting waitHours since the last successful completion. The default eligible window is Monday 00:00–04:00 UTC; explicit default update windows replace it. The CLI must be invoked during the window; it does not install a scheduler. The desired branch remains reserved between windows so an outstanding PR is preserved. Failures do not consume the successful-maintenance checkpoint. Closed maintenance updates can retry in a later week.

Offline tests use local path packages, a local vendored Cargo registry, a local Git remote and disabled tool auto-installation. They exercise actual npm, aube, Bun, Yarn, Cargo and Go commands, deterministic output, targeted Cargo/Go updates, clone reuse/incremental fetch, environment scrubbing, tool failure without API writes, output bounds, timeouts, weekly maintenance and API additions/deletions. The aube tests regenerate real pnpm and npm lockfiles from local path packages; pnpm itself is not a test dependency, so the byte-for-byte comparison against pnpm was made in a scratch project. Terraform command construction/failure boundaries are covered without contacting a provider registry; live registry/signature behavior remains a sandbox check.

Sandbox expectations and prerequisites ​

The supplied report pins Bun 1.1.30 in mise.toml, predating text bun.lock support. Repository pins are honored: generation reports an actionable branch error rather than silently choosing a newer tool. Recommended prerequisite: merge the existing patchula/oven-sh-bun-1.x PR targeting Bun 1.4.3, then rerun. This also satisfies the default-tool policy without adding an override.

With compatible tools and unchanged catalogs/config, seven additional dependency branches and PRs are eligible:

text
patchula/github-com-google-uuid-1.x
patchula/go-1.x
patchula/hono-4.x
patchula/typescript-5.x
patchula/typescript-7.x
patchula/zod-3.x
patchula/zod-4.x

Prettier joins the existing pin branch, bringing it to six table rows; it does not get a separate PR. If the Bun update is merged first, that resolved PR/head leaves the active set: the supplied 16-open baseline would usually become 22 open PRs after seven new ones, before other merged updates, limits or catalog changes. Without the Bun prerequisite, only the two Go branches can proceed; Bun-bearing plans, including the combined pin branch, fail without moving existing heads.

Cargo has no update proposals in the supplied report: anyhow/serde resolve to current eligible versions. Do not expect invented Cargo PRs. A newer eligible target will use its normal dependency branch with a generated Cargo.lock; scheduled refresh can independently maintain it when enabled. Terraform likewise needs an eligible provider plan or enabled maintenance.

Commands ​

From the repository root, with an installation token supplied through the existing environment variable (never written in commands or files):

sh
CARGO_NET_OFFLINE=true mise run ci
CARGO_NET_OFFLINE=true mise exec -- cargo test --locked -p patchula-update --test write verified_receipts -- --nocapture
mise exec -- cargo build --release --locked -p patchula-cli
mise exec -- target/release/patchula -vv run --dry-run --json --cache sandbox.sqlite OWNER/SANDBOX > sandbox-wave3-dry.json 2> sandbox-wave3-dry.trace
mise exec -- target/release/patchula -vv run --json --cache sandbox.sqlite OWNER/SANDBOX > sandbox-wave3-run.json 2> sandbox-wave3-run.trace
mise exec -- target/release/patchula -vv run --json --cache sandbox.sqlite OWNER/SANDBOX > sandbox-wave3-repeat.json 2> sandbox-wave3-repeat.trace

Compare repeat write/total timing and request traces. Expect one write-phase inventory query per page and no REST reads or mutations when every planned branch/PR matches a receipt. Ordinary lookup conditional requests remain. The dry run performs no tool execution or write-phase requests; generated lockfile diffs are only known after a real run.

Open decisions: merge the existing Bun update before the live sweep (recommended) or explicitly design a minimum-tool override; retain the Monday maintenance default or choose explicit update windows; inspect Terraform provider locking in a live sandbox before broad rollout. Private registries/modules will fail with the scrubbed environment: scoped registry credential injection should be a separately designed opt-in, not inherited process secrets.

Validation ​

CARGO_NET_OFFLINE=true mise run ci passed: 356 tests across 61 suites, zero failures and one intentionally ignored recording test. The fixture scan checked 22 cassettes; formatting, Clippy with -D warnings, tests and the workspace build passed. Full log: /tmp/ci.log; stage logs are kept under $PATCHULA_CI_LOG_DIR (default /tmp/patchula-ci.XXXXXX). mise run docs regenerated the CLI reference. No live sandbox mutation or push was performed.

CI tail:

text
[build] $ cargo build --workspace --locked
   Compiling patchula-update v0.1.0 (crates/update)
   Compiling patchula-conformance v0.1.0 (conformance)
   Compiling patchula-engine v0.1.0 (crates/engine)
   Compiling patchula-server v0.1.0 (crates/server)
   Compiling patchula-cli v0.1.0 (crates/cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.63s