[#419] feat: mirrors:regenerate — local, deterministic mirror realignment - #476
[#419] feat: mirrors:regenerate — local, deterministic mirror realignment#476rucka wants to merge 14 commits into
Conversation
Verdict
PR: #476 · Author: rucka · Reviewer: independent reviewer agent · Date: 2026-09-01 · Story: US-419 · Type: feature Classification matrix — per dimension
Tier = max(assessed) = yellow, confirming the story's refinement-time tier. No drift. AssessmentsSecurity — Input validationVerdict: green — the only external input is the repo's own tree; Security — Output handlingVerdict: green — output is regenerated Markdown written by the pre-existing Security — AuthenticationVerdict: green — no auth surface touched. Security — AuthorizationVerdict: green — no access-control path touched. The new write is a local Security — Introduced vulnerabilitiesVerdict: green — 0 introduced, 1 pre-existing (CI Details
Evidence: CostVerdict: Architecture (Coupling)Verdict: not assessed — Bug fix — Red test before fixVerdict: not applicable — not a bug fix. Filed and labelled as a feature/tooling story; no defect reference requiring a reproduction test. DetailsFindings by severityCritical (must fix before merge) None. Major (must fix before merge)
Minor (must fix before merge — same bar as Major, just lower impact)
Questions (informational, never blocking — includes anything that only fires on a contrived scenario)
Positive feedback
Functionality & requirements (AC coverage)
Edge cases: dirty tree covered by test Testing & quality gates
Adoption compliance
Tech debt
Documentation
Performance & deployment
|
Bound re-review to immutable deltas and require a complete finite-state inventory before review or remediation.
b1e4e27 to
82ca2de
Compare
Require real boundary evidence for externally-defined state and repair claims.\n\nRefs: #416
…ment - scripts/regenerate-mirrors.sh: thin wrapper over the CLI's existing `pair update --source <local dataset> --offline` path; no generation logic - root script `mirrors:regenerate`; no check mode (the guards are the checker) - fail-loud: non-zero + reason on no git tree, no dataset, no toolchain - tests first: drift regenerated, idempotent, no published KB fetched, authored changes untouched, both failure paths - Task: T-1 — Root script for local-source mirror regeneration Refs: #419
…, not pair update - PRE_PUSH_REMEDY names `pnpm mirrors:regenerate`; new MIRROR_REMEDY_SCRIPT is dead-advice-checked like REMEDY_SCRIPT (both remedy steps must exist) - mirror-guard + skill-md-mirror print the same command, stated once as MIRROR_REGENERATE_COMMAND — the guard whose failure the contributor reads is the guard whose remedy AC-3 renames - DEVELOPMENT.md + development-setup.mdx: paragraph stays byte-identical modulo the ADL link form; both command lists gain the script - descriptive references to the `pair update` TRANSFORM are a different claim, untouched - Task: T-2, T-3 — rename the remedy in the gate message and both docs Refs: #419
- Phase 1 becomes "Realign Generated Mirrors, then Quality Gate": the step runs BEFORE the gate, because drift is what turns the gate red and a red gate HALTs — after it the remedy would be unreachable in its only case - the command is read from adoption (`mirror-realign-command`), never named in the skill: publish-pr ships to every adopter, `mirrors:regenerate` is ours - commits only the generated paths, alone, named a regeneration; no-op is silent (no commit, no output row); non-zero exit HALTs before any PR side effect - way-of-working declares the key; ADL records the ordering + indirection - conformance guard over the prose + whole-file mirror reproducibility - Task: T-4, T-5 — wire the step into publish-pr and guard it Refs: #419
Pre-existing drift this branch never touched: four adoption files still carry bare `/skill-name` references the install-time rewriter resolves to their prefixed form. Regenerated by `pnpm mirrors:regenerate`, committed separately — exactly the shape /pair-capability-publish-pr now produces. Refs: #419
… tests' timeout - 2026-07-31-pre-push-gate-is-check-only.md: the Open Decision is closed by this story, with what actually shipped (incl. the two guard messages the card did not list) and a pointer to the phase-order ADL - regenerate-mirrors.test.ts: explicit 120s per-test timeout — each case builds the CLI and runs a 7-registry regeneration, and vitest's 5s default is measured while turbo runs every other package in parallel (it flaked in the full gate) - verified AC-6: `format`/`format:check`/`prettier:*`/`mdlint:*`/`quality-gate` are byte-identical to main; only `mirrors:regenerate` is added - Task: T-6 — verify format unchanged and close the ADL Refs: #419
…view round 1) Major: publish-pr Phase 1 staged "the paths the command owns", resolved from the adoption's owned-path globs. Root `.pair/**` is on that list and holds 117 tracked authored files under `.pair/adoption/**` — a contributor's unstaged prose landed in `chore: regenerate mirrors from local dataset`. Now: snapshot porcelain before and after, stage exactly what changed. No adopter enumerates globs. Also: Notes carve out the Phase-1 write instead of denying it; way-of-working states the writer/checker scope asymmetry (guards check dataset-sourced mirrors only) and marks its tree list descriptive; MIRROR_REGENERATE_COMMAND now tied to root package.json; AC-7's missing-turbo / build-failure / missing-dist branches tested, plus a TMPDIR-leak trap; refusal assertions no longer pass vacuously on a signal kill; PRE_PUSH_REMEDY names both mirror trees and its docblock scopes the byte-identity claim to the two documents; ADL records the thin-script-in-vitest deviation; turbo inputs widened so none of these guards can replay a stale PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…ee rule (review round 1) Minor findings 86783a8 did not reach. mirror-guard.test.ts cast `find(...)` to string: reword the guard's remedy line and the test dies with `TypeError: Cannot read properties of undefined` instead of naming the broken contract. Asserted, not cast. mirror-realignment.test.ts asserted `toContain('git add -A')` next to the negated form — satisfied by the exact prose it exists to forbid ("stage everything with `git add -A`"). Deleted; the regex proves presence and polarity. ADL's rejected alternative said "Phase 2, after the push" — nothing is pushed at Phase 2 (the PR is created in Phase 4), so a later reader is given a reason that is not the one the same paragraph gives. Now "after the gate". regenerate-mirrors.sh: TOOLCHAIN_ROOT is the script's own tree, so a fresh `git worktree add` (no node_modules) exits 1 and publish-pr HALTs. Verified, and intended — that tree fails the gate one step later anyway. Header says so. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
… the CLI it runs (review round 2)
Minor 1 — publish-pr Phase 1 compared two `git status --porcelain` snapshots to
decide what the realign command wrote. A porcelain entry encodes STATUS, not
content: HEAD carries a drifted mirror, the contributor holds an uncommitted
edit to that same file, the command regenerates it — same unstaged-modified
entry on both reads. The agent read NO CHANGE: hand-edit destroyed with nothing
reported, stale mirror still pushed, `skills:conformance` red on the PR this
step exists to keep green. And the step-4 Verify ("git status still shows every
pre-existing unstaged authored change") PASSED on exactly that state. Now the
before snapshot carries a `git hash-object` digest of every dirty path; the
staged set adds the pre-dirty paths whose digest moved; those are named on the
`Mirrors:` row (`overwrote uncommitted changes in: <paths>`); the Verify reads
the digest, not the listing. Measured against the real script:
regenerate-mirrors.test.ts, 'overwrites a pre-dirty mirror while
`git status --porcelain` stays byte-identical'.
Minor 2 — regenerate-mirrors.test.ts AC1/AC2 build and run apps/pair-cli and
assert the real `pair update --source` transform, but @pair/dev-tools declares
no dependency on @pair/pair-cli, so no input or task edge covered that tree.
Measured at 0a6712e: a comment appended to
apps/pair-cli/src/registry/skill-refs.ts replayed `1 cached, 125ms >>> FULL
TURBO`. `dependsOn: ["build", "@pair/pair-cli#build"]` on #test and
#test:coverage — a task edge follows the whole closure and cannot go stale like
a hand-listed set of trees. After: 2 cached / 4; content-ops probe 0 cached.
web-cloud-environment.test.ts gains a requiredDependsOn guard, mutation-proven.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
Phase 1's before-snapshot ran `git hash-object <path>` over every dirty porcelain entry. Two of three ordinary shapes are fatal: ` D gone.md` -> "could not open … for reading" (exit 128, which the step's own non-zero -> HALT turns into a PR blocked by its own snapshot pass), `?? newdir/` -> "Unable to hash newdir/" — and that entry is identical before and after however many files under it the run rewrote, so the untracked subtree kept the status-vs-content blindness the digest was added to close. Now: `--untracked-files=all` (per-file `?? dir/a.md` entries, hashable), digest only entries whose worktree file exists (a recreated deletion moves its porcelain entry anyway), and `-w` so the overwritten bytes land in the ODB — the Mirrors: row now carries `(recover: git cat-file -p <sha> > <path>)` instead of naming a loss nobody can undo. Executed against the real script (regenerate-mirrors.test.ts): all three shapes in one fixture, snapshots byte-identical before/after while both digests move, and `git cat-file -p` prints the hand-edits back. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…commit (review round 4) - mirror-guard: the MISSING branch and the orphan-sweep remedy now name MIRROR_REGENERATE_COMMAND, like the drifted branch already did. Both are reached by ADDING a dataset file: `pair update` installs the published KB, so the new file is in no release and every other mirror is overwritten with released content. - publish-pr Phase 1 snapshot is `git status --porcelain -z -uall`: the default format quotes/octal-escapes a path with a space or a non-ASCII byte, so the entry was silently dropped from the digest (and `git add` refused the quoted string as a pathspec). - the regeneration commit is made BY PATHSPEC: a plain `git commit` after `git add <paths>` commits the whole index, sweeping in prose the contributor staged before the run. - skill-md-mirror docblock + unreadable-path message name the constant.
…s reason (review round 5) A pathspec resolves against paths git already knows, so the newly CREATED mirror — the shape a contributor gets by adding a dataset file — aborts the commit with 'did not match any file(s) known to git'. Stage first, always. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
54ae57b to
d24fec8
Compare
…f; HALT on untracked files under written trees (review round 6) Three round-6 findings on PR #476, all measured against the real script in a fixture repo: - Major: a mirror registry deletes a contributor's untracked/staged-new draft under it; its vanished `??` (or `A.`→`AD`) entry put it in the step-4 set and `git add`/the pathspec commit aborted Phase 1 after the destructive run, with no report row. Such paths now leave `git add` and the pathspec and are named `removed untracked: <path> (recover: git cat-file -p <sha> > <path>)`. - Minor: a path whose render equals HEAD moves its entry when rewritten (`M.`→`MM`, `D.`→`D.`+`??`, `.M`→gone) but equals HEAD after `git add`; `git commit -- <paths>` over only such paths exits 1 (`nothing to commit`). Recipe now runs `git diff --cached --quiet -- <paths>` first; recover rows are driven by the digest comparison whether or not a commit was made; Verify compares against the cached name list. - Minor: the writer indexes untracked `.pair/adoption/**` files into `.pair/llms.txt` (dangling link + WIP filename in history). Step 3 now HALTs on `??`/`A.` entries under the trees the adoption names as written, remedy `git stash push -u -- <paths>` / `git stash pop`; stated in way-of-working and the script header. Tests: 3 real-script cases (regenerate-mirrors.test.ts), 4 conformance pins (mirror-realignment.test.ts). .claude mirror regenerated via `pnpm mirrors:regenerate`, never hand-edited. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FfhvsS5rippi6aUbbGrf6F
7b55900 to
adb9627
Compare
PR Information
PR Title: [#419] feat: mirrors:regenerate — local, deterministic mirror realignment
Story/Epic: #419 — standalone tooling/process defect
Type: Feature
Priority: P1 (Should-Have)
Assignee: rucka
Labels: tech-debt, risk:yellow
Summary
What Changed
pnpm mirrors:regenerate(scripts/regenerate-mirrors.sh) — wrapspair update --source <local dataset> --offline, no new generation logic. Fail-loud (no git tree / no dataset / no turbo / build failure / missingdist/cli.js⇒ exit 1), no check mode.PRE_PUSH_REMEDY,DEVELOPMENT.md, its docs-site twin, and every failure branch of both mirror guards — drifted, missing, and the orphan sweep's "add it to the dataset and regenerate" (new sharedMIRROR_REGENERATE_COMMAND, tied to the rootpackage.jsonscript). The missing and orphan branches matter most: both are reached by ADDING a dataset file, which is the one casepair updateprovably cannot serve — the new file is in no release, so the guard stays red and every other local mirror is overwritten with released content./pair-capability-publish-prruns the regeneration in Phase 1, before the gate, reading the command from adoption (mirror-realign-command) — absent ⇒ step skipped, zero-config default — and commits what it wrote separately from the story's own commits.git status --porcelain -z --untracked-files=allbefore/after comparison paired with agit hash-object -wdigest of the already-dirty paths — never a path glob. What the run removed (a contributor's draft under a mirror registry) leaves that set and is named with its recover sha; the set isgit add-ed, checked withgit diff --cached --quiet(empty ⇒ no-op, not a failed commit), then committed by pathspec, so nothing the contributor had already staged is swept in and a mirror the run just created still lands. Before any of it runs, an untracked file under the trees the command writes into HALTs the step (mirror tree ⇒ deleted; add tree ⇒ indexed intollms.txt). See "The staging rule" below: it is the load-bearing part of the wiring.gate:compositionextended (MIRROR_REMEDY_SCRIPT) so a two-step remedy naming a nonexistent script is dead-advice-checked like the first step already was.Why This Change
pair updateinstalls the latest published knowledge base; the mirror-equality guards need regeneration from the working tree's dataset. The wrong remedy is the most plausible reason three of seven recorded drift incidents were hand-ported mirrors instead of regenerated ones.Story Context
User Story: As a contributor whose push was blocked by the mirror-equality guard, I want an explicit command that realigns the generated mirrors from the local dataset, named by the gate and run by
/pair-capability-publish-pr, so the remedy is deterministic and one step.Acceptance Criteria: AC-1…AC-8 (root script; remedy renamed in
PRE_PUSH_REMEDY/DEVELOPMENT.md/ docs twin / both guards; publish-pr wiring + separate commit; fail-loud, never a silent success over a no-op; no check mode;format/format:checkuntouched) — all covered.The staging rule (read this first)
The skill cannot say "stage the paths the command owns": the adoption declares a command, not a path list, deliberately. Resolving ownership from globs is wrong here for a concrete reason — root
.pair/**is where this repo's generated mirrors land and holds 117 tracked authored files under.pair/adoption/**(git ls-files .pair/adoption | wc -l→ 117). A contributor who editsway-of-working.md, leaves it unstaged and runs the skill would get their prose committed underchore: regenerate mirrors from local dataset— a commit they never wrote.Phase 1 step 3 therefore snapshots
git status --porcelain -z --untracked-files=allbefore the command and takes agit hash-object -wdigest of every dirty entry whose worktree file exists; step 4 re-reads both after. The staged set is every path whose porcelain entry appeared, disappeared or changed, plus every already-dirty path whose digest moved. Nevergit add -A, never a glob, and no adopter enumerates owned globs anywhere: the command's own effect is the declaration.And the commit is made by pathspec, not from the index.
git add <paths>followed by a plaingit commitcommits the WHOLE index — so the same harm the rule above prevents for unstaged prose re-enters through anything the contributor had already staged before the run. MEASURED: withM authored.md(staged prose) andM mirror.md(regenerated),git add mirror.md && git commit -m 'chore: regenerate mirrors from local dataset'produces a commit listing both. It is an ordinary state, not an exotic one — this skill is standalone, explicitly runs on a dirty tree, and a resumed or interrupted/pair-process-implementleaves a populated index; and the step-4 Verify would only notice after the commit exists, which is not a HALT condition, so the mislabelled commit would be pushed.git commit -m "…" -- <paths>commits the pathspec alone and leavesM authored.mdstaged and byte-identical.But the pathspec replaces the index as the commit's scope, not the
git addas its step. A pathspec resolves against paths git already knows (index or HEAD), and the single most common way this step produces a path at all is a contributor adding a file to the dataset — the one case a published-KB install provably cannot serve — so the run creates its mirror and the entry is?? <path>, which git does not know. MEASURED, untrackedbrandnew.md:git commit -m 'chore: regenerate mirrors from local dataset' -- brandnew.md→error: pathspec 'brandnew.md' did not match any file(s) known to git, exit 1, and the commit aborts whole — so the mirror it just regenerated never lands, the branch is pushed without it, and its ownskills:conformancejob goes red: the exact drift this step exists to remove, now caused by the step. The reason a droppedgit addis invisible is the other half of the table, measured on the same tree: unstagedM tracked.mdandD gone.mddo commit by pathspec (git show --name-status→M tracked.md,D gone.md), so the recipe works on every drifted or removed mirror and fails on the first NEW one. The skill therefore states both the step and the asymmetry:git add <paths>, thengit commit -m "…" -- <paths>— and thegit addcosts the earlier property nothing, since the pathspec still scopes the commit (measured on the same fixture:A authored.mdis still staged and byte-identical after).Why the digest half is not redundant — a porcelain entry encodes status, not content. Concrete case: HEAD carries a drifted mirror, the contributor is holding an uncommitted hand-edit to that same file, the command regenerates it. The entry is the same unstaged-modified
M <path>on both reads. Under a status-only comparison the agent concludes NO CHANGE: no commit, noMirrors:row, silence — while (a) the uncommitted work is destroyed on disk with nothing reported, and (b) the regenerated content is never committed, so the branch still pushes the stale mirror andskills:conformancegoes red on the very PR this step exists to keep green. So those paths are staged and committed like any other write, each named in the output with a way back (overwrote uncommitted changes in: <path> (recover: git cat-file -p <sha> > <path>)), and the step-4 Verify reads the digest of every pre-existing dirty path not in the staged set — nevergit status's listing, which an overwrite leaves in place and which would therefore certify the loss it is meant to catch.Why each flag in that recipe is load-bearing — the recipe has to hold for the tree a real contributor is standing in, which also contains deletions and not-yet-committed directories. Measured in a scratch repo (
rm gone.md;mkdir newdir && echo a > newdir/a.md; hand-edittracked.md):git status --porcelainprintsD gone.md/M tracked.md/?? newdir/, andgit hash-objectover those paths givesfatal: could not open 'gone.md' for reading(exit 128), a valid sha, andfatal: Unable to hash newdir/(exit 128).-z— the default porcelain format is not a list of paths: it QUOTES and octal-escapes anything with a space or a non-ASCII byte. MEASURED overwith space.md,caffè.md,new file.md,nuovo-caffè.md:git status --porcelain -uallprintsM "with space.md",M "caff\303\250.md"and the??twins, and all four resolve to no such file — so the entry fails the exists test and is dropped from the digest in silence, which is the very status-vs-content blindness the digest exists to close, re-entering through the parser. Concrete loss: a generateddocs/My Guide.mdalready carrying an uncommitted hand-edit is overwritten, the entry is unchanged on both reads, the digest was never taken ⇒ NO CHANGE — hand-edit destroyed with norecover:row, regenerated bytes never staged, stale mirror pushed and the branch's own conformance job red. The reverse shape costs the step outright: a NEW generated file with a space is caught by status, and thengit add '"con spazio.md"'→fatal: pathspec … did not match any files, mid-step.-zprints raw bytes, NUL-separated, never quoted — so the snapshot is split on NUL (a filename may contain a newline), and a rename entry's<old>arrives as a second field to be consumed rather than read as an entry (which is also what removes the default format'sold -> newambiguity).--untracked-files=all— the default reports a not-yet-committed directory as ONE?? dir/entry: identical before and after however many files under it the run rewrote, and unhashable. A generated mirror living under such a directory would be regenerated, read as NO CHANGE, and never staged — exactly the status-vs-content blindness the digest exists to close, in the one shape the digest could not reach. Expanded per file,?? dir/a.mdhashes like anything else. (git diffis no substitute: it never reports untracked paths at all.)non-zero exit → HALTand blocks the PR on a condition the snapshot pass itself created. Deletions are safe to skip, and that is a property rather than a concession: a deleted path the run recreates moves its porcelain entry (D→Mor gone), so status already catches it. The digest is only needed where status cannot move — which is also why the step-4 Verify's "still has a file on disk" qualifier is not a hole.-w— plaingit hash-objecthashes the bytes and discards them, so after the overwrite the contributor's edit is in no HEAD, no index, no disk and no ODB: the report would name a path nobody can restore.-wwrites the blob, andgit cat-file -p <sha>prints it back. MEASURED:SHA=$(git hash-object -w tracked.md)→ overwrite →git cat-file -p $SHAprintsv2-handedit; the same sha taken without-w→fatal: Not a valid object name.Executed against the real script, not argued.
regenerate-mirrors.test.ts→ "the documented before/after recipe survives every ordinary porcelain shape" puts all three shapes in one fixture (a deleted tracked file, an installed file inside an untracked directory, a committed-drifted mirror carrying an uncommitted hand-edit). After the run,after.entries === before.entries— status is blind to both writes — while the digests of.pair/knowledge/index.mdand.pair/knowledge/sub/note.mdhave both moved and both files hold dataset content, andgit cat-file -p <before sha>returns each hand-edit verbatim. Its sibling "the snapshot recipe sees a path with a space and a non-ASCII byte — the default parse does not" runs the same shape over.pair/knowledge/con spazio.mdand.pair/knowledge/caffè.md: the pre-fix parse digests neither, the-zrecipe digests both and hands the bytes back. And "the regeneration commit carries only the regenerated paths, never a pre-STAGED authored file" runs both commit forms on one fixture — index-based listsauthored.md+ the mirror, pathspec lists the mirror alone. Its sibling "stages a newly created mirror before committing it — a pathspec alone cannot name it" holds both rows in one fixture: a dataset file added after convergence (so the run creates?? .pair/knowledge/new-guide.md) next to a drifted tracked mirror. The bare pathspec commit over both is refused andHEADis asserted unmoved; over the tracked path alone it succeeds, which is the whole reason the omission stays silent; the documentedgit add+ pathspec form commits exactly the two and leaves the pre-stagedsrc/authored.tsuntouched. Three more run the three paragraphs above end to end: "deletes an uncommitted file under a mirror registry — nothing to stage, only the-wblob survives" (both removed shapes plus a genuine regeneration in one set: the naivegit add→ 128, the naive pathspec commit → 1 with HEAD unmoved, the documented recipe landsnew-guide.mdalone, andgit cat-file -prestores both drafts byte-for-byte); "a non-empty set whose cached diff is empty is a no-op, never a failed commit" (the three render-equals-HEAD shapes, then the mixed set whose commit lists a subset); "indexes an untracked adoption file into the generated llms.txt — stash it before the run" (the dangling link measured inHEAD:.pair/llms.txt, then the stash/run/pop remedy asserted to its postcondition).What the run removes is not stageable, and has to be named — the
knowledge,githubandagentsregistries arebehavior: "mirror"(apps/pair-cli/config.json): the target is made equal to the dataset, so a file only the target has is deleted, a contributor's draft included. MEASURED with the real script: untracked?? .pair/knowledge/wip-draft.md→ run exit 0 → file gone, entry gone; the entry "disappeared", so the rule above puts it in the set, andgit add .pair/knowledge/wip-draft.md→fatal: pathspec '.pair/knowledge/wip-draft.md' did not match any files, exit 128. The staged-new shape is worse:A. .pair/knowledge/staged-draft.md→AD;git addexits 0 (it stages the removal, dropping the index's only copy) and the pathspec commit fails instead —error: pathspec '.pair/knowledge/staged-draft.md' did not match any file(s) known to git, exit 1, HEAD unmoved, taking the genuinenew-guide.mdin the same set down with it. Either way Phase 1 dies after the destructive run — regenerated mirrors uncommitted, branch pushed stale, its own conformance job red — and the draft is destroyed with no report row, becauseoverwrote uncommitted changes in:fires on a digest that moved, never on an entry that vanished, though its-wblob exists (git cat-file -p <sha>→# my wip draft). So a before entry HEAD does not know (??/A., porcelainXYwith.for the blank column) whose file is gone after the run goes neither ingit addnor in the pathspec, and is namedremoved untracked: <path> (recover: git cat-file -p <sha> > <path>).A non-empty set whose cached diff is empty is a no-op, not a failed commit — a path whose dataset render already equals HEAD moves its entry when the run rewrites it, so it is in the set, yet after
git addits index entry equals HEAD. MEASURED with the real script, three shapes in one fixture: staged hand-editM. a.md→MM a.md; staged deletionD. b.md→D. b.md+?? b.md; unstaged hand-edit.M c.md→ entry gone.git add a b cexit 0;git diff --cached --quiet -- a b cexit 0 (empty);git commit -m … -- a b c→nothing to commit, working tree clean, exit 1 — and the two hand-edits are gone from disk and index, with no row, because they entered the set through the entry comparison. Mixed with a genuined.md: cached-quiet exit 1, the pathspec commit succeeds and lists[d]— a subset of the set. The recipe now runsgit diff --cached --quiet -- <paths>between thegit addand the commit (empty ⇒ no commit, the no-op branch), drives the recover rows from the digest comparison alone, whether or not a commit was made, and the Verify compares the commit's file list togit diff --cached --name-only -- <paths>rather than to the set.The writer reads the whole target tree, untracked files included — so the run must start with none under the trees it writes into. The
adoptionregistry isbehavior: "add"(a file only the target has survives) andgenerateLlmsTxt(apps/pair-cli/src/registry/llms-generation.ts) indexes everything it finds on disk under.pair/adoption/**. MEASURED with the real script: untracked?? .pair/adoption/tech/wip-note.md→ run exit 0 →.pair/llms.txtgains- [adoption note](.pair/adoption/tech/wip-note.md)while the note stays??; under the staging rulellms.txt(entry appeared) commits and the note (entry unchanged) does not, soHEAD:.pair/llms.txtcarries a dangling link and the contributor's private WIP filename, withHEAD:.pair/adoption/tech/wip-note.mdabsent. Bytes untouched — the story's edge case held on bytes only — derived output leaked. Phase 1 step 3 therefore reads the before snapshot's??/A.entries against the trees the adoption'smirror-realign-commandentry names as written (this repo's already listed them, descriptively) and HALTs before running the command — the one point where a HALT costs nothing — naming each path and the remedygit stash push -u -- <paths>/git stash popafter Phase 1 (or commit/move them). Measured to its postcondition: stashed, the run leaves the tree clean andllms.txtlink-free; popped, the note is back as??. The skill owns no globs, so when the adoption names no written trees the check is skipped and theremoved untracked:row is the only net (a deleted draft is caught after the fact; an indexed one is not). HALT rather than auto-stash is deliberate: the skill's only write to the branch stays the regeneration commit.Writer and checker are not the same scope, and the asymmetry is the writer's. The guards compare dataset-sourced mirrors (a target-tree file with no dataset counterpart is compared to nothing), while the command additionally rewrites
/skill-nametokens and.skills/**link paths across the whole installed.pair/**tree. Evidence on this branch:6655439dregeneratedadr-021,adr-022,adr-023andcollaborative-workflow.context.md— four files with no dataset counterpart (find packages/knowledge-hub/dataset -name 'adr-021*'→ nothing), which had sat drifted on a greenmain. That region accumulates drift undetected and then lands, unrelated, in whichever PR next runs the writer. Documented inway-of-working.mdand in the script header rather than left to be rediscovered; the separate commit is what keeps it readable.Changes Made
Implementation Details
scripts/regenerate-mirrors.sh+mirrors:regenerate— builds the CLI through turbo first (cached; a staledist/would regenerate with yesterday's transform), pinsINIT_CWDto the repo root (it outranks both the positional target and the cwd), resolves the target tree fromgit rev-parse --show-toplevel.TOOLCHAIN_ROOT(the script's own tree) andTARGET_ROOT(the tree being realigned) are split, which is what makes the happy path exercisable against a throwaway fixture — the same splitscripts/format-lib/run-format.shuses. Consequence, stated in the header because it is intended: a freshly created linked worktree has nonode_modules/, so the script exits 1 withrun pnpm install firstand publish-pr HALTs before creating the PR — that tree cannot pass the gate one step later either.PRE_PUSH_REMEDY,DEVELOPMENT.md, the docs-site twin, and both guards' failure messages — a scope extension past the card's literal "three places", because the guard whose failure the contributor reads is literally AC-3's Given. Descriptive references to thepair updatetransform (what a mirror is compared against) are a different claim and were left alone./pair-capability-publish-prwiring in Phase 1, before the gate — not Phase 2 as the card proposed: mirror drift is what turns the gate red and a red gate HALTs, so a step after it is unreachable in its only case; and a gate that ran first judged a tree the PR no longer contains.mirror-realignment.test.ts(26) pins the phase ordering to the Phase 1 span, the adoption-read (the skill must not name this repo's script), the skip-when-absent default, the digest-paired staging rule, the hashable-shape scoping +-wrecoverability, the NUL-separated read, the stage-then-pathspec commit, theremoved untracked:exclusion + row, the cached-empty no-op branch, the pre-run HALT on untracked files under the written trees (skill +way-of-working.md), and the content-based Verify.turbo.jsoncache keys:#test/#test:coverageinputs widened to$TURBO_ROOT$/scripts/**+$TURBO_ROOT$/package.json, and@pair/dev-tools#test(:coverage)nowdependsOn: ["build", "@pair/pair-cli#build"]— those tests build and run the real CLI, and dev-tools declares no dependency on it, so a regression in the regeneration transform replayed a localFULL TURBOPASS. A task edge covers the whole--filter=@pair/pair-cli...closure and, unlike a hand-listed set of trees, cannot go stale as that closure grows. Guarded by arequiredDependsOncase inweb-cloud-environment.test.ts.Files Changed
scripts/regenerate-mirrors.sh,packages/dev-tools/src/quality-gates/regenerate-mirrors.test.ts,packages/knowledge-hub/src/conformance/mirror-realignment.test.ts, ADL2026-09-01-publish-pr-realigns-mirrors-before-the-gate.mdpackage.json(one line),turbo.json,scripts/regenerate-mirrors.shheader,DEVELOPMENT.md,apps/website/content/docs/contributing/development-setup.mdx,apps/website/content/docs/reference/skills-catalog.mdx,packages/dev-tools/src/quality-gates/pre-push-gate-composition.{ts,test.ts},packages/knowledge-hub/src/tools/{mirror-guard,skill-md-mirror}.{ts,test.ts},packages/knowledge-hub/src/conformance/web-cloud-environment.test.ts,packages/knowledge-hub/dataset/.skills/capability/publish-pr/SKILL.md+ its.claudemirror,.pair/adoption/tech/way-of-working.md, two other decision-log files,.pair/llms.txt(generated)6655439d: three ADRs + one subdomain context file — pre-existing drift this branch never authored (see above)Testing
Test Coverage
regenerate-mirrors.test.ts— 19, every one shelling out to the real script against a real fixture git repo, never a source-string regex. Seven of them run the skill's documented snapshot/commit recipe verbatim around that script: over all three ordinary porcelain shapes, over a path with a space and a non-ASCII byte, over both commit forms with a populated index, over a newly created mirror (the??row a pathspec cannot name), over the two removed shapes (??andA.drafts under a mirror registry), over the three render-equals-HEAD shapes plus a mixed set, and over an untracked adoption file indexed intollms.txtwith the stash remedy applied to its postcondition.mirror-realignment.test.ts— 26 over the skill prose and whole-file mirror reproducibility; plus 2 inweb-cloud-environment.test.ts(turbodependsOn), 2 inpre-push-gate-composition.test.ts, 2 inmirror-guard.test.ts(the drifted AND the missing branch), and the orphan-sweep remedy case moved off the'pair update'literal.never git add -Ain the skill prose fails the conformance suite; reverting the digest recipe to a baregit hash-objectover every dirty path fails 3 of its cases; reverting the commit recipe to the pathspec without itsgit addfails the stage-first case (1 failed / 21 passed); the four round-6 pins were red against the pre-fix prose (4 failed / 22 passed) and green after; dropping@pair/pair-cli#buildfails with@pair/dev-tools#test is missing dependsOn @pair/pair-cli#build.Test Results
Testing Strategy
git status --porcelainempty).git status --porcelainafter is byte-identical to before (both listM .pair/knowledge/index.mdandM src/authored.ts) whilegit hash-objecton the mirror moved and the authored file's did not. That is the executed proof the staging rule needs a digest.con spazio.mdandcaffè.md, drifted on HEAD and hand-edited in the worktree. The default parse digests neither (both quoted/escaped ⇒ no such file); the-zparse digests both, sees both overwrites whileafter.entries === before.entries, and recovers both hand-edits from the ODB. The pathspec failure is measured in the same case.M authored.mdstaged before the run, the index-basedgit add <paths> && git commitproduces a commit listing the contributor's prose alongside the mirror; the documentedgit commit -m … -- <paths>on the same state lists the mirror alone and leaves the staged entry untouched.?? .pair/knowledge/new-guide.md. The pathspec commit without agit addis refused (did not match any file(s) known to git,HEADunmoved); over the drifted tracked mirror alone it succeeds, which is why the omission is silent;git add+ pathspec commits exactly the two paths and still leaves the pre-staged authored file alone.?? .pair/knowledge/wip-draft.md, staged-newA. .pair/knowledge/staged-draft.md, and a new dataset file → run. Both drafts gone (??entry vanished,A.→AD); naivegit add→ 128 / naive pathspec commit → 1, HEAD unmoved; documented recipe commitsnew-guide.mdalone;git cat-file -p <before sha> > <path>restores both drafts.git rm/ unstaged edit → run →MM,D.+??, gone;git add+git diff --cached --quietexit 0 + pathspec commit →nothing to commit, exit 1; hand-edits recoverable only from the-wblobs; then a mixed set whose commit lists the one real path.?? .pair/adoption/tech/wip-note.md→ run →.pair/llms.txtlinks it; committed by the rule, the link is dangling in HEAD.git stash push -u -- <note>→ run → clean tree, no link →git stash pop→ note back.dist/cli.js. The last three run the real script copied into the fixture's ownscripts/(TOOLCHAIN_ROOT is derived from the script's dirname, so that is the only way to reach them) driven by anode_modules/.bin/turbostub. Dropping the post-build-f "$CLI"check makesexec node "$CLI"regenerate from a staledist/and exit 0 — the silent success AC-7 forbids; that case is now red.EXIT HUP INT TERMtrap leftTMPDIRempty. The two explicitrm -fcalls stay: the success path ends inexec, which replaces the process and runs no EXIT trap.apps/pair-cli/src/registry/skill-refs.tsand re-runningturbo run test --filter @pair/dev-toolsgave1 cached, 125ms >>> FULL TURBObefore thedependsOnedge and2 cached / 4, 12.7safter; the same probe onpackages/content-ops/src/index.tsgives0 cached.Quality Assurance
Review Areas
format:git diff origin/main -- package.jsonis exactly one added line.format,format:check,prettier:*,mdlint:*andquality-gateare byte-identical tomain(AC-6).DEVELOPMENT.mdand the docs twin differ on exactly one line — the ADL link form (relative path vs GitHub blob URL) — as ADL2026-07-31requires.Documentation
Knowledge Sharing
2026-09-01-publish-pr-realigns-mirrors-before-the-gate.md— Phase 1 vs Phase 2; command read from adoption, never named in the skill; the content-aware staging rule and why HALT-and-report was rejected for it; and the thin-script-in-vitest deviation from the "scripts are never unit-tested" rule (its bounded exception is recorded inway-of-working.mdtoo).2026-07-31-pre-push-gate-is-check-only.md— its Open Decision is now closed by this story, listing what shipped.Reviewer Guide
Testing the Changes
Review Focus Areas
Mirrors:row can appear withno commit. This repository has no path with a space or a non-ASCII byte today (git ls-files | grep '[^ -~]\| 'is empty), so the-zhalf is proven by fixture, not by this tree. In particular: committing the regenerated bytes over an overwritten hand-edit (rather than HALTing) is a deliberate call, recorded in the ADL's Alternatives; the-wblob plus therecover:hint on theMirrors:row is what makes that call survivable for the contributor.Dependencies & Related Work
Follow-up Work
Pre-Submission Checklist
pnpm quality-gateexit 0DEVELOPMENT.md, docs site,way-of-working.md, two ADLs)