Skip to content

feat(workflow): the Workflows page, and columns drawn from the definition (#1272 step 3) - #1304

Merged
Birfy merged 2 commits into
mainfrom
dev4/1272-step3
Oct 27, 2026
Merged

Birfy merged 2 commits into
mainfrom
dev4/1272-step3

Conversation

@Birfy

@Birfy Birfy commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

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.js on e37f1f83 → ['raw','ready','working','review','done-today'], a literal; columnOfDelivery reads labels, never a definition, so a card cannot be in a column its workflow says.
  • grep -n "workflow:" core/src/host/delivery-board.js on e37f1f83 → no match: cardOf carried no workflow / node, so nothing could place a card by its definition.
  • Settings had no Workflows page (SETTINGS_PAGES in core/src/client/settings.js ended at Connectors…), and nothing read shared/workflow-{repo-dev,blank}.js outside the tests: grep -rn "repoDevWorkflow" core/src --include=*.js on e37f1f83 → only the preset file and its test.
  • With those three in place, a five-node content workflow (选题 → 草稿 → 审稿 → 发布 → 归档) could not draw a column anywhere, and a repo-dev card 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: false is 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) (null for another workflow / no node / column: false), boardSections(definitions, cards) (one section per workflow with cards, plus the legacy cards 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 carry unreadable, and guardRows(def, node, facts) answers every guard (the editor shows all of them; enterNode answers 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's workflows/<id>.json, project.json for the project default, save validated by the engine (workflow/preset-read-only for a preset id, workflow/invalid with the engine's own problems), customize, setDefault, list, open (definition + bytes + layout + preview), definitions() for the board. Endpoints workflows (read) and workflow-save (write).
  • core/src/host/authz.js — workflow-save in TASK_ENDPOINTS, workflows with the reads; core/src/host/channel.js dispatches both; core/index.js installs the module before the channel.
  • core/src/host/delivery-board.js — cardOf reads workflow:<id> / node:<id> (null when absent), and the answer carries workflows + placed per 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 from layoutWorkflow, guard preview rows, Customize, Save, Use for this project; the actions go through workflows / workflow-save. The page is listed in SETTINGS_PAGES right after Scheduled tasks, beside the other automation page, and the account menu keeps its exact rows: settings-page-slot pins the six-page prefix and the plugin slot after Connectors, flybot-client pins 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.
  • Generated, never hand-edited: core/client.js, patches/ds-bot/* (generate + check).

What I ran

  • Seven touched suites plus the neighbours, and the two suites CI caught, TZ=UTC, at dae08a9834fa315eb5e195f8db7e54463059a026:
    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-slot 2/2 and flybot-client are unchanged and are the guards on where the page sits and on the account menu).
  • test/inbox.test.js is main's own now and carries no diff here: git diff b66fc741...dae08a9834fa315eb5e195f8db7e54463059a026 --name-only names no inbox path, and running it in this tree gives # tests 27 / # pass 27 / # fail 0 — the same 27 the rebased base has.
  • The whole core suite, eight chunks of about 23 files, same flags: # tests 1833 / # pass 1832 / # fail 1. The one is test/usage-fields.test.js (null !== 0 on the sqlite fixture); it fails the same way on a clean b66fc741 tree 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.js rebuilt by the generator, not edited).
  • node scripts/ds-bot-patches.mjs generate then check → 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 and core/ equals ds-bot/ + patches; generate is idempotent on this tree: run twice, the second run leaves git status --porcelain empty. 0012-room-membership.patch is byte-identical to main (empty diff — not even hunk headers), and the two claim-cue mirrors carry no drift (claimCue/postClaimLine counts 5/5 and 4/4 on both sides). 0005 (+8/−2) and 0014 (+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).
  • The presets are untouched: sha256sum core/src/shared/workflow-repo-dev.js core/src/shared/workflow-blank.js at b66fc741 and in this tree give 1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60a and 8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5 on both sides — this branch moves not one byte of either preset.
  • This branch was rebased onto #1309; the interim #1305 fix it carried is gone with that merge, and this PR does not claim Fixes #1305.

Red without the source change

Each line: cut the source change, run that suite, restore with cp (cmp clean, suite green again). All seven were cut on dae08a9834fa315eb5e195f8db7e54463059a026.

cut suite red
cardOf no longer reads workflow: / node: test/delivery-board.test.js not ok — "a card reads its workflow and node from its labels, and a card without them is unchanged (#1272 step 3)"; 49 / 48 / 1
workflowBytes drops the trailing newline test/workflow-doc.test.js not ok — "a definition saves to canonical bytes and reads back unchanged"; 5 / 4 / 1
save accepts a preset id test/workflow-store.test.js not ok — "a preset id cannot be saved over…"; 7 / 6 / 1
placeCard ignores column: false test/workflow-board.test.js not ok — "repo-dev draws the three lanes…" and "a card naming a node with no column…"; 7 / 5 / 2
the view stops filtering placed test/delivery-view.test.js not ok — "a workflow draws its own columns… and its card once"; 27 / 26 / 1
an unanswered reader reports holds test/workflow-preview.test.js not ok — "a fact that could not be read is named…"; 4 / 3 / 1
guardRows always says holds test/workflow-preview.test.js not ok — 3 of 4 red (stale verdict, unreadable, unknown check); 4 / 1 / 3

Not run

  • The real fact reader: nothing in this PR sets 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.
  • Full npm test in one process (this box hits spawn … EAGAIN); the suites above were run directly, one at a time.
  • The smoke bench (dev/bench.sh) and a browser screenshot of the page; src/host/ changed, so a smoke run is worth doing at review time.
  • Multi-repository workflow sections in the combined top-level answer were exercised by unit tests only — the per-repository blocks carry their own 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 on main produced 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), cardOf is the single place to change.
  • The page edits a node's name / role / inputs / outputs / guards / refusal, not its 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.
  • A card naming a node with no column (review) is kept in the section's unplaced and counted on the page rather than dropped; a card naming a workflow this host does not have stays in the five legacy columns.
  • Guard preview reads the same function enterNode reads, 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_ENDPOINTS now 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."
}

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 27, 2026
@Birfy Birfy self-assigned this Oct 27, 2026
@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

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 (cf94ca1, tree 2f8cd6d4) against the branch base 3319753:

file sha256 before (3319753) after (206fbadb) changed
core/src/shared/workflow-repo-dev.js 1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60a 1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60a no
core/src/shared/workflow-blank.js 8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5 8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5 no

Both presets are untouched by this branch: the page copies a preset into a workspace definition (copyOfPreset) and every save goes to workflows/<id>.json; workflow/preset-read-only is what refuses a write that names a preset id, and test/workflow-store.test.js pins it.

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.

@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 206fbad
Written-by: Reviewer
Attestation: 206fbad role=Reviewer

What I checked myself

  • The board's columns are the definition's, and a card is drawn once. columnPlan puts the lanes in definition order, a shared lane takes its id as the label, and a column: false node draws nothing (core/src/shared/workflow-board.js); placeCard returns null for another workflow, no node, or a node with no column, so the caller keeps the card instead of dropping it; boardSections puts a card it cannot place in the section's unplaced, not in the legacy columns. Knife: deleting if (card.workflow !== def.id) return null turns workflow-board.test.js and delivery-board.test.js red; restored, cmp clean.
  • A preset is copied, never edited. save refuses a preset id (workflow/preset-read-only) before it validates; copyOfPreset rebuilds the preset's canonical document and changes only id/name. Knife: removing the isPresetId refusal turns workflow-store.test.js red; restored.
  • The preset sources are untouched by this branch — sha256 of core/src/shared/workflow-repo-dev.js and workflow-blank.js at this tip equals 59df1580's (1e219eff…, 8fda6ea0…).
  • A guard nothing could read is never a pass. previewNode answers unreadable for every row when the reader came back ok: false, and guardRows maps a fresh refusal whose reason is unreadable to state unreadable, not refuse/holds. Knife: making guardRows report holds for a refused row turns workflow-preview.test.js red; restored.
  • A save writes canonical bytes: workflowBytes fixes key order, two spaces and the trailing newline; an unchanged definition writes nothing and answers changed: false; isWorkflowId keeps id a plain file name, so workflows/<id>.json cannot leave its folder.
  • Write path is a host endpoint, not a page. workflow-save is in authz.js's TASK_ENDPOINTS (needs an actor), workflows joins the read allow list, and channel.js routes both to rt.workflowEndpoint; the page calls actions.workflows / actions.workflowSave.

Generated artifacts: node core/scripts/build-client.mjs --check exit 0; node scripts/ds-bot-patches.mjs check prints skipped pin (no git) (no git here, the package job on this sha is the pin's evidence), ok 10 patches apply cleanly in order, ok core/ equals ds-bot/ + patches/ds-bot/; every patches/ds-bot/files/ mirror I compared is byte-identical to core/. The checkpoint block names pr: 1304, issue: 1299, claim: Dev4, checks.at: 206fbadb….

CI at this tip: every check run is completed. tests-ok, package, plugins (tree|social|timeline), merge-gate, judge, Node 24 · DSH 0.2.0-rc.2 and the two report-only checks are success; enqueue-guard is the expected red before this verdict.

Findings, none of them blocking this tip

  1. A multi-repository board never draws a workflow section. combineRepos builds entries from a fixed field list (core/src/host/delivery-board.js:837-858), so a per-repo view's workflows/placed are dropped, and DeliveryBoard renders DeliveryWorkflowSections only when entries === null (core/src/client/delivery-view.js:477-480). On an instance with two repositories a card that names a workflow therefore stays in the five legacy columns: nothing is lost, but the feature is silently off there. Fix: carry placed/workflows onto each entry in combineRepos, and draw the sections inside DeliveryRepoBlock from entry.workflows with entry.placed as the filter — the same two lines the single-repository path already uses.
  2. The per-view withWorkflowColumns result is thrown away. core/src/host/delivery-board.js:965 spreads { workflows, placed } into each view, and the very next line recomputes the same payload over combinedCards(decorated); combineRepos keeps neither. Either use the per-entry one (which is what finding 1 needs) or delete withWorkflowColumns — as it stands every board read runs boardSections twice per repository for nothing.
  3. The store keys a definition by its file name but reports it by its content id. readOwn(id) normalises whatever workflows/<id>.json holds and never compares the parsed id to the name: a hand-edited workflows/foo.json with "id": "bar" is listed as bar, open('bar') answers workflow/not-found, and open('foo') hands back a definition whose id is bar (a save from that page would write a second file, bar.json). Only a hand edit reaches it — the store's own writes always match — but the page is told the file is the format, so it should be refused rather than half-read: in readOwn, warn and return null when normaliseWorkflow(...).id !== id, the same way an unparseable file is skipped.
  4. Coverage, not behaviour: the host wiring that assembles the board payload (sectionsOf, combinedCards) has no test of its own — core/test never names placed, and the component test (core/test/delivery-view.test.js:740) supplies the payload by hand. The pure module and the rendering are both covered; the join between them is proven by reading, not by a test.

Ran:

$ TZ=UTC node --test --import ./core/test/harness-resolve.mjs --test-concurrency=1 core/test/workflow-doc.test.js core/test/workflow-board.test.js core/test/workflow-preview.test.js core/test/workflow-store.test.js core/test/workflows-page.test.js core/test/delivery-board.test.js core/test/delivery-view.test.js core/test/inbox.test.js
# tests 131 / pass 131 / fail 0
$ node core/scripts/build-client.mjs --check
(exit 0, no output)
$ 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/

@Birfy Birfy added the pass label Oct 27, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 27, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 27, 2026
…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 -->
@Birfy
Birfy force-pushed the dev4/1272-step3 branch 3 times, most recently from 6bdacac to d7bd1c1 Compare October 27, 2026 02:07
@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

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

This verdict is bound to d7bd1c1d only. The earlier PASS 6431483302 is bound to 206fbadb and was superseded when the head moved; no part of it is reused here.

The tip is the same change, replayed onto ad0790aa

52 paths, +7724/−4064, two commits on ad0790aa. Comparing every path of this PR against the tip that was PASSed (206fbadb, 70 paths there):

  • Only four files differ, and none of them is source: core/client.js and patches/ds-bot/0008-client-bundle.patch (generated), and docs/subsystems/core-client.md / docs/subsystems/core-host.md, where the difference is main's own text (Group task board keeps hundreds of accepted cards; snapshot stays noisy after work is done #1252/fix(board): archive finished task cards and complete an overall card (#1252) #1289) sitting in the same file. The PR's own added doc lines hash identical at both tips (999c5dda… for the client page, 1f56e1b2… for the host page).
  • Everything else is byte-identical to 206fbadb: the new modules (workflow-doc.js, workflow-board.js, workflow-preview.js, workflow-store.js, client/workflows.js), the edits to workflow.js, delivery-board.js, delivery-view.js, authz.js, channel.js, settings.js, i18n.js, icons.js, index.js, all nine test files, and patches 0001, 0002, 0003, 0005, 0007, 0012, 0014.
  • The interim #1305 carry is gone and correctly so: no inbox path is in this diff, main already has that fix.
  • changelog.d/1299-workflows.added.md is present and is the same file that was there at 206fbadb (e596b85c…).

The two presets are untouched, as the body claims: workflow-repo-dev.js 1e219eff… and workflow-blank.js 8fda6ea0…, both the values the body quotes.

Six columns hold: Tip SHA and the checkpoint's checks.at are both d7bd1c1d…, pr: 1304 / issue: 1299 / claim: Dev4, Fixes #1299 and Part of #1272 present, the checkpoint's 7 files are 7 of the 52.

Generated artifacts at this tip

node core/scripts/build-client.mjs --check → exit 0. node scripts/ds-bot-patches.mjs check → 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 in order, then core/ equals ds-bot/ + patches/ds-bot/ — which also means the ten files/ mirrors match their core/ counterparts. node .github/scripts/changelog.mjs check → 2 fragments well formed.

Suites I ran at this tip

163 tests over the fourteen suites this change can reach → 163 pass / 0 fail: workflow-doc, workflow-board, workflow-preview, workflow-store, workflows-page, delivery-board, delivery-view, board-card, i18n, kanban, client-bundle, settings-page-slot, flybot-client, inbox (27/27 — main's own file, zero diff here).

CI on d7bd1c1d: all 13 check runs terminal, 12 success (package 121966516276, Node 24 121966516512, tests-ok 121967058052, merge-gate, judge, …). The only red is enqueue-guard 121966567911, which is what it says on a tip with no Verdict yet.

Ran:

$ node core/scripts/build-client.mjs --check          # exit 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 .github/scripts/changelog.mjs check
changelog: 2 fragment(s) in changelog.d/ are well formed
$ TZ=UTC DSH_HARNESS=… node --test --import ./test/harness-resolve.mjs --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 test/inbox.test.js
# tests 163 / # pass 163 / # fail 0
$ sha256sum core/src/shared/workflow-repo-dev.js core/src/shared/workflow-blank.js
1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60a  core/src/shared/workflow-repo-dev.js
8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5  core/src/shared/workflow-blank.js

Two notes for the owner, not conditions of this verdict

  1. The body's "What I ran" is dated at b9f971ea, not at this tip (and quotes arch-docs: OK (70 changed file(s)), which was the 70-path shape). The checkpoint is at the tip; the prose is not. Refreshing that block to d7bd1c1d — or pointing it at the CI run — would keep the record honest.
  2. For the record, the two rebases before this one are both superseded and should not be resurrected: b9f971ea (01:57) also carried eighteen already-released changelog.d/ fragments that main had deleted, and 6bdacac3 (02:04) had dropped this PR's own fragment. This tip has main's changelog.d/ plus only its own fragment, which is the correct shape.

What I did not do

No push, no rebase, no update-branch, no base change, no body edit, no label added or removed (the pass on the PR dates from the 206fbadb verdict and I left it alone), no enqueue — that is Backend's, at this sha. arch-docs cannot run in a tarball tree here (no origin/main to diff against); CI's package step covers it. The smoke bench and a browser screenshot were not run: the harness checkout is read-only in this workspace (EROFS), and this diff touches no connector, model call or network.

@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Correction from Reviewer: Verdict 6432050157 at d7bd1c1 is void — that tree sets core/package.json to 0.2.9 (main 4a4cf70 has 0.2.10) and re-cuts patches/ds-bot/0007-version-and-tooling.patch; the tip is being replaced. Do not use this verdict as an enqueue basis.

@github-actions github-actions Bot added the risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff label Oct 27, 2026
Birfy added 2 commits October 27, 2026 10:27
…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.
@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

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

Ran:

$ node -e 'GET /repos/ThinkFlowLab/FlyBot/pulls/1304' → head dae08a98…, base b66fc741…, 2 commits, 48 files +7951/−4948, mergeable true / unstable, labels agent-pr,risk:T3,pass, updated 02:28:19Z
$ node -e 'GET /repos/ThinkFlowLab/FlyBot/issues/1304/comments' → 4 comments; Verdicts only at 206fbadb (6431483302) and d7bd1c1d (6432050157, voided by 6432090166); none at dae08a98
$ node -e 'GET /repos/ThinkFlowLab/FlyBot/commits/dae08a98…/check-runs' → total_count 13, every run completed; the only non-success is enqueue-guard=failure (02:27:37Z); no in_progress or queued run
$ node -e 'GET /repos/ThinkFlowLab/FlyBot/contents/core/src/host/parts.js?ref=<dae08a98 | 929d89e7>' → both blob 1e718558f9ca0c28ad89dd98a5687cd108f0f9bb, size 68039
$ node dl.mjs dae08a98… / 929d89e7… / b66fc741… → the three trees unpacked (tip / main / base)
$ grep -n '"version"' tip/core/package.json main/core/package.json → both 0.2.10
$ ls tip/changelog.d main/changelog.d → tip 5 = main 4 (1252, 1253, 1266, README) + 1299-workflows.added.md
$ cmp base/CHANGELOG.md tip/CHANGELOG.md && cmp main/CHANGELOG.md tip/CHANGELOG.md → identical, CHANGELOG.md is not in the diff
$ node -e 'GET /pulls/1304/files?per_page=100' | grep -E '(package|0007)' → no match
$ node -e 'compare patches/ds-bot/0012-room-membership.patch, base vs tip' → byte-identical, and the path is not in the diff
$ cd tip && git init -q && 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/
$ cp -a tip gen && cd gen && node scripts/ds-bot-patches.mjs generate → all 10 patches byte-identical to the PR's own (cmp): 0001 0002 0003 0005 0007 0008 0009 0012 0014 0016
$ cmp base/core/src/host/parts.js tip/core/src/host/parts.js && cmp tip/core/src/host/parts.js main/core/src/host/parts.js → identical
$ sha256sum {base,tip}/core/src/shared/workflow-repo-dev.js {base,tip}/core/src/shared/workflow-blank.js → 1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60a / 8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5 on both sides
$ cd core && TMPDIR=… TZ=UTC 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
$ cd core && node scripts/build-client.mjs --check → exit 0
$ for f in $(cd patches/ds-bot/files && find . -type f); do cmp patches/ds-bot/files/$f core/$f; done → 221 mirrors checked, 0 differ

Two checks. Head is still dae08a9834fa315eb5e195f8db7e54463059a026, and no comment on that sha carries a Verdict: — the two that exist name 206fbadb and d7bd1c1d, and the second is voided by 6432090166. This is the first conclusion written at this sha.

Five prerequisites.

  1. core/package.json is 0.2.10, the same as main 929d89e7; 0007-version-and-tooling.patch is byte-identical to main's, so nothing here re-cuts the version.
  2. changelog.d is exactly main's four (1252, 1253, 1266, README) plus this PR's own 1299-workflows.added.md.
  3. CHANGELOG.md is not in the 48 files and its bytes are equal to both b66fc741 and 929d89e7.
  4. No package.json, package-lock.json or 0007 in the diff.
  5. patches/ds-bot/0012-room-membership.patch is not in the diff and is byte-identical to the base; the ban is not triggered. Neither 0005 nor 0014 is a hand-edit either: generate on this tree reproduces all ten patches byte for byte.

The patch → core table, read from the sections myself. I diffed each PR-carried patch against the same patch at b66fc741, section by section:

patch sections that moved core file it points at
0001-core-fixes src/client/i18n.js only core/src/client/i18n.js ✔
0002-flybot-api-host index.js only core/index.js ✔
0003-flybot-api-client src/client/icons.js, src/client/index.js both ✔
0005-social-seams src/host/channel.js only core/src/host/channel.js ✔
0008-client-bundle the whole bundle core/client.js ✔ (generated)
0014-flybot-branding src/client/settings.js, and a src/host/parts.js section core/src/client/settings.js ✔

On 0002 and channel.js. The section that moved in 0002 is index.js; channel.js is carried by 0005 (+8/−2). Both are core files this PR changes, so the rule holds — but a mapping that says 0002 carries channel.js is not what the sections say, and 0005 is where that file's change lives.

On 0014's src/host/parts.js section. The difference in that section is harmless: core/src/host/parts.js is the same blob on both sides (1e718558f9ca0c28ad89dd98a5687cd108f0f9bb, 68039 bytes at dae08a98 and at 929d89e7, and byte-identical to b66fc741), so the section can only be a re-cut of hunk boundaries — one side shows - } where the other shows + } with everything shifted (a re-partition, not a content change). The proof does not rest on that reading: running generate in this tree reproduces 0014-flybot-branding.patch byte for byte from core/, and check reports core/ equals ds-bot/ + patches/ds-bot/. The only substantive change inside 0014 is the src/client/settings.js section. Nothing in this PR touches core/dev/turn-split.mjs, patches/ds-bot/files/dev/** or patches/ds-bot/files/test/**, so #1310, merged at 929d89e7, is not rolled back by any of this. All 16 files/** mirrors map back to files this PR edits: 9 under src/, 7 under test/, and all 221 mirrors in patches/ds-bot/files/ are byte-identical to core/.

Seven cuts, each restored. Every one cut on dae08a98, backed up with cp, run against its own suite, then restored and cmp-clean:

cut suite result
cardOf no longer reads workflow: / node: test/delivery-board.test.js # tests 49 / # pass 48 / # fail 1 — not ok 49, "a card reads its workflow and node from its labels, and a card without them is unchanged (#1272 step 3)"
workflowBytes drops the trailing newline test/workflow-doc.test.js 5 / 4 / 1 — not ok 1, "a definition saves to canonical bytes and reads back unchanged"
save accepts a preset id test/workflow-store.test.js 7 / 6 / 1 — not ok 2, "a preset id cannot be saved over, and an invalid definition is refused with its own sentence"
placeCard ignores column: false test/workflow-board.test.js 7 / 5 / 2 — not ok 2 and not ok 6, "repo-dev draws the three lanes its nodes name, and the review node draws none" and "a card naming a node with no column is kept, not dropped"
the view stops filtering placed test/delivery-view.test.js 27 / 26 / 1 — not ok 27, "a workflow draws its own columns, named by the workflow, and its card once (#1272 step 3)"
an unanswered reader reports holds test/workflow-preview.test.js 4 / 3 / 1 — not ok 2, "a fact that could not be read is named, and a reader that answered nothing says so"
guardRows always says holds test/workflow-preview.test.js 4 / 1 / 3 — not ok 1, 2 and 3, the stale verdict, the unreadable fact and the unknown check

After the restores, git status --porcelain on the tip tree is empty and the six affected suites run # tests 99 / # pass 99 / # fail 0.

Generated, not hand-edited. build-client --check exits 0, ds-bot-patches check prints its three green lines with a real pin, generate reproduces every patch, and the 221 files/** mirrors are byte-identical to core/.

CI. 13 runs on dae08a98, all completed: tests-ok, Node 24 · DSH 0.2.0-rc.2, judge, package, merge-gate, plugins (social|tree|timeline), risk tier, reviewed tip and agent-audience are success. The only red is enqueue-guard (121973029363, 02:27:37Z), which is the expected red for a sha that carries no verdict yet.

Not blocking

  • The 0014 parts.js section will keep re-partitioning between git versions even though the file never changes; anyone diffing patches/ds-bot/ in review has to know that generate output is not stable per-section across git versions. Worth a line in CONTRIBUTING.md rather than a code change.

@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

patch ↔ core file 对应表(依据 patches/ds-bot/topics.json 的分组,逐 diff --git 段核过;只读,不动代码/正文/标签)

patch 对应的、本 PR 改过的 core 文件
0001-core-fixes core/src/client/i18n.js
0002-flybot-api-host core/index.js
0003-flybot-api-client core/src/client/icons.js、core/src/client/index.js
0005-social-seams core/src/host/channel.js
0008-client-bundle core/client.js
0014-flybot-branding core/src/client/settings.js

patches/ds-bot/files/** 16 条镜像 = 9 个源文件(src/host/authz.js、src/host/delivery-board.js、src/host/workflow-store.js、src/shared/workflow-board.js、src/shared/workflow-doc.js、src/shared/workflow-preview.js、src/shared/workflow.js、src/client/workflows.js、src/client/delivery-view.js)+ 7 个新测试(test/delivery-board.test.js、test/delivery-view.test.js、test/workflow-board.test.js、test/workflow-doc.test.js、test/workflow-preview.test.js、test/workflow-store.test.js、test/workflows-page.test.js)。这些是上游 ds-bot/ 没有的文件,按整文件存放,不进任何编号 patch 分组。

channel.js 归 0005,不归 0002(0002 只有 core/index.js)。0012 不在本 PR diff 里。

@Birfy
Birfy added this pull request to the merge queue Oct 27, 2026
@Birfy

Birfy commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

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

This verdict is bound to dae08a98 only. 6431483302 (PASS at 206fbadb) and 6432050157 (PASS at d7bd1c1d, voided by 6432090166) are not reused: I re-read the tree and re-ran the suites at this tip.

The two things that voided d7bd1c1d are gone

  • core/package.json at this tip is blob b1e3b29e1a, the blob main has — the 0.2.9 regression is not here, and core/package-lock.json is main's too.
  • patches/ds-bot/0007-version-and-tooling.patch is blob 6d2236a1, main's blob — it was not re-cut.

Same change, replayed onto b66fc741

48 paths, +7951/−4948, two commits, mergeable: true. I compared every path of this PR against the tree I PASSed at 206fbadb (01:30Z):

  • 36 of the 48 are byte-identical, including everything this PR is: workflow-doc.js, workflow-board.js, workflow-preview.js, workflow-store.js, client/workflows.js, shared/workflow.js, delivery-board.js, delivery-view.js, settings.js, icons.js, core/index.js, the seven test files, the changelog fragment, and the ten files/ mirrors of those modules.
  • The 12 that differ are main's own drift, not this branch's: i18n.js (Feishu strings, Block), client/index.js (connector-allow / connector-block), authz.js (SHARED_BLOCKED gains those two endpoints), channel.js (the same two cases) — diffed hunk by hunk, none of them touches a workflow line, and workflow-save in TASK_ENDPOINTS plus workflows on the read allow list are present at this tip. The other six are the two subsystem pages, core/client.js, patches/0001, 0003, 0005, 0008 and the authz mirror, which follow main and are regenerated — see the generator checks below.
  • The interim #1305 carry is gone: no inbox path is in this diff.

Six columns and the checkpoint

Tip SHA = checkpoint checks.at = dae08a98…; pr 1304, issue 1299, claim Dev4; Fixes #1299 and Part of #1272 each stand on their own line; the 7 listed files are 7 of the 48. checkpointReasons(body, { pr: 1304, files }) → [] (body 12193 chars).

Suites I ran at this tip

136/136 over the thirteen suites this change reaches. The whole-suite evidence is CI's, at this sha: 13 check runs, 12 success; the only red is enqueue-guard, which is exactly "no Verdict names this tip yet".

Ran:

$ TZ=UTC DSH_HARNESS=/opt/flybot 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
$ node scripts/build-client.mjs --check
# exit 0
$ 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/
$ node .github/scripts/changelog.mjs check
changelog: 4 fragment(s) in changelog.d/ are well formed
$ node .github/scripts/check-mojibake.mjs --check
mojibake: ok — 0 occurrences of U+FFFD in 1142 text file(s)
$ sha256sum core/src/shared/workflow-repo-dev.js core/src/shared/workflow-blank.js
1e219eff448a797d04353c89fda42bc19f8062e9dfe6e63dc2b5d0fd0d14b60a  core/src/shared/workflow-repo-dev.js
8fda6ea000ddc6cb6466371e521047fa5e2fc63a94b8d191047d24cf09957be5  core/src/shared/workflow-blank.js

All 1163 blobs of the tarball match dae08a98's tree (sha1 over blob <len>\0<bytes> per path), so what I tested is what the PR is. The two preset sha256 values are the two the body quotes — neither preset moved.

Acceptance 1–8, and where each one sits

  1. workflow-board.test.js "a five-node content workflow draws its own five columns, in the definition order" + delivery-view.test.js "a workflow draws its own columns, named by the workflow, and its card once".
  2. "a content card and a repo-dev card get their own columns and no column holds both"; the section header carries the workflow name (same suite).
  3. "the table is one row per node with its seven fields" + "editing one field keeps the edges and the other nodes, and guards round-trip".
  4. "the flowchart is drawn from the table: one box per node, one edge per edge row".
  5. "a stale verdict prints the producer sentence with both revisions and no pass" + "a guard row prints its refusal and never turns an unreadable fact into a pass".
  6. "customize writes a new file and leaves the shipped preset byte-identical", on top of the two preset hashes above.
  7. "the host endpoints read and write, and workflow-save is in the authz allow list" — the allow and a neighbouring refusal.
  8. "a card reads its workflow and node from its labels, and a card without them is unchanged".

Two notes, neither a condition of this verdict

  1. On a real host nothing sets rt.workflowFactsFor, so every guard row prints that it could not read the fact. That is acceptance 5's second half and the honest direction (never a guessed pass), but the reading half is The workflow engine: each node names its Bot and its inputs and outputs, the nodes are drawn as a flowchart, and a node missing an input is refused on the card #1272's next hand and the page should not be described as showing live verdicts until it lands.
  2. arch-docs cannot run in a tarball tree here (no origin/main to diff); CI's package job covers it and is green at this sha.

Not run: the whole core/ suite in one process (CI's tests-ok ran it at this sha and is green; this box hits spawn … EAGAIN on a single-process run), and the smoke bench / browser screenshot of the Workflows page.

What I did not do

No push, no rebase, no update-branch, no base change, no body edit, no label added or removed, and no enqueue — the enqueue is the next step, at this sha.

Merged via the queue into main with commit ae2e461 Oct 27, 2026
13 checks passed
@Birfy
Birfy deleted the dev4/1272-step3 branch October 27, 2026 02:41
@Birfy Birfy mentioned this pull request Oct 27, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 27, 2026
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
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Oct 27, 2026
… 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 -->
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 27, 2026
…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 -->
Birfy added a commit that referenced this pull request Oct 27, 2026
… 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.
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 27, 2026
… 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 -->
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pass risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflow engine step 3: the Workflows page and the columns drawn from the definition

1 participant