Skip to content

Contributor mode: test #11 and #14 under a user token, scope allow-lists across owners - #23

Open
theaspirational wants to merge 40 commits into
anima-research:mainfrom
theaspirational:contributor-token-merge-actions
Open

theaspirational wants to merge 40 commits into
anima-research:mainfrom
theaspirational:contributor-token-merge-actions

Conversation

@theaspirational

Copy link
Copy Markdown
Contributor

Why

#11 adds merge, update-branch, Actions and review-read tools, written for App mode. Contributor mode (#6) runs the same server with a user token across several owners. This PR checks #11 and #14 under a user token and closes the two gaps that showed up there. It changes no #11/#14 behaviour.

Stacked: builds on #6, #16, #22, #11 and #14 (all merged here, #11 and #14 as plain merge commits with no local edits beyond keeping both imports in src/github-adapter.ts). Please review only the top five commits (718bcfb..8477506, 6 files, +454/−8). It can be retargeted or split once those land.

What

  • Tests under a user token (test/githubAdapter.test.ts): every new tool (merge_pull_request, update_branch, resolve_thread/unresolve_thread, list_reviews, list_workflow_runs, dispatch_workflow, rerun_workflow_run) through GitHubAdapter/Octokit against a local HTTP server, in both auth modes. Asserts the user-token header on every request, no App/installation endpoint, clean errors on 401/403 and GraphQL denials with no retry, empty-by-default allow-lists, the head-sha guard, and update_branch: write only to a head that is nobody's base #14's refusals. No production change was needed for user-token auth.
  • Scopes and refusals (README): classic tokens need repo (not public_repo) for workflow dispatch and re-runs; changing workflow files can need workflow; update_branch refuses a fork head even when the token owns the fork (update_branch: write only to a head that is nobody's base #14, intended).
  • Allow-list breadth across owners: an unscoped GITHUB_MERGE_BASE_ALLOW / GITHUB_WORKFLOW_ALLOW entry applies to every repository in GITHUB_REPOS. In contributor mode that can include a third party's upstream. README and deploy/github-mcpl.env.example now say so and use owner/name:value; readConfig logs a startup warning (advisory, nothing refused or narrowed) when an unscoped entry is set and the repositories span more than one owner.
  • Dispatched runs have no event in contributor mode (README): there is no webhook and the poller only reads checks on the token user's own PR heads, so poll list_workflow_runs.
  • Server-level test: merge_pull_request and dispatch_workflow through GitHubMcplServer in token mode: refused before the host grant, succeed after featureSets/update, refused after revocation.

Tests

npm test: 832/832 on Node 22.12.0 and 22.23.3 (with the optional event-gate matcher from @animalabs/agent-framework@0.30.0 installed outside the lockfile). All synthetic: no live GitHub calls, no real credentials. Real token scopes and account rights are not tested here.

Co-authored with GPT-6-Astra.

The-Glassy and others added 30 commits October 17, 2026 09:18
Opt-in second auth + ingress mode alongside the GitHub App mode. When
GITHUB_TOKEN_FILE is set (and the App vars are absent) the adapter
authenticates every REST call with that token directly instead of minting
installation tokens; GITHUB_REPOS may then span several owners since writes
are still checked against the allow-list. Both auth configs at once is
refused as ambiguous, and GITHUB_WEBHOOK_PORT is refused in this mode.

Ingress is a new notifications poller (GET /notifications?participating=true,
If-Modified-Since/Last-Modified, X-Poll-Interval honoured) that re-reads each
notified issue/PR for new comments, review comments, submitted reviews and
state changes, normalizing them through the existing normalize.ts pipeline so
they produce the same tags/metadata/channels as webhook or repo-poll events.

Tests cover config validation for both modes, the token auth header, and the
poller's 304 path, event shaping, cross-restart dedup and interval handling.
…s place and fails

Contributor mode keeps its design, a user token with GET /notifications
as its only ingress. This commit changes how the poller reads, and
closes a way the user token let search_issues reach past GITHUB_REPOS.

Listing. Each pass lists all=true&participating=true&since=W, every
page, with If-Modified-Since. W is the listing's Last-Modified less a
second; without that header, it is the newest stamp the listing showed,
less a second. Read and unread threads are listed alike, so the poller
marks nothing read and leaves the user's inbox as it was. The poller
drops notifications from repositories outside GITHUB_REPOS before any
read, since the token reaches every repository its user can read.

Reads. The poller reads each listed thread since its own cursor: its
comments, and for a pull request its review comments and reviews, every
page. The thread's row comes from the issues endpoint and goes through
normalizePolledIssue, as App mode's repository poll does, so a merge
arrives as pull.closed with whoever merged it as the actor. GitHub can
stamp a notification 20 s or more after the activity it reports, so a
thread read for the first time starts five minutes before GitHub's
previous answer to a listing.

Failures. W depends only on what each listing shows, not on any thread's
read, so one failing thread holds no other thread back. A thread whose
read or delivery fails waits on a retry list that keeps its cursor, and
is read first on later passes, for up to five passes in a row. Then it's
dropped, with one thread.read_failed event on its channel that names the
cause and carries chat:mention, so a host wakes the agent for it. A
thread that is gone or out of the token's reach is dropped at once.
GitHub's rate limit counts no pass against a thread and ends the pass's
reads. A thread with more comments and reviews to read at once than one
read takes is read from its latest update on, and its event says its
backlog was skipped.

Search. search_issues drops user:, org: and owner: qualifiers as it
already dropped repo:, including one right after a quote or a bracket,
since GitHub widens a search to each of them. It leaves out any result
on the page from another repository, saying how many. This applies in
App mode too.

Start checks. At start the server checks GITHUB_BOT_LOGIN against GET
/user and each GITHUB_REPOS entry against GitHub's spelling of it, and a
mismatch stops the start with the value to set. readConfig refuses
GITHUB_APP_INSTALLATION_ID beside GITHUB_TOKEN_FILE, and drops a token
file's leading whitespace or byte-order mark.

Docs and tests. The README names the classic token the notifications
endpoints require, its narrowest scopes, and an account the agent alone
posts from. The two tests that expect a world-readable secret file to be
refused now chmod the file, since a 077 umask narrowed the mode they
asked for and made them fail.
…'s edge, tag polled merges, drop the head read and the second recipe

This commit removes two parts, the pull request head read and the
separate token recipe, and fixes what a second internal review of the
whole change found.

Cuts. getPolledThread reads only the issues endpoint. The head sha it
also read cost a request on every read of an open pull request. It
served pull.synchronize, and the sha: line of a polled pull.opened.
GitHub lists a pull request's thread when someone pushes to it, but a
polled event's actor is the pull request's author, so a maintainer's
push to the agent's own branch would have read as the agent's own
anyway. So this mode delivers nothing for a push, and a polled opening
comes without a sha. The agent learns of new commits from what comes
with them, or by reading the pull request. A review takes its sha from
its own commit_id. A reopened pull request now arrives as nothing, as
through a webhook. examples/recipe-contributor-token.json is gone. Its
wake policies were recipe-contributor.json's, field for field, and the
README points at that recipe for both modes.

Reviews. listReviewsSince reads page 1, whose Link header names the last
page, then reads back from the last page until a page starts before
since. The list takes no since and lists reviews in the order they were
submitted, so reading every page from the first counted a pull request's
whole review history against the 2,000-row cap: past 2,000 reviews a
pull request could never be read again, and every read paid for all of
them. Now the cap counts only pages that hold new reviews, and a Link
that names a next page but no last fails the read rather than being
taken for one page.

Cursors. A thread's cursor keeps the keys of the rows its next read takes
again, from a second before its since, and that read drops them itself.
Before, it relied on the dedup ring, which keeps 2,000 keys, so a thread
that sat quiet while other threads filled the ring delivered its last
comment a second time when it moved.

Merges. A polled merge is tagged github:pr=merged beside
github:pr=closed, as a webhook delivery's is, from the issues row's
pull_request.merged_at. App mode's repository poll gets this too.

Smaller fixes. The read-failed event's actor is github-mcpl[server], a
login no GitHub account can have, so the event is never from-self, and
its text no longer points the agent at the read tools, which can fail
the same way. The poll interval is capped at setTimeout's limit, so a
very large GITHUB_POLL_INTERVAL_MS doesn't poll every millisecond.

Docs. The README says what this mode can't see (pushes, review requests,
check runs, an edit to an open pull request, an edit on an older thread
read for the first time), who a polled edit is attributed to, that the
backlog cap is per list, and what the event pipeline's first read of a
thread costs. The contributor recipe says review requests reach it with
the App only.

