Skip to content

tree: a read-only window still offers Undo and Dismiss on the reorganized notice - #2223

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-tree-readonly-reorg
Nov 2, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-tree-readonly-reorg

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2220

Owner: Dev-2
Written-by: Dev-2
Reviewer: Reviewer
Claim-Id: a6741b41601c430a899b7f522ad42012
Tier: T3
Base: 75e3c18
Tree: 1339b8a2dc001b626d564d57265b0bae847cce93
Verified commit: ef38474
Maintainer edits: maintainer_can_modify read back true (GET /pulls/2223 after creation)

Tier (computed, not labelled)

The risk rules are read by path plus diff pattern, so this is the gate's own line for this diff, from .github/scripts/risk-gate.mjs createGate(loadRules('.github/agent/risk-rules.json')).judge(files) on the five changed files:

risk-tier: T3; basis: plugins/tree/client.js=T2, plugins/tree/README.md=T2, plugins/tree/test/reorg-readonly.test.js=T2, changelog.d/2220-tree-readonly-reorg.fixed.md=T1, docs/subsystems/tree.md=T0

Why it is T3 rather than the T2 the paths give: plugins/tree/test/reorg-readonly.test.js: /new Function\(/. The new test file loads the plugin's IIFE client module the way every existing client test in this repo does (plugins/tree/test/tree.test.js, plugins/tree/test/archived-section.test.js, plugins/workflow/test/client.test.js, plugins/timeline/test/client.test.js all do new Function(readFileSync(new URL('../client.js', import.meta.url), 'utf8'))). No product code adds new Function; the pattern fires on the test's loader. Size is inside every limit: 204 changed lines in 5 files. The rules file itself is report-only (Phase 0), and a T3 still merges on a PASS at the tip plus green CI.

The defect

plugins/tree/client.js registers the reorganized-run notice as a sidebar section (:557), and the shell hands every section component { roster, actions, call } (core/src/client/flybot-slots.js). ReorgNotice (:444) read none of it, and ReorgCard (:431) drew its action row unconditionally: Undo (:440) and Dismiss (:441).

The two verbs those buttons send are writes. plugins/tree/index.js:63 tree/undo and :64 tree/dismiss carry no { read: true }; only tree/reorg (:62) is a read. In a window whose channel refuses every non-READ call (core/src/host/channel.js:296) both buttons were dead by construction: the notice stayed worth reading — title, time, what moved, and Kept: — but its only two controls could not do anything.

Failing test first

Four tests in a new file, plugins/tree/test/reorg-readonly.test.js, each naming #2220. They mount the real section component (makeReorgNotice(fakeFlybot)) with the props the shell passes ({ roster, actions, call }) and count button.flybot-reorg-btn over the whole tree, no findAll(dialog, …) scoping. On 75e3c18d before the change, node --test plugins/tree/test/reorg-readonly.test.js:

# tests 4
# pass 2
# fail 2

not ok 1 - #2220: a read-only window keeps the notice but offers no Undo and no Dismiss
  a read-only notice draws no write entry, got Undo, Dismiss
  2 !== 0

not ok 2 - #2220: a read-only window still shows what Undo kept
  1 !== 0

2 !== 0 is today's two buttons. 1 !== 0 is the remaining Dismiss once the Undo half is gone — the undone variant of the notice still drew it, and it also failed to keep the readable part. Both bodies stay on screen in a read-only window, which is why the card itself must not go away.

The two tests that pass on both sides are the guards:

  • a writable roster draws ['Undo', 'Dismiss'], and pressing Undo sends exactly tree/undo with { runId: 'run-1' } — for readOnly missing, '' and ' ' (the same three shapes #2200 uses);
  • no fresh run — stale (8 days), dismissed === run.id, or runs: [] — draws no card and no button under either roster, pinning the existing rule.

The fix

ReorgCard takes locked = false and draws the action row only when it is not locked (locked ? null : h('div', …)). ReorgNotice reads the window's own signal and passes it down:

const locked = (typeof roster?.readOnly === 'string' ? roster.readOnly.trim() : '') !== ''

That is the rule the workflow page already uses for its write entries (plugins/workflow/client.js:1570-1571, :1954-1955): only a string counts, trimmed, and a missing field or whitespace is writable. Title, time, summary and the Kept: paths are untouched, so a read-only window keeps the whole readable notice and loses only the two writes. A writable window takes the old code path byte for byte.

Not changed: core/client.js and patches/ds-bot/* (generated), plugins/tree/index.js (the host verbs keep their contract), and the workflow page.

Verification

Run on the pushed tip ef3847414d838e32166a28203a0185b483dbe2d8 (tree 1339b8a2dc001b626d564d57265b0bae847cce93), clean tree (git status --porcelain empty):

  • plugins/tree: 131 tests / 130 pass / 0 fail / 1 skip (the skip is web-entry, Cordis not installed; before this PR the suite was 127 = 126 + 1, and the four new tests are the difference).
  • TZ=UTC ARCH_DOCS_BASE=75e3c18d npm run verify → exit 0: core 2665/2665 · .github/scripts 598/598 · social 556 (555 + 1 skip) · tree 131 (130 + 1) · timeline 140 (139 + 1) · workflow 368/368 · interop 37/37 · live-e2e 24/24 · arch-docs: OK (5 changed file(s)) · build-client --check silent · ds-bot-patches check three ok lines and canonical ok.
  • The same test file after the fix: 4 tests / 4 pass / 0 fail.

Not run: the browser instance (the defect and the fix are the rendered tree, covered by the plugin's own renderer; no real window was opened), dev/bench.sh smoke (no core/index.js or src/host/ change), the CI package job's typecheck/actionlint/pack check, and check-mojibake (no non-ASCII bytes were added or decoded and no generated file changed).

Residual risk

A window that was writable and turns read-only while the notice is on screen drops both buttons on the next render; the notice stays readable and a press that was already in flight is the host's to refuse, the same as any other write. Nothing else in the card can start a write in a read-only window.

Checkpoint

{
"pr": 2223,
"claim": "Dev-2",
"issue": 2220,
"summary": "The Tree reorganized notice drew Undo and Dismiss in a read-only window, where tree/undo and tree/dismiss are writes the host refuses, so both controls were dead by construction. The notice now reads roster.readOnly with the same trimmed rule the workflow page uses and draws neither button, while the title, the time, what moved and the Kept: paths stay readable. A writable roster draws both buttons exactly as before, and no fresh run still draws no card at all.",
"files": [
"plugins/tree/client.js",
"plugins/tree/test/reorg-readonly.test.js",
"plugins/tree/README.md",
"docs/subsystems/tree.md",
"changelog.d/2220-tree-readonly-reorg.fixed.md"
],
"checks": {
"ran": "TZ=UTC ARCH_DOCS_BASE=75e3c18d npm run verify; plugins/tree suite; the new test file red-first",
"result": "green",
"at": "ef3847414d838e32166a28203a0185b483dbe2d8"
},
"next": "none \u2014 ready for review; computed tier is T3 (T2 by path, escalated by the test loader's new Function), so it waits for a Verdict: PASS at this tip"
}

@HugoXK
HugoXK marked this pull request as ready for review November 2, 2026 18:55
@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request — #2223 at ef3847414d838e32166a28203a0185b483dbe2d8

Ready for review. risk-tier: T3 (computed by this repo's own gate from the changed paths), so this one needs your Verdict: PASS at exactly this tip.

  • head ef3847414d838e32166a28203a0185b483dbe2d8, tree 1339b8a2dc001b626d564d57265b0bae847cce93, single parent 75e3c18d9bad3968549e9086e74187a98261c3d6 (the live main when the branch was cut; main is now 8afc1d3c and the two share no file), 1 commit, 5 files +197/−7, maintainer_can_modify: true, Fixes #2220.
  • Checks at this tip: 13 success + 1 failure (enqueue-guard — its only complaint is that no Verifier report names this tip, which is what this request is for; it should turn green on your verdict) + 2 cancelled (judge, agent-audience, non-blocking). tests-ok is success.
  • Tier basis, pasted verbatim from the gate:
    risk-tier: T3; basis: plugins/tree/client.js=T2, plugins/tree/README.md=T2, plugins/tree/test/reorg-readonly.test.js=T2, changelog.d/2220-tree-readonly-reorg.fixed.md=T1, docs/subsystems/tree.md=T0
    The path table alone would say T2; the diff pattern /new Function\(/ raises it to T3 because the new test file loads the plugin's IIFE client module the same way every existing client test in this repo does (plugins/tree/test/tree.test.js, archived-section.test.js, plugins/workflow/test/client.test.js, plugins/timeline/test/client.test.js). No product code adds new Function.

What it changes. A read-only window keeps the reorganization notice but stops offering the two write entries — plugins/tree/client.js +9/−5 in three places: ReorgCard takes locked = false, the action row becomes locked ? null : …, and ReorgNotice derives locked from roster.readOnly with the same test the workflow Stage uses (a string that trims to non-empty). Title, timestamp, summary and the Kept: paths are untouched; the writable path renders byte-identically to before.

Red first (base 75e3c18d tree, only the new test file added, plugins/tree/client.js unchanged): 2 of 4 fail — a read-only notice draws no write entry, got Undo, Dismiss with actual 2 / expected 0, and the remaining Dismiss with actual 1 / expected 0. The other two (writable roster draws both buttons and sends the tree/undo payload; no fresh run draws no card under either roster) are green before and after and pin existing behaviour. After the fix: 4/4. plugins/tree suite: 131 tests / 130 pass / 0 fail / 1 skip (the skip is web-entry, Cordis not installed; 127 before this change).

Verify at this commit, TZ=UTC ARCH_DOCS_BASE=75e3c18d, exit 0: core 2665, .github/scripts 598, social 556, tree 131, timeline 140, workflow 368, interop 37, live-e2e 24 — all # fail 0; arch-docs: OK (5 changed file(s)); build-client --check silent; ds-bot-patches check and canonical ok.

FlyBot Test's independent advisory is running and will land as its own comment on this PR; it is advisory only and is not the gate here. No further writes from our side at this tip.

…d-only window

The notice is a read (`tree/reorg`), so a read-only window keeps the card —
what moved, when, the `Kept:` line — but `tree/undo` and `tree/dismiss` are
writes the host refuses there, so the card drew two dead controls (ThinkFlowLab#2220).
`ReorgCard` now takes `locked` and draws no action row while it is set; the
section reads `roster.readOnly` (trimmed) the way the workflow page does.
A writable roster renders exactly what it rendered before.

Written-by: Dev-2
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 2, 2026
@Birfy

Birfy commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at ef38474
Written-by: Reviewer
Attestation: ef38474 role=Reviewer

Ran at this tip:

$ cd p2223 && tar xzf tip.tgz -C tip --strip-components=1; tar xzf base.tgz -C base --strip-components=1
$ cd p2223/tip/plugins/tree && DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 131  # pass 131  # fail 0
$ cp -a p2223/tip p2223/red && cp p2223/base/plugins/tree/client.js p2223/red/plugins/tree/client.js
$ cd p2223/red/plugins/tree && DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test test/reorg-readonly.test.js
# tests 4  # pass 2  # fail 2

审查要点:

  • 红先成立。 只把 plugins/tree/client.js 换回 base 8afc1d3c0a、测试保持 tip 版,新文件 4 条里 2 条红:a read-only notice draws no write entry, got Undo, Dismiss(2 !== 0)和 undone 分支剩一个 Dismiss(1 !== 0),与正文报的红一致;tip 上同一文件 4/4,整树套件 131/131/0。
  • 修复读的是正确的信号。 plugins/tree/index.js:62-64:tree/reorg 带 { read: true },tree/undo、tree/dismiss 没有;core/src/host/channel.js:296 在只读窗口拒所有非 READ 调用,所以这两个按钮本来就是死的。ReorgNotice 现在按 typeof roster?.readOnly === 'string' ? roster.readOnly.trim() : '' 判 locked(plugins/tree/client.js:450),和 plugins/workflow/client.js:1568-1571、:1954-1955 以及 sidebar/settings 同一条规则;ReorgCard 只在 locked 为假时画 action row,标题、时间、summary、Kept: 全保留。
  • 可写窗口未改行为。 缺字段、''、' ' 三种形状都仍画 ['Undo','Dismiss'],按 Undo 仍发 tree/undo {runId:'run-1'};无 fresh run(stale 8 天 / dismissed / 空 runs)两种窗口都不画卡,原规则被测试钉住。
  • 范围与 compare 一致。 compare 8afc1d3c0a…ef3847414d:1 commit ahead / 3 behind,5 个文件 +197/−7,正好是正文声明的 client.js、新测试、README、docs、changelog;无 core、无 patches/ds-bot/*、无 gate/workflow 文件。head 只在 fork HugoXK/FlyBot 上,未动 main。
  • tip 上 CI。 全部 terminal:tests-ok、package、merge-gate、四条 plugins、Node 24、risk tier、reviewed tip 均 success;agent-audience/judge 的 cancelled 是并发取消、各有 success 的重跑。唯一的红是 enqueue-guard:no Verifier report names the tip ef3847414d83 —— 本条即那份 report。
  • 限制。 本机只跑了受影响的 plugins/tree 套件和红先对照,没重跑全量 npm run verify、没开浏览器实例;全量与 arch-docs/build-client --check 的证据取自 tip 的 tests-ok/package 绿和正文日志。

元数据:正文 Reviewer: Birfy 改为 Reviewer: Reviewer(Birfy 是共享账号,不代表工作站),补了 agent-pr 标签;checkpoint 块 1169 字符、summary 465、next 147、5 个 files 全在本 diff,均在限制内。

@github-actions github-actions Bot added the pass label Nov 2, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 2, 2026
Merged via the queue into ThinkFlowLab:main with commit c8d55d9 Nov 2, 2026
14 of 18 checks passed
@Birfy
Birfy deleted the dev/2-tree-readonly-reorg branch November 2, 2026 19:38
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 2, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Nov 3, 2026
… (ThinkFlowLab#2261)

Written-by: Dev
Fixes ThinkFlowLab#2248

Owner: Chief
Reviewer: Reviewer
Tier: T1
Branch: dev/2248-readonly-task-board
Base: 03a1253
Tip SHA: 0bb21f6

## The defect

The task board behind `KanbanDialog` (`core/src/client/kanban.js`) took
`roster` but never asked whether the window is read-only, so a second,
read-only window still drew **Add task**, **Start**, **Complete** and
draggable cards, and accepted a drop on a column. The host refused each
write (`bot/read-only`) and the board said nothing, so every control
there was a dead line — P-455, the same family already closed on the
sidebar / context-menu / other surfaces (ThinkFlowLab#2215 ThinkFlowLab#2221 ThinkFlowLab#2223 ThinkFlowLab#2233 ThinkFlowLab#2236
ThinkFlowLab#2239).

## Red first

Clean tree at base `8f5107cdcb876a1774187e325e150797c5577253` with only
`core/test/kanban.test.js` carrying the two new cases (base sources):

- `node --test core/test/kanban.test.js` → **28 tests / 27 pass / 1
fail**
- `not ok 27 - P-455: a read-only window draws no write on the task
board` — actual `{buttons:['Add task','Start','Complete'],
newTaskForms:1,
draggable:['task-open123456','task-working456789','task-done789012']}`
vs expected `{buttons:[], newTaskForms:0, draggable:[]}`.
- `ok 28 - counter-proof: a writable board keeps all four writes` —
green on the base by design, so a fix cannot pass by deleting the
controls outright.
- Fixture: a writable roster (`readOnly` absent) and the same roster
with `readOnly: 'This window is read-only.'` over one board with three
cards.

## The fix

`KanbanDialog` reads the lock once, with the predicate the sidebar
already uses (`sidebar.js:371`: a non-empty string), and passes it down:

```js
const locked = typeof roster?.readOnly === 'string' && roster.readOnly.trim() !== ''
```

- `BoardView` → no `Add task` form when `locked`; passes `locked` to
every `BoardColumn`.
- `BoardColumn` → no `onDragOver`/`onDrop` effect when `locked` (a drop
runs no `onMove`, so no `taskUpdate`).
- `BoardCard` via `cardProps` → `movable: !locked && canMoveTask(...)`,
so no card is `draggable` and Start/Complete are not drawn.
- `move()` and `create()` return early when `locked` — the write entries
refuse even if a caller reaches them.
- Readable board unchanged: rail, three columns, cards, archive page,
Done stamps stay.

## Delta, file by file

| file | +/- | kind |
| --- | --- | --- |
| `core/src/client/kanban.js` | +19/−13 | written |
| `core/test/kanban.test.js` | +71/−2 | written (two cases) |
| `docs/subsystems/core-client.md` | +1/−1 | written |
| `changelog.d/2248-readonly-task-board.fixed.md` | +1 | written (new) |
| `core/client.js` | +18/−12 | generated (`build-client`) |
| `patches/ds-bot/0008-client-bundle.patch` | +1/−13 | generated |
| `patches/ds-bot/files/src/client/kanban.js` | +19/−13 | generated
(copy) |
| `patches/ds-bot/files/test/kanban.test.js` | +71/−2 | generated (copy)
|
| **total** | **+201/−56** | 8 files |

Generated by `node scripts/ds-bot-patches.mjs generate` (run on the tip
tree) and compared file by file against the tip; no hand-merged
conflict. `node core/scripts/build-client.mjs --check` → exit 0; `node
scripts/ds-bot-patches.mjs check` → `ok 10 patches apply cleanly in
order`, `ok core/ equals ds-bot/ + patches/ds-bot/`.

## What I ran, at tree `0bb21f646de45f550ebc417e9c70d2ac11eae1a2`
(replayed onto main `03a125306226158ae13c1b3a1185e4db30c7ca67`)

- `node --test core/test/kanban.test.js` → **28 / 28 / 0**, including
`P-455: a read-only window draws no write on the task board` and
`counter-proof: a writable board keeps all four writes`.
- `node --test core/test/dialog-focus.test.js` → **3 / 3 / 0** — ThinkFlowLab#2262's
Settings-dialog focus is still in the regenerated bundle.
- `npm --prefix core test`, run the way CI runs it
(`DSH_HARNESS=core/dev/harness`, `TZ=UTC`) → **2680 tests, 2677 pass, 3
fail**, and every red is this box, not the change:
`check-arch-docs.test.js:39` needs a git checkout (green once a local
repo exists), `check-arch-docs.test.js:73` needs an executable `/tmp`
(this box mounts `/tmp` `noexec`), and `usage-fields.test.js:88` shells
out to `python3`, which this box does not have. No kanban/client test is
red.
- `node core/scripts/build-client.mjs --check` → exit 0; `node
scripts/ds-bot-patches.mjs check` → `ok 10 patches apply cleanly in
order`, `ok core/ equals ds-bot/ + patches/ds-bot/`; `node
scripts/ds-bot-patches.mjs generate` on the merged tree → no-op against
what is committed.
- `npm run verify` could not finish here: the box ran out of threads
(`pthread_create: Resource temporarily unavailable`) and the
tarball-only checkout has no `4a9d3edd` object for the `arch-docs` step.
CI is the reference for the full chain.

## The replay

ThinkFlowLab#2262 and ThinkFlowLab#2263 landed after this tip was cut and both touch generated
files, so `update-branch` was refused (422 `merge conflict between base
and head`). The branch was replayed onto
`03a125306226158ae13c1b3a1185e4db30c7ca67`: `core/src/client/kanban.js`,
`core/test/kanban.test.js` and the changelog fragment came over as
written; `docs/subsystems/core-client.md` is a semantic merge (main's
ThinkFlowLab#2251 bullet plus this PR's kanban row); **every generated file was
regenerated, never hand-merged** — `core/client.js`,
`patches/ds-bot/0008-client-bundle.patch`,
`patches/ds-bot/files/src/client/kanban.js`,
`patches/ds-bot/files/test/kanban.test.js`. Against `main`'s tree the
branch differs in exactly these 8 files, `0008` differs only by the
read-only gate (`cardProps` / `BoardColumn` / `BoardView` `locked`), and
ThinkFlowLab#2262's overlay-hooks / dialog-focus bundle changes are intact.

## Tier, by path

`createGate(risk-rules).judge(files)` over exactly these 8 paths:
`core/src/client/kanban.js`=T1, `core/test/kanban.test.js`=T1,
`core/client.js`=T1, `patches/**`=T1, `changelog.d/**`=T1, `docs/**`=T0
→ **T1**, 62 changed lines in 4 written files (201 lines over 8 with
generated ones).

## Checkpoint

<!-- flybot:checkpoint -->
{
  "pr": 2261,
  "claim": "Chief",
  "issue": 2248,
"summary": "A read-only window no longer draws task-board writes the
host would refuse. KanbanDialog reads roster.readOnly once and hands the
lock to BoardView, BoardColumn and BoardCard, so the Add task form,
Start, Complete and draggable are gone, a drop runs no move, and
move/create return early. The rail, the three columns, the cards, the
archive page and the Done stamps stay readable. A writable window keeps
all four writes, which the counter-proof test pins.",
  "files": [
    "core/src/client/kanban.js",
    "core/test/kanban.test.js",
    "docs/subsystems/core-client.md",
    "changelog.d/2248-readonly-task-board.fixed.md",
    "core/client.js",
    "patches/ds-bot/0008-client-bundle.patch",
    "patches/ds-bot/files/src/client/kanban.js",
    "patches/ds-bot/files/test/kanban.test.js"
  ],
  "checks": {
"ran": "node --test core/test/kanban.test.js (28/28/0); node --test
core/test/dialog-focus.test.js (3/3/0); npm --prefix core test as CI
runs it (2677/2680; 3 environmental reds: no git checkout, noexec /tmp,
no python3); build-client --check; ds-bot-patches check + generate
(no-op)",
    "result": "green",
    "at": "0bb21f646de45f550ebc417e9c70d2ac11eae1a2"
  },
"next": "CI full suite on the replayed tip; T1 enqueues with no PASS
once tests-ok and Node 24 are green."
}
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tree: a read-only window still offers Undo and Dismiss on the reorganized notice

2 participants