Repository navigation
fix(rest): require a send capability on the REST email door and bound the sender address - #27469
Conversation
… its sender WIP: capability gate, sentBy bound to the caller, from bounded to the configured sender via the new optional IEmailService.getDefaultFrom. Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
…der bounds Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
… for the email send door bounds Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
… measure what lies past it Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 10 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 174 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0e004ed7e0accd9eff51823b622b1dfe7c548b2b && git checkout 0e004ed7e0accd9eff51823b622b1dfe7c548b2b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc 1b5c8eef84dfeb09fbd6be44435f73f5ae85ab04 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff 1b5c8eef84dfeb09fbd6be44435f73f5ae85ab04
node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc
|
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Triage seat, under gap rule (b) (#27062): this head is over 60 minutes old with no record and no claim. The review runs at Generated by Claude Code |
Contract reviewServed-tier: Inputs read: PR #27469 body, file list (12 files), net diff vs ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Cited code, read to judge the scenario the member is accepted against (the door's refusals fail closed):
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…aders map and derives the organization from the caller The REST send door now accepts custom headers only as the contract's Record<string, string> (a plain object, header field names, single-line string values) before its reserved-sender-name check, stamps organizationId from the caller's vetted active organization (refusing a body that names another), and refuses identityMail. The SMTP transport hands nodemailer custom headers as an explicit name/value list, one entry per key. The spec contract test pins getDefaultFrom like isConfigured, the module header states which sys_email columns the capability gates, and the changeset declares the narrowing. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Triage seat, under gap rule (b) (#27062): this head is over 60 minutes old with no record and no claim; a delta from triage's FAIL on Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Ruled: maintainer, in the PM session session_017xRBcVy4VXmkQjn2MGeaWk, 2026-10-29 · 2026-10-29T13:05Z Question (human floor, from the contract review): which capability the REST email-send door requires. Answer, verbatim option chosen: 「沿用 manage_platform_settings」 — keep Generated by Claude Code |
Contract reviewServed-tier: Inputs read: PR #27469 body, file list (15 files), net diff vs ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Cited code, read to judge the scenario the member is accepted against (the door's refusals fail closed):
Delta since the prior record (
|
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk {
"issue": null,
"status": "done",
"branch": "claude/harden-email-send-door",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27469",
"head": "1b5c8eef84dfeb09fbd6be44435f73f5ae85ab04",
"session": "session_017xRBcVy4VXmkQjn2MGeaWk — subagent run, the parent PM session's id",
"premise_still_valid": true,
"summary": "Rework of the REST email send door on PR 27469, two commits on top of 5a8a0233 (5ba07d1f, 1b5c8eef), pushed to the same branch with no force push; the PR stays draft. Must-fix 1: the door now accepts custom `headers` only as the contract's string-valued header map (a plain object, every key an RFC 5322 header field name, every value a single-line string) before the reserved-sender-name check, and refuses any other shape with 403 PERMISSION_DENIED in the door's nested envelope without calling the service. Two further gaps of the same class turned up while I measured it against nodemailer 10.2.1 in this tree. I fixed both in place under the bounded in-place exemption (same defect class, a mechanical fix, files already in this PR's reviewed scope, the same gate family). First, the SMTP transport (`packages/plugins/plugin-email/src/transports/smtp.ts`) now hands nodemailer the headers as an explicit name/value list, one entry per key. Second, header names must be valid field names. The mechanism and a local stream-transport reading are kept OFF GitHub, in the PM-only notes file /tmp/claude-0/-home-user-objectstack/bdda939c-7424-5594-af38-0e6fe5d7eeed/scratchpad/pr-27469/PRIVATE-mechanism-notes.md. Must-fix 3: the door stamps `organizationId` from the caller's vetted `tenantId` (the source meta-item-read-gate.ts documents). It refuses a body naming another organization, or naming one when the caller has none, and refuses `identityMail` for any value but false; the forwarded message never carries the caller's `identityMail`. Must-fix 2: the changeset now reads `fix(rest)!:`, `Clause-②: yes (narrowing)`, `Metadata-protocol: none` and a `Skills-docs:` line, carries a BREAKING banner and exactly one ADR-0087 marker (`not-required (no-migration-prescription)`, accepted by the gate), and stays patch on all four packages. Should-fixes: a getDefaultFrom compile-time parity pin in packages/spec/src/contracts/email-service.test.ts, and a module header that now says the capability gates the `mail` settings namespace, the whole `sys_identity_email` row and only the two body columns of `sys_email`. `replyTo` is deliberately left unbounded: SendEmailInput.replyTo declares it a caller-supplied Reply-To address with no sender claim, so the contract gives no configured value to bound it to. The capability choice (manage_platform_settings vs a dedicated send capability) is left as is, flagged pending the maintainer. Docs, the client JSDoc, the rest-server docblock and the integration-system checklist clause revision 10 now state all four bounds. CONFLICT, not resolved by me: the dispatch asks for the PR body to declare the new Clause-② line, but os-dev.md forbids a dev PATCH of a PR body ('PR 正文 dev 只写一次…⛔ 不 PATCH;事后要改的报告点名改法,席位代写'), and os-dev.md wins, so the body is unchanged. A complete replacement body is ready for the seat at /tmp/claude-0/-home-user-objectstack/bdda939c-7424-5594-af38-0e6fe5d7eeed/scratchpad/pr-27469/pr-body.md. It has no HTML comments and no angle-bracket fragments outside details/summary, and its Clause-② and Metadata-protocol lines read declared `yes (narrowing)` / `none` through scripts/pm/clause2-line.mjs. Suggested title: `fix(rest)!: require a send capability on the REST email door and bound the sender, headers, organization and identity mail`. Until the body is rewritten, the live PR body still says `Clause-②: no`, and its Acceptance notes still say organizationId/identityMail pass through.",
"tests": "All at head 1b5c8eef, each under scripts/pm/os-verify-lock.sh, with each part's exit captured separately. rest test: Test Files 335 passed (335), Tests 6614 passed, 326 skipped, exit 0 (+19 = the new door cases; email-send-door-bounds.test.ts is 36 cases). plugin-email test: 44/44 files, 748 passed, exit 0. client test: 52/52 files, 680 passed, exit 0. spec test: 750/750 files, 22315 passed, 1 todo, exit 0. typecheck exit 0 on rest, plugin-email, client and spec; check:test-typecheck is OK on each (spec holds its ledgered 52 files / 240 errors unchanged, so the new @ts-expect-error pins are compiled). Targeted first: the door, rest.test.ts and meta-state-route-engine-outage files 266/266; the smtp, sys-email-payload.wire and email-service files 61/61 (the wire test still reads the custom header on the real nodemailer wire after the list-form change); spec email-service.test 8/8. The first targeted run went red 3/266 on my own refusal wording (the value message did not name `headers`). I fixed the message, not the test, and committed it as 1b5c8eef. Ablations: committed first; 8 legs through scripts/ablation-replace.mjs in wrap mode, src-resolved, so no dist leg was needed. Each leg landed on disk (anchor count 1 to 0, blob changed), went red as predicted and restored to HEAD (blob == HEAD: email-routes.ts 0cd9f8785d03, smtp.ts ad530551a057; git diff HEAD empty; git status --porcelain empty at the end). Results: shape check off 2/36 red (null, string); field-name check off 3/36 (space, colon, non-ASCII); value check off 3/36 (object, number, multi-line); org refusal off 2/36; org stamp removed 3/36; identityMail refusal off 3/36; identityMail strip removed 1/36; SMTP list form reverted 2/26 in smtp.test.ts. The list-of-entries headers case is covered twice: it stays green under the shape leg alone and under the value leg alone, because the other check still refuses it.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1b5c8eef: 110 derived. The first stderr line names objectstack-ai/objectstack at 1b5c8eef, with a STALE TREE warning (behind origin/main; 6 scripts/pm inputs changed there). None of this PR's files changed on main, so I did not merge main (AGENTS.md MA-discipline 10). Every command ran with its exit captured before any pipe. --ran reconciliation: 110 derived, 109 run, 1 NOT-MEASURED (derived from a recorded exit 3), 0 UNRUN, exit 0. NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET (needs every package dist). check:skill-examples (no client-react dist) and check:type-check-debt (its re-measure never got a lock turn) first exited 3. After the closure build both were re-run: 272 prose examples type-check, exit 0; type-check debt re-measure OK, 0 raw tsc errors, exit 0. Changeset gates: check-adr-0087-registration accepts the one declared-breaking changeset [BREAKING+bang+clause-②-narrowing] as not-required (no-migration-prescription), exit 0. check-changeset-no-major driven offline (--event) with the proposed body: LEVEL AXIS reads yes (narrowing) + none at patch, exit 0. With the current live body it still passes, reading `no` + none, exit 0. Clause-② line checker (readClause2Line / readMetadataProtocolLine from scripts/pm/clause2-line.mjs): changeset and proposed body both read declared yes/narrowing and declared none. CI check-runs on 1b5c8eef: 50 runs, 41 success, 9 skipped, 0 failure, 0 pending. Required contexts: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core success, Governed Surface Queue Guard success, Type Check UI success, Test UI success, Build & E2E UI success, Live E2E UI success, Lint UI skipped (path). The TypeScript Type Check red the PM flagged on 5ba07d1f was its 'Type Check · workspace' lane concluding cancelled at 11:18:29, superseded by my push of 1b5c8eef at 11:17:21 (the run's conclusion is cancelled). It was not a type error, and the same check is success on 1b5c8eef. Not run locally: test:repo (CI Test Core green); check:dev-prereqs and check:skill-frame-freshness (this round's spec change is a test file, not a spec build input).",
"dogfood_battery": "1b5c8eef · dogfood closure built (69 packages, exit 0), then `pnpm --filter @objectstack/dogfood test` under the lock, no shard and no path (lock held 2198s; parts sequenced with ';' and each exit echoed: BUILD 0, DOGFOOD 0) · Test Files 348 passed, 3 skipped (351) · Tests 2806 passed, 20 skipped (2826) · no failures",
"mcp_calls": "0",
"api_writes": "1 — POST repos/objectstack-ai/objectstack/issues/27469/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Plus 2 git pushes to claude/harden-email-send-door (not REST). No PR body, label, assignee or draft-state write.",
"deviations": [
"PR body not edited: os-dev.md forbids a dev PATCH of a PR body and wins over the dispatch; a replacement body is staged for the seat (path in summary).",
"Scope widened, bounded in-place fix: smtp.ts list-form headers plus the header-field-name rule close two same-class gaps beyond the dispatched fix (mechanism in the private notes file).",
"Refusal status for an off-shape headers value is 403 PERMISSION_DENIED, matching the governing record's pin and the door's other bounds, not 400.",
"identityMail: false is admitted (it restates the door's answer, like sentBy = caller); every other value is refused, and the forwarded message never carries the key.",
"Commit trailer: the model-free pair AGENTS.md requires, not the harness reminder's model-named Co-Authored-By."
],
"open_questions": [
{
"question": "Who writes the PR body and title for the Clause-② change?",
"options": ["A the seat writes the staged body (scratchpad pr-body.md) and the `fix(rest)!:` title", "B the dev PATCHes it, against os-dev.md"],
"recommendation": "A, because os-dev.md reserves post-open body edits to the seat, and the staged text already reads clean through the Clause-② reader and the offline level axis."
},
{
"question": "Capability for the send door (human floor, left as is): keep manage_platform_settings or add a dedicated send capability?",
"options": ["A keep manage_platform_settings (current)", "B add a send capability to PLATFORM_CAPABILITIES on its own card"],
"recommendation": "Maintainer's call. The PR keeps A and its 维护者速读 names it as the one pending action."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: plugin-email normalizeMessage forwards any non-empty `headers` object from server-internal callers without checking the contract's string-valued map shape. Those callers are typed, and the HTTP door is now the bound for outside input, so this is a read-only inference with no public door: it goes in the staged body's Acceptance notes, not on a card."
]
} |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Delta record. Same head as the full record 6497633638 (triage seat, FAIL); the head did not move. The only change since is the PR body, rewritten by the PM seat (the PR's Inputs read: the PR body over the REST API (not the GraphQL view), the changeset on the committed tree, the door module, the bounds suite and the docs page on the head, the shared declaration reader ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentUnchanged from 6497633638 ④ and not redone: the body edit touched no file in Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk ACCEPT → head 维护者速读
席位意见:收。契约复核在本 head 上 PASS(6497855687,判的是 6497633638 之后的增量),dogfood 全套 348 个文件、0 失败。 席位核对
Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: yes (narrowing) — the REST send door refuses callers and bodies it admitted (the breaking half), and the published
IEmailServicecontract gains an optionalgetDefaultFrom()(a widening, the same move asisConfigured()on the same interface). No spec key, export or config field is removed or renamed.Metadata-protocol: none
Skills-docs: none — no file under
skills/**teachesPOST /api/v1/email/send,client.email.sendorIEmailService(agit grepofskills/for each names nothing), so no skill line changes. The runtime-services page,content/docs/kernel/runtime-services/email-service.mdx("Over HTTP"), states the four bounds in this change.No tracking issue: this hardening was sent straight to a dev seat, so the body has no closing line.
Review tier: this change narrows who may call a public REST door and what they may send through it. It needs one contract-review-tier review of the current head before it enters the queue.
维护者速读
POST /api/v1/email/send在发送前做四道校验,不满足一律 403PERMISSION_DENIED、不发信:① 调用方须持有manage_platform_settings;②sentBy一律记为当前登录用户;③ 发件人限定为部署配置的发件地址,自定义headers只接受"字符串到字符串"的普通对象(合法头名、单行值),且不得设From/Sender/Return-Path;④ 邮件日志行的organizationId由服务端取调用方当前组织,不接受调用方另填别的组织,也不接受identityMail(身份类邮件只由服务端自己发)。SMTP 通道把自定义头按"一个键一个头"的显式列表交给 nodemailer。from、他人sentBy、别的组织organizationId、identityMail: true,或自定义头不是普通字符串对象的调用都会被拒。flow、通知、认证邮件走服务内部调用,不受影响。回滚即 revert 本 PR。packages/rest/src/email-routes.ts:headersRefusal()与第 4 道(组织/身份邮件)分支,以及转发前的organizationId盖章;②packages/plugins/plugin-email/src/transports/smtp.ts:自定义头改为显式列表;③packages/rest/src/email-send-door-bounds.test.ts。pnpm --filter @objectstack/rest exec vitest run src/email-send-door-bounds.test.ts,预期 36/36 通过。manage_platform_settings(本 PR 现状),还是另立专用的发信能力(需在 spec 的PLATFORM_CAPABILITIES新增一项,另起一张卡)。本 PR 保持现状,等你一句话。What changes
POST {base}/email/send(packages/rest/src/email-routes.ts, both the unscoped and the environment-scoped mount) now applies four bounds between the auth floor and the send. Each one answers before the mail service is called, and each refusal is403 PERMISSION_DENIEDin the nested ADR-0112 envelope ({ error: { code, message } }), the shapeenforceAuthalready uses:manage_platform_settings(EMAIL_SEND_CAPABILITY), read from the resolved context'ssystemPermissions. It is the capability themailsettings namespace reads and writes on, the onesys_identity_emailrequires for the whole row, and the onesys_emailrequires for its two body columns (body_text,body_html; the rest of asys_emailrow is not gated on it). The check runs before the provider lookup, so a refused caller gets the same answer whether or not mail is wired.sentByis always the authenticated caller. A body that names a different user is refused; a body that names the caller, or no one, is stamped with the caller.IEmailService.getDefaultFrom()). Afromnaming another address is refused; one naming the configured address is accepted and the configured value is forwarded in its place. A service that cannot report its sender, or a deployment with none configured, refuses a callerfrom. Customheadersare accepted in one shape only, the contract's string-valued header map: a plain object whose every key is a header field name (printable ASCII, no space, control character or colon) and whose every value is a single-line string. Any other shape is refused, and so are customFrom,SenderandReturn-Pathheaders, so the reserved-name check reads each header exactly as a transport will.organizationIdfrom thetenantIdthe identity resolver vetted onto the execution context (the sourcemeta-item-read-gate.tsdocuments), and the service stamps it verbatim ontosys_email.organization_id. A body naming another organization is refused, and so is a body naming one when the caller has no active organization.identityMailis refused for any value butfalse: identity mail is sent by the server itself, and its log is not a destination an HTTP caller chooses. The forwarded message never carries the caller'sidentityMail.replyTois deliberately not bounded:SendEmailInput.replyTodeclares it a caller-supplied Reply-To address and makes no sender claim of it, so there is no configured value to bound it to.To support bound 3,
IEmailService(packages/spec/src/contracts/email-service.ts) gains an optionalgetDefaultFrom(). plugin-email'sEmailServiceimplements it by reading the livedefaultFrom, so a sender changed in the Mail settings is followed. The member is optional, so existing implementations stay valid, and the door treats a missing member as unknown.plugin-email's SMTP transport (
packages/plugins/plugin-email/src/transports/smtp.ts) now hands nodemailer custom headers as an explicit list of name/value entries, one per key of the headers map, so every key is read as exactly one header name. A well-formed map puts the same headers on the wire as before (the existing wire test,sys-email-payload.wire.test.ts, still reads them there).Server-internal senders (flows, notifications, auth mail, the messaging email channel) call
IEmailService.senddirectly and are unchanged.Docs: the
client.email.sendJSDoc;content/docs/kernel/runtime-services/email-service.mdx("Over HTTP", now four bounds and thereplyTonote); theintegration-system.email-template-renderchecklist clause, revision 10, whose step (d) now also drives the headers-shape, organization and identity-mail refusals and the organization stamp.Tests
packages/rest/src/email-send-door-bounds.test.ts(36 cases), through the realHonoHttpServertransport over a contract-typed mail-service double. Every refusal case also asserts the service was never called; every admission case asserts what it was called with.from; unentitled or no capability set 403 with neither service nor provider called; no provider 403 vs 501; anonymous 401; non-object body 400;sentBy: foreign 403, the caller's own 200;from403 (string and object forms), restated configured address 200 with the configured value forwarded, no configured sender or no accessor 403 for a callerfromwhile a send without one succeeds,From/sender/Return-Path403, other headers pass through;nulland a string are each 403 with nothing sent; an empty map and a well-formed map are forwarded as sent;organizationIdforwarded;true,'true'and1are 403 with nothing sent;falseis admitted and the forwarded message carries noidentityMail.packages/plugins/plugin-email/src/transports/smtp.test.ts: the field mapping now expects the list form, and a new case pins one name/value entry per key, in order.packages/spec/src/contracts/email-service.test.ts: a compile-time parity pin forgetDefaultFrom, the wayisConfiguredis pinned (absent stays assignable; either address form orundefinedaccepted; a number, or an object withoutaddress, refused by@ts-expect-errorin a filecheck:test-typecheckcompiles).packages/plugins/plugin-email/src/email-service.test.ts(unchanged in this round):getDefaultFromreturns the constructor sender, followssetDefaultFrom, and isundefinedwhen none is set.Acceptance notes
manage_platform_settingsis platform-scoped, so an organization administrator cannot send through this door. That matches who can configure the transport today. A dedicated send capability would be aPLATFORM_CAPABILITIESaddition and is left to the maintainer's decision; this PR keeps the current choice.fromthat restates the configured address with its own display name is accepted, and the configured display name is sent. A per-send display name is no longer possible through this door.replyTostays caller-controlled (see What changes). Bounding it would need a contract that reserves it, whichSendEmailInputdoes not.normalizeMessagestill forwards any non-emptyheadersobject from server-internal callers, which are typed against the contract. The HTTP door is the bound for outside input.Verification
Tree
objectstack-ai/objectstackat1b5c8eef(all readings below were taken at this head unless a line says otherwise).Package tests and typecheck (at 1b5c8ee)
pnpm --filter @objectstack/rest test: Test Files 335 passed (335), Tests 6614 passed, 326 skipped. Exit 0. (+19 over the previous head: the 19 new door cases.)pnpm --filter @objectstack/plugin-email test: Test Files 44 passed (44), Tests 748 passed. Exit 0.pnpm --filter @objectstack/client test: Test Files 52 passed (52), Tests 680 passed. Exit 0.pnpm --filter @objectstack/spec test: Test Files 750 passed (750), Tests 22315 passed, 1 todo. Exit 0.typecheckexit 0 on all four (rest,plugin-email,client,spec); eachcheck:test-typecheckOK,specholding its ledgered 52 files / 240 errors unchanged, the other three at 0.--filter 'PKG^...', 38 packages), lock verdictcommand-exit 0.Ablations (committed first; each leg mutated through scripts/ablation-replace.mjs in wrap mode, restored to HEAD and proven by blob hash and an empty git diff HEAD)
The door test resolves
./rest-serverand./email-routesfromsrc, andsmtp.test.tsresolves./smtp.jsfromsrc, so no rebuild leg was needed. Every leg's restore readblob == HEAD(email-routes.ts0cd9f8785d03,smtp.tsad530551a057) withgit diff HEADempty, andgit status --porcelainwas empty after the last leg.null, a string). The list-of-entries case stays refused by the value check.identityMail: falsecase, which also reads the stamp).true,'true',1).identityMail: falseforwarded).smtp.test.ts(the field mapping and the list-form case).Full dogfood battery (the Clause-② line carries the narrowing arm)
@objectstack/dogfooddependency closure (69 packages, exit 0):pnpm --filter @objectstack/dogfood test, no shard, no path. Test Files 348 passed, 3 skipped (351); Tests 2806 passed, 20 skipped (2826). Exit 0.Derived gate families (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, at 1b5c8ee)
--ranreconciliation over the recorded exit codes: 110 derived, 109 run, 1 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET, needs every package'sdist). CI'sLint & Repo Gatesis green on this head.check:skill-examplesandcheck:type-check-debtfirst exited 3 on prerequisites (noclient-reactdist; the debt re-measure did not get a lock turn) and were re-run after the closure build: 272 prose examples type-check across 3 surfaces, exit 0; the type-check debt re-measure OK, 0 raw tsc errors, exit 0.check-adr-0087-registrationaccepts the one declared-breaking changeset (BREAKING+bang+clause-②-narrowing) withnot-required (no-migration-prescription);check-changeset-no-majordriven offline with this body as the PR payload readsClause-②: yes (narrowing),Metadata-protocol: none, a non-protocol change atpatch, exit 0.origin/main, with 6 changedscripts/pm/inputs; none of this PR's files changed onmainin that range, somainwas not merged.CI on 1b5c8ee
50 check runs: 41 success, 9 skipped (opt-in and matrix placeholders,
Lint UIpath-skipped), 0 failure, 0 pending. All ten required contexts are success exceptLint UI, which is path-skipped. The redTypeScript Type Checkon the previous head 5ba07d1 was itsType Check · workspacelane cancelled by the push of 1b5c8ee, not a type error.Generated by Claude Code
Body refreshed by the PM seat 2026-10-29T13:18Z after contract review 6497633638 (must-fix 1: the body carried the stale Clause-② line) and os-dev report 6497744791.
🤖 Generated with Claude Code
https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk