Skip to content

fix(repo): validate and fetch a --repo mirror instead of reusing it blind [SC-A2.1] - #73

Merged
AetherAI3 merged 3 commits into
mainfrom
supercluster/a2-project-context
Aug 19, 2026
Merged

fix(repo): validate and fetch a --repo mirror instead of reusing it blind [SC-A2.1]#73
AetherAI3 merged 3 commits into
mainfrom
supercluster/a2-project-context

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

Problem

aether agent --repo owner/name could silently start work from a stale tip.

An existing local mirror was reused on the strength of a single check —
src/core/repo.ts:76:

if (existsSync(join(dir, ".git"))) return { dir, cloned: false };

There is no git fetch anywhere in this codebase. So that mirror was never refreshed. createWorktree then branched off it with no start-point argument, and prCreateHint went on to invite a PR from whatever it found.

The mirror path is derived from the slug alone (~/.aether-agent/repos/<owner>-<name>), so the remote was never validated either — any directory sitting at that path was accepted as the requested repo.

Root cause

Reuse was decided by path existence, which answers "is there a git repo here" and was being read as "is this the right repo, and is it current". Those are three different questions.

Contract

refreshMirror runs before an existing mirror is handed back, and guarantees:

  1. The origin really is the repo asked for. Compared through parseRepoSpec, so https / ssh / .git / trailing-slash forms normalize — rather than adding a second, subtly different URL parser. A mismatch throws; it is never quietly used.
  2. The mirror is fetched, and the resulting tip is reported.
  3. A fetch that cannot happen reports unknown, with the git error attached. It never degrades to fresh as a convenience.

Read-only with respect to the user's tree: remote get-url, fetch, rev-parse. Never checkout, reset, clean, merge, pull, rebase.

Auth stays the user's own git/gh configuration, inherited from the environment. No Aether credential is passed, and none is reachable from this function.

Implementation

ensureLocalClone now takes an injected Runner, defaulting to the existing defaultRunner() from worktree.ts. It previously called spawnSync directly and was therefore untestable — this is the same seam the gated-worktree flow already uses, so no second runner abstraction is introduced (there are already two in this repo; that was flagged as a hazard during recon).

The user-facing line stops rounding off. (reusing local clone) was equally true of a mirror last fetched a week ago:

⎇ repo octocat/hello-world (fetched) @ a1b2c3d

⎇ repo octocat/hello-world (NOT REFRESHED — Could not resolve host: github.com)
  ! this worktree will branch off whatever the mirror already had;
    its base is not known to match the remote.

Tests

Written test-first — all six were added and confirmed failing to compile against the missing export before refreshMirror existed.

test proves
an existing mirror is fetched, not silently reused fetch appears in the recorded argv
a mirror pointing at a different repo is rejected throws, never returns the wrong repo
a failed fetch reports unknown, never fresh offline cannot masquerade as current
refreshing never checks out, resets or cleans all six mutating verbs absent from argv
no Aether credential is handed to git or gh no aek_, Authorization, http.extraheader, GIT_ASKPASS, x-access-token
a fresh mirror reports the exact base commit the tip a worktree would branch from is recorded

Mutation-checked. Rewriting every state: "unknown" to state: "fresh" fails "a failed fetch reports unknown, never fresh" — 11 pass / 1 fail. Restoring gives 12 / 12.

Gates at 7f55b0e:

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

Baseline on clean 41a7e261 measured in the same session: 922 / 0.

Security notes

  • No credential is read, written, or passed. Verified by test, not only by inspection.
  • parseRepoSpec's existing argument-injection guards (leading -, ., ..) are unchanged and still load-bearing — cloneArgs emits the slug with no -- separator.
  • The remote-mismatch path throws rather than falling back, so a wrong-repo mirror cannot be worked in.

Known limits

  • Ahead/behind counts are not computed. refreshMirror reports the remote tip, not a divergence measure. The full ProjectContextSnapshot (dirty counts, upstream name, ahead/behind) is a later slice of this lane.
  • createWorktree still branches with no explicit start-point. It now branches off a fetched mirror, which closes the reported failure, but pinning the worktree to freshness.remoteTip explicitly is a follow-up.
  • The two worktree roots are untouched. ~/.aether-agent/worktrees and ~/.config/aether/worktrees still coexist with no registry and no migration. Separate slice.
  • ensureLocalClone remains uncovered end-to-end; only refreshMirror is directly tested. Its spawnSync clone path still has no seam.

Dependency and merge order

Independent of SC-A0 (#72) — branched from origin/main and touches no file that lane touches. Either can merge first.

Per the integration order this lands after SC-A0 and before SC-A3.

Unrelated observation

test/diagnostics.test.ts"a hanging backend cannot stall the fast report" failed once under full-suite load at 1818ms, and passes in isolation at 82ms. Its 50ms budget is load-sensitive. Pre-existing fragility, unrelated to this change; left for the lane that owns that file rather than touched opportunistically here.

…lind

Lane SC-A2, slice 1 of the project-continuity work.

`aether agent --repo owner/name` reused an existing local mirror on the
strength of one `existsSync` check:

    if (existsSync(join(dir, ".git"))) return { dir, cloned: false };

There is no `git fetch` anywhere in this codebase, so that mirror was never
refreshed. `createWorktree` then branched off it with no start-point argument,
meaning a task could silently begin from a tip that was current days ago, and
`prCreateHint` would go on to invite a PR from it.

The mirror path is derived from the slug alone
(~/.aether-agent/repos/<owner>-<name>), so the remote was never validated
either: any directory sitting at that path was accepted as the requested repo.

Adds `refreshMirror`, called by `ensureLocalClone` before an existing mirror is
returned. It guarantees three things:

  1. the mirror's origin really is the repo that was asked for — compared
     through parseRepoSpec so https/ssh/.git/trailing-slash forms normalize
     rather than needing a second, subtly different URL parser
  2. the mirror is fetched, and the resulting tip is reported
  3. when the fetch cannot happen — offline, auth expired, remote gone — the
     result is "unknown" with the git error attached. It never degrades to
     "fresh" as a convenience

Read-only with respect to the user's tree: it runs remote get-url, fetch and
rev-parse. Never checkout, reset, clean, merge, pull or rebase. A test asserts
each of those six verbs is absent from the recorded argv.

Auth stays the user's own git/gh configuration, inherited from the environment.
A test asserts no Aether credential shape (aek_, Authorization,
http.extraheader, GIT_ASKPASS, x-access-token) reaches the git argv.

`ensureLocalClone` now takes an injected Runner, defaulting to the existing
`defaultRunner()` from worktree.ts. It previously called spawnSync directly and
was therefore untestable; this is the same seam the gated-worktree flow already
uses, so no second runner abstraction is introduced.

The user-facing line stops rounding off. "(reusing local clone)" was equally
true of a mirror last fetched a week ago:

    ⎇ repo octocat/hello-world (fetched) @ a1b2c3d
    ⎇ repo octocat/hello-world (NOT REFRESHED — Could not resolve host: github.com)
      ! this worktree will branch off whatever the mirror already had;
        its base is not known to match the remote.

Written test-first: the six tests were added and confirmed failing to compile
against the missing export before `refreshMirror` existed.

Mutation-checked: rewriting every `state: "unknown"` to `state: "fresh"` fails
"a failed fetch reports unknown, never fresh" (11 pass / 1 fail); restoring
gives 12 / 12.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            928 pass / 0 fail  (922 on clean 41a7e26)

Noted, not fixed here: test/diagnostics.test.ts "a hanging backend cannot stall
the fast report" failed once under full-suite load at 1818ms and passes in
isolation at 82ms. Its 50ms budget is load-sensitive. Pre-existing fragility,
unrelated to this change, left for the lane that owns that file.
@AetherAI3
AetherAI3 marked this pull request as ready for review August 19, 2026 12:46
@AetherAI3
AetherAI3 merged commit 1b11faf into main Aug 19, 2026
5 checks passed
@AetherAI3
AetherAI3 deleted the supercluster/a2-project-context branch August 19, 2026 12:49
AetherAI3 added a commit that referenced this pull request Aug 19, 2026
…nknown base (#83)

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
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