Repository navigation
Commit 689d606
test(merge-driver): pin the os-regen deferral refusal on the linked-worktree path, and make the self-test exit non-zero when it fails (#18165)
Fixes #17602
## PREMISE P was measured first, and it HOLDS — so the driver is not the
defect
The card asks whether a zero exit from `merge=os-regen` can be trusted.
The `domain:devx`
seat's mechanism reading (comment `5631161285`) narrowed that to one
falsifiable premise:
is `content/docs/references/**` really "generated whole", as
`.gitattributes:84` declares?
If yes, the unconditional deferral is the design and `markPending` +
regeneration closes
the loop; if no, the row is MIXED without a `mixed` field and the
deferral is a silent
deletion — and the defect would be in the declaration.
Measured on `origin/main` `394ccf549`, with the post-driver state
reproduced exactly (the
artifact missing main's side while the source still carries it), the
generator re-run, and
both controls lit:
```
before mutation after mutation after gen:docs
main's #groupBy whitespace note 2 0 2 <- P
FIRING CONTROL (marker injected into the mdx) 0 1 0
EMPTY CONTROL (a string never written) 0 0 0
blob sha of content/docs/references/ui/view.mdx
HEAD 2fa435c
mutated 2e40a12d888d9babaa254974a69b3feece62a93a (the mutation landed on disk)
regenerated 2fa435c (byte-identical to HEAD)
```
The note is not hand-written: it is a `.describe()` at
`packages/spec/src/ui/view.zod.ts:917`,
rendered into the page by `gen:docs`. The firing control proves the
generator rewrites this
file wholesale rather than skipping or appending — the injected marker
is gone. Regeneration
reproduces the page **byte for byte**.
⇒ `.gitattributes:84` is true for this family.
`scripts/git-merge-regen.mjs` behaved exactly
as its own comment says it should, and ⛔ nothing in this PR changes the
driver's merge
behaviour, weakens a gate, adds an exemption, or makes the driver prefer
a side in a new case.
## So the work is ask 2 — and the safety net DOES hold, but nothing
proved it
The remaining question is the one the ruling names: on the path this
incident took — a
human or agent hand-repairing a merge **in a linked worktree** — does
the refusal actually
fire? Measured live, in this worktree, with a real merge of a real
routed path:
```
git merge exit 0 (the card's zero exit, reproduced)
marker written to .git/worktrees/.../os-regen-pending YES
the ordinary commit right after REFUSED by .githooks/pre-commit
the push REFUSED by .githooks/pre-push
```
The net holds. What is unguarded is that **no test says so.** The driver
ends every deferral
by *printing* "The pre-commit hook will not let this commit through
until you do", and the two
cases that come closest each stop one step short in the same direction:
- `endToEnd()` performs a real merge, asserts the marker exists, and
never commits — so git
never runs a hook.
- `check-regen-pending.mjs`'s deferred-merge fixture sets
`core.hooksPath=/dev/null` ("the
fixture drives the script itself") and writes the marker by hand — it
grades the checker's
verdict, never git's willingness to invoke it, and never the path the
driver wrote to.
The joint property had no coverage at all, and the one arrangement in
which it can break is
the one AGENTS.md Prime Directive #11 makes mandatory. `markPending()`
in the driver and
`gitDirPath()` in the checker are two **independent**
`--absolute-git-dir` call sites; in a
primary checkout that directory and `--git-common-dir` are the **same
directory**, so a
regression to the common dir keeps every single-tree fixture green while
every real merge in
every agent's worktree silently loses its refusal.
## What landed
**1. `endToEndWorktreeRefusal()` — a sixteenth self-test battery.** It
builds a throwaway
repo with a linked worktree, reaches the REAL `.githooks` and the REAL
checker through
untracked symlinks (so the *relative* `core.hooksPath` resolves per
worktree, as
`setup-git-hooks.mjs` registers it — a hook this file wrote out would
test an imitation),
merges a routed path inside the linked worktree, and asserts nine legs:
| leg | assertion |
|---|---|
| 1 | the marker lands in the LINKED worktree's own git dir |
| 2 | …and NOT in the shared common git dir — the discrimination only a
linked worktree can make |
| 3 | the ordinary commit after the merge is REFUSED, by git, through
the real `pre-commit` |
| 4 | …naming the artifact, so the refusal is this one and not some
other failure |
| 5,6 | the push is REFUSED too, by the real `pre-push`, via its
os-regen leg |
| 7,8,9 | **FIRING CONTROL** — with the gate clean the same commit is
ACCEPTED, the marker is gone, and the push goes through |
Legs 7-9 are what legs 1-6 rest on: a hook that can only fail — a
missing interpreter, a
typo'd path, node erroring — satisfies every refusal assertion while
proving nothing, and
from the outside that reads exactly like "the net holds".
**2. The self-test's exit code now agrees with its own verdict.** Found
by the ablation below,
not by reading. The `--self-test` dispatch computed `failures` from the
booleans its callees
return and printed `✗ merge driver wiring is inconsistent — N
failure(s)` from that count —
then exited **0**. The only thing that ever set a non-zero code was
`fail()`, which every
callee happened to call. The two agreed by convention, not by
construction, and
`pnpm check:merge-driver` chains this file with `&&`: the first callee
to report a failure by
returning `false` alone would have made the whole battery advisory. The
verdict line and the
exit code now come from the same count.
This is an in-place fix under the bounded exemption — same defect class
as the card (a failed
check reporting as a pass), mechanical, on a file this PR already
declares, in the same gate
family, adding no new verification surface.
## Ablation — the battery can fail, and the existing cases cannot catch
what it catches
`markPending()` regressed to the shared common git dir (the exact
regression class the case
exists for), run from the committed state, mutation proven on disk
before the run and the
restore proven byte-identical after:
```
occurrences of the replaced line 1 -> 0 injected marker 0 -> 1
blob sha HEAD 4506a6d -> ebd8f1077e725251161a067d50905c3d8b116c10
self-test exit under the ablation 1 (before fix 2: 0 — see above)
✗ endToEndWorktreeRefusal — all six refusal legs red, firing control red
✓ end-to-end: conflicting packages/spec/spec-changes.json … recorded as pending <- STILL GREEN
✓ end-to-end (mixed): anchors-only deferred to OURS … <- STILL GREEN
✓ probe gate: `git merge-tree` reaches the driver and records nothing <- STILL GREEN
restore: blob sha back to 4506a6d…, residual injected text 0, `git diff HEAD` empty
```
Three pre-existing end-to-end cases stay green through a regression that
disarms the refusal
for every worktree in the container. That asymmetry is the case's whole
justification.
## Which of the three unreliable signals this makes trustworthy
Triage `5628590957` records three cards describing one apparatus whose
every signal is
unreliable, and asks which one a change repairs. This PR repairs **the
driver's exit code
being the only evidence the merge was safe** (#17602): after it, the
claim "the hook refuses
until regeneration has run" is a test rather than a sentence, on the
path real merges take.
It also, incidentally, moves **#13799**'s signal for this one member:
the `os-regen-merge`
self-test (`.github/workflows/lint.yml`) is named in #13799's
population, and fix 2 means that
battery can no longer report a failure at exit 0. ⛔ That is one member,
⛔ not #13799.
It does **not** touch **#16717** — `check:merge-driver` remains red on
macOS for the
runner-missing classifier's reasons, so a green local run of this
battery is not evidence
there. The new case is platform-neutral (git, symlinks, `pnpm -s` on a
stub), but it inherits
#16717's blind spot like every other case in the file.
## Effect on #16612 (shares this file, serialized behind this card)
**Neither easier nor harder — but it is now instrumented.** #16612
proposes retiring the only
`mixed` row, which would drop `system-context.mdx` onto the
unconditional-deferral branch this
card measured. Nothing here changes that branch. What changes is that
whoever takes #16612 now
has a case that proves the deferral's refusal actually fires end to end,
so the question
"is the safety net behind the deferral real?" is answerable by running
`pnpm check:merge-driver`
instead of by argument. ⛔ Not fixed in passing; ⛔ not re-priced here.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived
**32** commands from the real diff (1 path,
`scripts/git-merge-regen.mjs`, vs merge base
`394ccf549`). All 32 run and exit 0, reconciled with `--ran`. Exit codes
landed to disk per
command. `pnpm check:merge-driver` — the gate that owns this battery —
is among them.
`pnpm lint`'s scan was not narrowed and needs no narrowing argument: the
whole set ran at
`3d8e4c63e` — `eslint . --no-inline-config --format json` over **6752**
files, **0** errors,
**0** warnings. (The config declares it never enables type-aware linting
for any file, so a
one-file diff could not have moved another file's verdict in any case.)
changeset: **`skip-changeset`**, measured rather than assumed. The diff
is one root
`scripts/**` file; the root package is `private: true`; all 70 published
packages declare
`files[]` and none can reach root `scripts/`; the new symbol occurs in
zero published package
sources, against a lit positive control (`defineStack` hits in
`packages/spec/src`).
## Acceptance notes
- `SELF_TEST_BATTERY_FLOOR` moved 15 to 16 and the roster/dispatch
gained the new callee, as
that mechanism requires. The callee-count prose in the same block moved
with it; one number
there ("the other thirteen") was already off by one against a roster of
fifteen and is now
correct at fifteen against sixteen. Mechanical, in the block being
edited.
- Noted, not filed: `check-regen-pending.mjs`'s deferred-merge fixture
still sets
`core.hooksPath=/dev/null` and writes its marker by hand. That is
deliberate and correct for
what it grades (the checker's verdict machine), and the new case covers
the half it cannot;
a second end-to-end fixture there would duplicate it. Successor: whoever
next widens that
fixture's scenario set.
- Noted, not filed: the driver's `--self-test` prints its verdict and,
before this PR, derived
its exit code elsewhere. The sibling self-tests chained beside it in
`check:merge-driver`
(`git-env.mjs`, `check-regen-pending.mjs`) were not audited for the same
shape — out of scope
here, and neither is known to have it. Successor: none currently queued.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d588fb2 commit 689d606
1 file changed
Lines changed: 236 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
| 81 | + | |
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| |||
455 | 455 | | |
456 | 456 | | |
457 | 457 | | |
458 | | - | |
| 458 | + | |
459 | 459 | | |
460 | 460 | | |
461 | 461 | | |
| |||
470 | 470 | | |
471 | 471 | | |
472 | 472 | | |
473 | | - | |
| 473 | + | |
474 | 474 | | |
475 | 475 | | |
476 | | - | |
| 476 | + | |
477 | 477 | | |
478 | 478 | | |
479 | 479 | | |
| |||
497 | 497 | | |
498 | 498 | | |
499 | 499 | | |
| 500 | + | |
500 | 501 | | |
501 | 502 | | |
502 | 503 | | |
| |||
507 | 508 | | |
508 | 509 | | |
509 | 510 | | |
510 | | - | |
| 511 | + | |
511 | 512 | | |
512 | 513 | | |
513 | 514 | | |
| |||
517 | 518 | | |
518 | 519 | | |
519 | 520 | | |
520 | | - | |
| 521 | + | |
521 | 522 | | |
522 | 523 | | |
523 | 524 | | |
| |||
529 | 530 | | |
530 | 531 | | |
531 | 532 | | |
532 | | - | |
| 533 | + | |
533 | 534 | | |
534 | 535 | | |
535 | 536 | | |
| |||
542 | 543 | | |
543 | 544 | | |
544 | 545 | | |
545 | | - | |
| 546 | + | |
546 | 547 | | |
547 | 548 | | |
548 | 549 | | |
| |||
1624 | 1625 | | |
1625 | 1626 | | |
1626 | 1627 | | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
| 1641 | + | |
| 1642 | + | |
| 1643 | + | |
| 1644 | + | |
| 1645 | + | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
| 1652 | + | |
| 1653 | + | |
| 1654 | + | |
| 1655 | + | |
| 1656 | + | |
| 1657 | + | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
| 1663 | + | |
| 1664 | + | |
| 1665 | + | |
| 1666 | + | |
| 1667 | + | |
| 1668 | + | |
| 1669 | + | |
| 1670 | + | |
| 1671 | + | |
| 1672 | + | |
| 1673 | + | |
| 1674 | + | |
| 1675 | + | |
| 1676 | + | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
| 1681 | + | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
| 1695 | + | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| 1699 | + | |
| 1700 | + | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
| 1707 | + | |
| 1708 | + | |
| 1709 | + | |
| 1710 | + | |
| 1711 | + | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
| 1735 | + | |
| 1736 | + | |
| 1737 | + | |
| 1738 | + | |
| 1739 | + | |
| 1740 | + | |
| 1741 | + | |
| 1742 | + | |
| 1743 | + | |
| 1744 | + | |
| 1745 | + | |
| 1746 | + | |
| 1747 | + | |
| 1748 | + | |
| 1749 | + | |
| 1750 | + | |
| 1751 | + | |
| 1752 | + | |
| 1753 | + | |
| 1754 | + | |
| 1755 | + | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
| 1761 | + | |
| 1762 | + | |
| 1763 | + | |
| 1764 | + | |
| 1765 | + | |
| 1766 | + | |
| 1767 | + | |
| 1768 | + | |
| 1769 | + | |
| 1770 | + | |
| 1771 | + | |
| 1772 | + | |
| 1773 | + | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
| 1784 | + | |
| 1785 | + | |
| 1786 | + | |
| 1787 | + | |
| 1788 | + | |
| 1789 | + | |
| 1790 | + | |
| 1791 | + | |
| 1792 | + | |
| 1793 | + | |
| 1794 | + | |
| 1795 | + | |
| 1796 | + | |
| 1797 | + | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
| 1808 | + | |
| 1809 | + | |
| 1810 | + | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
| 1816 | + | |
| 1817 | + | |
| 1818 | + | |
| 1819 | + | |
| 1820 | + | |
| 1821 | + | |
| 1822 | + | |
| 1823 | + | |
| 1824 | + | |
| 1825 | + | |
| 1826 | + | |
| 1827 | + | |
| 1828 | + | |
| 1829 | + | |
| 1830 | + | |
| 1831 | + | |
| 1832 | + | |
| 1833 | + | |
| 1834 | + | |
| 1835 | + | |
| 1836 | + | |
| 1837 | + | |
| 1838 | + | |
| 1839 | + | |
| 1840 | + | |
| 1841 | + | |
| 1842 | + | |
| 1843 | + | |
| 1844 | + | |
| 1845 | + | |
1627 | 1846 | | |
1628 | 1847 | | |
1629 | 1848 | | |
| |||
1754 | 1973 | | |
1755 | 1974 | | |
1756 | 1975 | | |
1757 | | - | |
| 1976 | + | |
1758 | 1977 | | |
1759 | 1978 | | |
1760 | 1979 | | |
| |||
1773 | 1992 | | |
1774 | 1993 | | |
1775 | 1994 | | |
| 1995 | + | |
1776 | 1996 | | |
1777 | 1997 | | |
1778 | 1998 | | |
| |||
1790 | 2010 | | |
1791 | 2011 | | |
1792 | 2012 | | |
| 2013 | + | |
| 2014 | + | |
| 2015 | + | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
| 2019 | + | |
1793 | 2020 | | |
1794 | 2021 | | |
1795 | 2022 | | |
| |||
0 commit comments