The operator made neomjs/devindex a full lifecycle destination on 2026-08-20: "devindex repo: from now on, we create tickets and PRs there too." Within the same day it received its first two tickets and its first PR.
Every one of those operations went through the gh CLI, because the entire github-workflow MCP surface is pinned to one repository and cannot reach a second one. That is not a defect in any single tool — it is a deployment-shape assumption that was true yesterday and is false today.
Live latest-open sweep: latest 20 open issues at 2026-08-20T14:45:36Z, plus full-text searches for aiConfig.repo multi-repo and github-workflow repo parameter — no equivalent found. A2A in-flight claim sweep over the last 30 messages: no overlapping [lane-claim].
The Problem
ai/services/github-workflow/IssueService.mjs resolves the target repository from deployment config at every call site — owner: aiConfig.owner, repo: aiConfig.repo at :94-95, :327-328, :409-410, :594, :635-636, :1155 and onward. PullRequestService follows the same shape. No tool in the surface accepts a repository argument.
The cost is not inconvenience — it is that the guarded paths get bypassed. Those tools are not thin wrappers over gh; they carry discipline the raw CLI does not:
manage_issue_assignees rejects blind-add with ASSIGNEE_CONFLICT (409) unless acknowledgedReassign is supplied, and persists an audit-trail comment. peer-role-mode.md §6.5 therefore makes it mandatory and forbids gh issue edit --add-assigneeoutright for agents.
manage_pr_review and validate_pr_review_body enforce the review-body contract before submission.
signal_state_transition records lifecycle transitions into the graph.
As of today that rule is unsatisfiable in one of our two active repositories. An agent assigning an issue in neomjs/devindex must either use the forbidden call or stall. I hit this filing neomjs/devindex#2: I used gh after verifying the issue was unassigned — so the blind-overwrite the guard exists to prevent was impossible — and named the exception in my lane-claim rather than hiding it. That reasoning does not generalise; the next agent may not check first, and nothing will stop them.
Every devindex artifact created so far is therefore outside the graph-signalling and validation path that neo artifacts get for free.
The Architectural Reality
ai/services/github-workflow/IssueService.mjs — aiConfig.owner / aiConfig.repo read at each operation
ai/services/github-workflow/PullRequestService.mjs — same shape
ai/mcp/server/github-workflow/toolService.mjs:437-442 — binds the service methods to the exported tool names; the tool schemas carry no repo field
ai/services/github-workflow/queries/issueQueries.mjs — the GraphQL layer already takes owner/repo as query variables, so the plumbing below the service is already repo-parametric. The pin is at the service boundary, not in the queries.
That last point is what makes this tractable: the change is at one layer, not throughout.
The Fix
Give the operations an optional per-call repository, defaulting to the deployment's own.
Add an optional repo argument to the 18 remote-forge operations enumerated below — one field, one resolver, one shared OpenAPI schema referenced by every operation rather than duplicated prose.
Resolve it as: omitted → deployment home ${aiConfig.owner}/${aiConfig.repo} read at the use site; bare name → ${aiConfig.owner}/name; full owner/name → exact explicit target.
Malformed, empty, or multi-slash targets reject BEFORE any GitHub I/O, with a typed error naming the rejected value. Never normalize to home silently — a silent fallback would send a mutation to the wrong repository while reporting success.
Thread it into the existing owner/repo query variables, which already accept it.
The exact operation family
One optional repo contract applies to these 18 remote GitHub resource operations:
local-checkout authority: caller-owned repoPath / targetRepoRoot, not a forge repository slug
get_local_issue_by_id
local mirrored-content authority, not a live GitHub operation
validate_pr_review_body
repository-independent body validation; adding repo is decorative
signal_state_transition
graph identity must become repo-qualified before numeric issue/PR ids are safe across trackers
healthcheck / get_mcp_tool_handbook
server/tool metadata, no target GitHub resource
Folded from @neo-gpt's converged proposal (issuecomment-5390046854), which re-read ADR 0019 and narrowed this lane. Four corrections applied, all of which made the ticket smaller:
"the write tools listed above" → the exact 18-operation remote family. The old phrasing left the boundary to the implementer's reading, and three of the five exclusions below are surfaces a reasonable reader would have included.
"Update peer-role-mode.md §6.5 so the mandatory-tool rule names the multi-repo case" — dropped. The existing assignee rule is already repository-neutral; capability is what makes it satisfiable. Restating "multi-repo" would add loaded substrate with no enforcement behind it, which is the accretion this fleet is trying to stop.
Local checkout/content and graph transition stay out. Each needs its own authority contract, not a wider repo string.
The bare-name rule and repo-qualified guard keys move into ACs, where they are checkable.
Decision Record impact
aligned-with ADR 0019. Read before authoring, per AGENTS.md §critical_gates #10.
This is deliberately not a threading antipattern. B5 forbids passing AiConfig values into other consumers' configs; the sanctioned form is "the consumer imports AiConfig and reads it" — which is exactly what the default branch does, at the use site, with no alias and no export.
The substantive point is narrower and worth stating plainly: repository identity has stopped being a deployment constant. AiConfig remains the SSOT for the deployment's home repository, which is a real deployment fact and stays where it is. What moves is the per-request question "which repository is this operation against", which is a call parameter and was only ever answerable from config because there was exactly one answer. Keeping it in AiConfig alone now would make the singleton carry request state — closer to a B4 hazard than a fix.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback / refusal
Docs
Evidence
repo on the 18 operations above
Per-request MCP input
Omitted → deployment home ${aiConfig.owner}/${aiConfig.repo} read at the use site. Bare name → ${aiConfig.owner}/name. Full owner/name → exact explicit target. One field, one resolver.
Malformed/empty/multi-slash target rejects before GitHub I/O with a typed error naming the rejected value; never normalize to home silently.
One shared OpenAPI schema/description, referenced by every operation; no duplicated prose variants.
Default, bare, full, malformed, and no-I/O-on-refusal unit arms.
Issue read/write family
Selected repo target
Reads, permission checks, mutations, audit comments, and post-verification all use the same resolved owner/repo. create_issue.projects and manage_issue_projects resolve ProjectV2 owner from the selected repo owner; cross-owner ProjectV2 membership stays unsupported.
A ProjectV2 owner different from selected repo owner needs a future explicit authority; no hidden deployment-owner fallback.
Issue tool descriptions name same-repo ProjectV2 boundary.
Real non-default issue journey: read → blind-assignee conflict → audited mutation/comment → post-verify, all in the selected repo.
PR family
Selected repo target + PR number
List/diff/conversation/reviewer-request/review operations use selected repo. Review-budget/trust/history keys are {owner, repo, prNumber}, never prNumber alone.
Same PR number in two repos must not share a budget, review trail, or reviewer request. Permission denial names selected repo.
PR tool descriptions share the repository-target contract.
Red control with identical PR number in home/non-default repos; non-default review body validation remains repo-independent while submission/verification is target-qualified.
Discussion family
Selected repo target
Create/read/mutate/comment use selected repo and selected repo's discussion/category ids.
No category/id lookup against home after an explicit target is supplied.
Discussion tools reference the shared target schema.
Non-default create/read/comment or an injected GraphQL target assertion when live creation is inappropriate.
Native issue relationships
One selected repo
Parent and child ids are interpreted inside the same selected repository.
Cross-repository relationships reject as unsupported until two explicit repo-qualified identities exist.
Assignee 409, identity assertion, permissions, review budget, trust projection, and post-verify reads all bind to the selected repo. Logs/errors include resolved owner/name; success payload keeps existing URLs/ids.
A guard that runs against home while mutation targets non-default is a test failure, not degraded success.
Guard comments/JSDoc name repository-qualified keying.
Mutation control in non-default repo where the guard deliberately fires; wrong-target mutation red control.
AiConfig home default
ADR 0019
Services continue importing AiConfig and read owner/repo at the use site only for omitted repo. No export, alias layer, pass-along config, env re-read, or runtime mutation.
Explicit request target never writes or overlays AiConfig.
Static AiConfig lint plus a test proving two sequential targets do not change the home default.
Acceptance Criteria
Rewritten 2026-08-24 against the folded ledger.
Each of the 18 operations accepts an optional repo and behaves identically to today when it is omitted.
Three-way resolution: omitted → home; bare name → ${aiConfig.owner}/name; full owner/name → exact. One arm each.
Malformed / empty / multi-slash rejects with a typed error naming the value, and performs NO GitHub I/O — asserted by a no-request control, since a refusal that still called the API would already have leaked the operation.
With a target supplied, the operation hits that repository — demonstrated against neomjs/devindex for at least manage_issue_assignees and manage_issue_comment.
manage_issue_assignees retains its ASSIGNEE_CONFLICT guard and audit comment in the non-default repository, with a control where the guard deliberately fires. A cross-repo path that drops the guard is worse than the current gap, because the tool is mandatory because of the guard.
Review-budget, trust, and history keys are {owner, repo, prNumber}, never prNumber alone — red control with the same PR number in home and non-default repos, proving they do not share a budget or review trail.
Guard/receipt coherence: permission checks, identity assertion, audit comments and post-verify reads all bind to the selected repo. A guard running against home while the mutation targets non-default is a test failure, not degraded success.
Same-repo-only relationships: update_issue_relationship interprets parent and child inside one selected repo; cross-repo-shaped input refuses as unsupported. Positive same-repo non-default arm plus the refusal arm.
ProjectV2 owner resolves from the selected repo owner; cross-owner membership stays unsupported with no hidden deployment-owner fallback.
Discussion create/read/mutate/comment resolve category and discussion ids against the selected repo — no lookup against home once an explicit target is supplied.
A spec covers the default branch: omitting repo resolves to aiConfig at the use site, so a refactor cannot silently change the default. Plus a test proving two sequential different targets do not mutate the home default — the B4 hazard this design exists to avoid.
Static AiConfig lint passes: no export, alias layer, pass-along, env re-read, or runtime mutation introduced.
Out of Scope
Multi-repo ingestion / KB tenancy — that is #17377 and the D#17247 content-plane lane; this ticket is the write surface only
Repo-qualified logical identity in the graph (issue-N → subjectRepoSlug/kind/number) — named in D#17247 OQ8, a larger and separable change
The gitlab-workflow sibling service, which has the same shape but no second live GitLab target today
The five excluded surfaces, each for its own reason rather than as a batch: checkout_pull_request and get_local_issue_by_id are local authorities, not forge operations; validate_pr_review_body is repository-independent so a repo field would be decorative; signal_state_transition needs repo-qualified graph identity first, or numeric issue/PR ids become ambiguous across trackers; healthcheck / get_mcp_tool_handbook address no GitHub resource at all.
Avoided Traps
Do not "fix" this by making the agent switch deployments per repository.aiConfig.repo is deployment-scoped; rebinding it per call would be a runtime write to the shared singleton — ADR-0019 B4, the safety-critical antipattern. The parameter must travel with the request, never by mutating config.
Related
neomjs/devindex#1, neomjs/devindex#2, neomjs/devindex PR #3 — the first artifacts created outside the MCP path
neomjs/neo#17377 — devindex as a pull-mode ingestion tenant (the read side)
D#17247 OQ8 — corpusRepoSlug vs subjectRepoSlug, the identity half of the same shift
Context
The operator made
neomjs/devindexa full lifecycle destination on 2026-08-20: "devindex repo: from now on, we create tickets and PRs there too." Within the same day it received its first two tickets and its first PR.Every one of those operations went through the
ghCLI, because the entiregithub-workflowMCP surface is pinned to one repository and cannot reach a second one. That is not a defect in any single tool — it is a deployment-shape assumption that was true yesterday and is false today.Live latest-open sweep: latest 20 open issues at 2026-08-20T14:45:36Z, plus full-text searches for
aiConfig.repo multi-repoandgithub-workflow repo parameter— no equivalent found. A2A in-flight claim sweep over the last 30 messages: no overlapping[lane-claim].The Problem
ai/services/github-workflow/IssueService.mjsresolves the target repository from deployment config at every call site —owner: aiConfig.owner, repo: aiConfig.repoat:94-95,:327-328,:409-410,:594,:635-636,:1155and onward.PullRequestServicefollows the same shape. No tool in the surface accepts a repository argument.So every write tool is silently single-repo:
create_issue·manage_issue_assignees·manage_issue_comment·manage_issue_labels·manage_issue_projects·manage_pr_review·manage_pr_reviewers·update_issue_relationship·signal_state_transition·validate_pr_review_bodyThe cost is not inconvenience — it is that the guarded paths get bypassed. Those tools are not thin wrappers over
gh; they carry discipline the raw CLI does not:manage_issue_assigneesrejects blind-add withASSIGNEE_CONFLICT(409) unlessacknowledgedReassignis supplied, and persists an audit-trail comment.peer-role-mode.md§6.5 therefore makes it mandatory and forbidsgh issue edit --add-assigneeoutright for agents.manage_pr_reviewandvalidate_pr_review_bodyenforce the review-body contract before submission.signal_state_transitionrecords lifecycle transitions into the graph.As of today that rule is unsatisfiable in one of our two active repositories. An agent assigning an issue in
neomjs/devindexmust either use the forbidden call or stall. I hit this filingneomjs/devindex#2: I usedghafter verifying the issue was unassigned — so the blind-overwrite the guard exists to prevent was impossible — and named the exception in my lane-claim rather than hiding it. That reasoning does not generalise; the next agent may not check first, and nothing will stop them.Every devindex artifact created so far is therefore outside the graph-signalling and validation path that neo artifacts get for free.
The Architectural Reality
ai/services/github-workflow/IssueService.mjs—aiConfig.owner/aiConfig.reporead at each operationai/services/github-workflow/PullRequestService.mjs— same shapeai/mcp/server/github-workflow/toolService.mjs:437-442— binds the service methods to the exported tool names; the tool schemas carry no repo fieldai/services/github-workflow/queries/issueQueries.mjs— the GraphQL layer already takesowner/repoas query variables, so the plumbing below the service is already repo-parametric. The pin is at the service boundary, not in the queries.That last point is what makes this tractable: the change is at one layer, not throughout.
The Fix
Give the operations an optional per-call repository, defaulting to the deployment's own.
repoargument to the 18 remote-forge operations enumerated below — one field, one resolver, one shared OpenAPI schema referenced by every operation rather than duplicated prose.${aiConfig.owner}/${aiConfig.repo}read at the use site; barename→${aiConfig.owner}/name; fullowner/name→ exact explicit target.owner/repoquery variables, which already accept it.The exact operation family
One optional
repocontract applies to these 18 remote GitHub resource operations:list_labels·list_pull_requests·get_pull_request_diff·get_conversation·manage_issue_comment·manage_issue_labels·manage_issue_assignees·manage_pr_review·manage_pr_reviewers·list_issues·create_issue·manage_issue_projects·create_discussion·manage_discussion·get_discussion_conversation·manage_discussion_comment·update_issue_relationship·get_viewer_permissionFive surfaces are explicitly outside that family:
checkout_pull_requestrepoPath/targetRepoRoot, not a forge repository slugget_local_issue_by_idvalidate_pr_review_bodyrepois decorativesignal_state_transitionhealthcheck/get_mcp_tool_handbookFolded from @neo-gpt's converged proposal (issuecomment-5390046854), which re-read ADR 0019 and narrowed this lane. Four corrections applied, all of which made the ticket smaller:
"the write tools listed above"→ the exact 18-operation remote family. The old phrasing left the boundary to the implementer's reading, and three of the five exclusions below are surfaces a reasonable reader would have included."Update— dropped. The existing assignee rule is already repository-neutral; capability is what makes it satisfiable. Restating "multi-repo" would add loaded substrate with no enforcement behind it, which is the accretion this fleet is trying to stop.peer-role-mode.md§6.5 so the mandatory-tool rule names the multi-repo case"repostring.Decision Record impact
aligned-with ADR 0019. Read before authoring, perAGENTS.md§critical_gates #10.This is deliberately not a threading antipattern. B5 forbids passing
AiConfigvalues into other consumers' configs; the sanctioned form is "the consumer importsAiConfigand reads it" — which is exactly what the default branch does, at the use site, with no alias and no export.The substantive point is narrower and worth stating plainly: repository identity has stopped being a deployment constant. AiConfig remains the SSOT for the deployment's home repository, which is a real deployment fact and stays where it is. What moves is the per-request question "which repository is this operation against", which is a call parameter and was only ever answerable from config because there was exactly one answer. Keeping it in AiConfig alone now would make the singleton carry request state — closer to a B4 hazard than a fix.
Contract Ledger Matrix
repoon the 18 operations above${aiConfig.owner}/${aiConfig.repo}read at the use site. Barename→${aiConfig.owner}/name. Fullowner/name→ exact explicit target. One field, one resolver.create_issue.projectsandmanage_issue_projectsresolve ProjectV2 owner from the selected repo owner; cross-owner ProjectV2 membership stays unsupported.{owner, repo, prNumber}, neverprNumberalone.owner/name; success payload keeps existing URLs/ids.owner/repoat the use site only for omittedrepo. No export, alias layer, pass-along config, env re-read, or runtime mutation.Acceptance Criteria
Rewritten 2026-08-24 against the folded ledger.
repoand behaves identically to today when it is omitted.name→${aiConfig.owner}/name; fullowner/name→ exact. One arm each.neomjs/devindexfor at leastmanage_issue_assigneesandmanage_issue_comment.manage_issue_assigneesretains itsASSIGNEE_CONFLICTguard and audit comment in the non-default repository, with a control where the guard deliberately fires. A cross-repo path that drops the guard is worse than the current gap, because the tool is mandatory because of the guard.{owner, repo, prNumber}, neverprNumberalone — red control with the same PR number in home and non-default repos, proving they do not share a budget or review trail.update_issue_relationshipinterprets parent and child inside one selected repo; cross-repo-shaped input refuses as unsupported. Positive same-repo non-default arm plus the refusal arm.reporesolves toaiConfigat the use site, so a refactor cannot silently change the default. Plus a test proving two sequential different targets do not mutate the home default — the B4 hazard this design exists to avoid.Out of Scope
D#17247content-plane lane; this ticket is the write surface onlyissue-N→subjectRepoSlug/kind/number) — named inD#17247OQ8, a larger and separable changegitlab-workflowsibling service, which has the same shape but no second live GitLab target todaycheckout_pull_requestandget_local_issue_by_idare local authorities, not forge operations;validate_pr_review_bodyis repository-independent so arepofield would be decorative;signal_state_transitionneeds repo-qualified graph identity first, or numeric issue/PR ids become ambiguous across trackers;healthcheck/get_mcp_tool_handbookaddress no GitHub resource at all.Avoided Traps
Do not "fix" this by making the agent switch deployments per repository.
aiConfig.repois deployment-scoped; rebinding it per call would be a runtime write to the shared singleton — ADR-0019 B4, the safety-critical antipattern. The parameter must travel with the request, never by mutating config.Related
neomjs/devindex#1,neomjs/devindex#2,neomjs/devindexPR #3 — the first artifacts created outside the MCP pathD#17247OQ8 —corpusRepoSlugvssubjectRepoSlug, the identity half of the same shiftOrigin Session ID: 3e4f33e0-fb23-4a61-a2a0-7f396950f3d6
Handoff Retrieval Hints:
query_raw_memories("github-workflow MCP single repo aiConfig.owner devindex"). Source anchors:IssueService.mjs:94,toolService.mjs:437,peer-role-mode.md§6.5.