fix: address Copilot review round 1 on the SDK watch

Three findings, all real, all fixed.

1. The analysis job's tool grant was too broad. `Bash(gh api:*)` against a
   token holding `issues: write` would have let an injected instruction
   PATCH, close or relabel any issue in the repo — and this agent reads
   untrusted upstream release notes by design, so the prose constraints
   in the prompt are not a control. `gh api` is gone; the two
   mutating-capable commands are pinned to the matrix issue by NUMBER, so
   a call against any other issue does not match the prefix and is
   denied. `--disallowedTools` now names the file-writing tools and
   `git`/`gh pr`/`gh issue edit`/`gh issue close` explicitly, so a later
   edit that widens the whitelist cannot silently restore them. The
   prompt states the exact command shape the grant permits.

2. Targeting the highest `latest` in a lockstep group was wrong in both
   directions. npm publishes a release one package at a time, so a sweep
   landing mid-publish would have told maintainers to move all four
   packages to a version three of them do not have — and written a
   marker for that version, which would then suppress the real filing
   once the publication completed, so the release would never be tracked
   at all. The target is now the LOWEST `latest` across the group: the
   newest version the whole group has actually reached. That stays
   actionable during a partial publish (on 2.0.0 it files 2.1.0 rather
   than going silent), waits correctly when only the in-flight half is
   ahead, and produces a different marker when the publish completes so
   the real release gets its own issue. The body explains itself when a
   `Latest` in the table sits above the target.

3. A failure after an irreversible issue creation lost the issue. The
   sweep exited before emitting `filed`, so the issue never reached the
   analysis job — and the next night's retry would match its own marker,
   emit `[]`, and leave it permanently unanalyzed with nothing left to
   notice. Each group is now isolated, `filed` is appended the moment an
   issue exists, and the emit happens in a `finally`. The run still fails
   afterwards, loudly, but never at the cost of the record. The analyze
   job's gate changed to match: it consumes `filed` whenever the sweep
   named something, whether or not the sweep job itself went green.

Fixing 3 exposed a second instance of the bug CI caught earlier: the
test helper passed `output: undefined` to suppress the emit, but a
destructuring default fires on `undefined`, so it selected
`process.env.GITHUB_OUTPUT` instead of overriding it. Once the emit moved
into a `finally`, the throwing cases reached it and leaked a real
`filed=[]` into the runner's job-output file. The helper now passes
`null`, which suppresses a default and is falsy. Verified by running the
suite with `GITHUB_OUTPUT` set and confirming no file is created.

Test fixtures that bumped a single package's `latest` were modeling a
partial publication without meaning to, so `groupAt`/`latestAt` now make
"completed lockstep release" and "publication in flight" separate,
named cases. 42 tests in this file, 580 across `scripts/`, green with
the runner's environment set and unset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019JWUZxLDnPgLQzjBrtk1x9
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-06 09:52:54 -04:00
commit 3b3c7e35d1

View file

@ -130,6 +130,7 @@ The board write needs `organization projects: write`, which `GITHUB_TOKEN` canno
- **Two upstreams, two issues.** `client`/`core`/`server`/`server-legacy` ship from `modelcontextprotocol/typescript-sdk` in lockstep and share one issue; `ext-apps` ships from its own repo and gets its own. A fifth `@modelcontextprotocol/*` package added to the root manifest and not added to `SDK_GROUPS` **fails the sweep loudly** rather than going unwatched — that guard is the point, since a hardcoded group table is otherwise a silent blind spot.
- **It compares the INSTALLED version, not the declared range.** The four SDK packages are pinned exactly, so the two agree for them; `ext-apps` is a caret range whose lockfile already resolves higher, and comparing the declared string would file an issue for a bump `npm install` has already taken.
- **The target is the LOWEST `latest` across a group — the version the whole group has reached — not the highest.** npm publishes a lockstep release one package at a time, so a sweep landing mid-publish sees one package ahead of its three siblings. Targeting the highest would name a version three of them do not have *and* write a marker that suppresses the real filing once the publication completes, so the release would never be tracked at all. Taking the minimum keeps the issue actionable and lets the completed release file its own.
- **It never boards, like the monthly sweep** — no `PROJECT_TOKEN` exists in this org — so the issue arrives labeled and milestoned and `/issue-triage` places it.
- **It never closes an issue either.** A further release files its own issue and leaves a **supersession comment** on the older one; closing is a maintainer act, since the card may already have moved. An issue closed for the same target keeps suppressing it, so a maintainer's "not planned" is not re-argued nightly.