Skip to content

feat(mcp): resume_run completes a paused screen flow — the caller's own run, behind run_action's gates - #19985

Merged
hotlong merged 4 commits into
mainfrom
claude/issue-15705-mcp-resume-run
Sep 24, 2026
Merged

hotlong merged 4 commits into
mainfrom
claude/issue-15705-mcp-resume-run

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #15705
Clause-②: yes

What this adds

run_action on a flow action whose flow stops on a screen node answers status: "paused" with a runId and the screen to fill in. Until now nothing on the MCP surface could submit that screen, so the run stayed parked: an agent could start such an action but never finish it. This PR adds the MCP tool resume_run({ runId, values?, confirm? }). It submits the screen's field values and the run continues.

This is item ① of the maintainer's ruling recorded on the card (comment 5793364219, 「15705同意」). Item ② (the explicit caller-provenance signal) already landed separately and is not touched here. The two expectations #15787 delivered (the headless screen verdict, and list_actions publishing flow inputs) are not touched either. With this PR the card's last open expectation is delivered, so it closes the card.

  • Tool (packages/mcp/src/mcp-http-tools.ts): resume_run is registered in registerActionTools, directly after run_action, when the bridge implements the new OPTIONAL McpActionBridge.resumeRun. It sits in the same actions:execute OAuth family. Both transports register tools through the one wireBridgeTools composition, so it exists wherever run_action does. Its input schema is closed (strictToolInput, the existing unknown-key convention): runId (required), values (a record) and confirm (the contract's confirmation member). The REST door's inputs / variables, and params / data / fields, are named as aliases in the refusal and never accepted. run_action's description names resume_run only where it is registered.
  • Bridge (packages/runtime/src/domains/mcp.ts): buildMcpBridge implements resumeRun through the new resumeActionRun.
  • Shared answer table (packages/runtime/src/domains/automation.ts): the REST resume arm's inline mapping of the engine result (six coded refusals plus 400 FLOW_FAILED with its details) moved unchanged into the exported classifyResumeResult. The REST arm and the MCP door both call it, and both build their error through the same deps.error. The REST arm's answers are byte-identical: the existing REST resume pins (http-dispatcher.test.ts, automation-resume-*.test.ts) pass unchanged.
  • Docs: content/docs/ai/actions-as-tools.mdx (table row + a "Completing a paused screen flow" section), content/docs/ai/connect-mcp.mdx (tool count and table), the generated SKILL.md (skill-md.ts) and packages/mcp/README.md.

Measured before building (the order's mechanism assumptions)

  1. No resume verb in packages/mcp/src: holds. At 2c1011b0 the only resume hits were the stdin-resume prose in mcp-server-runtime.ts.
  2. "Reuse the REST door's checks": measured, and they do not cover what the ruling asks for. POST /automation/:name/runs/:runId/resume checks (i) the anonymous floor, (ii) a closed body and value shapes, and (iii) the engine's resumeAuthority gate on the suspended NODE (a screen declares 'any'; "an unclaimed pause is fail-closed" is about node types that declare no authority, not about callers). It never asks who is calling. It does not check run ownership, the action's AI exposure or permissions, or whether the caller can read the record. The engine also resumes under the identity stored in the run (resumeInternal restores run.context), so the data nodes of a runAs: 'user' flow run as the user who STARTED the run. So the MCP verb reuses the door's engine-answer table (one table, identical codes), and applies the admission run_action applies, plus an ownership check. Details below.
  3. MCP run_action on a flow action answers ok: true and starts the run on a row the caller cannot read (or that does not exist) — the flow door still turns a denied load into an implicit grant #16370's precedent: loadActionSubjectRecord + refuseDeniedSubjectLoad (action-execution.ts). The resume door calls the same two functions on the run's subject record, so a record the caller can no longer read is refused with run_action's exact envelope.
  4. Is the flow name recoverable? Yes, and it is not needed. getRun(runId).flowName recovers it, and the REST resume arm never reads :name (it hands only parts[2], the run id, to resume). So the verb takes only runId, which is what the paused envelope carries.
  5. Where run_action is registered: one composition, wireBridgeTools, called by handleHttpRequest (HTTP) and bridgeDataTools (the long-lived stdio server). resume_run is registered in the same function, so both transports have it. (The production stdio host, createStdioDataBridge, carries no action seam, so neither run_action nor resume_run is served there today. That is unchanged.)

Which paused runs become completable over MCP, and no others

resume_run continues a run only when ALL of these hold. Each check is named with the refusal it answers:

# Check Refusal
1 The automation service implements resume, getRun and getSuspendedScreen (without getRun ownership cannot be checked, so this fails closed) 501 NOT_IMPLEMENTED
2 The run is the caller's own and still paused: getRun(runId).trigger.userId equals the caller's userId 404 RESOURCE_NOT_FOUND, the same code, status and message for an unknown id, a finished run and another user's run (existence non-disclosure, as #16370 does for records)
3 A type: 'flow' action whose target is the run's flow, on the run's object (the object-less key when the run carries no object), admits the call through run_action's own gates in run_action's order, with the same helpers: system-object guard, ai.exposed, requiredPermissions, ADR-0126 activation, ai.requiresConfirmation 403 PERMISSION_DENIED (system object, not AI-exposed, missing capability, no flow action targets the flow); 409 ACTION_DISABLED; 428 ACTION_CONFIRMATION_REQUIRED with run_action's details
4 The subject record is read again in the caller's scope (#16370's two functions) 404 RECORD_NOT_FOUND
5 The run is parked on a screen (getSuspendedScreen returns one); a run waiting on a timer wait, an approval or anything else is left alone 409 RESOURCE_CONFLICT
6 The engine accepts the submission; the signal is built one field at a time ({ variables: values }), never spread (#3801) the shared table: 403 / 400 (INVALID_SIGNAL, INVALID_SCREEN_INPUT) / 404 / 503 / 409, and 400 FLOW_FAILED with errorMessage / summary / runId / repairable / status, all identical to the REST door

Every refusal in 1–5 is thrown before resume() is called, so a refused call consumes nothing and the run stays parked. The confirmation gate applies on resume because the flow's writes happen after the screen: a run started with confirm: true needs confirm: true again to be resumed. With several candidate actions, the first one that passes admits the call. That is exactly the set of calls run_action could admit for this flow. When none passes, the first candidate's refusal is served.

The success value is run_action's envelope, { ok, action, objectName, recordId?, result }. result is the engine's own answer: a run that pauses on its next screen comes back paused with the next screen, so a multi-screen wizard is walked one resume_run per screen.

Not claimed. This does not make every paused run completable over MCP. Runs waiting on non-screen nodes, runs another user started, and runs of flows no AI-exposed action targets stay refused. A run whose own suspension carries no screen (for example a parent parked on a subflow node where the screen is only on the child) is refused 409 unless getSuspendedScreen answers for it. The REST resume door's behaviour is unchanged.

Pins — each one shown able to fail

packages/runtime/src/mcp-resume-run.test.ts (21 tests) drives the real buildMcpBridge through HttpDispatcher against a stateful automation double. The double keeps paused runs with their trigger identity and screens, checks required screen fields, and records the task the flow would create. The engine's own resume rules stay pinned on the real engine in @objectstack/service-automation.

  • (a) run_action without the inputs pauses. resumeRun with the values completes the run, resume receives exactly { variables: values }, and the task lands once, for that lead, as that caller. A two-screen wizard pauses again and a second call finishes it. A missing required field is the engine's 400 and the run stays resumable.
  • (b) another user's run (with record access, so only ownership is under test), a record reassigned away from the caller, a missing capability, a run whose only action is not AI-exposed, a run no flow action targets, a disabled action, a confirmation-gated action without confirm, and a non-screen pause. Each is asserted as code + status, resume is never called, and the run is shown to be still parked afterwards.
  • (c) an unknown runId gives the identical envelope to another user's run and to a finished run (only the id differs). A service without getRun gives 501.
  • (d) door against door: for all six coded engine refusals and both FLOW_FAILED shapes (stranded and plain), the MCP refusal's { code, status, message, details } equals the REST door's for the same engine result.

packages/mcp/src/mcp-resume-run-tool.test.ts (8), mcp-stdio-tools.test.ts (+1, and a control) and mcp-http-tools.unknown-argument-keys.test.ts (the sweep now covers twelve tools, plus an inputs → values case) pin the tool: registered only with resumeRun and under actions:execute, a closed schema in tools/list on HTTP and on stdio, exact forwarding of { values, confirm }, and a coded refusal that keeps its envelope. The SKILL.md drift guard's full bridge now carries resumeRun, so the skill must document the tool.

Mutation legs. Each leg was run from committed source with scripts/ablation-replace.mjs in WRAP mode: the anchor must hit once, the write is verified on disk, and the restore is proven by blob hash plus an empty git diff HEAD, with a trap on EXIT/INT/TERM. The mutated subjects are reached through relative source imports, so no dist/ rebuild leg is owed.

Leg Mutation Result
M1 drop the ownership condition 2 red: another user's run; unknown-vs-foreign envelope equality
M2 drop the record re-read refusal 1 red: the reassigned record
M3 drop the permission gate 1 red: the missing capability
M4 drop the exposure gate 1 red: the console-only action
M5 disable the confirmation gate 1 red: the confirmation case
M6 disable the screen-only check 1 red: the non-screen pause
M7 MCP bypasses the shared table's status 9 red: all 8 door-against-door rows and the engine-400 control
M8 match candidates by any flow 5 red: capability, exposure, no-action, disabled, confirmation
M9 never register the tool 9 red (the tool file and the stdio listing)
M10b drop confirm forwarding in the tool 1 red: exact forwarding

The first try at M10 did nothing: the tool refused the write because its replacement text was already present in the file, so no test ran. M10b redid it with a unique anchor.

Verification (head 0b6b2d78)

  • @objectstack/runtime local project: 274 files, 3837 passed, 1 skipped. Repo project: 2 files, 69 passed.
  • @objectstack/mcp: 32 files, 344 passed.
  • @objectstack/dogfood (it drives MCP over HTTP, showcase-mcp-http-identity): 137 files passed, 1 skipped; 1103 tests passed, 3 skipped, after building its dependency closure.
  • pnpm --filter @objectstack/runtime run typecheck and the same for @objectstack/mcp: exit 0, including check:test-typecheck, which compiles the new test files.
  • Gates re-derived on the real diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (15 paths vs merge base 2c1011b01): 89 derived, 89 run, all exit 0. Reconciled with --ran: "89 run, 0 NOT-MEASURED (a DERIVED zero)". Three gates first answered exit 3 = PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt, all of which read built output). After a full turbo run build of ./packages/*, all three measured green. Also run beyond the derivation, all exit 0: the five roster gates whose rosters sit in these paths (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census) and check:published-readme-exports.
  • Lint: pnpm lint (eslint . --no-inline-config) exit 0 over the whole repo at 0b6b2d78, not narrowed. A narrowed run came first (the 11 changed TS files, --format json: 0 errors, 0 warnings) and the full run supersedes it.

Acceptance notes

  • run_action answers its exposure and permission refusals as plain text tool errors. resume_run answers the same reasons coded (PERMISSION_DENIED / 403, the code and status REST /actions uses for the permission gate), because a new door should be machine-readable. The two tools therefore spell one reason two ways. Aligning run_action is outside this card.
  • packages/mcp/README.md and the runtime bridge's test file sit outside the claim's declared file surface (packages/mcp/src/**, domains/mcp.ts, domains/automation.ts, the MCP docs pages, the changeset). The README is the published package's own list of its tools and scopes, and the test file pins the bridge. Both are named here and in the report.
  • The SKILL.md surface guard's "full bridge" still has no aggregate, so aggregate_records is outside what the guard sees and the SKILL.md does not list it. This predates this PR.

Generated by Claude Code

…ehind run_action's gates

WIP: runtime bridge + shared resume refusal table + MCP tool registration.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/mcp, @objectstack/runtime, touching 25 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/mcp/README.md, packages/mcp/src/skill-md.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/mcp/README.md, packages/mcp/src/skill-md.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 33 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 2c1011b01bc071c545f72f2761647b8d9ab56375 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48cb002c712df0220ebde24346a742b154c718d9 — the merge of head 0b6b2d782f7465679ddf8e634c895a1b74c2ec20 into base 2c1011b01bc071c545f72f2761647b8d9ab56375, 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 48cb002c712df0220ebde24346a742b154c718d9 && git checkout 48cb002c712df0220ebde24346a742b154c718d9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1011b01bc071c545f72f2761647b8d9ab56375 0b6b2d782f7465679ddf8e634c895a1b74c2ec20 && git checkout -B drift-repro 2c1011b01bc071c545f72f2761647b8d9ab56375 && git merge --no-ff 0b6b2d782f7465679ddf8e634c895a1b74c2ec20

node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0b6b2d782f7465679ddf8e634c895a1b74c2ec20

① Derived judgments

  1. New tool resume_run on the published MCP surface (packages/mcp/src/mcp-http-tools.ts:1180-1261) → right. Registered inside registerActionTools, after the grantedScopes early-return at :1031 so it is in the actions:execute family; only when typeof bridge.resumeRun === 'function' (:1036, :1180). Both transports reach it through the one wireBridgeTools composition (pinned on HTTP in mcp-resume-run-tool.test.ts:66-89, on stdio in mcp-stdio-tools.test.ts:258-289, with a no-resumeRun control at :254).
  2. Input schema closed; aliases refused, never accepted (:1203-1240) → right. strictToolInput with additionalProperties:false, required:['runId'], properties exactly runId / values (z.record) / confirm (keyed off AI_ACTION_CONFIRMATION_MEMBER = 'confirm', packages/spec/src/contracts/ai-service.ts:237). inputs/variables/params/data/fields/answers/id/run_id/run are alias hints in the refusal only; mcp-http-tools.unknown-argument-keys.test.ts:189-204 proves inputs is refused and the bridge is not reached; the sweep now counts 12 tools (:216-221). Forwarding is exactly { values, confirm } (:1248-1252, pinned :108-120).
  3. McpActionBridge.resumeRun? — optional published type (:244-262, re-exported via packages/mcp/src/index.ts:22) → right, additive: an existing bridge literal still type-checks; a bridge without it lists no resume_run.
  4. classifyResumeResult / ResumeRefusal exported from packages/runtime/src/domains/automation.ts:1514-1620 → right, and NOT public API: packages/runtime/src/index.ts re-exports nothing from ./domains/ (only export * from '@objectstack/core' at :247) and tsup builds src/index.ts alone (tsup.config.ts:8). Internal cross-module export only.
  5. REST resume arm byte-identity (automation.ts:2392-2404 vs removed :2286-2369) → right. The six code → (status, fallback) rows and the FLOW_FAILED 400 arm (code, conditional errorMessage/summary, ...verdict from the unchanged resumeFailureDetails(deps, svc, parts[2], result)) are reproduced one-for-one; a non-string or unknown code falls to FLOW_FAILED in both versions; deps.error(message, status, details) and deps.success(result) are called with the same arguments. The Map lookup vs sequential === chain is semantically identical. The body validation, anonymous floor and signal assembly above (:2330-2391) are untouched. Door-against-door pins (mcp-resume-run.test.ts:452-486) cover all six rows plus both FLOW_FAILED shapes.
  6. Authorization: ownership + same gates as run_action, same order (packages/runtime/src/domains/mcp.ts:835-936) → right. Order is: service capability (501) → getRun(runId).trigger.userId === ec.userId and status === 'paused' (404) → per-candidate isSystemObjectName → actionAiExposureError → actionPermissionError → disabledActionRefusal → actionConfirmationRefusal (:954-979) → loadActionSubjectRecord + refuseDeniedSubjectLoad (:906-909) → screen-only (409) → resume. This is invokeBusinessAction's order (action-execution.ts:2130-2236) minus the name-resolve, the param contract (n/a on resume) and isHeadlessInvokableAction (trivially true for type:'flow' + target + automation present, :589). All helpers are the same exported functions. The candidate filter (:882-887) matches run_action's own key space: type:'flow', not declarative-update, target === run.flowName, object equals trigger.object (set by dispatchFlowAction at action-execution.ts:958 and stored by buildRunTrigger, engine.ts:1142-1158). "First passing candidate admits" cannot widen the set beyond what run_action admits for that flow. The trigger's userId is ec?.userId at start (:959) and the same ec at resume, so agent/onBehalfOf principals compare consistently. Every refusal is thrown before resume(); pinned in (b) with resume never called and the run still parked (:325-418). Mutation legs M1-M8 reported red per gate.
  7. Existence non-disclosure (mcp.ts:791-796, :863-866) → right. Unknown id, finished run, foreign run: one code (RESOURCE_NOT_FOUND, derived from 404), one message template, evaluated at the same point before any metadata read; pinned by envelope equality (:420-439). The only messages that name the flow (403 no-candidate, 409 non-screen) are reachable solely for the caller's own paused run.
  8. Identity on resume → right/safe. The engine continues under run.context (engine.ts:6783), i.e. the starter; the ownership gate makes starter == caller, and requiredPermissions/activation are re-evaluated against the caller's current ec, so revocation since the start is honoured at the door. Ownership compares userId only (no tenant), which is exactly the precedent refuseUnrelatedScreenRead uses (automation.ts:1081-1085); the automation service is resolved per envId. Consistent; not a bypass.
  9. Fail-closed without getRun/getSuspendedScreen/resume (:849-861) → right, 501 NOT_IMPLEMENTED; pinned (:441-449).
  10. values validation → right. Tool: z.record(string, unknown); bridge: re-refuses null/array/non-object (:844-846); field-level contract (missing required, undeclared key) is the engine's INVALID_SCREEN_INPUT (engine.ts:7294-7300, screen-input-contract.ts:207) → 400 via the shared table, run stays parked (pinned :312-322). Signal built field-by-field, never spread (:929-930, pinned :294).
  11. Docs/README/SKILL.md/changeset claims → all true of the code: actions-as-tools.mdx gate bullets and "steps 2-4 again" map to items 6-7; "undeclared field → 400" holds (item 10); "same code and status as the REST route" holds (item 5); connect-mcp.mdx "Twelve tools" matches the full-bridge listing (pinned 12); README.md scope sentence matches the early return at :1031; skill-md.ts:171-177 lists resume_run unconditionally exactly as it already does run_action, and the surface guard's full bridge now carries resumeRun (skill-md-surface-guard.test.ts:59-63); run_action's description names resume_run only when registered (:1071-1078, pinned :85-88).
  12. Refusal codes → right. New-door refusals are coded (PERMISSION_DENIED/403, ACTION_DISABLED/409, ACTION_CONFIRMATION_REQUIRED/428 with run_action's details, RECORD_NOT_FOUND/404, RESOURCE_CONFLICT/409, NOT_IMPLEMENTED/501), built through the same deps.error builder as REST (resumeDoorError, :768-781) and carried intact by errorResultFromThrown (:308-329). The run_action text-vs-coded inconsistency is disclosed and out of card.

② Semver level

.changeset/15705-mcp-resume-run.md: @objectstack/mcp: minor, @objectstack/runtime: minor → right. mcp gains a new tool and an optional interface member (additive public surface). runtime gains an optional bridge member on buildMcpBridge; classifyResumeResult is not on the package's export path (item 4). No published REST or MCP behaviour changes for existing callers. Prose claims (own-run only, same gates, 403/404/409/428 mapping, both transports, REST byte-identity, optional member) all verified above. Not major, correctly.

③ Boundary flags

Implemented-by: claude/issue-15705-mcp-resume-run
Reviewed-by: session_01EcrTi7s5oDYPHS4Pi7h31d

VERDICT: PASS

Isolated at-tier reviewer adopted by the director seat, summon #29, on the maintainer's instruction 「帮我处理并跟进到合并」 · fed only card #15705, ruling 5793364219, the PR body, the dev's flags and the code · CI on this head: 35 runs, 33 success / 2 skipped


Generated by Claude Code

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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP run_action on a screen flow dead-ends: the screen pauses even when every input is bound, and list_actions never surfaces flow input variables

2 participants