Skip to content

fix(repo): pin a --repo worktree to the fetched revision, refuse an unknown base - #83

Merged
AetherAI3 merged 1 commit into
mainfrom
fix/repo-exact-remote-base
Aug 19, 2026
Merged

fix(repo): pin a --repo worktree to the fetched revision, refuse an unknown base#83
AetherAI3 merged 1 commit into
mainfrom
fix/repo-exact-remote-base

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

Problem

--repo could report a freshly fetched base and then start work days behind it.

PR #73 added remote validation and a fetch, and recorded "pinning the worktree to freshness.remoteTip explicitly" as a follow-up. That follow-up is load-bearing.

git fetch advances remote refs and FETCH_HEAD. It does not move the mirror's checked-out HEADrefreshMirror says so in its own contract: "never checks out, resets, merges, pulls or cleans."

And worktreeAddArgs built:

["-C", repoRoot, "worktree", "add", "-b", branch, dir]   // no start point

code.ts read co.freshness.remoteTip only to print it.

The reachable shape:

mirror HEAD   A
origin/main   A-B-C
FETCH_HEAD    C
printed       "(fetched) @ C"
worktree cut  A          ← days behind what was just reported

The summary this project shipped — "--repo fetches instead of branching off a stale copy" — was true of the fetch and false of the worktree.

Contract

1. The start point is explicit. worktreeAddArgs and createWorktree take an optional startRevision, appended last so git reads it as the start point. Omitted, behaviour is unchanged — a plain --worktree run still branches from the user's own checkout, which is the intended base there.

2. An unnameable base is refused, not warned about. Previously an unfetchable mirror printed a warning and carried on. Proceeding is the dangerous half: it starts work on an unknown base immediately after printing a reassuring line. Now:

✗ refusing to start: the base for owner/repo is not known to match the remote.
  Could not resolve host: github.com
  a worktree cut now would branch off whatever the mirror already had.
  reconnect and retry, or work in a local checkout you control.

Exit 1. No worktree, no run.

Tests

Three argv tests, plus a real-git canary that asserts the resulting checkout rather than the arguments — because arguments were never the thing in doubt.

It builds a remote at A, clones it, advances the remote to C, fetches, then:

  • asserts FETCH_HEAD == C
  • asserts the mirror's HEAD is still Athis is the defect itself, pinned as a fixture
  • cuts the worktree
  • requires git rev-parse HEAD inside the worktree to equal C, and to not equal A

Mutation-checked, and this is the evidence the bug was real: reverting worktreeAddArgs to drop the start point fails the canary with the worktree must start at the fetched revision — the worktree lands on A. Restored: 19/19.

command result
npm run typecheck exit 0
npm test 1090 pass / 0 fail

Why refuse rather than warn

Fail-open was defensible while the base was merely unverified. It is not defensible once we know the worktree silently starts from a stale commit — the failure is invisible, the output actively reassuring, and the resulting branch is what a user then opens a PR from.

If an override is wanted later it should be an explicit flag with its own name, not the absence of a check.

Scope

src/core/worktree.ts, src/commands/code.ts, test/worktree.test.ts. No change to a plain --worktree run, and no change to the fetch/validation contract from #73.

Credit

Found in review of merged main at 37fff3c, not by this project's own tests — which is worth recording, because #73's tests asserted the argv and the freshness report, and both were correct. Nothing asserted the commit the worktree actually landed on. That gap is what the new canary closes.

…nknown base

PR #73 added remote validation and a fetch, and recorded "pinning the worktree
to freshness.remoteTip explicitly" as a follow-up. That follow-up turns out to
be load-bearing, not cosmetic.

`git fetch` advances remote refs and FETCH_HEAD. It does NOT move the mirror's
checked-out HEAD — refreshMirror says so in its own contract ("never checks
out, resets, merges, pulls or cleans"). And worktreeAddArgs built:

    ["-C", repoRoot, "worktree", "add", "-b", branch, dir]

with no start point, so `worktree add` branched off whatever the mirror already
had. code.ts read co.freshness.remoteTip only to print it.

The reachable shape was:

    mirror HEAD   A
    origin/main   A-B-C
    FETCH_HEAD    C
    printed       "(fetched) @ C"
    worktree cut  A

so a run could report a fresh base and then start days behind it. The summary
this project shipped — "--repo fetches instead of branching off a stale copy" —
was true of the fetch and false of the worktree.

Two changes.

1. worktreeAddArgs and createWorktree take an optional startRevision, appended
   last so git reads it as the start point. Omitted, behaviour is unchanged, so
   a plain --worktree run still branches from the user's own checkout.

2. code.ts pins to co.freshness.remoteTip, and REFUSES when the base cannot be
   named. Previously an unfetchable mirror printed a warning and proceeded.
   Proceeding is the dangerous half: it starts work on an unknown base while
   having just printed a reassuring line. A run that cannot establish its base
   now exits 1 and says why.

Tests: three argv tests plus a real-git canary that asserts the resulting
checkout rather than the arguments. It builds a remote at A, clones it, moves
the remote to C, fetches, asserts the mirror's HEAD is still A (the defect
itself), cuts the worktree, and requires `git rev-parse HEAD` inside it to
equal C.

Mutation-checked, and this is the evidence the bug was real: reverting
worktreeAddArgs to drop the start point fails the canary with "the worktree
must start at the fetched revision" — the worktree lands on A. Restored, 19/19.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            1090 pass / 0 fail
@AetherAI3
AetherAI3 merged commit 63aa197 into main Aug 19, 2026
5 checks passed
@AetherAI3
AetherAI3 deleted the fix/repo-exact-remote-base branch August 19, 2026 13:47
AetherAI3 added a commit that referenced this pull request Aug 22, 2026
…notes claim

On 2026-08-20 this repository described five different releases at once.
package.json said 0.2.0. package-lock.json still said 0.1.0 — the #81 bump never
touched it. RELEASE_NOTES.md's top entry described a 0.2.0 whose feature list was
written at 477f0fc, before #72 added `aether skills`, `aether capabilities` and
`aether support-bundle`. The npm registry served 0.1.0 and nothing else. And a
packed aether-agents-0.2.0.tgz sat in the repo root from #83 until #90 deleted
it, containing a build with no skills runtime in it at all.

Nothing was lying on purpose. The four statements drifted apart because nothing
compared them.

0.2.0 -> 0.3.0, not 0.2.1
-------------------------
#72 is a backwards-compatible feature addition — a skills runtime, three new CLI
commands, six built-in skills shipped inside the package — that the v0.2.0 notes
never mentioned. Semver makes that a minor bump.

Reusing 0.2.0 was rejected on identity grounds. 0.2.0 already names a specific
dated artifact: the August 19 notes entry, the README availability paragraph,
install.sh's pin example, and that committed tarball. Publishing different
contents under the same version would make two materially different artifacts
answer to one name, which is the defect this release exists to close. 0.2.0 is
retired unused; it will never be published.

Reconciled
----------
- package.json, package-lock.json (both version fields) and src/version.ts all
  say 0.3.0.
- RELEASE_NOTES.md leads with v0.3.0 covering 477f0fc..ed094dc: one feature,
  nine user-visible fixes, three test-only commits, one unwired module (#86 ship
  rail, which no command invokes), one docs commit. The v0.2.0 entry is marked
  superseded rather than rewritten — it is the record of what was written that
  day, not an install instruction.
- README and the release log name 0.3.0, and both still say plainly that
  `npm i -g aether-agents` gives you 0.1.0, because it does.

New gate: test/release_coherence.test.ts
----------------------------------------
Gate A asserts every place that names the release names the same one — including
both package-lock version fields, which is the drift nothing was watching.

Gate B is the load-bearing one: for every feature the release notes claim, the
code behind it must be present in the file list `npm pack` would actually ship,
and the command exposing it must be in the CLI registry. The source checkout's
dist/ is NOT the package — the allowlist is dist/src plus four docs.

Mutation-checked. Adding "!dist/src/commands/skills.js" to the files allowlist
silently drops `aether skills` from the tarball:

  verify:production  ->  {"ok":true, ... "packedFiles":523}   MISSED IT
  release_coherence  ->  FAIL: dist/src/commands/skills.js
                              (agent skills runtime — `aether skills`)

Restored: 7/7 pass, 524 packed files.

New: npm run release:candidate
------------------------------
release.yml only runs after a founder has already tagged and published, so
everything it checks is checked too late to change the decision. This runs the
same sequence, in the same order, before the tag exists — against a detached
git worktree of a specific commit, never the dirty checkout. A dirty tree is
refused unless --allow-dirty, which marks the report commitBound:false rather
than pretending.

Every CLI proof runs the binary npm installed from the packed tarball into a
clean prefix: --version, --help, `skills list`, `capabilities`, and the handoff
demo (the demo harness is not shipped, so it is placed beside the installed
package and resolves the CLI and its imports from the tarball's own dist/src).

Skipped steps are recorded as "not-run" with a reason and can never read as a
green tick — the full npm test suite is release.yml's gate and says so in the
report. No string is handed to a shell: npm is reached through its own JS
entrypoint under node, so every argument stays an argument.

Also
----
- scripts/verify-production.ts exports createPackReport, so a gate reasoning
  about what a user receives asks npm instead of reading the build directory.
- test/release_canaries.test.ts's coverage map said canaries 5 and 7 were
  unwritable. #86 and #87 wrote them two commits later; the excuse outlived the
  condition. The map is now enforced by a test that reads the delegated files.
- docs/releases/OPERATOR-PACKET-v0.3.0.md: the founder-owned publish sequence,
  with what is proven and what is explicitly not.

Gates at this commit:
  npm run typecheck                      exit 0
  release + production test files        21 pass / 0 fail
  npm run verify:production --tag v0.3.0 ok

Full npm test NOT run locally (a tool_executor test does not return on this
Windows box); CI is the authority.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant