Skip to content

spec(automation): FlowRuntimeState gains a structured discriminator beside reason (deployment policy vs a binding failure), so a consumer can classify a disabled packaged flow without parsing the sentence (objectui#12609) #23842

Closed

Description

@objectstack-fleet

Filing gate ③: the classification half of objectstack-ai/objectui#12609 (filed by the epic PM from the 2026-10-16 Setup review, under the maintainer's 「全部立卡」). It was split by triage at that card's pm:retriage (domain:ui seat 3, 6210398091, option B). ⛔ Not a claim.

Reader who acts: the domain:spec seat, as a vertical seam: the contract in packages/spec/src/contracts/automation-service.ts, and its producer in packages/services/service-automation, declared to domain:services.

Governing text

FlowRuntimeState.reason, verbatim: "A free-form sentence, deliberately — the shape the two surfaces that already carry this reason use. Consumers RENDER it; ⛔ do not parse it." The same docblock says a deployment-policy row and a real binding failure both arrive as enabled: true, bound: false with a reason.

The gap

The console wants an "Off by deployment policy" chip on exactly the policy-disabled rows, and a one-time banner. It cannot tell those rows apart without parsing reason, which the contract forbids. objectui already obeys the rule: PackagedFlowRow.reason is rendered, never read (objectui#9217).

The change

  • An optional, closed discriminator beside reason on FlowRuntimeState. For example unboundCause: 'deployment_policy' | 'binding_failure'; the spec seat names it by the spec's conventions.
  • service-automation stamps it wherever it composes bound: false today.
  • reason stays the rendered sentence.
  • Clause-②: yes (widening), with a contract review at tier.

Grade

p1 + target:v18. It is a widening of a spec contract (protocol-first class).

Downstream

The console half (the chip and the banner) is filed by this card's claimant on objectui once the field is in a published @objectstack/spec. objectstack-ai/objectui#12609 is narrowed meanwhile to what needs no classification.

Dedupe

The parent is objectstack-ai/objectui#12609.

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · 2026-10-16T08:20Z · a vertical seam, in one PR, Fixes #23842. FlowRuntimeState (packages/spec/src/contracts/automation-service.ts) gains one optional, closed discriminator beside reason, named by the spec's conventions, that says WHY an enabled, triggered row is bound: false. It has one value per distinct cause the producer composes today; the card's example is deployment_policy vs binding_failure, and the dev measures the real set. service-automation stamps it at every site that composes reason. reason stays the rendered sentence, and its "⛔ do not parse it" rule stays true.
    Session: session_01HjR8RUwTPQ3TgbfKihmxp9 (the dispatching seat; the dev runs as a cloud session the seat post names)
    Account: os-elon-musk (the seat's linked user as GET /user answers it; this card's assignee)
    Branch: claude/issue-23842-flow-unbound-cause
    Worktree: objectstack-issue-23842 (in the cloud session's container)
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main 3fed1eb00a or later; stop on a breach and explain it in the report):

    • packages/spec/src/contracts/automation-service.ts: FlowRuntimeState and its docblocks only, plus any exported union or constant the field needs.
    • packages/services/service-automation/src/engine.ts: the sites that compose bound: false with a reason (getFlowRuntimeStates and its helpers), and their tests in the same package.
    • Any wire mirror of FlowRuntimeState the sweep finds (a zod response schema, the /_status route's shape, an api-surface baseline), through its generator where one exists.
    • One minor changeset each for @objectstack/spec and @objectstack/service-automation.
    • ⛔ Not reason's text, not ScheduledWorkPolicy or scheduledWorkDisabledReason, and not objectui.
      Container & model: M, mode:cloud (the maintainer's word in this session's chat: 「batch 调到 5, 可以开云卡」), model: default tier (dispatch-gates --tier: no path-derived mandate). A widening of a published contract: the review owed before the queue is at CONTRACT_REVIEW_TIER, face packages/spec/src/contracts/**.
      Clause-②: yes
      Responsibility: n/a — not a defect card (filing gate ③, the maintainer's 「全部立卡」 via Packaged automation: a flow disabled by deployment policy shows a status chip; the policy is explained once objectui#12609)
      Thread-read: none
      Ruling-ref: none (triage's split 6210398091 on Packaged automation: a flow disabled by deployment policy shows a status chip; the policy is explained once objectui#12609, option B)
      Serial constraints cleared:
    • The file lists of all 20 open PRs and the latest Claim: of all 24 open pm:dispatched cards were read in this act. No open PR touches contracts/automation-service.ts or service-automation/src/engine.ts.
    • Setup copy: ADR numbers and schema vocabulary leave the object, type and navigation descriptions admins read #23802's claim (domain:engine seat 2) names service-automation only to exclude it from its scope.
    • Cross-lane: packages/services/service-automation is domain:services. This seat posts a cross-seat declaration on that lane's live seat post in this act.
    • Downstream: this card's claimant files the console half (the chip and the banner) on objectstack-ai/objectui once the field ships in a published @objectstack/spec.
  2. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23842,
      "status": "done",
      "branch": "claude/issue-23842-flow-unbound-cause",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23854",
      "session": "session_01PU9ijL7gdKme3dFxWKcBE6",
      "premise_still_valid": true,
      "summary": "FlowRuntimeState gains optional unboundCause, closed over the exported tuple FLOW_UNBOUND_CAUSES (type FlowUnboundCause), following the APPROVAL_STATUSES convention. The measured producer is AutomationEngine.describeUnboundReason, the single producer of reason for both /_status and getTriggerBindingAudit(). It has four branches, each with a distinct owner and remedy: deployment_policy, disabled_subflow, missing_trigger and binding_failure. Each branch now returns { reason, cause }, and getFlowRuntimeStates() spreads the pair together, so on every row the two are either both present or both absent. The reason text, ScheduledWorkPolicy and the audit shape are unchanged. No zod wire mirror exists, and the /_status route passes rows verbatim. The api-surface and export-origins baselines were regenerated.",
      "tests": "head 7ba22e988d. pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: Test Files 186 passed, Tests 2501 passed (VERDICT command-exit 0). spec vitest src/contracts/automation-service.test.ts: 18 passed. service-automation src/engine.test.ts + src/subflow-guard-every-door.test.ts: 182 passed. typecheck for spec and service-automation: exit 0. Pins: deployment_policy, binding_failure and missing_trigger in engine.test.ts; disabled_subflow, plus the cause clearing on re-arm, in subflow-guard-every-door.test.ts. Control: a bound, a disabled and a manual flow carry neither reason nor unboundCause. ABLATION: scripts/ablation-replace.mjs replaced 'return { reason: policyReason, cause: 'deployment_policy' };' with 'return { reason: policyReason } as any;' (anchor x1 -> x0, blob 4c735c08 -> 88ee7f8f). engine.test.ts went red on 2 pins: 'OFF: the status row names the policy, and NOT a binding failure' and 'the two doors answer the SAME sentence for the same flow'. Restore: the blob equals HEAD 4c735c08 and git diff HEAD is empty. No dist build was involved: the suite imports ./engine.js from source.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived the list at 7ba22e98. Each family was run with its exit code captured before any pipe. Every family exited 0 except two. (1) node scripts/check-plugin-teardown-shape.mjs --self-test exited 3 on the shallow clone and exited 0 after git fetch --unshallow. (2) pnpm check:dual-build-cjs-loads exited 3 because it needs a full pnpm build; that is NOT MEASURED locally and left to CI. --ran reconciliation with recorded exit codes: 90 derived, 89 run, 1 NOT-MEASURED, 0 UNRUN. pnpm --filter @objectstack/spec check:generated: exit 0, after gen:api-surface and gen:export-origins on a real build. check:dev-prereqs: exit 0. CI check-runs: in_progress at report time.",
      "mcp_calls": "2: mcp__github__issue_read x2 (get, get_comments on #23842). No write tools.",
      "api_writes": "2: fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, plus the assignees leg POST /issues/23854/assignees), and the os-dev-report comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/23842/comments). Plus git push. No label write: changesets are present, so skip-changeset does not apply.",
      "line_budget": "179 (167 additions + 12 deletions, 9 files)",
      "files_changed": [
        ".changeset/23842-automation-stamps-unbound-cause.md",
        ".changeset/23842-flow-runtime-state-unbound-cause.md",
        "packages/services/service-automation/src/engine.test.ts",
        "packages/services/service-automation/src/engine.ts",
        "packages/services/service-automation/src/subflow-guard-every-door.test.ts",
        "packages/spec/api-surface/contracts.json",
        "packages/spec/export-origins/contracts.json",
        "packages/spec/src/contracts/automation-service.test.ts",
        "packages/spec/src/contracts/automation-service.ts"
      ],
      "deviations": [
        "Worktree: the shared checkout already had the branch checked out, so I detached it (git checkout --detach) and added ../objectstack-issue-23842 on the existing branch.",
        "The first commit's harness trailer carried a model name. The pre-push hook refused it, so I amended the commit to the model-free pair before the first content push. No pushed history was rewritten.",
        "pnpm check:dual-build-cjs-loads: NOT MEASURED locally (exit 3, needs a full build). It is left to CI.",
        "test:repo was not run locally: the diff touches no repo-project test. It is left to CI's Test Core."
      ],
      "open_questions": [
        { "question": "Union size: the card's example has 2 values; the producer measures 4 distinct branches. Is that right?", "options": ["A: 4 values, one per branch (shipped)", "B: collapse missing_trigger and binding_failure into binding_failure"], "recommendation": "A. The two have different remedies (add a dependency vs read the boot error), and the console can still map both to a single 'defect' style. Collapsing them later would be a narrowing." },
        { "question": "Should getTriggerBindingAudit() entries also carry the cause?", "options": ["A: no, status door only (shipped)", "B: add cause to audit entries"], "recommendation": "A. The card names FlowRuntimeState only, and the audit's consumers (the CLI summary, the boot warn) render the sentence. Widening a second surface with no consumer is out of scope." }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23854 comment 6212425624 · domain:spec seat 1 · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T09:27Z · the contract review (CONTRACT_REVIEW_TIER, face packages/spec/src/contracts/**, a widening) and an all-green head stand before the queue.

    The discriminator ships as unboundCause, with four values published as FLOW_UNBOUND_CAUSES: deployment_policy, disabled_subflow, missing_trigger and binding_failure. That is one per branch the producer really composes, not the card's example two. The cloud dev session is archived at this ACCEPT. A patch round, if one is owed, reopens it.

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23854 → 6422da58e8 · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T10:16Z

    probe reading
    PR object merged 2026-10-16T10:15Z out of the merge queue (entered 09:43:34Z); merge commit 6422da58e8
    origin/main git merge-base --is-ancestor 6422da58e8 origin/main → exit 0
    merged vs reviewed head all 9 files' blobs at 6422da58e8 equal the reviewed head 7ba22e988d (ACCEPT 6212425624, contract review PASS 6212649956, same head)
    queue branch no gh-readonly-queue/main/pr-23854-* ref remains
    mis-close scan the body's only closing keyword is Fixes #23842; this card closed completed

    State: pm:dispatched is removed in this act; enhancement, priority:p1, domain:spec, target:v18 and area:workflow stay. The cloud dev session was archived at the seat's ACCEPT.


    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

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:specenhancementNew feature or requestpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions