From 42e93fb270a9401522c8e431bfaedb102d40aa0a Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 23 Oct 2026 16:41:29 -0700 Subject: [PATCH] Never auto-finish an ongoing plan Three upkeep areas (Device Picker v2 upkeep, iFixit Pro on the store, Troubleshooting pages) would have been marked done the moment automatic calls went on, because every one of their issues is closed. For upkeep, closed issues mean nothing is asked of it right now, not that it is finished; marked done, the next two PRs would reopen it, and it would flip between done and active. An ongoing plan now parks when it's idle and is never finished on its own; Decide still asks a person. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- frontend-v2/src/model/autoCalls.test.ts | 11 +++++++++++ shared/model/autoCalls.ts | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3f3a9fa..926ec8c1 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,8 @@ pass), it: - finishes a plan: one under way whose issue closed, or all of whose issues closed, with no PRs open, is done, or dropped when they were closed as not planned. One past its hard end with none open, its status untouched - since, is done. + since, is done. Never an ongoing plan: upkeep has no end, so closed + issues only mean nothing is asked of it, and it parks once it's idle. - plans a project: one with 3 or more PRs open or merged in the last 14 days that never had a plan gets one, in progress from its first open PR's week (or its first PR's, when all have merged) with no end, named diff --git a/frontend-v2/src/model/autoCalls.test.ts b/frontend-v2/src/model/autoCalls.test.ts index 6f269135..3ac33b14 100644 --- a/frontend-v2/src/model/autoCalls.test.ts +++ b/frontend-v2/src/model/autoCalls.test.ts @@ -318,6 +318,17 @@ describe('finished', () => { expect(calls([plan], { p1: [] }, { rows: dropped })[0]).toMatchObject({ status: 'dropped' }); }); + it('never finishes an ongoing plan, whose issues closing means nothing is asked of it', () => { + const plan = item(1, { start: '2026-09-07', end_kind: 'ongoing' }); + const issues = new Map([['p1', { total: 3, open: 0, done: 3, dropped: 0, lastClosedAt: ago(2) }]]); + const finishes = (c: ReturnType) => + c.filter(x => x.kind === 'status' && (x.status === 'done' || x.status === 'dropped')); + const rows = rowsFor(plan, [project('p1')], { issues }); + expect(finishes(calls([plan], { p1: [] }, { rows }))).toEqual([]); + const closed = rowsFor(plan, [project('p1')], { closed: closedOn('done', 3) }); + expect(finishes(calls([plan], { p1: [] }, { rows: closed }))).toEqual([]); + }); + it('finishes a plan past its hard end with none open, until someone reopens it', () => { const plan = item(1, { end_kind: 'hard', start: '2026-08-03', weeks: 4, status_at: ago(60) }); const rows = rowsFor(plan, [project('p1')]); diff --git a/shared/model/autoCalls.ts b/shared/model/autoCalls.ts index 923ca88f..6540b7f6 100644 --- a/shared/model/autoCalls.ts +++ b/shared/model/autoCalls.ts @@ -26,7 +26,8 @@ import { isUnderWay, mondayOf, planEnd, type RoadmapItem } from './roadmap'; * - finished: a plan under way whose issue closed, or all of whose issues * closed, with none of its PRs open, is done (dropped when they were * closed as not planned); one past its hard end with none open, its - * status untouched since, is done (model/decide.ts answeredOnItsOwn) + * status untouched since, is done (model/decide.ts answeredOnItsOwn). + * Never an ongoing plan: upkeep has no end, so it parks when idle * - new: a project with DECIDE_MIN_PRS or more PRs open or merged in the * last LIVE_DAYS, that never had a plan (not even one since removed), * gets one in progress, with no end; put down parks it once it's idle. @@ -239,6 +240,9 @@ export function autoCalls({ for (const r of row.reasons) { const item = row.item; if (!item || !isUnderWay(item.status) || !answeredOnItsOwn(r, item, timeZone)) continue; + // upkeep has no end: closed issues mean nothing is asked of it right + // now, not that it's finished; it parks once it's idle instead + if (item.end_kind === 'ongoing') continue; if (r.kind === 'issue_closed') { const as = r.as === 'dropped' ? 'not planned' : 'completed'; setStatus(item, r.as, `Finished: its issue was closed as ${as} on ${r.on}`);