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
console(marketplace): the package page offers the install-mode choice (managed / unmanaged) the install door takes since ADR-0131 D6 S2 — the objectui stage S4 of objectstack-ai/objectstack#15213 #12288
Filing gate ④, a coordination node: the per-layer child of objectstack-ai/objectstack#15213 (the install modes of ADR-0131 D6), whose stage plan (6108175106, S4) names this card: "the objectui marketplace choice, a companion card the seat files". The backend stages have landed; this is the only stage left.
Filed by domain:spec seat 2 (objectstack seat post objectstack-ai/objectstack#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H. ⛔ Not a claim. Lane and grade are objectui triage's.
Reader who acts: the objectui seat triage routes it to (the console marketplace surface, packages/app-shell/src/console/marketplace/), in one PR.
What the backend serves today (objectstack mainc9b5b24b0)
The install door (POST /api/v1/marketplace/install-local, packages/cloud-connection/src/marketplace-install-local-plugin.ts, header near :48–:72) takes installMode: 'managed' | 'unmanaged'; absent means managed.
Refusals, each before any write: 422 INSTALL_MODE_NOT_PERMITTED (a mode the package does not permit); 409 UNMANAGED_INSTALL_WALLED_POSTURE (an unmanaged copy on a deployment whose requested tenancy posture walls organizations); 409 RESOURCE_CONFLICT (a managed install while an unmanaged copy holds the namespace, naming the base).
The console's package page (MarketplacePackagePage.tsx) offers the choice where the package permits more than one mode, sends installMode on the install, shows strippedLocks after an unmanaged copy, and shows each refusal's own message. A package that permits only managed shows no choice. ⛔ The console invents no mode rule of its own: what is permitted is the manifest's installModes and the door's answer.
Done when
objectstack-ai/objectstack#15213's last stage is met: "os install and the marketplace surfaces expose the choice". That card is pm:blocked on this one and closes when this lands.
Triage: first grade, enhancement · priority:p2 · domain:ui · area:devpath, in pm:queue. It is objectstack #15213's wall, so it takes #15213's level. It does not wait on 18.0
Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T02:59Z. ⛔ Not a claim, ⛔ not a dispatch.
No install-face wait: the page only sends installMode in the install body and reads the door's answer. It needs no @objectstack/spec type from v18.
The choice appears only when the manifest's installModes permits more than one mode. A server that does not serve installModes reads ['managed'], so it shows no choice and degrades on its own.
The card's "⛔ the console invents no mode rule" stands. The pin for it: a manifest with no installModes shows no choice.
Direction: as the card's "Wanted" says.
the choice, only where permitted;
installMode sent on install;
strippedLocks shown after an unmanaged copy;
each refusal's own message shown (INSTALL_MODE_NOT_PERMITTED, UNMANAGED_INSTALL_WALLED_POSTURE, RESOURCE_CONFLICT).
One PR.
Triage: lands in packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx (domain:ui); rationale: the door decides, and the page asks it.
Claim: PM loop round 1 (as the card's "Wanted" and triage 6137228499 direct: the package page offers the install-mode choice only where the manifest's installModes permits more than one mode, sends installMode on the install, shows strippedLocks after an unmanaged copy, and shows each refusal's own message; ⛔ the console invents no mode rule of its own)
Session: session_01RZYixBnXPWvcm2YenWo6eJ
Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12288-marketplace-install-mode
Worktree: objectui-issue-12288
Domain: domain:ui
Seat: domain:ui#2
File surface, on objectui mainf0532d8:
The page:packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx, the install-local action (installLocal({ packageId }) at :524) and the page's install controls.
The client:packages/app-shell/src/console/marketplace/marketplaceApi.ts, installLocal (:742): its input takes installMode, and its answer surfaces strippedLocks and the refusal codes.
Strings:packages/i18n/src/locales/*.ts, all ten packs, for the choice, the stripped-locks notice and any refusal the door's own message does not already carry.
Co-located tests, and .changeset/12288-marketplace-install-mode.md (@object-ui/app-shell; @object-ui/i18n if touched; ⛔ never major).
⛔ No server change, no new spec key, no mode rule the manifest and the door do not state.
(Stop on breach; explain in the report.)
Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
Clause-②: yes
Responsibility: n/a — not a defect card
Thread-read: 6137228499
Serial constraints cleared: read at 2026-10-13T05:34Z on objectui mainf0532d8:
The files: no open PR touches packages/app-shell/src/console/marketplace/ (4 open PRs read: objectui#12309 grid saves, two dependabot next bumps, the release PR objectui#11600). No commit has touched that folder on main since 2026-10-12T05:00Z.
Neighbours: seat 1's in-flight objectui#12292, objectui#12300 and objectui#12302 share no file. This seat's objectui#12301 (OAuthConsentPage.tsx) and objectui#12285 (AppContent.tsx, data-objectstack) share none, except possibly the ten locale packs: ordinary concurrency.
The epic: objectui#7611's handoff 6117462399 says the epic seat routes "marketplace install under a wall" after objectstack W2 lands. Nothing of it is in flight, and this card shows the door's UNMANAGED_INSTALL_WALLED_POSTURE refusal as served; it adds no wall rule.
Clause-②: yes covers the new pack keys and the installLocal input that gains installMode. If the dev measures that installLocal is not reachable from @object-ui/app-shell's published entry and no key is added, the report narrows it to no and says why.
os-dev-report
{
"issue": 12288,
"status": "done",
"branch": "claude/issue-12288-marketplace-install-mode",
"pr": "#12317",
"session": "session_01RZYixBnXPWvcm2YenWo6eJ — this run's harness-stamped id (mode:subagent, the PM's own id; identity bit is the branch)",
"premise_still_valid": true,
"summary": "The package page reads the package's installModes from the manifest the install door itself judges (GET /api/v1/marketplace/packages/:id/versions/latest/manifest, nested manifest: or flat, the door's declaredPackageManifest reading) and draws a managed / unmanaged radio choice only when it permits more than one distinct mode. The choice starts unpicked and the install waits for a pick. No key or no readable manifest means no choice and no installMode sent; one declared mode means no choice and that mode is sent (a sole unmanaged package also says before the click that the install is a copy). An unmanaged answer gets its own sentence naming the base and lists each strippedLocks entry (item, lock, the package's reason verbatim) with no install-time suggestions. The three install-mode refusals show the envelope's own message, and a requested copy answered without installMode: 'unmanaged' (a runtime predating the key) says the package was installed managed. Ten locale packs gain eight keys; the changeset bumps @object-ui/app-shell and @object-ui/i18n minor.",
"hypotheses": {
"H1_installModes_in_a_payload_the_page_already_reads": "FALSIFIED as stated. The page read only the catalog detail (GET /api/v1/marketplace/packages/:id, the control plane's sys_package row passed through verbatim by MarketplaceProxyPlugin) and the install-local listing; neither carries installModes in anything measurable here (cloud repo not attached: gh api repos/objectstack-ai/cloud answers 403 'not enabled for this session'; egress to marketplace.objectos.ai / cloud.objectos.ai refused by the container proxy, CONNECT 403). A served payload the page CAN read does carry it: the door fetches GET {cloud}/api/v1/marketplace/packages/:id/versions/:vid/manifest and judges installModes on data.manifest (judgeInstallMode), and the browse proxy forwards every GET under /api/v1/marketplace/ except install-local. The page now reads that path at latest, the version install-local fetches when given no versionId. Route: a new read getDeclaredInstallModes in marketplaceApi.ts (the fenced file, beside installLocal).",
"H2_only_install_local_takes_installMode": "CONFIRMED. POST /api/v1/cloud-connection/install takes { package_id, seed_sample_data } and forwards only { environment_id, seed_sample_data } to install_package (cloud-connection-plugin.ts, objectstack e540e7ffb). The read and the choice exist only where features.installLocal makes install-local the page's door.",
"H3_refusal_envelope_and_strippedLocks_shape": "CONFIRMED. Refusals answer { success: false, error: { code, message, details } } with 422 / 409 / 409; readApiError already surfaces message, and the page's existing default branch shows it, so no code branch was added. strippedLocks entries are { type, name, lock (MetadataLock minus 'none'), reason? } per ImportUnmanagedPackageResponseSchema (PR objectstack-ai/objectstack#23075). The unmanaged door and the lock strip ship in the same pending v18 changesets (15213-install-modes.md, 15213-unmanaged-lock-strip.md), so no released runtime answers unmanaged without the list: typed required, no fallback.",
"H4_unmanaged_only_package": "MEASURED on the door's own test ('refuses managed for a copy-only package — 422, naming unmanaged'): absent installMode is refused INSTALL_MODE_NOT_PERMITTED. The page sends the one mode the manifest declares, which follows from the manifest and the door; no choice is drawn, and a one-line note says the install is an unmanaged copy (reusing the mode label and hint keys).",
"H5_installLocal_reachable_from_published_entry": "NOT reachable by name: src/index.ts exports MarketplacePackagePage (props unchanged) but not marketplaceApi.ts. Clause-② stays yes for the eight new keys in the published @object-ui/i18n packs."
},
"tests": "All heavy runs via os-verify-lock.sh; all at d5441f2 unless stated. (1) Closure build pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: 'VERDICT command-exit 0'. (2) pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/app-shell/src/console/marketplace/ at d5441f2: 'Test Files 115 passed (115) / Tests 1534 passed | 13 skipped (1547)', exit 0. (3) pnpm exec vitest run --maxWorkers=2 packages/app-shell/ packages/i18n/ at 50ec625: 'Test Files 1 failed | 1398 passed | 1 skipped (1400) / Tests 2 failed | 14023 passed | 22 skipped', VERDICT command-exit 1; the failure was de-quote-pairing-3876.test.ts's German quote census (85 -> 87 for the two new matched spans), updated in adf0131 and green in run (2). Delta 50ec625..d5441f2 is that test plus installLocal overload signatures; esbuild output of marketplaceApi.ts is identical at 50ec625 and d5441f2 (sha256 prefix 55c8987bb164 both; control: BASE 99865b9eb3fc differs), so (3) stands for HEAD's runtime code. (4) pnpm --filter @object-ui/app-shell type-check (echoed tsc --noEmit && tsc -p tsconfig.test.json): exit 0 at d5441f2, 0 'error TS'; it was exit 2 at adf0131 (TS2339 hotLoaded on the union, MarketplacePage.tsx's org-package install) and was fixed in marketplaceApi.ts by an overload, no MarketplacePage.tsx edit. tsc -p tsconfig.test.json --listFiles lists the new test file (1 hit; control sibling 1 hit). (5) pnpm --filter @object-ui/i18n type-check: exit 0. (6) check:i18n-keys, check:i18n-drift ('0 en value(s) changed (8 key(s) added, 0 removed'), check:i18n-dead-keys (0 of the 8 keys among 336 candidates), check:control-bytes ('OK (scanned 8592 tracked text file(s)'), check:new-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), node scripts/check-changeset-presence.mjs: each exit 0 at d5441f2. (7) ESLint, declared narrowing: 14 changed .ts/.tsx files (count from --format json), 0 errors, 47 warnings = BASE per file (11 / 36, measured by linting the BASE blobs via --stdin-filename); population = eslint.config.js **/*.{ts,tsx} block; invariance = config not type-aware (tseslint.configs.recommended, no parserOptions.project / projectService) and no eslint-rules/*.js reads the filesystem (grep exit 1; control pattern hits scripts/). Repo-wide pnpm lint declared to CI. ABLATION at 50ec625 via objectstack scripts/ablation-replace.mjs (wrap mode, anchors x1 -> x0, replacement x0 -> x1 each), new test file 17 tests, directions declared before the run: A1 no installMode sent -> 6 failed | 11 passed; A2 choice never offered -> 9 failed | 8 passed; A3 unmanaged answer not read -> 2 failed | 15 passed; A4 nested manifest not read -> 1 failed | 16 passed. Each restore 'blob == HEAD and git diff HEAD is empty', plus a driver trap and a final per-path HEAD-blob check. Subject imported from src by relative path, so no build sat between mutation and run.",
"gates": [
{ "command": "pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build", "verdict": "os-verify-lock: VERDICT command-exit 0", "exit": 0, "sha": "f0532d8 tree + pack edits (pre-commit)" },
{ "command": "pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/app-shell/src/console/marketplace/", "verdict": "Test Files 115 passed (115)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/ packages/i18n/", "verdict": "Test Files 1 failed | 1398 passed | 1 skipped (1400)", "exit": 1, "sha": "50ec625 (failure fixed in adf0131, re-run green at d5441f2 in the row above)" },
{ "command": "pnpm --filter @object-ui/app-shell type-check", "verdict": "TC_APPSHELL_EXIT=0, 0 error TS", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm --filter @object-ui/i18n type-check", "verdict": "TC_I18N_EXIT=0", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:i18n-keys", "verdict": "Every in-scope call-site key resolves against the en pack (3367 keys)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:i18n-drift", "verdict": "0 en value(s) changed (8 key(s) added, 0 removed", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:i18n-dead-keys", "verdict": "report-only; 0 of the 8 new keys among the 336 candidates", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm exec vitest run packages/i18n/", "verdict": "inside the 115-file run above", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:control-bytes", "verdict": "check-control-bytes: OK (scanned 8592 tracked text file(s); skipped 85 binary)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:new-line-citations", "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0", "exit": 0, "sha": "d5441f2" },
{ "command": "node scripts/check-changeset-presence.mjs", "verdict": "13 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm exec eslint --format json CHANGED_FILES (14)", "verdict": "0 errors, 47 warnings, equal to BASE per file", "exit": 0, "sha": "d5441f2" }
],
"line_budget": "n/a — no skills/** in the diff",
"files_changed": [
".changeset/12288-marketplace-install-mode.md",
"packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx",
"packages/app-shell/src/console/marketplace/marketplaceApi.ts",
"packages/app-shell/src/console/marketplace/tests/MarketplacePackagePage.installMode-12288.test.tsx",
"packages/i18n/src/tests/de-quote-pairing-3876.test.ts",
"packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts"
],
"clause_2": "yes — eight new keys in the published @object-ui/i18n packs; installLocal / getDeclaredInstallModes / the new types are not exported from the @object-ui/app-shell entry; MarketplacePackagePage keeps its props",
"deviations": [
"Fence: marketplaceApi.ts gains a second function, getDeclaredInstallModes, beside installLocal (H1 falsified; same file). packages/i18n/src/tests/de-quote-pairing-3876.test.ts is edited as the locale packs' co-located census test (85 -> 87, the established per-PR update). MarketplacePage.tsx is NOT edited: the type break the union caused there is fixed by installLocal overloads in marketplaceApi.ts.",
"Narrowing: the full app-shell + i18n suite ran at 50ec625, not re-run at d5441f2; the delta is one i18n test (re-run green) and type-only overloads (esbuild output identical, proven with a control). ESLint narrowed to the changed files with the three-part proof; repo-wide lint declared to CI.",
"Lock: one acquisition returned exit 99 (queue-timeout, holder objectui#12301's run) and was re-queued under the same slot; my full-suite run then held the lock 52m22s (shared box, 1400 files at 2 workers), during which sibling devs queued.",
"Attribution: commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body ends with the session-URL footer, per os-dev.md and objectui AGENTS.md over the harness reminder's model-named trailer and emoji footer line.",
"The PR body and the relay's actions file were written to this run's issue-12288 scratchpad subdirectory under 12288-prefixed names (the relay takes a file), and read back byte-identical from the platform (10801 bytes)."
],
"mcp_calls": "0 — no MCP tool called",
"api_writes": "2 — (1) fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches carrying one pr_create, executed as POST /repos/objectstack-ai/objectui/pulls (draft, objectui#12317) + POST /repos//issues/12317/assignees ['os-support-ai'], read back identical; (2) post-stamped: POST /repos//issues/12288/comments (this report). Zero label writes (the PR's three path labels are labeler.yml's). git push x4 (empty branch probe, 9b10bb8, adf0131 carrying 50ec625, d5441f2) — not REST.",
"open_questions": [
{
"question": "The page now downloads the package's full latest manifest (proxied, cached 2-24h by the runtime's marketplace proxy) on every admin view of a package page on an install-local runtime, only to read installModes. Should the control plane project the key onto the catalog detail instead?",
"options": [
"A Keep the manifest read: it is the exact payload and version the door judges, so the page and the door cannot disagree; cost is one manifest transfer per page view.",
"B Control plane serves installModes on the detail / version row (a cloud card): cheaper, but a second copy of the key the door does not read, which can drift from the manifest it judges."
],
"recommendation": "A, because the card's rule is 'what is permitted is the manifest's installModes and the door's answer', and A reads exactly that; revisit only if the transfer cost is measured to matter."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed · no objectui guide describes the package page's install flow (content/docs/guide/console.md names the marketplace route only), so AGENTS.md #2 had no page to update; the install-modes authoring page is objectstack's content/docs/deployment/install-modes.mdx",
"carrier: none · noted, not filed · an unmanaged copy writes no ledger entry under the package's id, so the install-local listing does not list it and the page keeps offering Install; a later managed pick is refused RESOURCE_CONFLICT naming the base. Observed only in the test harness against a fake door (no public-door measurement), so no reach",
"carrier: none · noted, not filed · the unmanaged answer's seeded: { mode: 'skipped' } is not shown on the page",
"carrier: the PR that moves objectui to @objectstack 18 · noted, not filed · LocalInstallMode and LocalInstallStrippedLock mirror InstallModeSchema / ImportUnmanagedPackageResponseSchema, which @objectstack/spec 17.7.0 does not export",
"carrier: none · noted, not filed · marketplaceApi.ts's local-install block header points at the install door's former framework path; the door lives in objectstack packages/cloud-connection/src/"
]
}
os-dev-report
{
"issue": 12288,
"round": "patch round 1",
"status": "done",
"branch": "claude/issue-12288-marketplace-install-mode",
"pr": "#12317",
"head": "6618b25",
"session": "session_01RZYixBnXPWvcm2YenWo6eJ — this run's harness-stamped id (mode:subagent, the PM's own id; identity bit is the branch)",
"premise_still_valid": true,
"summary": "CI red root-caused and fixed: check:spec-symbols (Type Check step 19) refused the local helper isPlainRecord in marketplaceApi.ts because @objectstack/spec/data exports a function of that name with the same semantics (!!v && typeof v === 'object' && !Array.isArray(v), typed as a type guard to a record of string keys and unknown values). It is now imported from the spec (app-shell already value-imports @objectstack/spec/data; the package is sideEffects: false). The seat's hypothesis (rule 2 reading the comments on LocalInstallMode / LocalInstallStrippedLock) is FALSIFIED: the gate's only finding was rule 1 on isPlainRecord, and after the import all four rule families pass with those comments in place (none of the new symbols is in the claim ledger). The two comments were still reworded to describe the install door's answer without citing spec symbols, per the seat's direction. Review item: while the install-mode choice is unpicked, the disabled Install now carries its reason the console's way (DeclaredActionsBar's shape): a focusable wrapper span that opens a tooltip on hover and focus, and aria-describedby on both the button and the wrapper onto an sr-only copy of the new key marketplace.install.mode.required (ten packs). No mode is pre-selected. The branch was merged with origin/main 068a540 first (eae1153).",
"step19_reproduced": {
"command": "pnpm check:spec-symbols (node scripts/check-spec-symbol-derivation.mjs), on d5441f2 merged with origin/main 068a540",
"exit": 1,
"failure_lines": [
"❌ a spec-named symbol is hand-written, not derived:",
"• @object-ui/app-shell declares 1 spec-named symbol the spec already owns:",
"function isPlainRecord packages/app-shell/src/console/marketplace/marketplaceApi.ts:LINE (exported by @objectstack/spec/data)",
"(the gate printed the line number it computed on that tree; elided here per objectui AGENTS.md #11, since a card has no same file)"
],
"passing_rerun_at_6618b25": [
"✅ spec symbol derivation: 1520 files scanned against 5140 spec export names; 22 declared dialects, 0 untriaged collisions in 0 packages.",
"✅ spec alignment claims: 2 declared deliberate copies, 12 unbacked claims in 5 packages.",
"✅ spec member citations: 1520 sources + 184 documentation pages; nothing cites a key its spec symbol does not declare.",
"exit 0; --claim-ledger lists the 12 unbacked claims (types 7, core 2, app-shell 1 RecordLookupBinding, plugin-view 1, react 1): none is this branch's"
]
},
"battery_type_check_job_order": [
"all at 6618b25, clean tree; steps numbered as ci.yml's type-check job lists them (GitHub's numbering is +1: it counts 'Set up job')",
{ "step": 6, "command": "node scripts/check-type-check-coverage.mjs", "exit": 0 },
{ "step": 8, "command": "pnpm check:phantom-deps", "exit": 0 },
{ "step": 9, "command": "pnpm check:self-import", "exit": 0 },
{ "step": 10, "command": "pnpm check:unreferenced-sources", "exit": 0 },
{ "step": 11, "command": "pnpm check:doc-example-readers", "exit": 0 },
{ "step": 12, "command": "pnpm check:handler-key-reads", "exit": 0 },
{ "step": 13, "command": "pnpm check:metadata-write-doors", "exit": 0 },
{ "step": 14, "command": "pnpm check:published-tsconfig-exclude", "exit": 0 },
{ "step": 15, "command": "pnpm check:side-effects-array", "exit": 0 },
{ "step": 16, "command": "pnpm check:element-data-source-declaration", "exit": 0 },
{ "step": 17, "command": "pnpm check:esm-specifiers", "exit": 0, "note": "--specifiers-only reads source, says so; not a dist reading" },
{ "step": 18, "command": "pnpm check:spec-symbols", "exit": 0, "note": "GitHub's step 19; red at d5441f2" },
{ "step": 19, "command": "pnpm check:action-forward-parity", "exit": 0 },
{ "step": 20, "command": "pnpm check:designer-field-key-parity", "exit": 0 },
{ "step": 21, "command": "pnpm check:icon-record-names", "exit": 0 },
{ "step": 22, "command": "pnpm check:i18n-keys", "exit": 0 },
{ "step": 23, "command": "pnpm check:i18n-drift", "exit": 0, "verdict": "0 en value(s) changed (9 key(s) added, 0 removed" },
{ "step": 24, "command": "pnpm check:i18n-designer-parity", "exit": 0 },
{ "step": 25, "command": "pnpm type-check:scripts", "exit": 0, "errors": 0 },
{ "step": 26, "command": "pnpm type-check:vitest-config", "exit": 0, "errors": 0 },
{ "step": 30, "command": "root builds (select-type-check-scope --root-builds: i18n, plugin-dashboard, plugin-grid) with the console closure: pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' --filter @object-ui/i18n --filter @object-ui/plugin-dashboard --filter @object-ui/plugin-grid build", "exit": 0 },
{ "step": 29, "command": "pnpm --filter @object-ui/app-shell type-check (echoed: tsc --noEmit && tsc -p tsconfig.test.json)", "exit": 0, "errors": 0 },
{ "step": 29, "command": "pnpm --filter @object-ui/i18n type-check (echoed: tsc --noEmit && tsc -p tsconfig.test.json)", "exit": 0, "errors": 0 },
{ "step": 29, "command": "pnpm --filter @object-ui/console type-check (echoed: tsc --noEmit && tsc -b tsconfig.node.json --force)", "exit": 0, "errors": 0 },
{ "step": 31, "command": "pnpm type-check:vitest-setup", "exit": 0, "errors": 0 },
{ "step": 29, "command": "the other 32 packages of CI's affected scope (select-type-check-scope --base 068a540: 35 packages, reached through @object-ui/i18n)", "result": "NOT MEASURED locally, reason: their only input from this branch is nine added keys in the i18n packs (a widening of an object literal) and app-shell's unexported marketplace module; CI's Type Check runs them" }
],
"tests": "Through os-verify-lock (VERDICT command-exit 0, held 521s): pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/app-shell/src/console/marketplace/ plus the three layout tests that reference marketplaceApi (AiUsageIndicator.aiOff-11799, AiUsageIndicator, StorageUsageBanner) at 6618b25: 'Test Files 118 passed (118) / Tests 1569 passed | 13 skipped (1582)', exit 0. The new test file is 18/18, including the new pin: with the choice unpicked, Install is disabled, toHaveAccessibleDescription('«marketplace.install.mode.required»'), user-event Tab reaches the [data-disabled-reason] wrapper that contains the button and carries the same description; CONTROL after picking managed: enabled, no description, no wrapper. ABLATION (objectstack ablation-replace.mjs, wrap mode, anchor x1 -> x0, replacement x0 -> x1; expected 1 red, declared before the run): no wrapper -> 'Tests 1 failed | 17 passed (18)', the failing case exactly the disabled-reason pin; restore 'blob == HEAD (e8d061329e76) and git diff HEAD is empty', plus the driver's own per-path HEAD-blob check. Lock-free at 6618b25: check:i18n-dead-keys exit 0 (none of the nine keys a candidate), check:control-bytes exit 0 ('OK (scanned 8604 tracked text file(s)'), check:new-line-citations exit 0 ('0 new citation(s)'), node scripts/check-changeset-presence.mjs exit 0. ESLint, declared narrowing to the 14 changed .ts/.tsx files: 0 errors, 47 warnings, equal per file to BASE (11 / 36, same rules), invariance as in round 0 (config not type-aware, no eslint-rules/*.js reads the filesystem).",
"check_runs": "d5441f2: total_count 33 = returned 33; 29 success, 3 skipped, 1 failure (Type Check, job 115181953992); no other red. 6618b25 at report time: 32 runs, 23 in_progress (Type Check among them), 6 success, 3 skipped — in_progress is the honest value; not waited on.",
"files_changed_this_round": [
"packages/app-shell/src/console/marketplace/marketplaceApi.ts (isPlainRecord imported from @objectstack/spec/data; two type comments reworded)",
"packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx (disabled Install's reason: wrapper, tooltip, aria-describedby)",
"packages/app-shell/src/console/marketplace/tests/MarketplacePackagePage.installMode-12288.test.tsx (the disabled-reason pin; the no-installModes case asserts no wrapper)",
"packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts (marketplace.install.mode.required)",
".changeset/12288-marketplace-install-mode.md (nine keys; the disabled Install's reason)"
],
"commits_this_round": ["eae1153 merge origin/main 068a540", "6618b25 the two fixes"],
"pr_body_sentences_to_change": [
"'What changed', marketplaceApi.ts bullet: 'LocalInstallMode and LocalInstallStrippedLock mirror spec members (InstallModeSchema, the strippedLocks entry) that the @objectstack/spec 17.7.0 objectui resolves does not export yet.' -> 'LocalInstallMode and LocalInstallStrippedLock are the console's reading of the install door's installMode and strippedLocks; the @objectstack/spec 17.7.0 objectui resolves declares neither (only lock is typed from the spec's MetadataLock). isPlainRecord is imported from @objectstack/spec/data.'",
"'What changed', page bullet 1: after 'The install button waits for a pick.' add 'While it waits, the disabled Install says why (marketplace.install.mode.required): a focusable wrapper opens the reason on hover and focus, and it is the button's accessible description.'",
"The packs bullet and the 'Clause-② yes' paragraph: 'eight keys' -> 'nine keys', adding mode.required to the list.",
"'Verification': the readings are d5441f2's; this round's are in this report (head 6618b25)."
],
"deviations": [
"The seat's step-19 hypothesis was falsified (rule 1 on isPlainRecord, not rule 2 on the type comments); the comments were reworded anyway, as directed.",
"Type-check scope narrowed to app-shell, i18n and console plus the three repo-root programs; the other 32 packages of CI's affected scope are NOT MEASURED locally (reason above).",
"The branch was merged with origin/main 068a540 before the fixes (eae1153), per os-dev rule 1 for a resumed branch."
],
"mcp_calls": "0 — no MCP tool called",
"api_writes": "1 — post-stamped: POST /repos//issues/12288/comments (this report). Reads only otherwise (GET check-runs x2, GET branch via git fetch). No PR body PATCH, no label write. git push x1 (d5441f2..6618b25, fast-forward, no force) — not REST.",
"open_questions": [],
"out_of_scope_findings": []
}
Reviewed against GitHub and the diff, not the reports. Inputs: claim 6139065390, reports 6140651420 (round 0) and 6141117364 (patch round 1).
Shape:
Draft on main. The first line is Fixes #12288, and no other card number sits beside a closing keyword.
Clause-②: yes is line-initial.
15 files, 917 changed lines. check-governed-merges --pr reads NOT governed.
The surface is the claim's: the page, marketplaceApi.ts, the ten packs, co-located tests, and the packs' own de-quote-pairing-3876 census test (85 → 87).
The fix, read against the diff and objectstack main:
The page reads installModes from GET /api/v1/marketplace/packages/:id/versions/latest/manifest. With no versionId, the install door fetches that same path (cloud, after a public-R2 copy of the same object), and the browse proxy forwards that GET.
The page reads the key where the door reads it: a nested manifest: or a flat body.
A choice is drawn only when the manifest permits more than one mode, and it starts unpicked. One declared mode is sent as declared. An absent or unreadable key sends nothing, so the door's default stands. ⛔ The page invents no mode rule.
strippedLocks are listed with the package's reasons verbatim, and the three refusals show the envelope's own message.
While the choice is unpicked, the disabled Install carries its reason the console's way: a focusable wrapper, a tooltip on hover and focus, and aria-describedby to marketplace.install.mode.required. That is pinned, including Tab.
A correction to the seat: I attributed the round-0 Type Check red (check:spec-symbols) to a spec-alignment claim in the new types' comments. The dev reproduced the gate and proved that wrong. The gate's finding was rule 1, a hand-written isPlainRecord that @objectstack/spec/data already exports, and it is now imported from there. The gate list in the dispatch also omitted the Type Check job's pre-tsc battery. That was the seat's omission, not the dev's.
The dev's open question (read the full manifest per view, or have the control plane project installModes): answered A, the manifest read. It is the one payload the door judges, and the runtime proxy caches versions/*.
Contract review: PASS 6141307076 on head 6618b25. Its non-blocking flags are below.
CI on 6618b25: 33 checks: 30 success, 3 skipped, none red. Type Check, Lint and Repo Gates are success on this head.
MarketplacePage.tsx's org-catalog install sends no mode. A copy-only org package installed from there gets the door's INSTALL_MODE_NOT_PERMITTED. That is the door's answer, outside this card's package-page scope. carrier: none.
The unmanaged answer's seeded: { mode: 'skipped' } is not shown on the page; the CLI prints it. carrier: none.
LocalInstallMode / LocalInstallStrippedLock are to be replaced by the spec's types once objectui resolves @objectstack 18. carrier: the PR that moves objectui to 18.
Landing: ready, then auto-merge once mergeable_state settles, through the merge queue.
The merge: through the merge queue (added_to_merge_queue 2026-10-13T08:18Z, merged 2026-10-13T08:31Z). Fixes #12288 closed this card completed.
Landed by content, not by sha:
All 15 files of the PR on maincea429b are blob-identical to the accepted head 6618b25. That head is covered by ACCEPT 6141326960 and contract review PASS 6141307076.
marketplaceApi.ts on main carries getDeclaredInstallModes, and it imports isPlainRecord from @objectstack/spec/data.
Publishing: nothing deploys on merge. The change ships with the maintainer's next chore: release packages, as @object-ui/app-shell and @object-ui/i18nminor. objectstack's console sees it at its next .objectui-sha pin move.
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
enhancement·priority:p2·domain:ui·area:devpath, inpm:queue. It is objectstack #15213's wall, so it takes #15213's level. It does not wait on 18.0Triage seat (seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T02:59Z. ⛔ Not a claim, ⛔ not a dispatch.Unblocks: objectstack-ai/objectstack#15213
filing-gate.md:42). feat(spec,objectql,cli): the template install mode — a package copied once into the environment ledger, fully editable, refused on shared-database multi-tenant postures (ADR-0131 D6) objectstack#15213 is p2, and this is its last stage (S4).installModein the install body and reads the door's answer. It needs no@objectstack/spectype from v18.installModespermits more than one mode. A server that does not serveinstallModesreads['managed'], so it shows no choice and degrades on its own.installModesshows no choice.installModesent on install;strippedLocksshown after an unmanaged copy;INSTALL_MODE_NOT_PERMITTED,UNMANAGED_INSTALL_WALLED_POSTURE,RESOURCE_CONFLICT).packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx(domain:ui); rationale: the door decides, and the page asks it.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 (as the card's "Wanted" and triage
6137228499direct: the package page offers the install-mode choice only where the manifest'sinstallModespermits more than one mode, sendsinstallModeon the install, showsstrippedLocksafter an unmanaged copy, and shows each refusal's own message; ⛔ the console invents no mode rule of its own)Session:
session_01RZYixBnXPWvcm2YenWo6eJAccount:
os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12288-marketplace-install-modeWorktree:
objectui-issue-12288Domain:
domain:uiSeat:
domain:ui#2File surface, on objectui
mainf0532d8:packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx, the install-local action (installLocal({ packageId })at:524) and the page's install controls.packages/app-shell/src/console/marketplace/marketplaceApi.ts,installLocal(:742): its input takesinstallMode, and its answer surfacesstrippedLocksand the refusal codes.packages/i18n/src/locales/*.ts, all ten packs, for the choice, the stripped-locks notice and any refusal the door's own message does not already carry..changeset/12288-marketplace-install-mode.md(@object-ui/app-shell;@object-ui/i18nif touched; ⛔ nevermajor).Container & model:
M,mode:subagent,model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"Clause-②: yes
Responsibility:
n/a — not a defect cardThread-read: 6137228499
Serial constraints cleared: read at 2026-10-13T05:34Z on objectui
mainf0532d8:packages/app-shell/src/console/marketplace/(4 open PRs read: objectui#12309 grid saves, two dependabotnextbumps, the release PR objectui#11600). No commit has touched that folder onmainsince 2026-10-12T05:00Z.OAuthConsentPage.tsx) and objectui#12285 (AppContent.tsx,data-objectstack) share none, except possibly the ten locale packs: ordinary concurrency.6117462399says the epic seat routes "marketplace install under a wall" after objectstack W2 lands. Nothing of it is in flight, and this card shows the door'sUNMANAGED_INSTALL_WALLED_POSTURErefusal as served; it adds no wall rule.Clause-②: yescovers the new pack keys and theinstallLocalinput that gainsinstallMode. If the dev measures thatinstallLocalis not reachable from@object-ui/app-shell's published entry and no key is added, the report narrows it tonoand says why.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 12288,
"status": "done",
"branch": "claude/issue-12288-marketplace-install-mode",
"pr": "#12317",
"session": "session_01RZYixBnXPWvcm2YenWo6eJ — this run's harness-stamped id (mode:subagent, the PM's own id; identity bit is the branch)",
"premise_still_valid": true,
"summary": "The package page reads the package's
installModesfrom the manifest the install door itself judges (GET /api/v1/marketplace/packages/:id/versions/latest/manifest, nestedmanifest:or flat, the door'sdeclaredPackageManifestreading) and draws a managed / unmanaged radio choice only when it permits more than one distinct mode. The choice starts unpicked and the install waits for a pick. No key or no readable manifest means no choice and noinstallModesent; one declared mode means no choice and that mode is sent (a soleunmanagedpackage also says before the click that the install is a copy). Anunmanagedanswer gets its own sentence naming the base and lists eachstrippedLocksentry (item, lock, the package's reason verbatim) with no install-time suggestions. The three install-mode refusals show the envelope's own message, and a requested copy answered withoutinstallMode: 'unmanaged'(a runtime predating the key) says the package was installed managed. Ten locale packs gain eight keys; the changeset bumps @object-ui/app-shell and @object-ui/i18n minor.","hypotheses": {
"H1_installModes_in_a_payload_the_page_already_reads": "FALSIFIED as stated. The page read only the catalog detail (GET /api/v1/marketplace/packages/:id, the control plane's sys_package row passed through verbatim by MarketplaceProxyPlugin) and the install-local listing; neither carries
installModesin anything measurable here (cloud repo not attached:gh api repos/objectstack-ai/cloudanswers 403 'not enabled for this session'; egress to marketplace.objectos.ai / cloud.objectos.ai refused by the container proxy, CONNECT 403). A served payload the page CAN read does carry it: the door fetches GET {cloud}/api/v1/marketplace/packages/:id/versions/:vid/manifest and judgesinstallModesondata.manifest(judgeInstallMode), and the browse proxy forwards every GET under /api/v1/marketplace/ except install-local. The page now reads that path atlatest, the version install-local fetches when given no versionId. Route: a new readgetDeclaredInstallModesin marketplaceApi.ts (the fenced file, besideinstallLocal).","H2_only_install_local_takes_installMode": "CONFIRMED. POST /api/v1/cloud-connection/install takes { package_id, seed_sample_data } and forwards only { environment_id, seed_sample_data } to install_package (cloud-connection-plugin.ts, objectstack e540e7ffb). The read and the choice exist only where features.installLocal makes install-local the page's door.",
"H3_refusal_envelope_and_strippedLocks_shape": "CONFIRMED. Refusals answer { success: false, error: { code, message, details } } with 422 / 409 / 409; readApiError already surfaces
message, and the page's existing default branch shows it, so no code branch was added. strippedLocks entries are { type, name, lock (MetadataLock minus 'none'), reason? } per ImportUnmanagedPackageResponseSchema (PR objectstack-ai/objectstack#23075). The unmanaged door and the lock strip ship in the same pending v18 changesets (15213-install-modes.md, 15213-unmanaged-lock-strip.md), so no released runtime answers unmanaged without the list: typed required, no fallback.","H4_unmanaged_only_package": "MEASURED on the door's own test ('refuses
managedfor a copy-only package — 422, namingunmanaged'): absent installMode is refused INSTALL_MODE_NOT_PERMITTED. The page sends the one mode the manifest declares, which follows from the manifest and the door; no choice is drawn, and a one-line note says the install is an unmanaged copy (reusing the mode label and hint keys).","H5_installLocal_reachable_from_published_entry": "NOT reachable by name: src/index.ts exports MarketplacePackagePage (props unchanged) but not marketplaceApi.ts. Clause-② stays
yesfor the eight new keys in the published @object-ui/i18n packs."},
"tests": "All heavy runs via os-verify-lock.sh; all at d5441f2 unless stated. (1) Closure build
pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: 'VERDICT command-exit 0'. (2)pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/app-shell/src/console/marketplace/at d5441f2: 'Test Files 115 passed (115) / Tests 1534 passed | 13 skipped (1547)', exit 0. (3)pnpm exec vitest run --maxWorkers=2 packages/app-shell/ packages/i18n/at 50ec625: 'Test Files 1 failed | 1398 passed | 1 skipped (1400) / Tests 2 failed | 14023 passed | 22 skipped', VERDICT command-exit 1; the failure was de-quote-pairing-3876.test.ts's German quote census (85 -> 87 for the two new matched spans), updated in adf0131 and green in run (2). Delta 50ec625..d5441f2 is that test plus installLocal overload signatures; esbuild output of marketplaceApi.ts is identical at 50ec625 and d5441f2 (sha256 prefix 55c8987bb164 both; control: BASE 99865b9eb3fc differs), so (3) stands for HEAD's runtime code. (4)pnpm --filter @object-ui/app-shell type-check(echoedtsc --noEmit && tsc -p tsconfig.test.json): exit 0 at d5441f2, 0 'error TS'; it was exit 2 at adf0131 (TS2339hotLoadedon the union, MarketplacePage.tsx's org-package install) and was fixed in marketplaceApi.ts by an overload, no MarketplacePage.tsx edit.tsc -p tsconfig.test.json --listFileslists the new test file (1 hit; control sibling 1 hit). (5)pnpm --filter @object-ui/i18n type-check: exit 0. (6) check:i18n-keys, check:i18n-drift ('0 en value(s) changed (8 key(s) added, 0 removed'), check:i18n-dead-keys (0 of the 8 keys among 336 candidates), check:control-bytes ('OK (scanned 8592 tracked text file(s)'), check:new-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), node scripts/check-changeset-presence.mjs: each exit 0 at d5441f2. (7) ESLint, declared narrowing: 14 changed .ts/.tsx files (count from --format json), 0 errors, 47 warnings = BASE per file (11 / 36, measured by linting the BASE blobs via --stdin-filename); population = eslint.config.js**/*.{ts,tsx}block; invariance = config not type-aware (tseslint.configs.recommended, no parserOptions.project / projectService) and no eslint-rules/*.js reads the filesystem (grep exit 1; control pattern hits scripts/). Repo-widepnpm lintdeclared to CI. ABLATION at 50ec625 via objectstack scripts/ablation-replace.mjs (wrap mode, anchors x1 -> x0, replacement x0 -> x1 each), new test file 17 tests, directions declared before the run: A1 no installMode sent -> 6 failed | 11 passed; A2 choice never offered -> 9 failed | 8 passed; A3 unmanaged answer not read -> 2 failed | 15 passed; A4 nested manifest not read -> 1 failed | 16 passed. Each restore 'blob == HEAD and git diff HEAD is empty', plus a driver trap and a final per-path HEAD-blob check. Subject imported from src by relative path, so no build sat between mutation and run.","gates": [
{ "command": "pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build", "verdict": "os-verify-lock: VERDICT command-exit 0", "exit": 0, "sha": "f0532d8 tree + pack edits (pre-commit)" },
{ "command": "pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/app-shell/src/console/marketplace/", "verdict": "Test Files 115 passed (115)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/ packages/i18n/", "verdict": "Test Files 1 failed | 1398 passed | 1 skipped (1400)", "exit": 1, "sha": "50ec625 (failure fixed in adf0131, re-run green at d5441f2 in the row above)" },
{ "command": "pnpm --filter @object-ui/app-shell type-check", "verdict": "TC_APPSHELL_EXIT=0, 0 error TS", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm --filter @object-ui/i18n type-check", "verdict": "TC_I18N_EXIT=0", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:i18n-keys", "verdict": "Every in-scope call-site key resolves against the en pack (3367 keys)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:i18n-drift", "verdict": "0 en value(s) changed (8 key(s) added, 0 removed", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:i18n-dead-keys", "verdict": "report-only; 0 of the 8 new keys among the 336 candidates", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm exec vitest run packages/i18n/", "verdict": "inside the 115-file run above", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:control-bytes", "verdict": "check-control-bytes: OK (scanned 8592 tracked text file(s); skipped 85 binary)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm check:new-line-citations", "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0", "exit": 0, "sha": "d5441f2" },
{ "command": "node scripts/check-changeset-presence.mjs", "verdict": "13 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)", "exit": 0, "sha": "d5441f2" },
{ "command": "pnpm exec eslint --format json CHANGED_FILES (14)", "verdict": "0 errors, 47 warnings, equal to BASE per file", "exit": 0, "sha": "d5441f2" }
],
"line_budget": "n/a — no skills/** in the diff",
"files_changed": [
".changeset/12288-marketplace-install-mode.md",
"packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx",
"packages/app-shell/src/console/marketplace/marketplaceApi.ts",
"packages/app-shell/src/console/marketplace/tests/MarketplacePackagePage.installMode-12288.test.tsx",
"packages/i18n/src/tests/de-quote-pairing-3876.test.ts",
"packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts"
],
"clause_2": "yes — eight new keys in the published @object-ui/i18n packs; installLocal / getDeclaredInstallModes / the new types are not exported from the @object-ui/app-shell entry; MarketplacePackagePage keeps its props",
"deviations": [
"Fence: marketplaceApi.ts gains a second function, getDeclaredInstallModes, beside installLocal (H1 falsified; same file). packages/i18n/src/tests/de-quote-pairing-3876.test.ts is edited as the locale packs' co-located census test (85 -> 87, the established per-PR update). MarketplacePage.tsx is NOT edited: the type break the union caused there is fixed by installLocal overloads in marketplaceApi.ts.",
"Narrowing: the full app-shell + i18n suite ran at 50ec625, not re-run at d5441f2; the delta is one i18n test (re-run green) and type-only overloads (esbuild output identical, proven with a control). ESLint narrowed to the changed files with the three-part proof; repo-wide lint declared to CI.",
"Lock: one acquisition returned exit 99 (queue-timeout, holder objectui#12301's run) and was re-queued under the same slot; my full-suite run then held the lock 52m22s (shared box, 1400 files at 2 workers), during which sibling devs queued.",
"Attribution: commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body ends with the session-URL footer, per os-dev.md and objectui AGENTS.md over the harness reminder's model-named trailer and emoji footer line.",
"The PR body and the relay's actions file were written to this run's issue-12288 scratchpad subdirectory under 12288-prefixed names (the relay takes a file), and read back byte-identical from the platform (10801 bytes)."
],
"mcp_calls": "0 — no MCP tool called",
"api_writes": "2 — (1) fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches carrying one pr_create, executed as POST /repos/objectstack-ai/objectui/pulls (draft, objectui#12317) + POST /repos//issues/12317/assignees ['os-support-ai'], read back identical; (2) post-stamped: POST /repos//issues/12288/comments (this report). Zero label writes (the PR's three path labels are labeler.yml's). git push x4 (empty branch probe, 9b10bb8, adf0131 carrying 50ec625, d5441f2) — not REST.",
"open_questions": [
{
"question": "The page now downloads the package's full latest manifest (proxied, cached 2-24h by the runtime's marketplace proxy) on every admin view of a package page on an install-local runtime, only to read
installModes. Should the control plane project the key onto the catalog detail instead?","options": [
"A Keep the manifest read: it is the exact payload and version the door judges, so the page and the door cannot disagree; cost is one manifest transfer per page view.",
"B Control plane serves
installModeson the detail / version row (a cloud card): cheaper, but a second copy of the key the door does not read, which can drift from the manifest it judges."],
"recommendation": "A, because the card's rule is 'what is permitted is the manifest's installModes and the door's answer', and A reads exactly that; revisit only if the transfer cost is measured to matter."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed · no objectui guide describes the package page's install flow (content/docs/guide/console.md names the marketplace route only), so AGENTS.md #2 had no page to update; the install-modes authoring page is objectstack's content/docs/deployment/install-modes.mdx",
"carrier: none · noted, not filed · an unmanaged copy writes no ledger entry under the package's id, so the install-local listing does not list it and the page keeps offering Install; a later managed pick is refused RESOURCE_CONFLICT naming the base. Observed only in the test harness against a fake door (no public-door measurement), so no reach",
"carrier: none · noted, not filed · the unmanaged answer's
seeded: { mode: 'skipped' }is not shown on the page","carrier: the PR that moves objectui to @objectstack 18 · noted, not filed · LocalInstallMode and LocalInstallStrippedLock mirror InstallModeSchema / ImportUnmanagedPackageResponseSchema, which @objectstack/spec 17.7.0 does not export",
"carrier: none · noted, not filed · marketplaceApi.ts's local-install block header points at the install door's former framework path; the door lives in objectstack packages/cloud-connection/src/"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 12288,
"round": "patch round 1",
"status": "done",
"branch": "claude/issue-12288-marketplace-install-mode",
"pr": "#12317",
"head": "6618b25",
"session": "session_01RZYixBnXPWvcm2YenWo6eJ — this run's harness-stamped id (mode:subagent, the PM's own id; identity bit is the branch)",
"premise_still_valid": true,
"summary": "CI red root-caused and fixed:
check:spec-symbols(Type Check step 19) refused the local helperisPlainRecordin marketplaceApi.ts because@objectstack/spec/dataexports a function of that name with the same semantics (!!v && typeof v === 'object' && !Array.isArray(v), typed as a type guard to a record of string keys and unknown values). It is now imported from the spec (app-shell already value-imports@objectstack/spec/data; the package issideEffects: false). The seat's hypothesis (rule 2 reading the comments on LocalInstallMode / LocalInstallStrippedLock) is FALSIFIED: the gate's only finding was rule 1 onisPlainRecord, and after the import all four rule families pass with those comments in place (none of the new symbols is in the claim ledger). The two comments were still reworded to describe the install door's answer without citing spec symbols, per the seat's direction. Review item: while the install-mode choice is unpicked, the disabled Install now carries its reason the console's way (DeclaredActionsBar's shape): a focusable wrapper span that opens a tooltip on hover and focus, andaria-describedbyon both the button and the wrapper onto an sr-only copy of the new keymarketplace.install.mode.required(ten packs). No mode is pre-selected. The branch was merged with origin/main 068a540 first (eae1153).","step19_reproduced": {
"command": "pnpm check:spec-symbols (node scripts/check-spec-symbol-derivation.mjs), on d5441f2 merged with origin/main 068a540",
"exit": 1,
"failure_lines": [
"❌ a spec-named symbol is hand-written, not derived:",
"• @object-ui/app-shell declares 1 spec-named symbol the spec already owns:",
"function
isPlainRecordpackages/app-shell/src/console/marketplace/marketplaceApi.ts:LINE (exported by@objectstack/spec/data)","(the gate printed the line number it computed on that tree; elided here per objectui AGENTS.md #11, since a card has no same file)"
],
"passing_rerun_at_6618b25": [
"✅ spec symbol derivation: 1520 files scanned against 5140 spec export names; 22 declared dialects, 0 untriaged collisions in 0 packages.",
"✅ spec alignment claims: 2 declared deliberate copies, 12 unbacked claims in 5 packages.",
"✅ spec member citations: 1520 sources + 184 documentation pages; nothing cites a key its spec symbol does not declare.",
"exit 0;
--claim-ledgerlists the 12 unbacked claims (types 7, core 2, app-shell 1 RecordLookupBinding, plugin-view 1, react 1): none is this branch's"]
},
"battery_type_check_job_order": [
"all at 6618b25, clean tree; steps numbered as ci.yml's type-check job lists them (GitHub's numbering is +1: it counts 'Set up job')",
{ "step": 6, "command": "node scripts/check-type-check-coverage.mjs", "exit": 0 },
{ "step": 8, "command": "pnpm check:phantom-deps", "exit": 0 },
{ "step": 9, "command": "pnpm check:self-import", "exit": 0 },
{ "step": 10, "command": "pnpm check:unreferenced-sources", "exit": 0 },
{ "step": 11, "command": "pnpm check:doc-example-readers", "exit": 0 },
{ "step": 12, "command": "pnpm check:handler-key-reads", "exit": 0 },
{ "step": 13, "command": "pnpm check:metadata-write-doors", "exit": 0 },
{ "step": 14, "command": "pnpm check:published-tsconfig-exclude", "exit": 0 },
{ "step": 15, "command": "pnpm check:side-effects-array", "exit": 0 },
{ "step": 16, "command": "pnpm check:element-data-source-declaration", "exit": 0 },
{ "step": 17, "command": "pnpm check:esm-specifiers", "exit": 0, "note": "--specifiers-only reads source, says so; not a dist reading" },
{ "step": 18, "command": "pnpm check:spec-symbols", "exit": 0, "note": "GitHub's step 19; red at d5441f2" },
{ "step": 19, "command": "pnpm check:action-forward-parity", "exit": 0 },
{ "step": 20, "command": "pnpm check:designer-field-key-parity", "exit": 0 },
{ "step": 21, "command": "pnpm check:icon-record-names", "exit": 0 },
{ "step": 22, "command": "pnpm check:i18n-keys", "exit": 0 },
{ "step": 23, "command": "pnpm check:i18n-drift", "exit": 0, "verdict": "0 en value(s) changed (9 key(s) added, 0 removed" },
{ "step": 24, "command": "pnpm check:i18n-designer-parity", "exit": 0 },
{ "step": 25, "command": "pnpm type-check:scripts", "exit": 0, "errors": 0 },
{ "step": 26, "command": "pnpm type-check:vitest-config", "exit": 0, "errors": 0 },
{ "step": 30, "command": "root builds (select-type-check-scope --root-builds: i18n, plugin-dashboard, plugin-grid) with the console closure: pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' --filter @object-ui/i18n --filter @object-ui/plugin-dashboard --filter @object-ui/plugin-grid build", "exit": 0 },
{ "step": 29, "command": "pnpm --filter @object-ui/app-shell type-check (echoed: tsc --noEmit && tsc -p tsconfig.test.json)", "exit": 0, "errors": 0 },
{ "step": 29, "command": "pnpm --filter @object-ui/i18n type-check (echoed: tsc --noEmit && tsc -p tsconfig.test.json)", "exit": 0, "errors": 0 },
{ "step": 29, "command": "pnpm --filter @object-ui/console type-check (echoed: tsc --noEmit && tsc -b tsconfig.node.json --force)", "exit": 0, "errors": 0 },
{ "step": 31, "command": "pnpm type-check:vitest-setup", "exit": 0, "errors": 0 },
{ "step": 29, "command": "the other 32 packages of CI's affected scope (select-type-check-scope --base 068a540: 35 packages, reached through @object-ui/i18n)", "result": "NOT MEASURED locally, reason: their only input from this branch is nine added keys in the i18n packs (a widening of an object literal) and app-shell's unexported marketplace module; CI's Type Check runs them" }
],
"tests": "Through os-verify-lock (VERDICT command-exit 0, held 521s):
pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/app-shell/src/console/marketplace/plus the three layout tests that reference marketplaceApi (AiUsageIndicator.aiOff-11799, AiUsageIndicator, StorageUsageBanner) at 6618b25: 'Test Files 118 passed (118) / Tests 1569 passed | 13 skipped (1582)', exit 0. The new test file is 18/18, including the new pin: with the choice unpicked, Install is disabled,toHaveAccessibleDescription('«marketplace.install.mode.required»'), user-event Tab reaches the[data-disabled-reason]wrapper that contains the button and carries the same description; CONTROL after pickingmanaged: enabled, no description, no wrapper. ABLATION (objectstack ablation-replace.mjs, wrap mode, anchor x1 -> x0, replacement x0 -> x1; expected 1 red, declared before the run): no wrapper -> 'Tests 1 failed | 17 passed (18)', the failing case exactly the disabled-reason pin; restore 'blob == HEAD (e8d061329e76) and git diff HEAD is empty', plus the driver's own per-path HEAD-blob check. Lock-free at 6618b25: check:i18n-dead-keys exit 0 (none of the nine keys a candidate), check:control-bytes exit 0 ('OK (scanned 8604 tracked text file(s)'), check:new-line-citations exit 0 ('0 new citation(s)'), node scripts/check-changeset-presence.mjs exit 0. ESLint, declared narrowing to the 14 changed .ts/.tsx files: 0 errors, 47 warnings, equal per file to BASE (11 / 36, same rules), invariance as in round 0 (config not type-aware, no eslint-rules/*.js reads the filesystem).","check_runs": "d5441f2: total_count 33 = returned 33; 29 success, 3 skipped, 1 failure (Type Check, job 115181953992); no other red. 6618b25 at report time: 32 runs, 23 in_progress (Type Check among them), 6 success, 3 skipped — in_progress is the honest value; not waited on.",
"files_changed_this_round": [
"packages/app-shell/src/console/marketplace/marketplaceApi.ts (isPlainRecord imported from @objectstack/spec/data; two type comments reworded)",
"packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx (disabled Install's reason: wrapper, tooltip, aria-describedby)",
"packages/app-shell/src/console/marketplace/tests/MarketplacePackagePage.installMode-12288.test.tsx (the disabled-reason pin; the no-installModes case asserts no wrapper)",
"packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts (marketplace.install.mode.required)",
".changeset/12288-marketplace-install-mode.md (nine keys; the disabled Install's reason)"
],
"commits_this_round": ["eae1153 merge origin/main 068a540", "6618b25 the two fixes"],
"pr_body_sentences_to_change": [
"'What changed', marketplaceApi.ts bullet: 'LocalInstallMode and LocalInstallStrippedLock mirror spec members (InstallModeSchema, the strippedLocks entry) that the @objectstack/spec 17.7.0 objectui resolves does not export yet.' -> 'LocalInstallMode and LocalInstallStrippedLock are the console's reading of the install door's installMode and strippedLocks; the @objectstack/spec 17.7.0 objectui resolves declares neither (only lock is typed from the spec's MetadataLock). isPlainRecord is imported from @objectstack/spec/data.'",
"'What changed', page bullet 1: after 'The install button waits for a pick.' add 'While it waits, the disabled Install says why (marketplace.install.mode.required): a focusable wrapper opens the reason on hover and focus, and it is the button's accessible description.'",
"The packs bullet and the 'Clause-② yes' paragraph: 'eight keys' -> 'nine keys', adding mode.required to the list.",
"'Verification': the readings are d5441f2's; this round's are in this report (head 6618b25)."
],
"deviations": [
"The seat's step-19 hypothesis was falsified (rule 1 on isPlainRecord, not rule 2 on the type comments); the comments were reworded anyway, as directed.",
"Type-check scope narrowed to app-shell, i18n and console plus the three repo-root programs; the other 32 packages of CI's affected scope are NOT MEASURED locally (reason above).",
"The branch was merged with origin/main 068a540 before the fixes (eae1153), per os-dev rule 1 for a resumed branch."
],
"mcp_calls": "0 — no MCP tool called",
"api_writes": "1 — post-stamped: POST /repos//issues/12288/comments (this report). Reads only otherwise (GET check-runs x2, GET branch via git fetch). No PR body PATCH, no label write. git push x1 (d5441f2..6618b25, fast-forward, no force) — not REST.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
ACCEPT: PR objectui#12317 (head
6618b25) ·domain:uiseat 2 ·session_01RZYixBnXPWvcm2YenWo6eJ· 2026-10-13T08:16ZReviewed against GitHub and the diff, not the reports. Inputs: claim
6139065390, reports6140651420(round 0) and6141117364(patch round 1).main. The first line isFixes #12288, and no other card number sits beside a closing keyword.Clause-②: yesis line-initial.check-governed-merges --prreads NOT governed.marketplaceApi.ts, the ten packs, co-located tests, and the packs' ownde-quote-pairing-3876census test (85 → 87).main:installModesfromGET /api/v1/marketplace/packages/:id/versions/latest/manifest. With noversionId, the install door fetches that same path (cloud, after a public-R2 copy of the same object), and the browse proxy forwards that GET.manifest:or a flat body.strippedLocksare listed with the package's reasons verbatim, and the three refusals show the envelope's own message.aria-describedbytomarketplace.install.mode.required. That is pinned, including Tab.Type Checkred (check:spec-symbols) to a spec-alignment claim in the new types' comments. The dev reproduced the gate and proved that wrong. The gate's finding was rule 1, a hand-writtenisPlainRecordthat@objectstack/spec/dataalready exports, and it is now imported from there. The gate list in the dispatch also omitted theType Checkjob's pre-tsc battery. That was the seat's omission, not the dev's.installModes): answered A, the manifest read. It is the one payload the door judges, and the runtime proxy cachesversions/*.6141307076on head6618b25. Its non-blocking flags are below.6618b25: 33 checks: 30 success, 3 skipped, none red.Type Check,LintandRepo Gatesare success on this head.RESOURCE_CONFLICT. This was seen only against the test's fake door, so it has no reach and is not filed here. Pointer left on feat(spec,objectql,cli): the template install mode — a package copied once into the environment ledger, fully editable, refused on shared-database multi-tenant postures (ADR-0131 D6) objectstack#15213.strippedLocksas required. objectstack's15213-install-modesand15213-unmanaged-lock-stripchangesets must ship in one release. Pointer on feat(spec,objectql,cli): the template install mode — a package copied once into the environment ledger, fully editable, refused on shared-database multi-tenant postures (ADR-0131 D6) objectstack#15213.MarketplacePage.tsx's org-catalog install sends no mode. A copy-only org package installed from there gets the door'sINSTALL_MODE_NOT_PERMITTED. That is the door's answer, outside this card's package-page scope.carrier:none.seeded: { mode: 'skipped' }is not shown on the page; the CLI prints it.carrier:none.LocalInstallMode/LocalInstallStrippedLockare to be replaced by the spec's types once objectui resolves@objectstack18.carrier:the PR that moves objectui to 18.mergeable_statesettles, through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR objectui#12317 →
maincea429b·domain:uiseat 2 ·session_01RZYixBnXPWvcm2YenWo6eJ· 2026-10-13T08:32Zadded_to_merge_queue2026-10-13T08:18Z, merged 2026-10-13T08:31Z).Fixes #12288closed this cardcompleted.maincea429bare blob-identical to the accepted head6618b25. That head is covered by ACCEPT6141326960and contract review PASS6141307076.marketplaceApi.tsonmaincarriesgetDeclaredInstallModes, and it importsisPlainRecordfrom@objectstack/spec/data.chore: release packages, as@object-ui/app-shelland@object-ui/i18nminor. objectstack's console sees it at its next.objectui-shapin move.Blocked-by:this card) gets a pointer comment with the two contract-review notes for it.pm:dispatchedcomes off with this note.Generated by Claude Code