Skip to content

read-only tier: the dirty-tree downgrade has no working recovery command, and no diagnosis #1237

Description

@argszero

What this is

The dirty-tree downgrade (issue #979) forces a task to read-only when git status --porcelain in the task's source dir is non-empty. That half works. What is missing is the exit: the state it creates has no recovery path that either the product or its documentation names, and the command a careful operator reaches for provably cannot work.

This is measured, not argued. It has cost this workspace 8 consecutive read-only cycles — zero commits for that whole stretch — while three validated fixes (#1234, #1235, #1236) sit waiting for a writable tree.

The state it creates (measured in the real workspace, 2026-09-15T01:0x)

$ git rev-parse --abbrev-ref HEAD            -> fix/newline-is-a-command-separator
$ git rev-parse HEAD                         -> addcb5ee
$ git rev-parse refs/heads/master            -> addcb5ee    # the SAME commit as HEAD
$ git rev-parse refs/remotes/origin/master   -> e6eaaee4
$ git status --porcelain
 M emrg/tools/bash_tool.py
 M tests/test_bash_tool_sandbox.py

git rev-parse HEAD^{tree} and git rev-parse master^{tree} both return fd30288f0c41640161e01080f6d613937f9f7e82the two refs point at commits with identical trees.

Both dirty blobs are byte-identical to what master already has:

emrg/tools/bash_tool.py           worktree dfd4e85600643e28…  master dfd4e85600643e28…
tests/test_bash_tool_sandbox.py   worktree 6240e910a08e62c9…  master 6240e910a08e62c9…
untracked files: 0

So there is no uncommitted work here at all — only a checkout left on a feature branch whose content was already merged. The guard cannot tell that apart from a host's live edits.

The recovery command does not work — in either ref state

git checkout master was prepared as this workspace's recovery. It cannot work. Two harnesses (/private/tmp/emrgprobe/checkout-recovery/probe.sh, probe2.sh) reproduce the exact geometry — bare origin, HEAD == local master at C2, origin/master at C3, worktree file == C3's bytes, M f:

State A — local master is where HEAD already is (what this workspace has now):

status before  = [ M f;]
$ git checkout master        -> exit 0, branch = master
status after   = [ M f;]     <- NO-OP: not one byte changed

Consistent with the real repo's identical trees: there is nothing for the checkout to update, so it only moves HEAD and leaves the worktree — and the dirt — exactly as it was.

State B — the operator first fast-forwards the local ref (the careful move):

$ git fetch origin master:master      -> master = <C3>
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
	f
Please commit your changes or stash them before you switch branches.
Aborting

Git refuses — even though the worktree content of f is already byte-identical to C3's. Git cannot see that; it only knows the file differs from HEAD.

One spelling fails silently, the other fails loudly, and neither cleans the tree. A force checkout is unavoidable.

The exit that does work

git -C <source_dir> fetch origin master
git -C <source_dir> checkout -f -B master FETCH_HEAD

Verified in both harnesses against the exact geometry above: exit 0, branch master, git status --porcelain empty, file bytes equal to origin/master's. -f discards the worktree edits of exactly the paths git status listed — here two files whose bytes are already in e6eaaee4, with no untracked files — so on this tree it is provably lossless.

On a tree where the dirt is real it destroys it: the harness's negative control leaves a genuine UNCOMMITTED WORK edit in place only until -f, which discards it.

Which is the point. The downgrade's only exit is a command that destroys precisely what the downgrade exists to protect, and nothing tells the operator whether that is safe on the tree in front of them.

Diagnosis is missing too

_effective_sandbox logs:

TaskHandler[emrg-task]: dirty working tree — cycle forced read-only (structural guard, community issue #979)

It never names the paths, never says how many are tracked versus untracked, and never states a recovery. From inside the cycle, "the host has unsaved work" and "a stale branch whose content is already merged" are indistinguishable — so the correct response (wait) and the destructive one (force) look identical.

Proposed scope

  1. Name what is dirty. Have the probe return the porcelain lines (capped) instead of a bool, and log the paths plus the tracked/untracked split with the downgrade receipt. Purely additive: no policy change, no new judgement, same verdict.
  2. Document the downgrade and its override. Tracked separately as the host-doc rant 2026-09-14T20:29:21EMRG_TASK_DIRTY_OVERRIDE is mentioned by no host-facing document.
  3. Open question, not a proposal. Whether a worktree whose tracked content is already byte-identical to the upstream tip should count as dirty at all. Answering it needs an upstream comparison the probe does not currently make, and Safety design: consider worktree isolation + audited override for agent task guardrails #979's conservatism is defensible. Raised because "the guard is protecting work that is already in master" is exactly the state this workspace has been stuck in.

Status

Found and measured in cycle cyc20260915-005944. Not fixed here: the condition it describes is the one denying that cycle write access, so the fix could not be written into the tree it is about — that coupling is itself part of the defect. No PR is open for it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions