Skip to content

feat(spec): declare IWebhookService.handleRedeliver, the optional transport-neutral webhook redeliver member - #22767

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22754-webhooks-redeliver-member
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22754-webhooks-redeliver-member

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22754
Clause-②: yes (widening)

What this does

Segment 1 of the webhooks bridge in #22564's stage 2, under the maintainer's ruling on #22438 (director record 6079645593, verbatim 「A + 扫类」). It declares the forward target that a dispatcher domain will call for POST /api/v1/webhooks/redeliver. On a kernel with no http.server (the hosted shape), that endpoint answers 404 ROUTE_NOT_FOUND today (stage-1 measurement 6093135788, accepted at 6093165613).

Nothing implements or calls the member yet. The runtime dispatcher domain and the plugin-webhooks implementation are the next segments.

  • packages/spec/src/contracts/webhook-service.ts (new): the contract IWebhookService for the webhooks service slot. It has one optional member, handleRedeliver?(request: Request), which returns a Promise of a web-standard Response. Its docblock states five things:
    • What it serves. The same answers as the plugin's self-hosted route: 401 UNAUTHENTICATED with no signed-in user; 400 INVALID_REQUEST for a non-JSON body; 400 MISSING_REQUIRED_FIELD without a usable deliveryId; 404 RESOURCE_NOT_FOUND for a row outside the caller's active organization; 409 with DELIVERY_NOT_ELIGIBLE or DELIVERY_NEVER_SENT when the outbox refuses the replay; 500 INTERNAL_ERROR otherwise; and 200 with the row's id and new status.
    • Authentication and tenant. It resolves the caller from the request's own credentials, through the kernel's auth service, and scopes the replay to the caller's active organization. That is the self-hosted rule, so both kernel shapes judge the same caller by one rule in one place. It takes no ExecutionContext.
    • Transport neutrality. There is no Hono, raw-app or http.server type in the signature.
    • Its one caller. That is the runtime HttpDispatcher's exact POST /webhooks/redeliver domain, which forwards the request with its body unread. The path is not on the ADR-0069 allow-list, so the dispatcher door stays stricter than the self-hosted mount, never looser. The plugin's raw-app mount is not a caller, so the path is never mounted twice.
    • The absence rule. If the webhooks slot or the member is absent, the caller answers a typed 404 or 501, never ROUTE_NOT_FOUND.
  • packages/spec/src/contracts/index.ts: exports the new contract from @objectstack/spec/contracts.
  • packages/spec/src/contracts/webhook-service.test.ts: the type-level pin.
    • RedeliverIsOptional uses the {} extends Pick form, because toEqualTypeOf cannot tell an optional member from a required one typed with undefined.
    • RedeliverTakesOneRequest asserts the parameters are exactly one Request.
    • RedeliverAnswersAResponse asserts the return type is a Promise of Response.
    • BarrelExportsTheContract asserts the barrel exports this same contract.
    • The value-level pin is that an empty object typed as IWebhookService compiles and carries no member.
    • No docblock prose is pinned, because no consumer parses it.
  • packages/spec/api-surface/contracts.json and packages/spec/export-origins/contracts.json: one regenerated line each, for the new export.
  • .changeset/22754-spec-webhooks-redeliver-member.md: @objectstack/spec minor (pre mode next), with its own Clause-②: yes (widening) line.

The slot: a new webhooks contract, not the messaging service's

The card makes the slot this segment's contract call, decided on the four axes. The PM's suggested route was the messaging service's contract, since redeliverHttp lives there. This PR departs from that suggestion, for the reasons below.

