Skip to content

feat(plugin-approvals): excludeSubmitter keeps the submitter from approving their own request - #25722

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-4z84a6
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-4z84a6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread
Fixes #25573

Clause-②: no
Metadata-protocol: none

What this does

An approval node's excludeSubmitter: true now keeps the request's submitter from approving their own request. The key was declared by #25609 (PR #25632), and its pairing with onEmptyApprovers: 'auto_approve' is refused at the parse (#25635 ruled A, PR #25700). Until this PR the approval engine parsed the key and read nothing.

Rulings carried, not re-decided:

Where the exclusion lives (measured)

  • One seam covers every approver type. user, field, manager, position, team, department, org_membership_level and expression all resolve through ApprovalService.expandApprovers, called from openNodeRequest. The exclusion runs on its RESOLVED output, so it needs no per-type code and also catches an out-of-office hop that lands on the submitter. The removed slots are the addresses the submitter acts under without naming one (actingAddresses from approver-address.ts: the user id and the account's email). A position:P literal is never removed, because other holders take it.
  • Four writers put people on a slate, so four places read the option. The open (openNodeRequest), the reassign door (reassign), the SLA sweep (escalateRequest, action reassign), and the decision (decideNode), which is the one check every approval passes through. The mid-tally slate update in decideNode only shrinks the open-time slate, so it needs nothing.

Choices made inside the ruling's words

The order left two questions to this card. Both are answered by "never approved by the submitter" and "never a silent pass":

  1. The fallbackApprovers slate and the admin_rescue administrators are screened too. Measured before this change: both slates go through the same expandApprovers with no submitter screen, so a submitter on the fallback list, or a submitter who is an organization administrator, would get their own request back one route later. Screening them is the only reading under which "never approved by the submitter" holds. A fallback slate that names only the submitter degrades to admin_rescue, exactly as a fallback that resolves to no one already does. An organization whose only administrator is the submitter opens on the slate that names no one (the existing break-glass outcome), never on the submitter.
  2. Delegation, reassign and escalation hold the exclusion too. Out-of-office delegation is applied inside expandApprovers, so the open-time screen covers it. After the open: reassign refuses a target that is the submitter (id or email) or a position only the submitter holds, with VALIDATION_FAILED, asked after the authorization branches like the plugin-approvals: reassign accepts a to that resolves to no principal and strands the request on an unresolvable slate #25485 target check. An SLA reassign escalation leaves the submitter off the new slate; when the submitter is its only target, it is not applied and the pending approvers are told the SLA lapsed, the shape plugin-approvals: an SLA escalation with action: reassign writes a literal escalateTo that resolves to no one onto the slate, stranding the request the way #25485's door did #25598 gives an unresolvable target. decideNode refuses an approval decided by the submitter with FORBIDDEN, the privileged override included. A rejection by the submitter stands, because it approves nothing.

The route note

When the exclusion took the submitter off a slate, the request writes one system note on its timeline (action: 'comment', no actor), stored as data in the node-config snapshot under __submitterExclusionNote with the route the request took: remaining_approvers, fallback, administrators or no_one. It reuses the #25576 shape: the row's comment is the English, and the approvals door renders the reader's language from new approvalSubmitterExcluded* catalog sentences in en, zh-CN, ja-JP and es-ES. When the exclusion emptied the slate and the request went to the administrators, the administrators' routing note now says "the only approver it resolved to was the request's submitter, whom excludeSubmitter removes" instead of "no approver on the node resolved to a person", which would have been false. A node with the option on whose slate never named the submitter writes no note.

Declared surface widening: packages/spec (seat-approved)

The claim's file surface was packages/plugins/plugin-approvals/src/**. #25609 handed this card an edit in packages/spec: the excludeSubmitter describe opened with [PLANNED — not yet enforced] and ended "The approval engine does not read this key yet, so today the submitter is NOT removed", and both the TSDoc and liveness/flow.json's _note said "When #25573 lands, that marker and that last sentence leave and this sentence is rewritten." Leaving them would make shipped text false. The server seat approved the edit within this fence: no schema shape, default or refusal change. Spec files touched:

  • packages/spec/src/automation/approval.zod.ts: marker and last sentence dropped from the describe; the TSDoc's PLANNED paragraph rewritten to name the consumer.
  • packages/spec/liveness/flow.json: the _note PLANNED RECORD sentence rewritten as a LIVE RECORD naming the consumer seams.
  • packages/spec/src/automation/approval-exclude-submitter-25609.test.ts: the §5 status pin flipped from "opens with the planned marker" to "carries no planned marker".
  • content/docs/references/automation/approval.mdx: regenerated by check:generated --fix (the one artifact it proved stale).

Tests

  • New packages/plugins/plugin-approvals/src/exclude-submitter.integration.test.ts: 14 cases on a real ObjectQL over SqlDriver (better-sqlite3), with real administrator resolution. Covers a field step and a position step resolving to the submitter (administrators, fallback, fail), a shared slate minus the submitter (per_group tags), the email slot and an out-of-office hop onto the submitter, a fallback naming only the submitter, a submitter who is the organization's only administrator (and whose override is refused), the zh-CN render, the decision guard, the reassign door, the SLA escalation, and three controls with the option off or never triggered.
  • New src/submitter-exclusion-note.test.ts: 3 cases (every route in every shipped locale, the strict shape read, the administrators' note clause).
  • src/approver-address-readers.test.ts: the PERSON_SITES ledger key follows the rename localizeAdminRescueNote → localizeRoutingNotes (same two a.actor_id presence tests).

Ablations (WRAP mode of scripts/ablation-replace.mjs, each restored and proven blob == HEAD, git diff HEAD empty; tests import src/, so no dist rebuild is involved):

  • A1, open-time filter removed: 8 failed / 6 passed.
  • A2, decision guard disabled: 2 failed / 12 passed. The first attempt was a no-op (the replacement contained the anchor, the tool refused it) and was re-run with a non-overlapping replacement.
  • A3, reassign refusal deleted: 1 failed / 13 passed.
  • A4, escalation filter removed: 1 failed / 13 passed.
  • A5, route note disabled: 6 failed / 8 passed.

Gates (local, on head 0aed0b90)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 119 commands derived; all 119 run, each exit code captured before any pipe, all 0 on the final head. --ran reconciliation: "119 derived, 119 run, 0 NOT-MEASURED, 0 UNRUN".
  • Reds found and fixed on the way: check-tenant-audit-census (and its --self-test, which reads the tree) counted the route note's new sys_approval_action insert, 238 to 239 write call sites. Regenerated with node scripts/tenant-audit-census.mjs --write and the hand-written prose counts on content/docs/permissions/tenant-audit-census.mdx updated to match; both exit 0.
  • Prerequisite refusals cleared, not skipped: check-plugin-teardown-shape --self-test (exit 3 on this shallow clone until its pinned fixture commit was fetched), check:skill-examples (needed @objectstack/client-react built), check:i18n (needed its extract closure built), check:dual-build-cjs-loads (needed built output). Each re-run green.
  • @objectstack/plugin-approvals: pnpm test 98 files / 1276 tests passed; pnpm typecheck exit 0 (includes check:test-typecheck). @objectstack/spec: approval-exclude-submitter-25609.test.ts 28/28; check:generated green after --fix regenerated the one stale artifact.
  • Scoped lint: eslint --no-inline-config over packages/plugins/plugin-approvals/src and the two touched spec files, exit 0.
  • CI owns the full farm (pnpm lint, Test Core, Build Core, the type-check lanes).

Acceptance notes


Generated by Claude Code

…y approval slate (WIP)

Claude-Session: https://claude.ai/code/session_01LXX9EBHog6ZMo6apV2jvXX
Co-authored-by: Claude <noreply@anthropic.com>
…n, the reassign door and the SLA escalation

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-approvals, @objectstack/spec, touching 42 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/flow.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via approvals.listActions (sdk, the route ledger binds it to GET /api/v1/approvals/requests/:id/actions, selected by route anchor /approvals/requests/:id/actions), listActions (sdk, the bare tail of client method approvals.listActions, bound to GET /api/v1/approvals/requests/:id/actions))
  • content/docs/automation/approvals.mdx (via sys_approval_action (literal, a string literal in openNodeRequest))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), sys_approval_action (literal, a string literal in openNodeRequest))
  • content/docs/automation/workflows.mdx (via sys_approval_action (literal, a string literal in openNodeRequest))
  • content/docs/plugins/packages.mdx (via sys_approval_action (literal, a string literal in openNodeRequest))

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

  • content/docs/releases/v16.mdx (via sys_approval_action (literal, a string literal in openNodeRequest))
  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via ApprovalNodeConfigSchema (symbol, a top-level const), sys_approval_action (literal, a string literal in openNodeRequest))
  • content/docs/releases/v18/18-1.mdx (via ApprovalService (symbol, a top-level class))

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/flow.json) — pages documenting those are invisible to this run
  • 4 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 — 144 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 f6ba1343ba41cb173baadfc36e6069123d3f2cfd → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (lane part, TIER_DEFAULT) · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-25T00:11Z

Head 0aed0b90bb, report 6391228711 on #25573, claim 6390673655.

item reading
Shape draft, base main, Fixes #25573 (only #25573 next to a closing keyword), Clause-②: no and Metadata-protocol: none line-initial in the body and the changeset. Not governed (check-governed-merges --pr 25722), 916 changed lines.
Runtime, seat's read of approval-service.ts every new branch is gated on excludeSubmitter === true through excludedSubmitterSlots (returns [] when the option is off or there is no submitter), so off-path behaviour is untouched. The exclusion runs on the resolved slate (one seam for every approver type), screens the fallback and admin_rescue slates, routes an emptied slate through onEmptyApprovers, and records the route as a timeline note. After the open: decideNode refuses the submitter's approve (FORBIDDEN, override included; reject stays), reassign refuses the submitter or a position only they hold (VALIDATION_FAILED), and the SLA reassign leaves them off. These are the ruling's "never approved by the submitter" and "never a silent pass" held at every slate writer; the dev's choice on the fallback/admin slates is stated in the PR body as the order asked.
Surface outside the claim (1) the fenced spec hand-off (describe, TSDoc, liveness/flow.json _note, the §5 pin, regenerated approval.mdx), as the seat allowed on the thread and noted on #6017 (6390733466); (2) the tenant-audit census regen (238 → 239) and its prose counts in content/docs/permissions/tenant-audit-census.mdx and docs/audits/…counts.md, forced by one new engine insert call site. Both declared.
Changeset sentence check Before / at the open / on the record / after the open / spec / unchanged: each sentence matches a hunk read above; @objectstack/plugin-approvals and @objectstack/spec both patch.
Tests new exclude-submitter.integration.test.ts and submitter-exclusion-note.test.ts; five ablations reported red and restored; package 1276/1276, integration 14/14.
Gates dev union 119/119 exit 0 at this head. CI at this act: 18 success, 6 opt-in skipped, 15 in progress, 0 failed.
Contract face the diff touches packages/spec/src/automation/approval.zod.ts (a published schema file): a CONTRACT_REVIEW_TIER record is owed on this head before ready; an isolated reviewer is on it. needs:contract-review is set on this PR until it lands.
Landing ready + auto-merge through the relay once the contract review is PASS on this head and every check is green or an expected skip.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0aed0b90bb7cc68dac571fc160d5331aa6351c40
Local-runs: none
Scope: packages/spec/src/automation/approval.zod.ts + packages/spec/liveness/flow.json, packages/spec/src/automation/approval-exclude-submitter-25609.test.ts, content/docs/references/automation/approval.mdx, packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/submitter-exclusion-note.ts, packages/plugins/plugin-approvals/src/admin-rescue-note.ts, packages/plugins/plugin-approvals/src/approval-node.ts, packages/plugins/plugin-approvals/src/approver-address.ts, packages/plugins/plugin-approvals/src/exclude-submitter.integration.test.ts, .changeset/25573-exclude-submitter-runtime.md · remainder, the lane seat's ACCEPT at TIER_DEFAULT as a standalone PR of those files: packages/plugins/plugin-approvals/src/translations/messages.ts, packages/plugins/plugin-approvals/src/submitter-exclusion-note.test.ts, packages/plugins/plugin-approvals/src/approver-address-readers.test.ts, content/docs/permissions/tenant-audit-census.mdx, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md

Inputs: card #25573 (body, all six comments, the dev report 6391228711); the rulings it cites, read where it points — 6377731715 and 6377842908 on #25496, 6377859800 and 6377888641 on #25446, 6386627950 on #25635, and #25609's report 6383750916 with its landing 6384560185; PR #25722's body, its 14-file list and the net diff origin/main...0aed0b90 (merge-base 47af3dde); the check-runs on the head. Nothing built, run or re-run locally; the --template --paths print of record-recognisers.mjs is the only script this review executed.

① Derived judgments

  1. Face and tier — right. reviewFace on the file list: schema hit packages/spec/src/automation/approval.zod.ts, no rules hit. No governed path, so Tier S/H landing rules do not apply; this record is owed by the schema face alone.

  2. The spec change is text-only; the accept set is unchanged — right. excludeSubmitter stays z.boolean().optional() with no default inside the same strict object; checkExcludeSubmitterPolicyPairing is untouched. The describe drops its [PLANNED — not yet enforced] opener and its closing "does not read this key yet … keep any guard" sentence; every other sentence is byte-identical, and content/docs/references/automation/approval.mdx is that row regenerated. The §5 pin flips with it (^Segregation of duties, no PLANNED), and the liveness _note turns its PLANNED RECORD into a LIVE RECORD naming the consumer seams. This is the hand-off spec(automation): an approval step's excludeSubmitter option, default off — the protocol half of #25573 (ruling 6377731715, letter B) #25609 wrote into the TSDoc and the _note themselves ("When plugin-approvals: a field or position approver step has no "not the submitter" guard, so a manager who submits their own request can approve it #25573 lands, that marker and that last sentence leave and this sentence is rewritten"), so the edit past the dispatch's packages/plugins/plugin-approvals/src/** fence is owed by the spec's own text; leaving it would have shipped a false sentence (Prime Directive chore: version packages #10, declared ≠ enforced).

  3. Declaration vs runtime with the option on — right.

    • describe, "removed from the slate approvers resolves to, whatever approver type produced them": openNodeRequest filters the RESOLVED output of expandApprovers (withoutSubmitter), one seam for every approver type and for an out-of-office hop that lands on the submitter; the removed slots are actingAddresses({ userId, email }), the user id and the account email; a position:P literal is never removed (other holders take it; the decision guard covers a later staffing).
    • describe, "If that leaves nobody, the request takes the node's onEmptyApprovers route … and never passes silently": the emptied slate enters the existing slateNamesAPerson empty-slate policy unchanged — fail throws NO_APPROVERS naming the exclusion, fallback expands the declared slate, the default admin_rescue opens on the organization's administrators. auto_approve is not overridden at runtime (ruling A, 6386627950): the only runtime caller at head, approval-node.ts, runs ApprovalNodeConfigSchema.safeParse first and that refuses the pair; exec-context-annotation.pin.ts is a type pin, not a caller.
    • describe, "the approval record shows that the exclusion changed the route": __submitterExclusionNote { action_id, route } on the node-config snapshot plus one sys_approval_action comment row with no actor; route is one of remaining_approvers | fallback | administrators | no_one; rendered in the reader's language by localizeRoutingNotes (the renamed localizeAdminRescueNote, same overlay rule: comment row, no actor, id match; the PERSON_SITES ledger key follows the rename). The administrators' routing note gains submitter_excluded: true, whose clause replaces the now-false "no approver on the node resolved to a person". Written only when a slate lost the submitter; a node with the option on whose slate never named them writes none (pinned).
    • TSDoc, "takes the submitter off every slate the request can open on (approvers, the fallback slate and the admin_rescue administrators)": all three expandApprovers outputs pass through withoutSubmitter. The right reading of the ruling's "never approved by the submitter": a fallback list or an administrators' slate naming the submitter would otherwise hand their own request back one route later. An organization whose only administrator is the submitter opens on the slate that names no one (the existing break-glass outcome), said at warn and on the timeline — never a pass.
    • TSDoc, "the decision, the reassign door and the SLA escalation keep the slot out of the submitter's hands after the open": decideNode refuses approve when config.excludeSubmitter === true and recordedActor(context) equals raw.submitter_id, the privileged override included, before any write; reject stays theirs (it approves nothing). reassign calls assertReassignTargetIsNotSubmitter after the authorization branches and after assertReassignTargetResolves, so its .every() over position holders cannot fire on an unstaffed position; it refuses the submitter's id, their email, or a position only they hold, with VALIDATION_FAILED. escalateRequest filters the reassign escalatees, and a target that was the submitter alone becomes notify with the escalate row saying why (the plugin-approvals: an SLA escalation with action: reassign writes a literal escalateTo that resolves to no one onto the slate, stranding the request the way #25485's door did #25598 shape). The _note names these seams by their real method names (escalateRequest is at approval-service.ts:7166 on the head).
  4. Option off — behaviour unchanged, right. excludedSubmitterSlots returns [] unless config.excludeSubmitter === true and a submitter exists; all four seams gate on that list (or on the same === true test in decideNode), so no filter, no refusal, no note and no extra read applies when the key is absent or false — "Omitted or false (the default), the slate stays as resolved" holds, and the controls pin slate, timeline, approve and reassign. The one restructured branch on the off path: admin-rescue adoption moved from administrators.length to slateNamesAPerson(administratorSlate). Equivalent with the option off, because organizationAdministrators filters blank ids and applyOooDelegation returns exactly one non-empty id per user entry, so the new "every administrator … resolves to the request's submitter" sentence is reachable only through the exclusion. The admin slate's group tags and OOO substitutions are now collected apart and merged only when adopted — the same result as before whenever the slate is adopted.

  5. Contract-first, no consumer leniency — right. The runtime reads the key by strict === true; no alias, no ?? fallback (Prime Directive Add comprehensive test suite for Zod schema validation #12). The request snapshot carries the authored key (configSnapshot = { ...input.config }), so decideNode, reassign and escalateRequest read the same value the open did.

  6. Public surface — right. packages/plugins/plugin-approvals/src/index.ts is unchanged between origin/main and the head; submitter-exclusion-note.ts and the widened AdminRescueNote / adminRescueReason are internal; the spec's exports are unchanged. Clause-②: no with no arm is the correct declaration.

  7. Generated artifacts — right. The describe change moves one artifact, approval.mdx, and it is in the diff. Type Check · source gates (the lane carrying check:authorable-surface and check:docs) and Spec property liveness (reads liveness/flow.json) concluded success on this head.

② Semver level

.changeset/25573-exclude-submitter-runtime.md: @objectstack/plugin-approvals: patch, @objectstack/spec: patch; its body carries Clause-②: no and Metadata-protocol: none at line start, no arm, no BREAKING banner, and they match the PR body's two lines and the dispatch claim's. Right:

  • Both packages the diff moves are graded. packages/spec/src/** moved (describe text publishes), so the spec entry is owed and patch is its level.
  • Metadata-protocol: none: what an author can write is unchanged — no key, accepted value, type, requirement or default moved; the key's contract sentence is byte-identical minus a status marker. The key's own added is spec(automation): an approval step's excludeSubmitter option, default off — the protocol half of #25573 (ruling 6377731715, letter B) #25609's pending changeset, unreleased (the Version Packages PR is open), so the release that carries the key carries its minor, and this runtime rides as patch. Not semantic: the meaning the describe declared is what the runtime now does; the marker named a status, not a meaning.
  • The level follows the kind alone (none → patch); the feat: type does not decide it. No ADR-0087 disposition is owed (nothing narrowed, no banner). Check Changeset concluded success on the head.

③ Boundary flags

Dev report 6391228711 on #25573 (head 0aed0b90, session session_01LXX9EBHog6ZMo6apV2jvXX). PR author objectstack-fleet[bot]; head repo equals base repo (not a fork); draft; 836 additions / 80 deletions, under the 3,000-line ceiling.

Deviations declared (3), each answered:

  1. Spec edit past the claim's file fence — answered in ① item 2: owed by spec(automation): an approval step's excludeSubmitter option, default off — the protocol half of #25573 (ruling 6377731715, letter B) #25609's own hand-off text; no shape, default or refusal change (verified on the diff). Right.
  2. Tenant-audit census regenerated for the new sys_approval_action insert, prose counts 238 → 239 — lane remainder; Lint · docs & code guards concluded success. Right.
  3. Reviewer request not sent — no write outside the relay. Right.

open_questions: none declared, none found.

out_of_scope_findings (2, carrier none), each judged:

  1. An SLA escalation with action: 'auto_approve' still approves an overdue request on an excludeSubmitter step — an authored ADR-0042 timeout policy applied under the system context (recordedActor is null there), recorded as an escalate row; not the submitter deciding and not an emptied-slate pass. The acceptance note is the right carrier.
  2. A direct openNodeRequest caller bypassing the parse could pair the key with auto_approve — no such caller exists at head (approval-node.ts parses first), and ruling A rules out a runtime override. The acceptance note is the right carrier.

Flag raised by this review, answered, not a FAIL: the describe is a strict subset of the TSDoc — it does not say that the fallback and admin_rescue slates are screened too, nor name the three post-open refusals (approve, reassign, escalation). Nothing it says is false; declared ⊆ enforced is the safe direction of Prime Directive #10; each refusal carries its own prescription; and the timeline note tells the author which route the request took. One sentence on the describe at the next spec touch of this key closes it — carried as an acceptance note for the lane seat, no card.

Check-runs on 0aed0b90 as read at 2026-10-25T00:23Z: 31 success, 10 skipped (opt-in and full-battery lanes), 0 failure; still in progress: Test Core shards 1/6, 3/6, 4/6, 5/6 and 6/6 (2/6 concluded success). Concluded success among the gates this review leans on: Lint & Repo Gates, Governed Surface Queue Guard, Build Core, Check Changeset, Spec property liveness, Type Check · source gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger. This verdict judges the contract on the concluded gates; the landing still waits for every check green, and a red on the pending Test Core shards is the dev's to fix on a new head, which re-owes this record.

Implemented-by: claude/project-thread-4z84a6
Reviewed-by: session_01R1q2xdtYgU6FKrZ7LgjPCC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 25, 2026 00:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 25, 2026 00:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 888d686 Oct 25, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4z84a6 branch October 25, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-approvals: a field or position approver step has no "not the submitter" guard, so a manager who submits their own request can approve it

2 participants