Shape: Multi-Repo / Per-Service

In a multi-repo layout, each service is its own git repository — a backend repo, a frontend repo, a desktop repo, a marketing-site repo — each cloned, built, tested, and deployed independently. The shared GAIA project config lives above them at a non-git project root that ties the services together for planning, while each service repo carries only the slice of config its own CI needs.

This is distinct from Shape: Microservices (many services in one monorepo) and from the single-clone "config above a published sub-tree" case. Here the config is in no service clone at all.

When this shape applies

The config-visibility problem

Every generated GAIA workflow resolves the project config by looking at the checkout root, then CLAUDE_PROJECT_ROOT, then walking up from the working directory. In a multi-repo layout this finds nothing: a service repo's CI clones only that service repo, so there is no parent directory in the runner to walk up into, and CLAUDE_PROJECT_ROOT points at a path that does not exist on the runner. With no config found, the workflow takes the "no config → do everything" branch — the full test suite runs and the whole service redeploys on every change, defeating selective tests and per-component deploy for the entire multi-repo project.

The fix is to project a minimal, self-contained config slice into each service repo, checked in at its own .gaia/config/project-config.yaml, so the workflow resolves it at the service repo's own checkout root.

Central config with per-service repositories

Declare each stack's owning repository with stacks[].repository (in owner/repo form). Stacks without a repository are assumed to live alongside the central config (the single-repo default).

# <project-root>/.gaia/config/project-config.yaml  (the central config)
project_name: acme-platform
platforms: [web]
ci_cd:
  promotion_chain:
    - { id: staging, branch: staging }
    - { id: main,    branch: main }
release:
  strategy: conventional-commits
  version_files: [backend/package.json, frontend/package.json]
environments:
  - { id: production, branch: main, kind: deploy }
stacks:
  - name: backend
    language: typescript
    paths: ["backend/**"]
    repository: acme/backend
    cross_refs: ["shared-lib"]
  - name: frontend
    language: typescript
    paths: ["frontend/**"]
    repository: acme/frontend
    cross_refs: ["shared-lib"]
  - name: shared-lib
    language: typescript
    paths: ["shared/**"]
    repository: acme/shared-lib
  - name: marketing
    language: html
    paths: ["marketing/**"]
    repository: acme/marketing

Projecting per-service slices

From the project root, project a slice for each service repo:

# Drive it via the config-CI skill (recommended):
/gaia-config-ci --project-slice acme/backend --out ../backend/.gaia/config/project-config.yaml

# …or call the deterministic script directly:
plugins/gaia/scripts/project-config-slice.sh \
  --config .gaia/config/project-config.yaml \
  --service acme/backend \
  --out ../backend/.gaia/config/project-config.yaml

--service accepts an owner/repo value (matched against stacks[].repository) or a stack name. Commit the generated slice into the service repo so its CI resolves it at the service's checkout root.

What a slice contains

The slice is the minimal self-contained config that service needs — not the whole multi-service config. For acme/backend above (which cross_refs shared-lib):

Unrelated services (frontend, marketing) are excluded. Each slice carries a DO NOT EDIT BY HAND header naming its source and the regenerate command.

Keeping slices in sync

The projection is idempotent: whenever the central config changes (a new stack, a changed promotion chain, a new cross-ref), re-run --project-slice for each affected service and commit the refreshed slice. A simple project-root script can loop over every stacks[].repository and regenerate all slices in one pass.

Cross-repo affected-set / deploy (current scope). Within a service repo, selective tests narrow to that service's changed files and the carried cross_refs closure. Full cross-repo affected-set hand-off (one service's change triggering a dependent service's pipeline) and cross-repo deploy ordering are tracked as follow-on work — the slice declares the cross-repo dependencies so they are visible, and the promotion-push rail still guarantees a full suite at the final-tier gate.

You'll know it worked when