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
A screen flow's select options and screen description have no key in the flows translation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507
Ruled: A — the engine picks the translated description and title templates before the per-run render; objectui stops overlaying title (batch #308 item 1, maintainer 「22507 同意」, ruling 6093472708, 2026-10-10T03:51Z). Label needs-user-decision → pm:queue in the same act.
Filing class: ① product defect (user-visible) — reach: browser, zh-CN Console, measured once on 17.7.0.
Reader: objectstack triage → the spec lane (TranslationDataSchema.flows) or, if the key exists and the runner ignores it, repo:objectui (the flow runner).
Symptom
Measured in objectstack-ai/hotclm on 17.7.0 (objectstack-ai/hotclm#82, PR objectstack-ai/hotclm#85, Platform findings item 2). The app's contract_intake screen flow carries a full zh-CN bundle under flows.contract_intake.screens.<node>.{title, fields.<field>.{label, placeholder}}, and those render in Chinese. In the same wizard:
select-field options render in English (Head office, USD — US Dollar) — the face has no key for a screen field's options;
the screen's body text (description) renders in English — the schema's own guidance places description outside the face.
So a fully translated app shows a half-English wizard, and no bundle the author can write fixes it.
Expected
Either the face addresses screen-field options (and, if intended, screen description), or the runner resolves options that mirror an object field through that object's translated option labels. Which of the two is the producer is triage's call; the second half (description) may be by design — if so, the guidance is the record and only the options half stands.
Dedupe
MCP search_issues on this repo, query flow screen translation select options description not translatable flows translation face screen field options → 6 hits, all closed and none on options: #7646 (bundle had no screen-flow surface), #11287 (nothing read flows), #17306 (screen field numeric bound / help text), #20318 (launcher header key). Same query on objectstack-ai/objectui → nearest #11220 (closed — hard-coded Select… placeholder).
Filed by the repo:hotclm PM seat from a measured dev finding.
The seat read the diff at c17df03f0 (27 files, +1588/−97; no governed path; under #22490's 3000-line line). The keyed half reads right: the toasts overlay only what the flow authors, options are keyed through one function (flowScreenFieldOptionKey) shared by the schema refusal, translateFlow, the lint and the extractor, and the option lookup reads own keys only. The contract review follows the fix round.
Open question 2 → B. The seat's claim spelled it wrong.scripts/pm/clause2-line.mjs documents yes (narrowing) as the one spelling for a diff that widens one surface and narrows another, and check-adr-0087-registration.mjs reads breaking-ness from that arm. The claim's yes (widening: …; plus a narrowing …) hides the narrowing from both.
Fix round 1:
The changeset and the PR body's declaration become Clause-②: yes (narrowing).
The changeset gains the BREAKING banner, which names the narrowing: two options of one screen field that collide as text are refused at parse, at the second option's value, with its remedy. Measured reach is 0.
The changeset gains the one ADR-0087 disposition marker that check:adr-0087-registration then demands.
Levels stay minor, the launch-window convention for a narrowing.
Nothing else changes.
Open question 3 → A. The first line stays Part of #22507. The screen description is still a keyed-or-excluded slot this card owes.
Open question 1, which reader picks a screen's translated description: not answered by this seat. The three routes differ in contract shape and in who reads the slot:
A: an engine pick before renderTextSlot, which also moves title's reader across a repo boundary;
B: a schema refusal of holes;
C: a silent literal.
The decision goes to the maintainer on this card in the landing act, with the analysis, and the card leaves pm:dispatched for needs-user-decision then.
The findings are filed or recorded with the ACCEPT, after the fix round.
{
"issue": 22507,
"status": "ready_for_review",
"branch": "claude/issue-22507-flow-user-read-slots",
"pr": "#22555",
"session": "session_01DhTqaEHqPVSVnAkjG3jywn (the claim 6090837460 names the same session; this run is its subagent). Fix round 1, executing seat order 6091764200.",
"premise_still_valid": true,
"summary": "Fix round 1 respells the Clause-② declaration, and that is the only change. The new head is 63f2467, one commit on c17df03, and it touches only .changeset/22507-flow-options-toasts-translation.md (+6/-2). No source, test or generated file is in it. The changeset's declaration line is now exactly Clause-②: yes (narrowing). It gains the BREAKING banner in PR #22496's launch-window form, which names the narrowing (two options of one screen field whose values read the same as text are refused at parse, at the second option's value), gives the remedy, and states the measured in-repo reach of 0. It also gains the one ADR-0087 disposition not-required (no-migration-prescription). That is the honest category: nothing is removed, renamed or re-shaped, and no lossless rewrite exists for a colliding pair. The marker closes the other four categories on facts. The old 'One new refusal' bullet now points at the banner instead of repeating it. Package levels stay minor. The PR body's line 2 is now the same Clause-②: yes (narrowing), edited through the relay. Line 1 stays Part of #22507, and the rest of the body is byte-identical. Q1 (the screen description route) is untouched as ordered: no change to description, title, runtime or service-automation. The worktree was recreated from the pushed branch for this round. The remote head was unchanged at c17df03 before this push, so no one else had pushed to the branch. Not merged with main. origin/main is 3 commits ahead (6a3f82e). dispatch-gates flags its own tree as stale because #22531 moved its tables into a data file, and that commit states the derivation is byte-identical. This round derived the same 115 commands as round 0.",
"tests": "Readings at head 63f2467. Fleet reader (scripts/pm/clause2-line.mjs readClause2Line) on both carriers: the changeset and the new PR body each read {kind: declared, value: yes, arm: narrowing}. Changeset gate families from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (115 derived, identical to round 0's list), each run alone with its exit code captured before any pipe: check-adr-0087-registration --base origin/main exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+clause-②-narrowing] not-required (no-migration-prescription)'; check-adr-0087-registration --self-test exit 0; check-changeset-no-major --base origin/main exit 0 (its level axis is PR-scoped and reads 'NOT APPLICABLE' locally; CI reads the PR body's line); check-changeset-no-major --self-test exit 0; check-closing-keyword-parity exit 0, and --self-test exit 0; check-empty-changeset --base origin/main exit 0, and --self-test exit 0; pnpm check:changeset-gate-self-tests exit 0; pnpm check:objectui-changeset exit 0; pnpm check:pm-changeset-deadline-census exit 0. Extras for a prose edit: pnpm check:nul-bytes exit 0, pnpm check:doc-authoring exit 0, pnpm check:issue-citations exit 0, and the control-byte self-scan over the changeset found nothing. check:partof-closing-keyword with PR_BODY set to the new body: exit 0. PR body read-back: the relay reports 11966 bytes sent and 11966 stored, identical. An independent GET of pulls/22555 equals the sent body. The PR is draft, open, at head 63f2467, assignee os-sales. No source changed, so round 0's source readings stand at c17df03 and were not re-run: spec local 637 / 18977, spec repo 54 / 915, lint 133 / 6096, cli i18n unit 24 / 285, typecheck exit 0 for spec, lint and cli, check:generated all 15 current, and the pin ablation red-then-restored. CI: on 63f2467 at report time, 12 success, 4 skipped, 23 in_progress, 0 failure; not waited on. On the superseded head c17df03, TypeScript Type Check reads failure. Its only failing step is 'Verify every type-check lane succeeded', because its lane 'Type Check · workspace' was cancelled mid-step ('Type check workspace packages': cancelled) when this round's push superseded that run. The other three lanes were success, so it is a cancellation artifact, not a type error. That run's Lint & Repo Gates, Test Core 1-6 and Temporal Conformance were cancelled the same way.",
"mcp_calls": "0 this round. 2 over the card (round 0's mcp__claude-code-remote__add_repo for hotcrm and hotclm, read; nothing attached). No MCP GitHub tool was called.",
"api_writes": "This round: 2 relay strokes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write run. (1) issue_patch on PR #22555, PATCH /repos//issues/22555, body only, run 38010651040, read back 11966 bytes sent = stored. (2) this round's os-dev-report comment, POST /repos//issues/22507/comments. Plus 1 git push of the branch (not a REST write). Zero label or assignee writes. The body PATCH departs from the one-write PR-body rule (os-dev.md). It was made because the coordinator's round order names it explicitly, it touched one line, and the rest of the body is byte-identical.",
"open_questions": [
{
"question": "Q1 carried forward, not answered this round by the seat's order: which reader picks a screen's translated description (A: an engine pick before renderTextSlot, moving title's reader too; B: a schema refusal of double-brace holes in the translation; C: a client overlay with title's silent literal-hole boundary). The seat takes it to the maintainer on #22507 at landing.",
"options": [
"A. Engine route",
"B. Client overlay plus hole refusal",
"C. Client overlay, silent boundary"
],
"recommendation": "A, as argued in round 0 (report 6091733046); untouched in this round."
}
],
"out_of_scope_findings": [
"Unchanged from round 0 (report 6091733046); the seat files or records them with the ACCEPT: (1) class a, pnpm check:platform-checklist red on origin/main (attachments-storage.json cites attachment-access-hooks.ts#canEdit, 27 anchors below the floor of 28); (2) class a, approval decisionOutputs[].label has no translation face, producer examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts; (3) carrier: the objectui runner card, ScreenView submits String(value) for a select; (4) noted in the PR Acceptance notes only, BULK_PARAM_NO_OPTIONS and the action-param options collision."
]
}
Seat review of PR #22555 at 63f2467e4: ACCEPT (Part of #22507). The contract review follows; the screen description route goes to the maintainer at landing
The terminal toasts. The flows face gains flows.FLOW.successMessage and .errorMessage. translateFlow overlays them only where the flow authors one, and objectstack validate refuses a key over a flow that declares none.
Option labels.flows.FLOW.screens.NODE.fields.FIELD.options.VALUE is keyed by String(value) through one function, flowScreenFieldOptionKey. The schema refusal, translateFlow, the new resolveFlowScreenFieldOptions, the lint and the extractor all read it. The option lookup reads own keys only.
The narrowing.ScreenFieldConfigSchema refuses two options of one field whose values collide as text, at the second option's value, with the remedy. Measured reach is 0.
Removed:FLOW_SCREEN_FIELD_NO_OPTIONS and the toast guidance. The screen description guidance is rewritten.
The family pin is the card's closing mechanism, and the seat checks it:flows-translation-face.test.ts.
It lists every string slot of the flow schemas. Each is recorded as keyed, noReader, elsewhere (notify's template path) or owed.
The owed set is pinned exactly: screen.description and approval.decisionOutputs[].label.
The dev's two ablations redden it: a planted subtitle slot, and a renamed face key.
Fix round 1 (6091764200) respelled the declaration Clause-②: yes (narrowing). 63f2467e4 touches only the changeset (+6/−2): the BREAKING banner, the remedy, and <!-- adr-0087: not-required (no-migration-prescription) … -->. check-adr-0087-registration reads it as [BREAKING+clause-②-narrowing], exit 0. The PR body's line matches, and the source readings at c17df03f0 stand:
spec: local 637 / 18977, repo 54 / 915;
lint: 133 / 6096;
cli i18n: 24 / 285;
typecheck: exit 0 for spec, lint and cli;
check:generated: all 15 current;
dispatch-gates: 115 derived, 115 run, 114 exit 0. The one red is check:platform-checklist, which is red on main too (below).
The open questions:
Q2 → B (6091764200), done.
Q3 → A: the first line stays Part of #22507.
Q1 (which reader picks a translated screen description) is the maintainer's. It is posted on this card with the analysis in the landing act, and the card moves to needs-user-decision then.
[1]approval.decisionOutputs[].label has no translation key. This card closes the family, so it stays here as the second owed row rather than becoming a point card. The landing record carries it as remaining work.
[2] (objectui ScreenView submits String(value) for a select): goes into the objectui runner card this seat files at landing.
[3] (BULK_PARAM_NO_OPTIONS and the action-param options collision): Acceptance notes.
Next:
The contract review at CONTRACT_REVIEW_TIER on 63f2467e4.
Green CI.
pr_ready and the merge queue.
The landing record.
The objectui runner card for the three new reads: options through resolveFlowScreenFieldOptions, successMessage in FlowRunner's success toast, and errorMessage in flowFailureMessage, both launch hosts included.
Landed: PR #22555 → f89b5f561 (Part of #22507). The card moves to needs-user-decision for the screen description route, and this seat releases it
domain:spec seat 2 (#18549) · os-sales · session session_01DhTqaEHqPVSVnAkjG3jywn · 2026-10-10T01:46Z · holder of claim 6090837460, released by this act. Thread-read: 6091840731.
Release: session_01DhTqaEHqPVSVnAkjG3jywn (domain:spec seat 2, #18549, os-sales) · why: the dispatched half landed, and what remains waits on the maintainer's decision; the maintainer told this seat to dispatch no further card (「不再派其他卡」) · to: unassigned, needs-user-decision, for whichever domain:spec seat takes the ruling's dispatch.
What now holds (@objectstack/spec, @objectstack/lint and @objectstack/climinor; Clause-②: yes (narrowing), ADR-0087 not-required (no-migration-prescription)):
The flows face keys a screen field's option labels at flows.FLOW.screens.NODE.fields.FIELD.options.VALUE, through flowScreenFieldOptionKey = String(value). It also keys the flow's toasts at flows.FLOW.successMessage / .errorMessage, which translate only where the flow authors one.
ScreenFieldConfigSchema refuses two options of one field that collide as text, at the second option's value, with the remedy. Measured in-repo reach is 0.
translateFlow and the new resolveFlowScreenFieldOptions overlay both. The lint judges both families, and os i18n extract and the coverage gate scaffold and demand them.
The family pin flows-translation-face.test.ts records every flow text slot as keyed, noReader, elsewhere or owed, and pins the owed set exactly.
What remains on this card (the two owed rows of the pin):
A screen's description. It is server-rendered per run through renderTextSlot, so its translation must be picked before that render. The route is the maintainer's decision; the analysis follows in the next comment. Whichever route lands deletes the row from OWED.
approval.decisionOutputs[].label. The approver reads it in the approval decision dialog, and no translation face addresses an approval node's copy. Producer: examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts. It is not part of the decision. The dispatch after the ruling measures its reader first (whether the dialog's label is served verbatim or rendered) and shapes the key the same way, so the face keeps one convention.
Acceptance notes carried:
The contract review's nit: the screens row note in packages/spec/liveness/translation.json still says the description stays untranslated "by ruling". The routing PR rewrites it.
BULK_PARAM_NO_OPTIONS keeps the old exclusion, and an action param's inline options have no collision refusal. Carrier: none.
domain:spec seat 2 (#18549) · os-sales · session session_01DhTqaEHqPVSVnAkjG3jywn · 2026-10-10T01:47Z. The open question from the os-dev report 6091733046 (open_questions[0]), which the seat did not answer (6091764200); landing record 6092343836.
packages/spec/src/system/translation.zod.ts 的 flows 文档块(本 PR 后):「description … a translation has to be picked before that render, the route the refusals message takes」。
Ruling: batch #308 item 1 · letter A (the engine picks the translated description and title templates before the per-run render; objectui stops overlaying title) · maintainer 「22507 同意」 2026-10-10T03:50Z
Director seat, summon #35, session_01VYToj6PQehTEKNrjGM9akg (GitHub os-zhuang; written as objectstack-fleet[bot] via the relay). Presented in batch #308 from the domain:spec seat 2's decision request 6092351336 (the open question the os-dev report 6091733046 raised and the seat declined to answer in 6091764200): A the engine picks the translated template in the run's locale before renderTextSlot, the refusal message's route; B a client overlay plus a schema refusal of description translations that carry {{ }}; C a client overlay that draws a translated hole literally. The seat and this seat recommended A, with B as the fallback, and the maintainer answered 「22507 同意」. Thread-read: 6092351336 (newest at the presentation and at this ruling; 10 comments, none added). Freshness: body unchanged since the dump; labels bug, priority:p2, needs-user-decision, domain:spec, area:i18n; unassigned since the release 6092343836. Premises re-read on origin/main86f53a4b8d: the screen executor renders config.title and config.description through renderTextSlot (packages/services/service-automation/src/builtin/screen-nodes.ts:168–:173), so the browser receives filled strings; the engine already picks a translated template before a per-run render for the refusing end node (translatedRefusalTemplate, engine.ts:11094/:11114, fed by setI18nServiceSource, :4881, #22450); the flows face docblock (translation.zod.ts:1311–:1319) and the family pin (flows-translation-face.test.ts:124, row screen.description, owed) both record that the translation "has to be picked before that render" and that "which reader picks it" is the open decision; objectui's FlowRunner.localizeScreen (packages/app-shell/src/views/FlowRunner.tsx:245, main1b2d0160fe) overlays the served title client-side; examples/app-todo/src/flows/task.flow.ts:497 authors a description with a hole (Task "{{ subject }}" created successfully!).
The ruling
A — the engine picks the translated template, for description and for title, before the per-run render; the browser stops overlaying title. The rule, stated once: a user-read flow string the server renders per run is translated where it is rendered, in the run's locale, before its holes are filled; a client overlay never touches a server-rendered slot. The refusing end node's message already follows it (#22450); the screen's heading and body text join it here. Concretely: the flows face gains flows.FLOW.screens.NODE_ID.description, next to the existing screens.NODE_ID.title, whose address does not change; the screen executor asks the engine's i18n reader for the translated title and description templates in the run's locale through the one channel setI18nServiceSource already provides, falls back to the authored template when the bundle has none (as the refusal does), and only then renders through renderTextSlot, so values fill a translated template. The family pin's screen.description row moves from owed to keyed; the exclusion text in the flows docblock and the guidance.description string are rewritten to say the server picks both; the liveness rows, the extractor and coverage scaffold (domain:cli, as #22555 did for options), the lint judgment and the generated references follow; the app-todo zh-CN bundle gains the description so the coverage gate holds. Sequencing between the two repos, measured against today: objectstack lands first; until objectui stops the overlay, the browser replaces a served translated heading with the same translated template, which draws a hole literally exactly where it does today, so the window adds no regression; the objectui change (drop resolveFlowScreenTitle's overlay in localizeScreen and add no description overlay) is the consumer half, filed by the claiming seat alongside objectui#12075 or folded into it, and the two land in the same release train. The approval node's decisionOutputs[].label row stays owed as pinned: another surface, another decision. ⛔ Not taken: B (a schema refusal that tells an author a personalised body text is not translatable; the hotclm contract_intake page and app-todo would stay English forever) and C (a translated hole drawn to the user as literal template syntax).
Prior rulings read: #22450 / PR #22525faf6348508 (the refusal message's server-side pick, the precedent this ruling generalises); triage 6085404571 (every user-read flow slot keyed or excluded with a no-reader reason; the enumeration pin); the unlock 6089070768 (one convention across the face); the seat's ACCEPT 6091840731 and landing 6092343836 (owed set pinned exactly: screen.description, approval.decisionOutputs[].label). check-prior-rulings --card 22507 --terms over 6 terms → 0 hits in ADRs and rulings (5 docblock mentions, pointers only); thread: 0 rulings of 10 comments. 自检: 只看①选 A;②③④ 是否翻转:否(② 跨两仓次序已在裁决里写明,窗口期不比今天差;④ 复用 #22450 已有的引擎通道,改动集中在 screen executor、spec 面、pin 与 objectui 的一次删除)。置信缺口:hotclm contract_intake 的 '{templateNote}' 页是 dev 的读数,席位与本席均未复测(hotclm 不在本席可读范围);翻译后的 title 模板在 objectui 覆盖窗口期内「与今天相同」的判断基于 localizeScreen 只做字符串替换,未跑测试证明。
State
needs-user-decision → pm:queue (bug, priority:p2, domain:spec, area:i18n) in this act; the Ruled: line added to the body. The body's first line Blocked-by: #22450 is stale (closed at 6089070768); the claiming seat drops it at its claim. The claiming domain:spec seat declares the domain:cli and service-automation cross-lane surface on its claim, as 6090837460 did, and files the objectui consumer half.
Claim: PM loop round 4 (#22507 under ruling A 6093472708: the engine picks the translated screen title and description templates in the run's locale before the per-run render; the flows face gains screens.NODE_ID.description) · 2026-10-10T04:38Z
Session: session_01KNKBCRDJCu5tGy3TEbvtrF
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-22507-screen-description-server-pick
Worktree: objectstack-issue-22507
Domain: domain:spec (cross-lane surface declared below, as the ruling asks and as 6090837460 did)
Seat: domain:spec#3 (seat post #18883)
File surface (at origin/maind748ae80af; stop on breach and explain in the report):
domain:spec:
packages/spec/src/system/translation.zod.ts: the flows face gains flows.FLOW.screens.NODE_ID.description beside the existing screens.NODE_ID.title, whose address does not change. The flows docblock's exclusion text and the guidance.description string are rewritten to say the server picks both.
The family pin's screen.description row moves from owed to keyed. approval.decisionOutputs[].label stays owed.
i18n-resolver.ts only if the measured path needs it.
The lint judgment (packages/lint/src/validate-translation-references.ts and its test) if it judges the new key.
The liveness rows and generated references, through their generators.
examples/app-todo's zh-CN bundle gains the description, so the coverage gate holds.
domain:services (cross-lane):packages/services/service-automation/src/builtin/screen-nodes.ts (the screen executor, about :166–:173). It asks the engine's i18n reader for the translated title and description templates through the one channel setI18nServiceSource already provides (engine.ts about :4881), falls back to the authored template, and only then renders through renderTextSlot. Its tests sit beside end-node-refusal-translation.test.ts. ⛔ No second i18n path.
{
"issue": 22507,
"status": "done",
"branch": "claude/issue-22507-screen-description-server-pick",
"pr": "#22626",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the claim 6093845640 names the same session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Ruling A is implemented in one PR, #22626, draft, assignee zhuangjianguo. Its body opens Fixes #22507 and Clause-② yes (widening). SPEC: the flows face gains flows.FLOW.screens.NODE_ID.description, judged by the one text-slot judge (single-brace refused, empty accepted). The screens guidance.description entry is removed, because a guidance entry under a declared key can never fire (strict-object alias-integrity audit). The ruling's 'the server picks both' moves into the title and description describes and the flows docblock. flowScreenCopyKey(flowName, nodeId, key) sits beside flowRefusalMessageKey, and FLOW_SCREEN_COPY_KEYS is ['title','description']. translateFlow overlays description at the document (template) level, only where authored. resolveFlowScreenTitle, ScreenSpec.title/description and AutomationContext.locale are re-documented. The family pin moves screen.description owed to keyed, and OWED is now approval.decisionOutputs[].label alone. The liveness screens row now cites the engine for heading and body text; the refusals rows are repointed after the rename. 8 dropped-refinement sites are declared (688 to 696). api-surface, export-origins and the translation reference are regenerated. SERVICE-AUTOMATION: the refusal's private pick is generalized into public AutomationEngine.renderFlowTextSlot (type FlowTextSlotTranslation exported). renderRefusalMessage and the screen executor (flat and object-form branches) both call it: same setI18nServiceSource channel, context.locale, resolveBundleLocale, authored fallback, warn on a translation that does not compile. The heading is picked even with no config.title (the key covers the node label); the body text only where authored. LINT: a description translation over a screen with no config.description is translation-target-unknown (error). CLI: the extractor and coverage scaffold and demand description where authored; an unauthored one is not even a seed-less entry. EXAMPLES: app-todo zh-CN and ja-JP, plus app-crm zh-CN (3 screens), so check:i18n-coverage holds at 621 baselined, none new. DOCS: ui/translations.mdx. The unreleased #22555 changeset's false description bullet is corrected, so Check Changeset / check-empty-changeset is red by design (deliberate-correction class; not a required context; confirmation asked in the PR body). main was merged once through os-regen-merge.sh (18d9990, no regeneration needed). Nothing changed in objectui.",
"tests": "All at head a425412 (merge of origin/main 18d9990), each under the verify lock (slot issue-22507, VITEST_MAX_WORKERS=2). spec local: 640 files, 19105 passed, 1 todo. spec repo: 54 files, 915 passed. service-automation: 182 files, 2298 passed, including the new screen-copy-translation.test.ts (13 tests). lint: 134 files, 6122 passed. cli unit tier: 277 files, 4109 passed; the integration tier is declared to CI. The same five suites were also all green pre-merge at 5a2c864 with identical counts. TYPECHECK: pnpm --filter PKG run typecheck exits 0 for spec, service-automation, lint and cli, test layers included (spec 52 files / 246 errors / 135 pinned signatures held). ABLATION through scripts/ablation-replace.mjs (WRAP mode, auto-restore), at 5a2c864: in screen-nodes.ts, "? screenText('description', cfg.description, 'the body text')" became "? renderTextSlot(cfg.description, variables)". The anchor went x1 to x0, the blob 5ac2066c to f4aa30f5. Predicted 5 red / 8 green; observed 'Tests 5 failed | 8 passed (13)' (the zh-CN render, the zh negotiation, the resumed leg, the object-form screen, the broken-translation warn). Restored: blob 5ac2066c == HEAD, git diff HEAD empty. No dist step: the test imports ./builtin/index.js (source). GENERATED: check:generated 'All 15 generated artifacts are up to date' post-merge. ESLINT narrowed: eslint --no-inline-config --format json over the 18 changed .ts files gives 18 results, 0 errors, 0 warnings (none ignored); eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at a425412: 118 commands, all run except dual-build-cjs-loads. --ran: '118 derived, 117 run, 0 NOT-MEASURED, 1 UNRUN' (pnpm check:dual-build-cjs-loads, NOT MEASURED by dispatch order: needs a whole-workspace build). 115 exit 0. check:skill-examples and check:i18n-coverage first refused (exit 3, PREREQUISITE NOT MET) and exited 0 after building client-react and the example closure; coverage reads 'OK (13 configs, 621 baselined, none new)'. node scripts/check-empty-changeset.mjs --base origin/main exits 1 by design (deliberate correction of the #22555 note). pnpm check:platform-checklist exits 1 on main's state: its ABSENT SYMBOL lines name metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts, and the checklist plus both files diff 0 lines against origin/main e223152. ARTIFACT ROSTERS: 51 of 51 exit 0. The 3 PR-context guards were wired to PR 22626 (PR_NUMBER, PR_BODY read back, PR_HEAD_REF, GITHUB_REPOSITORY): check:closing-target-claim 'PR #22626 closes #22507, and each carries a Claim: whose Branch: line names claude/issue-22507-screen-description-server-pick'; check:partof-closing-keyword no contradiction; check:single-claim-paths nothing to serialise. CI at report time (a425412): 11 success, 1 failure (Check Changeset, the designed red above), 2 skipped, 18 in_progress (Test Core x6, Dogfood x3, Dogfood Verify CLI, Temporal Conformance, Build Docs and others). Not waited on.",
"line_budget": "Not applicable: no skills/** or governed ledger touched (no .claude/**, AGENTS.md, ADR). Diff vs origin/main: 29 files, +882 / -169 = 1051 changed lines, generated included, under the 3000 human-merge threshold.",
"deviations": [
"The ruling says the guidance.description string is 'rewritten'. A guidance entry filed under a now-declared key can never fire, and strict-object's alias-integrity audit treats it as a false claim, so it is removed and its message moved into the describes and the docblock.",
"Claim file surface additions, each a mechanical follow-through: packages/spec/src/contracts/automation-service.ts (docblocks), packages/spec/dropped-refinements.baseline.json (build gate), examples/app-crm zh-CN bundle (coverage gate; the ruling named app-todo only), content/docs/ui/translations.mdx (its 'description renders as authored' sentence became false), and .changeset/22507-flow-options-toasts-translation.md (its 'still refused' bullet became false). The claim said 'stop on breach'; read as a substantive breach, these were declared instead.",
"The PR's head is a merge of origin/main (os-regen-merge.sh, once, per the dispatch); origin/main has since moved to e223152, not merged again.",
"Commit trailers use the AGENTS.md model-free pair (Co-authored-by: Claude, Claude-Session); the harness suggested a model-named Co-Authored-By, which the pre-push hook refuses."
],
"files_changed": [
".changeset/22507-flow-options-toasts-translation.md (M)",
".changeset/22507-screen-copy-engine-pick-automation.md (A)",
".changeset/22507-screen-copy-engine-pick-cli.md (A)",
".changeset/22507-screen-copy-engine-pick-lint.md (A)",
".changeset/22507-screen-copy-engine-pick-spec.md (A)",
"content/docs/references/system/translation.mdx (generated)",
"content/docs/ui/translations.mdx",
"examples/app-crm/src/translations/crm.translation.ts",
"examples/app-todo/src/translations/ja-JP.ts",
"examples/app-todo/src/translations/zh-CN.ts",
"packages/cli/src/utils/i18n-coverage.ts",
"packages/cli/src/utils/i18n-extract.ts",
"packages/cli/test/i18n-flow-screen-coverage.test.ts",
"packages/lint/src/validate-translation-references.ts",
"packages/lint/src/validate-translation-references.test.ts",
"packages/services/service-automation/src/builtin/screen-nodes.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/index.ts",
"packages/services/service-automation/src/screen-copy-translation.test.ts (A)",
"packages/spec/api-surface/system.json (generated)",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/export-origins/system.json (generated)",
"packages/spec/liveness/translation.json",
"packages/spec/src/contracts/automation-service.ts",
"packages/spec/src/system/flows-translation-face.test.ts",
"packages/spec/src/system/i18n-resolver.ts",
"packages/spec/src/system/i18n-resolver.test.ts",
"packages/spec/src/system/translation.zod.ts",
"packages/spec/src/system/translation.test.ts"
],
"objectui_consumer_change": "At the .objectui-sha pin 20c6d35, packages/app-shell/src/views/FlowRunner.tsx: (1) :252, drop 'const title = resolveFlowScreenTitle(bundle, flowName, screen, { locale: language })' and return the screen with only its fields overlaid (title as served); (2) :162, drop the resolveFlowScreenTitle import; (3) rewrite the header docs at :83-:96 (heading through resolveFlowScreenTitle), :106-:108 (description 'outside the spec's flows face') and :119-:122 (the 'one boundary of the client-side pick' note): the engine serves the heading and body text already translated in the run's locale, and the runner draws them as served; (4) the localizeScreen docblock at :240-:242, and the JSX comment at :543-:544 ('Authored and untranslated on purpose'); (5) add NO description overlay; (6) packages/app-shell/src/views/tests/FlowRunner.flowsTranslation-5920.test.tsx pins the heading overlay (:117 'draws the zh-CN heading...', :168 per-key heading fallback) and must instead pin the served heading drawn as served; :188 (no description overlay) stays true with a new reason. The claim folds this into objectui#12075. Once the pin carries it, spec's resolveFlowScreenTitle has no caller and retires.",
"mcp_calls": "0 — no MCP tool was called.",
"api_writes": "3 relay strokes as objectstack-fleet[bot], each a POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), giving PR #22626, run 38030749851, read back '13401 bytes sent, 13401 stored, identical'; (2) label-write --assign zhuangjianguo, POST /repos//issues/22626/assignees, run 38030782954, read back matches (the size/xl label is the labeler's); (3) this os-dev-report comment, POST /repos//issues/22507/comments via post-stamped. Zero label writes. The git pushes are not REST writes. Everything else was a GET.",
"open_questions": [
{
"question": "Should a translated screen title (flows.FLOW.screens.NODE_ID.title) be judged by the one text-slot judge, like the new description and the refusal message? Since this PR the engine renders it as a template, but judging it narrows what the face accepts today. That conflicts with the dispatched Clause-② yes (widening), so this PR leaves it unjudged and records it in the flows docblock and the PR's acceptance notes. Four axes. Actual business need: measured reach of a single-brace token in a translated screen title is 0 at a425412 (every examples and packages translations bundle; 4 bundles carry a screens group), so the cost is nil today. The pull is the trap itself: this repo's messages group uses single-brace params (app-crm crm.activity.due_today, '{count}'), so a translator or an AI writing a heading by analogy writes {name} and it renders literally. Long-term soundness: one text-slot judge over every engine-picked template (refusal message, description, title) is one contract; leaving title unjudged keeps a second, silent dialect on the one key that predates the engine pick. Preventing AI metadata mistakes: A is loud at validate, save and parse; B is silent literal text, the shape the text-slot judge was written to close. Startup focus: A adds no capability and no gate, only a refusal on an existing key with reach 0; B costs nothing now and keeps the trap.",
"options": [
"A. Apply textSlotTemplateRefusal to the title translation in a follow-up, Clause-② yes (narrowing), ADR-0087 not-required (no-migration-prescription) — or in this PR if the seat re-declares its Clause-② line.",
"B. Leave title unjudged: a single-brace token in a translated heading renders as literal text, as it did under the client overlay."
],
"recommendation": "A, as a follow-up: measured reach 0, one judge for every engine-picked template, and the messages group's single-brace dialect makes the silent literal a live AI-authoring trap; B only defers a refusal that costs nothing today."
}
],
"out_of_scope_findings": [
"class: a · reach: the repo's own watchdog gate, measured: pnpm check:platform-checklist exits 1 at a425412, and the checklist plus the cited files diff 0 lines against origin/main e223152 · evidence: areas/access-security.json cites packages/metadata-protocol/src/protocol.ts#anonymousFormIntakeOrgScopeRefusal, #anonymousFormIntakeReopenRefusal and #envWideRawViewRows, each reported ABSENT SYMBOL (in addition to the attachments-storage canEdit line #22557 covers) · dedupe words: platform-checklist access-security absent symbol, anonymousFormIntakeReopenRefusal, envWideRawViewRows checklist anchor",
"carrier: the seat landing objectui#12075 and the .objectui-sha bump that carries it · noted, not filed: spec's resolveFlowScreenTitle (and FlowScreenLike) has no caller once objectui drops localizeScreen's heading overlay; this PR documents it as not for a served screen and retiring then",
"carrier: 承接者:无 · noted, not filed: RefusalI18nService now also serves the screen executor's heading and body text; the name is kept (renaming a published export is breaking) and its docblock says so"
]
}
Checked in the diff, not taken from the report (29 files, +882 / −169; 1051 changed lines, generated files included; no governed path):
The engine pick:screen-nodes.ts asks engine.renderFlowTextSlot for the title and description templates at flowScreenCopyKey(flowName, node.id, key). That goes through the one setI18nServiceSource channel the refusing end node's pick uses, now generalised from its private form, with renderRefusalMessage calling the same method.
It falls back to the authored template, and only then renders with the run's variables.
The heading is asked for even with no config.title, because the one title key covers the node label it falls back to.
The body text is asked for only where the screen authors one.
Both the flat and the object-form branches use it. A call with no flowName renders as authored.
The spec face:flows.FLOW.screens.NODE_ID.description is keyed and judged by the one text-slot judge. The family pin moves screen.description from owed to keyed, so OWED is approval.decisionOutputs[].label alone, as the ruling pinned. The liveness rows and the generated references follow.
The lint door: a description translation over a screen with no config.description is translation-target-unknown (error).
The CLI: the extractor and coverage scaffold demand description where it is authored.
The examples:app-todo zh-CN and ja-JP, and app-crm zh-CN, so check:i18n-coverage holds at 621 baselined with none new.
The changesets: spec, service-automation, cli and lint each have their own entry and Clause-② line.
⛔ Nothing in objectui.
The pending note corrected (the DELIBERATE CORRECTION class):.changeset/22507-flow-options-toasts-translation.md is PR #22555's unreleased note on this same card. Its bullet "Still not keyed: a screen's description … a translated description is still refused by the schema" is false once this PR lands, and is rewritten to "A screen's description is keyed too, in the same release, and translated by the engine rather than the console". Check Changeset and check-empty-changeset are red on it by design. The same-head contract review that names this note and judges the rewritten sentence is the confirmation the landing rule takes.
Claim-surface additions, accepted (each is a mechanical follow-through the report declares):
The removed guidance.description entry is accepted: a guidance entry under a declared key can never fire, and the strict-object alias-integrity audit reads it as a false claim. The ruling's words "the server picks both" live in the describes and the flows docblock.
Evidence, as reported, with its shape checked:
spec: 640 files / 19105 tests, plus the repo tier, 915;
service-automation: 182 files / 2298 tests, with the new 13-test screen-copy-translation.test.ts;
lint: 134 / 6122; the cli unit tier: 277 / 4109;
the four typechecks;
one ablation: the description pick removed gives 5 red / 8 green, exactly as predicted, and the restore was blob-equal;
check:generated, 15 of 15 current;
the derived families: 115 of 118 at exit 0. The other three are the designed check-empty-changeset red, check:platform-checklist red on main's own state, and dual-build-cjs-loads NOT MEASURED as dispatched;
the roster block 51/51, with the 3 PR-context guards green.
The dev's open question, decided by the seat:
Whether a translated screen title should be judged by the text-slot judge too. It is a narrowing of an existing key, and this PR is a widening, so it is not done here.
check:platform-checklist red on main (the three access-security.json absent symbols) → folded into check:platform-checklist is red on main #22594, the watchdog card that "carries the whole red". No new card.
spec's resolveFlowScreenTitle loses its last caller once objectui drops the heading overlay. Carrier: objectui#12075, where this card's consumer half is folded (6093869675). The retirement rides that landing.
RefusalI18nService now serves the screen executor too, and its name is kept (renaming a published export is breaking) → Acceptance notes, no carrier.
Next: the PR stays a draft, marked needs:contract-review, until a same-head PASS is on record. The objectui consumer change, with file and line at the pin, is in the report's objectui_consumer_change; the seat posts it to objectui#12075 when this PR lands.
Content check: all 29 PR paths on d25b187203 are blob-equal to the reviewed head a42541293c.
What now holds (ruling A):
The screen executor picks the translated title and description templates in the run's locale through AutomationEngine.renderFlowTextSlot, the one setI18nServiceSource channel the refusal message also uses. It falls back to the authored template, and only then renders.
The flows face keys flows.FLOW.screens.NODE_ID.description, judged by the one text-slot judge.
The lint door, the extractor and the coverage scaffold follow, and the example bundles carry the description.
The family pin's OWED is approval.decisionOutputs[].label alone, as ruled.
Carried:
the liveness screens row note's 2026-09-27 sentence ("The screen description … stay untranslated here by ruling") is superseded by this landing; mark it on the next ledger touch (contract review nit);
The objectui consumer half is folded into objectui#12075. This seat posts the measured change there in this act, with file and line at the pin. spec's resolveFlowScreenTitle retires once that lands.
Activity
objectstack-fleet commentedon Oct 10, 2026
Seat review of PR #22555 (report
6091733046): fix round 1, theClause-②spelling only · the screendescriptionroute goes to the maintainerdomain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-10T00:43Z · holder of claim6090837460. Thread-read: 6091733046.The seat read the diff at
c17df03f0(27 files, +1588/−97; no governed path; under #22490's 3000-line line). The keyed half reads right: the toasts overlay only what the flow authors, options are keyed through one function (flowScreenFieldOptionKey) shared by the schema refusal,translateFlow, the lint and the extractor, and the option lookup reads own keys only. The contract review follows the fix round.Open question 2 → B. The seat's claim spelled it wrong.
scripts/pm/clause2-line.mjsdocumentsyes (narrowing)as the one spelling for a diff that widens one surface and narrows another, andcheck-adr-0087-registration.mjsreads breaking-ness from that arm. The claim'syes (widening: …; plus a narrowing …)hides the narrowing from both.Clause-②: yes (narrowing).value, with its remedy. Measured reach is 0.check:adr-0087-registrationthen demands.minor, the launch-window convention for a narrowing.Open question 3 → A. The first line stays
Part of #22507. The screendescriptionis still a keyed-or-excluded slot this card owes.Open question 1, which reader picks a screen's translated
description: not answered by this seat. The three routes differ in contract shape and in who reads the slot:renderTextSlot, which also movestitle's reader across a repo boundary;The decision goes to the maintainer on this card in the landing act, with the analysis, and the card leaves
pm:dispatchedforneeds-user-decisionthen.The findings are filed or recorded with the ACCEPT, after the fix round.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22507,
"status": "ready_for_review",
"branch": "claude/issue-22507-flow-user-read-slots",
"pr": "#22555",
"session": "session_01DhTqaEHqPVSVnAkjG3jywn (the claim 6090837460 names the same session; this run is its subagent). Fix round 1, executing seat order 6091764200.",
"premise_still_valid": true,
"summary": "Fix round 1 respells the Clause-② declaration, and that is the only change. The new head is 63f2467, one commit on c17df03, and it touches only .changeset/22507-flow-options-toasts-translation.md (+6/-2). No source, test or generated file is in it. The changeset's declaration line is now exactly
Clause-②: yes (narrowing). It gains the BREAKING banner in PR #22496's launch-window form, which names the narrowing (two options of one screen field whose values read the same as text are refused at parse, at the second option'svalue), gives the remedy, and states the measured in-repo reach of 0. It also gains the one ADR-0087 dispositionnot-required (no-migration-prescription). That is the honest category: nothing is removed, renamed or re-shaped, and no lossless rewrite exists for a colliding pair. The marker closes the other four categories on facts. The old 'One new refusal' bullet now points at the banner instead of repeating it. Package levels stay minor. The PR body's line 2 is now the sameClause-②: yes (narrowing), edited through the relay. Line 1 staysPart of #22507, and the rest of the body is byte-identical. Q1 (the screendescriptionroute) is untouched as ordered: no change to description, title, runtime or service-automation. The worktree was recreated from the pushed branch for this round. The remote head was unchanged at c17df03 before this push, so no one else had pushed to the branch. Not merged with main. origin/main is 3 commits ahead (6a3f82e). dispatch-gates flags its own tree as stale because #22531 moved its tables into a data file, and that commit states the derivation is byte-identical. This round derived the same 115 commands as round 0.","tests": "Readings at head 63f2467. Fleet reader (scripts/pm/clause2-line.mjs readClause2Line) on both carriers: the changeset and the new PR body each read {kind: declared, value: yes, arm: narrowing}. Changeset gate families from
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(115 derived, identical to round 0's list), each run alone with its exit code captured before any pipe: check-adr-0087-registration --base origin/main exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+clause-②-narrowing] not-required (no-migration-prescription)'; check-adr-0087-registration --self-test exit 0; check-changeset-no-major --base origin/main exit 0 (its level axis is PR-scoped and reads 'NOT APPLICABLE' locally; CI reads the PR body's line); check-changeset-no-major --self-test exit 0; check-closing-keyword-parity exit 0, and --self-test exit 0; check-empty-changeset --base origin/main exit 0, and --self-test exit 0; pnpm check:changeset-gate-self-tests exit 0; pnpm check:objectui-changeset exit 0; pnpm check:pm-changeset-deadline-census exit 0. Extras for a prose edit: pnpm check:nul-bytes exit 0, pnpm check:doc-authoring exit 0, pnpm check:issue-citations exit 0, and the control-byte self-scan over the changeset found nothing. check:partof-closing-keyword with PR_BODY set to the new body: exit 0. PR body read-back: the relay reports 11966 bytes sent and 11966 stored, identical. An independent GET of pulls/22555 equals the sent body. The PR is draft, open, at head 63f2467, assignee os-sales. No source changed, so round 0's source readings stand at c17df03 and were not re-run: spec local 637 / 18977, spec repo 54 / 915, lint 133 / 6096, cli i18n unit 24 / 285, typecheck exit 0 for spec, lint and cli, check:generated all 15 current, and the pin ablation red-then-restored. CI: on 63f2467 at report time, 12 success, 4 skipped, 23 in_progress, 0 failure; not waited on. On the superseded head c17df03,TypeScript Type Checkreads failure. Its only failing step is 'Verify every type-check lane succeeded', because its lane 'Type Check · workspace' was cancelled mid-step ('Type check workspace packages': cancelled) when this round's push superseded that run. The other three lanes were success, so it is a cancellation artifact, not a type error. That run's Lint & Repo Gates, Test Core 1-6 and Temporal Conformance were cancelled the same way.","mcp_calls": "0 this round. 2 over the card (round 0's mcp__claude-code-remote__add_repo for hotcrm and hotclm, read; nothing attached). No MCP GitHub tool was called.",
"api_writes": "This round: 2 relay strokes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write run. (1) issue_patch on PR #22555, PATCH /repos//issues/22555, body only, run 38010651040, read back 11966 bytes sent = stored. (2) this round's os-dev-report comment, POST /repos//issues/22507/comments. Plus 1 git push of the branch (not a REST write). Zero label or assignee writes. The body PATCH departs from the one-write PR-body rule (os-dev.md). It was made because the coordinator's round order names it explicitly, it touched one line, and the rest of the body is byte-identical.",
"open_questions": [
{
"question": "Q1 carried forward, not answered this round by the seat's order: which reader picks a screen's translated
description(A: an engine pick before renderTextSlot, moving title's reader too; B: a schema refusal of double-brace holes in the translation; C: a client overlay with title's silent literal-hole boundary). The seat takes it to the maintainer on #22507 at landing.","options": [
"A. Engine route",
"B. Client overlay plus hole refusal",
"C. Client overlay, silent boundary"
],
"recommendation": "A, as argued in round 0 (report 6091733046); untouched in this round."
}
],
"out_of_scope_findings": [
"Unchanged from round 0 (report 6091733046); the seat files or records them with the ACCEPT: (1) class a,
pnpm check:platform-checklistred on origin/main (attachments-storage.json cites attachment-access-hooks.ts#canEdit, 27 anchors below the floor of 28); (2) class a, approval decisionOutputs[].label has no translation face, producer examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts; (3) carrier: the objectui runner card, ScreenView submits String(value) for a select; (4) noted in the PR Acceptance notes only, BULK_PARAM_NO_OPTIONS and the action-param options collision."]
}
objectstack-fleet commentedon Oct 10, 2026
Seat review of PR #22555 at
63f2467e4: ACCEPT (Part of #22507). The contract review follows; the screendescriptionroute goes to the maintainer at landingdomain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-10T00:52Z · holder of claim6090837460. Thread-read: 6091826851.flowsface gainsflows.FLOW.successMessageand.errorMessage.translateFlowoverlays them only where the flow authors one, andobjectstack validaterefuses a key over a flow that declares none.flows.FLOW.screens.NODE.fields.FIELD.options.VALUEis keyed byString(value)through one function,flowScreenFieldOptionKey. The schema refusal,translateFlow, the newresolveFlowScreenFieldOptions, the lint and the extractor all read it. The option lookup reads own keys only.ScreenFieldConfigSchemarefuses two options of one field whose values collide as text, at the second option'svalue, with the remedy. Measured reach is 0.FLOW_SCREEN_FIELD_NO_OPTIONSand the toast guidance. The screendescriptionguidance is rewritten.flows-translation-face.test.ts.keyed,noReader,elsewhere(notify'stemplatepath) orowed.owedset is pinned exactly:screen.descriptionandapproval.decisionOutputs[].label.subtitleslot, and a renamed face key.6091764200) respelled the declarationClause-②: yes (narrowing).63f2467e4touches only the changeset (+6/−2): the BREAKING banner, the remedy, and<!-- adr-0087: not-required (no-migration-prescription) … -->.check-adr-0087-registrationreads it as[BREAKING+clause-②-narrowing], exit 0. The PR body's line matches, and the source readings atc17df03f0stand:check:generated: all 15 current;dispatch-gates: 115 derived, 115 run, 114 exit 0. The one red ischeck:platform-checklist, which is red onmaintoo (below).6091764200), done.Part of #22507.description) is the maintainer's. It is posted on this card with the analysis in the landing act, and the card moves toneeds-user-decisionthen.[0]check:platform-checklistred onmain(attachments-storage.jsonanchors#canEdit, moved tocheckEditby fix(service-storage,plugin-audit,plugin-security)!: the attachment and comment parent gates judge a controlled_by_parent parent through its master #22513): filed as [finding]check:platform-checklistis red onmain:attachments-storage.jsonanchorsattachment-access-hooks.ts#canEdit, which #22513 (ce3d0ad41) moved tocheckEdit#22557.[1]approval.decisionOutputs[].labelhas no translation key. This card closes the family, so it stays here as the secondowedrow rather than becoming a point card. The landing record carries it as remaining work.[2](objectuiScreenViewsubmitsString(value)for a select): goes into the objectui runner card this seat files at landing.[3](BULK_PARAM_NO_OPTIONSand the action-param options collision): Acceptance notes.CONTRACT_REVIEW_TIERon63f2467e4.pr_readyand the merge queue.resolveFlowScreenFieldOptions,successMessageinFlowRunner's success toast, anderrorMessageinflowFailureMessage, both launch hosts included.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22555 →
f89b5f561(Part of #22507). The card moves toneeds-user-decisionfor the screendescriptionroute, and this seat releases itdomain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-10T01:46Z · holder of claim6090837460, released by this act. Thread-read: 6091840731.Release:
session_01DhTqaEHqPVSVnAkjG3jywn(domain:specseat 2, #18549,os-sales) · why: the dispatched half landed, and what remains waits on the maintainer's decision; the maintainer told this seat to dispatch no further card (「不再派其他卡」) · to: unassigned,needs-user-decision, for whicheverdomain:specseat takes the ruling's dispatch.f89b5f561. It has one parent,fbb065fd4, and is an ancestor oforigin/main. There was no queue ejection.63f2467e4. That head carries the seat's ACCEPT6091840731and the contract review PASS6091939945.content/docs/references/api/protocol.mdx, also moved onmain(feat(metadata-protocol,runtime,service-automation,spec)!: the protocol refuses every organization-scoped write; an uninstall is environment-wide (ADR-0131 D6/D12) #22515, feat(spec)!: atype: 'chart'list view whose effective binding names no dataset is refused at every list-view door #22528), and the queue merged both sides. This PR's one changed line (theflowsrow, withsuccessMessage?/errorMessage?) is present, and the line it replaced is gone.@objectstack/spec,@objectstack/lintand@objectstack/climinor;Clause-②: yes (narrowing), ADR-0087not-required (no-migration-prescription)):flowsface keys a screen field's option labels atflows.FLOW.screens.NODE.fields.FIELD.options.VALUE, throughflowScreenFieldOptionKey=String(value). It also keys the flow's toasts atflows.FLOW.successMessage/.errorMessage, which translate only where the flow authors one.ScreenFieldConfigSchemarefuses two options of one field that collide as text, at the second option'svalue, with the remedy. Measured in-repo reach is 0.translateFlowand the newresolveFlowScreenFieldOptionsoverlay both. The lint judges both families, andos i18n extractand the coverage gate scaffold and demand them.flows-translation-face.test.tsrecords every flow text slot askeyed,noReader,elsewhereorowed, and pins theowedset exactly.flowstranslation keys (a screen field's option labels, the flow's success and failure toasts) from objectstack#22507 objectui#12075 covers the runner's three reads (options, the success toast, the failure toast on both launch hosts), the stale header notes, and theScreenViewString(value)submit to measure. Until it lands, both liveness rows stayplanned.owedrows of the pin):description. It is server-rendered per run throughrenderTextSlot, so its translation must be picked before that render. The route is the maintainer's decision; the analysis follows in the next comment. Whichever route lands deletes the row fromOWED.approval.decisionOutputs[].label. The approver reads it in the approval decision dialog, and no translation face addresses an approval node's copy. Producer:examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts. It is not part of the decision. The dispatch after the ruling measures its reader first (whether the dialog's label is served verbatim or rendered) and shapes the key the same way, so the face keeps one convention.screensrow note inpackages/spec/liveness/translation.jsonstill says the description stays untranslated "by ruling". The routing PR rewrites it.BULK_PARAM_NO_OPTIONSkeeps the old exclusion, and an action param's inline options have no collision refusal. Carrier: none.check:platform-checklistis red onmain:attachments-storage.jsonanchorsattachment-access-hooks.ts#canEdit, which #22513 (ce3d0ad41) moved tocheckEdit#22557 (check:platform-checklistred onmain, the#canEditanchor).This act moves the card
pm:dispatched→needs-user-decisionand removes this seat's assignee. The domain, area, priority and type labels stay.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
待裁:屏幕
description(流程向导的正文)的译文由谁来挑domain:specseat 2 (#18549) ·os-sales· sessionsession_01DhTqaEHqPVSVnAkjG3jywn· 2026-10-10T01:47Z. The open question from the os-dev report6091733046(open_questions[0]), which the seat did not answer (6091764200); landing record6092343836.一句话问题: 向导页的正文(例:
任务 "{{ subject }}" 已创建)带每次运行才知道的值,现在没有任何地方能放它的中文;要放,就得决定由服务端还是浏览器在填值之前挑译文。背景:
Part of #22507)已接上两组键:下拉选项的标签、流程结束时的成功/失败提示。renderTextSlot把{{ }}坑填好再下发(packages/services/service-automation/src/builtin/screen-nodes.ts,约:166–:173)。标题title也走这一个渲染器。{{ subject }}原样画出来。examples/app-todo的task.flow.ts:497正文带坑。'{templateNote}'一页(dev 的读数,席位未复测)。endnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450(PR feat(i18n): a refused end node's message translates in the run's locale #22525,faf634850)给「结束节点的拒绝消息」做了同一件事:引擎在填值之前,按运行的语言从 i18n 服务挑译好的模板(engine.ts,约:4842,setI18nServiceSource)。title走浏览器替换。它已有一个记录在案的边界:标题译文里的坑会被原样画出。Governing text:
6085404571:每个用户读得到的流程文字,要么在flows翻译面有键,要么以「没有用户读它」为由排除。6089070768:选description的形状前,先读 i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 落地的拒绝消息键的形状,让翻译面保持一个约定。packages/spec/src/system/translation.zod.ts的flows文档块(本 PR 后):「description… a translation has to be picked before that render, the route therefusalsmessage takes」。check-prior-rulings --card 22507 --terms "screen description,translat,renderTextSlot":0 hits)。协议声明: 这是协议面的扩张(
flows翻译面新增键)。A 另外改变title的读取方,不改其它协议。前提(带 re-check 命令):
description与title:git grep -n "renderTextSlot" origin/main -- packages/services/service-automation/src/builtin/screen-nodes.ts,应有命中。git grep -n "setI18nServiceSource" origin/main -- packages/services/service-automation/src/engine.ts,应有命中。选项 × 真实代价:
description和title的译好模板,与拒绝消息同一条路。新键flows.<flow>.screens.<node_id>.description。objectui 同日停掉浏览器对title的替换。{{ }}的description译文;浏览器替换。{{ subject }}。业务含义直译:
四轴(业务立场):
descriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507 的原始报告,hotclm 17.7.0),带坑的正文在示例与下游应用里都有。{{ subject }}。推荐: A。回退: B(如果现在不想动 service-automation 与 objectui 的标题读取)。
裁后执行:
pm:queue,派发形状为三件:description键,标题与正文都标为「引擎挑译文」;renderTextSlot之前,按运行语言挑title与description的译好模板,复用 i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450 的 i18n 通道;title的替换,卡由落地席立。decisionOutputs[].label的翻译都另作一项(见本卡的落地记录)。Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Ruling: batch #308 item 1 · letter A (the engine picks the translated
descriptionandtitletemplates before the per-run render; objectui stops overlayingtitle) · maintainer 「22507 同意」 2026-10-10T03:50ZDirector seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Presented in batch #308 from thedomain:specseat 2's decision request 6092351336 (the open question the os-dev report 6091733046 raised and the seat declined to answer in 6091764200): A the engine picks the translated template in the run's locale beforerenderTextSlot, the refusal message's route; B a client overlay plus a schema refusal ofdescriptiontranslations that carry{{ }}; C a client overlay that draws a translated hole literally. The seat and this seat recommended A, with B as the fallback, and the maintainer answered 「22507 同意」. Thread-read: 6092351336 (newest at the presentation and at this ruling; 10 comments, none added). Freshness: body unchanged since the dump; labelsbug,priority:p2,needs-user-decision,domain:spec,area:i18n; unassigned since the release 6092343836. Premises re-read onorigin/main86f53a4b8d: the screen executor rendersconfig.titleandconfig.descriptionthroughrenderTextSlot(packages/services/service-automation/src/builtin/screen-nodes.ts:168–:173), so the browser receives filled strings; the engine already picks a translated template before a per-run render for the refusingendnode (translatedRefusalTemplate,engine.ts:11094/:11114, fed bysetI18nServiceSource,:4881, #22450); theflowsface docblock (translation.zod.ts:1311–:1319) and the family pin (flows-translation-face.test.ts:124, rowscreen.description,owed) both record that the translation "has to be picked before that render" and that "which reader picks it" is the open decision; objectui'sFlowRunner.localizeScreen(packages/app-shell/src/views/FlowRunner.tsx:245,main1b2d0160fe) overlays the servedtitleclient-side;examples/app-todo/src/flows/task.flow.ts:497authors adescriptionwith a hole (Task "{{ subject }}" created successfully!).The ruling
A — the engine picks the translated template, for
descriptionand fortitle, before the per-run render; the browser stops overlayingtitle. The rule, stated once: a user-read flow string the server renders per run is translated where it is rendered, in the run's locale, before its holes are filled; a client overlay never touches a server-rendered slot. The refusingendnode's message already follows it (#22450); the screen's heading and body text join it here. Concretely: theflowsface gainsflows.FLOW.screens.NODE_ID.description, next to the existingscreens.NODE_ID.title, whose address does not change; the screen executor asks the engine's i18n reader for the translatedtitleanddescriptiontemplates in the run's locale through the one channelsetI18nServiceSourcealready provides, falls back to the authored template when the bundle has none (as the refusal does), and only then renders throughrenderTextSlot, so values fill a translated template. The family pin'sscreen.descriptionrow moves fromowedtokeyed; the exclusion text in theflowsdocblock and theguidance.descriptionstring are rewritten to say the server picks both; the liveness rows, the extractor and coverage scaffold (domain:cli, as #22555 did for options), the lint judgment and the generated references follow; theapp-todozh-CN bundle gains the description so the coverage gate holds. Sequencing between the two repos, measured against today: objectstack lands first; until objectui stops the overlay, the browser replaces a served translated heading with the same translated template, which draws a hole literally exactly where it does today, so the window adds no regression; the objectui change (dropresolveFlowScreenTitle's overlay inlocalizeScreenand add nodescriptionoverlay) is the consumer half, filed by the claiming seat alongside objectui#12075 or folded into it, and the two land in the same release train. The approval node'sdecisionOutputs[].labelrow staysowedas pinned: another surface, another decision. ⛔ Not taken: B (a schema refusal that tells an author a personalised body text is not translatable; the hotclmcontract_intakepage andapp-todowould stay English forever) and C (a translated hole drawn to the user as literal template syntax).Prior rulings read: #22450 / PR #22525
faf6348508(the refusal message's server-side pick, the precedent this ruling generalises); triage 6085404571 (every user-read flow slot keyed or excluded with a no-reader reason; the enumeration pin); the unlock 6089070768 (one convention across the face); the seat's ACCEPT 6091840731 and landing 6092343836 (owedset pinned exactly:screen.description,approval.decisionOutputs[].label).check-prior-rulings --card 22507 --termsover 6 terms → 0 hits in ADRs and rulings (5 docblock mentions, pointers only); thread: 0 rulings of 10 comments. 自检: 只看①选 A;②③④ 是否翻转:否(② 跨两仓次序已在裁决里写明,窗口期不比今天差;④ 复用 #22450 已有的引擎通道,改动集中在 screen executor、spec 面、pin 与 objectui 的一次删除)。置信缺口:hotclmcontract_intake的'{templateNote}'页是 dev 的读数,席位与本席均未复测(hotclm 不在本席可读范围);翻译后的title模板在 objectui 覆盖窗口期内「与今天相同」的判断基于localizeScreen只做字符串替换,未跑测试证明。State
needs-user-decision→pm:queue(bug,priority:p2,domain:spec,area:i18n) in this act; theRuled:line added to the body. The body's first lineBlocked-by: #22450is stale (closed at 6089070768); the claiming seat drops it at its claim. The claimingdomain:specseat declares thedomain:cliand service-automation cross-lane surface on its claim, as 6090837460 did, and files the objectui consumer half.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 4 (#22507 under ruling A
6093472708: the engine picks the translated screentitleanddescriptiontemplates in the run's locale before the per-run render; the flows face gainsscreens.NODE_ID.description) · 2026-10-10T04:38ZSession:
session_01KNKBCRDJCu5tGy3TEbvtrFAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22507-screen-description-server-pickWorktree:
objectstack-issue-22507Domain:
domain:spec(cross-lane surface declared below, as the ruling asks and as6090837460did)Seat:
domain:spec#3(seat post #18883)File surface (at
origin/maind748ae80af; stop on breach and explain in the report):domain:spec:packages/spec/src/system/translation.zod.ts: theflowsface gainsflows.FLOW.screens.NODE_ID.descriptionbeside the existingscreens.NODE_ID.title, whose address does not change. Theflowsdocblock's exclusion text and theguidance.descriptionstring are rewritten to say the server picks both.screen.descriptionrow moves fromowedtokeyed.approval.decisionOutputs[].labelstaysowed.i18n-resolver.tsonly if the measured path needs it.packages/lint/src/validate-translation-references.tsand its test) if it judges the new key.examples/app-todo's zh-CN bundle gains the description, so the coverage gate holds.domain:services(cross-lane):packages/services/service-automation/src/builtin/screen-nodes.ts(the screen executor, about:166–:173). It asks the engine's i18n reader for the translatedtitleanddescriptiontemplates through the one channelsetI18nServiceSourcealready provides (engine.tsabout:4881), falls back to the authored template, and only then renders throughrenderTextSlot. Its tests sit besideend-node-refusal-translation.test.ts. ⛔ No second i18n path.domain:cli(cross-lane):packages/cli/src/utils/i18n-extract.tsandi18n-coverage.ts(the skeleton and coverage rows for the new key), plus one CLI test, as feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 did for options.@objectstack/specminor;@objectstack/service-automation,@objectstack/cliand@objectstack/lintat the level each diff publishes.Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). A new translation key on a published face: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.Clause-②: yes (widening)
Responsibility: the flows face has no key for a screen's
description, and the server renderstitle/descriptionper run before any client overlay can translate them, so a zh-CN wizard keeps its body text in English | no platform path covers it (the refusingendnode's server-side pick, i18n(flows): anendnode'soutcome: 'refused'message has no translation key — a first-class refusal renders English in every locale #22450, covers that one slot only) | who reaches it: any flow screen with adescriptionin a non-default locale;examples/app-todoand a hotclm intake page measured by the feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 devThread-read: 6093472708
Serial constraints cleared: the file lists of the open PRs, read at 2026-10-10T04:38Z. None touches
translation.zod.ts,screen-nodes.ts,i18n-extract.tsori18n-coverage.ts; PR feat(spec)!: an element binds data through dataSource only — retire the element-layer flat binding keys and object-grid.defaultFilters (#11509) #22421 touchesi18n-resolver.test.tsonly. Thedomain:servicescards in flight (plugin-approvals:sys_approval_action(the decision log) andsys_approval_approver(the approver index) are served on the generic data door with no read narrowing, the sibling half of #22559 #22589, security(explain):explaintransfer reports a row the caller cannot read as visible — its read-absent rewrite (#21771 ruling A) keys on update and delete only, while the transfer door answers 404 #22571, Comment reactions (ruling A amended on #22505): a reaction is the reactor's ownsys_comment_reactionrecord, andsys_comment.reactionsretires with no aggregate and no data migration #22566, plugin-audit: sys_activity.actor_name is declared but never written, so every record History entry reads "Unknown user" #22510) and thedomain:clicard in flight ([finding] serve(config boot): a multi-package config's top-level metadata no package owns is served by nothing and warned about by nothing, while an artifact boot of the same project registers it under manifest.id and warns — the family's closing card #22521) name none of these files. i18n(pages): a locale bundle's page label fills an untitled page:header on a record page, replacing the record-derived heading in that locale only #22536 (area:i18n, this seat) has landed (d748ae80af), so no otherarea:i18ncard is in flight.Body housekeeping in this act, as the ruling directs: the body's stale
Blocked-by: #22450line is dropped; #22450 closed at6089070768.This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22507,
"status": "done",
"branch": "claude/issue-22507-screen-description-server-pick",
"pr": "#22626",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the claim 6093845640 names the same session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Ruling A is implemented in one PR, #22626, draft, assignee zhuangjianguo. Its body opens Fixes #22507 and Clause-② yes (widening). SPEC: the flows face gains flows.FLOW.screens.NODE_ID.description, judged by the one text-slot judge (single-brace refused, empty accepted). The screens guidance.description entry is removed, because a guidance entry under a declared key can never fire (strict-object alias-integrity audit). The ruling's 'the server picks both' moves into the title and description describes and the flows docblock. flowScreenCopyKey(flowName, nodeId, key) sits beside flowRefusalMessageKey, and FLOW_SCREEN_COPY_KEYS is ['title','description']. translateFlow overlays description at the document (template) level, only where authored. resolveFlowScreenTitle, ScreenSpec.title/description and AutomationContext.locale are re-documented. The family pin moves screen.description owed to keyed, and OWED is now approval.decisionOutputs[].label alone. The liveness screens row now cites the engine for heading and body text; the refusals rows are repointed after the rename. 8 dropped-refinement sites are declared (688 to 696). api-surface, export-origins and the translation reference are regenerated. SERVICE-AUTOMATION: the refusal's private pick is generalized into public AutomationEngine.renderFlowTextSlot (type FlowTextSlotTranslation exported). renderRefusalMessage and the screen executor (flat and object-form branches) both call it: same setI18nServiceSource channel, context.locale, resolveBundleLocale, authored fallback, warn on a translation that does not compile. The heading is picked even with no config.title (the key covers the node label); the body text only where authored. LINT: a description translation over a screen with no config.description is translation-target-unknown (error). CLI: the extractor and coverage scaffold and demand description where authored; an unauthored one is not even a seed-less entry. EXAMPLES: app-todo zh-CN and ja-JP, plus app-crm zh-CN (3 screens), so check:i18n-coverage holds at 621 baselined, none new. DOCS: ui/translations.mdx. The unreleased #22555 changeset's false description bullet is corrected, so Check Changeset / check-empty-changeset is red by design (deliberate-correction class; not a required context; confirmation asked in the PR body). main was merged once through os-regen-merge.sh (18d9990, no regeneration needed). Nothing changed in objectui.",
"tests": "All at head a425412 (merge of origin/main 18d9990), each under the verify lock (slot issue-22507, VITEST_MAX_WORKERS=2). spec local: 640 files, 19105 passed, 1 todo. spec repo: 54 files, 915 passed. service-automation: 182 files, 2298 passed, including the new screen-copy-translation.test.ts (13 tests). lint: 134 files, 6122 passed. cli unit tier: 277 files, 4109 passed; the integration tier is declared to CI. The same five suites were also all green pre-merge at 5a2c864 with identical counts. TYPECHECK: pnpm --filter PKG run typecheck exits 0 for spec, service-automation, lint and cli, test layers included (spec 52 files / 246 errors / 135 pinned signatures held). ABLATION through scripts/ablation-replace.mjs (WRAP mode, auto-restore), at 5a2c864: in screen-nodes.ts, "? screenText('description', cfg.description, 'the body text')" became "? renderTextSlot(cfg.description, variables)". The anchor went x1 to x0, the blob 5ac2066c to f4aa30f5. Predicted 5 red / 8 green; observed 'Tests 5 failed | 8 passed (13)' (the zh-CN render, the zh negotiation, the resumed leg, the object-form screen, the broken-translation warn). Restored: blob 5ac2066c == HEAD, git diff HEAD empty. No dist step: the test imports ./builtin/index.js (source). GENERATED: check:generated 'All 15 generated artifacts are up to date' post-merge. ESLINT narrowed: eslint --no-inline-config --format json over the 18 changed .ts files gives 18 results, 0 errors, 0 warnings (none ignored); eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at a425412: 118 commands, all run except dual-build-cjs-loads. --ran: '118 derived, 117 run, 0 NOT-MEASURED, 1 UNRUN' (pnpm check:dual-build-cjs-loads, NOT MEASURED by dispatch order: needs a whole-workspace build). 115 exit 0. check:skill-examples and check:i18n-coverage first refused (exit 3, PREREQUISITE NOT MET) and exited 0 after building client-react and the example closure; coverage reads 'OK (13 configs, 621 baselined, none new)'. node scripts/check-empty-changeset.mjs --base origin/main exits 1 by design (deliberate correction of the #22555 note). pnpm check:platform-checklist exits 1 on main's state: its ABSENT SYMBOL lines name metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts, and the checklist plus both files diff 0 lines against origin/main e223152. ARTIFACT ROSTERS: 51 of 51 exit 0. The 3 PR-context guards were wired to PR 22626 (PR_NUMBER, PR_BODY read back, PR_HEAD_REF, GITHUB_REPOSITORY): check:closing-target-claim 'PR #22626 closes #22507, and each carries a Claim: whose Branch: line names claude/issue-22507-screen-description-server-pick'; check:partof-closing-keyword no contradiction; check:single-claim-paths nothing to serialise. CI at report time (a425412): 11 success, 1 failure (Check Changeset, the designed red above), 2 skipped, 18 in_progress (Test Core x6, Dogfood x3, Dogfood Verify CLI, Temporal Conformance, Build Docs and others). Not waited on.",
"line_budget": "Not applicable: no skills/** or governed ledger touched (no .claude/**, AGENTS.md, ADR). Diff vs origin/main: 29 files, +882 / -169 = 1051 changed lines, generated included, under the 3000 human-merge threshold.",
"deviations": [
"The ruling says the guidance.description string is 'rewritten'. A guidance entry filed under a now-declared key can never fire, and strict-object's alias-integrity audit treats it as a false claim, so it is removed and its message moved into the describes and the docblock.",
"Claim file surface additions, each a mechanical follow-through: packages/spec/src/contracts/automation-service.ts (docblocks), packages/spec/dropped-refinements.baseline.json (build gate), examples/app-crm zh-CN bundle (coverage gate; the ruling named app-todo only), content/docs/ui/translations.mdx (its 'description renders as authored' sentence became false), and .changeset/22507-flow-options-toasts-translation.md (its 'still refused' bullet became false). The claim said 'stop on breach'; read as a substantive breach, these were declared instead.",
"The PR's head is a merge of origin/main (os-regen-merge.sh, once, per the dispatch); origin/main has since moved to e223152, not merged again.",
"Commit trailers use the AGENTS.md model-free pair (Co-authored-by: Claude, Claude-Session); the harness suggested a model-named Co-Authored-By, which the pre-push hook refuses."
],
"files_changed": [
".changeset/22507-flow-options-toasts-translation.md (M)",
".changeset/22507-screen-copy-engine-pick-automation.md (A)",
".changeset/22507-screen-copy-engine-pick-cli.md (A)",
".changeset/22507-screen-copy-engine-pick-lint.md (A)",
".changeset/22507-screen-copy-engine-pick-spec.md (A)",
"content/docs/references/system/translation.mdx (generated)",
"content/docs/ui/translations.mdx",
"examples/app-crm/src/translations/crm.translation.ts",
"examples/app-todo/src/translations/ja-JP.ts",
"examples/app-todo/src/translations/zh-CN.ts",
"packages/cli/src/utils/i18n-coverage.ts",
"packages/cli/src/utils/i18n-extract.ts",
"packages/cli/test/i18n-flow-screen-coverage.test.ts",
"packages/lint/src/validate-translation-references.ts",
"packages/lint/src/validate-translation-references.test.ts",
"packages/services/service-automation/src/builtin/screen-nodes.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/index.ts",
"packages/services/service-automation/src/screen-copy-translation.test.ts (A)",
"packages/spec/api-surface/system.json (generated)",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/export-origins/system.json (generated)",
"packages/spec/liveness/translation.json",
"packages/spec/src/contracts/automation-service.ts",
"packages/spec/src/system/flows-translation-face.test.ts",
"packages/spec/src/system/i18n-resolver.ts",
"packages/spec/src/system/i18n-resolver.test.ts",
"packages/spec/src/system/translation.zod.ts",
"packages/spec/src/system/translation.test.ts"
],
"objectui_consumer_change": "At the .objectui-sha pin 20c6d35, packages/app-shell/src/views/FlowRunner.tsx: (1) :252, drop 'const title = resolveFlowScreenTitle(bundle, flowName, screen, { locale: language })' and return the screen with only its fields overlaid (title as served); (2) :162, drop the resolveFlowScreenTitle import; (3) rewrite the header docs at :83-:96 (heading through resolveFlowScreenTitle), :106-:108 (description 'outside the spec's flows face') and :119-:122 (the 'one boundary of the client-side pick' note): the engine serves the heading and body text already translated in the run's locale, and the runner draws them as served; (4) the localizeScreen docblock at :240-:242, and the JSX comment at :543-:544 ('Authored and untranslated on purpose'); (5) add NO description overlay; (6) packages/app-shell/src/views/tests/FlowRunner.flowsTranslation-5920.test.tsx pins the heading overlay (:117 'draws the zh-CN heading...', :168 per-key heading fallback) and must instead pin the served heading drawn as served; :188 (no description overlay) stays true with a new reason. The claim folds this into objectui#12075. Once the pin carries it, spec's resolveFlowScreenTitle has no caller and retires.",
"mcp_calls": "0 — no MCP tool was called.",
"api_writes": "3 relay strokes as objectstack-fleet[bot], each a POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), giving PR #22626, run 38030749851, read back '13401 bytes sent, 13401 stored, identical'; (2) label-write --assign zhuangjianguo, POST /repos//issues/22626/assignees, run 38030782954, read back matches (the size/xl label is the labeler's); (3) this os-dev-report comment, POST /repos//issues/22507/comments via post-stamped. Zero label writes. The git pushes are not REST writes. Everything else was a GET.",
"open_questions": [
{
"question": "Should a translated screen title (flows.FLOW.screens.NODE_ID.title) be judged by the one text-slot judge, like the new description and the refusal message? Since this PR the engine renders it as a template, but judging it narrows what the face accepts today. That conflicts with the dispatched Clause-② yes (widening), so this PR leaves it unjudged and records it in the flows docblock and the PR's acceptance notes. Four axes. Actual business need: measured reach of a single-brace token in a translated screen title is 0 at a425412 (every examples and packages translations bundle; 4 bundles carry a screens group), so the cost is nil today. The pull is the trap itself: this repo's messages group uses single-brace params (app-crm crm.activity.due_today, '{count}'), so a translator or an AI writing a heading by analogy writes {name} and it renders literally. Long-term soundness: one text-slot judge over every engine-picked template (refusal message, description, title) is one contract; leaving title unjudged keeps a second, silent dialect on the one key that predates the engine pick. Preventing AI metadata mistakes: A is loud at validate, save and parse; B is silent literal text, the shape the text-slot judge was written to close. Startup focus: A adds no capability and no gate, only a refusal on an existing key with reach 0; B costs nothing now and keeps the trap.",
"options": [
"A. Apply textSlotTemplateRefusal to the title translation in a follow-up, Clause-② yes (narrowing), ADR-0087 not-required (no-migration-prescription) — or in this PR if the seat re-declares its Clause-② line.",
"B. Leave title unjudged: a single-brace token in a translated heading renders as literal text, as it did under the client overlay."
],
"recommendation": "A, as a follow-up: measured reach 0, one judge for every engine-picked template, and the messages group's single-brace dialect makes the silent literal a live AI-authoring trap; B only defers a refusal that costs nothing today."
}
],
"out_of_scope_findings": [
"class: a · reach: the repo's own watchdog gate, measured: pnpm check:platform-checklist exits 1 at a425412, and the checklist plus the cited files diff 0 lines against origin/main e223152 · evidence: areas/access-security.json cites packages/metadata-protocol/src/protocol.ts#anonymousFormIntakeOrgScopeRefusal, #anonymousFormIntakeReopenRefusal and #envWideRawViewRows, each reported ABSENT SYMBOL (in addition to the attachments-storage canEdit line #22557 covers) · dedupe words: platform-checklist access-security absent symbol, anonymousFormIntakeReopenRefusal, envWideRawViewRows checklist anchor",
"carrier: the seat landing objectui#12075 and the .objectui-sha bump that carries it · noted, not filed: spec's resolveFlowScreenTitle (and FlowScreenLike) has no caller once objectui drops localizeScreen's heading overlay; this PR documents it as not for a served screen and retiring then",
"carrier: 承接者:无 · noted, not filed: RefusalI18nService now also serves the screen executor's heading and body text; the name is kept (renaming a published export is breaking) and its docblock says so"
]
}
objectstack-fleet commentedon Oct 10, 2026
✅ ACCEPT: PR #22626 at
a42541293c(Fixes #22507), ruling A. The contract review atCONTRACT_REVIEW_TIERis owed before it landsdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T06:31Z · holder of claim6093845640. Report:os-dev-report6094650615. Thread-read: 6094650615.Checked in the diff, not taken from the report (29 files, +882 / −169; 1051 changed lines, generated files included; no governed path):
screen-nodes.tsasksengine.renderFlowTextSlotfor thetitleanddescriptiontemplates atflowScreenCopyKey(flowName, node.id, key). That goes through the onesetI18nServiceSourcechannel the refusingendnode's pick uses, now generalised from its private form, withrenderRefusalMessagecalling the same method.config.title, because the onetitlekey covers the node label it falls back to.flowNamerenders as authored.flows.FLOW.screens.NODE_ID.descriptionis keyed and judged by the one text-slot judge. The family pin movesscreen.descriptionfromowedtokeyed, soOWEDisapproval.decisionOutputs[].labelalone, as the ruling pinned. The liveness rows and the generated references follow.descriptiontranslation over a screen with noconfig.descriptionistranslation-target-unknown(error).descriptionwhere it is authored.app-todozh-CN and ja-JP, andapp-crmzh-CN, socheck:i18n-coverageholds at 621 baselined with none new.service-automation, cli and lint each have their own entry andClause-②line.The pending note corrected (the DELIBERATE CORRECTION class):
.changeset/22507-flow-options-toasts-translation.mdis PR #22555's unreleased note on this same card. Its bullet "Still not keyed: a screen'sdescription… a translateddescriptionis still refused by the schema" is false once this PR lands, and is rewritten to "A screen'sdescriptionis keyed too, in the same release, and translated by the engine rather than the console".Check Changesetandcheck-empty-changesetare red on it by design. The same-head contract review that names this note and judges the rewritten sentence is the confirmation the landing rule takes.Claim-surface additions, accepted (each is a mechanical follow-through the report declares):
packages/spec/src/contracts/automation-service.ts(docblocks only);dropped-refinements.baseline.json(+8 declared sites, a build gate);examples/app-crm's zh-CN bundle (the coverage gate);content/docs/ui/translations.mdx(a sentence made false);The removed
guidance.descriptionentry is accepted: a guidance entry under a declared key can never fire, and the strict-object alias-integrity audit reads it as a false claim. The ruling's words "the server picks both" live in the describes and the flows docblock.Evidence, as reported, with its shape checked:
service-automation: 182 files / 2298 tests, with the new 13-testscreen-copy-translation.test.ts;check:generated, 15 of 15 current;check-empty-changesetred,check:platform-checklistred onmain's own state, anddual-build-cjs-loadsNOT MEASURED as dispatched;The dev's open question, decided by the seat:
titleshould be judged by the text-slot judge too. It is a narrowing of an existing key, and this PR is a widening, so it is not done here.titleis not judged by the text-slot judge, so a single-brace{name}in a translated heading renders literally while the description and refusal message beside it are refused #22627 with the dev's analysis (reach 0; one judge for every engine-picked template). Triage grades it, and it comes after this PR.out_of_scope_findings:check:platform-checklistred onmain(the threeaccess-security.jsonabsent symbols) → folded into check:platform-checklist is red on main #22594, the watchdog card that "carries the whole red". No new card.resolveFlowScreenTitleloses its last caller once objectui drops the heading overlay. Carrier: objectui#12075, where this card's consumer half is folded (6093869675). The retirement rides that landing.RefusalI18nServicenow serves the screen executor too, and its name is kept (renaming a published export is breaking) → Acceptance notes, no carrier.Next: the PR stays a draft, marked
needs:contract-review, until a same-head PASS is on record. The objectui consumer change, with file and line at the pin, is in the report'sobjectui_consumer_change; the seat posts it to objectui#12075 when this PR lands.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22626 →
d25b187203(Fixes #22507). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T07:21Z · holder of claim6093845640.d25b187203, a squash with one parent,ad1495796e. The queue did not eject it.6094676110, the contract review PASS6094764765ata42541293c, and the pre-queue record6094849350, withCheck Changesetred by design on the corrected feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555 note.d25b187203are blob-equal to the reviewed heada42541293c.titleanddescriptiontemplates in the run's locale throughAutomationEngine.renderFlowTextSlot, the onesetI18nServiceSourcechannel the refusal message also uses. It falls back to the authored template, and only then renders.flows.FLOW.screens.NODE_ID.description, judged by the one text-slot judge.OWEDisapproval.decisionOutputs[].labelalone, as ruled.screensrow note's 2026-09-27 sentence ("The screendescription… stay untranslated here by ruling") is superseded by this landing; mark it on the next ledger touch (contract review nit);titleis not yet judged by the text-slot judge → i18n(flows): a translated screentitleis not judged by the text-slot judge, so a single-brace{name}in a translated heading renders literally while the description and refusal message beside it are refused #22627;check:platform-checklistred onmain→ check:platform-checklist is red on main #22594.resolveFlowScreenTitleretires once that lands.descriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507 alone. spec:ListView.userActions.editInlinedefaults to true in v18 andeditInline: falseis the opt-out; objectui reads an absent key as on (maintainer ruling 2026-10-10, after objectui#5144) #22605 closed in the same queue group through PR docs(skills): a list view edits in place by default; a read-only list declares editInline: false (#22605, skill half) #22630.This act removes
pm:dispatched; the domain, priority and area labels stay.Generated by Claude Code