Skip to content

feat(push): requires push and the stack push key mount PushServicePlugin under os serve - #29968

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-rx3xpv
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-rx3xpv

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29820

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for requires: ['push'] or the push key); this diff teaches both in skills/objectstack-platform/SKILL.md (the top-level key enumeration and the requires table) and in the new content/docs/kernel/runtime-services/push-service.mdx

维护者速读

  • 改了什么:requires: ['push'] 加上栈定义里新的顶层 push 键,objectstack serve(和 @objectstack/verify 的 bootStack)就会挂上 PushServicePlugin,不再需要宿主代码;托管 relay(含不写 push 键、push: {})与内联密钥在启动时被拒,报错写明原因与改法。
  • 为什么改:此前只有在代码里手动组合插件才有推送服务,按栈文件运行的部署里 notify 选 push 渠道无处投递;relay 发送端尚未上线,若照常启动会挂上推送渠道、把每条推送都答成已发送而实际什么都没发出。
  • 风险与回滚:纯增量:只有声明 requires: ['push'] 的栈会走新路径,未声明的启动不变;回滚即回退本 PR。唯一新增的启动失败是"声明了 push 却只配了 relay(或没配)",这是本卡要求的拒绝。
  • 先看:① packages/services/service-push/src/capability-arg.ts(relay 与 schema 拒绝及其报错文字)② packages/core/src/capability-composition.ts 的 push 分支(serve 与 verify 共用的一条规则)③ skills/objectstack-platform/SKILL.md 的两处改动(Tier H,受管面)。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --project unit src/commands/serve-stack-email-sms-door.contract.test.ts,预期 push 段全部通过:direct 配置注册出 vendors 为 ['apns'] 的推送服务,无键 / {} / relay 三种都以 "its sender has not shipped … transport: 'direct'" 被拒。
  • 需要你做的:本 PR 触及 skills/**(Tier H),需要一位 GOVERNED_APPROVERS 维护者批准后才能合并。

What changes

  • @objectstack/spec
    • push joins PLATFORM_CAPABILITY_TOKENS, provided by @objectstack/service-push, edition open. It is requested, never always-on, because its schema default (the relay) is refused at boot.
    • The stack definition gains a top-level push key (PushServiceConfigSchema, secrets by credentialRef only) with a COMPOSE_KEY_DISPOSITIONS row single, classified deployment in the unmanaged-install table and listed among the artifact-envelope keys.
    • push-config.zod.ts's docblock now names the stack key and objectstack serve as its reader. Only PushRelayTransportConfigSchema keeps [PLANNED — not yet served].
  • @objectstack/core: CAPABILITY_PROVIDERS.push names PushServicePlugin, and resolveCapabilityArgument('push', …) reads resolvePushCapabilityArg off the provider module, as the email and sms readers are read. One rule serves both objectstack serve and bootStack.
  • @objectstack/service-push: resolvePushCapabilityArg(stack.push) parses the key and throws, before the kernel boots:
    • on the relay (absent key, null, {}, transport: 'relay'): PUSH_RELAY_NOT_SERVED_MESSAGE (names the missing relay sender, prescribes transport: 'direct' with an apns and/or fcm block), plus where the key is written on this boot path;
    • on anything the schema refuses, an inline secret first among them: each path and the schema's guidance, never the value. A block with no transport has its invalid_union opened to the member the schema's own rule picks, so the guidance is not lost to "Invalid input".
    • On a stack that requires push, either refusal fails the boot as Capability "push" (@objectstack/service-push) failed to start: ….
  • @objectstack/cli, @objectstack/verify: depend on @objectstack/service-push, so the provider the open-edition token names resolves out of the box. The cli's vitest config gains the anchored source alias.
  • Docs: a new services.push page (content/docs/kernel/runtime-services/push-service.mdx): the two vendors, credential references, the relay's not-served status and its boot message, the device routes, dead-token retirement and the grading table; index rows in the runtime-services index, the kernel index and the versioning matrix. The generated push-config reference is regenerated.
  • Skill (skills/objectstack-platform/SKILL.md, Tier H): push is added to the top-level key enumeration (the required gate check:skill-top-level-keys refuses the diff without it) and to the requires table. The new tokens are paid for in the same file by deleting a stale path and count ("registry in packages/cli/src/commands/serve.ts — its 19 opt-in entries" becomes "registry in @objectstack/core"; the registry moved to core and holds more than 19 entries).
    • File: 429 → 429 lines, 6011 → 6004 tokens (ceiling 6007 before this diff's deletion paid it).
    • Bundle (every SKILL.md): 4539 → 4539 lines; whole shipped tree 224969 tokens after, 7 fewer than before (this is the only skills/** file touched).

Why

Ruling C on #29820: the token, the key and the os serve reader land together, so no released version has a push token that boots to nothing or a key nothing reads. The relay is refused rather than served because a served relay would mount the messaging push channel and answer every push sent while nothing left the server. A token with no CAPABILITY_PROVIDERS row is not dropped silently: a new reverse pin asserts every open-edition token has a serve mount.

Tests

  • packages/spec/src/kernel/platform-capabilities.test.ts: the token, its package and edition, not always-on, classify installable.
  • packages/spec/src/stack-email-sms.test.ts: the key: a direct block is carried through; a vendor block without transport reads as direct; relay, {} and absence are accepted at authoring; verdict parity with PushServiceConfigSchema; disposition single; an inline key refused at ['push','apns'] with STACK_SCHEMA_INVALID/422, value not echoed; a secret pasted into credentialRef refused; compose names push.
  • packages/services/service-push/src/capability-arg.test.ts (new): the direct argument constructs a plugin that registers push; absent / null / {} / relay / staging relay are refused with the relay message; inline APNs key, PEM in credentialRef, a relay credential and a vendor-less direct block are refused with their guidance and no echoed value.
  • packages/core/src/capability-composition.test.ts: the reader is called with stack.push only ({} when absent); its refusal propagates; a provider module without the export is refused by name.
  • packages/cli/src/commands/serve-stack-email-sms-door.contract.test.ts: serve composition: the table row; resolveServedCapabilities(['push']) contains push and ([]) does not; a direct block reaches the plugin and init registers vendors ['apns']; no key / {} / relay refused with the relay message; a raw-config inline serviceAccount refused, value not echoed.
  • packages/cli/test/serve-capability-vocabulary.test.ts: new reverse pin, every open-edition token has a serve mount (a provider entry or a tier); serve-capability-identity.test.ts gains the push plugin name; option-b-reader-acceptance.pin.test.ts lists push among envelope keys.
  • packages/verify/src/harness.served-composition.test.ts: bootStack mounts the push plugin from a direct fcm block; a plain boot has none; requires: ['push'] with no key rejects with the relay refusal.

Acceptance notes

Verification

Local test runs (all under scripts/pm/os-verify-lock.sh)
  • Full build pnpm exec turbo run build --concurrency=2: 120/120 tasks, VERDICT command-exit 0; @objectstack/spec rebuilt again after the last spec src change, exit 0.
  • pnpm --filter @objectstack/spec test: 808/808 files, 24288 passed, 1 todo. pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/spec test:repo: 89/89 files, 1524/1524 tests.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 377/377 files, 5316/5316 tests. cli typecheck exit 0. The integration tier was not run locally: the diff touches no spawn entry, so CI runs it.
  • pnpm --filter @objectstack/core test and typecheck: exit 0. pnpm --filter @objectstack/service-push test and typecheck: exit 0. pnpm --filter @objectstack/verify test: 48/48 files, 408/408 tests; typecheck exit 0.
  • pnpm --filter @objectstack/metadata-protocol test: 311 passed, 4 skipped files; 22320 passed, 26 skipped tests.
  • ESLint (--no-inline-config) over the 19 changed .ts/.mjs files: exit 0. CI runs the full pnpm lint.
Ablation: the relay boot refusal

node scripts/ablation-replace.mjs turned if (config.transport !== 'direct') { in packages/services/service-push/src/capability-arg.ts into if (false /* ablated-29820 */) { (anchor count 1 → 0, blob 8638e2857a5c → 2c1a8ad32f78).

  • Ablated: src/capability-arg.test.ts 5 failed / 7 passed (all five relay rows: absent, null, {}, transport: 'relay', staging endpoint); cli serve-stack-email-sms-door.contract.test.ts 3 failed / 7 passed (no key, {}, relay).
  • Restored: blob back to HEAD 8638e2857a5c, git diff HEAD empty; the same two files 12/12 and 10/10.
Gates: dispatch-gates
  • node scripts/pm/dispatch-gates.mjs --commands: 142 commands on the final tree, each run and its exit code recorded; all 142 exit 0.
  • --ran: "142 derived famil(ies) accounted for — 142 run, 0 NOT-MEASURED".
  • Four gates were red on the first pass and are fixed in this diff: check:skill-top-level-keys (push missing from the skill's key list), check:test-source-alias (cli vitest alias), check:docs-audit-scope (the new page in the audit scope), and check-plugin-teardown-shape --self-test (exit 3 from a shallow clone; green after fetching its base commit).
  • pnpm --filter @objectstack/spec run check:generated: exit 0.
  • check-changeset-no-major with this body: exit 0 ("no major bump"; LEVEL AXIS ✓ for yes (widening) and added; no moved package graded patch). Control: the same diff declared Metadata-protocol: none exits 1, refused only because the entries are graded minor.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. ESLint over the 19 changed files passes; the full pnpm lint is left to CI.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzgjFwEiezTWdb7bk9i

…gin under os serve

The push capability token (spec, open edition, @objectstack/service-push),
the top-level push stack key (PushServiceConfigSchema, single on compose),
the core provider row and its argument through resolvePushCapabilityArg,
which refuses the not-yet-served relay (an absent key included) and an
inline secret before the kernel boots. cli and verify depend on
service-push. Adds the services.push runtime-services page.

Claude-Session: https://claude.ai/code/session_01WGufzgjFwEiezTWdb7bk9i
Co-authored-by: Claude <noreply@anthropic.com>
…n the push composition in bootStack

The envelope-key lists in spec (assembled body, unmanaged install) and
the cli option-B pin classify the new top-level push key with email and
sms; the verify relay pin asserts the reader refusal, before the kernel
starts.

Claude-Session: https://claude.ai/code/session_01WGufzgjFwEiezTWdb7bk9i
Co-authored-by: Claude <noreply@anthropic.com>
…cli source aliases

skills/objectstack-platform names the new top-level push key and the
push token (check:skill-top-level-keys), paid for by dropping its stale
registry path and count; the hand-written docs list gains the
services.push page; the cli vitest config resolves service-push to src.

Claude-Session: https://claude.ai/code/session_01WGufzgjFwEiezTWdb7bk9i
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/core, @objectstack/service-push, @objectstack/spec, @objectstack/verify, touching 13 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/cli/package.json, packages/cli/vitest.config.ts, packages/services/service-push/src/index.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/frontend/cli.mdx (via invalid_union (literal, a string literal in leafIssues))
  • content/docs/getting-started/examples.mdx (via COMPOSE_KEY_DISPOSITIONS (symbol, a top-level const object))
  • content/docs/permissions/capabilities.mdx (via PLATFORM_CAPABILITY_TOKENS (symbol, a top-level const object))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via PLATFORM_CAPABILITY_TOKENS (symbol, a top-level const object))
  • content/docs/releases/v17/17-0.mdx (via invalid_union (literal, a string literal in leafIssues))
  • content/docs/releases/v17/17-4.mdx (via COMPOSE_KEY_DISPOSITIONS (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/cli/package.json, packages/cli/vitest.config.ts, packages/services/service-push/src/index.ts, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 47 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 186 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 630e31daad5fb8bddb17e5a12a70b74011c766f2 — the merge of head 3612ed4d30422bb73fdbf5e858a1e42e566b06e6 into base 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 630e31daad5fb8bddb17e5a12a70b74011c766f2 && git checkout 630e31daad5fb8bddb17e5a12a70b74011c766f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb 3612ed4d30422bb73fdbf5e858a1e42e566b06e6 && git checkout -B drift-repro 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb && git merge --no-ff 3612ed4d30422bb73fdbf5e858a1e42e566b06e6

node scripts/docs-audit/affected-docs.mjs --json 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3b8c724cf21fe2b39b2c635c6ef2a2448fa952fb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0f588fe6af5c74301cc811687869e8d508503318
Local-runs: none
Scope: packages/spec/src/kernel/platform-capabilities.ts, packages/spec/src/kernel/unmanaged-install.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/push-config.zod.ts, skills/objectstack-platform/SKILL.md + packages/core/src/capability-providers.ts, packages/core/src/capability-composition.ts, packages/core/src/stack-auth.ts (CAPABILITY_TO_TIER), packages/services/service-push/src/capability-arg.ts, packages/services/service-push/src/push-plugin.ts, packages/services/service-push/src/push-service.ts, packages/services/service-push/src/credential.ts, packages/services/service-push/src/transports/apns.ts, packages/cli/src/commands/serve.ts (the capability loop, lines 5078-5225), packages/cli/src/utils/capability-preflight.ts, packages/verify/src/required-providers.ts, packages/spec/src/kernel/unmanaged-install.test.ts, packages/cli/test/serve-capability-vocabulary.test.ts, scripts/check-skill-top-level-keys.mjs, skills/objectstack-platform/evals/config-plugins-ops.json, content/docs/kernel/runtime-services/push-service.mdx, content/docs/getting-started/quick-start.mdx, content/docs/getting-started/examples.mdx, content/docs/permissions/capabilities.mdx, content/docs/frontend/cli.mdx, packages/services/service-messaging/src/push-channel.ts. The rest of the diff (tests, the generated push-config reference, the docs index rows, the changeset, the lockfile, the cli vitest alias, the docs-audit scope) is the lane seat's ACCEPT at TIER_DEFAULT.

① Derived judgments

Judged against seat ruling C (comment 6601454188): the token, the top-level push key and the os serve reader land together; the token's edition is true; a relay configuration (the empty default included) is refused at boot naming the missing sender and pointing at transport: 'direct'; an inline secret is refused; no capability is dropped silently; a services.push page; the push-config docblock and markers made true.

  1. push joins PLATFORM_CAPABILITY_TOKENS and PLATFORM_CAPABILITY_PROVIDERS (@objectstack/service-push, edition open), not on the always-on slate. Right. defineStack now accepts requires: ['push'] (widening). Edition open is true on this head: packages/cli/package.json and packages/verify/package.json both gain @objectstack/service-push: workspace:*, so serve's await import(spec.pkg) and the preflight's cliRequire fallback (makeProviderResolver, which tries the project root, then the cli's own location) resolve it out of the box, exactly as email, sms and mailbox resolve. PR feat(notifications): the push channel — service-push (APNs + FCM) and its channel shell in service-messaging #29794 is on main, so the 404 the first executor measured (6601490513) no longer applies. Validate Package Dependencies is green on the head.
  2. Top-level push key on ObjectStackDefinitionSchema = PushServiceConfigSchema.optional(). Right. Before this head the top level refused push as an unknown key; it is now authorable (widening, Metadata-protocol: added). COMPOSE_KEY_DISPOSITIONS.push = 'single' beside email / sms, enforced by the table's satisfies clause over StackDefinitionKey, so STACK_DEFINITION_KEYS carries it and the skill gate (check-skill-top-level-keys.mjs, which reads the table's keys out of the source text in both directions) forced the skill edit. Authoring accepts a direct block, a relay block, {} and absence, and refuses an inline secret by key name (strict-object guidance, credentialRef as the remedy) or pasted into credentialRef (the one-token regex). Right: refusing the relay is the boot's rule, not authoring's, so a stack written for the relay survives service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential) #29920 unchanged.
  3. UNMANAGED_INSTALL_KEY_DISPOSITIONS.push = 'deployment' and the artifact-envelope key list. Right. unmanaged-install.test.ts holds the table total over the stack shape and the export shape, so the row was forced; deployment is the correct class (read by a stack's boot, never by an install), the same as email / sms. The install door therefore never judges a push block in an export, which is what a credential-free deployment key wants.
  4. CAPABILITY_PROVIDERS.push in core, and the push branch of resolveCapabilityArgument. Right. The branch reads resolvePushCapabilityArg off the provider module (the same providerReader pattern as email / sms, refused by name when the module lacks the export) and hands it stack.push ?? {} only, no env. One rule serves both objectstack serve (serve.ts 5156) and @objectstack/verify's constructServedProviders (required-providers.ts 153), so the verify test's cannot build it wrapper and serve's Capability "push" (@objectstack/service-push) failed to start: wrapper carry the same inner message.
  5. The boot refusal (capability-arg.ts) and PushServicePlugin.init agree. Checked by reading both: each parses with PushServiceConfigSchema and refuses on config.transport !== 'direct'; absent key, null, {}, transport: 'relay' (with or without endpoint) all read as the relay through the union's unionFallback and are refused with PUSH_RELAY_NOT_SERVED_MESSAGE (names "its sender has not shipped", prescribes transport: 'direct' with an apns and/or fcm block), plus a sentence saying where the key is written on this boot path. An inline secret (key, privateKey, serviceAccount, …) is refused by path and guidance, never by value; leafIssues opens the no-transport invalid_union to the member the schema's own rule picks (relay first, direct when apns or fcm is present), which matches the member order of the discriminated union on this head. A host that composes the plugin itself in plugins[] wins over the resolver (hasPluginMatching), and then the plugin's own init refusal applies, with the same rule. Right.
  6. A stack with requires: ['push'] and no push key fails the boot. Right, and intended by ruling C ("refuse it at boot naming the missing sender"): push is in declaredRequires, so serve's catch rethrows as a hard boot error; verify's bootStack rejects likewise (pinned). os build / os validate do not boot providers, so they pass such a stack; the preflight classifies push as ok (installed) or installable, exactly as sms.
  7. No silent drop. Right. The new reverse pin in serve-capability-vocabulary.test.ts holds every open-edition spec token to a CAPABILITY_PROVIDERS row or a CAPABILITY_TO_TIER tier. Checked by hand on the head: the 25 open-edition tokens are i18n, ui, auth (tiers) plus 22 tokens that are exactly the 22 keys of CAPABILITY_PROVIDERS; the pin passes and is non-vacuous (asserts push is among them). This also answers the first executor's out-of-scope observation (6601490513).
  8. services.push docs page. Right against the cited code: vendors() / send() and PushSendResult match push-service.ts; send() never throws (the service catches transport throws into failed, retryable: true); the grading table matches push-channel.ts (delivered / invalid_recipient twice / permanent twice / retryable / transport_not_configured); the device routes match protocol.zod.ts 3578-3580; credential_unresolved is credential.ts 35, surfaced through credentialFailure. The boot message quoted is serve's wrapper plus PUSH_RELAY_NOT_SERVED_MESSAGE, verbatim. The canonical-source row points at push-service.ts because packages/spec/src/contracts/ has no push contract (only email and sms), which is correct, not drift.
  9. push-config.zod.ts docblock and markers. Right. The "why it is not a stack key" paragraph on main is replaced by the true reader paragraph; [PLANNED — not yet served] stays on PushRelayTransportConfigSchema alone, and the direct schema's describe says "read by the push service". The claim "No OS_PUSH_* environment variable carries any of it" is true: the reader reads no env. The generated content/docs/references/system/push-config.mdx is regenerated from it (Spec property liveness green).
  10. Skill. The key enumeration gains push (gate-forced, both directions); the requires table row sms push → @objectstack/service- + token is true; "registry in @objectstack/core" is true on this head (CAPABILITY_PROVIDERS lives in packages/core/src/capability-providers.ts, serve.ts holds a handle), and the deleted "19 opt-in entries" count was false on main already (22 rows now). The token ceiling is paid in the same file (dev measured 6011 → 6004); Lint · docs & code guards, which carries the ratchet, is still pending.
  11. Check-runs on the head. Completed success: Auto Label, Build UI Deps, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · repo & tooling gates, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, PR-body guards, Spec property liveness, Validate Package Dependencies, filter. Skipped (opt-in or matrix placeholders): 18.x upgrade harness, Dogfood Verify CLI, Packed-tarball smoke, Schedule gate, Temporal Conformance, UI Published Artifacts, Dogfood Regression Gate matrix shard. Still pending at the time of this record: Build Core, Build Docs, Build & E2E UI, Check Changeset, Lint UI, Lint · CI release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · scoped · query-options erasure, Live E2E UI, Plan Test Core shards, Test UI (1-16/16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace. The verdict below holds on the committed tree; the pending gates are the lane seat's merge condition, and a red among them reopens this record.

② Semver level

.changeset/29820-push-capability-serve.md grades @objectstack/spec, @objectstack/core, @objectstack/service-push, @objectstack/cli and @objectstack/verify all minor. That matches what the diff publishes: a new vocabulary token and a new authorable top-level key (spec, widening), a new provider row and resolver branch (core), a new export resolvePushCapabilityArg (service-push), a new dependency plus a new boot refusal that only a stack declaring requires: ['push'] can meet (cli, verify). Nothing is narrowed: no token, key or accepted shape is removed, and a stack that does not declare push boots exactly as before. Clause-②: yes (widening) is on the PR body and in the changeset body; Metadata-protocol: added likewise, which is the right kind for a new top-level key. The Skills-docs: line is on both and is judged in ③ 6. Check Changeset is pending on the head.

③ Boundary flags

  1. Deviation: file surface wider than the order's wanted list (core, service-push, verify, unmanaged-install table, artifact-envelope list, the skill, docs-audit scope, cli vitest alias). Answered: each is forced by a gate or a pin named in ① (the dispositions tables are total over the stack shape; the skill enumeration is gate-reconciled; the alias gate names the import), and ruling C itself put the reader in the same PR. Accepted.
  2. Deviation: Tier H, skills/** touched. Answered: Governed Surface Queue Guard is green; a GOVERNED_APPROVERS approval is a merge condition the lane seat collects, not a review finding.
  3. Deviation: ESLint over changed files only; full pnpm lint left to CI. Answered: Lint · ESLint is pending on the head and is the gate of record; nothing in this review depends on it.
  4. Deviation: post-PR assignee, reviewer and subscription steps skipped. Procedural, outside this record.
  5. open_questions: []. Nothing to answer.
  6. Skills-docs: follow-up owed — skills/objectstack-platform/evals/config-plugins-ops.json. Right, by the rule in .claude/skills/pm-dispatch/SKILL.md 313: updated here requires the package's evals to exercise what the diff teaches, and none of the ten evals on this head asks for requires: ['push'] or the push key (eval 3 names automation, triggers, job, messaging only). The seat files the owed card and rewrites the line to its number before ACCEPT. The owed card should also carry ④ 2 below.
  7. Out-of-scope c: the skill's requires table omitted mailbox and package-registry before this diff. Confirmed pre-existing on main (the table on main has the same rows minus push); the dev noted it, did not fix it, which is right for a Tier H file at its token ceiling. Noted for the skills lane; not a finding on this PR.
  8. First executor's observation (6601490513): no pin that every open-edition token has a serve mount. Answered by this PR's reverse pin (① 7). Closed.
  9. Relay sender stays with service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential) #29920; ADR-0012 untouched (ADR-0012 amendment: the push channel's relay transport, and vendor as a token type rather than a plugin boundary (ruling cloud#3519) #29817). Right: the diff edits neither, and the boot refusal is the stated interim ("refuse it at boot naming the missing sender", ruling C's first option).
  10. Docs-drift bot's three pages (content/docs/permissions/capabilities.mdx, content/docs/getting-started/examples.mdx, content/docs/frontend/cli.mdx), read on the head: capabilities.mdx names PLATFORM_CAPABILITY_TOKENS as a closed vocabulary with three example tokens, never an enumeration, so it stays true; examples.mdx names COMPOSE_KEY_DISPOSITIONS by its disposition values and lists i18n, api, server, runtimeModule as examples of single-valued keys (email, sms, appName were already single on main and already unlisted), so it stays true; cli.mdx is the ObjectUI schema CLI page and says nothing about capabilities or stack keys. No finding on any of the three. The page that does go stale is a fourth one, ④ 3.

④ Design judgment

packages/spec/src/kernel/platform-capabilities.ts — holds. The token's comment, provider row and edition are true on this head (① 1); the token is requested, never always-on, which is the only honest placement for a capability whose default configuration is refused (an always-on push would fail every boot). Tried: declaring push with the package absent → the preflight prints the pnpm add @objectstack/service-push hint and serve's missingProviderMessage reads identically, both now followable since the package is on main. No finding.

packages/spec/src/kernel/unmanaged-install.ts — holds. deployment is the right class (① 3). No finding.

packages/spec/src/stack.zod.ts — holds against ruling C and against its first consumer (resolvePushCapabilityArg). Tried: push: {} → authoring accepts, boot refuses with the relay message; push: { transport: 'relay', endpoint: 'https://…' } → same; push: { transport: 'direct' } → refused at authoring and boot ("names at least one vendor block"); push: { transport: 'direct', apns: { …, key: 'PEM' } } → refused at authoring (['push','apns'], STACK_SCHEMA_INVALID 422) and again at boot for a compiled artifact, value never echoed; push: { fcm: { projectId, credentialRef: '{"type":…}' } } → refused at push.fcm.credentialRef; push: null → refused at authoring (.optional() does not admit null), read as {} and refused at boot. Two stacks with different push blocks → named on compose (single). One should-fix:

  1. should-fix — a push key with the capability off is a silent no-op. push is the first deployment key that is read only when its token is declared (email and sms are always-on, so their keys are always read). A stack that writes a complete direct push block and forgets requires: ['push'] boots cleanly, mounts no push service, and a notify naming push answers transport_not_configured, permanent. The describe says "read when requires names push" and the docs page says so too, so the contract is stated, but nothing at boot says the key went unread. A one-line warn in serve's capability loop (or in resolveServedCapabilities's caller) when stack.push is present and push is not among the served tokens would close it; not a defineStack cross-field refinement, because requires may live in a package body. Not a must-fix: ruling C's "does not drop a capability silently" is about tokens, which ① 7 pins.

packages/spec/src/system/push-config.zod.ts — holds. Every sentence of the docblock was checked against the reader, the plugin and serve (① 5, ① 9); the [PLANNED] marker survives only where the thing is still unserved. No finding.

skills/objectstack-platform/SKILL.md — holds against the spec it teaches on the head, with one should-fix. The enumeration and the table row are true (① 10). Against the card's scenario (an author who reads the skill and writes requires: ['push']): the skill's rule "Declaring is a demand" plus the push row teach that the token boots @objectstack/service-push, and that is the one row in the table whose boot is refused unless the stack also carries a direct push block with credentialRef names. The author meets the refusal with its remedy, so no false teaching ships, but the skill does not teach the key's shape or the relay refusal anywhere, and its ceiling has 3 tokens to spare.

  1. should-fix — carry the push key's teaching on the owed Skills-docs card. The follow-up card (③ 6) should add, beside the eval, one clause on the push row ("needs the top-level push key with transport: 'direct' and credentialRef names; the relay default is refused at boot until its sender ships"), paid for inside the ceiling. Not a must-fix: the skill states nothing false, and the docs page the PR adds teaches the whole shape.

Cited docs, outside hits, one should-fix:

  1. should-fix — content/docs/getting-started/quick-start.mdx now states a false completeness. Its "Where each piece of metadata goes" table is introduced as "the full authorable set", and the "Not in the table, and why" paragraph enumerates the keys that configure the stack rather than declare metadata (manifest, i18n, api, server, appName, email, sms, requires, tiers, plugins, devPlugins, devHint, devLogins, demoStaff). push is in neither on this head, so the page's "every top-level key is in the table or explained here" is false for the one key this PR adds. One clause in that paragraph, beside email and sms ("push carries no credentials either, only credentialRef names"), fixes it. The docs-drift bot did not flag this page (no touched symbol is named on it), and no gate reads the paragraph, which is why it is a should-fix and not a FAIL: the hits files hold, and the stale sentence is on an ungated hand-written page.

Minor, not graded: serve.ts's comment at 5144-5153 still says "email and sms the deployment's mail and SMS configuration, read by the readers their own packages export"; the code beneath it already handles push through the same rule. A comment-only nit for the next edit of that file.

No must-fix found. Ruling C's every clause is met on the tree: token, key and reader in one PR; a true open edition; relay (absent, null, {}, transport: 'relay') refused at boot naming the missing sender and the direct remedy; inline secrets refused twice; the reverse pin against a silent drop; a services.push page; the push-config docblock and markers true.

Implemented-by: claude/project-thread-rx3xpv
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

…t push in the quick-start

A push key on a stack whose requires does not name push is read by nothing;
objectstack serve now logs one boot warning naming the key and the fix. The
quick-start lists push beside email and sms among the stack configuration
keys, and the changeset points its Skills-docs follow-up at #29969.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzgjFwEiezTWdb7bk9i
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz · delta review on top of record 6612508737

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3612ed4d30422bb73fdbf5e858a1e42e566b06e6
Local-runs: none
Scope: packages/spec/src/kernel/platform-capabilities.ts, packages/spec/src/kernel/unmanaged-install.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/push-config.zod.ts, skills/objectstack-platform/SKILL.md + packages/cli/src/commands/serve.ts (the warning at 3028-3032 and formatUnrequestedPushKeyWarning at 6494-6508, read against the served-capability resolution at 3007-3009), packages/cli/src/commands/serve-push-key-advisory.test.ts, content/docs/getting-started/quick-start.mdx, content/docs/kernel/runtime-services/push-service.mdx, .changeset/29820-push-capability-serve.md, packages/core/src/stack-collections.ts (stackDeclaredCapabilities), packages/cli/src/utils/merge-boot-config.ts, packages/cli/vitest.config.ts (the tier walk). Everything the delta does not touch is carried by record 6612508737 on head 0f588fe.

① Derived judgments

This is a delta review: one commit (3612ed4, "fix(serve): warn when a stack sets push without requiring it, and list push in the quick-start") on top of the head record 6612508737 passed. The delta touches five files, 80 insertions and 5 deletions, none of them a hits file, so judgments 1 through 10 of the full record carry over unchanged; the diff reaches no spec schema, no provider table, no reader and no skill. The delta is judged against the three should-fixes the seat asked for (④ 1, ④ 3 and the ③ 6 card number of the full record).

  1. Should-fix ④ 1, the boot warning: done, and placed where the full record asked. formatUnrequestedPushKeyWarning(stack, requires) returns one plain line, or null, and serve.ts calls it at 3031, directly after const requires: string[] = [...served.tokens] (3008) and the tier resolution, and before the runtime import. requires there is the served list (declared tokens from the top level or the package bodies via stackDeclaredCapabilities, the auth implied email, the host defaults, the always-on slate), not the declared list alone, which is the right reading: a package-body requires: ['push'] (the reason the full record ruled out a defineStack cross-field refinement) keeps the warning silent, and push is on neither the always-on slate nor a host default, so the served list carries push exactly when a stack asked for it. config at that point is the merged boot config; mergeBootConfig spreads every authored top-level key except api, so a compiled artifact's push block is seen as the authored one is. One call per boot, no loop, so "one boot warning" is true.
  2. It fires only when it should. The predicate is push !== undefined && push !== null && !requires.includes('push'). A direct block, a relay block and {} all count as written; absence, push: undefined and a non-object config say nothing. push: null also says nothing, which is consistent with authoring (.optional() refuses null, so the shape only arises in a hand-edited artifact) and with reading null as "nothing written"; noted below, not graded. The unit test pins every one of these cases plus the requested-push silence, and a source-text pin holds the call site after the served.tokens line and console.warn within 200 characters of it, which is what keeps the "served, not declared" placement from drifting.
  3. Wording. "The stack definition sets a push key, but requires does not name 'push', so the push service is not started and the key is not read. Add 'push' to requires to turn mobile push on." Each clause is true on the head: without the token CAPABILITY_PROVIDERS.push is never consulted, PushServicePlugin is not mounted and resolvePushCapabilityArg never runs (full record ① 4, ① 6). The remedy is the one the docs page prescribes. The channel is console.warn(chalk.yellow(\ ⚠ ...`)), the same shape as serve's other boot advisories (2859, 3294, 3614, 3868); chalk` is imported at line 7.
  4. No secret is echoed. The function reads only the presence of stack.push and never serialises it; the test "never repeats the configuration" pins that a credentialRef name does not appear in the line. Nothing in the delta reads an environment variable.
  5. The test file is collected. packages/cli/src/commands/serve-push-key-advisory.test.ts imports ./serve.js relatively, as ten sibling tests in the same directory already do; packages/cli/vitest.config.ts derives its two exact-path include lists from a walk of the package (unitTestFiles / integrationTestFiles in vitest-tiers.ts), so a new file needs no registration. Plan Test Core shards is in progress on the head.
  6. Should-fix ④ 3, quick-start: done. push is added to the "Not in the table, and why" enumeration between sms and requires, and the parenthetical beside email / sms gains "push names each vendor secret by credentialRef and is read only when requires names push". Both halves are true on the head: PushServiceConfigSchema's apns and fcm blocks each carry a credentialRef and refuse inline secrets (full record ① 2), and the key is read only through the push capability branch (① 4). The page's completeness claim is true again.
  7. Should-fix ③ 6, changeset card number: done. The Skills-docs: line on the changeset and on the PR body both now read "follow-up owed — skills(objectstack-platform): an eval case for the push capability and push stack key (follow-up of #29820) #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: ...)". skills(objectstack-platform): an eval case for the push capability and push stack key (follow-up of #29820) #29969 is open, domain:skills, titled "skills(objectstack-platform): an eval case for the push capability and push stack key (follow-up of spec: give push a served door (a platform capability, a config.push stack key and a services.push docs page), so a deployment can turn it on without host code #29820)", and its ask is the eval the full record named (turn push on from a stack file with requires: ['push'] and a direct credentialRef block, no inline secret, and the relay refusal pinned). The two lines agree with each other.
  8. Docs and changeset prose stay true. push-service.mdx gains "A push key without push in requires is not read: objectstack serve logs one boot warning naming the key and the fix. With the token, objectstack serve mounts PushServicePlugin..." and the changeset body gains the matching sentence; both describe exactly what ① 1 through ① 3 found. The claim is scoped to objectstack serve, which is correct: @objectstack/verify's bootStack does not warn (the seat asked for serve only, and the page's sentence about bootStack mounting the service the same way is still true).
  9. Nothing in the delta reopens a judgment of the full record. No spec token, key, disposition, reader, provider row, schema shape, skill line or ADR is touched; the warning is additive advice ahead of behaviour that does not change, and the boot path for a stack that declares push is unchanged. ① 1 through ① 10 of 6612508737 stand as written.
  10. Check-runs on the head 3612ed4. Completed success: Check Documentation Links, Governed Surface Queue Guard, PR-body guards, filter. Skipped (opt-in or matrix placeholders): 18.x upgrade harness, Dogfood Regression Gate matrix shard, Dogfood Verify CLI, Packed-tarball smoke, Schedule gate, Temporal Conformance, UI Published Artifacts. Still pending at the time of this record: Build Core, Build Docs, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Lint · CI release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, Plan Test Core shards, Spec property liveness, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, Validate Package Dependencies. The verdict holds on the committed tree; the pending gates are the lane seat's merge condition, and a red among them reopens this record.

② Semver level

Unchanged from the full record: the changeset still grades @objectstack/spec, @objectstack/core, @objectstack/service-push, @objectstack/cli and @objectstack/verify minor, with Clause-②: yes (widening) and Metadata-protocol: added on the changeset and the PR body. The delta adds a boot advisory to the cli (diagnosis ahead of unchanged behaviour), a test, and prose; it removes nothing and narrows no accepted shape, so minor on the cli still fits and no other package's grade moves. Check Changeset is pending on the head.

③ Boundary flags

  1. Flags 1 through 5 and 7 through 10 of the full record carry over unchanged; the delta touches none of the surfaces they name (no skills/** file, no new package dependency, no ADR).
  2. ③ 6 of the full record (the owed Skills-docs card) is now answered by skills(objectstack-platform): an eval case for the push capability and push stack key (follow-up of #29820) #29969, named on both the changeset and the PR body. The card carries the eval the full record asked for and the relay pin; whether it also carries the full record's ④ 2 clause for the skill's push row is the seat's matter on that card, not a finding on this PR.
  3. The delta's test file lives in packages/cli/src/commands/ beside its siblings rather than in packages/cli/test/; both locations are walked by the tier derivation, so this is placement, not a gap.
  4. @objectstack/verify's bootStack does not log the warning. The seat asked for os serve only, the docs scope the claim to objectstack serve, and verify's consumer is a test suite that asserts on the mounted services rather than reading stderr. Not asked, not owed; noted for the verify lane if it ever wants parity.

④ Design judgment

The five hits files (platform-capabilities.ts, unmanaged-install.ts, stack.zod.ts, push-config.zod.ts, SKILL.md): untouched by the delta; the full record's ④ for each carries over. Of its three should-fixes, ④ 1 (the silent push key) and ④ 3 (the quick-start completeness) are closed by this commit, and ④ 2 is on the owed card #29969.

packages/cli/src/commands/serve.ts, the delta — holds against the code it cites. The warning is derived from the served requires (not the declared set), which is the only reading that respects package-body declarations and the always-on slate at once; it is advice, never a refusal, so a stack that boots today boots the same way with one extra stderr line. Tried on the head by reading: direct block, no token → one yellow line, boot continues, no push service (as before); push: {}, no token → the same line (the key was written, so it is right to say it went unread); direct block with requires: ['push'] in a package body only → stackDeclaredCapabilities lifts it, served tokens carry push, silent; --preset minimal with requires: ['push'] → declared tokens survive the preset, silent; push: null in a hand-edited artifact, no token → silent (consistent with authoring's refusal of null and with "nothing written"). No finding.

Minor, not graded: the comment at serve.ts 5144-5153 (email and sms readers) still omits push, as the full record already noted; the delta does not reach it.

Docs and changeset — every added sentence was checked against ① 1 through ① 4 and is true; no sentence claims more than serve does.

No must-fix found. The three should-fixes are each done as asked; the warning is correctly placed, correctly scoped, echoes nothing, and nothing in the delta reopens a judgment of record 6612508737.

Implemented-by: claude/project-thread-rx3xpv
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

席位核对 — ACCEPT · domain:spec#2 · head 3612ed4d30422bb73fdbf5e858a1e42e566b06e6

  • Shape: push becomes turn-on-able from a stack file, in one PR per ruling C (6601454188): the push platform capability (@objectstack/service-push, edition open, now a dependency of the cli and verify), the top-level push stack key carrying PushServiceConfigSchema (credentials by credentialRef only), and the os serve reader composing PushServicePlugin. A relay config (the empty default included) is refused at boot, naming the missing relay sender (service-push: send through the hosted relay transport (push-relay.zod.ts wire, cloud binding credential) #29920) and pointing at transport: 'direct'. An inline secret is refused at authoring and at boot. A push key without push in requires gets one boot warning. The PR also adds the services.push runtime-services page, makes the push-config docblock true, and adds push to the objectstack-platform skill's key list. 32 files.
  • Ruling: C, seat ruling 6601454188: nothing lands ahead of its reader. Blocked-by feat(notifications): the push channel — service-push (APNs + FCM) and its channel shell in service-messaging #29794 is cleared (merged 2026-11-03T16:30Z).
  • Contract review: record 6612508737 (CONTRACT_REVIEW_TIER, head 0f588fe6, PASS, no must-fix) and delta record 6613694045 (head 3612ed4d, PASS). Should-fixes 1 and 3 and the changeset card number were folded into 3612ed4d. Should-fix 2 (the skill row clause) is carried to skills(objectstack-platform): an eval case for the push capability and push stack key (follow-up of #29820) #29969 (6612527491).
  • Declarations: Clause-②: yes (widening), Metadata-protocol: added, five packages minor; Skills-docs: follow-up owed — #29969.
  • Gates: every check-run on the head is complete: 59 success, 7 skipped, none red.
  • Landing: ⛔ GOVERNED, Tier H. skills/objectstack-platform/SKILL.md is on the register. The seat does not flip it ready or arm auto-merge. It waits for the maintainer's merge or an authorized APPROVED review (GOVERNED_APPROVERS); the seat then lands it.

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

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

2 participants