| name | gaia-config-ci |
| description | Scaffold or regenerate a CI pipeline with quality checks. Use when "setup CI pipeline" or /gaia-config-ci (formerly /gaia-ci-setup); pass --regenerate to refresh generated workflows with the backup-before-overwrite UX and *.user-steps.yml include pattern; pass --project-slice <service> to project a minimal per-service config slice into a multi-repo / per-service service repo. |
| argument-hint | [--preset solo|small-team|standard|enterprise|custom] [--regenerate] [--project-slice <owner/repo|stack> [--out <path>]] |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
| deprecated_aliases | ["gaia-ci-setup"] |
| deprecated_since | sprint-37 |
| orchestration_class | light-procedural |
Setup
!${CLAUDE_PLUGIN_ROOT}/skills/gaia-ci-setup/scripts/setup.sh
Mission
You are scaffolding a CI/CD pipeline for the project. You detect the CI platform, select a promotion chain preset (or build a custom chain), define pipeline quality gates, configure secrets management, set deployment strategy, and generate the pipeline configuration file.
This skill is the native Claude Code conversion of the legacy _gaia/testing/workflows/ci-setup workflow. It follows the canonical skill pattern established by the code-review skill.
Write context: This skill uses allowed-tools: Read Grep Glob Bash Write Edit because it writes pipeline configuration files and modifies global.yaml.
Foundation script integration: This skill relies on validate-gate.sh from plugins/gaia/scripts/ as a dependency check in setup.sh (the foundation script must be present and executable before the skill body runs). The skill's finalize.sh does NOT post-check ci_setup_exists, since this skill is the producer of .gaia/artifacts/test-artifacts/ci-setup.md and a post-check on the producer's own output is tautological (success path) or misleading (failure path). Deterministic operations (config resolution, gate verification) belong in bash scripts, not LLM prompts.
Critical Rules
- Knowledge fragments are bundled in this skill's
knowledge/ directory -- load them JIT when referenced by a step.
- Before scaffolding, check for existing CI config files (
.github/workflows/, .gitlab-ci.yml, Jenkinsfile, .circleci/config.yml). If found, warn the user and offer to merge or overwrite rather than silently replacing (AC-EC1).
- The
validate-gate.sh foundation script MUST be present and executable at plugins/gaia/scripts/validate-gate.sh. If missing or not executable, HALT with: "validate-gate.sh not found or not executable -- the foundation script must be installed first" (AC-EC3, AC-EC5).
- The
resolve-config.sh foundation script MUST be present and executable. If missing, HALT with dependency error.
- The promotion chain written to
global.yaml MUST use the canonical field order: id, name, branch, ci_provider, merge_strategy, ci_checks (AC4).
- Pipeline configuration MUST include quality gate checks: lint, unit, test at minimum.
- Sprint-status.yaml is NEVER written by this skill (Sprint-Status Write Safety rule).
--project-slice <service> runs the per-service projection mode (below) and is mutually exclusive with the scaffold/--regenerate flow — when present, skip Steps 1-9 entirely and run only the projection.
Per-Service Config Projection (--project-slice)
For multi-repo / per-service layouts — where each service is its own git repository (a backend repo, frontend repo, …) and the shared .gaia/ project config lives at a non-git project root above them — a service repo's CI clones only that service repo, so the central config (which sits above the clone) is invisible to it. The workflow config-resolution chain (checkout-root → CLAUDE_PROJECT_ROOT → upward-walk) finds nothing, and selective tests / per-component deploy / version-bump all fall back to "do everything."
--project-slice solves this by projecting the minimal self-contained config slice each service repo needs, so it can be checked into that repo's .gaia/config/ and resolved at the service repo's own checkout root.
When the invocation contains --project-slice <service>:
-
Resolve the central config via resolve-config.sh (or --config <path>).
-
Run the deterministic projection:
"${CLAUDE_PLUGIN_ROOT}/scripts/project-config-slice.sh" \
--config "<central project-config.yaml>" \
--service "<owner/repo or stack name>" \
[--out "<service-repo>/.gaia/config/project-config.yaml"]
The slice contains the service's own stacks[] entries (selected by repository match, or by stack name) PLUS the transitive cross_refs closure, and carries ci_cd.promotion_chain, release, environments, platforms, and project_name verbatim — so the promotion-push full-suite rail still fires and version-bump still resolves inside the service repo.
-
The script is idempotent — re-run it whenever the central config changes to refresh each service repo's slice. The emitted slice carries a DO NOT EDIT BY HAND header naming the source + the regenerate command.
-
Exit codes: 0 ok, 1 usage/IO error, 2 no stack matches the service. On exit 2, name the available stacks[].repository / name values so the operator can pick a valid service id.
Mapping prerequisite: each stack that lives in its own repo SHOULD declare stacks[].repository: owner/repo in the central config (added to the schema for this purpose). Stacks without a repository are assumed to live in the same repo as the central config (the single-repo default), and can still be sliced by name.
This mode does NOT scaffold or regenerate workflows — it only emits the config slice. Generate/refresh the service repo's actual workflows by running the normal /gaia-config-ci scaffold inside that service repo against its projected slice.
Multi-Stack Workflow Generation
When a project has two or more entries in stacks[] (or a deployable shape with multiple components), the generator produces the full multi-stack CI configuration:
-
selective-tests.yml — an affected-set narrowing workflow that runs only the test suites for changed stacks on PRs, with full-suite escalation on promotion merges to the final tier of ci_cd.promotion_chain. The workflow delegates to the existing engine scripts (selective-test-driver.sh chains detect-affected.sh, cross-refs-walk.sh, reconcile-stale-graph.sh, apply-test-policy.sh, generate-pipeline.sh).
-
gaia-release.yml — a per-component release workflow scaffold that scopes version-bump and release to the affected components only, using workflow_dispatch with an optional component input. The release step itself is a placeholder the operator completes with their project's release tooling (the generated file marks this with a TODO(operator) comment).
-
Engine-script delivery — the engine scripts that the workflows invoke are vendored under .gaia/ci-scripts/ at the project root with a MANIFEST.sha256 drift manifest. The manifest lists every vendored script with its sha256 hash, enabling a regenerate-and-diff lint to detect stale copies when the framework updates a script.
Single-stack projects (stacks[] with exactly one entry, or explicit --stack flag) continue to receive only gaia-pre-merge.yml — no selective-tests, no release workflow, no engine-script delivery.
The multi-stack detection is automatic when --config is passed to the generator. The detection counts stacks[].name entries in the provided project config and routes accordingly.
Tracked in-repo CI config slice (gen-ci-config.sh)
A related-but-distinct layout: a single repo whose canonical .gaia/config/project-config.yaml lives above the checkout root at an untracked project root AND is .gitignored inside the repo (the published repo must not ship a particular project's config). The config is then in no checkout, so CI's config-resolution chain finds nothing and selective tests fall back to the full suite on every PR.
The fix is a tracked, CI-scoped config slice committed into the repo at .gaia/ci-config.yaml (via a .gitignore negation), which the CI workflows resolve at the checkout root. Generate it with:
"${CLAUDE_PLUGIN_ROOT}/scripts/gen-ci-config.sh" \
--config "<canonical project-config.yaml>" \
[--strip-prefix "<sub-tree>/"] \
--out ".gaia/ci-config.yaml"
The slice carries ONLY the CI-matrix inputs — stacks (matrix construction), ci_cd.promotion_chain (so the promotion-push full-suite rail fires), and test_policy — with all comments stripped and no secrets, local paths, environments, release, or distribution. Pass --strip-prefix <sub-tree>/ when the canonical config uses project-root-relative globs (e.g. gaia-public/plugins/**) but CI checks out the sub-tree as the root, so the slice's globs are rebased to the checkout root.
The CI config-resolution precedence is: GAIA_CONFIG → .gaia/ci-config.yaml (this tracked slice) → CLAUDE_PROJECT_ROOT/.gaia/config/project-config.yaml → upward-walk from CWD → no-config full-suite fallback. The slice is a derived projection of the canonical config — regenerate it whenever the canonical config changes; a drift lint (regenerate-and-diff) catches divergence (lockfile pattern).
Steps
Step 1 -- Detect CI Platform
- Scan for existing CI config files in the project:
.github/workflows/, .gitlab-ci.yml, Jenkinsfile, .circleci/config.yml.
- If existing config found: warn the user and present options -- merge with existing, overwrite, or abort (AC-EC1).
- If no config found: note that no existing CI platform was detected.
- Ask which CI platform to use: GitHub Actions, GitLab CI, Jenkins, CircleCI, or other.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 1 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=platform-detected
Step 2 -- Preset Selection (Promotion Chain)
- Check if
ci_cd.promotion_chain already exists in global.yaml.
- If it exists: warn user and offer [o]verwrite / [s]kip / [e]dit (redirect to
/gaia-ci-edit).
- Present the 4 canonical presets: solo, small-team, standard, enterprise, plus custom.
- In YOLO mode: auto-select
standard preset.
- For custom: prompt for each environment field (id, name, branch, ci_provider, merge_strategy, ci_checks).
- Write the selected chain to
global.yaml preserving all existing fields.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 2 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" preset="$PRESET" stage=preset-selected
Step 3 -- Define Pipeline
- Configure build, lint, test, coverage, and deploy gates.
- Map gates to the selected CI platform's syntax.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 3 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=pipeline-defined
Step 4 -- Quality Gates
- Load knowledge fragment:
knowledge/contract-testing.md for consumer-driven contract patterns in CI pipelines
- Define pass/fail thresholds: coverage percentage, test pass rate.
- Configure gate enforcement (blocking vs advisory).
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 4 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=quality-gates-defined
Step 5 -- Secrets Management
- Identify required secrets from architecture and product requirements.
- Document how to add secrets to the selected CI platform.
- Define environment-level separation for staging vs production secrets.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 5 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=secrets-configured
Step 6 -- Deployment Strategy
- Define staging deployment: auto-deploy on merge after gates pass.
- Define production deployment: manual approval gate.
- Define rollback procedure.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 6 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=deployment-strategy-defined
Step 7 -- Monitoring and Notifications
- Configure pipeline failure notifications.
- Add pipeline status badge for README.
- Recommend metrics dashboard for pipeline health.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 7 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=monitoring-configured
Step 8 -- Generate Pipeline Config
-
Deterministic generator. Before
invoking the LLM authoring path, run the deterministic generator. It
reads the project's stack from .gaia/config/test-environment.yaml
(auto-detected by /gaia-bridge-enable / /gaia-init) and emits a
runnable gaia-pre-merge.yml for the supported (provider, stack)
matrix. This closes the gap where
a headless YOLO /gaia-ci-setup invocation produced no workflow at
all because there was no script to author one (only LLM authoring),
leaving the init-generated exit 1 stub in place forever.
_stack="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field stacks.0.language 2>/dev/null || true)"
[ -n "$_stack" ] || _stack="$(awk '/^detected-stack:/{print $2; exit}' .gaia/config/test-environment.yaml 2>/dev/null || true)"
_provider="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --field ci_platform.provider 2>/dev/null || echo github-actions)"
_config="$(${CLAUDE_PLUGIN_ROOT}/scripts/resolve-config.sh --path 2>/dev/null || true)"
bash "${CLAUDE_PLUGIN_ROOT}/skills/gaia-ci-setup/scripts/generate-pipeline.sh" \
--provider "$_provider" \
--config "$_config" \
--stack "$_stack" \
--project-root "${CLAUDE_PROJECT_ROOT:-.}" \
|| true
-
After the deterministic generator runs (succeeds for supported combos,
no-ops for unsupported ones), if the generated workflow still carries
the init-stub marker line GAIA pre-merge gate is not yet configured,
the orchestrating LLM authors the pipeline by hand for the unsupported
(provider, stack) combo. The LLM authoring step is REQUIRED when the
generator returned non-zero; for supported combos the generator's
output is canonical.
-
Validate the generated config syntax. The validation step is wrapped in the retry loop documented below under Schema Validation Retry Loop -- see that subsection for entry, body, exit, and abort semantics. The loop wraps validate-gate.sh (do not duplicate its logic inline).
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 8 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" schema_retry_count="$SCHEMA_RETRY_COUNT" stage=pipeline-config-generated --paths "$CI_CONFIG_PATH"
Schema Validation Retry Loop
Implements the /gaia-ci-setup Schema Validation Retry Loop. Verified by the valid-first-pass, single-retry, and multi-retry test cases.
The Step 8 schema validation invocation is wrapped in a retry loop so the user can iteratively correct CI configuration violations within a single /gaia-ci-setup invocation instead of restarting the workflow.
Entry conditions. The loop is entered exactly once per /gaia-ci-setup invocation, immediately after the pipeline config file has been generated and is ready for schema validation. The first iteration runs the existing validate-gate.sh invocation unchanged.
Loop body.
- Invoke
validate-gate.sh against the current CI configuration.
- On pass: the loop exits immediately on the first attempt with no violations output emitted, and the skill proceeds to Step 9 (Generate Output). This is the valid-first-pass path — no retry loop is invoked when the configuration is valid on the first attempt.
- On failure: render the violations list using the format documented under Violation Output Format below, then prompt the user:
Correct the violations above and press [c] to re-validate, or [x] to abort.
- On
[c]: re-read the CI configuration file from disk (so the user's edits are picked up) and re-invoke validate-gate.sh. Repeat from step 1.
- On
[x]: enter the abort path documented below.
Exit conditions. The loop exits in exactly two ways:
- Pass exit.
validate-gate.sh returns success. The skill proceeds to Step 9. The pass exit is taken on the very first attempt for a valid configuration (no violations, no prompt) and on every subsequent attempt where the user has corrected all outstanding violations.
- Abort exit (
[x]). The skill aborts cleanly with a summary of the remaining violations (N violations remaining — run /gaia-ci-setup again after correction) and exits non-zero. The abort exit is distinct from the pass exit and is the only forced exit path other than pass.
No hard retry cap. The loop has no hard cap on iterations. The user controls convergence — there is no arbitrary retry limit that forces an abort before the user has finished correcting the configuration. This guarantee is verified by the multi-retry test case (3 consecutive failures before pass — the loop must not abort prematurely).
Prompt mode interactions. In YOLO mode the retry loop still prompts [c]/[x]. Violations require human input and cannot be auto-answered — this matches the engine's open-question indicator handling.
Atomic write semantics. The skill does NOT write a partial .gaia/artifacts/test-artifacts/ci-setup.md on the abort path. If ci-setup.md generation already occurred before validation in a future revision, that ordering must be documented here so users understand what the abort path leaves behind. Today the artifact is written by Step 9 (after validation passes), so the abort path leaves no ci-setup.md behind.
Violation Output Format
Each schema violation is rendered as a {field, expected, actual} triplet. The triplet is the canonical machine-parseable record so downstream tooling (lint-SKILL-md.js, future regression tests, automation hooks) can consume it without re-parsing free-form prose.
Violations:
- field: promotion_chain[0].branch
expected: a non-empty string identifying the git branch
actual: <missing>
- field: promotion_chain[1].ci_provider
expected: one of: github_actions | gitlab_ci | jenkins | circleci
actual: travis
Multiple violations are emitted as an ordered list. Field names use dotted-path notation matching the canonical global.yaml schema. The expected value describes the schema constraint in human-readable form; the actual value is the literal value found in the configuration (or <missing> when the field is absent). The triplet contract MUST remain stable so lint and regression tooling can verify the format mechanically.
Step 9 -- Generate Output
- Generate the CI/CD pipeline configuration document at
.gaia/artifacts/test-artifacts/ci-setup.md.
- Include: pipeline stages, quality gates, secrets management, deployment strategy, monitoring setup.
- When the generated workflow is written to disk (e.g.
.github/workflows/gaia-pre-merge.yml), prepend the four-line header emitted by ${CLAUDE_PLUGIN_ROOT}/scripts/lib/ci-regen-header.sh emit <hash> where <hash> is the sha256 of the CI-relevant config sections (computed via the same helper's hash subcommand). The header records: attribution, DO NOT EDIT warning referencing --regenerate, source-hash, and the ISO-8601 generated-at timestamp.
- Immediately after the workflow file is written, invoke
${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-user-steps.sh scaffold <ci-file> to drop a sibling *.user-steps.yml scaffold next to it (no-op when the user-steps file already exists). This is the AC8 first-run scaffold path.
!${CLAUDE_PLUGIN_ROOT}/scripts/write-checkpoint.sh gaia-ci-setup 9 ci_provider="$CI_PROVIDER" ci_config_path="$CI_CONFIG_PATH" stage=output-generated --paths .gaia/artifacts/test-artifacts/ci-setup.md
Four-Phase Stitching Order
When /gaia-config-ci --regenerate rewrites a gaia-*.yml workflow, the engine at ${CLAUDE_PLUGIN_ROOT}/scripts/lib/ci-workflow-stitcher.sh composes the output in this fixed, non-negotiable four-phase order:
- GAIA template scaffold — the managed workflow body as generated from
project-config.yaml.
steps_before_gaia — entries from gaia-{base}.user-steps.yml are spliced before the managed steps block.
gaia-generated jobs ∪ user-jobs — entries from gaia-{base}.user-jobs.yml are YAML-unioned into the managed jobs: map (last-writer-wins on key collision; collision detection is handled separately).
steps_after_gaia — entries from gaia-{base}.user-steps.yml are spliced after the managed steps block.
Overlay shapes
jobs:
coverage-upload:
runs-on: ubuntu-latest
steps:
- run: echo coverage
notify-slack:
runs-on: ubuntu-latest
steps:
- run: echo slack
steps_before_gaia:
- name: user-pre
run: echo before
steps_after_gaia:
- name: user-post
run: echo after
Invariants
- Block-level edges only. Per-step
insert_after / insert_before markers are NOT honored (deliberate scope cut — those are template forks, not overlays).
- GAIA-generated steps and jobs are never reordered or modified. The stitcher composes around them; it does not rewrite them.
- Comments preserved. Comments in both the template-derived sections and the overlay-derived sections survive the stitch.
- Deterministic. Same inputs → byte-identical output. Sort key: alphabetical by overlay filename, then declaration order within each overlay file.
gaia_ci_stitch <managed-yml> [<output-path>] is the single function in the stitcher; downstream consumers (Sub-flow C of --regenerate mode) source it and call directly.
Auto-Rename Migration
Orchestration contract (caller responsibility)
The gaia_auto_rename_migration helper at ${CLAUDE_PLUGIN_ROOT}/scripts/lib/auto-rename-migration.sh is decision-driven — it consumes a per-file decision from the env-var GAIA_MIGRATE_DECISION_{basename_with_underscores} (e.g., GAIA_MIGRATE_DECISION_ci_yml=y). The production caller (the LLM running /gaia-config-ci --regenerate) is responsible for the interactive orchestration:
- Pre-flight enumerate. Source the helper and call
gaia_auto_rename_migration with no decisions set — it will list candidates and (default-to-skip) write .gaia/memory/.config-stale for each. Read the list of candidates from .github/workflows/*.yml filtered through gaia_ci_classify == unprefixed.
- Per-file
AskUserQuestion. For EACH candidate, dispatch AskUserQuestion with the canonical three options:
y — gaia-rename + overlays — managed by GAIA; you keep custom logic via overlay stubs.
n — user-rename — file is yours; GAIA will never modify it again.
s — skip-all — defer the decision; .gaia/memory/.config-stale is written and /gaia-help will surface the deferred migration.
- Set decision env-var + re-invoke. Export
GAIA_MIGRATE_DECISION_{basename_with_underscores}=<choice> for each resolved decision, then re-invoke gaia_auto_rename_migration to execute the renames + backups.
- Y-branch regen step. AFTER the helper renames a file to
gaia-{base}.yml, the orchestrator MUST re-enter Sub-flow C for that file: generate the canonical body (Sub-flow C step 1), stitch overlays (step 2), apply template_overrides (step 4), prepend header (step 5). The helper itself does NOT regenerate the body — it only renames + scaffolds the empty overlay stubs.
Three branches (helper-level behavior)
- (y) Rename to
gaia-{base}.yml + scaffold empty overlay stubs (gaia-{base}.user-jobs.yml, gaia-{base}.user-steps.yml). Body regen is the orchestrator's responsibility (see step 4 of the orchestration contract above) — the helper does NOT call back into the canonical template generator.
- (n) Rename to
user-{base}.yml (byte-identical content; the file is now user-owned).
- (s) Skip-all — leave the file untouched and write
.gaia/memory/.config-stale. The deferred migration is surfaced by /gaia-help.
Backup contract
Backup-first BEFORE any rename: .gaia-backup/ci-regen-{ISO-8601-timestamp}/ at PROJECT_ROOT (NOT under .gaia/), mode 0755, files mode 0644, sha256-verified byte-identical copy of every file the migration is about to mutate.
Non-interactive guard
In non-interactive contexts (GAIA_NONINTERACTIVE=1), the migration requires BOTH --force CLI arg AND GAIA_MIGRATE_ALLOW_FORCE=1 env-var. Either alone HALTs with:
auto-rename-migration.sh: HALT: non-interactive auto-rename migration requires --force AND GAIA_MIGRATE_ALLOW_FORCE=1
Idempotency
Already-prefixed files (gaia-*.yml or user-*.yml) are classified by gaia_ci_classify as generated / user-authored / overlay and never re-fire the prompt. Subsequent regen runs produce byte-identical output (extends the determinism guarantee).
template_overrides: Declarative Overrides
ci_cd.template_overrides: is a three-field declarative surface in project-config.yaml that lets project owners modify generated workflows without authoring overlay files. The interpreter lives at ${CLAUDE_PLUGIN_ROOT}/scripts/lib/template-overrides.sh and runs on the stitched workflow stream during /gaia-config-ci --regenerate.
The three fields
ci_cd:
template_overrides:
disable: [shellcheck]
timeout_overrides:
bats-tests: 15
adapter_versions:
markdownlint: "0.41.0"
Disable-allowlist
The closed enum of security-critical job names that CANNOT be disabled is hard-coded:
commitlint
adr-048-guard
no-claude-attribution
secrets-scan
nfr-082-credential-audit
Any disable: entry matching one of these names (after hyphen+case canonicalization — commit-lint / Commit-Lint / commitlint all collide with the canonical token) is REJECTED with a non-zero exit and an actionable error.
Per-field validation
- Unknown
disable: name → WARNING (graceful — catalog may drift between framework releases).
timeout_overrides: minutes outside [1, 360] → HARD ERROR.
- Unknown
adapter_versions: adapter name → HARD ERROR (pinning to a non-existent adapter is always a typo).
- Unparseable semver in
adapter_versions: → HARD ERROR (expects N.N.N with optional prerelease/build).
Sequencing
The interpreter runs against the already-stitched workflow stream (the ci-workflow-stitcher.sh output), not the raw template. Override application is the final pass before the workflow is written to disk.
Regen Contract — gaia- Prefix Is the Ownership Boundary
/gaia-config-ci --regenerate is permitted to overwrite only files classified as generated by the canonical helper ${CLAUDE_PLUGIN_ROOT}/scripts/lib/ci-prefix-detection.sh. Source the helper and call gaia_ci_classify <path> to obtain one of:
| Classification | Match rule (first match wins) | Regen behavior |
|---|
overlay | gaia-*.user-jobs.yml OR gaia-*.user-steps.yml | NEVER overwrite. Stitch via ci-regen-user-steps.sh (see Sub-flow C step 2). |
generated | basename starts with gaia- (and did not match overlay) | Eligible for regen. Header + body fully overwritten. |
user-authored | basename starts with user- | NEVER touch. Refuse with actionable error. |
unprefixed | none of the above | NEVER touch. Route through the auto-rename migration prompt. |
The four-value enum is exhaustive and ordered — overlay precedence is load-bearing so that gaia-ci.user-jobs.yml is classified overlay (not generated), preventing regen from stomping overlay files. The unprefixed state is the migration-trigger surface and is intentionally distinct from user-authored (fail-safe-conservative fallback).
Refuse-to-touch error for user-authored / overlay / unprefixed:
/gaia-config-ci: refusing to overwrite <path> (classification: <kind>).
Regen owns gaia-*.yml only — per GAIA's CI customization layered model.
- user-authored files: yours; regen will never modify them.
- overlay files (gaia-*.user-jobs.yml / gaia-*.user-steps.yml): stitched, never overwritten.
- unprefixed files: run /gaia-config-ci to migrate via the auto-rename flow.
ci-prefix-detection.sh is the single source of truth for this classification. Sub-flow A's ci-regen-detect-edit.sh MUST consult gaia_ci_classify before scanning for manual edits — no file outside the generated class is a candidate for the regen loop.
--regenerate Mode
When the user invokes /gaia-config-ci --regenerate, this mode replaces Steps 1-9 entirely. It is the deterministic refresh path for previously-generated workflow files. The mode does not write a step-level checkpoint — it is a re-entry into the generator, not a new phase, and the existing Step 9 checkpoint covers the regenerated artifact.
Sub-flow A — Manual-edit detection.
For each generated workflow file (.github/workflows/gaia-*.yml), run ${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-detect-edit.sh <file>. The exit code drives the next sub-flow:
0 (clean) — no manual edits. Proceed silently to sub-flow C (regenerate in place). NO prompt is shown — this is the silent regen path; no manual edit means no user prompt.
1 (edited) — proceed to sub-flow B (backup-or-merge prompt).
2 (no header) — treat as edited; proceed to sub-flow B.
Sub-flow B — Backup-or-merge prompt.
Present the four-option prompt with b as the default option:
The generated CI workflow has manual edits. Choose how to proceed:
(d) show diff — preview the difference, then re-prompt.
(b) backup — copy current file to .gaia-backup/{ci-file}-{ts}/, then regenerate. (default)
(m) merge manually — cancel; you reconcile by hand.
(f) force overwrite — proceed without backup.
[d/b/m/f] (default: b):
Resolve the answer:
d — render diff -u <file> <regenerated-content> and re-issue the prompt.
b — invoke ${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-backup.sh <file> to create the backup directory, then proceed to sub-flow C. The backup directory uses the convention .gaia-backup/{ci-file}-{ISO-8601-timestamp}/ at project root.
m — abort the regen for this file. Write the .gaia/memory/.config-stale flag via ${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-stale-flag.sh write so subsequent commands surface the stale-config warning.
f — proceed to sub-flow C without creating a backup.
Sub-flow C — Regenerate the workflow.
Compute the canonical content for the workflow:
- Generate the body content from the current
project-config.yaml (the same deterministic generator used by Step 9).
- Overlay stitching. Source
${CLAUDE_PLUGIN_ROOT}/scripts/lib/ci-workflow-stitcher.sh and call gaia_ci_stitch <managed-yml> against the generated workflow. The stitcher composes the four-phase order: GAIA scaffold → steps_before_gaia → GAIA jobs ∪ user-jobs.yml → steps_after_gaia. The legacy ${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-user-steps.sh extract-before/extract-after primitives remain available for callers that need the raw blocks; the canonical entry-point for full stitching is gaia_ci_stitch.
- Write protection. BEFORE writing any file, pass the prospective target through
${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-user-steps.sh assert-protected <path> — the helper exits non-zero for any *.user-steps.yml path so the regenerate flow CANNOT touch a user-steps file regardless of which option (d/b/m/f) the user chose.
- Apply
template_overrides:. Source ${CLAUDE_PLUGIN_ROOT}/scripts/lib/template-overrides.sh and call gaia_apply_template_overrides <stitched-yml> <project-config.yaml> against the stitched workflow stream. The interpreter performs the three passes (disable, timeout_overrides, adapter_versions) and enforces the closed-enum disable allowlist. On non-zero exit, ABORT the regen for this file — no partial write, no header prepended, the user must fix project-config.yaml and re-run.
- Compute the body sha256 via
${CLAUDE_PLUGIN_ROOT}/scripts/lib/ci-regen-header.sh hash (over the CI-relevant config sections), prepend the four-line header via ... emit <hash>, and write the result back to the workflow file. The header lines are:
# Generated by /gaia-config-ci from project-config.yaml
# DO NOT EDIT this file by hand — run \/gaia-config-ci --regenerate` to refresh.`
# Source hash: sha256:<hex>
# Generated at: <ISO-8601-UTC>
- After every workflow has been refreshed successfully, clear the stale flag via
${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-stale-flag.sh clear.
Sub-flow D — Stale-flag lifecycle.
The flag file .gaia/memory/.config-stale is the single signal that the project's CI workflows are out-of-date relative to project-config.yaml:
write — set on m (merge-manually) in sub-flow B and on n (defer) in the post-edit prompt (sub-flow E).
check — emitted at the top of /gaia-* runs that consume the config; exits 0 with a stderr warning when present, exits 1 silently when absent.
clear — invoked at the end of sub-flow C after a successful regen for every workflow.
Sub-flow E — Post-edit prompt.
Other config-mutating editors (/gaia-config-env, /gaia-config-stack, /gaia-config-rubric, etc.) detect when their edits touch the CI-relevant sections (ci_cd, environments, stacks) and invoke the shared prompt helper:
!${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-post-edit-prompt.sh print
The user answers y (regenerate now), n (defer), or d (show diff). The editor then resolves the side-effect via:
!${CLAUDE_PLUGIN_ROOT}/scripts/ci-regen-post-edit-prompt.sh handle <answer>
Answer y returns to the caller for an immediate /gaia-config-ci --regenerate. Answer n writes the stale flag (sub-flow D). Answer d returns a diff-hint pointing at /gaia-config-show ci_cd for a preview.
No prompt when no manual edit. If sub-flow A returns clean for every file in the regen set, no prompt is presented — files are regenerated in place silently.
*.user-steps.yml is never modified. Across every option of sub-flow B, sub-flow C, and the scaffold path in Step 9, the assert-protected guard ensures no *.user-steps.yml is overwritten, deleted, moved, or backed up.
Validation
- [script-verifiable] SV-01 — Pipeline stages defined (build, lint, test, coverage)
- [script-verifiable] SV-02 — Quality gate thresholds set
- [script-verifiable] SV-03 — Secrets management documented (required secrets, environment separation)
- [script-verifiable] SV-04 — Deployment strategy defined (staging, production, rollback)
- [script-verifiable] SV-05 — Monitoring and notifications configured (failure alerts, status badge)
- [script-verifiable] SV-06 — Pipeline config generated
- [LLM-checkable] LLM-01 — CI platform confirmed by user (not just auto-detected)
- [LLM-checkable] LLM-02 — Gates are enforced (blocking, not advisory)
Finalize
!${CLAUDE_PLUGIN_ROOT}/skills/gaia-ci-setup/scripts/finalize.sh
Next Steps
- Primary:
/gaia-readiness-check — validate implementation readiness now that CI is scaffolded.