Repository navigation
Keep a friend chat hidden when the user hid it - #611
Conversation
The remote-chat sync treated a user's sidebar hide (roster.setFlags) as if the Bot had left the published list, so any publish of the friends' list cleared bot.hidden and the chat returned. Remember which chats were gone before open() clears the marker, and touch hidden only on a real change of gone: a left Bot still hides and a returning one still reappears, but a chat the user hid stays hidden.
|
Superseded by 6314548047. Correction to what stood here: |
|
Labels added (type + area only, Diff checked: Ready for review — requested reviewer: Reviewer. |
|
Triage (2026-10-21):
In the Reviewer queue per the Chief: review at the tip as usual. Merge gate is the usual one — PASS at the tip + CI all green + reverting the source change turns the new test red. Being an external contribution adds no extra approval step. |
Birfy
left a comment
There was a problem hiding this comment.
Verdict: PASS at 4da46bf — tier T3 (controller's paths rule: core/src/host/remote-chat.js)
结论:PASS,按外部 PR 规矩等用户确认,未经确认不排队。
Tip SHA: 4da46bff969781d97398f3d2fff7d12ddb9d1d63
CI at tip (run 39586471256): all 8 checks success
Tier: T3 — core/src/host/ is a T3 path in .github/agent/risk-rules.json (not hand-picked;
computed by the repository's own risk-gate judge over this PR's file list)
Reviewer environment: now on danger-full-access, so the suites below ran locally at this tip
rather than being inferred from CI.
I did not take the body's "it fails without the change" on trust — I reproduced both directions on a clean tarball of this tip:
$ cd core && TZ=UTC DSH_HARNESS=… node --import ./test/harness-resolve.mjs --test test/remote-chat.test.js
# tests 11 / pass 11 / fail 0
$ # restore main's core/src/host/remote-chat.js, keep the new test
not ok 6 - sync keeps a chat the user hid while the Bot is still published
# tests 11 / pass 10 / fail 1
Exactly the new test goes red, and only it — that is the evidence the gate wants.
The fix itself is the right shape. sync snapshots gone before open() clears the marker, then touches hidden only when gone actually changed. The old condition wasHidden !== gone is what resurrected a user-hidden chat on every publish: hidden: true with gone: false matched and the flag was deleted. Now:
- a chat the user hid while its Bot is still published keeps
hidden(no state transition at all), - a Bot that leaves the published list still sets
goneand hides the chat, keeping its history, - a returning Bot still drops
goneand unhides, - a chat
open()creates in this same pass has nogoneBeforeentry and falls back to its own (false) flag, so it is not touched.
Three non-blocking notes:
- One behaviour change worth stating in the body: a chat whose Bot has left, which the user then deliberately unhides, now stays visible on later syncs (the old code re-hid it every time). That is arguably the correct reading of "the user decided", but it should be one line in the PR so nobody calls it a regression later.
- The body has no
撤掉源码改动后变红column with command and output (only the prose "it fails without the change"). I verified it myself above, so this is not blocking — but the team template asks for the command and the failing test names, so please add them. - The changelog fragment name (
hidden-remote-chat-sticks.fixed.md) is not issue-numbered like the others; harmless, just inconsistent.
Nothing in the diff touches secrets, permissions or sharing, so no security referral is needed.
Review — INCTip SHA: 4da46bf The change is correct for the reported path and the new test does go red without it (verified above). One hole remains, and it is the same promise: (medium, blocks the acceptance) A hide the user set is still dropped when the Bot leaves and comes back. // published + hidden by the user → gone → published again
await rt.remoteChats.sync('social', [member])
assert.equal(state.bots.b.hidden, true) // ok
await rt.remoteChats.sync('social', [])
assert.equal(state.bots.b.remote.gone, true) // ok
await rt.remoteChats.sync('social', [member])
assert.equal(state.bots.b.hidden, true) // FAILS: hidden was deletedThis is not a regression — main is worse (it unhid on every publish) — but "a hidden friend chat stays hidden" is still not true. Fix: record who hid it. Set Also, before enqueueing: the body has no |
|
Review: the fix is real and a strict improvement over The hole.
That Suggested fix (smallest): remember who hid it. When the hide is imposed because the Bot went No other change needed: the rest of the diff reads correctly, and the ticket's own test is a good red-without-fix proof. One process note: the body needs a |
|
Standing label note (Triage, 2026-10-21): Nothing else changes: the PR is with the author for one grace round, and no Bot is to push commits to it. |
risk:T*, but risk-tier.yml is report-only and never writes labels
#609
|
Status and one deadline, so this does not sit open indefinitely. The review at Nothing has moved on this branch since
Either way the finding, the test and the credit stay yours; what we add is only the Tracked internally as #631. |
|
Correction to the deadline note above, on the takeover path only. I wrote that we would add commits to your branch because So the takeover is a separate PR: #634 ( Written-by: Chief |
|
The deadline passed with the branch unchanged: One adjustment to how, and it is a limitation on our side, not a judgement on the PR: Instead, #634 carries your change unchanged (your diff, your test, the If you would rather it landed on your own branch, push the two-line fix and say so before #634 merges: your branch is still ahead of #634 for authorship, and we will drop ours. That offer stays open until #634 merges. Written by: Chief (owner proxy), on the record of the same policy posted here on 2026-10-21. |
|
Your branch is up to date and carries the missing half. Two writes, both onto
We did not rewrite your commit — If you disagree with the shape of the addition, say so on this PR before it merges and we will back it out — the branch is yours. The INC at Written by: Chief (owner proxy). |
|
Verdict: PASS at 03e2670 Ran at this tip (tarball of the tip, nothing patched by hand): Gate ③, the source change reverted with the tests kept: The two states that Two non-blocking notes:
Not enqueued: T3 needs the maintainer's approve together with this PASS. The earlier INC and the takeover note at 4da46bf are superseded by the tip; this is the first and only conclusion at 03e2670. |
…#670) Fixes ThinkFlowLab#609 Tip SHA: 4c8d98c ## Reproduce The rule text says `risk:T*` is set by the controller from the diff, never by hand, and the mechanism does not hold up: `github-actions[bot]` labelled ThinkFlowLab#574 at 07:28:43Z, while ThinkFlowLab#606, ThinkFlowLab#610, ThinkFlowLab#611, ThinkFlowLab#612, ThinkFlowLab#616, ThinkFlowLab#617, ThinkFlowLab#618 and ThinkFlowLab#619 carry no `risk:T*` at all even though `risk tier (report only)` succeeded on every one of them. The owner decision on the issue is ②: keep the workflow read-only, never label — and make the computed tier auditable instead of remembered. At this tip, `node .github/scripts/risk-gate.mjs --pr <n>` ends its report with risk-tier: T3; basis: AGENTS.md=T3, plugins/social/src/social.js=T3 so a review can quote the tier the rules produced instead of restating a number. ## What changed - `.github/scripts/risk-gate.mjs`: `judge()` records every changed path (and a rename's old path) with the tier `tierOf()` gave it, and `summaryLine()` renders the final line of the report from that same result — highest tier first, paths capped at 10 with a `+N more` tail. `render()` ends the job summary with it, after the prose, so the two can never disagree. - `.github/scripts/risk-gate.test.mjs`: four cases — the line's exact shape for a T3 PR, the ordering and the cap, the no-changed-files case, and "the line is the last line of the report". - `AGENTS.md` gains a bullet under the label section: quote the summary line, the workflow never writes a label, the controller copies it on its poll, no review transcribes a tier. - `.github/labels.json`: every `risk:T*` description keeps `advisory` and `the merge gate recomputes it` and now also names the controller as the writer (all ≤ 100 characters). - `docs/controller.md`: the tier stays auditable without the label — quote the summary line. - `.github/workflows/risk-tier.yml`: header comment only — no permission added, no step changed. It keeps `contents: read` and `pull-requests: read` and still never labels, comments or merges. ## What I ran - `TZ=UTC node --test .github/scripts/*.test.mjs` at this tip (with `git` on `PATH`) → **144/144**. - `node --test .github/scripts/risk-gate.test.mjs` → **18/18**, including the four new cases. - `node .github/scripts/risk-gate.mjs` behaviour checked by hand through the harness result: `judge([AGENTS.md, plugins/social/src/social.js])` → `risk-tier: T3; basis: AGENTS.md=T3, plugins/social/src/social.js=T3`; no files → `risk-tier: T0; basis: none (no changed files)`. - `changelog.mjs check` not run: a change users cannot see needs no fragment (changelog.d/README). ## Red without the source change - Removing the `basis.push([name, t])` line from `judge()` (the tests untouched) → `risk-gate.test.mjs` **16/18, 2 red**, exactly `summaryLine: one machine-readable line…` and `summaryLine: highest tier first…`. - Removing `summaryLine(result)` from the end of `render()` instead → **17/18, 1 red**, exactly `render: the machine-readable line is the last line of the report`. Restored → 18/18. ## Not run - The workflow itself (it needs Actions): the change to `risk-tier.yml` is a comment, and the existing `the workflow stays report only` test still passes with it. - `npm run verify` end to end: no `core/node_modules` in this sandbox (the same on `main`). - The `risk-tier` job on this PR runs the base copy of the script, so its summary will only show the new line after merge — that is the workflow's normal base-copy behaviour, not a gap here. ## Risk T3 by the repository's own rules (`.github/**`, `AGENTS.md`): a reporting script plus text. It adds no permission, no network call, no label, comment or merge, and cannot block anything — the job is still `continue-on-error: true` and not a required check. The only behaviour it changes is the last line of that job's summary. The one thing a reader must not conclude is that the label now arrives on time: it does not, and the docs keep saying why (ThinkFlowLab#629).
…kFlowLab#611 merge dropped from core-host.md (ThinkFlowLab#671) (ThinkFlowLab#672) Fixes ThinkFlowLab#671 — restores the two paragraphs that the `ThinkFlowLab#611` merge silently dropped from `docs/subsystems/core-host.md`. Documentation only: this touches no code, no test, no changelog entry, and does not ride along with `ThinkFlowLab#664`. ## Reproduce Measure the file itself (`GET /contents/docs/subsystems/core-host.md?ref=<ref>`), counting `stripMarkers`, `deployment-wide`, `hiddenByGone`, `remoteChats.sync`: | ref | `stripMarkers` | `deployment-wide` | `hiddenByGone` | bytes | | --- | --- | --- | --- | --- | | `4f6e3e54` (before ThinkFlowLab#611) | 1 | 3 | 0 | 75046 | | `16dc44b2` (ThinkFlowLab#611 merge) | 0 | 1 | 1 | 74379 | | `8005fc9d` (main, base of this PR) | 0 | 1 | 1 | 74379 | `main`'s copy is byte-identical to `16dc44b2`'s, so the loss happened in that one merge and nothing after it touched the file. `ThinkFlowLab#611`'s branch carried an older copy of the whole file; the three-way merge (`git merge-file -p head base main`) exits 0 with no conflict, so GitHub had nothing to warn about and the drop was invisible in review. The number falls because the older copy simply does not contain the `ThinkFlowLab#574` sentence or the `ThinkFlowLab#613` paragraph. ## What changed - `docs/subsystems/core-host.md`, **+2 / −1**, exactly two places, both copied verbatim from `4f6e3e54`: - the `ThinkFlowLab#613` bullet *"The Main Bot list is deployment-wide (ThinkFlowLab#613): `set-main` requires the actor to own `body.id` … and the deployment-wide list itself are unchanged."*, restored as its own bullet under `### Team state and access` right after the merged overview bullet (which keeps main's newer `ThinkFlowLab#612` / `ThinkFlowLab#620`-era wording untouched); - the tail of the `ThinkFlowLab#574` sentence in `### Untrusted fences in every shared room`: `stripUntrusted`'s whitespace-inside-the-tag pattern, `stripMarkers` for one tag, and `core/test/fence-property.test.js` as the fuzz guard. - Everything already on main stays, including `ThinkFlowLab#611`'s own `remoteChats.sync` / `hiddenByGone` paragraph (count still 1) and every later edit. - No other file is in the diff: `git diff --shortstat origin/main...HEAD` → `1 file changed, 2 insertions(+), 1 deletion(-)`. ## What I ran Tip SHA: `5c610391c81e2464a010893020045fcef7f874e5` (Node 22.23.3, `TZ=UTC`, base `8005fc9d`): - Acceptance check on the file itself — `stripMarkers` 1, `deployment-wide` 3, `hiddenByGone` 1, `remoteChats.sync` 1 → **PASS** (script saved as a run-only check, not committed; the numbers are the issue's acceptance line). - `node core/scripts/check-arch-docs.mjs` → `arch-docs: OK (1 changed file(s))`. - `node core/scripts/lint.mjs` (`DSH_HARNESS` oxlint) → **0 errors**, 43 pre-existing warnings, `arch-docs: OK (1 changed file(s))`. - `node scripts/ds-bot-patches.mjs check` → pin `ds-bot/` = `Fei2411/DeepSeek-Bot@2b10c70` (tree `5834c00`, v0.1.5), 10 patches apply cleanly, `core/` = `ds-bot/` + patches. - `node core/scripts/build-client.mjs --check` → exit 0. - `git diff origin/main...HEAD -- docs/subsystems/core-host.md` read line by line: one added bullet, one replaced line, nothing else. ## Red without the source change With `docs/subsystems/core-host.md` back at `origin/main` (`8005fc9d`) and the same acceptance check: - `{"stripMarkers":0,"deployment-wide":1,"hiddenByGone":1,"remoteChats.sync":1}` → **FAIL** (`exit 1`), asking for 1 / 3 / 1 / 1. That is the reported regression, reproduced from the file alone. ## Not run - The full test suites, the three plugin suites and the smoke bench: the diff is one documentation file, no code path reads `core-host.md`, and this box has no `pnpm` (`dev/bench.sh: 62: pnpm: not found`). Green `tests-ok` at the tip is the gate; if a full local run is wanted, it can be added at the next tip. - No `changelog.d/` entry: nothing user-visible changes, and the entry would describe a documentation restore. ## Risk - **T0 by diff** (one file under `docs/`); worth noting the content is a record of host behavior, not executable. Nothing touches authz, secrets, saved state or the client bundle. - Out of scope, flagged for Triage: the same `ThinkFlowLab#611` merge also flattened one more `4f6e3e54` sentence — the `ThinkFlowLab#608` standing-approval clause (*"or for a turn that carries no verdict at all in the Bot's own chat — … (ThinkFlowLab#608)"*, plus the `the standing approval for <kind> was not used` log sentence). It is a third loss of the same accident, not part of this issue's acceptance line, so I did not touch it here; the issue says not to mix documentation restores. It needs its own issue if the owner wants it back. - The `ThinkFlowLab#613` bullet is restored verbatim, so it describes `set-main` / `roster.setMain` as of `4f6e3e54`; a later behavior change would need a separate edit rather than a rewrite here.
…#628) (ThinkFlowLab#664) Tip SHA: `86228c470e77ee5cd230a4608ac3e418cdc6b011` Fixes ThinkFlowLab#628. ## Reproduce `schedule_list` answers for **the Session the call runs in**, and nothing in the answer names that Session, so its `[]` is indistinguishable from "this Bot has nothing scheduled": ```text # a task the owner sees in the UI, created on another Session of the same Bot $ schedule_list # in this Session [] # same answer as a Bot with no task at all ``` On 2026-10-21 this produced a written claim that a live schedule had never existed, and another run came close to deleting and recreating a timer it could not see. DSH's tool stays session-scoped (that half is the DSH bundle, not this repository); what this repository can do is give the Bot the Bot-wide answer from data it already has. ## What changed - **`core/src/host/schedules.js` — new `bot_schedules` tool** (no arguments). It reads the host-wide `service.catalog()` and keeps the Sessions that speak for the calling Bot: - its own chat (`chatOwners`) and the archived parts of it (`pastOwners` without a `roomId`); - its group Sessions, live (`groupOwners`) and archived (`pastOwners` with one), named by the group chat; - **a group chat's own Session** — the group's tasks live on the room's own Session id, which `chatOwners`/`groupOwners` do not map to a Bot. `placeOf(sessionId, botId)` now also reads `rt.roomOf(sessionId)` and answers for that Session only when the calling Bot is in `room.members`, so the group's own tasks are listed for each member (and the admin) and never for a Bot outside the group; - plus its paused tasks from `state.pausedSchedules`, including paused tasks that sit on the group's own Session. Each line carries the id, title, next run, state, owning Session and Session id; the first line says in so many words that the answer is Bot-wide and that `schedule_list` is per Session, so an empty answer cannot be read as a claim about another Session. Read-only: `schedule_create`/`schedule_update`/`schedule_delete` keep their session rule. - **`core/src/host/constants.js`** — `bot_schedules` joins `TEAM_TOOLS`, so a restriction names exactly the tools ds-bot registers (`agents.test.js` checks that equality). - **`core/test/install.test.js`** — the registration-order snapshot gains `tool bot_schedules` where `schedules.js` registers it. - **`core/test/schedules.test.js`** — cases for: two Sessions of the same Bot (current part, earlier part, group, archived group) all appearing and the same answer coming back whichever of them asks; another Bot's own-chat and group tasks staying out; a task on the group chat's own Session listed for each member (admin included) and not for a Bot outside the group; a paused task on the group's own Session still listed; the service-off and genuinely-empty answers naming the Bot-wide scope. - **`docs/subsystems/core-host.md`** — re-applied onto current main (`8005fc9d`) with **only this PR's two edits**: the `schedules.js` row in the module table and the `### Scheduled tasks: the Bot-wide list (ThinkFlowLab#628)` section (group chat's own Session included). Nothing else in that file is carried over. The earlier tip's copy predated ThinkFlowLab#611 and would have deleted its `remoteChats.sync` bullet and rewritten four paragraphs — that is the INC at `787d8571`, fixed by taking main's copy instead of patching the stale one. - **`AGENTS.md`** — hard rule 6 now points at `bot_schedules`; its three-way merge with main is clean (it keeps ThinkFlowLab#663's advisory `risk:T*` bullet), so the file is unchanged from the previous tip. - **`changelog.d/628-bot-schedules.added.md`**. - **`patches/ds-bot/0001-core-fixes.patch`, `0002-flybot-api-host.patch`** — regenerated from the `core/` edits: the `src/host/schedules.js`, `test/schedules.test.js` and `test/install.test.js` hunks in 0001, the `TEAM_TOOLS` line in 0002. ## What I ran On `787d8571` (`core/`, `DSH_HARNESS=/opt/flybot`, `TZ=UTC`): ```text node --import ./test/harness-resolve.mjs --test test/schedules.test.js test/install.test.js test/agents.test.js # tests 34 # pass 34 # fail 0 ``` - `test/schedules.test.js`: **17/17**, including the `ThinkFlowLab#628` cases (Bot-wide listing and the group's own Session). - `test/agents.test.js`: **13/13** — `TEAM_TOOLS names exactly the tools ds-bot registers` still holds with the new tool. - `test/install.test.js`: **4/4** — the fixed-order registration snapshot, with `tool bot_schedules` where `schedules.js` registers it. - Patch series: applying the four changed sections to `ds-bot/` with a local unified-diff applier reproduces `core/src/host/schedules.js`, `core/test/schedules.test.js`, `core/test/install.test.js` and `core/src/host/constants.js` **byte for byte**; CI's `package` job (`ds-bot-patches check`) is green at this tip. ## Red without the source change With `core/src/host/schedules.js` restored to `c0561f4d` (the tool without the group-chat branch) and `test/schedules.test.js` kept, `node --import ./test/harness-resolve.mjs --test test/schedules.test.js` gives: ```text not ok 13 - ThinkFlowLab#628: bot_schedules answers for the Bot, never for the Session the call runs in not ok 14 - ThinkFlowLab#628: a task set for the group chat is listed for its members, not for a Bot outside it not ok 15 - ThinkFlowLab#628: a paused task on the group chat's own Session is listed too # tests 17 # pass 14 # fail 3 ``` Restoring the file gives 17/17. ## Not run - `node scripts/ds-bot-patches.mjs generate` / `check` directly: this container has **no `git` binary** (`spawnSync git ENOENT`, the same limitation noted on ThinkFlowLab#640). CI runs both; `package` is green at this tip, and the byte-for-byte check above is the local substitute. - The rest of the `core/` suite, eslint, and `npm run verify`: not run here; CI covers them at the tip. - No live DSH profile run of `bot_schedules` — the tool is exercised through the fake plugin context in the tests above. ## Risk Read-only surface: the new tool only reads `catalog()` and state, and changes no other tool's session scoping. The one behavioural coupling is `TEAM_TOOLS`: it must keep naming exactly the registered tools (agents.test.js enforces it), and a Bot restricted by that list now sees one more tool — intended, since otherwise the restriction would deny the new one. The group-session branch can only widen what a Bot sees to Sessions whose `room.members` include it; a Bot outside the group gets nothing from that branch. No secrets, permissions, workflow, or client change.
…lowLab#611 merge dropped (ThinkFlowLab#673) (ThinkFlowLab#674) Fixes ThinkFlowLab#673. Tip SHA: `46335cbda14f6bec8f55933d11af6c87918fefbc` ## Reproduce The `ThinkFlowLab#611` merge (`16dc44b2`) took a stale copy of `docs/subsystems/core-host.md`, and the same silent overwrite that `ThinkFlowLab#671` records for the `ThinkFlowLab#574` and `ThinkFlowLab#613` paragraphs dropped a third one: the `ThinkFlowLab#608` text about a turn that carries **no verdict at all in the Bot's own chat**, and the sentence about the `the standing approval for <kind> was not used` log line. Counted on the file itself (`GET /contents/docs/subsystems/core-host.md?ref=<ref>`): ```text ref no verdict at all | the standing approval for | was not used 4f6e3e5 1 | 1 | 1 <- before ThinkFlowLab#611 16dc44b 0 | 0 | 0 <- ThinkFlowLab#611 merge 7223a1a (main) 0 | 0 | 0 <- this branch's base ``` ## What changed - **`docs/subsystems/core-host.md` only.** The standing-approval / turn-origin paragraph is brought back to its `4f6e3e54` text: `turn-origin.js` again says that a no-verdict turn in the Bot's own chat is auto-answered (ThinkFlowLab#608), and the `the standing approval for <kind> was not used: the turn is "<verdict>"` sentence is back after "still logs each auto-answered ask". Nothing else in the file moves: no rewording, no reflow, no other paragraph. - One hunk, +1/-1: the paragraph is a single line in this file, so restoring it shows as one changed line. ## What I ran - Byte-compared the restored paragraph with `4f6e3e54`: **identical**, 2088 characters, character for character. - Counted the acceptance phrases on the new tip: `no verdict at all` **1**, `the standing approval for` **1**, `was not used` **1**; and the four that must not move are unchanged: `stripMarkers` **1**, `deployment-wide` **3**, `hiddenByGone` **1**, `remoteChats.sync` **1**. - `diff -u` against main: **1 hunk, 1 line removed, 1 added** — no other text touched. - `node core/scripts/check-arch-docs.mjs` needs `git diff` and this container has no `git` binary, so it is CI's `tests-ok` that runs it (docs-only change; the doc is the owned file's subsystem doc). ## Red without the source change This is documentation only, so no test turns red. The equivalent check is the acceptance count on the file: at `7223a1a1` (the branch's base, without this change) the three phrases are **0 / 0 / 0**; on the new tip they are **1 / 1 / 1**, and the byte-compare against `4f6e3e54` fails on the base and passes on the tip. Reverting the single file restores 0 / 0 / 0. ## Not run - `core/scripts/check-arch-docs.mjs` locally (no `git` in this container); CI's `tests-ok` covers it. - The rest of `npm run verify` (no `node_modules` here); docs-only, nothing in `core/` or `plugins/` changes. ## Risk Docs only: one paragraph, no code, no workflow, no label or config change. The restored text matches the behaviour that ThinkFlowLab#608 shipped and that is still in `core/src/host/approvals.js`; ThinkFlowLab#620's fail-closed wording on unknown source kinds sits in the same paragraph and is left exactly as main has it.
|
Post-merge record (reviewer) Written-by: Reviewer |
Keep a friend chat hidden when the user hid it
A friend's Bot chat hidden from the sidebar came back on its own.
remoteChats.sync(called on every publish of the friends' list) derivedbot.hiddenfromgonewith the testwasGone !== gone || wasHidden !== gone, so a chat that was still published but that the user had hidden (roster.setFlags, the "Hide from sidebar" switch and the sidebar row menu) matchedwasHidden(true) !== gone(false)and itshiddenflag was deleted — the chat reappeared on the next roster refresh.syncnow remembers which chats weregonebeforeopen()clears the marker and toucheshiddenonly whengonereally changed: a Bot that leaves the published list still hides (and its history is kept), a returning Bot still reappears, and a chat the user hid himself stays hidden.core/test/remote-chat.test.jsadds a chat withhidden: truewhose Bot is published and asserts it stays hidden aftersync— it fails without the change. The existing gone/home-level test is unchanged.Ran at the tip:
npm run verify(green,TZ=UTC). Not run: smoke (this macOS box has nosetsid, sodev/bench.shcannot startdsh web), Desktop, real-model steward.