Skip to content

feat(init): speak the Vercel Workflows protocol#1160

Draft
MathurAditya724 wants to merge 5 commits into
mainfrom
feat/vercel-workflows-protocol
Draft

feat(init): speak the Vercel Workflows protocol#1160
MathurAditya724 wants to merge 5 commits into
mainfrom
feat/vercel-workflows-protocol

Conversation

@MathurAditya724

Copy link
Copy Markdown
Member

Updates the init CLI to talk to the migrated init server, which moved from the Mastra workflow engine to Vercel Workflows. That migration (getsentry/cli-init-api#183) replaced the Mastra client contract (/api/workflows/sentry-wizard/{create-run,start-async,resume-async}) with a new one: POST /api/wizard, GET /api/run, POST /api/resume. Pointing the CLI at the new server currently 404s because those endpoints don't match.

Changes

  • Add src/lib/init/workflow-client.ts — a shim presenting the same createRun/startAsync/resumeAsync/runById surface the wizard runner already uses, while speaking the new protocol underneath. It maps the server's { status, seq, request } into the existing WorkflowRunResult shape and polls GET /api/run between suspends.
  • Resume targets the deterministic hook token wizard:<runId>:<seq>, reconstructed from the runId (returned by /api/wizard) and the seq reported by /api/run — the token is never received from the server, so no capability leaks.
  • Replace MastraClient in wizard-runner.ts with the shim; drop the now-unused @mastra/client-js dependency.
  • Update wizard-runner.test.ts to mock createWorkflowClient instead of MastraClient.prototype.getWorkflow.

Testing

vitest run test/lib/init — 418 passing. Typecheck + biome clean on changed files.

Notes / follow-ups

MathurAditya724 and others added 2 commits July 1, 2026 06:07
The init server migrated from the Mastra workflow engine to Vercel
Workflows, which exposes a different HTTP contract (/api/wizard,
/api/run, /api/resume with deterministic hook tokens) instead of the
Mastra client's create-run/start-async/resume-async.

Add a workflow-client shim (src/lib/init/workflow-client.ts) that
presents the same createRun/startAsync/resumeAsync/runById surface the
wizard runner already uses, mapping the server's { status, seq, request }
shape onto WorkflowRunResult and polling GET /api/run between suspends.
Resume targets the hook token wizard:<runId>:<seq> reconstructed from the
runId + seq (never received from the server).

Replace MastraClient in wizard-runner with the shim; drop the now-unused
@mastra/client-js dependency. Update the runner tests to mock
createWorkflowClient instead of MastraClient.prototype.getWorkflow.

Tests: init suite green (418 pass).
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-1160/

Built to branch gh-pages at 2026-07-01 07:10 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Codecov Results 📊

❌ Patch coverage is 17.14%. Project has 5198 uncovered lines.
❌ Project coverage is 81.31%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
src/lib/init/workflow-client.ts 7.53% ⚠️ 86 Missing
src/lib/init/wizard-runner.ts 80.00% ⚠️ 1 Missing and 1 partials
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    81.48%    81.31%    -0.17%
==========================================
  Files          397       398        +1
  Lines        27712     27814      +102
  Branches     18013     18060       +47
==========================================
+ Hits         22579     22616       +37
- Misses        5133      5198       +65
- Partials      1860      1867        +7

Generated by Codecov Action

Comment thread src/lib/init/workflow-client.ts
runById creates its own isolated seqRef, so after tryRecoverCurrentRunState
recovers via workflow.runById(), the run's seqRef stays stale. The next
resumeAsync then builds a token targeting the wrong suspend hook.

Fix: carry _seq on WorkflowRunResult, expose updateSeq() on WorkflowRun,
and call syncSeqFromRecovery() in both recovery paths of resumeWithRecovery.
@MathurAditya724

Copy link
Copy Markdown
Member Author

good catch — runById was indeed writing the recovered seq into its own local seqRef that got discarded, so the next resumeAsync would build a stale token. fixed in b306e85: toWorkflowRunResult now carries _seq on the result, WorkflowRun exposes updateSeq(), and both recovery paths in resumeWithRecovery call syncSeqFromRecovery() before returning.

Comment thread src/lib/init/types.ts
* state through a separate `seqRef`. Without this, `resumeAsync` would build a
* stale token after recovery.
*/
_seq?: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_seq: -1 sentinel overwrites a valid seqRef after runById recovery when server omits seq

In toWorkflowRunResult, the suspended branch emits _seq: seqRef.current unconditionally. During runById recovery, pollUntilSettled is invoked with a fresh seqRef = { current: -1 }. If the server returns a suspended state with a request but no seq field (seq?: number is optional in RunStatePayload), the guard if (typeof state.seq === 'number') is skipped, so seqRef.current stays -1 and _seq: -1 is emitted. syncSeqFromRecovery in wizard-runner.ts only checks typeof recovered._seq === 'number' — which -1 satisfies — and calls run.updateSeq(-1), overwriting the run's previously valid seq. The next resumeAsync then builds an invalid token wizard:<runId>:-1. Fix by guarding the emission (_seq: seqRef.current >= 0 ? seqRef.current : undefined) or the update (if (typeof recovered._seq === 'number' && recovered._seq >= 0)).

Evidence
  • runById (workflow-client.ts) creates a fresh seqRef = { current: -1 } and calls pollUntilSettled(runId, seqRef).
  • In toWorkflowRunResult, the suspended branch only sets seqRef.current = state.seq when typeof state.seq === 'number', but emits _seq: seqRef.current unconditionally — so a missing seq leaves _seq: -1.
  • RunStatePayload.seq is typed seq?: number, so an absent seq on a suspended response is within contract.
  • syncSeqFromRecovery (wizard-runner.ts line 734) accepts any number including -1 and calls run.updateSeq(-1), replacing a valid seq.
  • resumeAsync then builds wizard:${runId}:${seqRef.current} = wizard:<runId>:-1, an invalid resume token.
Also found at 2 additional locations
  • src/lib/init/wizard-runner.ts:730-737
  • src/lib/init/workflow-client.ts:106-110

Identified by Warden find-bugs · GZZ-3R9

Surface what the wizard is doing and why it fails under the existing
--verbose / --log-level=debug flags (no new flag needed). The
workflow-client shim now logs, via a `wizard-client` tag:
  - the target base URL at construction (reveals wrong/stale server),
  - each POST/GET request, response status, and error body,
  - run id, resume token, and poll settle transitions.

Also improve the readiness health check to log the URL + non-ok status
(previously only network errors were logged), so a 404 from a
misconfigured MASTRA_API_URL is visible.

Tests: init suite green (418 pass).
When the init server errors (e.g. a crashed dev server returning a Bun
HTML error overlay), the workflow client was surfacing the entire raw
body — hundreds of KB of markup — in the CLI error. Extract the
meaningful bit instead: the `error` field from JSON envelopes, or the
first "<Something>Error: message" line from an HTML page, capped at 500
chars. Falls back to a short hint for opaque HTML.

Tests: init suite green (418 pass).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant