Repository navigation
feat(workflow): the Workflows page, and columns drawn from the definition (#1272 step 3) - #1304
Conversation
|
Evidence the six columns did not carry: the preset files are byte-identical. The issue asks for the preset sha256 before/after. Computed on the branch tree (
Both presets are untouched by this branch: the page copies a preset into a workspace definition ( Posted as a comment rather than edited into the body because the body is frozen until the one-time rebase after #1309 lands; it goes into the "What I ran" column then. |
|
Verdict: PASS at 206fbad What I checked myself
Generated artifacts: CI at this tip: every check run is completed. Findings, none of them blocking this tip
Ran: |
…Lab#1305) (ThinkFlowLab#1309) Written-by: Dev Fixes ThinkFlowLab#1305 Owner: Dev Reviewer: Reviewer Tip SHA: 7abd38a Docs-not-needed: needsCount gains an optional now and the two assertions pass NOW; no behaviour change for the badge (its callers still pass three arguments), no user-facing API, and docs/subsystems/core-client.md already describes the Inbox badge. `main` has been red since `2026-10-27T00:55Z` on `Node 24` because two assertions in `core/test/inbox.test.js` compare the badge count against a model built with a fixed clock while the badge helper read the wall clock. The fix is **explicit `NOW` / a frozen clock, never an adjusted expectation**: `7` and `8` stay exactly as they are, so the suite cannot go red again next week when the 7-day window slides one more notch. ## Reproduce - On a clean tree of `main` `59df1580` (tree `4cfba88956de2c5e9ab144dd743b78493b846994`): `cd core && node --import ./test/harness-resolve.mjs --test test/inbox.test.js` → `26 tests / 24 pass / 2 fail`, `Expected values to be strictly equal: 7 !== 6` at `test/inbox.test.js:122`, `8 !== 6` at `:297`. - The whole suite at that tree: `1767 tests / 1761 pass / 6 fail` — the two inbox cases plus four that are red on this machine only (see *Not run*). - `main`'s own last green `Node 24` run was `59df1580` at `00:49Z`; the same commit's code is red from `01:02Z` (job `121945236810` on `ThinkFlowLab#1303`, and `8c256fbe` on `ThinkFlowLab#1304` at `00:56:45Z`, job `121943086386`). ## What changed - `core/src/client/inbox.js:274` — `needsCount = (roster, work, tasks, now) => inboxModel({ roster, work, tasks, now }).needs.length`. `now` is optional and forwarded as-is: `inboxModel` already defaults it to `Date.now()`, so the badge's real callers (`inbox.js`, three arguments) keep the wall clock and nothing user-visible moves. - `core/test/inbox.test.js:122` and `:297` — pass `NOW` (`needsCount(roster, work, tasks, NOW)`, `needsCount(roster, endedWork, tasks, NOW)`). The expected values are untouched: `model.needs.length` (7) and `model.needs.length + 1` (8). - `core/client.js` rebuilt with `node core/scripts/build-client.mjs` (1 line), then `node scripts/ds-bot-patches.mjs generate` regenerated `patches/ds-bot/0008-client-bundle.patch`, `patches/ds-bot/files/src/client/inbox.js` and `patches/ds-bot/files/test/inbox.test.js`. `git add -A` first, then `check` — the sequence matters, an untracked mirror makes `check` falsely green. Same shape as the interim `206fbadb` on `ThinkFlowLab#1304` (`fix(inbox): the badge count reads the clock the list reads (ThinkFlowLab#1305)`), so the two branches merge without a conflict; once this lands, rebasing `ThinkFlowLab#1304` will drop that commit as empty. ## What I ran At `7abd38a0e199b4ba24578b2d9597a21c0a71a82e`: - `cd core && node --import ./test/harness-resolve.mjs --test test/inbox.test.js` → `# tests 26 / # pass 26 / # fail 0`, run twice. - `cd core && DSH_HARNESS=<harness> node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/*.test.js` → `# tests 1767 / # pass 1763 / # fail 4`; the four are `test/composer-label.test.js`, `test/header-effects.test.js`, `test/read-page.test.js` and `fixture usage.sqlite has one real calls row with all five fields`, and all four fail identically on the clean `59df1580` tree (`10 tests / 6 pass / 4 fail` for those files there), so none of them is from this diff. The two inbox cases are `ok` in the full run (`ok 773`, `ok 787`). - `node core/scripts/build-client.mjs --check` → exit 0. - `git add -A && node scripts/ds-bot-patches.mjs check` → `ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70`, `ok 10 patches apply cleanly in order`, `ok core/ equals ds-bot/ + patches/ds-bot/`. - `node core/scripts/check-arch-docs.mjs` → `arch-docs: OK (6 changed file(s))` (with the `Docs-not-needed:` trailer above, which is also in the branch commit). - `node .github/scripts/check-mojibake.mjs --check` → `mojibake: ok — 0 occurrences of U+FFFD in 1114 text file(s)`. - `cd core && npm run lint` → `Found 67 warnings and 0 errors` (the warnings are pre-existing). ## Red first The red is the base, and it is the *same two cases* the fix turns green — so the red-first evidence is the test change on its own, with the source line still old: 1. `core/test/inbox.test.js` changed to pass `NOW`, `core/src/client/inbox.js` untouched → `26 tests / 24 pass / 2 fail`, `expected: 7 actual: 6` (`:122`) and `expected: 8 actual: 6` (`:297`). 2. Add the one-line `now` parameter → `26/26`. Reverting only that line from this tip gives the first result back, with the other 25 tests still green. ## Not run (and why) - The four reds above (`composer-label`, `header-effects`, `read-page`, the usage fixture) are green in CI on `main` and red here on the clean base tree too; this PR does not touch them. - Plugins (`social`/`tree`/`timeline`), `.github/scripts`, interop and live-e2e suites: no file outside `core/src/client/inbox.js`, `core/test/inbox.test.js` and the generated bundle/patches changes in this PR, and CI runs them. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1309, "claim": "Dev", "issue": 1305, "summary": "The Inbox badge read the wall clock while the two assertions built their model from a fixed NOW, so the suite went red at 00:55Z when the 7-day window slid one notch. needsCount now takes an optional now and forwards it to inboxModel (the badge's own callers pass three arguments, so the clock default is unchanged), and test/inbox.test.js:122 and :297 pass NOW. Expected values 7 and 8 are untouched: the assertion stopped reading the clock instead of the number being adjusted. 26/26 inbox tests here, generated client bundle and patches mirrors rebuilt, ds-bot-patches check green.", "files": ["core/src/client/inbox.js", "core/test/inbox.test.js", "core/client.js", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/files/src/client/inbox.js", "patches/ds-bot/files/test/inbox.test.js"], "checks": {"ran": "node --import ./test/harness-resolve.mjs --test test/inbox.test.js && node core/scripts/build-client.mjs --check && node scripts/ds-bot-patches.mjs check", "result": "green", "at": "7abd38a0e199b4ba24578b2d9597a21c0a71a82e"}, "next": "Reviewer judges this tip; then the merge queue" } ``` <!-- /flybot:checkpoint -->
6bdacac to
d7bd1c1
Compare
|
Verdict: PASS at d7bd1c1 This verdict is bound to The tip is the same change, replayed onto
|
…tion (#1272 step 3) (#1299) A definition is a file: `workflows/<id>.json` in the workspace, served by `workflows` and written by `workflow-save` (in authz's TASK_ENDPOINTS). `workflow-doc.js` makes the bytes canonical, so a save of an unedited definition is byte-identical, and `Customize` copies a preset into a new file instead of editing the shipped one. The board's columns come from the definitions in play: a card names `workflow:<id>` and `node:<id>` with two labels, `workflow-board.js` lays it in that definition's lane, and the view draws one section per workflow whose header names the workflow. A card that names no workflow keeps the five columns it had, so nothing moves and no second card schema is introduced. The page is a node table with the flowchart drawn from it, and each guard shows the sentence this definition would print under the facts read now — or says the fact could not be read, never a pass.
…nt menu alone The page list and the account menu are pinned by three tests that the first commit did not run: `settings-page-slot` (the six-page prefix and the plugin slot after Connectors) and `flybot-client` (the account menu's exact labels). Adding the page before `Capabilities` and a menu row beside `Connect apps` moved both. Workflows now sits right after Scheduled tasks, beside the other automation page, so the pinned prefix, the plugin slot and the menu are unchanged; the subsystem note says that instead of promising a menu row that no longer exists. `core/client.js` and `patches/ds-bot/` are regenerated, not edited.
d7bd1c1 to
dae08a9
Compare
|
Verdict: PASS at dae08a9 Ran: Two checks. Head is still Five prerequisites.
The patch → core table, read from the sections myself. I diffed each PR-carried patch against the same patch at
On On Seven cuts, each restored. Every one cut on
After the restores, Generated, not hand-edited. CI. 13 runs on Not blocking
|
|
patch ↔ core file 对应表(依据
|
|
Verdict: PASS at dae08a9 This verdict is bound to The two things that voided
|
Mechanical release that folds the four changelog fragments now on `main` into `## [0.2.11] - 2026-10-27` and bumps the core to `0.2.11`. Covered by the [standing approval for mechanical release PRs](AGENTS.md#standing-approval-mechanical-release-prs). ## Why The user wants to try the **Workflows** page on their own instance; v0.2.10 was cut at 2026-10-27T00:49Z and therefore predates ThinkFlowLab#1304 (`ThinkFlowLab#1272` step 3), which merged at 02:41Z. So this release carries it. ## What changed (only the mechanical release files) - `CHANGELOG.md`: `changelog.mjs release 0.2.11 --date 2026-10-27` folded 4 fragments; `[Unreleased]` left empty. No hand edits. - `changelog.d/`: the 4 folded fragments removed (`1252-board-archive.changed.md`, `1253-claim-cue.fixed.md`, `1266-feishu-connector-row.changed.md`, `1299-workflows.added.md`), `README.md` untouched. - `core/package.json`, `core/package-lock.json`: `0.2.10` → `0.2.11`. - `patches/ds-bot/0007-version-and-tooling.patch`: its three added version lines bumped by hand (no `git` binary on this box). Since v0.2.10: 9 commits on `main` (base `ae2e4615`). ## Ran at this tip (Node 22.23.3, fresh tarball of `main` `ae2e4615`) - `changelog.mjs check` → 4 fragments well formed before the fold, 0 after; `release 0.2.11` wrote the section. - `check-release-version.mjs 0.2.11` → exit 0; `release-notes.mjs 0.2.11` renders the new section. - Diff of this commit: 8 files — the four mechanical files modified plus 4 fragment deletions. Nothing else. Not run: `ds-bot-patches.mjs generate`/`check` (no git), `npm ci` + `npm run harness` + `TZ=UTC npm run verify` (no dependency tree), smoke, typecheck, actionlint — CI covers them at this tip. No tag or release is created by this PR. Preflight (routine step 0, fact-source self-check): latest tag `v0.2.10`, `core/package.json` `0.2.10`, 4 unfolded fragments → **behind**, so this beat releases. `Test` on `main` `ae2e4615` is green (run `40439886391`); no `v0.2.11` release or tag exists. Tip SHA: 65dbb6e Written-by: Release
… path (ThinkFlowLab#1313) fix(report): point the three turn-split commands at the script's real path Fixes ThinkFlowLab#1311 Owner: Dev1 Reviewer: Reviewer Written-by: Dev-1 Tip SHA: 7cacd6e ## What changed `docs/reports/2026-10-26-turn-input-split.md` printed `node dev/turn-split.mjs` at lines 124, 165 and 182. Root `dev/` holds only `interop/` and `live-e2e/`; the script is `core/dev/turn-split.mjs`, so all three commands failed with `Cannot find module` when copied — including the `--stats --room` one that §7's fan-out paragraph tells a reader to run. Those three lines now read `node core/dev/turn-split.mjs …`: three characters each, 3 insertions / 3 deletions in that one file, nothing else in the report touched (§4's pre-existing `31,139` / `31,138` mismatch stays out of scope and unchanged). A new test, `core/test/report-command-paths.test.js`, extracts every `node <script>` token the report prints and asserts the script is a file at the tree root, so a later edit cannot drift from the checkout again. A second case pins the extractor itself (`node dev/x.mjs --flag` → `dev/x.mjs`, `node --version` → nothing), so it cannot pass vacuously. Both run inside `npm run verify`'s core suite — the core output carries `Subtest: the node-command reader finds a script and its line` and `Subtest: every node command in the turn-split report names a script that exists at the tree root`. ## Red first — my own measurement, at `929d89e7`, before the doc was touched `node --import ./test/harness-resolve.mjs --test test/report-command-paths.test.js`, exit 1, `# tests 2 / # pass 1 / # fail 1`: ``` commands the report prints but the tree cannot run: line 124: node dev/turn-split.mjs line 165: node dev/turn-split.mjs line 182: node dev/turn-split.mjs ``` Lines 33 and 36 already printed `node core/dev/turn-split.mjs …` and resolved; they are unchanged. At this tip the same command is 2/2, exit 0. ## Verify at the tip tree — exit 0 `TZ=UTC npm run verify` on tree `493466821d6fe480d9ecb72ce007223c36bd06ce`: - **core suite: 1839 cases at base `ae2e4615`, 1841 at this tip — delta +2**, exactly the two cases the new file adds (both subtest names appear in that run). The earlier "1801" was the count at my previous tip `df1c4b73` on base `4a4cf707` (1795 there, +6 from ThinkFlowLab#1259), so it predated ThinkFlowLab#1288 and was never this base's number. Measured chain, so nobody has to re-derive it: `4a4cf707` 1795 → `b66fc741` (ThinkFlowLab#1288) 1802 → `929d89e7` (ThinkFlowLab#1310 merge) 1808 → `ae2e4615` (ThinkFlowLab#1304) 1839 → this tip 1841. - social 525, tree 125, timeline 125, `.github/scripts` 509, interop 37, live-e2e 24 — every suite `# fail 0`. - core lint 69 warnings / 0 errors; plugins 36 / 9 / 2 warnings, 0 errors. The new file appears in no lint line. - `arch-docs: OK (3 changed file(s))`, rerun with the base named: `cd core && ARCH_DOCS_BASE=ae2e4615 node scripts/check-arch-docs.mjs`. The script's default base is the local `origin/main` ref, which is stale here; with the sha it prints those 3 paths, exactly this diff. - `node scripts/ds-bot-patches.mjs check`, from its own output: `ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)` / `ok 10 patches apply cleanly in order` / `ok core/ equals ds-bot/ + patches/ds-bot/`, exit 0. - `node scripts/build-client.mjs --check` exit 0, inside the verify chain. Mirrors: the report sits outside them, the new test sits inside — so `patches/ds-bot/files/test/report-command-paths.test.js` was written by `generate` and is byte-identical to the source (`cmp`), never hand-edited. Generator context churn on `0014-flybot-branding.patch` was reverted, so the diff holds only these three files. ## Base Base is `ae2e4615` (`main` as of my read immediately before pushing). `main` moved from `929d89e7` while this was being prepared, so the single commit was rebased onto the new tip: the two commits share no path, the rebase was clean, and the tree verified above (`493466821d6fe480d9ecb72ce007223c36bd06ce`) is the rebased one. ## Not run No smoke test (nothing under `core/index.js` or `core/src/host/` changed); no typecheck, actionlint or pack check (the CI `package` job runs them, `verify` does not); no live provider calls. ## Checkpoint <!-- flybot:checkpoint --> { "pr": 1313, "claim": "Dev1", "issue": 1311, "summary": "Lines 124, 165 and 182 of the turn-split report printed `node dev/turn-split.mjs`, a path the tree lacks, so every copied command failed with Cannot find module; they now print `core/dev/turn-split.mjs` and nothing else in that file changed. A new test extracts every `node <script>` token the report prints and asserts the script exists at the tree root, plus a case pinning the extractor so it cannot pass vacuously. Red at 929d89e on those three lines, green in verify's core suite at this tip: core 1839 to 1841, the two cases it adds. Its mirror under patches/ds-bot/files/ is generated.", "files": [ "core/test/report-command-paths.test.js", "docs/reports/2026-10-26-turn-input-split.md", "patches/ds-bot/files/test/report-command-paths.test.js" ], "checks": { "ran": "TZ=UTC npm run verify", "result": "green", "at": "7cacd6ed15ad4cb6f0e784b8dabdc6968866e0f3" }, "next": "none — ready for review" } <!-- /flybot:checkpoint -->
…hinkFlowLab#1307) Written-by: Dev3 Fixes ThinkFlowLab#1287 Owner: Dev3 Reviewer: Reviewer2 Tip SHA: cdbe206 ## Reproduce On this base a task cannot be copied: `core/src/client/schedules.js` offers Retry / Edit / Pause·Resume / Delete and no copy, and `schedule-create` (`core/src/host/schedules.js`) reads only `target`/`title`/`prompt`/`when`, so `{ target, copyFrom }` answers `Give the task a name`. With `core/src/host/schedules.js` restored to the base version and the tests kept, three host tests fail (`not ok 9/10/11`, `# tests 22 / # pass 19 / # fail 3`), and with `core/src/client/schedules.js` restored the client test file fails to build (`No matching export ... CopyRow`). ## What changed - `schedule-create` takes an optional `copyFrom: { sessionId, id }`; the source is resolved through the same ownership check `schedule-update` uses, or from `state.pausedSchedules` when it is paused. - An active source is written as a **new row** in DSH's storage domain inside `service.serialize`: a fresh `schedule-<uuid>` id (collision retried, never overwriting), the source's record under it, `status: 'active'`, an empty `deliveryHistory`, no `lastDelivery`. `service.create` is not used: it re-anchors an `every` task to `now + interval` (ThinkFlowLab#1245) and would carry the source's delivery record, which `checkMissed` reads as "already delivered", so the copy would never fire. No re-arm is needed — the copy's instant is the source's, and the source's own row already keeps it in the driver's global minimum. - A paused source has no next run to keep, so its copy goes through `create` as Resume does. Refusals: unknown source Session or id (`No such task`), unknown target (`No such Bot or group chat`), the same Bot or group chat (`Pick a different Bot or group chat to copy it to`), a passed one-time task (`Pick a time in the future`). Quiet hours are checked as on create. - Client: **Copy** on each row (duplicate icon) turns the row into `CopyRow` — the create form's target picker minus the chat the task already runs in — and posts the same `schedule-create` with `copyFrom`. No new endpoint, no new state, no new config key. - Docs: `docs/subsystems/core-host.md` (**Scheduled tasks: copy**) and `core-client.md`; fragment `changelog.d/1287-copy-scheduled-task.added.md`. - `patches/ds-bot/0008-client-bundle.patch` is regenerated with the real `generate` under git 2.39.5 and taken back into the tree on its own: the earlier one was pieced together while this box had no git, and its last hunk ended without a newline, which a strict applier reads as a truncated hunk. - `patches/ds-bot/0001-core-fixes.patch` is regenerated in the same run, and it has to be: its hunks carry `src/client/i18n.js`, `src/client/schedules.js`, `src/host/schedules.js` and `test/schedules.test.js`, so restoring only `0008` leaves those four reported as differing from `ds-bot/ + patches/ds-bot/`. Both files come from `generate`; the rest of `patches/ds-bot/` is the base's own copy, plus `files/test/schedules-client.test.js` which mirrors the new client test. ## What I ran Rebased onto `ae2e4615`; the tree was rebuilt from a tarball of that commit (`git diff --name-only` against it is the 12 files of this PR). `TZ=UTC`, `TMPDIR` outside the harness home, git 2.39.5 on `PATH`: - `node --import ./test/harness-resolve.mjs --test test/inbox.test.js test/schedules.test.js test/schedules-client.test.js` in `core/` → `# tests 66 / # pass 66 / # fail 0` - `node scripts/ds-bot-patches.mjs check` → `ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)`, `ok 10 patches apply cleanly in order`, `ok core/ equals ds-bot/ + patches/ds-bot/` - `node scripts/build-client.mjs --check` (in `core/`) → exit 0, `client.js` rebuilt from `src/` - also green: `quiet-hours` 9/9, `part-archive` 4/4, `i18n` 2/2, `client-bundle` 2/2, `authz` 20/20, `flybot-client` 14/14, `bot-messages` 27/27, `tool-schema` 6/6 - `cmp` over every `patches/ds-bot/files/**` mirror against `core/` → 0 differ ## Red without the source change At base `ae2e4615`, with the source change reverted and the tests kept: - Host, `core/src/host/schedules.js` restored: `not ok 9 - ThinkFlowLab#1287: a copy lands on the target’s current part with the source’s next run, leaving the source alone`, `not ok 10 - ThinkFlowLab#1287: an unknown source, an unknown target, the same chat and a passed one-time task are refused`, `not ok 11 - ThinkFlowLab#1287: a copy keeps a daily rule’s time and zone, and an earlier part’s task goes to the current one`; `# tests 22 / # pass 19 / # fail 3`. - Client, `core/src/client/schedules.js` + `core/client.js` restored: `ERROR: No matching export in "src/client/schedules.js" for import "CopyRow"`, `not ok 1 - test/schedules-client.test.js`, `# tests 1 / # pass 0 / # fail 1`. ## Not run - `arch-docs`, `oxlint` and `npm run verify`: no oxlint install here and the tree is a tarball rather than a clone; CI runs the rest. - No click-through of the Copy row in a running app (no DSH desktop session here); the client test drives the component tree instead. ## Risk The copy path reads DSH's private storage domain (`getDomain`/`serialize`/`table('tasks')`/`get`/`put` and the row shape) — the same one `moveSchedules` already depends on — and sits outside that function's warn-and-continue loop, so a renamed `get`/`put` rejects the copy and writes nothing. A copy of an already-overdue recurring task fires once soon after it is created: it keeps the source's instant rather than re-anchoring. `lastDelivery` and `deliveryHistory` are deliberately not copied, and a test pins both. The two patch files must travel together: `0001` and `0008` are a pair from one `generate`, and either one alone is refused by `check`. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1307, "claim": "Dev3", "issue": 1287, "summary": "Copy on a task row sends the task to another Bot or group chat: the same title, prompt and timing, on the target's current part. The host writes it as a new row in DSH's storage domain, so the copy keeps the source's next run instead of re-anchoring it (ThinkFlowLab#1245) and starts with no delivery record. A paused source goes through create, and the same chat, an unknown source or target and a passed one-time task are refused.", "files": ["core/src/host/schedules.js", "core/src/client/schedules.js", "core/test/schedules.test.js", "core/test/schedules-client.test.js", "patches/ds-bot/0001-core-fixes.patch", "core/client.js"], "checks": { "ran": "node scripts/ds-bot-patches.mjs check; node --import ./test/harness-resolve.mjs --test test/inbox.test.js test/schedules.test.js test/schedules-client.test.js", "result": "green", "at": "cdbe206b937007e738807d387ad4888a56c9967c" }, "next": "hand to Reviewer2 at this tip; ThinkFlowLab#1304 is merged, so nothing is queued in front of it" } ``` <!-- /flybot:checkpoint -->
… set (#1317) Written-by: Dev3 Fixes #1317 Owner: Dev3 Reviewer: Reviewer2 Docs-not-needed: no page under docs/subsystems/ owns .github/scripts/ or AGENTS.md (arch-docs reads 0 owned files for this diff); the rule text is AGENTS.md and it moves with the code. ## Reproduce At `2c02cd15` any new commit voids a PASS, including the one `PUT /pulls/<n>/update-branch` writes to bring a branch up to date. `decide` compares the whole 40-character tip and nothing else: ``` reviewer-agent: names 175328d0c1b2, not the tip abaf938b0c1d ``` That answer is right when the code changed and wrong when only `main` moved: with a PASS at `175328d0c1b2…` and a head `abaf938b0c1d…` that merged `main` in, every changed path can carry the same blob at both tips and the pull request still pays a whole second review (#1304 was PASSed three times for one change set, once per update-branch). The same `decide` arms `enqueue-guard` and `merge-gate`, so all three paths answer alike — and all three were wrong alike. ## What changed - **New `.github/scripts/change-set.mjs`**: `sameChangeSet({ gh, repo, reviewed, head, paths })` reads `git/trees/<sha>?recursive=1` for both tips and compares the blob sha of every changed path. `{ok:true}` only when every path exists at both tips with the same blob. Fail closed everywhere else — no path list, no reader, a 404, `truncated:true`, no `tree` in the answer, a path missing on either side — and each refusal says which read failed (`git tree of 175328d0c1b2: 404`, `…: truncated`) or which path differs, so "the code changed" and "I could not look" never read the same. - **The comparison rejects a 100-entry list.** Each caller reads one page (`pulls/<n>/files?per_page=100`), so a list that fills the page may be cut short and file 101 could be the file that moved: that refuses as `the changed-file list stops at 100 entries, so it may be cut short` instead of comparing a part of the change set. A 99-entry list compares normally. - **`review-gate.mjs#decide`**: three optional inputs (`changedPaths`, `sameChangeSet`, `gh`). In the `parsed.tip !== headSha` branch — the one comparison point #1317 names — the conclusion is now compared before it is dropped: a match marks the entry `carriedFrom` and keeps it, a mismatch or an unreadable tree refuses with the reason appended. Without a path list the old refusal stands unchanged. - **A conclusion written at the head always decides.** `const exact = atTip.filter((e) => !e.carriedFrom)` and only carried ones are read when the head carries none. This is a new rule, and #1317 needs it: a carried PASS written after an INC at the head would otherwise win on timestamp alone, and the INC would stop vetoing. - **The allow sentence names both full shas and the comparison**: `reviewer-agent's PASS at <T> still describes this head <H>: the change set is identical (every changed path carries the same blob at both tips)`, and the result carries `carried: true` so every renderer can say which PASS it read. - **The three readers stay one cut.** `merge-gate.mjs#judge` passes `changedPaths: fileNames(files)` from the entry's own file list and accepts an injected comparison; `enqueue-guard.mjs#decidePull` forwards it; the ALLOW tails in `enqueue-guard.mjs` and `enqueue-rules.mjs` say why (`… the change set is the one that PASS names, so the tip moved without changing it`). `review-gate.mjs#main` reads the changed-file list itself. - **`AGENTS.md` three places**, all the same rule: `:34` (the pipeline contract's one-conclusion-per-tip), `:174` (Gates — where the queue reader and the `risk-tier` summary point, so leaving it would keep a contradicting sentence two screens below the new one, and the rule now also carries the exception's CI/queue obligation), and `:203` (Merge gates: the mechanism, the fail-closed list, and the precedence rule). - **Fixture**: `.github/scripts/fixtures/workflow-cases.json` gains a top-level `changeSetCases` array — `tip-moved-same-change-set` (allow, with the exact sentence), `tip-moved-one-blob-changed` (refuse, `the change set differs at core/package.json`), `tip-moved-tree-unreadable` (refuse, `the change set could not be read (git tree of 175328d0c1b2: 404)`). It is a new key rather than entries in `cases`, because `core/test/workflow-repo-dev.test.js` consumes `cases` one by one and adding there would mean editing `core/`, which #1317's boundary excludes. ## What I ran At the tree of this commit, `TZ=UTC`, Node v22.23.3: - `node --test ".github/scripts/*.test.mjs"` → `# tests 524 / # pass 522 / # fail 0 / # skipped 2`. - `node --test .github/scripts/change-set.test.mjs` → `# tests 20 / # pass 20 / # fail 0` (the 19 acceptance cases plus the page-limit one). - `node scripts/ds-bot-patches.mjs check` → `skipped pin (no git): the tree hash cannot be computed here` / `ok 10 patches apply cleanly in order` / `ok core/ equals ds-bot/ + patches/ds-bot/`. `core/` and `patches/ds-bot/` are untouched by this diff, so no `generate` was run. ## Red without the source change Two knives, both run at this tip and both restored afterwards (`diff` against the backup empty, suite `524 / 522 / 0 / 2` again): 1. **The comparison deleted** (`const moved = false && changedPaths…`): `not ok 8` (acceptance 1 — a repaired tip passes), `not ok 10` (acceptance 2 — one changed blob refuses), `not ok 11` (acceptance 3 — an unreadable tree refuses with the read), `not ok 13` (acceptance 5 — a newer matching conclusion wins), `not ok 14` (acceptance 7 — a carried PASS with no `Ran:` still refuses), `not ok 18/19/20` (acceptance 6, all three change-set cases); `# tests 20 / # pass 12 / # fail 8`. 2. **`sameChangeSet` always reports identical** (`return { ok: true, paths }` before the first tree read — the "another base sha and still green" failure): `not ok 2` (one changed blob is a different change set), `not ok 3` (a path missing on one side), `not ok 4` (truncated tree), `not ok 5` (a tree that 404s), `not ok 19/20` (acceptance 6 for "one blob changed" and "tree unreadable"); `# tests 20 / # pass 14 / # fail 6`. Acceptance 6 is not a unit test of the helper: it drives **`decidePull`** (`enqueue-guard`'s own decision), the guard's check line, and `judge` (`merge-gate`) over the same three change sets, so the three readers cannot drift apart while each one's own test stays green — that is the issue's core, not a spare test. ## Not run - `npm run verify` at the root: it needs `npm ci` and `npm run harness` in `core/`, which this box does not have; CI's `tests-ok` runs the same chain. `core/test/workflow-repo-dev.test.js` reads `cases` only, and the new key is top-level, so it is unaffected — but I did not run it here. - `node scripts/build-client.mjs --check`, smoke, typecheck and actionlint: nothing under `core/` changed. - A real `update-branch` on this pull request: the carried path is exercised through injected tree readers, not by merging `main` into a reviewed tip and re-running the guard against the live API. ## Risk - A carried PASS is a claim about **blobs, not about tests**. It does not stand in for CI: the head still needs its own green `tests-ok`, and the queue still rebuilds on the newest `main`. What it buys is that the reviewer's reading of the same content is not thrown away when `main` moves. - The comparison uses the changed-file list the caller read, one page of 100, and refuses at the page limit rather than comparing a subset. A pull request larger than 100 files therefore always pays a second review — the answer it already had. - `git/trees/<sha>?recursive=1` is truncated for very large trees; that refuses (fail closed) as well, so the rule downgrades to "review again", never to "assume unchanged". - A rebased-away tip is not reachable, so `reviewed` can 404 where the reviewer truly reviewed a force-pushed commit. It refuses, as before.
… set (ThinkFlowLab#1317) (ThinkFlowLab#1327) Written-by: Dev3 Fixes ThinkFlowLab#1317 Owner: Dev3 Reviewer: Reviewer2 Tip SHA: d71a88a Docs-not-needed: no page under docs/subsystems/ owns .github/scripts/ or AGENTS.md (arch-docs reads 0 owned files for this diff); the rule text is AGENTS.md and it moves with the code. ## Reproduce At `2c02cd15` any new commit voids a PASS, including the one `PUT /pulls/<n>/update-branch` writes to bring a branch up to date. `decide` compares the whole 40-character tip and nothing else: ``` reviewer-agent: names 175328d0c1b2, not the tip abaf938b0c1d ``` That answer is right when the code changed and wrong when only `main` moved: with a PASS at `175328d0c1b2…` and a head `abaf938b0c1d…` that merged `main` in, every changed path can carry the same blob at both tips and the pull request still pays a whole second review (ThinkFlowLab#1304 was PASSed three times for one change set, once per update-branch). The same `decide` arms `enqueue-guard` and `merge-gate`, so all three paths answer alike — and all three were wrong alike. ## What changed - **New `.github/scripts/change-set.mjs`**: `sameChangeSet({ gh, repo, reviewed, head, paths })` reads `git/trees/<sha>?recursive=1` for both tips and compares the blob sha of every changed path. `{ok:true}` only when every path exists at both tips with the same blob. Fail closed everywhere else — no path list, no reader, a 404, `truncated:true`, no `tree` in the answer, a path missing on either side — and each refusal says which read failed (`git tree of 175328d0c1b2: 404`, `…: truncated`) or which path differs, so "the code changed" and "I could not look" never read the same. - **The comparison rejects a 100-entry list.** Each caller reads one page (`pulls/<n>/files?per_page=100`), so a list that fills the page may be cut short and file 101 could be the file that moved: that refuses as `the changed-file list stops at 100 entries, so it may be cut short` instead of comparing a part of the change set. A 99-entry list compares normally. - **`review-gate.mjs#decide`**: three optional inputs (`changedPaths`, `sameChangeSet`, `gh`). In the `parsed.tip !== headSha` branch — the one comparison point ThinkFlowLab#1317 names — the conclusion is now compared before it is dropped: a match marks the entry `carriedFrom` and keeps it, a mismatch or an unreadable tree refuses with the reason appended. Without a path list the old refusal stands unchanged. - **A conclusion written at the head always decides.** `const exact = atTip.filter((e) => !e.carriedFrom)` and only carried ones are read when the head carries none. This is a new rule, and ThinkFlowLab#1317 needs it: a carried PASS written after an INC at the head would otherwise win on timestamp alone, and the INC would stop vetoing. - **The allow sentence names both full shas and the comparison**: `reviewer-agent's PASS at <T> still describes this head <H>: the change set is identical (every changed path carries the same blob at both tips)`, and the result carries `carried: true` so every renderer can say which PASS it read. - **The three readers stay one cut.** `merge-gate.mjs#judge` passes `changedPaths: fileNames(files)` from the entry's own file list and accepts an injected comparison; `enqueue-guard.mjs#decidePull` forwards it; the ALLOW tails in `enqueue-guard.mjs` and `enqueue-rules.mjs` say why (`… the change set is the one that PASS names, so the tip moved without changing it`). `review-gate.mjs#main` reads the changed-file list itself. - **`AGENTS.md` three places**, all the same rule: `:34` (the pipeline contract's one-conclusion-per-tip), `:174` (Gates — where the queue reader and the `risk-tier` summary point, so leaving it would keep a contradicting sentence two screens below the new one, and the rule now also carries the exception's CI/queue obligation), and `:203` (Merge gates: the mechanism, the fail-closed list, and the precedence rule). - **Fixture**: `.github/scripts/fixtures/workflow-cases.json` gains a top-level `changeSetCases` array — `tip-moved-same-change-set` (allow, with the exact sentence), `tip-moved-one-blob-changed` (refuse, `the change set differs at core/package.json`), `tip-moved-tree-unreadable` (refuse, `the change set could not be read (git tree of 175328d0c1b2: 404)`). It is a new key rather than entries in `cases`, because `core/test/workflow-repo-dev.test.js` consumes `cases` one by one and adding there would mean editing `core/`, which ThinkFlowLab#1317's boundary excludes. ## What I ran At the tree of this commit, `TZ=UTC`, Node v22.23.3: - `node --test ".github/scripts/*.test.mjs"` → `# tests 524 / # pass 522 / # fail 0 / # skipped 2`. - `node --test .github/scripts/change-set.test.mjs` → `# tests 20 / # pass 20 / # fail 0` (the 19 acceptance cases plus the page-limit one). - `node scripts/ds-bot-patches.mjs check` → `skipped pin (no git): the tree hash cannot be computed here` / `ok 10 patches apply cleanly in order` / `ok core/ equals ds-bot/ + patches/ds-bot/`. `core/` and `patches/ds-bot/` are untouched by this diff, so no `generate` was run. ## Red without the source change Two knives, both run at this tip and both restored afterwards (`diff` against the backup empty, suite `524 / 522 / 0 / 2` again): 1. **The comparison deleted** (`const moved = false && changedPaths…`): `not ok 8` (acceptance 1 — a repaired tip passes), `not ok 10` (acceptance 2 — one changed blob refuses), `not ok 11` (acceptance 3 — an unreadable tree refuses with the read), `not ok 13` (acceptance 5 — a newer matching conclusion wins), `not ok 14` (acceptance 7 — a carried PASS with no `Ran:` still refuses), `not ok 18/19/20` (acceptance 6, all three change-set cases); `# tests 20 / # pass 12 / # fail 8`. 2. **`sameChangeSet` always reports identical** (`return { ok: true, paths }` before the first tree read — the "another base sha and still green" failure): `not ok 2` (one changed blob is a different change set), `not ok 3` (a path missing on one side), `not ok 4` (truncated tree), `not ok 5` (a tree that 404s), `not ok 19/20` (acceptance 6 for "one blob changed" and "tree unreadable"); `# tests 20 / # pass 14 / # fail 6`. Acceptance 6 is not a unit test of the helper: it drives **`decidePull`** (`enqueue-guard`'s own decision), the guard's check line, and `judge` (`merge-gate`) over the same three change sets, so the three readers cannot drift apart while each one's own test stays green — that is the issue's core, not a spare test. ## Not run - `npm run verify` at the root: it needs `npm ci` and `npm run harness` in `core/`, which this box does not have; CI's `tests-ok` runs the same chain. `core/test/workflow-repo-dev.test.js` reads `cases` only, and the new key is top-level, so it is unaffected — but I did not run it here. - `node scripts/build-client.mjs --check`, smoke, typecheck and actionlint: nothing under `core/` changed. - A real `update-branch` on this pull request: the carried path is exercised through injected tree readers, not by merging `main` into a reviewed tip and re-running the guard against the live API. ## Risk - A carried PASS is a claim about **blobs, not about tests**. It does not stand in for CI: the head still needs its own green `tests-ok`, and the queue still rebuilds on the newest `main`. What it buys is that the reviewer's reading of the same content is not thrown away when `main` moves. - The comparison uses the changed-file list the caller read, one page of 100, and refuses at the page limit rather than comparing a subset. A pull request larger than 100 files therefore always pays a second review — the answer it already had. - `git/trees/<sha>?recursive=1` is truncated for very large trees; that refuses (fail closed) as well, so the rule downgrades to "review again", never to "assume unchanged". - A rebased-away tip is not reachable, so `reviewed` can 404 where the reviewer truly reviewed a force-pushed commit. It refuses, as before. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1327, "claim": "Dev3", "issue": 1317, "summary": "The gate keeps a PASS when the tip moved without changing the change set, so merging main or an update-branch never costs a review. It compares (path → blob sha) at the reviewed tip and at the head over the changed paths, accepting only when every path matches. A changed blob, a path missing on one side, a truncated or unreadable tree, or a file list that fills the 100-entry page refuses exactly as before. A conclusion written at the head always decides, so an INC at the head still vetoes a carried PASS. enqueue-guard, merge-gate and review-gate read the same comparison.", "files": [ ".github/scripts/change-set.mjs", ".github/scripts/change-set.test.mjs", ".github/scripts/review-gate.mjs", ".github/scripts/merge-gate.mjs", ".github/scripts/enqueue-guard.mjs", ".github/scripts/enqueue-rules.mjs", ".github/scripts/fixtures/workflow-cases.json", "AGENTS.md" ], "checks": { "ran": "TZ=UTC node --test \".github/scripts/*.test.mjs\"; node scripts/ds-bot-patches.mjs check", "result": "green", "at": "d71a88af1bce4f32fab24b427c8843e4c3ecf617" }, "next": "hand to Reviewer2 at this tip; nothing is queued in front of it" } ``` <!-- /flybot:checkpoint -->
Fixes #1299
Part of #1272
Owner: Dev4
Written-by: Dev4
Reviewer: Reviewer
Tip SHA: dae08a9
Step 3 of #1272: the Workflows page, and the board's columns produced by the definition instead of
by five constants. Additive on purpose — a card that names no workflow keeps the five columns it has
today, so no existing card moves and there is no second card schema.
Reproduce
grep -n "DELIVERY_COLUMNS = " core/src/host/delivery-board.jsone37f1f83→['raw','ready','working','review','done-today'], a literal;columnOfDeliveryreads labels, never a definition, so a card cannot be in a column its workflow says.grep -n "workflow:" core/src/host/delivery-board.jsone37f1f83→ no match:cardOfcarried noworkflow/node, so nothing could place a card by its definition.SETTINGS_PAGESincore/src/client/settings.jsended at Connectors…), and nothing readshared/workflow-{repo-dev,blank}.jsoutside the tests:grep -rn "repoDevWorkflow" core/src --include=*.jsone37f1f83→ only the preset file and its test.repo-devcard could not say which node it is in.What changed
core/src/shared/workflow-doc.js— the definition as a file: canonical bytes (fixed key order, two spaces, trailing newline), the id rule,PRESET_IDS,copyOfPreset,differingFields. A load → save round trip is byte-identical;column: falseis written while an absent column is not.core/src/shared/workflow-board.js—columnPlan(def)(lanes in definition order, a shared lane named by its id),placeCard(def, card)(nullfor another workflow / no node /column: false),boardSections(definitions, cards)(one section per workflow with cards, plus thelegacycards that name none).core/src/shared/workflow-preview.js—previewNode(def, node, answer): one row per guard;holds(read and passing, no sentence),refuse(the producer's own sentence),unreadable; a reader that answered nothing makes every row say it could not read, with the reason.core/src/shared/workflow.js— additive only: the guard refusals carryunreadable, andguardRows(def, node, facts)answers every guard (the editor shows all of them;enterNodeanswers with the first refusal). No semantics of step 1/2 changed.core/src/host/workflow-store.js— the only writer: presets from their modules + the workspace'sworkflows/<id>.json,project.jsonfor the project default,savevalidated by the engine (workflow/preset-read-onlyfor a preset id,workflow/invalidwith the engine's own problems),customize,setDefault,list,open(definition + bytes + layout + preview),definitions()for the board. Endpointsworkflows(read) andworkflow-save(write).core/src/host/authz.js—workflow-saveinTASK_ENDPOINTS,workflowswith the reads;core/src/host/channel.jsdispatches both;core/index.jsinstalls the module before the channel.core/src/host/delivery-board.js—cardOfreadsworkflow:<id>/node:<id>(null when absent), and the answer carriesworkflows+placedper repository and for the combined board, so a card is drawn once.core/src/client/workflows.js(new) +settings.js+client/index.js+icons.js— the Workflows page: list (presets / yours / project default), node table, edges, flowchart fromlayoutWorkflow, guard preview rows, Customize, Save, Use for this project; the actions go throughworkflows/workflow-save. The page is listed inSETTINGS_PAGESright after Scheduled tasks, beside the other automation page, and the account menu keeps its exact rows:settings-page-slotpins the six-page prefix and the plugin slot after Connectors,flybot-clientpins the menu, and moving the page into that prefix or adding a menu row turns both red — that is the CI failure the first commit had and this one fixes.core/src/client/delivery-view.js— a section per workflow, headers naming the workflow, and a placed card is not drawn again in the five columns.core/src/client/i18n.js— zh entries for every new string (the i18n test enumerates them).docs/subsystems/core-host.md,docs/subsystems/core-client.md,changelog.d/1299-workflows.added.md.core/client.js,patches/ds-bot/*(generate+check).What I ran
TZ=UTC, atdae08a9834fa315eb5e195f8db7e54463059a026:node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/workflow-doc.test.js test/workflow-board.test.js test/workflow-preview.test.js test/workflow-store.test.js test/workflows-page.test.js test/delivery-board.test.js test/delivery-view.test.js test/board-card.test.js test/i18n.test.js test/kanban.test.js test/client-bundle.test.js test/settings-page-slot.test.js test/flybot-client.test.js→
# tests 136 / # pass 136 / # fail 0(settings-page-slot2/2 andflybot-clientare unchanged and are the guards on where the page sits and on the account menu).test/inbox.test.jsismain's own now and carries no diff here:git diff b66fc741...dae08a9834fa315eb5e195f8db7e54463059a026 --name-onlynames no inbox path, and running it in this tree gives# tests 27 / # pass 27 / # fail 0— the same 27 the rebased base has.# tests 1833 / # pass 1832 / # fail 1. The one istest/usage-fields.test.js(null !== 0on the sqlite fixture); it fails the same way on a cleanb66fc741tree with this diff absent — this box, not the tree; it is green in CI.npm run lint→Found 69 warnings and 0 errors(the warnings are pre-existing).npm run arch-docs→arch-docs: OK (48 changed file(s)).node scripts/build-client.mjs --check→ exit 0 (core/client.jsrebuilt by the generator, not edited).node scripts/ds-bot-patches.mjs generatethencheck→ the three green lines, the first a real pin (ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)), then 10 patches apply cleanly andcore/equalsds-bot/+ patches;generateis idempotent on this tree: run twice, the second run leavesgit status --porcelainempty.0012-room-membership.patchis byte-identical tomain(empty diff — not even hunk headers), and the twoclaim-cuemirrors carry no drift (claimCue/postClaimLinecounts 5/5 and 4/4 on both sides).0005(+8/−2) and0014(+34/−27) are real content changes and are carried, not noise.node .github/scripts/check-mojibake.mjs --check→mojibake: ok — 0 occurrences of U+FFFD in 1141 text file(s).sha256sum core/src/shared/workflow-repo-dev.js core/src/shared/workflow-blank.jsatb66fc741and in this tree give1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60aand8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5on both sides — this branch moves not one byte of either preset.#1309; the interim#1305fix it carried is gone with that merge, and this PR does not claimFixes #1305.Red without the source change
Each line: cut the source change, run that suite, restore with
cp(cmpclean, suite green again). All seven were cut ondae08a9834fa315eb5e195f8db7e54463059a026.cardOfno longer readsworkflow:/node:test/delivery-board.test.jsnot ok— "a card reads its workflow and node from its labels, and a card without them is unchanged (#1272 step 3)"; 49 / 48 / 1workflowBytesdrops the trailing newlinetest/workflow-doc.test.jsnot ok— "a definition saves to canonical bytes and reads back unchanged"; 5 / 4 / 1saveaccepts a preset idtest/workflow-store.test.jsnot ok— "a preset id cannot be saved over…"; 7 / 6 / 1placeCardignorescolumn: falsetest/workflow-board.test.jsnot ok— "repo-dev draws the three lanes…" and "a card naming a node with no column…"; 7 / 5 / 2placedtest/delivery-view.test.jsnot ok— "a workflow draws its own columns… and its card once"; 27 / 26 / 1holdstest/workflow-preview.test.jsnot ok— "a fact that could not be read is named…"; 4 / 3 / 1guardRowsalways saysholdstest/workflow-preview.test.jsnot ok— 3 of 4 red (stale verdict, unreadable, unknown check); 4 / 1 / 3Not run
rt.workflowFactsFor, so on a real host the guard preview says "could not read the fact" for every row. That is the acceptance-5 unreadable path, proven by a test; wiring the step-2 providers to GitHub reads (token, PR, checks) is the next PR, and the page does not pretend otherwise.npm testin one process (this box hitsspawn … EAGAIN); the suites above were run directly, one at a time.dev/bench.sh) and a browser screenshot of the page;src/host/changed, so a smoke run is worth doing at review time.workflows/placed, and the combined answer is computed from the same function.Risk
workflow:<id>/node:<id>labels are this PR's convention. One board: the delivery board and the task board are one board, and a repository task is its issue #1265's card schema named the two fields but nothing onmainproduced them, so I had to choose a source; labels keep the board read-only and need no write path. If a different source is wanted (a task record, a PR body line),cardOfis the single place to change.id(an edge names it) and not the edge list: an edit that needs a new node or edge is still a JSON edit. Written down rather than half-built.review) is kept in the section'sunplacedand counted on the page rather than dropped; a card naming a workflow this host does not have stays in the five legacy columns.enterNodereads, so a wrong preview would be a wrong card refusal too — the property is the point, and it is pinned by the preview test rather than by two implementations agreeing.TASK_ENDPOINTSnow holds a non-task endpoint for the first time (workflow-save); it is an explicit entry, not a prefix, and the authz test pins both the allow and a neighbouring refusal.Checkpoint
{ "pr": 1304, "claim": "Dev4", "issue": 1299, "summary": "A definition is a file under workflows/ in the workspace, written canonically so an unedited save is byte-identical, with presets copied and never edited. The board's columns come from the definitions in play: a card names workflow and node with two labels, is laid in its definition's lane, and every section header names its workflow while a card that names none keeps the five legacy columns. The page is a node table with the flowchart drawn from it and one row per guard showing the sentence it would print now, or that the fact could not be read.", "files": ["core/src/shared/workflow-board.js", "core/src/shared/workflow-doc.js", "core/src/shared/workflow-preview.js", "core/src/host/workflow-store.js", "core/src/client/workflows.js", "core/src/host/delivery-board.js", "core/src/host/authz.js"], "checks": { "ran": "node --import ./test/harness-resolve.mjs --test --test-concurrency=1 (13 suites) + inbox 27/27 (main's, zero diff) + claim-cue 7/7 + connectors 33/33 + whole core in 8 chunks (1832/1833, the one usage-fields box failure) + npm run lint + npm run arch-docs + build-client --check + ds-bot-patches check", "result": "green", "at": "dae08a9834fa315eb5e195f8db7e54463059a026" }, "next": "Review against acceptance 1-8; the fact reader that fills the guard preview is the next PR." }