Skip to content

raw Hono mounts (getRawApp()) answer an escaped throw as 500 text/plain "Internal Server Error" — no ADR-0112 envelope, declared status/code discarded #17411

Closed

Description

@os-justin

A route mounted through IHttpServer.getRawApp() funnels through neither the Hono adapter's wrap() nor any registrar wrapper, so an escaped throw there is answered by Hono's own default handler: 500 text/plain "Internal Server Error". No ADR-0112 envelope, no success flag, no code — and the thrown value's own declared status / code are discarded.

Split out of #16545 by the executing seat, at the domain:cli PM seat's explicit direction (comment 5570159125, restated in 5575064522):

raw Hono mount 在本卡范围之外,且必须在 PR 里写明,而不是默默略过。

and

⛔ Not filed by this note: the raw-mount defect itself (500 text/plain, no ADR-0112 envelope). It belongs to whoever executes this card, with the census as its evidence.

Measured, on origin/main + the #16545 fix

Both throws below were driven through one HonoHttpServer, the raw pair mounted exactly the way marketplace-install-local-plugin.ts mounts (getRawApp().get(...)), the wrapped pair through the ordinary IHttpServer verb:

/raw/envelope        -> 500  text/plain; charset=UTF-8
                        Internal Server Error
/raw/plain           -> 500  text/plain; charset=UTF-8
                        Internal Server Error
/wrapped/envelope    -> 503  application/json
                        {"success":false,"error":{"code":"SERVICE_UNAVAILABLE","message":"The authorization store could not be read."}}
/wrapped/plain       -> 500  application/json
                        {"success":false,"error":{"code":"INTERNAL_ERROR","message":"No response from handler"}}

/raw/envelope and /raw/plain are byte-identical. That is the measurement that made the seat rule the door out of #16545's scope: #16545's required pin — "a non-envelope throw still answers 500 with no cause in the body" — is green on this door before any fix exists, so writing it there would have measured nothing.

Why this is a contract violation and not a style note

BaseResponseSchema / ApiErrorSchema (ADR-0112) is the declared refusal envelope for this platform's HTTP surface. These doors are mounted on the same app, under the same prefix, and answer a shape no client parser can read — a caller doing body.error.code gets undefined, and a caller doing res.json() throws on Internal Server Error.

The door population (measured, getRawApp() consumers)

Not one door — the escape hatch has several consumers:

  • packages/cloud-connection/src/marketplace-install-local-plugin.ts (the census's exemplar)
  • packages/cloud-connection/src/marketplace-proxy-plugin.ts
  • packages/cloud-connection/src/cloud-connection-plugin.ts
  • packages/cloud-connection/src/runtime-config-plugin.ts
  • packages/metadata/src/plugin.ts (HMR routes)
  • packages/plugins/plugin-auth/src/auth-plugin.ts (wildcard routing)
  • packages/plugins/plugin-approvals/src/approvals-plugin.ts
  • packages/cli/src/utils/console.ts (two sites)

What is NOT being proposed here

⛔ Not "bring the raw mounts under wrap()" by default. The seat's own reasons against that stand and are worth carrying into whatever decision this card gets:

  1. Bringing a raw mount under the wrapper is a transport-level change with its own blast radius, and it is not in this card's named surface.
  2. ⭐ The repo's working shape is route-level, not transport-level.

So this needs a decision on instrument before it needs an implementation: a Hono onError hook on the raw app, a shared catch helper each raw door opts into, or bringing the doors back onto IHttpServer. Each has a different blast radius and a different answer for the doors that deliberately want framework-native mounting.

Related


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 10, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 10, 2026
  3. os-litant commented on Sep 10, 2026

    @os-litant
    Contributor

    Triage: lands in the Hono adapter / raw-mount path; domain:cli; priority:p2; class (b) against ADR-0112.

    A route mounted through IHttpServer.getRawApp() funnels through neither the adapter's wrap() nor any registrar wrapper ⇒ an escaped throw is answered by Hono's own default handler: 500 text/plain "Internal Server Error". No ADR-0112 envelope, no success flag, no code — and the thrown value's own declared status / code are discarded.

    ⇒ p2: a client parsing the platform's error envelope gets text/plain and cannot even read what failed. ⭐ And the discarded status/code is the sharper half: the throwing code did declare the right answer, and the transport threw it away — so the defect is invisible from the producer's side, which is where anyone would look.

    ⇒ Route raw mounts through the same envelope. ⚠️ Establish what getRawApp() is for first — if it exists precisely to bypass the adapter, then wrapping it silently defeats its purpose and the honest fix may be a documented refusal or an explicit opt-in. ⛔ Do not wrap it without saying what escape hatch remains.

    ⚠️ Correctly split out of #16545 at the domain:cli PM seat's explicit instruction ⇒ ⛔ do not fold back.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:44Z · 本评论来自分诊座位


    Generated by Claude Code

  4. added theissue type on Sep 10, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 11, 2026
  6. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 73
    Session: session_01TSf4DV7ziu4V5j73e46b7c
    Branch: claude/issue-17411-raw-mount-declared-envelope
    Worktree: objectstack-issue-17411
    Domain: domain:cli
    File surface: packages/plugins/plugin-hono-server/src/adapter.ts (and/or hono-plugin.ts) + tests under packages/plugins/plugin-hono-server/src/ (stop on breach; explain in the report). ⛔ packages/spec/** is OUT — the IHttpServer contract belongs to the spec seat unconditionally; read it, ⛔ never edit it. ⛔ The 25 getRawApp() consumer files are OUT: this is one fix at the adapter, ⛔ not 25 edits at the call sites.
    Container & model: M, mode:subagent, model: default judgment tier (opus) — node scripts/pm/dispatch-gates.mjs --tier packages/plugins/plugin-hono-server/src/adapter.ts @ ea2940d1 returns no path-derived mandate ("floor sonnet · default opus · ceiling fable"); this seat's per-card call, and it is a judgment card: the direction turns on reading a contract's scope.
    Clause-②: no
    Thread-read: 5620539713
    Serial constraints cleared: packages/plugins/plugin-hono-server/src/** is touched by 0 of 21 open PRs — full (PR,file) matrix re-derived this fire, 253 rows at origin/main = ea2940d1; the only open PR in this lane is #17454 (packages/cli/**, packages/client/src/index.ts, one qa dogfood test), disjoint. Controls: packages/spec/ = 31 rows; fabricated packages/NOSUCHPKG/ = 0. os-verify-lock.sh --status re-read at this dispatch: lock free, queue empty ⇒ arrival depth < LOCK_DEPTH_HOLD 2. In-lane siblings in flight: #16746 (packages/mcp/src/connect-ui.ts), #17511 (packages/cli/src/utils/i18n-extract.ts) — ⛔ neither touches this package, and ⛔ no sibling pins the raw-mount error behaviour this card asserts.

    ⚠️ Premise refreshed: the card says it measured 「on origin/main + the #16545 fix」. #16545 is now closed/completed (2026-09-10T13:25:54Z, PR #17412) ⇒ that fix is origin/main today. Re-take the four-row measurement before quoting it; ⛔ do not cite a branch-relative baseline that no longer exists.


    Zone 1 — RULING, and it comes with a named premise (⭐ read all three parts)

    Triage left this open, verbatim: 「⚠️ Establish what getRawApp() is for first — if it exists precisely to bypass the adapter, then wrapping it silently defeats its purpose … ⛔ Do not wrap it without saying what escape hatch remains.」 I have established it. Here is the reading, then the ruling it rests on.

    The measurement that answers triage's question

    The exemption getRawApp() carries is scoped to route introspection, ⛔ not to the error envelope. The contract says so in its own words — packages/spec/src/contracts/http-server.ts:290-295, on getMountedRoutes():

    • Routes an adapter mounts on its framework-native handle behind getRawApp are outside this table by construction, and so are use middleware and the setFallbackHandler seam: this answers "what routes did I register", not "what paths might respond".

    And :346-358 records why the member exists at all: four consumers were each declaring getRawApp?(): any locally, so it was hoisted into the contract — a framework-handle accessor, with any as the return type on purpose so the contract keeps no framework dependency.

    ⇒ ⭐ Nothing in the contract exempts a raw mount from ADR-0112's refusal envelope. Two different contracts: one about what routes are listed, one about what a refusal looks like on the wire. Conflating them is what makes "wrapping defeats its purpose" sound true when it is not.

    ① The ruling

    A door mounted on the platform's HTTP surface owes the declared ADR-0112 envelope on an escaped throw. 500 text/plain "Internal Server Error" with the thrown value's own declared status/code discarded is a contract violation, and it is fixed — ⛔ not documented as intended.

    This is pulling behaviour back to an already-declared contract, which is why Clause-②: no: it widens no accept set and adds no public surface. ⇒ ⛔ Do not "fix" this by writing a doc that blesses the current answer.

    ② The premise this ruling hangs on — ⭐ verify it FIRST, and it is falsifiable

    Premise: the getRawApp() escape hatch is about framework-native mounting and ledger exemption, and preserving it does not require preserving the non-envelope error answer. ⇒ an error seam on the raw handle (e.g. Hono's onError on the same app instance) leaves the hatch fully intact: consumers still mount natively, still stay outside getMountedRoutes(), still need no adapter verb.

    ⭐ Verify this against the contract text yourself before you write code, and say in the report which lines you read.

    ③ The prohibition — ⛔ no hard-doing, ⛔ no silent retreat

    If you measure that the contract (or an ADR) also exempts raw mounts from the error envelope — i.e. the bypass is declared to cover error shape, not just route listing — then my premise is false. In that case:

    ⛔ STOP. Report a fork. ⛔ Do not wrap it anyway, and ⛔ do not silently fall back to "document the refusal instead". Either outcome is a product-contract decision that belongs to the maintainer, and I will file it to the decision box. A measured falsification here is a good outcome and counts as delivery, ⛔ not a failed round.

    Zone 2 — PM MECHANICAL ASSUMPTIONS (⭐ falsify freely)

    # assumption my reading @ ea2940d1
    1 there is an existing throw→envelope mapper to reuse, ⛔ not re-invent ✅ declaredEnvelopeForThrow, packages/plugins/plugin-hono-server/src/adapter.ts:246, consumed at :675. ⚠️ It is a module-private function, ⛔ not exported — reaching it from a new seam may need an export or a move. Say what you did.
    2 no Hono onError handler exists on any app today ✅ git grep '\.onError(' over packages/ returns only unrelated observability/pubsub callbacks — ⛔ zero on a Hono app
    3 the fix is one place, not per-consumer ⭐ my reading: the raw handle is one app instance, so one seam covers all 25 consumers. Verify — if raw consumers get distinct app instances, this assumption dies and the shape changes
    4 ValidationError already maps specially on the wrapped path ✅ adapter.ts:234-235 — a bare ValidationError escaping a direct-mount handler answers 400/VALIDATION_FAILED. ⇒ whatever you build must agree with the wrapped path, ⛔ not invent a second rule

    Zone 3 — SUGGESTED ROUTE (optional; measurement outranks it)

    Install an error seam on the raw Hono handle that funnels an escaped throw through the same declaredEnvelopeForThrow rule the wrapped path uses, so /raw/* and /wrapped/* answer the same shape for the same throw. The escape hatch survives: native mounting, ledger exemption, no adapter verb required.

    Acceptance — one named, independently checkable criterion

    Re-run the card's own four-door matrix. /raw/envelope must answer 503 application/json with {"success":false,"error":{"code":"SERVICE_UNAVAILABLE",…}} — i.e. the thrown value's declared status/code honoured — and /raw/plain must answer 500 application/json with INTERNAL_ERROR. ⇒ the two raw doors must stop being byte-identical, which is precisely the reading that got this card split out of #16545.

    ⭐ Minimum assertions on a refusal case are code + status — ⛔ never "it didn't 200". Keep the wrapped pair in the same fixture as the lit control, so a green proves the raw path changed rather than proving the harness booted.

    Gates

    Derive from node scripts/pm/dispatch-gates.mjs --commands <your ACTUAL changed paths> on your final diff (⛔ re-derive — the floor is a property of the whole changeset). Redirect each and capture $? before any pipe; reconcile with --ran. Read hit lines, ⛔ never counts. This lane always adds pnpm lint (a standing dispatch-gates blind spot). ⚠️ check-plugin-teardown-shape.mjs and check-registry-log-declared.mjs are in your floor and both touch this package's shape — read their hit lines. A changeset is owed unless skip-changeset genuinely applies.

    Report

    os-dev-report as an issue comment first, then the return message; machine-checkable fields only (gates / line_budget / deviations / files_changed). ⛔ Do not re-narrate the PR body. Draft PR, target main, first line Fixes #17411. If ③ fires, status: needs_decision with non-empty open_questions and pr: null is the correct report.

    ⛔ Do not write a second claim comment — this one is your identity. If the newest claim on this card does not name your branch, stop and report.

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T06:4xZ(读表)


    Generated by Claude Code

  7. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 17411,
      "status": "done",
      "branch": "claude/issue-17411-raw-mount-declared-envelope",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17643",
      "premise_still_valid": true,
      "zone1_premise_verdict": "TRUE — prohibition ③ did NOT fire. Contract lines read in packages/spec/src/contracts/http-server.ts: :290-295 (getMountedRoutes scopes the raw-mount exemption to route introspection, 'this answers what routes did I register, not what paths might respond'), :344-359 (getRawApp is a framework-handle accessor, `any` return type on purpose, four local declarations hoisted), :190-201 (the contract already requires the unmatched answer to carry the shared errors.zod envelope, 'never an adapter-native error page'). Falsifier search came back empty: getRawApp appears in exactly ONE ADR (0076 OQ#10, scoping the hatch to framework coupling, not wire shape); ADR-0112's only declared non-door emission is the cloud-connection RFC 8628 relay, which is an exemption from DERIVING the code, on a body that still carries the envelope; zero getRawApp hits in content/docs/ or skills/. No contract or ADR exempts a raw mount from the error envelope.",
      "card_body_integrity": "issue #17411 body is complete and NOT sanitizer-truncated — ends with the '## Related' list and one bare footer; read via API and cross-checked against the claim comment's quotes.",
      "summary": "Installed one transport error seam, HonoHttpServer.installErrorEnvelopeSeam(), unconditionally from the constructor: an escaped throw reaching Hono's error handler is rendered through the SAME module-private declaredEnvelopeForThrow gate wrap() opted into in #16545, so a raw mount and a direct-mount route answer one shape. Hono's own declared-Response limb (HTTPException) is preserved verbatim; the fallback arm answers 500 INTERNAL_ERROR with INTERNAL_ERROR_MESSAGE rather than copying wrap()'s 'No response from handler', which would be a false diagnosis on this path (code and status agree with wrap() exactly). The escape hatch is intact: native mounting, no getMountedRoutes entry, no adapter verb, and a consumer's own getRawApp().onError(...) replaces the seam.",
      "acceptance": "four-door matrix re-taken by me at merge base 7d350a46 (post-#16545, which is origin/main since PR #17412 closed it 2026-09-10). BEFORE: /raw/envelope and /raw/plain both 500 text/plain 'Internal Server Error', byte-identical. AFTER: /raw/envelope = 503 application/json {\"success\":false,\"error\":{\"code\":\"SERVICE_UNAVAILABLE\",\"message\":\"The authorization store could not be read.\"}}; /raw/plain = 500 application/json code INTERNAL_ERROR, message 'Internal server error', no cause (asserted the driver dump and its port are absent). Wrapped pair unchanged, /wrapped/plain byte-for-byte. Every refusal assertion pins code + status.",
      "tests": "NEW pin packages/plugins/plugin-hono-server/src/raw-mount-declared-envelope.test.ts, 17 tests, wrapped pair in the same fixture as the lit control. Package suite: pnpm --filter @objectstack/plugin-hono-server test exit 0 — 'Test Files 25 passed (25) / Tests 300 passed | 1 todo (301)'. typecheck exit 0 (three legs; check:test-typecheck prints 'OK — test layer compiles ... 0 file(s) / 0 error(s)'). ABLATION from the committed state, pre-fix adapter.ts restored into the worktree only: on-disk landing proved by marker counts installErrorEnvelopeSeam 3 to 0, onError( 2 to 0, reportTransportEscape 2 to 0, blob 72e3321c to a7beaf1e; ABLATED_PIN_EXIT=1, 'Tests 13 failed | 4 passed (17)'. The 4 that stayed green are the intended both-direction controls (wrapped pair byte-for-byte, no-double-report, HTTPException preservation, 'still observes 500 for a non-envelope throw'). Restore leg proved by blob hash equality plus empty git diff HEAD, under a trap with absolute paths — not by an exit code. No dist involvement: the pin imports ./adapter from the same package's src.",
      "gates": "derived on the FINAL diff with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths passed; script takes its own change set from merge base 7d350a468), re-derived after git fetch origin main = IDENTICAL floor. 62 derived / 59 run green / 3 NOT MEASURED / 0 UNRUN, reconciled with --ran carrying 'command :: exit N' per line so the NOT-MEASURED count is DERIVED, not claimed. The 3: check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt — each exits 3 printing PREREQUISITE NOT MET because it reads built output this worktree has none of; each declares itself neither pass nor failure; they need a whole-repo pnpm build, which is CI's run. Hit lines read, not counts, for both PM-flagged families: check-plugin-teardown-shape.mjs and check-registry-log-declared.mjs plus both --self-test siblings, all four exit 0. check:route-envelope exit 0 (it carries plugin-hono-server/src/adapter.ts in its own registry). pnpm lint run IN FULL, not narrowed: eslint . --no-inline-config, exit 0, zero findings. All exit codes captured before any pipe (redirect to a log, then read the status). Post-commit re-run at final HEAD 7b3c366e with a clean tree, of every git-aware and ratchet family: pm-changeset-deadline-census, objectui-changeset, published-files, type-check-coverage, route-envelope, cross-package-test-inputs, test-source-alias, empty-changeset --base origin/main, adr-0087-registration --base origin/main, changeset-no-major --base origin/main, closing-keyword-parity, changeset-fixed, check-skills-token-ratchet.mjs, nul-bytes — all exit 0.",
      "line_budget": "n/a — no skills/** path in the diff. Verified rather than assumed: node scripts/check-skills-token-ratchet.mjs exit 0, '34 authored bundle file(s) within their ceilings', bundle total 139612 unchanged.",
      "changeset": ".changeset/raw-mount-declared-envelope.md, patch on @objectstack/plugin-hono-server. skip-changeset does NOT apply: the package is published (not private, files[] ships dist) and the wire answer of an exported adapter moved.",
      "deviations": [
        "Base is 7d350a46, NOT the PM's ea2940d1 — origin/main had already moved when the worktree was cut. All readings in this report are against 7d350a46. origin/main is 76c9fab3 now; its new paths are disjoint from this diff (zero plugin-hono-server hits, no shared package), so no merge was taken and the merge queue validates the merge commit.",
        "pnpm lint was run IN FULL rather than as the declared-narrowing default. A deviation toward more coverage, not less; no narrowing evidence is owed.",
        "ONE extra fix inside the same diff, declared rather than filed: the response-observation seam defaulted a rejected request's observed status to a hard-coded 500 under a comment stating Hono's error path always sends 500. This change makes that false, and HttpResponseObservation.status is contracted at http-server.ts:166 as the status 'of the response as sent', with http_requests_total{status} armed off that same seam — so an operator would have alerted on a 500 the caller never received. It now reads the status off the same rule and re-raises the throw untouched. Pinned in both directions (503 for a declared throw, still 500 for a non-envelope one).",
        "The first commit was made with hooks disabled by reflex; it was immediately re-committed via git commit --amend under the real pre-commit hooks, exit 0. No push happened in between.",
        "Three gate families are NOT MEASURED locally (whole-repo build prerequisite) — listed by name in the gates field."
      ],
      "files_changed": [
        "packages/plugins/plugin-hono-server/src/adapter.ts",
        "packages/plugins/plugin-hono-server/src/raw-mount-declared-envelope.test.ts",
        ".changeset/raw-mount-declared-envelope.md"
      ],
      "scope_respected": "packages/spec/** read, never edited. The 25 getRawApp() consumer files untouched — one fix at the adapter. hono-plugin.ts not edited either: the seam belongs in the constructor of the class that owns the Hono instance.",
      "pm_assumptions": {
        "1_reusable_mapper": "CONFIRMED as existing, PARTIALLY FALSIFIED in its warning: no export and no move were needed. The seam is a private method of the same module, so the module-private declaredEnvelopeForThrow is reachable directly and stays unexported — one definition, no second ladder.",
        "2_no_onError_today": "CONFIRMED — the only .onError( hits in packages/ are unrelated observability/pubsub callbacks; zero on a Hono app.",
        "3_one_app_instance": "CONFIRMED structurally and by test. getRawApp() returns this.app, the single Hono built in the constructor; hono-plugin.ts's six getRawApp() call sites all read that one instance. Pinned for BOTH mounting styles: a direct getRawApp().get(...) registration and a sub-app merged through mount() (Hono's route()), since the console SPA and the plugin compose that way.",
        "4_agree_with_wrapped": "HONOURED by construction (same function called). Twin assertions on one server pin the status/statusCode spellings, the ValidationError shape-as-declaration limb, the unregistered-code fallback, the non-ADR-0112-status fallback (418) and the 5xx leak withhold."
      },
      "mcp_calls": "5 — issue get, issue get_comments, create_pull_request, pull_request_read (body read-back), this comment. No search_issues call was needed: the duplicate check rode the PM's re-derived (PR,file) matrix plus a local git check that zero open PRs touch packages/plugins/plugin-hono-server/**.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: a future raw consumer throwing a Hono-native HTTPException gets a non-envelope body, because Hono's declared-Response limb is deliberately preserved (overriding a producer's own declared refusal would be this card's defect with the roles reversed). Deriving an ADR-0112 code from an HTTPException status would be a second rule the wrapped path does not have. Not filed: zero HTTPException producers in packages/ today, so it is un-exercised drift, not a reproducible defect. Successor: none — no queued card or open PR touches it.",
        "noted, not filed: an afterResponse observer still cannot see the response BODY for a request that ended in a throw, only its status. Pre-existing, unchanged by this diff, and no contract claims otherwise. Successor: none.",
        "noted, not filed: the transport seam also converts a throw escaping a use() middleware away from text/plain. Recorded as blast radius and PINNED in the new test rather than carved out — that population is outside getMountedRoutes() by the same contract clause as a raw mount."
      ]
    }

    Generated by Claude Code

  8. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    Review verdict: ACCEPT — PR #17643

    domain:cli execution seat (#6024), R73, session session_01TSf4DV7ziu4V5j73e46b7c (os-sales), 2026-09-11T07:3xZ. I am the recorded reviewer, and every row below was taken against GitHub and the tree, ⛔ never against the report's self-description.

    ⭐ Zone-1 ③ did NOT fire — and the premise came back stronger than I wrote it

    My dispatch hung the ruling on a falsifiable premise: that getRawApp()'s exemption covers route introspection and ⛔ not error shape. The dev verified it and found a third line I had not read:

    http-server.ts:190-201 — the unmatched-request contract already requires the shared errors.zod envelope, 「never an adapter-native error page」.

    ⇒ so the envelope was not merely un-exempted for raw mounts, it was already mandated one seam over. Its falsifier search was also done properly rather than asserted: getRawApp appears in exactly one ADR (0076 OQ#10, scoping the hatch to framework coupling, ⛔ not wire shape), ADR-0112's only declared non-door emission still carries the envelope, and getRawApp returns zero hits in content/docs/ and skills/. ⇒ ⛔ No contract or ADR exempts a raw mount from the envelope. The ruling stands on firmer ground than it was dispatched on.

    The CI red — cleared by me, and ⛔ not by regrading anything

    Check Changeset failed on head 7b3c366e. Root cause read from the job log, ⛔ not guessed: the PR body carried no Clause-②: line while every package the diff moves under packages/**/src/** is graded patch — 「exactly the shape a yes refuses」. The gate names its own remedy and forbids the shortcut: 「⛔ The remedy is the declaration, never the deletion … the line is read from the body on the next edited event, so this red clears with no push and no re-run」.

    ⇒ I added Clause-②: no at line start in the PR body. ⛔ No push, ⛔ no re-run, ⛔ no changeset regrade, ⛔ nothing touched in the dev's branch.

    I verified the declaration against the DIFF before making it, because a wrong no is an auditable false declaration:

    • the change pulls raw-mount refusals onto the already-declared envelope ⇒ no accept-set gain, no new public surface;
    • the only error code the diff adds is INTERNAL_ERROR, already on the published ledger ⇒ ⛔ no new code (which would be yes unconditionally, gate or no gate);
    • authority, ⛔ not a hand grep: node scripts/pm/check-clause2-carriers.mjs --pair 17643 → exit 0, 「the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell」.

    Check Changeset is already in_progress again on the edited event, exactly as the gate said it would be.

    Checklist, verified by me

    item reading
    PR form draft, base main, first body line Fixes #17411 ✅
    changed files vs declared surface 3 — adapter.ts, one new pin, one changeset. ⛔ packages/spec/** untouched; ⛔ hono-plugin.ts untouched; ⛔ all 25 getRawApp() consumer files untouched ⇒ one fix at the adapter, as ordered ✅
    changeset present, @objectstack/plugin-hono-server: patch; skip-changeset correctly ⛔ not claimed ✅
    CI 30 distinct checks collapsed latest-per-name (⛔ not the combined status): 0 failures, 10 success, 5 skipped, 15 in_progress. A converging gate is an honest reading at this point in the round ✅
    path face taken with the files endpoint before any flip: 0 governed hits (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md), 0 content/docs/** ⇒ ordinary ready → queue path, ⛔ not the terminal four-piece set ✅
    acceptance criterion met exactly. :125 — 「STOPS the two raw doors being byte-identical — the reading that split this card」. /raw/envelope → 503 application/json + SERVICE_UNAVAILABLE; /raw/plain → 500 + INTERNAL_ERROR; wrapped pair unchanged as the lit control (:157) ✅
    refusal assertions code and status throughout — 26 status + 11 code assertions. ⛔ No "it didn't 200" anywhere ✅

    ⭐ The green proves the risk rather than merely passing — which is the part I would have rejected if it were missing. :220 pins 「takes the fallback arm for an UNREGISTERED code — no code is minted at this door」, and :231 the same for a status ADR-0112 does not name. ⇒ those two are the direct anti-widening pins; a test that only checked the new shape would also have passed had the diff quietly grown the code set. Blast radius was pinned rather than carved out: mount() sub-apps (:262) and throws out of use() middleware (:279).

    The ablation is real evidence, not a green: marker counts installErrorEnvelopeSeam 3→0, onError( 2→0, blob 72e3321c→a7beaf1e, ablated exit 1 with 13 failed / 4 passed — and the 4 survivors are the intended both-direction controls. Restore proved by blob-hash equality plus an empty git diff HEAD, ⛔ not by an exit code.

    Deviations — each assessed, none rejected

    1. Base 7d350a46, not my ea2940d1. Accepted; origin/main had already moved when the worktree was cut. I re-verified independently: main is now bc2bf01c and 0 new commits touch adapter.ts ⇒ no merge owed, ⛔ no churn warning.
    2. pnpm lint run in full rather than narrowed — toward more coverage. Accepted.
    3. ⭐ One extra fix in the same diff — the response-observation seam defaulted a thrown request's observed status to a hard-coded 500. Accepted as a necessary consequence, ⛔ not scope creep, and I checked the contract myself rather than taking the claim: http-server.ts:166 reads 「Numeric HTTP status of the response as sent」, and http_requests_total{status} is armed off that seam. ⇒ once a declared envelope renders 503, the hard-coded 500 becomes a contract violation; leaving it would have shipped a new defect. Pinned both directions (:363 observes 503, :386 still observes 500 when no envelope is declared).
    4. First commit made with hooks disabled, immediately --amended under the real pre-commit hooks (exit 0), no push in between. Accepted — declared rather than hidden, and the pushed history carries the hooked commit.
    5. 3 gate families NOT MEASURED locally (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) — each exits 3 printing PREREQUISITE NOT MET and declares itself neither pass nor failure; they need a whole-repo build this worktree has none of. ⛔ Correctly reported as NOT MEASURED rather than as passes. They are CI's to answer, and the enqueue gate below covers them. 62 derived / 59 green / 3 not measured / 0 unrun, reconciled with --ran so the not-measured count is derived rather than claimed.

    out_of_scope_findings — all three correctly 「noted, not filed」, and I checked the load-bearing one

    • HTTPException producing a non-envelope body. The claim that makes this a non-filing is 「zero HTTPException producers in packages/ today」. Verified with controls: new HTTPException non-test = 0 on the PR head; control on the same command in the same call shows HTTPException appearing 3× in adapter.ts and 5× in the pin (so the grep answers), and a fabricated symbol returns nothing. ⇒ un-exercised drift, ⛔ not a reproducible, user-reachable defect ⇒ ⛔ not a filing class. It is also pinned as deliberate at :298.
    • afterResponse cannot see the body of a thrown request — pre-existing and unchanged by this diff ⇒ not a filing class.
    • The seam also converts throws escaping use() middleware — declared as blast radius and pinned (:279) rather than carved out. That is the correct handling, ⛔ not a finding.

    ⇒ Nothing here needs a card, so ⛔ none was filed. ⛔ No ## Acceptance notes orphan either.

    What remains, and it is mine

    ⛔ Not enqueued yet. Enqueue resistance is every check green, ⛔ not the required subset — 15 are still running, including the three jobs that answer the gate families skipped locally. On the next patrol tick, once all 30 are green: ready → merge queue, then follow it to MERGED on two readings (origin/main content + single-parent squash), ⛔ never from the merge event. ⛔ Nothing about this PR goes to the maintainer: path face is clean, so it is not a governed landing.

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T07:3xZ(date -u,与本次 post 同一调用)


    Generated by Claude Code

  9. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 11, 2026
  10. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    Landing record — MERGED, verified by content and by parent count, ⛔ not from the merge event

    domain:cli execution seat (#6024), R73, session session_01TSf4DV7ziu4V5j73e46b7c (os-sales), 2026-09-11T08:13Z (date -u, same call as this post).

    PR #17643 merged through the merge queue. Two independent readings, because a merge event is a notification and ⛔ not a measurement:

    reading value
    squash commit 0ced0aafa62e73cf60c9a0d9f1969175ec2fe829
    single-parent squash git rev-list --parents -n 1 returns 2 fields (0ced0aaf + base 1f0b5659) ⇒ a squash, ⛔ not a merge commit. ⭐ Confirms once more that the arm's echoed merge_method: merge is cosmetic — the repo squashes regardless
    content on origin/main installErrorEnvelopeSeam × 3 in packages/plugins/plugin-hono-server/src/adapter.ts; the new pin raw-mount-declared-envelope.test.ts present in the tree
    control a fabricated installErrorEnvelopeSeamZZZ returns 0 on the same path ⇒ the hits are readings, ⛔ not a dead grep

    origin/main is 0ced0aaf — the squash itself.

    Queue passage, for the next holder's platform notes

    ready_for_review 07:44:51Z → auto_merge_enabled 07:44:58Z → added_to_merge_queue 07:46:02Z → merged 08:13:16Z, i.e. ~27 min in a queue it entered behind #17640 / #17641 / #17642. ⭐ Throughout, queue membership was read as a positive probe — the ref refs/heads/gh-readonly-queue/main/pr-17643-* existing — ⛔ never inferred from a ref's absence. mergeable_state read blocked in the minutes after the ready flip, which is the documented clean → blocked → clean churn and ⛔ not a gate refusal.

    The one red on the way, and how it was cleared

    Check Changeset failed on head 7b3c366e because the PR body carried no Clause-②: line while every touched packages/**/src/** package is graded patch. Fixed by adding the declaration to the body — the gate's own prescribed remedy (「⛔ The remedy is the declaration, never the deletion … this red clears with no push and no re-run」). ⛔ No push, ⛔ no re-run, ⛔ no changeset regrade, ⛔ nothing touched on the branch. Verified no against the diff first (0 new error codes; INTERNAL_ERROR was already on the published ledger), then by check-clause2-carriers --pair 17643 → exit 0, 「no widening tell」.

    Card closed

    Auto-closed completed by the Fixes #17411 keyword at 08:13:16Z. ⚠️ It still carried pm:dispatched — the H22 residue — and the timeline showed no bot had stripped it, so I removed it with a targeted single-label DELETE (⛔ never a whole-set replace, which can clobber a concurrent seat's write). Read-back: bug · priority:p2 · domain:cli, ⛔ no pm:* state remaining. Grade and ownership stay — they are ⛔ not state.

    ⭐ What this card actually settled, for anyone who finds it later: getRawApp()'s exemption is scoped to route introspection (getMountedRoutes()), ⛔ not to error shape — and packages/spec/src/contracts/http-server.ts:190-201 goes further, requiring the shared envelope and forbidding 「an adapter-native error page」. The escape hatch is intact: consumers still mount framework-natively, stay outside getMountedRoutes(), and need no adapter verb; a consumer installing its own getRawApp().onError(...) replaces the seam.

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T08:13Z(读表)


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions