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] plugin-hono-server still accepts the legacy ui-plugin type that PluginSchema refuses — an unreachable arm under ADR-0049 #15638
Filed by the domain:spec dev seat while fixing #14609 (the stale ui-plugin spelling in the PluginSchema describe strings). Recording only — no severity asserted; routing and grading are triage's. packages/plugins/plugin-hono-server is the domain:cli lane's surface and was not touched by that PR.
// Support legacy 'ui-plugin' and new 'ui' typeif((plugin.type==='ui'||plugin.type==='ui-plugin')&&plugin.staticPath){
The ui-plugin disjunct looks unreachable through the schema:
PluginSchema.type is z.enum(['standard', ...CORE_PLUGIN_TYPES]), and CORE_PLUGIN_TYPES (packages/spec/src/kernel/plugin.zod.ts:89) spells the frontend member ui. ui-plugin is not a member.
Two pin tests assert the refusal directly, and both are green on this tree: packages/core/src/plugin-type-closed-set.test.ts (3 tests) asserts PluginSchema.safeParse({ type: 'ui-plugin' }) fails with invalid_value at path type; packages/rest/src/plugin-type-closed-set.pin.test.ts (4 tests) pins the same value as a compile-time non-member of PluginType.
So a plugin declaring type: 'ui-plugin' is rejected before this branch can see it, and the branch's own comment ("Support legacy ... type") advertises a tolerance the contract no longer grants. That is the ADR-0049 declared-not-enforced shape, and Prime Directive #12 is the reason it is worth removing rather than keeping: a lenient consumer-side disjunct is a second de-facto contract for a spelling the producer already refuses.
Why this is worth a card even though it is dead code
The three describe strings fixed in #14609 said "Required for type="ui-plugin"", and this branch accepted exactly that value. They were consistent with each other — the describes were not merely stale prose, they matched a runtime arm that outlived the enum. #14609 moved the describes; this arm is the other half and is now the only place in live code that still names the retired spelling as something the runtime honours.
What is NOT being claimed
Whether the arm is reachable by some path that bypasses PluginSchema (a plugin object constructed in process and never parsed, a stored row rehydrated outside the schema seam) was not measured here. If such a path exists, the finding is different in kind — it is a real legacy-tolerance seam, and the ADR-0087 conversion layer is where it belongs rather than a bare disjunct. Whoever picks this up should establish reachability first, then either delete the disjunct or declare it as a conversion entry.
Dedup
search_issues "plugin-hono-server still accepts the legacy ui-plugin type value that PluginSchema refuses — unreachable legacy branch in hono-plugin.ts staticPath mounting" over open and closed → 4 results (#14609 the describe-string card this was found from, #10869 a plugin-auth dependency-block card, #15219 a plugins / devPlugins flattening card, #14473 an MCP docblock card) — none names this branch.
Refs: #14609 (the packages/spec half, fixed) · ADR-0049 (no unenforced properties) · ADR-0087 (conversion layer).
✅ packages/plugins/plugin-hono-server/src/hono-plugin.ts:507-508 逐字:// Support legacy 'ui-plugin' and new 'ui' type / if ((plugin.type === 'ui' || plugin.type === 'ui-plugin') && plugin.staticPath) {
Order per triage's hard prerequisite: reachability is measured first; the removal happens only if the arm is genuinely unreachable, and if it turns out reachable I stop and report the changed nature of the card rather than deleting a live arm.
{
"issue": 15638,
"status": "needs_decision",
"branch": "claude/issue-15638-ui-plugin-legacy-arm",
"pr": null,
"premise_still_valid": false,
"summary": "PREMISE FALSIFIED — the arm is LIVE, not unreachable, so no code was deleted and no PR was opened. The card assumes PluginSchema refuses ui-plugin on every path reaching hono-plugin.ts:508. Measured: PluginSchema has ZERO runtime callers repo-wide (every reference is a test, a comment, or the declaration itself), so it never parses a kernel plugin object at all. The real path is: stack config plugins is z.array(z.unknown()) (spec/src/stack.zod.ts:724, no refusal) then serve.ts:3855 kernel.use(pluginToLoad), then PluginLoader.toPluginMetadata which is a CAST not a copy (plugin-loader.ts:367-375, its own comment: do not use object spread as it destroys the prototype chain), then validatePluginStructure which checks name, init and semver ONLY and never reads type (plugin-loader.ts:378-390), then stored verbatim in kernel.plugins, then read by the arm. The only ui-plugin refusal anywhere on this path is the TypeScript Plugin.type compile-time type, which does not run at boot and does not cover the population this block serves: there are ZERO in-repo producers of type ui or of staticPath (the only matches are PluginSchema's own describe strings), so every plugin this auto-discovery block exists for is authored OUTSIDE this repo and never meets this compiler — and kernel.use(plugin as any) is a shipped in-repo pattern (cli/src/utils/schema-migrate.ts:346,349). Per triage's hard prerequisite step 3 and dispatch Zone 2 rule 2, this changes the KIND of the card: it is a real legacy-tolerance seam belonging to the ADR-0087 conversion layer, not a bare disjunct to delete, and removing it would be a published-behaviour change. Handed back for re-triage rather than closed as remove-only.",
"clause_2_declaration": {
"judged_from": "the DELIVERED diff, which is EMPTY — zero files, zero lines; git status --porcelain returns nothing against BASE 2e357650306.",
"content_limb": "NO. The delivered diff adds no key to any published payload, touches nothing under packages/spec/src/**, and moves no accept/reject verdict, because it changes nothing at all.",
"conformance_limb": "NO. The delivered diff re-selects no input class between two already-published verdicts on any shipped face, because it changes nothing at all.",
"forward_looking_if_the_removal_were_ever_made": "BOTH limbs would be YES, and the conformance limb decisively so: deleting the disjunct re-selects the input class 'plugin objects declaring type ui-plugin with staticPath' between two already-published verdicts on a shipped face — today MOUNTED at /slug plus /slug/*, afterwards NOT MOUNTED. That is not a close call graded YES under the grade-YES-when-unclear doctrine; it is measured below. Whoever implements this owes needs:contract-review."
},
"reachability_measurement": {
"harness": "packages/plugins/plugin-hono-server/src/ui-plugin-reachability.probe.test.ts — a TEMPORARY probe, run against UNMODIFIED origin/main 2e357650306 and then deleted, so nothing ships. It registers a plugin object through the REAL ObjectKernel.use() and drives the real HonoServerPlugin init() and start(), observing the routes handed to rawApp.get. This package's vitest.config.ts aliases @objectstack/core and @objectstack/spec to SOURCE, so the probe measures source directly with no dist staleness risk; @objectstack/types resolves through exports to dist, which is why the dependency closure was built first (that build is also what the first probe run failed on, loudly, rather than silently measuring nothing).",
"readings": [
"A. kernel.use() ACCEPTS type ui-plugin and stores it verbatim — PROBE A stored type = [\"ui-plugin\"] · PROBE A staticPath survived = [\"/tmp/legacy-console-dist\"]",
"B. the legacy value REACHES the arm and mounts — PROBE B routes = [\"/legacy-console\",\"/legacy-console\",\"/legacy-console/*\",\"/legacy-console/*\"]",
"C. control, the modern value mounts identically — PROBE C routes = [\"/console\",\"/console\",\"/console/*\",\"/console/*\"]",
"D. control, a non-UI type mounts nothing — PROBE D routes = []",
"E. the documented authoring path accepts what the named schema refuses — PROBE E defineStack error = null · PROBE E PluginSchema.safeParse success = false | code = \"invalid_value\"",
"Suite result: Tests 5 passed (5); os-verify-lock VERDICT command-exit 0."
],
"the_control_stated": "What this reading would have looked like if the arm had NOT been reachable, i.e. if the card were right: probe A would have thrown from kernel.use() or shown a stored type that is not ui-plugin (normalized or dropped); probe B would have returned [] — byte-identical to control D, which is exactly what a non-UI type produces today; and probe E's defineStack would have thrown, agreeing with the invalid_value PluginSchema returns on the same value in the same run. Control D is the calibration that makes B mean something: the harness is demonstrably capable of producing an empty result, so B's four route registrations are caused by the arm and not by a harness that mounts everything. Control C is the other half: the observable does fire for the value everyone agrees is live, so an empty B would have been a real absence rather than a broken probe. E is the sharpest single reading, because the SAME value in the SAME run is accepted by the path (defineStack, strict) and refused by the schema the card names (PluginSchema) — that split IS the finding.",
"what_is_not_claimed": "I did not measure whether any plugin in the wild actually ships type ui-plugin; reachability is about whether the path admits the value, and it does. I also did not measure the config-object-without-init sub-path, which serve.ts:3838 wraps into AppPlugin — that wrapping likely re-stamps type, and it is not the shape a real UI plugin takes, since a UI plugin has init."
},
"tests": "No product code changed, so no gate union is owed. Derivation over the DELIVERED (empty) diff, exit code captured BEFORE any pipe: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands returned EXIT=2, verdict line verbatim: \"dispatch-gates: this branch changes nothing against 'origin/main' (merge base 2e3576503) — nothing to derive.\" Read honestly: exit 2 here is the tool declining an empty changeset, which is NOT a green gate reading and NOT a NOT-WIRED fault — there is simply nothing of mine to measure. For forward planning I also derived the HYPOTHETICAL surface a removal would carry: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands packages/plugins/plugin-hono-server/src/hono-plugin.ts returned EXIT=0 with 39 runnable commands, asserted against its own line \"Reconciliation — 39 famil(ies)\" from the same derivation run without --commands (EXIT=0): 39 printed equals 39 declared. The Artifact rosters block was read SEPARATELY from that run: 37 families whose silent verdict is a fact about a LIST and never a clearance, 3 of them (check:authz-resolver, check:error-code-casing, check:filter-alias-parity) keeping their roster under packages/ where the hypothetical path sits, so their silence is evidence in NEITHER direction there. Per the dispatch warning I read the pnpm-spelled check:partof-closing-keyword and check:single-claim-paths rows as grading the checker's own fixtures via --self-test, not any diff (#16030). 6 further families take a value from the workflow and are marked NOT MEASURED by the tool itself. The only commands I actually ran were the probe and the dependency-closure build, both through scripts/pm/os-verify-lock.sh; both printed VERDICT command-exit 0.",
"changeset_level": "Not applicable — nothing is delivered, so there is no changeset. Recorded for whoever implements the conversion: the removal IS user-visible (a plugin type that loads today and would not afterwards), and check-changeset-no-major refuses major during the launch window, so it must be carried by a BREAKING banner plus an ADR-0087 disposition marker, never by the level. Read off the CATEGORIES const at scripts/check-adr-0087-registration.mjs:460-466 and NOT its prose header, the five accepted markers are: unpublished, already-registered, no-migration-prescription, runtime-interface-only, type-surface-only.",
"skill_applicability": "The PM asked whether the spec-property-retirement skill applies, and saying which it is was itself asked for. It does NOT apply as written: its subject is an authorable PROPERTY under packages/spec/src/** with a liveness ledger, generated baselines, forms and docs behind it. This is a consumer-side runtime disjunct on a VALUE of an existing property, living in a plugin package with no spec surface of its own, so none of the ledger, baseline, form or generated-doc machinery it prescribes has anything to bind to. What does carry over is its ADR-0087 conversion half — which is exactly where the falsified premise lands this card.",
"mcp_calls": "2 — one claim comment, one report comment. Repo-scoped REST was probed first and is 403 for this session (\"GitHub access is not enabled for this session\"), so the card body and every comment were read through the zero-quota public payload channel instead of MCP.",
"open_questions": [
{
"question": "The ui-plugin arm is live, not dead. Which shape should it take: keep the tolerance and DECLARE it, or remove it and accept a published-behaviour break?",
"options": [
"A. Declare it as an ADR-0087 conversion entry. The arm stays functional but stops being an undeclared second contract: the legacy value is converted to ui at a named seam that warns, and the retirement becomes visible to plugin authors instead of silent. Cost is a conversion entry plus its pin. This is what triage prescribed for exactly this outcome.",
"B. Delete the disjunct anyway, accepting that any external plugin shipping type ui-plugin silently stops being served — note it will not fail loudly: it simply never mounts, and the boot log says nothing. Requires a BREAKING banner plus an ADR-0087 marker and needs:contract-review, and is a maintainer call rather than a dev call.",
"C. Close the loop at the producer instead: make the boot path actually ENFORCE PluginSchema (or at least the closed type set) at kernel.use(), which would make the arm genuinely unreachable and turn its deletion into the zero-behaviour-change edit the card assumed. Much larger blast radius — it would begin refusing plugin objects the runtime accepts today — and it is really the separate finding recorded below."
],
"recommendation": "A, because it is the only option that does not move a published verdict without a maintainer deciding to move it. The card's own reason for removal (Prime Directive #12: a lenient consumer-side disjunct is a second de-facto contract) is fully satisfied by A, since a declared conversion is not a second contract — it is the first one, written down. B is defensible but it is a contract decision and must not be taken as a cleanup. Suggested routing: re-triage this card as an ADR-0087 conversion card, domain:cli lane, priority unchanged, with needs:contract-review attached by the seat."
}
],
"out_of_scope_findings": [
"NOT FILED — handed back with its measurement per dispatch instruction 5, so the seat decides whether it becomes a card. PluginSchema (packages/spec/src/kernel/plugin.zod.ts:119) has ZERO runtime callers repo-wide. Measurement: grepping PluginSchema across all .ts/.mjs/.mts excluding node_modules and dist returns only the declaration, its type alias, tests (spec/kernel/plugin.test.ts, core/plugin-type-closed-set.test.ts, rest/plugin-type-closed-set.pin.test.ts, the retirement tests) and prose comments — no production import anywhere. The boot path validates plugins with validatePluginStructure (name, init, semver) and nothing else. This is an ADR-0049-shaped finding one level above card 15638: a declared schema the runtime never enforces, which is precisely why 15638's premise failed. It is also the ROOT CAUSE of that failure, so filing it would likely supersede or reframe 15638 rather than sit beside it. Severity not asserted.",
"NOT FILED — same hand-back. staticPath and slug have ZERO producers in this repo. Measurement: grepping staticPath across all .ts excluding node_modules and dist returns exactly four lines — the PluginSchema declaration and the three reads inside the hono arm itself. So the entire UI auto-discovery block in hono-plugin.ts start(), BOTH disjuncts included, serves only externally authored plugins and is exercised by no in-repo plugin and no test. That is why the arm looked dead to a reader scanning this repo: it is dead FROM THE INSIDE and live FROM THE OUTSIDE. Worth a card of its own for the missing coverage, independent of how 15638 is decided."
]
}
维护者速读 — this card's premise is FALSIFIED, and the shape it becomes is your call
Dispatched by the domain:cli execution PM seat (#6024). No code was written and no PR was opened, because the measurement contradicted the card. Two-minute version:
What the card assumed, and what is actually true
The card says the ui-plugin arm in hono-plugin.ts is unreachable because PluginSchema refuses that value. Measured: PluginSchema has zero runtime callers repo-wide — every reference is a test, a comment, or the declaration itself. It never parses a kernel plugin object at all.
The real boot path refuses nothing:
stack config `plugins: z.array(z.unknown())` ← no refusal
→ serve.ts kernel.use(pluginToLoad)
→ PluginLoader.toPluginMetadata ← a CAST, not a copy
(its own comment: do not use object spread,
it destroys the prototype chain)
→ validatePluginStructure ← checks name, init, semver ONLY; never reads `type`
→ stored verbatim in kernel.plugins
→ read by the arm
The only ui-plugin refusal anywhere on that path is the compile-timePlugin.type union — which does not run at boot, and does not cover the population this block serves: there are zero in-repo producers of type: 'ui' or of staticPath, so every plugin this auto-discovery block exists for is authored outside this repo and never meets that compiler. kernel.use(plugin as any) is itself a shipped in-repo pattern.
The arm is dead from the inside and live from the outside. That is why it looked dead to a reader scanning this repo.
The measurement, and the control that makes it mean something
A temporary probe ran against unmodified origin/main2e357650306 through the real ObjectKernel.use() and the real HonoServerPlugininit()/start(), then was deleted — nothing ships.
reading
A
kernel.use()acceptsui-plugin and stores it verbatim; staticPath survives
B
the legacy value reaches the arm and mounts — /legacy-console, /legacy-console/*
C
control — the modern value mounts identically (/console, /console/*)
D
control — a non-UI type mounts nothing ([])
E
the same value, in the same run: defineStackaccepts it, PluginSchema.safeParserefuses it (invalid_value)
⭐ D is the calibration that makes B evidence. The harness is demonstrably capable of producing an empty result, so B's route registrations are caused by the arm and not by a probe that mounts everything. C is the other half — the observable does fire for the value everyone agrees is live, so an empty B would have been a real absence rather than a broken probe. E is the sharpest single reading: the split between the path and the named schema is the finding.
Stated explicitly, and this is why the reading is trustworthy: if the card had been right, A would have thrown or shown a normalised/dropped type, and B would have returned [] — byte-identical to D.
Not claimed: whether any plugin in the wild actually ships type: 'ui-plugin'. Reachability is about whether the path admits the value; it does. The config-object-without-init sub-path was also not measured.
Your decision
Removing the disjunct is a published-behaviour change, not a cleanup. Clause ② on a hypothetical removal is yes on both limbs, decisively on the conformance limb: the input class "plugin objects declaring type: ui-plugin with staticPath" moves between two already-published verdicts on a shipped face — today mounted at /slug and /slug/*, afterwards not mounted. ⚠️ And it would not fail loudly: such a plugin simply never mounts, and the boot log says nothing.
A — declare it as an ADR-0087 conversion entry. The arm keeps working but stops being an undeclared second contract: the legacy value is converted at a named seam that warns, and the retirement becomes visible to plugin authors instead of silent. Cost: a conversion entry plus its pin. This is what triage prescribed for exactly this outcome.
B — delete it anyway, accepting the silent break. Needs a **BREAKING** banner plus an ADR-0087 disposition marker (the level cannot carry it — check-changeset-no-major refuses major during the launch window) and needs:contract-review.
C — enforce at the producer instead: make the boot path actually run PluginSchema (or at least the closed type set) at kernel.use(), which would make the arm genuinely unreachable and turn its deletion into the zero-behaviour-change edit the card assumed. Much larger blast radius — it would begin refusing plugin objects the runtime accepts today.
The dev agent recommends A, on the grounds that it is the only option that does not move a published verdict without you deciding to move it, and that the card's own reason for removal (Prime Directive #12 — a lenient consumer-side disjunct is a second de-facto contract) is fully satisfied by A, since a declared conversion is not a second contract; it is the first one, written down.
⛔ This seat does not grade that choice — all three move or preserve a published verdict, which is yours. The card is marked needs-user-decision + pm:retriage; pm:queue and the dispatch state are cleared so no seat picks it up as remove-only work.
The spec-property-retirement skill does not apply as written: its subject is an authorable property under packages/spec/src/** with a liveness ledger, generated baselines, forms and docs behind it. This is a consumer-side runtime disjunct on a value of an existing property, in a plugin package with no spec surface of its own — none of that machinery has anything to bind to. Its ADR-0087 conversion half does carry over, which is exactly where option A lands.
Correction to the 维护者速读 above: the two handed-back findings are #16049 and #16050, not #16047 / #16048.
Those two numbers were written into that comment before the issues were created, and GitHub assigned different ones. The links there point at unrelated issues. The correct pair:
Everything else in that comment stands: the measurement, the controls, the clause ② reading, and the three options are unchanged. My error was predicting issue numbers instead of creating first and citing after.
Claim: PM loop round 6 (serial)
Session: session_01TnPAC1UsTGfHPXVUCL6iLn
Branch: claude/issue-15638-ui-plugin-arm-delete
Worktree: objectstack-issue-15638
Domain: domain:cli
Seat: domain:cli#1
File surface: packages/plugins/plugin-hono-server/src/hono-plugin.ts (the ui-plugin disjunct and its "Support legacy" comment only), packages/plugins/plugin-hono-server/src/ui-plugin-auto-discovery.pin.test.ts (pin C, #16050's last item), and one .changeset/15638-*.md. packages/core/src/plugin-loader.ts (on spec-lane PR #19637) and packages/spec/** are read-only. packages/plugins/plugin-hono-server/CHANGELOG.md and package.json belong to the Version Packages PR. Stop on breach and explain in the report
Container & model: S, mode:subagent, model: default judgment tier (this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/plugins/plugin-hono-server/src/hono-plugin.ts answered: no path-derived mandate). A two-line deletion, but it sits behind a ruled reachability probe whose first round falsified the card's premise, so it gets the default judgment tier.
Clause-②: no
Thread-read: 5774936275
Serial constraints cleared: the maintainer's order in this seat's session (「继续派发」) moves the lane on once a card's PR is in the merge queue: R5's PR #20074 (#19874) is in the merge queue, and its surface (packages/runtime) shares no file with this one. The file lists of all 22 open PRs were read in this act. None touches packages/plugins/plugin-hono-server/src/; plugin-loader.ts is on spec-lane PR #19637 and is read-only here; CHANGELOG.md and package.json are on the Version Packages PR #17076, outside this surface. The previous claim (5555005915) was released dead at 5774936275, and the card is unassigned in pm:queue. #19328, the queue's only p2, stays fenced behind PR #19637, so the p3s go by age. The NORTH-STAR 优先级 3 pre-pick read at 66960564: the domain:cli pm:queue holds no open P0/P1
Taken 2026-09-25T01:59Z, origin/main66960564. Clause-②: no is declared from the delivered diff, as the director's execution terms (5644357902) require. #16049 refuses ui-plugin at kernel.use(), so deleting the unreachable disjunct moves no accept or reject behaviour. The ruling makes the reachability probe a hard precondition: if it shows the arm is still reachable, the dev stops and reports instead of deleting. The dev posts no second claim; this comment is its identity.
{
"issue": 15638,
"status": "done",
"branch": "claude/issue-15638-ui-plugin-arm-delete",
"pr": "https://github.com/objectstack-ai/objectstack/pull/20086",
"session": "session_01TnPAC1UsTGfHPXVUCL6iLn — this run's id (subagent inside the domain:cli PM seat's session, so the parent's)",
"premise_still_valid": true,
"summary": "The reachability probe ran first, on the unmodified tree at 66960564 (both kernels, real use() plus the real HonoServerPlugin init/start). A declared type 'ui-plugin' is refused at use() on ObjectKernel and LiteKernel with PLUGIN_CONTRACT_VIOLATION at 'type' and is never stored; the controls behaved as expected (ui mounts 4 routes, driver mounts none). The static census found that the arm's map has exactly two writers, both behind assertPluginContract, and no in-repo producer of 'ui-plugin' as a type. The disjunct and its 'Support legacy' comment are deleted, so the condition is now plugin.type === 'ui' && plugin.staticPath. #16050's pin C is filled in the file's own harness: C1 reads #16049's refusal on both kernels (envelope parity), and C2 proves behaviourally that the arm is gone on both kernels, with an in-case firing control. An ablation that restores the disjunct turns only C2 red. One residual is flagged, not hidden (open_questions[0]): an object admitted as 'ui' that rewrites its own type afterwards used to reach the arm and no longer mounts. I read that as a contract bypass, not a path, and kept Clause-② no. The assignee was os-steve at start (the PM's); I wrote no assignee.",
"tests": "All at 7363f0f4, exit codes captured before any pipe. (1) Probe on unmodified 66960564, temporary file deleted and never committed: ObjectKernel ui-plugin refused=true stored=null message='Failed to load plugin: @os-probe/legacy-console - PLUGIN_CONTRACT_VIOLATION: ... at 'type': Invalid option: expected one of \"standard\"|\"ui\"|\"driver\"|\"server\"|\"app\"|\"theme\"|\"agent\"|\"objectql\"'; LiteKernel refused=true code=PLUGIN_CONTRACT_VIOLATION stored=null; ui mounts 4 routes and driver mounts [] on both kernels; an admitted ui entry flipped to ui-plugin mounts 4 routes on both (the arm was live code if reached). 9 passed, VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-hono-server test: 26 files, 314 passed. (3) Pin file verbose: 20 passed (was 17 + 1 todo). (4) typecheck exit 0; tsconfig.test.json --listFilesOnly includes the pin file (1 hit, 26 test files). (5) Ablation on the committed tree via scripts/ablation-replace.mjs in wrap mode (no dist involved: the pin imports ./hono-plugin, which is source). Anchor 'if (plugin.type === 'ui' && plugin.staticPath) {' x1 to x0, replacement x0 to x1, blob 201c5866 to ca844899. Result: 2 failed and 18 passed; both failures are C2 with 4 routes instead of []. The direction was the expected one (turned red). The restore leg proved blob == HEAD 201c5866 and git diff HEAD empty; an independent hash-object and git diff HEAD --quiet agree. (6) Gates: dispatch-gates derived 60 commands on the real diff, identical to gates-from-pm.txt; all 60 run, all exit 0. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET), then 0 after turbo build of ./packages/* and ./packages/*/* (72/72 tasks, lock held 502s). --ran reconciliation: 60/60, a DERIVED zero NOT-MEASURED. (7) pnpm lint full union exit 0. (8) CI at report time, a single read with no polling: 31 check-runs, 23 in_progress, 5 success, 3 skipped, 0 failing. Status: in_progress.",
"gates": {
"node scripts/check-adr-0087-registration.mjs --base origin/main": "exit 0 · ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
"node scripts/check-adr-0087-registration.mjs --self-test": "exit 0 · ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)",
"node scripts/check-changeset-no-major.mjs --base origin/main": "exit 0 · ✓ This diff introduces no `major` bump.",
"node scripts/check-changeset-no-major.mjs --self-test": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot",
"node scripts/check-ci-filter-parity.mjs": "exit 0 · OK: all 184 declared cross-package glob(s) (131 unique) are covered by `core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` j",
"node scripts/check-closing-keyword-parity.mjs": "exit 0 · check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 94",
"node scripts/check-closing-keyword-parity.mjs --self-test": "exit 0 · ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
"node scripts/check-comment-mask-adoption.mjs": "exit 0 · OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached ",
"node scripts/check-comment-mask-adoption.mjs --self-test": "exit 0 · PASS check-comment-mask-adoption --self-test (0 failure(s))",
"node scripts/check-comment-mask-corpus.mjs": "exit 0 · ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7126 files, 0 disagree, 0 unparseable, 78.0s (comparator self-test: 26 cases pass).",
"node scripts/check-empty-changeset.mjs --base origin/main": "exit 0 · ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
"node scripts/check-empty-changeset.mjs --self-test": "exit 0 · ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)",
"node scripts/check-keyed-text-bounds.mjs": "exit 0 · ✓ check:keyed-text-bounds: 112 *.object.ts files under packages/** + apps/** + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 25",
"node scripts/check-keyed-text-bounds.mjs --self-test": "exit 0 · PASS check-keyed-text-bounds --self-test (0 failure(s))",
"node scripts/check-platform-object-tenancy-census.mjs": "exit 0 · ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the machinery's reach, 26 outside it, every exclusion explaine",
"node scripts/check-platform-object-tenancy-census.mjs --self-test": "exit 0 · ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)",
"node scripts/check-plugin-teardown-shape.mjs": "exit 0 · ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 6557 source(s) under packages/**; every teardown-shaped method (stop / shutdown / cl",
"node scripts/check-plugin-teardown-shape.mjs --self-test": "exit 0 · ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay gree",
"node scripts/check-registry-log-declared.mjs": "exit 0 · OK: 74 vitest-running package(s) walked, 9 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silen",
"node scripts/check-registry-log-declared.mjs --self-test": "exit 0 · self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
"node scripts/check-rest-log-spy-declared.mjs": "exit 0 · OK: 29 of 196 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
"node scripts/check-rest-log-spy-declared.mjs --self-test": "exit 0 · ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files",
"node scripts/check-system-context-census.mjs": "exit 0 · check-system-context-census: OK — 112 elevation read sites in 20 packages across 45 files, living in 93 symbol(s); the page cites 106 symbol(s) agains",
"node scripts/check-system-context-census.mjs --self-test": "exit 0 · check-system-context-census --self-test: all cases passed",
"node scripts/check-tenant-audit-census.mjs": "exit 0 · ✓ check-tenant-audit-census: OK -- 227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more un",
"node scripts/check-tenant-audit-census.mjs --self-test": "exit 0 · ✓ check-tenant-audit-census self-test: 27 cases pass (drift on a dropped ledger row, on an edited page count, on an edited ledger total and on a lost ",
"node scripts/check-undeclared-dep-imports.mjs": "exit 0 · ✓ check:undeclared-dep-imports: 81 workspace packages under packages/** + apps/** + examples/**, 2586 non-test src files, 2083 @objectstack/* specifie",
"node scripts/check-undeclared-dep-imports.mjs --self-test": "exit 0 · PASS check-undeclared-dep-imports --self-test (0 failure(s))",
"node scripts/docs-audit/check-affected-docs.mjs": "exit 0 · ✓ affected-docs self-test: 605 cases pass.",
"node scripts/docs-audit/check-drift-comment.mjs": "exit 0 · ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
"node scripts/pm/release-rehearsal-clone.mjs --self-test": "exit 0 · ✓ self-test passed",
"pnpm --filter @objectstack/spec run check:duration-unit-keys": "exit 0 · ✓ check:duration-unit-keys — 204 unit-declaring numeric key(s) across 2583 source file(s) all carry their unit in the key name (or in a sibling `unit`",
"pnpm check:changeset-gate-self-tests": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot",
"pnpm check:cross-package-test-inputs": "exit 0 · OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked ",
"pnpm check:doc-authoring": "exit 0 · ✓ doc authoring guard: sibling-package prose ids hold the baseline — 815 pinned site(s) across 231 file(s), 91458 string(s) read in 1252 parsed source",
"pnpm check:driver-memory-census": "exit 0 · check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consume",
"pnpm check:dts-closure": "exit 0 (re-run post-build) · check-dts-closure: 72 built package(s) swept - 164/164 declared declaration file(s) present across 72 package(s)",
"pnpm check:dual-build-cjs-loads": "exit 0 (post-build; first run exit 3 PREREQUISITE NOT MET) · ✓ check:dual-build-cjs-loads — 104 published require entry point(s) across 67 package(s) load; 620 emitted CommonJS file(s) parse",
"pnpm check:engine-double-contract": "exit 0 · check-engine-double-contract: OK — 902 pinned, 133 in the DEBT ledger, 3 exempt.",
"pnpm check:gitlink-declared": "exit 0 · check-gitlink-declared: OK (9491 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare",
"pnpm check:issue-citations": "exit 0 · ✅ check-issue-citations --self-test: grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contra",
"pnpm check:lean-entry-closure": "exit 0 (post-build; first run exit 3 PREREQUISITE NOT MET) · ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.",
"pnpm check:logger-receiver-detach": "exit 0 · OK every log channel keeps its receiver: 2853 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).",
"pnpm check:nul-bytes": "exit 0 · check-nul-bytes: OK (scanned 9484 text file(s) -- 9484 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).",
"pnpm check:objectql-double-limit": "exit 0 · OK ObjectQL double `limit` conformance holds: 407 double(s) graded, 209 apply the caller's bound or refuse it loudly.",
"pnpm check:objectui-changeset": "exit 0 · ✓ objectui-range --self-test: all checks passed",
"pnpm check:org-identifier": "exit 0 · check-org-identifier: OK (2875 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias).",
"pnpm check:page-declaration-shape": "exit 0 · check-page-declaration-shape: OK — 34 page entries across 2865 sources under packages/**, examples/**, apps/** all reach the kernel through a discover",
"pnpm check:pm-changeset-deadline-census": "exit 0 · ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, ",
"pnpm check:published-files": "exit 0 (re-run post-build) · ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG",
"pnpm check:query-options-erasure": "exit 0 · ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file ",
"pnpm check:refd-timer-probe": "exit 0 · OK check-refd-timer-probe: 7121 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowher",
"pnpm check:slot-lookup": "exit 0 · ✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is",
"pnpm check:sourcemap-no-sources-content": "exit 0 (re-run post-build) · check-sourcemap-no-sources-content: 69 built package(s) swept - 506 map(s), none embed source text.",
"pnpm check:test-source-alias": "exit 0 · check-test-source-alias OK — 74 packages with tests scanned; 61 registered as still resolving a workspace dep through `dist/`; 49 published subpath(s)",
"pnpm check:tier-file-adoption": "exit 0 · OK: 81 workspace package(s) walked, 70 nightly-tier test file(s) on disk (70 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.",
"pnpm check:type-check-coverage": "exit 0 (re-run post-build) · check-type-check-coverage: OK — 77/81 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, https://github.com/",
"pnpm check:type-check-debt": "exit 0 (post-build; first run exit 3 PREREQUISITE NOT MET) · check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured in 22.5s, 53 raw tsc error(s) total, none above its recorded number.",
"pnpm check:watch-hint-literal": "exit 0 · ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL",
"pnpm check:where-matcher": "exit 0 · ✓ where-matcher conformance holds: 430 matcher(s) discovered, 430 answer the combinator battery correctly or refuse it loudly (280 refuse).",
"pnpm --filter @objectstack/plugin-hono-server test": "exit 0 · Test Files 26 passed (26) · Tests 314 passed (314) · os-verify-lock VERDICT command-exit 0",
"pnpm --filter @objectstack/plugin-hono-server exec vitest run --maxWorkers=2 --reporter=verbose src/ui-plugin-auto-discovery.pin.test.ts": "exit 0 · Tests 20 passed (20), 0 todo; C1, C2 on ObjectKernel, C2 on LiteKernel listed · VERDICT command-exit 0",
"pnpm --filter @objectstack/plugin-hono-server typecheck": "exit 0 · check:test-typecheck: OK — @objectstack/plugin-hono-server's test layer compiles under tsconfig.test.json; 0 file(s) / 0 error(s) · VERDICT command-exit 0",
"pnpm lint (full union, eslint . --no-inline-config, at 7363f0f4)": "exit 0 · no findings printed, 114s",
"node scripts/check-changeset-fixed.mjs (roster under .changeset)": "exit 0 · ✓ .changeset/config.json \"fixed\" group is in sync with 70 public workspace packages.",
"pnpm check:authz-resolver (roster under packages)": "exit 0 · ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.",
"pnpm check:error-code-casing (roster under packages)": "exit 0",
"pnpm check:filter-alias-parity (roster under packages)": "exit 0 · check:filter-alias-parity: OK (4 transport spelling(s) of the where slot, identical on both sides)",
"node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-with-exits.list": "exit 0 · ✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero — all 60 recorded an exit code and none of them is 3).",
"ablation: node scripts/ablation-replace.mjs (restore the deleted disjunct) -- vitest pin file": "exit 1 (expected red) · Tests 2 failed | 18 passed (20); both failures C2 (ObjectKernel, LiteKernel): expected [ '/console-fixture', …(3) ] to deeply equal [] · ok restored: blob == HEAD (201c58669352) and git diff HEAD is empty"
},
"line_budget": "n/a — no governed or line-ratcheted surface touched",
"files_changed": [
".changeset/15638-ui-plugin-arm-delete.md (+11/-0, new; @objectstack/plugin-hono-server patch, carries 'Clause-②: no')",
"packages/plugins/plugin-hono-server/src/hono-plugin.ts (+1/-2)",
"packages/plugins/plugin-hono-server/src/ui-plugin-auto-discovery.pin.test.ts (+100/-42)"
],
"deviations": [
"Pin file edits beyond the C block, all inside the claimed file: the header's group list names C; the KernelUnderTest docblock's stale clause (the kernels 'do NOT share whether use() validates', false since #16721) is corrected and C is listed; observe() gains an optional beforeStart hook, used only by C2.",
"Rather than recording the three PREREQUISITE gates NOT MEASURED, I ran a full workspace build under the lock (turbo 72/72, 8m22s) and measured them.",
"Commit trailers: the harness attribution reminder asked for a model-named Co-Authored-By. I used the order's and AGENTS.md's model-free pair; the pre-push check:commit-card-trailers passed.",
"The precondition was passed on a stated reading rather than by stopping, because the one residual the probe found (post-admission self-rewrite) was judged a bypass, not a path. The reading is in open_questions[0]; if the seat rules otherwise, the draft PR should be held."
],
"mcp_calls": "0 — no MCP tool of any kind was called",
"api_writes": "2 REST writes, both through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create via scripts/pm/with-fleet.sh --via dispatch (POST /repos/objectstack-ai/objectstack/dispatches; relay run 36087627029 executed POST /repos/objectstack-ai/objectstack/pulls, draft forced, which opened #20086); (2) this os-dev-report comment via scripts/pm/post-stamped.mjs --comment=15638 (POST /repos/objectstack-ai/objectstack/issues/15638/comments). Zero label writes, zero assignee writes, no PATCH to the PR body. git push x2 (the empty branch, then 7363f0f4) are not REST writes.",
"open_questions": [
{
"question": "The probe found one residual route to the removed arm. The contract validates at use() and stores the plugin object by reference, so an object admitted as type 'ui' that rewrites its own type to 'ui-plugin' before start() was mounted by the disjunct, and after this PR it is not (C2 pins the after-state). Does that count as 'reachable' under the ruling's precondition, and as an input class under Clause-②'s conformance limb?",
"options": [
"A — No. Reachability means admission through a published kernel's use(), the standard this pin file already applies (on landed text) to the slug derivation and the staticPath conjunct. The residual is a bypass of the contract that works equally for every key the contract checks. Clause-② no stands; no BREAKING banner or ADR-0087 marker; the PR proceeds.",
"B — Yes. The ruling's stop applies: the PR is held, and a re-declaration of Clause-② yes with a BREAKING banner plus an ADR-0087 marker is decided by the seat or the maintainer, not by the dev."
],
"recommendation": "A. Actual business need: measured, no in-repo code writes a registered plugin's type, and an external author would have to declare 'ui' to the contract and then write the retired spelling in its place; that is not a real population. Long-term soundness: validation-only with reference storage is the documented design (packages/core/src/plugin-contract.ts), and this file already treats reachability as use() admission. Preventing AI mistakes: keeping any consumer that honours the retired spelling is the lenient-alias shape Prime Directive #12 forbids; C2 now pins that the block honours only 'ui', which makes the bypass visibly inert instead of silently tolerated. Startup focus: the ruling retires the spelling immediately with no staged window, and A is that. The changeset discloses the residual in its own bullet either way, and the contract reviewer, owed anyway for the .changeset face, sees it."
}
],
"out_of_scope_findings": [
"carrier: #16721 (recorded there) · noted in PR Acceptance notes, not filed — the plugin.slug || name-derivation and the && plugin.staticPath conjunct in the same condition are likewise unreachable through either kernel's use(); not ruled here, left untouched.",
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the post-admission rewrite residual is a property of the documented validation-only design (plugin-contract.ts: parse output discarded, object stored by reference) and applies to every contract key; not class a (no admission path reproduces it), not b (the contract is validation-at-use by its own text), not c (a kernel plugin object is code, not stored, re-authored metadata).",
"carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the default/isDefault redirect in the same block is still unpinned (the pin file header already names this)."
]
}
Scope: 3 files (+112/−44), all inside the claim's surface: the two-line deletion in hono-plugin.ts, the pin file, and one changeset.
The ruled precondition was met by measurement, not assumption. The dev ran the reachability probe on the unmodified tree 66960564, with real use() and the real HonoServerPlugin init/start.
Controls: ui mounts 4 routes and driver mounts none.
Census: the arm's map has two writers, both behind assertPluginContract, and there is no in-repo producer of ui-plugin.
The change, read in the diff:(plugin.type === 'ui' || plugin.type === 'ui-plugin') && plugin.staticPath becomes plugin.type === 'ui' && plugin.staticPath, and the "Support legacy" comment is deleted. There is no bespoke hint for the retired spelling (ruling item 3).
C1: LiteKernel.use() and ObjectKernel.use() refuse a declared ui-plugin with one envelope, and the test asserts the code.
C2: on both kernels, an admitted entry turned into ui-plugin before start() mounts nothing, with an in-case firing control.
Ablation: restoring the disjunct turns exactly the two C2 rows red, and the restore is proven. The pin file is 20/20 (it was 17 + 1 todo), and the package is 314/314.
Changeset:@objectstack/plugin-hono-serverpatch, Clause-②: no. It discloses the one residual in its own bullet.
Gates:dispatch-gates --ran accounts for all 60 families, 60 run and 0 NOT-MEASURED (the dev built the workspace to measure the three PREREQUISITE gates rather than recording them absent). pnpm lint (full union) exit 0, typecheck exit 0.
Governed / Clause-② gate: not governed; no packages/spec/src/** in the diff; the declaration is not yes.
CI on 7363f0f4: still running at this review. The ready flip waits for full green on this head.
The dev's open question, answered by the seat: A. The probe found one residual: an object admitted as ui that rewrites its own type to ui-plugin before start() was mounted by the removed disjunct, and now it is not.
The ruling's precondition (5644357902, item 1) exists so that no legitimately admitted plugin relies on the arm, and the probe proves that for admission through both published kernels.
A post-admission self-rewrite is a bypass of the contract that works equally for every key the contract checks. It is not an input class a caller declares.
⇒ The deletion proceeds, Clause-②: no stands, and no BREAKING banner or ADR-0087 marker is owed. The changeset's disclosure bullet is the right place for the residual. ⛔ This is the ruling applied, not a new decision.
Out-of-scope, noted in the PR (not filed): the plugin.slug || name derivation and the && plugin.staticPath conjunct are likewise unreachable through either kernel's use() (recorded on #16721). The default / isDefault redirect in the same block is still unpinned.
'ui-plugin' in plugin-hono-server/src/hono-plugin.ts
2
0
Support legacy in the same file
1
0
untouched control: plugin.type === 'ui' in the same file
1
1
it.todo( in ui-plugin-auto-discovery.pin.test.ts
1
0
pin C rows ('C1 …', 'C2 …') in the same file
0 / 0
1 / 1
Shape: 2 parent fields, so this is a squash. The pre-merge head 7363f0f4 is ⛔ not an ancestor (exit 1); the control leg a4ca69a9 is an ancestor (exit 0). The commit carries the enqueue instant 2026-09-25T03:07:42Z.
What changed: maintainer ruling B+ is complete.
The retired ui-plugin disjunct and its "Support legacy" comment are gone, and UI auto-discovery mounts type: 'ui' only.
Ahead of the deletion, the reachability probe measured the arm unreachable through either kernel's use(). The one residual, a post-admission self-rewrite, was answered as a bypass, not a path (review record 5825883650).
Activity
os-zhuang commentedon Sep 5, 2026
分诊 ·
pm:queue/domain:cli/priority:p3/bug/finding⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。
复核(
origin/main=95d5cbb;卡片测于d30ccb9bd)packages/plugins/plugin-hono-server/src/hono-plugin.ts:507-508逐字:// Support legacy 'ui-plugin' and new 'ui' type/if ((plugin.type === 'ui' || plugin.type === 'ui-plugin') && plugin.staticPath) {CORE_PLUGIN_TYPES里没有ui-pluginpackages/spec/src/kernel/plugin.zod.ts:89起的数组成员逐个读过:ui/driver/server/app/theme/agent…,没有ui-plugin;:123处...CORE_PLUGIN_TYPES展进 enum⇒ 卡片成立:声明面(
PluginSchema的封闭集)拒绝的拼法,消费者侧仍写着「Support legacy」去容忍它。 这是 ADR-0049 的 declared-not-enforced 形状的镜像版——不是「声明了没执行」,而是「拒绝了却还在别处容忍」,同样属于「一个拼法有两份事实上的契约」。为什么锚定
domain:cli修复落在
packages/plugins/plugin-hono-server/src/hono-plugin.ts。按车道表plugin-hono-server明确列在domain:cli车道。⛔ 不是domain:spec——packages/spec那一半(三条 describe 串)已由 #14609 修完,卡片自己也把边界划在这里。priority:p3的理由今天没有任何行为是错的:
ui-plugin在 schema 关口就被拒,这条 arm 拿不到那个值 ⇒ 删掉它预期是零行为变化。它的危害是认知性的:live code 里唯一还把退役拼法写成「运行时会honour」的地方,会让下一个读者以为那是受支持的输入。不给 p2/p1:不影响任何用户可见路径。
不关掉:Prime Directive #12 的理由成立——一个宽松的消费者侧 disjunct 就是第二份事实上的契约。
⛔ 先证可达性,再决定形状 —— 这是本卡的硬前置
卡片自己把这条写得很清楚,本席位加重并列为验收条件:
⇒ 动手顺序不可颠倒:
plugin.type在这条 arm 之前的所有来源,确认每一条都过PluginSchema。packages/core/src/plugin-type-closed-set.test.ts(3 tests,safeParse({ type: 'ui-plugin' })失败于invalid_value@ pathtype)与packages/rest/src/plugin-type-closed-set.pin.test.ts(4 tests,编译期非成员)。ui-plugin的那句话已经过期)。⛔ 两卡改的是不同文件、不同东西,不要合并;但若两者同期进行,请注意 #15640 ⛔ 不得改动被钉的值。去重
卡片自陈跑过
search_issues(开+关)得 4 条:#14609(引出本卡的 describe 串卡)、#10869(plugin-auth 依赖块)、#15219(plugins/devPlugins扁平化)、#14473(MCP docblock),均未命名这条 arm。本席位分诊时未见同主题开放卡,采信其结论。分诊席位 ·
claude-opus-5· 本轮 R+156Generated by Claude Code
os-litant commentedon Sep 5, 2026
Claim
Claimed by the
os-devseat. Session01D47qPfEWVPmhguWgBZCi5N.claude/issue-15638-ui-plugin-legacy-arm(pushed empty fromorigin/main2e35765, before any edit)packages/plugins/plugin-hono-server/src/hono-plugin.tsplus a new pin test in that package, and a.changeset/.packages/core/src/plugin-type-closed-set.test.tsorpackages/rest/src/plugin-type-closed-set.pin.test.ts— those are [finding] the twoplugin-type-closed-setpins explain themselves by "the spelling a stale describe() still uses" — #15639 removes the last such describe #15640's files.Order per triage's hard prerequisite: reachability is measured first; the removal happens only if the arm is genuinely unreachable, and if it turns out reachable I stop and report the changed nature of the card rather than deleting a live arm.
Generated by Claude Code
os-litant commentedon Sep 5, 2026
os-dev-report
{ "issue": 15638, "status": "needs_decision", "branch": "claude/issue-15638-ui-plugin-legacy-arm", "pr": null, "premise_still_valid": false, "summary": "PREMISE FALSIFIED — the arm is LIVE, not unreachable, so no code was deleted and no PR was opened. The card assumes PluginSchema refuses ui-plugin on every path reaching hono-plugin.ts:508. Measured: PluginSchema has ZERO runtime callers repo-wide (every reference is a test, a comment, or the declaration itself), so it never parses a kernel plugin object at all. The real path is: stack config plugins is z.array(z.unknown()) (spec/src/stack.zod.ts:724, no refusal) then serve.ts:3855 kernel.use(pluginToLoad), then PluginLoader.toPluginMetadata which is a CAST not a copy (plugin-loader.ts:367-375, its own comment: do not use object spread as it destroys the prototype chain), then validatePluginStructure which checks name, init and semver ONLY and never reads type (plugin-loader.ts:378-390), then stored verbatim in kernel.plugins, then read by the arm. The only ui-plugin refusal anywhere on this path is the TypeScript Plugin.type compile-time type, which does not run at boot and does not cover the population this block serves: there are ZERO in-repo producers of type ui or of staticPath (the only matches are PluginSchema's own describe strings), so every plugin this auto-discovery block exists for is authored OUTSIDE this repo and never meets this compiler — and kernel.use(plugin as any) is a shipped in-repo pattern (cli/src/utils/schema-migrate.ts:346,349). Per triage's hard prerequisite step 3 and dispatch Zone 2 rule 2, this changes the KIND of the card: it is a real legacy-tolerance seam belonging to the ADR-0087 conversion layer, not a bare disjunct to delete, and removing it would be a published-behaviour change. Handed back for re-triage rather than closed as remove-only.", "clause_2_declaration": { "judged_from": "the DELIVERED diff, which is EMPTY — zero files, zero lines; git status --porcelain returns nothing against BASE 2e357650306.", "content_limb": "NO. The delivered diff adds no key to any published payload, touches nothing under packages/spec/src/**, and moves no accept/reject verdict, because it changes nothing at all.", "conformance_limb": "NO. The delivered diff re-selects no input class between two already-published verdicts on any shipped face, because it changes nothing at all.", "forward_looking_if_the_removal_were_ever_made": "BOTH limbs would be YES, and the conformance limb decisively so: deleting the disjunct re-selects the input class 'plugin objects declaring type ui-plugin with staticPath' between two already-published verdicts on a shipped face — today MOUNTED at /slug plus /slug/*, afterwards NOT MOUNTED. That is not a close call graded YES under the grade-YES-when-unclear doctrine; it is measured below. Whoever implements this owes needs:contract-review." }, "reachability_measurement": { "harness": "packages/plugins/plugin-hono-server/src/ui-plugin-reachability.probe.test.ts — a TEMPORARY probe, run against UNMODIFIED origin/main 2e357650306 and then deleted, so nothing ships. It registers a plugin object through the REAL ObjectKernel.use() and drives the real HonoServerPlugin init() and start(), observing the routes handed to rawApp.get. This package's vitest.config.ts aliases @objectstack/core and @objectstack/spec to SOURCE, so the probe measures source directly with no dist staleness risk; @objectstack/types resolves through exports to dist, which is why the dependency closure was built first (that build is also what the first probe run failed on, loudly, rather than silently measuring nothing).", "readings": [ "A. kernel.use() ACCEPTS type ui-plugin and stores it verbatim — PROBE A stored type = [\"ui-plugin\"] · PROBE A staticPath survived = [\"/tmp/legacy-console-dist\"]", "B. the legacy value REACHES the arm and mounts — PROBE B routes = [\"/legacy-console\",\"/legacy-console\",\"/legacy-console/*\",\"/legacy-console/*\"]", "C. control, the modern value mounts identically — PROBE C routes = [\"/console\",\"/console\",\"/console/*\",\"/console/*\"]", "D. control, a non-UI type mounts nothing — PROBE D routes = []", "E. the documented authoring path accepts what the named schema refuses — PROBE E defineStack error = null · PROBE E PluginSchema.safeParse success = false | code = \"invalid_value\"", "Suite result: Tests 5 passed (5); os-verify-lock VERDICT command-exit 0." ], "the_control_stated": "What this reading would have looked like if the arm had NOT been reachable, i.e. if the card were right: probe A would have thrown from kernel.use() or shown a stored type that is not ui-plugin (normalized or dropped); probe B would have returned [] — byte-identical to control D, which is exactly what a non-UI type produces today; and probe E's defineStack would have thrown, agreeing with the invalid_value PluginSchema returns on the same value in the same run. Control D is the calibration that makes B mean something: the harness is demonstrably capable of producing an empty result, so B's four route registrations are caused by the arm and not by a harness that mounts everything. Control C is the other half: the observable does fire for the value everyone agrees is live, so an empty B would have been a real absence rather than a broken probe. E is the sharpest single reading, because the SAME value in the SAME run is accepted by the path (defineStack, strict) and refused by the schema the card names (PluginSchema) — that split IS the finding.", "what_is_not_claimed": "I did not measure whether any plugin in the wild actually ships type ui-plugin; reachability is about whether the path admits the value, and it does. I also did not measure the config-object-without-init sub-path, which serve.ts:3838 wraps into AppPlugin — that wrapping likely re-stamps type, and it is not the shape a real UI plugin takes, since a UI plugin has init." }, "tests": "No product code changed, so no gate union is owed. Derivation over the DELIVERED (empty) diff, exit code captured BEFORE any pipe: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands returned EXIT=2, verdict line verbatim: \"dispatch-gates: this branch changes nothing against 'origin/main' (merge base 2e3576503) — nothing to derive.\" Read honestly: exit 2 here is the tool declining an empty changeset, which is NOT a green gate reading and NOT a NOT-WIRED fault — there is simply nothing of mine to measure. For forward planning I also derived the HYPOTHETICAL surface a removal would carry: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands packages/plugins/plugin-hono-server/src/hono-plugin.ts returned EXIT=0 with 39 runnable commands, asserted against its own line \"Reconciliation — 39 famil(ies)\" from the same derivation run without --commands (EXIT=0): 39 printed equals 39 declared. The Artifact rosters block was read SEPARATELY from that run: 37 families whose silent verdict is a fact about a LIST and never a clearance, 3 of them (check:authz-resolver, check:error-code-casing, check:filter-alias-parity) keeping their roster under packages/ where the hypothetical path sits, so their silence is evidence in NEITHER direction there. Per the dispatch warning I read the pnpm-spelled check:partof-closing-keyword and check:single-claim-paths rows as grading the checker's own fixtures via --self-test, not any diff (#16030). 6 further families take a value from the workflow and are marked NOT MEASURED by the tool itself. The only commands I actually ran were the probe and the dependency-closure build, both through scripts/pm/os-verify-lock.sh; both printed VERDICT command-exit 0.", "changeset_level": "Not applicable — nothing is delivered, so there is no changeset. Recorded for whoever implements the conversion: the removal IS user-visible (a plugin type that loads today and would not afterwards), and check-changeset-no-major refuses major during the launch window, so it must be carried by a BREAKING banner plus an ADR-0087 disposition marker, never by the level. Read off the CATEGORIES const at scripts/check-adr-0087-registration.mjs:460-466 and NOT its prose header, the five accepted markers are: unpublished, already-registered, no-migration-prescription, runtime-interface-only, type-surface-only.", "skill_applicability": "The PM asked whether the spec-property-retirement skill applies, and saying which it is was itself asked for. It does NOT apply as written: its subject is an authorable PROPERTY under packages/spec/src/** with a liveness ledger, generated baselines, forms and docs behind it. This is a consumer-side runtime disjunct on a VALUE of an existing property, living in a plugin package with no spec surface of its own, so none of the ledger, baseline, form or generated-doc machinery it prescribes has anything to bind to. What does carry over is its ADR-0087 conversion half — which is exactly where the falsified premise lands this card.", "mcp_calls": "2 — one claim comment, one report comment. Repo-scoped REST was probed first and is 403 for this session (\"GitHub access is not enabled for this session\"), so the card body and every comment were read through the zero-quota public payload channel instead of MCP.", "open_questions": [ { "question": "The ui-plugin arm is live, not dead. Which shape should it take: keep the tolerance and DECLARE it, or remove it and accept a published-behaviour break?", "options": [ "A. Declare it as an ADR-0087 conversion entry. The arm stays functional but stops being an undeclared second contract: the legacy value is converted to ui at a named seam that warns, and the retirement becomes visible to plugin authors instead of silent. Cost is a conversion entry plus its pin. This is what triage prescribed for exactly this outcome.", "B. Delete the disjunct anyway, accepting that any external plugin shipping type ui-plugin silently stops being served — note it will not fail loudly: it simply never mounts, and the boot log says nothing. Requires a BREAKING banner plus an ADR-0087 marker and needs:contract-review, and is a maintainer call rather than a dev call.", "C. Close the loop at the producer instead: make the boot path actually ENFORCE PluginSchema (or at least the closed type set) at kernel.use(), which would make the arm genuinely unreachable and turn its deletion into the zero-behaviour-change edit the card assumed. Much larger blast radius — it would begin refusing plugin objects the runtime accepts today — and it is really the separate finding recorded below." ], "recommendation": "A, because it is the only option that does not move a published verdict without a maintainer deciding to move it. The card's own reason for removal (Prime Directive #12: a lenient consumer-side disjunct is a second de-facto contract) is fully satisfied by A, since a declared conversion is not a second contract — it is the first one, written down. B is defensible but it is a contract decision and must not be taken as a cleanup. Suggested routing: re-triage this card as an ADR-0087 conversion card, domain:cli lane, priority unchanged, with needs:contract-review attached by the seat." } ], "out_of_scope_findings": [ "NOT FILED — handed back with its measurement per dispatch instruction 5, so the seat decides whether it becomes a card. PluginSchema (packages/spec/src/kernel/plugin.zod.ts:119) has ZERO runtime callers repo-wide. Measurement: grepping PluginSchema across all .ts/.mjs/.mts excluding node_modules and dist returns only the declaration, its type alias, tests (spec/kernel/plugin.test.ts, core/plugin-type-closed-set.test.ts, rest/plugin-type-closed-set.pin.test.ts, the retirement tests) and prose comments — no production import anywhere. The boot path validates plugins with validatePluginStructure (name, init, semver) and nothing else. This is an ADR-0049-shaped finding one level above card 15638: a declared schema the runtime never enforces, which is precisely why 15638's premise failed. It is also the ROOT CAUSE of that failure, so filing it would likely supersede or reframe 15638 rather than sit beside it. Severity not asserted.", "NOT FILED — same hand-back. staticPath and slug have ZERO producers in this repo. Measurement: grepping staticPath across all .ts excluding node_modules and dist returns exactly four lines — the PluginSchema declaration and the three reads inside the hono arm itself. So the entire UI auto-discovery block in hono-plugin.ts start(), BOTH disjuncts included, serves only externally authored plugins and is exercised by no in-repo plugin and no test. That is why the arm looked dead to a reader scanning this repo: it is dead FROM THE INSIDE and live FROM THE OUTSIDE. Worth a card of its own for the missing coverage, independent of how 15638 is decided." ] }Generated by Claude Code
os-litant commentedon Sep 5, 2026
维护者速读 — this card's premise is FALSIFIED, and the shape it becomes is your call
Dispatched by the
domain:cliexecution PM seat (#6024). No code was written and no PR was opened, because the measurement contradicted the card. Two-minute version:What the card assumed, and what is actually true
The card says the
ui-pluginarm inhono-plugin.tsis unreachable becausePluginSchemarefuses that value. Measured:PluginSchemahas zero runtime callers repo-wide — every reference is a test, a comment, or the declaration itself. It never parses a kernel plugin object at all.The real boot path refuses nothing:
The only
ui-pluginrefusal anywhere on that path is the compile-timePlugin.typeunion — which does not run at boot, and does not cover the population this block serves: there are zero in-repo producers oftype: 'ui'or ofstaticPath, so every plugin this auto-discovery block exists for is authored outside this repo and never meets that compiler.kernel.use(plugin as any)is itself a shipped in-repo pattern.The arm is dead from the inside and live from the outside. That is why it looked dead to a reader scanning this repo.
The measurement, and the control that makes it mean something
A temporary probe ran against unmodified
origin/main2e357650306through the realObjectKernel.use()and the realHonoServerPlugininit()/start(), then was deleted — nothing ships.kernel.use()acceptsui-pluginand stores it verbatim;staticPathsurvives/legacy-console,/legacy-console/*/console,/console/*)[])defineStackaccepts it,PluginSchema.safeParserefuses it (invalid_value)⭐ D is the calibration that makes B evidence. The harness is demonstrably capable of producing an empty result, so B's route registrations are caused by the arm and not by a probe that mounts everything. C is the other half — the observable does fire for the value everyone agrees is live, so an empty B would have been a real absence rather than a broken probe. E is the sharpest single reading: the split between the path and the named schema is the finding.
Stated explicitly, and this is why the reading is trustworthy: if the card had been right, A would have thrown or shown a normalised/dropped type, and B would have returned
[]— byte-identical to D.Not claimed: whether any plugin in the wild actually ships
type: 'ui-plugin'. Reachability is about whether the path admits the value; it does. The config-object-without-initsub-path was also not measured.Your decision
Removing the disjunct is a published-behaviour change, not a cleanup. Clause ② on a hypothetical removal is yes on both limbs, decisively on the conformance limb: the input class "plugin objects declaring⚠️ And it would not fail loudly: such a plugin simply never mounts, and the boot log says nothing.
type: ui-pluginwithstaticPath" moves between two already-published verdicts on a shipped face — today mounted at/slugand/slug/*, afterwards not mounted.**BREAKING**banner plus an ADR-0087 disposition marker (the level cannot carry it —check-changeset-no-majorrefusesmajorduring the launch window) andneeds:contract-review.PluginSchema(or at least the closed type set) atkernel.use(), which would make the arm genuinely unreachable and turn its deletion into the zero-behaviour-change edit the card assumed. Much larger blast radius — it would begin refusing plugin objects the runtime accepts today.The dev agent recommends A, on the grounds that it is the only option that does not move a published verdict without you deciding to move it, and that the card's own reason for removal (Prime Directive #12 — a lenient consumer-side disjunct is a second de-facto contract) is fully satisfied by A, since a declared conversion is not a second contract; it is the first one, written down.
⛔ This seat does not grade that choice — all three move or preserve a published verdict, which is yours. The card is marked
needs-user-decision+pm:retriage;pm:queueand the dispatch state are cleared so no seat picks it up as remove-only work.Two findings handed back, filed separately
dateRangeresolves on the UTC calendar, not the process-local one (#15825) #16047 —PluginSchemahas zero runtime callers; the boot path validatesname/init/semver and nothing else. This is the root cause of this card's failed premise and may supersede or reframe it.sourcethrougherrors[]instead of a raw TypeError #16048 —staticPathandslughave zero producers in this repo, so the entire UI auto-discovery block (both disjuncts) is exercised by no in-repo plugin and no test.One answered question
The
spec-property-retirementskill does not apply as written: its subject is an authorable property underpackages/spec/src/**with a liveness ledger, generated baselines, forms and docs behind it. This is a consumer-side runtime disjunct on a value of an existing property, in a plugin package with no spec surface of its own — none of that machinery has anything to bind to. Its ADR-0087 conversion half does carry over, which is exactly where option A lands.Generated by Claude Code
os-litant commentedon Sep 5, 2026
Correction to the 维护者速读 above: the two handed-back findings are #16049 and #16050, not #16047 / #16048.
Those two numbers were written into that comment before the issues were created, and GitHub assigned different ones. The links there point at unrelated issues. The correct pair:
PluginSchemahas zero runtime callers; the boot path validatesname/init/semver and nothing else. The root cause of this card's falsified premise, and upstream of this card's options B and C.staticPathandslughave zero producers in this repo, so the entire UI auto-discovery block (both disjuncts) is exercised by no in-repo plugin and no test.Everything else in that comment stands: the measurement, the controls, the clause ② reading, and the three options are unchanged. My error was predicting issue numbers instead of creating first and citing after.
Generated by Claude Code
24 remaining items
objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 6 (serial)
Session:
session_01TnPAC1UsTGfHPXVUCL6iLnBranch:
claude/issue-15638-ui-plugin-arm-deleteWorktree:
objectstack-issue-15638Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/plugins/plugin-hono-server/src/hono-plugin.ts(theui-plugindisjunct and its "Support legacy" comment only),packages/plugins/plugin-hono-server/src/ui-plugin-auto-discovery.pin.test.ts(pin C, #16050's last item), and one.changeset/15638-*.md.packages/core/src/plugin-loader.ts(on spec-lane PR #19637) andpackages/spec/**are read-only.packages/plugins/plugin-hono-server/CHANGELOG.mdandpackage.jsonbelong to the Version Packages PR. Stop on breach and explain in the reportContainer & model:
S,mode:subagent,model: default judgment tier(this act'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/plugins/plugin-hono-server/src/hono-plugin.tsanswered: no path-derived mandate). A two-line deletion, but it sits behind a ruled reachability probe whose first round falsified the card's premise, so it gets the default judgment tier.Clause-②: no
Thread-read: 5774936275
Serial constraints cleared:
the maintainer's order in this seat's session (「继续派发」) moves the lane on once a card's PR is in the merge queue: R5's PR #20074 (#19874) is in the merge queue, and its surface (packages/runtime) shares no file with this one. The file lists of all 22 open PRs were read in this act. None touches packages/plugins/plugin-hono-server/src/; plugin-loader.ts is on spec-lane PR #19637 and is read-only here; CHANGELOG.md and package.json are on the Version Packages PR #17076, outside this surface. The previous claim (5555005915) was released dead at 5774936275, and the card is unassigned in pm:queue. #19328, the queue's only p2, stays fenced behind PR #19637, so the p3s go by age. The NORTH-STAR 优先级 3 pre-pick read at 66960564: the domain:cli pm:queue holds no open P0/P1Taken 2026-09-25T01:59Z,
origin/main66960564.Clause-②: nois declared from the delivered diff, as the director's execution terms (5644357902) require. #16049 refusesui-pluginatkernel.use(), so deleting the unreachable disjunct moves no accept or reject behaviour. The ruling makes the reachability probe a hard precondition: if it shows the arm is still reachable, the dev stops and reports instead of deleting. The dev posts no second claim; this comment is its identity.objectstack-fleet commentedon Sep 25, 2026
os-dev-report
{ "issue": 15638, "status": "done", "branch": "claude/issue-15638-ui-plugin-arm-delete", "pr": "https://github.com/objectstack-ai/objectstack/pull/20086", "session": "session_01TnPAC1UsTGfHPXVUCL6iLn — this run's id (subagent inside the domain:cli PM seat's session, so the parent's)", "premise_still_valid": true, "summary": "The reachability probe ran first, on the unmodified tree at 66960564 (both kernels, real use() plus the real HonoServerPlugin init/start). A declared type 'ui-plugin' is refused at use() on ObjectKernel and LiteKernel with PLUGIN_CONTRACT_VIOLATION at 'type' and is never stored; the controls behaved as expected (ui mounts 4 routes, driver mounts none). The static census found that the arm's map has exactly two writers, both behind assertPluginContract, and no in-repo producer of 'ui-plugin' as a type. The disjunct and its 'Support legacy' comment are deleted, so the condition is now plugin.type === 'ui' && plugin.staticPath. #16050's pin C is filled in the file's own harness: C1 reads #16049's refusal on both kernels (envelope parity), and C2 proves behaviourally that the arm is gone on both kernels, with an in-case firing control. An ablation that restores the disjunct turns only C2 red. One residual is flagged, not hidden (open_questions[0]): an object admitted as 'ui' that rewrites its own type afterwards used to reach the arm and no longer mounts. I read that as a contract bypass, not a path, and kept Clause-② no. The assignee was os-steve at start (the PM's); I wrote no assignee.", "tests": "All at 7363f0f4, exit codes captured before any pipe. (1) Probe on unmodified 66960564, temporary file deleted and never committed: ObjectKernel ui-plugin refused=true stored=null message='Failed to load plugin: @os-probe/legacy-console - PLUGIN_CONTRACT_VIOLATION: ... at 'type': Invalid option: expected one of \"standard\"|\"ui\"|\"driver\"|\"server\"|\"app\"|\"theme\"|\"agent\"|\"objectql\"'; LiteKernel refused=true code=PLUGIN_CONTRACT_VIOLATION stored=null; ui mounts 4 routes and driver mounts [] on both kernels; an admitted ui entry flipped to ui-plugin mounts 4 routes on both (the arm was live code if reached). 9 passed, VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-hono-server test: 26 files, 314 passed. (3) Pin file verbose: 20 passed (was 17 + 1 todo). (4) typecheck exit 0; tsconfig.test.json --listFilesOnly includes the pin file (1 hit, 26 test files). (5) Ablation on the committed tree via scripts/ablation-replace.mjs in wrap mode (no dist involved: the pin imports ./hono-plugin, which is source). Anchor 'if (plugin.type === 'ui' && plugin.staticPath) {' x1 to x0, replacement x0 to x1, blob 201c5866 to ca844899. Result: 2 failed and 18 passed; both failures are C2 with 4 routes instead of []. The direction was the expected one (turned red). The restore leg proved blob == HEAD 201c5866 and git diff HEAD empty; an independent hash-object and git diff HEAD --quiet agree. (6) Gates: dispatch-gates derived 60 commands on the real diff, identical to gates-from-pm.txt; all 60 run, all exit 0. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET), then 0 after turbo build of ./packages/* and ./packages/*/* (72/72 tasks, lock held 502s). --ran reconciliation: 60/60, a DERIVED zero NOT-MEASURED. (7) pnpm lint full union exit 0. (8) CI at report time, a single read with no polling: 31 check-runs, 23 in_progress, 5 success, 3 skipped, 0 failing. Status: in_progress.", "gates": { "node scripts/check-adr-0087-registration.mjs --base origin/main": "exit 0 · ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).", "node scripts/check-adr-0087-registration.mjs --self-test": "exit 0 · ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)", "node scripts/check-changeset-no-major.mjs --base origin/main": "exit 0 · ✓ This diff introduces no `major` bump.", "node scripts/check-changeset-no-major.mjs --self-test": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot", "node scripts/check-ci-filter-parity.mjs": "exit 0 · OK: all 184 declared cross-package glob(s) (131 unique) are covered by `core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` j", "node scripts/check-closing-keyword-parity.mjs": "exit 0 · check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 94", "node scripts/check-closing-keyword-parity.mjs --self-test": "exit 0 · ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.", "node scripts/check-comment-mask-adoption.mjs": "exit 0 · OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached ", "node scripts/check-comment-mask-adoption.mjs --self-test": "exit 0 · PASS check-comment-mask-adoption --self-test (0 failure(s))", "node scripts/check-comment-mask-corpus.mjs": "exit 0 · ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7126 files, 0 disagree, 0 unparseable, 78.0s (comparator self-test: 26 cases pass).", "node scripts/check-empty-changeset.mjs --base origin/main": "exit 0 · ✓ No changeset from the merge base modified or deleted by this diff (#17712).", "node scripts/check-empty-changeset.mjs --self-test": "exit 0 · ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)", "node scripts/check-keyed-text-bounds.mjs": "exit 0 · ✓ check:keyed-text-bounds: 112 *.object.ts files under packages/** + apps/** + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 25", "node scripts/check-keyed-text-bounds.mjs --self-test": "exit 0 · PASS check-keyed-text-bounds --self-test (0 failure(s))", "node scripts/check-platform-object-tenancy-census.mjs": "exit 0 · ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the machinery's reach, 26 outside it, every exclusion explaine", "node scripts/check-platform-object-tenancy-census.mjs --self-test": "exit 0 · ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)", "node scripts/check-plugin-teardown-shape.mjs": "exit 0 · ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 6557 source(s) under packages/**; every teardown-shaped method (stop / shutdown / cl", "node scripts/check-plugin-teardown-shape.mjs --self-test": "exit 0 · ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay gree", "node scripts/check-registry-log-declared.mjs": "exit 0 · OK: 74 vitest-running package(s) walked, 9 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silen", "node scripts/check-registry-log-declared.mjs --self-test": "exit 0 · self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.", "node scripts/check-rest-log-spy-declared.mjs": "exit 0 · OK: 29 of 196 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.", "node scripts/check-rest-log-spy-declared.mjs --self-test": "exit 0 · ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files", "node scripts/check-system-context-census.mjs": "exit 0 · check-system-context-census: OK — 112 elevation read sites in 20 packages across 45 files, living in 93 symbol(s); the page cites 106 symbol(s) agains", "node scripts/check-system-context-census.mjs --self-test": "exit 0 · check-system-context-census --self-test: all cases passed", "node scripts/check-tenant-audit-census.mjs": "exit 0 · ✓ check-tenant-audit-census: OK -- 227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more un", "node scripts/check-tenant-audit-census.mjs --self-test": "exit 0 · ✓ check-tenant-audit-census self-test: 27 cases pass (drift on a dropped ledger row, on an edited page count, on an edited ledger total and on a lost ", "node scripts/check-undeclared-dep-imports.mjs": "exit 0 · ✓ check:undeclared-dep-imports: 81 workspace packages under packages/** + apps/** + examples/**, 2586 non-test src files, 2083 @objectstack/* specifie", "node scripts/check-undeclared-dep-imports.mjs --self-test": "exit 0 · PASS check-undeclared-dep-imports --self-test (0 failure(s))", "node scripts/docs-audit/check-affected-docs.mjs": "exit 0 · ✓ affected-docs self-test: 605 cases pass.", "node scripts/docs-audit/check-drift-comment.mjs": "exit 0 · ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).", "node scripts/pm/release-rehearsal-clone.mjs --self-test": "exit 0 · ✓ self-test passed", "pnpm --filter @objectstack/spec run check:duration-unit-keys": "exit 0 · ✓ check:duration-unit-keys — 204 unit-declaring numeric key(s) across 2583 source file(s) all carry their unit in the key name (or in a sibling `unit`", "pnpm check:changeset-gate-self-tests": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot", "pnpm check:cross-package-test-inputs": "exit 0 · OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked ", "pnpm check:doc-authoring": "exit 0 · ✓ doc authoring guard: sibling-package prose ids hold the baseline — 815 pinned site(s) across 231 file(s), 91458 string(s) read in 1252 parsed source", "pnpm check:driver-memory-census": "exit 0 · check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consume", "pnpm check:dts-closure": "exit 0 (re-run post-build) · check-dts-closure: 72 built package(s) swept - 164/164 declared declaration file(s) present across 72 package(s)", "pnpm check:dual-build-cjs-loads": "exit 0 (post-build; first run exit 3 PREREQUISITE NOT MET) · ✓ check:dual-build-cjs-loads — 104 published require entry point(s) across 67 package(s) load; 620 emitted CommonJS file(s) parse", "pnpm check:engine-double-contract": "exit 0 · check-engine-double-contract: OK — 902 pinned, 133 in the DEBT ledger, 3 exempt.", "pnpm check:gitlink-declared": "exit 0 · check-gitlink-declared: OK (9491 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare", "pnpm check:issue-citations": "exit 0 · ✅ check-issue-citations --self-test: grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contra", "pnpm check:lean-entry-closure": "exit 0 (post-build; first run exit 3 PREREQUISITE NOT MET) · ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.", "pnpm check:logger-receiver-detach": "exit 0 · OK every log channel keeps its receiver: 2853 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).", "pnpm check:nul-bytes": "exit 0 · check-nul-bytes: OK (scanned 9484 text file(s) -- 9484 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).", "pnpm check:objectql-double-limit": "exit 0 · OK ObjectQL double `limit` conformance holds: 407 double(s) graded, 209 apply the caller's bound or refuse it loudly.", "pnpm check:objectui-changeset": "exit 0 · ✓ objectui-range --self-test: all checks passed", "pnpm check:org-identifier": "exit 0 · check-org-identifier: OK (2875 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias).", "pnpm check:page-declaration-shape": "exit 0 · check-page-declaration-shape: OK — 34 page entries across 2865 sources under packages/**, examples/**, apps/** all reach the kernel through a discover", "pnpm check:pm-changeset-deadline-census": "exit 0 · ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, ", "pnpm check:published-files": "exit 0 (re-run post-build) · ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG", "pnpm check:query-options-erasure": "exit 0 · ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file ", "pnpm check:refd-timer-probe": "exit 0 · OK check-refd-timer-probe: 7121 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowher", "pnpm check:slot-lookup": "exit 0 · ✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is", "pnpm check:sourcemap-no-sources-content": "exit 0 (re-run post-build) · check-sourcemap-no-sources-content: 69 built package(s) swept - 506 map(s), none embed source text.", "pnpm check:test-source-alias": "exit 0 · check-test-source-alias OK — 74 packages with tests scanned; 61 registered as still resolving a workspace dep through `dist/`; 49 published subpath(s)", "pnpm check:tier-file-adoption": "exit 0 · OK: 81 workspace package(s) walked, 70 nightly-tier test file(s) on disk (70 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.", "pnpm check:type-check-coverage": "exit 0 (re-run post-build) · check-type-check-coverage: OK — 77/81 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, https://github.com/", "pnpm check:type-check-debt": "exit 0 (post-build; first run exit 3 PREREQUISITE NOT MET) · check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured in 22.5s, 53 raw tsc error(s) total, none above its recorded number.", "pnpm check:watch-hint-literal": "exit 0 · ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL", "pnpm check:where-matcher": "exit 0 · ✓ where-matcher conformance holds: 430 matcher(s) discovered, 430 answer the combinator battery correctly or refuse it loudly (280 refuse).", "pnpm --filter @objectstack/plugin-hono-server test": "exit 0 · Test Files 26 passed (26) · Tests 314 passed (314) · os-verify-lock VERDICT command-exit 0", "pnpm --filter @objectstack/plugin-hono-server exec vitest run --maxWorkers=2 --reporter=verbose src/ui-plugin-auto-discovery.pin.test.ts": "exit 0 · Tests 20 passed (20), 0 todo; C1, C2 on ObjectKernel, C2 on LiteKernel listed · VERDICT command-exit 0", "pnpm --filter @objectstack/plugin-hono-server typecheck": "exit 0 · check:test-typecheck: OK — @objectstack/plugin-hono-server's test layer compiles under tsconfig.test.json; 0 file(s) / 0 error(s) · VERDICT command-exit 0", "pnpm lint (full union, eslint . --no-inline-config, at 7363f0f4)": "exit 0 · no findings printed, 114s", "node scripts/check-changeset-fixed.mjs (roster under .changeset)": "exit 0 · ✓ .changeset/config.json \"fixed\" group is in sync with 70 public workspace packages.", "pnpm check:authz-resolver (roster under packages)": "exit 0 · ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.", "pnpm check:error-code-casing (roster under packages)": "exit 0", "pnpm check:filter-alias-parity (roster under packages)": "exit 0 · check:filter-alias-parity: OK (4 transport spelling(s) of the where slot, identical on both sides)", "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-with-exits.list": "exit 0 · ✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero — all 60 recorded an exit code and none of them is 3).", "ablation: node scripts/ablation-replace.mjs (restore the deleted disjunct) -- vitest pin file": "exit 1 (expected red) · Tests 2 failed | 18 passed (20); both failures C2 (ObjectKernel, LiteKernel): expected [ '/console-fixture', …(3) ] to deeply equal [] · ok restored: blob == HEAD (201c58669352) and git diff HEAD is empty" }, "line_budget": "n/a — no governed or line-ratcheted surface touched", "files_changed": [ ".changeset/15638-ui-plugin-arm-delete.md (+11/-0, new; @objectstack/plugin-hono-server patch, carries 'Clause-②: no')", "packages/plugins/plugin-hono-server/src/hono-plugin.ts (+1/-2)", "packages/plugins/plugin-hono-server/src/ui-plugin-auto-discovery.pin.test.ts (+100/-42)" ], "deviations": [ "Pin file edits beyond the C block, all inside the claimed file: the header's group list names C; the KernelUnderTest docblock's stale clause (the kernels 'do NOT share whether use() validates', false since #16721) is corrected and C is listed; observe() gains an optional beforeStart hook, used only by C2.", "Rather than recording the three PREREQUISITE gates NOT MEASURED, I ran a full workspace build under the lock (turbo 72/72, 8m22s) and measured them.", "Commit trailers: the harness attribution reminder asked for a model-named Co-Authored-By. I used the order's and AGENTS.md's model-free pair; the pre-push check:commit-card-trailers passed.", "The precondition was passed on a stated reading rather than by stopping, because the one residual the probe found (post-admission self-rewrite) was judged a bypass, not a path. The reading is in open_questions[0]; if the seat rules otherwise, the draft PR should be held." ], "mcp_calls": "0 — no MCP tool of any kind was called", "api_writes": "2 REST writes, both through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create via scripts/pm/with-fleet.sh --via dispatch (POST /repos/objectstack-ai/objectstack/dispatches; relay run 36087627029 executed POST /repos/objectstack-ai/objectstack/pulls, draft forced, which opened #20086); (2) this os-dev-report comment via scripts/pm/post-stamped.mjs --comment=15638 (POST /repos/objectstack-ai/objectstack/issues/15638/comments). Zero label writes, zero assignee writes, no PATCH to the PR body. git push x2 (the empty branch, then 7363f0f4) are not REST writes.", "open_questions": [ { "question": "The probe found one residual route to the removed arm. The contract validates at use() and stores the plugin object by reference, so an object admitted as type 'ui' that rewrites its own type to 'ui-plugin' before start() was mounted by the disjunct, and after this PR it is not (C2 pins the after-state). Does that count as 'reachable' under the ruling's precondition, and as an input class under Clause-②'s conformance limb?", "options": [ "A — No. Reachability means admission through a published kernel's use(), the standard this pin file already applies (on landed text) to the slug derivation and the staticPath conjunct. The residual is a bypass of the contract that works equally for every key the contract checks. Clause-② no stands; no BREAKING banner or ADR-0087 marker; the PR proceeds.", "B — Yes. The ruling's stop applies: the PR is held, and a re-declaration of Clause-② yes with a BREAKING banner plus an ADR-0087 marker is decided by the seat or the maintainer, not by the dev." ], "recommendation": "A. Actual business need: measured, no in-repo code writes a registered plugin's type, and an external author would have to declare 'ui' to the contract and then write the retired spelling in its place; that is not a real population. Long-term soundness: validation-only with reference storage is the documented design (packages/core/src/plugin-contract.ts), and this file already treats reachability as use() admission. Preventing AI mistakes: keeping any consumer that honours the retired spelling is the lenient-alias shape Prime Directive #12 forbids; C2 now pins that the block honours only 'ui', which makes the bypass visibly inert instead of silently tolerated. Startup focus: the ruling retires the spelling immediately with no staged window, and A is that. The changeset discloses the residual in its own bullet either way, and the contract reviewer, owed anyway for the .changeset face, sees it." } ], "out_of_scope_findings": [ "carrier: #16721 (recorded there) · noted in PR Acceptance notes, not filed — the plugin.slug || name-derivation and the && plugin.staticPath conjunct in the same condition are likewise unreachable through either kernel's use(); not ruled here, left untouched.", "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the post-admission rewrite residual is a property of the documented validation-only design (plugin-contract.ts: parse output discarded, object stored by reference) and applies to every contract key; not class a (no admission path reproduces it), not b (the contract is validation-at-use by its own text), not c (a kernel plugin object is code, not stored, re-authored metadata).", "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the default/isDefault redirect in the same block is still unpinned (the pin file header already names this)." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 25, 2026
In-seat review: ACCEPT, PR #20086 at head
7363f0f48b3e47020b98e3d16e1e97ac8c61aaeb, and the dev's open question answered: Adomain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-25T02:51Z · reviewed on GitHub andorigin/mainReview checklist
main. The first line isFixes #15638, the only closing binding, and the second is a line-initialClause-②: no, copied from the claim. [finding] The hono UI auto-discovery block has no in-repo producer and no test — staticPath and slug are written nowhere but the block that reads them #16050 is named without a closing keyword ("[finding] The hono UI auto-discovery block has no in-repo producer and no test — staticPath and slug are written nowhere but the block that reads them #16050 stays open here; the seat handles it at landing"). One footer; no model identifier.hono-plugin.ts, the pin file, and one changeset.66960564, with realuse()and the realHonoServerPlugininit/start.type: 'ui-plugin'is refused atuse()onObjectKernelandLiteKernelalike, withPLUGIN_CONTRACT_VIOLATION … at 'type', and is never stored. That is [finding] PluginSchema has zero runtime callers — the boot path validates name, init and semver only, so the declared plugin contract is never enforced #16049's refusal.uimounts 4 routes anddrivermounts none.assertPluginContract, and there is no in-repo producer ofui-plugin.(plugin.type === 'ui' || plugin.type === 'ui-plugin') && plugin.staticPathbecomesplugin.type === 'ui' && plugin.staticPath, and the "Support legacy" comment is deleted. There is no bespoke hint for the retired spelling (ruling item 3).it.todo, in the file's own harness:LiteKernel.use()andObjectKernel.use()refuse a declaredui-pluginwith one envelope, and the test asserts thecode.ui-pluginbeforestart()mounts nothing, with an in-case firing control.@objectstack/plugin-hono-serverpatch,Clause-②: no. It discloses the one residual in its own bullet.dispatch-gates --ranaccounts for all 60 families, 60 run and 0 NOT-MEASURED (the dev built the workspace to measure the three PREREQUISITE gates rather than recording them absent).pnpm lint(full union) exit 0, typecheck exit 0.packages/spec/src/**in the diff; the declaration is notyes.7363f0f4: still running at this review. The ready flip waits for full green on this head.The dev's open question, answered by the seat: A. The probe found one residual: an object admitted as
uithat rewrites its owntypetoui-pluginbeforestart()was mounted by the removed disjunct, and now it is not.5644357902, item 1) exists so that no legitimately admitted plugin relies on the arm, and the probe proves that for admission through both published kernels.5557089123).Clause-②: nostands, and no BREAKING banner or ADR-0087 marker is owed. The changeset's disclosure bullet is the right place for the residual. ⛔ This is the ruling applied, not a new decision.Out-of-scope, noted in the PR (not filed): the
plugin.slug ||name derivation and the&& plugin.staticPathconjunct are likewise unreachable through either kernel'suse()(recorded on #16721). Thedefault/isDefaultredirect in the same block is still unpinned.objectstack-fleet commentedon Sep 25, 2026
LANDED —
3c48234b3e86fc4bd42b4a61e71fcf14ade5164a(PR #20086), verified by CONTENT onorigin/maindomain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-25T03:30Z338feda6, the merge's parent)origin/main'ui-plugin'inplugin-hono-server/src/hono-plugin.tsSupport legacyin the same fileplugin.type === 'ui'in the same fileit.todo(inui-plugin-auto-discovery.pin.test.ts'C1 …','C2 …') in the same file7363f0f4is ⛔ not an ancestor (exit 1); the control lega4ca69a9is an ancestor (exit 0). The commit carries the enqueue instant 2026-09-25T03:07:42Z.ui-plugindisjunct and its "Support legacy" comment are gone, and UI auto-discovery mountstype: 'ui'only.kernel.use(), now pinned on both kernels (C1).use(). The one residual, a post-admission self-rewrite, was answered as a bypass, not a path (review record5825883650).mainand it merged.Fixes #15638. In the same stroke as this comment,pm:dispatchedis stripped and the assignee cleared.fix(pm): read the ownership markers through the undecorated line, and…
docs(agents): a claim pre-check hit is a hint, not a claim (objectsta…
feat(pm): name the two measured claim near-miss forms so the sweep so…
fix(pm): the near-miss ownership census excludes seat posts and relea…
docs(adr-0026): spell the UI plugin package type `ui`, note the origi…
docs(adr-0026): caption the section 3.1 example as the proposed shape…
fix(plugin-hono-server): drop the retired ui-plugin arm from UI auto-…