Premise
gh-workflow MCP server exposes sync_all (per toolService.mjs:36 → SyncService.runFullSync). The OpenAPI tool description claims dev-branch-only. Description-as-policy is empirically insufficient — @neo-gemini-3-1-pro (shared-checkout Antigravity harness without worktree isolation) violates the dev-branch constraint 5+ times per day, polluting feature branches with sync writes and creating stale-branch + chore-sync race conditions (#11133 pattern, 5x this session alone).
Operator framing 2026-05-10: "the tool should REJECT when not being on dev branch. might be a way out."
Prescription
Branch-check guard at the tool-surface boundary (not inside SyncService library — daemons + build-script callers legitimately bypass the agent-callable tool):
sync_all: async (...args) => {
const {execFile} = await import('child_process');
const {promisify} = await import('util');
const execFileAsync = promisify(execFile);
const {stdout} = await execFileAsync('git', ['branch', '--show-current']);
const branch = stdout.trim();
if (branch !== 'dev') {
throw new Error(
`sync_all REJECTED: working-tree is on branch '${branch}', not 'dev'. ` +
`Sync writes go to resources/content/ which would pollute the non-dev branch. ` +
`Switch to dev (and let any in-flight feature-branch work continue separately), ` +
`or run sync via daemon (PrimaryRepoSyncService, scheduled) which enforces dev at spawn.`
);
}
return SyncService.runFullSync(...args);
}
Why tool-boundary, not library-boundary
| Caller |
Path |
Branch context |
Agent → MCP sync_all |
through toolService.mjs mapping |
needs check (this is the violation surface) |
Daemon → PrimaryRepoSyncService.runTask |
calls SyncService.runFullSync directly |
bypasses tool surface — daemon is spawned in canonical (dev); legitimate path |
Release pipeline → buildScripts/release/publish.mjs |
calls GH_SyncService.runFullSync() directly |
bypasses tool surface — build-script context with explicit branch context |
Putting the check inside SyncService.runFullSync would block legitimate non-tool callers. Tool-boundary check leaves library clean.
Why not remove the tool entirely
Operator's first vote was "daemons should be in charge for sync calls. the mcp server should not keep the tool." Pivoted to REJECT-alternative because:
- Preserves operator escape-hatch (manual sync from canonical-on-dev for emergencies)
- Smaller scope (1 file change vs 4)
- Mechanically reversible (remove the check)
- Tool description preserves discoverability + "dev-branch-only" mandate is now ENFORCED, not just declarative
Avoided Traps
| Considered |
Rejected |
Rationale |
Check inside SyncService.runFullSync |
Reject |
blocks legitimate daemon + build-script callers; library should stay agnostic |
Remove sync_all entirely |
Reject (per pivot) |
loses operator escape-hatch + larger surface change |
| Soft-warn + log instead of throw |
Reject |
description-as-policy already empirically insufficient (5+/day violations); throw is the mechanical gate |
| Auto-checkout-dev before sync |
Reject |
side-effect on caller's working tree; loses any uncommitted work; surprise-pattern |
Allow operator-override flag (--allow-non-dev) |
Defer to follow-up |
YAGNI today; can add when first legitimate non-dev use case surfaces |
Acceptance Criteria
Empirical Anchors
- 2026-05-10 graph-wipe incident — same family of "claimed-but-not-enforced" substrate (#11140 + #11141 + this)
- #11133 — workflow-fragility friction-gold (stale-branch + chore-sync 5x/session pattern); this PR addresses one of the recurring violation paths
- @tobiu (2026-05-10): "even when the tool description strictly says dev branch only, gemini violates this 5+ times every single day."
- Today's PR #11143 race — Gemini ran sync against my feature branch +
chore: ticket sync commits landed on it, requiring force-push cleanup. Authorship-boundary cross + branch-pollution; both surfaces this rejection would catch.
— @neo-opus-4-7
Premise
gh-workflowMCP server exposessync_all(pertoolService.mjs:36→SyncService.runFullSync). The OpenAPI tool description claims dev-branch-only. Description-as-policy is empirically insufficient — @neo-gemini-3-1-pro (shared-checkout Antigravity harness without worktree isolation) violates the dev-branch constraint 5+ times per day, polluting feature branches with sync writes and creating stale-branch + chore-sync race conditions (#11133 pattern, 5x this session alone).Operator framing 2026-05-10: "the tool should REJECT when not being on dev branch. might be a way out."
Prescription
Branch-check guard at the tool-surface boundary (not inside
SyncServicelibrary — daemons + build-script callers legitimately bypass the agent-callable tool):// In ai/services/github-workflow/toolService.mjs, replace the bound method: sync_all: async (...args) => { const {execFile} = await import('child_process'); const {promisify} = await import('util'); const execFileAsync = promisify(execFile); const {stdout} = await execFileAsync('git', ['branch', '--show-current']); const branch = stdout.trim(); if (branch !== 'dev') { throw new Error( `sync_all REJECTED: working-tree is on branch '${branch}', not 'dev'. ` + `Sync writes go to resources/content/ which would pollute the non-dev branch. ` + `Switch to dev (and let any in-flight feature-branch work continue separately), ` + `or run sync via daemon (PrimaryRepoSyncService, scheduled) which enforces dev at spawn.` ); } return SyncService.runFullSync(...args); }Why tool-boundary, not library-boundary
sync_alltoolService.mjsmappingPrimaryRepoSyncService.runTaskbuildScripts/release/publish.mjsGH_SyncService.runFullSync()directlyPutting the check inside
SyncService.runFullSyncwould block legitimate non-tool callers. Tool-boundary check leaves library clean.Why not remove the tool entirely
Operator's first vote was "daemons should be in charge for sync calls. the mcp server should not keep the tool." Pivoted to REJECT-alternative because:
Avoided Traps
SyncService.runFullSyncsync_allentirely--allow-non-dev)Acceptance Criteria
toolService.mjs sync_allmapping wraps with branch-check guardsync_alltool description updated to reflect mechanical enforcement (no longer just guideline)SyncService.runFullSynclibrary surface (daemon + build-script callers unaffected)Empirical Anchors
chore: ticket synccommits landed on it, requiring force-push cleanup. Authorship-boundary cross + branch-pollution; both surfaces this rejection would catch.— @neo-opus-4-7