You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
metadata-protocol: over drafts [package-unbound, Q] with no active row, the strict draft read naming no package serves its version from the package-unbound draft but its content from whichever row the store returns first #23715
Found while delivering #23657 (PR #23706), outside its scope.
Filing gate: (a) a reachable defect, measured on a real kernel by #23657's cloud dev. The seat ruled the measurement (6198239592, point 4: "measure whether the strict draft read serves its content from a different row than its version… If it diverges, this seat files the card"). The dev reported it in 6199998142 on #23657 (open_questions[0], out_of_scope_findings[0], class a). Filed by domain:engine seat 1 (#6367) · sunhaolin · session_01GP3comALHRcRBixS2TEnLF. ⛔ Not a claim. Reader who acts: triage, to grade and route. The read address is in packages/metadata-protocol.
What happens
State. An item with no active row, and two drafts: a package-unbound draft and package Q's draft. Today's doors reach it: a draft save naming no package writes the unbound draft, and then ?mode=draft&package=Q writes Q's.
Request. The strict draft read naming no package: GET /api/v1/meta/TYPE/NAME?state=draft.
Answer, as measured:
version comes from the package-unbound draft, in both storage orders (headAt → unboundItemDraftAddress, the draft address).
Content comes from the first row the store returns (findServedOverlayRow with no package predicate; packagelessReadAddress answers undefined with no active binding).
On the protocol suite's in-memory double, with Q's draft stored first, the read served Q's body (label 'q') under the unbound draft's token.
With the unbound draft first, it served the unbound body.
On better-sqlite3 (the REST harness), both orders built returned the unbound row first. Content and version agreed there, but only by storage order.
Why it matters. This is the #8907 coin-toss shape. An author (or an AI) reads Q's body, then sends If-Match with the token it was given. That save writes the unbound draft, which is a different row from the one it read.
Predates PR #23706. With an unbound draft present, the save's draft address was the unbound draft before that PR too. PR #23706 leaves this read alone by the seat's ruling.
Wanted
The strict draft read naming no package takes its content from the draft address the save, publish and discard use (unboundItemDraftAddress). Content and version then name the same row in every storage order.
Pins:
[package-unbound, Q] drafts, no active row, in both storage orders: the read's content and version come from the same row, and an If-Match save with that token edits the row that was read;
a CONTROL with one draft only;
a negative control that fails when the fix is removed.
Order: after PR #23706 (#23657), which changes the draft door's address in sys-metadata-repository.ts and the spec description of the save's draft arm.
Dedupe
The words strict draft read content version different row package-unbound draft beside package draft findServedOverlayRow packagelessReadAddress no active binding match:
From PR #23706's contract review 6200477973 (③), a description-only tidy for whichever PR closes this card. PublishMetaItemRequestSchema.packageId (PR #23654's text) says a publish naming no package is refused when "(or, with none, the drafts)" are bound to two or more packages. It does not carry the "no package-unbound draft" qualifier that SaveMetaItemRequestSchema.packageId's text now has (PR #23706). The code does not refuse when an unbound draft sits beside two packages' drafts: unboundItemDraftAddress returns the unbound draft first. Align the publish text with the save's in the same PR. It is a packages/spec describe-only change, cross-lane domain:spec.
Triage: first grade · bug · priority:p1 · target:v18 · domain:engine · pm:queue · land after PR #23706 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T19:53Z
Read fresh. An item with no active row and two drafts (package-unbound, and Q's) answers the strict draft read naming no package (GET …?state=draft) with mismatched halves:
its version from the unbound draft (headAt → unboundItemDraftAddress);
its content from whichever row the store returns first (findServedOverlayRow with no package predicate).
On the in-memory double, Q's body was served under the unbound draft's token. An author (or AI) that reads that body and saves with If-Match writes a different row from the one it read: #8907's coin-toss shape. Measured on a real kernel by #23657's dev, at the seat's ruled measurement.
Grade p1 + target:v18: the fix changes which row a /meta read door serves (filing-gate.md 〈定级判据〉, the protocol class). It is reachable through today's doors in two saves.
Lane domain:engine (packages/metadata-protocol).
Order: it lands after PR #23706 (#23657, open). That PR settles the save address this read must match. The seat's rider (6200550012, the PublishMetaItemRequestSchema.packageId describe tidy) rides the closing PR.
Scope:
The strict draft read naming no package takes its content from the same address as its version.
Pins: both storage orders on the in-memory double, and better-sqlite3. CONTROL: a single-draft item is unchanged.
Claim: PM loop round 2 · 2026-10-15T20:27Z
Session: session_01GP3comALHRcRBixS2TEnLF (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
Account: sunhaolin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23715-strict-draft-read-address
Worktree: objectstack-issue-23715
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface: packages/metadata-protocol/src/protocol.ts, the strict draft read's content address only (findServedOverlayRow / packagelessReadAddress as the draft read naming no package takes them), so content comes from the address headAt gives its version (unboundItemDraftAddress). Also the rider 6200550012: packages/spec/src/api/protocol.zod.ts, PublishMetaItemRequestSchema.packageId's description only, with its regenerated artifacts (cross-lane domain:spec; declared on #18883 before the queue). Plus pins and one changeset .changeset/23715-*.md. ⛔ Not the D9.9 / saveMetaItem region (PR #23704, #23514), not reassignOrphanedMetadata (#23690), not sys-metadata-repository.ts's write address (landed with PR #23706). Stop on any other breach and explain it in the report.
Container & model: M, mode:cloud (the maintainer's order on seat post #6367: 「后续 重活开发 都可以单独开云卡」), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier at 9e0945cf4: "no path-derived mandate", with the Clause-② SUSPECT hint on packages/spec/src/**; the build runs at the default tier, and the review runs at CONTRACT_REVIEW_TIER on the schema face, since a published description changes)
Clause-②: yes
Responsibility: the risk is produced by packages/metadata-protocol's strict draft read naming no package. It takes its version from the draft address (unboundItemDraftAddress) but its content from the first row a package-agnostic findServedOverlayRow returns, so over [package-unbound, Q] drafts with no active row the two halves can name different rows | no platform path covers it: PR #23706 aligned the save, publish and discard addresses, not this read's content | an author (or an AI) reaches it through GET /api/v1/meta/TYPE/NAME?state=draft after two saves today's doors accept, and an If-Match save on the read's token then writes a row other than the one read (measured by #23657's dev, report 6199998142).
Thread-read: 6200989991
Serial constraints cleared: the ordered-after PR #23706 (#23657) is on main (c03077e294). The other open work on protocol.ts is in different regions: PR #23704 (#23514: the D9.9 refusal and saveMetaItem) and #23690 (reassignOrphanedMetadata's view limb). Whichever lands second merges main. No other open PR or claim touches the strict draft read's address.
Dispatch · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T20:29Z: mode:cloud, the dev session session_01HTBbFnY86QT5g7BxeMo3Ub, titled "⚡ engine #23715 strict draft read content follows its version". It builds at the default tier on branch claude/issue-23715-strict-draft-read-address, the claim 6201610005. The seat's review runs at CONTRACT_REVIEW_TIER, because a published description changes.
Measure first, on main9e0945cf4, over [package-unbound, Q] drafts with no active row, in both storage orders, on the in-memory double and on better-sqlite3. That is the negative control.
The fix: the strict draft read naming no package takes its content from the address its version comes from (unboundItemDraftAddress). The write, publish and discard addresses do not change. Every other draft read door naming no package is measured too.
Rider 6200550012:PublishMetaItemRequestSchema.packageId's description gets the "no package-unbound draft" qualifier the save's text carries. It is description only and regenerated, with '@objectstack/spec': patch. The domain:spec hunk is declared on [PM seat] domain:spec · seat 3 — 🟢 baozhoutao · session_01D6F9axfoPzR7t8G1J7Ytzz #18883 before the queue.
os-dev-report
{
"issue": 23715,
"status": "done",
"branch": "claude/issue-23715-strict-draft-read-address",
"pr": "#23727",
"session": "session_01HTBbFnY86QT5g7BxeMo3Ub",
"premise_still_valid": true,
"summary": "H1 confirmed on main 9e0945c (in-memory double): over drafts [package-unbound, Q] with no active row and Q's draft stored first, GET ?state=draft served Q's body ('q') under the package-unbound draft's token; the same divergence held for active rows bound to two packages with drafts [package-unbound, P], P's first. getMetaItem now takes the strict draft read's content from strictDraftReadAddress (a private helper over packagelessDraftAddress, the address headAt gives the version and the save/publish/discard use); findServedOverlayRow/servedOverlayRowCandidates accept packageId null = the package-unbound row only (existing callers unchanged). H3: the previewDrafts arm shared the address (Q-first served 'q' while the strict read served 'unbound') and now takes the same draft address; the layered read is active-only, no draft door. Rider 6200550012 applied: PublishMetaItemRequestSchema.packageId description carries the 'no package-unbound draft' qualifier; protocol.mdx regenerated. On better-sqlite3 every order already agreed on main: sys_metadata's (type, name, package_id, bundle_member) index returns the NULL-bound row first, so those pins are positive pins; the double is the negative control. Write, publish and discard addresses untouched; no accept set narrows. Deviations: see open_questions[3] (shared-checkout branch) and [4] (history split with --force-with-lease before the PR existed).",
"tests": "Negative control on main 9e0945c: vitest run src/protocol.packageless-save-address.test.ts -t 23715 -> Tests 2 failed | 4 passed (expected 'q' to be 'unbound'; expected 'p' to be 'unbound'). REST better-sqlite3 pins on main: 4 passed (store agrees by index order; recorded, not a negative control). After fix (head eaa4b0e): metadata-protocol full suite pnpm --filter @objectstack/metadata-protocol test -> Test Files 236 passed | 3 skipped, Tests 20572 passed | 19 skipped; protocol.packageless-save-address + sys-metadata-repository.draft-package-inherit -> Tests 81 passed; rest meta-draft-head-package-inheritance + meta-draft-read-door-census (after rebuilding metadata-protocol dist) -> Tests 30 passed. Ablation via scripts/ablation-replace.mjs on protocol.ts (mutation restoring main's address logic; suite imports ./protocol.js from src, no dist leg): Tests 2 failed | 55 passed (57), exactly the two divergence pins; restore: blob a54a42e44382 == HEAD, git diff HEAD empty. A first ablation attempt nulled the draft address without restoring main's contentPackageId and also reddened the active-binding CONTROL (3 failed); discarded as not faithful to main, recorded here. Typecheck metadata-protocol / spec / rest: exit 0 each.",
"gates": "Local, captured before any pipe: pnpm --filter @objectstack/spec check:generated -> after --fix regenerated check:docs only: 'All 14 generated artifacts are up to date'. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at eaa4b0e (no paths): 111 commands; run all; --ran with recorded exit codes -> '111 derived, 110 run, 1 NOT-MEASURED, 0 UNRUN'. NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: some package has no dist; CI runs it). First-pass exit 3 re-run to exit 0 after satisfying prerequisites: node scripts/check-plugin-teardown-shape.mjs --self-test (fetched pinned fixture commit 621a487) and pnpm --filter @objectstack/spec run check:skill-examples (built client-react). Local-only: pnpm check:dev-prereqs exit 0. CI on PR #23727 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; Check Changeset, Lint jobs, Type Check jobs, Governed Surface Queue Guard, Spec property liveness and the rest in_progress (not waited on, per role file).",
"mcp_calls": "7 — read-only: issue_read x2 (get, get_comments on #23715), search_pull_requests x1 (open PRs touching protocol.ts), pull_request_read x3 (get_files #23724, get_files #23704, get_check_runs #23727), issue_read get_comments x1 (report read-back). No write tool.",
"api_writes": "2 relay dispatches (scripts/pm/with-fleet.sh --via dispatch, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23727/assignees; (2) comment = POST /repos//issues/23715/comments (this report). git push is not REST. No label write: a changeset exists and the dispatch named no labels.",
"open_questions": [
{
"question": "Clause-② level: the claim's Clause-②: yes is copied to the PR body and changeset, but check-changeset-no-major refuses a yes when every package is patch, while the dispatch named '@objectstack/spec': patch. Factually this diff puts no new key on a published payload.",
"options": [
"A keep yes, grade @objectstack/metadata-protocol minor and spec patch (PR #23706's shape) — done",
"B the seat rewrites the line to Clause-②: no and both packages go patch"
],
"recommendation": "A as shipped, because the role file says copy the claim's line and the level rule then needs one minor; B is the seat's call (PR body edit: 'Clause-②: yes' -> 'Clause-②: no', changeset line likewise, metadata-protocol minor -> patch)."
},
{
"question": "Ambiguous addresses (active rows bound to two or more packages, or drafts bound to two or more packages with no package-unbound draft): the read serves the package-unbound draft when stored (the row its version names), else keeps the best-effort first match with version null instead of answering 404 or 409.",
"options": [
"A keep best-effort (no accept-set narrowing; the save naming none is refused there anyway) — done",
"B answer 409 RESOURCE_CONFLICT like the draft doors (a narrowing, Clause-② yes (narrowing), ADR-0087 marker)"
],
"recommendation": "A, because it fixes the content/version split everywhere a version names a row without narrowing a read door; B would be a separate decision under the four axes (AI-error-proofing favours it, startup scope and no-narrowing-without-ruling favour A)."
},
{
"question": "Scope: the triage scope names the strict draft read; the dispatch's H3 says fix every draft read door naming no package that shares the address. The previewDrafts arm shared it and was fixed in the same commit series.",
"options": [
"A keep the preview fix in this PR — done",
"B split it out"
],
"recommendation": "A, because it is the same address and one helper; one extra assertion pins it."
},
{
"question": "Deviation: git worktree add -b claude/issue-23715-strict-draft-read-address collided with a local branch of that name checked out in the shared /home/user/objectstack checkout (at 9e0945c, no commits of its own). I detached the shared checkout's HEAD and deleted that local branch so the worktree could take the name.",
"options": [
"A accept (no work lost: the branch pointed at origin/main's tip) ",
"B treat as a seat-discipline finding"
],
"recommendation": "A, recorded so the seat knows the shared checkout is now on a detached HEAD at 9e0945c."
},
{
"question": "Deviation: a commit swept the preview-arm change into a spec-only message; before the PR existed I split it (reset --soft, two commits) and pushed with --force-with-lease=claude/issue-23715-strict-draft-read-address:4b88f9809 (the sha last pushed; replaced by 4b03f30 + 3faac8a) (all five AGENTS.md §3 conditions held; tree byte-identical). main moved 3 commits (f40d877, 76560a1, a515abd: rest analytics refactor, objectql, organizations) with no overlap; not merged.",
"options": [
"A accept",
"B merge main now"
],
"recommendation": "A; merge only on conflict or a main-caused red per role file."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the list's draft-preview merge (mergePackageAwareOverlay) was not measured for drafts [package-unbound, Q] with no active binding; it carries no per-item version (PR #23727 Acceptance notes). Dedupe words: list draft preview mergePackageAwareOverlay package-unbound draft beside package draft no active binding.",
"Open PRs touching protocol.ts at opening (different regions): #23704 (D9.9 sentinel-owner save refusal, +212/-7) and #23724 (reassignOrphanedMetadata view limb, +15/-5)."
]
}
Armed with pr_ready + automerge_enable through the fleet-write relay (run 38817150726). The PR event recorded the enqueue at 22:28:34Z. After the merge, git ls-remote finds no gh-readonly-queue branch for PR #23727.
origin/main
069fd40c49 is on origin/main (git merge-base --is-ancestor exit 0). It is the squash of PR #23727: the same 6 files, +292/−13. The merge event fired at 23:00:30Z.
Cards closed
Since the enqueue at 22:28Z, three cards closed. #23715 is this PR's card. #23498 is domain:spec's own card, closed by PR #23524 in the same window. #23604 is domain:services' own card, closed by PR #23728. This PR names neither. Nothing unexpected.
Status:#23715 is closed, and its pm:dispatched is removed in this stroke. The rider 6200550012 is carried: PublishMetaItemRequestSchema.packageId now carries the "no package-unbound draft" qualifier. The cloud dev session session_01HTBbFnY86QT5g7BxeMo3Ub is archived.
Activity
objectstack-fleet commentedon Oct 15, 2026
Rider ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T19:29ZFrom PR #23706's contract review 6200477973 (③), a description-only tidy for whichever PR closes this card.
PublishMetaItemRequestSchema.packageId(PR #23654's text) says a publish naming no package is refused when "(or, with none, the drafts)" are bound to two or more packages. It does not carry the "no package-unbound draft" qualifier thatSaveMetaItemRequestSchema.packageId's text now has (PR #23706). The code does not refuse when an unbound draft sits beside two packages' drafts:unboundItemDraftAddressreturns the unbound draft first. Align the publish text with the save's in the same PR. It is apackages/specdescribe-only change, cross-lanedomain:spec.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p1·target:v18·domain:engine·pm:queue· land after PR #23706 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T19:53ZRead fresh. An item with no active row and two drafts (package-unbound, and Q's) answers the strict draft read naming no package (
GET …?state=draft) with mismatched halves:headAt→unboundItemDraftAddress);findServedOverlayRowwith no package predicate).On the in-memory double, Q's body was served under the unbound draft's token. An author (or AI) that reads that body and saves with
If-Matchwrites a different row from the one it read: #8907's coin-toss shape. Measured on a real kernel by #23657's dev, at the seat's ruled measurement.Grade p1 +
target:v18: the fix changes which row a/metaread door serves (filing-gate.md〈定级判据〉, the protocol class). It is reachable through today's doors in two saves.Lane
domain:engine(packages/metadata-protocol).Order: it lands after PR #23706 (#23657, open). That PR settles the save address this read must match. The seat's rider (
6200550012, thePublishMetaItemRequestSchema.packageIddescribe tidy) rides the closing PR.Scope:
findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 · 2026-10-15T20:27Z
Session:
session_01GP3comALHRcRBixS2TEnLF(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
sunhaolin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23715-strict-draft-read-addressWorktree:
objectstack-issue-23715Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/metadata-protocol/src/protocol.ts, the strict draft read's content address only (findServedOverlayRow/packagelessReadAddressas the draft read naming no package takes them), so content comes from the addressheadAtgives itsversion(unboundItemDraftAddress). Also the rider 6200550012:packages/spec/src/api/protocol.zod.ts,PublishMetaItemRequestSchema.packageId's description only, with its regenerated artifacts (cross-lanedomain:spec; declared on #18883 before the queue). Plus pins and one changeset.changeset/23715-*.md. ⛔ Not the D9.9 /saveMetaItemregion (PR #23704, #23514), notreassignOrphanedMetadata(#23690), notsys-metadata-repository.ts's write address (landed with PR #23706). Stop on any other breach and explain it in the report.Container & model:
M,mode:cloud(the maintainer's order on seat post #6367: 「后续 重活开发 都可以单独开云卡」),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tierat9e0945cf4: "no path-derived mandate", with the Clause-② SUSPECT hint onpackages/spec/src/**; the build runs at the default tier, and the review runs atCONTRACT_REVIEW_TIERon the schema face, since a published description changes)Clause-②: yes
Responsibility: the risk is produced by
packages/metadata-protocol's strict draft read naming no package. It takes itsversionfrom the draft address (unboundItemDraftAddress) but its content from the first row a package-agnosticfindServedOverlayRowreturns, so over[package-unbound, Q]drafts with no active row the two halves can name different rows | no platform path covers it: PR #23706 aligned the save, publish and discard addresses, not this read's content | an author (or an AI) reaches it throughGET /api/v1/meta/TYPE/NAME?state=draftafter two saves today's doors accept, and anIf-Matchsave on the read's token then writes a row other than the one read (measured by #23657's dev, report 6199998142).Thread-read: 6200989991
Serial constraints cleared: the ordered-after PR #23706 (#23657) is on
main(c03077e294). The other open work onprotocol.tsis in different regions: PR #23704 (#23514: the D9.9 refusal andsaveMetaItem) and #23690 (reassignOrphanedMetadata's view limb). Whichever lands second mergesmain. No other open PR or claim touches the strict draft read's address.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Dispatch ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T20:29Z:mode:cloud, the dev sessionsession_01HTBbFnY86QT5g7BxeMo3Ub, titled "⚡ engine #23715 strict draft read content follows its version". It builds at the default tier on branchclaude/issue-23715-strict-draft-read-address, the claim 6201610005. The seat's review runs atCONTRACT_REVIEW_TIER, because a published description changes.main9e0945cf4, over[package-unbound, Q]drafts with no active row, in both storage orders, on the in-memory double and on better-sqlite3. That is the negative control.versioncomes from (unboundItemDraftAddress). The write, publish and discard addresses do not change. Every other draft read door naming no package is measured too.PublishMetaItemRequestSchema.packageId's description gets the "no package-unbound draft" qualifier the save's text carries. It is description only and regenerated, with'@objectstack/spec': patch. Thedomain:spechunk is declared on [PM seat] domain:spec · seat 3 — 🟢 baozhoutao · session_01D6F9axfoPzR7t8G1J7Ytzz #18883 before the queue.Clause-②: yes, after PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706's precedent.Fixes #23715, then anos-dev-reportcomment here.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23715,
"status": "done",
"branch": "claude/issue-23715-strict-draft-read-address",
"pr": "#23727",
"session": "session_01HTBbFnY86QT5g7BxeMo3Ub",
"premise_still_valid": true,
"summary": "H1 confirmed on main 9e0945c (in-memory double): over drafts [package-unbound, Q] with no active row and Q's draft stored first, GET ?state=draft served Q's body ('q') under the package-unbound draft's token; the same divergence held for active rows bound to two packages with drafts [package-unbound, P], P's first. getMetaItem now takes the strict draft read's content from strictDraftReadAddress (a private helper over packagelessDraftAddress, the address headAt gives the version and the save/publish/discard use); findServedOverlayRow/servedOverlayRowCandidates accept packageId null = the package-unbound row only (existing callers unchanged). H3: the previewDrafts arm shared the address (Q-first served 'q' while the strict read served 'unbound') and now takes the same draft address; the layered read is active-only, no draft door. Rider 6200550012 applied: PublishMetaItemRequestSchema.packageId description carries the 'no package-unbound draft' qualifier; protocol.mdx regenerated. On better-sqlite3 every order already agreed on main: sys_metadata's (type, name, package_id, bundle_member) index returns the NULL-bound row first, so those pins are positive pins; the double is the negative control. Write, publish and discard addresses untouched; no accept set narrows. Deviations: see open_questions[3] (shared-checkout branch) and [4] (history split with --force-with-lease before the PR existed).",
"tests": "Negative control on main 9e0945c:
vitest run src/protocol.packageless-save-address.test.ts -t 23715-> Tests 2 failed | 4 passed (expected 'q' to be 'unbound'; expected 'p' to be 'unbound'). REST better-sqlite3 pins on main: 4 passed (store agrees by index order; recorded, not a negative control). After fix (head eaa4b0e): metadata-protocol full suitepnpm --filter @objectstack/metadata-protocol test-> Test Files 236 passed | 3 skipped, Tests 20572 passed | 19 skipped; protocol.packageless-save-address + sys-metadata-repository.draft-package-inherit -> Tests 81 passed; rest meta-draft-head-package-inheritance + meta-draft-read-door-census (after rebuilding metadata-protocol dist) -> Tests 30 passed. Ablation via scripts/ablation-replace.mjs on protocol.ts (mutation restoring main's address logic; suite imports ./protocol.js from src, no dist leg): Tests 2 failed | 55 passed (57), exactly the two divergence pins; restore: blob a54a42e44382 == HEAD, git diff HEAD empty. A first ablation attempt nulled the draft address without restoring main's contentPackageId and also reddened the active-binding CONTROL (3 failed); discarded as not faithful to main, recorded here. Typecheck metadata-protocol / spec / rest: exit 0 each.","gates": "Local, captured before any pipe:
pnpm --filter @objectstack/spec check:generated-> after--fixregenerated check:docs only: 'All 14 generated artifacts are up to date'.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat eaa4b0e (no paths): 111 commands; run all;--ranwith recorded exit codes -> '111 derived, 110 run, 1 NOT-MEASURED, 0 UNRUN'. NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: some package has no dist; CI runs it). First-pass exit 3 re-run to exit 0 after satisfying prerequisites: node scripts/check-plugin-teardown-shape.mjs --self-test (fetched pinned fixture commit 621a487) and pnpm --filter @objectstack/spec run check:skill-examples (built client-react). Local-only: pnpm check:dev-prereqs exit 0. CI on PR #23727 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; Check Changeset, Lint jobs, Type Check jobs, Governed Surface Queue Guard, Spec property liveness and the rest in_progress (not waited on, per role file).","mcp_calls": "7 — read-only: issue_read x2 (get, get_comments on #23715), search_pull_requests x1 (open PRs touching protocol.ts), pull_request_read x3 (get_files #23724, get_files #23704, get_check_runs #23727), issue_read get_comments x1 (report read-back). No write tool.",
"api_writes": "2 relay dispatches (scripts/pm/with-fleet.sh --via dispatch, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23727/assignees; (2) comment = POST /repos//issues/23715/comments (this report). git push is not REST. No label write: a changeset exists and the dispatch named no labels.",
"open_questions": [
{
"question": "Clause-② level: the claim's
Clause-②: yesis copied to the PR body and changeset, but check-changeset-no-major refuses a yes when every package is patch, while the dispatch named '@objectstack/spec': patch. Factually this diff puts no new key on a published payload.","options": [
"A keep yes, grade @objectstack/metadata-protocol minor and spec patch (PR #23706's shape) — done",
"B the seat rewrites the line to
Clause-②: noand both packages go patch"],
"recommendation": "A as shipped, because the role file says copy the claim's line and the level rule then needs one minor; B is the seat's call (PR body edit: 'Clause-②: yes' -> 'Clause-②: no', changeset line likewise, metadata-protocol minor -> patch)."
},
{
"question": "Ambiguous addresses (active rows bound to two or more packages, or drafts bound to two or more packages with no package-unbound draft): the read serves the package-unbound draft when stored (the row its version names), else keeps the best-effort first match with version null instead of answering 404 or 409.",
"options": [
"A keep best-effort (no accept-set narrowing; the save naming none is refused there anyway) — done",
"B answer 409 RESOURCE_CONFLICT like the draft doors (a narrowing, Clause-② yes (narrowing), ADR-0087 marker)"
],
"recommendation": "A, because it fixes the content/version split everywhere a version names a row without narrowing a read door; B would be a separate decision under the four axes (AI-error-proofing favours it, startup scope and no-narrowing-without-ruling favour A)."
},
{
"question": "Scope: the triage scope names the strict draft read; the dispatch's H3 says fix every draft read door naming no package that shares the address. The previewDrafts arm shared it and was fixed in the same commit series.",
"options": [
"A keep the preview fix in this PR — done",
"B split it out"
],
"recommendation": "A, because it is the same address and one helper; one extra assertion pins it."
},
{
"question": "Deviation:
git worktree add -b claude/issue-23715-strict-draft-read-addresscollided with a local branch of that name checked out in the shared /home/user/objectstack checkout (at 9e0945c, no commits of its own). I detached the shared checkout's HEAD and deleted that local branch so the worktree could take the name.","options": [
"A accept (no work lost: the branch pointed at origin/main's tip) ",
"B treat as a seat-discipline finding"
],
"recommendation": "A, recorded so the seat knows the shared checkout is now on a detached HEAD at 9e0945c."
},
{
"question": "Deviation: a commit swept the preview-arm change into a spec-only message; before the PR existed I split it (reset --soft, two commits) and pushed with --force-with-lease=claude/issue-23715-strict-draft-read-address:4b88f9809 (the sha last pushed; replaced by 4b03f30 + 3faac8a) (all five AGENTS.md §3 conditions held; tree byte-identical). main moved 3 commits (f40d877, 76560a1, a515abd: rest analytics refactor, objectql, organizations) with no overlap; not merged.",
"options": [
"A accept",
"B merge main now"
],
"recommendation": "A; merge only on conflict or a main-caused red per role file."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the list's draft-preview merge (mergePackageAwareOverlay) was not measured for drafts [package-unbound, Q] with no active binding; it carries no per-item version (PR #23727 Acceptance notes). Dedupe words: list draft preview mergePackageAwareOverlay package-unbound draft beside package draft no active binding.",
"Open PRs touching protocol.ts at opening (different regions): #23704 (D9.9 sentinel-owner save refusal, +212/-7) and #23724 (reassignOrphanedMetadata view limb, +15/-5)."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23727 comment 6203548294 ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T22:26Z. Contract review 6203538652 PASS ateaa4b0ed56(CONTRACT_REVIEW_TIER, schema face).Clause-②: yesis kept as shipped. The rider 6200550012 is carried. Landing follows through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T23:01Zpr_ready+automerge_enablethrough the fleet-write relay (run 38817150726). The PR event recorded the enqueue at 22:28:34Z. After the merge,git ls-remotefinds nogh-readonly-queuebranch for PR #23727.origin/main069fd40c49is onorigin/main(git merge-base --is-ancestorexit 0). It is the squash of PR #23727: the same 6 files, +292/−13. The merge event fired at 23:00:30Z.domain:spec's own card, closed by PR #23524 in the same window. #23604 isdomain:services' own card, closed by PR #23728. This PR names neither. Nothing unexpected.Status: #23715 is closed, and its
pm:dispatchedis removed in this stroke. The rider 6200550012 is carried:PublishMetaItemRequestSchema.packageIdnow carries the "no package-unbound draft" qualifier. The cloud dev sessionsession_01HTBbFnY86QT5g7BxeMo3Ubis archived.Generated by Claude Code