# Mirror Workflow — How Gallery Mirrors Stay in Sync > **Audience**: AI sessions, operators, contributors > **Doctrine**: FVW v8.2 §17.5-§17.6 > **Status**: ACTIVE (per the 2026-07-22 gallery entry shapes decision) --- ## Overview Gallery mirrors (`_module_kind: "gallery-mirror"`) are copies of modules whose canonical home is in another repo. The mirror must stay in sync with the source, OR the gallery entry becomes stale. There are **two ways** to keep mirrors in sync: 1. **Push from app CI** (recommended, default) — the source app's CI pushes the mirror to the gallery on release tags 2. **Pull from source by gallery** (fallback) — the gallery's workflow pulls from `_canonical_source` on a schedule **Default is push from app CI.** Apps own the timing of their releases. --- ## Mode 1: Push from App CI (default) ### How it works 1. App's CI runs on release tag (e.g. `v1.2.0`) 2. CI builds the module mirror: recipe book + (optionally) src/ + (optionally) tests/ 3. CI pushes the mirror to the gallery: - Either as a PR (reviewable) - Or as a direct push to `main` (if the app is trusted) 4. Gallery's `accept-mirrors.yml` workflow validates the push 5. Gallery's `registry.json` is updated with the new version ### The App's CI Template In the app repo (e.g. `avidtech6/vibecoder-standalone`), at `.github/workflows/push-gallery-mirror.yml`: ```yaml name: Push Module Mirrors to Gallery on: push: tags: - 'v*' # release tags only jobs: push-mirrors: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Find modules marked as "original" run: | # Find every module with _module_kind: "original" for module_dir in modules/*/; do if [ -f "${module_dir}module-meta.json" ]; then kind=$(jq -r '._module_kind' "${module_dir}module-meta.json") if [ "$kind" = "original" ]; then echo "Pushing ${module_dir} to gallery" # ... push logic fi fi done - name: Push to gallery via PR # Use the gallery's accept-mirrors API or open a PR run: | # ... (the actual push logic depends on auth setup) ``` ### The Gallery's Accept Workflow In the gallery (`avidtech6/fv-module-gallery`), at `.github/workflows/accept-mirrors.yml`: ```yaml name: Accept Mirror Pushes on: push: branches: [main, mirror-push] pull_request: branches: [main] jobs: accept: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Validate the mirror run: | # For every changed module in gallery/modules/ or gallery/forks/: # 1. Check _module_kind is "gallery-mirror" or "app-fork" # 2. Check _canonical_source is set # 3. Check recipe-book/recipe.md exists and has valid version # 4. Check the source still exists (GitHub API) # 5. Update _last_verified, _mirror_status: "healthy" # 6. Update registry.json - name: Update registry.json run: | # ... (the actual registry update logic) ``` --- ## Mode 2: Pull from Source by Gallery (fallback) ### How it works 1. Gallery's `pull-mirrors.yml` workflow runs daily 2. For every `gallery-mirror`, the workflow: - Checks if `_canonical_source` is reachable - If reachable, pulls the source's recipe book (and src/ if mirror includes code) - Compares to the gallery copy - If different, updates the gallery copy + bumps `_last_verified` - If unreachable, marks as `orphaned` (per §17.6) 3. The gallery commits the changes (or opens a PR if the changes are significant) ### The Gallery's Pull Workflow In the gallery, at `.github/workflows/pull-mirrors.yml`: ```yaml name: Pull Mirrors from Sources (Daily) on: schedule: - cron: '0 3 * * *' # 3 AM daily workflow_dispatch: # manual trigger jobs: pull: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: For every gallery-mirror, pull from source run: | python3 scripts/pull-mirrors.py # This script: # 1. Walks gallery/modules/ and gallery/forks/ # 2. For each module with _module_kind: "gallery-mirror": # a. Fetches from _canonical_source via GitHub API # b. Compares checksums # c. If different, updates the gallery copy # d. Updates _last_verified # 3. Commits changes (or opens PR) - name: Report orphans if: failure() run: | # If any source is unreachable, mark as orphaned (per §17.6) ``` --- ## What Gets Mirrored **Always mirrored (recipes only):** - `recipe-book/recipe.md` - `recipe-book/codex.md` - `recipe-book/rules.md` - `recipe-book/module-meta.json` - `recipe-book/ingredients.json` - `recipe-book/diffs/` (if exists) - `recipe-book/trace-atlas/` (if exists) - `recipe-book/dna/` (if exists) - `recipe-book/plan.md` - `recipe-book/coverage-matrix.md` **Optionally mirrored (operator's call):** - `src/` (if Q3 said "recipes+code" or "full") - `tests/` (if Q3 said "full") - `package.json` or `composer.json` (if exists) - `tsconfig.json` (if TypeScript) **Never mirrored:** - `node_modules/` (would GB-bloat the gallery) - `dist/` / `build/` (regenerable) - `.git/` (would duplicate history) - `*.log` (transient) - `coverage/` (regenerable) - Any file > 10 MB (single-file soft cap) --- ## Mirror Metadata The mirror's `module-meta.json` is updated by the workflow: ```json { "_module_kind": "gallery-mirror", "_canonical_source": "avidtech6/ai-shell/ai-toast", "_canonical_version": "1.2.3", // from the source's release tag "_mirror_status": "healthy", // healthy | orphaned | reattached | promoted "_last_verified": "2026-07-22T17:50:00Z", "_cleanup_threshold_days": 30, // per §17.6 ... } ``` --- ## Failure Modes ### Source is temporarily unreachable - Workflow fails for that mirror, others succeed - Mirror is marked as `orphaned` if the next 3 daily runs also fail - After 30 days of being orphaned, mirror is promoted to `gallery-original` (per §17.6) ### Source has changed schema - Mirror update fails validation - Workflow opens an issue / PR with the diff - Operator decides whether to accept ### Source has been deleted/archived - Mirror is marked as `orphaned` immediately - After 30 days, promoted to `gallery-original` - If source comes back, reattach logic runs (per §17.6) --- ## See also - FVW v8.2 §17.5 — Module Canonical Home + Mirror Model - FVW v8.2 §17.6 — Mirror Cleanup + Auto-Promotion - `gallery-pact/AI-PUBLISHING-GUIDE.md` — the 4 questions - `gallery-pact/CLEANUP-WORKFLOW.md` — orphan → gallery-original flow - `/workspace/position-gallery-entry-shapes-v4.md`