You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
cloud-connection panel: Disconnect lands on "Not connected" even when the server answers revoked:false, so the cloud-side connection stays active with no warning #12860
Filing gate: ① a user-visible defect with a named landing and a measured reach. Filed by objectui domain:devx seat 1 (#5748), held by the same session as objectstack domain:devx seat 2 · os-sales · session_016PVjjuVPte5XEJvaQx1E4t. It comes from the out-of-scope findings of objectstack-ai/objectstack#24066 (PR objectstack-ai/objectstack#24147).
Reader: the domain:ui seat, in packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx, at disconnect(). On main4b3d6edae, line 261 calls unbind and then refreshes the status, and it never reads the answer.
The objectstack#24066 dev measured the server half on a dev boot of examples/app-showcase at objectstack main9ffe8af09, with the control plane unreachable:
POST /api/v1/cloud-connection/unbind answered {revoked:false, cleared:true}.
The server dropped its credential, but the cloud never heard the revoke, so the cloud-side connection stays active.
The panel half is read from code. No console was built for a browser check.
disconnect() ignores revoked.
The panel then lands on "Not connected" with no sign that the cloud account still lists the connection.
Done when
The panel tells the operator when the revoke did not reach the cloud. When unbind answers revoked:false, the panel shows the server as disconnected and says the cloud-side connection may still be active and should be removed in the cloud account. The message uses the existing i18n and alert blocks, with no new widget.
revoked:true is unchanged.
A test pins both answers.
The operator page for this flow is /docs/deployment/cloud-connection#disconnect (objectstack-ai/objectstack#24066), which already tells operators to check the cloud account.
Why p3: it needs the control plane to be unreachable at the moment of a disconnect. The operator page already tells operators to check the cloud account (/docs/deployment/cloud-connection#disconnect).
Premise: the panel's disconnect() calls unbind and never reads revoked (read from source, as the card says).
Claim: PM loop round 3 (priority:p3 bug, area:devpath; filed by objectui domain:devx seat 1 from objectstack-ai/objectstack#24066's findings and graded by the triage seat, 6230920138: the filer's domain:ui stands, and both unbind answers are covered now that objectstack-ai/objectstack#24148 has landed)
Session: session_01SJn1GEkrbbBQkhGC26wEDw
Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12860-unbind-revoke-notice
Worktree: objectui-issue-12860 (in the cloud session's own container)
Domain: domain:ui
Seat: domain:ui#4
File surface:
packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx: disconnect() reads the unbind answer.
revoked:false lands on the not-connected view with a notice, in the panel's existing alert idiom. The notice says the server is disconnected but ObjectStack Cloud may still list the connection, and to remove it in the cloud account.
packages/i18n/src/locales/*.ts: the notice's key or keys under cloudConnection, in all ten packs.
Tests beside the panel (cloud-connection/__tests__/), and changesets for @object-ui/app-shell and @object-ui/i18n (patch).
⛔ The bind flow, the status read, the bound card's fields and the route BASE are unchanged. No new widget. packages/components/src/ui/** stays out.
Stop on breach; explain in the report.
Container & model: S, mode:cloud, model: opus (cloud on the maintainer's stance 「可以的话,ui 后续开发尽量用云 session,并且要求截图给我看」; lane scope on 「你也可以接ui车道的卡」; batch 3 on 「当前任务处理完,后续并发降到3」, this session's chat, 2026-10-16)
Clause-②: yes
Clause-② reason: one or two new @object-ui/i18n keys under cloudConnection in all ten packs. No exported type or prop moves.
Responsibility: n/a — after a Disconnect whose revoke never reached the cloud, the panel says "Not connected" and nothing warns that the cloud still lists the connection
Thread-read: 6230920138
Serial constraints cleared:
No open objectui PR touches packages/app-shell/src/console/cloud-connection/**, and no pm:dispatched objectui claim names it (read at dispatch).
This seat's objectui#12725 phase 4 writes metadata-admin/i18n.ts, not the ten packs. No open PR writes the packs at dispatch. If one lands first, this branch merges origin/main.
{
"issue": 12860,
"status": "needs_decision",
"branch": "claude/issue-12860-unbind-revoke-notice",
"pr": null,
"session": "session_01B9twfw7wdL98RoK5XCyGXZ (dispatched by PM seat domain:ui#4, session_01SJn1GEkrbbBQkhGC26wEDw)",
"premise_still_valid": true,
"summary": "The code for rulings 1-3 and 5 is done and pushed (ada4643): disconnect() reads the unbind answer. revoked:false lands on Not connected with a warning notice. revoked:true is unchanged. A 403 PERMISSION_DENIED keeps the bound card and shows a localized 'needs Setup access' line. I am not handing it over as done, because ruling 4 (put the four new strings under cloudConnection) cannot pass a repo rule. ADR-0060 lists cloudConnection as a frozen namespace, and AGENTS.md section 1 says no key may be added under it in any locale. packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts fails all 10 per-locale cases ('no key is added under a frozen namespace unless the live list carries it'). AGENTS.md bans appending to product-specific.allowlist.json to make it pass, and raising the ceiling is an ADR-0060 amendment (a governed file) that the maintainer must approve. That file is also outside the claimed file surface. I checked all four PM mechanism assumptions on main b279016 and all four hold. On main, a 403 already shows the error phase with the server's English message, not 'Not connected'.",
"tests": "At HEAD ada4643. New file packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.unbindAnswer-12860.test.tsx, 7 cases, all passing: revoked:false shows the notice (en), revoked:false shows the notice (zh), 403 setup.access is not Not connected and is localized (en), the same (zh), revoked:true shows no notice, the notice survives an unbound refresh and clears on the next Connect, and CONTROL: a held-session 403 (PASSWORD_EXPIRED, a real AuthGateCodeSchema member) keeps the error phase and the server's message. Full run 'pnpm exec vitest run packages/app-shell/src/console/cloud-connection/ packages/i18n/ --maxWorkers=2' under os-verify-lock: VERDICT command-exit 0, Test Files 88 passed (88), Tests 1347 passed | 13 skipped (1360). CONTROL: the bind-flow pins in bindError, bindErrorLocale, displayLocale-10331 and unboundBody-12655 pass unchanged inside that run. Ablation: I committed the fix first, then used a Python replacer that asserts its anchor appears exactly once, with a trap that restores via 'git checkout HEAD --'. The packs and panel resolve from source through the vitest alias (@object-ui/i18n points to packages/i18n/src), so no dist rebuild was needed. A1: revoked===false changed to a constant false; 3 cases went red (notice en, notice zh, survives/clears). A2: constant true; revoked:true went red. A3: SETUP_ACCESS_REFUSED changed to '__never__'; 403 en and zh went red. A4: removed the setRevokeNotReached(false) call in connect(); survives/clears went red. A5: matched any code instead of PERMISSION_DENIED; CONTROL went red. Each case went red exactly as predicted. Afterwards 'git diff HEAD' was empty (diff-empty=yes). For A4 the grep -cF count reads 2/2 because the anchor spans two lines; the proof for A4 is the assert-once anchor plus the red result. Not passing: product-neutral-renderer.ratchet.test.ts exits 1 with 10 failures. That is the blocker described in open_questions.",
"gates": "Each exit code captured before any pipe. pnpm exec vitest run (cloud-connection/ + packages/i18n/) = 0. vitest product-neutral-renderer.ratchet.test.ts = 1 (10 failed, BLOCKER). pnpm --filter @object-ui/i18n type-check = 0. @object-ui/app-shell type-check = 0 (echoed 'tsc --noEmit && tsc -p tsconfig.test.json'). @object-ui/i18n build = 0. @object-ui/app-shell build = 0. Both builds ran after the dependency closure built: 'turbo run build --filter=@object-ui/app-shell^... --concurrency=2' under the lock, VERDICT command-exit 0, 28/28. A first i18n build attempt before the closure existed exited 2 with TS6305 (core dist not built); that is not a measurement. Lint, normal mode: app-shell 'eslint src/console/cloud-connection/' = 0, 6 warnings, all no-explicit-any on existing catch (err: any) sites or the existing mount-effect setState, none on added lines. 'pnpm --filter @object-ui/i18n lint' = 0 (33 warnings). 'eslint packages/i18n/src/locales/' = 0. pnpm check:i18n-keys = 0. pnpm check:i18n-drift = 0 ('No en value changed in this range'). node scripts/check-changeset-presence.mjs = 0. Narrowed, declared: I linted app-shell over the touched directory only, not the whole package; repo-wide 'pnpm lint' is CI's. CI check-runs: NOT MEASURED, because no PR is open (degraded channel).",
"mcp_calls": "3: claude-code-remote add_repo (objectstack, access read; answered read_available, nothing attached); github issue_read method=get #12860; github issue_read method=get_comments #12860. No write tools.",
"api_writes": "0 REST writes. git push only: feature branch twice (the empty branch, then ada4643), evidence branch once (fa619dc).",
"open_questions": [
{
"question": "Ruling 4 (four new keys under cloudConnection) conflicts with ADR-0060's frozen namespace, and product-neutral-renderer.ratchet.test.ts fails in all ten locales. Which way should the copy land?",
"options": [
"A. The maintainer approves an ADR-0060 frozen-baseline amendment that admits exactly cloudConnection.bound.disconnectNeedsSetupAccess and cloudConnection.unbound.revokeNotReached.{disconnected,cloudMayList,removeInCloud} (a governed docs/adr PR the seat drafts). Then this branch adds the same four entries to product-specific.allowlist.json; the PM widens the file surface by that one file. Real business need: measured, see live evidence (the real showcase /unbind answered revoked:false). Long-term: grows a frozen product surface by four leaves, against ADR rule 3 ('may only shrink'), but through the ADR's own approval path, not a workaround. AI-error axis: best of the options, because the ratchet stays strict and the exception needs a named human approval. Startup focus: smallest change that delivers the card. Cost: one governed ADR edit, plus one allowlist edit here.",
"B. Move the copy into a new neutral namespace (for example a generic connection-revoke block) and rephrase it to drop the product name. The ratchet goes green with no ADR edit. Real business need: same as A. Long-term: this is the loophole ADR-0060 names ('product copy filed under a neutral name ... is review's to catch'), and the panel itself stays product code. AI-error axis: worst, because it teaches routing around a gate by renaming. Startup focus: no added scope, but it is a workaround. Not recommended.",
"C. The producer composes it: the cloud-connection plugin's page metadata carries the copy (ADR-0060 rule 1), which needs a contract door for a conditional, answer-driven notice in objectstack (needs:contract-review). Long-term: the north-star shape. Startup focus: far larger than a p3 needs. Not now.",
"D. Close as won't-do-in-UI: the operator page /docs/deployment/cloud-connection#disconnect already tells operators to check the cloud account, and a 403 already avoids 'Not connected' on main. Startup focus: strongest. Real business need: weakest, because the panel keeps showing a misleading 'Not connected' with no hint."
],
"recommendation": "A, because the defect is a measured, user-visible false state ('Not connected' while the cloud still lists the connection). ADR-0060's ratchet exists to make exactly this kind of growth a named, human-approved exception, which is stricter than B's rename and much smaller than C. Under A the branch needs one more commit (four allowlist entries), and the pr_body_draft first line changes to 'Fixes #12860'."
},
{
"question": "Choices I made under the rulings, recorded for review (not blocking).",
"options": [
"403: I built the preferred shape. The bound card stays and an AlertCircle row in destructive tokens sits above Disconnect. The match is on error.code === 'PERMISSION_DENIED', not on status 403, because ADR-0069's held-session refusal is also a 403 with its own code and an actionable message; that refusal keeps today's error phase (the CONTROL case pins this).",
"Notice: an AlertCircle row in border-warning/40 bg-warning/10 with the icon in text-warning (the --color-warning theme token from apps/console/src/index.css; the body text stays foreground for contrast), role=status. The notice state is held outside Phase, so it survives any refresh that finds the server unbound and clears on Connect or on a bound status.",
"No docs link: I measured zero docs URLs in the panel and in app-shell (no docs base exists), so I invented none.",
"Three sentences, one key each, joined with a space. In zh this leaves a space after the full stop ('。 '), visible in the after screenshot; the maintainer may prefer different joining for CJK.",
"One changeset file lists both @object-ui/app-shell and @object-ui/i18n as patch."
],
"recommendation": "Keep as built unless the maintainer wants the CJK join changed."
}
],
"out_of_scope_findings": [],
"deviations": [
"Ruling 4 cannot pass ADR-0060 / AGENTS.md section 1 (frozen namespace cloudConnection); hence needs_decision. I did not append to the allowlist (banned, and outside the file surface).",
"Contract conflict, file vs dispatch: os-dev.md says the dev opens the draft PR and posts os-dev-report on the card first. I followed the dispatch's degraded channel (no PR, no card comment, body below), because the fleet-write relay it names is refused (403) in this container. I did not probe the relay myself.",
"Worktree: the shared checkout had the branch name checked out with no commits of its own. I detached the shared checkout and deleted the empty local branch, then ran the dispatch's 'git worktree add' command as given.",
"Lock path: AGENTS.md names /home/user/objectstack/scripts/pm/os-verify-lock.sh, but in this container the read clone lives at /home/user/objectstack-ai/objectstack, so I used that path. objectui has no scripts/ablation-replace.mjs or ablation-dist-preflight, so I used an inline assert-once replacer with a trap; no dist was involved because tests resolve from source.",
"Live run: start-backend.sh boots with OS_CLOUD_URL=off, which does not mount CloudConnectionPlugin (/status answered 404 ENDPOINT_NOT_FOUND). I stopped it and restarted the same prepared showcase (published @objectstack 18.0.0-next.2) on :4610 with OS_CLOUD_URL=http://127.0.0.1:9, an unreachable control plane, so the plugin mounts."
],
"files_changed": [
"packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx",
"packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.unbindAnswer-12860.test.tsx (new)",
"packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts (+4 keys each under cloudConnection)",
".changeset/12860-cloud-connection-unbind-answer.md (app-shell + i18n, patch)"
],
"screenshots": [
"before (main b279016 panel, real unbind answered revoked:false, zh-CN light): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/before-main-revoked-false-zh.png?raw=true",
"after (branch, same real answer, notice shown): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/after-branch-revoked-false-zh.png?raw=true",
"after (branch, 403 PERMISSION_DENIED fulfilled, bound card plus localized line): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/after-branch-403-zh.png?raw=true",
"bound card before clicking Disconnect (status fulfilled): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/bound-before-disconnect-zh.png?raw=true"
],
"line_budget": "n/a",
"pr_title_draft": "fix(console): Cloud Connection panel reads the unbind answer on Disconnect (objectui#12860)",
"pr_body_draft": "(held by the seat with the branch until the decision below is ruled; the dev's draft is kept for the PR the ruling opens)",
"head": "ada4643"
}
Relayed by seat domain:ui#4 (session_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final report. This is the degraded channel: the session pushed claude/issue-12860-unbind-revoke-notice at ada4643 and returned status: needs_decision, so no PR is opened yet. The conflict is the seat's own ruling 4 (the new copy under cloudConnection), which ADR-0060 freezes. The seat measured it before writing the decision that follows this comment.
What was measured (dev report relayed above, objectui ada4643):
docs/adr/0060-product-neutral-renderer.md rule 3 says the frozen surface "may only shrink". Its frozen-baseline block lists cloudConnection with 26 keys.
AGENTS.md: "⛔ Never append to that list to turn the test green … growing it is an ADR amendment the maintainer approves."
docs/adr/** is a governed surface (check-governed-queue-guard).
② 项目长远合理性:A 走 ADR 审批路给冻结面加 4 条,迁移时随之删除;C 是终局但重;B 是绕路。
③ 防 AI 犯错:A 让门禁严格、例外具名;B 教 AI 改名绕门禁。
④ 创业阶段不扩散:D 零成本;A 最小交付;C 最大。
After the ruling:
A: the seat sends the same dev branch one more commit, and the PR then carries Fixes #12860 and the ADR edit, so it is governed and waits for your approval to merge:
the ADR-0060 frozen-baseline block admits exactly the four keys, citing this ruling;
product-specific.allowlist.json gets the same four entries;
the zh join drops the space after 「。」, which the dev flagged.
D: the seat closes this card as not planned with this record, and the branch ada4643 stays unmerged.
B or C: the seat records the ruling and re-scopes the card. C files the objectstack contract card first.
In every case this card leaves needs-user-decision in the same write as the ruling record.
The claim 6234200417 is released in this act: the card is back on needs-user-decision, with no assignee. The feature branch claude/issue-12860-unbind-revoke-notice (ada4643) holds the finished code. The evidence branch claude/issue-12860-evidence (fa619dc) is never merged.
Ruling — D · close as not planned · 2026-10-17T16:04Z
The maintainer, in seat domain:ui#4's session chat (session_01SJn1GEkrbbBQkhGC26wEDw), answering decision 6234491015 verbatim: 「D」.
What D means here:
Nothing ships for this card. The four new cloudConnection.* keys would have grown ADR-0060's frozen surface, which rule 3 says "may only shrink". The maintainer kept that rule over the notice.
ADR-0060, its frozen baseline and product-specific.allowlist.json are unchanged.
The finished code stays unmerged on claude/issue-12860-unbind-revoke-notice (ada4643), and the screenshots stay on claude/issue-12860-evidence (fa619dc). No PR is opened. The dev session session_01B9twfw7wdL98RoK5XCyGXZ stays archived.
If this surface is ever re-opened, the route is the one ADR-0060 rule 1 names: the cloud-connection plugin supplies its copy through page metadata (decision row C). It is not a renamed neutral namespace (row B).
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: first grade,
priority:p3·area:devpath·pm:queue· the filer'sdomain:uistands · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T06:55ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
/docs/deployment/cloud-connection#disconnect).disconnect()callsunbindand never readsrevoked(read from source, as the card says).security) gates the sameunbindroute on Setup access. Its refusal (a403) is a second answer this panel should not read as "Not connected". The claimant covers both answers if #24148 has landed by then.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 3 (
priority:p3bug,area:devpath; filed by objectuidomain:devxseat 1 from objectstack-ai/objectstack#24066's findings and graded by the triage seat,6230920138: the filer'sdomain:uistands, and bothunbindanswers are covered now that objectstack-ai/objectstack#24148 has landed)Session:
session_01SJn1GEkrbbBQkhGC26wEDwAccount:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12860-unbind-revoke-noticeWorktree:
objectui-issue-12860(in the cloud session's own container)Domain:
domain:uiSeat:
domain:ui#4File surface:
packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx:disconnect()reads theunbindanswer.revoked:falselands on the not-connected view with a notice, in the panel's existing alert idiom. The notice says the server is disconnected but ObjectStack Cloud may still list the connection, and to remove it in the cloud account.revoked:trueis unchanged.403refusal (cloud-connection: bind/start, bind/poll and unbind admit any signed-in user (no Setup Access check), and GET /status serves the approving account's email and organization id to anonymous callers objectstack#24148'ssetup.accessgate) never reads as "Not connected".packages/i18n/src/locales/*.ts: the notice's key or keys undercloudConnection, in all ten packs.cloud-connection/__tests__/), and changesets for@object-ui/app-shelland@object-ui/i18n(patch).BASEare unchanged. No new widget.packages/components/src/ui/**stays out.Stop on breach; explain in the report.
Container & model:
S,mode:cloud,model: opus(cloud on the maintainer's stance 「可以的话,ui 后续开发尽量用云 session,并且要求截图给我看」; lane scope on 「你也可以接ui车道的卡」; batch 3 on 「当前任务处理完,后续并发降到3」, this session's chat, 2026-10-16)Clause-②: yes
Clause-② reason: one or two new
@object-ui/i18nkeys undercloudConnectionin all ten packs. No exported type or prop moves.Responsibility: n/a — after a Disconnect whose revoke never reached the cloud, the panel says "Not connected" and nothing warns that the cloud still lists the connection
Thread-read: 6230920138
Serial constraints cleared:
unbindroute'ssetup.accessgate) is closedcompleted, landed as objectstack PR #24167. Its403is the second answer this panel covers.packages/app-shell/src/console/cloud-connection/**, and nopm:dispatchedobjectui claim names it (read at dispatch).metadata-admin/i18n.ts, not the ten packs. No open PR writes the packs at dispatch. If one lands first, this branch mergesorigin/main.objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{ "issue": 12860, "status": "needs_decision", "branch": "claude/issue-12860-unbind-revoke-notice", "pr": null, "session": "session_01B9twfw7wdL98RoK5XCyGXZ (dispatched by PM seat domain:ui#4, session_01SJn1GEkrbbBQkhGC26wEDw)", "premise_still_valid": true, "summary": "The code for rulings 1-3 and 5 is done and pushed (ada4643): disconnect() reads the unbind answer. revoked:false lands on Not connected with a warning notice. revoked:true is unchanged. A 403 PERMISSION_DENIED keeps the bound card and shows a localized 'needs Setup access' line. I am not handing it over as done, because ruling 4 (put the four new strings under cloudConnection) cannot pass a repo rule. ADR-0060 lists cloudConnection as a frozen namespace, and AGENTS.md section 1 says no key may be added under it in any locale. packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts fails all 10 per-locale cases ('no key is added under a frozen namespace unless the live list carries it'). AGENTS.md bans appending to product-specific.allowlist.json to make it pass, and raising the ceiling is an ADR-0060 amendment (a governed file) that the maintainer must approve. That file is also outside the claimed file surface. I checked all four PM mechanism assumptions on main b279016 and all four hold. On main, a 403 already shows the error phase with the server's English message, not 'Not connected'.", "tests": "At HEAD ada4643. New file packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.unbindAnswer-12860.test.tsx, 7 cases, all passing: revoked:false shows the notice (en), revoked:false shows the notice (zh), 403 setup.access is not Not connected and is localized (en), the same (zh), revoked:true shows no notice, the notice survives an unbound refresh and clears on the next Connect, and CONTROL: a held-session 403 (PASSWORD_EXPIRED, a real AuthGateCodeSchema member) keeps the error phase and the server's message. Full run 'pnpm exec vitest run packages/app-shell/src/console/cloud-connection/ packages/i18n/ --maxWorkers=2' under os-verify-lock: VERDICT command-exit 0, Test Files 88 passed (88), Tests 1347 passed | 13 skipped (1360). CONTROL: the bind-flow pins in bindError, bindErrorLocale, displayLocale-10331 and unboundBody-12655 pass unchanged inside that run. Ablation: I committed the fix first, then used a Python replacer that asserts its anchor appears exactly once, with a trap that restores via 'git checkout HEAD --'. The packs and panel resolve from source through the vitest alias (@object-ui/i18n points to packages/i18n/src), so no dist rebuild was needed. A1: revoked===false changed to a constant false; 3 cases went red (notice en, notice zh, survives/clears). A2: constant true; revoked:true went red. A3: SETUP_ACCESS_REFUSED changed to '__never__'; 403 en and zh went red. A4: removed the setRevokeNotReached(false) call in connect(); survives/clears went red. A5: matched any code instead of PERMISSION_DENIED; CONTROL went red. Each case went red exactly as predicted. Afterwards 'git diff HEAD' was empty (diff-empty=yes). For A4 the grep -cF count reads 2/2 because the anchor spans two lines; the proof for A4 is the assert-once anchor plus the red result. Not passing: product-neutral-renderer.ratchet.test.ts exits 1 with 10 failures. That is the blocker described in open_questions.", "gates": "Each exit code captured before any pipe. pnpm exec vitest run (cloud-connection/ + packages/i18n/) = 0. vitest product-neutral-renderer.ratchet.test.ts = 1 (10 failed, BLOCKER). pnpm --filter @object-ui/i18n type-check = 0. @object-ui/app-shell type-check = 0 (echoed 'tsc --noEmit && tsc -p tsconfig.test.json'). @object-ui/i18n build = 0. @object-ui/app-shell build = 0. Both builds ran after the dependency closure built: 'turbo run build --filter=@object-ui/app-shell^... --concurrency=2' under the lock, VERDICT command-exit 0, 28/28. A first i18n build attempt before the closure existed exited 2 with TS6305 (core dist not built); that is not a measurement. Lint, normal mode: app-shell 'eslint src/console/cloud-connection/' = 0, 6 warnings, all no-explicit-any on existing catch (err: any) sites or the existing mount-effect setState, none on added lines. 'pnpm --filter @object-ui/i18n lint' = 0 (33 warnings). 'eslint packages/i18n/src/locales/' = 0. pnpm check:i18n-keys = 0. pnpm check:i18n-drift = 0 ('No en value changed in this range'). node scripts/check-changeset-presence.mjs = 0. Narrowed, declared: I linted app-shell over the touched directory only, not the whole package; repo-wide 'pnpm lint' is CI's. CI check-runs: NOT MEASURED, because no PR is open (degraded channel).", "mcp_calls": "3: claude-code-remote add_repo (objectstack, access read; answered read_available, nothing attached); github issue_read method=get #12860; github issue_read method=get_comments #12860. No write tools.", "api_writes": "0 REST writes. git push only: feature branch twice (the empty branch, then ada4643), evidence branch once (fa619dc).", "open_questions": [ { "question": "Ruling 4 (four new keys under cloudConnection) conflicts with ADR-0060's frozen namespace, and product-neutral-renderer.ratchet.test.ts fails in all ten locales. Which way should the copy land?", "options": [ "A. The maintainer approves an ADR-0060 frozen-baseline amendment that admits exactly cloudConnection.bound.disconnectNeedsSetupAccess and cloudConnection.unbound.revokeNotReached.{disconnected,cloudMayList,removeInCloud} (a governed docs/adr PR the seat drafts). Then this branch adds the same four entries to product-specific.allowlist.json; the PM widens the file surface by that one file. Real business need: measured, see live evidence (the real showcase /unbind answered revoked:false). Long-term: grows a frozen product surface by four leaves, against ADR rule 3 ('may only shrink'), but through the ADR's own approval path, not a workaround. AI-error axis: best of the options, because the ratchet stays strict and the exception needs a named human approval. Startup focus: smallest change that delivers the card. Cost: one governed ADR edit, plus one allowlist edit here.", "B. Move the copy into a new neutral namespace (for example a generic connection-revoke block) and rephrase it to drop the product name. The ratchet goes green with no ADR edit. Real business need: same as A. Long-term: this is the loophole ADR-0060 names ('product copy filed under a neutral name ... is review's to catch'), and the panel itself stays product code. AI-error axis: worst, because it teaches routing around a gate by renaming. Startup focus: no added scope, but it is a workaround. Not recommended.", "C. The producer composes it: the cloud-connection plugin's page metadata carries the copy (ADR-0060 rule 1), which needs a contract door for a conditional, answer-driven notice in objectstack (needs:contract-review). Long-term: the north-star shape. Startup focus: far larger than a p3 needs. Not now.", "D. Close as won't-do-in-UI: the operator page /docs/deployment/cloud-connection#disconnect already tells operators to check the cloud account, and a 403 already avoids 'Not connected' on main. Startup focus: strongest. Real business need: weakest, because the panel keeps showing a misleading 'Not connected' with no hint." ], "recommendation": "A, because the defect is a measured, user-visible false state ('Not connected' while the cloud still lists the connection). ADR-0060's ratchet exists to make exactly this kind of growth a named, human-approved exception, which is stricter than B's rename and much smaller than C. Under A the branch needs one more commit (four allowlist entries), and the pr_body_draft first line changes to 'Fixes #12860'." }, { "question": "Choices I made under the rulings, recorded for review (not blocking).", "options": [ "403: I built the preferred shape. The bound card stays and an AlertCircle row in destructive tokens sits above Disconnect. The match is on error.code === 'PERMISSION_DENIED', not on status 403, because ADR-0069's held-session refusal is also a 403 with its own code and an actionable message; that refusal keeps today's error phase (the CONTROL case pins this).", "Notice: an AlertCircle row in border-warning/40 bg-warning/10 with the icon in text-warning (the --color-warning theme token from apps/console/src/index.css; the body text stays foreground for contrast), role=status. The notice state is held outside Phase, so it survives any refresh that finds the server unbound and clears on Connect or on a bound status.", "No docs link: I measured zero docs URLs in the panel and in app-shell (no docs base exists), so I invented none.", "Three sentences, one key each, joined with a space. In zh this leaves a space after the full stop ('。 '), visible in the after screenshot; the maintainer may prefer different joining for CJK.", "One changeset file lists both @object-ui/app-shell and @object-ui/i18n as patch." ], "recommendation": "Keep as built unless the maintainer wants the CJK join changed." } ], "out_of_scope_findings": [], "deviations": [ "Ruling 4 cannot pass ADR-0060 / AGENTS.md section 1 (frozen namespace cloudConnection); hence needs_decision. I did not append to the allowlist (banned, and outside the file surface).", "Contract conflict, file vs dispatch: os-dev.md says the dev opens the draft PR and posts os-dev-report on the card first. I followed the dispatch's degraded channel (no PR, no card comment, body below), because the fleet-write relay it names is refused (403) in this container. I did not probe the relay myself.", "Worktree: the shared checkout had the branch name checked out with no commits of its own. I detached the shared checkout and deleted the empty local branch, then ran the dispatch's 'git worktree add' command as given.", "Lock path: AGENTS.md names /home/user/objectstack/scripts/pm/os-verify-lock.sh, but in this container the read clone lives at /home/user/objectstack-ai/objectstack, so I used that path. objectui has no scripts/ablation-replace.mjs or ablation-dist-preflight, so I used an inline assert-once replacer with a trap; no dist was involved because tests resolve from source.", "Live run: start-backend.sh boots with OS_CLOUD_URL=off, which does not mount CloudConnectionPlugin (/status answered 404 ENDPOINT_NOT_FOUND). I stopped it and restarted the same prepared showcase (published @objectstack 18.0.0-next.2) on :4610 with OS_CLOUD_URL=http://127.0.0.1:9, an unreachable control plane, so the plugin mounts." ], "files_changed": [ "packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx", "packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.unbindAnswer-12860.test.tsx (new)", "packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts (+4 keys each under cloudConnection)", ".changeset/12860-cloud-connection-unbind-answer.md (app-shell + i18n, patch)" ], "screenshots": [ "before (main b279016 panel, real unbind answered revoked:false, zh-CN light): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/before-main-revoked-false-zh.png?raw=true", "after (branch, same real answer, notice shown): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/after-branch-revoked-false-zh.png?raw=true", "after (branch, 403 PERMISSION_DENIED fulfilled, bound card plus localized line): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/after-branch-403-zh.png?raw=true", "bound card before clicking Disconnect (status fulfilled): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/bound-before-disconnect-zh.png?raw=true" ], "line_budget": "n/a", "pr_title_draft": "fix(console): Cloud Connection panel reads the unbind answer on Disconnect (objectui#12860)", "pr_body_draft": "(held by the seat with the branch until the decision below is ruled; the dev's draft is kept for the PR the ruling opens)", "head": "ada4643" }Relayed by seat
domain:ui#4(session_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final report. This is the degraded channel: the session pushedclaude/issue-12860-unbind-revoke-noticeatada4643and returnedstatus: needs_decision, so no PR is opened yet. The conflict is the seat's own ruling 4 (the new copy undercloudConnection), which ADR-0060 freezes. The seat measured it before writing the decision that follows this comment.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Decision — Cloud 连接面板的断开提示:新文案放哪 · 2026-10-17T13:33Z
维护者速读: 这张卡的代码已经写完并推上分支(
ada4643),7 个新测试全过。卡住的是文案放哪,这是席位派发时犯的错:cloudConnection这个 i18n 命名空间。cloudConnection列为冻结命名空间,规则 3 写明冻结面"只能缩小"。所以product-neutral-renderer.ratchet.test.ts在 10 个语言里全红。所以要你定:这 4 句文案怎么进来,还是不做。
做出来的效果(showcase 后端真实返回
revoked:false,zh-CN;证据分支,不合入):cloudConnection下,同样被冻结。What was measured (dev report relayed above, objectui
ada4643):docs/adr/0060-product-neutral-renderer.mdrule 3 says the frozen surface "may only shrink". Its frozen-baseline block listscloudConnectionwith 26 keys.docs/adr/**is a governed surface (check-governed-queue-guard).cloudConnection.unbound.revokeNotReached.disconnected…cloudMayList…removeInCloudcloudConnection.bound.disconnectNeedsSetupAccesscloudConnection加这 4 个 key,同一个 PR 往白名单加同样 4 条业务含义:
四轴(业务立场):
revoked:false:服务器丢了凭据,但 Cloud 不知道,面板却说「未连接」。这是用户看得到的误导状态,不是猜测。触发条件是断开那一刻控制面不可达,所以定 p3。推荐: A;回退: D。 置信缺口:A 会让 ADR-0060 的冻结面第一次增长。如果你更看重"冻结面只缩不增"这条原则,就选 D,它是诚实的选择:提示没了,但操作文档仍然告诉运维去云账户检查。
自检:只看①选 A;②③④是否翻转:②在意冻结面原则时翻向 D,④在意成本时翻向 D,③不翻。
os-decision-facets
revoked:false时面板误显示「未连接」,用户看得到。After the ruling:
Fixes #12860and the ADR edit, so it is governed and waits for your approval to merge:product-specific.allowlist.jsongets the same four entries;ada4643stays unmerged.needs-user-decisionin the same write as the ruling record.The claim
6234200417is released in this act: the card is back onneeds-user-decision, with no assignee. The feature branchclaude/issue-12860-unbind-revoke-notice(ada4643) holds the finished code. The evidence branchclaude/issue-12860-evidence(fa619dc) is never merged.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Ruling — D · close as not planned · 2026-10-17T16:04Z
The maintainer, in seat
domain:ui#4's session chat (session_01SJn1GEkrbbBQkhGC26wEDw), answering decision6234491015verbatim: 「D」.What D means here:
cloudConnection.*keys would have grown ADR-0060's frozen surface, which rule 3 says "may only shrink". The maintainer kept that rule over the notice.product-specific.allowlist.jsonare unchanged.main. Whenunbindanswersrevoked:false, the panel lands on 「未连接」 with no notice. When it answers 403, the panel also lands on 「未连接」. The operator page/docs/deployment/cloud-connection#disconnect(docs: no page tells a self-hosted operator how to connect their ObjectOS server to ObjectStack Cloud (Setup → Cloud Connection → Connect, then approve in the cloud account) objectstack#24066) still tells operators to check the cloud account after a disconnect.claude/issue-12860-unbind-revoke-notice(ada4643), and the screenshots stay onclaude/issue-12860-evidence(fa619dc). No PR is opened. The dev sessionsession_01B9twfw7wdL98RoK5XCyGXZstays archived.State change, in this one write:
needs-user-decisionremoved;not_planned.No assignee and no pm-state are left on the card.
Generated by Claude Code