Skip to content

feat(plugin-approvals,rest,client): a record-anchored approval lock read, { locked } for every caller who can read the record - #26361

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-rzpauf
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-rzpauf

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26314

Clause-②: yes (widening)
Metadata-protocol: none

What changes

#25934 ruling A ("readers"): the record lock gets a carrier of its own, so a member who can read a record but is not on its approval is no longer offered an edit the server then refuses with 409 RECORD_LOCKED.

  • @objectstack/spec: one optional member on IApprovalService, getRecordLock?(object, recordId, context): Promise<{ locked: boolean } | null>, with its doc block. No schema, no exported type.
  • @objectstack/plugin-approvals: ApprovalService.getRecordLock reads the record as the caller (engine.find with the caller's context, fields: ['id'], limit: 1). An unreadable record answers null, the same as a missing id; a caller refused the object's read is refused for every id. locked comes from the pending-request lookup the hook already uses.
  • One lock judgement: nodeConfigLocksRecord(config) (lockRecord !== false) in lifecycle-hooks.ts is now the only spelling. The beforeUpdate record-lock hook, the request row's lock_record mirror and the new read all call it; the read goes through recordHeldByLockingApproval, which shares the hook's lookup and its bookkeeping-object exemption.
  • @objectstack/rest: GET /api/v1/approvals/records/:object/:recordId/lock answers { success: true, data: { locked } } and nothing else. Every null answers one 404 RECORD_NOT_FOUND body, so an unreadable record and a missing one cannot be told apart. The closed query-parameter set is empty (route rule 5). A service without the member answers 501. Ledgered as sdk.
  • @objectstack/client: client.approvals.getRecordLock(object, recordId).
  • Docs: content/docs/automation/approvals.mdx gains a "Reading the lock" paragraph with a curl example.

#8652's ruling is untouched: request content stays behind approvalsVisibleToReaders, and the tests assert the non-participant still gets no request row.

Spec change: needs the spec seat's sign-off

The only packages/spec/** change is packages/spec/src/contracts/approval-service.ts, new lines 935–972: the doc block plus the optional member getRecordLock? inserted right after getRequest(...) in IApprovalService. Nothing else under packages/spec/ changed. It is optional so existing implementations and the spec's own minimalService literal still satisfy the contract.

Tests

  • packages/qa/dogfood/test/approval-record-lock-read.dogfood.test.ts (new): the card's four cases through the REST door on a real boot with the real SecurityPlugin, flows and approvals plugin, on two private objects:
    1. a non-participant who can read and edit the held record gets exactly { success: true, data: { locked: true } }, still gets [] from the request list, and their PATCH is still 409 RECORD_LOCKED;
    2. a read-only reader gets { locked: true };
    3. a caller who cannot read the record (their GET /data is 404) gets the same status and body as for a non-existent id (404 RECORD_NOT_FOUND);
    4. a pending node with lockRecord: false, and the held record after approval, both read { locked: false } and the edit lands.
  • packages/plugins/plugin-approvals/src/record-lock-read.integration.test.ts (new, 4 tests): real ObjectQL over SQLite with the real plugin start; also pins lock_record and the read agreeing on every request.
  • packages/rest/src/rest-approvals-record-lock-route.test.ts (new, 5 tests): preservation (extra keys from a service are dropped), one 404 body for every null, the empty closed query set, 501, and a 403 relay.
  • packages/client/src/client.test.ts: URL encoding and envelope unwrapping.
  • packages/rest/src/execctx-consumer-census.test.ts: counts move by one for the new route.

Dogfood does not run on PR CI, so it was run locally under the verify lock.

At head 6ba5f8cc: plugin-approvals 99 files / 1280 tests pass, client 52 / 672 pass, the new dogfood file 1 / 1 pass. Rest (324 files / 6483 pass, 326 skipped) and spec (725 files / 21974 pass) ran at 99f67d74; the later merges of main changed no file in either package.

Gates

  • dispatch-gates.mjs --commands derived 114 commands; all 114 ran at 6ba5f8cc with exit 0, reconciled with --ran (114 run, 0 not measured, 0 unrun). main moved 13 commits during the run; none touches a file in this diff.
  • typecheck on spec, plugin-approvals, rest and client: exit 0. Spec check:generated: exit 0.
  • test:repo on core, objectql, rest, runtime, spec and types: exit 0.
  • Scoped eslint over the touched files and check-nul-bytes: exit 0.

Ablations (through the REST door, dogfood)

  1. Lock judgement mutated to always lock: red at approval-record-lock-read.dogfood.test.ts:217, expected locked: false for the lockRecord: false record, received true. Restored, blob equals HEAD.
  2. As-caller read mutated to a system context: red at :209, expected 404 for the caller who cannot read the record, received 200. Restored, blob equals HEAD.

Each mutation was rebuilt into dist and confirmed on disk by scripts/ablation-dist-preflight.mjs before the run, and --absent after the restore. Ablation 2's declaration build failed (TS6133, context unused once the mutation dropped its use); the JS build still carried the mutation, which the preflight found in both index.js and index.mjs, so the run is valid.

Deviations from the claim

  • The claim named plugin-approvals; the route, ledger row and client method live in packages/rest (approvals-routes.ts, rest-route-ledger.ts) and packages/client/src/index.ts. Agreed with the dispatching seat before the change so the ledger row ships as sdk, not as a gap.
  • Skill teaching is not in this PR: skills/** is a governed surface and would make the whole PR need maintainer approval. A follow-up card is named in the report.

Acceptance notes

  • pendingRequestFor reads with limit: 1, so with two pending requests on one record the hook judges only the first one it gets. The new read reuses the same lookup, so the two still agree; whether two pending requests on one record can happen is not verified here.
  • Overlap: open feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 also edits approval-service.ts (different regions) and content/docs/automation/approvals.mdx (a nearby section); whichever lands second may need a trivial rebase.

Generated by Claude Code

…reader of the record

IApprovalService.getRecordLock (optional) answers { locked } to every caller
who can read the record, read as the caller; an unreadable record answers
null exactly as a missing id. The lock judgement is lifted to one function
(nodeConfigLocksRecord) the record-lock hook, the request-row mirror and the
new read all call. REST: GET /approvals/records/:object/:recordId/lock,
ledgered as sdk with client.approvals.getRecordLock.

Claude-Session: https://claude.ai/code/session_01QAsn9ToxaDxbnRkP6HxERw
Co-authored-by: Claude <noreply@anthropic.com>
…at the service, the door and a real boot; docs and changeset

Claude-Session: https://claude.ai/code/session_01QAsn9ToxaDxbnRkP6HxERw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/client, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/spec, touching 20 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 222755ebbf20c564b85d3da9e04f1bd9ebcbf280.

⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 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 — 151 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 222755ebbf20c564b85d3da9e04f1bd9ebcbf280 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 222755ebbf20c564b85d3da9e04f1bd9ebcbf280

⚠️ 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 222755ebbf20c564b85d3da9e04f1bd9ebcbf280 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6ba5f8cc3ea2ea0cb52f8a5c328e043f241380bd
Local-runs: none
Scope: packages/spec/src/contracts/approval-service.ts + packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/lifecycle-hooks.ts, packages/rest/src/approvals-routes.ts, packages/rest/src/rest-route-ledger.ts, packages/rest/src/error-response.ts (read, unchanged), packages/types/src/response-envelope.ts (read, unchanged), packages/client/src/index.ts, .changeset/26314-record-anchored-approval-lock-read.md, content/docs/automation/approvals.mdx, packages/plugins/plugin-approvals/src/record-lock-read.integration.test.ts, packages/rest/src/rest-approvals-record-lock-route.test.ts, packages/qa/dogfood/test/approval-record-lock-read.dogfood.test.ts. The remainder of the diff (packages/client/src/client.test.ts, packages/rest/src/execctx-consumer-census.test.ts) is the lane seat's ACCEPT at TIER_DEFAULT.

Base: merge-base with origin/main is 1a298dda8d9a36e0c594a1f219277110fc939f43; six commits on the head, two of them merges of main. Inputs read: card #26314 body and all three comments (6427176260 triage, 6427951121 claim, 6429820983 os-dev-report), rulings 6422939157 and 6419712936 on #25934 and 5299823744 on #8652, PR #26361 body, file list and net diff, and the head's check-runs.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged:

  1. IApprovalService gains one OPTIONAL member, getRecordLock?(object, recordId, context), answering a promise of { locked: boolean } or null. Widening only: every existing implementation, and the spec's own minimalService test literal (approval-service.test.ts:228), still satisfies the interface. No exported schema or type is added. The spec's generated surfaces (api-surface/contracts.json, api-surface-signatures.json) list export names, not interface members, so no artifact regeneration is owed; Spec property liveness and the lint gates are green. RIGHT.

  2. Claim "read AS THE CALLER (object read, then row-level access)": ApprovalService.getRecordLock (approval-service.ts:8673) calls this.engine.find(objectName, { where: { id }, fields: ['id'], limit: 1, context }), byte-for-byte the anchor read of addRecordReaderVisibleIds (:8392), the 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 precedent. The caller's context is the one the door resolved; no SYSTEM_CTX on this path. RIGHT.

  3. Claim "unreadable equals missing, no path tells them apart": three outcomes exist and every one is id-independent in the order it is reached. (a) Object read refused: the engine throws before any id-dependent step; the door relays it through handleRouteError at the refusal's own status, the same status for an existing or a missing id. (b) Object read allowed but the row is filtered by RLS, or does not exist: one and the same find returns [], getRecordLock answers null, and the door writes one 404 RECORD_NOT_FOUND whose message names the object only (No such 'object' record), never the id. (c) Row visible: 200 { success: true, data: { locked } }. The pending-request lookup runs only after (c), so neither its result nor its timing is observable for an unreadable record, and the lock judgement never consults the caller. The integration test pins (a) and (b) at the service (unreadable _b equals MISSING; outsider gets PERMISSION_DENIED 403 for both ids), the route test pins one 404 body with the id absent, and the dogfood run pins (b) and (c) through the real SecurityPlugin on private objects, status and body equal for the stranger's unreadable id and a non-existent id. RIGHT. Coverage note, not a defect: arm (a) is exercised end to end only with a stand-in middleware (integration) and a mocked rejection (route test); with the real SecurityPlugin the dogfood stranger holds object read narrowed to own rows, so the real plugin's denial shape at the door is judged by reading resolveErrorResponse (status carried through), not by a test. The property holds structurally because the throw precedes every id-dependent step.

  4. Claim "one lock judgement shared with the beforeUpdate hook and lock_record": nodeConfigLocksRecord (lifecycle-hooks.ts:403) is the only executable spelling of lockRecord !== false in the plugin's non-test sources (a git grep at the head finds the rule's text otherwise only in two comments and the function's own doc block). Its three callers are the hook (:592), the request row mirror rowFromRequest (approval-service.ts:1290) and the read (recordHeldByLockingApproval, :439). RIGHT.

  5. Claim "shares the hook's lookup": recordHeldByLockingApproval calls pendingRequestsForRecords(engine, object, [id]), which for one id delegates to pendingRequestFor (limit 1, SYSTEM_CTX); the hook's by-id path is gatingRequests → asIdList(ctx.input.id) → the same pendingRequestsForRecords. The bookkeeping-object exemption is one function, isApprovalBookkeepingObject, used by both. The read deliberately omits the hook's two WRITE-scoped exemptions (the opening run's own writes, mirror-only writes); those are properties of a writer and a payload, not of the record, and the doc block says so. RIGHT.

  6. Import direction: approval-service.ts now imports from lifecycle-hooks.js; lifecycle-hooks.ts imports nothing from approval-service.ts (its local imports are @objectstack/core, @objectstack/spec/data, ./payload-redaction.js). No cycle. RIGHT.

  7. Public REST surface: GET /api/v1/approvals/records/:object/:recordId/lock, ledgered as sdk with client approvals.getRecordLock, registered under the same dataPath and isScoped convention as the sibling approval routes. The door serves { locked: lock.locked === true } and drops any extra key an implementation hands back. Closed query set is APPROVAL_RECORD_LOCK_PARAMS = [], refused via refuseUnknownQueryParams with the located 400 before the service is asked. RIGHT.

  8. Claim "501 on absence": if (!svc || typeof svc.getRecordLock !== 'function') return respond501(res), the family's existing helper. Note only: the 501 body is the family's flat { code, message } dialect while the 404 and 400 are the nested envelope; that is the pre-existing shape of every approval route's 501, not something this diff introduces. RIGHT.

  9. Client: client.approvals.getRecordLock(object, recordId) encodes both path segments and unwraps the envelope; pinned by client.test.ts. Docs paragraph in approvals.mdx states the same contract as the door (404 for unreadable equals missing, 501 on absence, locked only). RIGHT.

  10. The route's resolveExecCtx site is bare and guarded by enforceAuth on the next line, so the execctx census moves 61 → 62 sites, 45 → 46 bare, 144 → 145 mentions. RIGHT by construction; the census test is in the CI test run.

  11. Disclosure posture against the rulings: { locked } only; no request id, node, participants or pending; listRequests and getRequest for the non-participant still answer [] and null (integration test), the request list through the door still answers [] for the editor (dogfood). 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 ruling 5299823744 is untouched; console: a non-participant reader is offered inline edit on a record whose pending approval locks it (lockRecord: true), and the save answers 409 RECORD_LOCKED #25934 ruling A parameters (a) carrier, (b) disclosure to every reader as the caller, (c) payload are each met. RIGHT.

② Semver level

Changeset .changeset/26314-record-anchored-approval-lock-read.md: patch on @objectstack/spec, @objectstack/plugin-approvals, @objectstack/rest, @objectstack/client, the four packages the diff moves. Both declaration lines are present and identical on the PR body and the changeset body: Clause-②: yes (widening) and Metadata-protocol: none. The kind is none (a service contract member, a route and a client method; nothing an author writes in metadata changes), and by the level rule none takes patch. The widening arm is right: an optional member added, a route added, a method added; nothing removed, renamed, refused or tightened, so no BREAKING banner and no ADR-0087 disposition is owed. Check Changeset is green on the head.

Clause-②: yes (widening), matched.

③ Boundary flags

Dev flags from the PR body (Deviations, Acceptance notes) and the os-dev-report 6429820983 (deviations, out_of_scope_findings; open_questions is empty):

  • Deviation 1, route, ledger row and client method live in packages/rest and packages/client, not plugin-approvals: ANSWERED. approvals-routes.ts and rest-route-ledger.ts are where every approval route and ledger row lives; the claim comment itself names the ledger and IApprovalService, and the seat agreed the sdk row before the change. The right files were touched.
  • Deviation 2, skill teaching deferred because skills/** is Tier H: ESCALATED to the seat. Triage 6427176260 requires the PR or "a follow-up card it names" to teach the new route with an eval prompt. The report carries a proposed card title and dedupe words but no filed card number, so nothing is named yet. The seat files that card (skills/objectstack-automation: the lock read plus one eval prompt) so the deferral has a number, before or at landing. Not a defect of this diff.
  • Acceptance note 1, pendingRequestFor reads with limit: 1: ANSWERED. The read and the hook share that lookup (①.5), so they cannot disagree; and the service refuses a second pending request on one record (DUPLICATE_REQUEST, approval-service.ts:3602 and :5451), so the "two pending requests on one record" case is refused upstream of both. No card owed.
  • Acceptance note 2, overlap with open feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 on approval-service.ts and approvals.mdx: ANSWERED. Different regions; the single-writer-path check is green on this head; whichever lands second merges main.
  • PR body "Spec change: needs the spec seat's sign-off" (also triage's lane note and the claim's "the seat obtains the spec seat's sign-off before landing"): this record is the contract-tier review of the spec hunk on its merits (①.1 to ①.3) and finds it sound; the lane sign-off itself is the seat's process obligation and stays ESCALATED to the seat as outstanding until it is recorded.
  • Dogfood does not run on PR CI; the new dogfood file and the two ablations were run by the dev locally under the verify lock. Noted; this review ran nothing locally.

Check-runs on the head, read 2026-10-26T23:37:50Z: 46 runs, 27 success, 7 skipped (opt-in and matrix placeholders), 0 failure, 12 in progress (Build Core, Test Core 1/8 to 8/8, Type Check UI, Type Check consumer gates, Type Check workspace). Green at that time includes Check Changeset, Governed Surface Queue Guard, Spec property liveness, every Lint job, the claim and single-writer-path checks, Type Check source gates and debt ledger. The landing rule still requires every check green; this verdict is on the diff and the contract, with those twelve conclusions recorded as not yet read.

Implemented-by: claude/project-thread-rzpauf
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T23:56Z · head 6ba5f8cc3e · #26314

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape: a draft against main. The body carries Fixes #26314, and Clause-②: yes (widening) and Metadata-protocol: none line-initial, identical to the changeset .changeset/26314-record-anchored-approval-lock-read.md (patch on spec, plugin-approvals, rest and client, the four packages the diff publishes from). check-governed-merges --pr 26361: NOT governed (0 of 13 paths), 907 changed lines.
  2. Contract review: the diff touches packages/spec/src/contracts/approval-service.ts, so the at-tier review was owed (schema face, CONTRACT_REVIEW_TIER). Its record is comment 6429948897 on this PR, on head 6ba5f8cc3e: PASS. Under pm-dispatch cross-repo-coordination.md (a spec surface found inside a dispatched card stays on the card and is reviewed at tier), that record is the spec half's review; the order's separate spec-seat sign-off is withdrawn by the seat that wrote it.
  3. Rulings checked against the diff (the seat's own reading of the non-spec remainder):
    • Disclosure as the caller: ApprovalService.getRecordLock reads the record with engine.find under the caller's context (fields: ['id'], limit: 1). Empty answers null; the route turns every null into one 404 RECORD_NOT_FOUND body that names neither the id nor the cause. A caller with no read on the object is refused before any id-dependent step, the same for every id.
    • Payload: the route sends { locked: lock.locked === true } and nothing else; the rest test pins that extra keys from a service are dropped.
    • One lock judgement: nodeConfigLocksRecord in lifecycle-hooks.ts is the only spelling of lockRecord !== false. The beforeUpdate hook, the lock_record mirror in rowFromRequest and the new read all call it, and the read shares the hook's pending-request lookup.
    • 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 untouched: request content stays behind approvalsVisibleToReaders; the dogfood case asserts the non-participant still lists [].
    • Route rules: the closed query-parameter set is empty and refused with the located 400; a service without the member answers 501; the ledger row ships as sdk with approvals.getRecordLock.
    • Tests and ablations: the card's four cases run through the REST door on a real boot (dogfood, run locally under the verify lock since dogfood does not run on PR CI). Ablating the lock judgement reds :217, and ablating the as-caller read reds :209, each confirmed in dist by the ablation preflight.
  4. Gates: at this stamp the head's 48 check-runs read 41 success and 7 skipped (path-filtered UI, dogfood matrix placeholders and opt-in harnesses), with none failing or in progress; Test Core 1–8, Check Changeset, Dogfood Regression Gate and Governed Surface Queue Guard are green; mergeable, clean. Locally the dev reports all 114 derived gate commands exit 0, reconciled with --ran (0 not measured), and the new dogfood file 1 of 1 passing.
  5. Accepted deviations:
  6. Out of scope: skills(automation): teach the record-anchored approval lock read (GET /approvals/records/:object/:recordId/lock, { locked } for every reader) with one eval prompt #26362 above. The limit: 1 note in the body is answered by the contract review: the service refuses a second pending request on one record (DUPLICATE_REQUEST). feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 overlaps approval-service.ts and approvals.mdx in other regions; whichever lands second merges main.
  7. Landing: this seat runs pr_ready and then automerge_enable through the relay, confirms MERGED on both readings, and records the landing on plugin-approvals: a record-anchored lock read, { locked: boolean } for every reader of the record, so a non-participant is not offered an edit the server refuses (#25934 ruling A) #26314.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 23:58
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 23:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 6a2866a Oct 27, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-rzpauf branch October 27, 2026 00:19
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants