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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
a type: form action fired from a related-list row (record_related) opens a blank CREATE form instead of the clicked row's edit form — the bridge puts the row on the action definition, executeForm reads only the page context #13063
Filing gate: ① a product defect with a named landing point and a reproduction (pm:queue class).
Reader: the objectui lane owning packages/app-shell/src/views/RelatedRecordActionsBridge.tsx and packages/core/src/actions/ActionRunner.ts (executeForm) — the fix PR there.
Dedupe (open AND closed, semantic search on objectstack-ai/objectui): "related list row action type form opens blank create form instead of clicked row edit form recordId RelatedRecordActionsBridge executeForm" → 9 hits, all closed; nearest #4278 (form action from a record opened an empty create form — the record_section leg, fixed) and #4292 (?recordId= carries no object — the reason executeForm ignores action.objectName); #11270 placed record_related actions on related rows. None covers the related-row dispatch of a type: form action. First-round wave cards (#13052#13054#13055#13056) checked.
reach: public entry measured — Project Detail → Tasks related list → row menu "Log Time" on a stock showcase boot, console built at objectui 4016370a: opens a blank create form (3 rows, 3 fresh loads).
Path: records-forms.action-location-matrix
QA-source: objectstack-ai/objectstack#24616 · records-forms.action-location-matrix · acceptance[2]
Checklist item priority: P1
type: Bug
Filed on behalf of the QA runner session session_01NvpZQ6sRwAVRNDi4UsF2c7 (its GitHub scope did not include this repository) by the 18.0 QA wave dispatch session session_01CfRDy7SuTfUqaTu2AEH2Hi.
What is wrong
A type: 'form' action placed at record_related and fired from a related-list row opens the declared form as an empty CREATE form for the child object, instead of the clicked row's edit form. The same action fired from the child's own record page (record_section / title bar) carries recordId correctly.
Reproduction
Stock showcase boot; sign in as admin.
Open /_console/apps/com.example.showcase/showcase_project/record/PROJECT_ID?tab=related (any project with tasks).
Tasks related list → a row's "Row actions" menu → "Log Time".
Expected: /_console/forms/showcase_task.form?recordId=TASK_ID&recordObject=showcase_task, prefilled with that task (what the same action does from Task Detail).
Actual: bare /_console/forms/showcase_task.form — Title empty, Project "Select…", no GET of the clicked task.
Mechanism (objectui 4016370a)
RelatedRecordActionsBridge.tsxrunRowAction retargets the row action by setting objectName: childObject / recordId on the action DEFINITION, then calls the page's shared execute.
Expected fix shape: the related-row bridge hands the row as the action's execution CONTEXT (record + objectName), not as fields on the definition, so executeForm's boundary check sees the child object.
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on objectui origin/mainc2191654a4.
The premise holds as code-read:packages/core/src/actions/ActionRunner.ts:2343executeForm(action) takes only the action, and the bridge (RelatedRecordActionsBridge.tsx) puts the row on the action definition. The measurement is the card's: 3 rows, 3 fresh loads.
p1: the item fails, so its P1 is the upper bound. The failure form is a wall: "Log Time" from a related row opens a blank CREATE form instead of the row's edit form, so the action cannot do its job from that position.
Claim: PM loop round 14 (priority:p1)
Session: session_01XVp19E1MYUTFM3dnkwz8gB
Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-13063-related-row-form-action
Worktree: objectui-issue-13063
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/app-shell/src/views/RelatedRecordActionsBridge.tsx (runRowAction hands the row as the action's execution context), packages/core/src/actions/ActionRunner.ts (executeForm reads the record and object from the context it is given) only if the bridge cannot pass a per-call context through the existing runner API, tests beside them, and one .changeset/13063-related-row-form-action.md (@object-ui/coreminor, @object-ui/app-shellpatch). ⛔ No new export; ⛔ executeForm keeps ignoring action.objectName (objectui#4292). Stop on breach; explain in the report.
Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier over this surface reads no path-derived mandate)
Clause-②: yes (amended: ActionRunner is re-exported from @object-ui/core's actions barrel, so execute(action, callContext?) widens a published method; @object-ui/coreminor, the objectui#12967 precedent)
Responsibility: @object-ui/app-shell's related-row bridge puts the row on the action definition, where executeForm never reads it | the record_section leg already carries recordId correctly (objectui#4278) | every type: form action placed at record_related and fired from a related-list row (showcase "Log Time", measured 3/3)
Thread-read: 6273652415
Serial constraints cleared: no open objectui PR touches RelatedRecordActionsBridge.tsx or ActionRunner.ts.
Claimed at: 2026-10-19T18:21Z
{
"issue": 13063,
"status": "done",
"branch": "claude/issue-13063-related-row-form-action",
"pr": "https://github.com/objectstack-ai/objectui/pull/13073",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "Premise held on origin/main 083e807358. The bridge retargeted a row action through the definition only, while executeForm reads the record and object from the runner context, which names the parent page. The runner had no per-call context path (execute took the action only; updateContext mutates the shared runner), so ActionRunner.execute gained an optional second parameter, callContext (no new export). With it, the dispatch runs on a one-shot runner over the merged context and shares the original's handlers. The ctx mirror follows the merge, and the base objectFields/objectActions pair is dropped when callContext names another object. RelatedRecordActionsBridge.runRowAction now calls useAction().runner.execute(def, { record: row, objectName: CHILD }); the definition keeps objectName/recordId/_rowRecord for the api/flow/script handlers and the update path. Toolbar dispatches (no row) are unchanged, and executeForm still ignores action.objectName. Measured: the update, api, flow and script row actions read the row/object from the definition first, so they are unchanged. The url/navigation templates now bind the row instead of the parent record; this is the ruling's sanctioned change. Deviation: the bridge bypasses the provider's execute wrapper (loading/error/result state). No useAction() consumer reads those fields; recorded in the PR Acceptance notes.",
"tests": "Head 3dafcd4657. Union vitest --maxWorkers=1 over 52 files (two new pins + every test naming RelatedRecordActionsBridge + core ActionRunner*.test.ts + packages/react/src/context/__tests__): 'Test Files 52 passed (52) / Tests 604 passed (604)', lock VERDICT command-exit 0. Closure build pnpm --filter '@object-ui/app-shell^...' build: VERDICT 0. type-check core+app-shell: VERDICT 0 (first run exit 2 on untyped vi.fn spies in the new core test; fixed in 3dafcd4657, rerun 0). eslint --no-inline-config on the 4 changed TS/TSX files: 0 errors; warnings equal base (core 12, app-shell 13); type-aware linting off (--print-config shows no parserOptions.project/projectService). Battery exit 0: phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, new-line-citations, check-hand-rolled-comment-mask, check-changeset-fixed, check-changeset-presence. check:spec-floors exit 1 (no-artifact; needs a whole-workspace build), recorded. Control-byte self-scan: no hits. Ablation (objectstack scripts/ablation-replace.mjs, source-aliased test so no dist): anchor 'runner.execute(def as ActionDef, rowContext)' x1 to x0, blob 84152195036a to c1ce37eefa57. Pin file 'Tests 2 failed | 2 passed (4)': (a) Received '/forms/task.edit' (the defect); the api control failed only on its context assertion (objectName project, record P-1) while its definition assertions held; child-record-page and page-level controls stayed green. Restore: blob after restore == HEAD blob 84152195036a, git diff HEAD empty. Declared to CI: full packages/core and packages/app-shell test dirs.",
"mcp_calls": "0",
"api_writes": "1 relay dispatch (fleet-write pr_create: POST /repos/objectstack-ai/objectui/pulls draft + POST /repos/objectstack-ai/objectui/issues/13073/assignees) + this os-dev-report comment through the relay; plus git pushes of the branch (4). No label writes (objectui: no labels named in the dispatch).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none. ActionProvider's execute takes one argument, so a per-call context bypasses its loading/error/result wrapper. Noted in PR Acceptance notes, not filed. Dedupe words: ActionProvider execute call context wrapper loading",
"carrier: none. A type: form action at list_item fired from an object list's row (ObjectView, no record in the runner context) reads no record either, so it opens bare /forms/NAME by the same mechanism. Code read only, not reproduced at a public door, so not filed. Dedupe words: list_item form action row recordId ObjectGrid executeForm"
]
}
Inputs: card #13063 (body, the triage comment, the claim as amended, the os-dev-report), PR #13073 (body, 5 files, net diff against main at fb85637e14), the check-runs on the head, and the head's source of packages/core/src/actions/ActionRunner.ts, packages/core/src/actions/index.ts, packages/app-shell/src/views/RelatedRecordActionsBridge.tsx and packages/react/src/context/ActionContext.tsx (read only, through the GitHub contents API). The showcase's record_related placements were read from examples/app-showcase/src/ui/actions/index.ts in the objectstack checkout.
① Derived judgments
Public-surface change: ActionRunner.execute(action: ActionDef) becomes execute(action, callContext?: Partial of ActionContext). ActionRunner reaches @object-ui/core's public face through export * from './ActionRunner.js' in the actions barrel, so this is a widening of a published method. No new export, no new file under src/ other than tests. Right.
A caller passing no callContext is unchanged. The only inserted statement in the old path is if (callContext) return this.scopedTo(callContext).execute(action); ahead of the existing try. With the argument absent the branch is not taken and the body that follows is byte-for-byte the previous execute. The bridge passes undefined when the row is not an object, which takes that same unchanged path. The provider's own execute wrapper still calls runner.execute(action) with one argument. Right.
The one-shot runner shares every handler the original holds. The class fields at head are handlers, scripts, evaluator, context, confirmHandler, toastHandler, modalHandler, navigationHandler, paramCollectionHandler, resultDialogHandler, translate, and the readonly builtinExecutors. scopedTo assigns the two Maps by reference and the six handlers plus translate by value. Not copied: context (replaced by the merged one, by design), evaluator (rebuilt by the constructor over the merged context, which is the point: the evaluator must read the row), and builtinExecutors (a per-instance table of arrow functions bound to this, so the fresh instance's own table dispatches to the fresh instance's executors; copying it would be wrong). Because the Maps are shared by reference, a handler registered on the original after the scoped runner is built is still seen. The constructor applies withIdentityAlias to the merged bag, so os.user is re-derived from user; the test pins it. current_user, which the provider binds through updateContext, lives on this.context and so is carried by the spread. Nothing a dispatch path reads is missing. Right.
Dropping objectFields / objectActions on an object change is right. Every reader of those two keys at head is readContextObjectFields / readContextObjectActions, which answer only when readContextObjectName(context) === objectName. On the shared runner a retargeted row action (action.objectName = child, context = parent) therefore already got undefined for both. On the scoped runner the context now names the child, so without the drop the parent's field map and declarations would answer for the child; the drop restores the same undefined the update path and declaredUpdatePatch saw before. When callContext names the same object (a self-relation) the pair is kept, which is the correct reading, and the test pins both branches. The ctx mirror gets the same treatment so ${ctx.record.*} and ${record.*} agree. Right.
executeForm still ignores action.objectName (objectui#4292). The method is untouched in the diff and at head reads recordId from this.context.record / this.context.data and the object from readContextObjectName(this.context) only. The scoped runner's this.context names the row and the child, which is exactly the shape a dispatch from the child's own record page has. The ban holds. Right.
update / api / flow / script unchanged; url and navigation templates now bind the row. Confirmed by reading the head: executeUpdateOperation takes action.objectName || readContextObjectName(...) and the row from params._rowRecord; the console's api / flow / script handlers read action.objectName, action.recordId, params.recordId and _rowRecord from the definition before any context, and the bridge still writes all of these on the definition (pinned by the api control). The row runs with a context that now also names the row, which is additive for those handlers. navigateTo evaluates ${record.*} through this.evaluator and ${ctx.recordId} through buildInterpolationContext (this.context.record?.id ?? this.context.recordId, then the ctx mirror merged on top); on the scoped runner both resolve to the row. The record page's provider context carries record, objectName, objectFields and no flat recordId, so the mirror does not re-introduce the parent's id. Safety for existing authored record_related url actions: the ruling (triage comment and the card's expected fix shape) sanctions the row as the execution context, so a url template at that location reading the parent was relying on the defect; the showcase declares exactly one action at record_related (showcase_log_time, type: form) and no url template there, so no shipped example changes meaning. The change is correct and bounded. One further consequence the dev did not list, same direction: the runner's own executeAPI fallback (no registered api handler) builds its body from action.params ?? this.context.data, which callContext does not touch, so it is unchanged. Right.
Bypassing ActionProvider's execute wrapper is not a user-visible regression. The wrapper only sets the provider's loading / error / result state and catches a throw; the loading indicator and the error or success toast a user sees come from the runner itself (handlePostExecution through the shared toastHandler, the shared confirmHandler, the shared paramCollectionHandler), all of which the scoped runner holds. runner.execute has its own try/catch returning { success: false, error }, so the wrapper's catch adds nothing for this path. Measured at head: the only non-test useAction() consumers that destructure anything beyond execute / updateContext / runner are none (useObjectActions reads loading / error from useActionRunner, a separate hook); empty-state-link.tsx already dispatches through useAction().runner directly, so the shape has a precedent. The one observable difference is that the provider no longer re-renders its subtree for a related-row dispatch, which nothing depends on. Right; carried forward under ③ as the dev flagged it.
Smaller points, all right: the [runner] dependency replaces [execute] and runner is memoised on the serialised context, so runRowAction is as stable as before; 'objectName' in callContext with a base context lacking objectName reads as changed and drops a pair that is absent anyway; the fallback useAction() (no provider) builds a fresh runner each render, which was equally true of the old execute and is not a regression.
Tests: the app-shell pin mounts the real provider, bridge, renderer and related list and observes the navigation handler, with three controls (child record page, page-level parent action after a row dispatch, a non-form row action); the ablation reported in the PR body turns the defect case and the api context assertion red and leaves the controls green. The core pin covers the merge, one-shot-ness, handler sharing, the alias, the mirror and the pair drop. No test is modified or deleted.
Check-runs on eabce85cf175: Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check, Changeset Overwrite Report, Line Citation Gate, Governed Surface Queue Guard, label and Live E2E (informational) are success; Test (coverage) and the coverage shards are skipped by design. Still in progress at the time of this record: Type Check, Lint, Repo Gates, Build & E2E, Build Docs, Bundle Analysis, Test (dist pins) and the sixteen Test (shard n/16) jobs. Their conclusions are the gate verdicts for those families; this record does not wait for them, and the PASS below is conditional on none of them concluding failure.
② Semver level
.changeset/13063-related-row-form-action.md declares @object-ui/core: minor and @object-ui/app-shell: patch. Both packages have src/ edits in the fixed group, so check-changeset-presence requires a declaration, and one is present (the Changeset Declaration run is green). minor on core is right: a widened signature of a published method is an additive public-surface change, and the objectui#12967 precedent plus the AGENTS.md rule that objectui never declares major make minor the ceiling as well as the floor. patch on app-shell is right: the bridge's change is a behaviour fix behind an unchanged component API. check-changeset-no-major is green. The body names the widened method and states that the argument is optional, which matches the diff. Clause-②: yes in the PR body matches the amended claim and the fact that a published method's signature changed.
③ Boundary flags
The os-dev-report's open_questions is empty. Dev flags and out-of-scope findings:
Bypass of the provider's execute wrapper (loading / error / result not set for related-row dispatches), carrier none. Judged under ① as not a regression; the suggested follow-up (an optional second argument on the provider's execute) belongs to @object-ui/react, outside this card's file surface, and needs no card to land this fix. Answered.
url / navigation row templates now bind the row. Judged under ① as the ruling's sanctioned change with no shipped example affected. The dev added no separate url pin; the form pin covers the same context path, which is acceptable at this tier. Answered.
check:spec-floors exit 1 no-artifact locally (needs a whole-workspace build). That family is answered by the head's Repo Gates / Build & E2E runs, still in progress above. Answered as far as this record can.
Out-of-scope: a type: form action at list_item fired from an object list row (no record in the runner context) opens bare /forms/NAME by the same mechanism; code-read only, not reproduced, not filed. Not this card's defect and not introduced here; the new callContext is the natural carrier for that fix. Escalated to the seat as a candidate card, no action required on this PR.
Minor: the PR body's verification section names head 3dafcd4657, two commits before the reviewed head eabce85cf1; the diff reviewed here is the net diff at the reviewed head, and the check-runs quoted are on it.
PR #13073 at head eabce85 (Fixes #13063, Clause-②: yes). Reviewed against:
the claim 6274196031, amended to Clause-② yes because ActionRunner is published;
triage 6273652415;
the dev report 6274878854 plus the changeset round;
the contract review PASS6275047442.
Premise: holds. RelatedRecordActionsBridge retargeted a row action through the definition only, while executeForm reads the record and object from the runner context (the parent page). So "Log Time" on a related row opened a blank create form.
Diff read by the seat:
ActionRunner.execute(action, callContext?): with a callContext, it runs once on a scoped runner over the merged context. That runner shares every handler and registry by reference. The ctx mirror follows the merge, and objectFields / objectActions are dropped when the object changes.
Without callContext, execute is byte-for-byte unchanged.
The bridge passes { record: row, objectName: CHILD }. The definition keeps objectName / recordId / _rowRecord, so the update, api, flow and script handlers are unchanged.
executeForm still ignores action.objectName (objectui#4292).
url / navigation templates at record_related now bind the row. That is the ruling's sanctioned change, and no shipped example reads the parent there.
Changeset:@object-ui/coreminor (a widened published method; objectui#12967 precedent) and @object-ui/app-shellpatch.
Pins:
RelatedRecordActionsBridge.rowFormContext-13063 (real provider, bridge and related list; controls: the child record page, a page-level parent action after a row dispatch, an api row action).
Ablation: with the row on the definition only, (a) received /forms/task.edit. Restored by blob hash.
Local gates: a 52-file union, 604 tests; closure build and type-check for core and app-shell; eslint 0 errors; the battery and changeset checks all 0.
Acceptance notes (not filed):
A related-row dispatch bypasses ActionProvider's loading / error / result wrapper. No useAction() consumer reads it, and toasts, confirm and params come from the shared runner (judged no regression).
A type: form action at list_item fired from an object-list row has the same blank-form mechanism. That is code-read only, with no public-door reproduction, so it is not filed; the new callContext is its natural carrier.
Commits: model-free trailers. The PR went through the fleet-write relay.
CI at head: 36/36 check-runs success or skipped on eabce85 (filter=latest), mergeable_state clean.
Verdict: ACCEPT. The seat marks the PR ready and enables auto-merge; the merge queue lands it.
Merged as 9f1dd79 on main. Verified by content: all 5 files in 9f1dd79^1..9f1dd79 are blob-identical to the accepted head eabce85. The squash message is model-free.
What landed:
A type: form action at record_related fired from a related-list row now opens that row's edit form (/forms/NAME?recordId=ROW&recordObject=CHILD).
The bridge passes the row as the dispatch's execution context through ActionRunner.execute(action, callContext?) (@object-ui/coreminor).
Contract review PASS 6275047442.
Seen by the showcase's "Log Time" from a project's Tasks list once the console pin carries 9f1dd79.
The card closed via Fixes #13063; pm:dispatched is removed with this note.
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p1 ·
bug·domain:ui·area:records·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T17:52ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on objectui
origin/mainc2191654a4.packages/core/src/actions/ActionRunner.ts:2343executeForm(action)takes only the action, and the bridge (RelatedRecordActionsBridge.tsx) puts the row on the action definition. The measurement is the card's: 3 rows, 3 fresh loads.domain:ui: carry the row's record context intoexecuteForm(as therecord_sectionleg does sincetype: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278). Pin atype: formaction fired from a related row opening that row.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 14 (
priority:p1)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13063-related-row-form-actionWorktree:
objectui-issue-13063Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/views/RelatedRecordActionsBridge.tsx(runRowActionhands the row as the action's execution context),packages/core/src/actions/ActionRunner.ts(executeFormreads the record and object from the context it is given) only if the bridge cannot pass a per-call context through the existing runner API, tests beside them, and one.changeset/13063-related-row-form-action.md(@object-ui/coreminor,@object-ui/app-shellpatch). ⛔ No new export; ⛔executeFormkeeps ignoringaction.objectName(objectui#4292). Stop on breach; explain in the report.Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tierover this surface reads no path-derived mandate)Clause-②: yes (amended:
ActionRunneris re-exported from@object-ui/core's actions barrel, soexecute(action, callContext?)widens a published method;@object-ui/coreminor, the objectui#12967 precedent)Responsibility:
@object-ui/app-shell's related-row bridge puts the row on the action definition, whereexecuteFormnever reads it | therecord_sectionleg already carriesrecordIdcorrectly (objectui#4278) | everytype: formaction placed atrecord_relatedand fired from a related-list row (showcase "Log Time", measured 3/3)Thread-read: 6273652415
Serial constraints cleared: no open objectui PR touches
RelatedRecordActionsBridge.tsxorActionRunner.ts.Claimed at: 2026-10-19T18:21Z
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13063, "status": "done", "branch": "claude/issue-13063-related-row-form-action", "pr": "https://github.com/objectstack-ai/objectui/pull/13073", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "Premise held on origin/main 083e807358. The bridge retargeted a row action through the definition only, while executeForm reads the record and object from the runner context, which names the parent page. The runner had no per-call context path (execute took the action only; updateContext mutates the shared runner), so ActionRunner.execute gained an optional second parameter, callContext (no new export). With it, the dispatch runs on a one-shot runner over the merged context and shares the original's handlers. The ctx mirror follows the merge, and the base objectFields/objectActions pair is dropped when callContext names another object. RelatedRecordActionsBridge.runRowAction now calls useAction().runner.execute(def, { record: row, objectName: CHILD }); the definition keeps objectName/recordId/_rowRecord for the api/flow/script handlers and the update path. Toolbar dispatches (no row) are unchanged, and executeForm still ignores action.objectName. Measured: the update, api, flow and script row actions read the row/object from the definition first, so they are unchanged. The url/navigation templates now bind the row instead of the parent record; this is the ruling's sanctioned change. Deviation: the bridge bypasses the provider's execute wrapper (loading/error/result state). No useAction() consumer reads those fields; recorded in the PR Acceptance notes.", "tests": "Head 3dafcd4657. Union vitest --maxWorkers=1 over 52 files (two new pins + every test naming RelatedRecordActionsBridge + core ActionRunner*.test.ts + packages/react/src/context/__tests__): 'Test Files 52 passed (52) / Tests 604 passed (604)', lock VERDICT command-exit 0. Closure build pnpm --filter '@object-ui/app-shell^...' build: VERDICT 0. type-check core+app-shell: VERDICT 0 (first run exit 2 on untyped vi.fn spies in the new core test; fixed in 3dafcd4657, rerun 0). eslint --no-inline-config on the 4 changed TS/TSX files: 0 errors; warnings equal base (core 12, app-shell 13); type-aware linting off (--print-config shows no parserOptions.project/projectService). Battery exit 0: phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, new-line-citations, check-hand-rolled-comment-mask, check-changeset-fixed, check-changeset-presence. check:spec-floors exit 1 (no-artifact; needs a whole-workspace build), recorded. Control-byte self-scan: no hits. Ablation (objectstack scripts/ablation-replace.mjs, source-aliased test so no dist): anchor 'runner.execute(def as ActionDef, rowContext)' x1 to x0, blob 84152195036a to c1ce37eefa57. Pin file 'Tests 2 failed | 2 passed (4)': (a) Received '/forms/task.edit' (the defect); the api control failed only on its context assertion (objectName project, record P-1) while its definition assertions held; child-record-page and page-level controls stayed green. Restore: blob after restore == HEAD blob 84152195036a, git diff HEAD empty. Declared to CI: full packages/core and packages/app-shell test dirs.", "mcp_calls": "0", "api_writes": "1 relay dispatch (fleet-write pr_create: POST /repos/objectstack-ai/objectui/pulls draft + POST /repos/objectstack-ai/objectui/issues/13073/assignees) + this os-dev-report comment through the relay; plus git pushes of the branch (4). No label writes (objectui: no labels named in the dispatch).", "open_questions": [], "out_of_scope_findings": [ "carrier: none. ActionProvider's execute takes one argument, so a per-call context bypasses its loading/error/result wrapper. Noted in PR Acceptance notes, not filed. Dedupe words: ActionProvider execute call context wrapper loading", "carrier: none. A type: form action at list_item fired from an object list's row (ObjectView, no record in the runner context) reads no record either, so it opens bare /forms/NAME by the same mechanism. Code read only, not reproduced at a public door, so not filed. Dedupe words: list_item form action row recordId ObjectGrid executeForm" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
eabce85cf175ea26258099839934f0a104531775Local-runs: none
Inputs: card #13063 (body, the triage comment, the claim as amended, the os-dev-report), PR #13073 (body, 5 files, net diff against
mainatfb85637e14), the check-runs on the head, and the head's source ofpackages/core/src/actions/ActionRunner.ts,packages/core/src/actions/index.ts,packages/app-shell/src/views/RelatedRecordActionsBridge.tsxandpackages/react/src/context/ActionContext.tsx(read only, through the GitHub contents API). The showcase'srecord_relatedplacements were read fromexamples/app-showcase/src/ui/actions/index.tsin the objectstack checkout.① Derived judgments
Public-surface change:
ActionRunner.execute(action: ActionDef)becomesexecute(action, callContext?: Partial of ActionContext).ActionRunnerreaches@object-ui/core's public face throughexport * from './ActionRunner.js'in the actions barrel, so this is a widening of a published method. No new export, no new file undersrc/other than tests. Right.A caller passing no
callContextis unchanged. The only inserted statement in the old path isif (callContext) return this.scopedTo(callContext).execute(action);ahead of the existingtry. With the argument absent the branch is not taken and the body that follows is byte-for-byte the previousexecute. The bridge passesundefinedwhen the row is not an object, which takes that same unchanged path. The provider's ownexecutewrapper still callsrunner.execute(action)with one argument. Right.The one-shot runner shares every handler the original holds. The class fields at head are
handlers,scripts,evaluator,context,confirmHandler,toastHandler,modalHandler,navigationHandler,paramCollectionHandler,resultDialogHandler,translate, and the readonlybuiltinExecutors.scopedToassigns the two Maps by reference and the six handlers plustranslateby value. Not copied:context(replaced by the merged one, by design),evaluator(rebuilt by the constructor over the merged context, which is the point: the evaluator must read the row), andbuiltinExecutors(a per-instance table of arrow functions bound tothis, so the fresh instance's own table dispatches to the fresh instance's executors; copying it would be wrong). Because the Maps are shared by reference, a handler registered on the original after the scoped runner is built is still seen. The constructor applieswithIdentityAliasto the merged bag, soos.useris re-derived fromuser; the test pins it.current_user, which the provider binds throughupdateContext, lives onthis.contextand so is carried by the spread. Nothing a dispatch path reads is missing. Right.Dropping
objectFields/objectActionson an object change is right. Every reader of those two keys at head isreadContextObjectFields/readContextObjectActions, which answer only whenreadContextObjectName(context) === objectName. On the shared runner a retargeted row action (action.objectName= child, context = parent) therefore already gotundefinedfor both. On the scoped runner the context now names the child, so without the drop the parent's field map and declarations would answer for the child; the drop restores the sameundefinedthe update path anddeclaredUpdatePatchsaw before. WhencallContextnames the same object (a self-relation) the pair is kept, which is the correct reading, and the test pins both branches. Thectxmirror gets the same treatment so${ctx.record.*}and${record.*}agree. Right.executeFormstill ignoresaction.objectName(objectui#4292). The method is untouched in the diff and at head readsrecordIdfromthis.context.record/this.context.dataand the object fromreadContextObjectName(this.context)only. The scoped runner'sthis.contextnames the row and the child, which is exactly the shape a dispatch from the child's own record page has. The ban holds. Right.update/api/flow/scriptunchanged; url and navigation templates now bind the row. Confirmed by reading the head:executeUpdateOperationtakesaction.objectName || readContextObjectName(...)and the row fromparams._rowRecord; the console'sapi/flow/scripthandlers readaction.objectName,action.recordId,params.recordIdand_rowRecordfrom the definition before any context, and the bridge still writes all of these on the definition (pinned by theapicontrol). The row runs with a context that now also names the row, which is additive for those handlers.navigateToevaluates${record.*}throughthis.evaluatorand${ctx.recordId}throughbuildInterpolationContext(this.context.record?.id ?? this.context.recordId, then thectxmirror merged on top); on the scoped runner both resolve to the row. The record page's provider context carriesrecord,objectName,objectFieldsand no flatrecordId, so the mirror does not re-introduce the parent's id. Safety for existing authoredrecord_relatedurl actions: the ruling (triage comment and the card's expected fix shape) sanctions the row as the execution context, so a url template at that location reading the parent was relying on the defect; the showcase declares exactly one action atrecord_related(showcase_log_time,type: form) and no url template there, so no shipped example changes meaning. The change is correct and bounded. One further consequence the dev did not list, same direction: the runner's ownexecuteAPIfallback (no registeredapihandler) builds its body fromaction.params ?? this.context.data, whichcallContextdoes not touch, so it is unchanged. Right.Bypassing
ActionProvider's execute wrapper is not a user-visible regression. The wrapper only sets the provider'sloading/error/resultstate and catches a throw; the loading indicator and the error or success toast a user sees come from the runner itself (handlePostExecutionthrough the sharedtoastHandler, the sharedconfirmHandler, the sharedparamCollectionHandler), all of which the scoped runner holds.runner.executehas its owntry/catchreturning{ success: false, error }, so the wrapper's catch adds nothing for this path. Measured at head: the only non-testuseAction()consumers that destructure anything beyondexecute/updateContext/runnerare none (useObjectActionsreadsloading/errorfromuseActionRunner, a separate hook);empty-state-link.tsxalready dispatches throughuseAction().runnerdirectly, so the shape has a precedent. The one observable difference is that the provider no longer re-renders its subtree for a related-row dispatch, which nothing depends on. Right; carried forward under ③ as the dev flagged it.Smaller points, all right: the
[runner]dependency replaces[execute]andrunneris memoised on the serialised context, sorunRowActionis as stable as before;'objectName' in callContextwith a base context lackingobjectNamereads as changed and drops a pair that is absent anyway; the fallbackuseAction()(no provider) builds a fresh runner each render, which was equally true of the oldexecuteand is not a regression.Tests: the app-shell pin mounts the real provider, bridge, renderer and related list and observes the navigation handler, with three controls (child record page, page-level parent action after a row dispatch, a non-form row action); the ablation reported in the PR body turns the defect case and the
apicontext assertion red and leaves the controls green. The core pin covers the merge, one-shot-ness, handler sharing, the alias, the mirror and the pair drop. No test is modified or deleted.Check-runs on
eabce85cf175:Changeset Bump Policy,Changeset Declaration,Changeset Claim Re-read,Changeset Fixed Group Check,Changeset Overwrite Report,Line Citation Gate,Governed Surface Queue Guard,labelandLive E2E (informational)are success;Test (coverage)and the coverage shards are skipped by design. Still in progress at the time of this record:Type Check,Lint,Repo Gates,Build & E2E,Build Docs,Bundle Analysis,Test (dist pins)and the sixteenTest (shard n/16)jobs. Their conclusions are the gate verdicts for those families; this record does not wait for them, and the PASS below is conditional on none of them concluding failure.② Semver level
.changeset/13063-related-row-form-action.mddeclares@object-ui/core: minorand@object-ui/app-shell: patch. Both packages havesrc/edits in the fixed group, socheck-changeset-presencerequires a declaration, and one is present (theChangeset Declarationrun is green).minoron core is right: a widened signature of a published method is an additive public-surface change, and the objectui#12967 precedent plus the AGENTS.md rule that objectui never declaresmajormakeminorthe ceiling as well as the floor.patchon app-shell is right: the bridge's change is a behaviour fix behind an unchanged component API.check-changeset-no-majoris green. The body names the widened method and states that the argument is optional, which matches the diff.Clause-②: yesin the PR body matches the amended claim and the fact that a published method's signature changed.③ Boundary flags
The os-dev-report's
open_questionsis empty. Dev flags and out-of-scope findings:loading/error/resultnot set for related-row dispatches), carrier none. Judged under ① as not a regression; the suggested follow-up (an optional second argument on the provider'sexecute) belongs to@object-ui/react, outside this card's file surface, and needs no card to land this fix. Answered.formpin covers the same context path, which is acceptable at this tier. Answered.check:spec-floorsexit 1no-artifactlocally (needs a whole-workspace build). That family is answered by the head'sRepo Gates/Build & E2Eruns, still in progress above. Answered as far as this record can.type: formaction atlist_itemfired from an object list row (no record in the runner context) opens bare/forms/NAMEby the same mechanism; code-read only, not reproduced, not filed. Not this card's defect and not introduced here; the newcallContextis the natural carrier for that fix. Escalated to the seat as a candidate card, no action required on this PR.3dafcd4657, two commits before the reviewed headeabce85cf1; the diff reviewed here is the net diff at the reviewed head, and the check-runs quoted are on it.Implemented-by:
claude/issue-13063-related-row-form-actionReviewed-by:
session_01XVp19E1MYUTFM3dnkwz8gBVERDICT: PASS
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT · objectui#13063 · PR #13073 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T19:13ZPR #13073 at head
eabce85(Fixes #13063,Clause-②: yes). Reviewed against:the claim
6274196031, amended to Clause-② yes becauseActionRunneris published;triage
6273652415;the dev report
6274878854plus the changeset round;the contract review PASS
6275047442.Premise: holds.
RelatedRecordActionsBridgeretargeted a row action through the definition only, whileexecuteFormreads the record and object from the runner context (the parent page). So "Log Time" on a related row opened a blank create form.Diff read by the seat:
ActionRunner.execute(action, callContext?): with acallContext, it runs once on a scoped runner over the merged context. That runner shares every handler and registry by reference. The ctx mirror follows the merge, andobjectFields/objectActionsare dropped when the object changes.callContext,executeis byte-for-byte unchanged.{ record: row, objectName: CHILD }. The definition keepsobjectName/recordId/_rowRecord, so theupdate,api,flowandscripthandlers are unchanged.executeFormstill ignoresaction.objectName(objectui#4292).record_relatednow bind the row. That is the ruling's sanctioned change, and no shipped example reads the parent there.Changeset:
@object-ui/coreminor(a widened published method; objectui#12967 precedent) and@object-ui/app-shellpatch.Pins:
RelatedRecordActionsBridge.rowFormContext-13063(real provider, bridge and related list; controls: the child record page, a page-level parent action after a row dispatch, anapirow action).ActionRunner.callContext-13063(merge, one-shot, handler sharing, alias, mirror, pair drop)./forms/task.edit. Restored by blob hash.Local gates: a 52-file union, 604 tests; closure build and type-check for core and app-shell;
eslint0 errors; the battery and changeset checks all 0.Acceptance notes (not filed):
ActionProvider's loading / error / result wrapper. NouseAction()consumer reads it, and toasts, confirm and params come from the shared runner (judged no regression).type: formaction atlist_itemfired from an object-list row has the same blank-form mechanism. That is code-read only, with no public-door reproduction, so it is not filed; the newcallContextis its natural carrier.Commits: model-free trailers. The PR went through the fleet-write relay.
CI at head: 36/36 check-runs success or skipped on
eabce85(filter=latest),mergeable_stateclean.Verdict: ACCEPT. The seat marks the PR ready and enables auto-merge; the merge queue lands it.
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed · objectui#13063 · PR #13073 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T19:33Z9f1dd79onmain. Verified by content: all 5 files in9f1dd79^1..9f1dd79are blob-identical to the accepted headeabce85. The squash message is model-free.type: formaction atrecord_relatedfired from a related-list row now opens that row's edit form (/forms/NAME?recordId=ROW&recordObject=CHILD).ActionRunner.execute(action, callContext?)(@object-ui/coreminor).6275047442.9f1dd79.Fixes #13063;pm:dispatchedis removed with this note.Generated by Claude Code
fix(app-shell): a related-list row form action opens the clicked row,…