Skip to main content

Umbrella Sync Routing

For: Teams using umbrella (multi-repo) SpecWeave projects Version: 1.0.366+


What Is Umbrella Sync Routing?

When you run SpecWeave as an umbrella project (multiple repos under one workspace), sync routing determines which repository receives GitHub issues, JIRA tickets, and ADO work items for each increment.

Without umbrella mode, there's one target per platform — all issues go to the same repo/project. With umbrella mode, each child repo can have its own sync targets, and SpecWeave routes tickets to the right place based on the **Project**: field in your spec.


Quick Summary

ModeConfigBehavior
Single repoNo umbrella sectionAll tickets → global sync config
Umbrellaumbrella.enabled: true**Project**: field controls where tickets go

The **Project**: field is the single source of truth for routing. When it matches a child repo, tickets go to that repo's sync targets. When it matches the umbrella project name, tickets go to the umbrella's own targets. No match → global fallback.


How It Works

The Decision Flow

When SpecWeave syncs an increment to external tools, it reads the **Project**: field from the spec's user stories and resolves the sync target:

resolveSyncTarget(projectName, config)

├─ umbrella.enabled = false (or absent)?
│ └─ GLOBAL targets (single-repo mode)

│ (umbrella mode)

├─ projectName matches umbrella.projectName?
│ └─ umbrella.sync targets (umbrella-scoped work)

├─ projectName matches a childRepo name or id?
│ └─ childRepo.sync targets (per-repo routing)

├─ projectName prefix matches a childRepo? (e.g., US-VSK-001 → prefix VSK)
│ └─ childRepo.sync targets (prefix-based fallback)

└─ no match
└─ GLOBAL targets (fallback)

Where the Project Name Comes From

SpecWeave reads the project name from your spec.md in this order:

  1. YAML frontmatter: project: my-app (newer format)
  2. Markdown body: **Project**: my-app (legacy format, still fully supported)
  3. User story field: The **Project**: line inside each ### US-NNN section

Configuration

Example

{
"sync": {
"github": { "owner": "acme", "repo": "acme-platform" },
"jira": { "projectKey": "PLAT" }
},
"umbrella": {
"enabled": true,
"projectName": "acme-platform",
"sync": {
"github": { "owner": "acme", "repo": "acme-platform" },
"jira": { "projectKey": "PLAT" }
},
"childRepos": [
{
"id": "frontend",
"name": "frontend",
"prefix": "FE",
"path": "repositories/acme/frontend",
"sync": {
"github": { "owner": "acme", "repo": "frontend" },
"jira": { "projectKey": "FE" }
}
},
{
"id": "backend",
"name": "backend",
"prefix": "BE",
"path": "repositories/acme/backend",
"sync": {
"github": { "owner": "acme", "repo": "backend" },
"jira": { "projectKey": "BE" }
}
}
]
}
}

Config Reference

FieldRequiredDescription
sync.githubYesGlobal GitHub target (fallback when no child repo matches)
sync.jiraNoGlobal JIRA target
sync.adoNoGlobal ADO target
umbrella.enabledYesEnables umbrella mode and per-repo routing
umbrella.projectNameNoName of the umbrella project (for umbrella-scoped increments)
umbrella.syncNoSync targets for umbrella-scoped work
umbrella.childRepos[].sync.githubNoPer-repo GitHub target
umbrella.childRepos[].sync.jiraNoPer-repo JIRA target
umbrella.childRepos[].sync.adoNoPer-repo ADO target

Scenarios

Scenario 1: Single Repo (No Umbrella)

Setup: Standard single-repo project. No umbrella section in config.

{
"sync": {
"github": { "owner": "alice", "repo": "my-app" },
"jira": { "projectKey": "APP" }
}
}

Behavior: Every increment syncs to alice/my-app on GitHub and APP in JIRA. No routing logic — there's only one target.

IncrementGitHub IssueJIRA Issue
0001-auth-flowalice/my-app#1APP-1
0002-dashboardalice/my-app#2APP-2

Scenario 2: Umbrella Mode

Setup: umbrella.enabled: true with child repos that have sync config.

Using the config from the Example above:

2a. Spec says **Project**: frontend

Resolver matches childRepos[0].name === "frontend" → routes to the frontend repo's sync targets.

IncrementGitHub IssueJIRA Issue
0010-login-pageacme/frontend#1FE-1

2b. Spec says **Project**: backend

Resolver matches childRepos[1].name === "backend" → routes to the backend repo's sync targets.

IncrementGitHub IssueJIRA Issue
0011-api-authacme/backend#1BE-1

2c. Spec says **Project**: acme-platform (umbrella itself)

Resolver matches umbrella.projectName → uses umbrella.sync targets. Use this for cross-cutting work that belongs to the umbrella, not a specific child repo (CI pipelines, shared config, etc.).

IncrementGitHub IssueJIRA Issue
0012-ci-pipelineacme/acme-platform#1PLAT-1

2d. Child repo has no sync config

If a child repo is registered but has no sync field, the resolver matches by name but returns empty targets. Callers fall back to the global config.

{
"id": "shared-lib",
"name": "shared-lib",
"prefix": "SH",
"path": "repositories/acme/shared-lib"
}
IncrementGitHub IssueJIRA Issue
0013-types-updateacme/acme-platform#2 (global fallback)PLAT-2 (global fallback)

Tip: Add sync config to child repos to get proper per-repo routing.

2e. Unknown project name

If the **Project**: field doesn't match any child repo name, ID, or prefix, the resolver falls back to global config.

IncrementGitHub IssueJIRA Issue
0015-mystery-projectacme/acme-platform#3 (global)PLAT-3 (global)

How Agents Use the Project Field

When /sw:increment creates a new increment, the increment skill detects umbrella mode and passes child repo context to the PM agent (sw:pm). The PM then sets the **Project**: field in each user story.

Umbrella mode (umbrella.enabled: true)

  • The increment skill reads umbrella.childRepos and passes the list to the PM
  • The PM designs cross-cutting user stories — a single increment can span multiple repos
  • Each user story gets **Project**: <child-repo-name> based on which repo owns that work
  • Example: A "login feature" becomes US-FE-001: Login Page (**Project**: frontend) + US-BE-001: Auth API (**Project**: backend)
  • Umbrella-scoped work (CI, shared config) uses the umbrella project name
  • All increments live in the umbrella root .specweave/increments/, not in child repos

Single-project mode (umbrella.enabled: false or absent)

  • All user stories get the same **Project**: value
  • Standard single-project spec design
  • All routing goes to the single global target

Key insight: Routing is determined at spec creation time, not at sync time. The **Project**: field is the contract between planning and sync.


Setup

New umbrella project

When you run specweave migrate-to-umbrella, it automatically:

  1. Sets umbrella.enabled: true
  2. Registers the first child repo
  3. Copies global sync config to umbrella.sync

You then add sync config to each child repo as needed.

Adding sync targets to child repos

After migration, edit .specweave/config.json to add per-repo sync targets:

{
"umbrella": {
"childRepos": [
{
"id": "my-service",
"name": "my-service",
"sync": {
"github": { "owner": "acme", "repo": "my-service" },
"jira": { "projectKey": "SVC" }
}
}
]
}
}

Or run /sw:sync-setup to configure sync targets interactively.


Partial Sync Config

Child repos don't need all three platforms configured. If a child repo only has github in its sync config, GitHub issues route to that repo while JIRA/ADO fall back to global.

{
"id": "docs-site",
"sync": {
"github": { "owner": "acme", "repo": "docs-site" }
}
}
PlatformTargetWhy
GitHubacme/docs-siteMatched from child repo
JIRAPLAT (global)No JIRA config on child → fallback
ADOGlobalProject (global)No ADO config on child → fallback

Troubleshooting

Tickets going to wrong repo

  1. Check that your spec has **Project**: repo-name matching a childRepos[].name
  2. Verify the child repo has a sync section in config.json
  3. Confirm umbrella.enabled is true

Tickets always going to global

  • Is umbrella.enabled set to true?
  • Does the **Project**: field in your spec match a child repo name exactly?
  • Does that child repo have a sync section?

Child repo has sync config but tickets go to global

The **Project**: field must exactly match childRepos[].name or childRepos[].id. Check for typos or case mismatches.