Tests cover the review read, the cursor's edge, the merge tag, the
event's actor and text, and the interval cap; each fails without its
change.
…'s own pull requests, default GITHUB_BOT_LOGIN to the token's login

- A notification whose reason is review_requested arrives as pull.review_requested, keyed by the
  notification's id and updated_at; one refused waits on its thread's retry entry, and one beside a
  skipped backlog still arrives. Both example recipes let a review request on a bot's pull request
  past github-other-bots.
- Each pass whose reads met no rate limit searches the token user's open pull requests in
  GITHUB_REPOS and looks up the checks on each head, up to 20 a pass in turns, emitting App mode's
  checks.completed once per result. A head read stands while the pull request's updated_at does,
  for ten minutes at most; a rate limit ends the lookups until the next pass.
- GITHUB_BOT_LOGIN is optional in contributor mode: unset, startup takes it from GET /user, and
  stops when GitHub doesn't answer. A set value is still checked against the token.
- README: what a pass costs and sees now, the env table, and that an APPROVE through github.review
  is the user's own approval and counts toward branch protection.
…moves, and fix the reviews cap at a later boundary

Checks: a head whose finished result was delivered is looked up again only
when its pull request's updated_at moves or its head read is ten minutes
old, so a re-run on the same head still arrives. With 20 open pull requests
whose checks have finished, that's, by our reading, about 240 core requests
an hour where it was about 1,320, besides the search each pass. The module header no longer
says App mode does the same per pass, and the README says what a 304 pass
costs.

Reviews: a read past the cap now reads that page and fails only if it
holds a new review, as page 1 already did, so 2,000 new reviews above an
old page 2 are one read.

Also: a review request refused beside a backlog skipped on every pass
stops at the retry limit; three adapter errors name their operation once;
the README says a review request comes once per listed update with that
reason, and covers the 100 most recently updated pull requests; and tests
for a review request across a rate limit, malformed stored review
requests and a failed search.
… or it has none

A failure is reported while other runs still go, and the last commit left
such a head until it moved or ten minutes passed, so a later failure on it
waited. ChecksSummary gains an optional unfinished flag, set when a run is
going or wasn't read, and the poller leaves a head only once its result is
finished. A head with no check runs two minutes after its head was read is
left the same way, so a repository without CI no longer costs a lookup each
pass; the two minutes cover a push whose runs aren't created yet.

The module header and README time the ten minutes from the head read, the
README says the search runs on passes whose thread reads met no rate limit,
and the changelog and events doc name the 100 most recently updated pull
requests. Tests for an unfinished failure, a head with no runs, the turn
moving past heads left alone, a known result on a refreshed head, a mixed
page past the reviews cap, and the operation named once in two more errors.
…he flag that decides it

Text and tests only. The module header, README and events doc name both
ways a head is left (a result with every run finished, or no runs two
minutes after its head read) and that a failure beside running runs is
looked up each pass; NO_CHECKS_SETTLE_MS says it is a choice, not a
measured bound; isCompletedSummary's doc says decided rather than
finished. Tests for summarizeChecks' unfinished flag, a repeated
unfinished failure, a head whose runs still go past two minutes, and the
failed search being made.
…llow-lists

Two new feature sets and one read tool, so an agent can carry a pull request
from review to merge without a human running gh for it:

- github.merge: merge_pull_request (merge/squash/rebase at the head sha the
  caller read; optional delete_branch) and update_branch. Merges only into
  the bases GITHUB_MERGE_BASE_ALLOW names, empty by default, so granting the
  feature set alone merges nothing. Draft, closed, merged and moved-head pull
  requests are refused before GitHub is asked; delete_branch never touches a
  fork's branch or a base branch.
- github.actions: list_workflow_runs, dispatch_workflow, rerun_workflow_run.
  Dispatch and re-run act only on workflow files GITHUB_WORKFLOW_ALLOW names
  (empty by default), and a dispatch runs only on a base branch. Built for
  re-requesting a review from a workflow that skips comments from bots.
- list_reviews (github.read): every submitted review, paged to the end.

app-manifest.json now asks for contents: write: GitHub refuses
resolveReviewThread to an App without it, which is why resolve_thread failed
with "Resource not accessible by integration". README's Security section
lists every permission by the feature set that needs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewest reviews

- update_branch writes a merge commit, so it now takes a merge's fences: an
  open pull request into a GITHUB_MERGE_BASE_ALLOW base, and a head that is
  not itself a base branch. Granting github.merge alone writes nothing.
- delete_branch also asks GitHub: a protected branch, the default branch, or
  the base of another open pull request is kept, so a back-merge
  release -> dev never deletes a release branch left out of the allow-lists.
- The sha guard accepts a 7+ hex-digit prefix in any case (the tools print
  12-character shas) and sends the full head sha; a malformed sha is an
  argument error, not "moved".
- list_reviews reads GraphQL reviews(last: 50) with totalCount, so a long
  history drops its oldest reviews, never the newest verdict.
- Test for the 409 race (no branch deletion after it); the workflow
  allow-list's errors name workflow files, not branches; README and the
  manifest no longer claim no tool writes a commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update_branch writes a merge commit onto a pull request's head branch, and
the module header said it refuses a head that is itself a base branch. It
checked only the allow-lists, so a head GitHub knows as a base but no
allow-list names was still written to: on a `main → dev` pull request with
`dev` the only allowed base, it asked GitHub to merge `dev` into `main`. A
head in a fork was written to as well, though the README says a write
outside GITHUB_REPOS is refused and delete_branch leaves a fork's branch to
its owner.

update_branch now refuses a head whose repository GitHub no longer reports,
or that isn't the pull request's own, before it asks GitHub about the head.
After the allow-list and sha checks, and before the write, it asks
branchFacts about the head, as delete_branch does, and refuses a default
branch, a protected branch, or another open pull request's base. If
branchFacts fails, it refuses and says nothing was written.

Both fences compare owner and name ignoring case, as GitHub does: a pull
request's repository keeps the spelling GITHUB_REPOS gave it, while GitHub
reports the head repository in its own.

deletionBlockedOnGitHub is renamed baseOnGitHub, since both paths ask it.
The module header, the tool's description, the BranchFacts docs, the README
and the unreleased fragment say what update_branch refuses, that only
merging refuses a draft, and that a moved head is refused given the
caller's sha. The README and baseOnGitHub's doc say what GitHub can't tell:
an unprotected branch that isn't the default and that nothing targets reads
as nobody's base.

Tests: update_branch refuses a `main → dev` head GitHub reports as default,
protected or another open pull request's base; refuses a head in a fork, in
another repository of the same owner, or in an unknown one; writes nothing
when branchFacts fails; and asks before it writes. A head repository spelled
in another case is the pull request's own, for update and deletion, and
deletion keeps a head in another repository of the same owner.
FakeGitHubApi.branchFactsError throws while it is set.
Three tests drive a server over a stream pair written as raw lines:
- a request whose JSON string holds U+2028 or U+2029 gets its answer;
- a malformed line is logged, and the next request is answered;
- a stream error ends serve() and is logged.

On main, with @animalabs/mcpl-core 0.3.0 and no 'error' listener on the
connection, each of the three ends the test process with an unheard
'error'.
0.4.0 reads a line only at \n, so U+2028 and U+2029 inside a JSON
string no longer split a frame on Node 24. It also closes a connection
once when one of its streams fails, and reports that as 'error'. Its
acceptTcp no longer leaves listeners on the server, so the server's own
'error' between accepts is the caller's to hear.

- package.json: @animalabs/mcpl-core ^0.4.0, and engines.node >=22.12.0,
  0.4.0's own floor. The lockfile takes 0.4.0.
- serve() listens for its connection's 'error' and logs it. A malformed
  line leaves the connection open; a stream error closes it, so serve()
  returns: over TCP the next host can connect, and under --stdio the
  process shuts down as it does when stdin ends.
- The TCP server gets an 'error' listener once it is listening, which
  logs. A failed accept while one is pending still rejects that accept.
- scripts/e2e/fake-host.mjs recognises a closed connection by
  ConnectionClosedError's name: two of 0.4.0's three texts for it no
  longer say "connection closed". The comment above it names the class.
- README.md and deploy/github-mcpl.env.example give the Node floor as
  22.12.0.
- Two changelog fragments: the Node floor (breaking) and the fix.
On Node 22, readline doesn't end a line at U+2028 or U+2029, so the test
for those frames passes there with @animalabs/mcpl-core 0.3.0 too. CI now
runs on Node 22 and 24, so it can fail where the split happens. The test
now asserts each of the two requests is answered with a result, as the
malformed-line test does, rather than with any answer.
main now carries the faster test suite (anima-research#18), which gives CI's job a
10-minute timeout where this branch adds the Node 22 and 24 matrix. The
job keeps both.
main now runs CI the testing setup's way (anima-research#19, anima-research#20): the whole suite once
at a pinned Node, and an install and start check on engines' floor, Node 24
and the newest Node release. With engines at >=22.12.0 here, the
resolution keeps that shape at this branch's floor:

- The suite job runs at Node 22.12.0, engines' floor, in place of this
  branch's Node 22 and 24 matrix of whole suites.
- The install and start check runs on Node 24 and on the newest release.
  main's 22.0.0 leg checked the old floor; the floor is now 22.12.0, where
  the suite job runs.
- README's development line keeps this branch's "Node 22.12 or newer",
  which covers npm test as well as the server.
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
…nd write safety

Keep repository poll cursors alongside notification watermarks. Preserve possiblyDone and answered error fields, adapt notification error construction, and retain the CI-required wake matcher checks.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
…ations

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
theaspirational and others added 10 commits October 29, 2026 06:35
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Retain both ThreadKind from contributor delivery (anima-research#6/anima-research#22) and SubmittedReviewState from review reads in the sole import conflict.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
…fork heads

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Contributor mode must exercise the real tool/HTTP path, not inherit App-only confidence. Cover token headers, clean permission failures, shared allow-lists, SHA guards, and anima-research#14 base/fork refusals without live GitHub calls.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
Classic-token workflow writes have documented scope requirements beyond the public_repo contributor setup. Explain those limits and the shared safety fences without adding permission escalation or an App-token fallback.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
A user token can reach both a personal fork and third-party upstreams. Preserve unscoped semantics but warn at config startup, recommend scoped examples, and test permission breadth and isolation in both directions.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
User-token mode has no workflow-run webhook ingress. Tell contributors to poll dispatched runs and distinguish the own-PR-head TTL schedule for rerun check results.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>
A user-token adapter alone did not prove GitHubMcplServer routing and host-grant enforcement. Drive merge and dispatch through tools/call with synthetic token transport, including pre-grant and revoked-grant refusals.

Co-Authored-By: GPT-6-Astra <noreply@openai.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: needs a maintainer's decision. The change belongs, but part of it needs a maintainer's call.

Reviewed commit 8477506 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits, with concerns. The PR's own work (user-token tests for the merge and Actions tools, README and env-example notes on scopes, and an advisory startup warning) fits the project. But the diff also carries five unmerged PRs, and #11's merge tool contradicts the base README's "No merge, ever".

5 reasons with sources

Those parts need their own decisions.

  • The top commits only test and document behaviour that #11 and #14 add, plus one startup warning that refuses nothing. The warning at src/types.ts:1630-1642 only logs when an unscoped GITHUB_MERGE_BASE_ALLOW or GITHUB_WORKFLOW_ALLOW entry is set and the repositories span more than one owner. The rest is tests against a local HTTP server and README/env text. Tests and documentation for features the project ships nearly always fit. (src/types.ts (readConfig); test/githubAdapter.test.ts; test/contributor-mode.test.ts)
  • The work sits where the project already keeps such things. Config validation stays in readConfig. Scope guidance goes in the README's contributor-mode section and the env example, and the tests go in the existing test files. (README.md (Configuration, Contributor mode sections); deploy/github-mcpl.env.example)
  • The diff is not one change: it carries #6, #16, #22, #11 and #14, which the description asks reviewers to skip. Besides the five-commit, six-file top layer, the diff includes contributor mode, the notifications poller, tools-merge.ts, tools-actions.ts and changelog fragments for those PRs. The description says it can be retargeted once those land. It should not be merged ahead of them. (README.md (base); PR diff (10,543 lines))
  • The stacked #11 reverses a principle the base README states, so the maintainers need to accept merge tools in #11 itself. The base README says "No merge, ever. Not in v1, not behind a flag." It also says "one process per owner" (line 7), which the stacked contributor mode (#6) relaxes. This PR assumes both decisions are already made. If #11 or #6 is turned down, this PR has no purpose. (README.md (base), lines 21-22)
  • The new startup warning arrives without a changelog fragment, against the project's changelog.d convention. changelog.d/README.md asks for one fragment file per change. The review found that the user-visible warning has none. That gap is small and easy to fix. (changelog.d/README.md)

Findings

No findings of medium or high severity.

5 more findings, less severe
  • Low severity, documentation, src/types.ts:1630-1642. New startup warning has no changelog.d fragment. The top commits add an operator-visible behaviour: readConfig now takes a logger argument and logs a warning for each unscoped GITHUB_MERGE_BASE_ALLOW / GITHUB_WORKFLOW_ALLOW entry when GITHUB_REPOS spans more than one owner. changelog.d/README.md asks for one fragment per change. None of the fragments (contributor-mode.added.md, merge-and-actions.added.md, merge-and-actions.changed.md) mentions the warning or the advice to scope entries as owner/name:value. As a result, the release notes will leave out the one runtime change in this PR.
  • Low severity, documentation, deploy/github-mcpl.env.example:25-36. Env example describes contributor-mode multi-owner use but gives no contributor-mode variables. The new comment says contributor (user-token) mode may list several owners, and that unscoped merge/workflow entries reach a third party's upstream. README line 380 says the launcher reads contributor-mode variables from this same file. The template, however, has no GITHUB_TOKEN_FILE line and leaves GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY_FILE and a [bot] GITHUB_BOT_LOGIN uncommented. An operator who follows it for contributor mode and adds a token file gets readConfig's 'set together with GITHUB_APP_ID' startup error. The file also leaves GITHUB_PR_BASE_ALLOW=dev unscoped, so the template itself does not follow the new owner/name:value advice.
  • Low severity, test coverage, test/contributor-mode.test.ts:268-297. Multi-owner scope-isolation test covers only merge and dispatch, not update_branch or rerun_workflow_run. The test that shows unscoped entries reach both owners, and that scoped entries isolate each repository, calls only merge_pull_request and dispatch_workflow. update_branch also reads GITHUB_MERGE_BASE_ALLOW, and rerun_workflow_run reads GITHUB_WORKFLOW_ALLOW. If either one handled a scoped entry differently across owners, for example by matching the owner case-insensitively or ignoring the scope, nothing would catch it. Both writes need cases in the per-repository loop.
  • Low severity, test coverage, test/githubAdapter.test.ts:3240-3253. 401/403 test for resolve/unresolve_thread fails the membership read, not the mutation. The test overrides entry.route with a 401/403 answer. For resolve_thread and unresolve_thread that route is POST /graphql, and every GraphQL request goes to it, including the thread-membership query that runs before the mutation (the next test is named 'after membership is established'). So the first GraphQL read is the request that fails, and the assertion that exactly one call was made confirms that the mutation never ran. The PR claims clean 401/403 handling for the thread mutations under a user token, but that path is not exercised. The fake should answer queries normally and return the HTTP error only to the mutation.
  • Low severity, test coverage, test/contributor-mode.test.ts:328-341. Subprocess warning test depends on the working directory and an exact stderr line count. The test runs spawnSync(process.execPath, ['--import','tsx',... "import { readConfig } from './src/types.ts'"]) without setting cwd, so it only passes when the test runner is started from the repository root. It also asserts that stderr has exactly two lines. Any other output on stderr, such as a Node or tsx deprecation or experimental warning on a newer runtime (CI also runs 'latest' for the start check), would fail it for reasons unrelated to the warning. Set cwd to the repository root and match only the two warning lines.

For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants