Repository navigation
Projects: make the status calls from PR activity, and let the labeling job read the API - #533
Merged
Merged
Conversation
The labeling job runs with its workflow's installation token, which the API refused, so its reads of roadmap status and project names were skipped in CI. An installation token on the org Pulldasher serves now reads; any write with one is refused before GitHub is asked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server is about to name the plans it makes from a project's slug, so the words for a slug move from the board's portfolio model to shared/model/projectLabel.ts, where both can read them. The board keeps importing it from portfolio.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decide asks people to confirm things that already happened: a plan picked up or put down again, finished by its issues or its end, a new project with no plan. Nobody answers, so plans rot. shared/model/ autoCalls.ts turns those facts into roadmap writes: picked up again by a PR opened or labeled in after the status was set, put down after idleDays with no PR activity, finished when its issues close or its hard end passes with nothing open, a plan for a project with 3 or more PRs, and a lead for a plan with none. Each rule only reads what happened after the last status change, so a person's call stands. decideQueue takes auto, which leaves those reasons out (answeredOnItsOwn) and keeps the judgment calls. issue_closed now says how many PRs are open, which is what decides whether it can be answered on its own. labeledBy tells a person's project label from the job's, counting a script run with a person's token as the job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lib/auto-calls.js reads what Decide reads plus every project's PRs, runs the shared rules, and writes each call as pulldasher. A status change is a call like a person's (updated_by, updated_at, status_at) and only lands while the plan's status is still what was read, so a person's change in between wins. A lead fills in only while empty and leaves updated_at alone, so it doesn't count as an answer to Decide. Why goes in the new roadmap_items.auto_note, with auto_at (migration 0042). It runs when the work sync finishes, which webhooks on project issues and the hourly pass start, before open boards are told to fetch again. projects.autoCalls (default on) turns it off, and then Decide asks those calls again; projects.idleDays sets when a plan is put down. GET /api/v1/decide leaves the answered reasons out and says auto_calls. bin/auto-calls is a dry run of what it would do now, --apply writes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/v1/projects adds, per project: status, its plan's status or null; last_activity, the day its newest PR opened or merged; and examples, up to 5 of its labeled PRs with by: person or job, a person's first, newest first within each. A bot's label is the job's, and so is one a seed script added with a person's token, read from the new projects.machineLabelRuns windows (seeded with the October runs). A label renamed after it was put on loses who added it and when (git-manager getLabelsFromEvents), so it reads as the job's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The board's Decide, its tab count and My work now pass the server's auto_calls flag to decideQueue, so they leave out the same reasons the API does. Decide's guesses list gets a Changed on its own fold: each plan Pulldasher changed in the last 7 days, with why, linked to the plan. The dummy board shows one, a filled lead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Any GitHub App installed on the org passed the installation check, and so would a fork PR's GITHUB_TOKEN. A read key replaces it: config.js apiReadKeys lists SHA-256 digests of keys made for a job, a Bearer value whose digest matches one (compared in constant time) reads, and any write with one is refused before anything else is checked. The labeling job sends one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
projects.autoCalls is now off unless it's true, so a deploy changes no plan by itself. The README says how to turn it on: deploy, run bin/auto-calls to see what it would do, then set it true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server is the one process that makes the roadmap calls, so a script run beside it can't make the same call twice. bin/auto-calls now only says what the server would do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A done or dropped plan is picked up again only after 2 PRs open (or are labeled in while open) since it was marked so: one fix after a finish isn't a return. A parked plan still picks up on 1. Put down also waits on the plan's latest update and on a person's last change, so a plan someone is talking about isn't parked for quiet PRs. A status call carries the status_at it read, and the write only lands while status_at matches too (status_at <=> ?), so a call someone made again since wins. Leads are filled only on plans planned or in progress, and never on a plan whose status moves in the same pass, which also kept a lead's note from replacing the status call's. A new plan starts the week of Decide's since for it, its first open PR, not the first PR it ever had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decide dropped every reason Pulldasher could answer, so with the calls on it stopped asking about work that moved with no new PR, a finished plan whose PRs were still open, reopened work with no plan, and a project whose plan someone removed. Nobody answered those. Now the server works out the calls it makes this pass (callsFrom) and Decide leaves out only the reasons those calls answer (withoutAnswered): a picked up plan's reopened and moving, a finished plan's issue_closed, issues_done and ended, and new where a plan is made. /api/v1/decide and /projects-data send them as pending_calls, kept a minute and forgotten on every project write, and the board's Decide, tab count and My work filter by the same list. decideQueue's auto option is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A person who changed a plan after Pulldasher's call made the plan theirs, so the line goes. A status call leaves updated_by as pulldasher; a lead fill leaves who changed it last alone, so that line stays until anyone changes the plan after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The labeling job re-sorts its own filings at each sweep, so its fresh label is a guess. A PR the job labeled (a bot, or a machineLabelRuns window) counts toward a pick-up or an automatic new plan only once its label is projects.settleDays old (default 3). A person's label, or a link, counts at once. Decide's own new question still counts every PR. Also: - a plan whose project issue closed after it stopped is never picked up; Decide asks about that instead - a done or dropped call answers every reason on its plan - Decide and /projects-data load with no calls when reading them fails, and log it once until a read works - last_activity is the team's day and counts pushes, as put down does - an ended plan's end is the team's day, not UTC's - pick-up reads the plan Decide reads for the project (lastPlan) - examples leave out PRs closed without merging Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the round 3 review: - a PR the labeling job filed wakes a plan only by when it opened, never by when its label went on, so a seed run filing old PRs isn't new work (fixbot#3312, opened Sep 11 and seed-labeled Oct 10, picked up a plan parked Oct 1). A person labeling an open PR in still counts. - a new plan is made for any tracked project with 3 or more settled PRs open or merged in the last 14 days and no plan ever, so a burst that already merged gets one too; put down parks it once it's quiet. Decide's own new question is unchanged. - an empty lead stays empty when a person changed the plan after its project's PRs last moved, since they may have cleared it. - pick-up's closed-issue check reads the plan's last change, as Decide does. - bin/auto-calls --at YYYY-MM-DD runs the rules as of that day, and the README's steps to turn the calls on run it for today and 28 days ahead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
🤖
Projects are meant to show what people are actually working on, without anyone keeping them up. Today they rot: the "PHPStan and static analysis" plan stayed active with no lead two months after PHPStan was removed (iFixit/ifixit#64290), because the call to close it waited in Decide for a person who never came. And the labeling job can't read this API in CI at all, since it only has an app token, so it never sees a plan's status or name.
Companion PRs: iFixit/ci-actions#25 (the job) and iFixit/ifixit#65831 (the skill and the job's caller).
Changes
apiReadKeysholds SHA-256 digests; a Bearer value that matches one can GET and never write. App tokens stay refused: any app on the org, or a fork PR's token, would have passed an org check.projects.autoCallsis turned on: a parked plan with a new PR goes active (a done or dropped one after two), an active plan idle for 28 days is parked, a plan whose issues all closed (or whose hard end passed) with nothing open is done, a project with 3+ PRs open or merged in the last 14 days and no plan ever gets one, and a planned or active plan with no lead gets one unless a person changed it since its last PR activity. A PR the labeling job filed counts only once its label isprojects.settleDays(3) old, and by when it opened, never by when a seed script labeled it; a plan whose issue closed after its last change isn't picked up. Each is recorded aspulldasherwith its reason inauto_note. Decide drops the questions a call answered (all of them for a plan it finishes), and lists the last week's calls under "Changed on its own"./api/v1/projectstells the job more:status,last_activity, andexamples(titles a person filed first). Labels from the seed scripts run with a person's token count as the job's (projects.machineLabelRuns).Warning
Apply migration 0042 before deploying. Every roadmap read selects
auto_note, so the roadmap, Decide and the projects API all fail without it. Automatic calls ship off: runbin/auto-calls(dry run only) on prod for today and with--at28 days ahead, read both, then setprojects.autoCalls: true. The first sync after that makes every pending call at once.QA
bin/auto-callson dev lists the calls it would make; spot-check five against the board.apiReadKeys,curl -H "Authorization: Bearer $KEY" .../api/v1/projectsreturns 200 withexamples.PATCH /api/v1/roadmap/:idgets 403.Decisions worth a look
auto_note. Reverting is editing the plan.reopened.loadProjectPullsreads every labeled PR on each sync (markedponytail:).🤖 Generated with Claude Code