feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had - #18319
Conversation
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…r vitest Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 7 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cbdc39434111fc2fdbeeb4b9843938ee653edc30 && git checkout cbdc39434111fc2fdbeeb4b9843938ee653edc30
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2 1d4c99bcbdf082c937d06157d488d8ac191939b6 && git checkout -B drift-repro 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2 && git merge --no-ff 1d4c99bcbdf082c937d06157d488d8ac191939b6
node scripts/docs-audit/affected-docs.mjs --json 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2
|
This PR is red on purpose, and it is blocked on one maintainer decision — not on an unhandled defectWhat is failing, read from Why. The card's ruling adopts The reachability question that decides whether those 21 matter was answered by a failing probe with a lit control, before the change: So Why nothing was done about them. The dispatch order for this card was explicit: if the 21 turned out to be real, stop and report — ⛔ do not rewrite twenty-one fixture ids on an implementer's own authority, and ⛔ do not weaken the regex. Both prohibitions still hold. Weakening is additionally refused by the ruling itself, which requires the pattern 「declared once and referenced from both sites so the two cannot drift again」; admitting underscores on one side re-opens exactly the drift the card exists to close. What is needed. One decision, and it is the maintainer's because the ruling's own population statement is what turned out to be wrong:
The decision is recorded on the card at #17534 and is with the maintainer now. This PR stays draft with no auto-merge and no enqueue until it is answered; the rest of the lane's queue keeps moving. Not blocked, and already fixed in this PR — a second premise that turned out to be real: every Generated by Claude Code |
…omain manifest ids `manifest.id` now enforces the reverse-domain rule, and 21 fixture declarations under `packages/qa` carried an underscore in their last segment. 20 of them are renamed here, underscore to hyphen, exactly the repair the refusal message itself suggests. The 21st, `com.dogfood.flow_fixture` in `packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts`, is left alone: it is not a single-occurrence declaration. The same literal appears twice more in `packages/spec/src/kernel/manifest.test.ts`, as the sample input for the "repairs a dotted value in place" refusal-message assertions. The rename mandate covers single-occurrence declarations, so that one is reported rather than swept. `test/flow-node.dogfood.test.ts` therefore still refuses at boot. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
`com.dogfood.flow_fixture` was held back in the previous commit because the literal also appears twice in `packages/spec/src/kernel/manifest.test.ts`, so it was not a single-occurrence declaration and the mechanical rename mandate did not obviously cover it. Those two hits are deliberate NEGATIVE TEST INPUT, not references to this fixture: line 643 asserts the refusal for that value suggests `com.dogfood.flow-fixture`, and line 653 feeds it alongside `blank`, `my_app` and `support_desk` as a batch of ids the schema must refuse. They are left exactly as they are — renaming them would delete the test of the rule this card adds. The new id is the one the refusal itself prescribes for this value, the same underscore-to-hyphen repair the other 20 took. `manifest.test.ts` stays green (66 passed) with the fixture renamed, which is what shows the two are independent. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Status refresh — the blocker changed, and my earlier comment on this PR is now the wrong description of itMy comment Measured at head The fact that decides how serious this is: of the 63 from the first pass, 62 are in a Current CI, head Why nothing further is being pushed. Renaming ~46 more fixtures across seven additional packages is a scope decision, not a repair — and an hour ago I wrongly told the implementer to fold two The full measurement and the question now with the maintainer are on the card: #17534 (comment) This PR stays draft, with no auto-merge and no enqueue, until that is answered. The 21 ruled renames stay as they are — they are correct and they are landed on this branch. Generated by Claude Code |
…pellings
The maintainer ruled that the fixtures outside `packages/qa` are renamed in
this PR too. Each new id is the one the schema's own refusal prescribes for
the old value, so the tree and the diagnostic agree.
Renamed (18 ids, 16 files): `packages/cli` 14, `packages/lint` 3,
`packages/metadata` 1, `packages/plugins/plugin-dev` 3 (see below), plus the
docblock noted last.
Three of these were NOT single-literal swaps, and are called out because a
naive rename would have silently deleted what the test pins:
* `dev-i18n-packages-reader.test.ts` builds a dependency CYCLE between two
packages, and the `dependencies` keys address the ids BY NAME. The ids and
both dependency keys are renamed together; renaming only the ids would
have dissolved the cycle and left the test asserting nothing. It still
reports `Circular dependency detected`.
* `metadata-type-schema-gate.test.ts` and `format-zod-union.test.ts` and
`authoring-rule-command-parity.test.ts` each carry a `namespace` that
repeats the old id, and the first also carries a route path
`/api/v1/apps/gate_probe/things`. Only `manifest.id` is renamed —
`namespace` has its own rule and the path pairs with it.
* `plugin-dev/src/index.ts` is a DOCBLOCK example, not a fixture: prose
teaching `manifest: { id: 'my-app' }`, an id its own schema now refuses.
It cannot red a test; it is corrected because it is published teaching.
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…ges/qa
19 more ids, each the target the schema's own refusal prescribes for the old
value, rewritten at the exact offset of the `manifest.id` literal so that a
`namespace`, a `name` or a route path repeating the old spelling is left alone.
Scope of THIS commit is deliberately the single-occurrence ones. Every id here
occurs exactly once in its own file, so the rename cannot silently unhook an
assertion that addresses the old value.
Not included, and reported rather than guessed:
* 15 ids whose literal occurs MORE than once in its own file, so renaming
the declaration alone would leave assertions naming the old value —
`pkg-a` (7 occurrences), `test` (6), `a` (6), `my-app` (5), `demo` (3) and
ten more. These need a coordinated rename of the id and every reference,
which is reading work per file, not a mechanical swap.
* `com.test.14397` — the schema offers NO suggestion for it, because the
failing segment starts with a digit and no mechanical repair exists.
* `artifact-granted-permissions.test.ts` — not a rename at all: it pins the
ADR-0130 D4 door ORDER, that `''` passes the schema and is caught one door
later. The new rule moves the refusal to door 1, so no conforming id can
preserve what the test pins.
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
The last 16, each read before it was touched. Every replacement is anchored on
the `id:` key, so a `name`, a `namespace` or a field named the same is left
alone, and each file's occurrence count was asserted before and after.
Reading first is what made these safe, and two would have gone wrong without it:
* `artifact-collections.test.ts` has `'a'` six times, but only two are the
package id — the other four are a FIELD named `a`
(`{ a: { name: 'a', type: 'text' } }`). A whole-file swap would have
renamed a field the assertions address.
* `package-registry.test.ts` and `lint-protocol-range.test.ts` repeat the old
id as a `namespace`, and `serve-host-config.test.ts`,
`metadata-collection.test.ts` and others repeat it as a free-form `name`.
`namespace` has its own rule and `name` has none; neither is renamed.
Where the repeat WAS a real reference it moved with the id: all seven `pkg-a`
in `packages-capability-gate.test.ts` (package record ids and manifest ids in
the same mock), both in `packages-uninstall-envelope.test.ts`, both `test-app`
registrations in objectql, and both `locale-producer-app` sites in runtime.
`com.test.14397` becomes `com.test.card-14397`: the schema offers no suggestion
for it, because the last segment starts with a digit, so the name was ruled
rather than derived. `ManifestSchema` confirms the replacement parses.
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…nifest-id-reverse-domain-regex
…tree The os-regen merge sequence's collection commit. `api-surface/kernel.json` and `export-origins/kernel.json` were edited on BOTH sides, so the merge driver ran, exited 0 and silently kept one side; step 2 took main's side and this commit carries the regeneration from the merged tree, built first so api-surface reads a dist that matches src rather than reporting phantom removals. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…s on the merged tree The merge of origin/main routed six generator-owned artifacts through the os-regen driver, which exits 0 while keeping one side. Regenerated from the merged sources on a committed base, so main's additions and this branch's `MANIFEST_ID_PATTERN` / `MANIFEST_ID_EXAMPLES` entries are both present. Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE Co-authored-by: Claude <noreply@anthropic.com>
…ED direction Ruling option A, component 2. `ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, and `AssembledPackageBodySchema` inherits it, so a `packages[]` entry whose `manifest.id` is empty is refused at the schema door — before `artifactPackageId`'s `id || name` fallback is consulted. D4 states that order, cites the pattern, and records the direction the change reverses on a consent path: what used to leave a `''`-keyed consent record unbound with the package loaded anyway (fail-OPEN) now refuses the artifact outright (fail-CLOSED). D4's two branches are unchanged; this is an addition to the record, not a reversal of a decision. Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE Co-authored-by: Claude <noreply@anthropic.com>
`origin/main` landed `packages/spec/src/api/package-api.test.ts` while this branch was open, and two of its cases measured the accept set this branch narrows: the control that completes the domain-handler-registry drive (`pkg-a` is not reverse-domain notation, so completing the missing keys no longer makes it parse) and the residual that ran the other way (a whitespace-only `id` used to parse here while the door answered 400). Both are rewritten to what is now measured, each with a lit control proving the id is what decides it. No behaviour moves and the pattern is not relaxed. Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE Co-authored-by: Claude <noreply@anthropic.com>
CI on this branch's merge head reds `packages/cli`'s integration tier: the #18490 file composes an artifact whose `manifest.id` is `''` and parses it through `ObjectStackDefinitionSchema`, which this branch's pattern now refuses. That file said it would red first and that the pins under it would then measure nothing, so the red is converted into the pin it asked for: the first case now pins the refusal itself, on `manifest.id`, with a lit control proving the id is what decides it, and the three cross-door cases keep measuring that the build names a package exactly as the runtime fold does at an identity a command can actually hand down. The divergence the file was written around is structurally closed — a parsed package's id is non-empty by construction, so the owner's `id || name` never falls back. The degenerate fixture is not kept alive by bypassing the parse. Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Isolated at-tier reviewer, spawned for this head alone; reviewed 2026-09-20T16:56Z. Bases: merge-base ① Derived judgments
Also run in the worktree, every build/test through ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL ⛔ F1 — ①6/③: the PR-body claim 「zero literal refused Generated by Claude Code |
… to reverse-domain
`ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, so every remaining literal
`manifest.id` in the tree that the pattern refuses is either a fixture that would
not survive its own schema or published guidance teaching an author a spelling the
runtime rejects. Both are renamed here; nothing about the pattern moves.
Fixtures (no assertion reads the id string, so each case keeps its meaning):
- packages/lint/src/validate-translation-references.test.ts — the 13
`crm_core` / `crm_service` package ids become `com.example.crm-core` /
`com.example.crm-service` (these arrived from `main` through the merge).
- packages/create-objectstack/src/rewrite-identity.test.ts — the scaffolded
config fixture's `id: 'x'` becomes `com.example.x`, the spelling its own
sibling fixture in the same file already used.
Published guidance:
- packages/spec/prompts/create-new-project.md — `com.example.my_erp` →
`com.example.my-erp` (this file ships in the spec tarball).
- content/docs/getting-started/your-first-project.mdx — `my-app` →
`com.example.my-app`, which is what `create-objectstack` now derives for a
project named `my-app`; `namespace: 'my_app'` is untouched (a namespace admits
underscores and this key does not).
- content/docs/api/declarative-endpoints.mdx, content/docs/protocol/kernel/http-protocol.mdx
— `acme-crm` → `com.acme.crm`; `namespace: 'acme'` untouched.
- content/docs/api/metadata-api.mdx — `plugin-auth` → `com.objectstack.plugin-auth`
on both request bodies and both echoed responses.
- packages/services/service-package/README.md — `crm` → `com.example.crm`,
including the `get`/`delete` calls that address the same package.
- docs/qa/platform-checklist/areas/{api-backend,platform-core}.json — the scratch
probe ids the two install-door items POST move to reverse-domain spellings;
both items bump `revision` and append the history entry that edit owes.
Deliberately NOT renamed, each for a stated reason:
- packages/runtime/src/domain-handler-registry.test.ts:591 `pkg-a` — it pins the
HTTP install door's residual, which answers 201 to an id every other door now
refuses; renaming it would delete the only pin on that gap.
- the `FROM` examples in this PR's own changeset — showing the refused spelling is
what a FROM → TO migration note is for.
- the `manifestId` values in the local install-ledger fixtures
(packages/cli/src/commands/doctor-ledger-*.test.ts,
packages/cloud-connection/src/*.test.ts) — that is a plain `manifestId: string`
field on the ledger entry interface, judged by no schema; it is not
`ManifestSchema.id` and not `PackageSchema.manifestId`.
- the `Not A Reverse Domain` fixtures in packages/spec — the negative controls of
the template-manifest gate and its test.
Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Isolated at-tier reviewer, round 2 on this head; reviewed 2026-09-20T18:12Z. Merge-base ① Derived judgments
Suites and gates run here, each through ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL ⛔ F1 (residual) — ①1: the PR-body claim 「zero literal Generated by Claude Code |
…to reverse-domain
`protocol.package-publish-audit-rows.test.ts` declared `const PKG =
'pkg_helpdesk'`, which reaches `manifest: { id: PKG, namespace: NS }` on the
`registry.getPackage` stub and is refused by `MANIFEST_ID_PATTERN`. The value
becomes `com.example.helpdesk`.
Every use reads the constant -- the three `toContain(PKG)` assertions included
-- so each case keeps its meaning; the sibling assertion on the namespace
prefix (`helpdesk_`) is unaffected, since the new value does not contain it.
Verified against the BUILT `ManifestSchema.shape.id` after a spec build, with a
lit control on the judge: 'pkg_helpdesk' refused, 'com.example.helpdesk'
accepted.
Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Isolated at-tier reviewer, round 3 on this head; reviewed 2026-09-20T19:07Z. Merge-base ① Derived judgments
Suites and gates run here, each through ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Round 2's ⛔ F1 (residual) is closed at Generated by Claude Code |
|
Provenance — carriers cleared on PASS, director seat (
Generated by Claude Code |
|
Enqueued by the director seat ( Generated by Claude Code |
Part of #17534 — this lands the ruled change on the authoring contract, the refusal, both scaffolders and the ADR-0087 entry, plus (2026-09-20, under ruling batch #200 item 3) the merge of⚠️ The PR is therefore a GOVERNED diff (
origin/mainand ruling Q3-A component 2: the ADR-0130 D4 record-keeping block.docs/adr/**, tier H, 1 of 95 paths, under the 5000-line line): it lands by the maintainer's own merge or an authorized APPROVED review from aGOVERNED_APPROVERSaccount, after the at-tier contract review.Part of, notFixes: #17534 closes when the seat closes it.Clause-②: yes —
manifest.id's accept set narrows (the reverse-domain pattern is enforced where a bare string was accepted); the carrier of record is on the card (5691010940).What the ruling asked for, and what is here
1. One declaration, referenced from both sites.
MANIFEST_ID_PATTERNis exported frompackages/spec/src/kernel/manifest.zod.ts;ManifestSchema.idandPackageSchema.manifestIdboth reference it.manifest.test.tsholds a per-field verdict table across the two, so a future edit to either has to break a test to drift. The.describe()moves to "must match reverse-domain notation" in the same commit as the regex — #17921's self-contained half, the wording constraint, is satisfied by construction.2. Refusal with a remedy (#4001). The message names the key, echoes the value, shows both documented examples, and prescribes a repair it has already checked against the pattern. Built on the existing
formatSuggestionfromshared/suggestions.zod.ts— no new message builder.The suggestion arm is conditional on purpose: a bare word gets
com.example.NAME; a value that already carries a dot gets its underscores hyphenated in place; a value nothing mechanical rescues gets no suggestion rather than a wrong one. Every candidate is tested against the pattern before it is offered — #17461's lesson applied to the remedy, not only to the examples.The two
@examples were verified first, as the ruling required.com.steedos.crmandorg.apache.supersetboth match. They are no longer a comment:MANIFEST_ID_EXAMPLESis the one list the TSDoc, the refusal and the pin all read, so an example that fails its own rule is now a red test rather than bad advice.3. Both scaffolders derive a conforming id. The bundled template ships
com.example.blank, andcreate-objectstackinterpolatesderiveManifestId(projectName).os initderives from the project name too. The second half prerequisite 4 named is real and is fixed here:os init my-appsanitizes the namespace tomy_appand every template interpolated that namespace into the id, producingcom.example.my_app— refused by this very schema. One project name, two identifiers, contradictory rules; neither can be read off the other. Pinned from both sides against the importedMANIFEST_ID_PATTERN, never a restated regex.4. Migration. ADR-0087 semantic entry
manifest-id-reverse-domain-required— a structured TODO, no automatic rename, because the value IS the identity: the registry addresses a package bymanifest_id, an installed row is keyed on it and a dependent declares it. The changeset carries the matching disposition marker and shipsminorunder the launch-window convention. Sub-question (1) is answered in the entry's ownreasonfrom the TSDoc's words: the rule is "unique across the entire ecosystem", so a package that is never published is still named for the ecosystem it may join.The former blocker — landed, gate green
The 21
packages/qamanifest ids (and, after the maintainer widened the scope on 2026-09-15, the further fixtures acrosspackages/cli,runtime,plugins,spec,objectql,lint,rest) were renamed inside this PR under the 2026-09-15 rulings on #17534; at head1d4c99bcbd(the F1 rounds after the at-tier reviews 5751237820 and 5751666135; round 3 PASS 5751970203) zero literalmanifest.idvalues the pattern refuses remain in the tree outside the declared residues — the install-door residual pinpackages/runtime/src/domain-handler-registry.test.ts:591(pkg-a, kept on purpose: it pins the HTTP install door that still reads the id positionally), this PR's own changeset FROM → TO examples (a migration note must show the refused spelling), and the spec template-manifest negative controls — measured by the round-3 reviewer's eight-leg sweep (brace-key, JSON key, window,manifestId,const manifest, aliased body,id: CONSTANT, factory body, plus an inverse constant leg) over 9070 tracked files through the BUILTManifestSchema.shape.idwith a 12-shape injected control, 12/12 caught (record 5751970203; the dev's own sweeps are on #17534, 5751523635 and the round after it); theDogfood Regression Gate(1/3, 2/3, 3/3) is green on CI. The 2026-09-20 merge had carried 13crm_core/crm_serviceids frommainintopackages/lint/src/validate-translation-references.test.ts, and seven published guidance sites (packages/spec/prompts/create-new-project.md, fivecontent/docspages,service-package/README.md) plus two platform-checklist items and onecreate-objectstackfixture were still teaching refused ids; all renamed in the F1 round (44 occurrences over 10 files). The merge oforigin/main(2026-09-20,scripts/pm/os-regen-merge.sh, merge commit only) forced two pin repairs —packages/spec/src/api/package-api.test.tsandpackages/cli/src/utils/nav-contribution-groups.package-id.test.tslanded onmainwhile this branch was open and measured the accept set this branch narrows; each is rewritten to the measured reality with a lit control,MANIFEST_ID_PATTERNuntouched. The ADR-0130 D4 block records the entry door order (DOOR 1, the schema, refuses an emptymanifest.idasINVALID_ARTIFACT_PACKAGE_ENTRY/ 422; DOOR 2,artifactPackageId, never runs for that spelling), citesMANIFEST_ID_PATTERNandPackageSchema.manifestId, and names the fail-OPEN → fail-CLOSED direction and its population — an addition to D4, not a reversal.In-package fixtures repaired here
The same defect existed inside the packages this card does own, and those are fixed — mechanical, single-literal, and each fixture keeps pinning exactly what it pinned before: three bare ids in
packages/spectests (p,acme), four inpackages/clitests (todo,demo,bad,a/b), and the five golden corpus entries inpackages/cli/src/lint/corpus.ts. That last one is not a fixture: it is the worked-example set the metadata generator imitates, and it was teaching bare-word ids.packages/create-objectstack/vitest.config.tsgains an anchored source alias for the one spec subpath its tests now import, becausepnpm check:test-source-aliascorrectly refuses a pin whose verdict is a function of build state.Not touched, deliberately
docs/adr/0087-metadata-protocol-upgrade-contract.md— the registration this gate wants is the changeset's disposition marker plus the entry file, both present. Editing the ADR would make this a governed-surface diff for no gain. Also untouched: every file in open PR #18198,packages/spec/src/shared/retired-key.ts,packages/spec/src/ui/chart.zod.ts.Verification
Run at
d0d859edbc(the original round). Re-run at the final head6783718d6a(2026-09-20): 137 derived gate families, 137 run, 0 NOT-MEASURED; runtime 270 files / 3734 passed; spec 502 files / 14733 passed, typecheck andcheck:generated(15/15) green; cli unit 221 files / 3127 passed and integration shards 26/207 + 25/220 passed; create-objectstack 16 files / 215 passed;pnpm lintrepo-wide exit 0; ADR anchor/symbol/link gates andcheck-adr-0087-registration --base origin/mainexit 0. The readings of record are in theos-dev-reporton #17534 (5751052645).node scripts/pm/dispatch-gates.mjs --ranpnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/cli exec vitest run --project unitpnpm --filter @objectstack/cli typecheckpnpm --filter create-objectstack testpnpm --filter create-objectstack typecheckpnpm lint(repo-wide,eslint . --no-inline-config)pnpm --filter @objectstack/spec check:generatedapi-surface,export-origins,content/docs/referencespackages/qadogfood tierpackages/cli's integration tier is declared to CI: the diff touches no spawn entry, nobin/, notest/helpers/serve-process.tsand no driver or kernel boot path.Original author: Claude Code session
session_01KB5PFtxuy1x3dcR5gxudx6(the implementation rounds); merge, ADR-0130 D4 block and pin repairs by the director seat's round,session_012GcsUbuqFGBibkEDMRC1eE.Generated by Claude Code