Repository navigation
A declared webhook in a scaffolded app is silently inert: os validate passes without the 'webhooks' and 'realtime' capabilities, and with 'webhooks' alone the activation answers enabled while nothing is ever enqueued (WARN only) #24910
Description
Ruled: 6303859748 · letter A · 2026-10-20T23:14Z
Blocked-by: #24997
Filing gate: ① product defect with a named location and a reproduction (a declared, activated delivery that is never made, with the loss logged at WARN only).
- Category: ①. reach: the published 18.0.0 package, measured once through public entry points. os validate, os dev, POST /api/v1/security/_activation/webhook/NAME, and POST/PATCH /api/v1/data/OBJECT, in a fresh create-objectstack@18.0.0 project.
- Who acts: the seat that owns packages/plugins/plugin-webhooks, together with the owner of the CLI's capability resolution / validate rules. Locations: packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts (~373, the "[webhook-auto-enqueuer] disabled" WARN); the capability preflight in packages/cli/src/utils/capability-preflight.ts, which does not tie a non-empty webhooks collection to the 'webhooks' token or 'webhooks' to 'realtime'.
- Dedup: local scan of the 626 newest issues (state=all, open and closed, release text: three pending changesets in the next 18.0.0 pre-release carry sentences that later same-release PRs make false (Hono currentRecord 'held'; the organization_id drop gate 'inventory only') #23703 to pm-dispatch: merge
domain:engineanddomain:servicesintodomain:server(maintainer 「甲」, 「就叫 server 吧」), with a no-interrupt transition — and movepackages/qa(dogfood) todomain:devx#24907) for webhook near requires / capability / realtime / inert / silent, "auto-enqueuer] disabled" and "declared-webhook reader": 0 hits. Semantic search "declared webhooks silently inert when webhooks capability not required": 10 results, none about this. The nearest is enforce-or-remove: the declared webhook'slabel/descriptionkeys — are they read anywhere since declared webhooks come from the registry? #24900 (open; the label/description keys of a declared webhook, a different question). On PostgreSQL every webhook delivery is dropped at enqueue — "[webhook-auto-enqueuer] enqueue failed … Converting circular structure to JSON (Timeout)" — and the loss is logged at WARN #24835 is the PostgreSQL enqueue failure; this card is driver-independent and was measured on SQLite.
reach: in a create-objectstack@18.0.0 project, add webhooks: [defineWebhook(...)] to objectstack.config.ts; os validate, os dev, activation 200, then a record write enqueues nothing (SQLite, published npm 18.0.0).
QA-source: #24908 · smoke:npm-18.0.0 · E5
type: Bug
Reproduction (published 18.0.0, SQLite)
- npm create objectstack@18.0.0 my-app -- --skip-skills; cd my-app. The scaffold's requires is ['automation', 'triggers'].
- In objectstack.config.ts, add the import defineWebhook from '@objectstack/spec/automation' and the stack key
webhooks: [defineWebhook({ name: 'my_app_note_changed', object: 'my_app_note', triggers: ['create','update','delete'], url: 'http://127.0.0.1:3999/hook', method: 'POST', isActive: true })] - npx os validate: "Validation passed", including "Checking that every required capability has a provider". npx os build exit 0.
- npx os dev --no-watch -p PORT -d file:FRESH. The boot log says nothing about webhooks. Sign in as the dev admin.
- POST /api/v1/security/_activation/webhook/my_app_note_changed with body {"enabled": true}: 503 SERVICE_UNAVAILABLE "Cannot verify the declaration of webhook ... no declared-webhook reader is bound to this deployment".
- Change requires to ['automation', 'triggers', 'webhooks'], rebuild, and boot a fresh file.
- Boot: one WARN "[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available {hasEngine:true, hasRealtime:false, hasMessaging:true}".
- Activation: 200 {"type":"webhook","name":"my_app_note_changed","enabled":true}.
- POST a my_app_note (201), then PATCH it (200). Wait 5 s. GET /api/v1/data/sys_http_delivery: total 0.
- Add 'realtime' to requires as well: the same steps give 2 delivery rows (declared:my_app_note_changed, pending), which is the expected result.
What is wrong
- The stack declares something that never happens, and every door says yes: validate passes, the activation door answers enabled:true, and the record writes answer 200. In step 6 the only trace is one WARN at boot, before any write. Under AGENTS.md "Degradation log levels", a dropped durable write is an error, said once, with its consequence and its fix.
- Nothing ties the pieces together. A non-empty webhooks collection does not imply (or require) the 'webhooks' token, and the 'webhooks' token does not imply 'realtime', which its enqueuer cannot run without. automation/webhooks.mdx names neither token. The working combination is visible only in examples/app-showcase's requires list.
Acceptance: a stack whose metadata declares webhooks, but whose requires cannot dispatch them, is refused at os validate or at boot with the missing token named, or the tokens are implied. And the activation door does not answer enabled:true on a deployment whose enqueuer is disabled.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 13 · 2026-10-20T16:32Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24910-declared-webhook-capabilitiesWorktree:
objectstack-issue-24910Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/plugins/plugin-webhooks/src/**(the outbox plugin's auto-enqueuer gate and its log level, the declared-webhook reader) and its tests; cross-lane intodomain:cli, declared on #22648 at this claim:packages/cli/src/utils/capability-preflight.tswith thevalidate/servecall sites that read it, andpackages/runtime/src/domains/catalog-activation.ts(thewebhookactivation subject), with their tests;content/docs/automation/webhooks.mdx; one new.changeset/24910-*.md. ⛔ Notpackages/core/**(capability-providers.ts,capability-composition.ts,dependency-resolver.tsaredomain:engine's), ⛔ notpackages/spec/**, ⛔ no token implied or auto-added torequires(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus), quoting this dispatch's --tier output "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable"Clause-②: no
Responsibility:
plugin-webhooks disables its auto-enqueuer with one boot WARN while the activation door answers enabled:true, and the CLI's capability preflight does not tie a non-empty webhooks collection to the tokens its dispatch needs | none: the working requires combination is visible only in examples/app-showcase | every app author who declares a webhook per automation/webhooks.mdx in a scaffold whose requires is ['automation', 'triggers']Thread-read: none
Serial constraints cleared:
No open PR touches plugin-webhooks, capability-preflight.ts or catalog-activation.ts (all 14 open PRs' files read at this claim). #24835 (PR #24906, eee65afc89) and its round 1 (auto-enqueuer.ts, 3fbef41b4a) have landed. #24900 (domain:spec, pm:queue, the declared webhook's label/description keys) is not in flight.Seat ruling (third tier, PM discretion; the maintainer's veto window applies). Governing text: AGENTS.md "Degradation log levels —
warnvserror"; the decision frame's third axis (a loud refusal at publish over consumer-side tolerance; a declared capability the runtime does not honour is never allowed). The card's acceptance offers refusal or implication, and these texts settle it for refusal.os validaterefuses a stack whosewebhookscollection is non-empty whilerequireslacks a token its dispatch needs, naming each missing token.servereads the same rule.webhookactivation subject does not answerenabled: truewhile the auto-enqueuer is disabled.error, with its consequence and its fix.webhookswithoutrealtimecannot deliver a declared webhook (webhook-outbox-plugin.ts:369–374at94842bad2a).os validatecan read both the stack'swebhookscollection and itsrequireswithout booting.packages/coreorpackages/spec.os validate's accept set narrows. That owes one contract-review-tier review before the queue, which the seat runs at review.Readings taken on
origin/main94842bad2a.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24910,
"status": "needs_decision",
"branch": "claude/issue-24910-declared-webhook-capabilities",
"pr": "#24946",
"session": "session_011TdZ8GjixNZFLz1xJD9zzK — this run's harness-stamped id (subagent of the seat; the claim comment 6295896264 names it)",
"premise_still_valid": true,
"summary": "Delivered the ruling's parts (b), (c) and the docs in draft PR #24946 (Part of #24910, so merging it does not close the card). (c): plugin-webhooks decides at kernel:ready whether its auto-enqueuer can run. When it cannot (no realtime) and the stack declares webhooks, the boot logs ONE error that names each declared webhook, the consequence (never delivered while writes succeed) and the fix (add 'realtime' to requires). With no declared webhook it stays the old warn, and autoEnqueue: false stays silent. (b): the declaredWebhooks reader is bound already knowing that verdict (dispatchUnavailable()). POST /api/v1/security/_activation/webhook/NAME refuses ON with 503 SERVICE_UNAVAILABLE and writes nothing there. OFF is still written, an undeclared name is still 404 and the authority gates still answer first. Part (a), the os validate refusal, is NOT implemented: P3 is falsified as stated. The honest home of 'a stack declaring webhooks names webhooks + realtime in requires' is packages/spec's defineStack. The same declared-capability class already lives there (validateTriggerCapability, validateHierarchyScopeCapability), and every door reads it. That is a hard stop, so this run reports a fork, with no token implied.",
"tests": "All on head 02d22e1 (code identical to 96a99eb, where the ablation ran). (1) pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: Test Files 23 passed (23), Tests 231 passed (231). It includes the new src/auto-enqueuer-disabled-report.test.ts: a real LiteKernel, real ObjectQL over a real in-memory SqlDriver, the webhook from defineWebhook through engine.registerApp, and a real MessagingService; 4 cases: no realtime = 1 error + reader names 'realtime' + insert/update enqueue 0; CONTROL with realtime = 0 errors + null + 2 deliveries declared:my_app_note_changed created/updated; no declared webhook = warn only; autoEnqueue:false = silent + null. typecheck: exit 0 (check:test-typecheck OK, 0 debt). (2) pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2: Test Files 435 passed (435), Tests 7609 passed | 19 skipped (7628). catalog-activation-door.test.ts 71 passed (7 new cases). typecheck exit 0. (3) Ablation: one leg per changed behaviour, each through scripts/ablation-replace.mjs in WRAP mode (anchor x1 to x0, blob moved), each restore proven blob == HEAD with git diff HEAD empty. Subjects resolve through relative src imports, so no dist was involved. Leg A, the error branch never taken: 1 failed | 3 passed ('expected [] to have a length of 1'). Leg B, the reader born without the gap: 2 failed | 2 passed. B's FIRST attempt was a no-op: its replacement 'null,' already occurred in the anchor, the tool refused ('replacement count moved 1 -> 1') and restored, and B was re-run with a distinct replacement. Leg C, the door never asks: 4 failed | 67 passed (200 where 503 was expected). The card's own path, a published 18.0.0 scaffold outside the workspace: NOT MEASURED, reason: an npm install plus a boot on a shared box; the defect is reproduced in-tree by leg 1 against its control.",
"gates": "Local, head 02d22e1. Closure build: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-webhooks^...' --filter '@objectstack/runtime^...' build, VERDICT command-exit 0. The CLI closure was not built: packages/cli is untouched. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set = 7 files vs merge base 7169612; stderr names the tree at 02d22e1) printed 100 commands. Each ran once, sequentially, exit captured before any pipe (runner PID 2913, nohup, waited with tail --pid). First pass: 97 exit 0, 3 exit 3 PREREQUISITE NOT MET (spec check:skill-examples, check:dual-build-cjs-loads, check:i18n). After pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2 under the lock (76/76 tasks, VERDICT command-exit 0, tree still clean), the three re-ran and exited 0 (skill-examples '262 prose examples type-check'; i18n 'all bundles in sync'). --ran: '100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED', exit 0. Not derived-family: check-platform-checklist was not run, since no exported symbol was renamed or deleted. CI on PR #24946 head 02d22e1 at report time, 25 check-runs: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' and 'Packed-tarball smoke (opt-in)' skipped; in_progress: Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, 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, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter. Test Core and Build Core had not appeared yet.",
"mcp_calls": "0 — no MCP tool was called",
"api_writes": "2 — two relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, which the relay ran as POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24946/assignees [os-justin], run 39465476791 success, body read back byte-identical; (2) comment, POST /repos//issues/24910/comments (this report). Plus 3 git pushes (the empty-branch probe, the WIP commit, the docs/changeset commit), which are not REST. No labels written.",
"files_changed": [
".changeset/24910-declared-webhook-undeliverable.md",
"content/docs/automation/webhooks.mdx",
"packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts",
"packages/plugins/plugin-webhooks/src/declared-webhooks.ts",
"packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts",
"packages/runtime/src/domains/catalog-activation-door.test.ts",
"packages/runtime/src/domains/catalog-activation.ts"
],
"deviations": [
"The PR's first line is 'Part of #24910', not the dispatch's 'Fixes #24910': part (a) is forked and still open. os-dev.md requires Part-of when merging must not close the card.",
"Part (a) is not implemented (the P3 fork). capability-preflight.ts, validate.ts and serve.ts are untouched. From the #22648 declaration (comment 6295927555), only catalog-activation.ts and its test were touched.",
"Changeset bumps: @objectstack/plugin-webhooks patch, @objectstack/runtime patch. Its Before/After covers the boot report and the activation door. It states that os validate / os build are unchanged, because the validate narrowing was not built.",
"Clause-② was copied verbatim from the claim as 'Clause-②: no', into the PR body and the changeset. See open_questions on the arm."
],
"open_questions": [
{
"question": "Where does part (a) live: the refusal of a stack whose webhooks collection is non-empty while requires lacks 'webhooks' and/or 'realtime'? Premises measured on origin/main 7169612. P1 holds: realtime.subscribe is the auto-enqueuer's only source, and messaging IS in PLATFORM_ALWAYS_ON_CAPABILITIES. P2 holds. H4: autoEnqueue:false has 0 production callers, only 3 plugin-webhooks tests and 1 dogfood test.",
"options": [
"A — a third defineStack declared-capability refusal in packages/spec/src/stack.zod.ts, beside validateTriggerCapability / validateHierarchyScopeCapability, with a new ADR-0112 code (e.g. STACK_WEBHOOK_CAPABILITY_REQUIRED); domain:spec. Business need: covers every door that loads a stack (os validate, os build, os serve/dev, verify's bootStack, cloud's runtime). Fixture triage: the dogfood webhook-materialization fixture declares a webhook with no requires and deliberately boots without realtime; 6 other lint/spec files spell webhooks: [ and must be scanned. Long-term: one class, one home, and 'serve reads the same rule' with no second call site. AI-error: refused at defineStack, even in a test. Startup scope: no new gate; one error code; defineStack's accept set narrows (Clause-② narrowing, minor). Cloud's resolver for 'webhooks' is NOT MEASURED; the trigger rule measured cloud's resolveCapabilityDependencies, so measure that first. Effort S–M.",
"B — a rule in packages/cli/src/utils/capability-preflight.ts, read by validate/build through preflightDeclaredCapabilities and by serve through a new call; domain:cli. Business need: covers the CLI doors only. bootStack (an app's own tests) and cloud stay silent on the inert stack. Long-term: a second home for one class (two members in spec, one in the CLI). The preflight's charter is provider resolution ('Not a registry rule: it reads node_modules, not the stack', validate.ts step 3b). AI-error: loud at the CLI, silent in-process. Startup scope: no spec lane, no error code. Effort S.",
"C — plugin-webhooks adds the realtime plugin to its dependencies: boot-time 'Missing dependency' only, invisible to os validate. It refuses a webhooks stack with no declared webhook and autoEnqueue:false hosts, which H4 forbids. Rejected."
],
"recommendation": "A, because it joins the existing declared-capability class in the one place every door reads, so validate, build, serve, an app's bootStack tests and cloud all refuse in one vocabulary. Its extra cost, one cloud measurement and one error code, is smaller than B's permanent second home for one class. Under A or B alike, a host that wires WebhookOutboxPlugin through plugins[] without naming the tokens is refused: the token is the declaration, as for triggers, and H4 found no production host of that shape."
},
{
"question": "Does the activation door's change take the narrowing arm? On a deployment without realtime, POST _activation/webhook/NAME with enabled:true moves from 200 (row written) to 503 SERVICE_UNAVAILABLE (nothing written). That is a REST accept-set narrowing on that composition.",
"options": [
"A — keep 'Clause-②: no' as the claim declared: a 200 that switched nothing is a defect, and the refusal is the fix (patch).",
"B — re-declare 'Clause-②: no (narrowing)', following .changeset/24813-adopt-orphans-readonly-package.md (a REST door's 200 turned into a refusal was declared narrowing). The changeset would then need: runtime 'minor', a BREAKING FROM/TO block and an adr-0087 not-required (no-migration-prescription) marker."
],
"recommendation": "B, by the #24813 precedent: a client that switched a declared webhook on gets a new status. The seat owns the line, though, so this run copied 'no'. The rewrite is the changeset file alone, plus the body's Clause line."
}
],
"out_of_scope_findings": [
"carrier: #24910 (this card's open half; the seat) · noted, not filed — on the same composition ('webhooks' without 'realtime') an organization-authored sys_webhook row is never delivered either. With no declared webhook that stays at warn, as the ruling scoped (c). Known from code reading only (AutoEnqueuer never starts, so no row is dispatched); not measured through a public door, so it is not filable here. Dedupe words: authored sys_webhook inert without realtime, auto-enqueuer disabled warn authored rows, webhooks realtime requires authored.",
"carrier: none · noted, not filed — under '--preset minimal' messaging is not mounted. plugin-webhooks' dependencies on com.objectstack.service.messaging would then refuse the boot in the kernel's dependency resolver ('Missing dependency', dependency-resolver.ts:212). NOT MEASURED; mentioned only because the new boot line's fix names that preset."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24910,
"round": 2,
"status": "done",
"branch": "claude/issue-24910-declared-webhook-capabilities",
"pr": "#24946",
"session": "session_011TdZ8GjixNZFLz1xJD9zzK — this run's harness-stamped id (subagent of the seat; the claim comment 6295896264 names it)",
"premise_still_valid": true,
"summary": "Round 2, per the seat's review. PR #24946 now carries part (c) and the docs only, and stays 'Part of #24910'. Removed: the activation door's refusal (packages/runtime is back to main's bytes, empty diff), and the declared-webhook reader's dispatchUnavailable() verdict with its DeclaredWebhookDispatch interface and constructor parameter. The door was the verdict's only consumer, and the boot report never read it. bootWebhookTable and the kernel:ready hook are byte-identical to main again. Kept as built: ONE error at boot naming each declared webhook, the consequence and the fix; warn when no webhook is declared; silent under autoEnqueue: false. What (c) needs in declared-webhooks.ts is readDeclaredWebhookNames() alone. The docs name 'webhooks' + 'realtime' and claim no refusal. The changeset is @objectstack/plugin-webhooks patch only, Clause-②: no, and its 'Unchanged' names os validate / os build and the activation door. Merged origin/main at feb09db first (packages/cli only, no overlap). Open questions 1 and 2 go to the maintainer, and the seat writes that decision.",
"tests": "On head 9e0ec86. pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: Test Files 23 passed (23), Tests 231 passed (231). It includes the four-case real-kernel pin src/auto-enqueuer-disabled-report.test.ts (real LiteKernel, real ObjectQL over an in-memory SqlDriver, defineWebhook through engine.registerApp, real MessagingService): no realtime = 1 error naming the webhook and "add 'realtime' to
requires", 0 enqueued; CONTROL with realtime = 0 errors, 2 deliveries declared:my_app_note_changed created/updated; no declared webhook = warn only; autoEnqueue:false = silent. pnpm --filter @objectstack/plugin-webhooks typecheck: exit 0 (check:test-typecheck OK, 0 debt). Ablation leg A, re-run on 9e0ec86 through scripts/ablation-replace.mjs in WRAP mode: anchor 'if (declared.length > 0) {' x1 to x0, blob 1ca4e8261438 to e5ba05b5a301; 'Tests 1 failed | 3 passed (4)', 'expected [] to have a length of 1 but got +0'. Restore: blob-equal 1ca4e82614386e5569c64d6db634b6f1eadc65e2 == HEAD, git diff HEAD empty. Legs B and C are retired along with the code they tested. packages/runtime was not re-tested: its diff against main is 0 lines (git diff feb09db -- packages/runtime).","gates": "Local, head 9e0ec86, all under the verify lock. Closure: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-webhooks^...' build, VERDICT command-exit 0. Full pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2: 76/76 tasks (62 cached), VERDICT command-exit 0, tree clean afterwards. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; stderr names the tree at 9e0ec86; 5 files vs merge base feb09db) printed 100 commands, the same set as round 1. A single runner (lock-wrapped, PID 7870, nohup, waited with tail --pid) ran each command once with its exit captured before any pipe; lock VERDICT command-exit 0 after 774s. Result: 100 of 100 exit 0. --ran: '100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED', exit 0. CI: the round-1 head 02d22e1 completed with 42 check-runs, 36 success and 6 skipped, none red. The round-2 head 9e0ec86 had 25 check-runs at report time: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' and 'Packed-tarball smoke (opt-in)' skipped; the rest in_progress (Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, 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, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter).",
"mcp_calls": "0 — no MCP tool was called in either round",
"api_writes": "1 this round: one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches, running comment = POST /repos//issues/24910/comments (this round-2 report). Plus 1 git push (02d22e1..9e0ec86: the merge of origin/main and the round-2 commit), which is not REST. The PR body was not PATCHed: round2_pr_markdown below is for the seat to append. Round 1's writes (pr_create stroke and report comment 6296915123) are unchanged. No labels written. The PR now carries documentation, size/l, tests and tooling; those were set by the repo's labelers, not by this run.",
"files_changed": [
".changeset/24910-declared-webhook-undeliverable.md",
"content/docs/automation/webhooks.mdx",
"packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts",
"packages/plugins/plugin-webhooks/src/declared-webhooks.ts",
"packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts"
],
"deviations": [
"The PR body is now stale where it describes part (b) and dispatchUnavailable(). It was not PATCHed, per the patch-round order; round2_pr_markdown opens by saying so, for the seat to append.",
"Local git worktree was removed after round 1 and recreated on the same branch for round 2 (no remote change in between)."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: #24910 (the open half; the seat) · noted, not filed — unchanged from round 1. On a 'webhooks'-without-'realtime' composition an authored sys_webhook row is never delivered either, and with no declared webhook that stays at warn. Known from code reading only; not measured through a public door."
],
"round2_pr_markdown": "## Round 2 (head
9e0ec86422): the boot report and the docs only\n\nThe seat's review ruled that the activation door's200to503change is a REST accept-set narrowing, which is protocol class. That goes to the maintainer together with part (a), so this PR now carries part (c) and the docs. The sections above that describe (b), the activation door's refusal anddispatchUnavailable(), no longer describe this diff.\n\n- Removed:\n - The activation door's refusal.packages/runtime/src/domains/catalog-activation.tsandcatalog-activation-door.test.tsare back tomain's bytes, so the PR'spackages/runtimediff is empty.\n - The declared-webhook reader'sdispatchUnavailable()verdict, together with itsDeclaredWebhookDispatchinterface and constructor parameter. The door was its only consumer. The boot report never read it: it builds its sentence from the wiring it resolves itself.bootWebhookTableand thekernel:readyhook are byte-identical tomainagain.\n- Kept, as built: ONEerrorat boot when the auto-enqueuer cannot run and the stack declares webhooks. It names each declared webhook, the consequence and the fix. The oldwarnstays when no webhook is declared, andautoEnqueue: falsestays silent. The only addition todeclared-webhooks.tsisreadDeclaredWebhookNames(), which the report needs to name the webhooks.\n- Docs:webhooks.mdx§3.0 names'webhooks'+'realtime'. It claims no refusal by the activation door or byos validate; it says only that switching a declared webhook on delivers nothing while'realtime'is missing.\n- Changeset:@objectstack/plugin-webhookspatchonly,Clause-②: no. "Unchanged" namesos validate/os buildand the activation door.\n- Mergedorigin/mainatfeb09db14a; it touched onlypackages/cli, with no overlap.\n\nFiles now:.changeset/24910-declared-webhook-undeliverable.md,content/docs/automation/webhooks.mdx,packages/plugins/plugin-webhooks/src/{auto-enqueuer-disabled-report.test,declared-webhooks,webhook-outbox-plugin}.ts.\n\nTests on9e0ec86422:\n-pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: 23 files, 231 tests passed.typecheckexit 0.\n- The four-case real-kernel pin: no realtime gives 1 error and 0 enqueued; the CONTROL with realtime gives 2 deliveries; no declared webhook gives the warn only;autoEnqueue: falseis silent.\n- Ablation leg A, re-run on this head: the error branch is never taken, and the run reads "1 failed | 3 passed" ("expected [] to have a length of 1"). The restore was proven by blob equality with HEAD and an emptygit diff HEAD.\n\nGates on9e0ec86422:dispatch-gates.mjs --commands --repo objectstack-ai/objectstackprinted 100 commands (5 files vs the merge basefeb09db14). All 100 ran under the verify lock and exited 0, after a fullturbo run build(76/76).--ranreported "100 run, 0 NOT-MEASURED", exit 0. Round 1's head02d22e1eecfinished CI with 36 success and 6 skipped out of 42 check-runs, none red."}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24946 comment
6297908339·domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T18:15ZGenerated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed (part) ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T18:44Zadded_to_merge_queue2026-10-20T18:23Z →merged2026-10-20T18:43Zorigin/mainby content19e5338cc8(one parent,8cfe1f94cb), 5 files, all underplugin-webhooks,webhooks.mdxand the changeset.declared-webhooks.tsexportsreadDeclaredWebhookNames.packages/runtimeis untouchedPart of #24910, so this card stays openA correction to the record. The squash commit's title, "… and refused at the activation door", is round 1's PR title, which was never updated after the patch round. The activation door is NOT changed (the squash touches no
packages/runtimefile). The changeset, which the release notes are built from, says so correctly: "The activation door … answers exactly as before."What changes for users: when a stack declares webhooks but the deployment cannot dispatch them (no
realtime), the boot logs ONEerrornaming each webhook, the consequence and the fix, instead of a bare warning.webhooks.mdxnames the two tokens a declared webhook needs (ACCEPT6297908339).What is left: parts (a), the
defineStackrefusal, and (b), the activation door. Both are protocol class. The decision is posted below, and the card moves toneeds-user-decision.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
决策请求 ·
domain:servicesseat 1 ·os-justin·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T18:45Z依据:os-dev 报告
6296915123(本卡),读数基于origin/main71696121a6。席位在认领6295896264中的第三档裁决(响亮拒绝,⛔ 不隐式补 token),其前提 P3 被实测证伪。本卡能由席位落地的部分是 PR #24946(Part of #24910):启动时对每个声明的 webhook 报一次error,并补文档。剩下两半都属协议类,交维护者裁。一句话问题: 应用作者照文档声明了一个 webhook,校验、构建、启动、激活全都说「好」,记录写入也成功,但 webhook 一次都不会投递。要不要在应用加载时就把这种配置拒掉?拒在哪一层?
背景
requires是['automation', 'triggers']。声明 webhook 后,os validate通过;只加webhooks而不加realtime时,激活接口回enabled: true,但从不入队。error,点名每个声明的 webhook、后果(写入成功但永不投递)和修法(requires加realtime)。bootStack)不报错;enabled: true。defineStack里的validateTriggerCapability([finding]requires: ['triggers']withoutautomationvalidates, but at boot the record-change trigger is not installed and every flow "will never run" #20332,错误码STACK_TRIGGER_CAPABILITY_REQUIRED)与validateHierarchyScopeCapability。每一道加载 stack 的门都读它们。Governing text
warnvserror」:从外面看系统一切正常,而它声称会落地的东西没有落地 ⇒error。packages/spec/src/stack.zod.ts:3258的声明式能力类([finding]requires: ['triggers']withoutautomationvalidates, but at boot the record-change trigger is not installed and every flow "will never run" #20332)。filing-gate.md):「协议类:修复改 spec 接受/声明面 …… REST/数据门收窄亦算 …… 维护者裁决」。协议声明、是否改协议
两半都改协议:
defineStack的接受集收窄,并新增一个错误码;后者按 #24813 的先例(
.changeset/24813-adopt-orphans-readonly-package.md)声明为Clause-②: no (narrowing):minor 加 BREAKING 横幅,附 adr-0087 not-required 标记。前提(各带 re-check 命令)
git grep -n "tryGetService<IRealtimeService>" packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.tsmessaging在常开能力清单里,无需声明。re-check:git grep -n "PLATFORM_ALWAYS_ON_CAPABILITIES" packages/spec/src/kernel/platform-capabilities.tsautoEnqueue: false没有生产调用方。re-check:git grep -n "autoEnqueue: false" -- packages examples ':!*.test.ts',在71696121a6上命中 3 处:1 处 CLI 门禁脚本(check-app-nav-i18n.mjs:352)、2 处注释;其余只在测试里。webhooks的能力解析未测量。trigger 规则当初测过 cloud 的resolveCapabilityDependencies;选 A 时须先测这一步。选项 × 真实代价 × 工作量
defineStack中,声明了 webhook 却缺webhooks或realtime的 stack 被拒,新错误码如STACK_WEBHOOK_CAPABILITY_REQUIREDbootStack测试、cloud 运行时都用同一句话拒绝domain:spec,1 个 PR(S–M),另加 dogfood 的 webhook 物化夹具修正与 6 处示例复查;先例 #20332,行数未测capability-preflight.ts加规则,validate、build、serve 读取domain:cli,1 个 PR(S);无先例error;validate、测试、激活接口仍说「好」激活接口那一半(在 A 或 B 之外的附加项):
ON(503,不写入)。代价:REST 收窄、BREAKING。dev 已在 PR fix(plugin-webhooks,runtime): a declared webhook the deployment can never deliver is reported at error and refused at the activation door #24946 第一版实测过,补丁轮已移除。业务含义直译:
四轴(业务立场)
automation/webhooks.mdx声明 webhook 的 scaffold 应用都会撞上(scaffold 的requires不含这两个 token);examples/app-showcase的requires写对了组合;defineStack就看到响亮拒绝,测试里也一样;B 只在跑 CLI 时拒;D 让 AI 在日志之外看不到任何信号。defineStack的收窄只拒掉一种今天就不工作的配置。推荐:A,不做 A′
defineStack的一类统一规则,trigger、hierarchy、webhook 同在一处,所有加载门共用一个词表。参照:Kubernetes 的准入校验在 API 服务器一处执行,kubectl 与控制器都无法绕过。webhooks的解析未测;裁后执行(选 A 时)
domain:spec立一张执行卡,指明stack.zod.ts的第三条声明式能力拒绝、错误码登记(error-code-ledger.zod.ts)、cloud 解析的先测项、dogfood 夹具修正,以及Clause-②: no (narrowing)与契约级复核。needs-user-decision;裁定 A 后转pm:blocked,Blocked-by:指向那张 spec 卡,spec 卡落地时关闭本卡。sys_webhook行同样永不投递,没有声明 webhook 时仍只是warn;只读过代码,未经公开门实测。随 A 一并记入 spec 卡的「Unchanged」说明,不另立卡。相关单与 PR
Part of)6295927555requires: ['triggers']withoutautomationvalidates, but at boot the record-change trigger is not installed and every flow "will never run" #20332(validateTriggerCapability)os-decision-facets
defineStack响亮拒绝,优于启动日志里的一条 error、更优于静默的enabled: true。Prior rulings read:
validateTriggerCapability·STACK_TRIGGER_CAPABILITY_REQUIRED·REST door narrowing BREAKING→ 2 precedents (#20332 instack.zod.ts:3258,.changeset/24813-…); ADR none named; thread: none.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Ruling: batch #379 item 4 · letter A · maintainer 「同意」 2026-10-20T23:05Z
Director seat, summon #45 continuation 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #379 item 4, from the decision 6298521209 (domain:servicesseat 1), the dev reports 6296915123 / 6297443354 and the landing record 6298509196 (PR #24946,19e5338cc8, part (c)); the director's chat delta re-checked onorigin/maine4d604efd39fthe declared-capability class (stack.zod.ts:4510validateTriggerCapability,:4432validateHierarchyScopeCapability,:3263STACK_TRIGGER_CAPABILITY_REQUIRED) and the enqueuer's single event source (webhook-outbox-plugin.ts:426), and put the timing of A under the standing ruling 「甲」 (#60156292005854), which the card's decision did not cite. The maintainer's 「同意」 takes letter A with the director's timing: A′ not taken, no token implied, the refusal in 19.0. Thread read through 6298521209 at this write; nothing newer.Governing text:AGENTS.md "Degradation log levels" (what claims to land and does not ⇒error);packages/spec/src/stack.zod.ts:3258(the declared-capability class, #20332); 「甲」 rule 2 (a narrowing that no ADR-0087 conversion carries ships its loud half in 18.x and its refusal in 19.0).What the ruling binds
defineStack, as the third declared-capability refusal beside trigger and hierarchy: a stack that declares a webhook whilerequireslacks'webhooks'or'realtime'is refused with one code,STACK_WEBHOOK_CAPABILITY_REQUIRED, by every door that loads a stack. B (a CLI-only preflight rule), C (a plugin hard dependency) and D (the boot error alone) are not taken.realtimetorequireschanges the deployment's composition, not a spelling, and the maintainer did not take it. So 18.x ships the loud half (PR fix(plugin-webhooks,runtime): a declared webhook the deployment can never deliver is reported at error and refused at the activation door #24946's booterror, already landed, plus a warning atos validate/os buildnaming the webhook, the missing tokens and the 19.0 refusal), and the refusal waits for 19.0.sys_webhookrow on awebhooks-without-realtimecomposition is equally undelivered (code-read only).Workload of the chosen letter (the card's column):
domain:spec, one PR (S–M) plus the dogfood webhook fixture correction and the example apps'requiresre-check; precedent #20332. Split by 「甲」 into the 18.x warning (#24995) and the 19.0 refusal (#24997).Execution: #24995 (
domain:specp1pm:queue, the 18.x warning) and #24997 (domain:specp1pm:on-hold,Restart-when:the 19.0 line opens), both filed by the director in this act. State:needs-user-decision→pm:blocked,Blocked-by: #24995; this card closes when #24995 lands (the 19.0 refusal has its own card). The cross-lane declaration on #22648 (6295927555) forpackages/cliis released: A moves the rule into spec, so the CLI preflight is not edited.objectstack-fleet commentedon Oct 20, 2026
Ruling: batch #379 item 4 · letter A · amended · maintainer 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 2026-10-20T23:11Z
Director seat, summon #45 continuation 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Amends the timing clause of 6303779439 (this card, batch #379 item 4). After that record the maintainer wrote, in this session's chat: 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 (v19 is not considered for now; protocol adjustments are, as a rule, made as soon as possible; no retirement version is planned around). Letter A stands; its timing changes. Thread read through 6303779439 at this write; nothing newer.Governing text:the maintainer's sentence above, which supersedes rule 2 of 「甲」 (#60156292005854) for the refusal half: a narrowing that no conversion carries no longer waits for 19.0.What the amendment binds
defineStackrefusal (STACK_WEBHOOK_CAPABILITY_REQUIRED) ships now, in 18.x, as one PR with a BREAKING changeset in the packages: POST /packages/:id/adopt-orphans skips requireWritablePackage, so it can bind loose items into a read-only package #24813 shape (BREAKING banner,adr-0087: not-required), contract review atCONTRACT_REVIEW_TIER. No separate warning step: the refusal is the loud half.requirescannot dispatch it —STACK_WEBHOOK_CAPABILITY_REQUIREDindefineStack, the third declared-capability refusal (ruling A on #24910; ships in 18.x by the maintainer's re-timing) #24997 becomes the 18.x execution card (pm:on-hold→pm:queue, itsRestart-when:line removed and its body re-timed in this act). spec: the loud half for a declared webhook whoserequirescannot dispatch it —os validate/os buildwarn, naming the missing token and the 19.0 refusal (ruling A on #24910 under 「甲」 rule 2; 18.x) #24995 (the 18.x warning) is closed as superseded; nothing to build there.requiresre-checked, the cli cross-lane declaration on [PM seat] domain:cli · seat 2 — 🟢 os-justin · session_01LmjU2x1faLZ39DVTFyVca6 #22648 released.State:
needs-user-decision→pm:blocked,Blocked-by: #24997; this card closes when #24997 lands.objectstack-fleet commentedon Oct 21, 2026
Close:
completed. Ruling A landed through #24997Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-21T10:07Z. ⛔ Not a claim.Ruling A (batch #379 item 4, amended for 18.x) states that this card closes when #24997 lands. #24997 closed
completedat 2026-10-21T07:40Z.Generated by Claude Code