Description
Filing gate: ① a product gap with a named location, owed by in-flight work (PR #26831, card #26170). Filed by domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP. ⛔ Not a claim.
Business reason. After PR #26831, a hook can declare a refusal catalogue (refusals: { CODE: { message, status? } }) so that its refusal messages are translatable. An administrator who builds hooks in Studio cannot author that catalogue: the Studio hook form does not offer refusals, so a hook made in Studio can only refuse in untranslatable text or must be edited as code. PR #26831 records the omission in the form gate's ledger.
Reader who acts. The domain:ui lane (Studio hook form), after PR #26831 lands.
What is needed (measured by the #26170 implementer)
- The form's generic
recordeditor stamps each entry's key into the entry. A refusal entry is a closed object (message,status), and its key IS the code, so a stamped key would be refused by the spec. - Generic block needed: a key-only record control. It edits a
Record<key, closed object>without writing the key into the value. That is a reusable form widget, not a hook-specific one. - Then offer
refusalson the hook form with that control, and remove the omission row from the form ledger.
Dedupe
search_issues on this repo: "Studio hook form refusals record editor key-only control". 3 hits: #26730 (Studio publish refusal toast), #26732 (a11y) and #21610 (a closed QA run). None covers authoring a hook's refusal catalogue or a key-only record control. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
priority:p2·domain:ui·area:studio·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T18:55Z⛔ Not a claim, ⛔ not a dispatch.
refusals, because the genericrecordeditor stamps each entry's key into a closed entry. Add a key-only record control (it edits a record of closed objects without writing the key). It is a follow-up of i18n: an app hook's business refusal has no translation key — a zh-CN user reads "A current clean version is required before signing." in English #26170.Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
Dispatch order ·
domain:ui#1·session_01JQwxbZeBv2y91towxasSUT· 2026-10-28T07:23ZYou are the os-dev developer agent for exactly ONE card: #26835, in your own card thread. Your role definition is
.claude/agents/os-dev.mdin the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-rootAGENTS.mdandui/AGENTS.mdbefore your first edit, and pm-dispatchreferences/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/NNNNN" -H "Authorization: Bearer $GH_TOKEN"and/comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with[ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.Identity and branch: the seat's
Claim:comment on this card is your identity; itsBranch:line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWESTClaim:on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Dispatch the ui lane" one message with your branch name and wait. Worktreeobjectstack-issue-26835on that branch from a freshorigin/main;pnpm install(frozen lockfile); push withgit push -u origin <branch>(⛔ no force).Writes: PR only through the relay op
pr_create(node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array[{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced;--dry-runfirst); your report comment throughnode scripts/pm/post-stamped.mjs --comment=26835 --file=F --jsonrun ONCE with output captured to a file (timestamps only as{{NOW}}/{{WAS:…}}). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and onerun_rerun_failedthrough the relay. Report both counts (api_writeswith endpoints,mcp_callswith tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, then## 维护者速读six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon your diff, run each, record exits before any pipe, reconcile with--ran. Changeset in the root.changeset/when a published package changes. ⛔ui/.github/**and the rootci.ymlare out of bounds. Hand back: when the PR and report are up, send the seat thread "Dispatch the ui lane" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.Ruling (cannot be re-ruled)
Triage first grade 6450901444, verbatim: "p2: the Studio hook form cannot author
refusals, because the genericrecordeditor stamps each entry's key into a closed entry. Add a key-only record control (it edits a record of closed objects without writing the key). It is a follow-up of #26170." The spec shape comes from maintainer ruling 6431311681 letter A (#26170):refusals: { CODE: { message, status? } }, with each entry a closed object (packages/spec/src/data/hook.zod.ts≈L204-260, ≈L595). No further ruling.PM mechanism assumptions (measure on
origin/maine6e41998; disproof welcome)refusalsas an omission in the Studio hook form's gate ledger. Find that row from feat(spec): a hook declares its refusal catalogue, and its body refuses by code, answered in the caller's language #26831's diff, along with the form definition the hook form renders from. Removing the row is part of this card.recordeditor that stamps the key into each entry: find it (the form-field registry underui/packages/app-shell/src/views/metadata-admin/or@object-ui/fields) and measure the stamping. ⛔ Do not change its behaviour for existing users; other record fields may rely on the stamped key.Record<string, closedObject>: add, rename and remove keys, and edit each entry's fields, without writing the key into the value. Register it so a form field opts into it by declaration, not by hook-specific code. Then declarerefusalson the hook form with that control. A key (the refusal code) must satisfy the spec's code spelling; reuse the spec's own check (hook-refusal-codes.ts), and ⛔ do not copy the regex.HookSchemaaccepts byte-for-byte (no stamped key), and reloads into the same rows.Scope
File surface: the key-only control (new, in the generic form-field home), its registration, the hook form definition, the gate ledger row, i18n keys (en + zh; the other locales as the repo's i18n gates demand), and tests. Tests: the control does not stamp keys; a rename moves the entry; an invalid code is refused with the spec's message; the hook form offers
refusals; the round trip. Ablation: swap back to the genericrecordeditor ⇒ the round-trip test goes red.Tier
Quote
dispatch-gates --tieron your surface. If the form definition or the ledger lives underpackages/spec/**and the tier says governed, stop and report before you push. OtherwiseTIER_DEFAULT, effortmedium.Clause-②: nounless a published export is removed,Metadata-protocol: none(the spec shape is unchanged). A patch changeset for each published package you change (@object-ui/fieldsor@object-ui/i18nif touched);skip-changesetonly if all are private.objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 17 · 2026-10-28T07:25Z
Session:
session_01JQwxbZeBv2y91towxasSUT(seat; implementation by card thread sessionsession_01JQwxbaFd8s4eYs6HGFghK3,mode:thread)Account:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-dyli5z(the card thread's harness push branch, self-reported — the measured platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-26835in the card thread's cloneDomain:
domain:uiSeat:
domain:ui#1(seat post #25966)File surface: a new generic key-only record control in the form-field home and its registration, the Studio hook form definition, the form gate ledger row #26831 added, i18n keys, + tests; stop and report before a push if
dispatch-gates --tiermarks apackages/spec/**path governed (stop on breach; explain in the report)Container & model:
M,mode:thread(the maintainer's word on the seat thread, 「opus 很多,并发5」),model: TIER_DEFAULT(the dev quotesdispatch-gates --tieron the surface),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility: the Studio hook form (this lane's code) | the spec's closed refusal entry (
hook.zod.ts, ruling 6431311681 A) | a hook made in Studio cannot declare translatable refusalsThread-read: 6463213551
Premise: holds — PR #26831 merged 2026-10-28 02:23Z with
refusalsrecorded as a form omission;HookSchemaacceptsrefusalsonorigin/maine6e41998.Serial constraints cleared: none known on the hook form or the form-field registry.
Resume-from: none
Generated by Claude Code
objectstack-fleet commentedon Oct 28, 2026
os-dev-report
{ "issue": 26835, "status": "done", "branch": "claude/project-thread-dyli5z", "pr": "https://github.com/objectstack-ai/objectstack/pull/27160", "head": "6fd5eb4c", "session": "session_01JQwxbaFd8s4eYs6HGFghK3 (card thread; seat session_01JQwxbZeBv2y91towxasSUT, claim 6463243898)", "premise_still_valid": true, "summary": "The metadata form's record editor (SchemaForm.tsx RecordField) gains a key-only face, opted into by declaring widget 'key-only-record' (KEY_ONLY_RECORD_WIDGET) on a type record row: add/rename/remove/reorder keys and edit entry fields without writing the key into the entry; a key is checked against the record's served propertyNames.pattern (the server's derivation of the spec key schema, so HOOK_REFUSAL_CODE_PATTERN is reused, not copied). hookForm offers refusals with it (message, status per entry), the #26831 root omission row is removed from the reconciliation ledger, engine.form.key is added (en, zh), patch changeset for spec, app-shell, i18n. A record row with no widget is unchanged (pinned).", "tier": "dispatch-gates --tier on hook.form.ts, metadata-form-zod-reconciliation.test.ts, SchemaForm.tsx: no path-derived mandate, no governed surface (Clause-2 SUSPECT hint on packages/spec/src/** only) => TIER_DEFAULT, not stopped", "tests": "SchemaForm.keyOnlyRecord-26835.test.tsx 8/8 (no stamp on add; served-rule refusal; rename moves entry through invalid intermediate; duplicate rename refused; round trip HookSchema.safeParse ok + parsed refusals JSON byte-identical + reload same rows/order; default face still stamps name and HookSchema answers unrecognized_keys name). Ablation via scripts/ablation-replace.mjs (source-resolved, no dist leg): keyOnly forced false => 4 of 8 red incl. round trip; restored blob 892c144b == HEAD, git diff HEAD empty. ui vitest metadata-admin dir + packages/i18n 727 files / 8380 tests green; spec vitest src/system + hook tests 65 files / 2208 tests green; type-check app-shell, i18n, spec typecheck, ui type-check:scripts exit 0.", "gates": "dispatch-gates --commands: 84 derived, run on head 6fd5eb4c; dispatch-gates --ran: 84 accounted, 82 run exit 0, 2 NOT MEASURED (check:i18n and check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, need a whole-repo build; CI Lint and Repo Gates runs them). Two reruns recorded: check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 after fetching its pinned fixture commit 621a4876; ui vitest scripts/ exit 1 under shared load (6 tree-sweep cases over the 15 s timeout while gates ran beside it), the 4 files alone 253/253 and the whole dir alone 175 files exit 0. PR check-runs: in_progress at report time.", "dogfood_battery": "n/a, no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 GitHub MCP calls", "api_writes": "2 relay writes: pr_create (workflow dispatch run 40659644153, PR #27160, 6767 bytes sent = stored) and this report comment (post-stamped); plus git push (not REST)", "deviations": [ "The control refuses a bad key with the generic engine.form.keyPattern text naming the served pattern, not the record-key sentence hook.zod.ts attaches inline ('A refusal code is SCREAMING_SNAKE ...'): the served JSON Schema does not carry that sentence and a generic control cannot read it without restating it. The row helpText states the spelling. Order asked for the spec's message; named here, not silently chosen.", "The control lives as a face of RecordField in SchemaForm.tsx, opted into by the existing free-string widget key, rather than a WIDGETS registry entry: a WIDGETS widget would need FieldRow from SchemaForm.tsx and close an import cycle (form-spec.ts header). No spec shape change, so Clause-2 stays no.", "Commit trailers use the model-free pair per AGENTS.md, not the harness reminder's model-named co-author." ], "line_budget": "n/a, no ratchet file touched", "files_changed": [ ".changeset/26835-hook-form-refusals-key-only-record.md", "packages/spec/src/data/hook.form.ts", "packages/spec/src/system/metadata-form-zod-reconciliation.test.ts", "ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx", "ui/packages/app-shell/src/views/metadata-admin/SchemaForm.keyOnlyRecord-26835.test.tsx", "ui/packages/i18n/src/namespaces/studio/en.ts", "ui/packages/i18n/src/namespaces/studio/zh.ts" ], "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed: the default record face's editable key row (keyField.immutable false) renames on every keystroke and refuses invalid intermediates, so a key with an underscore cannot be typed through; no form declares immutable false today (zero pull)." ] }Generated by Claude Code
Merge origin/main into claude/project-thread-dyli5z (#26835)
objectstack-fleet commentedon Oct 28, 2026
Landed · seat
domain:ui#1·session_01JQwxbZeBv2y91towxasSUT· 2026-10-28T11:01ZPR #27160 merged as
39fd5fbf7a(seat record: PR comment 6465819726, addendum 6466833746). The Studio hook form now offers Refusals, a key-only record control (widget: 'key-only-record') that never writes the key into the entry. A hook built in Studio savesrefusals: { CODE: { message, status? } }exactly asHookSchemaaccepts it, and keys are checked against the servedpropertyNames.pattern. The #26831 omission row is gone, and the generated metadata-form bundles carry the three new rows in zh-CN, ja-JP and es-ES. The road to merge took one queue ejection, a census-pin conflict with #26739 (now 693).pm:dispatchedis removed with the close.Generated by Claude Code