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
cli: os serve asks the multi-node gate with OS_CLUSTER_REPLICAS only, so a topology declared by OS_EXPECT_MULTI_NODE=true boots multi-node with no gate (#31600 follow-up) #31719
Filing gate: ① product defect with a named seam and a measured reach (out-of-scope finding of #31600's os-dev-report 6728607050, confirmed by the contract review on PR #31718). ⛔ Not a claim.
reach: os start on open core with OS_EXPECT_MULTI_NODE=true and OS_CLUSTER_DRIVER=redis booted the redis driver multi-node with no gate registered, measured on fc4c3072 (main) and c96d6e73 (PR #31718 head, with OS_AUTH_SECRET set); the same deployment declared as OS_CLUSTER_REPLICAS=2 is denied by the gate.
Business reason: multi-node-gate.ts states that a DECLARED multi-node topology with no gate registered is refused outright (#13537, fail closed). declaresMultiNode (split-brain-guard.ts) has two spellings of that declaration, and every other multi-replica reader (split-brain guard, #31624's cache planner, #31600's auth-secret refusal) honours both. The gate call honours one. An operator who declares replicas with the other spelling gets a multi-node boot the gate exists to refuse, and the two spellings answer differently for the same topology.
Reader: the next domain:server seat, from pm:queue.
Dedupe (open AND closed): OS_EXPECT_MULTI_NODE gate → #31600, #31602, #31601, #14116 (closed), #13609 (closed); none on point (#14116 is the sequencing decision for #13537's gate, not this spelling gap).
class: b · type: Bug
Grade: p2. A gate refusal is skipped on one spelling; no data is wrong, and the split-brain guard still refuses a memory driver.
packages/cli/src/commands/serve.ts (the checkMultiNodeAllowed(Number(process.env.OS_CLUSTER_REPLICAS)) call, near line 3166 on fc4c3072): a topology declaresMultiNode(process.env) reads as declared reaches the gate as declared. Use declaresMultiNode from @objectstack/service-cluster; ⛔ never a second reading of the two spellings. Measure what count the gate should receive when only OS_EXPECT_MULTI_NODE=true is set (the gate takes a desired count; "declared, count unknown" must not read as "not declared"), and say it in the PR.
The refusal line already names both spellings (OS_CLUSTER_REPLICAS>1 / OS_EXPECT_MULTI_NODE=true); keep it.
Tests: OS_EXPECT_MULTI_NODE=true + redis + no gate → refused, red on main, green after. CONTROL: single node unchanged; OS_CLUSTER_REPLICAS=2 unchanged; a registered gate that admits the count still boots.
Surface
Expected file surface: packages/cli/src/commands/serve.ts, possibly packages/services/service-cluster/src/multi-node-gate.ts (only if the gate's input needs a "declared, no count" form), tests, root .changeset/.
Clause-②: no (narrowing): a boot that started on one spelling is refused, as it already is on the other.
Lane:domain:server, for the cli os serve gate query.
Grade: p2 holds. A topology declared only by OS_EXPECT_MULTI_NODE=true boots with no multi-node gate. The feat(auth)!: refuse to boot multi-replica without an explicit OS_AUTH_SECRET (#3814 F3e) #31600 refusals (for example the auth-secret one) don't fire there; the deployment is misconfigured, not exposed. If a seat finds a path where that leaves a security posture open, it goes through the security channel, not this card.
Claim: PM loop round 7 · 2026-11-08T23:59Z
Session: session_01ADHkmSNEnhcDvnrhmAucKu (card thread, dev) · seat session_01ADHkmQKZzp4DLtqiCkZZRM
Account: os-steve
Branch: claude/project-thread-mv5dxj
Worktree: objectstack-issue-31719
Domain: domain:server
Seat: domain:server#2
File surface: packages/cli/src/commands/serve.ts (the multi-node gate call region only), packages/services/service-cluster/src/multi-node-gate.ts (export only, if needed), tests, root .changeset/. ⛔ No packages/spec/**, ui/**, helm/
Container & model: S, mode:thread, model: default tier, effort: medium
Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: epic #3814 multi-replica; the gate reads the topology through the one existing reading (declaresMultiNode), never a second parse
Thread-read: none
Premise: holds as filed, read at claim on origin/main35a9c694: serve.ts calls checkMultiNodeAllowed(Number(process.env.OS_CLUSTER_REPLICAS)), so OS_EXPECT_MULTI_NODE=true alone reaches the gate as single node
Serial constraints cleared: same file as #31715 (this seat, claim 6728560405). The dev builds now in the gate call region only, merges main once #31715's PR merges, and #31715 lands first. Seat 4 passed it here (mailbox #31577, 6728946367)
Scope: Fixes #31719. A topology declaresMultiNode(process.env) reads as multi-node reaches the gate as multi-node, with "declared, count unknown" measured and stated in the PR; the refusal line is unchanged. Order: /mnt/project-files/server-seat2/standard-order.md + /mnt/project-files/server-seat2/31719-delta.md. Summons: the maintainer's 「队列中很多任务啊」 (project chat, 2026-11-08T21:26Z).
{
"issue": 31719,
"status": "done",
"branch": "claude/project-thread-mv5dxj",
"pr": "https://github.com/objectstack-ai/objectstack/pull/31750",
"session": "session_01ADHkmSNEnhcDvnrhmAucKu",
"premise_still_valid": true,
"summary": "os serve now asks the multi-node gate about the topology declaresMultiNode reads (from the same lazily loaded service-cluster namespace), with no second parse of either spelling in serve.ts. A topology declared only as OS_EXPECT_MULTI_NODE=true is asked as 2, the smallest count the gate reads as declared multi-node; undeclared and counted declarations are asked exactly as before. The refusal line is byte-identical. A new integration-tier boot test pins the refusal (red on main, green after) plus three controls.",
"tests": "New packages/cli/test/serve-multi-node-gate-declaration.integration.test.ts (4 real os serve boots). Green on ed640403: Tests 4 passed. Red on main (serve.ts from 35a9c694): Tests 2 failed | 2 passed — the EXPECT+no-gate refusal case (no '[cluster] multi-node not authorized (' line) and the admitting-gate control (gate asked with undefined, not 2); the single-node and OS_CLUSTER_REPLICAS=2 controls passed on main. Ablation A1 via ablation-replace.mjs ('? 2' -> '? 1 // ABLATION-A1', anchor 1->0, blob ecf1ee26f7d7 -> 457b8d187bd2): same 2 red; restored, blob == HEAD, git diff HEAD empty; no rebuild needed (runServe runs src through tsx). cli unit tier on ed640403: 407/407 files, 5775 tests passed. cli typecheck exit 0. Nightly fixtures (diagnosis + anchored-import e2e) 2 files/14 tests passed on 20b3fb9c.",
"gates": "dispatch-gates --commands on ed640403: 72 families, all exit 0; --ran: '72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero)'. Same 72 green on 20b3fb9c before the main merge. eslint --no-inline-config on the 5 touched cli files: exit 0. CI check-runs: in_progress at report time (not waited on).",
"dogfood_battery": "ed640403 · os-verify-lock VERDICT command-exit 0 · Test Files 440 passed | 3 skipped (443) · Tests 3652 passed | 41 skipped (3693) · no failures (also green on 20b3fb9c with the same counts)",
"mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #31719; no write tools",
"api_writes": "2 — pr_create via fleet-write relay (PR #31750); this os-dev-report comment via post-stamped.mjs on #31719",
"files_changed": [
"packages/cli/src/commands/serve.ts",
"packages/cli/src/commands/serve-multi-node-cap-advisory.pin.test.ts",
"packages/cli/test/serve-multi-node-gate-declaration.integration.test.ts",
"packages/cli/test/serve-cluster-driver-diagnosis.e2e.test.ts",
"packages/cli/test/serve-app-anchored-optional-import.e2e.test.ts",
".changeset/31719-multi-node-gate-expect-declaration.md"
],
"line_budget": "serve.ts +36/-2 (gate call region only: cast type, destructure, __requestedNodes); 3 forced test edits named in the PR body",
"deviations": [
"Two e2e fixture files outside the declared surface needed a declaresMultiNode export (serve now calls it); named as forced edits.",
"The new boot test is .integration.test.ts, not .e2e (e2e runs nightly only); it declares '// test-budget: 40' from a local 29.04 s reading, no CI reading yet.",
"The boot test's driver is an app-local redis stand-in delegating to memory, so the red leg on main also ends at the split-brain guard; the gate's own refusal line is what separates main from the fix.",
"First cli unit run had 2 prerequisite refusals (cli dist not built); green after building cli, and 407/407 on the final head."
],
"open_questions": [
{
"question": "A cap-aware gate with cap 1 now gets allowMultiNode(2) for an uncounted OS_EXPECT_MULTI_NODE=true and returns capped; formatMultiNodeCapAdvisory then prints 'OS_CLUSTER_REPLICAS declares 2', a count the operator did not write. Keep, or reword the advisory for the uncounted case?",
"options": [
"A keep as is: no shipped gate is cap-aware today; reword when one ships",
"B follow-up card: the advisory names the declaration spelling when OS_CLUSTER_REPLICAS is unset (outside the gate call region)"
],
"recommendation": "A, because no gate in this repo or the EE distribution expresses a cap yet, and the advisory is outside this card's region"
}
],
"security_findings": "none",
"out_of_scope_findings": [
"class: b · reach: content/docs/kernel/cluster.mdx says a gate denial makes os serve downgrade 'rather than failing'; with a declared topology the split-brain guard refuses the boot (multi-node-gate.ts module doc says so) · evidence: cluster.mdx line ~670 vs multi-node-gate.ts '⛔ What a denial ACTUALLY does at boot' · dedupe: cluster.mdx downgrade rather than failing, multi-node gate denial docs, split-brain guard refuses boot"
]
}
serve.ts: when the environment declares multi-node but sets no OS_CLUSTER_REPLICAS, the replica count counts as unknown, and the gate is asked for 2 replicas instead of passing as single-node.
formatMultiNodeCapAdvisory(verdict, {countDeclared}) has a separate wording for the uncounted case.
Activity
objectstack-fleet commentedon Nov 8, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: confirmed ·
domain:server· p2 ·pm:queue· 2026-11-08T23:54Z⛔ Not a claim.
domain:server, for the clios servegate query.OS_EXPECT_MULTI_NODE=trueboots with no multi-node gate. The feat(auth)!: refuse to boot multi-replica without an explicit OS_AUTH_SECRET (#3814 F3e) #31600 refusals (for example the auth-secret one) don't fire there; the deployment is misconfigured, not exposed. If a seat finds a path where that leaves a security posture open, it goes through the security channel, not this card.Generated by Claude Code
objectstack-fleet commentedon Nov 8, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
Claim: PM loop round 7 · 2026-11-08T23:59Z
Session:
session_01ADHkmSNEnhcDvnrhmAucKu(card thread, dev) · seatsession_01ADHkmQKZzp4DLtqiCkZZRMAccount:
os-steveBranch:
claude/project-thread-mv5dxjWorktree:
objectstack-issue-31719Domain:
domain:serverSeat:
domain:server#2File surface:
packages/cli/src/commands/serve.ts(the multi-node gate call region only),packages/services/service-cluster/src/multi-node-gate.ts(export only, if needed), tests, root.changeset/. ⛔ Nopackages/spec/**,ui/**,helm/Container & model: S,
mode:thread,model: default tier,effort: mediumClause-②: no (narrowing)
Metadata-protocol: none
Responsibility: epic #3814 multi-replica; the gate reads the topology through the one existing reading (
declaresMultiNode), never a second parseThread-read: none
Premise: holds as filed, read at claim on
origin/main35a9c694:serve.tscallscheckMultiNodeAllowed(Number(process.env.OS_CLUSTER_REPLICAS)), soOS_EXPECT_MULTI_NODE=truealone reaches the gate as single nodeSerial constraints cleared: same file as #31715 (this seat, claim 6728560405). The dev builds now in the gate call region only, merges main once #31715's PR merges, and #31715 lands first. Seat 4 passed it here (mailbox #31577, 6728946367)
Scope: Fixes #31719. A topology
declaresMultiNode(process.env)reads as multi-node reaches the gate as multi-node, with "declared, count unknown" measured and stated in the PR; the refusal line is unchanged. Order:/mnt/project-files/server-seat2/standard-order.md+/mnt/project-files/server-seat2/31719-delta.md. Summons: the maintainer's 「队列中很多任务啊」 (project chat, 2026-11-08T21:26Z).objectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmSNEnhcDvnrhmAucKu
os-dev-report
{ "issue": 31719, "status": "done", "branch": "claude/project-thread-mv5dxj", "pr": "https://github.com/objectstack-ai/objectstack/pull/31750", "session": "session_01ADHkmSNEnhcDvnrhmAucKu", "premise_still_valid": true, "summary": "os serve now asks the multi-node gate about the topology declaresMultiNode reads (from the same lazily loaded service-cluster namespace), with no second parse of either spelling in serve.ts. A topology declared only as OS_EXPECT_MULTI_NODE=true is asked as 2, the smallest count the gate reads as declared multi-node; undeclared and counted declarations are asked exactly as before. The refusal line is byte-identical. A new integration-tier boot test pins the refusal (red on main, green after) plus three controls.", "tests": "New packages/cli/test/serve-multi-node-gate-declaration.integration.test.ts (4 real os serve boots). Green on ed640403: Tests 4 passed. Red on main (serve.ts from 35a9c694): Tests 2 failed | 2 passed — the EXPECT+no-gate refusal case (no '[cluster] multi-node not authorized (' line) and the admitting-gate control (gate asked with undefined, not 2); the single-node and OS_CLUSTER_REPLICAS=2 controls passed on main. Ablation A1 via ablation-replace.mjs ('? 2' -> '? 1 // ABLATION-A1', anchor 1->0, blob ecf1ee26f7d7 -> 457b8d187bd2): same 2 red; restored, blob == HEAD, git diff HEAD empty; no rebuild needed (runServe runs src through tsx). cli unit tier on ed640403: 407/407 files, 5775 tests passed. cli typecheck exit 0. Nightly fixtures (diagnosis + anchored-import e2e) 2 files/14 tests passed on 20b3fb9c.", "gates": "dispatch-gates --commands on ed640403: 72 families, all exit 0; --ran: '72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero)'. Same 72 green on 20b3fb9c before the main merge. eslint --no-inline-config on the 5 touched cli files: exit 0. CI check-runs: in_progress at report time (not waited on).", "dogfood_battery": "ed640403 · os-verify-lock VERDICT command-exit 0 · Test Files 440 passed | 3 skipped (443) · Tests 3652 passed | 41 skipped (3693) · no failures (also green on 20b3fb9c with the same counts)", "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #31719; no write tools", "api_writes": "2 — pr_create via fleet-write relay (PR #31750); this os-dev-report comment via post-stamped.mjs on #31719", "files_changed": [ "packages/cli/src/commands/serve.ts", "packages/cli/src/commands/serve-multi-node-cap-advisory.pin.test.ts", "packages/cli/test/serve-multi-node-gate-declaration.integration.test.ts", "packages/cli/test/serve-cluster-driver-diagnosis.e2e.test.ts", "packages/cli/test/serve-app-anchored-optional-import.e2e.test.ts", ".changeset/31719-multi-node-gate-expect-declaration.md" ], "line_budget": "serve.ts +36/-2 (gate call region only: cast type, destructure, __requestedNodes); 3 forced test edits named in the PR body", "deviations": [ "Two e2e fixture files outside the declared surface needed a declaresMultiNode export (serve now calls it); named as forced edits.", "The new boot test is .integration.test.ts, not .e2e (e2e runs nightly only); it declares '// test-budget: 40' from a local 29.04 s reading, no CI reading yet.", "The boot test's driver is an app-local redis stand-in delegating to memory, so the red leg on main also ends at the split-brain guard; the gate's own refusal line is what separates main from the fix.", "First cli unit run had 2 prerequisite refusals (cli dist not built); green after building cli, and 407/407 on the final head." ], "open_questions": [ { "question": "A cap-aware gate with cap 1 now gets allowMultiNode(2) for an uncounted OS_EXPECT_MULTI_NODE=true and returns capped; formatMultiNodeCapAdvisory then prints 'OS_CLUSTER_REPLICAS declares 2', a count the operator did not write. Keep, or reword the advisory for the uncounted case?", "options": [ "A keep as is: no shipped gate is cap-aware today; reword when one ships", "B follow-up card: the advisory names the declaration spelling when OS_CLUSTER_REPLICAS is unset (outside the gate call region)" ], "recommendation": "A, because no gate in this repo or the EE distribution expresses a cap yet, and the advisory is outside this card's region" } ], "security_findings": "none", "out_of_scope_findings": [ "class: b · reach: content/docs/kernel/cluster.mdx says a gate denial makes os serve downgrade 'rather than failing'; with a declared topology the split-brain guard refuses the boot (multi-node-gate.ts module doc says so) · evidence: cluster.mdx line ~670 vs multi-node-gate.ts '⛔ What a denial ACTUALLY does at boot' · dedupe: cluster.mdx downgrade rather than failing, multi-node gate denial docs, split-brain guard refuses boot" ] }Stamped 2026-11-09T02:23Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
ACCEPT → PR #31750 comment 6731900433 · head
9a7ee97a· 2026-11-09T04:00Z · contract review PASS 6731888362 (CONTRACT_REVIEW_TIER, narrowing) · landing: pr_ready + automerge_enableobjectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
落地 · 2026-11-09T04:22Z
2b752aa4(ACCEPT 6731900433; contract review at CONTRACT_REVIEW_TIER PASS, record 6731888362)Fixes #31719)Landed:
serve.ts: when the environment declares multi-node but sets noOS_CLUSTER_REPLICAS, the replica count counts as unknown, and the gate is asked for 2 replicas instead of passing as single-node.formatMultiNodeCapAdvisory(verdict, {countDeclared})has a separate wording for the uncounted case.cluster.mdxnow describes this behaviour.