fix(devx): resolve a regen row's gen:/check: in its declared owner, not only in packages/spec - #13616
Merged
os-project-manager merged 1 commit intoAug 31, 2026
Conversation
…ot only in packages/spec `git-merge-regen.mjs --self-test` resolved every row's script names in `packages/spec/package.json` and nowhere else, so an artifact owned by ROOT tooling could not be registered for `merge=os-regen` at all — however exactly it matched the pathology the driver exists for. The refusal was correct about the tree and wrong about the world: it read as "you named a script that does not exist" when the truth was "this artifact is not owned by packages/spec", and an author following it literally moves root tooling into a package it does not belong to, purely to satisfy a lookup path. Rows now carry an `owner` (defaulting to @objectstack/spec, which is what all 13 declared implicitly), and the refusal names the manifests it searched. The owner is DECLARED rather than searched for, because a lookup-only widening would have left the worse half standing. Two consumers need to know WHICH manifest owns a row, not merely that some manifest has the name: the driver PRINTS a regeneration command and `check-regen-pending.mjs` SPAWNS one, and both were bound to `packages/spec`. A root-owned row under a widened lookup would have reconciled green and then been spawned in a directory that does not define its script — measured, `pnpm -s check:sdui-lockstep` exits 254 there and 0 at the repo root — leaving the artifact permanently stale and every commit refused. Registered-and-unreconcilable is a worse defect than unregisterable. `reconcileOwnership()` pins the rule against the real root manifest on every run, including the case a permissive lookup would fail: a root-only script name must NOT resolve under @objectstack/spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
This was referenced Aug 31, 2026
os-project-manager
marked this pull request as ready for review
August 31, 2026 03:37
os-project-manager
deleted the
claude/issue-13585-merge-regen-root-manifest
branch
August 31, 2026 03:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13585
git-merge-regen.mjs --self-testresolved every regen row'sgen:/check:names inpackages/spec/package.jsonand nowhere else, so an artifact owned by root tooling could not beregistered for
merge=os-regenat all — however exactly it matched the pathology the driver existsfor. The refusal was correct about the tree and wrong about the world: it read as "you named a
script that does not exist" when the truth was "this artifact is not owned by
packages/spec."Rows now carry an
owner, defaulting to@objectstack/spec— which is what all 13 existing rowsdeclared implicitly — and the refusal names the manifests it searched.
Verified on
1997df337.必答項 — is
check:merge-driver's reconciliation still two-way?Yes, and that requirement is what chose the shape of the fix. The answer has two halves.
1. The lookup stayed exact, so nothing became merely permissive
A name is looked for in one manifest — the row's declared owner's — never in "any manifest that
happens to have it". A widened lookup would have made resolution non-deterministic the moment two
manifests defined the same
gen:name, and would have accepted a row that names a root-only scriptwhile claiming a package owner. It is pinned as a live case rather than argued:
reconcileOwnership()asserts thatcheck:merge-driver, a root-only script name, resolvesunder
ROOT_OWNERand does not resolve under@objectstack/spec. A permissive lookup passesevery other assertion in that block and fails exactly that one.
Both directions of the
.gitattributes↔ table reconciliation are untouched, and an owner thatresolves to no manifest is a refusal, not a skip — an unresolvable owner means a row whose
scripts were never verified, which is the state this reconciliation exists to make impossible.
2. The half that a one-sided loosening would have left standing — measured, not reasoned
This is the "registered but unreconcilable" seam, and it was real. Two consumers need to know
which manifest owns a row, not merely that some manifest has the name:
git-merge-regen.mjsprints a regeneration command at merge time;check-regen-pending.mjs(thepre-commitgate) spawns one.Both were bound to
packages/spec— the runner's spawn directory waspackages/specunconditionally. Measured before the
ownerfield existed:So a lookup-only widening would have produced a row that reconciles green and is then
permanently unsatisfiable in the hook. Driven end to end against the real
check-regen-pending.mjs, same root-owned row, the only difference being theownerdeclaration:A — root-owned row, no
ownerdeclared (what a lookup-only widening leaves behind):Every commit refused, forever, with remedy advice that cannot work.
B — the same row declaring
owner: ROOT_OWNER(this PR):That is why the owner is declared rather than searched for: one declaration, read by the gate,
the driver's advice and the hook's spawn, so the command the driver prints is the command the gate
runs. Registered-and-unreconcilable is a worse defect than unregisterable, and a widened lookup
alone would have manufactured it.
Non-empty control — rejected before, registered after
The artifact that produced the finding,
docs/audits/2026-08-tenant-audit-write-call-sites.counts.md,lives on PR #13584's branch and is not on
main, so it could not be used. The control used insteadis
packages/sdui-parser/objectui-lockstep.jsonwithgen:sdui-lockstep/check:sdui-lockstep— the only root-owned
gen:/check:pair in the root manifest onmain, i.e. a genuinelyroot-owned artifact and not a
packages/specentry testing a path that already worked.Before the fix (registration attempted on
origin/main, then backed out) — the card's exactrefusal shape, reproduced on a different artifact:
After the fix, same row, still no
owner— still refused, because the loosening is not blanket,and the refusal now names what it searched and points at the right repair:
After the fix, declaring
owner: ROOT_OWNER— registers, andpnpm check:merge-driveris greenend to end (exit 0):
And a third leg, an owner nobody can resolve refuses rather than skipping:
All three registrations were temporary, applied on top of the committed implementation and backed
out with
git checkout HEAD -- ...; restoration was proven by an emptygit diff HEADplus aHEAD-blob-hash comparison per file, not by an exit code.
Why the control artifact is not registered permanently here
packages/sdui-parser/objectui-lockstep.jsoncan now be registered, and deliberately is not.gen:sdui-lockstepre-reads objectui's side, which needs an objectui checkout the merger is notholding — so the deferred regeneration would be unrunnable at exactly the moment
pre-commitdemandsit, and the driver only ever defers. Its conflict is also not the shape the driver exists for: the
record holds one side at a named revision, so two branches that each re-recorded it disagree
about which objectui revision the parity was verified against, and no regeneration answers that.
Registering it to give this PR a permanent control would be the test dictating the code, so the
durable guard is
reconcileOwnership()instead — it reads the real root manifest through the samefunctions the driver and the hook use, on every
check:merge-driverrun.The refusal now names which manifests were searched
Per the card: a refusal that misdescribes its own cause sends the reader to the wrong repair. The new
text names the owner and manifest per dead row, lists every manifest searched, states that an
undeclared row defaults to
@objectstack/spec, and explicitly refuses the repair the old textimplied — moving root tooling into a package to satisfy a lookup path.
PM mechanism assumptions — verified
"All currently-registered paths are spec-owned or spec-adjacent; the single-manifest assumption
has never been stressed." Confirmed, with a correction to the count. There are 13
registered paths on
main, not 14 (the card's14was measured with its own candidate rowalready added). Ten are under
packages/spec/; the other three —docs/protocol-upgrade-guide.md,docs/audits/2026-07-unknown-key-strictness-ledger.counts.mdand
content/docs/references/**— are outsidepackages/. All 13 name scripts defined inpackages/spec/package.json, so the assumption held by coincidence of ownership, never byconstraint.
"
docs/protocol-upgrade-guide.mdis outsidepackages/with only its scripts inside — the realboundary is the manifest, not the artifact path." Confirmed, and stronger than stated: it is
one of three such rows, not one. The boundary was always the manifest.
"The two-way reconciliation is the fragile half; if loosening it cleanly is impossible without a
declared-owner field, say so." Confirmed, and that is the shape delivered. A widened lookup
cannot supply what the driver's advice and the hook's spawn need — which package — so the
declared-owner field is not one of two acceptable options here but the only one that keeps the
reconciliation two-way. See the 必答項 section.
On #13335
Triage's grading placed #13335 as the symptom of this cause, with this card blocking it. That
blocking relationship does not hold, and the acceptance case could not be run as specified.
skills/*/references/_index.mdis written bygen:skill-refsand gated bycheck:skill-refs, andboth of those are defined in
packages/spec/package.json(lines 257-258), not in the rootmanifest:
So that artifact was never blocked by the single-manifest lookup — it is spec-owned and could
have been registered at any time. Registering it here would therefore have been the vacuous control
this lane rejects: a
packages/specentry exercising a path that already worked. It is also adisposition question in its own right (route it, or record why it must keep text-merging), which is
the substance of that card and not of this one. #13335 stays open and untouched; nothing under
skills/**is in this diff. The card's own body hedged this correctly — "If that card's generatoris also root-owned, this one blocks it" — and it is not.
Verification
Gate family derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(15 families + 2 convention-triggered for editing a gate script), not guessed. All run at
1997df337:check:merge-driver— green, including the new✓ owner resolution: 11 case(s) pinnedlinecheck:agent-test-spelling,check:bash32-floor,check:cli-command-ids,check:cross-package-test-inputs,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,check:watch-hint-literal— greencheck-ci-filter-parity.mjs,check-cross-package-test-inputs.mjs,check-shard-attestation.mjs— greenbare-root-worklist.mjs --self-test,check:pm-dispatch-gates(the two gate-script conventions) — green@objectstack/speccheck:generated(all 14 generated artifacts up to date) andcheck:docs(230 generated files in sync) — green, after a full spec build
pnpm lint— the whole repo,eslint . --no-inline-config, green. No narrowing claimed.check:nul-bytes— green; the diff also self-scanned for raw control bytes, nonecheck-declaration-mirrors.mjs(the.d.mtsmirror of the edited runner) — greendist-freshness.test.ts,schema-tree-freshness.test.ts,build-schemas-check-mode.test.ts— 3 files, 84 tests passed —plus
check:scripts-typecheck— greencheck-test-completeness.mjs— NOT MEASURED, by the gate's own declaration: it grades a savedturbo run testlog and exits 3 (PREREQUISITE NOT MET) when run without one, which is the shapethe derived family invokes. Not a red.
No changeset: this PR releases nothing from any package — the diff is three root tooling scripts —
so it carries
skip-changeset.Generated by Claude Code