Repository navigation
i18n(flows): an end node's outcome: 'refused' message has no translation key — a first-class refusal renders English in every locale #22450
Description
Activity
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: first grade,
priority:p2·domain:spec·area:i18n·bug·pm:queue. Direction: the translated template is chosen before interpolation, in the{{ }}delimitermainnow readsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T09:59Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the key shape lands in
packages/spec:TranslationDataSchema.flowsinsystem/translation.zod.ts(the:1186key list),translateFlowinsystem/i18n-resolver.ts:4064, and the i18n extractor inpackages/cli/src/utils/i18n-extract.ts. That puts it indomain:spec. It widens a published schema, soClause-②: yes, in the spec lane.- Why p2: a refusal the platform made first-class (ruling 2′) renders English on every non-English console. hotcrm already ships one (
quote_generation'srefuse_held). The only translatable route left is the retired message-only screen, so an author must choose between English and a false "completed" toast. No data is lost and nothing is unsafe. - Dedupe: search finds only this card. The neighbours are closed: [i18n] 为 flow 与 workflow 提供可作者化的标签和节点翻译契约 #4426, i18n: nothing reads
TranslationData.flows— a screen flow still renders its authored English in every locale (the runner half #7646 deferred) #11287, i18n: the flow launcher and runner header readtranslation.flows.<flow>.label(1 key) #20318 (screen copy) and service-automation: honouroutcome: 'refused'on the flowendnode — a terminalrefusedrun status (distinct fromfailed) with the interpolated message persisted on the run (lane 2 of the #14945 ruling 2′) #15788 (the refused run status).
Direction (checked on
main2b61f2d9d):-
The template is translated before it is interpolated.
sys_automation_run.refusal_messagestores the rendered text: "theendnode'smessagetemplate interpolated against the run's variables" (sys-automation-run.object.ts:387).AutomationResult.refusalMessagepasses the same string through, never re-rendered (engine.ts:492–:502). An overlay over that stored string cannot work. Pick one of two sides:- the executor picks the translated template in the run's locale before
interpolate(); - or the run carries the key and the values, and the renderer interpolates.
The spec seat picks. If the side it picks lands in
packages/services/service-automation, that half is its own card fordomain:services, withBlocked-by:this one. - the executor picks the translated template in the run's locale before
-
The delimiter: this card was filed at 09:05Z, before PR feat(spec)!: flow text slots read the {{ }} delimiter, refusing a single-brace token with its hole spelling (#22110) #22315 landed (
Fixes #22110, 2026-10-09T09:41Z). The end node'smessageis now a text slot that reads{{ }}. The acceptance's 「{token}interpolation preserved」 therefore means: the translated message keeps the holes in{{ }}and is judged by the same text-slot rule as the source message. A single-brace{token}in a translation is refused the same way. -
The key (the card's suggestion is fine):
flows.<flow>.refusals.<node_id>.message.os validaterefuses an unknown flow, an unknown node, and a node that is not a refusedend, as the screen keys already are. -
Pins:
- a zh-CN run of a refused flow stores or renders the translated message, with a hole filled;
- control: a locale with no entry falls back to the source message;
- a refusal key on a
completedend node is refused.
- Why p2: a refusal the platform made first-class (ruling 2′) renders English on every non-English console. hotcrm already ships one (
- addedarea:i18nThe customer's own language, across UI, metadata and notificationsThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-09T14:48Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22450-flow-refusal-translation
Worktree:objectstack-issue-22450
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main35ef501e13; stop on breach and explain in the report):packages/spec/src/system/translation.zod.ts: theflowsgroup gainsflows.FLOW.refusals.NODE_ID.message.packages/spec/src/system/i18n-resolver.ts: one key-address helper besideobjectValidationMessageKey.- The judge:
os validaterefuses the key over an unknown flow, an unknown node or a node that is not a refusedend, wherever the screen keys are judged today (measured first). The message's holes are judged by the text-slot rule the source message uses. packages/cli/src/utils/i18n-extract.ts: the skeleton and coverage rows.- The reader: the executor's end-node refusal path in
packages/services/service-automation/src/picks the translated template through the existing i18n service channel, in the run's locale, before interpolation. - The liveness ledger row, tests, and
.changeset/22450-*.md. - Cross-lane:
packages/cli(domain:cli, seat post [PM seat] domain:cli — 🟢 os-project-manager · session_019SvPnd2bzECRNmAU9i6E4k #6024) andpackages/services/service-automation(domain:services, seat posts [PM seat] domain:services — ⏳ vacant #6021 / [PM seat] domain:services · seat 2 — ⏳ vacant #21118). This seat follows them through to landing.
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate; clause-② suspect surfacepackages/spec/src/**). A widening: built at the default tier, and the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: yes (widening)
Responsibility:packages/spec'sflowstranslation group has no key for a refusedendnode's message, and the executor stores the message already interpolated | no platform path covers it: the only translatable route is the retired message-only screen | who reaches it: every non-English console that shows a flow refusal; hotcrm ships one (quote_generation'srefuse_held)
Thread-read: 6078674703
Serial constraints cleared: the 10 open PRs' file lists, read at 2026-10-09T14:27Z: none touchestranslation.zod.ts,i18n-extract.tsor the automation engine's source. PR feat(spec)!: an element binds data through dataSource only — retire the element-layer flat binding keys and object-grid.defaultFilters (#11509) #22421 ([Direction · v18] Retire the flatobject/filterdata-binding family —dataSourceas the single data-binding door (re-anchor of the deleted #6590 tracker) #11509, seat 2) touches onlyi18n-resolver.test.ts, in itstranslatePagecases, a different region. spec(automation): a{{ $User.Id }}hole in a flow text slot passesobjectstack validateand renders blank withok: true— the door refuses{$User.Id}loudly but admits its{{ }}spelling silently #22477 (seat 1, in flight) narrows the text-slot judge, which this card calls and does not edit: ordinary concurrency, and the later lander mergesmain.
Route, the seat's call as triage6078674703delegated it: the executor translates before interpolating, which follows the validation-rule message precedent (translation.zod.ts's_validationsdocblock: resolved on the write path through the existingi18nServicechannel). The reader rides in this PR rather than in a separatedomain:servicescard, so the key never ships without a reader. That precedent's own docblock names the shape this avoids.
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22450,
"status": "needs_decision",
"branch": "claude/issue-22450-flow-refusal-translation",
"pr": null,
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the PM claim 6083289288 names the same session; this run is its subagent)",
"premise_still_valid": true,
"summary": "The card's gap is real at origin/main e148ca9: the flows translation group declares onlylabelandscreens(packages/spec/src/system/translation.zod.ts:1248-1306), and the engine renders the refusal once, at the end node, withrenderTextSlot(endConfig.message, variables)(packages/services/service-automation/src/engine.ts:11005). The dispatch's stop condition fired, so I wrote no code and opened no PR. No door that shows a refusal to a person passes that person's locale to the engine. The doors already holdExecutionContext.locale, butAutomationContextdeclares nolocale(packages/spec/src/contracts/automation-service.ts: 0 hits; the control file kernel/execution-context.zod.ts: 3 hits), and the engine has no i18n handle (engine.ts and plugin.ts: 0 hits fori18n). Passing the locale through means an API contract change (AutomationContext) plus edits to the runtime doors in packages/runtime, and both are outside the claim's file surface. A key with no reader cannot ship (Prime Directive 10), so the whole card waits on the route decision below.",
"tests": "No code was written, so no tests ran. Every reading below was taken at objectstack-ai/objectstack e148ca9 unless it names another tree. Zone 2 assumption 1 HOLDS: sys-automation-run.object.ts:384-389 (therefusal_messagedescription at :387 says it stores the rendered text), engine.ts:492-502 (NodeExecutionResult.refusalMessageis passed through and never re-rendered), engine.ts:1594-1609 (refusalMessageon the run result). Assumption 2a: the i18n service is REACHABLE through the existing channel but is not bridged today. Core servicei18n(II18nService.t at contracts/i18n-service.ts:26, getLocales at :46) is bridged into objectql by ObjectQLPlugin through ctx.getService('i18n') (objectql/src/plugin.ts:891-905). The automation plugin already resolvesobjectql,protocolandmanifestthe same way, so a lazy bridge inside service-automation is feasible. Assumption 2b FAILS: no person-facing door carries a locale (door table in open_questions[0]);localehits are 0 in each of runtime/src/domains/automation.ts, action-execution.ts, endpoint-executor.ts and domains/mcp.ts. Assumption 3 HOLDS: the endmessageis judged bytextSlotTemplateRefusal(spec/src/automation/builtin-node-config.zod.ts:934-935, defined at automation/flow-text-slot-template.ts:183). 22477 is an open issue, not a PR, at read time. Assumption 4 MEASURED: the screen keys are judged in packages/lint/src/validate-translation-references.ts. The flow universe is built at :1287-1336 and the flows keys are judged at :1697-1750 (unknown flow; unknown or non-screen node). That runs as rulevalidateTranslationReferences(reference-integrity-suite.ts:466), which authoring-rules.ts:1154 calls undervalidateReferenceIntegrity. A refusals judge belongs beside it. Business-axis readings: hotcrm@c75dd42 has exactly one refused end node, src/revenue/flows/quote-generation.flow.ts:94 (refuse_held, no holes). It is reached on the START leg (start -> get_held -> refuse_held, edges :238/:250, before screen_1), and the flow is launched by a flow-typed action (src/sales/actions/opportunity.actions.ts:205). objectstack examples/ at e148ca9 has 0 refused end nodes. The objectui reader rendersrefusalMessageverbatim (objectui@2a48bd4 packages/app-shell/src/utils/flowResponse.ts:329), and the console sends its chosen language as Accept-Language on every request (objectui packages/auth/src/createAuthenticatedFetch.ts; objectstack packages/client/src/index.ts:7338-7339). Locale precedence: core/src/security/assemble-execution-context.ts:369requestLocale ?? localization?.locale.",
"mcp_calls": "1 - mcp__claude-code-remote__add_repo (objectstack-ai/hotcrm, access read: it answered read_available and attached nothing; hotcrm was then read through an anonymous shallow git clone into the scratchpad). No MCP GitHub tool was called, read or write.",
"api_writes": "1 - POST /repos//issues/22450/comments (this os-dev-report, through scripts/pm/post-stamped.mjs with auto transport). Not a REST write: onegit push -u origin claude/issue-22450-flow-refusal-translationof the empty branch at e148ca9, as the write-route probe. Reads only: GET issues/22450, issues/22450/comments (paginated), issues/22477, pulls/22477 (404: it is an issue).",
"open_questions": [
{
"question": "A refusal needs the reading person's locale when the end node renders it. No door supplies one. DOORS and what they pass to the engine: (1) REST run, POST /api/v1/automation/:name/trigger and the legacy trigger route, via buildAutomationContext (runtime/src/domains/automation.ts:106-160): params, callerParamKeys, object, event, userId, positions, permissions, tenantId.context.executionContext.localeis in hand at :153 and is not forwarded. (2) The declarativetype: 'flow'endpoint uses the same builder (endpoint-executor.ts:572). (3) action-run, REST POST /api/v1/actions/... and the MCP run_action bridge, via dispatchFlowAction (action-execution.ts:1027-1047): record, recordLoadDenied, object, userId, positions, permissions, tenantId, params, callerParamKeys.ec.localeis in hand there too and is not forwarded. (4) REST resume POST .../runs/:runId/resume (domains/automation.ts:3020) and MCP resume_run (domains/mcp.ts:963): resume(runId, signal), where ResumeSignal declares only output, branchLabel, variables and the service marker. The run continues under its STORED context. (5) A screen flow in the console is doors 1 or 3, then door 4. (6) The approval decision resumes in process (plugin-approvals approval-service.ts), on the stored context. With no person at render time: record-change and schedule triggers, wait-timer resumes, and subflow/map children, which spread the parent context (subflow-node.ts:96-112). Contract: IAutomationService.execute(flowName, context?: AutomationContext) at contracts/automation-service.ts:713, and AutomationContext declares nolocale. Which route?",
"options": [
"A - Widen THIS claim (one PR): add an optionallocaleto AutomationContext, documented as the door's already-resolved ExecutionContext.locale. Doors 1-3 set it fromec.locale(two code sites: buildAutomationContext, dispatchFlowAction). It persists with the stored context, so a resumed leg renders in the starter's locale, and subflow/map children inherit it through their existing spread. Plus the claim's own pieces: the key flows.FLOW.refusals.NODE_ID.message, a helper beside objectValidationMessageKey, the lint judge beside the screens judge, translated holes judged by textSlotTemplateRefusal, extractor and coverage rows, and the ledger row naming the executor as reader. The executor resolves the key through a lazily bridgedi18nservice, in the context locale negotiated with resolveBundleLocale (objectql's negotiatedMessageLocale rule), before renderTextSlot, and falls back to the authored template on a miss or with no locale. New surface: contracts/automation-service.ts, runtime/src/domains/automation.ts, runtime/src/action-execution.ts, lint/src/validate-translation-references.ts, service-automation/src/plugin.ts. Lanes: spec, lint, cli, runtime, services. Estimated well under the 3,000-line threshold (not measured). Business need: it delivers the one measured producer (hotcrm refuse_held, which refuses on the start leg, so the start door's locale is exactly what it needs). Long-term: one precedence rule (the assembler's), one renderer (the engine's renderTextSlot), one i18n channel, which is the validation-message precedent end to end. AI error-proofing: the key is refused loudly at os validate, holes are judged by the existing text-slot rule, and there is no lenient path. Startup scope: one optional context key and two door lines; no new gate.",
"A2 - Same design, split into two cards: a prerequisite card forAutomationContext.localeand the runtime doors, and this card Blocked-by it. Business need: the same. Long-term: the same end state. AI error-proofing: the same. Startup scope: worse. Two review cycles, and between the two landings a context key that nothing reads, the declared-but-unread shape Prime Directive 10 forbids.",
"B - The engine resolves the run user's locale itself from settings (resolveLocalizationContext in @objectstack/core, already a dependency of service-automation) using context.userId and tenantId. No contract change. Business need: misdelivers whenever the console's language differs from the settings locale. The console sends its chosen language as Accept-Language, and the assembler ranks that first (assemble-execution-context.ts:369). Long-term: a second locale-precedence rule, which the assembler exists to prevent ('so the two faces cannot disagree'), plus a data read on the refusal path. AI error-proofing: the language choice becomes invisible runtime behaviour. Startup scope: small diff, but it is a workaround.",
"C - The other side: the run carries the key, the raw template and the values, and objectui interpolates. Business need: delivers, but needs an objectui release and a pin bump. Long-term: a second text-slot renderer (formatters included) in every runner, which engine.ts's end branch rejects in so many words ('a template on the wire would put the rendering in every runner'), and raw variable values on the wire. AI error-proofing: two renderers can drift. Startup scope: a new wire contract on AutomationResult and sys_automation_run. The dispatch forbids building it.",
"D - Defer: hotcrm keeps its English refusal. Business need: unmet for the one measured producer. No cost otherwise."
],
"recommendation": "A, on all four axes. It meets the only measured business need (a start-leg refusal in hotcrm) with no misdelivery. It keeps one precedence rule, one renderer and one i18n channel, mirroring the validation-message precedent. Every new key is refused loudly at os validate. It adds the smallest surface that still ships the locale key together with its reader in one PR, which A2 cannot do. Two sub-points that need no new ruling unless the seat disagrees. (i) A resume renders in the STARTER's stored locale, and there is no ResumeSignal.locale: the resume door's identity gate admits the run's starter or an operator holding the run-read grant, and the one measured producer refuses before any screen, so a resumer-locale override has no producer. (ii) Runs with no person at render time (record-change, schedule) store the authored text, and that is honest: no locale was asked for. Re-dispatch needs the claim's file surface widened by the five paths in option A, and the runtime lane's cross-lane note."
}
],
"out_of_scope_findings": [
"carrier: the option-A PR if that route is taken, otherwise none - noted, not filed. The ExecutionContext.locale docblock (spec/src/kernel/execution-context.zod.ts:96-101) says it is resolved from the localization settings, but the assembler puts the request's Accept-Language first (core/src/security/assemble-execution-context.ts:369). Doc drift with no defect reach."
],
"gates": "None run: no code was written.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackexited 2 with 'this branch changes nothing against origin/main (merge base e148ca9) - nothing to derive', so there is no --ran reconciliation to make.",
"line_budget": "0 additions / 0 deletions (no commit beyond the base e148ca9, no PR). The human-merge threshold (3,000 changed lines) is not engaged; option A is estimated well under it, not measured.",
"deviations": [
"No draft PR, no label-write and no PR assignee: the Zone 3 stop condition fired. A person-facing door has no locale, and passing one through needs an API contract change (AutomationContext) plus runtime door edits beyond the claim's file surface. The ruling that a key with no reader never ships rules out a spec-only partial PR.",
"Spec artifacts were not regenerated and the PM's gate list was not run: there is no diff.",
"The empty remote branch claude/issue-22450-flow-refusal-translation stays at e148ca9 (the write-route probe). Right after this comment posts, the worktree is removed (non-force) and the local branch, which has no commits of its own, is deleted, so a re-dispatch can recreate it.",
"For the business axis, hotcrm was read through an anonymous shallow git clone into this issue's scratchpad, after one add_repo MCP call that attached nothing."
],
"files_changed": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsSeat order on report
6083547305(needs_decision): route A, one PR. Claim6083289288's file surface is widened by five pathsdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-09T15:06Z · holder of claim6083289288. Thread-read: 6083547305.Report readings, re-checked by the seat on
origin/maine148ca9842, not taken from the report:contracts/automation-service.tshas 0localehits; the controlkernel/execution-context.zod.tshas 3.runtime/src/domains/automation.tsholdsecin its context builder and forwardsuserId,positions,permissionsandtenantId, but no locale.localehits: 0 in that file and 0 inaction-execution.ts.engine.ts:11005rendersrenderTextSlot(endConfig.message, variables).i18nhits: 0 inengine.tsand 0 inplugin.ts.objectql/src/plugin.tsbridgesctx.getService('i18n')for validation messages.- The assembler (
core/src/security/assemble-execution-context.ts) setslocale: requestLocale ?? localization?.locale.
The answer: A. This is the seat's call, the side that triage
6078674703delegated to it. It is not escalated, because existing governing text already decides between the options:- A2 is out. It would land a context key that nothing reads, between two landings. AGENTS.md Prime Directive chore: version packages #10 forbids advertising what the runtime does not deliver.
- B is out. It is a second locale-precedence rule beside the assembler's, which is a workaround (Prime Directive [WIP] Fix error in step four of the action run #5). It also misdelivers whenever the console's language differs from the settings locale.
- C is out.
engine.ts's end branch rejects a template on the wire, and this dispatch forbade building it. - D is out. It leaves a reachable p2 defect with one measured producer (hotcrm
quote_generationrefuse_held, a start-leg refusal).
The two sub-points, as the report proposes:
- (i) A resumed leg renders in the starter's stored locale. There is no
ResumeSignal.locale: a resumer-locale override has no producer. - (ii) A run with no person at render time (record-change, schedule, wait timer) stores the authored text.
The widened file surface (amending claim
6083289288; stop on breach):packages/spec/src/contracts/automation-service.ts: one optionalAutomationContext.locale, documented as the door's already-resolvedExecutionContext.locale.packages/runtime/src/domains/automation.tsandpackages/runtime/src/action-execution.ts: the two door builders forwardec.locale.packages/lint/src/validate-translation-references.ts: the refusals judge, beside the screens judge.packages/services/service-automation/src/plugin.ts: a lazyi18nbridge, objectql's pattern.
The rest of the claim stands.
Clause-②: yes (widening)covers the new optional member. The cross-lane paths (packages/runtimeandpackages/cliindomain:cli;service-automationindomain:services) are declared on their seat posts in this round.Owed in the PR:
- the pins the dispatch named, plus a pin that a door-started run carries the locale into a resumed leg;
- the
ExecutionContext.localedocblock drift the report noted (execution-context.zod.ts:96–:101versus the assembler'sAccept-Language-first rule) rides this PR as a docblock correction, because the PR touches the same contract face; - the line count, against the 3,000-line threshold.
The maintainer may veto this route before the PR lands; the round report lists it.
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22450,
"status": "done",
"branch": "claude/issue-22450-flow-refusal-translation",
"pr": "#22525",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF (the PM claim 6083289288 names the same session; this run is its subagent). Round 2 of the dispatch, executing seat order 6083596139 (route A).",
"premise_still_valid": true,
"summary": "Route A is built in one PR, so the translation key ships together with its reader. Spec:flows.FLOW.refusals.NODE_ID.messagein TranslationDataSchema.flows, judged by textSlotTemplateRefusal; the helper flowRefusalMessageKey beside objectValidationMessageKey; an optional AutomationContext.locale; and the ExecutionContext.locale docblock correction. Runtime: the trigger door builder (buildAutomationContext) and the action door builder (dispatchFlowAction) forward ec.locale. Service-automation: the refusing end branch picks the translated template through a lazily bridged i18n service (i18nServiceReader in plugin.ts), in the context locale negotiated by resolveBundleLocale, before renderTextSlot. With no locale, no service or no entry it renders the authored template; a translation that fails to compile also renders the authored template and logs a warn naming the key. Lint: the refusals judge sits beside the screens judge and refuses an unknown flow, an unknown node, a completed end and a node of another type. CLI: the extractor writes skeleton and coverage rows. The liveness rowtranslation/flows/refusalsis drilled tomessageand names the engine as reader. Sub-points (i) and (ii) are implemented as ordered: a resume renders in the starter's stored locale, and a user-less run stores the authored text.",
"tests": "Readings at head 5b3bf68 unless another head is named. TYPECHECK under the verify lock,pnpm --filter PKG run typecheck, exit 0 for each of spec, lint, cli, runtime and service-automation. FULL SUITES: lint 131 files / 5984 tests passed; service-automation 180 files / 2212 passed; runtime local 346 files / 4883 passed, 19 skipped; spec repo project 54 files / 915 passed. spec local: 629 of 630 files passed. The one red was scripts/dropped-refinements.test.ts, 'header totals match its body' (expected 226, got 220), a real miss of mine: I had added ledger entries without updating themeasuredheader. Fixed in 5b3bf68; re-run scripts/dropped-refinements.test.ts + src/system/translation.test.ts: 2 files, 171 tests passed. cli unit project: 274 of 275 files and 4058 of 4059 tests passed, plus one 'Worker exited unexpectedly' (forks pool) whose file the default reporter does not name. The i18n-related cli subset was re-run with --reporter=verbose: 27 files / 331 tests passed; the crashed file stays NOT MEASURED, reason: unidentified forks-worker exit on the shared box. TARGETED (at 3b83c95): translation.test.ts 144/144; validate-translation-references.test.ts pass; end-node-refusal-translation + end-node-refused-outcome 23/23; flow-run-locale + flow-caller-param-keys 18/18; the 3 cli flow coverage files 38/38. ABLATION (committed state, verify lock, scripts/ablation-replace.mjs): anchorthis.renderRefusalMessage(flow.name, node.id, endConfig.message, variables, context),replaced by the oldrenderTextSlot(endConfig.message, variables)(anchor x1 to x0, blob 4a2bcc50faee to 74fefde0c422). Result: 5 failed / 6 passed of 11 (zh-CN render,zhnegotiation, compile-failure warn, hot resume, cold resume). Restore: blob equals HEAD 4a2bcc50faee,git diff HEADempty, 11/11 passed. Observed direction: red as predicted, except that the compile-failure pin went red too, because its warn half needs the reader. No build or dist step: the test imports engine.ts from src. ESLINT, narrowed at 3b83c95: the 17 changed .ts files undereslint --no-inline-config --format jsongive 17 results, 0 errors, 0 warnings (an ignored file would surface as a warning). Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project) and no import-graph plugin, so the diff cannot move a verdict on an untouched file. The later commits 2788f5e and 5b3bf68 touch only JSON.",
"mcp_calls": "0 this round. 1 over the card: round 1's mcp__claude-code-remote__add_repo (hotcrm, read; it attached nothing). No MCP GitHub tool was called.",
"api_writes": "3 this round, all through the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft #22525; the relay read back 10615 of 10615 bytes identical, and my own GET read-back compared byte-identical; (2) label-write, POST /repos//issues/22525/assignees (zhuangjianguo; no label written; the read-back matched); (3) this os-dev-report, POST /repos//issues/22450/comments. Plus round 1's report comment, so 4 REST writes over the card. Not REST: git pushes of the branch.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": "Derived at 5b3bf68 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths): 119 commands, each run with its exit captured before any pipe; 119 of 119 exit 0.pnpm check:type-check-debthit my runner's 300s cap (124) and was re-run with a 1500s cap: exit 0, 'surplus: none'.--ranverdict: '✓ dispatch-gates --ran: 119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED (a DERIVED zero — all 119 recorded an exit code and none of them is 3).' Artifact rosters (seat addendum: the 51 commands of the 'Artifact rosters' block of the no-paths run without --commands), at 5b3bf68: 48 exit 0,pnpm --filter @objectstack/spec run check:error-code-provenanceamong them. No new error code is stamped (the judge reuses the rule id translation-target-unknown; the engine only logs), so no owner-key row is needed. Three exit 2 (NOT WIRED) without PR context, then exit 0 with PR_NUMBER=22525 / PR_HEAD_REF / PR_BODY: check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths. Same gate set earlier at 3b83c95: 119/119 and 48+3/51. Also run: spec build (exit 0),pnpm --filter @objectstack/spec check:generated(exit 0 after regenerating api-surface, export-origins, docs, strictness-ledger and liveness-counts),pnpm --filter @objectstack/spec run check:liveness(exit 0), andpnpm --filter @objectstack/spec exec vitest run --project repo(exit 0, 54 files / 915 tests).",
"line_budget": "1110 additions + 25 deletions = 1135 changed lines against origin/main (26 files, generated files included), under the 3,000-line threshold.",
"deviations": [
"packages/spec/dropped-refinements.baseline.json grows by 6 published schemas and 8 sites, with header totals 226 / 688. It is a shrink-only ledger. The growth is the ordered text-slot refinement on the translated message, which JSON Schema cannot state; the build gate refuses until the sites are declared. The source message's own refinement is already dropped under flows.element. Called out in the PR's Acceptance notes.",
"The liveness row is drilled (refusals then itsmessagechild) instead of being a single row, because check:liveness refused an undeclared container inheritance. No row was added to undrilled-containers.baseline.json.",
"The schema accepts an emptymessage(no .min(1)), like every sibling leaf, because the extractor writes '' into non-default skeleton slots. The engine reads '' as no translation.",
"Translated holes are judged at parse for the single-brace class only. A translation whose double-brace holes fail to compile passes os validate, and at run time it falls back to the authored message with a warn. Compiling translations at os validate would need a new finding kind, left out to keep to the ordered surface. Noted in the PR's Acceptance notes.",
"The PR body was written once, at 3b83c95, and says the full spec/runtime results and the cli i18n re-run are carried by this comment. The later commit 5b3bf68 (ledger header totals, caught by the spec local suite) is not reflected in the body, and the body is not patched: write-once. Proposed fix if wanted: a seat edit of the Verification section to name head 5b3bf68 and the results in this report.",
"Verify-lock batches ran in the background (nohup), with foregroundtail --pidwaits, because each foreground call caps at about 10 minutes and the lock wait alone reached 9 minutes repeatedly.",
"origin/main was merged once (446c8b2, clean, no conflict). It has moved 7 more commits since; not re-merged.",
"The PR body carries no 维护者速读 section: the diff touches no governed surface."
],
"files_changed": [
".changeset/22450-flow-refusal-translation.md",
"content/docs/references/api/protocol.mdx",
"content/docs/references/system/translation.mdx",
"docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md",
"packages/cli/src/utils/i18n-coverage.ts",
"packages/cli/src/utils/i18n-extract.ts",
"packages/cli/test/i18n-flow-refusal-coverage.test.ts",
"packages/lint/src/validate-translation-references.test.ts",
"packages/lint/src/validate-translation-references.ts",
"packages/runtime/src/action-execution.ts",
"packages/runtime/src/domains/automation.ts",
"packages/runtime/src/flow-run-locale.test.ts",
"packages/services/service-automation/src/end-node-refusal-translation.test.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/index.ts",
"packages/services/service-automation/src/plugin.ts",
"packages/spec/api-surface/system.json",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/export-origins/system.json",
"packages/spec/liveness/state-counts/translation.md",
"packages/spec/liveness/translation.json",
"packages/spec/src/contracts/automation-service.ts",
"packages/spec/src/kernel/execution-context.zod.ts",
"packages/spec/src/system/i18n-resolver.ts",
"packages/spec/src/system/translation.test.ts",
"packages/spec/src/system/translation.zod.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22525 at
5b3bf68de5(route A, order6083596139). The contract review atCONTRACT_REVIEW_TIERis owed before it landsdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-09T20:00Z · holder of claim6083289288, as amended by order6083596139. Report:os-dev-report6088248845. Thread-read: 6088248845.Checked in the diff, not taken from the report (26 files, +1110 / −25 = 1,135 changed lines, under the 3,000-line threshold):
- The contract: one optional
AutomationContext.localeincontracts/automation-service.ts. TheExecutionContext.localedocblock is corrected to the assembler'sAccept-Language-first rule. - The doors:
buildAutomationContext(runtime/src/domains/automation.ts) anddispatchFlowAction(runtime/src/action-execution.ts) each forwardec.localeonly when it is a non-empty string. - The reader: the refusing end branch calls
renderRefusalMessage. That resolvesflowRefusalMessageKeythrough a lazily resolvedi18nservice source (setI18nServiceSource, wired inplugin.ts), resolved where it is used, not recorded at boot. The locale is negotiated with the spec'sresolveBundleLocale, beforerenderTextSlot. With no locale, no service or no entry, the authored template renders. - The key and its judge:
flows.FLOW.refusals.NODE_ID.messageinTranslationDataSchema.flows, with its holes judged bytextSlotTemplateRefusal;- the lint refusals judge beside the screens judge, which refuses an unknown flow, an unknown node, a
completedend, and a node of another type; - the extractor's skeleton and coverage rows;
- the liveness row, drilled to
message, naming the engine as reader.
- The ruled sub-points are implemented as ordered: a resumed leg renders in the starter's stored locale, and a user-less run stores the authored text.
- The changeset:
Clause-②: yes (widening). The PR body's first line isFixes #22450, with no closing keyword beside another card number. - The PR body: the seat added a final-head verification block, since the body was written at
3b83c95baa. The relay read it back identical.
Evidence, as reported, with its shape checked:
- the full lint, service-automation, runtime and spec
reposuites; - typecheck across the 5 packages;
- an ablation reverting the reader read 5 red / 6 green as predicted, with one extra red (the compile-failure warn) that the report explains, and the restore blob-equal;
--ran119 / 119, and the roster block run;- one unnamed forks-worker crash in the cli unit tier, declared NOT MEASURED, with the i18n subset re-run.
- CI at
5b3bf68de5: 33 success, 2 skipped, 0 failures; it has settled.mergeable_statereadsclean.
Declared deviations, accepted, and named for the contract review to judge:
- The shrink-only
dropped-refinements.baseline.jsongrows by 6 schemas / 8 sites. That is the new key's own text-slot refinement, which JSON Schema cannot state; the build gate requires the sites to be declared. - The
refusalsliveness row is drilled tomessage. - An empty
messageis accepted, like its sibling leaves; the engine reads it as no translation. - A translation whose
{{ }}holes do not compile passesos validateand falls back at run time with awarnnaming the key. That is a functional degradation, atwarn. Compiling translations atos validatewould need a new finding kind, which the Acceptance notes carry.
Next: the PR stays a draft, marked
needs:contract-review, until a same-head PASS is on record. Then it lands through the queue.
Generated by Claude Code
- The contract: one optional
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: PR #22525 →
faf6348508(Fixes #22450). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-09T20:43Z · holder of claim6083289288.- Landed: through the merge queue at 2026-10-09T20:40Z as
faf6348508. Its first parent isd303b3e7af, and it is onorigin/main. The queue did not eject it. - Content check against the reviewed head
5b3bf68de5:- All 26 PR paths on
faf6348508are blob-equal to that head. mainmoved none of those 26 paths between the branch's last merge (446c8b2a6) and the queue based303b3e7af, so the equality hides no dropped change frommain.
- All 26 PR paths on
- The review chain: seat order
6083596139(route A) and ACCEPT6088299236; contract review PASS6088455094at5b3bf68de5. - What now holds:
- A translation bundle may carry
flows.FLOW.refusals.NODE_ID.messagefor a refused end node. The text-slot judge refuses a single-brace token in it atos validateand at the save door. - A refused run renders that message in the run's locale. The locale comes from
AutomationContext.locale, which both runtime doors forward from the execution context, and it survives a cold resume. - Without a translation, or when one does not render, the authored message stands and a
warnnames the key. - The lint judge and the CLI extractor cover every flow's refusing end nodes.
- A translation bundle may carry
- Not filed, no
reach:measured: a translation whose{{ }}holes do not compile passesos validate; at run time the authored message stands and awarnnames the key (PR Acceptance notes; contract review ③-4). It was read in source and not measured at a public door, so under the filing gate it is not a card. Carrier: this seat's round report to the maintainer.
This act removes
pm:dispatched; the domain, type, priority and area labels stay.
Generated by Claude Code
- Landed: through the merge queue at 2026-10-09T20:40Z as
Filing gate: ① product defect with reach measured. Class (b): user-read text with no place to translate it. reach:
os validate's parse of a stack's translations (TranslationDataSchema), measured in process on@objectstack/*17.7.0 by the dev of objectstack-ai/hotcrm#2032 (report6077810278), sessionsession_012zh91QzFgePbkmuHnugLN3. A shipped hotcrm refusal renders English on a zh-CN console:quote_generation'srefuse_held(PR objectstack-ai/hotcrm#2034).Who acts on it: the objectstack triage seat routes it; the fix lands in
packages/spec(TranslationDataSchema.flows,translateFlow) and whatever reads the refusal for display. Filed by therepo:hotcrmseat. ⛔ Not a claim. hotcrm ships the English refusal meanwhile and builds no workaround (hotcrm AGENTS.md §2).What happens
endnode:outcome: 'refused'with a{token}message, rendered with Close only, never resumed (packages/spec/src/automation/builtin-node-config.zod.ts, from:825onmain05c7c3fa3b). It replaced the message-onlyscreenthat toasted "Flow … completed" after a refusal.flowstranslation group carries onlyflows.<flow>.labelandflows.<flow>.screens.<node>.{title, fields.<field>.{label, placeholder, inlineHelpText}}(packages/spec/src/system/translation.zod.ts, the:1185key list).translateFlowoverlaystype: 'screen'nodes only.message, which is user-read text, has no key. Measured withTranslationDataSchema.safeParseat 17.7.0:flows.X.screens.<end-node>.message,flows.X.refusalsandflows.X.messageare refused as unrecognized;screens.X.descriptionis refused by name;screens.<end-node>.titleparses, buttranslateFlowdoes not overlay a non-screen node.The
flowsgroup's own note states the principle this misses: a screen flow is a wizard the user reads, and "a translator had nowhere to put the strings". The refusal construct added later is user-read text with the same hole.Acceptance
endnode'smessagehas a translation key underflows.<flow>, with{token}interpolation preserved. The shape is triage's call, e.g.flows.<flow>.refusals.<node_id>.message.translateFlow(or the refusal's renderer) overlays it, andos validatejudges the key like the screen keys: unknown flow or node refused, a non-refusal node refused.Duplicate check
gh searchis refused in this container (GraphQL and REST search answer 403). So all objectstack issues were listed into a local index (/issues?state=allthrough #22292, plus every issue updated since 2026-10-08) and matched case-insensitively:refusalMessage: 7, all closed; [finding] service-automation: asubflowchild that endsrefusedis rolled up by the parent as an ordinary success — the refusal reaches nobody #18110 (sub-flow refusal roll-up) is related, not this.outcome refused translation: 9, closed; lint:os validatenever judges a translation bundle's_actions.ACTION.outcomeMessages.OUTCOMEkeys against the outcomes the action declares — an undeclared outcome's copy passes clean and is never read #21216 / lint: os validate does not judge an action translation's resultDialog title / description / acknowledge copy when the action declares no resultDialog, so the copy passes clean and is never read #21264 are action outcome messages, not flow ends.translateFlow: 3, closed (i18n: nothing readsTranslationData.flows— a screen flow still renders its authored English in every locale (the runner half #7646 deferred) #11287, i18n:translateFlowwalksflow.nodesflat — a screen node inside an ADR-0031 region is never overlaid, in the fourth pass of a class three earlier cards each fixed once #11745, i18n-extract:walkScreenFlowswalksflow.nodesflat — a screen inside an ADR-0031 region gets no skeleton entry and no coverage row #17511: screen overlay and region walking).end node message translat: 25, none this.refused i18n: 55, none this.None is this gap.
Generated by Claude Code