Skip to content

feat(spec,service-automation): FlowRuntimeState carries WHY a flow is not armed - #18635

Merged
os-bill merged 3 commits into
mainfrom
claude/issue-18235-flow-runtime-state-reason
Sep 17, 2026
Merged

os-bill merged 3 commits into
mainfrom
claude/issue-18235-flow-runtime-state-reason

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18235

Clause-②: yes

FlowRuntimeState gains reason — the optional sentence saying WHY a flow is not armed — and the automation engine populates it, so GET /automation/_status can finally tell a policy-disabled flow apart from a broken binding.

What was missing

Ruling G item 6 on #17396, verbatim:

OFF: neither trigger arms any flow. Every such flow is listed in getTriggerBindingAudit(), the CLI startup summary and Studio with a DISTINCT reason — disabled by deployment policy — ⛔ never as "binding failed".

PR #18198 delivered the first two surfaces and trimmed every published sentence that claimed the third, so nothing published was false. What was missing is that the third surface could not be BUILT: Studio's only status door is GET /automation/_statusgetFlowRuntimeStates()FlowRuntimeState, and that shape had no field a reason could travel in. Re-measured on origin/main at 7299b945a2 before writing a line:

packages/spec/src/contracts/automation-service.ts:499-517  interface FlowRuntimeState
  name / enabled / bound / status? / triggerType? / object?
DARK  occurrences of `reason` in that interface block   0
LIT   occurrences of `bound` in the same block          2

⇒ on the wire a policy-disabled flow was enabled: true, bound: false, triggerType: 'schedule' — byte-identical to one whose trigger is missing, which is the reading ruled item 6 forbids.

The three open questions, answered by measurement

1. Does the runtime producer move in this PR? YES. A declared-but-never-populated key is the ADR-0049 shape this repo files findings about, and the honesty rule is stricter still here: SCHEDULED_WORK_DISABLED_REASON's own docblock says ⛔ do not write that Studio reports this reason "until a reason reaches that wire shape: declared is not delivered". Shipping the key alone would leave that sentence exactly as false as it is today, and objectui#9217 exactly as blocked. The producer is AutomationEngine.getFlowRuntimeStates() (packages/services/service-automation/src/engine.ts), NOT packages/runtime/src/domains/automation.ts: that door reads the rows through the contract type and answers success({ flows, total }) — a verbatim pass-through with no field picking, so it needed no edit, only a pin.

2. Optional or required? OPTIONAL, measured. Producer set: the engine, plus the test doubles in packages/runtime (domain-handler-registry.test.ts, http-dispatcher.test.ts, automation-run-read-permission-gate.test.ts), packages/cli (serve-automation-summary.test.ts, serve-automation-shadowing.test.ts) and packages/qa/dogfood — every one of them writes { name, enabled, bound } at minimum. AutomationEngine implements IAutomationService, so a required key would also have turned its inline return type red. And semantically a required key would demand a reason from rows that have none: a bound flow, a disabled flow, a manual flow.

3. Closed union or free string? FREE STRING, matching what already ships. getTriggerBindingAudit() answers { flowName, triggerType, reason: string } — a human sentence from a three-branch vocabulary, the policy branch being the shared SCHEDULED_WORK_DISABLED_REASON constant; the CLI startup summary prints that string. status and triggerType beside the new key are free strings too. objectui#9217's acceptance item 3 says it outright: "The exact reason-code shape is the platform's … if the platform ships a string, render the string." ⛔ No fourth vocabulary was invented.

How the vocabulary is held to ONE

Both doors now read one private describeUnboundReason() on the engine — same eligibility rule (enabled, unbound, declares a trigger), same three branches, same policy sentence read from the RECORDED refusal (policyDisabledFlows) and ⛔ never re-derived from a live resolveScheduledWorkPolicy() call. _status is served on demand, arbitrarily long after the bind — a strictly worse case for re-derivation than the audit's two boot-time callers, and re-derivation is the exact defect #18198's own round caught and fixed. Pinned as an identity between the two doors rather than as two copies of the expected text, so a future edit to either wording fails instead of forking the vocabulary.

File surface

path what
packages/spec/src/contracts/automation-service.ts FlowRuntimeState.reason?: string + its docblock — the card's subject
packages/services/service-automation/src/engine.ts the producer: describeUnboundReason(), read by getFlowRuntimeStates() and getTriggerBindingAudit()
packages/types/src/env.ts docblock only — this PR makes SCHEDULED_WORK_DISABLED_REASON's "Studio is NOT one of them" paragraph false, so it is corrected in the same landing (wire yes, rendering still objectui#9217)
content/docs/automation/flows.mdx docs prose only — its callout said the status door "has no field to say why", which this PR makes false; corrected on the same ground as env.ts (patch round)
packages/spec/src/contracts/automation-service.test.ts type-level identity pins + the runtime probe / lit / dark controls
packages/services/service-automation/src/engine.test.ts producer pins: policy row, anti-drift identity, record-not-environment, bind failure, DARK absence
packages/runtime/src/domain-handler-registry.test.ts the wire pin: _status carries reason through, and omits it where the producer wrote none
.changeset/18235-flow-runtime-state-reason.md @objectstack/spec minor, @objectstack/service-automation minor, @objectstack/types patch; Clause-②: yes (widening)

⛔ No generated artifact moved — see the DARK control below. Nothing under packages/spec/src/ui/, packages/spec/src/migrations/** or packages/spec/scripts/ is touched.

Red before green

check:generated is NOT a gate for this change, and that is a measurement, not an assumption: run on the clean tree it was green (15/15), and run again with the new field in place and a rebuilt dist — before regenerating anything — it was still green (15/15), because no spec artifact records interface MEMBERS (api-surface/contracts.json records "FlowRuntimeState (interface)", export-origins/contracts.json records its origin, declaration-map/ has no contracts.json at all, and api-surface-signatures.json has zero FlowRuntimeState hits). A gate never observed failing is not known to be a gate — so the gate that WAS observed failing is check:test-typecheck (CI's required TypeScript Type Check job), plus the producer pins:

LEG 1 — the producer stops publishing the key. Mutation written to disk and proved there (anchor occurrences 1 → 0, blob ec75d9cf29716a38):

Tests  4 failed | 154 passed (158)
  × OFF: the status row names the policy, and ⛔ NOT a binding failure
  × the two doors answer the SAME sentence for the same flow — one computation, no drift
  × the status row reports what HAPPENED, not what the environment says when it is read
  × ON: a genuine bind failure reads as one on the status row too

⭐ LIT control inside the same run: the 154 that still pass include every pre-existing ruling-G audit pin — the sibling door is untouched — and the DARK absence test passes in the mutated tree too, which is what a well-formed absence assertion must do.

LEG 2 — the contract stops declaring the key (anchor 1 → 0, blob 8d9231d40f89941c):

check:test-typecheck: 2 problem(s)
  • src/contracts/automation-service.test.ts: 4 type error(s) … ARRIVED: TS2339: Property 'reason' does not exist on type 'FlowRuntimeState'.
  • src/contracts/automation-service.test.ts: 1 type error(s) … ARRIVED: TS2353: Object literal may only specify known properties, and 'reason' does not exist in type 'FlowRuntimeState'.
LEG2 TYPECHECK EXIT=1

⚠️ Reported as observed, not as the template predicts: in LEG 2 the spec UNIT suite stayed green (17/17). The docblock pin reads the interface's comment, which the mutation left in place; it is check:test-typecheck that covers the key's existence. Both legs restored from HEAD under a trap, and the restore is proved by blob identity (ec75d9cf / 8d9231d4 both back) plus an empty git diff HEAD, not by an exit code.

Evidence

  • pnpm --filter @objectstack/spec check:generated15/15 up to date, clean tree and again at the final tree.
  • check:api-surface, check:docs, check:authorable-surface, check:export-origins, check:declaration-map, check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability, check:exported-any, check:liveness, check:empty-state — all green. ⚠️ Five of them first answered PREREQUISITE NOT MET (packages/spec/dist older than src), which is NOT MEASURED rather than red; re-run after pnpm --filter @objectstack/spec build, all five exit 0.
  • Typecheck: @objectstack/spec, @objectstack/service-automation, @objectstack/types, @objectstack/runtime — all 0.
  • Tests: spec 484 files / 13821 passed; service-automation 136 / 1639; runtime 264 / 3655; types 22 / 683.
  • node scripts/pm/check-widening-tells.mjs --declaration yes --diff PRDIFF — exit 0.
  • Gate reconciliation: node scripts/pm/dispatch-gates.mjs --ran83 derived, 81 run, 2 NOT MEASURED. The two are check:dual-build-cjs-loads and check:type-check-debt, both exit 3 (PREREQUISITE NOT MET: they need a whole-workspace build closure) — declared to CI, not claimed as passes.
  • pnpm check:nul-bytes green, plus a direct control-character sweep of all seven touched files: zero hits.

⭐ DARK control

  • No OTHER interface in contracts/** gained a member: the whole diff under packages/spec/src/contracts/ is one reason?: string; line plus its docblock, inside the FlowRuntimeState block.
  • No published payload outside FlowRuntimeState moved: every generated artifact is byte-unchanged (git status after the field + rebuild showed only source files), and check:generated agrees at 15/15.
  • On the wire, a row the producer left without a reason carries no reason key at all — asserted on both the engine row and the _status response.

Patch round — the at-tier review's one FAIL, discharged

The isolated at-tier contract review returned FAIL on one residual and PASS on everything else. The residual: content/docs/automation/flows.mdx still carried #18198's sentence that GET /automation/_status "has no field to say why", so a policy-disabled flow "is indistinguishable there" — false the moment this PR adds the field, and false on exactly the ground used to correct packages/types/src/env.ts in the same landing. One prose carrier had been corrected and its twin missed. I agree with the FAIL on the merits; no objection recorded.

Corrected the same way and no further: the reason reaches the WIRE, read from the recorded refusal, and ⛔ reaching the wire is still not being rendered — whether a console shows it as a distinct state is that console's own change, which this page does not claim.

Own sweep, independent of the one handed to me: no field to say why / indistinguishable there hits this file only — the other hits are in core/security, objectql, rest and spec, all unrelated — against a lit control of 19 files under content/ that mention _status, and content/docs/releases/** (the only other automation/_status mention) is release-owned and untouched. ⇒ one residual, not a class. No pin reads this paragraph: the four tests and scripts that name this page cite other sections, so no test is owed.

Re-derived after the docs commit: 105 gate families (22 newly derived by the content/ path), reconciled by dispatch-gates --ran at 105 accounted / 103 run / 2 NOT MEASURED / 0 unrun. All 22 new ones green, including check:skill-examples (258 prose examples type-check across 3 surfaces), check:docs-transcript-drift, check:doc-security-posture, check:corpus-claim-drift, check:doc-anchors and check:docs-audit-scope; three of them first answered PREREQUISITE NOT MET on an unbuilt @objectstack/lint / @objectstack/client-react and were re-run after building those closures. docs-audit/check-affected-docs.mjs, check:doc-authoring, check:keyed-text-bounds, check:nul-bytes and check:generated (15/15) re-run green on the new head. Tests were not re-run: the patch-round diff is one MDX paragraph plus one changeset sentence, and the only gate that compiles docs prose (check:skill-examples) is in the green list above.

Acceptance notes

  • noted, not filed: packages/cli/src/commands/serve.ts's startup banner re-declares the getFlowRuntimeStates() row shape inline and does not name reason; it reads the audit for its unbound section, so nothing is wrong today and the banner needs no change. Carrier: whoever next touches that banner's row type.
  • to file (class (a), reproducible defect; dedupe words: cross-package-test-inputs · init-created-files-summary · packages/spec/dist walk · #15565 tree-scoped walk · gate vacuous without dist): pnpm check:cross-package-test-inputs exits 1 on any tree where packages/spec/dist is BUILT — packages/cli/test/init-created-files-summary.e2e.test.ts descends packages/spec/dist/ and no declared glob reaches inside it. Proven not to belong to this PR: with this branch's entire diff reverted in the working tree the gate still exits 1 with the identical finding, and on a checkout with no packages/spec/dist it exits 0 — i.e. it passes vacuously wherever the lint job has not built spec. Filing is the seat's act; this PR does not touch it.

⚠️ This branch is 5 commits behind origin/main at the time of writing; none of those commits touches any of the seven paths above (verified with git log BASE..origin/main -- THE-SEVEN-PATHS, empty). The merge queue rebuilds the PR as merged onto current main and re-runs the required set there.

Authored with Claude Code in session session_01JbZnqu8bt6YqfJsr9vaFb3 (both the delivery round and this patch round).


Generated by Claude Code

… not armed

Ruling G item 6 on #17396 names three surfaces that must each carry a
DISTINCT reason for a flow unarmed because package-authored scheduled
work is off, and must never read as "binding failed". The third surface
— a console — could not be built: Studio's only status door answers
`FlowRuntimeState` rows, a shape with no field a reason could travel in,
so a policy-disabled flow reached the wire as `enabled: true,
bound: false` — indistinguishable from one whose trigger is missing.

- `FlowRuntimeState.reason?: string`, additive: present only on an
  enabled, unbound row that declares a trigger.
- The engine's two doors read ONE private `describeUnboundReason()`, so
  `getTriggerBindingAudit()` and `getFlowRuntimeStates()` cannot drift.
- The policy sentence is read from the RECORDED refusal, never
  re-derived from a live environment read at call time.
- `SCHEDULED_WORK_DISABLED_REASON`'s docblock corrected: the wire now
  carries the reason; rendering it stays objectui#9217.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
The assertion read the sentence as one line; the docblock wraps it, so the
pin failed on formatting rather than on content. Same `[\s*]+` form the
neighbouring #16495 pins in this file already use.

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

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/service-automation, @objectstack/spec, @objectstack/types, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/types/src/env.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via getFlowRuntimeStates (symbol, a method of class AutomationEngine), getTriggerBindingAudit (symbol, a method of class AutomationEngine))
  • content/docs/deployment/environment-variables.mdx (via getTriggerBindingAudit (symbol, a method of class AutomationEngine))
  • content/docs/deployment/tenancy-modes.mdx (via getTriggerBindingAudit (symbol, a method of class AutomationEngine))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/types/src/env.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 137 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 cb04f45b24d85db9709a773fc41731e4246febebpackageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json cb04f45b24d85db9709a773fc41731e4246febeb

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

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 119/119 CONTRACT_REVIEW_TIER
Head-sha: 25ab3b0e7da7fc28a25186efb9e11147bfc40682

Isolated at-tier clause-② review of PR #18635 for card #18235 (ruling G item 6 on #17396, comment 5642381255, verbatim: 「OFF: neither trigger arms any flow. Every such flow is listed in getTriggerBindingAudit(), the CLI startup summary and Studio with a DISTINCT reason — disabled by deployment policy — ⛔ never as "binding failed".」). Every reading below is taken from the tree at the head or from the diff against the merge-base 7299b945a2 (7 files, +321 / −37, matching the PR's counts); no seat comment on the card or the PR was used as a finding of fact. Independent controls were run in an own detached worktree at the head, with every ablation restored by git blob identity and an empty git diff HEAD, never by an exit code.

① Derived judgments

  1. FlowRuntimeState.reason?: string (@objectstack/spec, packages/spec/src/contracts/automation-service.ts:517-544) — a new OPTIONAL member on a published interface. Additive by measurement: the type pin FlowRuntimeStateReasonIsOptional holds that { name, enabled, bound } still satisfies the contract, and the Eq pin holds the member at exactly string | undefined, so both dropping it and narrowing it to a closed union go red. Free string is the shape the two surfaces that already carry this reason use (getTriggerBindingAudit() answers reason: string; serve.ts:6878 prints it verbatim). Every sentence in its docblock checks against the tree (items 3 and 4). PASS.
  2. The producer AutomationEngine.getFlowRuntimeStates() (@objectstack/service-automation, engine.ts:4093-4126) — the public method's row gains reason?. Populated only when the flow resolves a trigger AND isFlowEnabled(name) AND is not in boundFlowTriggers; on every other row the KEY is absent (...(reason === undefined ? {} : { reason })), not undefined-valued. On the row the ruling names — a flow refused because the switch is off — the sentence is SCHEDULED_WORK_DISABLED_REASON and never contains "binding failed". Reproduced (LEG 1): 158/158 at head; with the producer's spread removed (blob ec75d9cf29716a38) exactly the four status-door pins fail and 154 pass, the DARK absence pin among them; restored to ec75d9cf. PASS.
  3. "One computation feeds two doors" — TRUE in the tree, not two computations that currently agree: getFlowRuntimeStates() (call at :4110) and getTriggerBindingAudit() (call at :4187) both read the private describeUnboundReason() (:4157-4168). The three sentences that left the audit's ternary and the three that arrived in the method are byte-identical (diffed mechanically: removed set = added set, size 3), so it is the audit's own vocabulary, not a fourth one wearing the same words. The audit's OUTPUT is unchanged: same eligibility predicate (enabled, unbound, resolves a trigger), same iteration set (this.flows.keys()). One behavioural delta, judged harmless: the audit now calls resolveTriggerBinding() for disabled and bound flows too — that method has no logger call and no throw path (:3240-3380), and getFlowRuntimeStates() already called it for every flow. PASS.
  4. "Read from a RECORDED refusal, not re-derived live" — TRUE. describeUnboundReason() asks this.policyDisabledFlows.has(name); that set is written at the bind refusal (:3413-3427), cleared the moment a flow passes the gate (:3434) and dropped with the flow on unregister (:4072). The only resolveScheduledWorkPolicy() call in engine.ts is the bind-time one at :3413. The pin "the status row reports what HAPPENED" flips the switch on after the bind and asserts the row still says policy — it is one of the four that go red under LEG 1, so it is live. A moved environment between bind and status call therefore answers the right thing. PASS.
  5. The wire door GET /automation/_status (packages/runtime/src/domains/automation.ts:1778-1790) is a verbatim success({ flows, total }) pass-through with no field picking, so reason reaches the wire unedited and stays absent where the producer wrote none; the new runtime pin covers both halves and passes at head (2 passed). No route in packages/runtime, packages/rest or packages/adapters exposes getTriggerBindingAudit() (its only consumers are plugin.ts:1130 and serve.ts:6878), so _status really is the only status door a console can read. PASS.
  6. packages/types/src/env.ts:290-302, outside the card's subject — the constant's VALUE is untouched (the template literal at :309-315 is context in the diff); only the docblock moves. Before: 「⚠️ Those are the surfaces that carry it, and Studio is NOT one of them. Its only status door is GET /automation/_status, whose rows are FlowRuntimeState … — a shape with no reason field at all — so a policy-disabled flow reaches Studio as bound: false and nothing more. ⛔ Do not write that Studio reports this reason until a reason reaches that wire shape: declared is not delivered.」 After: 「⚠️ [[finding] ruling G item 6's third surface has no platform half: _status carries no reason, so a policy-disabled flow is indistinguishable from a broken binding — and objectui#9217 is blocked on a card that does not exist #18235] Studio's own door carries it now: GET /automation/_status answers FlowRuntimeState rows … and their optional reason holds this sentence verbatim for a policy-disabled flow — read from the engine's RECORDED refusal, the same computation the binding audit reads. What is on the wire is the reason; what a console DISPLAYS is its own card (objectui#9217, open). ⛔ So do not write that Studio renders this distinctly until that lands — reaching the wire is not being shown.」 Every clause of the "after" is a fact of this tree (items 2-5), and the "before" is falsified by this diff at "a shape with no reason field at all". Judged a genuine consequence of the change, not scope creep. PASS on the edit itself — and it is the standard the next item is judged by.
  7. A published sentence this diff falsifies and does not correct — FAIL. content/docs/automation/flows.mdx:2117-2120 (hand-written tree, editable in a code PR; written by feat(types,triggers,service-automation,runtime,cli,spec,lint)!: package-authored scheduled work is a deployment decision, off by default #18198 at f04be62aa6, its own diff line 193) still reads: 「Studio does not carry that reason today: GET /automation/_status reports each flow as bound or not and has no field to say why, so a policy-disabled flow is indistinguishable there from one whose trigger is missing. Read the startup summary or the audit for the reason.」 After this diff _status HAS the field and the two rows ARE distinguishable there — the paragraph is false on exactly the ground the PR used to correct env.ts in the same landing. Card [finding] ruling G item 6's third surface has no platform half: _status carries no reason, so a policy-disabled flow is indistinguishable from a broken binding — and objectui#9217 is blocked on a card that does not exist #18235 records that feat(types,triggers,service-automation,runtime,cli,spec,lint)!: package-authored scheduled work is a deployment decision, off by default #18198's review re-derived the trim tree-wide at its head with zero residual claims; re-deriving tree-wide at THIS head (carry that reason / no field to say why / indistinguishable there / no reason field / Studio is NOT one over content/docs, every README.md, skills/, docs/ and non-test src) finds exactly this one residual. packages/triggers/trigger-schedule/README.md:48, content/docs/deployment/environment-variables.mdx:90 and tenancy-modes.mdx:332 stay true (they name two surfaces without claiming exclusivity). The docs-drift check on this PR named this page via getFlowRuntimeStates / getTriggerBindingAudit. Remedy, one paragraph in the same PR: rewrite that callout to say the wire now carries reason (the audit's own sentence, read from the recorded refusal) while rendering it distinctly in Studio remains objectui#9217 — the same boundary the corrected env.ts docblock draws. Nothing else in ① needs to move.

② Semver level

  • @objectstack/spec minor — correct: a new optional member on a published interface is a widening of the shape a consumer reads against; nothing removed or renamed.
  • @objectstack/service-automation minor — correct: a public method's rows carry a new populated field; the audit's output is unchanged.
  • @objectstack/types patch — correct: docblock only, ships inside the .d.ts; constant value unchanged.
  • @objectstack/runtime correctly absent (test-only change). No breaking arm, so no ADR-0087 disposition is due.
  • Declarations agree: changeset Clause-②: yes (widening), PR body Clause-②: yes; node scripts/pm/check-clause2-carriers.mjs --pair 18635 exit 0; node scripts/pm/check-widening-tells.mjs --declaration yes --diff on the merge-base diff exit 0. The changeset matches what the diff ships. The FAIL in ① item 7 is a docs correction in an unpublished tree and changes no level.

③ Boundary flags

  1. The two unsatisfiable acceptance lines — verified as reported, both halves. (a) No published artifact records interface MEMBERS: at this head FlowRuntimeState occurs in packages/spec/api-surface/contracts.json:94 only as the bare token "FlowRuntimeState (interface)" and in export-origins/contracts.json:94 as its origin; declaration-map/ holds 14 files and no contracts.json; api-surface-signatures.json, json-schema.manifest/, authorable-surface/ and content/docs/references/ have 0 hits. Nothing regenerates, so check:generated structurally cannot go red for this diff. (b) The substituted gate really covers the key's existence — reproduced (LEG 2): pnpm --filter @objectstack/spec check:test-typecheck exit 0 at head; with reason?: string; removed (blob 8d9231d40f89941c) exit 1 with ARRIVED: TS2339: Property 'reason' does not exist on type 'FlowRuntimeState' ×4 and TS2353 ×1 in src/contracts/automation-service.test.ts; that file is inside the tsconfig.test.json program, its only ledger entry is one TS2739 (a different signature), and the ledger is shrink-only and maintainer-only, so the red cannot be absorbed. Restored to 8d9231d4. The substitution is accepted.
  2. env.ts outside the card's subject — answered in ① item 6: genuine consequence, not scope creep.
  3. check:cross-package-test-inputs red on a built tree — not this diff; carriers exist, read by direct GET (no search used): [regression] check:cross-package-test-inputs 的裁决取决于被 gitignore 的空目录 packages/spec/dist 存不存在 —— 构建过的工作树一律红,CI 绿只因那一步跑在构建之前(#18340 引入) #18348 open, p1, pm:dispatched; [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 open, finding; [finding] check:cross-package-test-inputs answers 1 or 0 depending on whether packages/spec has been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440 closed as not planned. Nothing to file.
  4. serve.ts:6863-6870 inline row type omits reason — agree, no defect: the banner takes its unbound reasons from the audit (:6877-6878), and the wider engine row is assignable to the narrower annotation. Non-blocking.
  5. Dual-carrier open question (A/B) — moot at review time: both carriers now agree, --pair 18635 exit 0.
  6. NOT MEASURED locally (check:dual-build-cjs-loads, check:type-check-debt) — on this head Type Check · debt ledger is success; the other rides Lint & Repo Gates, still running at review time. For the adopting seat's landing check, not the review's.
  7. CI on 25ab3b0e at review time — the main ruleset lists exactly the seven required contexts. Concluded success: Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Not yet concluded: Lint & Repo Gates, and TypeScript Type Check (the lint.yml:6455 aggregator behind Type Check · workspace, in progress). No red anywhere. Rework will move the head anyway.
  8. Branch 5 commits behind maingit log 7299b945a2..origin/main -- (the seven paths) is empty; verified.
  9. IAutomationService.getFlowRuntimeStates? docblock (automation-service.ts:684-700) still names triggerType / object as how "the caller can say WHY" — not false, now incomplete; may ride the rework. Non-blocking.
  10. objectui#9217 — still open, pm:blocked, precondition text unchanged (「once the platform carries the policy reason」; 「if the platform ships a string, render the string」). This PR is that platform half; unblocking is the seat's act after landing.
  11. ADR coverage — no ADR names FlowRuntimeState, _status, the audit or the reason constant, and no scripts/adr-anchors entry anchors the three touched source files; nothing to re-anchor.
  12. Docs the drift check namedenvironment-variables.mdx:90 and tenancy-modes.mdx:332 remain true; naming the third surface there is optional. flows.mdx is ① item 7.

Implemented-by: claude/issue-18235-flow-runtime-state-reason
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: FAIL


Generated by Claude Code

#18198 wrote that `GET /automation/_status` "has no field to say why", so a
policy-disabled flow "is indistinguishable there" from one whose trigger is
missing. This branch adds exactly that field, so the paragraph is false on
its own head — the same ground on which `packages/types/src/env.ts`'s
docblock was corrected in this landing. One prose carrier was corrected and
its twin was missed.

Corrected the same way, and no further: the reason reaches the WIRE, read
from the recorded refusal; whether a console renders it as a distinct state
is that console's own change, ⛔ not claimed here.

Swept for siblings: `no field to say why` / `indistinguishable there` hit
this file only (the other hits are core/security, objectql, rest and spec,
all unrelated), against 19 files under content/ mentioning `_status` — and
`content/docs/releases/**` is release-owned and untouched. One residual, not
a class.

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

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 143/143 CONTRACT_REVIEW_TIER
Head-sha: 3d558a3ac4eebc773897d1d3f3ac3f7fe6bc4ee2

Second isolated at-tier clause-② review of PR #18635 for card #18235 (ruling G item 6 on #17396, comment 5642381255, verbatim: 「OFF: neither trigger arms any flow. Every such flow is listed in getTriggerBindingAudit(), the CLI startup summary and Studio with a DISTINCT reason — disabled by deployment policy — ⛔ never as "binding failed".」). Re-spawned because the prior at-tier review of this PR (record 5713430342, head 25ab3b0e) returned FAIL on one ground — ③ item 1 below names it and says whether the patch round discharges it. Every reading here is taken from the tree at this head or from the diff against the merge-base 7299b945a2 (8 files, +328 / −41); no seat comment on the card or the PR was used as a finding of fact. Controls ran in an own detached worktree at this head (offline install, closure built with --no-cache --force), every ablation restored by git blob identity plus an empty git diff HEAD, never by an exit code. Tier evidence: 143/143 harness stamps on this reviewer's own transcript, one distinct value, equal to the constant imported live from scripts/pm/dispatch-gates.mjs; no disagreement.

① Derived judgments

  1. The code half is byte-identical to the head the prior review examined — established, not assumed. Per-path blob at 25ab3b0e vs 3d558a3a: engine.ts ec75d9cf = ec75d9cf; automation-service.ts 8d9231d4 = 8d9231d4; env.ts 7fc343fc = 7fc343fc; engine.test.ts 00a3938c; automation-service.test.ts c95073a7; domain-handler-registry.test.ts 51f8ccae — all SAME. The patch round (prev..head) moves exactly two files: .changeset/18235-flow-runtime-state-reason.md (+1 / −1, one sentence appended to the "Wire, not rendering" bullet, blob b6e3f74b914690d9) and content/docs/automation/flows.mdx (+7 / −4, blob b3af4bf83c791cb9). Nothing else moved.
  2. FlowRuntimeState.reason?: string (@objectstack/spec, packages/spec/src/contracts/automation-service.ts:517-544) — a new OPTIONAL member on a published interface; additive. The two type pins hold it at exactly string | undefined and hold { name, enabled, bound } assignable to the contract. Re-run at this head: src/contracts/automation-service.test.ts 17/17; pnpm --filter @objectstack/spec check:test-typecheck exit 0. Own LEG 2: with reason?: string; removed (blob 8d9231d40f89941c) that gate exits 1 with TS2339: Property 'reason' does not exist on type 'FlowRuntimeState' ×4 and TS2353 ×1, restored to 8d9231d4. Every clause of the docblock checks against the tree (items 2–5). PASS.
  3. The producer populates the key only where the ruling's row lives, and on nothing healthy. describeUnboundReason() (engine.ts:4157-4168) answers undefined unless the flow resolves a trigger, isFlowEnabled(name), and is not in boundFlowTriggers; getFlowRuntimeStates() (:4093-4126) spreads ...(reason === undefined ? {} : { reason }), so a bound, disabled or trigger-less row carries no key at all — not undefined-valued. The populated set is exactly the audit's set (the three unbound branches), a declared superset of the one row the ruling names; on that row the sentence is SCHEDULED_WORK_DISABLED_REASON, the policy branch outranking both binding branches at :4164, so it can never read "binding failed". Re-run at this head: engine.test.ts 158/158. Own LEG 1: with the spread removed (blob ec75d9cf29716a38) exactly the four status-door pins fail and 154 pass, the DARK absence pin among the 154 — a well-formed absence assertion; restored to ec75d9cf. PASS.
  4. One computation feeds two doors — TRUE in the tree, not two computations that currently agree. getFlowRuntimeStates() (:4110) and getTriggerBindingAudit() (:4187) both call the private describeUnboundReason(); the three sentences that left the audit's ternary are the three that arrived in the helper, byte for byte (SCHEDULED_WORK_DISABLED_REASON; the "registered but binding failed — see earlier warnings" template; the "no … trigger is registered — add requires: ['triggers'] …" template). The audit's OUTPUT is unchanged: same eligibility predicate, now inside the helper; same iteration set (this.flows.keys()). One behavioural delta, judged harmless: the audit now calls resolveTriggerBinding() for disabled and bound flows too — :3240-3380 has no logger call, no throw, no mutation and no env read (a pure resolver), and getFlowRuntimeStates() already called it for every flow. The anti-drift pin is an identity between the two doors AND asserts two distinct sentences, so it cannot pass vacuously. PASS.
  5. Read from a RECORDED refusal, not re-derived live — TRUE. policyDisabledFlows is added at the bind refusal (:3413-3427), deleted the moment a flow passes the gate (:3434) and dropped with the flow on unregister (:4072); describeUnboundReason() reads only .has(name) (:4164). The single resolveScheduledWorkPolicy() call in engine.ts is the bind-time one at :3413. The pin "the status row reports what HAPPENED, not what the environment says when it is read" flips the switch on after the bind and still expects the policy sentence; it is one of the four red under LEG 1, so it is live. PASS.
  6. The wire door GET /automation/_status (packages/runtime/src/domains/automation.ts:1778-1790) is a verbatim success({ flows, total }) pass-through, so reason reaches the wire unedited and stays absent where the producer wrote none. Re-run at this head: domain-handler-registry.test.ts 53/53, the new wire pin included. No route in packages/runtime, packages/rest or packages/adapters exposes getTriggerBindingAudit(); its consumers are plugin.ts:1130 (the kernel:bootstrapped warn loop) and serve.ts:6878packages/cli/src/utils/format.ts:1179, which prints ${u.reason} verbatim. PASS.
  7. packages/types/src/env.ts:289-302 — unchanged since the prior review (blob 7fc343fc); docblock only, the constant's value untouched (the diff's context lines). Every clause is a fact of this tree (items 2–5), and "(objectui#9217, open)" is true at review time (direct GET: open, pm:blocked). A genuine consequence of the change, not scope creep, and the standard the next item is judged by. PASS.
  8. content/docs/automation/flows.mdx:2117-2123 — the corrected paragraph is TRUE at this head and claims no more than the tree supports. Clause by clause: (a) "GET /automation/_status answers one row per flow" — [...this.flows.keys()].map(...), passed through verbatim (item 5); (b) "an unarmed flow's row holds the same sentence in its optional reason field" — for the callout's subject, an enabled time-triggered flow refused by the switch, the row's reason is SCHEDULED_WORK_DISABLED_REASON, the audit's own sentence (items 2–3, the identity pin); (c) "read from what the engine RECORDED when it refused the bind, never re-derived later, so a switch flipped since cannot turn it into a binding failure" — item 4, pinned; (d) "⛔ Reaching the wire is not the same as being rendered: whether a console shows it as a distinct state rather than as an error is that console's own change" — no rendering claim is made, which is the boundary the corrected env.ts docblock draws and objectui#9217 (still open) owns; (e) "The startup summary and the audit carry the same sentence" — format.ts:1179 prints the audit's reason, and the audit's sentence is the status door's by construction. One precision note, non-blocking: read outside its callout, "an unarmed flow's row" is wider than the producer's eligibility rule (a disabled or trigger-less flow is unarmed too and carries no key); the contract docblock states the exact rule ("present only on a row that is enabled, bound: false and declares a trigger") and the paragraph contradicts nothing. The prior FAIL's ground is discharged (③ item 1). PASS.

② Semver level

  • @objectstack/spec minor — correct: a new optional member on a published interface widens the shape a consumer reads against; nothing removed or renamed.
  • @objectstack/service-automation minor — correct: a public method's rows carry a new populated field; the audit's output is unchanged; the helper is private.
  • @objectstack/types patch — correct: docblock only, shipped inside the .d.ts; the constant's value is unchanged.
  • @objectstack/runtime correctly absent (test-only); content/docs/** is published by no package. The changeset's frontmatter names exactly the three published packages whose src moved.
  • No breaking arm, so no ADR-0087 disposition is due. The patch round's one added sentence names the second prose carrier and changes no level.
  • Declarations agree: changeset Clause-②: yes (widening), PR body Clause-②: yes; node scripts/pm/check-clause2-carriers.mjs --pair 18635 exit 0 (both carriers now hang needs:contract-review); node scripts/pm/check-widening-tells.mjs --declaration yes --diff on the merge-base diff exit 0. The changeset matches what the diff ships across all three packages.

③ Boundary flags

  1. The prior FAIL, named, and discharged. Record 5713430342 at head 25ab3b0e returned VERDICT: FAIL on exactly one ground (its ① item 7): content/docs/automation/flows.mdx:2117-2120 still said GET /automation/_status "has no field to say why, so a policy-disabled flow is indistinguishable there" — false the moment this diff added the field, on precisely the ground the same landing used to correct packages/types/src/env.ts; one prose carrier corrected, its twin missed. The patch commit 3d558a3a ("docs(automation): the status door carries the policy reason now") replaces that paragraph. Before (blob b3af4bf8): 「Studio does not carry that reason today: GET /automation/_status reports each flow as bound or not and has no field to say why, so a policy-disabled flow is indistinguishable there from one whose trigger is missing. Read the startup summary or the audit for the reason.」 After (blob 3c791cb9): 「The status door carries that reason too: GET /automation/_status answers one row per flow, and an unarmed flow's row holds the same sentence in its optional reason field — read from what the engine RECORDED when it refused the bind, never re-derived later, so a switch flipped since cannot turn it into a binding failure. ⛔ Reaching the wire is not the same as being rendered: whether a console shows it as a distinct state rather than as an error is that console's own change. The startup summary and the audit carry the same sentence.」 Judged clause by clause in ① item 7: true, and it does not say a console renders the reason. Discharged. No test is owed: of the sources that name this page, lint-flow-patterns.ts / .test.ts cite §"Per-iteration containment", validate-flow-trigger-readiness.test.ts the renewalReminder example, verify/automation-trigger-terminal-messages.test.ts the trigger-route prose, types/env.test.ts names it in a comment only, and check-doc-formula-expressions.mjs reads its visibleWhen examples — none reads this callout. Docs gates re-run at this head in the own worktree, all exit 0: check:nul-bytes, check:doc-authoring, check:keyed-text-bounds, check:doc-anchors, check:docs-single-h1, check:corpus-claim-drift.
  2. Is the residual really a single one? Re-derived with own controls — yes. Over content/, packages/, skills/, docs/, every README.md, and separately .claude/, apps/, examples/, scripts/ at this head (tests and CHANGELOG.md excluded), case-insensitive: no field to say why 0; carry that reason 0; no reason field 0; Studio is NOT one 0; declared is not delivered 0; until a reason reaches 0; Studio does **not** 0; reaches Studio as 0; indistinguishable there 3, all unrelated (core/security/resolve-authz-context.ts:861, objectql/hook-withheld-readonly-fault.ts:42, spec/data/filter-subtree-provenance.ts:16; plus scripts/pm/dispatch-gates.mjs:128). Second pass on Studio-shaped and rendering-shaped phrasings (Studio … bound: false; reaches Studio; Studio … cannot; Studio/console shows|renders|displays … reason; status badge … reason) — the only on-topic hit is the new contract docblock's historical sentence "Before this field existed the two reached Studio as the same enabled: true, bound: false row ([finding] ruling G item 6's third surface has no platform half: _status carries no reason, so a policy-disabled flow is indistinguishable from a broken binding — and objectui#9217 is blocked on a card that does not exist #18235)", true as history. Lit controls fired: _status in 19 files under content/ and 30 under the four other trees; SCHEDULED_WORK_DISABLED_REASON in 5 non-test sources. The sites that name two surfaces without claiming exclusivity remain true: doctor.ts:284, examples/app-todo/src/flows/task.flow.ts:10, migration 18.schedule-flow-acting-organization-required.ts:25/:83 and its registry.ts mirror, tenancy-modes.mdx:332, environment-variables.mdx:90, trigger-schedule/README.md:48, schedule-trigger.ts:369, flows.mdx:2195. The only other automation/_status mention in content/ is release-owned releases/v17/17-3.mdx:314 ("still serves enablement to any authenticated caller") — about auth, not the reason; untouched and true. ⇒ one residual, discharged; no second ground.
  3. The declared deviation (one PATCH of the PR body) — process observation, not a contract finding. Honest: declared in the dev's own record with the reason, the footer handling ("sent with NO footer", read back) and the count. Proportionate: the remedy was one body write whose need the patch commit created (an eighth touched path), and the patched body's File-surface set equals the diff's eight paths exactly, carries exactly one footer and zero angle-bracket mutations — measured here. Two caveats for the record: (a) the edit count is unverified — the proxy refuses GraphQL (403 by policy) and REST exposes no body-edit history; (b) the declaration's arithmetic ("six of the seven touched paths while the diff carried eight") does not reconcile with the file counts at either head (7 at 25ab3b0e, 8 at 3d558a3a), so either the pre-patch table was already short by one or the sentence miscounts — unreadable here either way. Two stale numerals remain in the body ("all seven touched files", "the seven paths above"); harmless.
  4. check:cross-package-test-inputs red on a built tree — not this diff; carriers exist, read by direct GET (no search used): [regression] check:cross-package-test-inputs 的裁决取决于被 gitignore 的空目录 packages/spec/dist 存不存在 —— 构建过的工作树一律红,CI 绿只因那一步跑在构建之前(#18340 引入) #18348 open, priority:p1, pm:dispatched; [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 open, finding; [finding] check:cross-package-test-inputs answers 1 or 0 depending on whether packages/spec has been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440 closed as not planned. Nothing to file.
  5. serve.ts:6860-6870 inline row type omits reason — agree, no defect: the banner's unbound section reads the audit (:6878format.ts:1179), and the wider engine row is assignable to the narrower annotation. Non-blocking.
  6. Dual-carrier A/B — moot: both carriers hang needs:contract-review and --pair 18635 exits 0.
  7. NOT MEASURED locally in the dev's report (check:dual-build-cjs-loads, check:type-check-debt) — on this head Type Check · debt ledger, TypeScript Type Check and Lint & Repo Gates (which carries check:dual-build-cjs-loads) are all success. Nothing left open for the adopting seat on those two.
  8. CI on 3d558a3a at review time — of the seven required contexts, concluded success: Lint & Repo Gates, TypeScript Type Check, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Dogfood Regression Gate (3/3 shards); Test Core all 6/6 shards success, the aggregator still in progress. No red anywhere. Landing waits on the queue's own rebuild regardless.
  9. Branch now 10 commits behind origin/main (32be735e, up from the body's "5"): git log 7299b945a2..origin/main -- the eight paths is empty (control: 0 commits touch packages/services/service-automation/ at all in that window). Verified.
  10. Two non-blocking staleness notes, neither false: IAutomationService.getFlowRuntimeStates? docblock (automation-service.ts:684-700) still names triggerType / object as how "the caller can say WHY" — now incomplete; and packages/lint/src/validate-flow-trigger-readiness.ts:559-562 quotes the audit's old if (!resolved) continue; // manual / screen flow — nothing to bind spelling, which now lives in describeUnboundReason() — the behaviour it describes is unchanged. Carrier: whoever next touches either docblock.
  11. objectui#9217 — open, pm:blocked, priority:p3; body precondition unchanged ("Once the platform carries the policy reason, the flow status must render it …", "if the platform ships a string, render the string"); Blocked-by: objectstack#17396; one triage comment cross-names [finding] ruling G item 6's third surface has no platform half: _status carries no reason, so a policy-disabled flow is indistinguishable from a broken binding — and objectui#9217 is blocked on a card that does not exist #18235. This PR is that platform half; unblocking is the seat's act after landing.
  12. ADR coverage — no ADR names FlowRuntimeState, _status, the audit, describeUnboundReason or the reason constant; no scripts/adr-anchors entry anchors the touched source files. Nothing to re-anchor.
  13. Docs the drift check namedenvironment-variables.mdx:90 and tenancy-modes.mdx:332 remain true; flows.mdx is ① item 7. Release-owned pages untouched. The dev's third out-of-scope note (mixed exit codes across three docs-family gates on an unbuilt prerequisite) is a legibility observation with no carrier readable here — unverified, nothing this PR owes.

Implemented-by: claude/issue-18235-flow-runtime-state-reason
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Clause-② adopted — 第二轮 PASS,双载体同笔剥

⏱️ 本条读数取自同一动作:2026-09-17T11:55Z。

复核记录:PR 线程评论 5713890477
所判 head:3d558a3ac4eebc773897d1d3f3ac3f7fe6bc4ee2 —— 采纳时复读,未动(draft: true,mergeable_state: clean)。
前一轮:5713430342,FAIL @ 25ab3b0e。⭐ 本轮记录逐字点名了它(本席核过,'5713430342' in body → True),符合「同 head 再起须引前次因」的同一要求。

逐字采纳。 ⛔ 未改写、未润色。needs:contract-review 已从 PR #18635 与卡 #18235 两侧同笔剥除

独立性对

Implemented-by: claude/issue-18235-flow-runtime-state-reason(mode:subagentReviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3(采纳席)。不同身份 ⇒ ⛔ 无 SELF-REVIEW。

档位证据

143/143 逐条 harness message.model 对实时 import 的 CONTRACT_REVIEW_TIER,1 个取值,0 条不一致,⛔ 未用 get_session。本席独立验泄漏面:实时 import 到的常量在正文里搜 → False;--pair 18635 在 C6/C7 上确认该记录在此 head 且立在 143/143 的盖章对照上。

⭐ 这一轮做了本席特意要求的三件事,而且做到了

  1. 点名并判定前一轮 FAIL:补丁段落逐字引了 before/after,判为 TRUE,且确认它没有声称 console 会渲染 —— 那条界限在页面上明写保留,objectui#9217 仍 open/pm:blocked、前置条件文字未变。
  2. ⛔ 没把上一轮的 PASS 项当已定:代码半边用逐路径 blob 证明与 25ab3b0e 逐字节相同(engine.ts ec75d9cf、automation-service.ts 8d9231d4、env.ts 7fc343fc),而不是引用上一份复核;两条消融腿自己重跑(LEG 1 恰好 4 个 status-door 钉子红、DARK 仍绿;LEG 2 check:test-typecheck 红带 TS2339 ×4)。
  3. 自己重扫残留:跨 content/ packages/ skills/ docs/ READMEs,另扫 .claude/ apps/ examples/ scripts/,八个短语全零;亮控 _status 在 19 + 30 个文件、reason 常量在 5 处 ⇒ 零是读数。⛔ 无第二个 FAIL 理由。

③ 的非阻塞条目,记下不动

⚠️ 复核同时读到:分支落后 origin/main 10 个 commit,但零个触及本 PR 的八条路径。落地前检会当场重测这一条,⛔ 不引用此处读数。

随后按 landing-operations.md 走落地前检 → 转 ready → 挂 auto-merge。


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 17, 2026 11:57
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 99fcb4a Sep 17, 2026
51 checks passed
@os-bill
os-bill deleted the claude/issue-18235-flow-runtime-state-reason branch September 17, 2026 12:22
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/m tests tooling

Projects

None yet

2 participants