Skip to content

ADR 0264: Tracked refresh convergence precedes landing

Status: accepted; generalizes the currency check from ADR 0034, extends the receipt boundary from ADR 0067, narrows the post-landing refresh from ADR 0098, and extends update regeneration from ADR 0247

Context

§

discern refresh maintains more tracked state than compiled Agent files. Its tracked effects include the managed .gitattributes block, provider MCP and settings entries, hooks, app worktree configuration, project rules, and the maintained ADR index. Materialized skills and Git-local receipt transport are separate local effects.

The gate checked several of those artifacts independently, but not the complete set. In particular, changing a [generated] path changed the expected .gitattributes block without making done red. A branch could therefore earn a clean gate receipt and land with stale committed attributes. Acceptance then ran the full refresh after fast-forwarding the trunk. That repaired the main checkout's bytes but could not put them into the already-validated commit, so it left the trunk checkout dirty. Its tracked-clean baseline was sampled after refresh; because the checkout was already dirty, the later check skipped instead of attributing the mutation.

This was a sequencing defect, not an attributes-only defect. Any tracked writer absent from the pre-landing currency checks could follow the same path. Adding one filename check would preserve the class.

Three constraints bind the repair:

  • done remains observational for refresh artifacts. It must show the pending effect, not overwrite a hand edit or auto-commit derived output.
  • An honored receipt proves the gate that issued it. A newer engine may add a cheap invariant that an older receipt never checked, so acceptance still needs current boundary preconditions.
  • Acceptance cannot undo a completed fast-forward when local convergence fails. The safe design must prevent tracked refresh work before that boundary and confine post-landing refresh to local effects.

Decision

§

A branch may earn or use landing proof only when the current read-only tracked-refresh plan is empty. The plan is the final net set of byte and mode effects an ordinary discern refresh would apply to files already tracked by Git.

The plan and apply paths share their transformations:

  • Provider MCP, hooks, worktree-app, and project-rules writers run through one ordered reconciler. Live refresh gives it real file effects; the planner gives it an in-memory overlay. Co-owners see earlier proposed writes in either mode.
  • Agent files use the same renderer as refresh and include executable-bit drift because refresh normalizes them to 0644.
  • .gitattributes uses its existing pure file plan, and the ADR index uses its existing expected-state computation.
  • The plan reports final paths, effect kind, and whether bytes or mode change. A malformed or unreadable tracked input is a planning error and fails closed.
  • Missing or not-yet-adopted outputs retain the existing policy: status may advise their first materialization, but a new standalone output does not become a convergence failure until the project tracks it. A first discern registration inside a shared MCP file remains setup/refresh work even when that container is tracked. The planner runs the same MCP writer against HEAD to distinguish that case from deletion of a committed registration; later in-place drift and deletion are guarded.

The lifecycle binds that one predicate at these points:

  1. status remains read-only and returns pending_tracked_refresh plus any tracked_refresh_plan_errors. Focused legacy fields for guidance, hooks, and the ADR index remain compatible projections.
  2. done evaluates the plan as a fail-fast precondition and again after all gate jobs, immediately before result and receipt construction. A non-empty or unprovable plan fails as refresh_drift, names the exact paths, and prescribes discern refresh → review → commit → discern done.
  3. accept repeats the current plan before the fast-forward on both paths, including an honored receipt. This preserves old receipts while preventing them from bypassing a newer cheap invariant.
  4. After a successful fast-forward, acceptance records tracked cleanliness immediately. It materializes only local/ignored Agent artifacts, then runs repository ensure and smoke. It never invokes a tracked refresh writer in the receiving checkout. Any tracked dirt present immediately after checkout, or introduced later by ensure/smoke, is reported without undoing the landing.
  5. update remains the effectful convergence boundary inside a worktree. After a real merge it commits every tracked path the live refresh actually changed, not only Agent files, the ADR index, and .gitattributes. On an already-up-to-date pass it does not create a bookkeeping commit: it reports the exact refreshed tracked paths and leaves them for review and an intentional commit.
  6. doctor --verbose reports these as execution-model boundaries, not incidental implementation details: done shows its initial and receipt-boundary checks, accept shows its current-engine landing check before the fast-forward, and update shows regeneration plus the merge-only commit or no-merge report behavior. Those ordered cores derive from the same pure plans as dry-run output.

The explicit noes: done does not run or commit refresh; accept does not repair tracked artifacts after landing; the gate does not infer currency from a hand-maintained filename list; provider files are not wholesale-generated or allowed to discard user-owned keys; and this decision does not try to police external .gitattributes rules outside discern's managed block.

Consequences

§
  • A green done result has a concrete convergence meaning: rerunning refresh cannot change a tracked artifact. A clean green run can therefore stamp a receipt without hiding derived work.
  • The original relocation case now fails before the expensive gate and names .gitattributes. Running refresh in the worktree produces the reviewable diff that belongs in the branch commit.
  • Old receipts remain useful. Acceptance still skips the expensive gate, but current cheap invariants can refuse them non-destructively.
  • Post-landing refresh can no longer dirty the trunk with a tracked file. The main checkout still gets its checkout-local skills, and repository ensure/smoke retain their fail-open-after-landing behavior.
  • Update's regeneration commit is slightly broader but safer: enrollment comes from paths the shared live reconciler actually changed after a clean merge. Shared provider files keep user content through their merge functions before being staged.
  • Status and done pay for a read-only provider reconciliation. The work is file reads and in-memory transforms, substantially cheaper than any declared gate job, but it is more work than the old collection of partial checks.
  • First-materialization policy remains asymmetric by design. A missing artifact that has not yet been adopted is advisory; malformed tracked input and drift in an adopted tracked artifact block.
  • A human or agent can inspect the complete ordering through discern doctor --verbose; adding a step to the canonical done, update, or accept plan without enrolling that model fails the execution-model guard.

Alternatives considered

§
  • Add .gitattributes to the existing gate checks. Rejected because MCP settings, worktree-app config, project rules, and the next tracked refresh writer would retain the same hole.
  • Run full refresh inside done. Rejected because a gate would erase or rewrite evidence before the agent reviewed it, and a mutating check cannot prove the committed tree without another commit-and-gate lap.
  • Keep full post-landing refresh and auto-commit its changes on trunk. Rejected because acceptance would land a commit the gate never validated, then manufacture a second commit nobody reviewed. It also races other work landing on the shared trunk.
  • Trust any honored receipt without a current preflight. Rejected because receipts deliberately survive engine upgrades; making their meaning depend forever on the issuing binary would strand every newly added cheap safety invariant.
  • Never write tracked refresh output during update. Rejected because update is already the explicit merge-and-converge operation. Regenerating after composition and committing those derived paths is what makes the merged branch the tree the next done validates.
choose openEsc close