What was measured first

  1. Confirmed. plugin-webhooks registers exactly one service, webhook.autoEnqueuer (webhook-outbox-plugin.ts:317). redeliverHttp has 0 hits in packages/spec/src (git grep exit 1). The messaging service registers messaging and notification (messaging-service-plugin.ts:213, :222). The candidate slot keys are therefore webhooks (the plugin's platform capability token, platform-capabilities.ts; registered by nothing yet) and messaging.
  2. Split, so the premise does not hold as stated. The messaging service owns the replay OPERATION: MessagingService.redeliverHttp(id, { tenantId }) resets a finished row to pending, applies the tenant wall and the parked-row refusal, consults each producer's veto, and wakes the dispatcher. plugin-webhooks owns the DOOR:
    • it requires a signed-in user;
    • it resolves the tenant from the session's active organization;
    • it reads the JSON body and maps each refusal code to a status;
    • it installs the veto that refuses to replay a webhook row whose subscription or signing secret is gone (installRedeliverGuard, called before the auto-enqueuer writes its first row).
      One more finding: there is no messaging service contract to declare on. ServiceSlotContracts has no messaging entry, and no IMessagingService exists. The messaging route would mint a new contract file too, so both options cost one new contract.
  3. Read. The handler (registerAdminRoutes) takes the session's user.id and session.activeOrganizationId and a JSON body { deliveryId }. It answers the envelope listed above. The docblock describes that behaviour, and the plugin is unchanged.

The four axes

  • Real business need. The measured need is the door on a hosted kernel that composes plugin-webhooks. On the webhooks slot, the door exists exactly where self-hosted has it. A door on the messaging slot would also exist on a kernel without plugin-webhooks. Nobody asked for that door, and the webhook veto is never installed there.
  • Long-term soundness. The ruling's shape is "a dispatcher domain to the service slot", which forwards and does not reimplement; /auth and /approvals/act are the precedents (ADR-0076 D11). Under this choice, plugin-webhooks keeps the door's rules in one place for both kernel shapes. On the messaging slot, the runtime domain would hold a second copy of the authentication, body parsing and refusal-to-status mapping, beside the plugin's raw mount.
  • Preventing AI mistakes. There is one copy of the door's rules, so a later edit cannot drift one shape from the other. The absence rule is a typed 501 from an empty slot. The tenant requirement stays where it is already enforced: plugin-webhooks' typed view of redeliverHttp makes its tenantId a required property typed as string-or-undefined, so a caller must decide it. The messaging route's real advantage was a typed tenantId at the contract. It is not lost here; it stays at the one place that calls the operation.
  • Startup-stage focus. Each option mints one contract with one optional member. This one adds a slot key that the plugin registers in its own segment, the same window handleActionPage had before its implementation landed. The messaging route would also need the dispatcher to detect plugin-webhooks' presence some other way, and no existing registration answers that: webhook.autoEnqueuer is registered only when auto-enqueue starts.

The precedent is kept, not departed from. The #22575 member shape (a web-standard Request in, Response out), its docblock structure, its pin form and its changeset level are all reused.

Why handleRedeliver

  • The verb handle… is this package's spelling for a function from a Request to a Response: IAuthService.handleRequest, IRealtimeService.handleUpgrade? and IApprovalService.handleActionPage?.
  • Redeliver is the route's own last segment and the plugin's own word ("redeliver endpoint", registerRedeliverGuard).
  • IWebhookService / handleRedeliver had 0 hits anywhere outside this diff before it: objectstack git grep exit 1 (control IApprovalService exit 0), and objectui at 023f00d46 exit 1 (control IAuthService: 2 hits).

Verification

Heavy runs went through scripts/pm/os-verify-lock.sh. The VERDICT line is quoted from each. The final head is 06119a91ff: two merges of origin/main (e86530088a, then 7098acaef9) on top of the two change commits. The second merge touched no packages/spec file.

  • Build. pnpm --filter @objectstack/spec build at 740277750d, and again after the first merge at bbac3326b9: VERDICT command-exit 0 both times. The build wrote no tracked file. handleRedeliver has 1 hit each in dist/contracts/index.d.ts and .d.mts, and 0 hits in any emitted .js / .mjs / .cjs, so the change is type-only. After the second merge, the packages it touched were rebuilt (turbo run build filtered to core, cli, plugin-email, service-sms and verify): Tasks: 59 successful, 59 total.
  • check:generated. Before regeneration, it found exactly two stale artifacts: api-surface/ ("0 breaking (removed/narrowed), 1 added", + IWebhookService (interface)) and export-origins/. After gen:api-surface and gen:export-origins, at 06119a91ff: "All 14 generated artifacts are up to date".
  • Typecheck at 06119a91ff. pnpm --filter @objectstack/spec typecheck (tsc --noEmit, check:scripts-typecheck and check:test-typecheck): VERDICT command-exit 0. check:test-typecheck reports OK. tsc -p tsconfig.test.json --listFiles lists both webhook-service.ts and webhook-service.test.ts, with 0 diagnostics in either.
  • Tests.
    • At 06119a91ff: pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/contracts/webhook-service.test.ts src/contracts/approval-service.test.ts src/contracts/core-service-contracts.test.ts gave Test Files 3 passed (3), Tests 19 passed (19).
    • At bbac3326b9, the whole spec suite (vitest run --project local --maxWorkers=2) gave Test Files 644 passed (644), Tests 19213 passed | 1 todo. Since then the only commit is the second merge, which touches no spec file.
    • Consumers' tests were not run. Nothing outside packages/spec imports the new contract (git grep exit 1), and the change emits no JavaScript.
  • Gates at 06119a91ff.
    • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set derived from the merge base 7098acaef9, 6 paths, 164 changed lines) derived 87 commands, and all 87 exited 0. They include check:api-surface ("public API surface + factory signatures unchanged"), check:export-origins, check:generated, check:docs, check:authorable-surface, check:exported-any, check:spec-docblock-symbol-anchors, check:adr-0087-registration ("this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)"), check:changeset-no-major, check:empty-changeset, check:doc-authoring, check:issue-citations ("every citation this change adds resolves", 5 of 5), check:nul-bytes, check:dual-build-cjs-loads and check:lean-entry-closure.
    • --ran reconciliation, on a record carrying each exit code: "87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED".
    • The ADR-0087 disposition: the changeset is minor with the (widening) arm, which the gate reads as non-breaking, so no disposition marker is owed and none is written.
  • Reverse verification, run from the committed 740277750d through scripts/ablation-replace.mjs (wrap mode, plus an outer trap restore on EXIT, INT and TERM).
    • The mutation made the member required: handleRedeliver?(request: Request) became handleRedeliver(request: Request). On disk the anchor went 1 → 0, the replacement 0 → 1, and the blob 3ed2ead393de → 28539f95c0f1.
    • The subject is imported relatively by the test (./webhook-service), so the type program reads src, and no dist rebuild was involved.
    • Expected direction: red. Observed: red. spec's test program reported webhook-service.test.ts(30,42): error TS2344: Type 'false' does not satisfy the constraint 'true' (RedeliverIsOptional) and (46,15): error TS2741: Property 'handleRedeliver' is missing (the value pin).
    • No consumer outside spec could turn red, because nothing implements the contract yet.
    • Restore leg. The blob after restore is 3ed2ead393de, equal to the HEAD blob, and git diff HEAD is empty.

Acceptance notes

  • ServiceSlotContracts gains no webhooks entry here. That ledger says an entry "is only made where the binding is evidenced — by the provider that registers the slot, or by dispatcher work that already proved the correspondence", and nothing registers webhooks until the plugin segment. That segment, or the first one to type the lookup, adds webhooks: IWebhookService (the approvals precedent has no entry either).
  • For the plugin segment (read only, not filed). registerAdminRoutes mounts the self-hosted route whenever the http-server slot and the messaging service are both present. The webhook veto is installed only inside bootAutoEnqueue, which returns early when autoEnqueue is false or realtime is absent. On those compositions, the self-hosted door runs without the veto. Whether a webhook-sourced row can exist there was not measured. The plugin segment edits exactly these functions, and the slot registration it adds should sit beside the veto, not beside the raw mount. Carrier: Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564's plugin segment.
  • For the dispatcher segment. The @objectstack/hono catch-all already leaves the raw request readable for every body (it parses a clone, packages/adapters/hono/src/index.ts), so the domain can forward the JSON body unread with no adapter change. Its approvals.ts sibling is the template: an exact route, POST only, and 501 on an empty slot or absent member.
  • check:api-surface records the new interface's name and kind, not its members, so the snapshot shows IWebhookService (interface) only.
  • Not run locally; these are CI's. The dispatch-gates artifact-roster block (50 families), the declared wide-population families, the CI type-check lanes (workspace turbo typecheck, ./examples/*, downstream-contract) and the path-scheduled jobs (Test Core, Dogfood, Build Core, Temporal Conformance) were not run. Three PR-context guards (check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths) need this PR to exist and were not run here.
  • Labels: none written. The dispatch's write budget was a push, one draft pr_create and one report comment, and it names no label. This diff carries a changeset, so skip-changeset does not apply.

Generated by Claude Code

…nsport-neutral webhook redeliver member

A new contract for the `webhooks` service slot (plugin-webhooks), with one
optional member that serves POST /api/v1/webhooks/redeliver as a
web-standard Request -> Response, so a dispatcher domain can reach the door
on a kernel with no raw app. Type-level pins and a minor changeset.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json, packages/spec/src/contracts/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/webhooks.mdx (via deliveryId (literal, a string literal in a comment in IWebhookService), /api/v1/webhooks/redeliver (route, a path literal in a comment in IWebhookService))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json, packages/spec/src/contracts/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 139 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 a18c514965f90e09fa9432c3e72e34426ebc03d6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 249835428e6b659bcac711392f598effd3dc24ad — the merge of head 06119a91ffd262d5c2880b310a42c423c72f4c55 into base a18c514965f90e09fa9432c3e72e34426ebc03d6, 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 249835428e6b659bcac711392f598effd3dc24ad && git checkout 249835428e6b659bcac711392f598effd3dc24ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a18c514965f90e09fa9432c3e72e34426ebc03d6 06119a91ffd262d5c2880b310a42c423c72f4c55 && git checkout -B drift-repro a18c514965f90e09fa9432c3e72e34426ebc03d6 && git merge --no-ff 06119a91ffd262d5c2880b310a42c423c72f4c55

node scripts/docs-audit/affected-docs.mjs --json a18c514965f90e09fa9432c3e72e34426ebc03d6

⚠️ 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 a18c514965f90e09fa9432c3e72e34426ebc03d6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

objectstack-fleet Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 06119a91ffd262d5c2880b310a42c423c72f4c55
Local-runs: none

Inputs read: card #22754 (body, claim 6104945556, os-dev-report 6105492881); ruling 6079645593 on #22438; sweep #22564 with 6093135788 / 6093165613; precedent #22575 and its PR #22608 (merge b27b89474d); PR #22767 body, 6-file list, net diff origin/main...06119a91ff (+164/−0); every check-run on the head. Code claims below are read at the head, never from the PR text.

① Derived judgments

  • Public surface, +1 export. IWebhookService (interface) is newly exported from @objectstack/spec/contracts via index.ts; api-surface/contracts.json and export-origins/contracts.json each gain exactly that one line, matching the diff ("1 added, 0 breaking"). Nothing removed, renamed or narrowed; no JS emitted. Right.
  • The member. handleRedeliver?(request: Request) returning a Promise of Response: optional, one web-standard Request in, web-standard Response out, no Hono, no raw app, no http.server type, no ExecutionContext. Same shape as spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575's handleActionPage (approval-service.ts:1148). Right.
  • Accept-set. A service object {} satisfies the contract (every member optional); nothing on main implements or calls it; no existing contract or member moved. The pins cover optionality ({} extends Pick), parameter list, return type, barrel identity and the value-level empty object, and the dev's ablation (member made required) turned exactly those pins red. Right.
  • The slot call (webhooks, a new contract, not messaging). Read at the head: the replay OPERATION is MessagingService.redeliverHttp(id, { tenantId }) (messaging-service.ts:366: tenant wall, parked-row refusal, per-source guard, dispatcher wake); the DOOR is plugin-webhooks' registerAdminRoutes (webhook-outbox-plugin.ts:388–458: session via the kernel auth service, tenant from session.activeOrganizationId, JSON body, code-to-status mapping) and installRedeliverGuard (:333–356). No IMessagingService exists and ServiceSlotContracts has no messaging entry, so either route mints one contract. A member on the messaging slot would exist on kernels without plugin-webhooks, where the veto is never installed, and the dispatcher domain would carry a second copy of the door's rules instead of forwarding as /auth and /approvals/act do (ADR-0076 D11). The ruling's shape, a forwarding domain to the producer's slot, is kept. Right, and the reasoning matches the code.
  • A slot nothing registers yet. webhooks is only the capability token at the head (platform-capabilities.ts:66, :213); no registerService('webhooks'…) anywhere (0 hits). The spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 precedent also declared its member before the plugin segment implemented it; the difference is that approvals was already a registered slot (approvals-plugin.ts:347). The card's scope names "a webhooks service contract (new)" as one of the two allowed answers, the claim's file surface allows "the contracts index export if the contract is new", and the ruling orders spec first because segments 2 and 3 read the shape. So this is the ruling's sanctioned declared-before-implemented window, not speculative surface. The residual (the slot NAME is docblock prose until the plugin segment registers it) is carried in ③.
  • Docblock status contract vs the handler at the head. 401 UNAUTHENTICATED with no signed-in user (:399–405); 400 INVALID_REQUEST for non-JSON (:411); 400 MISSING_REQUIRED_FIELD for a missing, non-string or blank (trimmed) deliveryId (:413–418); 404 RESOURCE_NOT_FOUND for a row outside the caller's organization (:438; http-outbox.ts:703–706 tenant wall); 409 for DELIVERY_NOT_ELIGIBLE (not finished, or veto refused, http-outbox.ts assertRedeliverAllowed) and DELIVERY_NEVER_SENT (attempts 0) (:449); 500 INTERNAL_ERROR otherwise (:452–456); 200 { success: true, data: { id, status } } (:435). "Any signed-in user may press it" is true: the only check is a non-empty user.id. Auth "through the kernel's auth service": resolveSession reads tryGetService(ctx, ['auth']) then api.getSession(...) (:474–483). Every listed status and code matches. Right.
  • "Not on the ADR-0069 allow-list; stricter, never looser." auth-gate.ts's allow-list is /auth, /health, /ready, /discovery and the exact /approvals/act; no /webhooks path. The self-hosted raw mount applies no gate at all, so a dispatcher door behind the gate is stricter. Matches stage 1's "no auth-gate entry". Right.
  • ServiceSlotContracts gains no webhooks entry. The ledger's own rule (core-service-contracts.ts:18–22): an entry is a claim, made only where the provider registers the slot or dispatcher work proved the binding. Nothing registers webhooks. The precedent added no approvals entry either: the non-core list is exactly security, shareLinks, objectql, http.server, http-server, and the ledger test pins its length at 5. Right; the plugin segment adds webhooks: IWebhookService beside its registration and moves that pin to 6.
  • No dispatcher collision. packages/runtime/src/http-dispatcher.ts and domains/ have 0 hits for webhooks or redeliver; the "one caller" named by the docblock does not exist yet and nothing else claims the prefix. Consistent with "the next segment".
  • Shape parity with spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575. Same docblock skeleton (bold lead; "What it serves, exactly as the plugin's self-hosted … serves it today" list; "Transport-neutral, deliberately"; the no-ExecutionContext paragraph; "Its one caller"; "Optional, and its absence is loud"; @param / @returns), same pin form, same changeset level and Clause line. The one addition, a barrel-identity pin, is owed because the contract file is new. Parity holds.
  • Claim surface honoured. 6 files, all inside the claim's file surface; the two stop-and-report files (data-engine.ts, approval-service.ts) untouched; no governed path; head repo equals base repo (not a fork); PR body carries Fixes #22754 and Clause-②: yes (widening).
  • Test import spelling. import type … from './webhook-service' without .js is the precedent test's own spelling (approval-service.test.ts:16–21) under the test tsconfig; the CI type-check lanes are the authority and are green (see gates).

② Semver level

  • @objectstack/spec minor is right: a new exported interface plus one optional member is additive (api-surface: 1 added, 0 breaking; no removal or rename; type-only). Not skip-changeset, since a released package gains a public type. The fixed-group bump is the config's normal consequence, as for spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575.
  • The changeset body carries its own Clause-②: yes (widening) line, as AGENTS.md §3 requires; yes takes at least minor, and (widening) is the correct arm.
  • ADR-0087 marker: none owed. check-adr-0087-registration judges only a changeset that declares a breaking change; minor plus widening is non-breaking, so no disposition marker is required and none is written. Right.
  • Clause-②: yes (widening)

③ Boundary flags

  • Dev flag: slot departure from the PM's optional messaging suggestion. Answered in ① (slot call): right. The card made the slot this segment's call on the four axes, and the measured premise did split (operation on messaging, door on the plugin).
  • Dev flag: no ServiceSlotContracts entry. Answered in ①: right under the ledger's evidence rule and the precedent; the entry belongs to the plugin segment.
  • Dev flag: stray /suite.pid in the dev container. Outside the repo and the diff; no review consequence.
  • Dev flag: trailer and footer form. Commits carry the repo's model-free pair per AGENTS.md; no review consequence.
  • Dev flag: labels, zero writes; skip-changeset not applicable. Right.
  • open_questions: none. Nothing to answer.
  • Out-of-scope finding (the redeliver veto gap), verified at the head. registerAdminRoutes runs from kernel:ready (:178) whenever http-server and messaging are present; installRedeliverGuard runs only inside bootAutoEnqueue (:309), which returns early at :288 (autoEnqueue: false) or :292 (no engine, realtime or messaging). The guard needs only messaging and engine, not realtime, so its gating behind the enqueuer's prerequisites is an ordering defect on main, pre-existing, untouched by this diff (the card forbids plugin code). Reach: source: 'webhook' rows are written only by the AutoEnqueuer (auto-enqueuer.ts:981, :1134), so on such a composition a webhook row exists only as a durable sys_http_delivery row written under an earlier or sibling composition; messaging's row-local refusals (DELIVERY_NEVER_SENT, non-terminal rows) still hold without the guard, so the exposure is the guard's cases alone (subscription gone, secret unrecoverable). No public-door reading was taken, so under the os-dev rule it is a read-only inference with no measured reach: not card-worthy as reported, and the named carrier, Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564's plugin segment, which edits exactly registerAdminRoutes and bootAutoEnqueue, is the right carrier. Escalated to the adopting seat: when triage files that segment, put this finding in the card body (as acceptance 6093165613 did for the alias-only read), with two asks: install the veto independently of the auto-enqueuer's realtime prerequisite, and register the webhooks slot beside the veto, not beside the raw mount.
  • Residual on the slot name, escalated. webhooks is docblock prose until the plugin segment registers it. The segment-2 (runtime domain) and segment-3 (plugin) cards must name webhooks as the slot key, cited to this docblock, so the dispatcher's lookup and the plugin's registration cannot drift.

Gates on 06119a91ffd262d5c2880b310a42c423c72f4c55 (the required contexts on main are TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard): every required context completed green, read live from the head's check-runs: TypeScript Type Check (all four lanes and the aggregate), Test Core (all six shards and the aggregate), Dogfood Regression Gate (all three shards and the aggregate), Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard. Every other check-run on the head is green or skipped by path (Build Docs, Console Pin Gate, Packed-tarball smoke); zero failures anywhere.

Implemented-by: claude/issue-22754-webhooks-redeliver-member
Reviewed-by: session_016njDy8ozy9B9Ns5Y8kAWEK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 05:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 05:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit e84aeb3 Oct 11, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22754-webhooks-redeliver-member branch October 11, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants