Skip to content

feat(automation): a paused screen says whether another screen follows, and the flow runner reads Next or Submit and counts steps - #26752

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-6imvw2
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-6imvw2

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26657

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: #26756

维护者速读

  • 改了什么:屏幕流暂停时,引擎按流程图在 ScreenSpec.progress 里说明「后面还有没有屏幕」(none / possible / certain),只有在每条路径都一致时才附带第几步、共几步;Console 的 FlowRunner 据此把按钮写成「下一步」或「提交」,并且只在总数确定时画「第 N 步,共 M 步」。
  • 为什么改:hotcrm 的发起合同向导每一步按钮都是「提交」,申请人在第 1 步就以为合同已经提交;也画不出步数,因为暂停的屏幕里没有这些信息,而流程在任何屏幕之后都可能分支,只有引擎知道。
  • 风险与回滚:契约只加一个可选成员,旧服务端不发它,旧客户端忽略它;没有 progress 时按钮照旧是「提交」。引擎侧只在屏幕暂停时读一次流程图。回滚即 revert 本 PR。
  • 先看:packages/services/service-automation/src/builtin/screen-nodes.ts 的 readScreenProgress(分支、环、扇出与子流程的判定),packages/spec/src/contracts/automation-service.ts 的 ScreenProgressSpec,ui/packages/app-shell/src/views/FlowRunner.tsx 的按钮与计步。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/builtin/screen-progress.test.ts,预期 13 个测试全绿(线性、分支、环、审批式暂停、子流程各有一条)。
  • 需要你做的:无,席位落地(需先过契约档复核)。

What changes

The shape: ScreenSpec.progress?: ScreenProgressSpec (packages/spec/src/contracts/automation-service.ts, with its wire spelling on TriggerFlowResponseSchema in api/automation-api.zod.ts, bound to the interface by the existing compile-time identity pin):

interface ScreenProgressSpec {
  nextScreen: 'none' | 'possible' | 'certain';
  step?: number;       // 1-based, only when every path from the start agrees
  stepCount?: number;  // only alongside step, when every path through this screen agrees
}

Why this shape:

  • A tri-state, not a boolean. A flow can branch after any screen. A boolean isLast would have to guess on a branch: false claims more screens follow when they may not, true claims the end when one may follow. 'possible' says exactly what the graph knows. The runner reads both 'possible' and 'certain' as "Next", because a later screen can still come; 'certain' stays distinct so a reader can tell the two apart without walking the graph.
  • The counts are optional members of the same object, each present only when exact. "Absent" means "the graph does not make it exact", never "zero" or "unknown server". The whole object is absent only when the engine cannot answer at all (a server that predates it, or the child-run case below), and a runner then keeps its old label.
  • One nested member, not three flat ones on ScreenSpec. The three come from one computation and mean nothing apart; one optional key keeps the "no answer" state a single check.
  • A served fact, not an authorable key. Nothing a metadata author writes changes (Metadata-protocol: none); no author can mis-declare it.

The engine fills it (packages/services/service-automation/src/builtin/screen-nodes.ts, readScreenProgress), reading the graph of the flow the run executes at the moment a screen pauses:

  • Every out-edge is a way the run may go; conditions are not evaluated ahead of time. nextScreen is plain reachability for 'none', and the fewest surely-pausing screens on any path to a point where the run may stop (a 0-1 BFS) for 'certain'.
  • A branch after a screen answers 'possible' — a decision with a branch that pauses on another screen and one that does not, a fault edge, a loop back. The engine never picks a branch.
  • step / stepCount are computed over strongly connected components, as the SET of screen totals the paths give; a count is served only when that set has one member. A loop through a screen, a parallel fan-out (several unconditional successors, an inclusive decision), a subflow, or a screen the caller may already have answered makes the count vary, so it is left out.
  • What counts as a screen: a screen node that surely pauses (an objectName, waitForInput: true, or declared fields without waitForInput: false), the executor's own rule. A field-less pass-through screen is not counted. A screen whose fields the caller supplied (judgeHeadlessScreen) may not pause, so it is "maybe". A non-screen node that can pause (an approval, a wait) serves no screen (only a screen node sets NodeExecutionResult.screen), so the stretch of screens may end there: a screen behind one is possible, never certain.
  • A subflow may serve screens of its child, or none, so the screen before one answers 'possible' with no total. A screen INSIDE a child run sees only the child graph: it keeps nextScreen when that graph shows another screen, and serves no progress when it does not, because the parent may continue with screens of its own.

The runner reads it (ui/packages/app-shell/src/views/FlowRunner.tsx): the flat step's primary button reads common.next while nextScreen is 'certain' or 'possible' and common.submit on 'none' or with no progress; the step counter (form.stepOf, the wording the form wizard already uses) is drawn beside the flow's name only when both step and stepCount are served. The object-form step's own button (flowRunner.saveAndContinue) is the form's and is unchanged.

Docs: content/docs/automation/flows.mdx gains "A paused screen says whether another one follows — Next, Submit and the step counter", with the authoring advice that follows from it (keep a wizard's screens on one straight path and branch after the last to get an exact counter).

Tests

  • New packages/services/service-automation/src/builtin/screen-progress.test.ts (13 pins). End to end through the engine: a linear three-screen wizard (certain 1/3, certain 2/3, none 3/3, then completes); an object-form step (none 1/1); a branched flow (possible, step 1, no total; past the branch none 2/2); a looped flow (certain then possible, no counts, and certain again after looping back); a pause that serves no screen (none 1/1 before an end, possible before a screen); a pass-through screen not counted; a subflow (possible before it; the child's first screen certain with no counts; the child's last screen with no progress). Over readScreenProgress directly: parallel fan-out, fault paths, a caller-answered screen, an inclusive decision, no start node and an unknown node.
  • packages/spec/src/api/automation-api.zod.test.ts: the trigger response keeps progress, and refuses a nextScreen outside the closed set, a missing nextScreen, step: 0 and a fractional step.
  • New ui/packages/app-shell/src/views/__tests__/FlowRunner.stepProgress-26657.test.tsx (6 pins): Next and "Step 1 of 3" on certain; Next and no counter on possible with no total; Submit and "Step 3 of 3" on none; Submit and no counter with no progress; a resume that pauses again relabels from the next screen; in zh the words are 下一步 / 提交 / 第 1 步,共 2 步.

Acceptance notes

  • No new translation keys. The runner reuses common.next, common.submit and form.stepOf, which every one of the ten bundles already carries (zh: 下一步 / 提交 / 第 {{current}} 步,共 {{total}} 步), so each defaultValue equals its en value.
  • Skills half owed, not written here. skills/** is a Tier H governed surface; teaching the screen row of skills/objectstack-automation/SKILL.md (and an eval) in this PR would make the whole PR Tier H. The docs half is here; the skills half is skills(objectstack-automation): the screen row does not teach that a paused screen's Next / Submit and step count are engine-served (follow-up owed by #26657) #26756.
  • File surface beyond the claim, all part of the same contract change: the wire spelling in packages/spec/src/api/automation-api.zod.ts (the existing TriggerFlowDataMatchesContract identity pin reds without it), its test, the docs page, and three regenerated spec artifacts (api-surface/contracts.json, export-origins/contracts.json, the api/ strictness count 446 → 447 for the new module-local z.object).
  • The engine reads the graph by the run's flowName from the engine's flow map (getFlow), the same map a run starts and resumes from, and checks the node is a screen in it before answering.
  • The object-form step's button still reads "Save & Continue" on its last step too; the signal now exists, but relabelling the form's own button is outside this card.

Verification

Head 653293e7b for everything below except the last block. Every run went through os-verify-lock, and each reading is the lock's own VERDICT line.

Engine (service-automation)
  • pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/screen-progress.test.ts → Tests 13 passed (13), VERDICT command-exit 0.
  • Whole package: pnpm --filter @objectstack/service-automation test → Test Files 213 passed (213), Tests 2744 passed (2744); typecheck (tsc --noEmit and check:test-typecheck) → OK, 0 errors.
Spec
  • vitest run src/api/automation-api.zod.test.ts → Tests 68 passed (68); pnpm --filter @objectstack/spec typecheck → exit 0 (VERDICT command-exit 0 for the pair). The TriggerFlowDataMatchesContract identity pin compiles, so the wire spelling and AutomationResult agree.
  • check:generated found three artifacts stale (api-surface/, export-origins/, the api/ strictness count); check:generated --fix rebuilt spec and regenerated exactly those (VERDICT command-exit 0), committed as their own commit.
Runner (ui)
  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/__tests__/FlowRunner (ui root) → Test Files 16 passed (16), Tests 100 passed (100), VERDICT command-exit 0.
  • pnpm --filter @object-ui/app-shell type-check (both tsc --noEmit and tsc -p tsconfig.test.json), after building @object-ui/app-shell^... → VERDICT command-exit 0.
  • pnpm check:new-line-citations (ui) → 0 new citation(s).
Ablations (one per changed behaviour, via scripts/ablation-replace.mjs)
  • Engine: anchor if (fansOut || loops(own)) return { nextScreen }; → if (fansOut) return { nextScreen }; (anchor 1 → 0, blob 1cecbf95085c → 11426a9602b7). screen-progress.test.ts: Tests 1 failed | 12 passed — the looped flow, which then served a step on a screen inside a loop. Restored: blob equals HEAD, git diff HEAD empty.
  • Runner: anchor const anotherScreen = progress !== undefined && progress.nextScreen !== 'none'; → const anotherScreen = false; (blob 89e6ec8b14af → 862b624909c2). The new runner file: Tests 4 failed | 2 passed — every Next pin (en and zh). Restored: blob equals HEAD, git diff HEAD empty.
  • Both test files import their subject from src, so no dist was involved.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 653293e7b: 108 commands, each run serially with its exit code recorded before any pipe. --ran: 108 derived famil(ies) accounted for — 106 run, 2 NOT-MEASURED. All 106 exit 0. NOT MEASURED: spec check:skill-examples and check:dual-build-cjs-loads, which exit 3 (prerequisite) without a whole-repo pnpm build; CI measures both. PR-body gates simulated through a local GITHUB_EVENT_PATH before opening: check-changeset-no-major, check-adr-0087-registration, check-empty-changeset exit 0; check-closing-keyword-parity --body binds exactly Fixes #26657.

Review follow-up at `094a249d9` (docs and TSDoc only)
  • The flows page no longer says every paused screen carries progress; it and the ScreenSpec.progress TSDoc now name the absent cases (no flow name, a node that is not a screen of the registered flow, no start node, a child-run screen with no further screen, an older server).
  • Re-ran the derived docs families on that head: check-docs-section-name, check-issue-citations, check-affected-docs, spec gen:schema then spec check:docs, check:docs-transcript-drift (after building @objectstack/lint...) → VERDICT command-exit 0; check-drift-comment, spec check:empty-state, check:liveness, check:variant-docs, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:issue-citations → each exit 0.

Generated by Claude Code

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/flows.mdx (via nextScreen (symbol, a field of interface ScreenProgressSpec), stepCount (symbol, a field of interface ScreenProgressSpec), nextScreen (literal, a string literal in readScreenProgress))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 147 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 74db43ef3d64f8315af68952ec45d9c46942fbc7 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 74db43ef3d64f8315af68952ec45d9c46942fbc7

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 653293e7b8177159c052c1f26227d001312b1375
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts, packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/builtin/screen-nodes.ts (readScreenProgress, the progress closure, the executor's own pause rule), packages/services/service-automation/src/builtin/subflow-node.ts and map-node.ts ($parentRunId, which node forwards a child screen), packages/services/service-automation/src/dag-traversal.ts (traverseNext edge selection, which visitShape models), packages/services/service-automation/src/builtin/logic-nodes.ts (the decision executor's branchLabel), packages/services/service-automation/src/screen-input-contract.ts (judgeHeadlessScreen, callerSupplied), packages/services/service-automation/src/engine.ts (getFlow, getActionDescriptor), packages/spec/src/automation/flow.zod.ts (FlowEdgeSchema type / isDefault / condition), packages/spec/src/automation/builtin-node-config.zod.ts (ScreenConfigSchema), ui/packages/app-shell/src/views/FlowRunner.tsx and ScreenView.tsx (the first consumer's read of ScreenSpec.progress), packages/spec/api-surface/contracts.json and export-origins/contracts.json (the public-surface delta). The rest of the diff (changeset, content/docs/automation/flows.mdx, the strictness-ledger count, the three test files) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs read: card #26657 (body, triage 6441476979, claim 6442765961, os-dev-report 6444639574), PR #26752 (body, 12-file list, net diff against merge-base 43731031), the head's check-runs (two polls). Rulings the card cites, read as cited: #26369 ruling 6431004737 (letter B, the Skills-docs: trigger) and #26368 ruling 6431014712 (letter A, the p1 yardstick).

Check-runs on the head at the time of this record: no run red. Completed success: Lint & Repo Gates, Governed Surface Queue Guard, Build & E2E UI, Check Changeset, Check PR Size, PR-body guards, Spec property liveness, Type Check · source gates, Type Check · debt ledger, every Lint · lane, Test Core (8/8), Dogfood Regression Gate, Check Documentation Links, Flag docs affected by code changes. Still in_progress: Build Core, Test Core (1–7/8), Test UI (1–16/16), Type Check · workspace, Type Check · consumer gates, Type Check UI, Lint UI. The required set is therefore not yet green: this record judges the contract and the design; the landing still waits for every check, as it always does.

① Derived judgments

  1. ScreenSpec.progress?: ScreenProgressSpec (contracts/automation-service.ts) — one optional member on a published interface. Accept-set change: a producer may now serve one more key; every existing producer and consumer stays valid. RIGHT, and correctly optional: the TSDoc's two reasons for absence (an older server; a child-run screen whose own graph ends) are both real in the cited executor.
  2. export interface ScreenProgressSpec — a new public export of @objectstack/spec/contracts; api-surface/contracts.json and export-origins/contracts.json each gain exactly that one row, nothing removed or narrowed. RIGHT: a widening, and the regenerated artifacts match the source.
  3. screenProgressSpecShape on TriggerFlowResponseSchema.data.screen.progress (api/automation-api.zod.ts) — module-local, z.object (strip), nextScreen: z.enum(['none','possible','certain']), step / stepCount int().min(1).optional(). RIGHT on three counts: (a) the closed enum and the 1-based bounds refuse what the contract's TSDoc forbids and nothing the engine serves (step = prior + 1 ≥ 1, stepCount = step + remaining ≥ step); (b) z.object rather than strictObject is the sibling convention of screenSpecShape / screenFieldSpecShape, whose own note explains the wire shape stays lenient for screens rehydrated from an older suspension; (c) the existing compile-time identity pin TriggerFlowDataMatchesContract binds this spelling to the interface, and Type Check · source gates is success on this head, so the two cannot drift.
  4. Zod strip semantics across versions — an older client parsing a newer server's response strips progress and behaves as before; a newer client against an older server sees no progress and the runner keeps Submit. RIGHT: the rollout is order-independent, as the PR body claims.
  5. No authorable key changes. FlowSchema, ScreenConfigSchema, FlowEdgeSchema are untouched; check:authorable-surface has nothing to regenerate and the dev's check:generated found none stale. RIGHT.
  6. Strictness ledger api/ 446 → 447 — one new non-strict z.object site in an untriaged directory, regenerated whole by its generator. RIGHT as an artifact; the choice it counts is item 3(b).
  7. Persisted suspension — suspended-run-store.ts stores screen_json as JSON with no schema on the way back, so a run suspended on this head rehydrates progress intact and a run suspended before it rehydrates without one. RIGHT; no migration is owed.
  8. One wire spelling only — no second schema in packages/spec/src/api or packages/rest spells the paused screen (grep for idVariable co-occurring with nodeId outside tests finds only the two hits files and their executor), so no route strips the key that another serves. RIGHT.
  9. The first consumer — ui/.../ScreenView.tsx derives its ScreenSpec from the spec interface (Omit<SpecScreenSpec,'fields'> & …), so FlowRunner reads screen.progress typed; common.next / common.submit / form.stepOf already exist in every bundle (the zh pin in the new test reads them). RIGHT: no translation key is added, as declared.

② Semver level

Changeset .changeset/26657-screen-progress-next-submit.md: @objectstack/spec patch, @objectstack/service-automation patch, @object-ui/app-shell patch; body carries Clause-②: yes (widening) and Metadata-protocol: none, byte-identical to the PR body. PR-body gates Check Changeset and PR-body guards are success on the head.

Clause-②: yes (widening) is RIGHT — a new optional member on a published interface, a new public interface export, and a new optional key on a published response schema; nothing narrowed, nothing removed.

Metadata-protocol: none is RIGHT, and the level patch follows from it. The brief asks this explicitly, so the reasoning in full: AGENTS.md Post-Task Checklist step 3 defines the kind as the change to what a metadata author can write — a new key, accepted value or metadata type is added, and scripts/pm/clause2-line.mjs records the maintainer ruling it implements (ruling A, 2026-10-21, #25105): the level of a changeset follows the METADATA PROTOCOL alone: minor when the protocol an author writes against is added to, narrowed or changed in meaning, patch otherwise. ScreenSpec.progress is a member the ENGINE serves on a paused run; no flow, screen node or edge an author writes gains a key, a value or a meaning (FlowSchema, ScreenConfigSchema, FlowEdgeSchema untouched; check:authorable-surface unchanged). It is a published-surface widening — the Clause-② line carries that — not a protocol addition in the kind's own vocabulary. The triage line every protocol addition is p1 (#26368 A) is the PRIORITY yardstick (「协议新增定 p1」) and grades the card; it does not redefine the Metadata-protocol: kind, which is read by clause2-line.mjs and governs the level. So the declaration must stay Metadata-protocol: none, and the level must stay patch on all three packages. A minor here would be the wrong declaration, not a safer one: it would claim an author-facing change this diff does not make.

③ Boundary flags

PR author is objectstack-fleet[bot] on the base repo (objectstack-ai/objectstack): no off-relay author to name. open_questions: [] — nothing to answer or escalate. Each declared deviation, answered:

  1. Files beyond the claimed surface (api/automation-api.zod.ts and its test, flows.mdx, three regenerated artifacts) — ACCEPTED: the wire spelling is bound to the interface by the identity pin and reds without it; the docs half is what [Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B's updated here sense names for the docs; the artifacts are the ones ① items 2 and 6 verify.
  2. No new translation keys — ACCEPTED, verified in ① item 9.
  3. Skills half not written here; Skills-docs: follow-up owed — skills/objectstack-automation/SKILL.md — ACCEPTED as a deferral, with one correction of its basis. Under SKILL.md:313 and ruling 6431004737 (letter B) the Skills-docs: line is OWED only when the kind is added / semantic / narrowed or the Clause-② arm is (narrowing); this PR is none + (widening), so by rule no line is owed at all, and the triage sentence Skills, docs and eval are owed ([Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B) was written on the premise of a protocol kind that the diff did not turn out to have. The dev wrote the line anyway, and a line that is present must be true (review-checklist.md:18): follow-up owed — FILES is a legal value, and the seat rewrites it to #N before ACCEPT, or the dev rewrites it to none — REASON. Deferring the skills/** edit is the correct split, not an omission: skills/** is Tier H (Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14), one such path would make the whole PR Tier H, and the directive's own prescription is to split off the governed files. The follow-up has real content — skills/objectstack-automation/SKILL.md:91 teaches the screen NODE's config and nothing about the paused payload a custom runner reads — so filing it is right; it is a seat duty on ACCEPT, not a must-fix on this head.
  4. Two wip: commit subjects — not reviewed here (squash-merge takes the PR title).
  5. app-shell type-check needed a local build first — environment; CI's Type Check UI measures it and is in progress.

One thing the body says that the tree does not quite bear out, for the lane seat's TIER_DEFAULT read of the prose face (outside hits, so not a finding of this record): content/docs/automation/flows.mdx opens its new section with Every paused screen carries progress, and three paragraphs later correctly says a child-run screen carries it only when its own flow shows another screen; the contract's TSDoc (a hits file, and right) says absent on an older server too. The first sentence should read a paused screen carries progress whenever the engine can answer or similar.

④ Design judgment

packages/spec/src/contracts/automation-service.ts — ScreenSpec.progress / ScreenProgressSpec. Judged against the executor that fills it and the runner that reads it, and against the scenario the card accepts it for (a wizard reads Next, Next, Submit and counts steps only when the count is exact; the client never walks the graph).

  • Tri-state over a boolean. Tried to break it with a branch after a screen: a boolean must guess; 'possible' says what the graph knows and the runner reads it as Next, which is the one label that is never a lie before a later screen (a run that instead finishes is answered by the completion toast). HOLDS.
  • Counts present only when exact; the object absent only when the engine cannot answer. Tried the three "absent" states against the executor: no progress is served when the executor runs outside a run (context.flowName unset), when the node is not a screen of the registered flow, and when a child-run screen's own graph ends. The TSDoc names only the last of these plus the older-server case. The runner's behaviour is identical in all four (keeps its old label), so nothing a consumer does differs; the TSDoc's cannot answer at all is simply under-enumerated. SHOULD-FIX (doc precision): list the first two or say including before the examples.
  • step without stepCount. The contract allows it ({ nextScreen: 'possible', step: 1 }), the schema allows it, the runner draws nothing without both, the docs say so. Consistent on all four surfaces. HOLDS.
  • A pause that serves no screen (approval, wait) counts as a possible end of the stretch. Read against the engine: only a screen executor sets NodeExecutionResult.screen and only subflow-node.ts forwards a child's screen (map-node.ts returns suspend with a correlation and NO screen, so a map's child screen never rides the parent's paused result). The role map — subflow → 'subflow', any other supportsPause: true descriptor → 'pause' — is therefore exactly the set of nodes that can serve a screen versus merely pause, and the engine itself refuses a pause from any type that does not declare supportsPause, so an approval executor registered by a plugin is classed 'pause' by invariant. HOLDS.
  • The child-run clamp. $parentRunId is set by both subflow-node.ts and map-node.ts on the child context and is persisted with the child run, so a child screen keeps only a non-'none' nextScreen and no counts. Correct in both directions: 'certain' / 'possible' inside the child are still true in the parent's run; 'none' and every count are not knowable from the child graph. The PR body is honest that a subflow-composed wizard therefore keeps Submit on the child's last screen — the claim is as narrow as the enforcement (Prime Directive chore: version packages #10). HOLDS; a parent-aware read is a separate card if anyone asks for it.

packages/spec/src/api/automation-api.zod.ts — screenProgressSpecShape. Judged against the interface it mirrors and the response it rides on.

  • The enum, the integer bounds and the optionality match the TSDoc exactly; the new test refuses 'maybe', a missing nextScreen, step: 0 and 1.5, and keeps both the full and the counts-less shape. The identity pin compiles. The .describe() texts restate the contract without adding a rule the interface lacks. HOLDS.
  • Lenient (z.object) over strict: the sibling shapes on the same response are lenient for a stated reason, and a strict mirror would make this one key the only place where an older producer's extra byte refuses a whole paused run. Right choice. HOLDS.

Cited engine code, read to judge the contract (not reviewed): readScreenProgress against traverseNext. Tried to break visitShape's model of how a visit continues against dag-traversal.ts:

  • Unconditional out-edges run as a parallel fan-out; a decision's conditional edges are exclusive-first unless mode: 'inclusive'; isDefault runs only when no conditional sibling held; a fault edge runs only on failure. visitShape encodes each of these: fansOut on more than one unconditional-or-default edge, on inclusive-or-non-decision conditional pairs, and on an unconditional edge beside a conditional one on an exclusive decision; mayStop when nothing unconditional or default follows, or when the node may pause or hand off to a child. MATCHES.
  • One shape the model over-reports: a decision authored with config.conditions[] and LABELLED unconditional edges (logic-nodes.ts:86 returns branchLabel: cond.label; traverseNext then narrows to the claimed edges) is read as a parallel fan-out of every labelled edge. nextScreen stays right (the 0-1 BFS takes the minimum over all branches, which is the right answer for an exclusive choice as well), but fansOut becomes true and every count is dropped — a label-routed wizard never gets a counter, even on a straight path before the decision. Conservative, never wrong, and the docs' authoring advice (branch only after the last screen) already steers authors past it. SHOULD-FIX (precision): treat the labelled out-edges of a decision that carries conditions as exclusive in visitShape; a candidate line for the follow-up card, not a blocker.
  • 'none' by reachability includes fault and back edges; 'certain' is the fewest surely-pausing screens on any path to a point where the run may stop, with the current screen's own mayStop seeding 0; loops collapse by SCC and a loop that holds a screen yields no count; a screen whose fields the caller may have supplied is 'maybe-screen' and judged with the SAME judgeHeadlessScreen and the SAME persisted context the executor will use when it reaches that screen, so the two verdicts cannot disagree. Each of the thirteen engine pins and five graph pins reads as a true statement of this model. HOLDS.
  • The role read uses the raw n.config of the registered flow while the executor reads the parsed ScreenConfigSchema; the three keys compared (objectName, waitForInput, fields) carry no schema default, so raw and parsed agree. HOLDS.

Must-fix: none. Should-fix: the TSDoc's under-enumerated absence cases (contracts/automation-service.ts), and the label-routed-decision precision in readScreenProgress (cited code; a follow-up line). Neither changes what a consumer must do with the key.

Implemented-by: claude/project-thread-6imvw2
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

The flows page opened with "every paused screen carries progress", which
its own subflow paragraph and the contract contradict. Name the absent
cases there and in the ScreenSpec.progress TSDoc: no flow name, a node
that is not a screen of the registered flow, no start node, a child-run
screen with no further screen, an older server.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh5Z4kFNVFmensPenL
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T15:14Z

Head 094a249d92dcba0fb134ea601be98e95259eddcb. Card #26657, claim 6442765961, dev report 6444639574.

  • Contract review: owed at CONTRACT_REVIEW_TIER for api/automation-api.zod.ts and contracts/automation-service.ts. PASS 6444906744 at 653293e7, with no must-fixes. The one commit since then, 094a249d, lands the findings only:

    • The ScreenSpec.progress TSDoc now lists each case where progress is absent (no flow name, a node that is not a screen of the registered flow, no start node).
    • flows.mdx no longer opens with "Every paused screen carries progress", which contradicted the contract.

    The diff from 653293e7 to 094a249d is those 2 files, +12/−5, with no code change.

  • Lane seat (rest of the diff, TIER_DEFAULT): the engine's readScreenProgress reads the graph the run resumes from. It answers possible on a branch, a fault edge, a loop back, a non-screen pause or a subflow, and it gives counts only when every path agrees. FlowRunner reuses common.next / common.submit / form.stepOf, so no new i18n keys are needed.

  • Should-fixes, not carried:

    • A decision authored with config.conditions[] and labelled edges is read as a fan-out. Its counts drop while nextScreen stays right. This is a precision follow-up, not a defect.
    • The object-form step's "Save & Continue" on its last step is outside this card.
  • Skills-docs: #26756. The skills row is a Tier H follow-up, judged the correct split by the review. Nothing is owed by rule (kind none, widening).

  • Shape: Fixes #26657, 12 files. @objectstack/spec, @objectstack/service-automation and @objectstack/app-shell are patch. Clause-②: yes (widening) and Metadata-protocol: none: progress is engine-served, and no authored schema moved. check-governed-merges --pr 26752: NOT governed, 809 changed lines.

  • Gates: check-runs on this head are 58 success, 7 skipped and 0 red. One PR-body guards run was cancelled and superseded by a success after the body edit.

  • Landing: Tier S. pr_ready + automerge_enable in this act.

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

Projects

None yet

2 participants