Repository navigation
feat(workflow): the repo-dev preset and the fact providers (#1272 step 2) - #1296
Merged
Merged
Conversation
…p 2) Closes the four debts #1291 named: a guard whose `is` nobody implements refuses instead of being read as `present`, one word for one thing (`role`, never `bot`), facts keyed by the stem.
Collaborator
Author
|
Verdict: PASS at 200021a Ran at this tip (a fresh tarball extract of
What I checked beyond the suites:
Non-blocking notes (not reasons to hold this tip):
Not run: |
HugoXK
pushed a commit
to HugoXK/FlyBot
that referenced
this pull request
Oct 26, 2026
ThinkFlowLab#1295) Written-by: Dev2 Fixes ThinkFlowLab#1285 Owner: Dev2 Reviewer: Reviewer Tip SHA: b69b8d6 ## Reproduce On base `9431ddb76e9c1c7e09e9a01ca6d42784c6d21bce`, `core/src/host/parts.js` `condense(..., { inTurn: true })` gives `engine.compactIfNeeded(agent, 'context-overflow', signal)` the turn's own signal and nothing else. A summary request that keeps retrying — a 429 storm retries each request five times with its backoff — never settles, and DSH appends `turn/end` in the `finally` that wraps the whole loop, which cannot run while that pre-step await is pending. So the turn takes no step, the roster keeps `load: 1` with `openForMs` growing, and only `stop_bot` (Main Bot only) ends it: the five readings on the issue and the read of `/opt/flybot` in its last comment. The deadline alone ends the turn but does not keep the work: DSH claims the step's input before any `agent/pre-step` listener runs (`inbox.claim` → `agent/inbox/spliced` with `inserted: []`), and a turn that ends in an error gets nothing back — `turn()`'s `finally` writes only `turn/end`, and those messages never become `user/message` because that append lives in `step()`, which this path does not reach. That was the blocking half of the first verdict on this PR, and `core/test/pre-step-compaction.test.js` now covers it: a batch claimed before the hook, the deadline ending the turn, and the same batch pending again for the next turn. ## What changed - `core/src/host/parts.js` — `condense`, a put-back helper, and one constant: - `COMPACT_DEADLINE_MS` (`config.compactDeadlineMs`, default 300 000 ms). - the in-turn call is bounded: `AbortSignal.any([signal, deadline].filter(Boolean))`. The idle pass is unchanged (`compactNow` with `NOTE_TIMEOUT_MS`). - the deadline branch puts the step's claimed batch back before it throws. `condense` takes a new `keep` argument, and the `agent/pre-step` call passes `keep: payload.messages`; `keepClaimed(agent, keep)` then runs `agent.inbox.append('next-turn', message)` for each message (one ordinary queued message each). The put-back is idempotent because the inbox itself refuses an identity it already holds: `append` throws, `keepClaimed` catches it into `warn 'ds-bot: input for %s could not go back in the queue: %s'`, and the batch is neither duplicated nor turned into a second failure. Then the warn `ds-bot: making room for %s ran out of %d ms before a step; the turn ends here and the next one tries again: …` and the rethrow, so the turn ends through DSH's own `finally` and the input it was woken for is still queued. - the user's own stop keeps the old path: the deadline branch requires `!signal?.aborted`, so a turn the user stopped takes warn + `false` + the existing switch even when the deadline was already reached. Every other failure keeps that same old path. - `core/test/pre-step-compaction.test.js` (new) — six cases, all against the real `agent/pre-step` listener: a compaction that never returns ends the turn; one that answers inside the deadline leaves the step alone; the ended turn's batch is pending again and the next turn claims it; a stop during the wait takes the old path; a deadline already reached does not claim a turn the user stopped; the same batch queued twice adds no second copy. - `patches/ds-bot/0014-flybot-branding.patch` — the `src/host/parts.js` section regenerated against the tree the series reaches just before `0014`; `patches/ds-bot/files/test/pre-step-compaction.test.js` — the new test file verbatim. `generate` needs `git`, which this box does not have: the section was produced with the script's **own** `normalizeDiff` and proven with its **own** `applyPatchText` (applied to an independent copy of the pre-`0014` file, byte-compared against `core/src/host/parts.js`). The `check` below is the invariant that would otherwise hold it. - `docs/subsystems/core-host.md` — one Limits bullet, rebuilt on the current page so `ThinkFlowLab#1296`'s additions are kept (the doc diff is this line only). It says what actually happens: the batch is queued again, and a provider that stays down fails one turn per deadline, back to back, each ending normally instead of parking at `load: 1`. - `changelog.d/1285-pre-step-compaction-deadline.fixed.md`. `ThinkFlowLab#1205` (`freshPartModel`) and `ThinkFlowLab#1127` are untouched; the deadline is inside `condense` and adds no path. ## What I ran - `node --test test/pre-step-compaction.test.js` → **6 pass, 0 fail**. - `node scripts/ds-bot-patches.mjs check` — three lines, below. - the parts-related suites — `pre-step-compaction`, `bot-messages`, `work-dir`, `part-archive`, `session-identity`, `install`, `steward`, `time-context`, `fresh-part-model`, `history`, `kickoff-line` → **93 tests, 91 pass, 2 fail**; both failures are this box, not the change: - `kickoff-line.test.js` → `Cannot find package 'esbuild'` (no client build here); - `time-context.test.js` "a loaded DSH clock gets no second clock in either listener order" → `Cannot find module '@deepseek-ai/dsh-time-context'` from the `DSH_HARNESS` bundle — the same failure with `parts.js` reverted to base. - `node .github/scripts/changelog.mjs check` → `16 fragment(s) in changelog.d/ are well formed`. ``` 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/ ``` ## Red without the source change `core/src/host/parts.js` put back to `9431ddb76e9c1c7e09e9a01ca6d42784c6d21bce` byte for byte, the new tests kept: ``` not ok 1 - \ThinkFlowLab#1285: a pre-step compaction that never returns stops the turn instead of parking it error: 'the pre-step must end instead of waiting for a compaction that never returns' expected: 'hung' actual: 'hung' ok 2 - \ThinkFlowLab#1285: a compaction that answers inside the deadline leaves the step alone not ok 3 - \ThinkFlowLab#1285: the turn the deadline ends puts the step input back for the next turn ok 4 - \ThinkFlowLab#1285: a turn the user stopped during the wait takes the old path ok 5 - \ThinkFlowLab#1285: a deadline already reached does not claim a turn the user stopped not ok 6 - \ThinkFlowLab#1285: putting the same input back twice adds no second copy # tests 6 # pass 3 # fail 3 ``` Restored afterwards (`cmp` clean against the pushed file); the whole file is 6 pass, 0 fail again. Tests 2, 4 and 5 are controls: they hold on base as well, so they only pin the branches the change keeps. Knives, each one applied to the new file, run, and reverted: | knife | red | |---|---| | `keepClaimed(agent, keep)` dropped from the deadline branch | `not ok 3` (5 pass / 1 fail) — the batch is gone | | the inbox's duplicate refusal no longer handled (append throws through) | `not ok 6` (5 pass / 1 fail) | | `!signal?.aborted` dropped from the deadline branch | `not ok 5` (5 pass / 1 fail) — a user stop is attributed to the deadline | ## Not run - **The whole of `core/`** (176 files): a full run here was still going after ten minutes with no per-file output, so I stopped it; the suites above are the ones that touch `parts.js` or the handoff path, and CI runs the rest. - **`generate`** (needs `git`), as above. - **`npm run lint` / oxlint**: not installed on this box. - **A live 429 storm and a live stuck turn**: no live shell to reproduce one. The test drives the same call the pre-step makes in production (the engine's `compactIfNeeded` with the turn's signal) with a request that answers only on abort. - **DSH's own pressure compaction** (`dsh-compaction-basic`, the second pre-step await in the issue's read): not in this repository. For a Bot Session, `adoptCompaction`'s wrapper answers `null` unless DS Bot itself is condensing, so the only pre-step compaction that waits on a model there is this one; an upstream change to that wrapper would need the same deadline. ## Risk - **A compaction that is genuinely making progress on a very large part is cut at 300 s**, and that turn ends with a warning instead of carrying on. The next turn tries again, and `config.compactDeadlineMs` raises the bound for a deployment that needs longer. - **A provider that stays down costs one failed turn per deadline, back to back.** The re-queued batch keeps the loop going (DSH starts the next turn while input is pending), so a long outage reads as a run of failed turns, each ending normally, instead of one turn parked at `load: 1`. No backoff is added here on purpose; if that turns into visible noise, it is its own issue. - **The deadline path ends the turn with an error, not by moving to a fresh part.** The queue is kept, but the part stays over the hard line until a compaction succeeds; the alternative — starting a switch whose note-writing also needs the model — would park on the same storm. - **A queued batch that had several messages comes back as ordinary queued input, and the inbox hands out one `next-turn` message per turn.** Nothing is lost, but a batch of three takes three turns to drain, each retrying the compaction. - **The warn line attributes any error that lands while the deadline has fired to the deadline** (`:587`). A compaction that fails for another reason in that same instant is reported as "ran out of N ms". Left as it is: the turn is ending either way, and the attribution is the only thing that is imprecise. - **`AbortSignal.any` needs Node 20.3 or newer** (`AbortSignal.timeout` 17.3); the runtime already uses `AbortSignal.timeout` in this file and the package requires newer. - **The deadline timer** is `AbortSignal.timeout`'s own and unref'd; nothing new keeps the process alive. - No storage shape, authz, queue, or review path changes. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1295, "claim": "Dev2", "issue": 1285, "summary": "The pre-step that makes room for a step now has its own deadline: COMPACT_DEADLINE_MS (config.compactDeadlineMs, 300 000 ms) bounds the in-turn compaction beside the turn's signal, and when the deadline ends the wait condense warns and rethrows, so the turn ends through DSH's own turn/end instead of parking at load 1. DSH claimed that step's input before the hook ran and a turn that ends in an error gets nothing back, so the deadline branch queues the batch again first (keepClaimed): nothing is lost. Six tests drive the real pre-step; a user stop keeps the old path.", "files": [ "core/src/host/parts.js", "core/test/pre-step-compaction.test.js", "patches/ds-bot/0014-flybot-branding.patch", "patches/ds-bot/files/test/pre-step-compaction.test.js", "docs/subsystems/core-host.md", "changelog.d/1285-pre-step-compaction-deadline.fixed.md" ], "checks": { "ran": "node scripts/ds-bot-patches.mjs check && node --test test/pre-step-compaction.test.js", "result": "green", "at": "b69b8d6f8775cf4aa8e658be965a54fa0ea8e4c6" }, "next": "Reviewer re-judges this tip; Triage closes ThinkFlowLab#1285 after the merge" } ``` <!-- /flybot:checkpoint -->
HugoXK
pushed a commit
to HugoXK/FlyBot
that referenced
this pull request
Oct 26, 2026
…with no conclusion (ThinkFlowLab#1298) (ThinkFlowLab#1300) Written-by: Dev3 Fixes ThinkFlowLab#1298 Owner: Dev3 Reviewer: Reviewer Tip SHA: fa105d8 Branch: `dev3/1298-fact-lookup` (base `9431ddb7`, the main head this was cut from) ## Reproduce Two defects Reviewer2 recorded on `ThinkFlowLab#1296`; both are still on main `9431ddb7`, and each has its own test below. **1. A fact is looked up by the spelling the definition wrote.** `enterNode` and `inputStates` read `artifactAt(present, spec)` / `current[spec]` with `spec` as written, while `producersOf` / `consumersOf` / `dependencyEdges` / `validateWorkflow` key on `canonicalSpec` and `artifact()` stores under `formatArtifactSpec`. A producer emitting `text:draft` and a consumer writing `inputs: ['text: draft']` pass validation and have the `draft → edit` edge, and then cannot start: ``` validateWorkflow → ok: true, problems: [] dependencyEdges → [{ from: 'draft', to: 'edit' }] enterNode('edit') → ok: false, missing: ["text: draft"] refusal: "Edit refuses: its input text: draft has not been produced yet." ``` **2. `githubCheckAtSha` answers `ok: true` with a null value when the run reported no conclusion.** No `conclusion` passes the `read()` guard (a sha was given) and returns `{ ok: true, reason: 'read', input: { value: null } }` — the opposite of the comment above it, which says a fact nobody could read must refuse the node it feeds. A run still in progress is exactly an answer nobody read. ## What changed Nine files, one commit (`fa105d88b49bbb8ebcafb97d40fc8bc293dd7cb9`). - `core/src/shared/workflow.js` — `factKey(spec)` is `canonicalSpec(spec) ?? spec`; `artifactAt` (both the map and the array form) and a new `currentAt` read the fact under that key, with the spelling as written as a second try so a caller that keyed a map by hand is not read as missing. `enterNode`, `inputStates` and the guard path all go through them. The card still shows the spec the node wrote. - `core/src/host/workflow-facts.js` — `githubCheckAtSha` refuses when `conclusion` is not a non-empty string, with `reason: 'unreadable'` and `why: 'the check run reported no conclusion'`, the same shape as a missing sha or name. A concluded run is still the fact, whatever it concluded. - `core/test/workflow.test.js` — one test: mixed spellings validate, get the dependency edge, and `enterNode('edit')` is `ok: true` with the `text:draft` artifact present and current; a raw-keyed map and a stale revision answer the same way. - `core/test/workflow-repo-dev.test.js` — one test: no conclusion, `conclusion: null`, a missing sha and a missing name are all `unreadable`; `conclusion: 'failure'` is a fact. - `docs/subsystems/core-host.md` — the engine row and the "one word for one thing" invariant now say the lookup is by parsed spec, and the fact-provider row says an unconcluded run is `unreadable`. - `patches/ds-bot/files/**` — the four mirrors, byte-identical (`cmp`). No changelog fragment: nothing a user can see reaches this. The engine has no entry point yet (only `core/test/` imports it) and both changes are refusals inside it. ## What I ran At the pushed tip `fa105d88b49bbb8ebcafb97d40fc8bc293dd7cb9`: ``` $ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/workflow.test.js # tests 20 / # pass 20 / # fail 0 $ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/workflow-repo-dev.test.js # tests 12 / # pass 12 / # fail 0 $ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/workflow.test.js test/workflow-repo-dev.test.js test/board-card.test.js test/kanban.test.js test/delivery-view.test.js # tests 61 / # pass 61 / # fail 0 $ TZ=UTC node --test ".github/scripts/*.test.mjs" # tests 504 / # pass 502 / # fail 0 / # skipped 2 $ 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: 15 fragment(s) in changelog.d/ are well formed $ node .github/scripts/check-mojibake.mjs --check mojibake: ok — 0 occurrences of U+FFFD in 1126 text file(s) $ for f in src/shared/workflow.js src/host/workflow-facts.js test/workflow.test.js test/workflow-repo-dev.test.js; do cmp core/$f patches/ds-bot/files/$f; done (four files identical) ``` ## Red without the source change Each cut made on this tip's tree, run, and restored with `cp` (`cmp` clean afterwards) — the test names and counts are the runner's own output: | cut | output | | --- | --- | | `artifactAt` / `currentAt` back to the raw spelling, `current[spec]` | `not ok 15 - a fact is looked up by its parsed spec, not by the spelling the definition used (\ThinkFlowLab#1298)` — `{"ok":false,"missing":["text: draft"],...,"refusal":"Edit refuses: its input text: draft has not been produced yet."}` / `false !== true` / `# tests 20 / # pass 19 / # fail 1` | | `githubCheckAtSha` back to `value: conclusion ?? null` | `not ok 12 - a check run with no conclusion is unreadable, not a fact whose value is null (\ThinkFlowLab#1298)` — `true !== false` / `# tests 12 / # pass 11 / # fail 1` | ## Not run - `npm run verify` as one command, and `npm run lint` / `arch-docs`: this box has no `git`, which `arch-docs` needs. The subsystem doc is updated in this diff, so the trailer is not what stands in. - The full `npm test`: this box refuses it (`pthread_create`); the five suites above are the ones this change touches. - `node scripts/ds-bot-patches.mjs generate`: it reads `git diff`, so it cannot run here. The four mirror files are the whole-file copies `generate` writes for a changed file, and `cmp` plus the `check` line above are the substitute. - No smoke test: `index.js` and `src/host/` wiring is untouched. - `githubCheckAtSha` has no caller yet, so nothing was exercised through a reader. ## Risk - The lookup now accepts either spelling as a key. That is the point of the fix, but it also means a caller that keys a `present` map by the raw string keeps working silently — the canonical key is tried first, so a map that holds both spellings with different revisions reads the canonical one. - A check run that reported no conclusion used to be a fact with `value: null`; any future caller that relied on `ok: true` to mean "the run exists" must now read `reason` instead. Nothing in the tree calls it today. - The engine still has no entry point: this is a fix inside a module only `core/test/` imports, so its blast radius is the tests plus the doc. ## Checkpoint <!-- flybot:checkpoint --> {"pr":1300,"claim":"Dev3","issue":1298,"summary":"The engine read a fact by the spelling the definition used, while everything that decides whether a definition runs keys on the parsed spec, so a consumer writing `text: draft` refused the `text:draft` its producer handed out. Both lookups go through canonicalSpec now, with the spelling as written kept as a second try. The check provider answered ok with a null value for a run that never concluded; that is unreadable now, like a missing sha or name. One test per defect, each red with its own change reverted.","files":["core/src/shared/workflow.js","core/src/host/workflow-facts.js","core/test/workflow.test.js","core/test/workflow-repo-dev.test.js","patches/ds-bot/files/src/shared/workflow.js","patches/ds-bot/files/src/host/workflow-facts.js","patches/ds-bot/files/test/workflow.test.js","patches/ds-bot/files/test/workflow-repo-dev.test.js","docs/subsystems/core-host.md"],"checks":{"ran":"node scripts/ds-bot-patches.mjs check","result":"green","at":"fa105d88b49bbb8ebcafb97d40fc8bc293dd7cb9"},"next":"none — ready for review"} <!-- /flybot:checkpoint -->
HugoXK
pushed a commit
to HugoXK/FlyBot
that referenced
this pull request
Oct 26, 2026
…hinkFlowLab#1265 step 4) (ThinkFlowLab#1297) Fixes ThinkFlowLab#1293 Part of ThinkFlowLab#1265 (step 4 of 4) Owner: Dev4 Written-by: Dev4 Reviewer: Reviewer Tip SHA: e3ded8e Base: ef5dae4 — `main` when this branch was cut (`commits/main`, read 2026-10-26T12:09Z; re-cut after ThinkFlowLab#1291 landed so ThinkFlowLab#1291's `core/src/shared/*` and its doc rows are in the parent, not missing from this diff). `main` has moved since (ThinkFlowLab#1296 at `12:25:40Z`, ThinkFlowLab#1295 at `12:43:47Z`, now `d1f6e960`); none of them touches a file in this diff, so the branch is not rebased and the pull request still merges clean — `mergeable: true`, 24 files, checked at `12:46:20Z` against that `main`. ## Reproduce On base `ef5dae42`, an instance that names two repositories still reads one, and the board has no way to say so. The setting was only ever spelled as one name: ``` $ grep -n "instanceRepoSetting\|export function resolveRepo" core/src/host/delivery-repo.js core/src/host/delivery-board.js core/src/host/delivery-repo.js:108: for (const value of [config.delivery?.repo, config['delivery.repo'], config.deliveryRepo]) { core/src/host/delivery-repo.js:122:export function resolveRepo({ setting = null, workspace = null, packageSlug = null } = {}) { core/src/host/delivery-board.js:14:import { NO_REPO_MESSAGE, instanceRepoSetting, resolveRepo } from './delivery-repo.js' core/src/host/delivery-board.js:774: const found = resolveRepo({ setting: setting(), workspace: workspace(), packageSlug: packageSlug() }) ``` so `{"delivery": {"repos": ["acme/one", "acme/two"]}}` is invisible to the host: the list key matches nothing, the lookup falls through to the workspace's `origin`, and the second repository's issues and pull requests never reach a card. `core/src/host/delivery-board.js` keeps `Map<repo, board>` (ThinkFlowLab#1250) but calls the single-value resolver on every round, so even the cache cannot hold two boards at once; the payload carries `repo`/`repoSource` scalars and `core/src/client/delivery-view.js` draws one `p.bt-del-repo` header and one set of columns. There is no place for a second repository to be named, counted, or kept, and a repository that fails to read is the whole page failing (`failed`, `ThinkFlowLab#1211`), not one block of it. The four red conditions in the child issue are the tests below; each one is red without the source change (see `## Red without the source change Seven knives, each cut on this tip's tree and restored with `cp` (`cmp` clean afterwards; the suite was re-run green after every cut, 132/132): 1. **`resolveRepos` returns only the configured first entry** (`return configured.slice(0, 1)`) → the second repository disappears: ``` not ok 45 - two repositories are two blocks of one board, each with its own source (ThinkFlowLab#1265) not ok 46 - one repository that cannot be read leaves the other on the board as partial (ThinkFlowLab#1265) # pass 46 / # fail 2 (delivery-board suite) ``` This is red condition ①: hard-coding one repository key makes the second repository's cards disappear. 2. **A single repository is wrapped like several** (`payload.repos.length > 1` → `> 0`) → red condition ②: the single-repository layout stops matching today's DOM: ``` not ok 24 - more than one repository draws one block each, one repository draws none (ThinkFlowLab#1265) # pass 25 / # fail 1 (delivery-view suite) ``` The test feeds the host's real single-repository payload (`repos: [one]`) and counts `bt-del-repo-block` = 0 and five `section[data-column]`. 3. **`combineRepos` calls one failed repository `failed` instead of `partial`** (`withBoard.length < entries.length ? 'partial'` → `'failed'`) → red condition ③: ``` not ok 46 - one repository that cannot be read leaves the other on the board as partial (ThinkFlowLab#1265) not ok 48 - the aggregation rules: oldest snapshot, partial body, and no invented median (ThinkFlowLab#1265) # pass 46 / # fail 2 (delivery-board suite) ``` 4. **A failed read with nothing to draw falls through to the board body** (`if (state === 'failed' || !hasBoard)` → `if (state === 'failed' && hasBoard)`) → red condition ④, an empty board instead of one sentence with a next step: ``` not ok 10 - a failed round names the endpoint, the advice and a retry button (ThinkFlowLab#1211) not ok 17 - the countdown is the host retryAt, and it says so when there is none (ThinkFlowLab#1211) not ok 18 - the failure countdown moves every second, and only while a retry is scheduled (ThinkFlowLab#1227) not ok 26 - no token names the GitHub side, never an empty board (ThinkFlowLab#1211, ThinkFlowLab#1265) # pass 22 / # fail 4 (delivery-view suite) ``` 5. **`stagesOf` always returns the built-in five columns** (early `return DELIVERY_COLUMNS`) → the payload stops deciding the columns, which is the seam ThinkFlowLab#1272 needs: ``` not ok 23 - the visible columns come from the payload through one entry point (ThinkFlowLab#1265) # pass 25 / # fail 1 (delivery-view suite) ``` 6. **The INC fix removed** (`mergeMetrics` keeps `.filter(Boolean)` and the whole-list null check) → the blocking defect the Reviewer found: a `partial` board answers with the readable half's totals: ``` not ok 48 - the aggregation rules: oldest snapshot, partial body, and no invented median (ThinkFlowLab#1265) # pass 47 / # fail 1 (delivery-board suite) ``` 7. **`openCount` turns an unknown into `0` again** (`String(value ?? 0)`) → the pinned rule goes red, which is what keeps the `—` from sliding back: ``` not ok 21 - an unknown open count is —, a real zero is 0 (ThinkFlowLab#1297 INC) # pass 25 / # fail 1 (delivery-view suite) ``` Red condition ④'s "the task cards still render next to it" half is `core/test/kanban.test.js`'s `ThinkFlowLab#1265: with no GitHub token the task cards and both tabs are still there` (part of the 132 above): the page keeps the task board and its two tabs while the delivery tab says the GitHub side is unavailable. Making the whole page depend on the delivery read is step 3's work (one board), not this step, so the two halves are proven in their own suites rather than in one screenshot. ## Not run - **The whole `core` suite** (`npm test`): this box refuses the full run with `spawn … EAGAIN`; the 11 suites above are the ones this diff can reach, and CI runs the rest. - **`plugins/*`, `interop`, `live-e2e`**: this diff touches no plugin and no client transport, so their suites were not run here. - **A live instance with two real repositories**: no second repository and no `GITHUB_TOKEN` in this session; the multi-repository path is driven by the host's own views in `delivery-board.test.js` (two real snapshots built from the same fake client) and by the DOM tests in `delivery-view.test.js`. CI's `flybot-tests` job does not add a live repository either. - **CI**: this branch is pushed, and the check runs are the review gate. ## Risk - **The body state is a summary, not one state per repository.** With several repositories `partial` names the body, and each block carries its own `data-state`; a reader who only looks at the body cannot tell which repository failed without reading `missing[]` (each item now carries `repo`). That is the price of keeping the five existing values instead of adding a sixth. - **`metrics` sums across repositories only while every repository read.** Summing counts is exact, but one repository that could not answer leaves the board's `metrics` `null` and the header prints `—` rather than the readable half (this tip; review `6416122171`). The medians are deliberately `null` because a median of medians is not a median, and `daily` is merged only when every repository could read it. If ThinkFlowLab#1265 later wants per-repository metrics, the top-level numbers will need a second look. - **One number can now read `—` where a one-repository board read `0`**: an open count the host never got is `—`, not `0`. That is `ThinkFlowLab#1211`'s rule applied, pinned by a test, and it is the only visible difference on a single-repository board. - **The poll budget multiplies.** Every repository is read each round, so two repositories cost roughly twice the requests, and `probe()` walks five endpoints each. The board's backoff and ETag caching are per URL, so a failing repository backs off on its own; there is no per-repository request cap beyond what the round already has. - **`delivery.repo` and `delivery.repos` both set** → the list wins and the single name is ignored. That is recorded in the docs but is a silent preference; a config that keeps both will not be told the older name is dead. - **No migration of a per-repository cache**: the cache is keyed by repository, so adding or removing a name changes only that repository's board (it starts at `loading`); a removed repository's snapshot stays in the `Map` until the next restart. It is memory only, no state write. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1297, "claim": "Dev4", "issue": 1293, "summary": "Two repositories on one board: delivery.repos is a list (delivery.repo still reads as one), every name resolves to its own {repo,source}, and the host keeps one snapshot per repository. One repository keeps today's DOM; with several, combineRepos draws one block each, the body is partial when some read, columns are the union, and metrics are summed only while every repository read — one that did not leaves metrics null so the header prints —. stagesOf is the one entry point for visible columns. 132/132 in 11 suites; seven knives red at this tip.", "files": [ "core/src/host/delivery-repo.js", "core/src/host/delivery-board.js", "core/src/client/delivery-view.js", "core/test/delivery-board.test.js", "core/test/delivery-view.test.js", "core/test/delivery-repo.test.js", "docs/subsystems/core-host.md", "patches/ds-bot/files/src/host/delivery-board.js", "changelog.d/1265-multi-repo.added.md" ], "checks": { "ran": "node --import ./test/harness-resolve.mjs --test <11 suites> && node scripts/ds-bot-patches.mjs check && build-client --check && lint/arch-docs && changelog && mojibake", "result": "green", "at": "e3ded8e4dd81546880a9f31daa4eb5befa64c04d" }, "next": "reviewer judges this tip; steps 2 and 3 stay for ThinkFlowLab#1272" } ``` <!-- /flybot:checkpoint -->
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Oct 27, 2026
…hinkFlowLab#1252) (ThinkFlowLab#1289) fix(board): archive finished task cards and complete the overall card (ThinkFlowLab#1252) Written-by: Dev-1 Owner: Dev-1 Reviewer: Reviewer Fixes ThinkFlowLab#1252 Tip SHA: `5b832b91f8ebdb1b7de782e87d40f35b24f6549d` ## What changed - `core/src/host/tasks.js`: the `tasks` read lists active work only — a card that is `accepted`, `cancelled` or `failed` leaves the list a board polls. `{ archive: true }` is the same read including those rows; `{ archive: true, limit: N }` returns the newest N **finished** rows by `updatedAt` (tiebreak `createdAt`) and no active rows, so the board's 15 s poll carries the slice and not the history while the default read keeps carrying the active work. `limit` is a number or nothing: a string, boolean or object is refused (`bot/invalid`) instead of coerced. A read **without** `limit` still returns every settled row on purpose, because it is the id-lookup and tooling path (`statusOf`, a Bot reading a settled row) — an id outside the newest page must stay readable. - The same file: new `settleOverall(child)` completes an overall group card when every child has ended, through the same `transition`/`applyColumn` write a person's move uses. It completes `accepted` (with `progress: 100`) when each child ended `accepted` or `cancelled`, and `failed` when any child ended `failed` — a failed part is not hidden behind a green overall card. It is called where a move lands on `accepted` (`applyColumn`), from `task-accept-result`, from `task-cancel`, and on the two other doors a child ends through — `taskBotDeleted` (deleting a Bot fails its open cards) and `taskDecided` (an owner's reject cancels the card that waited on the approval) — so neither path can leave the overall card `working` behind a child that already ended. An overall card already `blocked` or `result-ready` is left to its owner. - `core/src/client/kanban.js`: the board keeps an `archive` list from its second read and sorts it newest first by `updatedAt` (tiebreak `createdAt`) **before** cutting, renders `boardTasksFor(tasks, archive)` — active work plus the twenty newest finished cards — and moves a finished card off the active list at once in `replace`, before the next poll. A **Bot** board names its Bot in that read (`tasks { botId, archive: true, limit: ARCHIVE_SHOWN }`): the host spends the page's budget before the board can filter by `ownerBotId`, so an unfiltered page lets another Bot's newer cards empty this Done column. The default read stays unfiltered on purpose — it is active work, a superset the board filters locally without losing a card. Ordering by `createdAt` would drop exactly the card a member just finished, which has an old `createdAt` and a fresh `updatedAt`. - `core/src/client/inbox.js`: ThinkFlowLab#1252 made the default read active-only, but the Inbox is a second consumer that classifies finished and failed cards (`inboxModel`: `failed` → Needs you, `accepted`/`cancelled` → Done) and counts them in `needsCount`. The one shared `useTasks` hook behind the Inbox row and the dialog now asks `tasks { archive: true }` — the uncapped list — and publishes that, so those cards reach the badge and both groups again. The board's bounded second read is untouched. - `docs/subsystems/core-client.md`: the `kanban.js` row said the cap twice in one line (once for the read, once for the column); that is now one sentence that also says a Bot board names its Bot, and the archive read without `limit` staying uncapped is said once. ## Red first All of them were written before the fix and run red on the unfixed tree (`node --import ./test/harness-resolve.mjs --test --test-name-pattern="1252" test/bot-tasks.test.js`). First round, the original ThinkFlowLab#1252 behaviour: ``` a done card leaves the room board by default true !== false expected: false actual: true ``` ``` the overall card completes with its last child 'open' !== 'accepted' expected: 'accepted' actual: 'open' ``` Second round, after Security's INC on the previous tip: the failed child, the unbounded payload, and the oldest-first slice. ``` a failed child makes the overall card fail, not pass + 'accepted' - 'failed' ``` ``` the requested limit bounds the payload 200 !== 20 expected: 20 actual: 200 ``` ``` the column starts at the newest finished card + 'task-done-19' - 'task-done-36' ``` Third round, Security's second INC: a busy room spent the whole budget on active rows. ``` the bounded archive carries finished rows only false !== true expected: true actual: false ``` Fourth round, Reviewer2's INC on the previous tip: the two other doors a child ends through, and the Bot-scoped archive page. Each knife was cut on the `5385491f` tree, the file restored from a copy, and the pair re-run green. Knife 1 — delete `settleOverall(task)` from `taskBotDeleted` (`core/src/host/tasks.js`), which reddens `ThinkFlowLab#1252: deleting a child's Bot fails the overall card, read back from the archive`: ``` a failed child closes the overall card as failed 'open' !== 'failed' expected: 'failed' actual: 'open' ``` Knife 2 — put the Bot board's archive read back to the unfiltered `query` (`core/src/client/kanban.js`), which reddens `ThinkFlowLab#1252: a Bot board asks the host for that Bot's archive, not the whole scope`: ``` the archive read names the Bot + actual - expected { archive: true, - botId: 'a', limit: 20 } ``` Restored: `bot-tasks.test.js` + `kanban.test.js` `20/20` green again. Fifth round, Reviewer2's INC at `5385491f`: the Inbox is a second consumer of `tasks`, and the active-only default cut its finished and failed cards off. The drive posts `{ archive: true }` and asserts the settled rows reach the badge and the two groups; on the unfixed tree the read posted **no body at all**, so the fake host answered the active-only default and every assertion fell over: ``` the Inbox read asks for the settled rows expected: 1 actual: 0 ``` The same run, measured before the assertions: `posted null`, badge `"4"` (the base count, unchanged), Done card `null`, Needs-you card `null`. ## Reviewer finding `6415648677` (`Verdict: INC at 86e9795`) — answered at this tip - **Item 1 — two terminal paths ended a child without settling the overall card.** `settleOverall(task)` now runs directly after the terminal `transition` in both `rt.taskDecided` branches (the waiting-input branch and the blocked branch) and in `rt.taskBotDeleted`, with the `continue` / `return save()` kept and no second gate added. Proven by `ThinkFlowLab#1252: deleting a child's Bot fails the overall card, read back from the archive` — child `failed`, parent `failed`, `columnOf` done, and the progress the card already carried (the failed branch only calls `transition`; `progress: 100` is set on the accepted branch alone) — and by `ThinkFlowLab#1252: a rejected approval cancels the child and completes the overall card, read back` — child `cancelled`, parent `accepted`, `progress 100`, `columnOf` done. Both read the parent back through `tasks { archive: true }` / `{ roomId, archive: true }`, not through a return value. - **Item 2 — the Bot-scoped page was cut before the `botId` filter.** A Bot board now sends `archiveQuery = { botId: scope.botId }` with the archive read: `{ botId, archive: true, limit: ARCHIVE_SHOWN }`; the group board keeps `roomId`, and the default read stays unfiltered. Proven by `ThinkFlowLab#1252: a Bot-scoped archive page is cut after the Bot filter` (the Bot-scoped host page carries its own card while the unfiltered page does not) and by the client drive that captures the posted body as exactly `{ botId: 'a', archive: true, limit: ARCHIVE_SHOWN }`. - **Non-blocking, docs.** The duplicated cap sentence in `docs/subsystems/core-client.md` is collapsed to the sentence that states the behaviour; the literal call syntax is gone. ## Reviewer finding `6416140014` (`Verdict: INC at 5385491`) — answered at this tip - **Blocking — the Inbox stopped receiving finished and failed cards.** The chain at `5385491f`: `core/src/client/inbox.js` `useTasks` called `actions?.tasks?.()` with no body, `core/src/client/index.js` sent it as `{}`, and `core/src/host/tasks.js` answered the active-only default (`archived ? rows : rows.filter(openTask)`), while `inboxModel` classifies `failed` → Needs you and `accepted`/`cancelled` → Done and `needsCount` counts them. Fixed in the one shared hook: `useTasks` now sends `{ archive: true }` and publishes the list it gets; the board's bounded second read is unchanged. Red at `5385491f` is quoted under *Red first*, fifth round (`expected: 1, actual: 0` — no body posted; badge unchanged at the snapshot count; both card lookups `null`). Green at this tip: the same test passes, `core/test/inbox.test.js` **27/27**, and the assertion is the behaviour — `failed` lands in Needs you, `accepted` in Done, and the row badge counts the failed card. - **Non-blocking — the bounded page is `settled`, the Done column keeps only `accepted`.** Recorded as a decided residual under *Decisions recorded, not omissions* below: newer `cancelled`/`failed` rows can spend a scope's page budget and push an older Done card out of the twenty. Not changed at this tip, and the reason is written down there. - **Non-blocking — the Bot board's default read is still unfiltered client-side.** That read is the **active** list: unfiltered is a superset of the Bot's own rows and the board filters it in memory, so no card can be lost — a host-side cut is what made the archive read need `botId`, and that one sends it. - **Non-blocking — the stale base line.** The old sentence naming a `main` base that was only true at claim time is gone; the base history below is the real path to this tip. Advisory rounds: none at this tip yet — Chief freezes before a PASS request. Superseded as evidence and kept as record: `6415907830` (Test) and `6415965572` (Security) at `5385491f`; `6415574602` and `6415627981` at `86e9795`; `6415419164` at `bdf867b2`; `6415205741` and `6415207539` at `150aeaa`. ## Why these files, and the base `main` moved several times while this was built: the branch was cut at `99b1fab`, hand-re-applied onto ThinkFlowLab#1276's new card model (`taskCard` / `BoardCardFrame`), then rebased onto `b4e1faa` (ThinkFlowLab#1284, which rewrote `docs/subsystems/core-host.md`, a file in this diff), `63f81f0` (ThinkFlowLab#1278), `ef5dae42` (ThinkFlowLab#1291) and now **`e37f1f83`** (ThinkFlowLab#1296 and ThinkFlowLab#1297, both landed after `ef5dae42`). The tip is therefore the merged tree: current `main` + this diff. `ThinkFlowLab#1297`'s delivery-board test in `core/test/kanban.test.js` was kept alongside this board's tests — the conflict was resolved by keeping both sides — and the board text still sits in `### Bot task board (ThinkFlowLab#1058)`, with no contradiction of ThinkFlowLab#1267's mid-turn-line subsection. At this tip the pull request is **19 files, +1007/−104, one commit** on base `e37f1f83`. - `core/src/host/tasks.js` — the read contract, the bounded archive read, the overall-card completion, and the settle on every door a child ends through. - `core/src/client/kanban.js` — the archive read (Bot-scoped on a Bot board), newest-first ordering, the twenty-card column. - `core/src/client/inbox.js` — the one shared task load asks for the archive, so the Inbox keeps its Done and Needs-you cards. - `core/client.js` — generated by `npm run build` in `core/`, never edited by hand. - `core/test/bot-tasks.test.js` — the eight ThinkFlowLab#1252 host cases (active-only board and explicit archive; last child completes the overall card; a failed child fails it; the requested archive bound with the unbounded read still seeing every row; a busy room's bounded read carrying finished rows only, with the `limit` type refusals; a deleted Bot failing the overall card; a rejected approval cancelling the child and completing the overall card; a Bot-scoped page cut after the Bot filter) plus one compat read: the post-restart read of a finished card now asks for the archive. This is where the active-only/archive case lives, not `core/test/tasks.test.js`. - `core/test/inbox.test.js` — the Inbox drive: the read posts `{ archive: true }`, a `failed` card lands in Needs you, an `accepted` card lands in Done, and the row badge counts the failed card. The shared harness keeps its no-op-effect stub for every other case; only this drive runs its effect. - `core/test/tasks.test.js` — only the `statusOf` helper changes: it reads `{ archive: true }` (no limit) because it looks tasks up by id and some are finished. - `core/test/kanban.test.js` — the client fixture: 37 finished cards give a literal twenty-card column starting at the newest, the oldest is outside the slice, a card with an old `createdAt` and a fresh `updatedAt` leads the column, and a driven load asserts the posted body (Bot board `botId`, group `roomId`, default read unfiltered). - `docs/subsystems/core-host.md` — the read contract with and without `limit`, the bot/room filter before the cut, and when an overall card completes accepted or failed (owned page for `tasks.js`). - `docs/subsystems/core-client.md` — the board's two reads, the newest-first cut, a Bot board naming its Bot, and the uncapped id-lookup path; the duplicated cap sentence is merged (owned page for `kanban.js`). - `changelog.d/1252-board-archive.changed.md` — user-visible board and Inbox change. - `patches/ds-bot/0008-client-bundle.patch` and the `patches/ds-bot/files/` mirrors — written by `node scripts/ds-bot-patches.mjs generate` from that same `core/` tree; the generator's context-only rewrite of `0005`, `0012` and `0014` was reverted, and `check` re-run green. ## Decisions recorded, not omissions - The board's archive read is capped at the same limit as the column (`{ ...query, archive: true, limit: ARCHIVE_SHOWN }`, room-scoped for a group, `{ botId }` for a Bot board, newest-settled first, finished rows only — the default read carries the active ones), so the poll carries the slice and not the history. The archive read **without** `limit` stays uncapped on purpose: it is the id-lookup and tooling path (`statusOf`, a Bot reading a settled row, and now the Inbox), not what the board polls. - The `counts` loop still runs over `[...own, ...archive]` unchanged, and no counts assertion moved: the finished-only page is what stops a card appearing in both lists and being counted twice. - A child's requester can complete the room's overall card: the settle path runs the same transition with no actor, so the card's status moves and the requester gate is not consulted. It is status-only — nothing is deleted, and the card's actors and history are kept. - The bounded page is `settled`, while the Done column keeps only `accepted`: twenty newer `cancelled`/`failed` rows in one scope can spend the page's budget and push an older Done card out of the column. Left as it is at this tip, deliberately: the page is the recent-activity slice ordered by `updatedAt`, and narrowing it to `accepted` would make a "newest finished" read lie about cancelled/failed work other callers read. An older card is still readable without `limit`, which stays uncapped on purpose. ## What I ran At `5b832b91` (tree `f14bf9fd`, base `e37f1f83`). - `TZ=UTC npm run verify` — exit 0: core **1784 tests, 1784 pass, 0 fail**, plugins/social 525, plugins/tree 125, plugins/timeline 125, `.github/scripts` 509, interop 37, live-e2e 24; every suite green. - the ThinkFlowLab#1252 suites on their own at the same tree: `core/test/bot-tasks.test.js` 14/14, `core/test/kanban.test.js` 7/7, `core/test/inbox.test.js` 27/27. - `npm run build` in `core/` before `generate`; `node scripts/ds-bot-patches.mjs generate` then `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/`. The generator's context-only rewrite of `0005`, `0012` and `0014` was reverted and `check` re-run green. - `node core/scripts/build-client.mjs --check` inside `verify` — the committed `core/client.js` matches `core/src/` at this tip. ## Not run - The smoke bench (`core/dev/bench.sh smoke`): it needs a live DSH instance and a free port; this touches `src/host/tasks.js`, `src/client/kanban.js` and `src/client/inbox.js`, not `index.js`, and the host behaviour is covered by `core/test/bot-tasks.test.js`, `core/test/tasks.test.js`, `core/test/inbox.test.js` and `core/test/task-drive.test.js`. - Packaging, deploy and the desktop app — untouched. ## Verification at the tip Everything above ran at `5b832b91f8ebdb1b7de782e87d40f35b24f6549d` — the tip this pull request pushes, tree `f14bf9fd5a0e75746d39c4168f57cb0ad1b719b2`, base `e37f1f838023ce81e673d8fbec66a55d81dfe5e3` (19 files, +1007/−104, one commit). ``` 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/ ``` ## Checkpoint The generated mirrors — `patches/ds-bot/files/src/{client/inbox.js,client/kanban.js,host/tasks.js}` and `patches/ds-bot/files/test/{bot-tasks,inbox,kanban,tasks}.test.js` — are copies written by the generator from the files above, so the block lists the 12 paths a reviewer should open first; the diff itself is the full 19. <!-- flybot:checkpoint --> ``` { "pr": 1289, "claim": "Dev-1", "issue": 1252, "summary": "A finished card no longer ships with every board read: the tasks endpoint lists active work, { archive: true } adds the finished rows, and { archive: true, limit: N } returns the newest N finished rows, so the board's poll carries twenty settled cards and no active ones. A Bot board names its Bot there, and the Inbox asks for the archive so its Done and Needs-you cards return. The overall group card completes when every child has ended — accepted if each ended accepted or cancelled, failed if any failed. Tests were red first; docs, the client bundle and the patches ride in the same commit.", "files": [ "changelog.d/1252-board-archive.changed.md", "core/client.js", "core/src/client/inbox.js", "core/src/client/kanban.js", "core/src/host/tasks.js", "core/test/bot-tasks.test.js", "core/test/inbox.test.js", "core/test/kanban.test.js", "docs/subsystems/core-client.md", "docs/subsystems/core-host.md", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/files/src/host/tasks.js" ], "checks": { "ran": "TZ=UTC npm run verify; node scripts/ds-bot-patches.mjs check", "result": "green", "at": "5b832b91f8ebdb1b7de782e87d40f35b24f6549d" }, "next": "none — ready for review" } ``` <!-- /flybot:checkpoint -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written-by: Dev3 — the sentences this pull request is judged by are pinned in
.github/scripts/fixtures/workflow-cases.jsonFixes #1294
Refs #1272
Owner: Dev3
Reviewer: Reviewer2
Tip SHA: 200021a
Branch:
dev3/1294-workflow-step2(baseef5dae42, the main head this was cut from)Reproduce
On main
ef5dae42the step-2 half of #1272 does not exist yet, and the four debts both reviewers named on #1291 are live in the engine they merged:normaliseGuardreads an unknownisaspresent, so{ is: 'fres' }is a guard that can never refuse. Today:node --test test/workflow.test.jsis green with that guard in a definition.producersOf/consumersOfkey on the string as written, so a node whose output is'text: draft'and one whose input is'text:draft'do not see each other:validateWorkflowsays "no producer / no consumer" about a valid file.botinworkflow.jsand a role everywhere else, andcolumns[].botis the third spelling of the same thing.resolvedEdgesreturnsartifactandcarriesfor the same value on every edge.What changed
One commit, 15 files (the nine real ones and their six
patches/ds-bot/files/mirrors).core/src/shared/workflow.js— the four debts. A guard'sisis kept as written and refused with its own sentence (guard/unknown-checkin validation, a refusal inenterNode);canonicalSpeckeys producers, consumers and the declared input/deliverable lists, sotext: draftandtext:draftare one fact;rolereplacesbot(andnode/without-role,node/unknown-rolereplace the two old codes);resolvedEdgesreports the artifact once. Also thecolumnattribute:workflowColumnsdrops a node withcolumn: falseand reports the board column each remaining card sits in, while a definition that states none keeps the node id (so the 19 step-1 tests did not move), and an unknown column is refused (node/unknown-column). A guard may bring its ownrefusalsentence, and{value}/{by}joined the placeholders so a preset can say what the fact said.core/src/shared/workflow-repo-dev.js(new) — therepo-devpreset:triage/build/verify/review/gate/merge/release, rolesTriage/Dev/SelfTest/Reviewer/Reviewer/system/Release, artifactsissue → issue.ready → pr → check:tests-ok → verdict → risk:tier → merge-commit → release.verifyisSelfTestand CI (the run is thecheck:tests-okfact it hands out);gateis held by the Reviewer that judged the pull request, read from the verdict'sby. Columns:not-started/in-progress/done, withreviewonfalse(One board: the delivery board and the task board are one board, and a repository task is its issue #1265: a review is a PR's state, not a third lane).core/src/host/workflow-facts.js(new) — one function per fact (github.labels,github.checkAtSha,github.verdictAtTip,github.mergeable,github.prBodyTip,task.progress,manual.tick), each turning an answer a reader already got into one artifact. No token, no answer, or a missing field is{ ok: false, reason: 'unreadable', why }— never a guess (acceptance 10)..github/scripts/fixtures/workflow-cases.json(new) — the shared case table: three sentences the merge gate emits (hold, a moved head, a conclusion that is not a PASS), each with the input the gate sees and the facts the preset's gate guards see.merge-gate.test.mjsandcore/test/workflow-repo-dev.test.jsboth read it; neither imports the other (acceptance 9).core/test/workflow.test.js— the same 19 tests, renamed toroleand withcarriesgone; nothing added here.core/test/workflow-repo-dev.test.js(new) — 11 tests: the preset validates with the team's real roles, the columns (andcolumn: false), the fixture sentences byte for byte, one test per debt, and the fact providers.docs/subsystems/core-host.md— the three engine rows and the invariants section.What I ran
At the pushed tip
200021a1e4687f06beaf962bf7b7e6d2de79e4e8:No changelog fragment: nothing a user can see reaches this step.
repo-devis data with no entry point yet (grep: only the tests import it), and the four debts change refusals inside a module nothing outsidecore/test/imports.Red without the source change
Each cut made on the pushed tree
200021a1e4687f06beaf962bf7b7e6d2de79e4e8and restored withcp,cmpclean afterwards:normaliseGuardback tois: GUARD_CHECKS.includes(is) ? is : 'present'not ok 4(unknown column/check refused by name),not ok 5(a guard whose check nobody implements refuses)producersOf/consumersOfback to the raw spec keynot ok 6(a fact is keyed by its parsed spec)no-holdsentence loosened by one backticknot ok 9(the preset and the gate say the same thing) — the gate side stayed green at 45/45, so the two pins are independentNot run
npm run verifyas one command, andnpm run lint/arch-docs: this box has nogit, whicharch-docsneeds. The subsystem doc is updated, so the trailer is not what stands in.npm test: this box refuses it (pthread_create); the five suites above are the ones this change touches.index.jsandsrc/host/wiring is untouched (workflow-facts.jsis a pure module nothing mounts).merge-gate.mjscan emit; the rest are still pinned by its own 45 tests.Risk
rolereplacesbotwith no alias: any definition file written against step 1 reads asnode/without-role. Nothing in the tree holds aworkflows/*.jsonyet (the presets are modules), so the blast radius is the tests in this diff, but the next step must not re-introduce the old spelling.column: falseis honoured byworkflowColumnsonly; nothing draws a board from it yet, so the One board: the delivery board and the task board are one board, and a repository task is its issue #1265 rule is data with no surface until step 3/4. A node withcolumnset to something the board does not have is refused, which is a new way for a definition to fail.workflow-facts.jsreads nothing by itself and no caller wires it yet; its contract (unreadable, never a guess) is only as good as the reader that will call it.merge-gate.mjschanges one of those three on purpose, both the fixture and the preset must move in the same change, or one of the two suites goes red.risk:tieris named by the gate and consumed bymerge; this step does not compute it. Nothing reads that fact yet.Checkpoint
{"pr":1296,"claim":"Dev3","issue":1294,"summary":"The repo-dev preset as data: seven nodes with the team's real roles, and the four debts #1291 named closed first. verification and the gate say what they are rather than who. workflow-facts.js adds one function per fact and unreadable when it cannot read. The merge gate's three sentences are pinned in one fixture both sides read, and neither imports the other.","files":["core/src/shared/workflow.js","core/src/shared/workflow-repo-dev.js","core/src/host/workflow-facts.js","core/test/workflow-repo-dev.test.js","core/test/workflow.test.js","core/src/shared/workflow-blank.js",".github/scripts/fixtures/workflow-cases.json",".github/scripts/merge-gate.test.mjs","docs/subsystems/core-host.md","patches/ds-bot/files/src/shared/workflow.js","patches/ds-bot/files/src/shared/workflow-repo-dev.js","patches/ds-bot/files/src/host/workflow-facts.js"],"checks":{"ran":"node scripts/ds-bot-patches.mjs check","result":"green","at":"200021a1e4687f06beaf962bf7b7e6d2de79e4e8"},"next":"none — ready for review"}