Independent verification of Cloud's launcher wire shape, re-done in this
recovery round rather than carried over from the prior session's report.

$ git -C ../cloud rev-parse HEAD; git -C ../cloud branch --show-current
2ebb7325178ab4dc0d8d08a0a33f87cc7eebcdb7
feat/babysitter-webhook-routing

================================================================
A. pullRequest carries owner/repo (Bugbot: "Cloud PR coordinates always rejected")
   $ sed -n '270,282p' packages/web/lib/flows/launch-flow-deployment.ts
================================================================
      repositoryGrant = resolved.request;
      // GitHub's answer is authoritative for everything it carries; the event
      // contributes the action and, for reviews, the review itself.
      const { state: _state, merged: _merged, ...fromGithub } = resolved.pullRequest;
      pullRequest = {
        ...eventPullRequest,
        ...fromGithub,
        owner: deployment.repository.owner,
        repo: deployment.repository.name,
        labels: fromGithub.labels,
      };
      // The configured labels/contains filter runs against GitHub's current
      // view of the pull request, the same one the run will see.

   $ sed -n '356,366p' packages/web/lib/flows/launch-flow-deployment.ts   # the request body
          ...(deployment.inputs.agents?.length
            ? { harnesses: deployment.inputs.agents }
            : {}),
          inputs: {
            approver: deployment.inputs.approver,
            issue,
            ...(pullRequest ? { pullRequest } : {}),
            event: flowEventInput(input.payload, provider, input.deliveryId),
          },
          repositoryGrant,
          workspaceId: deployment.workspaceId,

================================================================
B. eventType is dotted (Bugbot: "Hosted event type will not match")
   $ grep -n -A3 'type: `${input.provider}' packages/web/lib/proactive-runtime/integration-watch-dispatcher.ts
================================================================
1603:    type: `${input.provider}.${input.eventType}`,
1604-    eventType: input.eventType,
1605-    provider: input.provider,
1606-    workspaceId: input.workspaceId,

================================================================
C. The two contracts declare the same eleven ids, in the same order.
   Cloud:  packages/web/lib/flows/github-change-request-subscriptions.ts
   Flows:  examples/babysitter/subscriptions.ts
================================================================
cloud ids (11):
   pull_request.opened
   pull_request.synchronize
   pull_request.reopened
   pull_request.ready_for_review
   pull_request.closed
   pull_request.labeled
   pull_request.unlabeled
   pull_request_review.submitted
   pull_request_review.dismissed
   check_run.completed
   issue_comment.created
flows ids (11):
   pull_request.opened
   pull_request.synchronize
   pull_request.reopened
   pull_request.ready_for_review
   pull_request.closed
   pull_request.labeled
   pull_request.unlabeled
   pull_request_review.submitted
   pull_request_review.dismissed
   check_run.completed
   issue_comment.created

identical and same order: True
cloud-only: none
flows-only: none

================================================================
D. The hosted-path fork check_run gap now documented in the README.
   $ sed -n '545,557p' packages/web/lib/flows/flow-trigger-sources.ts
================================================================
 * The pull number a check run belongs to. A `check_run` record carries every
 * pull request its head commit is part of; the first is the one the flow acts
 * on, and live state decides the rest.
 */
function githubCheckRunPullNumber(resource: Record<string, unknown>): number | undefined {
  const pulls = Array.isArray(resource.pull_requests) ? resource.pull_requests : [];
  for (const candidate of pulls) {
    const number = isRecord(candidate) ? readNumber(candidate.number) : undefined;
    if (number !== undefined) return number;
  }
  return undefined;
}


An empty pull_requests array yields number === undefined, so
githubPullRequestFromEvent returns null and launchFlowDeployment answers
skipped: "not_a_pull_request" — the delivery never reaches the flow:
   $ grep -n 'not_a_pull_request' packages/web/lib/flows/launch-flow-deployment.ts
36:  skipped?: "not_a_ticket" | "not_a_pull_request" | "pull_request_closed" | "filtered" | "unknown_provider";
227:    if (!eventPullRequest) return { ok: true, status: 200, skipped: "not_a_pull_request" };

This is Cloud's attribution, not a flows defect, and no flows-side
relaxation was added for it. The raw-webhook path already handles an
unattributed check_run (tests/wake.test.ts, 'a fork check_run with no PR
attribution still wakes and rereads').

================================================================
E. CORRECTION to section D, found after it was written.
   D said the fork gap was Cloud's check_run ATTRIBUTION and that
   changing it would close the gap. That is wrong. The grant model
   holds forks out of the hosted path regardless of attribution.
================================================================
$ grep -rn 'flow_pull_request_fork' packages/web/lib/flows/flow-deploy-launch.ts
171:      "flow_pull_request_fork",
329:      "flow_pull_request_fork",

$ sed -n '165,177p' packages/web/lib/flows/flow-deploy-launch.ts
  // `head.repo` is the repository the head branch lives in; GitHub reports
  // `null` once a fork has been deleted. Either way it is not this repository,
  // so the installation token cannot continue that branch.
  const headRepo = typeof head?.repo?.full_name === "string" ? head.repo.full_name : "";
  if (!headRepo || headRepo.toLowerCase() !== `${input.owner}/${input.repo}`.toLowerCase()) {
    throw new FlowDeployError(
      "flow_pull_request_fork",
      409,
      headRepo
        ? `Pull request #${input.number} comes from ${headRepo}; the repository's installation cannot push to a fork.`
        : `Pull request #${input.number}'s head repository is gone; nothing can continue its branch.`,
    );
  }

launchFlowDeployment calls this resolver for EVERY change-request
delivery, then treats the refusal as a hard failure on purpose:
$ sed -n '309,317p' packages/web/lib/flows/launch-flow-deployment.ts
      //
      // Every OTHER refusal stays a failure. A fork in particular is not an
      // answer: the installation grant cannot push to another repository and
      // an operator has to resolve that, so it must stay visible.
      if (error.code === "flow_pull_request_closed") {
        return { ok: true, status: 200, skipped: "pull_request_closed" };
      }
      return { ok: false, status: error.status, error: error.message };
    }

$ grep -n 'FLOW_TERMINAL_LAUNCH_STATUSES: ' packages/web/lib/proactive-runtime/integration-watch-deliveries.ts
1396:const FLOW_TERMINAL_LAUNCH_STATUSES: ReadonlySet<number> = new Set([403, 404, 409]);

So, on the hosted path, for a pull request from a fork:
  - ten of the eleven subscriptions fail TERMINALLY with 409
    flow_pull_request_fork, which Cloud intends to stay visible;
  - check_run.completed alone dies earlier and SILENTLY, skipped as
    not_a_pull_request because check_run.pull_requests is empty.
Fixing the attribution would convert that silent skip into the same
409. It would not enable fork CI. README corrected accordingly.
