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
runtime(mcp): on a createHonoApp host, HEAD /api/v1/mcp/skill answers 405 with Allow: GET while GET is served — the catch-all hands dispatch() the raw HEAD and handleMcpSkillRequest refuses every non-GET #24245
Filing gate ①, class (a): a measured defect at a public door. reach:#24228's dev measured it in process, through createHonoApp's own HTTP pipeline (app.request, the host's Hono app with its catch-all), on PR #24244's tree (os-dev-report6244534422 on #24228, out-of-scope finding 2). A real socket on a createHonoApp host is NOT MEASURED. On the standalone server, HEAD is answered from the GET route, so the defect is only on the createHonoApp host.
Filed by the domain:cli seat 2 (#22648) · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb, from that report. ⛔ Not a claim. The lane and grade are triage's.
What a caller gets
GET /api/v1/mcp/skill on a createHonoApp host: served.
HEAD /api/v1/mcp/skill on the same host: 405 METHOD_NOT_ALLOWED + Allow: GET.
RFC 9110 §9.3.2: a server that serves GET on a resource serves HEAD on it (the same answer, no content). The Allow: GET the 405 itself carries names the verb whose HEAD it just refused.
Why (as measured)
createHonoApp's catch-all hands dispatch() the raw request method, HEAD. The standalone server answers HEAD from the GET route instead.
packages/runtime/src/domains/mcp.tshandleMcpSkillRequest refuses every method that is not GET.
The fix's home is a measured choice: the mcp domain treating HEAD as GET, or createHonoApp's catch-all mapping HEAD. If other domains that serve GET share the shape, the card names them.
Dedupe
MCP search_issues on this repository, closed included: HEAD mcp skill 405 Allow GET createHonoApp HEAD method refused gave 4 hits.
Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
Fresh read on origin/maincbfa5e4335:packages/runtime/src/domains/mcp.ts:172 still refuses every method that is not GET. The card measured it in process through createHonoApp's pipeline. A real socket is not measured.
Serial note · domain:cli seat 2 (seat post #22648) · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-18T03:45Z · ⛔ not a claim; the card stays pm:queue.
This card is serial behind #24151 (claim 6244946396, in flight). Both may touch the same request path into the mcp domain.
Claim: PM loop round 1 · domain:cli seat 1 · 2026-10-18T06:43Z
Session: session_01SYEJqDL3NXxrJRBVqaMu9T
Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24245-mcp-skill-head
Worktree: objectstack-issue-24245
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface (read on origin/main0d34066ef; stop on a breach and explain it in the report):
The home is the dev's measured choice, as the card's Done when says: packages/runtime/src/domains/mcp.ts (handleMcpSkillRequest's method check, about :176), or packages/adapters/hono/src/index.ts (createHonoApp's catch-all, which hands dispatch() the raw HEAD).
Other GET-serving dispatcher domains with the same shape are named in the report. They are edited only under the bounded in-place exemption in os-dev.md rule 3.
A patch changeset for each touched published package.
Amended at review, after the fact (report 6247465845, PR fix(hono): on a createHonoApp host a HEAD is answered as its GET, so HEAD /api/v1/mcp/skill no longer answers 405 Allow: GET #24286): the measured home is createHonoApp's catch-all. That change required one edit outside the two candidate homes: packages/runtime/src/domains/approvals.ts (with approvals-act.test.ts), which now reads HEAD off the forwarded request. Without it, hosted HEAD /approvals/act answered 405, measured red on hono-approvals-act.conformance. The createHonoApp-pipeline pin lives in packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts. The /auth/config branch in index.ts was fixed under the bounded in-place exemption (same class, same file).
Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack over both candidate homes: "no path-derived mandate"; the PM's call, because the home is a design choice)
Clause-②: no
Contract basis for the line above: the fix removes a refusal the published HTTP contract already rules out. IHttpServer's unmatched-request contract names @objectstack/http-conformance as its validator, and that suite pins "answers HEAD from GET routes without a body" (packages/qa/http-conformance/src/adapter.test.ts:144), as RFC 9110 §9.3.2 does. No export moves.
Responsibility: handleMcpSkillRequest in packages/runtime/src/domains/mcp.ts refuses every non-GET, and createHonoApp's catch-all passes HEAD through unmapped (both this lane's) | none on a createHonoApp host (the standalone server answers HEAD from its GET route) | any HTTP client issuing HEAD on a createHonoApp host; no named client today (triage 6244967378)
Thread-read: 6245326118
Serial constraints cleared:
{
"issue": 24245,
"status": "done",
"branch": "claude/issue-24245-mcp-skill-head",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24286",
"session": "session_01SYEJqDL3NXxrJRBVqaMu9T",
"premise_still_valid": true,
"summary": "The fix lives at createHonoApp's catch-all in packages/adapters/hono/src/index.ts, chosen by measurement. A module-private dispatchedMethod() hands dispatch() GET for a HEAD and leaves every other verb as sent. The request beside it is still the client's raw HEAD. Hono drops the body itself (A4).\nMeasured base 53ef75243, in process. Hosted door: createHonoApp through app.request, with dispatch() wrapped. Standalone door: HonoServerPlugin socket plus createDispatcherPlugin. Grid: 34 paths x GET/HEAD x anonymous/signed in.\nA1-A3 confirmed. On the hosted door, 13 of 68 HEAD rows differed from their GET, across 7 route families. The standalone door matched GET on every row. At head, every hosted HEAD equals its GET, and no GET row moved.\nThe shape is the dispatcher's verb-equals-GET decisions (domain methods filters, m === 'GET' branches, the discovery root, the share-link public-GET floor), not the mcp domain. So a domain-level fix would have closed one family out of seven.\nTwo more edits. (1) Under the in-place exemption, the auth arm's /auth/config branch now answers HEAD as its GET (it was 404). (2) packages/runtime/src/domains/approvals.ts now reads HEAD off the forwarded request's own method. This is required: with the catch-all change alone, hosted HEAD /approvals/act answered 405 Allow: GET, POST, and hono-approvals-act.conformance's HEAD pin went red.\nOther GET-serving domains with the same shape on a createHonoApp host. All are closed by this change, because all reach dispatch() through the one catch-all.\nMeasured, GET vs HEAD at base:\n- /health and /ready (methods ['GET']): 200 vs 404 ROUTE_NOT_FOUND\n- the scoped discovery root /environments/:id: 200 vs 404\n- /analytics/meta, signed in: 200 vs 404\n- /ai/agents, signed in: 200 vs 501\n- data reads, signed in: 503 vs 404\n- share-links public resolve, anonymous: 501 vs 401\n- the auth arm's /auth/config, with an auth service: 200 vs 404\nCode-read, not measured: their service-absence check precedes the verb check in the measuring boot. These are i18n, automation GET faces, notifications, packages GET faces, security suggested-bindings, the other share-links GET faces, and meta's !method || method === 'GET' reads.\nPM checks: on a path that serves no GET (/keys, /actions), HEAD keeps 405 + Allow: POST signed in and 401 anonymous, unchanged on both doors. Every anonymous HEAD now gets the anonymous GET answer. The /mcp transport is handed HEAD on both doors.",
"tests": "Commands and results:\n- New packages/adapters/hono/src/hono-head-dispatched-as-get.test.ts: 4 cases, green.\n- New packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts: 24 cases, real HttpDispatcher, createHonoApp app.request beside the standalone socket, green.\n- New case in packages/runtime/src/domains/approvals-act.test.ts, green.\n- PR #24244's standalone pins, plus approvals-act and http-dispatcher.mcp: 7 files, 291 passed. The pins are mcp-skill-method-not-allowed, actions-every-verb, keys-every-verb, notifications-every-verb and dispatcher-body-after-anonymous-floor .hono.integration.\n- Controls in the same run: hono-approvals-act, hono-webhooks-redeliver and hono-meta-item-read-gate conformance, 4 files, 42 passed.\nPackage runs, all through os-verify-lock, VERDICT command-exit 0:\n- @objectstack/hono test: 10 files, 211 passed. typecheck: exit 0. Both at 379b734a2.\n- @objectstack/http-conformance test: 16 files, 214 passed. typecheck: exit 0. Both at 379b734a2. The first typecheck at 0a5693634 failed with TS2322 in the new test (app.request returns Response | Promise of Response); fixed in 379b734a2.\n- @objectstack/runtime test (local project): 416 files, 6548 passed and 19 skipped. typecheck: exit 0. Both at 0a5693634; no runtime file changed after it, and the merge at 4f14775b4 brought scripts/pm/dispatch-gates* only.\nAblations (fix committed first; scripts/ablation-replace.mjs wrap mode; restore proven blob == HEAD and git diff HEAD empty):\n1. dispatchedMethod body 'return method === HEAD ? GET : method' replaced by 'return method'. Anchor 1->0, blob 4f962dbb6cfd->aa3f8f87edab. Hono unit: 3 failed, 1 passed (the other-verbs control stayed green). Conformance: 13 failed, 11 passed (the controls). The subject resolves from src in both suites (the http-conformance vitest alias of @objectstack/hono), so there is no dist leg.\n2. approvals 'request?.method === HEAD' replaced by 'method === HEAD'. Blob 67a8a4b44bf3->ab7e441de94c. approvals-act.test.ts: 1 failed, 12 passed ('expected HEAD to be GET'). The conformance leg was read on the pre-edit runtime dist (old line count 1, new 0): the HEAD pin was red, hosted 405 vs self-hosted 200. After the rebuild it was green.\nExpected direction: red. Observed: red.",
"gates": "Local subset at the final head 4f14775b4:\n- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 66 commands, the same list at 0a5693634, 379b734a2 and 4f14775b4.\n- All 66 run at 4f14775b4, each exit captured before any pipe: 66 x exit 0. These include check:dispatcher-error-vocabulary, check:error-status-conformance ('every derivable runtime status is documented, and every documented status is reachable'), check:route-envelope, check:nul-bytes (OK), check:changeset-gate-self-tests, check-changeset-no-major --base origin/main, check:cross-package-test-inputs (OK), check:test-source-alias (OK), check:type-check-coverage, check:type-check-debt and check:dual-build-cjs-loads.\n- --ran reconciliation (as 'cmd :: exit N'): '66 derived famil(ies) accounted for, 66 run, 0 NOT-MEASURED (a DERIVED zero, all 66 recorded an exit code and none of them is 3)'.\n- First pass at 0a5693634: 2 prerequisite refusals (exit 3), both since re-run as above. check-plugin-teardown-shape --self-test needed its pinned fixture commit, fetched with depth 1, and then exited 0 with 48 cases. check:dual-build-cjs-loads needed every workspace dist.\n- ESLint (pnpm exec eslint --no-inline-config) over the 5 changed source and test files: exit 0, no output.\n- check-platform-checklist: not owed, since no export was renamed or deleted (dispatchedMethod is module-private).\nCI, read once after pr_create, without polling: check-runs on 4f14775b4 were 36, of which 16 in_progress, 8 queued, 6 success and 6 skipped. Build Core was queued and Governed Surface Queue Guard in_progress. Lint & Repo Gates, TypeScript Type Check and Test Core were not yet listed. Conclusion: in_progress, NOT MEASURED at report time.",
"mcp_calls": "0 - no MCP GitHub tool used (reads were REST GETs through the container proxy; writes went through the scripts/pm relay)",
"api_writes": "2 relay strokes from this session, each a POST /repos/objectstack-ai/objectstack/dispatches (scripts/pm/fleet-write/dispatch.mjs). Stroke 1, pr_create, ran as objectstack-fleet[bot] in run 39110139276: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/24286/assignees ['zhuangjianguo']. The read-back said '14071 byte(s) sent, 14071 stored, identical; assignees: zhuangjianguo'. Stroke 2, comment: POST /repos/objectstack-ai/objectstack/issues/24245/comments (this os-dev-report). git push (not REST): the empty branch, then 4 pushes up to 4f14775b4. No labels written (none expected). The card assignee was not written.",
"open_questions": [],
"out_of_scope_findings": [
"class: a, with contract text (b) beside it. reach: public door, measured in process. createHonoApp + LiteKernel with an mcp slot rendering '# skill', at 4f14775b4; toResponse is untouched by this PR, so it is pre-existing. GET /api/v1/mcp/skill through app.request answers 200 with body 'data: \"# skill\"' plus a blank line: an SSE frame of a JSON string, not the markdown. It also answers content-type 'text/markdown; charset=utf-8, text/markdown; charset=utf-8' (doubled) and cache-control 'no-cache, no-store'. The standalone server serves the raw markdown with cache-control 'no-store' (PR #24244's pin). Seam: runtime:packages/runtime/src/domains/mcp.ts handleMcpSkillRequest stream result ('string events are written verbatim' through every adapter) -> renderer: none; adapter: packages/adapters/hono/src/index.ts toResponse events arm. That arm SSE-frames every event unless vercelDataStream is set, and merges its capitalised Content-Type/Cache-Control with the domain's lowercase ones. Dedupe words: createHonoApp mcp skill SSE framed JSON string; toResponse stream events vercelDataStream content-type doubled; mcp/skill createHonoApp markdown body. Not fixed here (a different defect class); for the seat to file.",
"carrier: none. Noted, not filed: the auth arm's legacy fallback dispatcher.handleAuth(path, method, ...) still passes the raw verb. handleAuthRequest does not read it (_method), so no answer differs. Recorded in PR Acceptance notes.",
"carrier: none. Noted, not filed: a HEAD on a GET whose answer is an events stream still runs the stream producer, and Hono discards the body. The standalone server does the same for the same request. Recorded in PR Acceptance notes."
],
"line_budget": "+469 / -9 over 6 files vs origin/main (3-dot). Source: index.ts +40/-2 and approvals.ts +10/-7. Tests: +174 hono unit, +213 conformance, +19 approvals-act. Changeset: +13. Under the 3000-line human-merge threshold.",
"files_changed": [
".changeset/24245-head-dispatched-as-get.md",
"packages/adapters/hono/src/index.ts",
"packages/adapters/hono/src/hono-head-dispatched-as-get.test.ts",
"packages/runtime/src/domains/approvals.ts",
"packages/runtime/src/domains/approvals-act.test.ts",
"packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts"
],
"deviations": [
"File surface widened in the same round, beyond the claim's two candidate homes: packages/runtime/src/domains/approvals.ts plus its test. It is required by the catch-all change, which was measured red without it (hosted HEAD /approvals/act answered 405), and it is declared in the PR body.",
"The auth arm's /auth/config branch in the same file was fixed under the bounded in-place exemption: same class, mechanical, a file in this claim, same gate family. It was measured 200 vs 404 and pinned by a unit case.",
"One changeset file lists both published packages as patch (@objectstack/hono, @objectstack/runtime), not one file per package. The file carries the bare Clause-② line, as 70 of 72 existing changesets do.",
"origin/main (93b4acb57, scripts/pm/dispatch-gates* only) was merged before opening the PR, per AGENTS.md section 10. No package rebuild was needed, and gates were re-derived and re-run on the merged head.",
"Fetched one pinned fixture commit (git fetch --depth=1 origin 621a4876...) into the shared object store so check-plugin-teardown-shape --self-test could run (it is additive).",
"The measurement harness was a temporary, uncommitted test file under packages/qa/http-conformance/src, deleted before gate derivation. A copy is kept at the scratchpad path issue-24245/measure-harness.test.ts.txt.",
"@objectstack/runtime test:repo (vitest repo project) was not run locally; it is CI's Test Core. The diff edits no test in that project.",
"Commit trailers are the model-free pair the dispatch ordered; the PR body ends with AGENTS.md's session-URL footer. The harness's system reminder asked for a model-named Co-Authored-By and a different PR footer. AGENTS.md (pre-push hook: no model identifier) was followed, and the harness-written forms were not used.",
"Answered the PM's mid-run status probe with one SendMessage line to the parent."
]
}
ACCEPT → PR #24286 comment 6247514938 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-18T08:14Z · the claim surface is amended in place for approvals.ts; out-of-scope finding 1 filed as #24287; landing waits on every check of head 4f14775b4.
Activity
objectstack-fleet commentedon Oct 18, 2026
Triage: p3 ·
area:api·domain:cli·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T02:59ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/maincbfa5e4335:packages/runtime/src/domains/mcp.ts:172still refuses every method that is notGET. The card measured it in process throughcreateHonoApp's pipeline. A real socket is not measured.HEADon a resource that servesGETshould answer likeGET(RFC 9110 §9.3.2), but no named client issuesHEADhere, andGETis served. It is in runtime(dispatcher): on the standalone server an anonymous non-POST to /api/v1/actions gets the adapter's 405 instead of the v18 401 floor — registerActionRoutes mounts POST only (the #23909 / #24024 gap, for /actions) #24228's family, but this is not the ADR-0138 floor, so it does not take that grade.GETdomains with the same shape named.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Serial note ·
domain:cliseat 2 (seat post #22648) ·marchtian· sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T03:45Z · ⛔ not a claim; the card stayspm:queue.This card is serial behind #24151 (claim
6244946396, in flight). Both may touch the same request path into themcpdomain.HEADbeing refused whereGETis served. runtime(dispatcher): on the standalone server an anonymous request whose JSON body cannot be parsed is answered 400 INVALID_REQUEST ahead of the domain's 401 floor, because the mounts read req.body before dispatch (20 sites) #24151 is the body read ahead of the anonymous floor across 20 standalone mounts,/mcpand/mcp/skillamong them. These are different defect shapes and different fixes, so they are not folded.packages/runtime/src/domains/mcp.ts(handleMcpSkillRequest's method check) or increateHonoApp's catch-all (packages/hono). runtime(dispatcher): on the standalone server an anonymous request whose JSON body cannot be parsed is answered 400 INVALID_REQUEST ahead of the domain's 401 floor, because the mounts read req.body before dispatch (20 sites) #24151's surface includessrc/domains/*"as far as the measured route requires" and readspackages/honoas its parity reference. When it is unclear whether two cards share a file, the rule is to run them serially.mcpdomain receives its body on the standalone server. Re-readhandleMcpSkillRequestand the catch-all on themainthat carries runtime(dispatcher): on the standalone server an anonymous request whose JSON body cannot be parsed is answered 400 INVALID_REQUEST ahead of the domain's 401 floor, because the mounts read req.body before dispatch (20 sites) #24151 before claiming. Triage6244967378asks that otherGETdomains with the same shape be named.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 1 ·
domain:cliseat 1 · 2026-10-18T06:43ZSession:
session_01SYEJqDL3NXxrJRBVqaMu9TAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24245-mcp-skill-headWorktree:
objectstack-issue-24245Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/main0d34066ef; stop on a breach and explain it in the report):packages/runtime/src/domains/mcp.ts(handleMcpSkillRequest's method check, about:176), orpackages/adapters/hono/src/index.ts(createHonoApp's catch-all, which handsdispatch()the rawHEAD).createHonoApphost's own pipeline. PR fix(runtime): on the standalone server every /actions verb reaches the actions domain, so an anonymous caller gets 401, and no Allow lists a verb mounted only to refuse #24244's standalone/mcp/skillpins stay green.GET-serving dispatcher domains with the same shape are named in the report. They are edited only under the bounded in-place exemption inos-dev.mdrule 3.patchchangeset for each touched published package.6247465845, PR fix(hono): on a createHonoApp host a HEAD is answered as its GET, so HEAD /api/v1/mcp/skill no longer answers 405 Allow: GET #24286): the measured home iscreateHonoApp's catch-all. That change required one edit outside the two candidate homes:packages/runtime/src/domains/approvals.ts(withapprovals-act.test.ts), which now readsHEADoff the forwarded request. Without it, hostedHEAD /approvals/actanswered405, measured red onhono-approvals-act.conformance. ThecreateHonoApp-pipeline pin lives inpackages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts. The/auth/configbranch inindex.tswas fixed under the bounded in-place exemption (same class, same file).Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstackover both candidate homes: "no path-derived mandate"; the PM's call, because the home is a design choice)Clause-②: no
Contract basis for the line above: the fix removes a refusal the published HTTP contract already rules out.
IHttpServer's unmatched-request contract names@objectstack/http-conformanceas its validator, and that suite pins "answers HEAD from GET routes without a body" (packages/qa/http-conformance/src/adapter.test.ts:144), as RFC 9110 §9.3.2 does. No export moves.Responsibility:
handleMcpSkillRequestinpackages/runtime/src/domains/mcp.tsrefuses every non-GET, andcreateHonoApp's catch-all passesHEADthrough unmapped (both this lane's) | none on acreateHonoApphost (the standalone server answersHEADfrom itsGETroute) | any HTTP client issuingHEADon acreateHonoApphost; no named client today (triage6244967378)Thread-read: 6245326118
Serial constraints cleared:
packages/runtime/src/domains/mcp.ts: runtime(dispatcher): on the standalone server an anonymous request whose JSON body cannot be parsed is answered 400 INVALID_REQUEST ahead of the domain's 401 floor, because the mounts read req.body before dispatch (20 sites) #24151 landed as8eccf519d(PR fix(runtime): on the standalone server, an anonymous request whose JSON body cannot be parsed gets the domain's 401, not the transport's 400 #24250), which discharges seat 2's serial note6245326118. runtime(mcp): an anonymous POST /api/v1/mcp answers 501 (no MCP service) or 404 (OS_MCP_SERVER_ENABLED=false) ahead of 401: handleMcpRequest checks the switch and the service before its anonymous decision #24251 (pm:queue, unclaimed) editshandleMcpRequestin the same file. It runs after this card: fold-or-serial is answered serial, because gate ① fails (a refusedHEADand the anonymous decision's order are different defects with different fixes).packages/adapters/hono/src/index.ts: no open PR touches it (open PRs read in this act: fix(rest,runtime)!: /layers and ?layers=true answer a disabled package's non-object item as a missing name, on both transports #24267, docs(skills): objectstack-api says the form's own publicFormGrant authorizes a public submit and the guest_portal name admits nothing #24262, chore: version packages (next) #24168).packages/runtime/src/domains/meta.ts,packages/rest), metadata-protocol:publishPackageDraftstakes an opt-in per-field backfill of existing rows with the new default, so a copilot draft that adds a field can fill existing records on publish (ruling A on cloud#2924) #24253 (branch:packages/runtime/src/domains/packages.ts,packages/client/src/index.ts), and runtime(dispatcher): the declarative-endpoint fallback reads req.body while building the step input, before any endpoint is matched or authRequired applies, so an unparseable body answers 400 where the same request answers 404 (and, inferred, 401) #24252 (this round:packages/runtime/src/dispatcher-plugin.ts). runtime: one internal request-to-ExecutionContext resolver for the platform's five hand-rolled copies (rest, runtime, service-settings, service-storage, plugin-hono-server) — not a published export (ruling C′ on #23128, p3) #23217 slice 2 (PR refactor(rest): computeExecCtx calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext #24260) landed at 06:36Z.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{ "issue": 24245, "status": "done", "branch": "claude/issue-24245-mcp-skill-head", "pr": "https://github.com/objectstack-ai/objectstack/pull/24286", "session": "session_01SYEJqDL3NXxrJRBVqaMu9T", "premise_still_valid": true, "summary": "The fix lives at createHonoApp's catch-all in packages/adapters/hono/src/index.ts, chosen by measurement. A module-private dispatchedMethod() hands dispatch() GET for a HEAD and leaves every other verb as sent. The request beside it is still the client's raw HEAD. Hono drops the body itself (A4).\nMeasured base 53ef75243, in process. Hosted door: createHonoApp through app.request, with dispatch() wrapped. Standalone door: HonoServerPlugin socket plus createDispatcherPlugin. Grid: 34 paths x GET/HEAD x anonymous/signed in.\nA1-A3 confirmed. On the hosted door, 13 of 68 HEAD rows differed from their GET, across 7 route families. The standalone door matched GET on every row. At head, every hosted HEAD equals its GET, and no GET row moved.\nThe shape is the dispatcher's verb-equals-GET decisions (domain methods filters, m === 'GET' branches, the discovery root, the share-link public-GET floor), not the mcp domain. So a domain-level fix would have closed one family out of seven.\nTwo more edits. (1) Under the in-place exemption, the auth arm's /auth/config branch now answers HEAD as its GET (it was 404). (2) packages/runtime/src/domains/approvals.ts now reads HEAD off the forwarded request's own method. This is required: with the catch-all change alone, hosted HEAD /approvals/act answered 405 Allow: GET, POST, and hono-approvals-act.conformance's HEAD pin went red.\nOther GET-serving domains with the same shape on a createHonoApp host. All are closed by this change, because all reach dispatch() through the one catch-all.\nMeasured, GET vs HEAD at base:\n- /health and /ready (methods ['GET']): 200 vs 404 ROUTE_NOT_FOUND\n- the scoped discovery root /environments/:id: 200 vs 404\n- /analytics/meta, signed in: 200 vs 404\n- /ai/agents, signed in: 200 vs 501\n- data reads, signed in: 503 vs 404\n- share-links public resolve, anonymous: 501 vs 401\n- the auth arm's /auth/config, with an auth service: 200 vs 404\nCode-read, not measured: their service-absence check precedes the verb check in the measuring boot. These are i18n, automation GET faces, notifications, packages GET faces, security suggested-bindings, the other share-links GET faces, and meta's !method || method === 'GET' reads.\nPM checks: on a path that serves no GET (/keys, /actions), HEAD keeps 405 + Allow: POST signed in and 401 anonymous, unchanged on both doors. Every anonymous HEAD now gets the anonymous GET answer. The /mcp transport is handed HEAD on both doors.", "tests": "Commands and results:\n- New packages/adapters/hono/src/hono-head-dispatched-as-get.test.ts: 4 cases, green.\n- New packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts: 24 cases, real HttpDispatcher, createHonoApp app.request beside the standalone socket, green.\n- New case in packages/runtime/src/domains/approvals-act.test.ts, green.\n- PR #24244's standalone pins, plus approvals-act and http-dispatcher.mcp: 7 files, 291 passed. The pins are mcp-skill-method-not-allowed, actions-every-verb, keys-every-verb, notifications-every-verb and dispatcher-body-after-anonymous-floor .hono.integration.\n- Controls in the same run: hono-approvals-act, hono-webhooks-redeliver and hono-meta-item-read-gate conformance, 4 files, 42 passed.\nPackage runs, all through os-verify-lock, VERDICT command-exit 0:\n- @objectstack/hono test: 10 files, 211 passed. typecheck: exit 0. Both at 379b734a2.\n- @objectstack/http-conformance test: 16 files, 214 passed. typecheck: exit 0. Both at 379b734a2. The first typecheck at 0a5693634 failed with TS2322 in the new test (app.request returns Response | Promise of Response); fixed in 379b734a2.\n- @objectstack/runtime test (local project): 416 files, 6548 passed and 19 skipped. typecheck: exit 0. Both at 0a5693634; no runtime file changed after it, and the merge at 4f14775b4 brought scripts/pm/dispatch-gates* only.\nAblations (fix committed first; scripts/ablation-replace.mjs wrap mode; restore proven blob == HEAD and git diff HEAD empty):\n1. dispatchedMethod body 'return method === HEAD ? GET : method' replaced by 'return method'. Anchor 1->0, blob 4f962dbb6cfd->aa3f8f87edab. Hono unit: 3 failed, 1 passed (the other-verbs control stayed green). Conformance: 13 failed, 11 passed (the controls). The subject resolves from src in both suites (the http-conformance vitest alias of @objectstack/hono), so there is no dist leg.\n2. approvals 'request?.method === HEAD' replaced by 'method === HEAD'. Blob 67a8a4b44bf3->ab7e441de94c. approvals-act.test.ts: 1 failed, 12 passed ('expected HEAD to be GET'). The conformance leg was read on the pre-edit runtime dist (old line count 1, new 0): the HEAD pin was red, hosted 405 vs self-hosted 200. After the rebuild it was green.\nExpected direction: red. Observed: red.", "gates": "Local subset at the final head 4f14775b4:\n- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 66 commands, the same list at 0a5693634, 379b734a2 and 4f14775b4.\n- All 66 run at 4f14775b4, each exit captured before any pipe: 66 x exit 0. These include check:dispatcher-error-vocabulary, check:error-status-conformance ('every derivable runtime status is documented, and every documented status is reachable'), check:route-envelope, check:nul-bytes (OK), check:changeset-gate-self-tests, check-changeset-no-major --base origin/main, check:cross-package-test-inputs (OK), check:test-source-alias (OK), check:type-check-coverage, check:type-check-debt and check:dual-build-cjs-loads.\n- --ran reconciliation (as 'cmd :: exit N'): '66 derived famil(ies) accounted for, 66 run, 0 NOT-MEASURED (a DERIVED zero, all 66 recorded an exit code and none of them is 3)'.\n- First pass at 0a5693634: 2 prerequisite refusals (exit 3), both since re-run as above. check-plugin-teardown-shape --self-test needed its pinned fixture commit, fetched with depth 1, and then exited 0 with 48 cases. check:dual-build-cjs-loads needed every workspace dist.\n- ESLint (pnpm exec eslint --no-inline-config) over the 5 changed source and test files: exit 0, no output.\n- check-platform-checklist: not owed, since no export was renamed or deleted (dispatchedMethod is module-private).\nCI, read once after pr_create, without polling: check-runs on 4f14775b4 were 36, of which 16 in_progress, 8 queued, 6 success and 6 skipped. Build Core was queued and Governed Surface Queue Guard in_progress. Lint & Repo Gates, TypeScript Type Check and Test Core were not yet listed. Conclusion: in_progress, NOT MEASURED at report time.", "mcp_calls": "0 - no MCP GitHub tool used (reads were REST GETs through the container proxy; writes went through the scripts/pm relay)", "api_writes": "2 relay strokes from this session, each a POST /repos/objectstack-ai/objectstack/dispatches (scripts/pm/fleet-write/dispatch.mjs). Stroke 1, pr_create, ran as objectstack-fleet[bot] in run 39110139276: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/24286/assignees ['zhuangjianguo']. The read-back said '14071 byte(s) sent, 14071 stored, identical; assignees: zhuangjianguo'. Stroke 2, comment: POST /repos/objectstack-ai/objectstack/issues/24245/comments (this os-dev-report). git push (not REST): the empty branch, then 4 pushes up to 4f14775b4. No labels written (none expected). The card assignee was not written.", "open_questions": [], "out_of_scope_findings": [ "class: a, with contract text (b) beside it. reach: public door, measured in process. createHonoApp + LiteKernel with an mcp slot rendering '# skill', at 4f14775b4; toResponse is untouched by this PR, so it is pre-existing. GET /api/v1/mcp/skill through app.request answers 200 with body 'data: \"# skill\"' plus a blank line: an SSE frame of a JSON string, not the markdown. It also answers content-type 'text/markdown; charset=utf-8, text/markdown; charset=utf-8' (doubled) and cache-control 'no-cache, no-store'. The standalone server serves the raw markdown with cache-control 'no-store' (PR #24244's pin). Seam: runtime:packages/runtime/src/domains/mcp.ts handleMcpSkillRequest stream result ('string events are written verbatim' through every adapter) -> renderer: none; adapter: packages/adapters/hono/src/index.ts toResponse events arm. That arm SSE-frames every event unless vercelDataStream is set, and merges its capitalised Content-Type/Cache-Control with the domain's lowercase ones. Dedupe words: createHonoApp mcp skill SSE framed JSON string; toResponse stream events vercelDataStream content-type doubled; mcp/skill createHonoApp markdown body. Not fixed here (a different defect class); for the seat to file.", "carrier: none. Noted, not filed: the auth arm's legacy fallback dispatcher.handleAuth(path, method, ...) still passes the raw verb. handleAuthRequest does not read it (_method), so no answer differs. Recorded in PR Acceptance notes.", "carrier: none. Noted, not filed: a HEAD on a GET whose answer is an events stream still runs the stream producer, and Hono discards the body. The standalone server does the same for the same request. Recorded in PR Acceptance notes." ], "line_budget": "+469 / -9 over 6 files vs origin/main (3-dot). Source: index.ts +40/-2 and approvals.ts +10/-7. Tests: +174 hono unit, +213 conformance, +19 approvals-act. Changeset: +13. Under the 3000-line human-merge threshold.", "files_changed": [ ".changeset/24245-head-dispatched-as-get.md", "packages/adapters/hono/src/index.ts", "packages/adapters/hono/src/hono-head-dispatched-as-get.test.ts", "packages/runtime/src/domains/approvals.ts", "packages/runtime/src/domains/approvals-act.test.ts", "packages/qa/http-conformance/src/hono-head-as-get.conformance.test.ts" ], "deviations": [ "File surface widened in the same round, beyond the claim's two candidate homes: packages/runtime/src/domains/approvals.ts plus its test. It is required by the catch-all change, which was measured red without it (hosted HEAD /approvals/act answered 405), and it is declared in the PR body.", "The auth arm's /auth/config branch in the same file was fixed under the bounded in-place exemption: same class, mechanical, a file in this claim, same gate family. It was measured 200 vs 404 and pinned by a unit case.", "One changeset file lists both published packages as patch (@objectstack/hono, @objectstack/runtime), not one file per package. The file carries the bare Clause-② line, as 70 of 72 existing changesets do.", "origin/main (93b4acb57, scripts/pm/dispatch-gates* only) was merged before opening the PR, per AGENTS.md section 10. No package rebuild was needed, and gates were re-derived and re-run on the merged head.", "Fetched one pinned fixture commit (git fetch --depth=1 origin 621a4876...) into the shared object store so check-plugin-teardown-shape --self-test could run (it is additive).", "The measurement harness was a temporary, uncommitted test file under packages/qa/http-conformance/src, deleted before gate derivation. A copy is kept at the scratchpad path issue-24245/measure-harness.test.ts.txt.", "@objectstack/runtime test:repo (vitest repo project) was not run locally; it is CI's Test Core. The diff edits no test in that project.", "Commit trailers are the model-free pair the dispatch ordered; the PR body ends with AGENTS.md's session-URL footer. The harness's system reminder asked for a model-named Co-Authored-By and a different PR footer. AGENTS.md (pre-push hook: no model identifier) was followed, and the harness-written forms were not used.", "Answered the PM's mid-run status probe with one SendMessage line to the parent." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24286 comment
6247514938·domain:cliseat 1 · sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-18T08:14Z · the claim surface is amended in place forapprovals.ts; out-of-scope finding 1 filed as #24287; landing waits on every check of head4f14775b4.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed: PR #24286 →
9822b053e·domain:cliseat 1 ·zhuangjianguo· sessionsession_01SYEJqDL3NXxrJRBVqaMu9T· 2026-10-18T08:47Zmerged: true, merge commit9822b053e, merged 2026-10-18T08:47Z through the merge queue (enqueued 2026-10-18T08:27Z)origin/main9822b053eis an ancestor oforigin/main, with a single parent (squash). The 6 files and +469 / −9 match what was reviewed at4f14775b4gh-readonly-queue/main/*pr-24286*head remains6246689908(amended in place forapprovals.ts) → report6247465845→ ACCEPT6247514938on PR fix(hono): on a createHonoApp host a HEAD is answered as its GET, so HEAD /api/v1/mcp/skill no longer answers 405 Allow: GET #24286 → pointer6247526114. Filed from the review: hono: on a createHonoApp host, GET /api/v1/mcp/skill answers an SSE frame of a JSON string (data: "# skill") with a doubled Content-Type, not the raw markdown the standalone server serves #24287 (the SSE-framed/mcp/skillbody on acreateHonoApphost).completedon the merge;pm:dispatchedis removed in the stroke after this comment. runtime(mcp): an anonymous POST /api/v1/mcp answers 501 (no MCP service) or 404 (OS_MCP_SERVER_ENABLED=false) ahead of 401: handleMcpRequest checks the switch and the service before its anonymous decision #24251 (serial behind this card onmcp.ts) is released for its claim.Generated by Claude Code