Repository navigation
The 09:00 Project digest is empty on any signed-in install: it reads the board with no actor, so every card is filtered out #1846
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 233fe40b30d046298aedd6018da7f05e
Branch: dev/4-p234-digest-authz
Base: b4e05ac
At: 2026-10-30T15:05:43Z
Baseline
Re-read on live main b4e05acf9c2785eced88e94f1b70e4f15c860c9f (2026-10-30); #1835 added the metrics block above the board's row builder, so the host project-board.js positions moved. Current: core/src/host/project-board.js const visible = … :363, if (!visible(task, actor)) continue :368; core/src/host/project-assistant.js buildDigest :61-91 (the "no change" line :87), the read :478-479, since :450, setProjectDigest :452; core/src/host/tasks.js taskVisible :334-338, mayAct :218-221, owns :295-300; core/src/host/store.js setProjectDigest :514-521. core/test/project-assistant.test.js:11-25 still seeds no authSessions, and the board-path case is :577.
What happens
On any installation where someone has signed in, the Project Assistant's 09:00 digest says the Project did not change — every day, while the board is moving. The owner gets, and keeps, one line:
Project Assistant (P1) — 2026-10-30: no change since the previous digest.
The digest is not wrong about the date; it reads a board that is empty for the reader it uses, and there is no reader.
Why it is empty (live main b4e05acf)
Four legs, all present today:
- The digest reads the board with no actor:
core/src/host/project-assistant.js:478—const board = (await rt.projectBoard?.(project.id)) ?? [], and:479takesboard.rows. - The board paints a row only when
rt.taskVisible(task, actor)says so:core/src/host/project-board.js:363(const visible = typeof rt.taskVisible === 'function' ? rt.taskVisible : () => false) and:368(if (!visible(task, actor)) continue). - With
actorundefined every branch of that predicate is false:core/src/host/tasks.js:334-338—mayAct(:218) requiresactor.memberIdwhen authorization is enforced,ancestorOwned(:302-311) goes throughowns(:295-300) which requires the same, androomAdmin(:321-326) returns false on!actor?.memberId. Sorows = []. - "Enforced" is every real installation:
core/src/host/authz.js:87—rt.authzEnforced = () => Object.keys(state.authSessions || {}).length > 0, andmember-auth.jswritesstate.authSessionsthe first time anybody signs in.
With rows = [], buildDigest (project-assistant.js:61-91) fills none of its four sections and :87 returns that single "no change" line. The multi-owner "Needs X's decision" section is empty for the same reason (actionableFor([])).
It is worse than one wrong sentence
The wrong digest is also archived as the baseline for the next one:
project-assistant.js:452rt.setProjectDigest?.(project.id, note, owner)stores{ day, at, text }in that owner'sprojectMemory(core/src/host/store.js:514-521), 4000 chars max.- The next run reads it back as the cut-off:
:450since: memory.digest?.at ?? null, andbuildDigest'schanged(row)gates the "Merged since / Closed" sections onrow.updatedAt > since.
So every card that was completed before the first empty digest can never appear in "Merged since" or "Closed" again. The bug does not only emit a false sentence; it advances the "what counts as changed" watermark along with it.
Why CI stays green
core/test/project-assistant.test.js:11-25 seeds a host with no authSessions, so authzEnforced is false and mayAct short-circuits to true (tasks.js:219). The "through the real task and board path" case at :577 also runs rt.projectBoard(project.id) on a host nobody signed in to. No test asserts digest content under enforcement.
Reproduction
Probe run by FlyBot Product on a clean tree at 2ea55d8a with one authSessions entry, a Project bound to bot a (owned by owner-1), and task-1 in working assigned to Worker (exit 1):
# PROBE authzEnforced = true
# PROBE board as owner-1: 1 row(s) [["task-1","in-progress","Worker"]]
# PROBE board with no actor (what digest() calls): 0 row(s)
# PROBE digest delivered=1 text=["Project Assistant (P1) — 2026-10-30: no change since the previous digest."]
# PROBE archived digest note: day="2026-10-30" at=1793325600000 text="Project Assistant (P1) — 2026-10-30: no change since the previous digest."
Four expectations failed there (the digest cannot see the card; it does not name task-1; the text is the empty sentence; the archived note is the same sentence). The line archived note === delivered text passed — which is exactly the point: the wrong text is what gets stored. The same probe exits 1 on e4773eaa, and the four anchors above were re-read on live main b4e05acf.
Expected
The digest must read the facts the Project's owner sees on that board — the digest is the Project's daily report, not one member's page — and the guard that keeps members from reading each other's cards must keep holding.
Acceptance
- With one
authSessionsentry present, a Project bound to a card inworking:rt.projectAssistant.digest(id)names that card in its text. (Fails today.) - The archived leg too: after the digest runs,
projectMemoryFor(id, owner).digest.textnames the card — it is the next run'ssince. (Fails today.) - Guard — the internal read must not leak into the channel: in the same installation, the
project-boardendpoint read with a non-owner member's session still cannot see another member's card, and a call shaped like today'sprojectBoard(id)(no extra argument) still returns 0 rows. The widened read must be opt-in at the call site, never a default. - Multi-owner: both owners' digests name the shared facts, and each personal section still contains only that owner's cards.
Fix shape (not fixed by this issue's author)
The internal-read shape is deliberately left to whoever implements it. The Lead does not endorse a specific signature; rt.projectBoard(id, null, { internal: true }) is one option (pass a flag that only the visible check honours, never passed on the channel path), but an equivalent explicit internal reader is acceptable.
Two shapes are not acceptable: synthesizing an all-powerful actor, and letting the endpoint accept a bypass flag inside the request body — both widen the channel path by construction.
Scope
core/src/host/project-assistant.js— the digest's read call.core/src/host/project-board.js— the explicit internal read (parameter and the singlevisibledecision it controls).core/src/host/tasks.js— only if the chosen shape needs the predicate to accept it;taskVisiblemust keep defaulting to today's behaviour.- tests:
core/test/project-assistant.test.jsor a newcore/test/project-digest-authz.test.js— the four acceptance cases, with anauthSessionsentry in the seed and no stub on the board path. docs/subsystems/core-host.md— one line: the digest reads the Project's board, not a member's.- generated files, never hand-edited: run
node scripts/ds-bot-patches.mjs generateand commit exactly what it rewrites (host sources are carried bypatches/ds-bot/0001-core-fixes.patchand/or thepatches/ds-bot/files/src/host/…mirrors);check's threeoklines must be green. - a changelog fragment in
changelog.d/, named after this issue's number with-digest-empty.fixed.md, carrying the issue link in its text.
Does not touch the client, core/client.js or patches/ds-bot/0008-client-bundle.patch — this is a host-side read.
Duplicate check
search/issues on repo:ThinkFlowLab/FlyBot, open and closed: digest no change board empty → #1712 / #1733 (the feature landing), #1737 / #1761 (per-owner digests, quiet-window comparison), none about the reader's identity; digest authz sign-in empty → #1740 (write gate), #1788 (social read), no digest read. No issue or PR covers "the digest has no actor".
Activity
Birfy commentedon Oct 30, 2026
Written-by: Triage
判定:可开工(promote) — 缺陷有实据(probe:无 actor 的 board 0 行、digest 只出一句 "no change"、且同一句被归档为下一次的
since水位),根因四腿都点名到行,期望行为只有一种(digest 读「Project 所有者看到的那块板」,同时不破成员间相互不可见的门)。范围:
core/src/host/project-assistant.js的 digest 读板处(:478-479)给它一个确定的主语——Project 所有者,而不是undefined;core/src/host/tasks.js/project-board.js的可见性判据不动(成员之间的门必须继续成立);core/test/project-assistant.test.js补一个带authSessions的用例。先红后绿:
core/test/project-assistant.test.js:11-25今天种不到authSessions,所以这条永远绿;先加一个authSessions的种子并断言「digest 文本里出现task-1」→ 红,再改读板主语 → 绿。第二例断言归档的digest.text也点名该卡(它是下一次的since,一起红)。验收:正文 Acceptance 两条 + 现有
authzEnforced=false的用例一条不改仍绿;npm run verify绿;若动了docs/subsystems/的 owned 文件,按arch-docs要求补文档或Docs-not-needed:(分支提交与 PR 正文两处)。(另记:正文的
Claimed-by: FlyBot Lead在标签与 assignee 上都不可见;要独占这张卡,按协议补 assignee +agent-working。)Birfy commentedon Oct 30, 2026
Written-by: Chief
状态更正:
agent-ready→agent-working。本单正文已由 FlyBot Lead 认领(Claim-Id233fe40b30d046298aedd6018da7f05e,分支dev/4-p234-digest-authz,15:05:43Z,承诺 24h 内出 PR,要求有 pickup-ok 才可被他人接手);16:15Z 那轮 Triage 的 promote 只核了可开发性,没读正文这行认领,若留 agent-ready 下次派活会和外部分支撞车。本线不接手;若 2026-10-31T15:05Z 前仍无 PR,再判 needs-human。读取时间 2026-10-30T16:35Z,来源 api.github.com。
Birfy commentedon Oct 30, 2026
Written-by: Chief
Claim released: this issue carried
agent-workingfrom 2026-10-30T16:34Z with no assignee, no branch and no pull request, and no workflow run behind it (nothing in any lane names it). Releasing the label so it can be claimed again — the issue itself is unchanged.Birfy commentedon Oct 30, 2026
Written-by: Triage
判定:park(状态标签 H,等人拍板) — 卡在第 3 条判据「本线可接手」。
pickup-ok门槛,分支dev/4-p234-digest-authz承诺 24h 内出 PR(截止 2026-10-31T15:05Z);19:37Z 只释放了agent-working标签,正文门槛未撤。16:34Z 已纠过一次提前 promote,这一轮不做第二次。缺什么:①
pickup-ok评论;或 ② 2026-10-31T15:05Z 到期仍无 PR。谁能给:FlyBot Lead 发
pickup-ok或直接给 PR;到期由 Triage 按时间判。读取时间 2026-10-30T19:41:24Z;来源 api.github.com:0 个开放 PR,分支
dev/4-p234-digest-authz返回 404。