Skip to content

feat(automation)!: an approval decision resumes a flow as the person who decided - #24527

Merged
objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-24321-resume-as-decider
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/issue-24321-resume-as-decider

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24321

Clause-②: yes (narrowing). The narrowing: under runAs: 'user', a post-decision node that the submitter's rights admitted and the decider's do not is now refused (ruling A item 3). The widening: three published interfaces each gain one optional member, ResumeSignal.resumedBy (@objectstack/spec/contracts) and the resumedBy field on ApprovalResumeSurface's resume signal and on StrandedContinuationSignal (@objectstack/plugin-approvals). No schema shape moves: flow.runAs keeps its two values and only its describe changes.

After an approval decision (approve or reject), the flow's following nodes run as the person who decided: ruling A (comment 6257886738), with item 2 read on its measured population by ruling 6267719791 (Q1 A, Q2 A).

What changes

Measurement (ruling A item 5)

HotCRM's four branches: who holds the write each post-decision node makes

Read only, objectstack-ai/hotcrm at 296167936b06 (pinned to @objectstack/* 18.0.0-next.3). Rep = sales_rep (src/sales/profiles/sales-rep.profile.ts); manager = sales_manager (src/sales/profiles/sales-manager.profile.ts). Object edit and row policy are the same on every row: the manager holds crm_opportunity: { allowEdit, viewAllRecords, modifyAllRecords } (sales-manager.profile.ts:22) under the select row policy opportunity_private_owner_only (:117–:127, is_private == false || owner_id == current_user.id); the rep holds allowEdit with readScope: 'own' (sales-rep.profile.ts:52). Neither set carries a field entry for any column below.

branch node field(s) manager holds it rep holds it
opportunity_approval approve mark_approved approval_status, approved_date no: both readonly: true (opportunity.object.ts:485, :498) no, same
opportunity_status_change_approval approve apply_status stage, status_change_approval_status, approved_date stage yes; the other two no: readonly: true (:549, :498) stage yes on own deals; the other two no
opportunity_status_change_approval reject clear_request status_change_approval_status, requested_status requested_status yes; the verdict no: readonly: true (:549) requested_status yes on own; the verdict no
opportunity_qualification_approval reject clear_request qualification_approval_status, qualification_requested qualification_requested yes; the verdict no: readonly: true (:215) qualification_requested yes on own; the verdict no

Every one of these nodes writes a readonly: true column, which objectql strips from every non-system UPDATE (packages/objectql/src/engine.ts, the if (!opCtx.context?.isSystem) branch of the UPDATE path). So all four branches need elevation, and all four flows already declare runAs: 'system' (opportunity-approval.flow.ts:57, opportunity-status-change-approval.flow.ts:70, opportunity-qualification-approval.flow.ts:72). So do the other two HotCRM approval flows (account-approval.flow.ts:75, lead-conversion-approval.flow.ts:66). Recorded per item 3 as the pull for a finer, per-node elevation: the ordinary fields (stage, requested_status, qualification_requested) could then go through the manager's own rights while the verdict and stamp columns land as platform writes.

Under ruling 6267719791 Q1 A, this PR now reaches all four branches. They keep their elevation, and their post-decision writes name the manager (next table). The in-repo showcase_invoice_signoff (runAs: 'system', a post-decision update_record) is met with them.

After the change: attribution, and the run's report when a right is missing

Measured through the real doors, on the harness HotCRM's own suites boot (@objectstack/verify's bootStack, requires: ['automation', 'triggers', 'approvals', 'audit', 'sharing']). The fixture mirrors HotCRM's shapes: a private deal object with HotCRM's readonly approval_status and approved_date columns; a rep set (allowEdit, readScope: 'own'); a manager set (allowEdit, viewAllRecords, modifyAllRecords, with restricted_note non-editable); a record-after-create flow with a position-routed approval node and a post-decision update_record on each branch. The rep creates the deal with POST /api/v1/data/OBJECT; the manager decides with POST /api/v1/approvals/requests/ID/approve or /reject. It was a one-off measurement file, deleted after each run; the permanent pins are listed under Tests.

flow (runAs) decision updated_by before → after sys_audit_log update row readonly column run door
system (HotCRM shape) approve rep → manager {approval_status, approved_date} by manager approval_status: approved, approved_date set completed 200
system (HotCRM shape) reject rep → manager {requested_status, approval_status} by manager approval_status: rejected completed 200
user approve rep → manager {approved_note} by manager (not written) completed 200
user reject rep → manager {requested_status} by manager (not written) completed 200
user, the manager lacks the field approve rep → rep (nothing landed) none (not written) failed at post_approve: [Security] Field write denied: not permitted to edit [restricted_note] on 'OBJECT' 500 RESUME_FAILED, repairable: true, naming the request, the run, the node and the denial

All five rows were measured at c17394ee5, the final head. The rep's own create row stays theirs in every case. A pre-decision flow node keeps the submitter too: in the service-automation real-stack test, the submit node leaves updated_by and its audit row on the rep, on both postures.

Rulings

Open questions from the first round are ruled: 6267719791, Q1 A, Q2 A (maintainer 「同意」 2026-10-19T12:55Z). Q1 A: a runAs: 'system' flow keeps system privileges, and after the decision its attribution user is the decider. Q2 A: send-back, resubmit and recall keep item 1 as written.

Tests

At c17394ee5:

  • packages/services/service-automation/src/resume-decider-identity.integration.test.ts (new, 21 pins; 1.32 s of test time).
    • Envelope, read off a recording node that captures resolveRunDataContext(context). runAs: 'user' approve and reject carry the decider's resolved grants and none of the submitter's. A later pause keeps the decider; a second decision moves to its own decider while the run history keeps the trigger; a decider whose grants fail to resolve runs on the baseline.
    • Envelope, runAs: 'system': approve and reject carry { isSystem: true, userId: usr_mgr, positions: [], permissions: [] } with no grant resolved and the forwarded grants removed, and the run history keeps the trigger.
    • Envelope controls: a system run with no decider keeps the rep; a decider without the service marker, no decider, and no decision node are all unchanged.
    • Real stack (ObjectQLPlugin, driver-sql, identity objects, SecurityPlugin, SharingServicePlugin, AuditPlugin). runAs: 'user': updated_by and the sys_audit_log row name the manager after and the rep before; a guard shows the manager's write rests on the manager's own sets; an FLS-denied post-decision node is refused as a repairable strand naming the node and field, while the same node with no decider lands as the rep. runAs: 'system': a guard shows the readonly verdict column is stripped from the manager's own request; approve and reject land verdict under the unchanged elevation, with updated_by and the audit row naming the manager; with no decider, the rep.
  • packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts (one describe, 6 pins, on the file's existing pinned engine double, through the real ApprovalService and approval node). decide approve and reject run a runAs: 'user' flow's post-decision nodes as the decider with the decider's grants, and a runAs: 'system' flow's as { isSystem: true, userId: u1 }. The SLA-shaped decision keeps the run's identity. A stranded decision journals resumedBy: 'u1', and continueRestoredRun replays as the decider.
  • Suites at c17394ee5 (after merging origin/main to 04669a467): @objectstack/service-automation pnpm test 192 files / 2567 passed; @objectstack/plugin-approvals pnpm test 81 / 1118 passed; @objectstack/spec pnpm test 701 / 21311 passed (1 todo). typecheck is green for all three (check:test-typecheck OK, debt ledgers unchanged).
  • packages/services/service-automation/package.json gains @objectstack/plugin-sharing and @objectstack/plugin-audit as devDependencies (lockfile: 6 lines), each aliased to source in vitest.config.ts. A private object's write verdict belongs to record sharing, and the audit row is the audit plugin's.

Ablation (one-off, not kept)

Both runs went through scripts/ablation-replace.mjs on engine.ts. The suite imports the engine relatively, from source, so no build or dist leg applies. Each direction was predicted before its run.

  • Round 0, the whole switch. The decider read was replaced with undefined (blob 7172068a6966 → 6dfa06c97035). Predicted red on the decider pins; observed 8 failed / 6 passed of the 14 then present. Restored: blob == HEAD, git diff HEAD empty.
  • Round 1, the system half. || context?.runAs === 'system' was added to the early return (anchor hit 1 → 0; blob 019ddb6c6fd5 → 0a62cc66e57a). Predicted red on exactly the four system attribution pins; observed 4 failed / 17 passed. Failed: envelope system approve and reject, and real-stack system approve and reject. The system no-decider controls and the history pin stayed green. Restored: blob == HEAD 019ddb6c6fd5, git diff HEAD empty.

Gates

Local, at c17394ee5, after the final commit: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 127 families over this diff. All 127 were run, each recorded :: exit 0. --ran reconciliation: 127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN. The local-only check:dev-prereqs is OK. The changeset gates on the new text: check-adr-0087-registration --base origin/main OK (not-required (no-migration-prescription), read as BREAKING + bang + clause-② narrowing), with check-changeset-no-major and check-empty-changeset OK. An earlier start of this round's union was stopped and re-run from scratch after packages/spec was rebuilt: a comment-only edit had left its dist stale, and the fresh worktree lacked 21 package builds. CI's check-runs on this head are the seat's to read.

Round 2 (contract review 6270059532, rework 6270117310), head e5150923a. This round changes no code: two sentences of content/docs/automation/flows.mdx, and the decider pin file's header comment, which now names restored-approval-continuation.test.ts. origin/main was merged to 2667e2402 (e5150923a, via os-regen-merge.sh); its six commits touch no package this PR changes. The derived list was re-derived on the final diff (127 families) and run in full: every family :: exit 0, and --ran gives 127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN. check:dev-prereqs and check-adr-0087-registration --base origin/main are OK. The decider pin file runs 21/21 at e5150923a. The full package suites were last run at c17394ee5, as listed above.

Spec-lane files

Declared to the spec seat (contracts/** included, per ruling 6267719791 item 3):

  • packages/spec/src/automation/flow.zod.ts: the runAs describe and its comment (ruling A item 2's sentence, and item 2's new reading for 'system').
  • packages/spec/src/contracts/automation-service.ts: ResumeSignal.resumedBy (new optional member, both postures in its TSDoc); the TSDoc of AutomationContext.userId; and the parity sentence under AutomationContext.flowName, which now says the acting user moves to the decider on both postures.
  • Regenerated by pnpm --filter @objectstack/spec check:generated --fix (gen:docs, the one artefact it proved stale): content/docs/references/automation/flow.mdx, content/docs/references/api/automation-api.mdx.

Acceptance notes

  • A strand journalled before this release carries no resumedBy, so its repair replay continues as the trigger user. A run that paused before this release and is decided after it does switch. Carrier: none.
  • A subflow-hosted approval switches the child run only; the parent continues with its own context. The parent declares no decision node, so this follows item 1's last sentence. Carrier: none.
  • No ADR records a ruling on the resumed identity (docs/adr searched: ADR-0019, ADR-0044, ADR-0073, ADR-0118). ADR-0073 D2 (posture is not attribution) is the closest, and the system posture now follows it: elevation is posture, the decider is attribution. Per item 6 no ADR line is owed, and none was written.
  • The hand-written flows guide (content/docs/automation/flows.mdx) now says what the describe says, after contract review 6270059532 found two sentences this PR had made false (rework 6270117310). The runAs row reads: 'system' is elevated, and after an approval decision its attribution user is the decider; 'user' is the trigger user until a human decision and the decider after it. The "Who may resume" paragraph reads: a resumed run continues under the identity stored on the run, the user who started it or, after an approval decision, the decider. The caller gate after it (trigger.userId) is unchanged. content/docs/ui/actions.mdx was found true and is unchanged.
  • A hook that reads ctx.user on a write a runAs: 'system' approval flow makes after the decision now sees the decider. HotCRM's opportunity guards key on ctx.user?.id being present, which stays true; the changeset says so.

Generated by Claude Code

On resume after an approval decision (approve or reject), a runAs:'user'
run's nodes after the decision run with the decider's identity and
permissions, re-resolved through the run setup's grants resolver; the
trigger user is kept on the run's history. plugin-approvals hands the
decider to the engine on the service-stamped resume signal and journals it
for a repair replay. runAs:'system' runs and signals with no decider are
unchanged.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
A later pause persists the switched segment context, so a second resume
that names no decider continues as the decider rather than the trigger
user. Pins the data-op envelope (the decider's resolved grants, none of
the submitter's), the run history's trigger, the real-stack updated_by
attribution, the loud refusal of a node the decider may not perform, and
the unchanged controls (runAs:'system', no service marker, no decider, no
decision node).

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
decide resumes the run as the person who decided, with their own grants;
the SLA sweep's auto-decision names no decider; a stranded decision
journals its decider and the repair replay continues as them.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…as plugin-sharing

The envelope pins now record each post-decision node's data context with
a recording node instead of a data-engine double, and the new
plugin-sharing import resolves to source like the package's other aliases.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/flows.mdx (via AutomationContext (symbol, a top-level interface), FlowSchema (symbol, a top-level const))
  • content/docs/ui/actions.mdx (via AutomationContext (symbol, a top-level interface))

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

  • content/docs/releases/v15.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class), FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-3.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v17/17-4.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via FlowSchema (symbol, a top-level const))

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-automation/vitest.config.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2667e2402c48e9201550caca2adbcf9188d925b3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from df62bbd6883aedb7a97d1f52e7c8758e90514441 — the merge of head e5150923a095f4f77dcbc60486d1594fd2541a63 into base 2667e2402c48e9201550caca2adbcf9188d925b3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin df62bbd6883aedb7a97d1f52e7c8758e90514441 && git checkout df62bbd6883aedb7a97d1f52e7c8758e90514441
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2667e2402c48e9201550caca2adbcf9188d925b3 e5150923a095f4f77dcbc60486d1594fd2541a63 && git checkout -B drift-repro 2667e2402c48e9201550caca2adbcf9188d925b3 && git merge --no-ff e5150923a095f4f77dcbc60486d1594fd2541a63

node scripts/docs-audit/affected-docs.mjs --json 2667e2402c48e9201550caca2adbcf9188d925b3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2667e2402c48e9201550caca2adbcf9188d925b3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ider

Ruling 6267719791 Q1 A: after an approval decision a runAs:'system' run
keeps its elevation and its nodes carry the decider as their attribution
user ({ isSystem: true, userId: decider }), with no grant resolved and the
trigger user's authorization facts removed. The runAs describe, the
ResumeSignal / AutomationContext TSDoc and the approvals hand-off docs say
so. plugin-audit joins service-automation's test devDependencies.

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

The runAs:'system' control becomes attribution pins: after approve and
reject the post-decision writes keep their elevation (a readonly verdict
column lands) while updated_by, the sys_audit_log row and the data
context name the decider, with no grant resolved. A system run resumed
with no decider keeps the submitter.

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 19, 2026
… ruling

Clause-② re-graded yes (narrowing) for the three optional resumedBy
members; the reference docs are regenerated by check:generated --fix.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c17394ee557a1bf653988500254d3ba6a61e544b
Local-runs: none
Scope: packages/spec/src/automation/flow.zod.ts, packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/runtime-identity.ts, packages/services/service-automation/src/resume-decider-identity.integration.test.ts, packages/services/service-automation/package.json, packages/services/service-automation/vitest.config.ts, packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts, pnpm-lock.yaml, .changeset/24321-resume-as-decider.md, content/docs/references/automation/flow.mdx, content/docs/references/api/automation-api.mdx, packages/runtime/src/domains/automation.ts, packages/rest/src/approvals-routes.ts, packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, packages/plugins/plugin-approvals/src/index.ts, packages/services/service-automation/src/index.ts, content/docs/automation/flows.mdx, content/docs/ui/actions.mdx

Read at 2026-10-19T14:47Z on PR #24527 (draft, head repo = base repo), card #24321 (body and all twelve comments: ruling A 6257886738, the claim 6264284385, the round-0 report 6267118015, the return question 6267218798, the director delta 6267456613, the ruling 6267719791 Q1 A · Q2 A, the patch-round report 6269555250, the elevation-pull note 6269649076), the PR body and its one comment (the docs-drift advisory), the 13-file list, the net diff against merge base 04669a467 (+994 −42), and the 49 check-runs on the head — all completed, none in progress. The five required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Build Core, Governed Surface Queue Guard. Also success: Check Changeset (twice, the second after the body edit), Validate Package Dependencies, Spec property liveness, Build Docs, and the four card/branch claim checks. The skipped rows are matrix placeholders and full-battery-only jobs (Console Pin Gate, Temporal Conformance, dogfood shards, the packed-tarball opt-in), skipped on PR runs by design. Nothing was built, run or re-run locally; the two repo scripts used are the record template and the posting tool.

① Derived judgments

Each judged against the code at the head, not against the reports.

  1. runAs: 'user' continues as the decider, identity and grants together — right. AutomationEngine.resolveDecisionSegmentContext (engine.ts) takes a COPY of the persisted run.context, sets userId to the decider, records triggerUserId, deletes positions, permissions and org_user_ids, and calls seedRunGrants(run.flowName, switched, 'deciding') — the same method resolveRunContext calls with 'triggering' at run setup, so the decider's grants come from the one wired userGrantsResolver (positions, permission sets, peers; tenantId back-filled only when absent). seedRunGrants resolves nothing for runAs: 'system', a user-less context, a context already carrying every fact, or an engine with no resolver; a resolver throw is caught and warned, and the segment then runs on the baseline member set — narrower, never the submitter's grants (pinned). resolveRunDataContext's user arm hands ObjectQL exactly userId, positions, permissions, org_user_ids, tenantId, flowRunId, so nothing of the trigger user's reaches a post-decision write. A decider who already is the acting user is a no-op. One corner, named and right under ruling A item 1: a runAs: 'user' run whose trigger resolved no user (its pre-decision data ops are refused) runs its post-decision nodes as the decider — the decider is the human whose act runs that segment.
  2. runAs: 'system' keeps elevation and carries { isSystem: true, userId: DECIDER }, no grant resolved — right (ruling 6267719791 Q1 A). The same deletions and userId switch, then if (switched.runAs === 'system') return switched; BEFORE seedRunGrants. resolveRunDataContext's system arm emits isSystem: true, actor: svc:flow:NAME, the context's userId and tenantId, positions: [], permissions: [] — so elevation is untouched, the readonly column still lands, and updated_by, sys_audit_log.user_id and hook ctx.user name the decider (pinned on the real stack with AuditPlugin). The ObjectQL envelope of a system run never carried positions, so deleting the forwarded ones changes only the CEL current_user (runUserOf reads userId, positions, tenantId): after the decision a system run's current_user.positions is [] where a door-forwarded trigger's positions used to ride. That is inside Q1 A's letter (no permission resolution) and the honest shape — the alternative is the decider's id over the submitter's positions. Named for the seat; not a defect.
  3. Only approve and reject carry a decider — right (Q2 A). decide() is the single producer: resumedBy: actingUserId(context), the authenticated caller's userId or null. Recall, the revision-limit auto-reject, send-back and resubmit call serviceResume / resumeRecordedOutcome with no resumedBy. The SLA sweep calls decide(..., SYSTEM_CTX), so actingUserId is null and the run keeps its identity (pinned). The action link passes { ...SYSTEM_CTX, userId: person }, so its decider is the token's person. The verdict mirror is written with mirrorStatusField(..., actingUserId(context)) (approval-service.ts:4002) — the same expression — so "the person the verdict mirror already writes as" holds by construction on every door.
  4. A flow with no decision node, and an SLA decision, are unchanged — right. resolveDecisionSegmentContext returns run.context unchanged when the signal names no decider or lacks the marker; every engine-built continuation (engineBuilt, buildSubflowResumeSignal) carries neither. Subflow-hosted approval: the parent frame forwards the caller's signal to the child (which holds the approval node and switches), then reassigns signal = this.buildSubflowResumeSignal(childRun.context, childRes.output) before reaching its own switch, so the parent keeps its context — the PR's acceptance note is accurate. The degraded "child run is gone — continuing without child output" branch keeps the caller's signal, so there the parent would switch to the decider; a degraded path, consistent with ruling A's reading, noted.
  5. Authority of resumedBy — right; no caller outside the owning service can make the engine honour a decider. RESUME_AUTHORITY_SERVICE is Symbol.for('objectstack.automation.resume.service') (spec contracts/automation-service.ts:585). Setters at the head, outside tests: exactly one, ApprovalService.serviceResume (approval-service.ts:4045), which every approvals-side resume goes through. Readers: refuseGatedResume (engine.ts:7375, the automation: the generic run-resume route needs an authorization gate keyed on the suspended node #3801 gate) and resolveDecisionSegmentContext (engine.ts:6595, signal[RESUME_AUTHORITY_SERVICE] === true ? signal.resumedBy : undefined). The REST resume door (runtime/src/domains/automation.ts, RESUME_BODY_KEYS = ['inputs', 'variables', 'output', 'branchLabel']) answers 400 on a resumedBy key; a JSON body or header cannot carry a symbol-keyed property in any case; and an approval pause is refused at that door before anything (resumeAuthority: 'service'). The REST approve/reject door hands decide the authenticated resolveExecCtx context, with the body's actorId a hint the service pins to the caller. A script holding the import (or Symbol.for with the string) can mint the marker — that is in-process code, the trust level that can already write with isSystem: true; the TSDoc and the published CHANGELOG state that scope. The journalled decider rides sys_approval_request.node_config_json, a readonly: true column objectql strips from every non-system update, and continueRestoredRun is an in-process operator verb with no REST or MCP route. Controls pinned: a decider without the marker, and a marker without a decider, change nothing.
  6. The persisted run and its replays — right; the history still names the trigger user. A later pause persists the switched context (persistSuspendedRun({ ...run, context, ... }) at the re-suspend site), so a second decision, a cold resume or a no-decider continuation never falls back to the submitter; a second decider replaces the first and triggerUserId is kept (runTriggerUserId(context) ?? null, read back through hasOwnProperty, and null survives the JSON round trip). The engine's strand journal snapshots the pre-switch run.context (never mutated) and the approvals journal carries resumedBy, so continueRestoredRun re-applies the switch (pinned). buildRunTrigger reads runTriggerUserId, recordLog stamps the row with trigger.userId, and the REST starter gate isRunStarterOrRunStateReader compares run.trigger.userId — the trigger user keeps run visibility, the decider gains none. A strand journalled before this release carries no resumedBy and replays as the trigger user: a one-release transitional corner, stated in the PR; acceptable.
  7. The two new devDependencies — right; test-only. @objectstack/plugin-sharing and @objectstack/plugin-audit sit under devDependencies in service-automation/package.json (its dependencies are unchanged: core, formula, metadata-core, metadata-protocol, platform-objects, spec, types); the lockfile gains the two link: entries and nothing else; the anchored-regex vitest aliases read them from source; the only importer is the new integration test. No runtime edge. Validate Package Dependencies is success.
  8. The published-surface names are right. ApprovalResumeSurface and StrandedContinuationSignal are exported types of @objectstack/plugin-approvals (its index.ts), and ResumeSignal of @objectstack/spec/contracts, so the three optional members the Clause-② line names are published widenings. resolveRunDataContext is a public export of @objectstack/service-automation (its index.ts), so the cross-package test import is sound.

② Semver level

Right. The changeset bumps exactly the three packages whose published source moves — @objectstack/spec, @objectstack/service-automation, @objectstack/plugin-approvals, all published at 18.0.0-next.5 — each minor; the title carries the bang (feat(automation)!:); the body carries the BREAKING banner; one ADR-0087 marker, not-required (no-migration-prescription), whose argument is correct (no key, export, option, enum value or stored shape is removed, renamed or re-shaped; triggerUserId and the journal's resumedBy are additive); and Clause-②: yes (narrowing) in both the changeset and the PR body — yes for the three optional members on published interfaces, (narrowing) for the post-decision refusal under the decider's rights. AGENTS.md's rule holds: yes takes at least minor, (narrowing) is BREAKING, and the migration text ("What changes for you") is present. The seven regenerated runAs rows in the two reference pages match the describe byte-for-byte; the api-surface snapshot is unaffected (members, not exports); Check Changeset and TypeScript Type Check (which carries every spec gate) are success. The PR is not a fork, is a draft, touches no governed path, and changes 1,036 lines.

③ Boundary flags

Dev flags (deviations), round-0 report 6267118015 and patch round 6269555250:

  • "Part of" vs "Fixes" — resolved by ruling 6267719791 item 3; the body's first line is Fixes #24321 and the two card/branch claim checks re-ran success after the edit.
  • Clause-② kept no (narrowing) for re-grade — re-graded to yes (narrowing) as the ruling directs; verified in both carriers (② above).
  • contracts/automation-service.ts beyond the describe — declared to the spec seat in the PR body's "Spec-lane files"; it is a reviewFace hit here and is read in ① (items 5 and 8).
  • plugin-sharing and plugin-audit devDependencies and vitest aliases — judged test-only (① item 7); check:test-source-alias and check:cross-package-test-inputs ride in Lint & Repo Gates, success.
  • Pins placed in the existing restored-approval-continuation.test.ts — fine; check:engine-double-contract is in the green union.
  • runtime-identity.ts touched — needed (SeededRunContext.triggerUserId, runTriggerUserId); TSDoc corrected where the change made it false — right, and the system arm's text now matches the code.
  • The system switch also removes forwarded positions, permission sets and peers — right, with the current_user.positions corner named in ① item 2.
  • Two os-regen-merge.sh merges, the throwaway real-door measurement file in packages/verify (absent from the file list), the model-free commit trailers, the changeset/PR-title alignment — all in order; this record judges the net diff only.
  • open_questions: round 0's two are ruled (6267719791, Q1 A · Q2 A) and both are in the code (① items 2 and 3); the patch round lists none.
  • out_of_scope_findings (carrier: none): the pre-release strand and the subflow parent — both read in ① (items 6 and 4) and found as stated.

The claim's exclusions hold on the diff: approval-service.ts's hunks touch the ApprovalResumeSurface and StrandedContinuationSignal members and TSDoc, the serviceResume and resumeRecordedOutcome signatures, decide's signal, the journal spread and the continueRestoredRun spread — no row builder and no hand-built { isSystem: true, userId } write context (#24501, #24519); packages/objectql/**, packages/core/** and docs/adr/** are absent from the 13-file list.

Found by this review — the one defect, and the reason for the verdict. content/docs/automation/flows.mdx, a hand-written page the docs-drift advisory on this PR named, still restates the rule this PR changes, and the PR does not touch it. The runAs row of its flow-key table (line 76) says 'user' is "the triggering user, RLS-respecting" — a paraphrase of the OLD describe, now contradicted by the regenerated reference row. The "Who may resume" section (lines 1090–1091) says a resumed run's "data nodes run as the user who started it, whoever sent the values" — after this PR, a screen or wait pause reached after an approval decision continues as the DECIDER (the persisted context is the segment's), so that sentence is false in the multi-stage case; the caller gate it goes on to describe (trigger.userId) is still right. Under a BREAKING banner on exactly this key, the guide is an authoring trap on the narrowing's edge (Prime Directive #10, corollary): an author designing post-decision nodes from this page expects the submitter's rights. The fix is two sentences in a hand-written tree, editable in a code PR: make row 76 mirror the new describe, and make line 1090 read "the identity stored on the run — the user who started it, or, after an approval decision, the decider". content/docs/ui/actions.mdx, the other flagged page, speaks only of identity forwarding at the trigger door and stays true. Also noted, non-blocking: the header comment of resume-decider-identity.integration.test.ts names plugin-approvals' decision-resume-decider.test.ts, a file that does not exist at the head — the hand-off pins live in restored-approval-continuation.test.ts.

Every contract-level judgment above is right. The next head needs only the docs edit; ① and ② carry over to it unchanged unless the engine, approvals or spec files move.

Implemented-by: claude/issue-24321-resume-as-decider
Reviewed-by: session_01Nk17Z26X5DB59iQztDBADH

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

REWORK · patch round 2 · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T14:50Z · head c17394ee55

This answers contract review FAIL 6270059532. The seat checked its one finding at this head: it holds. Every contract-level judgment in that record (① and ②) is right, and this round changes none of them.

  1. content/docs/automation/flows.mdx, a hand-written page this PR makes false. The seat extends the claim's surface to these two sentences only, under the plugin-auth: seven refusals and one outage thrown from better-auth hooks carry no ADR-0112 code, the same gap #23945 closes for the invitation role cap #24243 / fix(service-storage)!: the family gate and the route judge the same credential #24388 precedent (a docs sentence the change makes false is corrected in the same PR):
    • :76, the runAs row of the flow-key table: "'user' (the triggering user, RLS-respecting; the default)". Make it mirror the new describe: the trigger user until a human decision, the decider after it. 'system' stays elevated, and after a decision its attribution user is the decider.
    • :1090–:1091, "Who may resume": "its data nodes run as the user who started it". Make it read: the identity stored on the run, which is the user who started it, or, after an approval decision, the decider. The caller gate described after it (trigger.userId) stays as written; the review found it true.
    • content/docs/ui/actions.mdx stays as it is; the review found it true.
  2. Non-blocking, in a file you already touch: the header of packages/services/service-automation/src/resume-decider-identity.integration.test.ts (:44) names plugin-approvals' decision-resume-decider.test.ts, which does not exist. Name restored-approval-continuation.test.ts, where the hand-off pins live.

Run the derived list again on the new head (--ran: 0 NOT-MEASURED, 0 UNRUN). After the push, the seat re-runs the CONTRACT_REVIEW_TIER review on that head.


Generated by Claude Code

The runAs row and the resume section of the hand-written flows guide now
say what the describe says: a 'user' run is the trigger user until a
human decision and the decider after it, a 'system' run's attribution
user is the decider after a decision, and a resumed run continues under
the identity stored on the run. The decider pin file's header names the
approvals test that holds the hand-off pins.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e5150923a095f4f77dcbc60486d1594fd2541a63
Local-runs: none
Scope: packages/spec/src/automation/flow.zod.ts, packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/runtime-identity.ts, packages/services/service-automation/src/resume-decider-identity.integration.test.ts, packages/services/service-automation/package.json, packages/services/service-automation/vitest.config.ts, packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts, pnpm-lock.yaml, content/docs/automation/flows.mdx, .changeset/24321-resume-as-decider.md, content/docs/references/automation/flow.mdx, content/docs/references/api/automation-api.mdx, packages/runtime/src/domains/automation.ts, packages/rest/src/approvals-routes.ts, packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, packages/plugins/plugin-approvals/src/index.ts, packages/services/service-automation/src/index.ts, content/docs/ui/actions.mdx

Read at 2026-10-19T16:15Z, the re-review after FAIL 6270059532 on c17394ee5. Inputs: PR #24527 (draft, head repo = base repo, title unchanged), its rewritten body, its three comments (the docs-drift advisory, the FAIL record, the seat's rework 6270117310), its 14-file list, card #24321 (body and all thirteen comments; new since the last record: the dev's round-2 report 6271153480), the net diff against the NEW merge base 2667e2402 (+997 −45, 14 files), and the check-runs on this head — 49 in all, read after waiting for the five that the body edit re-queued (Check Changeset and the four card/branch claim checks) to complete: every run completed, 42 success, 7 skipped, none failed, none in progress. The five required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Build Core, Governed Surface Queue Guard; so are Check Changeset (both runs), Validate Package Dependencies, Spec property liveness, Build Docs and the four claim checks (both runs each). The skipped rows are the same matrix placeholders and full-battery-only jobs as before. Nothing was built, run or re-run locally; the two repo scripts used are the record template and the posting tool.

What moved since c17394ee5, verified on the objects, not the reports. Two commits: be9efd1c5 (two files, +4 −4) and the merge e5150923a of origin/main 2667e2402 (parents be9efd1c5, 2667e2402). Blob-for-blob, twelve of the fourteen PR paths are identical to c17394ee5: the changeset, both regenerated reference pages, approval-service.ts, restored-approval-continuation.test.ts, package.json, engine.ts, runtime-identity.ts, vitest.config.ts, flow.zod.ts, automation-service.ts, pnpm-lock.yaml. The two that moved: content/docs/automation/flows.mdx (the runAs row at line 76 and the "Who is resuming" sentence at lines 1090–1091) and the header comment at line 44 of resume-decider-identity.integration.test.ts. A textual diff of the old and new net diffs shows exactly those two hunks and that one line, nothing else. Main's six incoming commits (#24521, #24520, #24552, #24555, #24526, #24532) touch no PR path and none of the files this record cites; the merge commit changes no PR path relative to its first parent. So the engine, approvals, spec and changeset files are unchanged, and ① and ② of record 6270059532 carry over to this head in full; they are restated below in brief, with the two rewritten sentences judged against the code.

① Derived judgments

Carried over unchanged (the code is blob-identical), each right:

  1. runAs: 'user' continues as the decider with identity and grants together — resolveDecisionSegmentContext copies the persisted context, switches userId, records triggerUserId, drops the trigger user's positions, permissions, org_user_ids, and resolves the decider's through seedRunGrants(..., 'deciding'), the trigger setup's own resolver; a resolver failure degrades to the baseline set, never the submitter's grants.
  2. runAs: 'system' keeps elevation and carries { isSystem: true, userId: DECIDER } with no grant resolved — the early return switched before seedRunGrants; resolveRunDataContext's system arm hands ObjectQL isSystem: true, the decider's userId, the tenant, empty positions and permissions. The one corner stands as named: a system run's post-decision CEL current_user.positions is [].
  3. Only approve and reject carry a decider — decide() is the sole producer (resumedBy: actingUserId(context), the same expression the verdict mirror is written with at approval-service.ts:4002); recall, auto-reject, send-back and resubmit hand none; the SLA sweep's SYSTEM_CTX yields none; the action link's token person is its decider.
  4. No decision node, SLA decisions, engine-built continuations and the subflow parent are unchanged — the parent frame reassigns signal = buildSubflowResumeSignal(...) before its own switch.
  5. Authority — RESUME_AUTHORITY_SERVICE is a Symbol.for marker set in exactly one non-test place (ApprovalService.serviceResume, approval-service.ts:4045) and read in two (refuseGatedResume, resolveDecisionSegmentContext); the REST resume door's closed key set (inputs, variables, output, branchLabel) answers 400 on resumedBy, a JSON body cannot carry a symbol key, an approval pause is refused at that door regardless, the REST approve door hands decide the authenticated context, the journalled decider rides a readonly: true column, and continueRestoredRun has no REST or MCP route. In-process code can mint the marker — the trust level that can already write isSystem: true.
  6. The persisted run and its replays — the re-suspend site persists the switched context, a second decision keeps triggerUserId, the strand journal plus the approvals journal's resumedBy re-apply the switch, and buildRunTrigger, recordLog and the REST starter gate all read the trigger user, so the history and run visibility stay with who started the run.
  7. The two devDependencies (@objectstack/plugin-sharing, @objectstack/plugin-audit) are test-only: devDependencies only, two link: lockfile lines, source aliases, one importer (the new integration test); dependencies unchanged.
  8. The three widened interfaces are published (ResumeSignal in @objectstack/spec/contracts; ApprovalResumeSurface and StrandedContinuationSignal exported from plugin-approvals' index), and resolveRunDataContext is a public export of service-automation.

Judged new on this head — the two rewritten sentences, against the code: right.

  • flows.mdx:76 now reads: 'system' is elevated, bypasses RLS, and after an approval decision its attribution user is the decider; 'user' is the default, RLS-respecting, the trigger user until a human decision, the decider after it, with the decider's permissions. That is resolveDecisionSegmentContext plus both arms of resolveRunDataContext, and it matches the regenerated reference row and the describe in flow.zod.ts; the row's user-less sentence is unchanged and still true.
  • flows.mdx:1090–1091 now reads: a resumed run continues under the identity stored on the run — the user who started it or, after an approval decision, the decider — whoever sent the values. That is the persisted context the re-suspend site writes and the generic door's resolveDecisionSegmentContext returns unchanged (no marker, no decider). The caller gate that follows (trigger.userId, read by isRunStarterOrRunStateReader off buildRunTrigger) is unchanged and still names the trigger user. content/docs/ui/actions.mdx is untouched and still true.
  • The test header at line 44 now names restored-approval-continuation.test.ts, where the hand-off pins live (verified: the only resumedBy pins in plugin-approvals are in that file).

② Semver level

Carried over, right: the changeset is blob-identical — minor on exactly the three published packages whose source moves (@objectstack/spec, @objectstack/service-automation, @objectstack/plugin-approvals, all at 18.0.0-next.5), the bang in the title, the BREAKING banner, one ADR-0087 marker not-required (no-migration-prescription) with a correct argument, Clause-②: yes (narrowing) in both the changeset and the PR body, with the migration text present. One fact main brought in is read here so it is not mistaken for a change of level: #24555 flips .changeset/pre.json from pre to exit, so the next version pass cuts the 18.0.0 GA instead of another next tag. That is release-lane state, outside this PR; it does not alter what this diff publishes or the level it declares, and Check Changeset is success on this head. The regenerated reference rows still match the describe byte-for-byte; the api-surface snapshot is unaffected; the PR is not a fork, is a draft, touches no governed path, and changes 1,042 lines.

③ Boundary flags

Implemented-by: claude/issue-24321-resume-as-decider
Reviewed-by: session_01Nk17Z26X5DB59iQztDBADH

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T16:23Z · head e5150923a0.

The contract review of record is PASS 6271787844 at CONTRACT_REVIEW_TIER on this head, after FAIL 6270059532 on c17394ee55. It covers the whole diff on both faces, the schema hits and the narrowing, and this seat review records the rest. The PR lands through the merge queue.

  • Shape:
    • The first line is Fixes #24321, and that is the only closing keyword in the body.
    • Clause-②: yes (narrowing) is at a line start in the body and in .changeset/24321-resume-as-decider.md.
      • The yes is for three optional members on published interfaces: ResumeSignal.resumedBy in @objectstack/spec/contracts, and the resumedBy on ApprovalResumeSurface's signal and on StrandedContinuationSignal in @objectstack/plugin-approvals.
      • The (narrowing) is for a post-decision node the decider may not perform, which is now refused.
    • The changeset is minor on spec, service-automation and plugin-approvals, feat(automation)!, with the BREAKING banner and adr-0087: not-required (no-migration-prescription). The gate accepts it.
    • Card and PR assignee: marchtian.
    • Not governed: check-governed-merges reads none of the 14 paths, and 1042 changed lines is ≤ 3000.
  • The rulings, met, as the review's ① reads them against the code:
    • Ruling A (6257886738): after approve or reject, a runAs:'user' run continues as the decider. Its identity and grants are re-resolved through the trigger setup's own resolver (seedRunGrants). A flow with no decision node, and an SLA decision, are unchanged. updated_by and sys_audit_log name the decider.
    • Ruling 6267719791 Q1 A: a runAs:'system' run keeps its elevation, and after a decision it carries { isSystem: true, userId: <decider> } with no grant resolved.
    • Ruling 6267719791 Q2 A: send-back, resubmit and withdraw carry no decider.
    • Authority: the engine honours resumedBy only beside the in-process Symbol.for marker, and ApprovalService.serviceResume is its one non-test setter. The REST resume door answers 400 on a resumedBy key.
    • Measurement, ruling A item 5: on hotcrm's shape, through the real doors, updated_by and the audit row name the manager after approve and after reject, under both postures. Under 'system' the readonly verdict columns still land. A 'user' node the manager may not perform fails as a repairable strand naming the node and the field.
  • Rework:
    • Return question 6267218798, ruled 6267719791.
    • Patch round 1: Q1 A in code, pins, docs and the changeset, and the Clause-② re-grade.
    • Patch round 2 (6270117310): two sentences in content/docs/automation/flows.mdx that this PR had made false, which was the FAIL's one finding, and one stale test-header reference.
  • Gates on this head:
    • 35 checks succeeded and 7 were skipped by design: the roster set, plus Auto Label and Check PR Size on the body-edit run. No check failed, and the five required contexts are green.
    • Locally the dev derived 127 families on this head, ran all 127 and all exited 0 (--ran: 0 NOT-MEASURED, 0 UNRUN).
  • Spec-lane declaration: 6269616435 on [PM seat] domain:spec · seat 2 — 🟢 zhuangjianguo · session_019oFY6Hi35J7atvgVFnmoaK #18549, before the queue. It covers the contracts/** widening, the runAs describe and the regenerated references.
  • Accepted deviations:
  • Out of scope, one line each:

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 16:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 1b40780 Oct 19, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24321-resume-as-decider branch October 19, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants