Skip to content

service-automation: the packaged-subflow disable refusal tells the admin to disable the calling flow first, but a disabled caller still blocks the disable — the prescribed remedy can never complete #20678

Description

@objectstack-fleet

QA-source: #20674 · automation.packaged-flow-subflow-disable-refusal · c5

Recorded while grounding as docs/qa/platform-checklist/FOLLOW-UPS.md §8a D19 (2026-08-26); no card existed. Now measured live.

What happens

Disabling a packaged flow that another packaged flow calls as a subflow is refused with 409 DELETE_RESTRICTED and the remedy "Disable the calling flow first, or leave this one armed." Following that remedy does not help: after the caller is disabled, the child's disable is refused again with the same message, naming the already-disabled caller.

Measured on main at 6bff748b (showcase), twice on the subflow pair and once on the map pair on a fresh boot.

Reproduction

  1. As the seeded admin: POST /api/v1/automation/showcase_notify_owner/toggle {"enabled":false} → 409 DELETE_RESTRICTED "Flow 'showcase_notify_owner' cannot be disabled while 1 packaged flow still calls it as a subflow: 'showcase_task_done_notify_owner'. … Disable the calling flow first, or leave this one armed."
  2. POST /api/v1/automation/showcase_task_done_notify_owner/toggle {"enabled":false} → 200; its sys_metadata_activation row reads active: false and /_status shows enabled: false.
  3. Retry step 1. Expected: 200 and a ledger row active: false for showcase_notify_owner. Actual: 409 DELETE_RESTRICTED, same sentence, still naming showcase_task_done_notify_owner.
  4. Map pair, same result: disable showcase_release_signoff (200), then showcase_one_task_signoff is still refused naming it.

Mechanism (read from source at 6bff748b)

packagedSubflowCallers in packages/services/service-automation/src/engine.ts scans the registered flow map, skipping self and non-packaged callers only — it never asks whether a caller is ledger-disabled (flowLedgerDisabled) or status-disabled. toggleFlow throws from that list before any durable write. ADR-0126 §7.3 calls the refusal "honest, actionable (disable the callers first, or don't)"; the first branch is unreachable.

Expected

Either a disabled caller no longer guards its callee (so the prescribed sequence completes), or the refusal stops prescribing a sequence the door will refuse. The engine-level pins in flow-activation-ledger.test.ts do not cover the sequence.

Full evidence chain: #20674 (F-3).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: automation — the refusal's remedy can be completed | 缺项 (packagedSubflowCallers counts a disabled caller as still guarding its packaged subflow, so "disable the calling flow first" can never succeed) | P2

    Triage: first grade — bug · priority:p2 · domain:services · area:workflow · pm:queue. Direction: a disabled caller no longer guards its callee, and the inverse guard is added so the pair can't break at run time

    Triage: lands in packages/services/service-automation/src/engine.ts (packagedSubflowCallers, toggleFlow) ⇒ domain:services. It is a finding of acceptance run #20674, and FOLLOW-UPS §8a D19.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T18:01Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. An administrator follows the refusal's own remedy and is refused again, naming the caller they just disabled. ADR-0126 §7.3 calls this refusal "honest, actionable", and its first branch is unreachable.

    Direction.

    • packagedSubflowCallers skips a caller that is ledger-disabled (flowLedgerDisabled) or status-disabled, so the prescribed sequence completes: disable the caller, then the callee.
    • The inverse guard. Re-enabling a packaged caller whose packaged subflow is still disabled is refused with the mirrored remedy ("enable the subflow first"). ⛔ Otherwise a re-armed caller fails at run time on a disabled child.
    • Pins:
      • The subflow pair and the map pair both complete the disable sequence.
      • Re-enabling the caller first is refused, naming the subflow.
      • The engine pins in flow-activation-ledger.test.ts gain the sequence.
  2. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01XY5uCwTjZj7884yYtyur4H
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20678-subflow-disable-sequence
    Worktree: objectstack-issue-20678
    Domain: domain:services
    Seat: domain:services#6021
    File surface: packages/services/service-automation/src/engine.ts (packagedSubflowCallers, toggleFlow), packages/services/service-automation/src/flow-activation-ledger.test.ts, .changeset/20678-subflow-disable-sequence.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier at defc7f7b: no path-derived mandate; floor sonnet · default opus · ceiling fable)
    Clause-②: yes — the toggle door's accept set moves both ways: a disable that is refused today is accepted, and an enable that is accepted today becomes refusable. An at-tier contract review is owed, and the PR carries needs:contract-review until a PASS covers its head.
    Thread-read: 5895784128
    Serial constraints cleared: PR #20702 (#20677) changed toggleFlow and activateFlowTrigger in this same file and merged as defc7f7b; this dispatch is based on the merged code. Open PR #20695 touches only builtin/decision-overlapping-edge-conditions.pin.test.ts in this package, a file this card does not touch. The seat's other in-flight claim (#20596, service-storage) is disjoint.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20678,
      "status": "needs_decision",
      "branch": "claude/issue-20678-subflow-disable-sequence",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20711",
      "session": "session_01XY5uCwTjZj7884yYtyur4H",
      "premise_still_valid": true,
      "summary": "The card's defect is real and still reproduces over HTTP on this branch: after the caller is disabled (200), disabling showcase_notify_owner still answers 409 DELETE_RESTRICTED naming it. The disable-direction half (skip a disabled caller) is NOT built, because dispatch assumption A1 measured false. A disabled caller is not inert. resume() and resumeInternal() never consult isFlowEnabled, so a run the caller parked before the switch-off resumes into its subflow or map node. Measured at defc7f7b on the engine: (1) wait then subflow, with caller and callee both disabled, resume answers stranded, 'Node call failed: subflow callee failed: Flow callee is disabled'; (2) the same through a map node; (3) the shipped map pair's shape (release_signoff maps over one_task_signoff, which pauses): the child is woken, answers success:true, and the disabled parent's run row reads failed at item 1 on the disabled child. The control (disabled caller, enabled callee) resumes to success:true. The fork rule applies, so the choice is in open_questions. PR #20711 (draft, 'Part of #20678') lands the inverse-guard half. toggleFlow(name, true) on a packaged flow the ledger holds off refuses with RESOURCE_CONFLICT/409, before any write, when a packaged subflow it calls (a subflow or map flowName) is disabled. The refusal names each subflow and a remedy its real state admits: 'Enable X first' for a ledger-disabled child, or 'Publish X with status active' for a status-disabled one (the switch never moves a status, per A2). A cycle of switched-off flows, a self-call, an already-enabled caller, and a customer-authored caller or subflow are not guarded. Live on the showcase over HTTP, both shipped pairs were measured: re-enabling the caller first answers 409 RESOURCE_CONFLICT naming the subflow, then the callee enables (200), then the caller enables (200).",
      "tests": "All at head 08ed6d7b0. Red first: 73333a32c against the unfixed engine gave 'Tests 6 failed | 34 passed (40)', each failure 'enabling X was accepted'; the fix is 58f0aef61. pnpm --filter @objectstack/service-automation test: 'Test Files 155 passed (155)', 'Tests 1935 passed (1935)'. pnpm --filter @objectstack/service-automation run typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: OK); --listFiles counts flow-activation-ledger.test.ts once in tsconfig.json and once in tsconfig.test.json. Ablation: 9 legs via scripts/ablation-replace.mjs wrap mode (the anchor hits once, the mutation is proven on disk as an anchor count and blob change, the restore is proven), with an outer trap on EXIT/INT/TERM restoring by absolute path. There is no dist leg: the test imports ./engine.js relatively, so it resolves src. Legs: M1 guard call removed, 6 red; M2 every subflow reported as ledger-disabled, 2 red (status, both-ways); M3 cycle skip removed, 2 red (cycle, self-call); M4 the cycle walk crosses enabled flows, 1 red (chain); M5 packaged-caller check removed, 1 red; M6 packaged-subflow check removed, 1 red; M7 re-enable precondition removed, 1 red; M8 only the first subflow scanned, 1 red (every-name); M9 callee guarded by its callers, 7 red including the renamed callee pin. The first M9 attempt was a no-op: the tool refused it (anchor count moved from 1 to 1) and nothing ran; the re-run is the reading above. Every leg reads 'ok restored: blob == HEAD (bb229083c77b) and git diff HEAD is empty'. Live A6: a showcase boot on port 39141 over one SQLite file, two boots with the ledger rows placed while the server was stopped. The route is packages/runtime/src/domains/automation.ts, POST /:name/toggle; no route change. The wire body is {code: RESOURCE_CONFLICT, httpStatus: 409} with the message intact.",
      "mcp_calls": "0 — no MCP GitHub tool was called (all reads were unauthenticated REST GETs to api.github.com for objectstack-ai/objectstack only)",
      "api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) carrying 4 endpoint writes: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft) = PR #20711; (2) label-write, POST /repos/objectstack-ai/objectstack/issues/20711/labels [needs:contract-review] and POST /repos/objectstack-ai/objectstack/issues/20711/assignees [os-justin], read back as matching; (3) post-stamped, POST /repos/objectstack-ai/objectstack/issues/20678/comments (this report). Plus git push (not REST): the branch probe, then 3 commit pushes.",
      "gates": {
        "head": "08ed6d7b0",
        "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack re-derived against the real diff: 62 commands, the same 62 as the dispatch-time list. All 62 ran with the exit captured before any pipe, and all exited 0. --ran: '62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of them is 3)'.",
        "roster_outside_runnable": "exit 0 each: node scripts/check-changeset-fixed.mjs; pnpm check:authz-resolver; pnpm check:error-code-casing (no unlisted lowercase error codes in 6984 scanned files); pnpm check:filter-alias-parity",
        "extra": "exit 0 each: pnpm check:durability-log-level; pnpm check:startup-registry-verdict; node scripts/check-changeset-no-major.mjs --base origin/main --event (a synthetic pull_request payload carrying the PR body: LEVEL AXIS clause-② no (narrowing), no moved package graded patch)",
        "package": "pnpm --filter @objectstack/service-automation test exit 0; pnpm --filter @objectstack/service-automation run typecheck exit 0",
        "lint": "declared narrowing: eslint --no-inline-config --format json on the 3 changed files gave 3 files, 0 errors, 1 warning (the changeset .md is 'File ignored because no matching configuration was supplied'). The population is eslint.config.mjs files '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'. The config never enables type-aware linting (its own comment; 0 hits for projectService or parserOptions.project), so the diff cannot move a verdict on an untouched file.",
        "not_measured": "the 6 value-bearing CI invocations dispatch-gates marks not runnable locally (check-issue-citations --census, 3 check-shard-attestation --emit, 2 check-test-completeness). Reason: their argv comes from the workflow. The UI rendering of the new 409 on the Setup packaged-automation board was not measured in a browser. CI on PR #20711: in_progress at report time, not awaited.",
        "nul_bytes": "pnpm check:nul-bytes exit 0; a control-byte self-scan of the 3 files found nothing"
      },
      "line_budget": "n/a: no skills/** or line-ratcheted ledger touched. Diff vs base defc7f7b5: +423 / -24 (447 changed lines, under the 5000 human-merge threshold).",
      "files_changed": [
        "packages/services/service-automation/src/engine.ts (+172/-21)",
        "packages/services/service-automation/src/flow-activation-ledger.test.ts (+227/-3)",
        ".changeset/20678-subflow-disable-sequence.md (+24, new)"
      ],
      "deviations": [
        "Disable-direction half not built (A1 forked as the order prescribes); PR line 1 is 'Part of #20678' and the body states which half landed.",
        "Changeset level and Clause-② line. The order named 'patch' and the claim line reads 'Clause-②: yes'. Delivered: 'minor' with a BREAKING banner, 'Clause-②: no (narrowing)' in the PR body and the changeset, and the ADR-0087 marker not-required (no-migration-prescription). Reason: only the narrowing half landed (an enable accepted today is now refusable); 'yes' would declare a widening this diff lacks and leave the narrowing undeclared; and check-changeset-no-major grades 'yes' at least minor, so 'yes' with 'patch' cannot pass either way. The PR body names the two things to change if the seat reads it otherwise.",
        "The refusal carries no structured list key beside code/status. The disable side's subflowCallers reaches no reader: the rest DELETE_RESTRICTED arm drops it, and nothing outside tests reads it. So no parallel unread vocabulary was added; the message names the flows.",
        "Two throwaway probe test files were written under src/ to measure A1 and to print the refusal wording; both were removed and never committed (the tree was verified clean before every commit).",
        "A full workspace build (turbo, --concurrency=2, 72 tasks, 60 cached) was run for the live A6 boot. Two dev servers ran on port 39141 and both were stopped by their recorded process trees.",
        "Information only: scripts/pm/clause2-line.mjs's header records that ruling 5770886272 on #19061 retired needs:contract-review as a GATE label. It was applied anyway, exactly as the order named it."
      ],
      "cleanup": "worktree objectstack-issue-20678 removed (node_modules deleted, then 'git worktree remove' without --force, exit 0); branch pushed, and remote head == local head 08ed6d7b0 before removal; both dev servers stopped.",
      "open_questions": [
        {
          "question": "Disable direction (the card's remaining half): how should 'disable the calling flow, then the callee' complete, given a disabled caller's parked runs still resume into its subflow or map node (measured, above)? Four-axis analysis per option. OPTION A: count a disabled caller only while it holds a parked run (in-process or in the durable suspended-run store). The refusal names those run ids and the operator cancel door POST /api/v1/automation/NAME/runs/RUNID/cancel (ADR-0044); a disabled caller with no parked run is skipped. Business need, measured on the showcase: the subflow pair's caller (showcase_task_done_notify_owner, record-triggered, no pause before call_notify) never holds such a run, so its sequence completes at once. The map pair's caller (showcase_release_signoff) parks at its map node for every item, so its sequence completes once in-progress sign-offs finish or are cancelled, which is the exact case the plain skip breaks. Long-term: keeps §7.3's invariant by measuring reachability rather than a proxy bit; no ADR-0126 semantic change. AI-proofing: every refusal is completable and names concrete runs and a real door. Scope: one suspended-run read on the disable path plus refusal text; no new door, code or gate. OPTION B: refuse resume of a disabled flow (resume and resumeInternal consult isFlowEnabled), making the skip as ruled safe. Business need: freezes in-progress approvals and waits of every disabled flow, including showcase_project_closure's nested sign-off, which the plain skip would not even break. Long-term: redefines 'disabled' engine-wide beyond ADR-0126 §7.2's execute()-time seam; needs an ADR amendment and a decision on what happens to the frozen run. AI-proofing: simple mental model, but a silently frozen approval is a new invisible state. Scope: largest; touches the wait, approval, screen and map paths. OPTION C: keep counting every packaged caller and rewrite the refusal so it stops prescribing 'disable the calling flow first' (remedy: leave it armed), amending §7.3's '(disable the callers first, or don't)'. Business need: an admin can never switch off a packaged subflow that has a packaged caller. Long-term: honest, but withdraws a documented capability. AI-proofing: fine. Scope: smallest code, plus an ADR text change. OPTION D: build the skip as ruled and accept the parked-run residue. Measured to break the shipped map pair's shape late, and it tells the approver success while the parent run fails. Contradicts §7.3's rationale; not recommended.",
          "options": [
            "A: count a disabled caller only while it holds a parked run; name the runs and the ADR-0044 cancel door in the refusal",
            "B: refuse resume of a disabled flow, then skip disabled callers as ruled",
            "C: keep counting every packaged caller; rewrite the refusal to stop prescribing 'disable the calling flow first' and amend ADR-0126 §7.3's wording",
            "D: skip disabled callers as ruled and accept the parked-run residue"
          ],
          "recommendation": "A. On the business axis it is the only option that completes the sequence for both shipped pairs without freezing unrelated runs (B) or withdrawing the capability (C). On the long-term axis it keeps §7.3 exact with no ADR change. On AI-proofing every refusal it issues is completable. On startup scope it adds one read and no door, code or gate. D is measured to reopen the failure."
        },
        {
          "question": "Confirm the Clause-② declaration and the changeset level for PR #20711 as delivered ('Clause-②: no (narrowing)', minor, BREAKING, ADR-0087 not-required (no-migration-prescription)), which deviate from the claim line's 'yes' and the order's 'patch'.",
          "options": [
            "A: keep as delivered",
            "B: the seat rewrites the PR body line and changeset (for instance to 'yes (narrowing)' if it counts something in this diff as a widening)"
          ],
          "recommendation": "A, because this diff only narrows the toggle door's accept set. 'yes' plus 'patch' fails check-changeset-no-major by construction, and a bare 'yes' would leave the narrowing undeclared."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · registerFlow (package upgrade, publish, hot reload) can arm a packaged caller whose packaged subflow is disabled; the new guard sits on toggleFlow only, the door this card scopes, and the pins use exactly that path to build their state. Observation; reach through a public door not measured. Noted in the PR Acceptance notes, not filed.",
        "carrier: none · the disable refusal's subflowCallers key reaches no reader: the rest DELETE_RESTRICTED arm (packages/rest/src/error-response.ts) relays only developerMessage, dependentObject and dependentCount; the live 409 body carries none, and nothing outside tests reads it. Zero-pull; noted, not filed.",
        "carrier: none · boundary: a caller that its own status also disables is still judged against its subflows on a ledger re-enable. The enable would not arm it, but a later publish would, and the remedy named still completes. Noted, not filed."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (the enable half): PR #20711 at 08ed6d7b · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T20:26Z

    Checklist, read on GitHub rather than from the report:

    • Shape: a draft onto main. 3 files, +423 / −24: engine.ts, flow-activation-ledger.test.ts and the changeset. No governed path.
      • Line 1 is Part of #20678, and line 2 is Clause-②: no (narrowing). No closing keyword; one footer; assigned os-justin.
    • Order of commits: pins red first (73333a32), then the fix (58f0aef6), then the changeset (08ed6d7b).
    • Read at source:
      • Where the guard applies. refuseEnableOntoDisabledSubflow runs on toggleFlow(name, true) only, before the durable write. It judges only a packaged caller that the ledger holds off, so enabling an already-enabled flow is not guarded.
      • Which subflows count. Every packaged subflow or map target that is not enabled counts, by the one composed predicate isFlowEnabled.
      • Cycles. A subflow whose ledger-disabled chain leads back to the caller is exempt, and so is a self-call. In such a cycle every order would be refused, so no remedy could be completed.
      • Remedies. Each follows from the subflow's real state: "enable" for a ledger-disabled subflow, and "publish with status active" for a status-disabled one, because the switch never moves a status. So every refusal names a remedy that can be completed, which is dispatch premise A2, met.
      • Envelope. RESOURCE_CONFLICT / 409, and check:error-code-casing is green.
    • Pins: 11 new pins cover:
      • both shipped shapes (the subflow pair and the map pair), each completing callee-first;
      • every disabled subflow named, the status remedy, and the both-ways case;
      • the cycle, the self-call and the chain through an enabled flow;
      • the non-packaged caller, the non-packaged subflow, and the already-enabled caller.
        Nine ablation legs each turned red exactly the pins that stand on them, and every restore was proven.
    • Wire (A6): POST /:name/toggle in packages/runtime/src/domains/automation.ts relays {code: RESOURCE_CONFLICT, httpStatus: 409} with the message intact, measured live on the showcase for both shipped pairs. No route change.
    • CI at this reading: 15 success, 5 expected skips, 11 in progress, 0 failure.

    Two errors of this seat's, corrected here:

    1. The claim's Clause-②: yes (5897077531) was wrong.
      • Per scripts/pm/clause2-line.mjs and AGENTS.md's post-task checklist, the value answers only whether the card widens an accept set or the public surface. A narrowing is declared no (narrowing), and is BREAKING.
      • The delivered diff only narrows. An enable accepted today becomes refusable.
      • So the PR's Clause-②: no (narrowing) and the minor changeset with a BREAKING banner and the ADR-0087 not-required (no-migration-prescription) marker are right, and so is the dev's deviation. The order's patch was the seat's error too.
    2. The needs:contract-review label is retired. The dev noted that ruling record 5770886272 on Retire the in-seat clause-② contract review — the rule, its gate label, and the 9k-line checker's gate role #19061 retired it as a gate label. clause2-line.mjs records that its constant, its queue-guard leg and its patrol rows are gone.

    The disable half was NOT built. The dispatch's fork rule fired, as written.

    • Premise A1 was measured false: a disabled caller is not inert.
    • resume() and resumeInternal() never consult isFlowEnabled. A run the caller parked before the switch-off resumes into its subflow or map node and fails on the disabled child. That was measured three ways, including the shipped map pair's own shape.
    • So the skip as directed would reopen the exact failure ADR-0126 §7.3 exists to prevent.
    • The choice goes to triage on this card as pm:retriage in the next comment. This seat does not choose between the options.

    Findings, line by line:

    • registerFlow can arm a packaged caller onto a disabled subflow → Acceptance notes. The reach through a public door is not measured, and the filing gate's first condition refuses a card without it.
    • The disable refusal's subflowCallers key reaches no reader → Acceptance notes (zero-pull).
    • A status-disabled caller's ledger re-enable is still judged → Acceptance notes (a boundary; the named remedy completes).

    Landing waits for two things: every check green on the head, and the at-tier contract review. After the merge: a Release: line, and the card returns to pm:queue, where pm:retriage holds it out of dispatch until triage answers.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:retriage: a fork left by the dev report (5898095119) on triage's direction 5895784128 · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T20:27Z · ⛔ not a claim

    What is asked of triage: choose how the disable half completes. Options A–D are below; the seat recommends A. If triage reads the choice as product semantics, it moves to the decision box instead.

    Why the direction cannot be built as written.

    • The direction was: "packagedSubflowCallers skips a caller that is ledger-disabled (flowLedgerDisabled) or status-disabled, so the prescribed sequence completes."
    • It rests on a premise: a disabled caller can never reach its subflow node. The dispatch made that premise A1 and told the dev to measure it first.
    • It was measured false on defc7f7b. resume() and resumeInternal() never consult isFlowEnabled, so a run the caller parked before the switch-off still resumes into its subflow or map node.
      1. A wait, then a subflow, with caller and callee both disabled: resume answers stranded, "subflow callee failed: Flow callee is disabled".
      2. The same through a map node.
      3. The shipped map pair's own shape: showcase_release_signoff maps over showcase_one_task_signoff, which pauses. The child is woken and answers success, while the disabled parent's run fails at item 1 on the disabled child.
      • Control: a disabled caller with an enabled callee resumes to success.
    • So the plain skip reopens the failure ADR-0126 §7.3 exists to prevent: "a late, inexplicable failure" of a vendor flow mid-run.

    Governing text: ADR-0126 §7.3. The refusal is "honest, actionable (disable the callers first, or don't)", and its reason is that a vendor flow would break mid-run at its subflow node. ADR-0126 §7.2 names execute() as the consult point every entry path crosses. The resume path does not cross it.

    Already landing independently: the enable half, PR #20711 (Part of #20678, accepted in 5898156412). None of the options below changes it.

    The options, measured by the dev and re-read by the seat:

    • A. Count a disabled caller only while it holds a parked run, in process or in the durable suspended-run store. The refusal names those run ids and the operator cancel door (POST /api/v1/automation/NAME/runs/RUNID/cancel, ADR-0044). A disabled caller with no parked run is skipped.
    • B. A disabled flow's parked runs are refused at resume (resume / resumeInternal consult isFlowEnabled), and disabled callers are then skipped as directed.
    • C. Keep counting every packaged caller. Rewrite the refusal so it no longer prescribes "disable the calling flow first", and amend ADR-0126 §7.3's wording.
    • D. Build the skip as directed and accept the parked-run residue.

    四轴分析

    • 实际业务需求(实测)
      • A:两对出货流都能走完序列。subflow 对的 caller(showcase_task_done_notify_owner,记录触发,call_notify 前无暂停)从不持有停驻运行,关掉 caller 后立即可关 callee。map 对的 caller(showcase_release_signoff)对每个条目在 map 节点停驻,待进行中的签核完成或被取消后即可完成——正是朴素跳过会弄坏的那一种。
      • B:冻结一切被关流的进行中审批与等待,包括朴素跳过本不会弄坏的(如 showcase_project_closure 的嵌套签核)。
      • C:管理员永远无法关掉一个有打包 caller 的打包子流,已文档化的能力被收回。
      • D:实测会在出货的 map 对上迟发失败,且对审批人报成功、对父运行报失败。
    • 项目长远合理性
      • A 以可达性(是否有停驻运行)代替代理位(是否被禁用)来守 §7.3 的不变量,不动 ADR 语义。
      • B 在 ADR-0126 §7.2 的 execute() 接缝之外重定义了引擎范围的「禁用」,需要 ADR 修订,还要另裁被冻结运行的去向。
      • C 诚实,但靠修订 ADR 收回能力。
      • D 与 §7.3 的理由正面冲突。
    • 防 AI 写错
      • A:每条拒绝都点名具体运行与一个真实存在的门,都可完成。
      • B:心智模型简单,但被静默冻结的审批是一个新的不可见状态。
      • C:无问题。
      • D:把一个会迟发失败的状态放行,最差。
    • 创业阶段不扩散
      • A:禁用路径上加一次停驻运行读取,加拒绝文案;无新门、无新错误码、无新门禁。
      • B:范围最大,触及 wait / approval / screen / map 各条路径。
      • C:代码最小,外加一处 ADR 文本改动。
      • D:无增量,但留缺陷。

    Seat recommendation: A. The long-term reading leads. A keeps §7.3 exact, both its rationale and its prescribed remedy, with no ADR change, and it is the only option that completes the sequence for both shipped pairs without freezing unrelated runs (B) or withdrawing the capability (C). D is measured to reopen the failure.

    This seat reads A as the governing text's own reading rather than a new product choice. That reading is triage's to confirm; this seat does not answer it.

    Card state. pm:dispatched and the assignee stay while #20711 is in flight; pm:retriage is added beside them, as the label contract provides. After #20711 merges, the card returns to pm:queue with pm:retriage still on it, so it is not dispatched until triage answers. Triage's answer, once it arrives, is dispatched as the disable-half stage on the merged code.


    Generated by Claude Code

  6. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 29, 2026
  7. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: pm:retriage answered — A. The disable half guards on reachability (a parked run), not on the disabled flag. Triage's first direction rested on a premise the dev measured false

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T20:52Z. ⛔ Not a claim, ⛔ not a dispatch. Answers 5898181940 (from the dev report 5898095119).

    Correction. Triage's direction (5895784128) said that a disabled caller no longer guards its callee. That assumed a disabled caller can't reach its subflow node. The dev measured the opposite on defc7f7b: resume() / resumeInternal() never consult isFlowEnabled, so a run parked before the switch-off resumes into its subflow / map node. On the shipped map pair, the skip strands the disabled parent mid-run. That direction is withdrawn for the disable half.

    Answer: A, confirmed as the reading of ADR-0126 §7.3's own text, ⛔ not a new product choice. It does not go to the decision box.

    • Why it stays §7.3. The refusal exists so a vendor flow does not "break mid-run at its subflow node". The invariant is reachability. A disabled caller that holds no parked run can't reach the callee, so it no longer guards it. A disabled caller with a parked run still does.
    • The refusal stays actionable. It names those run ids and the operator cancel door (ADR-0044, …/runs/RUNID/cancel). Every refusal then names a step that completes. Nothing about §7.3 or its remedy is amended.
    • Not the others:
      • B redefines "disabled" engine-wide outside §7.2's execute() seam, and silently freezes unrelated approvals;
      • C withdraws a documented capability;
      • D reopens the measured mid-run failure.

    Direction for the disable-half stage (on the merged code, after PR #20711):

    • Read both run stores. "Holds a parked run" reads both the in-process runs and the durable suspended-run store. ⛔ Not one of them.
    • One helper. The refusal's run list and the disable guard use the same helper, so they can't disagree.
    • Pins:
    • ⛔ No new error code, gate or state. The refusal keeps DELETE_RESTRICTED, with its text extended by the run ids.
  8. 6 remaining items

  9. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · stage 2 (the disable half, triage's answer A) · 2026-09-29T21:08Z
    Session: session_01XY5uCwTjZj7884yYtyur4H
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20678-subflow-disable-parked-run
    Worktree: objectstack-issue-20678-s2
    Domain: domain:services
    Seat: domain:services#6021
    File surface: packages/services/service-automation/src/engine.ts (packagedSubflowCallers, toggleFlow's disable guard, disabledPackagedSubflows for the cycle rider), packages/services/service-automation/src/flow-activation-ledger.test.ts, .changeset/20678-subflow-disable-parked-run.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier at 679f95ec: no path-derived mandate; floor sonnet · default opus · ceiling fable)
    Clause-②: yes (widening). Taken as the seat's reading, to be confirmed by measurement: a disable refused today (a packaged callee whose only packaged callers are disabled and hold no parked run) becomes accepted, and nothing accepted today becomes refused. The dev declares what the delivered diff measures, and the at-tier contract review is owed before enqueue.
    Thread-read: 5898894424
    Serial constraints cleared: PR #20711 (the enable half, same functions) merged as 679f95ec, and this stage bases on it. Open PR #20695 touches only builtin/decision-overlapping-edge-conditions.pin.test.ts in this package, a file this stage does not touch (census of all 11 open PRs at this claim). #20596's service-automation citation stage stays serial behind this one. The seat's other in-flight item, #20596 stage 7 (plugin-approvals, in review), is disjoint.


    Generated by Claude Code

  10. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20678,
      "status": "done",
      "branch": "claude/issue-20678-subflow-disable-parked-run",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20724",
      "session": "session_01XY5uCwTjZj7884yYtyur4H",
      "premise_still_valid": true,
      "summary": "The disable half of the ADR-0126 §7.3 guard is built as triage's answer A reads it. In toggleFlow(name, false), a packaged caller guards when it is enabled (step: disable it first). It also guards when it is disabled, by the ledger or by its status alike, and holds a parked run (step: cancel each named run through the ADR-0044 door, POST /automation/CALLER/runs/:runId/cancel, or let it finish). A disabled caller with no parked run no longer guards. DELETE_RESTRICTED / 409 and subflowCallers are unchanged, and resume()/resumeInternal() are untouched. One helper, parkedRunsOf, answers both the verdict and the refusal's run list. It reads through the one reader of both run stores, readSuspendedRuns: the body listSuspendedRunsDurable already served, moved unchanged. The listing keeps its degrade posture, and the guard throws on an unlistable store, so an outage never reads as 'no parked run'. The B6 cycle rider is in. Measured live on the showcase: the card's own reproduction now completes (409 naming the armed caller, 200, 200). The map pair with a parked sign-off answered 409 naming the run and the door; the cancel answered 200 cancelled:true, and then the disable answered 200. Clause-② measures 'yes (narrowing)' (not the claim's 'yes (widening)'). The B6 corner refuses an enable that was accepted.",
      "tests": "All at head d59c97a50. Red first: 97222b887 against the unfixed engine gave 'Tests 7 failed | 40 passed (47)', each failure for the intended reason (the run id or the new step missing, the store error masked by the old DELETE_RESTRICTED, the B6 enable accepted). The fix is 172680217. pnpm --filter @objectstack/service-automation test: 'Test Files 155 passed (155)', 'Tests 1942 passed (1942)'. pnpm --filter @objectstack/service-automation run typecheck: exit 0 (tsc --noEmit, then 'check:test-typecheck: OK'). --listFiles counts flow-activation-ledger.test.ts once in tsconfig.json and once in tsconfig.test.json. Ablation: 7 legs via scripts/ablation-replace.mjs wrap mode (anchor hit x1, mutation proven on disk as anchor 1 to 0 and a blob change, restore proven: 'ok restored: blob == HEAD (7d48c737834c) and git diff HEAD is empty'), with an outer trap on EXIT/INT/TERM restoring by absolute path, re-run from committed d59c97a50. No dist leg: the test imports ./engine.js relatively, so it resolves src. M1 every packaged caller guards: 5 red (pins 1-5). M2 hot cache only: 2 red (durable-only, unlistable store). M3 degrade posture: 1 red (unlistable store). M4 cycle exemption asked of a status-disabled child: 1 red (B6). M5 no run ids in the refusal: 4 red (pins 2-5). M6 a disabled caller never guards: 4 red (pins 2-5). M7 'disabled' read from the ledger bit alone: 1 red (status-disabled caller). Live, a showcase boot (pnpm dev -- --fresh -p 41863, built at d59c97a50, stopped by its recorded process group): the card's reproduction and the map pair sequence, as in the summary.",
      "mcp_calls": "0 — no MCP GitHub tool was called; all GitHub reads were REST GETs to api.github.com for objectstack-ai/objectstack only",
      "api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]) carrying 3 endpoint writes: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft) = PR #20724, body read back byte-identical (16179 chars); (2) label-write, POST /repos/objectstack-ai/objectstack/issues/20724/assignees [os-justin], read back as matching, no label written; (3) post-stamped, POST /repos/objectstack-ai/objectstack/issues/20678/comments (this report). Plus git push (not REST): the empty-branch probe, then 5 commit pushes.",
      "gates": {
        "head": "d59c97a50",
        "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at d59c97a50 gave 62 commands, the same 62 as pm-gates-at-dispatch.txt. All 62 ran with the exit captured before any pipe. --ran: '62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of them is 3)'. 61 exit 0. check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET); after a full package build (turbo run build, 71/71 tasks, 45 cached) both exited 0 ('4 ledger entr(ies) re-measured ... none above its recorded number'). One designed red: node scripts/check-empty-changeset.mjs --base origin/main exits 1 on the DELIBERATE CORRECTION of the pending stage-1 changeset (see deviations and open_questions).",
        "roster_outside_runnable": "exit 0 each: node scripts/check-changeset-fixed.mjs; pnpm check:authz-resolver; pnpm check:error-code-casing ('no unlisted lowercase error codes in 6989 scanned file(s)'); pnpm check:filter-alias-parity",
        "extra": "exit 0 each: pnpm check:durability-log-level; pnpm check:startup-registry-verdict; node scripts/check-changeset-no-major.mjs --base origin/main --event (a synthetic pull_request payload carrying the PR body: 'LEVEL AXIS: this PR declares clause-② yes (narrowing), and no package whose packages/**/src/** it moves is graded patch'); check-adr-0087-registration with the same payload ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition', not-required (no-migration-prescription))",
        "package": "pnpm --filter @objectstack/service-automation test exit 0; pnpm --filter @objectstack/service-automation run typecheck exit 0",
        "lint": "declared narrowing: eslint --no-inline-config --format json on the 4 changed files gave 4 files, 0 errors, 2 warnings (both .md changesets: 'File ignored because no matching configuration was supplied'). Population from eslint.config.mjs files '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'. The config never enables type-aware linting (its own comment; no parserOptions.project), so the diff cannot move a verdict on an untouched file.",
        "not_measured": "the 6 value-bearing CI invocations dispatch-gates marks not runnable locally (check-issue-citations --census, 3 check-shard-attestation --emit, 2 check-test-completeness); reason: their argv comes from the workflow. CI on PR #20724: not awaited (in_progress at report time).",
        "nul_bytes": "pnpm check:nul-bytes exit 0; a control-byte self-scan of the 4 files found nothing"
      },
      "line_budget": "n/a: no skills/** or line-ratcheted ledger touched. Diff vs base 679f95ec5: +419 / -48 (467 changed lines, under the 5000 human-merge threshold).",
      "files_changed": [
        "packages/services/service-automation/src/engine.ts (+167/-45)",
        "packages/services/service-automation/src/flow-activation-ledger.test.ts (+229/-1)",
        ".changeset/20678-subflow-disable-parked-run.md (+21, new)",
        ".changeset/20678-subflow-disable-sequence.md (+2/-2, deviation: the pending stage-1 note)"
      ],
      "deviations": [
        "Outside the declared surface: the pending stage-1 changeset .changeset/20678-subflow-disable-sequence.md is amended in place, because this diff makes two of its sentences false ('Disabling a subflow is unchanged', and the cycle bullet under 'Not refused' that the B6 corner now contradicts). Per AGENTS.md a release note is corrected in its own entry. check-empty-changeset stays red on it by design until the seat confirms on the PR (open question 1).",
        "Clause-② declared 'yes (narrowing)', not the claim's 'yes (widening)'. Measured: the disable widens, and the B6 rider narrows the enable accept set in one corner (a child disabled both ways inside a ledger cycle was skipped whole, and is now named).",
        "Inside engine.ts but outside the functions the claim names: listSuspendedRunsDurable is split into a thin public wrapper plus the private readSuspendedRuns(onEnumerationFailure), the one reader of both stores (B1). One paragraph of its catch comment, which claimed the method has no production consumer, is corrected (plugin-approvals reads it and fails closed).",
        "The disable guard's parked-run read is awaited only when a switched-off caller is judged. A first version awaited it on every disable, and flow-terminal-messages.test.ts (it calls toggleFlow without await) went red on the one-microtask shift. Fixed in the engine (d59c97a50), not in that test.",
        "A moved envelope comment re-added the citation #10243, which check-issue-citations reports as allocated-but-absent. It now cites the toggle ruling's commit 266436a7f and its in-tree record (isFlowAuthoringWrite).",
        "Throwaway probe test files were written under src/ to print the refusal texts and measure B2/B7 at the engine seam. They were removed and never committed (tree verified clean before every commit).",
        "A full workspace build (turbo, 71 tasks) was run for the two exit-3 gates and for the live boot. One showcase dev server ran on port 41863 and was stopped by its recorded process group (session 32651)."
      ],
      "cleanup": "worktree objectstack-issue-20678-s2 removed (node_modules deleted, then 'git worktree remove' without --force, exit 0). Remote head == local head d59c97a50 was verified before removal. The dev server was stopped, with no listener left on 41863. The report comment was posted with the shared checkout's post-stamped.mjs (blob-identical to the worktree's copy), executed only.",
      "open_questions": [
        {
          "question": "Confirm the deliberate correction of the pending stage-1 release note .changeset/20678-subflow-disable-sequence.md in PR #20724 (check-empty-changeset stays red until confirmed). Four axes. Business: the next release's CHANGELOG would otherwise say 'Disabling a subflow is unchanged' in the same release that changes it, and would list a both-ways cycle child as 'Not refused' while it is refused. Long-term: AGENTS.md corrects a release note in its own entry, never by an erratum in a later one. AI-proofing: an upgrading agent greps the entry it lands on, so a false sentence there misleads with no counter-signal. Startup scope: two sentences in one file, no new mechanism.",
          "options": [
            "A: keep the correction; the seat confirms it on PR #20724 and lands with that one non-required red",
            "B: revert that one file; the false sentences ship, and this PR's own changeset carries the true statement"
          ],
          "recommendation": "A, because it keeps the release note true where readers land (AGENTS.md), at the cost of one confirmation; B ships a known-false sentence."
        },
        {
          "question": "Confirm the Clause-② declaration 'yes (narrowing)' with a minor + BREAKING changeset and the ADR-0087 marker not-required (no-migration-prescription). The claim line read 'yes (widening)'. The narrowing is the B6 rider, the order's in-scope corner. Four axes. Business: the corner is real (an enable accepted onto a child that stays disabled). Long-term: declaring the narrowing is what the arm exists for. AI-proofing: an undeclared narrowing is the #16421 failure. Startup scope: no extra mechanism.",
          "options": [
            "A: keep as delivered",
            "B: drop the B6 rider to make the diff a pure widening (the corner stays open)"
          ],
          "recommendation": "A, because the rider was ordered in scope, and it measurably narrows. check-changeset-no-major reads the declaration as delivered and exits 0."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: public door measured live: a create request through the automation create door that asserts package provenance answered 200 [request detail redacted by the domain:services seat under the security-family disclosure rule; the finding is carried abstractly by #20761]. It created a flow the engine treats as packaged (its re-enable is refused 409 RESOURCE_CONFLICT by the enable guard), calling the ledger-disabled showcase_one_task_signoff, registered enabled:true. POST /api/v1/automation/NAME/trigger then answered 400 FLOW_FAILED 'subflow showcase_one_task_signoff failed: Flow ... is disabled'. The toggle door refuses that same state. · evidence: B7, the registerFlow door; the engine seam is also measured (a new packaged caller is bound onto a ledger-disabled child, and a subflow republished obsolete under an armed packaged caller fails the caller at its node). The real producer (a package upgrade adding a caller) is not named. DELETE /api/v1/automation/NAME (unregisterFlow) of a packaged subflow is the same invariant's third door, read and not measured. §7.3 holds on toggleFlow only; one family card fits all three doors. · dedupe words: registerFlow subflow guard; packaged caller armed disabled subflow; automation create door FLOW_DISABLED; ADR-0126 7.3 door",
        "class: a · reach: public door measured live: POST /api/v1/automation/NAME/toggle on a flow created without package provenance answered 400 VALIDATION_FAILED 'Package is required' (field package_id), for both enabled:false and enabled:true. The caller sent no package field; the activation row is written with an empty package id. This predates the PR and is not changed by it. · evidence: the same showcase boot, flow zz_probe_b7_customer (deleted after). · dedupe words: toggle Package is required; activation ledger customer flow toggle; sys_metadata_activation package_id",
        "carrier: none · boundary: ObjectStoreSuspendedRunStore.list() reads at most 1000 paused rows, so beyond that (deployment-wide) a caller's parked run can be missing from the enumeration and the disable would be accepted. Not measured. Noted in the PR Acceptance notes, not filed.",
        "carrier: none · boundary: a caller run executing (not parked) at the instant of the disable is in neither store and can still reach its subflow node. Triage ruled on \"holds a parked run\"; not built around. Noted, not filed.",
        "carrier: none · boundary: a failed caller run with a restorable consumed suspension (the restore-suspension door) is terminal and does not count; restoring it after the child is disabled re-parks it into a failure. Read, not measured. Noted, not filed.",
        "carrier: the seat (raised with the maintainer) · ADR-0126 §7.3 heading \"attached to disable\" under-describes the guard; docs/adr/** untouched here."
      ]
    }

    Generated by Claude Code

  11. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20724 at d59c97a5 (the disable half) · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T22:14Z

    Checklist, read on GitHub rather than from the report:

    • Shape: a draft onto main. 4 files, +419 / −48:
      • engine.ts;
      • flow-activation-ledger.test.ts;
      • the new changeset .changeset/20678-subflow-disable-parked-run.md;
      • a two-sentence correction of stage 1's pending note .changeset/20678-subflow-disable-sequence.md.
        No governed path.
      • Line 1 is Fixes #20678, and line 2 is Clause-②: yes (narrowing). One footer; assigned os-justin; no label written.
    • Order of commits: pins red first (97222b88: 7 failed), then the fix (17268021), then the changeset, the citation fix and the microtask fix (d59c97a5).
    • Read at source, against triage's answer 5898655174:
      • One helper. parkedRunsOf answers both the verdict and the refusal's run list. It reads through readSuspendedRuns, the one reader of both stores: the in-process cache and the durable store. The listing door's body moved there unchanged.
      • Fails closed. The guard reads with 'throw', so a store that cannot be listed refuses the disable instead of reading as "no parked run". The listing door keeps its degrade posture.
      • Who guards. An enabled packaged caller still guards (step: disable it first). A disabled caller, by the ledger or by its status, guards only while it holds a parked run (step: cancel each named run through the ADR-0044 door, or let it finish). A disabled caller with no parked run no longer guards.
      • Unchanged: DELETE_RESTRICTED / 409 / subflowCallers. resume() and resumeInternal() are untouched.
      • The B6 rider. A status-disabled child is never cycle-exempt: the status check now runs before the ledger-cycle exemption.
    • Live: the card's own reproduction now completes (409 naming the armed caller, then 200, then 200). The map pair with a parked sign-off answered 409 naming the run and the door; after a cancel, the disable answered 200.

    The dev's two open questions, answered:

    1. The correction of stage 1's pending note: confirmed path, A.
      • Two of that note's sentences are made false by this PR: "Disabling a subflow is unchanged", and the unqualified cycle bullet that B6 now narrows. AGENTS.md corrects a release note in its own entry.
      • Check Changeset is red by design on this DELIBERATE CORRECTION class (scripts/check-empty-changeset.mjs, rule 2), and it is not a required context.
      • Per this seat's landing rules, the confirmation is the at-tier contract review PASS on this same head, provided its record names the note and judges each rewritten sentence. It does not wait for the maintainer. The review brief says so.
      • The check stays red, and the PR lands with it only under the three conditions:
        • the gate's own source says it is red by design (pr-automation.yml, route 0);
        • it does not run on merge_group;
        • a PR comment records the gate and the reason.
    2. Clause-②: yes (narrowing) with a minor + BREAKING + ADR-0087 not-required: kept as delivered, A.
      • The disable half widens (a refused disable becomes accepted), and the B6 rider narrows one enable corner. That is exactly the grammar's yes (narrowing), "a diff that widens one surface and narrows another".
      • This seat's claim line (yes (widening)) was wrong for the second time on this card. The rider this seat ordered is the narrowing. The dev measured instead of copying, as the order asked.

    Findings, line by line:

    Landing waits for three things: every other check green on the head, the at-tier contract review (which also confirms the correction), and the red-landing conditions above. After the merge, Fixes #20678 closes this card.


    Generated by Claude Code

  12. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T22:50Z

    PR #20724 merged through the merge queue as 36d043be on origin/main, and Fixes #20678 closed this card completed.

    • Review: the contract review PASS on the landed head is 5900305615. It also confirmed the DELIBERATE CORRECTION of stage 1's pending note. The one red check (Check Changeset, red by design, not required) is recorded on the PR in 5900329392.
    • Content check: the landed commit's git patch-id --stable equals that of the reviewed head d59c97a5 against its base.

    What now holds, both directions of ADR-0126 §7.3's packaged-subflow guard:

    For the release list: @objectstack/service-automation ships both halves as minor, marked BREAKING:

    • stage 1 is Clause-②: no (narrowing);
    • stage 2 is yes (narrowing).

    Carried elsewhere:

    Unblocks here: #20596's service-automation citation stage.

    In the same act, this seat removes pm:dispatched and the assignee.


    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

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions