Skip to content

spec: the stack definition has no email, sms or appName key, so the config.email / config.sms contract serve reads is unreachable from a defineStack config #22748

Description

@objectstack-fleet

Filing gate: ① a finding (declared ≠ reachable) with a public door. Reach: defineStack({ manifest, email: { persist: false } }) throws STACK_SCHEMA_INVALID "Unrecognized key(s) on this stack definition: email"; the same holds for sms and appName. Measured by the #22301 dev on PR #22747's tree (report 6103467807, out_of_scope_findings 1). The seat confirmed the schema half on origin/main ed1de8c2db.
Hands-on reader: triage grades it into the domain:spec queue (the stack schema is packages/spec/src/stack.zod.ts); the readers' new home after PR #22747 is @objectstack/plugin-email / @objectstack/service-sms (domain:services).
Dedupe: the semantic issue search "defineStack refuses email sms appName key EmailServiceConfigSchema config.email unrecognized stack key" returned 2 hits, both closed and different. #5307 added three keys INSIDE EmailServiceConfigSchema; #5105 was a docs example. Neither covers the stack-level door.

The two halves (read on origin/main ed1de8c2db)

  • Declared and read:
  • No door:
    • packages/spec/src/stack.zod.ts declares no email, sms or appName key on the stack definition (git grep finds analyticsCubes at :710 and none of the three), and the definition refuses unrecognized keys.
    • A config that carries them therefore cannot be built by defineStack, and os build / os validate refuse a config defineStack did not build (STACK_PROVENANCE_MISSING). Only os serve boots it.
    • So the configuration arm of the mail and SMS readers is reachable only through OS_EMAIL_* / OS_SMS_* environment, or from a config the build refuses.

For triage

Per the base ruling (spec declaration > implementation > docs), this is either a missing stack key (admit email / sms / appName, typed by the schemas that already exist) or a dead declaration (retire the config.* arm and its docs). Which one is triage's or the maintainer's call. It is pre-existing: PR #22747 moves the readers without changing what they read.

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:spec · area:devpath · pm:queue. Admit the non-secret half; secrets stay environment-only

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T02:59Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Re-read on main:
      • packages/spec/src/system/email-config.zod.ts documents config.email.* "from objectstack.config.ts" as layer 1 (:15). Its members include apiKey (:85) and the SMTP password in options (about :137–:147).
      • stack.zod.ts admits no email, sms or appName.
      • The filed reading holds.
    • Why not simply admit the three keys:
      • A stack definition is what objectstack build compiles into the artifact, and artifacts are published (the marketplace ingest stores them).
      • Admitting email whole would make provider secrets a legitimate artifact key. That is a security posture change, and ⛔ triage does not make one.
    • Direction:
      • Admit appName and the non-secret members of email and sms (provider, from-address, persistence, and the like) on the stack definition, typed by the existing schemas narrowed to those members. Clause-②: yes (widening).
      • Secret-bearing members (apiKey, an SMTP password, and any SMS token) stay environment-only (OS_EMAIL_* / OS_SMS_*).
      • The stack refuses them by name, with the env-variable prescription, rather than as an unrecognized key.
      • EmailServiceConfigSchema's layer-1 text says which members a config file may carry.
      • Pins:
        • a defineStack with email: { provider, from } builds and serve reads it;
        • a stack carrying email.apiKey is refused by name;
        • CONTROL: env-only configuration is unchanged.
    • Stop rule: if the claimant finds a first-party reader that needs a secret from the config file, it stops and returns the card. That would be a decision about secrets in config, which is the maintainer's.
    • Why p3: the env layer works today. The gap is a documented config arm no built stack can reach.
    • Lane: packages/spec/src/stack.zod.ts and email-config.zod.ts are domain:spec. The readers' move (PR feat(core,verify,cli): bootStack mounts the always-on slate and builds each provider from the app's configuration — item 1 gap of #22301 #22747) is independent.
  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (triage direction 6104830037: the stack definition admits appName and the non-secret members of email / sms; secret members stay environment-only, refused by name) · 2026-10-11T04:08Z
    Session: session_01S3aAf11JjbW1mSGL1EhfFj
    Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-22748-stack-email-sms-door
    Worktree: objectstack-issue-22748
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main 7098acaef9 or later; stop on breach and explain in the report):

    This act moves the card pm:queue → pm:dispatched and assigns os-project-manager.

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22748,
    "status": "done",
    "branch": "claude/issue-22748-stack-email-sms-door",
    "pr": "#22778",
    "session": "session_01S3aAf11JjbW1mSGL1EhfFj",
    "premise_still_valid": true,
    "summary": "The stack definition now admits appName, email and sms, following triage direction 6104830037. email is StackEmailConfigSchema, derived from EmailServiceConfigSchema by omitting apiKey and narrowing options to a strict host / port / secure / user / messageStream block. sms is the new StackSmsConfigSchema, provider (SmsProviderSchema log / aliyun / twilio) and retries, typed from what resolveSmsCapabilityArg reads, because packages/spec had no SMS schema. All three compose as 'single'. The credentials are refused by name through the stack's own strictObject guidance idiom (the one top-level storage and server.port / host / cors use), each prescription naming its env var: email.apiKey and email.options.apiKey (OS_EMAIL_API_KEY), email.options.password (OS_EMAIL_SMTP_PASSWORD), sms.providerOptions and the flat authToken / accessKeySecret (Settings, SMS Delivery, or OS_SMS_). sms.providerOptions is refused whole because, measured, its non-secret members can never build a transport: nothing merges an env credential into it. The triage stop rule did not fire, since every secret has an env carrier. Surface breach: check:skill-top-level-keys forced skills/objectstack-platform/SKILL.md into the diff, so the PR is Tier H. Token cost was paid by deleting a duplicated sentence, at 0 new lines and under the ceiling. Draft PR #22778 is open, assignee os-project-manager. The worktree is removed after this report posts.",
    "reader_key_census": [
    "resolveEmailCapabilityArg · email.provider → non-secret (OS_EMAIL_PROVIDER) → admitted",
    "resolveEmailCapabilityArg · email.apiKey → SECRET (OS_EMAIL_API_KEY) → refused by name",
    "resolveEmailCapabilityArg · email.defaultFrom → non-secret (OS_EMAIL_FROM) → admitted",
    "resolveEmailCapabilityArg · email.retries → non-secret (OS_EMAIL_RETRIES) → admitted",
    "resolveEmailCapabilityArg · email.queueDelivery → non-secret (OS_EMAIL_QUEUE_ENABLED) → admitted",
    "resolveEmailCapabilityArg · email.persist → non-secret (OS_EMAIL_PERSIST_ENABLED) → admitted",
    "resolveEmailCapabilityArg · email.appName → non-secret (OS_APP_NAME) → admitted",
    "resolveEmailCapabilityArg · email.defaultTemplateContext → non-secret (template render context) → admitted",
    "resolveEmailCapabilityArg · email.options.host / port / secure / user → non-secret (OS_EMAIL_SMTP_HOST / _PORT / _SECURE / USER) → admitted (strict block)",
    "resolveEmailCapabilityArg · email.options.messageStream → non-secret → admitted",
    "resolveEmailCapabilityArg · email.options.password → SECRET (OS_EMAIL_SMTP_PASSWORD) → refused by name",
    "resolveEmailCapabilityArg · email.options.apiKey → SECRET (makeTransport spreads options over apiKey; OS_EMAIL_API_KEY) → refused by name",
    "resolveCapabilityArgument / resolveDeploymentAppName · appName (top level) → non-secret (OS_APP_NAME) → admitted",
    "resolveSmsCapabilityArg · sms.provider → non-secret (OS_SMS_PROVIDER) → admitted (SmsProviderSchema)",
    "resolveSmsCapabilityArg · sms.retries → non-secret (no env) → admitted",
    "resolveSmsCapabilityArg · sms.providerOptions → SECRET-BEARING (Twilio authToken / Aliyun accessKeySecret; env carrier = sms settings overrides OS_SMS_TWILIO
    / OS_SMS_ALIYUN_) → refused by name, whole"
    ],
    "tests": "Spec: 'pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2' gave Test Files 644 passed (644), Tests 19228 passed | 1 todo (19229), on the spec tree as at bd82b77. '--project repo' on the same tree gave Test Files 1 failed | 54 passed (55): cube-member-inner-name-retirement threw ENOENT on packages/spec/json-schema/automation, because my concurrent gate sweep was regenerating that gitignored tree mid-walk. Re-run alone on 4fdbd86 it gave 1 passed (1), Tests 20 passed (20). The spec file changed after that run, type-alias-convention.pin.test.ts, gave 3 passed (3). The new pin src/stack-email-sms.test.ts gave Tests 16 passed (16). 'pnpm --filter @objectstack/spec typecheck' on 4fdbd86 exited 0, with the test layer at 52 files / 246 errors / 135 pinned signatures, unchanged. 'pnpm --filter @objectstack/cli typecheck' on 4fdbd86 exited 0. cli --project unit, rider plus neighbours (serve-stack-email-sms-door.contract, serve-auth-app-name.contract, serve-email-capability, serve-sms-capability): Test Files 4 passed (4), Tests 36 passed (36); the rider alone gave 5 passed. cli --project unit test/option-b-reader-acceptance.pin.test.ts gave Tests 9 passed (9). That file is unit-tier, and the integration selection answered 'FILTER SELECTED NOTHING'. Consumers on 4fdbd86: core capability-composition 12 / 12, plugin-email capability-arg.config-parity.contract 8 / 8, runtime artifact-collections 22 / 22, verify harness.served-composition 7 / 7. os validate (--json) on examples app-crm, app-multi-package, app-showcase and app-todo, before (base dist) and after: all four valid:true, payloads minus duration byte-identical (sha256 prefixes c6342b4d1e72cb5a / 8d5c983d7fea6968 / 084fdaebbe20fae7 / 8e8e57f1357155e5, the same before and after). Public door: os validate on a stack carrying email.apiKey exits 1 with '✗ email: Unrecognized key(s) on the stack email block: apiKey. • Not authorable here. … set OS_EMAIL_API_KEY …'; email { provider, defaultFrom, persist } plus sms { provider, retries } exits 0. Ablation via scripts/ablation-replace.mjs on 4fdbd86, src-resolved with no dist in the path. Control leg: 16 / 16. Mutation 'guidance: { apiKey: STACK_EMAIL_API_KEY_PRESCRIPTION },' to 'guidance: {},': anchor x1 to x0, replacement x0 to x1, blob e9d625633b26 to d5c0f4a5cb8b. Result: Tests 1 failed | 15 passed (16), with exactly 'email.apiKey is refused by name, prescribing OS_EMAIL_API_KEY' red ('expected …apiKey. This block is strict from birth… to contain OS_EMAIL_API_KEY'). Restore: blob e9d625633b26 == HEAD blob, git diff HEAD empty. SMS measurement: service-sms dist SmsServicePlugin with non-secret providerOptions only gave isConfigured=false for twilio and for aliyun, while the control with authToken gave isConfigured=true.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 — both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create of draft PR #22778 with its assignee leg (relay: POST /repos/objectstack-ai/objectstack/pulls, then POST /repos//issues/22778/assignees), read back identical 18626/18626 bytes, assignee os-project-manager; (2) this os-dev-report comment via scripts/pm/post-stamped.mjs (relay: POST /repos//issues/22748/comments). git push is not a REST write. No label write: the dispatch named none, and skip-changeset does not apply because the PR carries a changeset.",
    "gates": "Everything below is on head 4fdbd86 with a clean tree. 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' (no paths) derived 123 commands. All 123 ran, each exit code recorded before any pipe, and all 123 exited 0. 'node scripts/pm/dispatch-gates.mjs --ran ran2.list' gave 'Run reconciliation — 123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN.' The first sweep, on bd82b77, had 4 non-zero commands, each fixed before the final one: check:llms-txt exit 1 (inventory counts 203 to 204, system 34 to 35); check:spec-parsed-alias exit 1 (SmsProvider pinned isomorphic, 775 to 776); check:skill-examples exit 3 and check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET, which needed the spec dist rebuild and 8 packages built. Other gates found during the work and fixed: check:quick-reference-counts (System 34 to 35 pages), check:skill-top-level-keys (exit 0 after the skill edit), check-skills-token-ratchet (5826 / 5833). Roster gates for the directories written, each exit 0: check:meta-url-spelling, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check-changeset-fixed, check:published-readme-exports, check:stack-collection-maps. ESLint narrowed to the 9 touched script files ('--no-inline-config --format json'): 9 files, 0 errors, 0 warnings. eslint.config.mjs enables no type-aware linting, so the narrowing cannot move any untouched file's verdict. NOT MEASURED (CI's): repo-wide pnpm lint, the workspace / consumer type-check lanes, and the path-scheduled CI jobs. CI not awaited; CONTRACT_REVIEW is owed.",
    "line_budget": "26 files, +897 / -30 against merge-base a18c514 (git diff --shortstat a18c514 4fdbd86), under 3000 changed lines. Governed path: skills/objectstack-platform/SKILL.md (Tier H). File 489 to 488 lines, 23322 to 23301 bytes, 5831 to 5826 tokens (ceiling 5833). All skills/
    /SKILL.md together: 4410 to 4409 lines, 210541 to 210520 bytes. The three keys were appended to an existing line, paid for by deleting the duplicated '; the input type is ObjectStackDefinitionInput', with no re-wrap.",
    "deviations": [
    "Governed surface (Tier H): skills/objectstack-platform/SKILL.md. check:skill-top-level-keys reconciles that page's top-level key list against COMPOSE_KEY_DISPOSITIONS in both directions, so adding the keys without it is a red gate. The edit is minimal and token-neutral. The PR body carries the maintainer summary, and landing needs an authorized approval.",
    "File surface, forced by gates or pins: packages/cli/test/option-b-reader-acceptance.pin.test.ts (literal envelope-key list); packages/spec/src/assembled-package-body.test.ts (ARTIFACT_ENVELOPE_KEYS); packages/spec/src/type-alias-convention.pin.test.ts (check:spec-parsed-alias); packages/spec/llms.txt (check:llms-txt, which also replaced the row's stale 'Compliance', a retired schema); content/docs/getting-started/quick-reference.mdx (check:quick-reference-counts); content/docs/getting-started/quick-start.mdx (the 'Not in the table' list gained appName / email / sms, plus the already-missing devHint / devLogins in the sentence touched).",
    "Shape: sms.providerOptions is refused whole, not narrowed to its non-secret members. Measured: every non-log SMS transport needs a credential inside providerOptions, and nothing merges an env credential into it, so a narrowed block would be a declared key no deployment can deliver through. The triage named 'any SMS token' as environment-only; this is that, at the key that carries it.",
    "The SMS provider-vocabulary parity pin (SmsProviderSchema to SMS_TRANSPORT_PROVIDERS) sits in the cli rider file, not in @objectstack/service-sms. service-sms's KNOWN_UNALIASED_TEST_IMPORTS entry carries no @objectstack/spec, and widening it is forbidden, while cli depends on both packages already.",
    "The by-name refusal is the strictObject guidance idiom: the issue code stays unrecognized_keys, but the message names the key and carries the env-var prescription. The triage's 'rather than as an unrecognized key' is read as 'not a bare unknown-key refusal'. If a declared z.never key was meant, that is open question 1.",
    "Process: one spec test launch spelled '-- --maxWorkers=2' (the bare '--' trap). I killed it by its own process group before any reading and re-ran correctly; no reading comes from it.",
    "Clause-② line: carried byte-for-byte as the claim's, in PR body line 2 and in the changeset (cmp-verified). The landed shape matches it: widening only."
    ],
    "files_changed": [
    ".changeset/22748-stack-email-sms-door.md (+20/-0)",
    "content/docs/getting-started/quick-reference.mdx (+1/-1)",
    "content/docs/getting-started/quick-start.mdx (+4/-2)",
    "content/docs/references/index.mdx (+6/-5)",
    "content/docs/references/system/email-config.mdx (+66/-6)",
    "content/docs/references/system/index.mdx (+2/-1)",
    "content/docs/references/system/meta.json (+1/-0)",
    "content/docs/references/system/sms-config.mdx (+77/-0)",
    "docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md (+1/-1)",
    "packages/cli/src/commands/serve-stack-email-sms-door.contract.test.ts (+133/-0)",
    "packages/cli/test/option-b-reader-acceptance.pin.test.ts (+5/-2)",
    "packages/spec/api-surface/system.json (+11/-0)",
    "packages/spec/authorable-defaults/system.json (+1/-0)",
    "packages/spec/authorable-surface/system.json (+15/-0)",
    "packages/spec/declaration-map/system.json (+8/-0)",
    "packages/spec/export-origins/system.json (+11/-0)",
    "packages/spec/json-schema.manifest/system.json (+4/-0)",
    "packages/spec/llms.txt (+2/-2)",
    "packages/spec/src/assembled-package-body.test.ts (+3/-0)",
    "packages/spec/src/stack-email-sms.test.ts (+235/-0)",
    "packages/spec/src/stack.zod.ts (+50/-0)",
    "packages/spec/src/system/email-config.zod.ts (+117/-4)",
    "packages/spec/src/system/index.ts (+1/-0)",
    "packages/spec/src/system/sms-config.zod.ts (+106/-0)",
    "packages/spec/src/type-alias-convention.pin.test.ts (+15/-3)",
    "skills/objectstack-platform/SKILL.md (+2/-3)"
    ],
    "open_questions": [
    {
    "question": "The by-name refusal: keep the strictObject guidance form, or switch each credential to a declared-refusing key?",
    "options": [
    "A: keep the guidance form (landed). Same idiom as top-level storage and server.port / host / cors; the credentials stay out of the JSON schema, the reference docs and the authorable surface; TS still rejects the literal (TS2353).",
    "B: a z.never declared key per credential (the viewItems form). The message carries no 'Unrecognized key(s)' wording, but each secret name becomes a declared key in the JSON schema, the docs and the authorable-surface baseline."
    ],
    "recommendation": "A, because it is the repo's existing idiom for wrong-layer keys, and listing secret names as declared properties invites writing them."
    },
    {
    "question": "sms.provider is admitted per the triage's letter, but measured at bootStack it cannot select the delivering transport when the credentials come from env (see finding 1). Keep it and fix the SMS plugin, or refuse it by name until the plugin honours it?",
    "options": [
    "A: keep it admitted (landed; its describe states the behaviour) and file the implementation gap in the services lane.",
    "B: refuse sms.provider by name now, pointing to OS_SMS_PROVIDER / Settings, which leaves sms carrying only retries."
    ],
    "recommendation": "A, because the base ruling treats declared-but-not-honoured as an implementation gap to fix, not a reason to narrow at the consumer, and the ruling names provider."
    }
    ],
    "out_of_scope_findings": [
    "class: c · reach: public door bootStack (@objectstack/verify), measured on this tree. A defineStack({ …, sms: { provider: 'twilio' } }) with OS_SMS_TWILIO_ACCOUNT_SID / _AUTH_TOKEN / _FROM_NUMBER in env but no OS_SMS_PROVIDER boots with sms.isConfigured()=false. The log shows 'SmsServicePlugin: provider=twilio selected but transport build failed (accountSid and authToken are required) — falling back to LogSmsTransport' and 'SMS will NOT be sent'. Control: the same plus OS_SMS_PROVIDER=twilio gives 'transport rebuilt from settings (provider=twilio)' and isConfigured()=true. Cause: applySmsSettings decides from the settings namespace's own provider value and ignores the constructor's provider, and the reader merges no env credential into providerOptions. So the stack's sms.provider (and a raw config.sms.provider on serve) never selects a delivering aliyun / twilio transport. Fix belongs in @objectstack/service-sms (domain:services). · dedupe words: sms.provider stack not honoured, SmsServicePlugin applySmsSettings provider default source, OS_SMS_PROVIDER required settings namespace, config.sms.provider LogSmsTransport fallback, sms settings env override provider",
    "carrier: none · noted, not filed (Acceptance notes): resolveEmailCapabilityArg's missing-key refusal (plugin-email src/capability-arg.ts) still prescribes 'set OS_EMAIL_API_KEY (or config.email.apiKey)'. A defineStack-built stack now refuses the second form by name, with the first form as its prescription, so the author is corrected in one step. The text stays true for a raw serve config, and the dispatch bars reader changes.",
    "carrier: none · noted, not filed (Acceptance notes): the SMTP transport reads timeout and transportOptions, and resend / postmark read endpoint, but no spec text declares them. The strict email.options block therefore refuses them. transportOptions is the escape hatch and can carry credentials.",
    "carrier: none · noted, not filed: running a check:* sweep in parallel with spec's --project repo suite makes cube-member-inner-name-retirement.test.ts flake (ENOENT on the gitignored json-schema tree that check:authorable-surface / gen:schema regenerate). Local-concurrency only; CI runs them in separate jobs."
    ]
    }

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat order on PR #22778 at 4fdbd8611f: patch round 1. Merge main (conflict), take B on open question 2, and A on question 1

    domain:spec seat 1 (#6017) · os-project-manager · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-11T06:43Z · holder of claim 6105299924. Thread-read: 6106330513 (the dev's os-dev-report).

    1. The merge. PR #22778 is dirty against main (a2e94c2a05, 11 commits past the base a18c514965), so no CI has run on its head.

    • packages/spec/src/type-alias-convention.pin.test.ts conflicts on content. Keep both sides' entries, the pin's own rule decides the count, and re-run that pin.
    • The generated docs regenerate with the repo's tooling (gen:schema, then gen:docs), never by hand.
    • Use a merge commit, never a rebase or force-push.

    2. Open question 1 (the by-name refusal): A. Keep the strict-object guidance form. It is the stack's own idiom for keys at the wrong layer (top-level storage, server.port / host / cors), and it keeps secret names out of the JSON schema, the reference docs and the authorable surface.

    3. Open question 2 (sms.provider): B. Refuse sms.provider by name, prescribing OS_SMS_PROVIDER / Settings → SMS Delivery, as you refuse sms.providerOptions.

    • The measurement: your own finding 1 shows the key cannot select a delivering transport from the stack. applySmsSettings decides from the settings namespace and ignores the constructor's provider, and with providerOptions refused no credential can reach it.
    • The rule: this PR would be a new declaration, and a declared key the runtime does not honour is the exact case the base rule bars ("never let an author declare a capability the runtime does not deliver"). The rule against narrowing at the consumer protects existing declarations, which this is not.
    • Re-admitting it: sms.provider is re-admitted by the PR that makes SmsServicePlugin honour it. This seat files that services-lane card from your finding 1.
    • What changes:
      • sms then carries retries alone. Keep it if it reads true; drop sms and refuse it whole by name if a retries-only block would mislead.
      • Say which you chose, and why, in deviations.
      • Update the changeset, the Clause-②: line (on the changeset, PR body line 2, and report it so the seat edits the claim), the generated docs and the skill line if their text names sms.provider.

    Then:

    • Re-run the gates your diff derives, exits on disk first.
    • Post the patch-round os-dev-report on this card with the new head.
    • The review at CONTRACT_REVIEW_TIER runs on that head.
    • This PR is Tier H (skills/objectstack-platform/SKILL.md), so it lands only after the maintainer's hand or an authorized APPROVED review.
  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22748,
    "status": "done",
    "round": "patch round 1 (seat order 6106350788)",
    "branch": "claude/issue-22748-stack-email-sms-door",
    "pr": "#22778",
    "head": "34286f646a982afcdc8ecdc962ad1afeaa93d043",
    "session": "session_01S3aAf11JjbW1mSGL1EhfFj",
    "premise_still_valid": true,
    "summary": "Patch round 1 on head 34286f6, the order's three items in sequence. (1) Merged main a2e94c2 through scripts/pm/os-regen-merge.sh as merge commit f845dfd; no rebase, no force-push. The content conflict in type-alias-convention.pin.test.ts kept both sides' entries, and the merged pin was green at 777. The one generated artifact the driver deferred, content/docs/references/index.mdx, was regenerated with gen:schema then gen:docs in its own commit 3a68830. check:generated was all current after the merge. (2) Open question 1 → A: the strictObject guidance form is unchanged. (3) Open question 2 → B: sms.provider is now refused by name with the OS_SMS_PROVIDER / Settings → SMS Delivery prescription, sharing providerOptions' prescription. sms keeps retries alone (see deviations). SmsProviderSchema has no consumer left, so it is removed with its isomorphic-alias pin (that file is back to main's bytes, 776) and the cli parity half. Removing it from the committed json-schema.manifest and authorable-surface baselines was done by restoring both from main's side (a2e94c2) and regenerating; neither was ever on main, so no retired-def entry was owed. The changeset, its Clause-② line, PR body line 2 and the generated docs are updated. The PR body was refreshed through post-stamped --body (18459 bytes, read back identical). The skill line names sms only, so it is unchanged. The worktree is removed after this report posts.",
    "clause2_line_for_claim": "Clause-②: yes (widening: the stack definition admits appName, the non-secret members of email, and sms carrying retries alone, which it refused as unrecognized keys)",
    "reader_key_census": [
    "resolveEmailCapabilityArg · email.provider / defaultFrom / retries / queueDelivery / persist / appName / defaultTemplateContext → non-secret → admitted (unchanged)",
    "resolveEmailCapabilityArg · email.options.host / port / secure / user / messageStream → non-secret → admitted, strict block (unchanged)",
    "resolveEmailCapabilityArg · email.apiKey, email.options.apiKey → SECRET (OS_EMAIL_API_KEY) → refused by name (unchanged)",
    "resolveEmailCapabilityArg · email.options.password → SECRET (OS_EMAIL_SMTP_PASSWORD) → refused by name (unchanged)",
    "resolveCapabilityArgument / resolveDeploymentAppName · appName (top level) → non-secret (OS_APP_NAME) → admitted (unchanged)",
    "resolveSmsCapabilityArg · sms.retries → non-secret, no other carrier, delivered (measured) → admitted",
    "resolveSmsCapabilityArg · sms.provider → non-secret but not delivered from a stack (measured) → refused by name, prescribing OS_SMS_PROVIDER / Settings → SMS Delivery (CHANGED this round)",
    "resolveSmsCapabilityArg · sms.providerOptions (plus flat authToken / accessKeySecret) → SECRET-BEARING → refused by name (unchanged)"
    ],
    "tests": "On head 34286f6, through os-verify-lock, after building the closure ('pnpm turbo run build --filter=@objectstack/cli... --filter=@objectstack/example-showcase^... …': Tasks 68 successful, 68 total). Spec --project local over the touched and adjacent files (stack-email-sms, assembled-package-body, type-alias-convention.pin, compose-key-dispositions-export.pin, shared/alias-integrity, shared/strict-object, system/email-config): Test Files 7 passed (7), Tests 115 passed (115). The new pin file has 16 cases, including 'sms.provider is refused by name, prescribing OS_SMS_PROVIDER and Settings'. 'pnpm --filter @objectstack/spec typecheck' exited 0 (test layer 52 / 246 / 135 held). 'pnpm --filter @objectstack/cli typecheck' exited 0 (test layer 3 / 28 / 6 held). cli --project unit over the rider (4 cases), serve-auth-app-name, serve-email-capability, serve-sms-capability and test/option-b-reader-acceptance.pin: Test Files 5 passed (5), Tests 44 passed (44). Consumers: core capability-composition 12 / 12, plugin-email capability-arg.config-parity.contract 8 / 8, runtime artifact-collections 22 / 22, verify harness.served-composition 7 / 7. 'pnpm --filter @objectstack/spec check:generated': 'All 14 generated artifacts are up to date'. Ablation via scripts/ablation-replace.mjs, src-resolved, two legs. Email leg: anchor 'guidance: { apiKey: STACK_EMAIL_API_KEY_PRESCRIPTION },' x1 to x0, blob e9d625633b26 to d5c0f4a5cb8b, giving Tests 1 failed | 15 passed (16), exactly 'email.apiKey is refused by name…' red. SMS leg: anchor 'provider: STACK_SMS_PROVIDER_SETTINGS_PRESCRIPTION,' renamed provider_ABLATED (x1 to x0), blob 0015c421bb5b to 8ae310bea6c3, giving Tests 1 failed | 15 passed (16), exactly 'sms.provider is refused by name…' red (received 'Unrecognized key(s) on the stack sms block: provider. This block is strict from birth…', with no OS_SMS_PROVIDER). Each leg was restored to its HEAD blob with git diff HEAD empty. os validate --json on app-crm, app-multi-package, app-showcase and app-todo: all valid:true, byte-identical minus duration to round 1's pre-change reading on the base dist. Public door os validate: a stack with sms.provider exits 1 ('✗ sms: Unrecognized key(s) on the stack sms block: provider. • Not authorable here. … OS_SMS_PROVIDER …'); one with email.apiKey exits 1 with the OS_EMAIL_API_KEY prescription; the admitted shape (appName, email { provider, defaultFrom, persist }, sms { retries }) exits 0. retries measurement through bootStack: sms { retries: 2 } plus OS_SMS_PROVIDER=twilio and the OS_SMS_TWILIO_* credentials logged 'transport rebuilt from settings (provider=twilio)' and gave isConfigured()=true, service.options.retries=2, transport=TwilioSmsTransport. NOT re-run this round: the full spec local / repo suites (round 1, pre-merge tree: 644 files / 19228 passed | 1 todo; repo 54 / 55, the flake 20 / 20 when re-run).",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 this round, both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) the PR #22778 body refresh via scripts/pm/post-stamped.mjs --body=22778 --ack-through=6106397226 (relay: PATCH /repos//issues/22778), 18459/18459 bytes read back identical; (2) this patch-round os-dev-report comment via post-stamped --comment=22748 (relay: POST /repos//issues/22748/comments). git push is not a REST write. No label write.",
    "gates": "On head 34286f6 with a clean tree. 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' (no paths) derived 123 commands, the same set as round 1. All 123 ran, each exit code recorded before any pipe, and all 123 exited 0; this includes check-changeset-no-major and check-adr-0087-registration over the new Clause-② line. 'node scripts/pm/dispatch-gates.mjs --ran ran3.list' gave 'Run reconciliation — 123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN.' Roster gates for the directories written, each exit 0: check:meta-url-spelling, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check-changeset-fixed, check:published-readme-exports, check:stack-collection-maps. ESLint narrowed to the 8 touched script files ('--no-inline-config --format json'): 8 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting, so the narrowing is complete. The derivation noted origin/main had moved to a8f24b0 (#22781, packages/verify only), with no commit touching what it derives from. That commit overlaps no file here, so no second merge was made. NOT MEASURED (CI's): repo-wide pnpm lint, the workspace type-check lanes, and the path-scheduled CI jobs.",
    "line_budget": "25 files, +858 / -27 against merge-base a2e94c2 (git diff --shortstat a2e94c2 34286f6), under 3000 changed lines. Governed path: skills/objectstack-platform/SKILL.md (Tier H), unchanged this round: 489 to 488 lines, 5831 to 5826 tokens (ceiling 5833); all skills//SKILL.md together 4410 to 4409 lines.",
    "deviations": [
    "sms keeps retries alone; it is not refused whole. retries is the SmsService attempt budget. setTransport swaps only the transport, so retries applies to whichever transport the settings namespace selects, measured through bootStack (OS_SMS_PROVIDER=twilio: TwilioSmsTransport, options.retries=2, isConfigured()=true). It has no environment or settings carrier (the reader reads only cfgSms.retries, and the sms settings manifest has no retries key), so refusing sms whole would make a live knob unreachable. A retries-only block does not mislead, because a provider written beside it is refused by name with the setting that works.",
    "SmsProviderSchema removed with its only consumer: the export, its type, its isomorphic-alias pin (type-alias-convention.pin.test.ts is back to main's bytes at 776) and the cli parity describe. It was never released; the change that makes SmsServicePlugin honour a constructor provider re-admits sms.provider and brings the vocabulary back.",
    "Baselines after the removal: gen:schema refused while system/SmsProvider and system/StackSmsConfig:provider were still in json-schema.manifest/system.json and authorable-surface/system.json. Both entries were added by this PR and never on main, so the files were restored from main's side (git restore --source=a2e94c2a05, worktree only) and regenerated. The diff against main is now additions only. No RETIRED_DEFS entry was owed, because nothing published was unpublished.",
    "Clause-② line changed with the shape. The seat edits the claim to: 'Clause-②: yes (widening: the stack definition admits appName, the non-secret members of email, and sms carrying retries alone, which it refused as unrecognized keys)'. It is carried byte-for-byte in the changeset and as PR body line 2 (cmp-verified).",
    "Merge: one content conflict, in type-alias-convention.pin.test.ts. The resolution kept main's 775 → 776 (#22726) note and added this branch's pin as 776 → 777. Next commit removed this branch's pin with SmsProviderSchema, so the file now equals main's.",
    "Docs-drift bot comment 6106397226: the five hand-written pages naming appName were re-read, and none states the key is unsettable in a config file or names serve.ts as the reader. No edit."
    ],
    "files_changed": [
    ".changeset/22748-stack-email-sms-door.md (+19/-0)",
    "content/docs/getting-started/quick-reference.mdx (+1/-1)",
    "content/docs/getting-started/quick-start.mdx (+4/-2)",
    "content/docs/references/index.mdx (+6/-5)",
    "content/docs/references/system/email-config.mdx (+66/-6)",
    "content/docs/references/system/index.mdx (+2/-1)",
    "content/docs/references/system/meta.json (+1/-0)",
    "content/docs/references/system/sms-config.mdx (+83/-0)",
    "docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md (+1/-1)",
    "packages/cli/src/commands/serve-stack-email-sms-door.contract.test.ts (+104/-0)",
    "packages/cli/test/option-b-reader-acceptance.pin.test.ts (+5/-2)",
    "packages/spec/api-surface/system.json (+9/-0)",
    "packages/spec/authorable-defaults/system.json (+1/-0)",
    "packages/spec/authorable-surface/system.json (+14/-0)",
    "packages/spec/declaration-map/system.json (+6/-0)",
    "packages/spec/export-origins/system.json (+9/-0)",
    "packages/spec/json-schema.manifest/system.json (+3/-0)",
    "packages/spec/llms.txt (+2/-2)",
    "packages/spec/src/assembled-package-body.test.ts (+3/-0)",
    "packages/spec/src/stack-email-sms.test.ts (+245/-0)",
    "packages/spec/src/stack.zod.ts (+51/-0)",
    "packages/spec/src/system/email-config.zod.ts (+117/-4)",
    "packages/spec/src/system/index.ts (+1/-0)",
    "packages/spec/src/system/sms-config.zod.ts (+103/-0)",
    "skills/objectstack-platform/SKILL.md (+2/-3)"
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "class: c · reach: public door bootStack, measured round 1 (sms.provider 'twilio' plus OS_SMS_TWILIO_
    without OS_SMS_PROVIDER gives isConfigured()=false; with OS_SMS_PROVIDER, true). The seat files the services-lane card per order 6106350788; this PR now refuses the key, so a stack can no longer reach the trap. A raw config.sms.provider on serve still can. · dedupe words: sms.provider stack not honoured, SmsServicePlugin applySmsSettings constructor provider ignored, OS_SMS_PROVIDER required, config.sms.provider LogSmsTransport fallback",
    "carrier: none · noted, not filed (Acceptance notes): resolveEmailCapabilityArg's missing-key text still prescribes 'or config.email.apiKey', a form a built stack refuses by name with OS_EMAIL_API_KEY as the prescription.",
    "carrier: none · noted, not filed (Acceptance notes): the SMTP transport reads timeout and transportOptions, and resend / postmark read endpoint, but no spec text declares them, so the strict email.options block refuses them."
    ]
    }

  6. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #22778 at 34286f646a (Fixes #22748). The review at CONTRACT_REVIEW_TIER runs next. It is Tier H (skills/**), so it lands by the maintainer's hand or an authorized APPROVED review

    domain:spec seat 1 (#6017) · os-project-manager · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-11T07:49Z · holder of claim 6105299924, whose Clause-②: line this seat edited to the landed shape. Reports: 6106330513 (round 0) and 6106819772 (patch round 1, seat order 6106350788). Thread-read: 6106819772.

    Checklist, read against GitHub:

    • PR shape: draft, base main, Fixes #22748, assignee os-project-manager. Line 2, the changeset and the claim carry the same line: Clause-②: yes (widening: the stack definition admits appName, the non-secret members of email, and smscarryingretries alone, which it refused as unrecognized keys).
    • Scope: 25 files, +858 / −27 against a2e94c2a05.
      • stack.zod.ts and system/email-config.zod.ts / sms-config.zod.ts.
      • The pins and a cli contract rider.
      • The generated baselines and reference docs, regenerated by the repo's tooling.
      • The changeset.
      • One token-neutral line in skills/objectstack-platform/SKILL.md, forced by check:skill-top-level-keys. That line makes the PR governed.
    • The shape, against triage 6104830037, by the reader-key census:
      • Admitted: the non-secret email members (provider, defaultFrom, retries, queueDelivery, persist, appName, defaultTemplateContext, and a strict options block of host / port / secure / user / messageStream), top-level appName, and sms.retries.
      • Refused by name, each naming the env var or Settings that carries it: email.apiKey, email.options.apiKey, email.options.password, sms.providerOptions with its flat tokens, and sms.provider (order B: measured not delivered from a stack).
      • The triage's stop rule did not fire: every secret has an env carrier.
    • Open questions: both answered by seat order 6106350788 (1 → A, the stack's guidance idiom; 2 → B). SmsProviderSchema left with its only consumer, and it was never released.

    Evidence:

    • Gates: 123 derived, 123 run, all exit 0 on the head (--ran: 0 NOT-MEASURED, 0 UNRUN). check:generated is current.
    • Pins: 16 cases. Two ablation legs (email.apiKey, sms.provider) each turn exactly their own pin red, and the restores are blob-equal.
    • Public door os validate: email.apiKey and sms.provider each exit 1 with their prescription. The admitted shape exits 0. The four example apps are byte-identical before and after.
    • sms.retries measured through bootStack: with OS_SMS_PROVIDER=twilio and the credentials in env, retries: 2 reaches TwilioSmsTransport and isConfigured() is true.
    • CI on the head: 33 success, 2 skipped.

    Filed and noted:

  7. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Handover: PR #22778 waits on a human merge (Tier H). This seat stands down, and the next domain:spec seat lands it

    domain:spec seat 1 (#6017) · os-project-manager · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-11T08:03Z · holder of claim 6105299924. The seat stands down by the maintainer's word (「当前任务处理完,合并后就下班」). ⛔ Not a release to pm:queue: the work is done and accepted.

  8. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22778 → 85edd61cdb (Tier H, merged on the maintainer's approval). The card is closed (completed) and pm:dispatched is removed

    domain:spec seat 1 (#6017) · os-project-manager · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-11T08:57Z · holder of claim 6105299924, released by this landing. The handover note 6106949020 is void: the seat landed it before standing down.

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

Metadata

Metadata

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions