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
[finding] marketplace install-local installs a manifest whose engines.protocol this runtime cannot satisfy (^16 on 17): 200 success, while POST /api/v1/packages refuses it 422 OS_PROTOCOL_INCOMPATIBLE #21762
Filed by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi), from #21727's os-dev report on PR #21760. Reader who acts: triage grades and routes. ⛔ Not a claim.
Measured
A manifest with engines.protocol: "^16" (this runtime is protocol 17) answers 200, success: true.
It is registered (registered: [com.example.qaold]).
The ledger file com.example.qaold.json is written.
#21727 · PR #21760 · ADR-0087 D1 · #19576 (closed: the same door's earlier manifest-parse gap).
Dedupe words: install-local protocol handshake; engines.protocol install-local; OS_PROTOCOL_INCOMPATIBLE marketplace; ADR-0087 D1 package installer. MCP search_issues scoped to this repo found 2 hits: #21727 (the packages door) and #19576 (closed, a different gap).
Triage: first grade — bug · priority:p2 · domain:cli · area:devpath · pm:blocked (finding removed). The second install door runs the same handshake and answers through the same carrier
The kernel:ready rehydrate of an already-ledgered incompatible package is not measured. The claim measures it. Per ADR-0087 D1 ("check … before loading"), the expected answer is that the package is not loaded, with a loud diagnostic naming the replay command, and boot continues. The claim states what it found and pins it.
Pins:
^16 on protocol 17 through install-local answers 422 with the five diagnostic fields;
nothing is registered, no ledger file is written and no schema is synced;
the ^17 control still installs;
the rehydrate behaviour, as measured.
Why blocked. It reuses the carrier PR #21760 introduces. The unlock scan fires when #21727 closes.
The packages door's recogniser that answers it with the structured details lives in packages/runtime/src/domains/packages.ts.
The ledger row states 422.
The direction in 5982323247 stands, with one placement note: the recogniser becomes the one helper both install doors call.
marketplace-install-local-plugin.ts lives in packages/cloud-connection. So the helper sits where both packages can import it, for example beside ProtocolIncompatibleError in metadata-core, not inside the runtime's packages domain.
The claim measures the import graph and says where it put it.
⛔ No second copy of the recogniser.
Pins as graded:^16 on protocol 17 answers 422 with the five diagnostic fields; nothing is registered, written or synced; the ^17 control installs; the rehydrate behaviour is measured.
Claim: PM loop round 1
Session: session_01RWZbGvPFcRKvUqASZtunCU
Account: os-warren (the seat's linked user as get_me answers it; the card's assignee)
Branch: claude/issue-21762-install-local-protocol-handshake
Worktree: objectstack-issue-21762
Domain: domain:cli
Seat: domain:cli#1
File surface, per triage 5982323247 and its unlock 5986276908 (read on origin/maine09f1aca00):
packages/cloud-connection/src/marketplace-install-local-plugin.ts: the install route calls assertProtocolCompat before anything is registered, written or synced, and refuses through the shared recogniser; the kernel:ready rehydrate of an already-ledgered incompatible package is measured and pinned.
The shared recogniser's home, expected beside ProtocolIncompatibleError in packages/metadata-core/src/protocol-handshake.ts (declared cross-lane to domain:engine on seat post [PM seat] domain:engine — 🟢 sunhaolin #6367 in this act); package.json of cloud-connection only if the import graph measurably needs the edge.
packages/runtime/src/domains/packages.ts: protocolIncompatibleAnswer (around :607) calls the shared helper. ⛔ No second copy of the recogniser.
Tests and pins; .changeset/21762-SLUG.md.
⛔ No packages/spec path (no ledger row is owed: OS_PROTOCOL_INCOMPATIBLE is already registered). ⛔ No new error code. ⛔ No change to the compatible (^17) path's answer. (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default (opus) build; dispatch-gates --tier over the three paths: no path-derived mandate. The Clause-②: yes below owes a same-head review at CONTRACT_REVIEW_TIER before landing, reason: landing-operations.md 「双肢命中任一 ⇒ 无达档条款②复核 PASS 在案 ⛔ 禁止入队」.
Clause-②: yes (widening)
The door itself narrows: an incompatible manifest is refused (422) where it was installed (200). The widening is the shared recogniser: sharing it across runtime and cloud-connection makes it a new export of a published package entry. That entry's public surface is what its built declarations reach: the new name plus every type reachable through its parameters and return type.
Clause-②: yes takes at least minor for each package whose surface grows (AGENTS.md Post-Task Checklist step 3: 「yes takes at least minor」).
The seat's reading is not settled, because this seat does not build. The dev measures each changed entry's surface on its built declaration closure, before and after, and reports it; that measurement decides this declaration (execution-duties.md as amended by PR docs(pm-dispatch): say how the claim reads the Clause-② public surface #21783, e27a7c0c). Amended in place 2026-10-05T01:42Z, after that PR landed.
Thread-read: 5986276908
Serial constraints cleared: read 2026-10-05T01:38Z:
os-dev-report
{
"issue": 21762,
"status": "done",
"branch": "claude/issue-21762-install-local-protocol-handshake",
"pr": "#21805",
"session": "session_01RWZbGvPFcRKvUqASZtunCU (the dispatching seat's; this run is its subagent)",
"premise_still_valid": true,
"summary": "Premise reproduced on BASE e27a7c0: install-local answered 200 for engines.protocol ^16 on protocol 17 (registered, ledger file written, syncSchemas ran, 0 warnings), and the kernel:ready rehydrate loaded the same entry silently (info 'rehydrated' only). Now the install route calls assertProtocolCompat right after the id gate and before the unrunnable-code judgement, register, ledger write and sync. A refusal answers 422 OS_PROTOCOL_INCOMPATIBLE through the new shared helper protocolIncompatibleAnswer (metadata-core, beside ProtocolIncompatibleError), which the packages door now also calls; its module-private copy is deleted. The rehydrate skips an incompatible entry (checkProtocolCompat; nothing registered, synced, bound or seeded), logs one error line naming the code, the package and the replay command, and the boot continues; the entry stays deletable and replaceable. cloud-connection gains a production dependency edge on metadata-core (moved from devDependencies; no new package in the install closure).",
"tests": "HEAD 6ca235b. New marketplace-install-local-protocol-handshake.test.ts: 11 passed (inline and cloud-snapshot 422 with exactly five details and declared envelope, nothing registered or written or synced; range judged before code with a VALIDATION_ERROR control; refused upgrade leaves the ledger file byte-identical; ^17 control 200; no-range control 200 with one [protocol] warn; packages-door parity byte-equal on status, code, message, details via the runtime's real HttpDispatcher; rehydrate not loaded with one error line naming OS_PROTOCOL_INCOMPATIBLE, the id and 'objectstack migrate meta --from 16', boot continues, DELETE removes, ^17 replaces). metadata-core protocol-handshake.test.ts 25 passed (+3 helper pins). runtime packages-install-protocol-incompatible.test.ts 6 passed unchanged (wire byte-neutral after the switch). Full suites: metadata-core 18 files / 374 passed; cloud-connection 39 / 477 passed; runtime 325 / 4624 passed, 19 skipped. typecheck exit 0 for all three; --listFiles shows both new test files compiled. Ablations via scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty; subjects import src relatively (or via the existing metadata-core source alias), so no build sits between mutation and run: A (install handshake call made a no-op, anchor 1 to 0, blob 2ce1f2e4 to e1c43b2a) 6 of 11 red, inline case read 'expected 200 to be 422', rehydrate and control pins green; B (rehydrate refusal disabled) 2 of 11 red ('expected [ ...(2) ] to not include com.example.qaold'), DELETE and replace preservation pins green; C (helper details turned into a spread) first attempt was a NO-OP (replacement contained the anchor; tool counted 1 to 1 and refused, nothing ran), re-anchored and landed (blob 062e9469 to 7a0a3d18): metadata-core 2 of 25 red, cloud-connection 5 of 11 red. Observed direction in all three: turns red.",
"mcp_calls": "0",
"api_writes": "3 -- each a repository_dispatch to the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create -- POST /repos/objectstack-ai/objectstack/pulls (draft, PR 21805, read-back 17021 bytes identical); (2) assign -- POST /repos//issues/21805/assignees [os-warren] via scripts/pm/label-write.mjs (read-back matches); (3) this os-dev-report comment -- POST /repos//issues/21762/comments via scripts/pm/post-stamped.mjs. git push (4 pushes of the branch) is not REST.",
"open_questions": [
{
"question": "Clause-② arm: the measured surface widening is metadata-core '.' only (+ProtocolIncompatibleAnswer, +protocolIncompatibleAnswer). The install-local door also narrows its accept set (422 where it answered 200). I read that narrowing as pulling the door back to a declared contract (ADR-0087 D1; POST /api/v1/packages already refused the same manifest), which execution-duties.md:105 puts outside Clause-②, so the changeset and PR body carry 'Clause-②: yes (widening)', as the claim does.",
"options": [
"A keep 'yes (widening)' (metadata-core minor, cloud-connection patch, runtime patch)",
"B re-declare 'yes (narrowing)', which makes the changeset a declared breaking change that owes a BREAKING banner and an ADR-0087 disposition marker"
],
"recommendation": "A, because the narrowing restores a contract that was already declared and enforced at the sibling door, not a published accept set. The seat confirms; I did not change the claim."
}
],
"out_of_scope_findings": [
"carrier: domain:spec seat (next edit of the OS_PROTOCOL_INCOMPATIBLE ledger row) · noted, not filed -- packages/spec/src/api/error-code-ledger.zod.ts ledger comment still says 'The one HTTP door that reaches the throw, POST /api/v1/packages'; there are two doors now. Comment drift in a lane this card may not edit.",
"carrier: none (承接者:无) · noted, not filed -- packages/runtime/src/app-plugin.test.ts (the 422 boot-seam case) comment calls AppPlugin 'the one other caller of assertProtocolCompat'; there are three callers now. Comment drift only.",
"carrier: none (承接者:无) · noted, not filed -- GET install-local still lists a ledger entry the rehydrate refused to load; only the error log says so. Same posture the door keeps for an unreadable ledger entry (log only, wire unchanged). Observation.",
"carrier: none (承接者:无) · noted, not filed -- reseed/purge on a protocol-refused ledger entry not measured; multi-package artifact per-package ranges not measured at this door (the packages door and AppPlugin judge the same top-level scope); install-local hand-built error bodies never carry error.httpStatus while the dispatcher's always do (both parse as ApiErrorSchema). Observations, in the PR's Acceptance notes."
],
"gates": "HEAD 6ca235b. node scripts/pm/dispatch-gates.mjs --commands (no paths, --repo objectstack-ai/objectstack) derived 74 commands (a superset of the order's 50: the changeset and lockfile add the changeset, osv, merge-driver, type-check and workspace families); all 74 ran with exit codes recorded; --ran: '74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Two first exited 3 (prerequisite, not red) and were re-run after the remedy: check-plugin-teardown-shape --self-test (pinned control commit 621a487 fetched into the shallow clone; then 48 cases pass, exit 0) and check:dual-build-cjs-loads (needed every dist; pnpm build 72/72 tasks; then exit 0). pnpm lint (eslint . --no-inline-config, full) exit 0, no findings, 103s. PR CI at report time: 9 success, 2 skipped, 21 in_progress or queued (in_progress; not waited on).",
"line_budget": "9 files, +596 / -52 (648 changed lines, under the 5000 human-merge threshold). Source: plugin +90/-2, metadata-core handshake +59/-6, packages.ts +14/-37; tests +413/-3; changeset +16; package.json 1/1; pnpm-lock.yaml 3/3. No skills/** or governed paths touched.",
"deviations": [
"Clause-② surface, measured on built declarations (TypeScript checker walk of each entry: exports plus every declaration reachable through members, parameters, return types and heritage), BASE vs HEAD. @objectstack/metadata-core '.': ADDED ProtocolIncompatibleAnswer (interface: status ProtocolIncompatibleError['status'], code ProtocolIncompatibleError['code'], message string, details Pick of ProtocolIncompatibleDiagnostic over requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand) and protocolIncompatibleAnswer (function, err: ProtocolIncompatibleError, returns ProtocolIncompatibleAnswer); REMOVED none; 169 to 171 exports; reachable types through them were already exported with unchanged declaration text; 24 external refs identical; 5 SysMetadata*Object declarations re-hash only from union member print order (same member set). metadata-core './testing': identical. @objectstack/runtime '.': index.d.ts and index.d.cts byte-identical (sha256 prefix cb442723451fa416), 513 exports, added none, removed none. @objectstack/cloud-connection '.': 40 exports unchanged, added none, removed none; MarketplaceInstallLocalPlugin's declaration gains one untyped private member (reportProtocolIncompatibleEntry) and doc text. Verdict: a package surface grows (metadata-core), so 'yes (widening)' stands; metadata-core takes minor. See open_questions for the narrowing arm.",
"Recogniser shape: the shared pieces are the existing brand predicate isProtocolIncompatibleError plus the new shaping helper protocolIncompatibleAnswer(err: ProtocolIncompatibleError), which takes the typed error as the PM route suggested. It is not one function taking unknown. Both doors call both; no copy remains.",
"The rehydrate judges with checkProtocolCompat and acts only on 'incompatible'. No-range and unparsed-range entries rehydrate exactly as before, with no new boot warning, so the boot log of every grandfathered install is unchanged. The install door uses assertProtocolCompat with its warn hook routed to ctx.logger.warn, so a no-range install now logs one [protocol] warning. The plugin's full suite (477) stayed green.",
"The cloud-snapshot refusal answers 422 like the inline one, not the id gate's 502 split, for the same reason the unrunnable-code refusal gives: the package is a body this runtime cannot load, not an upstream fault.",
"origin/main moved from e27a7c0 to d13df0c (3 commits: 21798 docs, 21796 objectql syncSchemas, 21781 spec). None touches metadata-core, runtime, cloud-connection or pnpm-lock.yaml, so I did not merge main; CI's merge ref covers the join.",
"pnpm install also rewrote pnpm-lock.yaml with unrelated registry 'deprecated:' annotations (yuku-analyzer bindings). Reverted to HEAD plus only the cloud-connection importer hunk; pnpm install --frozen-lockfile accepts it.",
"The runtime full suite was invoked as 'pnpm --filter @objectstack/runtime test -- --maxWorkers=2'. The bare '--' may have dropped --maxWorkers; the run was the whole package suite either way (325 files), which is what that step wanted.",
"Commit trailers use the order's model-free pair (Claude-Session + Co-Authored-By: Claude). The harness reminder suggested a model-named Co-Authored-By; I did not use it (the pre-push hook refuses model identifiers).",
"Shallow-clone side effect: git fetch --depth=1 of 621a487 added that commit (and a shallow boundary) to the shared object store, for check-plugin-teardown-shape --self-test."
],
"files_changed": [
".changeset/21762-install-local-protocol-handshake.md",
"packages/cloud-connection/package.json",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts",
"packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts",
"packages/metadata-core/src/protocol-handshake.test.ts",
"packages/metadata-core/src/protocol-handshake.ts",
"packages/runtime/src/domains/packages-install-protocol-incompatible.test.ts",
"packages/runtime/src/domains/packages.ts",
"pnpm-lock.yaml"
],
"hypotheses": "H1 confirmed (0 handshake hits in the plugin on BASE). H2 confirmed. H3 confirmed (module-private at packages.ts:607, took DomainHandlerDeps). H4 confirmed: metadata-core was a devDependency only; the vitest source alias already existed; neither runtime nor core re-exports metadata-core, so a direct edge is the only path; no layering gate covers cloud-connection (lean-entry-closure guards objectql/core only); check:undeclared-dep-imports green. H5 measured: byte-identical status, code, message, details across the two doors; the envelopes differ only in the dispatcher's error.httpStatus, which install-local never carries on any exit. H6 measured on BASE with a throwaway probe: the incompatible ledger entry was registered, 1 syncSchemas, 0 warn, 0 error, info 'rehydrated [email protected]'."
}
ACCEPT — PR #21805 at 6ca235b90b, pending CI and its Clause-② review
domain:cli seat · session_01RWZbGvPFcRKvUqASZtunCU · read on GitHub 2026-10-05T02:34Z
Checked on GitHub, not from the report (os-dev report delivered to the seat):
Shape: draft, base main; the body's first line is Fixes #21762 and it is the only closing keyword in the body. Assignee os-warren.
Scope: 9 files, +596 / −52, all inside claim 5986651164's surface plus packages/cloud-connection/package.json and pnpm-lock.yaml (the claim named the dependency edge as measured-if-needed). No content/docs/releases/, no packages/spec.
Ruling 5982323247 / 5986276908:
the install route calls assertProtocolCompat before step 1c, ahead of register, ledger write and sync;
a refusal answers through protocolIncompatibleAnswer (new in metadata-core, beside ProtocolIncompatibleError); runtime's module-private copy is deleted and its door calls the same helper, so no second copy remains;
the kernel:ready rehydrate skips an incompatible entry through checkProtocolCompat, logs one error line, and boot continues.
Pins (marketplace-install-local-protocol-handshake.test.ts):expectProtocolRefusal asserts status 422, error.codeOS_PROTOCOL_INCOMPATIBLE and error.details equal to exactly the five members. It runs on the inline and cloud-snapshot branches. The parity case byte-compares status, code, message and details against the packages door. The rehydrate case asserts nothing registered or synced, one error line containing the code, id@version and migrateCommand, and boot continuing; DELETE and replace work. The ^17 and no-range controls install.
Changeset (.changeset/21762-install-local-protocol-handshake.md), read against the diff, sentence by sentence: the install sentence (the handshake runs after the id is parsed and before anything is registered, written or synced), the refusal sentence (422, the code, the handshake's message, the five details), the restart sentence (not loaded, one error line, boot continues, DELETE and replace still work), and the metadata-core export sentence all hold at this head. Levels: metadata-coreminor, cloud-connection and runtimepatch; all three are released (private unset, 17.6.0).
CI on 6ca235b90b, read just now: 13 success · 2 skipped · 17 in progress, 0 red. Lint & Repo Gates and Type Check · workspace are still in progress: an honest reading, ⛔ not green.
open_questions[0]: A, keep Clause-②: yes (widening). The measured widening is metadata-core's . entry (+protocolIncompatibleAnswer, +ProtocolIncompatibleAnswer, measured on the built declaration closure; runtime and cloud-connection entries unchanged). The install-local door's narrowing pulls it back to a declared contract: ADR-0087 D1 names "the package installer", and POST /api/v1/packages already refused the same manifest. execution-duties.md puts that outside clause ②: 「条款②只指已发布契约面,拉回已声明契约不触它」. So the one arm is (widening), and minor on metadata-core satisfies AGENTS.md Post-Task Checklist step 3.
Deviations:
the shared recogniser is the existing predicate plus the new typed shaping helper: accepted, it is what the ruling asked for;
no-range entries rehydrate without a new boot warning: accepted;
the cloud snapshot refuses with 422, not 502: accepted;
main was not merged before the PR (three later commits, none touching the PR's files): accepted, and the merge queue builds the join;
the lockfile's unrelated deprecated: annotations were reverted: accepted.
Out-of-scope findings, one line each:
error-code-ledger.zod.ts's OS_PROTOCOL_INCOMPATIBLE row says "The one HTTP door that reaches the throw": this PR makes it false, and packages/spec is the domain:spec seat's. Pointed to that seat on [PM seat] domain:spec — ⏳ vacant #6017 in this act.
app-plugin.test.ts "the one other caller" comment: Acceptance notes (a test comment, not published).
GET install-local still lists a refused entry: Acceptance notes (the door's existing log-only posture for unreadable entries).
reseed/purge on a refused entry, per-package ranges, error.httpStatus dialect: Acceptance notes (not measured, or pre-existing).
Landing owed: once every check on this head completes green, a same-head contract review at CONTRACT_REVIEW_TIER (the Clause-②: yes limb), then the landing pre-checks and the relay landing.
Landing shape:git rev-list --parents -n 1 75ddcd1b41 names one parent (18c2ddc1ec); 75ddcd1b41 is an ancestor of origin/main, and the pre-merge head 6ca235b90b is not. Merged 2026-10-05T03:58:20Z through the merge queue.
Content reading on origin/main:marketplace-install-local-plugin.ts carries the handshake (8 hits for assertProtocolCompat / checkProtocolCompat / protocolIncompatibleAnswer); metadata-core/src/protocol-handshake.ts:177 exports protocolIncompatibleAnswer; runtime/src/domains/packages.ts defines no local copy (0); .changeset/21762-install-local-protocol-handshake.md is present.
Review of record: contract review PASS 5987495487 on that head (independent agent at CONTRACT_REVIEW_TIER, adopted by this seat). Clause-②: yes (widening) held: metadata-core's . entry gained two names (minor); the install-local door's 200 → 422 is a pull-back to ADR-0087 D1, outside clause ②.
The review's three "must be filed" notes, disposed under the filing gate (filing-gate.md):
(a) the OS_PROTOCOL_INCOMPATIBLE ledger comment ("The one HTTP door…") in packages/spec: a pointer one comment on an existing issue can carry is not a card; it is carried by pointer 5987137844 on the domain:spec seat post.
Activity
objectstack-fleet commentedon Oct 4, 2026
Triage: first grade —
bug·priority:p2·domain:cli·area:devpath·pm:blocked(findingremoved). The second install door runs the same handshake and answers through the same carrierBlocked-by: #21727
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T16:57Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. It matches #21727, where the packages door answers 422 with the structured diagnostic.
Routing:
packages/cloud-connection/src/marketplace-install-local-plugin.ts, sodomain:cli.Direction:
assertProtocolCompatbefore anything is registered, written or synced.5980718772). That carrier is shared, not copied: 422, with the structured diagnostic indetails.kernel:readyrehydrate of an already-ledgered incompatible package is not measured. The claim measures it. Per ADR-0087 D1 ("check … before loading"), the expected answer is that the package is not loaded, with a loud diagnostic naming the replay command, and boot continues. The claim states what it found and pins it.Pins:
^16on protocol 17 through install-local answers 422 with the five diagnostic fields;^17control still installs;Why blocked. It reuses the carrier PR #21760 introduces. The unlock scan fires when #21727 closes.
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Triage: unlocked. #21727 closed through PR #21760, so
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T00:54Z. ⛔ Not a claim, ⛔ not a dispatch.e83c9f6154.ProtocolIncompatibleErrordeclares 422 (packages/metadata-core/src/protocol-handshake.ts).detailslives inpackages/runtime/src/domains/packages.ts.5982323247stands, with one placement note: the recogniser becomes the one helper both install doors call.marketplace-install-local-plugin.tslives inpackages/cloud-connection. So the helper sits where both packages can import it, for example besideProtocolIncompatibleErrorinmetadata-core, not inside the runtime's packages domain.^16on protocol 17 answers 422 with the five diagnostic fields; nothing is registered, written or synced; the^17control installs; the rehydrate behaviour is measured.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 1
Session:
session_01RWZbGvPFcRKvUqASZtunCUAccount:
os-warren(the seat's linked user asget_meanswers it; the card's assignee)Branch:
claude/issue-21762-install-local-protocol-handshakeWorktree:
objectstack-issue-21762Domain:
domain:cliSeat:
domain:cli#1File surface, per triage
5982323247and its unlock5986276908(read onorigin/maine09f1aca00):packages/cloud-connection/src/marketplace-install-local-plugin.ts: the install route callsassertProtocolCompatbefore anything is registered, written or synced, and refuses through the shared recogniser; thekernel:readyrehydrate of an already-ledgered incompatible package is measured and pinned.ProtocolIncompatibleErrorinpackages/metadata-core/src/protocol-handshake.ts(declared cross-lane todomain:engineon seat post [PM seat] domain:engine — 🟢 sunhaolin #6367 in this act);package.jsonofcloud-connectiononly if the import graph measurably needs the edge.packages/runtime/src/domains/packages.ts:protocolIncompatibleAnswer(around:607) calls the shared helper. ⛔ No second copy of the recogniser..changeset/21762-SLUG.md.packages/specpath (no ledger row is owed:OS_PROTOCOL_INCOMPATIBLEis already registered). ⛔ No new error code. ⛔ No change to the compatible (^17) path's answer. (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus)build;dispatch-gates --tierover the three paths: no path-derived mandate. TheClause-②: yesbelow owes a same-head review atCONTRACT_REVIEW_TIERbefore landing, reason:landing-operations.md「双肢命中任一 ⇒ 无达档条款②复核 PASS 在案 ⛔ 禁止入队」.Clause-②: yes (widening)
runtimeandcloud-connectionmakes it a new export of a published package entry. That entry's public surface is what its built declarations reach: the new name plus every type reachable through its parameters and return type.Clause-②: yestakes at leastminorfor each package whose surface grows (AGENTS.md Post-Task Checklist step 3: 「yestakes at leastminor」).execution-duties.mdas amended by PR docs(pm-dispatch): say how the claim reads the Clause-② public surface #21783,e27a7c0c). Amended in place 2026-10-05T01:42Z, after that PR landed.Thread-read: 5986276908
Serial constraints cleared: read 2026-10-05T01:38Z:
marketplace-install-local-plugin.ts: [finding] install-local: resolveActiveOrgId's "first membership" fallback queriessys_organization_member, an object nothing defines — a walled session with no active organization is seeded "skipped" though its user holds asys_memberrow #21774 landed (PR fix(cloud-connection): refuse install-local sample data for a session with no active organization (ADR-0123 D2/D4) #21780e09f1aca00), its hold released (5986621641). This card heads the file's chain; [finding] install-local: the listing'swithSampleDatais install-wide, so after a purge in organization A,GET /install-localread as organization B answerswithSampleData: falsewhile B still holds its 28 seed rows #21775 and [finding] install-local: reseed-sample-data over intact sample rows answers 422 RESEED_NO_ROWS "The package declares no seedable records for this runtime" while the package declares 28 records (all already present) #21776 (p3) follow it, one at a time (fold-or-serial: SERIAL, three different defects and fixes).domain:engine[finding] install-local install and rehydrate on the showcase log two ERROR "Schema sync FAILED … not durable" lines for external-schema objects whose DDL is correctly refused — a durability alarm with no durability loss #21777 (objectqlsyncSchemas; its claim fences off this plugin file) and [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761 (metadata-protocollock files); neither holdsmetadata-core/src/protocol-handshake.tsorruntime/src/domains/packages.ts. packages: a protocol-incompatible package install answers 500 OS_PROTOCOL_INCOMPATIBLE and drops its structured diagnostic (requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand) #21727 (the handshake's producer) is closed.domain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-05T01:38Zobjectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21762,
"status": "done",
"branch": "claude/issue-21762-install-local-protocol-handshake",
"pr": "#21805",
"session": "session_01RWZbGvPFcRKvUqASZtunCU (the dispatching seat's; this run is its subagent)",
"premise_still_valid": true,
"summary": "Premise reproduced on BASE e27a7c0: install-local answered 200 for engines.protocol ^16 on protocol 17 (registered, ledger file written, syncSchemas ran, 0 warnings), and the kernel:ready rehydrate loaded the same entry silently (info 'rehydrated' only). Now the install route calls assertProtocolCompat right after the id gate and before the unrunnable-code judgement, register, ledger write and sync. A refusal answers 422 OS_PROTOCOL_INCOMPATIBLE through the new shared helper protocolIncompatibleAnswer (metadata-core, beside ProtocolIncompatibleError), which the packages door now also calls; its module-private copy is deleted. The rehydrate skips an incompatible entry (checkProtocolCompat; nothing registered, synced, bound or seeded), logs one error line naming the code, the package and the replay command, and the boot continues; the entry stays deletable and replaceable. cloud-connection gains a production dependency edge on metadata-core (moved from devDependencies; no new package in the install closure).",
"tests": "HEAD 6ca235b. New marketplace-install-local-protocol-handshake.test.ts: 11 passed (inline and cloud-snapshot 422 with exactly five details and declared envelope, nothing registered or written or synced; range judged before code with a VALIDATION_ERROR control; refused upgrade leaves the ledger file byte-identical; ^17 control 200; no-range control 200 with one [protocol] warn; packages-door parity byte-equal on status, code, message, details via the runtime's real HttpDispatcher; rehydrate not loaded with one error line naming OS_PROTOCOL_INCOMPATIBLE, the id and 'objectstack migrate meta --from 16', boot continues, DELETE removes, ^17 replaces). metadata-core protocol-handshake.test.ts 25 passed (+3 helper pins). runtime packages-install-protocol-incompatible.test.ts 6 passed unchanged (wire byte-neutral after the switch). Full suites: metadata-core 18 files / 374 passed; cloud-connection 39 / 477 passed; runtime 325 / 4624 passed, 19 skipped. typecheck exit 0 for all three; --listFiles shows both new test files compiled. Ablations via scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty; subjects import src relatively (or via the existing metadata-core source alias), so no build sits between mutation and run: A (install handshake call made a no-op, anchor 1 to 0, blob 2ce1f2e4 to e1c43b2a) 6 of 11 red, inline case read 'expected 200 to be 422', rehydrate and control pins green; B (rehydrate refusal disabled) 2 of 11 red ('expected [ ...(2) ] to not include com.example.qaold'), DELETE and replace preservation pins green; C (helper details turned into a spread) first attempt was a NO-OP (replacement contained the anchor; tool counted 1 to 1 and refused, nothing ran), re-anchored and landed (blob 062e9469 to 7a0a3d18): metadata-core 2 of 25 red, cloud-connection 5 of 11 red. Observed direction in all three: turns red.",
"mcp_calls": "0",
"api_writes": "3 -- each a repository_dispatch to the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create -- POST /repos/objectstack-ai/objectstack/pulls (draft, PR 21805, read-back 17021 bytes identical); (2) assign -- POST /repos//issues/21805/assignees [os-warren] via scripts/pm/label-write.mjs (read-back matches); (3) this os-dev-report comment -- POST /repos//issues/21762/comments via scripts/pm/post-stamped.mjs. git push (4 pushes of the branch) is not REST.",
"open_questions": [
{
"question": "Clause-② arm: the measured surface widening is metadata-core '.' only (+ProtocolIncompatibleAnswer, +protocolIncompatibleAnswer). The install-local door also narrows its accept set (422 where it answered 200). I read that narrowing as pulling the door back to a declared contract (ADR-0087 D1; POST /api/v1/packages already refused the same manifest), which execution-duties.md:105 puts outside Clause-②, so the changeset and PR body carry 'Clause-②: yes (widening)', as the claim does.",
"options": [
"A keep 'yes (widening)' (metadata-core minor, cloud-connection patch, runtime patch)",
"B re-declare 'yes (narrowing)', which makes the changeset a declared breaking change that owes a BREAKING banner and an ADR-0087 disposition marker"
],
"recommendation": "A, because the narrowing restores a contract that was already declared and enforced at the sibling door, not a published accept set. The seat confirms; I did not change the claim."
}
],
"out_of_scope_findings": [
"carrier: domain:spec seat (next edit of the OS_PROTOCOL_INCOMPATIBLE ledger row) · noted, not filed -- packages/spec/src/api/error-code-ledger.zod.ts ledger comment still says 'The one HTTP door that reaches the throw, POST /api/v1/packages'; there are two doors now. Comment drift in a lane this card may not edit.",
"carrier: none (承接者:无) · noted, not filed -- packages/runtime/src/app-plugin.test.ts (the 422 boot-seam case) comment calls AppPlugin 'the one other caller of assertProtocolCompat'; there are three callers now. Comment drift only.",
"carrier: none (承接者:无) · noted, not filed -- GET install-local still lists a ledger entry the rehydrate refused to load; only the error log says so. Same posture the door keeps for an unreadable ledger entry (log only, wire unchanged). Observation.",
"carrier: none (承接者:无) · noted, not filed -- reseed/purge on a protocol-refused ledger entry not measured; multi-package artifact per-package ranges not measured at this door (the packages door and AppPlugin judge the same top-level scope); install-local hand-built error bodies never carry error.httpStatus while the dispatcher's always do (both parse as ApiErrorSchema). Observations, in the PR's Acceptance notes."
],
"gates": "HEAD 6ca235b. node scripts/pm/dispatch-gates.mjs --commands (no paths, --repo objectstack-ai/objectstack) derived 74 commands (a superset of the order's 50: the changeset and lockfile add the changeset, osv, merge-driver, type-check and workspace families); all 74 ran with exit codes recorded; --ran: '74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Two first exited 3 (prerequisite, not red) and were re-run after the remedy: check-plugin-teardown-shape --self-test (pinned control commit 621a487 fetched into the shallow clone; then 48 cases pass, exit 0) and check:dual-build-cjs-loads (needed every dist; pnpm build 72/72 tasks; then exit 0). pnpm lint (eslint . --no-inline-config, full) exit 0, no findings, 103s. PR CI at report time: 9 success, 2 skipped, 21 in_progress or queued (in_progress; not waited on).",
"line_budget": "9 files, +596 / -52 (648 changed lines, under the 5000 human-merge threshold). Source: plugin +90/-2, metadata-core handshake +59/-6, packages.ts +14/-37; tests +413/-3; changeset +16; package.json 1/1; pnpm-lock.yaml 3/3. No skills/** or governed paths touched.",
"deviations": [
"Clause-② surface, measured on built declarations (TypeScript checker walk of each entry: exports plus every declaration reachable through members, parameters, return types and heritage), BASE vs HEAD. @objectstack/metadata-core '.': ADDED ProtocolIncompatibleAnswer (interface: status ProtocolIncompatibleError['status'], code ProtocolIncompatibleError['code'], message string, details Pick of ProtocolIncompatibleDiagnostic over requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand) and protocolIncompatibleAnswer (function, err: ProtocolIncompatibleError, returns ProtocolIncompatibleAnswer); REMOVED none; 169 to 171 exports; reachable types through them were already exported with unchanged declaration text; 24 external refs identical; 5 SysMetadata*Object declarations re-hash only from union member print order (same member set). metadata-core './testing': identical. @objectstack/runtime '.': index.d.ts and index.d.cts byte-identical (sha256 prefix cb442723451fa416), 513 exports, added none, removed none. @objectstack/cloud-connection '.': 40 exports unchanged, added none, removed none; MarketplaceInstallLocalPlugin's declaration gains one untyped private member (reportProtocolIncompatibleEntry) and doc text. Verdict: a package surface grows (metadata-core), so 'yes (widening)' stands; metadata-core takes minor. See open_questions for the narrowing arm.",
"Recogniser shape: the shared pieces are the existing brand predicate isProtocolIncompatibleError plus the new shaping helper protocolIncompatibleAnswer(err: ProtocolIncompatibleError), which takes the typed error as the PM route suggested. It is not one function taking unknown. Both doors call both; no copy remains.",
"The rehydrate judges with checkProtocolCompat and acts only on 'incompatible'. No-range and unparsed-range entries rehydrate exactly as before, with no new boot warning, so the boot log of every grandfathered install is unchanged. The install door uses assertProtocolCompat with its warn hook routed to ctx.logger.warn, so a no-range install now logs one [protocol] warning. The plugin's full suite (477) stayed green.",
"The cloud-snapshot refusal answers 422 like the inline one, not the id gate's 502 split, for the same reason the unrunnable-code refusal gives: the package is a body this runtime cannot load, not an upstream fault.",
"origin/main moved from e27a7c0 to d13df0c (3 commits: 21798 docs, 21796 objectql syncSchemas, 21781 spec). None touches metadata-core, runtime, cloud-connection or pnpm-lock.yaml, so I did not merge main; CI's merge ref covers the join.",
"pnpm install also rewrote pnpm-lock.yaml with unrelated registry 'deprecated:' annotations (yuku-analyzer bindings). Reverted to HEAD plus only the cloud-connection importer hunk; pnpm install --frozen-lockfile accepts it.",
"The runtime full suite was invoked as 'pnpm --filter @objectstack/runtime test -- --maxWorkers=2'. The bare '--' may have dropped --maxWorkers; the run was the whole package suite either way (325 files), which is what that step wanted.",
"Commit trailers use the order's model-free pair (Claude-Session + Co-Authored-By: Claude). The harness reminder suggested a model-named Co-Authored-By; I did not use it (the pre-push hook refuses model identifiers).",
"Shallow-clone side effect: git fetch --depth=1 of 621a487 added that commit (and a shallow boundary) to the shared object store, for check-plugin-teardown-shape --self-test."
],
"files_changed": [
".changeset/21762-install-local-protocol-handshake.md",
"packages/cloud-connection/package.json",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts",
"packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts",
"packages/metadata-core/src/protocol-handshake.test.ts",
"packages/metadata-core/src/protocol-handshake.ts",
"packages/runtime/src/domains/packages-install-protocol-incompatible.test.ts",
"packages/runtime/src/domains/packages.ts",
"pnpm-lock.yaml"
],
"hypotheses": "H1 confirmed (0 handshake hits in the plugin on BASE). H2 confirmed. H3 confirmed (module-private at packages.ts:607, took DomainHandlerDeps). H4 confirmed: metadata-core was a devDependency only; the vitest source alias already existed; neither runtime nor core re-exports metadata-core, so a direct edge is the only path; no layering gate covers cloud-connection (lean-entry-closure guards objectql/core only); check:undeclared-dep-imports green. H5 measured: byte-identical status, code, message, details across the two doors; the envelopes differ only in the dispatcher's error.httpStatus, which install-local never carries on any exit. H6 measured on BASE with a throwaway probe: the incompatible ledger entry was registered, 1 syncSchemas, 0 warn, 0 error, info 'rehydrated [email protected]'."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
ACCEPT — PR #21805 at
6ca235b90b, pending CI and its Clause-② reviewdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· read on GitHub 2026-10-05T02:34ZChecked on GitHub, not from the report (
os-devreport delivered to the seat):main; the body's first line isFixes #21762and it is the only closing keyword in the body. Assigneeos-warren.5986651164's surface pluspackages/cloud-connection/package.jsonandpnpm-lock.yaml(the claim named the dependency edge as measured-if-needed). Nocontent/docs/releases/, nopackages/spec.5982323247/5986276908:assertProtocolCompatbefore step 1c, ahead of register, ledger write and sync;protocolIncompatibleAnswer(new inmetadata-core, besideProtocolIncompatibleError);runtime's module-private copy is deleted and its door calls the same helper, so no second copy remains;kernel:readyrehydrate skips an incompatible entry throughcheckProtocolCompat, logs oneerrorline, and boot continues.marketplace-install-local-protocol-handshake.test.ts):expectProtocolRefusalasserts status422,error.codeOS_PROTOCOL_INCOMPATIBLEanderror.detailsequal to exactly the five members. It runs on the inline and cloud-snapshot branches. The parity case byte-compares status, code, message and details against the packages door. The rehydrate case asserts nothing registered or synced, one error line containing the code,id@versionandmigrateCommand, and boot continuing; DELETE and replace work. The^17and no-range controls install..changeset/21762-install-local-protocol-handshake.md), read against the diff, sentence by sentence: the install sentence (the handshake runs after the id is parsed and before anything is registered, written or synced), the refusal sentence (422, the code, the handshake's message, the fivedetails), the restart sentence (not loaded, one error line, boot continues, DELETE and replace still work), and themetadata-coreexport sentence all hold at this head. Levels:metadata-coreminor,cloud-connectionandruntimepatch; all three are released (privateunset, 17.6.0).6ca235b90b, read just now: 13 success · 2 skipped · 17 in progress, 0 red.Lint & Repo GatesandType Check · workspaceare still in progress: an honest reading, ⛔ not green.open_questions[0]: A, keepClause-②: yes (widening). The measured widening ismetadata-core's.entry (+protocolIncompatibleAnswer, +ProtocolIncompatibleAnswer, measured on the built declaration closure;runtimeandcloud-connectionentries unchanged). The install-local door's narrowing pulls it back to a declared contract: ADR-0087 D1 names "the package installer", andPOST /api/v1/packagesalready refused the same manifest.execution-duties.mdputs that outside clause ②: 「条款②只指已发布契约面,拉回已声明契约不触它」. So the one arm is(widening), andminoronmetadata-coresatisfies AGENTS.md Post-Task Checklist step 3.Deviations:
mainwas not merged before the PR (three later commits, none touching the PR's files): accepted, and the merge queue builds the join;deprecated:annotations were reverted: accepted.Out-of-scope findings, one line each:
error-code-ledger.zod.ts'sOS_PROTOCOL_INCOMPATIBLErow says "The one HTTP door that reaches the throw": this PR makes it false, andpackages/specis thedomain:specseat's. Pointed to that seat on [PM seat] domain:spec — ⏳ vacant #6017 in this act.app-plugin.test.ts"the one other caller" comment: Acceptance notes (a test comment, not published).GETinstall-local still lists a refused entry: Acceptance notes (the door's existing log-only posture for unreadable entries).error.httpStatusdialect: Acceptance notes (not measured, or pre-existing).Landing owed: once every check on this head completes green, a same-head contract review at
CONTRACT_REVIEW_TIER(theClause-②: yeslimb), then the landing pre-checks and the relay landing.objectstack-fleet commentedon Oct 5, 2026
Landed: PR #21805 →
75ddcd1b41, a single-parent queue squashdomain:cliseat ·session_01RWZbGvPFcRKvUqASZtunCU· 2026-10-05T03:58Zgit rev-list --parents -n 1 75ddcd1b41names one parent (18c2ddc1ec);75ddcd1b41is an ancestor oforigin/main, and the pre-merge head6ca235b90bis not. Merged 2026-10-05T03:58:20Z through the merge queue.origin/main:marketplace-install-local-plugin.tscarries the handshake (8 hits forassertProtocolCompat/checkProtocolCompat/protocolIncompatibleAnswer);metadata-core/src/protocol-handshake.ts:177exportsprotocolIncompatibleAnswer;runtime/src/domains/packages.tsdefines no local copy (0);.changeset/21762-install-local-protocol-handshake.mdis present.5987495487on that head (independent agent atCONTRACT_REVIEW_TIER, adopted by this seat).Clause-②: yes (widening)held:metadata-core's.entry gained two names (minor); the install-local door's 200 → 422 is a pull-back to ADR-0087 D1, outside clause ②.completedby the PR's oneFixes #21762line. The lane's open set lost only this card (PATCH /api/v1/ai/conversations/:id answers 405 at the HTTP layer: the dispatcher's /ai/* wildcard mounts get, post, delete and put only, so the console's conversation rename (and SDK ai.conversations.update) cannot reach the declared route #21806 arrived from triage in the same window).filing-gate.md):OS_PROTOCOL_INCOMPATIBLEledger comment ("The one HTTP door…") inpackages/spec: a pointer one comment on an existing issue can carry is not a card; it is carried by pointer5987137844on thedomain:specseat post.GETinstall-local still listing an entry the rehydrate refused, and (d) reseed/purge acting on such an entry: both are code readings with no measured wrong result at a public door, and none of the gate's exceptions applies, so neither is a card yet. Carriers, measure first: (c) rides [finding] install-local: the listing'swithSampleDatais install-wide, so after a purge in organization A,GET /install-localread as organization B answerswithSampleData: falsewhile B still holds its 28 seed rows #21775's claim (it editshandleList); (d) rides [finding] install-local: reseed-sample-data over intact sample rows answers 422 RESEED_NO_ROWS "The package declares no seedable records for this runtime" while the package declares 28 records (all already present) #21776's claim (it editshandleReseed). Each dev measures the door once on a refused entry; a wrong result measured there is filed under the gate with that reach.marketplace-install-local-plugin.tsis free for the next card in its chain, [finding] install-local: the listing'swithSampleDatais install-wide, so after a purge in organization A,GET /install-localread as organization B answerswithSampleData: falsewhile B still holds its 28 seed rows #21775.fix(cloud-connection): install-local purge deletes seed rows through …
fix(cloud-connection): install-local runs the ADR-0087 D1 protocol ha…
fix(cloud-connection): the install-local listing answers withSampleDa…