Skip to content

feat(spec): approval onEmptyApprovers gains 'fallback' with a sibling fallbackApprovers - #18525

Merged
os-litant merged 5 commits into
mainfrom
claude/issue-17931-approval-onemptyapprovers-fallback
Sep 17, 2026
Merged

os-litant merged 5 commits into
mainfrom
claude/issue-17931-approval-onemptyapprovers-fallback

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #17931

Clause-②: yes (widening) — minor. This adds an enum member and an authorable key; nothing an author can write is removed or renamed, so it is not breaking.

What this does

An approval node whose approvers resolve to nobody had three endings, and none of them named anyone: admin_rescue (the default — the request opens on a dead type:value slot and waits for a privileged admin), fail (the run dies), auto_approve (the record is waved through). This adds the fourth, ruled in the #16678 Phase 2 design §8.2: onEmptyApprovers: 'fallback' with a sibling fallbackApprovers list, at the node, not on the manager rung.

{
  approvers: [{ type: 'manager' }],
  onEmptyApprovers: 'fallback',
  fallbackApprovers: [{ type: 'org_membership_level', value: 'owner' }],
}
  • packages/spec — the fourth enum member, plus fallbackApprovers: z.array(ApprovalNodeApproverSchema).min(1).optional(). Pairing is enforced in BOTH directions by a superRefine, each message naming both keys: 'fallback' with no list is refused, and a list under any other policy is refused too, because nothing would ever read it. A refinement rather than a discriminated union, so the published JSON-Schema def stays one def (the Studio property form and registerFlow() both read it) and the ZodObject class, .shape and additionalProperties: false all survive — pinned.
  • packages/plugins/plugin-approvalsopenNodeRequest's empty-slate block resolves fallbackApprovers through the SAME expandApprovers call the primary slate uses (so approver types, OOO delegation and per_group tagging behave identically) and opens the request on those ids, never on the manager:undefined literal. The abandoned slate's per_group tags are dropped from the snapshot; the fallback's resolution inputs land under a fallback: prefix in __resolvedFrom, so the audit trail answers both "why did the fallback fire" and "who did it pick". A fallback that itself resolves to nobody, and a stored node declaring the policy with no list, both degrade to admin_rescue: a rescue path must not kill the run.
  • packages/lintMANAGER_ONLY_ROUTES gains the node-level escape as a route needing no write to sys_user.manager_id, and the empty-policy nudge enumerates all four members. The rule is NOT deleted and NOT silenced by a declared fallback: it reads shape, and a fallbackApprovers list can itself resolve empty. stackWiresManagerChain stays the one silencer. The approvals.mdx callout carries the same prose.
  • Pins — the manager row the approvals: a department approver never resolves when the business unit has organization_id = null (every seeded BU) #3807 warning table was missing, which is the row that pins the manager:undefined literal itself.

One card premise was false as spelled

The card says the sibling key reuses ApproverEntrySchema. There is no ApproverEntrySchema anywhere in this repo. Measured on origin/main fb6b2c369e: git grep -n "export const [A-Za-z]*Approver[A-Za-z]*Schema" -- packages/spec/src returns exactly one line — approval.zod.ts:416: export const ApprovalNodeApproverSchema — the dark control export const ZzzApproverNoSuchSchema exits 1, and a whole-repo git grep ApproverEntrySchema exits 1. The intent binds and was followed: the key reuses the existing approver entry shape, ApprovalNodeApproverSchema, and a pin proves it is not a second dialect (an entry the primary list would refuse is refused here too, by the same surface error).

Two other inherited premises drifted only in line numbers and are re-measured here: manager is in GRAPH_APPROVER_TYPES at approval-service.ts:860-862 (the card said 831-833), and the expanded to nobody warning is at :1779 (the card said 1730). Both facts hold. The manager:undefined literal holds and is now pinned.

One repair this change forced

ApproverType's .describe() still said sys_user.manager_id "has no product write surface" — an assertion #16678 retired when it landed the admin operation, and one the lint rule's own docblock named as the stale carrier. The generated reference renders that string once per shape that reuses the approver entry, so fallbackApprovers would have shipped a third copy of it. The sentence now describes the column and keeps pointing at the single authoritative remedy instead of asserting a fact of its own; a spec-side pin holds it. 4 lines carrying the old sentence leave content/docs/references/automation/approval.mdx, 0 arrive.

Verification

Measured on head 6d3f5a7946, all foreground, heavy runs through scripts/pm/os-verify-lock.sh (VERDICT command-exit read, never a bare $?).

run result
pnpm --filter @objectstack/spec test 483 files, 13791 passed, exit 0
pnpm --filter @objectstack/spec typecheck exit 0
pnpm --filter @objectstack/lint test 103 files, 3848 passed, exit 0
pnpm --filter @objectstack/lint typecheck exit 0
pnpm --filter @objectstack/plugin-approvals test 46 files, 764 passed, exit 0
pnpm --filter @objectstack/plugin-approvals typecheck exit 0
pnpm --filter '@objectstack/plugin-approvals^...' build exit 0 (dependency closure)
pnpm --filter @objectstack/spec check:generated exit 0 — 15 of 15 artifacts current
pnpm lint (whole repo, eslint . --no-inline-config) exit 0

Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 112 commands for this file surface; all 112 were run. 107 exit 0. The five that did not are each PREREQUISITE NOT MET or a pre-existing condition, none of them a verdict on this diff:

  • check:dual-build-cjs-loads exit 3, check:i18n exit 3, check:type-check-debt exit 3 — each prints PREREQUISITE NOT MET and names the full build it needs. NOT MEASURED, not green.
  • check:skill-examples exit 1 — same class in substance: it refuses because packages/client-react/dist holds no declarations ("a verdict now would be computed against a build that no longer matches src"). NOT MEASURED.
  • check:cross-package-test-inputs exit 1reproduced on a pristine merge-base tree with zero of this branch's changes, and it is not about any file here. Control, same commit fb6b2c369e, detached worktree: with no packages/spec/dist/ on disk it exits 0; after copying packages/spec/dist/ in, it exits 1 with the identical finding (@objectstack/cli descends a directory tree from packages/spec/dist/ … rooted in packages/cli/test/init-created-files-summary.e2e.test.ts). Filed as a finding below.

Ablation — every new pin proved non-vacuous. Each leg mutated the committed implementation, proved the mutation reached disk by grepping the removed marker to 0 and the injected marker to 1 (plus git diff --numstat), ran, restored with git checkout HEAD -- path, proved the restore by comparing git hash-object against the HEAD blob, and re-ran. A trap … EXIT INT TERM with absolute paths guarded the whole sequence; the working tree is clean.

leg mutation mutated restored
spec drop .superRefine(checkFallbackApproversPairing) 5 failed / 58 passed, exit 1 63 passed, exit 0
plugin approvers = fallbackApprovers becomes a no-op 2 failed / 306 passed, exit 1 308 passed, exit 0
lint drop the new escape from MANAGER_ONLY_ROUTES 1 failed / 46 passed, exit 1 47 passed, exit 0

The five spec failures are exactly the pairing pins (both arms, and each of the three negative-control policies); the two plugin failures are "opens on the declared fallback, never on the manager:undefined literal" and "resolves the fallback through the same expansion approvers use"; the lint failure is "names the node-level escape".

Acceptance notes

Noted, not filed — out of scope, listed for whoever next touches these files:

  • skills/objectstack-automation/SKILL.md line 728 and docs/qa/platform-checklist/areas/approvals.json both enumerate the onEmptyApprovers vocabulary and now list three of four members. Both are outside this PR's declared file surface, and skills/** is a governed surface whose inclusion would change how this PR lands, so neither was touched. Successor: the skills seat (that file's next editor) — needs its own card.
  • approval-approvers-may-resolve-empty's group-routed arm (position/team/department) now names the node-level policy too, because the same escape applies; its "add a guaranteed-staffed fallback approver" wording was also disambiguated, since fallbackApprovers is now a real key and "a fallback approver" had become two things.

To file (with dedupe words): check:cross-package-test-inputs fails on main as soon as packages/spec/dist/ exists on disk — the walk root is a gitignored build directory, so the gate is silent on an unbuilt tree and reports a real undeclared input on a built one. Reproduced above with a discriminating control. Dedupe words: cross-package-test-inputs, init-created-files-summary, packages/spec/dist, declared glob, #7802.


Generated by Claude Code

… fallbackApprovers

The node-level empty-slate policy can now NAME people. `fallbackApprovers`
reuses `ApprovalNodeApproverSchema` (not a second approver shape) and is
paired with the policy in BOTH directions by a `superRefine`: required when
the policy is 'fallback', refused under any other policy so a list nothing
reads can never ship.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…slate is empty

The `fallback` empty-slate policy resolves the node's declared
`fallbackApprovers` through the SAME expansion as `approvers` and opens the
request on those ids — never on the `manager:undefined` literal an empty
manager rung leaves behind. A fallback that itself resolves to nobody, or a
stored node declaring the policy with no list, degrades to admin_rescue.

Adds the missing `manager` row to the graph-approver warning table.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…e-only verdict

`approval-approvers-may-resolve-empty` now offers `onEmptyApprovers: 'fallback'`
as a route that needs no write to `sys_user.manager_id`, and the empty-policy
nudge enumerates all four members. The finding still fires when a fallback is
declared — the rule reads shape, and a fallback list can resolve empty too.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…face that exists

`sys_user.manager_id` gained a dedicated admin operation, and this string still
said the column "has no product write surface" — a sentence the generated
reference renders once per shape that reuses the approver entry, so this
change's new `fallbackApprovers` shape would have shipped a third copy of it.
The sentence now describes the column and keeps pointing at the one
authoritative remedy instead of asserting a fact of its own.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…prover-type text

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

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/approvals.mdx (via ApproverType (symbol, a top-level const object), admin_rescue (literal, a string literal in ApprovalNodeConfigSchema; a string literal in a comment in checkFallbackApproversPairing; a string literal in checkFallbackApproversPairing), auto_approve (literal, a string literal in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers), fallbackApprovers (literal, a string literal in checkFallbackApproversPairing), onEmptyApprovers (literal, a string literal in checkFallbackApproversPairing), org_membership_level (literal, a string literal in a comment in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), auto_approve (literal, a string literal in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers))
  • content/docs/getting-started/common-patterns.mdx (via auto_approve (literal, a string literal in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers))
  • content/docs/plugins/packages.mdx (via org_membership_level (literal, a string literal in a comment in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers))

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

  • content/docs/releases/v14.mdx (via org_membership_level (literal, a string literal in a comment in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers))
  • content/docs/releases/v16.mdx (via ApproverType (symbol, a top-level const object), org_membership_level (literal, a string literal in a comment in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers))
  • content/docs/releases/v17/17-0.mdx (via admin_rescue (literal, a string literal in ApprovalNodeConfigSchema; a string literal in a comment in checkFallbackApproversPairing; a string literal in checkFallbackApproversPairing), auto_approve (literal, a string literal in ApprovalNodeConfigSchema; a string literal in checkFallbackApproversPairing; a string literal in validateApprovalApprovers), onEmptyApprovers (literal, a string literal in checkFallbackApproversPairing))

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/authorable-surface/automation.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 136 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 2882528786ee33c870a9c14ee0c4d81030250c76packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 2882528786ee33c870a9c14ee0c4d81030250c76

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

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6d3f5a7946ed0159944a6ed72a68367326f5d6c6

