Skip to content

fix(runtime)!: POST /packages/:id/adopt-orphans refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED - #24861

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24813-adopt-orphans-writable-gate
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24813-adopt-orphans-writable-gate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24813

Clause-②: no (narrowing)

What changed

POST /api/v1/packages/:id/adopt-orphans now asks requireWritablePackage (that is, isWritablePackage, the ADR-0070 D2 read-only rule) for the path's id, with the verb adopt items into. It asks after requireManageMetadata and the walled refusal, and before the protocol is resolved: the order disable, publish, the manifest PATCH and delete already use (manage, then walled, then writable, then the service that writes). Adopting into a package the deployment ships is refused 422 WRITABLE_PACKAGE_REQUIRED: booted by code, installed from the marketplace, or a stored row whose manifest scope is system / cloud. The rule is not re-derived at this door.

  • packages/runtime/src/domains/packages.ts: the gate, one call plus its comment, on the adopt-orphans arm only.
  • packages/runtime/src/domains/packages-adopt-orphans-readonly.test.ts: the pins, 9 cases over a real store (better-sqlite3 :memory:, the real sys_metadata* objects, a real ObjectStackProtocolImplementation over a real ObjectQL). The orphans come from the real producer, a package-less saveMetaItem, and every refusal reads the rows back.
  • .changeset/24813-adopt-orphans-readonly-package.md: @objectstack/runtime minor under the launch-window convention for a breaking narrowing, with FROM / TO for an integrator, the Clause-② line, and the ADR-0087 disposition not-required (no-migration-prescription).

A CONTRACT_REVIEW_TIER review is owed before the queue (narrowing); the seat runs it.

Contract

Every input whose answer changes, old to new. A manage_metadata caller, not refused by the wall (the single posture, no tenancy service, the operator rung, or isSystem):

Request Before After
POST /packages/ID/adopt-orphans where ID is read-only (code-booted, marketplace-installed, scope: system, scope: cloud), and the protocol service has reassignOrphanedMetadata 200 with { success, reassignedCount, reassigned, targetPackageId }; every package-less sys_metadata row rebound to ID 422, error.code: WRITABLE_PACKAGE_REQUIRED, error.details.packageId: ID, error.details.docs: docs/adr/0070-package-first-authoring.md; nothing rebound, reassignOrphanedMetadata never called
The same read-only ID where the protocol service is absent or has no reassignOrphanedMetadata 501 Orphan adoption not supported 422 WRITABLE_PACKAGE_REQUIRED (the gate precedes the protocol)

Unchanged, each pinned or already pinned:

Request Answer (before and after)
Anonymous caller, any ID 401, the domain's anonymous floor
A caller without manage_metadata, read-only ID 403, the capability gate answers first (pinned here)
Under isolated / group, a holder below the operator rung, read-only ID 403 PERMISSION_DENIED naming the posture, the walled refusal answers first (pinned here)
A writable package (an org's own, scope: project) 200, every orphan bound to it (pinned here, rows read back)
A writable package, no adopting protocol 501 (pinned here)
An ID nothing installed 200, the orphans bound to that ID: the predicate treats an unregistered id as writable (pinned here, rows read back)
No ObjectQL registry 503

The response shape of the 200 arm and the request body (actor) are untouched.

Measured

Premise. reassignOrphanedMetadata (packages/metadata-protocol/src/protocol.ts, origin/main 41646a578) refuses an organization-scoped request first and then rebinds every package-less row (package_id null, empty or the sys_metadata sentinel, minus artifact-backed overlays) to request.targetPackageId. It asks no writability question and no existence question. So the premise holds and the producer to fix is the door, as the card's Wanted says.

Reproduction at base 41646a578 (a throwaway probe, not committed; same rig as the pins): adopting into each read-only id answered 200 {"success":true,"reassignedCount":2,...} and both orphans were bound to it: [["loose_grid","com.acme.codepkg"],["loose_board","com.acme.codepkg"]], and the same for the marketplace, system and cloud ids. With a protocol that cannot adopt, a read-only id answered 501 NOT_IMPLEMENTED.

The pins at 9c77155aa (committed ahead of the fix, against the unchanged arm): Tests 5 failed | 4 passed (9). The four read-only rows and the gate-precedes-protocol row are red (expected 200 to be 422, expected 501 to be 422); the two order rows and the two CONTROL rows are green.

At 6d9e82dc4 (the fix): the new pin file with the seven suites that exercise the adopt-orphans arm or the sibling gates (packages-capability-gate, packages-walled-write-refusal, packages-vetted-org-source, packages-protocol-handle-typing, packages-readonly-gate, packages-manifest-patch-readonly, package-duplicate-adopt-org-scope.integration): Test Files 8 passed (8), Tests 295 passed (295).

Ablation, on the committed fix (6d9e82dc4). scripts/ablation-replace.mjs (WRAP mode) replaced the gate call with const readOnly = null; plus a marker: anchor 1 to 0, marker 0 to 1, blob 3bc96afefdd0 to 4908b0439001. The test reaches packages.ts through http-dispatcher.ts's relative import, so src is what runs and no rebuild was needed. Result: Tests 5 failed | 4 passed (9), the same five rows red as on the base. Restore: blob equals HEAD 3bc96afefdd0, git diff HEAD is empty, git status --porcelain is clean, marker count 0.

The dispatch's mechanism assumptions

  1. No writability check in reassignOrphanedMetadata: confirmed by reading (above), and reproduced at the door on the base.
  2. Order: confirmed and pinned. Disable, publish, the manifest PATCH and delete ask manage, then walled, then requireWritablePackage, each before it resolves the service that writes. This arm now does the same. When several apply to one request the answer is, first to last: the anonymous floor 401; no manage_metadata 403; the walled 403 PERMISSION_DENIED; read-only 422; no adopting protocol 501; then the protocol's own answer.
  3. Unknown id: measured, unchanged. Before and after, adopting into an id nothing installed answers 200 and binds the orphans to that id (base probe: [["loose_grid","com.acme.nopkg"],["loose_board","com.acme.nopkg"]]; after: the CONTROL pin). It is not the same defect: isWritablePackage documents an unregistered id as a writable ADR-0048 authoring-workspace id, and the authoring door (saveMetaItem naming a package) accepts the same id. Noted below, not changed.

Pin sweep

Every test and dogfood case that sends POST .../adopt-orphans, read for its target:

  • packages-capability-gate.test.ts, packages-walled-write-refusal.test.ts: registry doubles with no manifests and no scope, so writable.
  • packages-vetted-org-source.test.ts: scope: 'project', writable.
  • packages-protocol-handle-typing.test.ts (/crm/adopt-orphans, expects 501): getPackage answers undefined and the double has no manifests, so writable; it still reaches its 501.
  • wall-package-doors-refusal.dogfood.test.ts, wall-environment-metadata-refusal.dogfood.test.ts: every target is created through POST /packages in the same test (authored), and the walled 403 precedes the gate.
  • packages/client (packages.adoptOrphans): only its own request-shape tests call it.

None names a read-only package, so no pin flips.

Tests and gates

All local readings below are at head e0844d5ef (git rev-parse --short HEAD), the final commit.

  • pnpm --filter '@objectstack/runtime^...' build (the dependency closure; this diff changes no package in it): verify-lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/runtime typecheck: VERDICT command-exit 0, with check:test-typecheck: OK (27 files / 190 errors / 68 pinned signatures held, unchanged). The new test file is in the tsconfig.test.json program (--listFiles count 1) and has zero tsc errors, so it needs no ledger entry.
  • pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2: Test Files 432 passed (432), Tests 6791 passed | 19 skipped (6810), VERDICT command-exit 0.
  • pnpm lint (repo-wide eslint . --no-inline-config): exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands (67 families, from merge base 41646a578): 67 exit 0. Two first exited 3 (PREREQUISITE NOT MET): check:type-check-debt, whose closure build never got a turn on the shared verify lock, and check:dual-build-cjs-loads, which needs every package's dist/. Re-run: check:type-check-debt under the lock, VERDICT command-exit 0 (--re-measure: OK, none above its recorded number); then check:dual-build-cjs-loads exit 0 (112 require entry points across 69 packages load). --ran reconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.
  • Among them: check-adr-0087-registration --base origin/main exit 0 (one declared-breaking changeset carrying not-required (no-migration-prescription)); check-changeset-no-major --base origin/main exit 0; check:nul-bytes exit 0; check:cross-package-test-inputs exit 0.
  • test:repo was not run locally: the diff touches no test in that project and the new test reads nothing outside its package. CI owns it, with Lint & Repo Gates, TypeScript Type Check, Test Core and Build Core on this PR.
  • origin/main was not merged before opening; CI runs on the merge ref.

Acceptance notes

  • Observation, not filed: adopting into an id nothing installed binds the environment's orphans to that id at 200 (measured above). By the predicate's documented rule an unregistered id is a writable authoring workspace, so this is the stated contract, not a violation; whether an orphan adoption should require an installed base is a product question this PR does not open.
  • Rows already bound: this PR stops new adoptions into a read-only package. Rows an earlier adoption already bound to one are not touched, and what the next boot does with them was not measured.
  • Doc surfaces left as they are (outside the claimed file surface): requireWritablePackage's doc block in packages.ts lists the doors that ask it and does not name this one (the arm's own comment does); the block is on the dispatch's hold-off list. The JSDoc of packages/client's packages.adoptOrphans does not mention the read-only refusal.
  • objectui already hides "Adopt loose items" for a read-only package (fix(app-shell): offer no write action on a read-only package in the Packages sheet objectui#13181); this PR refuses at the door itself.

Generated by Claude Code

…main)

POST /packages/:id/adopt-orphans binds package-less sys_metadata rows into
any id it is handed. These pins read the rows back over a real store: a
code-booted, marketplace, system-scoped or cloud-scoped target answers 200
and the orphans are rebound into it today. Committed ahead of the gate so
the reproduction is a commit.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…TABLE_PACKAGE_REQUIRED

POST /packages/:id/adopt-orphans now asks requireWritablePackage (the
ADR-0070 D2 predicate isWritablePackage) after the capability gate and the
walled refusal and before the protocol is resolved, the order its sibling
write arms use. reassignOrphanedMetadata asks no writability question of
its own, so the door bound the environment's package-less items into a
code-booted, marketplace, system- or cloud-scoped package at 200.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 1 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/system-context.mdx (via handlePackagesRequest (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7cacad5348d0f2665f1c069eccb5131bd955d768 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 08803b8dcaeeee3868a04a536316984f2ecdfccd — the merge of head e0844d5eff86bd4529648e907cf7e24068ae5a34 into base 7cacad5348d0f2665f1c069eccb5131bd955d768, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 08803b8dcaeeee3868a04a536316984f2ecdfccd && git checkout 08803b8dcaeeee3868a04a536316984f2ecdfccd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7cacad5348d0f2665f1c069eccb5131bd955d768 e0844d5eff86bd4529648e907cf7e24068ae5a34 && git checkout -B drift-repro 7cacad5348d0f2665f1c069eccb5131bd955d768 && git merge --no-ff e0844d5eff86bd4529648e907cf7e24068ae5a34

node scripts/docs-audit/affected-docs.mjs --json 7cacad5348d0f2665f1c069eccb5131bd955d768

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7cacad5348d0f2665f1c069eccb5131bd955d768 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e0844d5eff86bd4529648e907cf7e24068ae5a34
Local-runs: none
Scope: packages/runtime/src/domains/packages.ts, packages/runtime/src/domains/packages-adopt-orphans-readonly.test.ts + packages/metadata-protocol/src/package-writability.ts (isWritablePackage), packages/metadata-protocol/src/protocol.ts (reassignOrphanedMetadata), packages/objectql/src/engine.ts (registerApp), packages/runtime/src/http-dispatcher.ts (error envelope), packages/client/src/index.ts (packages.adoptOrphans), ../objectui packages/app-shell/src/views/metadata-admin/PackagesPage.tsx (checkout cd4623d06), docs/adr/0070-package-first-authoring.md, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, .changeset/24813-adopt-orphans-readonly-package.md (the prose face, judged in ②)

① Derived judgments

Net diff against main (merge base 41646a578): +320/-0 over three files. packages.ts gains one call plus its comment at the adopt-orphans arm (:2144–2153), and nothing else. No governed path is touched (Governed Surface Queue Guard: success). Every judgment below is read in the cited code at the head.

  1. The one narrowing: right.
    • POST /api/v1/packages/:id/adopt-orphans now calls requireWritablePackage(deps, qlService, id, 'adopt items into') (:2153). That is the same function disable (:1602), publish (:1629), revert (:2114), the manifest PATCH (:2277), delete (:2375) and install-overwrite (:1410) call. It is isWritablePackage (package-writability.ts:73–83), not re-derived.
    • A read-only id is either:
      • engine.manifests.has(id): code-booted, or marketplace-installed (registerApp(…, 'marketplace') writes that map, engine.ts:8519); or
      • a registry row whose manifest scope is system / cloud.
    • Such an id answers 422 with error.code WRITABLE_PACKAGE_REQUIRED (the dispatcher promotes details.code, http-dispatcher.ts:1158–1163), details.packageId and details.docs. The protocol is never resolved, so nothing is rebound.
    • Right, and exactly the card's Wanted.
  2. Premise: right. reassignOrphanedMetadata (protocol.ts:29044–) asks only refuseOrganizationScopedWrite. It then rebinds every package-less row (package_id null, '' or 'sys_metadata', minus artifact-backed overlays) to whatever targetPackageId it is handed. It makes no writability or existence check, so the producer to gate is the door.
  3. Refusal order: right against the siblings.
    • The adopt-orphans arm now runs: 401 domain floor (:1052–1073) → requireManageMetadata (:2139) → refuseWalledCatalogPackageWrite (:2142) → requireWritablePackage (:2153) → resolveProtocol (:2154) → 501 → protocol.
    • Disable (:1593/1596/1602), publish (:1615/1618/1629), PATCH (:2264/2267/2277) and delete (:2361/2364/2375) use that order.
    • Install asks after its 409 (:1389–1410). Revert deliberately asks after the protocol's stored-row answer (:2096–2114). Neither the PR body nor the arm's comment claims revert shares the order, only the answer.
    • Pinned: 403 capability first, 403 PERMISSION_DENIED walled first, and 422 before 501.
  4. No other request is newly refused. The gate's only false branch is the read-only predicate.
    • id can never be empty: parts is split('/').filter(Boolean) (:1078), so the predicate's empty-id false is unreachable here.
    • qlService is non-null at the arm, because the 503 registry guard at :1085 comes first.
    • The predicate is deliberately caller-insensitive, with no isSystem escape (package-writability.ts:46–52), exactly as at every sibling door. So a system caller adopting into a read-only package is refused too, which is still "adopting into a read-only package".
    • An unknown id stays writable by the predicate's own rule (:60–61) and still reaches the protocol: 200, orphans bound. Unchanged, and pinned with the rows read back.
  5. The PR body's contract rows: right.
    • Both changed rows (200→422; 501→422 for a read-only id on a protocol that cannot adopt) follow from the gate preceding resolveProtocol.
    • Each unchanged row checks against the code: 401 (floor), 403 (manage), 403 PERMISSION_DENIED (walled), writable 200, writable-no-protocol 501, unknown-id 200, and no-registry 503 (:1085).
    • The 200 shape and the actor body (:2159–2161) are untouched.
  6. Pin sweep: complete.
    • Every test at the head that sends the door was read: the four runtime domain tests the PR names, the two dogfood wall tests (their targets are created through POST /packages, so they are authored, and the walled 403 comes first), and the client's two request-shape tests.
    • The other greps call the protocol directly or name the route in a comment: objectql protocol-adopt-orphans-, metadata-protocol protocol.view-, package-duplicate-adopt-org-scope.integration, and spec package-lifecycle.
    • None targets a read-only package through the door.
  7. The pin file: sound.
  8. Consumers.
    • packages/client adoptOrphans (index.ts:2981–2988) is unchanged. Its JSDoc says "into this base", and a base is a writable package by ADR-0070 D2, so the JSDoc is not falsified. The 422 envelope surfaces through unwrapResponse, as at the sibling doors.
    • objectui's Packages sheet in this checkout (cd4623d06) offers "Adopt loose items" unconditionally (PackagesPage.tsx:927). Its origin/main (df943a2a1) carries no objectui#13181 either, so that PR could not be read here.
    • The sheet surfaces the envelope's message on failure (apiJson :178–200, readEnvelopeFailureText). So at this pin, a user who clicks it on a read-only package sees the refusal sentence with its remedy, and nothing breaks.
  9. Check-runs on the head, read at 2026-10-20T11:36Z.
    • 29 completed success: Check Changeset (which runs check-changeset-no-major and check-adr-0087-registration, pr-automation.yml:977); every Lint job, including Lint · repo & tooling gates, which runs check:doc-authoring; every Type Check job; Build Core; Governed Surface Queue Guard; Test Core 5/6 and 6/6; and the card-claim and single-writer checks.
    • 7 skipped (opt-in and matrix placeholders).
    • 4 still in_progress: Test Core (1/6), (2/6), (3/6), (4/6). NOT MEASURED, not a pass.
    • This verdict is rendered on the diff. The landing still waits on every check being green.

② Semver level

  • Level: right. @objectstack/runtime: minor, declared BREAKING (a REST door's accept set narrows).
    • During the launch window a breaking change ships as minor (the check-changeset-no-major.mjs header, "WHY THIS EXISTS").
    • The level axis enforces at least minor on a moved package for a no (narrowing) declaration (:1808–1821), and that is satisfied. Check Changeset: success.
  • Clause-②: no (narrowing): right. It is on the PR body and the changeset, the same arm, and it matches the diff: no new key on any published payload, and one door's accept set shrinks.
  • The ADR-0087 marker not-required (no-migration-prescription): right. No metadata moves, and nothing exported is removed or narrowed. The FROM/TO bullets describe a wire answer, not a consumer code rewrite, in the same shape as the landed fix(runtime)!: PATCH /packages/:id refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24719 changeset (9b2953d) that the same gate accepted.
  • The prose states the shipped behaviour truly. These all match the code: the read-only kinds as isWritablePackage and ADR-0070 D2 define them, the 501→422 consequence, the unchanged refusals, the unknown-id behaviour, and the integrator remedy.
  • One imprecision, inherited verbatim from the fix(runtime)!: PATCH /packages/:id refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24719 changeset. "Every other door that changes a package (…list…)" reads as a universal, while these doors ask no writable gate: PATCH /packages/:id/enable (:1574–1589), publish-drafts, discard-drafts, rollback, commit-revert and duplicate. The parenthetical list itself is accurate, and the sentence is about other doors, not this PR's shipped behaviour, so it does not falsify the entry. Not a FAIL ground.

③ Boundary flags

  • Unknown id adopts at 200 (dev's finding 1): answered. The predicate documents an unregistered id as a writable ADR-0048 authoring-workspace id, and the sibling doors treat it the same (PATCH reaches its 404, and install proceeds). This is the stated contract, so an acceptance note is the right carrier under Prime Directive chore: version packages #10. Whether adoption should require an installed base is a product question for the maintainer, outside this card.
  • Rows an earlier adoption already bound to a read-only package (finding 2): answered. That is outside the card's Wanted (gate the door). A stored row bound to a code package already has its own answer at the revert door (:2105–2110). Not re-measured here; it stays a note.
  • Doc surfaces that do not name this door (finding 3): answered.
    • requireWritablePackage's doc block (:405–460) is on the dispatch's hold-off list, and it already omits install-overwrite and the manifest PATCH.
    • The arm's own comment and the changeset name the refusal, and the client JSDoc's "base" is not falsified.
    • A docs-only follow-up at most; not a blocker.
  • Dev deviations: process only. These are the background test run, the stray gates-results.txt, the --ran record, the commit-trailer form and the post-report worktree cleanup. The file list confirms nothing stray was committed (three files, +320/-0), and the trailers follow AGENTS.md's precedence over the harness reminder.
  • Hold-offs respected: requireWritablePackage / isWritablePackage, the protocol, every other arm, packages/spec/** and objectui are untouched.

Implemented-by: claude/issue-24813-adopt-orphans-writable-gate
Reviewed-by: session_01LmjU2x1faLZ39DVTFyVca6

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T11:38Z · head e0844d5eff86bd4529648e907cf7e24068ae5a34 · claim 6288643382 → report 6289797498 → review of record 6289985821 (CONTRACT_REVIEW_TIER, PASS). This seat adopted it after re-reading its cited lines at source: packages.ts:1078 (filter(Boolean)), :1085 (the 503 registry guard), :2153–:2154 (gate, then protocol), and package-writability.ts (the manifests.has → scope → writable order).

Check Reading
Shape Draft, base main. First line Fixes #24813; Clause-②: no (narrowing) line-initial. Assignee os-justin = the card's. One changeset, @objectstack/runtime: minor with a BREAKING banner and an ADR-0087 disposition, as the launch-window convention requires.
Surface 3 files, +320 / −0: one gate call and its comment in packages.ts, a new pin file, and the changeset, all inside the claim. No hold-off is touched. check-governed-merges.mjs --pr 24861: NOT governed.
Mechanism The adopt-orphans arm asks requireWritablePackage after the capability and walled refusals and before the protocol, in the siblings' order. A read-only package answers 422 WRITABLE_PACKAGE_REQUIRED and binds nothing. reassignOrphanedMetadata asks no writability question, so the door is the right place for the gate. No other request is newly refused (the review's ①.4).
Evidence 5 of 9 pins were red on main and green after, with the same 5 red under the ablation, restored hash-equal. The runtime local project is 432 files / 6791 tests green.
Gates Local: 67/67 derived families exit 0, 0 UNRUN; pnpm lint exit 0. Check-runs on this head (40, latest per name), read at this act: 30 success, 7 skipped, 3 in_progress (Test Core 2–4/6). Landing waits on all of them.

Accepted deviations:

  • Two gate families first exited 3 (prerequisite / lock timeout) and were re-run to 0.
  • A stray untracked file was deleted before the reconciliation, and nothing stray was committed.
  • The commits use AGENTS.md's model-free trailers.

Out-of-scope dispositions (one line each):

  • Adopting into an id nothing installed answers 200 and binds there → Acceptance notes. It is the predicate's stated contract (an ADR-0048 authoring workspace), and whether adoption should require an installed base is a product question this seat can put to the maintainer.
  • Rows an earlier adoption already bound to a read-only package → Acceptance notes. The revert door has its own answer for them.
  • requireWritablePackage's doc block and the client JSDoc do not name this door → Acceptance notes. The arm's comment and the changeset do.
  • The changeset's "every other door" phrase, inherited from fix(runtime)!: PATCH /packages/:id refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24719's, reads wider than its list → Acceptance notes. The list is accurate.

Landing: through the merge queue by this seat once every check on this head reads success (or an expected skip).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

packages: POST /packages/:id/adopt-orphans skips requireWritablePackage, so it can bind loose items into a read-only package

2 participants