You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec(contracts): ApprovalActionRow declares the slot an approval action was taken as (an optional member), so the action log can show it once actor_id holds the person (#21411 ruling B) #21458
Split from #21411 by the triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U, answering that card's pm:retriage Q1. ⛔ Not a claim.
Grade: enhancement · priority:p1 · domain:spec · area:workflow · pm:queue. It is p1 because it gates #21411 (p1), whose PR consumes it and lands after it.
Why a card of its own
#21411's ruling B puts the deciding person in sys_approval_action.actor_id, and the slot an action was taken as in its own field. The ruled bullet "listActions shows the slot separately" needs the slot on the wire:
A new member on a published response contract widens the public surface. That is 强制条款², and so spec-lane work (references/execution-duties.md; the precedent is #21333).
Claim: PM loop round 1
Session: session_01YDt3PzwfrkuFzUBF89WPmM
Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-21458-approval-action-acted-as
Worktree: objectstack-issue-21458
Domain: domain:spec
Seat: domain:spec#2
File surface (at cfa4d740b7): packages/spec/src/contracts/approval-service.ts, where ApprovalActionRow gains one OPTIONAL string member, the slot address an approval action was taken as. Its name follows #21411's claim (acted_as), and its docblock takes via_override's form: what it records, when it is absent, and that it is never a person.
The pins the card names: the member is optional and string-typed; a row without it still parses; the api-surface row is present.
The regenerated api-surface/ and export-origins/ contract rows, and one @objectstack/spec changeset.
Container & model: S, mode:subagent, model: opus (dispatch-gates --tier at cfa4d740b7: no path-derived mandate; floor sonnet · default opus · ceiling CONTRACT_REVIEW_TIER). A published response contract widens, so the at-tier contract review runs as a separate isolated subagent after the PR opens.
Clause-②: yes
Thread-read: none
Serial constraints cleared: at 2026-10-02T20:34Z, no open PR touches contracts/approval-service.ts or the regenerated contract rows. #21411 (domain:services, os-bill) consumes this member, and its PR lands after this one, as the card orders.
Seat terms: p1, ordered ahead of this seat's p3 work under the maintainer's terms (「只处理 p0 p1 p2」, widened by 5952442849), batch 3.
os-dev-report
{
"pr": "#21479",
"issue": 21458,
"status": "done",
"branch": "claude/issue-21458-approval-action-acted-as",
"session": "session_01YDt3PzwfrkuFzUBF89WPmM (subagent: the parent's id; the relay read the same id from CLAUDE_CODE_REMOTE_SESSION_ID)",
"premise_still_valid": true,
"summary": "ApprovalActionRow (packages/spec/src/contracts/approval-service.ts) gains one optional string member, acted_as, declared next to via_override. Its docblock says what it records: the pending-approver slot the action was admitted under, in the slot's stored spelling (position:NAME, the deprecated role:NAME, an email or a user id). It says the member is never a person, because the person is actor_id (ADR-0118 D1). It says when the member is absent: the action was not admitted through a slot (a submitter action, a system action or an admin override), or the row predates the slot. So absent alone never proves that no slot was involved. The docblock names no tracker number. There is no authorable surface, no Zod schema, no request-side change, no producer, and no REST or client edit. The draft PR #21479 opens with 'Fixes #21458' and then 'Clause-②: yes'. It carries one @objectstack/spec minor changeset, modelled on the precedent's spec changeset. Premise checked at cfa4d74: there were zero acted_as hits under packages/ (the control via_override had hits in 16 files). The pinned branch of the approvals card had no member.",
"a1_name": "acted_as. Read from the #21411 dev report 5959282267: plan_on_yes.column ('sys-approval-action.object.ts gets acted_as: Field.text, maxLength 255') and open_questions[0] ('add one optional member, acted_as?: string, to ApprovalActionRow'). The seat claim 5958740584 names no spelling ('a plugin-object field recording the slot'). The branch claude/issue-21411-approval-actor-person holds no commit past 53fd35e, its empty claim marker (git ls-remote). The card's spelling and the approvals side's spelling agree, so there is no difference to report.",
"pins": [
"Optional: the exported type alias ActedAsIsOptional asserts that an empty object type extends the Pick of ApprovalActionRow on acted_as. An expectTypeOf equality cannot tell an optional member from a required 'acted_as: string | undefined', and this alias can. check:test-typecheck compiles the file (it is not in test-typecheck-debt.json).",
"String-typed: the exported alias ActedAsIsAString asserts Eq(ApprovalActionRow['acted_as'], string | undefined). The test also has an expectTypeOf equality.",
"A row without the member still conforms: a row literal with no acted_as is assigned to ApprovalActionRow, and reading the member answers undefined. No Zod schema exists for this row type (it is a TS interface), so 'parses' is read as 'type-checks'. The existing producer (rowFromAction) and the client SDK name the type only as ApprovalActionRow[]. No 'keyof' or 'Required' of the type exists in the repo.",
"The api-surface row is present: api-surface/contracts.json line 34 'ApprovalActionRow (interface)'; export-origins/contracts.json line 34 names its origin. Regenerating both (gen:api-surface, gen:export-origins) after a real build changed no byte, because these artifacts record export names, not members. check:api-surface reports 'unchanged'. This gate cannot see a member being added, which is why pins 1 to 3 carry the member."
],
"semver": {
"level": "@objectstack/spec minor, 'Clause-②: yes', header 'feat(spec): ...'. This is the shape of the precedent's spec changeset .changeset/21333-spec-boolean-comparand-contract.md (commit 9f13c94: '@objectstack/spec: minor', 'Clause-②: yes').",
"gate_lines": [
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: 'This diff introduces no major bump.' Run locally, the level axis printed NOT APPLICABLE (no pull_request payload).",
"node scripts/check-changeset-no-major.mjs --base origin/main --event (a pull_request payload carrying this PR body) :: exit 0 :: 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch.' · declaration line 'Clause-②: yes' · direction arm: none declared.",
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).'",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' and 'No changeset from the merge base modified or deleted by this diff.'"
],
"refusals": "none"
},
"gates": [
{
"command": "pnpm --filter @objectstack/spec build (plus the objectql and lint closures), under os-verify-lock, at cb86024",
"exit": 0,
"verdict": "VERDICT command-exit 0"
},
{
"command": "pnpm --filter @objectstack/spec check:generated at cb86024",
"exit": 0,
"verdict": "All 15 generated artifacts are up to date"
},
{
"command": "pnpm --filter @objectstack/spec run check:api-surface at cb86024",
"exit": 0,
"verdict": "@objectstack/spec public API surface + factory signatures unchanged"
},
{
"command": "pnpm --filter @objectstack/spec test at cb86024",
"exit": 0,
"verdict": "Test Files 602 passed (602); Tests 17738 passed, 1 todo (17739)"
},
{
"command": "pnpm --filter @objectstack/spec typecheck at cb86024",
"exit": 0,
"verdict": "check:test-typecheck: OK, 52 file(s) / 246 error(s) / 135 pinned signature(s) held"
},
{
"command": "pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/contracts/approval-service.test.ts at 828d3f2",
"exit": 0,
"verdict": "Tests 7 passed (7)"
},
{
"command": "pnpm check:doc-authoring",
"exit": 0,
"verdict": "17285 customer-facing string(s) across 1236 spec sources clean, no internal issue-id references"
},
{
"command": "pnpm check:nul-bytes",
"exit": 0,
"verdict": "scanned 9810 text file(s), no raw ASCII control bytes"
},
{
"command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE, at cb86024",
"exit": 0,
"verdict": "85 derived famil(ies) accounted for, 85 run, 0 NOT-MEASURED (a DERIVED zero: all 85 recorded an exit code and none of them is 3)"
}
],
"gates_note": "The derivation (--commands, no paths) gave the same 85 commands at 828d3f2, d576d5b and cb86024. All 85 ran at cb86024, each exit written to disk before any pipe, and all 85 exited 0. In the first pass at d576d5b, two exited 3 (PREREQUISITE NOT MET). check:lean-entry-closure passed once the objectql closure was built. check:dual-build-cjs-loads passed in the final pass, but some of the dists it read (for example packages/client/dist) had been built by an earlier gate from the tree before the merge. This diff emits no JavaScript (an interface member), and CI builds fresh. origin/main has since moved one more commit, aa46322 (a page-block spec change). It touches no file of this diff and was not merged in. dispatch-gates reported that none of the commits it can see touched what it derives from.",
"tests": "Spec package at cb86024: test 602 files, 17738 passed, 1 todo, exit 0; typecheck exit 0; check:generated exit 0. Ablations used scripts/ablation-replace.mjs in WRAP mode (anchor must hit, blob-change proof, trap restore), at d576d5b with the fix committed, measured by pnpm check:test-typecheck. (a) acted_as?: string changed to acted_as: string: anchor 1 to 0, blob 6ed13aee9a82 to 04260649fc42, RED 'src/contracts/approval-service.test.ts: 5 type error(s)'; restored to blob == HEAD 6ed13aee9a82 with git diff HEAD empty. (b) changed to acted_as?: string | null: blob to 675062e3d238, RED with 3 type errors; restored the same way. The test imports ./approval-service from src, so dist is not on the ablation path. Reverse check that the rebuilt .d.ts is what a consumer reads: a scratch module loaded @objectstack/spec/contracts from packages/spec/dist/contracts/index.d.mts (--traceResolution). The leg acted_as: 'position:finance' exited 0. The leg acted_as: 42 exited 2 with 'TS2322: Type number is not assignable to type string'. A stale .d.ts would have given an excess-property error on both legs.",
"line_budget": "87 changed lines (+85 / -2) across 3 files against the human-merge threshold of 5000: under. No skills/** file and no governed surface (.claude/, docs/adr/, AGENTS.md and the rest) is touched, so no skill line ratchet applies, and the PR is not governed.",
"files_changed": [
"packages/spec/src/contracts/approval-service.ts (+23 -0: the member and its docblock)",
"packages/spec/src/contracts/approval-service.test.ts (+47 -2: the pins; the shared Eq/Assert helpers moved up so both blocks use them)",
".changeset/21458-spec-approval-action-acted-as.md (+15: @objectstack/spec minor, Clause-②: yes)"
],
"deviations": [
"Merged origin/main 49524f6 into the branch (merge commit cb86024) before opening the PR, per AGENTS.md section 10, because the incoming commits touched packages/spec (other contract files). The derived gate union, the spec build, check:generated, test and typecheck were all re-run at cb86024.",
"I stopped the first full gate pass at d576d5b partway (my own runner, process group 15130), because I tightened the docblock and changeset wording after it had started. Its results are superseded by the pass at cb86024.",
"The dispatch's A3 says 'the regenerated artifact carries it'. api-surface records names, not members, so regeneration was a zero-byte diff. The existing 'ApprovalActionRow (interface)' row is the reading; the member itself is held by the type pins.",
"The card's 'a row without it still parses' has no parser to run, because there is no Zod schema for this interface. It is pinned as type-checks.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session plus 'Co-authored-by: Claude'), not the model-named Co-Authored-By from the harness reminder. The PR footer uses AGENTS.md's session-URL form. AGENTS.md takes precedence, and the pre-push gate passed."
],
"mcp_calls": "0",
"api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft, base main), giving #21479 (read-back: 8917 bytes sent, 8917 stored, identical); (2) label-write --issue 21479 --assign os-tesla, which is POST /repos//issues/21479/assignees (read-back matches; size/s was set by another actor and left alone); (3) this os-dev-report comment, which is POST /repos//issues/21458/comments through post-stamped.mjs. git push (4 pushes of this branch) is not a REST write. No label was added: the dispatch named none, and skip-changeset does not apply because the diff publishes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the DTO's actor_id member on ApprovalActionRow has no docblock. The ADR-0118 D1 description that the approvals card plans is for the object field (sys-approval-action.object.ts), not for this row type. It is recorded in the PR's Acceptance notes."
],
"conflicts_noted": [
"The window before the producer lands: until the approvals card's PR merges, no producer writes acted_as (every row omits it, which is the declared absent case), and actor_id still holds the slot literal on slot-gated rows. Triage ruled this order (land first, then consume). The changeset says that the approvals service's own changeset states when listActions starts returning the member."
]
}
domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5961025256 · 2026-10-02T22:44Z
Shape (read on GitHub): a draft against main. The first line is Fixes #21458, and Clause-②: yes sits at a line start. 3 files: contracts/approval-service.ts, its test, and one changeset.
Contract review: at-tier PASS5962569316 on this exact head.
CI on cb86024a: 32 success and 3 skipped by design (Build Docs path-filtered, Console Pin Gate, the opt-in packed-tarball smoke).
Governed surface:check-governed-merges --pr 21479 reads NOT governed. 87 changed lines.
The member:ApprovalActionRow gains one OPTIONAL acted_as?: string, next to via_override. It holds the slot address an action was admitted under, in the slot's stored spelling, and it is never a person (that is actor_id). Its absence means no slot was involved, or the row predates the member, so absence alone proves nothing.
Out-of-scope finding: the DTO's actor_id member has no docblock. → noted, not filed: it rides along naturally with #21411's PR, which documents the object field under ADR-0118 D1.
Blob check against the ACCEPTed head cb86024a (at-tier PASS 5962569316): 3 of 3 files are blob-equal.
Delivered:ApprovalActionRow declares one optional acted_as?: string, the slot address an approval action was admitted under, which is never a person. It is pinned optional and string-typed, and it ships in @objectstack/specminor.
Activity
objectstack-fleet commentedon Oct 2, 2026
Claim: PM loop round 1
Session:
session_01YDt3PzwfrkuFzUBF89WPmMAccount:
os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21458-approval-action-acted-asWorktree:
objectstack-issue-21458Domain:
domain:specSeat:
domain:spec#2File surface (at
cfa4d740b7):packages/spec/src/contracts/approval-service.ts, whereApprovalActionRowgains one OPTIONAL string member, the slot address an approval action was taken as. Its name follows #21411's claim (acted_as), and its docblock takesvia_override's form: what it records, when it is absent, and that it is never a person.api-surfacerow is present.api-surface/andexport-origins/contract rows, and one@objectstack/specchangeset.sys_approval_action.actor_id(asys_userlookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411's PR writes the member fromrowFromAction.Container & model:
S,mode:subagent,model: opus(dispatch-gates --tieratcfa4d740b7: no path-derived mandate; floor sonnet · default opus · ceilingCONTRACT_REVIEW_TIER). A published response contract widens, so the at-tier contract review runs as a separate isolated subagent after the PR opens.Clause-②: yes
Thread-read: none
Serial constraints cleared: at 2026-10-02T20:34Z, no open PR touches
contracts/approval-service.tsor the regenerated contract rows. #21411 (domain:services,os-bill) consumes this member, and its PR lands after this one, as the card orders.Seat terms: p1, ordered ahead of this seat's p3 work under the maintainer's terms (「只处理 p0 p1 p2」, widened by
5952442849),batch 3.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{
"pr": "#21479",
"issue": 21458,
"status": "done",
"branch": "claude/issue-21458-approval-action-acted-as",
"session": "session_01YDt3PzwfrkuFzUBF89WPmM (subagent: the parent's id; the relay read the same id from CLAUDE_CODE_REMOTE_SESSION_ID)",
"premise_still_valid": true,
"summary": "ApprovalActionRow (packages/spec/src/contracts/approval-service.ts) gains one optional string member, acted_as, declared next to via_override. Its docblock says what it records: the pending-approver slot the action was admitted under, in the slot's stored spelling (position:NAME, the deprecated role:NAME, an email or a user id). It says the member is never a person, because the person is actor_id (ADR-0118 D1). It says when the member is absent: the action was not admitted through a slot (a submitter action, a system action or an admin override), or the row predates the slot. So absent alone never proves that no slot was involved. The docblock names no tracker number. There is no authorable surface, no Zod schema, no request-side change, no producer, and no REST or client edit. The draft PR #21479 opens with 'Fixes #21458' and then 'Clause-②: yes'. It carries one @objectstack/spec minor changeset, modelled on the precedent's spec changeset. Premise checked at cfa4d74: there were zero acted_as hits under packages/ (the control via_override had hits in 16 files). The pinned branch of the approvals card had no member.",
"a1_name": "acted_as. Read from the #21411 dev report 5959282267: plan_on_yes.column ('sys-approval-action.object.ts gets acted_as: Field.text, maxLength 255') and open_questions[0] ('add one optional member, acted_as?: string, to ApprovalActionRow'). The seat claim 5958740584 names no spelling ('a plugin-object field recording the slot'). The branch claude/issue-21411-approval-actor-person holds no commit past 53fd35e, its empty claim marker (git ls-remote). The card's spelling and the approvals side's spelling agree, so there is no difference to report.",
"pins": [
"Optional: the exported type alias ActedAsIsOptional asserts that an empty object type extends the Pick of ApprovalActionRow on acted_as. An expectTypeOf equality cannot tell an optional member from a required 'acted_as: string | undefined', and this alias can. check:test-typecheck compiles the file (it is not in test-typecheck-debt.json).",
"String-typed: the exported alias ActedAsIsAString asserts Eq(ApprovalActionRow['acted_as'], string | undefined). The test also has an expectTypeOf equality.",
"A row without the member still conforms: a row literal with no acted_as is assigned to ApprovalActionRow, and reading the member answers undefined. No Zod schema exists for this row type (it is a TS interface), so 'parses' is read as 'type-checks'. The existing producer (rowFromAction) and the client SDK name the type only as ApprovalActionRow[]. No 'keyof' or 'Required' of the type exists in the repo.",
"The api-surface row is present: api-surface/contracts.json line 34 'ApprovalActionRow (interface)'; export-origins/contracts.json line 34 names its origin. Regenerating both (gen:api-surface, gen:export-origins) after a real build changed no byte, because these artifacts record export names, not members. check:api-surface reports 'unchanged'. This gate cannot see a member being added, which is why pins 1 to 3 carry the member."
],
"semver": {
"level": "@objectstack/spec minor, 'Clause-②: yes', header 'feat(spec): ...'. This is the shape of the precedent's spec changeset .changeset/21333-spec-boolean-comparand-contract.md (commit 9f13c94: '@objectstack/spec: minor', 'Clause-②: yes').",
"gate_lines": [
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: 'This diff introduces no
majorbump.' Run locally, the level axis printed NOT APPLICABLE (no pull_request payload).","node scripts/check-changeset-no-major.mjs --base origin/main --event (a pull_request payload carrying this PR body) :: exit 0 :: 'LEVEL AXIS: this PR declares clause-②
yes, and no package whosepackages/**/src/**it moves is gradedpatch.' · declaration line 'Clause-②: yes' · direction arm: none declared.","node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).'",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' and 'No changeset from the merge base modified or deleted by this diff.'"
],
"refusals": "none"
},
"gates": [
{
"command": "pnpm --filter @objectstack/spec build (plus the objectql and lint closures), under os-verify-lock, at cb86024",
"exit": 0,
"verdict": "VERDICT command-exit 0"
},
{
"command": "pnpm --filter @objectstack/spec check:generated at cb86024",
"exit": 0,
"verdict": "All 15 generated artifacts are up to date"
},
{
"command": "pnpm --filter @objectstack/spec run check:api-surface at cb86024",
"exit": 0,
"verdict": "@objectstack/spec public API surface + factory signatures unchanged"
},
{
"command": "pnpm --filter @objectstack/spec test at cb86024",
"exit": 0,
"verdict": "Test Files 602 passed (602); Tests 17738 passed, 1 todo (17739)"
},
{
"command": "pnpm --filter @objectstack/spec typecheck at cb86024",
"exit": 0,
"verdict": "check:test-typecheck: OK, 52 file(s) / 246 error(s) / 135 pinned signature(s) held"
},
{
"command": "pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/contracts/approval-service.test.ts at 828d3f2",
"exit": 0,
"verdict": "Tests 7 passed (7)"
},
{
"command": "pnpm check:doc-authoring",
"exit": 0,
"verdict": "17285 customer-facing string(s) across 1236 spec sources clean, no internal issue-id references"
},
{
"command": "pnpm check:nul-bytes",
"exit": 0,
"verdict": "scanned 9810 text file(s), no raw ASCII control bytes"
},
{
"command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE, at cb86024",
"exit": 0,
"verdict": "85 derived famil(ies) accounted for, 85 run, 0 NOT-MEASURED (a DERIVED zero: all 85 recorded an exit code and none of them is 3)"
}
],
"gates_note": "The derivation (--commands, no paths) gave the same 85 commands at 828d3f2, d576d5b and cb86024. All 85 ran at cb86024, each exit written to disk before any pipe, and all 85 exited 0. In the first pass at d576d5b, two exited 3 (PREREQUISITE NOT MET). check:lean-entry-closure passed once the objectql closure was built. check:dual-build-cjs-loads passed in the final pass, but some of the dists it read (for example packages/client/dist) had been built by an earlier gate from the tree before the merge. This diff emits no JavaScript (an interface member), and CI builds fresh. origin/main has since moved one more commit, aa46322 (a page-block spec change). It touches no file of this diff and was not merged in. dispatch-gates reported that none of the commits it can see touched what it derives from.",
"tests": "Spec package at cb86024: test 602 files, 17738 passed, 1 todo, exit 0; typecheck exit 0; check:generated exit 0. Ablations used scripts/ablation-replace.mjs in WRAP mode (anchor must hit, blob-change proof, trap restore), at d576d5b with the fix committed, measured by pnpm check:test-typecheck. (a) acted_as?: string changed to acted_as: string: anchor 1 to 0, blob 6ed13aee9a82 to 04260649fc42, RED 'src/contracts/approval-service.test.ts: 5 type error(s)'; restored to blob == HEAD 6ed13aee9a82 with git diff HEAD empty. (b) changed to acted_as?: string | null: blob to 675062e3d238, RED with 3 type errors; restored the same way. The test imports ./approval-service from src, so dist is not on the ablation path. Reverse check that the rebuilt .d.ts is what a consumer reads: a scratch module loaded @objectstack/spec/contracts from packages/spec/dist/contracts/index.d.mts (--traceResolution). The leg acted_as: 'position:finance' exited 0. The leg acted_as: 42 exited 2 with 'TS2322: Type number is not assignable to type string'. A stale .d.ts would have given an excess-property error on both legs.",
"line_budget": "87 changed lines (+85 / -2) across 3 files against the human-merge threshold of 5000: under. No skills/** file and no governed surface (.claude/, docs/adr/, AGENTS.md and the rest) is touched, so no skill line ratchet applies, and the PR is not governed.",
"files_changed": [
"packages/spec/src/contracts/approval-service.ts (+23 -0: the member and its docblock)",
"packages/spec/src/contracts/approval-service.test.ts (+47 -2: the pins; the shared Eq/Assert helpers moved up so both blocks use them)",
".changeset/21458-spec-approval-action-acted-as.md (+15: @objectstack/spec minor, Clause-②: yes)"
],
"deviations": [
"Merged origin/main 49524f6 into the branch (merge commit cb86024) before opening the PR, per AGENTS.md section 10, because the incoming commits touched packages/spec (other contract files). The derived gate union, the spec build, check:generated, test and typecheck were all re-run at cb86024.",
"I stopped the first full gate pass at d576d5b partway (my own runner, process group 15130), because I tightened the docblock and changeset wording after it had started. Its results are superseded by the pass at cb86024.",
"The dispatch's A3 says 'the regenerated artifact carries it'. api-surface records names, not members, so regeneration was a zero-byte diff. The existing 'ApprovalActionRow (interface)' row is the reading; the member itself is held by the type pins.",
"The card's 'a row without it still parses' has no parser to run, because there is no Zod schema for this interface. It is pinned as type-checks.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session plus 'Co-authored-by: Claude'), not the model-named Co-Authored-By from the harness reminder. The PR footer uses AGENTS.md's session-URL form. AGENTS.md takes precedence, and the pre-push gate passed."
],
"mcp_calls": "0",
"api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft, base main), giving #21479 (read-back: 8917 bytes sent, 8917 stored, identical); (2) label-write --issue 21479 --assign os-tesla, which is POST /repos//issues/21479/assignees (read-back matches; size/s was set by another actor and left alone); (3) this os-dev-report comment, which is POST /repos//issues/21458/comments through post-stamped.mjs. git push (4 pushes of this branch) is not a REST write. No label was added: the dispatch named none, and skip-changeset does not apply because the diff publishes.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the DTO's actor_id member on ApprovalActionRow has no docblock. The ADR-0118 D1 description that the approvals card plans is for the object field (sys-approval-action.object.ts), not for this row type. It is recorded in the PR's Acceptance notes."
],
"conflicts_noted": [
"The window before the producer lands: until the approvals card's PR merges, no producer writes acted_as (every row omits it, which is the declared absent case), and actor_id still holds the slot literal on slot-gated rows. Triage ruled this order (land first, then consume). The changeset says that the approvals service's own changeset states when listActions starts returning the member."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
ACCEPT — PR #21479 @
cb86024adomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5961025256· 2026-10-02T22:44Zmain. The first line isFixes #21458, andClause-②: yessits at a line start. 3 files:contracts/approval-service.ts, its test, and one changeset.5962569316on this exact head.cb86024a: 32 success and 3 skipped by design (Build Docspath-filtered,Console Pin Gate, the opt-in packed-tarball smoke).check-governed-merges --pr 21479reads NOT governed. 87 changed lines.5960329631(Q1) and ruling B5954022700on approvals:sys_approval_action.actor_id(asys_userlookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411:ApprovalActionRowgains one OPTIONALacted_as?: string, next tovia_override. It holds the slot address an action was admitted under, in the slot's stored spelling, and it is never a person (that isactor_id). Its absence means no slot was involved, or the row predates the member, so absence alone proves nothing.sys_approval_action.actor_id(asys_userlookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411's plan adds (dev report5959282267).string | undefined-required) and string-typed. Both ablations went red.api-surfacerecords export names only, so its zero-byte regeneration is expected, and the pins carry the member.@objectstack/specminor,Clause-②: yes, the objectql: a string comparand against a boolean field answers wrong rows under 200 — "true"/"false" match nothing, $ne "true" returns the true row, and ?f=true / ?f=false on the GET door both answer zero rows #21333 precedent's shape.sys_approval_action.actor_id(asys_userlookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411's PR (domain:services,os-bill) then writes the member fromrowFromAction. Until then, no row carries it, which is the declared absent case.Out-of-scope finding: the DTO's
actor_idmember has no docblock. → noted, not filed: it rides along naturally with #21411's PR, which documents the object field under ADR-0118 D1.Next: ready, auto-merge, the queue.
Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Landed: PR #21479 →
72217cd6ffdomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5961025256· 2026-10-02T23:15ZApprovalActionRowdeclaresacted_as, the slot an approval action was taken as #21479 went through the merge queue as72217cd6ff, with one parent (7097859633). This card closedcompletedby itsFixesline.cb86024a(at-tier PASS5962569316): 3 of 3 files are blob-equal.ApprovalActionRowdeclares one optionalacted_as?: string, the slot address an approval action was admitted under, which is never a person. It is pinned optional and string-typed, and it ships in@objectstack/specminor.sys_approval_action.actor_id(asys_userlookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411's PR (domain:services) now writes the member fromrowFromAction. A pointer is on approvals:sys_approval_action.actor_id(asys_userlookup) records the slot literal (position:<p>, or an email) instead of the deciding user, so the person who decided is on no column (ADR-0118 D1) #21411.pm:dispatchedis removed in this act. Domain, area and type labels stay.Generated by Claude Code
Merge origin/main into claude/issue-21411-approval-actor-person (brings
feat(approvals): the action log returns acted_as beside the person
feat(spec): `ApprovalActionRow` declares `acted_as`, the slot an appr…
fix(approvals): actor_id records the person who decided, acted_as the…
test(spec): contracts and conversions test titles state each cited de…