① Derived judgments

  1. Accept set, ApprovalNodeConfigSchema.onEmptyApprovers (approval.zod.ts:884): enum gains a fourth member 'fallback'; the three existing members and the admin_rescue default are unchanged (authorable-defaults/automation.json untouched, pinned by "leaves the omitted policy defaulting to admin_rescue"). Widening. Declared yes (widening) — correct.
  2. Accept set, new key fallbackApprovers (:906), z.array(ApprovalNodeApproverSchema).min(1).optional() on a .strict() shape: a key that was refused as unknown is now accepted. Widening. Bound to the one existing approver-entry shape (the card's ApproverEntrySchema does not exist; ApprovalNodeApproverSchema at :423 is the only one, and the pin "resolves entries through the approver shape approvers uses" holds it). Correct, and the corrected premise from the claim comment was followed.
  3. Accept set, superRefine(checkFallbackApproversPairing) (:728-766, attached at :947): refuses 'fallback' without a non-empty list (issue on fallbackApprovers) and refuses a list under any other policy (issue on onEmptyApprovers, naming the default's two readings). Neither arm narrows anything the pre-PR schema accepted: 'fallback' was refused as an enum value and fallbackApprovers as an unknown key. Net effect is widening only; the reverse arm is what stops a list nothing reads from shipping. Declaration correct. Note fallbackApprovers: [] under 'fallback' is refused twice (.min(1) and the refinement) — harmless.
  4. Public types ApprovalNodeConfig / ApprovalNodeConfigParsed: the onEmptyApprovers union widens and fallbackApprovers? is added. Additive; a downstream exhaustive switch would be a compile-time widening cost, which is what minor is for. Workspace and consumer-gate type checks are green on this head. Correct.
  5. Published JSON-Schema def automation/ApprovalNodeConfig (served as the Approval descriptor's configSchema to the Studio form and to registerFlow's key walk): onEmptyApprovers.enum gains 'fallback', properties.fallbackApprovers added, additionalProperties: false kept, still one def (refinement not representable and not emitted). PR's "one def survives" is correct and pinned.
  6. Published prose, ApproverType.describe() (:96-105) and its three rendered copies in content/docs/references/automation/approval.mdx plus the ## ApproverType paragraph: "has no product write surface" / "from outside the product" removed, the node-level fallback named. The write surface exists (admin-set-user-manager.ts), so this is a correction of a false claim, not a behaviour change. PR's "4 lines out, 0 arrive" matches the diff. Correct.
  7. Generated reference page: onEmptyApprovers row shows four members, a fallbackApprovers row, and a new nested-shape section for fallbackApprovers[number]. Consistent with the schema. Correct.
  8. packages/spec/authorable-surface/automation.json: one line added, automation/ApprovalNodeConfig:fallbackApprovers. authorable-surface.base.json correctly untouched (deletion-gate baseline, re-anchored only deliberately). api-surface, export-origins, declaration-map, json-schema.manifest correctly untouched: checkFallbackApproversPairing is module-private, no new export. Correct.
  9. Runtime, openNodeRequest (approval-service.ts:2743-2846): the 'fallback' branch runs only inside the existing "no concrete approver" block, after the unchanged fail throw and auto_approve return, resolves the declared list through the same expandApprovers call (same groups, substitutions, exprCtx), replaces the slate, drops the abandoned slate's groups entries, and namespaces the fallback's resolution inputs as fallback: keys in the stored __resolvedFrom snapshot (an additive key namespace in a persisted wire shape). The admin_rescue warning became conditional on the slate still being empty; for the three existing policies that condition is always true inside the block, so their behaviour is unchanged (negative controls pinned). No new error code; NO_APPROVERS unchanged. Three new warn strings, not codes. The manager:undefined literal is now pinned in the approvals: a department approver never resolves when the business unit has organization_id = null (every seeded BU) #3807 table and asserted absent under fallback. Correct, and this is the runtime honouring the declaration — the declared-but-unenforced trap is not present.
  10. Lint, approval-approvers-may-resolve-empty: MANAGER_ONLY_ROUTES gains the node-level escape, the group-routed hint is disambiguated ("approver entry"), the expression nudge enumerates all four members. Rule id, severity and the single silencer stackWiresManagerChain unchanged; the finding keeps firing with a fallback declared (pinned). Matches the card's binding scope item 3 and the "do not delete the rule" ruling. Correct.
  11. Published prose, content/docs/automation/approvals.mdx:302: "fallback without a fallbackApprovers list is refused at authoring time" — WRONG as to when. In this repo nothing parses ApprovalNodeConfigSchema before execution: registerFlow accepts the flow (its only config check is the undeclared-key walk, and fallbackApprovers is now a declared key), os lint has no pairing arm, defineFlow leaves node config as an open record. The refusal fires at flow execution, at the approval node's entry (approval-node.ts:133), loudly and naming both keys, before any request opens. The same guide uses "authoring time" for os lint at lines 25 and 102, so a reader will expect lint to catch it. Not an accept-set or runtime defect — the contract is enforced, later than the sentence says — but the sentence needs to say "when the node's config is parsed at flow execution" or a lint pairing arm needs to make it true. Low blast radius: one sentence.
  12. PR-body narrative: "a stored node declaring the policy with no list … degrade[s] to admin_rescue: a rescue path must not kill the run" — over-stated. Through the flow executor such a node fails at the Zod parse with the refinement's message; the degrade branch is reachable only by direct openNodeRequest callers, which the in-code comment ("a caller that bypassed the schema") says correctly. Not a contract defect; the body's framing is the loose one.
  13. Not in the accept set and correctly left alone: IApprovalService.openNodeRequest return type (unchanged), the liveness ledger (approval-node keys are outside its walk — flow.json classifies nodes.config as one live row), ADR text, i18n (no locale carrier exists).

Summary for ①: the card's Clause-②: yes (widening) is right; every accept-set change is a widening; the runtime, JSON-schema emission, generated reference and authorable-surface artifacts all carry the new member and key. Two prose characterisations (items 11 and 12) are wrong about when the pairing refusal happens; neither changes the accept set, the semver level, or whether the declaration is honoured.

② Semver level

Changeset .changeset/17931-approval-onemptyapprovers-fallback.md declares @objectstack/spec: minor, @objectstack/plugin-approvals: minor, @objectstack/lint: minor. Those are exactly the three published packages the diff touches (the other changed paths are content/docs and the spec's generated artifacts, which ship inside @objectstack/spec). AGENTS.md §3 rules yes takes at least minor and only (narrowing) is breaking; nothing an author can write is removed or renamed, so no migration text or ADR-0087 marker is owed and the check:adr-0087-registration gate (breaking changesets only) does not apply. Check Changeset is green on the head. Consistent with ①.

③ Boundary flags

Implemented-by: claude/issue-17931-approval-onemptyapprovers-fallback
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS

Basis: ① the card's yes (widening) is correct and every accept-set change is a widening honoured by the runtime, the JSON-schema emission and the generated artifacts; ② minor on the three touched published packages is consistent; ③ every flagged boundary is either filed with a verified issue or answered above. The one wrong characterisation that matters to a reader (item 11, "refused at authoring time") is a single doc sentence with no accept-set or runtime effect and is named for correction; it does not rise to a mis-declaration of the contract.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Clause-② carriers cleared — provenance

Both carriers stripped in one stroke by the dispatching seat, session_01LvwGppdonww4zGLWZo5rho (domain:spec execution seat).

Review of record comment 5706743883 on this PR
Head judged 6d3f5a7946ed0159944a6ed72a68367326f5d6c6
Served tier CONTRACT_REVIEW_TIER
Verdict PASS
Carriers cleared PR #18525 ✅ · card #17931 ✅ (both read back after the write)

How this verdict was produced. This seat's measured served model is claude-opus-5; CONTRACT_REVIEW_TIER is claude-fable-5-1 and the comparison is exact ⇒ the seat is not at tier. Per the downgrade fuse the verdict was routed to an isolated review subagent running at CONTRACT_REVIEW_TIER, fed only the card, its rulings and the PR itself, with an adversarial brief — ⛔ never the dispatch order, ⛔ never this seat's conclusions. The record is adopted verbatim. ⛔ Not an in-seat review and ⛔ not a quota downgrade.

Independence pair: a mode:subagent dev's branch against the seat session adopting the isolated reviewer's verdict ⇒ no SELF-REVIEW.

Boundary item carried to #18551, filed before this PR lands so a closing keyword cannot lose it. The review named it as a required correction to shipped prose; ⛔ the seat did not let it ride unnamed, and ⛔ did not fix it in-branch, because that would move the head and void an otherwise complete review record.

Pre-landing checks: ① review PASS on record ✅ · ② check-clause2-carriers --pair 18525 re-run after the strip ✅ · ③ re-taken at landing time, latest-run-per-check-name ✅. Governed-surface predicate: 0 paths hit the register ⇒ ordinary queue landing.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 17, 2026 00:55
@os-litant
os-litant added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit b0eb9a5 Sep 17, 2026
40 checks passed
@os-litant
os-litant deleted the claude/issue-17931-approval-onemptyapprovers-fallback branch September 17, 2026 08:06
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

3 participants