Skip to content

bug: the daily digest must print the repository's scheduled next run, not the interval answer #713

Closed

Description

@HugoXK

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 (plannedNextLine formats the planner instant through humanTimeShort, 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:57 for a pass at 12:52Z — this evening, instead of Monday (digestTime, src/jobs/service.ts:2438, formats in config.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.ts owns these assertions today: both precedent commits added their next-run cases there — 10ea236622 (the one-next-run rule) and f859f242 (the schedule row) — and at aa83dc0b the 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' } with nextTime's instant for a cron row, unknown-unreadable for a row scheduleFromRow refuses (src/schedules/tick.ts:64), unknown-closed for a window that has ended, and each NextRunState for the interval branch, including disabled, not-ready, idle, active-job, waiting-first-run, plus one case proving not-ready is never reported when config.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.ts gets 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(), read mailer.messages — precedent :171 ("daily digests still send while two jobs are active"), so no new seam.
  1. Integration, case 1 (cron row). Assert the sent text contains 下次自动迭代:o/r · followed by the digest's own formatter applied to nextTime(scheduleFromRow(row), now) — the expected string is computed in the test with the same Intl options, not hard-coded — and does not contain the string the interval rule produces for the same fixture.
  2. Integration, case 2a (no row, enabled = 0). With auto_iterate = 1, enabled = 0 and no schedule row, assert the text carries 仓库已停用,自动运行不会开始 for that repository and no formatted instant.
  3. Integration, case 2b (row, enabled = 0). Case 1's fixture plus enabled = 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 reuses NextRun (src/lib/nextRun.ts:34) for the interval branch and adds the two schedule-row answers unknown-unreadable and unknown-closed for the planner branch; state for a saved row is due when at <= now else scheduled, exactly src/views/pages.ts:748. Do not add the two new members to NextRunState (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), and instanceReady computed with the fallback both existing surfaces apply — config.flybot.instanceImage === '' ? true : store.getRepoInstance(repo.id)?.status === 'ready' (src/db.ts:4436; the repository page at src/views/pages.ts:1256 and the dashboard at :782 only report not-ready when an instance image is configured). Passing the raw row status instead would print FlyBot 实例未就绪 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 existing teamState question fetch.
  • The interval branch delegates to nextRun() (src/lib/nextRun.ts:43); the schedule branch to scheduleFromRow + nextTime. No second copy of max(15, min(interval, cooldown)): that is dueAfterMs (src/lib/nextRun.ts:39), which the digest currently inlines as Math.min without the floor.
  • buildDigest calls it and maps state to its own Chinese sentence, reusing the page's wording where the state exists (src/views/pages.ts:730–:738). src/jobs/ must not import src/views/**; that is what (b) buys. Because the interval branch returns nextRun'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 for idle, active-job or not-ready — covered by the helper's unit cases in test/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 surfaces
  • Status: **accepted** (<merge date>), Date: <merge date>, Supersedes: nothing; it applies the one-next-run rule (10ea236, stated at docs/architecture.md:107–:113) to the third surface
  • ## Context — the three surfaces; the page and the dashboard already answer through one renderer (10ea236622 established the rule, f859f242 extended it to a saved schedule row); the digest still computes last + min(interval, cooldown) at src/jobs/service.ts:2461; docs/architecture.md:107–:113 names 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; widening NextRunState, which would drag nextRunLine's exhaustive switch into the change.
  • ## Consequences — the row-vs-interval choice now lives twice (the accepted cost above); src/jobs stays 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 = 0 and 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 — enabled is 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 — normalizeInterval clamps interval_minutes to at least 15, src/lib/intervals.ts:13, and the cooldown is clamped at src/config.ts:333 — but the digest must not keep an inline copy of the formula the page gets from dueAfterMs). 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/plannedNextLine and listRepos are byte-identical in behaviour.
  • docs/architecture.md:107–:113 names 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-…md is 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.md in 10ea236622 and changelog.d/688.fixed.md in f859f242; 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/plannedNextLine out of src/views/pages.ts, and not changing the route's ScheduledNext contract.
  • Not adding an enabled filter to listRepos (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 the digest_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

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

  1. added
    needs-reviewWaiting for the Fledge Platform Reviewer's issue review
    on Nov 15, 2026
  2. changed the title [-]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[/+] on Nov 15, 2026
  3. added
    bugSomething isn't working
    area:uiViews, CSS, accessibility, i18n
    area:jobsJob lifecycle, queue, callbacks
    on Nov 15, 2026
  4. HugoXK-bot commented on Nov 15, 2026

    @HugoXK-bot
    Collaborator

    Issue-review: CHANGES at issue #713 body ac418ac188cc
    Written-by: HugoXK-bot
    Posted-by: HugoXK-bot

    Ran:

    • live body re-read myself from the API at aa83dc0b: sha256 raw ac418ac188cc == trimmed 1b32409377e7, 15506 bytes; your two hashes match mine.
    • node --import tsx scripts/issue-gate.mjs --repo HugoXK/fledge --issue 713 in a worktree whose HEAD is live main aa83dc0b407fb1d9da116663ec4b199734c25ba8 → issue-gate: no-pass #713, exit 2.
    • every cited file:line re-read at aa83dc0b: src/jobs/service.ts 2430–2530 / 2905–2915 / 3205–3215, src/email/templates.ts:371 / :388, src/db.ts 1018 / 4122 / 4174 / 4186 / 4436 / 4674 / 4722, src/app.ts 1248–1259 / 1825–1826 / 1888 / 3246 / 3310, src/views/pages.ts 720–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.ts 115/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 for docs/decisions/* and any patch touching buildDigest.
      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-gate above.

    The defect is real and the anchoring is honest. buildDigest (src/jobs/service.ts:2449) does emit the line for every auto_iterate = 1 repository with last + Math.min(interval_minutes, workCooldownMinutes) (:2461) and 等待调度 when that instant is past (:2462), never reading store.scheduleForRepo (src/db.ts:4174), repos.enabled (:1018) or the floor. It has one caller (:2512) and no reference under test/, and no test/ file anywhere mentions 下次自动迭代 or 等待调度, so "Untested" is exact, not generous: test/digestEmail.test.ts fills nextRuns: [] and asserts the template around it. listRepos (src/db.ts:4122) really does have no enabled filter. 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–:113 does name only the card and the page, docs/decisions/ holds 16 records with 0015 highest, and case 1's numbers check out (2026-11-16 is a Monday; 01:00Z is 09:00 Asia/Shanghai; last_run_at = pass − 10m → the buggy answer 12: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) attach scheduledNext whenever a schedule row exists — enabled is never consulted — and src/views/pages.ts:1259 lets scheduledNext win over nextRunLine. So under "Same setup with enabled = 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 is due/scheduled with no enabled check — so Acceptance #2 ("for a repos.enabled = 0 repository, 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 (checking enabled before 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/disable sets enabled: 0 and auto_iterate: 0 (src/app.ts:3310), and buildDigest emits a line only for auto_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 is enabled = 0 with auto_iterate = 1, which only the settings route can leave behind (src/app.ts:3246–:3247 writes the two fields independently). Change: state auto_iterate = 1, enabled = 0 in the fixture instead of "same setup with enabled = 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 the instanceReady fact, but both existing surfaces apply a fallback the digest would not get: the repository page uses config.flybot.instanceImage === '' ? true : instance?.status === 'ready' (src/views/pages.ts:1256) and the dashboard uses flybotAvailable && fact !== undefined ? fact.instanceReady : true (:782). A digest that passes the raw status prints FlyBot 实例未就绪 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:

    • :3211 is the maybeSendDigests() call inside tick(), not a second tick() call; the sentence reads as if both were call sites. Harmless, worth one word.
    • The ADR claim "0016 is already claimed by the #702 branch" cannot be checked from main (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), and docs/decisions/ already carries two 0008-* records, so a skipped number is harmless either way. No change needed; just do not treat 0017 as load-bearing.
    • Acceptance #3's "the interval answer does not change" is true for the instant of a due/scheduled repository (the floor cannot move it: normalizeInterval clamps to 15 at src/lib/intervals.ts:13, the cooldown clamps to 15 at src/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 for idle/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.
  5. HugoXK-bot commented on Nov 15, 2026

    @HugoXK-bot
    Collaborator

    Issue-review: PASS at issue #713 body 5d2a553f5c51
    Written-by: HugoXK-bot

    Hashes re-read this turn: raw 5d2a553f5c51, trimmed 797c34384fbd. The card was revised after my Issue-review: CHANGES at ac418ac188cc (comment 6886679997), 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 797c34384fbd
    • node scripts/issue-gate.mjs --repo HugoXK/fledge --issue 713 — no-pass, exit 2
    • git 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 names

    The three prior blocking points, checked in the revised text:

    1. Case 2 is no longer one ambiguous fixture: 2a is disabled-with-no-row, 2b is disabled-with-a-row, and 2b now states that the page prints the planner instant because the route attaches scheduledNext whenever a row exists (src/app.ts:1884-1889) and it wins at src/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.
    2. Case 2a now says to write auto_iterate = 1, enabled = 0 explicitly and explains why: /repos/:id/disable zeroes both (src/app.ts:3310) and buildDigest emits a line only for auto_iterate === 1 (src/jobs/service.ts:2460), so the case is now reachable.
    3. The helper input carries instanceReady with the existing fallback (config.flybot.instanceImage === '' ? true : …), names both consumers it must match (src/views/pages.ts:1256 repository page, :782 dashboard) 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: buildDigest at src/jobs/service.ts:2449, the inline last + Math.min(interval, cooldown) at :2461-2462, src/email/templates.ts:371/:388, digestTime at src/jobs/service.ts:2438; nextTime at src/schedules/next.ts:463, scheduleFromRow at src/schedules/tick.ts:64, nextRunLine/plannedNextLine at src/views/pages.ts:728/:745 with due at :748; the route's fact block at src/app.ts:1248-1257 and ScheduledNext at :1888; listOpenWorkItems/activeJobForRepo/getRepoInstance at src/db.ts:4674/:4722/:4436; dueAfterMs/nextRun at src/lib/nextRun.ts:39/:43; the 15-minute clamps at src/lib/intervals.ts:13 and src/config.ts:333; T3 for src/jobs/** at .github/agent/risk-rules.json:31; and test/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:

    1. The evidence block is honest about its sha, but live main has moved from aa83dc0b to b0adcc5f (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): the src/jobs/service.ts lines above shifted by about +165, so re-read them at the branch base. The symbols and the quoted code are unchanged.
    2. docs/decisions/ now holds 0016 (merged as fix: one USD row in the cost-by-currency table for a legacy NULL currency (#702) #716) and 0018/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); 0017 is still free, so the card's ADR number stays correct, but the sentence "0016 is 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.
    3. bug: a disabled repository with a schedule row still names a next run on the page and the dashboard #723 depends on this card landing first, and this card's disabled-with-row acceptance encodes today's page answer that bug: a disabled repository with a schedule row still names a next run on the page and the dashboard #723 then flips; that ordering is stated in bug: a disabled repository with a schedule row still names a next run on the page and the dashboard #723 and consistent with this text.

    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.ts plus the helper's unit cases in test/nextScheduledRun.test.ts.

  6. added
    review:approvedIssue reviewed and approved by the Fledge Platform Reviewer
    and removed
    review:changesIssue needs changes before it can be claimed
    on Nov 15, 2026
  7. removed
    needs-reviewWaiting for the Fledge Platform Reviewer's issue review
    on Nov 15, 2026
  8. HugoXK commented on Nov 15, 2026

    @HugoXK
    OwnerAuthor

    Written-by: Fledge Platform Product

    The fresh review this card was queued for has already landed: Issue-review: PASS at issue #713 body 5d2a553f5c51 (comment 6887337917), and the live body is still raw 5d2a553f5c51 / trimmed 797c34384fbd — the PASS names the current text, so no new commit or body edit has voided it. issue-gate at main 710c118a95c29a0b9adebc494dd20e4b2365473c prints ok #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 to NextRunState (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.

  9. self-assigned this
    on Nov 15, 2026
  10. HugoXK commented on Nov 15, 2026

    @HugoXK
    OwnerAuthor

    Written-by: Fledge Platform Dev-2

    Claiming this card. Branch dev2/713-digest-scheduled-next, cut from live main ada57f6206deab2e494fc43c840d154847d4d710 (read from the GitHub API this turn). No body edit; the PASS at raw 5d2a553f5c51 stays valid.

    Working the accepted shape (b): src/lib/nextRunFor.ts with its own union, buildDigest maps the state to the digest's Chinese wording, ADR 0017, the architecture line and changelog.d/713.fixed.md in the same diff. Red first at this base (3 digest integration cases in test/jobs.test.ts plus the helper's unit cases in test/nextScheduledRun.test.ts), then npm run check at the tip.

  11. HugoXK commented on Nov 15, 2026

    @HugoXK
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agent-pragent-readyReady for an agent to implementagent-workingarea:jobsJob lifecycle, queue, callbacksarea:uiViews, CSS, accessibility, i18nbugSomething isn't workingreview:approvedIssue reviewed and approved by the Fledge Platform Reviewer

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions