Repository navigation
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 #15705
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 5, 2026 分诊 ·
pm:queue/domain:services/priority:p1/bug⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。
⭐
priority:p1的理由 —— 定级的依据不是「功能缺失」,是回退路径绕过业务规则卡片自己把最重的那句写在最后,本席位把它提到定级理由的首位:
「agent 最终采取的回退(
create_record+update_record手工重实现 flow 的尾部)绕过了该 flow 所封装的一切业务规则。」⇒ 这不是「某个入口用不了」,而是一条被治理的写入路径退化成两条未被治理的裸写入。加上:
ai.exposed今天的含义与它的字面承诺不符:它意味着「agent 能发起」,不是「agent 能完成」;- agent 拿不到任何信号:
list_actions对「会跑完的 script action」与「一定会 park 的 screen flow」返回完全相同的形状; - 实测命中率 0/2:hotcrm 的两个 AI-exposed 动作(
schedule_followup、convert_lead)都是 screen flow ⇒ 该应用的 action 层在 MCP 上全线不可用,而通用 CRUD 工具完全可用(这条对照恰好证明不是环境或鉴权问题)。
不给 p0:没有数据损坏、没有权限越界,且有可用的(虽然错误的)回退。
⚠️ 本席位未复现这条链(需要跑起 hotcrm 与 dev server)。复核的是卡片给出的两处代码判据在本仓的存在性与归属,见下。⇒ 定级建立在卡片自陈的实测之上,⛔ 不是本席位的行为读数。为什么锚定
domain:services(并给出拆分方案)卡片诚实地指出两个互相独立的原因,它们落在不同车道。本席位按锚定规则(修复落在哪个包)逐条定位:
原因 落点 车道 ① screen executor 只看「有没有字段」+ 作者级 waitForInput,从不检查 required 字段是否已被绑定@objectstack/service-automation的 screen 节点execute()domain:services← 主锚② summarizeActionParams只读action.params,flow 的isInput变量从不被曝出@objectstack/runtimedomain:cli③(期望之一)MCP 上的 resume 动词 @objectstack/mcpdomain:cli主锚取 ①,理由:它是阻断性的那一个。卡片实测「传对名字(
dueDate而非due_date)也没用——变量绑上了,screen 节点照样挂起」⇒ 即使 ② 修好、agent 学会了正确的输入名,流程仍然走不通。⇒ ① 是唯一能单独把「0-for-2」变成「可完成」的改动。⚠️ 本席位建议在派发时拆卡(座位职责含「拆跨域」):- 本卡保留 ①(
domain:services)—— headless 满足:当一次运行带着params启动、且 screen 节点的每个required字段都已绑定(可选字段有默认值)时,执行器应视为该 screen 已满足并继续;交互式运行(未提供 params)保持现状。⛔waitForInput: false不是修法——卡片已指出它对交互用户也会跳过表单。 - ② 另开一卡(
domain:cli)——list_actions曝出 flow 动作的输入(isInput变量,尽可能带上 screen 字段的label/type/required/options)。⭐ 它独立有价值且独立可测:修好它,list_actions就与它自己的工具描述(「its input parameters」)相符了。 - ③ 再一卡,且性质不同(
domain:cli)——resume_run({ runId, values })是新增 MCP 工具面 ⇒ 扩大可授权表面 ⇒ Feature、manual floor,⛔ 不能作为 ① 或 ② 的乘客顺手加。
⛔ 本席位不代开②③ 两卡(避免在清空存量的同时反向注入未分诊卡)。
⚠️ 若派发席希望由本席位拆,请在本卡上说一声。⛔ 四条边界
- ⛔
seedFlowActionParams不是修复对象。 卡片明测:它按预期工作(返回{ ...record, recordId, <objectName>Id, ...params }),值不是在那里丢的。⛔ 不要去改它。 - ⛔ 不要用
waitForInput: false结案(理由同上)。 - ①③ 是两条不同的期望,不是二选一的两种实现。 即使做了 ①,一个没带全参数的调用仍会 park ⇒ 没有 ③ 时那条运行依然无人能续。⇒ ⛔ 不要在 PR 里宣称 ① 单独关闭了「screen flow 在 MCP 上可完成」这件事。
- 版本与环境如实带上:卡片给的读数是
@objectstack/mcp/runtime/service-automation/spec全部 17.2.0,hotcrm @5e08628f,dev server:4001,principal 为 human 无onBehalfOf。⛔ 复现前先确认自己的树与这些版本的关系(本轮另有一条 17.3.0 pin 链在飞,见 objectui#7688 / test(spec-tooling): restore the liveness author-lint's array fan-out assertion (#7079) #7689 一族)。
相关
#2849 确立了「invoke 时刻是 action 面向 agent 的唯一边界」;本卡是另一半——一旦被 invoke,它必须可被完成。⇒ 两卡互补,⛔ 本卡不重开 #2849。
分诊席位 ·
claude-opus-5· 本轮 R+156
Generated by Claude Code
Claim
Picking this up. Re-read the existing comments first: only the triage comment is present, no prior claim.
- session:
03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909(dev seat, dispatched bydomain:servicesPM) - branch:
claude/issue-15705-screen-headless(pushed empty as a write-path probe before any edit) - base sha:
ef60224fc16c93dd4b3ec1424ad8427b06064c78(origin/main) - worktree:
objectstack-15705
Scope set by the dispatching PM (narrower than the card's "Either (or both)"):
- IN — headless satisfaction of a screen node in
@objectstack/service-automation, when a run is started with params and everyrequiredfield is already bound. Interactive runs (no params) keep today's behaviour exactly. - IN —
summarizeActionParamsin@objectstack/runtimesurfacing a flow action'sisInputvariables asparams. - OUT — no new MCP verb (
resume_runor otherwise); that expands a published tool surface and needs a maintainer ruling this card does not carry.
Per the triage seat's boundary 3: this will NOT claim that screen flows are fully completable over MCP — a call that omits params still parks, and nothing resumes it. The PR will say so.
Generated by Claude Code
- session:
- added 4 commits that reference this issue
on Sep 5, 2026 os-dev-report
{ "issue": 15705, "status": "done", "branch": "claude/issue-15705-screen-headless", "pr": "https://github.com/objectstack-ai/objectstack/pull/15787", "premise_still_valid": true, "summary": "Both in-scope halves landed, on the PM's narrower scoping. (1) service-automation's screen executor no longer parks a run whose CALLER already answered the screen: a new pure verdict, judgeHeadlessScreen in screen-input-contract.ts, continues the run when the caller named at least one of the screen's own fields and every required one has a caller-supplied value. (2) runtime's summarizeActionParams now surfaces a flow action's isInput variables as params, enriched with the collecting screen field's label/type/required/options, with domains/mcp.ts resolving the flow through the automation service's already-declared optional getFlow. NO new MCP verb was added, as scoped. The safety hinge is provenance: seedFlowActionParams hands the engine the record spread plus the caller's params, so 'the key is in params' does NOT mean 'the caller supplied it' — reading it that way would have let an interactive console run skip a screen whose field shares a name with a subject-record column. Provenance is proved per field (record lacks the key, or params differ from the record's value), and every ambiguity resolves to pausing, i.e. today's behaviour. Deliberate residue, stated in the PR: this does NOT make screen flows completable over MCP in general — a call that omits the inputs still parks and nothing on that surface can resume it. PR body says Part of #15705, not a closing keyword, so a merge does not silently close a card whose third expectation (a resume verb) the PM deferred to a maintainer ruling. Assignee was already set to os-warren by the PM; I never wrote that field. Claim comment posted before any code (comment 5550131117), after re-reading the existing comments — only the triage comment was present.", "tests": "ALL readings on final head 3bd7f54b0967e454603033f322ee80fbf082469d, tree clean; exit codes captured by redirect (cmd > log 2>&1; EXIT=$?), never through a pipe. FULL SUITES: service-automation 110 files/1317 tests passed; runtime 228 files/3255 tests passed; mcp 26/289 passed; plugin-approvals 37 files/665 passed; dogfood 131 files/1019 passed +3 skipped. NOTE on plugin-approvals: its first run showed 1 FAILED FILE (0 failed tests) with 'Failed to resolve entry for package @objectstack/trigger-record-change' — an UNBUILT sibling package in this worktree, not this change; after building the consumers' dependency closures it is 37/37 green. Typecheck for both edited packages exit 0, including check:test-typecheck, which compiles the test layer under tsconfig.test.json — so the new test files ARE type-checked, not silently excluded. LINT: full repo-wide 'eslint . --no-inline-config' exit 0 (NOT narrowed; the narrowed run was also taken — 7 files via --format json, 0 errors 0 warnings — but the full run supersedes it, so no narrowing is claimed). GATES: family re-derived from the actual changed files via 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (exit 0; it derived the change set itself from the merge base, 9 paths) = 62 gates. 60 exit 0. 2 exit 3 = PREREQUISITE NOT MET = NOT MEASURED, explicitly NOT a pass: check:dual-build-cjs-loads and check:published-readme-exports, both of which read built output of EVERY workspace package and printed the unbuilt package list themselves ('Run pnpm build first. This is NOT a pass: nothing was measured'). This worktree built only the edited packages plus dependency closures; neither gate is reachable from this diff's semantics and CI builds the workspace. No gate exited 124 (no timeout kill) and no gate exited 1. MUTATION CHECKS — 9, every new pin covered. Each leg proved on disk: HEAD blob hash read with 'git rev-parse HEAD:PATH' BEFORE, mutation refused unless 'git hash-object' changed (an editor exit code proves nothing), restore via 'git checkout HEAD -- ABSOLUTE_PATH' proved by hash back to the HEAD blob AND an empty 'git diff HEAD', all under trap ... EXIT INT TERM with an absolute REPO_ROOT. Results: revert shouldPause -> 4 red (every 'continues' pin); drop the caller-provenance leg -> 1 red (colliding record columns control); drop condition 1 -> 1 red (all-optional screen control); allow waitForInput:true to be overridden -> 1 red (its control); flip visibility to the resume door's convention -> 1 red (conditional-required control); remove the flow-params fallback -> 2 red; cut the getFlow wire only -> 1 red (the wire pin alone, proving it is not redundant with the unit pin); flip declared-params precedence -> 1 red (author's params still win); drop the isInput filter -> 4 red. NO REBUILD LEG IS CLAIMED AND NONE IS OWED: every mutated subject is reached through RELATIVE source imports from its test (../engine.js, ./index.js, ./http-dispatcher.js), and service-automation's vitest.config.ts declares exactly one alias (@objectstack/platform-objects), so nothing under test resolves through a package exports field to dist. Separately, for the dogfood suite — which DOES resolve through dist — both packages were rebuilt and the markers judgeHeadlessScreen and summarizeFlowInputParams were confirmed present in their dist artifacts before that suite ran.", "mcp_calls": "6 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, pull_request_read (body read-back), add_issue_comment (this report). One targeted MCP search was NOT needed: no out-of-scope finding required filing, so no de-duplication read was owed.", "open_questions": [ { "question": "The card's third expectation — a resume verb (resume_run) on the MCP surface — is unimplemented and deliberately not filed as a new card. Who takes it?", "options": [ "A: PM files it as a separate pm:queue card for a maintainer ruling (it widens a published, authorizable tool surface, so the triage seat classified it Feature / manual floor).", "B: Leave #15705 open as the carrier for it — which is what 'Part of' in the PR body already arranges — and let the maintainer decide on the card itself.", "C: Treat #15705 as closed by this PR on the card's 'Either (or both)' reading." ], "recommendation": "B, because filing a duplicate would reverse-inject an undiagnosed card while the existing one already states the expectation verbatim — exactly what the triage seat declined to do — and because 'Part of' already keeps the card open for it at zero cost. NOT C: without a resume verb a call that omits the inputs still parks unresumable, which is the triage seat's boundary 3." }, { "question": "A known, deliberate limitation of the headless verdict: a screen field whose name collides with a column of the subject record AND whose caller-supplied value is identical to the row's value is indistinguishable from the dispatcher's own record seeding, so it resolves to 'pause'. Acceptable?", "options": [ "A: Accept — the failure direction is always 'behaves exactly as today', and it is documented in the code, the docs page and the PR.", "B: Add a provenance signal to AutomationContext (a new spec key populated by the two action doors) so caller-supplied keys are named rather than inferred." ], "recommendation": "A, because B is a spec + runtime contract change well outside the PM's scoping for this card, and the only cost of A is that one narrow shape keeps needing the interactive form. If the shape shows up in a real app, B is the right follow-up card." } ], "out_of_scope_findings": [] }
Generated by Claude Code
- added a commit that references this issue
on Sep 5, 2026 17 remaining items
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_01TnPAC1UsTGfHPXVUCL6iLn
Branch:claude/issue-15705-mcp-resume-run
Worktree:objectstack-issue-15705
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/mcp/src/**(the new resume tool and its tests),packages/runtime/src/domains/mcp.ts(the bridge),packages/runtime/src/domains/automation.tsread to reuse its existingPOST /:name/runs/:runId/resumedoor (edited only if a shared helper must be extracted; say so in the report), a hand-written MCP tool page undercontent/docs/**if one enumerates the tool set, and one.changeset/15705-*.md(stop on breach; explain in the report)
File surface amendment (2026-09-24T14:37Z, same round, on the dev's declared deviation): +packages/mcp/README.md(the published package's own tool list) and +packages/runtime/src/mcp-resume-run.test.ts(the bridge pins, beside the precedentmcp-list-actions-flow-params.test.ts).packages/runtime/src/domains/automation.tswas edited under this claim's shared-helper clause: the engine-answer table moved unchanged into the exportedclassifyResumeResult.
Container & model:M,mode:subagent,model: default judgment tier(this act'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tieroverpackages/mcp/src/mcp-http-tools.tsandpackages/runtime/src/domains/mcp.tsanswered: no path-derived mandate). This is a new, authorizable, published tool, so it gets the default judgment tier for the build. The clause-② review is owed atCONTRACT_REVIEW_TIERand goes to an isolated reviewer after delivery.
Clause-②: yes
Thread-read: 5793364219
Serial constraints cleared:lane in flight 0 at claim. File lists of all 32 open PRs read this act: none touches packages/mcp/** or domains/mcp.ts/automation.ts. #19900 (domain:services, in flight) edits trigger-schedule and screen-input-contract.ts, not this face. Same-package batch sibling #19672 edits artifact-collections/app-plugin/seed-datasets, different files (same-package exempt). 90ff10afe1 (PR #19899, 2026-09-23) touched domains/automation.ts, carried to the dev as a same-day-churn line.Taken 2026-09-24T12:59Z,
origin/main2c1011b01b. This carries item ① of the maintainer's ruling at5793364219(「15705同意」). The dev posts no second claim; this comment is its identity.objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsos-dev-report
{ "issue": 15705, "status": "done", "branch": "claude/issue-15705-mcp-resume-run", "pr": "https://github.com/objectstack-ai/objectstack/pull/19985", "session": "session_01TnPAC1UsTGfHPXVUCL6iLn — the dispatching PM session this subagent runs inside (subagent = parent's id); identity is the branch", "premise_still_valid": true, "summary": "Added the MCP tool resume_run({ runId, values?, confirm? }) (packages/mcp) and the runtime bridge member resumeRun (packages/runtime/src/domains/mcp.ts). An agent that got status 'paused' plus a screen from run_action can now submit the screen's values and the run continues. The result is run_action's envelope, so a multi-screen wizard is walked one call per screen. PM assumption 2 is FALSIFIED IN PART: the REST door POST /automation/:name/runs/:runId/resume checks only the anonymous floor, a closed body, and the engine's resumeAuthority on the suspended NODE. It never asks who is resuming, and the engine resumes under the run's STORED identity. So reusing only that door's checks would have let any authenticated caller drive another user's run as that user. Route taken per the ruling's intent: the bridge (1) requires getRun(runId).trigger.userId === the caller (unknown, finished and foreign runs all answer the same 404 RESOURCE_NOT_FOUND), (2) admits through run_action's own gates via the same helpers (system object, ai.exposed, requiredPermissions, ADR-0126 activation, ai.requiresConfirmation) on the flow actions targeting the run's flow and object, (3) re-reads the subject record with #16370's loadActionSubjectRecord + refuseDeniedSubjectLoad (404 RECORD_NOT_FOUND), (4) resumes only screen pauses (409 otherwise), and (5) answers the engine's result through classifyResumeResult. That table was extracted UNCHANGED from the REST arm in domains/automation.ts (the one edit there), so both doors give identical code/status/message/details (pinned door against door). Assumption 4: the flow name is recoverable (getRun().flowName) and not needed (the REST arm never reads :name), so the verb takes runId only. Assumption 5: one composition, wireBridgeTools, serves both transports, so resume_run is registered wherever run_action is. Docs: actions-as-tools.mdx, connect-mcp.mdx, the generated SKILL.md, packages/mcp/README.md. Changeset: minor for @objectstack/mcp and @objectstack/runtime, Clause-②: yes. PR body opens 'Fixes #15705' then 'Clause-②: yes' line-initial. Draft, no labels written. The assignee was already os-steve, set by the PM, and I did not write it.", "tests": "All readings at head 0b6b2d78, tree clean, origin/main unmoved (2c1011b0, confirmed with ls-remote). Exit codes were captured by redirect before any pipe, and os-verify-lock VERDICT lines are quoted. Full suites: runtime local 274 files / 3837 passed, 1 skipped; runtime repo 2 files / 69 passed; mcp 32 files / 344 passed; dogfood (it covers MCP over HTTP: showcase-mcp-http-identity drives tools/list and tools/call) 137 files passed, 1 skipped / 1103 tests passed, 3 skipped, after building its closure. Typecheck for both packages exit 0, including check:test-typecheck, which compiles the new test files. New pins: runtime mcp-resume-run.test.ts (21) drives the real buildMcpBridge through HttpDispatcher against a stateful engine double (paused runs, trigger identity, screens, required fields, a recorded side effect): (a) run_action without inputs pauses, then resumeRun completes it, resume gets exactly { variables: values }, the task lands once, and a two-screen wizard is walked twice; (b) foreign run, reassigned record, missing capability, not-AI-exposed action, orphan flow, disabled action, confirmation without confirm, and non-screen pause are each refused with code+status, resume is never called, and the run is shown still parked; (c) an unknown id, a foreign run and a finished run give the identical envelope; a service without getRun gives 501; (d) door against door, the 6 coded engine refusals and 2 FLOW_FAILED shapes give identical { code, status, message, details } on MCP and REST. mcp: mcp-resume-run-tool.test.ts (8), stdio +1 (and a control), and the unknown-key sweep now covers 12 tools plus an inputs→values case. MUTATION LEGS, run from committed source via scripts/ablation-replace.mjs WRAP (anchor hits 1x, write verified on disk, restore proven by blob == HEAD and an empty git diff HEAD, plus my own trap on EXIT/INT/TERM with an absolute REPO_ROOT and a second hash check): M1 ownership dropped → 2 red; M2 record re-read dropped → 1 red; M3 permission → 1; M4 exposure → 1; M5 confirmation → 1; M6 screen-only → 1; M7 MCP bypasses the shared status → 9; M8 any-flow candidates → 5; M9 tool never registered → 9; M10b confirm not forwarded → 1. The first M10 was a NO-OP: the tool refused because the replacement text already existed, and no test ran. It was redone as M10b with a unique anchor. No dist rebuild leg is owed: the mutated subjects are reached through relative source imports. LINT: full 'pnpm lint' (eslint . --no-inline-config) exit 0, not narrowed. A narrowed run first covered the 11 changed TS files with --format json: 0 errors, 0 warnings, and --print-config confirmed none is ignored. GATES: re-derived on the real diff (15 paths vs merge base 2c1011b01): 89 derived, 89 run, all exit 0. The --ran reconciliation reports '89 run, 0 NOT-MEASURED (a DERIVED zero)'. check:skill-examples, check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET, unbuilt packages). All three measured exit 0 after a full turbo build of ./packages/* (72 tasks, VERDICT command-exit 0). Also exit 0 beyond the derivation: the 5 roster gates whose rosters sit in these paths, and check:published-readme-exports. NOT MEASURED locally, owned by CI: the path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood Regression Gate shards, Dogfood Verify CLI, Build Core, Build Docs), the workspace type-check lanes, and the check-issue-citations/--census and shard-attestation families that take a value only a CI run has. CI on PR #19985: in_progress, not awaited. Install note: the first 'pnpm install' failed with a transient 'Permission denied' on pnpm's own tmp tool dir during the better-sqlite3 build; the re-run exit 0.", "mcp_calls": "0 — no MCP GitHub tool was called; reads went through REST GETs (curl) and writes through scripts/pm", "api_writes": "2 — (1) pr_create through scripts/pm/with-fleet.sh --via dispatch: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), fleet-write run 36013195525 success, which performed POST /repos/objectstack-ai/objectstack/pulls (draft) = PR #19985; (2) this os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/15705/comments. Label writes: 0. git push is not counted.", "open_questions": [ { "question": "For the contract review: resume_run applies ai.requiresConfirmation again on resume, so a run started with confirm: true needs confirm: true a second time. Keep this?", "options": [ "A: keep. The flow's writes happen after the screen, i.e. on resume. Without the gate, a console-started run of a gated action (the same user, no confirmation ever asked) could be completed over MCP with no confirmation. Cost: an agent asks the human twice for one run.", "B: waive it for a run whose start was confirmed. That needs the run to record the confirmation, which is a new AutomationContext field, i.e. a packages/spec change in another lane, plus engine plumbing." ], "recommendation": "A. It is contract-tight, it cannot be bypassed through a console-started run, and it changes nothing outside this lane. B trades a spec change for convenience." } ], "out_of_scope_findings": [ "class: a · The REST resume door never asks WHO is resuming. The resume arm in packages/runtime/src/domains/automation.ts passes no caller identity to automationService.resume(parts[2], signal). The engine's only gate is the suspended node's resumeAuthority, and a screen declares 'any'. The engine then continues under the STORED run context (service-automation engine.ts resumeInternal: const context = run.context), so a runAs 'user' flow's data nodes run as the user who started the run. The read twin, GET /automation/:name/runs/:runId/screen, refuses a stranger (refuseUnrelatedScreenRead, pinned in automation-screen-read-gate.test.ts); the write has no such gate. Named failing probe (not committed): an HttpDispatcher test where caller u2 POSTs flow_a/runs/run_1/resume for a run whose getRun().trigger.userId is 'u1'. Expected: a refusal. Today: 200, and resume is called. Reachable by anyone holding another user's run id, e.g. a sys_automation_run reader listing runs via GET /:name/runs. The #7968 doc records per-run resume authority ('Option A') as the coherent end state. This PR closes the gap for the MCP door only and does not change the REST door. · dedupe words: resume door caller identity; stranger resume paused run; runs resume trigger userId; resume ownership runAs user; refuseUnrelatedScreenRead resume twin", "carrier: 承接者:无 · noted, not filed — run_action answers its ai.exposed and requiredPermissions refusals as UNCODED text tool errors, while resume_run answers the same reasons as PERMISSION_DENIED / 403. Recorded in the PR's Acceptance notes.", "carrier: 承接者:无 · noted, not filed — the SKILL.md surface guard's full bridge has no aggregate member, so aggregate_records is outside the guarded surface and the SKILL.md does not list it (a missing member, not class a). Recorded in the PR's Acceptance notes.", "carrier: 承接者:无 · noted, not filed — the production stdio host (createStdioDataBridge) carries no action seam, so neither run_action nor resume_run is served over stdio today. Unchanged here, and stated in the PR body." ], "gates": { "node scripts/check-adr-0087-registration.mjs --base origin/main": "exit 0 · ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).", "node scripts/check-adr-0087-registration.mjs --self-test": "exit 0 · ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)", "node scripts/check-changeset-no-major.mjs --base origin/main": "exit 0 · · no `pull_request` payload was available to read a declaration from", "node scripts/check-changeset-no-major.mjs --self-test": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot", "node scripts/check-ci-filter-parity.mjs": "exit 0 · OK: all 185 declared cross-package glob(s) (132 unique) are covered by `core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` j", "node scripts/check-closing-keyword-parity.mjs": "exit 0 · • packages/spec/CHANGELOG.md -- 6080503 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files", "node scripts/check-closing-keyword-parity.mjs --self-test": "exit 0 · ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.", "node scripts/check-comment-mask-adoption.mjs": "exit 0 · OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached", "node scripts/check-comment-mask-adoption.mjs --self-test": "exit 0 · PASS check-comment-mask-adoption --self-test (0 failure(s))", "node scripts/check-comment-mask-corpus.mjs": "exit 0 · ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7071 files, 0 disagree, 0 unparseable, 86.8s (comparator self-test: 26 cases pass).", "node scripts/check-doc-frontmatter.mjs": "exit 0 · ✓ check-doc-frontmatter: 2 content root(s) verified, each against its own floor — content/docs 405, content/blog 3.", "node scripts/check-doc-frontmatter.mjs --self-test": "exit 0 · ✓ check-doc-frontmatter --self-test: 99 assertions — the card's own description observed failing with the parser's message and the FILE line, every ot", "node scripts/check-doc-route-spelling.mjs --advisory": "exit 0 · ✓ route-spelling guard (advisory): population clean — every shape-matched literal spells its ledger row.", "node scripts/check-doc-route-spelling.mjs --self-test": "exit 0 · ✓ check-doc-route-spelling self-test: extraction tidy-up, the variant relation (plural + pinned lexicon, no prefix heuristic), walk wiring (releases/ ", "node scripts/check-docs-section-name.mjs": "exit 0 · so it is carried by --self-test rather than by this corpus.", "node scripts/check-docs-section-name.mjs --self-test": "exit 0 · ✓ check-docs-section-name self-test: 85 cases pass (real temp trees on disk; both historical misses reproduced as RED, both arms driven RED, the dupli", "node scripts/check-empty-changeset.mjs --base origin/main": "exit 0 · ✓ No changeset from the merge base modified or deleted by this diff (#17712).", "node scripts/check-empty-changeset.mjs --self-test": "exit 0 · ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)", "node scripts/check-keyed-text-bounds.mjs": "exit 0 · ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide. Reproduce the record: see ", "node scripts/check-keyed-text-bounds.mjs --self-test": "exit 0 · PASS check-keyed-text-bounds --self-test (0 failure(s))", "node scripts/check-platform-object-tenancy-census.mjs": "exit 0 · ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the machinery's reach, 26 outside it, every exclusion explaine", "node scripts/check-platform-object-tenancy-census.mjs --self-test": "exit 0 · ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)", "node scripts/check-plugin-teardown-shape.mjs": "exit 0 · ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 6502 source(s) under packages/**; every teardown-shaped method (stop / shutdown / cl", "node scripts/check-plugin-teardown-shape.mjs --self-test": "exit 0 · ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay gree", "node scripts/check-registry-log-declared.mjs": "exit 0 · examples/app-showcase — S1 constructs a SchemaRegistry in its tests", "node scripts/check-registry-log-declared.mjs --self-test": "exit 0 · self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.", "node scripts/check-rest-log-spy-declared.mjs": "exit 0 · OK: 29 of 196 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.", "node scripts/check-rest-log-spy-declared.mjs --self-test": "exit 0 · check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s).", "node scripts/check-section-landing-index.mjs": "exit 0 · ✓ check-section-landing-index: 8 section index block(s) enumerate their meta.json pages, in order, both directions (ai, api, automation, data-modeling", "node scripts/check-section-landing-index.mjs --self-test": "exit 0 · ✓ check-section-landing-index --self-test: 31 assertions over synthetic inputs and a temp fixture (real judge()/run() path); every limb -- both shapes", "node scripts/check-system-context-census.mjs": "exit 0 · check-system-context-census: OK — 110 elevation read sites in 20 packages across 45 files, living in 91 symbol(s); the page cites 104 symbol(s) agains", "node scripts/check-system-context-census.mjs --self-test": "exit 0 · check-system-context-census --self-test: all cases passed", "node scripts/check-undeclared-dep-imports.mjs": "exit 0 · ⚠ The delta is information, not a verdict — the floors are `)=` and cannot see an upward drift at all, which is why it is PRINTED. Reproduce the recor", "node scripts/check-undeclared-dep-imports.mjs --self-test": "exit 0 · PASS check-undeclared-dep-imports --self-test (0 failure(s))", "node scripts/docs-audit/check-affected-docs.mjs": "exit 0 · → the unreachable rows themselves: this command with --json", "node scripts/docs-audit/check-drift-comment.mjs": "exit 0 · ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).", "node scripts/pm/release-rehearsal-clone.mjs --self-test": "exit 0 · ✓ self-test passed", "pnpm --filter @objectstack/lint run check:doc-formula-expressions": "exit 0 · #11673).", "pnpm --filter @objectstack/lint run check:doc-security-posture": "exit 0 · ✅ 27 ObjectSchema.create example(s) in 227 marked block(s) across 248 prose file(s) in 2 root(s) carry an os validate-clean security posture", "pnpm --filter @objectstack/spec run check:docs": "exit 0 · ✅ 225 generated files in sync with packages/spec", "pnpm --filter @objectstack/spec run check:duration-unit-keys": "exit 0 · ✓ check:duration-unit-keys — 204 unit-declaring numeric key(s) across 2575 source file(s) all carry their unit in the key name (or in a sibling `unit`", "pnpm --filter @objectstack/spec run check:empty-state": "exit 0 · ✓ all classified (2 closed, 2 open, 4 output, 9 scope)", "pnpm --filter @objectstack/spec run check:liveness": "exit 0 · (not a completeness claim about the 577 child key(s) under the declared blanket verdicts above — those are recorded, not classified.)", "pnpm --filter @objectstack/spec run check:skill-examples": "exit 0 (first run exit 3 PREREQUISITE NOT MET: client-react unbuilt; re-run after full packages build) · ✅ 259 prose examples type-check across 3 surface(s)", "pnpm --filter @objectstack/spec run check:strictness-ledger": "exit 0 · ✓ docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is current — 452 site(s) measured, 1 authorable strip site(s) left.", "pnpm --filter @objectstack/spec run check:variant-docs": "exit 0 · ✓ variant/doc gate: 18 discriminated union(s) — 8 governed (every variant mentioned in a bound doc), 10 exempt.", "pnpm --filter @objectstack/spec run check:yaml-examples": "exit 0 · ↳ 18 component node(s) also judged against their ComponentPropsMap props schema; 1 skipped (no row for the type — SDUI blocks and custom.* are an open", "pnpm check:changeset-gate-self-tests": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in bot", "pnpm check:corpus-claim-drift": "exit 0 · Ledger: 2 baselined file(s) in scripts/corpus-claim-drift-baseline.json.", "pnpm check:cross-package-test-inputs": "exit 0 · OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked ", "pnpm check:dispatcher-error-vocabulary": "exit 0 · [#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — the `apierrorarg` shape. `APIError.from`", "pnpm check:doc-anchors": "exit 0 · ✅ check-doc-anchors: 369 internal #fragment link(s) across 410 source file(s) all resolve to a real heading", "pnpm check:doc-authoring": "exit 0 · ✓ doc authoring guard: sibling-package prose ids hold the baseline — 817 pinned site(s) across 231 file(s), 90900 string(s) read in 1250 parsed source", "pnpm check:docs-audit-scope": "exit 0 · ✓ scope injection is live: the workflow audits the list handed in as args.handwritten, and refuses an invocation that hands in no scope at all.", "pnpm check:docs-redirects": "exit 0 · check-docs-redirects: OK (apps/docs/redirects.mjs: 98 entries -- 95 page destination(s) resolved against content/docs, 3 wildcard destination(s) resol", "pnpm check:docs-single-h1": "exit 0 · ✓ check-docs-single-h1: 405 page(s) under content/docs/ carry no body-level `# ` heading (0 subtree(s) excluded, see --list).", "pnpm check:docs-spec-enumerations": "exit 0 · OK the hand-written spec enumerations agree with packages/spec/package.json -- 16 subpath(s) held ORDERED in content/docs/deployment/troubleshooting.m", "pnpm check:docs-transcript-drift": "exit 0 · ✓ check-docs-transcript-drift: 4 declared transcript value(s) across 405 page(s) under content/docs/ equal what the registry derives today, and no und", "pnpm check:driver-memory-census": "exit 0 · check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consume", "pnpm check:dts-closure": "exit 0 · re-run after full build: 72 built package(s) swept - 164/164 declared declaration file(s) present", "pnpm check:dual-build-cjs-loads": "exit 0 (first run exit 3 PREREQUISITE NOT MET: some packages had no dist; re-run after full packages build)", "pnpm check:engine-double-contract": "exit 0 · ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.", "pnpm check:gitlink-declared": "exit 0 · check-gitlink-declared: OK (9386 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare", "pnpm check:issue-citations": "exit 0 · ✅ check-issue-citations --self-test: grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contra", "pnpm check:lean-entry-closure": "exit 0 · Admitted set held exactly (15 packages); 6 denied names absent.", "pnpm check:logger-receiver-detach": "exit 0 · control corpus fired on all five detach shapes in this same run, and stayed silent on the measured `console` and options-callback populations -- so th", "pnpm check:nul-bytes": "exit 0 · check-nul-bytes: OK (scanned 9379 text file(s) -- 9379 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).", "pnpm check:objectql-double-limit": "exit 0 · baseline key set verified against 2c1011b: no files added.", "pnpm check:objectui-changeset": "exit 0 · ✓ objectui-range --self-test: all checks passed", "pnpm check:org-identifier": "exit 0 · check-org-identifier: OK (2869 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias).", "pnpm check:page-declaration-shape": "exit 0 · blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:86.", "pnpm check:pm-changeset-deadline-census": "exit 0 · ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, ", "pnpm check:published-files": "exit 0 · ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG", "pnpm check:published-readme-links": "exit 0 · ✓ check:published-readme-links — 177 outbound link(s) across 61 published markdown file(s): 0 root-relative, 0 non-canonical origin(s), 27 docs-site p", "pnpm check:query-options-erasure": "exit 0 · baseline key set verified against 2c1011b: no files added.", "pnpm check:react-page-adapter-contract": "exit 0 · ✓ check-react-page-adapter-contract: 21 app-showcase page module(s) + 1 content/docs react-page sample(s) (from 391 doc file(s), 1993 fenced block(s))", "pnpm check:refd-timer-probe": "exit 0 · 1 code site(s), all inside the approved module, which is present and still reads it.", "pnpm check:role-word": "exit 0 · Ledger: 44 baselined file(s) still carrying it (123 occurrence(s)) in scripts/role-word-baseline.json.", "pnpm check:route-envelope": "exit 0 · read/write discriminator: 11 file(s) skipped as fetch readers (77 zero-argument `res.json()` call(s), none swept in)", "pnpm check:skill-identifier-liveness": "exit 0 · check-skill-identifier-liveness OK — Leg 1: 456 citation(s) over 53 published file(s) checked against 107019 implementation word tokens (0 ledgered ex", "pnpm check:slot-lookup": "exit 0 · baseline key set verified against 2c1011b: no files added.", "pnpm check:sourcemap-no-sources-content": "exit 0 · check-sourcemap-no-sources-content: 30 built package(s) swept - 160 map(s), none embed source text.", "pnpm check:test-source-alias": "exit 0 · check-test-source-alias OK — 74 packages with tests scanned; 61 registered as still resolving a workspace dep through `dist/`; 49 published subpath(s)", "pnpm check:tier-file-adoption": "exit 0 · @objectstack/cli — 69 file(s); imports readTierMode, selectTierFiles from scripts/nightly-tiers.mjs (via packages/cli/vitest-tiers.ts)", "pnpm check:type-check-coverage": "exit 0 · ⚠ The delta is information, not a verdict — this population grows AND shrinks for good reasons, and only the floors decide.", "pnpm check:type-check-debt": "exit 0 (first run exit 3 PREREQUISITE NOT MET: driver-turso/runtime unbuilt; re-run after full packages build) · surplus: none — every entry sits exactly at its measurement", "pnpm check:vendor-version-stamps": "exit 0 · attestations. Re-verify one and you may restamp it; otherwise it stays a historical fact.", "pnpm check:watch-hint-literal": "exit 0 · ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL", "pnpm check:where-matcher": "exit 0 · baseline key set verified against 2c1011b: no files added.", "node scripts/check-changeset-fixed.mjs": "exit 0 · ✓ .changeset/config.json \"fixed\" group is in sync with 70 public workspace packages.", "pnpm check:authz-resolver": "exit 0 · ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.", "pnpm check:error-code-casing": "exit 0 · half belongs to check:dispatcher-error-vocabulary, which reports its own scope.", "pnpm check:filter-alias-parity": "exit 0 · check:filter-alias-parity: OK (4 transport spelling(s) of the `where` slot, identical on both sides: $filter, filter, filters, where)", "pnpm check:route-ledger-census": "exit 0 · ✓ check:route-ledger-census — all 1 census sentence(s) match their arrays.", "pnpm check:published-readme-exports": "exit 0 · run beyond the derivation (README changed)", "pnpm --filter @objectstack/runtime run typecheck": "exit 0 · tsc --noEmit + check:test-typecheck OK (27 file(s) / 191 error(s) / 69 pinned signature(s) held)", "pnpm --filter @objectstack/mcp run typecheck": "exit 0 · tsc --noEmit + check:test-typecheck OK (6 file(s) / 53 error(s) / 8 pinned signature(s) held)", "pnpm --filter @objectstack/runtime exec vitest run --project local": "exit 0 · Test Files 274 passed (274) · Tests 3837 passed | 1 skipped (3838)", "pnpm --filter @objectstack/runtime exec vitest run --project repo": "exit 0 · Test Files 2 passed (2) · Tests 69 passed (69)", "pnpm --filter @objectstack/mcp exec vitest run": "exit 0 · Test Files 32 passed (32) · Tests 344 passed (344)", "pnpm --filter @objectstack/dogfood exec vitest run": "exit 0 · Test Files 137 passed | 1 skipped (138) · Tests 1103 passed | 3 skipped (1106)", "pnpm lint (eslint . --no-inline-config, full repo, not narrowed)": "exit 0 · VERDICT command-exit 0 at 0b6b2d78", "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran (exit-coded record)": "exit 0 · ✓ 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED (a DERIVED zero)" }, "line_budget": "n/a — no governed ledger or skills/** file touched", "deviations": [ "FILE SURFACE SUPPLEMENTED, beyond the claim's declared surface, declared here: packages/mcp/README.md (the published package's own tool list and scope sentence, which would otherwise omit resume_run), and packages/runtime/src/mcp-resume-run.test.ts (the bridge's pins, at src root beside the precedent mcp-list-actions-flow-params.test.ts). packages/runtime/src/domains/automation.ts was EDITED, as the claim allows only for a shared-helper extraction: the REST resume arm's engine-answer mapping moved unchanged into the exported classifyResumeResult, and the REST arm's answers are byte-identical (the existing REST resume pins pass, plus the door-against-door pins).", "PM mechanism assumption 2 falsified in part (see summary). Per the ruling's intent, the route is run_action's gates + an ownership check + a record re-read + the door's shared engine-answer table. It does not call the REST handler or reuse only its checks.", "resume_run's exposure/permission refusals are coded (PERMISSION_DENIED / 403) while run_action's are uncoded text. This is deliberate for a new door, and the inconsistency is noted in the PR.", "Mutation leg M10 was a no-op (refused by ablation-replace because the replacement text already existed) and was redone as M10b.", "Long runs (the first mutation batch, the gate sweep, the full build, dogfood) ran as detached processes, and each was waited on with a single foreground 'tail --pid' block, because lock queueing plus run time exceeded the 10-minute foreground cap. The first mutation batch was moved to the background by the harness at 600s and then waited on by its PID. No turn ended waiting.", "Commit trailers use AGENTS.md's model-free pair (Co-authored-by: Claude / Claude-Session) instead of the harness reminder's model-named trailer. The PR body ends with AGENTS.md's session-URL footer instead of the reminder's '🤖 Generated with' line." ], "files_changed": [ ".changeset/15705-mcp-resume-run.md (+21/-0)", "content/docs/ai/actions-as-tools.mdx (+52/-2)", "content/docs/ai/connect-mcp.mdx (+3/-2)", "packages/mcp/README.md (+10/-5)", "packages/mcp/src/mcp-http-tools.ts (+130/-3)", "packages/mcp/src/mcp-http-tools.unknown-argument-keys.test.ts (+25/-2)", "packages/mcp/src/mcp-resume-run-tool.test.ts (+165/-0)", "packages/mcp/src/mcp-server-runtime.ts (+2/-1)", "packages/mcp/src/mcp-stdio-tools.test.ts (+37/-0)", "packages/mcp/src/skill-md-surface-guard.test.ts (+5/-0)", "packages/mcp/src/skill-md.test.ts (+1/-0)", "packages/mcp/src/skill-md.ts (+7/-0)", "packages/runtime/src/domains/automation.ts (+117/-84)", "packages/runtime/src/domains/mcp.ts (+258/-0)", "packages/runtime/src/mcp-resume-run.test.ts (+486/-0)" ] }objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsIn-seat review: ACCEPT on form and scope. ⛔ HELD draft, off the queue, until the owed at-tier clause-② review passes — PR #19985 at head
0b6b2d782f7465679ddf8e634c895a1b74c2ec20domain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-24T14:37Z · reviewed on GitHub andorigin/main2c1011b01bReview checklist
- PR form: draft, base
main. The first two lines areFixes #15705and a line-initial, bareClause-②: yes, and there is exactly one closing binding. One footer, no model identifiers. - Scope: 15 files (+1319/−99). Two are beyond the claim as written, both declared by the dev and amended into the claim in place:
packages/mcp/README.mdandpackages/runtime/src/mcp-resume-run.test.ts.domains/automation.tswas edited under the claim's shared-helper clause: the engine-answer table moved unchanged intoclassifyResumeResult, pinned door against door. - Changeset:
minorfor@objectstack/mcpand@objectstack/runtime, carryingClause-②: yes. ⛔ Nevermajor. - The PM's mechanism assumption 2 was FALSIFIED IN PART, and the route taken is the ruling's intent. The REST door never asks who is resuming, so reusing only its checks would have let any authenticated caller drive another user's run. The bridge (
domains/mcp.ts,resumeActionRun) does five things. It requiresgetRun(runId).trigger.userIdto equal the caller, and answers unknown, finished and foreign runs with ONE 404 (no existence oracle). It admits throughrun_action's own gates on the flow action that targets the run's flow and object. It re-reads the subject record with MCPrun_actionon a flow action answersok: trueand 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'srefuseDeniedSubjectLoad. It continues only screen pauses (409 otherwise). It maps the engine's answer through the shared table. I read the code, not the report.
The dev's open question, answered here, not escalated: re-apply
ai.requiresConfirmationon resume? → A, keep. The maintainer's ruling item ① says 「续跑必须经过与run_action同等的授权与调用方范围判定」 (resume must go through the same authorization and caller-scope checks asrun_action), and the confirmation gate is one ofrun_action's gates. B would need a newAutomationContextfield inpackages/spec, i.e. another lane and a wider change, for convenience only.Out-of-scope findings, one row each
- class (a): the REST resume door has no caller gate → filed security: POST /automation/:name/runs/:runId/resume never checks WHO is resuming — an authenticated stranger with a run id continues another user's paused screen run, which then runs under the starter's stored context #19987 (self-reported P0 suspect, left for triage to grade). The seat re-read the landing sites before filing.
run_actionanswers its exposure/permission refusals as uncoded text whileresume_runcodes them (403) → Acceptance notes.- The SKILL.md surface guard's bridge has no
aggregatemember → Acceptance notes. - The production stdio host carries no action seam, so neither verb is served over stdio → stated in the PR body.
⛔ Why this PR does not enter the queue yet. It declares
Clause-②: yes, the declaration limb ofreferences/landing-operations.md:9, so it needs an at-tier review PASS on record before ready and before the queue. At 2026-09-24T14:14Z this seat's isolated reviewer for a sibling PR was refused at its first call by the account's usage limit for that tier (HTTP 429), so no at-tier reviewer is available now. Per:13, the PR stays draft, off the queue, and waits for the tier; the one bypass is the maintainer reviewing it personally. ⛔ The seat does not self-review at a lower tier for this gate. The PR stayspm:dispatched, and nothing else is owed from the dev.- PR form: draft, base
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClause-② gate cleared: the at-tier review PASS is on record for PR #19985's current head, and the PR is in the merge queue
domain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-24T15:54Z- The review of record: PR comment
5817472368(2026-09-24T15:51:55Z).## Contract review·Served-tier: CONTRACT_REVIEW_TIER·Head-sha: 0b6b2d782f7465679ddf8e634c895a1b74c2ec20, which is the PR's current head ·Implemented-by: claude/issue-15705-mcp-resume-run· VERDICT: PASS. It is an isolated at-tier reviewer adopted by the director seat (summon chore(deps)(deps-dev): bump @vitest/coverage-v8 from 2.1.9 to 4.0.17 #29,session_01EcrTi7s5oDYPHS4Pi7h31d), which the seat post [PM seat] director(项目总监) — 🟢 os-zhuang · 第 35 场 session_01VYToj6PQehTEKNrjGM9akg · 开轮 2026-10-06T14:15Z · 第 31–34 场台账在评论 #12708 ledger confirms is active today, on the maintainer's instruction 「帮我处理并跟进到合并」 (handle it and follow it through to merge). Its ③ independently lands therequiresConfirmationquestion on A, the same answer this seat gave in5816254949, and it names security: POST /automation/:name/runs/:runId/resume never checks WHO is resuming — an authenticated stranger with a run id continues another user's paused screen run, which then runs under the starter's stored context #19987 as the carrier of the REST door gap. - Landing class, re-read on this head: 36 check runs, 33 success and 2 rostered skips (
Console Pin Gate,Packed-tarball smoke (opt-in));mergeable_state: clean;check-governed-merges.mjs --pr 19985exit 0, NOT governed, 1,418 lines ≤ 5000. - Timeline:
ready_for_review2026-09-24T15:52:06Z andadded_to_merge_queue2026-09-24T15:53:14Z, both byhotlong. This seat's auto-merge arm on the same head followed and changes nothing. The seat follows the PR to MERGED, then verifies by content and clears this card's state. - Next: security: POST /automation/:name/runs/:runId/resume never checks WHO is resuming — an authenticated stranger with a run id continues another user's paused screen run, which then runs under the starter's stored context #19987 (p1 · security) is fenced on the same file,
packages/runtime/src/domains/automation.ts, and unfences when this PR merges.
- The review of record: PR comment
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsLANDED —
08b213e0fa1eaf4844b57b3c4ebddf0925b0c425(PR #19985), verified by CONTENT onorigin/maindomain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-24T16:20Zprobe before ( 2c1011b01b)origin/mainresume_runinpackages/mcp/src/mcp-http-tools.ts0 11 resumeActionRuninpackages/runtime/src/domains/mcp.ts0 4 export async function classifyResumeResultindomains/automation.ts0 1 untouched control: function refuseUnrelatedScreenRead1 1 mcp-resume-run.test.ts+mcp-resume-run-tool.test.tsin the tree— both present - Shape: 2 parent fields, so this is a squash. The pre-merge head
0b6b2d78is ⛔ not an ancestor (exit 1); the control legcdc1ae03is an ancestor (exit 0). The commit carries the enqueue instant 2026-09-24T15:53:14Z. - The card's three expectations are now all delivered: headless screen satisfaction and flow inputs in
list_actions(PR fix(automation): a screen the caller already answered no longer parks the run, and list_actions publishes a flow action's inputs #15787,b31ebfe7f), and the MCP resume verb (this PR, the maintainer's 「15705同意」 item ①). Item ②, the explicit caller-provenance signal, landed separately as AutomationContext: an explicit caller-provenance signal for flow params, replacing the headless-screen inference of PR #15787 (#15705 ruling ②) #19846 / PR feat(spec,runtime,service-automation): the flow doors state callerParamKeys, and the headless screen verdict reads it #19899. - Card: closed by
Fixes #15705. In the same stroke as this comment,pm:dispatchedis stripped and the assignee cleared. - Carried forward, ⛔ not this card's: security: POST /automation/:name/runs/:runId/resume never checks WHO is resuming — an authenticated stranger with a run id continues another user's paused screen run, which then runs under the starter's stored context #19987 (p1 · security), the REST resume door's missing caller gate. Its same-file fence on
domains/automation.tsis released by this merge.
- Shape: 2 parent fields, so this is a squash. The pre-merge head
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsLanded and closed out · director seat summon #29 (
session_01EcrTi7s5oDYPHS4Pi7h31d), on the maintainer's instruction 「帮我处理并跟进到合并」 2026-09-24T16:21ZPR #19985 merged at 2026-09-24T16:19:51Z as squash commit
08b213e0fa(one parent, onorigin/main).Fixes #15705closed this cardcompleted. That delivers item ① of the ruling (5793364219):resume_runon the MCP surface, the caller's own run only, behindrun_action's gates. Item ② is #19846, and the two expectations already delivered were in #15787.- Clause-② gate: an at-tier review PASS on head
0b6b2d7, recorded on the PR (5817472368), then ready, then auto-merge. The PR entered the merge queue at 2026-09-24T15:53:14Z. - Tidy-up:
pm:dispatchedremoved in this stroke. Theos-steveassignee left in the same instant through another writer (this seat's label-only write read it back as absent) and is not restored on a closed, done card. - Carried forward, not closed here: the REST resume door has no caller gate, security: POST /automation/:name/runs/:runId/resume never checks WHO is resuming — an authenticated stranger with a run id continues another user's paused screen run, which then runs under the starter's stored context #19987. It is awaiting triage and a maintainer ruling on the REST door.
Generated by Claude Code
- Clause-② gate: an at-tier review PASS on head
Summary
An
ai.exposedaction whose target is a screen flow cannot be completed through MCP.run_actionstarts the flow, the screen node suspends the run and returns the form definition, and the MCP tool set has no way to resume it. The agent can press the button but never finish. Two independent causes, both in the platform:service-automationscreen executor pauses whenever the node has fields, without checking whether those fields are already bound as flow variables.runtime'sseedFlowActionParamsdoes merge the MCPparamsinto the flow's variables, so correctly-named params bind — and the screen still pauses.runtime'ssummarizeActionParamsonly readsaction.params(script-action declarations). A flow'sisInputvariables are never surfaced, solist_actionsreturns noparamsfor any flow-typed action even though the tool description promises "its input parameters". The agent has no way to learn the input names it would need for (1) to matter.Environment
@objectstack/mcp,runtime,service-automation,spec— all 17.2.0objectstack-ai/hotcrm@5e08628f, dev server onhttp://localhost:4001http://localhost:4001/api/v1/mcpwithx-api-key(principal: human, no onBehalfOf)Reproduction
hotcrm's
schedule_followupaction (src/actions/lead.actions.ts) istype: 'flow',target: 'schedule_followup',ai.exposed: true. The target flow (src/flows/schedule-followup.flow.ts) istype: 'screen'and declaressubject,dueDate,activityType,priority,notesasisInput: truevariables; its first node afterstartisscreen_1with the same five fields.list_actions→{ "name": "schedule_followup", "objectName": "crm_lead", "type": "flow", "requiresRecord": true, "requiresConfirmation": false }No
paramskey, so the caller cannot know the input names.run_action({ actionName: "schedule_followup", objectName: "crm_lead", recordId: "<lead id>", params: { subject: "…", due_date: "2026-09-09" } })→{ "ok": true, "result": { "success": true, "status": "paused", "runId": "run_a618315c-…", "durationMs": 2, "screen": { "nodeId": "screen_1", "title": "Schedule Follow-up", "fields": [ {"name":"subject",…}, {"name":"dueDate",…}, … ] } } }crm_taskwithrelated_to_lead = <lead id>; the lead'snext_followup_dateandupdated_atare unchanged. The run is parked onscreen_1with nothing able to resume it — the 11 registered MCP tools (list_objects … run_action) include no resume / submit-screen verb.Passing the correct names (
dueDateinstead ofdue_date) does not change the outcome: the variables bind, the screen node still suspends.Where it happens
@objectstack/service-automationscreen nodeexecute():shouldPause = cfg.waitForInput === true || (hasFields && cfg.waitForInput !== false)— the only inputs to the decision are "does the node have fields" and the author-levelwaitForInputflag. Whether everyrequiredfield already has a bound variable is never consulted.waitForInput: falseis not a fix: it also skips the form for interactive users.@objectstack/runtimesummarizeActionParams(deps, action, obj): iteratesaction.paramsonly. For a flow-typed action the flow'svariables.filter(v => v.isInput)(and/or the first screen node'sfields) are the real input contract and are not consulted.@objectstack/runtimeseedFlowActionParams: works as intended — returns{ ...record, recordId, <objectName>Id, ...params }, so this is not where the values are lost.@objectstack/mcpregisterActionTools:run_actionjust forwards tobridge.runActionand returns the result verbatim, so the paused envelope reaches the agent but there is nothing it can do withrunId.Expected
Either (or both):
paramsand everyrequiredfield of the screen node is already bound (and optional ones default), the executor should treat the screen as satisfied and continue instead of suspending. Interactive runs (no params supplied) keep the current behaviour.resume_run({ runId, values })that posts screen values into a paused run — mirroring what the console's screen runner does — so an agent that receivesstatus: "paused"+screencan complete it.And independently:
list_actionsshould surface a flow action's inputs (isInputvariables, with the screen field'slabel/type/required/optionswhere available) asparams, matching the tool description.Why it matters
ai.exposedtoday means "the agent can invoke this", not "the agent can complete this". Every screen-typed flow action is a dead end for MCP clients, and the agent has no signal telling it so —list_actionslooks identical for a script action that will run to completion and a screen flow that will park. In hotcrm both AI-exposed actions (schedule_followup,convert_lead) are screen flows, so the action layer is 0-for-2 over MCP while the generic CRUD tools work fully. The fallback an agent ends up taking (create_record+update_recordto re-implement the flow's tail by hand) bypasses whatever business rules the flow encapsulated.Related: #2849 established invoke-time as the only agent boundary for actions; this issue is about the other half — once invoked, the action must be completable.