feat(upstream-watch): ledger on an orphan branch, commit-comment notices, on-demand dispatch - #253
Merged
Merged
Conversation
The first live runs on 2026-09-28 showed the comment ledger cannot work: the workflow token cannot comment on the locked #243, the routine reads one page of comments, and GitHub stops comments at 2,500. This design moves the record to refs/upstream-watch/ledger, notifies through an action digest on #243, fires the dispatch routine on demand, and removes the comment-ledger artifacts in the same change (decisions L1-L6).
Revision 2 after adversarial review by Claude and Codex and a live probe (pacphi/upstream-watch-probe-20260928, run 36451224053): the record moves to the orphan branch upstream-watch-ledger, committed only on days with new records; notification is a github-actions[bot] commit comment that mentions the maintainer (probe: reason mention); dispatch fires the routine with the thread in the payload and records each firing; liveness comes from the Actions API instead of heartbeat commits; #243 closes.
Nine tasks: the ledger store, the dispatcher, the notice, the registry's new watch policy with the comment ledger removed, record, ledger and the last-run check, the workflow, the docs (decision 15, ADR-0041 §7), and the gate. The spec now names watchPolicy.repo, the home repository that ledger.repo used to carry.
… dispatch pull requests
…ger branch and notice; the comment ledger is gone
… and renders the notice
…a dry run would fire, times out the trigger
- openPullRequest asks gh for isCrossRepository and takes the first pull
request from this repository: `--head` matches the branch name in any fork.
- dispatch() takes dryRun: where it would fire it adds { id, version, branch }
to wouldFire; the branch checks, firing limits and pull request lookups run
as before and only read.
- A fired record without fields is an error for its id, not a crash: the
dispatch-pr branch lookup is inside its try.
- The trigger call carries AbortSignal.timeout(30 s); a timeout is an error
like any other.
…nch by exit code - parseRecords requires string id, event and date, an object of fields and a UTC recordedAt; anything else is "events.ndjson line N is not a ledger record". - read() asks `git ls-remote --exit-code --heads` first: exit 2 is an empty ledger with no fetch, 0 fetches as before, anything else throws. git's message, which a translated git changes, is no longer matched.
…nce nothing, notice hint finds the run - report's last run counts scheduled runs only (event=schedule): pull request previews and manual dry runs also succeed. No scheduled run found is a warning in the text report. - ledger --recorded-since <time> selects by recordedAt; --since still selects by the event's date. The notice ends with --recorded-since and the run's time, so the hint shows every record of that run. - Ledger commit sentences go through commitSafe: `owner/repo no. n` and `no. n`, because GitHub turns ids in a commit message into references on those threads. The notice keeps its code-span ids. - record --dry-run runs dispatch with dryRun and prints wouldFire (always present; [] when nothing would fire, and on blind output). Text mode adds one "Would fire" line each. - A ledger commit that cannot be built after a firing keeps the fired records in the blind JSON and prints each session link to stderr.
- The preview and Record step summaries add "would fire N" and one line per entry of wouldFire. - The exit-3 comment names a ledger commit that could not be built. - A static test pins that the Verdict step fails on fetchErrors plus dispatchErrors.
…s, wouldFire and the rollout - UPSTREAM-WATCH.md: the last successful scheduled run; `ledger --recorded-since`; commit messages write ids as `owner/repo no. n`; `wouldFire` in record and the job summaries; exit 3 also covers a ledger commit that could not be built; clearing a dispatch stuck after two firings. - Both upstream-status skills: `report`, `check` and `ledger` only read; `record` fires the dispatch routine and builds a ledger commit, so an agent runs it only with --dry-run. - MAINTAINER.md and the glossary (`idle` in the notice row). - Spec and plan: the components, notice hint and Commits section match the code; rollout steps 3 and 4 start with a record=false run that reads wouldFire, and the rehearsal is its first entry (the agentic-qe entry the old step named is retired).
Closed
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.
Summary
The upstream watch's first scheduled run (36432957846) read every thread, then failed to post: the Actions token cannot comment on the locked ledger issue #243. This replaces the issue-comment ledger with a git ledger, commit-comment notices and on-demand dispatch (audit decision 15; ADR-0041 §7 updated).
events.ndjsonon the orphan branchupstream-watch-ledger, committed only on days with new records, built with git plumbing (scripts/upstream-watch/ledger-branch.mjs). The next window starts at the newest commit'sChecked-Attrailer, which holds when a read failed; no fixed lookback and no heartbeat commit.github-actions[bot]comments on that day's ledger commit and mentionswatchPolicy.notify.mention. A throwaway-repository probe showed this reaches the inbox and email (reasonmention). No issue is involved.recordfires the claude.ai routine's API trigger for a released fix whoseupstream/<id>branch does not exist yet, with the thread in the payload; at most two firings, three days apart. Each firing and the resulting draft pull request are recorded (fired,dispatch-pr); pull requests from forks are ignored. The routine no longer runs on a schedule.ledger [--id] [--event] [--since] [--recorded-since];reportshows the last successful scheduled run and warns after 48 hours.record --dry-runlistswouldFirewithout calling the trigger.env. Permissions:contents: write,actions: read,pull-requests: read; noissues.watchPolicy.repo(home repository),ledger: { branch, sentinel },notify: { mention }; the comment-ledger fields are removed (schemaVersionstays 6).commentsubcommand,check --ledger, the ledger-authors rules, theupstream-dispatchlabel and its steps, and their tests.docs/UPSTREAM-WATCH.md, ADR-0041 §7, audit decision 15, the glossary,MAINTAINER.mdand theupstream-statusskill describe only the new design.owner/repo no. n, so no ledger commit adds a "referenced this issue" entry to an upstream thread.Design:
docs/superpowers/specs/2026-09-28-upstream-watch-ledger-branch-design.md. Plan:docs/superpowers/plans/2026-09-28-upstream-watch-ledger-branch.md.After merge (maintainer)
gh workflow run upstream-watch.yml -f record=false -f since=2026-09-21T00:00:00Z, then readwouldFirein the job summary.gh secret set UPSTREAM_DISPATCH_TOKEN, and give the routine its new prompt (indocs/UPSTREAM-WATCH.md) and API trigger. Its schedule stays off (it is paused now).gh workflow run upstream-watch.yml -f since=2026-09-21T00:00:00Z: the ledger branch appears, and a notice arrives if something needs you.main(the job token can push branches).Test plan
node scripts/run-tests.mjs uniton Node 26.4.0: 5638 pass, 0 fail, 6 skipped (Windows-only)tsc -p tsconfig.json,eslint .(0 errors; warnings unchanged vs main),eslint src bin --rule 'complexity: [2, 50]',markdownlint-cli2,lychee --offline,node scripts/build-check.mjsChecked-At, a stale-parent push rejected)previewjob runsrecord --dry-runagainst the real threads🤖 Generated with Claude Code