Repository navigation
Breaking changes — read before upgrading from 2.x
- The GitHub token needs
contents: write— the agent registry is nowregistry.jsonon an orphanagent-registrybranch (sha compare-and-swap), so no branch ruleset may block that branch and the repository needs at least one commit. An existing "Agent Registry" issue is imported automatically on first use. get_activityoutput —heartbeatHistoryis removed; the response carriesheartbeatSourceandcurrentTask.progress/currentTask.branch.manage_automation— the rule actions (create_rule,update_rule,enable_rule,disable_rule,get_rule,list_rules,delete_rule) andai_analyze/schedule_triagingare gone: GitHub has no such API. Uselist_workflows,get_workflowanddelete_workflow.- Heartbeats fail loudly — a heartbeat that could not be written is now an error instead of a success; the first heartbeat or checkout on an older project adds the new
agent_*fields itself (the token needs project admin, or runagent_manage setup_fieldsonce). - Field types and view layouts —
create_fieldaccepts only the types GitHub can create (text, number, date, single_select, multi_select, iteration) and views only board, table and roadmap. - Node.js 22.12 or newer is required.
Added
- Project roles and views as claiming pools — the platform could neither read nor set who may see a board, and it scanned the whole board client-side on every claim.
manage_project/set_collaboratorsnow sets the role of a user or a team on a project through GitHub's nativeupdateProjectV2Collaboratorsmutation, with the four roles of itsProjectV2Rolesenum (ADMIN,WRITER,READER,NONE), andmanage_project/list_viewsnow answers with the configuration GitHub stores for a view: the server-sidefilterit selects its items with, the fields it groups and sorts by, and the fields it shows. A named view is therefore a pool the server selects from, instead of a filter the platform re-implements on every claim. GitHub has no read path for collaborator roles —ProjectV2has nocollaboratorsfield — so the roles that come back are the ones the call granted, and a role it did not touch is reported as unknown rather than guessed. - Native issue dependencies, issue types and reverse membership — the platform emulated "issue A is blocked by issue B" with a
blocked-by:#Nlabel plus body parsing, the issue kind with atype:label, and never asked which boards a task was on. GitHub has all three:Issue.blockedBy/Issue.blockingedges (written withaddBlockedBy/removeBlockedBy),Issue.issueType(written withupdateIssueIssueType, with the organization's type catalogue) andIssue.projectItems(includeArchived: true). They are reachable throughmanage_issues/list_dependencies,add_blocker,remove_blockerandlist_issue_types; the label emulation is still honoured, so a board wired up before the native edge keeps working. In the claim path the native edge decides whether a task is blocked, and a task that is already claimed on another board is no longer handed out a second time. - Product-level coverage of the agent lifecycle — the live-API suite now drives the whole loop the platform promises: register → checkout → context → heartbeat → work product → submit for review → a reviewer claims it from the review queue → approve, plus the rejection path, crash recovery through stale reclaim, the budget hard stop, the PM flow (
assign_task,smart_assign,get_swarm_status,rebalance_workload,converge_projectwith and without a reviewer,converge_until_done), native blockers withskipBlocked, view-scoped checkout pools, two agents racing for one task, and work-product validation. Each scenario asserts the resulting state in GitHub (issue status, project field values, registry records, comments), not just the tool's answer. The suite reads that state back through new helpers on the existing MCP test client; one full run costs about 4,500 GraphQL points. - Storage-seam unit suites — a shared fake GitHub (
src/__tests__/helpers/FakeGitHub.ts) models refs, commits and file contents with sha compare-and-swap (409 on a stale sha, 422 on an existing ref) and can hold writes back and release them one at a time, which makes registry races deterministic: the exact lost-update interleaving, many concurrent writers, two writers of the same agent, the bootstrap race, contention exhaustion and the one-time legacy migration. The work-product store's pagination and payload validation are pinned as well. - Roadmaps can now be persisted into an existing project — the
ai_plan/generate_roadmapaction acceptsprojectId(plus optionalprojectTitle,projectDescription,sprintDurationWeeks,targetMilestones) and plans from the issues already in that project. WithautoCreate: truethe milestones are created and assigned to those issues. Previously the compound surface could only propose a roadmap; the persisting path was unreachable and ignored itsprojectId. MCP_TOOL_GROUPSis documented correctly — the variable filters by group name (core,ai,agents,events,system), not by tool name. The old.env.examplecomment listed tool names, which would have exposed nothing.- Multi-select project fields —
multi_selectis one of the field types GitHub can create, so it is now offered: the field is created with its options (multiSelectOptions), a later update keeps the options that already exist (an option sent with its id keeps its identity, and the item values pointing at it), and a value can be set on an item (multiSelectOptionIds) and read back. - Tiered live E2E —
pnpm run test:e2e:smokeruns the product spine (register → checkout → heartbeat → submit → reviewer checkout from the review queue → approve → issue closed and claim cleared → deregister) in about 80 s for about 200–250 GraphQL points;pnpm run test:e2e:fullruns every scenario for about 4,500 of the 5,000 points per hour.E2E_TIER=smoke|fullselects the tier and the e2e runner accepts--tier.
Changed
- Heartbeats live on the claimed project item, not in the registry — every heartbeat used to be a commit on the
agent-registrybranch. A heartbeat now writesagent_heartbeat_at(ISO text),agent_progressandagent_work_branchon the task's project item, and the registry is written only when the agent's status or task changes, or when an idle agent's last beat is older than the 30-minute stale threshold. Reclaim, metrics and swarm status take the newer of the item beat and the registry beat. A project created before this change needs no manual step: the firstcheckout_taskor heartbeat per server process creates any missingagent_*field additively (concurrent first calls share one provisioning; a field of the wrong type is a hard error; a token that cannot create fields gets a message to runagent_manage setup_fieldswith a project-admin token). Breaking:get_activityno longer returnsheartbeatHistory; it returnsheartbeatSource(project_itemorregistry) andcurrentTask.progress/currentTask.branch.progressSummary,estimatedCompletionMinutesandblockerDescriptiontravel only on the heartbeat event. The heartbeat tool now fails loudly instead of reporting success when the write did not happen. - A failed token debit can no longer bypass a hard stop — the per-call debit after a tool ran was best-effort and only logged a warning. It is now retried once; if it still fails, the tool result carries
_meta.budgetDebitFailed, the failure is logged as an error, and the debit is parked in an in-memory ledger that the agent's nextcheckout_tasksettles first — the checkout is refused while it cannot be settled. - Lint and typecheck are hard gates —
pnpm run lintrunsbiome check --error-on-warnings, andnoExplicitAny/noNonNullAssertionare errors. The 814 warnings that had accumulated were fixed with real types (no suppressions, noas any), which surfaced the defects listed under Fixed.
Fixed
- A registered agent could vanish from the registry — the registry was a JSON array in an issue body, rewritten on every register, checkout, heartbeat, review, reclaim, budget change and metered tool call. GitHub issue writes have no compare-and-swap, so a write-then-verify loop could not see a writer that landed after its check: agent A registered, got success, and was dropped when a writer holding an older copy landed. With two server processes registering ten agents each, five of seven reported registrations were missing afterwards. The registry is now
registry.jsonon an orphanagent-registrybranch, written through the Contents API with the sha that was read: a stale write is rejected with 409, the mutation is re-applied to the fresh list and retried with jittered backoff, and sustained contention raises an error instead of warning and continuing. The same two-process run now keeps 20 of 20. On first use an existing "Agent Registry" issue — including the duplicate registries an intermittently empty RESTlabelsfilter used to create — is imported once (the record written last wins; a corrupt canonical body fails closed) and rewritten to a pointer. The token now needscontents: write, no branch ruleset may blockagent-registry, and the repository needs at least one commit. - The tool schemas advertised field types and view layouts GitHub does not have —
create_fieldofferedmilestone,assigneesandlabels, andcreate_view/update_viewoffered atimelinelayout. GitHub'sProjectV2CustomFieldTypeknows only TEXT, NUMBER, DATE, SINGLE_SELECT, MULTI_SELECT and ITERATION, andProjectV2ViewLayoutonly BOARD, TABLE and ROADMAP, so every such call ended in a GraphQL schema error. The public schemas now advertise exactly the creatable types and the three layouts, and a value that bypassed the schema (a stored or hand-built argument) is refused with a message that names the supported set instead of being silently turned into a text field. - The review queue was unreachable —
submit_for_reviewleftagent_claimed_byset, and the review queue only hands out unclaimed items, so a reviewer could never pick up submitted work and the approve/reject path was dead. Submitting now clears the claim and moves the item toagent_status=review; approving or rejecting releases every agent that still holds the task (the submitter and the reviewer). - Claims could land in the wrong project — claim, status and candidate selection used the issue's first project item, so an issue in several projects could be claimed or updated in the wrong one. The item matching the requested project is now used everywhere, and a PM assignment for an issue that is not in the requested project is refused instead of claimed.
- A corrupted agent registry was reported as "no agents" — a malformed registry silently looked like an empty one, so the platform forgot every agent. Malformed JSON and invalid agent records now fail loudly, naming where the registry lives.
- Work products were read partially and unvalidated — only the first 100 comments of an issue were read and each payload was cast without checking, so malformed work products were returned as valid. Comments are now read across pages and every payload is validated against the work-product schema.
- Project field inspection failures were swallowed — a failed field query looked like "this project has no fields", so duplicate or wrongly-typed fields were created, and a field with the wrong type was accepted. Inspection failures now propagate, an incompatible field type is a hard error with an actionable message, and a single-select field that is missing a status option gets the missing option added (a project created by an older version keeps working; options are never deleted).
- The compound agent tools accepted under-specified calls — every argument of
agent_work/agent_managewas optional, so clients discovered nothing and got runtime errors instead of a protocol validation error. The schemas are action-discriminated now, and the advertised schema keeps a flat top-level property list (so clients that only readpropertiesstill see every argument) plus aoneOfvariant per action that carries its required arguments. set_budgetignored the documented argument — the schema and the docs saidwarningThreshold, the executor readwarningFraction, so the warning threshold was silently dropped.warningThresholdis now accepted as an alias.rebalance_workloaddid not rebalance — it only reclaimed stale work although the documentation promised redistribution. It now reclaims stale tasks and hands the freed work to eligible idle agents through the same capability- and budget-aware assignment pathsmart_assignuses.converge_projectbypassed the lifecycle — it closed issues and left a comment, but the claim, the project status field and the owning agent's registry record stayed stale. With areviewerIdthe task is settled through the normal approve/reject path; otherwise the owner is released and the project state is cleared before the fallback comment and close.- Blocker references were partially ignored —
Blocked by #1, #2only blocked on#1. Every reference of every supported phrase is now checked. release_taskhid a failed release — a failed claim clear was swallowed, so the task looked released while it was still claimed. The failure is now reported and the registry claim is kept.- The reference agent loop was fiction —
examples/basic/agent-loop.tsreplayed a static plan with placeholder ids (agent-<id-from-register>,issueNumber: 42) and swallowed every step failure, so it never worked and always exited 0. It is now a runnable worker + reviewer cycle over@modelcontextprotocol/clientstdio againstnode build/index.js: every id is threaded from the previous response, a tool error,success:falseor missing id exits non-zero, an unknown project fails at preflight, an empty queue is a clean exit, and the agents it registered are deregistered in afinallyblock.--projectis required;--create-demo-task,--rejectand--plan(symbolic references, no credentials) are available. ResourceCache.set()left stale index rows — re-setting a key with different tags or namespaces kept the old tag and namespace index entries, sogetByTag/getByNamespacereturned a resource that no longer carried them. The replaced entry is now removed from every index first, and the real cache has direct coverage for the capacity cap, oldest-first eviction, TTL expiry and index consistency.manage_issues/remove_blockerreturned an empty, protocol-invalid result — the edge was removed but the MCP result carried no content block, so clients rejected the call. It now returns{success, issueId, blockingIssueId, message}.agent_work/validate_work_productdroppedworkProduct.agentId— the validated work product now reports the agent that submitted it.- Two writes to the same agent could revert each other — every registry writer read the agent, changed a copy and wrote the whole copy back; on a compare-and-swap retry that stale copy was written again, so a heartbeat racing a token debit (or
set_budget, or reclaim) silently undid the other write, and debits vanished.upsertAgentis replaced bycreateAgent(fails if the id exists) andupdateAgent(id, mutator), whose mutator runs on the freshly read record on every attempt; counters are applied as deltas. Live: 20 heartbeats and 20record_usagecalls fired concurrently from two server processes end with exactly the summed usage. - The typecheck gate never checked a test file —
tsconfig.test.jsoninheritedexclude: ["**/*.test.ts"]fromtsconfig.json, sopnpm run typecheckskipped every test. About 200 type errors had accumulated behind it (stale fixtures, mocks typed against APIs that had changed, aConfidenceScorerconfig key the scorer never read); all are fixed and the gate now coverssrc/**,src/__tests__/**andtests/**. - Generated code examples were thrown away —
TaskContextGenerationServicegenerated code examples and merged them into a field the guidance object never has. They now land incontextualReferences.codeExamples, merged with the references generator's and de-duplicated. - Roadmap generation saw every issue as "Untitled" — it read
titleandtypefrom project items that did not carry them. Items now carry the title the query already fetched, and the type comes fromcontentType. pnpm run docs:apiemitted an empty, wrong OpenAPI document — it walked zod 3 internals (every schema degraded to an empty object under zod 4), listed 134 internal granular tools instead of the 20 public ones, and could not run without GitHub credentials. It now uses the same definitions the server advertises (compound/compound-tool-definitions.ts), emits OpenAPI 3.1 with the hybrid agent schemas, and runs with no configuration.- Latent crashes in response mapping now fail with a clear error — a missing iteration field id, a non-string GitHub error payload (
isRateLimitErrorcalled.includeson it) and a traceability task withoutimplementsFeatures. setup_agent_fieldsfailed on every real project — the field query selected GraphQL types that do not exist (ProjectV2DateField,ProjectV2TrackerField, …), so GitHub rejected it, the code treated the project as having no fields and tried to create them again. A single-select field id (PVTSSF_…) was also passed tofield(name:)as if it were a name. The union's four members are now selected everywhere, through one shared fragment set, and any field node id is resolved to a name first.- Creating a sprint failed against the real GitHub API — GitHub keeps one iteration per start date and drops an iteration that overlaps an existing one or starts in the past; the mutation succeeds, the response simply does not contain the new iteration, and the call failed with
Iteration "…" was not found after creation. A new sprint now starts when the last one ends (and never in the past), existing iterations are re-sent with their ids, and the duration is rounded instead of ceiled (a 14-day sprint no longer becomes 15 days). Found by running the live-API E2E suite, which had never run before. - 9 compound AI actions crashed on a missing argument —
ai_analyze/suggest_labels,detect_duplicates,find_relatedandai_plan/calculate_capacity,prioritize_backlog,assess_risk,suggest_composition,generate_roadmap,generate_visualizationdereferenced an argument the compound schema never required, and died withTypeError: Cannot read properties of undefined (reading 'map'). They now fail with a message that names the parameter (teamMembers is required for calculate_capacity). - The compound schema and the implementation disagreed on field names —
ai_plan/calculate_capacityadvertisedteamMembers[].availabilityPercentwhile the analyzer wanted anavailabilityfraction (so availability was silently dropped), andhistoricalSprints[].namewhere the analyzer wantedsprintName.ai_analyze/find_relatedadvertisedrelationConfigwhile the executor readconfig, so relation settings were ignored. Both are mapped now. createRoadmapInGitHubhonored itsprojectId— milestones are now assigned to the project's own issues (the plan references project items, the milestone is set on the issue), instead of being created on the repository and attached to nothing.createRoadmapInGitHubreports failures — per-item errors were logged and swallowed; they are now returned in afailuresarray, andupdatedIssuescounts the issues that were really updated instead of being hardcoded to0..env.example/.npmignore/.gitignore/.dockerignorematch the repo — stale Jest, ESLint and Prettier entries removed, test sources kept out of the Docker build context,MCP_TOOL_GROUPScorrected.
Removed
- The automation rules were a fiction —
manage_automationofferedcreate_rule,update_rule,enable_rule,disable_rule,get_rule,list_rulesanddelete_ruleover aProjectV2Rulemodel, andai_analyzeofferedschedule_triaging. GitHub's Projects API has no such types and no such mutations: live introspection shows aProjectV2Workflowtype (id, name, number, enabled, project) with exactly one mutation,deleteProjectV2Workflow, and nocreateProjectV2Workflowat all — so every rule call failed with a GraphQL error ("Field 'createProjectV2Rule' doesn't exist on type 'Mutation'").manage_automationnow speaks the API that exists —list_workflows,get_workflow,delete_workflowoverProjectV2Workflow— and the rule model, the rule repository andai_analyze/schedule_triagingare gone (there is no way to schedule a job through the Projects API). The published surface drops from 169 to 164 actions; the action names changed, so this is a breaking change for clients that called the rule actions. - 82 unreachable granular executor registrations —
dispatchToolresolves an executor by the name the MCP client called, and a client can only call one of the 20 compound tools, so an executor registered under a granular name could never run.src/index.tsshrank from 1313 to 740 lines; a test now pins the invariant (every registered executor is a public tool, every public tool has an executor). - The test-only
issueIntelligenceExecutors/roadmapAIExecutorsmaps — dead in production, kept alive only by tautological tests. - Stale artifacts —
bun.lock(bun is not a supported package manager),jest.resolver.cjs(the runner is Vitest),src/__tests__/test-utils.ts.new.
Documentation
- README rewritten — shorter, hook-first, with the depth moved into
docs/(progressive disclosure). docs/and.planning/codebase/refreshed — the stack moved to Vitest 5, Biome 2.5, TypeScript 7, MCP SDK v2 and npm+pnpm; the old snapshots still described Jest, ESLint and Prettier.
📦 npm: npm install mcp-github-project-manager@3.0.0
See CHANGELOG.md for full details.