Skip to content

service-automation: a restart re-arms the trigger of a ledger-disabled packaged flow — registerTrigger ignores the activation ledger, and every matching event then logs an ERROR claiming a run-history record that is never written #20677

Description

@objectstack-fleet

QA-source: #20674 · automation.packaged-flow-disable-durable · c3

What happens

A packaged flow switched off through the ADR-0126 activation ledger comes back bound after a cold restart. Execution is still refused (no run is recorded, so the kill switch itself holds), but the trigger is armed again, every matching mutation fires it, and each firing is logged at ERROR as a failed trigger-fired run whose failure "is recorded in the flow's run history" — while the run history holds nothing.

Measured on main at 6bff748b (showcase, objectstack dev, file DB), reproduced on two consecutive cold boots over the same DB file.

Reproduction

  1. Boot with a file DB; as the seeded admin POST /api/v1/automation/showcase_urgent_task_alert/toggle {"enabled":false} → 200. GET /api/v1/automation/_status → enabled:false, bound:false; the ledger row reads active: false.
  2. Stop the server process entirely; boot again over the same DB file (OS_LOG_LEVEL=info shows the lines below).
  3. GET /api/v1/automation/_status. Expected: enabled:false, bound:false. Actual: enabled:false, bound:true, stable after waiting.
  4. Boot-log order: [Automation] Activation ledger: 1 packaged flow(s) are switched off for this installation and were left unbound — 'showcase_urgent_task_alert'. → [Automation] Bound 30 flow(s) from the protocol at kernel:ready → Trigger registered: record_change.
  5. POST /api/v1/data/showcase_task with priority: 'urgent' (plus the required title, project, status): the server logs ERROR Trigger-fired run of flow 'showcase_urgent_task_alert' failed (trigger 'record_change') — no caller holds this result and nothing retries the run; the terminal failure is recorded in the flow's run history… carrying the FLOW_DISABLED sentence; GET /api/v1/automation/showcase_urgent_task_alert/runs (also ?status=failed) shows no new run.

Mechanism (read from source at 6bff748b)

  • AutomationEngine.registerTrigger (packages/services/service-automation/src/engine.ts) iterates every registered flow not in boundFlowTriggers and calls activateFlowTrigger(name) when its trigger type matches — without consulting isFlowEnabled(name). activateFlowTrigger has no enablement check either.
  • Trigger plugins register their trigger at kernel:ready, which fires after AutomationServicePlugin.start() pulled the flows and ran hydrateFlowActivations() (which correctly unbound the flow and logged so).
  • registerFlow does honour the ledger ("A ledger-disabled flow is NOT re-armed here…"); this second arming path does not. The engine pins in flow-activation-ledger.test.ts cover hydration and re-registration, not a trigger registered after hydration.

Why it matters

  • /_status — the Studio status badges' source — reports a disabled flow as bound after every deploy.
  • An administrator's deliberate switch-off produces an ERROR line per matching event, and that line claims a run-history record that does not exist, which reads as a production failure.
  • The item's P0 contract ("a restart that re-arms the flow (enabled or bound true…) is a FAIL against the whole point of ADR-0126 §7.2") is red on main. RUNNER rule 7 applies: re-derive independently before acting.

Expected

After a restart a ledger-disabled flow stays unbound regardless of when its trigger type registers; no trigger-fired failure is logged for it.

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


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: automation — an administrator's switch-off survives a restart | 缺项 (registerTrigger arms every matching flow without consulting the activation ledger, so a ledger-disabled packaged flow comes back bound:true after a cold boot, and each matching event logs an ERROR claiming a run-history record that is never written) | P1

    Triage: first grade — bug · priority:p1 · domain:services · area:workflow · pm:queue. Direction: one enablement gate at the single arming point, so no arming path can bypass the ledger

    Triage: lands in packages/services/service-automation/src/engine.ts (registerTrigger / activateFlowTrigger) ⇒ domain:services. It is a finding of acceptance run #20674 (card #12438, epic #12150).

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

    Why p1, not p0.

    • The kill switch holds: no run executes, so nothing is written behind the administrator's back.
    • But the item's own contract is red on main: 「a restart that re-arms the flow … is a FAIL against the whole point of ADR-0126 §7.2」.
    • After every deploy, /_status (the Studio badge source) reports a switched-off flow as bound, and every matching event logs an ERROR that describes a record which doesn't exist.

    Direction.

    • One gate. activateFlowTrigger itself refuses to arm a flow isFlowEnabled answers false for. Then registerFlow, registerTrigger and any later arming path all inherit it. ⛔ Don't patch registerTrigger alone: that leaves the next path to repeat this.
    • The log line tells the truth. A trigger-fired failure says "recorded in the flow's run history" only when a run row is written. A FLOW_DISABLED refusal that reaches it anyway (for example, a race) is logged as what it is, ⛔ not as a production failure.
    • Pins:
      • A trigger type registered after hydrateFlowActivations() doesn't arm a ledger-disabled flow.
      • A cold restart over the same store keeps enabled:false, bound:false.
      • An enabled sibling still arms (the control).
    • Release: ADR-0126's switch-off is live in shipped versions, so this is recommended for the next release, and it is on the triage seat's release list.
  2. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Sep 29, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-09-29T18:00Z
    Session: session_01XY5uCwTjZj7884yYtyur4H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20677-ledger-disabled-stays-unbound
    Worktree: objectstack-issue-20677
    Domain: domain:services
    Seat: domain:services (seat 1, seat post #6021)
    Scope, per triage's grade 5895639312:


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20677,
    "status": "done",
    "branch": "claude/issue-20677-ledger-disabled-stays-unbound",
    "pr": "#20702",
    "session": "session_01XY5uCwTjZj7884yYtyur4H",
    "premise_still_valid": true,
    "summary": "The card's reproduction was reached on current main (14f80e2) at the engine level: a boot replay over one activation store in plugin.ts's order (pull, hydrateFlowActivations, kernel:ready protocol sync, registerTrigger, seal) read { enabled: false, bound: true } on the first restart. That is the card's /_status shape, and 10 of the 11 new pins were red on the unfixed engine (commit 1a7ce83). The fix (commit 7589676, engine.ts only) puts ONE enablement gate in activateFlowTrigger: it returns before arming when isFlowEnabled is false, so registerFlow, registerTrigger and toggleFlow's enable branch all inherit it, and registerFlow's caller-side check is folded into it. The trigger-fired callback now logs a FLOW_DISABLED refusal at info with no run-history claim, and says 'recorded in the flow's run history' only for status 'failed'. A never-dispatched failure keeps the error line without that claim. A2 measured: activateFlowTrigger has three callers. policyDisabledFlows, the /_status reason and the binding audit are unchanged for enabled flows. One behaviour change beyond the card's path, pinned: toggleFlow(name, true) on an obsolete or invalid flow no longer arms it, and a status-disabled flow is not re-armed by a later registerTrigger (both were armed-but-refusing before). The re-enable path for a ledger-disabled flow still arms, also pinned. PR #20551's api secret refusal is untouched. The PR is draft, assigned to os-justin, and CI was in_progress at the report (31 check-runs: 18 in_progress, 10 success, 3 skipped, 0 failed).",
    "tests": "At HEAD 7589676, the last commit; the package suite, typecheck, lint narrowing and gate union were all read after it. (1) pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/flow-activation-late-trigger.test.ts: 11 cases. On unfixed main: 'Tests 10 failed | 1 passed (11)', including 'restart 1: expected { enabled: false, bound: true } to deeply equal { enabled: false, bound: false }'. On the fix: green. (2) pnpm --filter @objectstack/service-automation test: 'Test Files 155 passed (155)', 'Tests 1924 passed (1924)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/service-automation typecheck: exit 0 (tsc --noEmit plus 'check:test-typecheck: OK'); tsc -p tsconfig.test.json --listFiles lists the new test (1 hit). (4) Ablation via scripts/ablation-replace.mjs in wrap mode, with a trap 'git checkout HEAD -- ABS_PATH' on EXIT INT TERM. The subject resolves from src through the relative ./engine.js import, so there is no dist leg. (a) Deleting the gate line 'if (!this.isFlowEnabled(flowName)) return;' (anchor x1 then x0, blob 566ed4d67921 then d09694f76678) gave 'Tests 8 failed | 3 passed (11)': every arming pin went red, and the 3 log-line pins do not depend on the gate. (b) FLOW_DISABLED branch changed to 'if (false) {': 'Tests 1 failed | 10 passed', with the info pin red ('expected [ Array(1) ] to deeply equal []'). (c) History condition forced true: 'Tests 1 failed | 10 passed', with the never-dispatched pin red. Each restore was proven by 'ok restored: blob == HEAD (566ed4d67921) and git diff HEAD is empty', and git status --porcelain was empty. (5) Gates: node scripts/pm/dispatch-gates.mjs --commands, re-derived at 7589676, matched the dispatch's 62 commands. With exit codes captured before any pipe, 60 exit 0. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, both exit 3 PREREQUISITE NOT MET because they need the whole workspace's dist/; CI owns them. --ran: '62 derived famil(ies) accounted for — 60 run, 2 NOT-MEASURED'. Roster families check-changeset-fixed, check:authz-resolver, check:error-code-casing and check:filter-alias-parity: exit 0. Also run, exit 0: check:durability-log-level, check:startup-registry-verdict, check:nul-bytes. (6) Lint narrowing: eslint --no-inline-config --format json on engine.ts and the new test gave '2 files, 0 errors, 0 warnings'. The changeset .md matches no config (print-config undefined). No type-aware linting is configured (no parserOptions.project or projectService), so the diff cannot move verdicts on untouched files. (7) The plugin-level (LiteKernel) cold boot is NOT MEASURED: it needs a new data-engine double under check:engine-double-contract, so the engine-level boot replay stands in for it.",
    "mcp_calls": "0 — no MCP GitHub tool called",
    "api_writes": "3 — each one repository_dispatch through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft, #20702, body read back byte-identical, 11029 bytes); (2) label-write assign, i.e. POST /repos//issues/20702/assignees os-justin (read back: assignee os-justin; labels size/m, documentation, tests and tooling come from bots, and none were written by this run); (3) post-stamped os-dev-report, i.e. POST /repos//issues/20677/comments. Plus git push (not REST). All other GitHub traffic was unauthenticated public GET reads.",
    "deviations": [
    "Clause-② stays no: no export, option, route or response shape moved.",
    "The cold-restart pin is engine-level (one shared InMemoryFlowActivationStore, plugin.ts boot order replayed), not a LiteKernel boot of the real plugin, for the reason in tests (7).",
    "Not merged forward: origin/main moved 3 commits since base (3b47a69, 0be8984, f379f57), none touching packages/services/service-automation or packages/spec/src/contracts; CI and the merge queue validate the merge ref.",
    "Beyond the dispatch list I also ran check:durability-log-level and check:startup-registry-verdict, because the diff edits a log level and an arming decision."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — a comment in packages/services/service-automation/src/api-trigger-secret-registration.test.ts says an obsolete flow 'is re-enabled by a toggle'. Under ADR-0126 toggleFlow moves only the ledger bit, so it never re-enables a status-disabled flow. This is comment drift outside the claim's file surface.",
    "carrier: none · noted, not filed — pre-existing edge: when recordLog throws on execute()'s failure arm, the result still carries status 'failed', so the trigger-fired line says the failure is recorded while the bookkeeping ERROR line says the row never landed. AutomationResult has no write-failed channel, the bookkeeping line is itself loud, and it was not measured through a public door."
    ]
    }

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20702 at 75896761 · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T18:41Z

    Checklist, read on GitHub rather than from the report:

    • Shape: a draft onto main, assigned os-justin. Fixes #20677 is line 1 and the only closing keyword, and Clause-②: no is line 2. 3 files, +403 / −17. No governed path.
    • Read at source, against triage's direction (5895639312):
      • One gate: if (!this.isFlowEnabled(flowName)) return; sits inside activateFlowTrigger, the one point every arming path crosses: registerFlow (its caller-side check folded in), registerTrigger, and toggleFlow's enable branch. registerTrigger's loop carries a ⛔ note against a second check. Not a registerTrigger-only patch.
      • The truthful line:
        • a FLOW_DISABLED refusal is logged at info, saying nothing ran and no run-history row records it;
        • the ERROR line claims "recorded in the flow's run history" only for status: 'failed' (a run that dispatched);
        • a never-dispatched failure keeps the error without that claim.
      • This reuses the file's logger. There is no second dialect.
    • Evidence:
      • Reached on main: an engine-level boot replay in plugin.ts's order read { enabled: false, bound: true } on the first restart, the card's shape. The pins were committed red first: 10 of 11 failed.
      • Ablations: removing the gate turned all 8 arming pins red. Each log-line branch reddens its own pin. Every restore was proven.
      • service-automation 1924 passed; typecheck exit 0.
      • 60 / 62 derived gates plus the 4 ⛔ rosters exit 0. check:dual-build-cjs-loads and check:type-check-debt are NOT MEASURED locally, lacking a whole-workspace build; CI decides them.
    • Behaviour beyond the card's path, accepted and pinned: it follows from the gate reading isFlowEnabled, which composes both disable dimensions.
      • Re-enabling the ledger bit of an obsolete / invalid flow no longer arms it; every run it fired would be refused.
      • A status-disabled flow is not re-armed by a later trigger registration.
      • The ledger re-enable path still arms.
    • Deviation accepted: the cold-restart pin is engine-level, replaying the boot order over one activation store, rather than a LiteKernel boot. A plugin-level boot would need a new engine double under check:engine-double-contract.
    • PR fix(trigger-api,service-automation): refuse an api flow with no per-flow secret, at arm time and at registration (#20529) #20551's arm-time api secret refusal is untouched.

    Findings:

    • A test comment in api-trigger-secret-registration.test.ts says an obsolete flow "is re-enabled by a toggle", which is now untrue under ADR-0126 → Acceptance notes. It is comment drift outside the surface and rides that file's next touch.
    • When recordLog throws on execute()'s failure arm, the result still says failed, so the line claims a record while the bookkeeping ERROR says the row never landed → Acceptance notes. This is pre-existing and not measured at a public door. The bookkeeping line is loud on its own.

    Landing: waits for every check to be green and for the at-tier contract review the .changeset prose owes. Triage marks this card recommended for the next release.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 758967616e1f8078c884bbea146c5980c19e7ad2
    Local-runs: none

    Inputs, and nothing else: card #20677 (body and all four comments — triage 5895639312, claim 5895758242, os-dev-report 5896376202, ACCEPT 5896413509), PR #20702 (body, file list, the net diff against main from merge-base 14f80e23: 3 files, +403 / −17), and the check-runs on this head. Read-only: git show / git diff against the shared object store; nothing built, run or re-run. This verdict is derived from the diff and the card, not adopted from the ACCEPT.

    ① Derived judgments

    1. Accept-set: none moved — right. packages/spec is untouched; no Zod schema, metadata key, route input, response shape or env var changes. execute()'s FLOW_DISABLED exit and describeDisabledFlow are byte-identical.
    2. Exports: none moved — right. activateFlowTrigger is private, src/index.ts is untouched, and the new test imports deployment-switch.test-support.ts, a pre-existing helper that is outside the tsup entry and exported by nothing.
    3. getFlowRuntimeStates() / GET /api/v1/automation/_status: a ledger-disabled flow whose trigger type registers after hydrateFlowActivations() now reads bound: false (was true) — right. That is the card's contract and ADR-0126 §7.2 ("the install-level row may also unbind the trigger"). reason is unchanged because describeUnboundReason already answered undefined for a disabled flow before reading anything else, so getTriggerBindingAudit() is unchanged too — the restart pin asserts the audit stays empty.
    4. One gate at the one arming point — right, and it is triage's direction, not a registerTrigger-only patch. Read at head: activateFlowTrigger has exactly three callers (registerTrigger :3382, registerFlow :4327, toggleFlow's enable branch :4718), and trigger.start( has exactly one call site in engine.ts (:3706), inside it. registerFlow's caller-side if (this.isFlowEnabled(name)) is folded into the gate; equivalent, since flowStatusDisabled is written at :4296 before the re-arm at :4327. isFlowEnabled composes both disable dimensions (ledger and obsolete / invalid status), so the gate inherits both.
    5. Behaviour beyond the card's path: toggleFlow(name, true) on an obsolete / invalid flow, and registerTrigger for a status-disabled flow, no longer arm — right. Before, both produced an armed flow every event of which execute() refused with FLOW_DISABLED and logged at ERROR: the card's defect through another door. The declared contract (FlowRuntimeState.enabled: "a disabled flow stays registered and never runs"; IAutomationService.toggleFlow: "Enable or disable a flow") promises no bound: true for a disabled flow, so this is a pull back to the contract, not a widening or narrowing of it. Both are pinned (the enable path still arms, second case; a STATUS-disabled flow…).
    6. FLOW_DISABLED reaching the trigger-fired callback is logged at info with no run-history claim — right. By the degradation rule (AGENTS.md "Degradation log levels"): nothing the system claims persisted failed to land, and the refusal is the switch working. The only residue paths are an event in flight at the switch-off (benign) and a stop() that threw, which deactivateFlowTrigger already reports at warn ("may keep firing until the process restarts") where it happens.
    7. The error line claims "recorded in the flow's run history" only when result.status === 'failed' — right. status is the contract's own discriminator (AutomationResult JSDoc: trigger-time refusals carry no status; the ran-and-failed exit stamps 'failed' after writing its row, as do retryExecution's exits). The call keeps the error(message, undefined, meta) shape — one physical line, envelope in meta third — so the three pins in engine-residual-log-cause.test.ts on this line still hold. One asymmetry, stated by the dev in the PR body: FLOW_INPUT_SCHEMA_INVALID writes one failed row yet carries no status, so its line now says "refused before it dispatched" (true — no node ran) and omits the row. Less informative, not false; no false claim remains on that line.
    8. policyDisabledFlows: a disabled scheduled flow now returns before the scheduled-work policy gate and records no policy refusal — right, and invisible on the wire, since describeUnboundReason returns before reading that set for a disabled flow.
    9. The new test file is a faithful pin — right. The restart case replays plugin.ts's real order, verified at head: start() → registerFlow (:992) → hydrateFlowActivations() (:1035); kernel:ready (:1122) → protocol re-sync registerFlow (:2089); kernel:bootstrapped (:1138) → sealNodeTypeVocabulary() (:1166); trigger plugins register at kernel:ready. It reads no filesystem path (the cross-package-inputs gate is not implicated), covers all four trigger kinds a plugin registers at kernel:ready with an enabled sibling as the control, and was committed red first (1a7ce83b7, 10 of 11 red on the unfixed engine). Each of the three ablations reddens exactly the pins that depend on the ablated line.
    10. Docs: nothing owed — right. content/docs/** and apps/docs/** are untouched, and at head nothing outside engine.ts, the new test and the CHANGELOG history describes the old line; the CHANGELOG entries (e218483) describe the Trigger-fired run of flow '…' failed lead phrase, which the error line keeps.

    ② Semver level

    • .changeset/20677-ledger-disabled-stays-unbound.md declares '@objectstack/service-automation': patch. The package is a released member of the changesets fixed group at 17.5.0; a bug fix in a released package takes patch, never skip-changeset (AGENTS.md Post-Task Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3). No export, option, route or response shape moves; a log line's wording and level are not a published contract. patch is right.
    • Clause-②: no — right. It is line 2 of the PR body and in the changeset body, matching the claim (5895758242). The criterion is whether the card widens the accept-set or expands the public surface; this diff pulls behaviour back to the declared contract (FlowRuntimeState, ADR-0126 §7.2, the FLOW_DISABLED classification) and adds nothing an author or consumer can newly write or read. No (widening) / (narrowing) arm and no ADR-0087 marker, correctly: the changeset is not breaking.
    • Changeset prose, sentence by sentence against the diff — holds. The toggle route, the /_status shape, the ERROR line's quoted claim, the three arming paths, the info level, "recorded … only for a run that dispatched and failed", the obsolete-flow re-enable note, and "The runtime refusal (FLOW_DISABLED) and its message are unchanged" (describeDisabledFlow untouched) are each true of the code at this head.
    • Check Changeset on the head: success.

    ③ Boundary flags

    Dev deviations (os-dev-report 5896376202):

    1. "Clause-② stays no" → answered: right (② above).
    2. "The cold-restart pin is engine-level, not a LiteKernel boot" → answered, accepted. The defect sits in the engine's arming path; plugin.ts is untouched and the replay follows its verified order (①.9). A plugin-level boot needs a new data-engine double under check:engine-double-contract, outside the claim's file surface. The card's five-step HTTP reproduction stays with the acceptance run (QA run · card:12438 (15/15) · 6bff748b · 2026-09-29 · 6 PASS / 1 PARTIAL / 8 FAIL / 0 BLOCKED / 0 NOT-RUN #20674) to re-read on a shipped build; not owed by this PR.
    3. "Not merged forward: origin/main moved 3 commits" → answered, accepted. main is now 5 commits past merge-base 14f80e23 (compare API behind_by: 5; local rev-list agrees); none of the five touches packages/services/service-automation/ or packages/spec/src/contracts/ (git log over those paths is empty). No semantic conflict; the queue validates the merge ref.
    4. "Also ran check:durability-log-level and check:startup-registry-verdict" → answered: additive, appropriate to a log-level and arming edit; CI's Lint & Repo Gates decides them for the record.

    open_questions: [] — nothing to answer.

    Out-of-scope findings (carrier: none):

    1. Comment drift in api-trigger-secret-registration.test.ts :79-80 ("an obsolete flow is re-enabled by a toggle") → answered: a note is the right carrier. Comment-only; that case asserts registration refusal, not toggle arming, and passes at this head. It rides the file's next touch.
    2. recordLog throwing on execute()'s failure arm leaves status: 'failed' on the result while the catch's own ERROR says the row never landed → answered: not a FAIL finding, and not escalated to a card. AutomationResult has no write-failed channel; adding one is a packages/spec contract change (Clause-② territory) that this card does not own. The catch's bookkeeping ERROR is itself loud and names the failed write, so no reader is left with only the false half. Not measured through a public door. The note stands as written.

    Check-runs on 75896761, read at 2026-09-29T18:52Z: 34 runs — 30 success, 3 skipped by path (Console Pin Gate, Build Docs, Packed-tarball smoke), 0 failed, 1 in progress (Lint & Repo Gates). Their conclusions are the gate verdicts; the one in progress is the landing gate's to read, not this record's, and landing waits for it (every check green, not a required subset).

    Implemented-by: claude/issue-20677-ledger-disabled-stays-unbound
    Reviewed-by: session_01XY5uCwTjZj7884yYtyur4H

    VERDICT: PASS


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

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

    PR #20702 merged through the merge queue as defc7f7b on origin/main, and Fixes #20677 closed this card completed.


    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:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions