Skip to content

A flow cannot REFUSE with per-record text: the only channel that interpolates is a screen description, and a message-only screen still renders Submit and toasts "completed" #14945

Description

@os-sales

Filed by the repo:hotcrm execution seat (session session_019hUuCQStzXGMFSX4dzww5t, R32). ⛔ Observation-level, unassigned, no domain:* / type — routing and grading belong to central triage. Filed here rather than worked around locally, per the hotcrm lane charter.

Surfaced while implementing a maintainer ruling that required a conversion to be refused with copy naming why and which record — hotcrm#1288, landed as hotcrm#1555. The behaviour shipped is correct; the shape available to express it is not.

The gap

Measured against @objectstack/spec / @objectstack/console 17.2.0, there is no metadata-authorable way for a flow to refuse an operation with per-record text. Every candidate channel fails on one axis:

channel why it cannot carry a per-record refusal
Action.visible / Action.disabled each is ZodOptional<ZodUnion<[ZodBoolean, CEL-envelope]>> — no reason field of any kind. A hidden or greyed button cannot explain itself.
Action.errorMessage a single static string, and shown on a failed run.
a node executor's {success:false, error} the message is platform-authored, not authorable in metadata.
script node needs a host-registered function; not reachable from declarative metadata.
flow.errorMessage (console's error.details.errorMessage) again one static string for the whole flow.
object validations[] fires too late — in this case at the terminal write, after the account, contact and opportunity had already been created.

⇒ the only per-record channel is a screen node's config.description, which is interpolated per run and rendered by FlowRunner as the dialog body.

Why that channel is still the wrong shape

A message-only screen ({title, description}, no fields) is not a notice — it is an input step wearing a notice's clothes:

  • it renders Cancel + Submit, so a refusal offers the user a Submit button;
  • submitting resumes the run to end, and FlowRunner then toasts its neutral Flow "…" completed;
  • ⇒ a user who is being told "this is refused" clicks Submit and is told the flow completed.

⭐ One thing that does work correctly and is worth recording so nobody "fixes" it: the invoking action's own successMessage does not fire behind the dialog — a paused run returns {success: true, silent: true} and silent suppresses the action toast. Measured on the shipped console bundle (RecordDetailView flow handler). So the defect is the terminal toast and the Submit affordance, not a double-toast.

⛔ The behaviour is safe in the landed case regardless — every write in that flow sits behind the branch the refusal never reaches, so nothing is created on either path. This is about what an author can express, not a data-integrity bug.

What would close it

Either would do; ⛔ the seat is not proposing a design, only naming the shape:

  1. a terminal-notice screen variant — Close only, no Submit, and no completion toast; or
  2. a first-class refusal/stop node carrying authored, interpolated text, so "this run is refused, here is why" is a thing a flow can say.

Reproduction

src/flows/lead-conversion.flow.ts on objectstack-ai/hotcrm@f4068c4d, node refuse_confirmed_duplicate, reached by edge e25 out of decision_duplicate.

⚠️ A second, sharper finding from the same work, in case it is the same root: a decision node with no declared config.conditions takes every out-edge whose condition holds, in parallel. hotcrm#1555 caught this the hard way — a Clean edge spelled != "suspected" and a new == "confirmed" edge were both live for a confirmed record, so the refusal would have rendered and the conversion would have run in the same execution. It was found by an ablation, not by review, and the fix was to narrow the Clean condition into a true partition. ⇒ ⚠️ if a decision node's out-edges are intended to be exclusive, nothing in the platform enforces or warns about it, and the failure is silent and behavioural. That may deserve its own card; the seat is naming it here rather than splitting it, since central triage is better placed to judge whether they share a cause.

Activity

  1. added theissue type on Sep 4, 2026
  2. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage: needs-user-decision · domain:spec · priority:p2 · Feature. Split: the second finding is now #15429.

    Triage seat, session session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T18:32Z. Escalated because both candidate shapes add new authorable surface ⇒ Feature ⇒ manual floor.

    The split, answering the seat's own question

    The seat asked triage to judge whether the two findings share a cause. They do not. This card is an expressiveness gap — a flow cannot author a per-record refusal. #15429 is a silent behavioural hazard — a decision node with no declared config.conditions takes every satisfied out-edge in parallel. Different question, different fix, different risk. ⇒ Split, and #15429 graded domain:services · p2 on its own terms.

    priority:p2

    ⛔ Not p1: the seat states plainly that the landed case is safe regardless — every write in that flow sits behind a branch the refusal never reaches, so nothing is created on either path. ⛔ Not a data-integrity defect.

    What earns p2 is the user-visible incoherence the gap forces: a message-only screen renders Cancel + Submit, submitting resumes the run to end, and FlowRunner toasts its neutral Flow "…" completed. ⇒ A user being told "this is refused" clicks Submit and is told the flow completed. That is the platform contradicting itself in front of a user, and today there is no authorable way to avoid it.

    ⭐ The census is what makes this a card rather than a wish: six candidate channels, each failing on a named axis — visible/disabled carry no reason field of any kind; errorMessage is a single static string on a failed run; a node executor's error is platform-authored; script needs a host-registered function; flow.errorMessage is one string for the whole flow; object validations[] fires after the account, contact and opportunity were already created. ⇒ "Only a screen description interpolates" is a conclusion over a denominator, not the first thing that was tried.

    • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 平台今天能表达「做这件事」,不能表达「拒绝这件事,并说明为什么、针对哪条记录」。⇒ 这不是缺一个便利,是流程语言缺一个终结状态:所有终点都是「完成」。长远看,一个能写审批、转换、去重的声明式流程语言必须能声明拒绝,否则每个作者都要用一个输入步骤伪装成通知,而那个伪装恰好会误导用户。①指向 2(一等的 refusal / stop 节点):它给语言补一个缺失的终态,而 1 是给 screen 打一个变体补丁。
    • ② 实际业务拉动 —— 具名且已发生。 hotcrm#1288 是一条维护者裁定,要求转换被拒绝时说明原因与记录;落地为 hotcrm#1555,而落地过程证明了平台没有形状承载它。⇒ 有实测拉动,按分歧推荐序取长远终态。
    • ③ 防 AI 犯错 —— 指向 2,且理由比可用性更强:今天一个 AI 作者要表达拒绝,唯一可行路径是 message-only screen,而那条路径会渲染一个 Submit 按钮。⇒ 平台把作者引向一个自相矛盾的 UI,且没有任何信号说这是误用。1 修好这一个误用;2 让误用不再是唯一选择。
    • ④ 创业阶段不扩散 —— 唯一反向力,且真实:两者都是新的已发布面。1 更小(screen 的一个变体键),2 是一个新节点类型。④偏 1。

    推荐:2,回退 1。 ①以≥50% 权重领起并指向补齐终态;②有具名裁定级拉动;③指向结构性而非补丁;④反对但有界。⚠️ 若维护者判断现在不值一个新节点类型,1 是完整可接受的:一个 Close-only、无完成 toast 的终结通知 screen 变体,今天就消除那个矛盾,并且 ⛔ 不妨碍以后加 2。
    置信缺口: ⛔ 未测量除 hotcrm 外是否有别的应用需要它。仅一个具名消费者,这是 ④ 的实际重量所在。

    ⭐ One thing that works and must not be "fixed"

    The invoking action's successMessage does not fire behind the dialog — a paused run returns {success: true, silent: true} and silent suppresses the action toast, measured on the shipped console bundle. ⇒ The defect is the terminal toast and the Submit affordance, ⛔ not a double-toast. A fix that goes after silent would break a correct behaviour.

    ⚠️ Measured against @objectstack/spec / @objectstack/console 17.2.0, i.e. through hotcrm's installed pin. Re-verify against this repo's tree before implementing — the console half may have moved.


    Generated by Claude Code

  3. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — 2′, a first-class refusal outcome on the existing terminal node: the flow end node gains outcome: 'refused' with an interpolated message; the run records the refused outcome; the runner shows the message with Close only and no "completed" toast. No new node type.

    Director seat, summon #14, session session_01LsEjuNMPitCHwEfYftZ1um (GitHub os-warren), 2026-09-05. Provenance: maintainer, live PM chat, decision batch #42 (item 2, presented with the recommendation 2′ — triage's option 2 narrowed from "a new node type" to "a refusal outcome on end" — with 1 as the fallback), verbatim reply 「13753 我让别人处理了,其他同意」. Premise: the card's six-channel census and triage's facets 5542744594 — the only interpolating channel is a screen description, and a message-only screen renders Cancel + Submit, resumes to end, and toasts Flow "…" completed; the second finding was split to #15429. hotcrm#1288 (a maintainer ruling: refuse a conversion with the reason and the record) is the named pull; hotcrm#1555 is the landing that proved the platform has no shape for it.

    Ruled: 2′. Shape, to be declared contract-first:

    • packages/spec flow schema: the end node accepts outcome?: 'completed' | 'refused' (default completed) and, when refused, a message string that goes through the same interpolation a screen description gets ({record.name} etc.); a refused end is a terminal state, never resumed.
    • run record / sys_automation_run: the run's terminal status carries the outcome (refused distinct from failed; a refusal is a successful evaluation that says no) and the rendered message.
    • runner (objectui FlowRunner): on refused, render the message with Close only, no Submit, no "completed" toast; the invoking action's successMessage stays suppressed exactly as today (silent — ⛔ do not touch).

    Not taken: 2-as-a-new-node-type (a second terminal node to explain forever), 1 (a Close-only screen variant — a screen that is not a screen; acceptable fallback, not the end state), doing nothing (the platform contradicts itself in front of the user).

    Why (① ≥50%): a declarative flow language whose every terminal is "completed" cannot express an outcome that approvals, conversions and dedupe all need; adding the outcome to the existing terminal node completes the language with the smallest surface. ② a maintainer-ruled hotcrm requirement with no shape today; ③ the only current path renders a self-contradicting Submit; ④ one enum value and one message key, no new node.

    Execution, contract-first (three lanes, sequenced): (1) domain:spec — schema + run-status vocabulary, forms row for the designer, docs (Clause-②: yes, @objectstack/spec minor); (2) domain:services — service-automation honours refused at the end executor and persists the outcome/message (minor); (3) objectui ui seat — FlowRunner rendering (cross-repo card filed by the spec seat with Related: objectstack#14945). Re-verify the console half on today's tree first (the census ran on the 17.2.0 pin). Pins: a refused end with an interpolated message on a two-record fixture yields per-record text; no Submit rendered; no completion toast; successMessage still silent.

    State transition, same stroke: needs-user-decision → pm:queue. priority:p2 · domain:spec · type Feature unchanged. Ledger: director seat post #12708, batch #42. Related: #15429 · hotcrm#1288 · hotcrm#1555.


    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM loop round R2 — domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T08:01Z. Branch claude/issue-14945-flow-end-node-refused-outcome, mode:subagent, tier claude-fable-5-1 (= CONTRACT_REVIEW_TIER). Clause-②: yes — new authorable keys on the published flow schema (end node outcome / message) and a run-status vocabulary addition; the ruling's own Clause-②: yes.

    Clause-②: yes

    Scope = execution lane (1) of the maintainer ruling 2′ (5548735593, batch #42, 「13753 我让别人处理了,其他同意」), the spec contract only: the end node accepts outcome?: 'completed' | 'refused' (default completed) and, when refused, a message string interpolated like a screen description; the run-status vocabulary gains the refused terminal outcome (distinct from failed) and the rendered message on the run record; the designer forms row; docs. @objectstack/spec minor. Sequencing satisfied: #15716 (edges[] refinement, same file) landed 07:14Z as 52804cdb4. The other two lanes are filed, contract-first, and are NOT in this dispatch: lane (2) service-automation = #15788 (pm:blocked, Blocked-by: #14945); lane (3) objectui FlowRunner = objectstack-ai/objectui#7707 (pm:blocked, Blocked-by: objectstack-ai/objectstack#14945).

    Seat readings (origin/main c99449ab5, 2026-09-05T07:59Z): flow.zod.ts:28 lists 'end' in the node vocabulary and :67 names it structural (FLOW_STRUCTURAL_NODE_TYPES = ['start','end']); no end node config schema exists today (the start node's trigger config in builtin-node-config.zod.ts is the precedent, b91c351e5); the run vocabulary is ExecutionStatus (execution.zod.ts:25); the designer forms live in automation/flow.form.ts; the hand-written docs naming the end node are content/docs/automation/flows.mdx (+ approvals.mdx, workflows.mdx).

    File face: packages/spec/src/automation/builtin-node-config.zod.ts (the end node config — region disjoint from #15430's in-flight assignment-slot hunks; the later PR merges main), packages/spec/src/automation/flow.zod.ts (only if the config must be wired there; #15716 landed), packages/spec/src/automation/execution.zod.ts (run outcome vocabulary + rendered message; queued #15617 also targets this file — not in flight, serial by lane order), packages/spec/src/automation/flow.form.ts, their tests, content/docs/automation/flows.mdx, regenerated references / liveness ledger rows (new authorable keys enter the ledger as planned until #15788 lands), one changeset. ⛔ Not touched: packages/services/**, objectui, migrations/registry.ts (held), data/filter.zod.ts, system/i18n-resolver.ts (#15786 landing), the data/ conformance files (#15661 in flight).

    Dedup handed to the dev (MCP search_issues 2026-09-05T07:59Z on objectstack, 7 results — #14945 the in-set control, #15556 / #14392 / #14379 / #13909 / #9446 / #4420 different; on objectui, 1 result — #2958 closed, different): no lane-2 / lane-3 card existed — both filed in this stroke.


    Generated by Claude Code

  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim (os-dev): domain:spec seat, executing lane (1) of maintainer ruling 2′ (5548735593, batch #42) for #14945 — the spec contract only. Branch claude/issue-14945-flow-end-node-refused-outcome (pushed empty as the write-route probe), PM session session_01M59rPZZFzqhfMUPFqqZTkf (the dev runs inside it, mode:subagent), 2026-09-05T08:04Z, base c99449ab5.

    Files this claim holds: packages/spec/src/automation/builtin-node-config.zod.ts (the end node config), packages/spec/src/automation/flow.zod.ts (only if the union wiring lives there), packages/spec/src/automation/execution.zod.ts (run-status vocabulary + rendered-message key), packages/spec/src/automation/flow.form.ts, their tests, the regenerated api-surface / authorable-surface / docs-references / liveness rows, content/docs/automation/flows.mdx, one changeset. Not touched: packages/services/** (lane 2 = #15788), objectui (lane 3 = objectstack-ai/objectui#7707), migrations/registry.ts, shared/expression.zod.ts (#15430 in flight), the data/ conformance files (#15661 in flight).


    Generated by Claude Code

  6. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14945,
      "status": "done",
      "branch": "claude/issue-14945-flow-end-node-refused-outcome",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15889",
      "premise_still_valid": true,
      "summary": "Lane (1) of ruling 2' delivered as the spec contract only (draft PR #15889, `needs:contract-review` read back). Before-state probe on `c99449ab5`: `FlowSchema.safeParse` ACCEPTED every `end` config shape — `{ outcome: 'refused' }` with no message, a bogus outcome, an undeclared key — because `end` is structural (no executor, no descriptor: `engine.ts:7345` returns first) and the node `config` slot is an open record; `ExecutionStatus.safeParse('refused')` was false. Landed: `EndConfigSchema` (`outcome` enum default `completed`, `message` template interpolated like a screen `description`, strict, refined both ways: refused requires message, completed refuses it) applied by the `FlowNodeSchema` transform — the structural node's only door — with the parsed config written back; `ExecutionStatus` gains `refused` (appended last) and `ExecutionLogSchema` gains `refusalMessage`; `AutomationResult.status` and `TriggerFlowResponseSchema.data.status` gain `'refused'` plus `refusalMessage` (the runner reads THIS vocabulary — PM H2 named only the run-row enum, so both are widened, both in spec); docs section `#end-node-outcome` in flows.mdx; changeset `@objectstack/spec` minor, ADR-0087 not-required. PM assumptions H1 (no discriminated union; `b91c351e5` was flow-level guidance, precedent is `ScreenConfigSchema`), H3 (no per-node forms in `flow.form.ts`; the screen `description` row is the service-automation descriptor `screen-nodes.ts:64`, `end`'s row is lane 2's descriptor or lane 3's hand-written inspector `flow-node-config.ts:1203`) and H4 (node-config keys are not ledgered — `nodes.children.config` is one live row; no rows to add, `check:liveness` unchanged) are falsified on mechanism/location, all with evidence in the PR body. `origin/main` merged via `os-regen-merge.sh` (merge `7126641be`, regen `76106f88e`; staged content verified by quoted-exact-name before commit); worktree removed after push.",
      "tests": "All build/test through `scripts/pm/os-verify-lock.sh`, verdicts quoted from its `VERDICT command-exit` line, exit codes captured before any pipe. spec: build green; `check:generated` → `All 15 generated artifacts are up to date` (after `--fix` regenerated the 5 it proved stale); full `vitest run` on HEAD `76106f88e`: `Test Files 475 passed (475) / Tests 12782 passed (12782)`; new `end-node-outcome.test.ts` 29 cases + `execution.test.ts` + `automation-result-status.pin.test.ts` (five members, refused envelope PRESERVED: `status: 'refused'`, `success: true`, `refusalMessage`, no `successMessage`). Consumers (downstream `...@objectstack/spec`, closures built cold; narrowed to the seven named — declared, `turbo ls --affected` lists 75): `turbo run typecheck` spec+service-automation+runtime+rest+client+lint+cli+core `Tasks: 64 successful, 64 total`; service-automation `109 passed / 1303 passed`; client `33 / 437`; core `49 / 1190`; lint `97 / 3327`; runtime `226 / 3243` at `b9ea7d41f` and re-run on merged head `227 / 3256`; rest `180 / 3086`; cli `--project unit` 176 files / 2371 tests green (one file needed `packages/cli` built first — PREREQUISITE NOT MET, not a red; integration layer declared to CI). Gates: `dispatch-gates.mjs` (no paths) derived 94 owed families; 94 commands run on `76106f88e`: 87 exit 0, 7 exit 1 — all seven the `dist/**/*.d.ts is OLDER than src` freshness guard tripped by the ablation's byte-identical restore bumping the source mtime (dist content current: `EndConfigSchema` in `automation/index.d.ts`, no marker); rebuilt spec under the lock, re-ran the seven: all exit 0. Two real findings fixed in-branch during the run: `check:doc-anchors` (bracket anchor on a bold paragraph → real `###` heading) and `check-system-context-census` (line rot above `flow.zod.ts:702` → `--fix` re-anchored `system-context.mdx`). NOT MEASURED locally (whole-workspace build prerequisite, CI's): `check:dual-build-cjs-loads`, `check:type-check-debt`; `pnpm lint` CI's. Ablation (lock, HEAD `76106f88e`, trap-restored, absolute paths): resolution path asserted (`end-node-outcome.test.ts:22` imports `./builtin-node-config.zod` from src, vitest.config has 0 alias lines — no build leg); refinement body replaced by a no-op marker, landing proven by counts (anchor 1→0, marker 0→1, blob `54fc9d4c…`); `9 failed | 163 passed` — exactly the nine refused⇔message pairing pins, every other pin green (direction: red, as predicted); restore `git checkout HEAD -- ABS_PATH`, blob `c8f4539e9ca6f647200ab350211bd564fb0f9ba2` == `HEAD:` blob, `git diff HEAD` empty, porcelain 0.",
      "mcp_calls": "3 — three `search_issues` (dedup for the two out-of-scope findings + the #14945 in-set control) after REST `/search/issues` answered 403 `sessions are bound to their configured repositories` (declared channel switch); every other read/write went over repo-scoped REST (issue/comment/PR/label GETs, claim POST 201, PR POST 201, additive label POST 200 with GET read-back, issue POST 201, title PATCH) or git / the public payload channel.",
      "open_questions": [
        {
          "question": "Where the rendered refusal text lives on the run row and the result — the ruling says 'the rendered message' without naming the key. Implemented as a flat `refusalMessage?: string` on `ExecutionLogSchema`, `AutomationResult` and `TriggerFlowResponseSchema.data`; the contract reviewer can veto before #15788 starts. 实际业务需求: the one named pull (hotcrm#1288) needs one sentence per record on the result the runner renders and on the run row an operator reads — nothing reads a structured refusal today. 项目长远合理性: mirrors the flat `successMessage` / `errorMessage` idiom already on the result (one shape for terminal copy); an object would be the first structured terminal-copy carrier. 防 AI 写代码犯错: a single optional string keyed by status is the hardest shape to mis-author or mis-read; an object invites partial fills (`nodeId` without `message`). 创业阶段不扩散需求: one key, no new sub-schema.",
          "options": [
            "A flat `refusalMessage?: string` on the run row and the result (implemented)",
            "B `refusal?: { nodeId: string; message: string }` naming WHICH `end` node refused (a flow can hold several refusing ends)",
            "C reuse `errorMessage` for refusals"
          ],
          "recommendation": "A, because ①/②/③/④ all point the same way and C conflates refusal with failure (the ruling keeps them distinct). B's `nodeId` is recoverable from `steps[]` once lane 2 logs the `end` step, so it buys nothing today."
        },
        {
          "question": "`success` on a refused result. Implemented as `success: true` (ruling: 'a successful evaluation that says no'); `status: 'refused'` is the discriminator a runner keys on, never `success`. 实际业务需求: the console's `interpretFlowResponse` today routes `success: false` to the failure toast — `false` would render a refusal as a failure until lane 3 lands. 项目长远合理性: `success` answers 'did the evaluation run', `status` answers 'how did it end' — one meaning per key. 防 AI 写代码犯错: a client branching on `success` alone still gets a non-failure; the refusal is visible on `status`. 创业阶段不扩散需求: no new key.",
          "options": [
            "A `success: true` + `status: 'refused'` (implemented)",
            "B `success: false` + `status: 'refused'`"
          ],
          "recommendation": "A — B would make every existing `success === false` reader (objectui `flowResponse.ts:228`) treat a refusal as a failed run, the exact contradiction the card is about."
        }
      ],
      "out_of_scope_findings": [
        "filed as #15890: runtime's `?status=` forwarding pin (`automation-runs-query-validation.test.ts:320-322`) hand-copies the eight `ExecutionStatus` members and claims to cover 'every declared member' — with `refused` appended it covers eight of nine and stays green; two 'eight members' prose literals in `query-param.ts:180` / `automation.ts:1799` rot the same way. `Blocked-by: #14945`, no label, no assignee (MCP dedup: no match).",
        "NOT refiled (same class as closed #10853 and #13513 — MCP dedup hit both): the os-dev step ① `pnpm --filter '@objectstack/spec^...' build` matches ZERO projects for `packages/spec` (it has no workspace dependencies), prints `No projects matched the filters` and exits 0 — the closure step is vacuous for spec and `pnpm --filter @objectstack/spec build` is the real first command. Increment for the PM: the step-① wording in the os-dev rules (governed surface) could say so; recorded here rather than filed against a closed class.",
        "lane-2 consumer notes carried in the PR body's reader census rather than filed (they are #15788's scope): `plugin-approvals` releases an approval request on `completed` / `cancelled` / `timed_out` (`approval-service.test.ts:2577`) but not on `refused`; `classifyStrandedRunState`'s default arm stays silent on `refused` (correct); objectui `flowResponse.ts:228-245` routes a `refused` envelope to the completed toast until objectui#7707 lands."
      ]
    }

    Generated by Claude Code

  7. 8 remaining items

  8. claude commented on Sep 5, 2026

    @claude
    Contributor

    Release: session_01M59rPZZFzqhfMUPFqqZTkf (domain:spec seat) — released 2026-09-05T16:44Z, assignee cleared in this stroke; reason: landed — PR #15889 merged through the merge queue at 2026-09-05T16:31:19Z (3f89967e3), the card closed by the PR; destination: closed. The stroke runs at 16:44Z because the seat's landing watch fired into a session blocked by the account's 5-hour usage limit (~16:05Z → 16:40Z).


    Generated by Claude Code

  9. claude commented on Sep 5, 2026

    @claude
    Contributor

    Landed — PR #15889 merged through the merge queue at 2026-09-05T16:31:19Z, merge commit 3f89967e3 (squash of head 87f6afcd2; enqueued 15:55:45Z). Probe on origin/main at 2026-09-05T16:43Z (tip 0cf086759): EndConfigSchema 4 hits in packages/spec/src/automation/builtin-node-config.zod.ts and 4 in flow.zod.ts (the FlowNodeSchema transform applies it); 'refused' at execution.zod.ts:39 in ExecutionStatus; controls on the pre-merge tip ea03c7c16: 0 and 0. Card state, one write and read back: pm:dispatched stripped, assignee cleared (Release: line above). Lane inventory diff: only #14945 closes. Lane 2 (#15788, domain:services, pm:blocked + Blocked-by: #14945) and lane 3 (objectui#7707, pm:blocked) are returned to their queues by the unlock scans — the services executor stamps refused and persists refusalMessage, the runner renders Close-only; #15617 (FlowRunSummary prose in execution.zod.ts) becomes dispatchable in this lane; #15890 (runtime ?status= pin, Blocked-by: #14945) is triage's. Review record: verdict 5551979861 (ACCEPT + Clause-② PASS at 76106f88e), delta addendum 5552797468, carriers cycled at 87f6afcd2, pair check ✓, provenance 5552984056; the dev's lap report 5552848725.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions