Repository navigation
Description
Written-by: Fledge Platform Product
Expected vs actual
One repository, one day, two different next-run answers. The repository page names the instant the scheduler will run it; the daily digest email names a time computed from last_run_at + min(interval_minutes, workCooldownMinutes). The email never reads the repository's schedule row, so for a cron repository the two surfaces disagree by days, and for a disabled repository with no schedule row the email promises a run the page says will never start.
Actual. buildDigest (src/jobs/service.ts:2449) computes the line for every repository with auto_iterate = 1:
2461 const next = Number.isFinite(last) ? new Date(last + Math.min(repo.interval_minutes, this.config.runner.workCooldownMinutes) * 60_000) : new Date()
2462 nextRuns.push({ repo: label, when: next.getTime() <= Date.now() ? '等待调度' : this.digestTime(next) })
rendered as · 下次自动迭代:<repo> · <when> at src/email/templates.ts:371 (text) and :388 (html). Neither line consults store.scheduleForRepo (src/db.ts:4174), repos.enabled (src/db.ts:1018) or the legacy-rule floor.
Untested. That formula has never been asserted: buildDigest has exactly one caller (src/jobs/service.ts:2512) and no reference anywhere under test/. test/digestEmail.test.ts builds nextRuns by hand and asserts the template, so the faulty line is the one part of the sentence no test can see — a red test has to drive jobs.tick(), not the template.
Expected. The same instant the repository page names, computed by the same rule. The page reads the schedule row in the route (src/app.ts:1825–:1826, ScheduledNext at :1888) and prints it through plannedNextLine (src/views/pages.ts:745) → nextRunLine (:728) → nextTime (src/schedules/next.ts:463); the dashboard does the same (src/app.ts:1248–:1257 → src/views/pages.ts:769). docs/architecture.md:107–:113 states the invariant: "One next-run rule … which rule answers is decided by the repository, never by the surface." The digest is a third surface of that rule and today is the only one outside it.
Minimal reproduction
DIGEST_HOUR=0, DIGEST_TIMEZONE=Asia/Shanghai, an approved user with email_daily_digest = 1, one repository o/r with auto_iterate = 1, enabled = 1, interval_minutes = 1440, last_run_at = <now - 10 minutes>, and a saved schedule row { kind: 'cron', expression: '0 9 * * 1-5', timezone: 'Asia/Shanghai' } via store.saveSchedule (src/db.ts:4186). Trigger the pass with jobs.tick() (src/jobs/service.ts:2912), which calls maybeSendDigests() at :3211.
- Repository page prints
2026-11-16 01:00:00 UTC— Monday 09:00 Asia/Shanghai (plannedNextLineformats the planner instant throughhumanTimeShort,src/lib/time.ts:30). - Digest email prints the interval answer —
last_run_at + 15 minutes, i.e. five minutes after the pass,11/14 20:57for a pass at12:52Z— this evening, instead of Monday (digestTime,src/jobs/service.ts:2438, formats inconfig.digest.timeZone).
Case 2a, deterministic — disabled with no schedule row. Setup as case 1 but auto_iterate = 1, enabled = 0 and no schedule row: the page prints 仓库已停用,自动运行不会开始 (src/views/pages.ts:730), and the digest still prints a time. store.listRepos (src/db.ts:4122) has no enabled filter, so a disabled repository reaches buildDigest unchanged. Write auto_iterate = 1, enabled = 0 explicitly rather than "same setup with enabled = 0": the /repos/:id/disable route sets both fields to 0 (src/app.ts:3310) and buildDigest emits a line only for auto_iterate === 1 (src/jobs/service.ts:2460), so a repository disabled through that button yields no line at all and the case would assert on an absent line. Only the settings route writes the two fields independently (src/app.ts:3246–:3247).
Case 2b, disabled with a row — the slice must preserve the page, not contradict it. Case 1 plus enabled = 0: the page still prints the planner instant, because the route attaches scheduledNext whenever a schedule row exists (src/app.ts:1884–:1889) and scheduledNext wins over nextRunLine (src/views/pages.ts:1259), so enabled is never consulted for a saved row. The digest must print that same planner instant. This is why the disabled assertion is narrowed to the no-row fixture: a helper that checked enabled before the row would disagree with the page on exactly the disabled-with-row repository, which is the same class of defect this card removes, pointed the other way.
Fixture note for case 1. The digest's 等待调度 branch is emitted when its computed time is <= now, which is exactly the page's due state (src/views/pages.ts:748). A fixture whose last_run_at is old enough that last_run_at + 15m is already past would make the buggy email print 等待调度 and could accidentally agree with a due page. Keep last_run_at inside the cooldown (now - 10m, clamp 15m) so the buggy answer is a future formatted time, and assert the two candidate strings differ before asserting the email carries the planner's one.
Proposed red test
Two existing files, split the way the tree already splits this surface — no new test file.
test/nextScheduledRun.test.tsowns these assertions today: both precedent commits added their next-run cases there —10ea236622(the one-next-run rule) andf859f242(the schedule row) — and ataa83dc0bthe file already covers the cooldown rule (:115), the page and dashboard agreeing (:135), disabled (:144), active job (:151), idle (:158) and "the dashboard answers with the schedule row, not the interval" (:173). The new helper's own cases belong in this file: the planner branch returning{ at, state: 'scheduled' }withnextTime's instant for a cron row,unknown-unreadablefor a rowscheduleFromRowrefuses (src/schedules/tick.ts:64),unknown-closedfor a window that has ended, and eachNextRunStatefor the interval branch, includingdisabled,not-ready,idle,active-job,waiting-first-run, plus one case provingnot-readyis never reported whenconfig.flybot.instanceImage === ''(the fallback in the shape below). This adds cases to an existing file; it does not narrow or re-open that file's current cases.test/jobs.test.tsgets only the three digest integration cases, because it already drives the pass:new Jobs(store, testConfig({ digest: { hour: 0, timeZone: 'Asia/Shanghai' } }), runnerStub, new MemoryMailer()), seed,await jobs.tick(), readmailer.messages— precedent:171("daily digests still send while two jobs are active"), so no new seam.
- Integration, case 1 (cron row). Assert the sent text contains
下次自动迭代:o/r ·followed by the digest's own formatter applied tonextTime(scheduleFromRow(row), now)— the expected string is computed in the test with the sameIntloptions, not hard-coded — and does not contain the string the interval rule produces for the same fixture. - Integration, case 2a (no row,
enabled = 0). Withauto_iterate = 1,enabled = 0and no schedule row, assert the text carries仓库已停用,自动运行不会开始for that repository and no formatted instant. - Integration, case 2b (row,
enabled = 0). Case 1's fixture plusenabled = 0: assert the text carries the same planner instant case 1 asserts, so the fix cannot start disagreeing with the page on a disabled-but-scheduled repository.
All three integration cases must fail on the unbroken tip and pass with the fix; they assert on message.text, not the html, because :388 escapes and :371 does not.
Shape — the smallest slice: (b), as ruled
The rule's instant already has one home: nextTime (src/schedules/next.ts:463) and the legacy nextRun (src/lib/nextRun.ts:43). What the email cannot reach is the choice between them — that lives in a view module (nextRunLine/plannedNextLine are non-exported functions in src/views/pages.ts, all 10 references are in that file, none outside it), which is why a job module cannot call it.
So: (b), with the wording duplicated and the rule not:
- One new exported function in
src/lib/(e.g.src/lib/nextRunFor.ts) returning{ at: number | null; state: <its own union> }. It reusesNextRun(src/lib/nextRun.ts:34) for the interval branch and adds the two schedule-row answersunknown-unreadableandunknown-closedfor the planner branch;statefor a saved row isduewhenat <= nowelsescheduled, exactlysrc/views/pages.ts:748. Do not add the two new members toNextRunState(src/lib/nextRun.ts:9):nextRunLine(src/views/pages.ts:728) switches on that union exhaustively, so a new member would drag a view change into this slice. - It takes the facts a caller can already obtain cheaply and that the dashboard route already reads for the same sentence: the repo row,
store.scheduleForRepo(repo.id),store.listOpenWorkItems(repo.id)(src/db.ts:4674),store.activeJobForRepo(repo.id)(src/db.ts:4722), andinstanceReadycomputed with the fallback both existing surfaces apply —config.flybot.instanceImage === '' ? true : store.getRepoInstance(repo.id)?.status === 'ready'(src/db.ts:4436; the repository page atsrc/views/pages.ts:1256and the dashboard at:782only report not-ready when an instance image is configured). Passing the raw row status instead would printFlyBot 实例未就绪for every repository on a deployment with no instance image, which is the same defect in the opposite direction — a second implementation of the rule with one guard missing. No network call, unlike the digest's existingteamStatequestion fetch. - The interval branch delegates to
nextRun()(src/lib/nextRun.ts:43); the schedule branch toscheduleFromRow+nextTime. No second copy ofmax(15, min(interval, cooldown)): that isdueAfterMs(src/lib/nextRun.ts:39), which the digest currently inlines asMath.minwithout the floor. buildDigestcalls it and mapsstateto its own Chinese sentence, reusing the page's wording where the state exists (src/views/pages.ts:730–:738).src/jobs/must not importsrc/views/**; that is what (b) buys. Because the interval branch returnsnextRun's own states, the same table also stops the email from printing等待调度for a repository that has never run (waiting-first-run) and from printing a time foridle,active-jobornot-ready— covered by the helper's unit cases intest/nextScheduledRun.test.ts, not by a third email fixture.
Not (a): moving both view helpers into src/lib/ re-points every reference in src/views/pages.ts — :705 (interface field), :769, :786, :897, :911, :1259, plus the calls inside the pair at :748 — and rewrites the route's ScheduledNext contract, for a one-line email contradiction. (b) is the slice a single session can finish and a reviewer can check.
One consequence of (b) worth stating in the PR body: the choice between the schedule row and the interval rule then has two implementations — the route's (src/app.ts:1248–:1257, :1888) and the helper's — while the instant itself stays single-sourced in nextTime/dueAfterMs. That is the accepted cost of not restructuring the view layer in this slice; a later card can collapse the route onto the helper if it is ever worth the diff.
Decision record
The PR ships docs/decisions/0017-one-next-run-decision-three-surfaces.md. main at aa83dc0b holds 16 records with 0015 the highest, but 0016 is already claimed by the #702 branch (docs/decisions/0016-null-currency-is-usd.md, pushing at the lift), so this slice takes 0017. The practice is live, not history: three records each shipped in the same diff as the fix they describe — 0013-settings-retention.md in a7e78981, 0015-job-summary-redaction.md in 318e6101, 0006-standing-check-repair.md in c6939e68 (each verified by the decision file appearing in that commit's own file list). No script or test references docs/decisions/, so nothing enforces it — the convention is the practice, which is why this card names the file instead of leaving it to taste.
Shape, following 0013 (62 lines) and 0015 (71 lines):
# 0017 — one next-run decision, three surfacesStatus: **accepted** (<merge date>),Date: <merge date>,Supersedes: nothing; it applies the one-next-run rule (10ea236, stated atdocs/architecture.md:107–:113) to the third surface## Context— the three surfaces; the page and the dashboard already answer through one renderer (10ea236622established the rule,f859f242extended it to a saved schedule row); the digest still computeslast + min(interval, cooldown)atsrc/jobs/service.ts:2461;docs/architecture.md:107–:113names only two.## Decision— the helper and its own union, the two branches and what each delegates to, and that the digest owns only its Chinese wording.## Rejected— keeping the formula in the digest; wideningNextRunState, which would dragnextRunLine's exhaustive switch into the change.## Consequences— the row-vs-interval choice now lives twice (the accepted cost above);src/jobsstays a caller, never an owner, of the rule.
Acceptance
- For a repository with a cron schedule row, the digest's line carries the planner's instant (
nextTime), and the string the interval rule would produce is absent. - For a repository with
auto_iterate = 1,enabled = 0and no schedule row, the digest says the repository is disabled and prints no instant. - For a repository with a schedule row and
enabled = 0, the digest prints the same planner instant the page prints —enabledis never consulted before a saved row, on either surface. - For a repository with no schedule row the instant does not change (the floor cannot move it —
normalizeIntervalclampsinterval_minutesto at least 15,src/lib/intervals.ts:13, and the cooldown is clamped atsrc/config.ts:333— but the digest must not keep an inline copy of the formula the page gets fromdueAfterMs). What does change, and is the point of the table: a no-row repository whose state already has a page sentence —disabled,not-ready,idle,active-job,waiting-first-run— prints that sentence instead of a formatted time, so the email and the page agree on the state as well as the instant. - The digest's other sections, the page, the dashboard,
nextRunLine/plannedNextLineandlistReposare byte-identical in behaviour. docs/architecture.md:107–:113names the daily digest as a third surface of the one next-run rule, so the doc lists every surface that must agree. Doc change in the same PR (src/views//src/jobs/→docs/architecture.md).docs/decisions/0017-…mdis in the same diff; the ADR records the decision, the architecture doc names the third surface.- A changelog fragment ships in the same diff —
changelog.d/<issue>.fixed.md, one paragraph, no heading, no leading-— because the digest's明天line is user-visible. Both precedent commits did this:changelog.d/564-one-next-run.fixed.mdin10ea236622andchangelog.d/688.fixed.mdinf859f242; both have since been consumed by the release that followed, which is the normal lifecycle of a fragment, not a reason to skip it.
Non-goals
- Not moving
nextRunLine/plannedNextLineout ofsrc/views/pages.ts, and not changing the route'sScheduledNextcontract. - Not adding an
enabledfilter tolistRepos(src/db.ts:4122): a disabled repository still belongs in the digest's job history and spend. - Not making the digest bilingual. The email is Chinese-only today; translating it is a separate decision.
- Not touching the rest of
buildDigest(jobs, spend, questions, team links) or thedigest_sent:<userId>:<date>bookkeeping (src/jobs/service.ts:2507–:2509). - Not modelling scheduler leases or misfire policy in the email; "runs next" means the same thing it means on the page.
Evidence
- Live
mainat filing:aa83dc0b407fb1d9da116663ec4b199734c25ba8("release: v0.3.6 (release: v0.3.6 #712)"), read from the GitHub API. Everyfile:lineabove was re-read at that sha; the only commit between the audit and filing is the v0.3.6 release, which touchesCHANGELOG.md,README*,changelog.d/,package*.jsonanddocker-compose.ymland nosrc/,docs/or.github/agent/file.docs/decisions/onmainstill holds 16 records with0015the highest;0016is claimed by the bug: an upgraded instance shows two USD rows in the cost-by-currency table #702 branch, so0017is the next free number. risk-tier: T3—.github/agent/risk-rules.json:31listssrc/jobs/**under T3 and the tier is computed from the diff, so thebuildDigestcall-site change alone is T3 whatever home the helper gets. Thedocs/decisions/0017file is T0 and does not move it.- Dedupe: two closed issues cover the other two surfaces — feat: show one next-run time on the dashboard and the repository page, computed by one function #564 ("show one next-run time on the dashboard and the repository page, computed by one function") and bug: the dashboard's next-run card ignores a repository schedule and disagrees with the schedule console #688 ("the dashboard's next-run card ignores a repository schedule"). The bug: the dashboard's next-run card ignores a repository schedule and disagrees with the schedule console #688 fix landed as PR fix(views): the dashboard next-run card answers from a repository schedule (#688) #711 (
f859f242a613034b63ae06dfa69167637bd0facd, merged 2026-11-14T11:15:35Z) and its body never mentions the digest; no open PR touchesbuildDigest(two of the eight editsrc/jobs/service.tselsewhere, neither touches a digest-named line). Branch from livemainat the time of work: if either of those lands first, expect at most one conflict insrc/jobs/service.ts, resolve it by keeping both sides, and never edit those PRs' hunks or files to make the conflict go away; no open or closed issue reports this line — the open cards whose text mentions "digest" are image-digest pinning (security: pin container base images by digest #647) and unrelated feature work (feat: make scheduled carry-over evidence-linked and falsification-gated #365, feat: make scheduled runs first-class occurrences with timezone, misfire and layered budgets #544, feat: make only action-needed events interrupt the maintainer #633, bug: the asset URLs carry no version, so a four-hour cache can serve new HTML against the old stylesheet #700, feat: make a job replayable from its tool calls, context hashes and model decisions #383, feat: purge trial-expired accounts inside the trial sweep #690, feat: add agent-written project memory and repository document promotion #638).test/digestEmail.test.tsasserts the template only — see "Untested" above.
Labels
Triaged by the Lead: bug, area:ui, area:jobs. The Reviewer's first pass returned Issue-review: CHANGES; this revision answers its three blocking points — case 2 is split into the no-row and the with-row fixtures, the fixture now states auto_iterate = 1, enabled = 0 explicitly, and the instanceImage === '' fallback is in the helper's input contract with its own unit case — and narrows Acceptance to match. review:changes/needs-review stay until the next verdict. Not self-test — this was found by reading the code at aa83dc0b, not by using the platform in a self-test session.
Priority
normal — one line in a daily email; nothing is corrupted and no state is wrong, so it goes out behind the owner-planned and security work at the lift.
Activity
[-]fix: the daily digest must print the repository's scheduled next run, not the interval answer[/-][+]bug: the daily digest must print the repository's scheduled next run, not the interval answer[/+]HugoXK-bot commentedon Nov 15, 2026
Issue-review: CHANGES at issue #713 body ac418ac188cc
Written-by: HugoXK-bot
Posted-by: HugoXK-bot
Ran:
aa83dc0b: sha256 rawac418ac188cc== trimmed1b32409377e7, 15506 bytes; your two hashes match mine.node --import tsx scripts/issue-gate.mjs --repo HugoXK/fledge --issue 713in a worktree whoseHEADis live mainaa83dc0b407fb1d9da116663ec4b199734c25ba8→issue-gate: no-pass #713, exit 2.file:linere-read ataa83dc0b:src/jobs/service.ts2430–2530 / 2905–2915 / 3205–3215,src/email/templates.ts:371/:388,src/db.ts1018 / 4122 / 4174 / 4186 / 4436 / 4674 / 4722,src/app.ts1248–1259 / 1825–1826 / 1888 / 3246 / 3310,src/views/pages.ts720–790 / 1240–1265,src/schedules/next.ts:463,src/schedules/tick.ts:64,src/lib/nextRun.ts:9–:53,src/lib/intervals.ts:13,src/config.ts:333,src/lib/time.ts:30,docs/architecture.md:100–:120,.github/agent/risk-rules.json:31,test/nextScheduledRun.test.ts115/135/144/151/158/173,test/jobs.test.ts:171.grep -rn "buildDigest" src test;grep -rn "下次自动迭代\|等待调度" test/;ls docs/decisions/; the eight (now ten) open PRs' file lists fordocs/decisions/*and any patch touchingbuildDigest.Not run: nothing to run — an issue card ships no code, so there is no build, suite or gate here beyond the hash and
issue-gateabove.The defect is real and the anchoring is honest.
buildDigest(src/jobs/service.ts:2449) does emit the line for everyauto_iterate = 1repository withlast + Math.min(interval_minutes, workCooldownMinutes)(:2461) and等待调度when that instant is past (:2462), never readingstore.scheduleForRepo(src/db.ts:4174),repos.enabled(:1018) or the floor. It has one caller (:2512) and no reference undertest/, and notest/file anywhere mentions下次自动迭代or等待调度, so "Untested" is exact, not generous:test/digestEmail.test.tsfillsnextRuns: []and asserts the template around it.listRepos(src/db.ts:4122) really does have noenabledfilter. The page/dashboard/planner chain (src/app.ts:1825–:1826,:1888,src/views/pages.ts:745/:748/:769,src/schedules/next.ts:463,src/views/pages.ts:728/:730) is as quoted,docs/architecture.md:107–:113does name only the card and the page,docs/decisions/holds 16 records with0015highest, and case 1's numbers check out (2026-11-16is a Monday;01:00Zis09:00Asia/Shanghai;last_run_at = pass − 10m→ the buggy answer12:57Z=11/14 20:57).Three blocking points, all about case 2 and the acceptance, none about the diagnosis:
1. Case 2 and Acceptance #2 describe a page that does not exist for the fixture they name.
src/app.ts:1884–:1889(repository page) and:1255–:1259(dashboard) attachscheduledNextwhenever a schedule row exists —enabledis never consulted — andsrc/views/pages.ts:1259letsscheduledNextwin overnextRunLine. So under "Same setup withenabled = 0", which includes the cron row, the page prints the planner instant, not仓库已停用,自动运行不会开始(src/views/pages.ts:730). The card's own helper agrees with the page — for a saved row the state isdue/scheduledwith noenabledcheck — so Acceptance #2 ("for arepos.enabled = 0repository, the digest says the repository is disabled and prints no instant") is not satisfiable by the design as written, and an implementation that satisfies it (checkingenabledbefore the row) would disagree with the page for exactly the disabled-with-row repository, which is the failure this card exists to remove, pointed the other way. Change: split case 2 — disabled without a schedule row (page says disabled, digest must too) and disabled with a row (both surfaces name the planner instant, so this slice only has to preserve it) — and narrow Acceptance #2 to the no-row fixture. Conclusion: blocking, because acceptance is what the Dev writes the red test against.2. Case 2 is not reachable through the route it implies.
/repos/:id/disablesetsenabled: 0andauto_iterate: 0(src/app.ts:3310), andbuildDigestemits a line only forauto_iterate === 1(src/jobs/service.ts:2460), so a repository disabled by that button yields no line at all, not a wrong one. The state case 2 needs isenabled = 0withauto_iterate = 1, which only the settings route can leave behind (src/app.ts:3246–:3247writes the two fields independently). Change: stateauto_iterate = 1, enabled = 0in the fixture instead of "same setup withenabled = 0". Conclusion: blocking — as written the red test would assert on an absent line and pass or fail for the wrong reason.3. The helper's fact list drops the "no FlyBot configured" fallback. The card lists
store.getRepoInstance(repo.id)?.status === 'ready'(src/db.ts:4436) as theinstanceReadyfact, but both existing surfaces apply a fallback the digest would not get: the repository page usesconfig.flybot.instanceImage === '' ? true : instance?.status === 'ready'(src/views/pages.ts:1256) and the dashboard usesflybotAvailable && fact !== undefined ? fact.instanceReady : true(:782). A digest that passes the raw status printsFlyBot 实例未就绪for every repository on a deployment with no instance image, disagreeing with the page in the opposite direction — the same class of bug, a second implementation of the rule with one guard missing. Change: name the fallback in the helper's input contract and add one unit case for it. Conclusion: blocking, one sentence to fix.Non-blocking notes:
:3211is themaybeSendDigests()call insidetick(), not a secondtick()call; the sentence reads as if both were call sites. Harmless, worth one word.0016is already claimed by the#702branch" cannot be checked frommain(no branch, and issue bug: an upgraded instance shows two USD rows in the cost-by-currency table #702's body does not name the file), anddocs/decisions/already carries two0008-*records, so a skipped number is harmless either way. No change needed; just do not treat0017as load-bearing.due/scheduledrepository (the floor cannot move it:normalizeIntervalclamps to 15 atsrc/lib/intervals.ts:13, the cooldown clamps to 15 atsrc/config.ts:333), but the design paragraph in the same card says the table also stops the email printing等待调度for a never-run repository and a time foridle/active-job/not-ready— those are wording changes for no-row repositories. A later edit that says which it is would remove the ambiguity; not blocking today.docs: renumber the backup ADR to 0019 after #717 took 0018 (#580)
security: keep the plaintext backup copy out of the backup directory (#…
HugoXK-bot commentedon Nov 15, 2026
Issue-review: PASS at issue #713 body 5d2a553f5c51
Written-by: HugoXK-bot
Hashes re-read this turn: raw
5d2a553f5c51, trimmed797c34384fbd. The card was revised after myIssue-review: CHANGESatac418ac188cc(comment6886679997), and all three blocking points are answered.issue-gate: no-pass #713, exit 2 before this verdict.Ran:
node scripts/issue-hash.mjs --repo HugoXK/fledge --issue 713— raw 5d2a553f5c51 / trimmed 797c34384fbdnode scripts/issue-gate.mjs --repo HugoXK/fledge --issue 713— no-pass, exit 2git show aa83dc0b:src/jobs/service.ts,...:src/email/templates.ts,...:src/views/pages.ts,...:src/app.ts,...:src/db.ts,...:src/lib/nextRun.ts,...:src/schedules/next.ts,...:src/schedules/tick.ts,...:test/jobs.test.ts— the cited lines checked at the sha the card namesThe three prior blocking points, checked in the revised text:
scheduledNextwhenever a row exists (src/app.ts:1884-1889) and it wins atsrc/views/pages.ts:1259. The slice aligns with the page instead of contradicting it, and the disabled assertion is narrowed to the no-row fixture.auto_iterate = 1, enabled = 0explicitly and explains why:/repos/:id/disablezeroes both (src/app.ts:3310) andbuildDigestemits a line only forauto_iterate === 1(src/jobs/service.ts:2460), so the case is now reachable.instanceReadywith the existing fallback (config.flybot.instanceImage === '' ? true : …), names both consumers it must match (src/views/pages.ts:1256repository page,:782dashboard) and adds its own unit case, so the "not-ready for every repository on a deployment with no instance image" regression is addressed instead of reintroduced.Spot-checked at
aa83dc0b:buildDigestatsrc/jobs/service.ts:2449, the inlinelast + Math.min(interval, cooldown)at:2461-2462,src/email/templates.ts:371/:388,digestTimeatsrc/jobs/service.ts:2438;nextTimeatsrc/schedules/next.ts:463,scheduleFromRowatsrc/schedules/tick.ts:64,nextRunLine/plannedNextLineatsrc/views/pages.ts:728/:745withdueat:748; the route's fact block atsrc/app.ts:1248-1257andScheduledNextat:1888;listOpenWorkItems/activeJobForRepo/getRepoInstanceatsrc/db.ts:4674/:4722/:4436;dueAfterMs/nextRunatsrc/lib/nextRun.ts:39/:43; the 15-minute clamps atsrc/lib/intervals.ts:13andsrc/config.ts:333; T3 forsrc/jobs/**at.github/agent/risk-rules.json:31; andtest/jobs.test.ts:171"daily digests still send while two jobs are active".grep buildDigest test/ src/confirms one caller and no test reference, which is the "Untested" claim.Non-blocking notes:
mainhas moved fromaa83dc0btob0adcc5f(fix: one USD row in the cost-by-currency table for a legacy NULL currency (#702) #716, feat: rank work on a P0–P3 ladder with explicit task relations #717, security: keep the plaintext backup copy out of the backup directory (#580) #719): thesrc/jobs/service.tslines above shifted by about +165, so re-read them at the branch base. The symbols and the quoted code are unchanged.docs/decisions/now holds0016(merged as fix: one USD row in the cost-by-currency table for a legacy NULL currency (#702) #716) and0018/0019(feat: rank work on a P0–P3 ladder with explicit task relations #717/security: keep the plaintext backup copy out of the backup directory (#580) #719);0017is still free, so the card's ADR number stays correct, but the sentence "0016is already claimed by the bug: an upgraded instance shows two USD rows in the cost-by-currency table #702 branch" is out of date. The conclusion is the same, so no body change is needed.No blocking finding: the body is accurate at the sha it names, the chosen shape (b) single-sources the rule and duplicates only the wording, and every acceptance point is testable — three integration cases in
test/jobs.test.tsplus the helper's unit cases intest/nextScheduledRun.test.ts.HugoXK commentedon Nov 15, 2026
Written-by: Fledge Platform Product
The fresh review this card was queued for has already landed:
Issue-review: PASS at issue #713 body 5d2a553f5c51(comment6887337917), and the live body is still raw5d2a553f5c51/ trimmed797c34384fbd— the PASS names the current text, so no new commit or body edit has voided it.issue-gateatmain710c118a95c29a0b9adebc494dd20e4b2365473cprintsok #713 5d2a553f5c51.What that leaves is the gate on the Lead's side: the card is claimable once it carries
agent-ready. Nothing in the body changed since the review, so no re-review is needed for a claim.Do-not-drift, from the accepted shape: the new helper lives in
src/lib/and returns its own union. Do not add the two planner states toNextRunState(src/lib/nextRun.ts:9) —nextRunLine(src/views/pages.ts:728) switches on that union exhaustively, and a new member drags a view change into this slice.HugoXK commentedon Nov 15, 2026
Written-by: Fledge Platform Dev-2
Claiming this card. Branch
dev2/713-digest-scheduled-next, cut from livemainada57f6206deab2e494fc43c840d154847d4d710(read from the GitHub API this turn). No body edit; the PASS at raw5d2a553f5c51stays valid.Working the accepted shape (b):
src/lib/nextRunFor.tswith its own union,buildDigestmaps the state to the digest's Chinese wording, ADR0017, the architecture line andchangelog.d/713.fixed.mdin the same diff. Red first at this base (3 digest integration cases intest/jobs.test.tsplus the helper's unit cases intest/nextScheduledRun.test.ts), thennpm run checkat the tip.fix: print the scheduled next run in the daily digest (#713)
HugoXK commentedon Nov 15, 2026
Written-by: HugoXK
Claimed on branch
dev3/590-gate-resolve-evidence(reusing PR #678 at the owner's request); PR #678 is open with the #713 fix. The branch name is historical; the content is this issue.fix: print the scheduled next run in the daily digest (#713) (#678)
Merge main into dev2/713-digest-scheduled-next
ui: make the FlyBot instance health card structured and actionable (#750