---
phase: 55-ci-optimization-consistency-audit
plan: 002
type: execute
wave: 1
depends_on: []
files_modified:
  - .github/workflows/ci.yml
autonomous: true
requirements:
  - CI-02
  - CI-03

must_haves:
  truths:
    - "ci.yml runs L0 through L4 (and L5) on merge to main — full quality gate"
    - "re-actors/alls-green aggregator provides single-status check for branch protection"
    - "L1 unit tests run on 3 OS x 3 Python plasma matrix (fast parallel)"
    - "L2 CLI tests run on 2 Python x 1 OS (narrower scope, platform-independent)"
    - "L3 plugin tests run on Node 20 single config"
    - "L4 E2E + audit tests run on single Python config"
    - "L5 journey tests run on single Python config (separate from gate or as informational)"
    - "Path-filtered triggers prevent unnecessary jobs: plugin-only changes skip L1/L2/L4; version-only changes run L0 only; OCR changes trigger L1+L2+L4"
  artifacts:
    - path: ".github/workflows/ci.yml"
      provides: "Full plasma matrix CI pipeline with L0-L5 merge gate"
      min_lines: 200
  key_links:
    - from: "ci.yml version-check (L0)"
      to: "scripts/check_version_sync.py"
      via: "CLI invocation"
      pattern: "version.check|check_version_sync"
    - from: "ci.yml unit-tests (L1)"
      to: "tests/"
      via: "pytest invocation"
      pattern: "pytest.*tests.*unit"
    - from: "ci.yml plugin-tests (L3)"
      to: "paperforge/plugin/"
      via: "npx vitest run"
      pattern: "vitest"
    - from: "ci.yml alls-green"
      to: "re-actors/alls-green@v1"
      via: "GitHub Action aggregator"
      pattern: "alls-green"
---

<objective>
**Rewrite `.github/workflows/ci.yml` with plasma matrix strategy, L0-L5 merge gate, path-filtered triggers, and `re-actors/alls-green` single-status check for branch protection.**

**Purpose:** Optimize CI resource usage by running fast, parallel L1 tests on 3 OS x 3 Python while constraining higher layers to narrower matrices. Path-filtered triggers prevent unnecessary job execution when changes are scoped to specific subsystems. The alls-green check provides a single pass/fail status for GitHub branch protection rules.

**Output:**
- `.github/workflows/ci.yml` — Full rewrite with plasma matrix, path filtering, L0-L5 gate, alls-green aggregator
</objective>

<execution_context>
@.planning/phases/55-ci-optimization-consistency-audit/55-CONTEXT.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md

@.github/workflows/ci.yml
@.github/workflows/ci-chaos.yml

@pyproject.toml

<interfaces>
Current ci.yml structure (24 lines, only plugin-tests):
```yaml
on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  plugin-tests:
    name: Plugin Tests (L3)
    runs-on: ubuntu-latest
    if: contains(join(...modified...), 'paperforge/plugin/') || ...
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm', cache-dependency-path: paperforge/plugin/package-lock.json }
      - run: npm ci (working-directory: paperforge/plugin)
      - run: npx vitest run --reporter=verbose (working-directory: paperforge/plugin)
```

Existing test layers:
| Layer | Dir | Command | Markers |
|-------|-----|---------|---------|
| L0 | scripts/check_version_sync.py | `python scripts/check_version_sync.py` | N/A |
| L1 | tests/ (root) | `pytest tests/ --ignore=... -v --tb=short --timeout=60` | not cli/e2e/journey/chaos |
| L2 | tests/cli/ | `pytest tests/cli/ -m cli -v --tb=short --timeout=60` | cli |
| L3 | paperforge/plugin/ | `npx vitest run --reporter=verbose` | N/A (vitest) |
| L4 | tests/e2e/ + tests/audit/ | `pytest tests/e2e/ tests/audit/ -m "e2e or audit" -v --tb=short --timeout=120` | e2e, audit |
| L5 | tests/journey/ | `pytest tests/journey/ -m journey -v --tb=short --timeout=120` | journey |

Note: L0 script (check_version_sync.py) may not exist yet — fall back to inline version check via `python -c "from paperforge import __version__; print(__version__)"` if script is missing.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Rewrite ci.yml with plasma matrix strategy, L0-L5 merge gate, and alls-green aggregator</name>
  <files>
    .github/workflows/ci.yml
  </files>
  <action>
    Rewrite `.github/workflows/ci.yml` from scratch. The new workflow must:

    **Triggers:**
    ```yaml
    on:
      push:
        branches: [main, master]
        paths-ignore:
          - '**.md'
          - 'docs/**'
      pull_request:
        branches: [main, master]
        paths-ignore:
          - '**.md'
          - 'docs/**'
    ```

    **Env (shared across jobs):**
    - `PYTHONIOENCODING: utf-8`

    **Jobs:**

    ---

    ### 1. `changes` — Path-filter detection (dorny/paths-filter)
    ```yaml
    changes:
      name: Detect Changed Paths
      runs-on: ubuntu-latest
      outputs:
        version: ${{ steps.filter.outputs.version }}
        plugin: ${{ steps.filter.outputs.plugin }}
        ocr: ${{ steps.filter.outputs.ocr }}
        core: ${{ steps.filter.outputs.core }}
        any_changed: ${{ steps.filter.outputs.any_changed }}
      steps:
        - uses: actions/checkout@v4
        - uses: dorny/paths-filter@v3
          id: filter
          with:
            filters: |
              version:
                - 'paperforge/__init__.py'
                - 'paperforge/plugin/manifest.json'
                - 'paperforge/plugin/versions.json'
                - 'CHANGELOG.md'
                - 'pyproject.toml'
              plugin:
                - 'paperforge/plugin/**'
                - 'paperforge/plugin/package.json'
              ocr:
                - 'paperforge/worker/ocr.py'
                - 'paperforge/ocr_diagnostics.py'
              core:
                - 'paperforge/**'
                - 'tests/**'
                - 'fixtures/**'
                - 'pyproject.toml'
                - 'scripts/**'
    ```

    ---

    ### 2. `version-check` — L0: Version consistency (always runs on relevant changes)
    ```yaml
    version-check:
      name: L0 — Version Sync Check
      needs: changes
      if: ${{ needs.changes.outputs.version == 'true' || needs.changes.outputs.core == 'true' }}
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-python@v5
          with: { python-version: "3.11" }
        - name: Install package
          run: pip install -e .
        - name: Check version consistency
          run: |
            if [ -f scripts/check_version_sync.py ]; then
              python scripts/check_version_sync.py
            else
              python -c "
            from paperforge import __version__
            import json
            print(f'PaperForge version: {__version__}')
            with open('paperforge/plugin/manifest.json') as f:
                manifest = json.load(f)
            assert manifest['version'] == __version__, f'Manifest version {manifest[\\\"version\\\"]} != {__version__}'
            print('Version sync: OK')
            "
            fi
    ```

    ---

    ### 3. `unit-tests` — L1: 3 OS x 3 Python plasma matrix
    ```yaml
    unit-tests:
      name: L1 — Unit Tests (${{ matrix.os }}, py${{ matrix.python-version }})
      needs: changes
      if: ${{ needs.changes.outputs.plugin != 'true' || needs.changes.outputs.core == 'true' }}
      strategy:
        fail-fast: false
        matrix:
          os: [ubuntu-latest, windows-latest, macos-latest]
          python-version: ["3.10", "3.11", "3.12"]
      runs-on: ${{ matrix.os }}
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-python@v5
          with: { python-version: "${{ matrix.python-version }}" }
        - name: Install package with test deps
          run: |
            pip install -e ".[test]"
        - name: Run unit tests (root tests, not layer-marked)
          run: |
            python -m pytest tests/ \
              --ignore=tests/sandbox \
              --ignore=tests/cli \
              --ignore=tests/e2e \
              --ignore=tests/journey \
              --ignore=tests/chaos \
              --ignore=tests/audit \
              -v --tb=short --timeout=60 \
              -x
    ```

    Notes:
    - `fail-fast: false` so a single OS failure doesn't cancel other OS runs
    - Exclude layer-specific test directories so L1 stays fast and focused
    - `-x` stops on first failure (fast feedback for PRs)
    - The `if` condition: skip L1 when ONLY plugin files changed (plugin changes never affect Python code)

    ---

    ### 4. `cli-tests` — L2: 2 Python x 1 OS (CLI contract tests)
    ```yaml
    cli-tests:
      name: L2 — CLI Contracts (py${{ matrix.python-version }})
      needs: changes
      if: ${{ needs.changes.outputs.plugin != 'true' || needs.changes.outputs.core == 'true' }}
      strategy:
        matrix:
          python-version: ["3.10", "3.12"]
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-python@v5
          with: { python-version: "${{ matrix.python-version }}" }
        - name: Install package with test deps
          run: pip install -e ".[test]"
        - name: Run CLI contract tests
          run: python -m pytest tests/cli/ -m cli -v --tb=short --timeout=60 -x
    ```

    ---

    ### 5. `plugin-tests` — L3: Node 20 single config
    ```yaml
    plugin-tests:
      name: L3 — Plugin Tests
      needs: changes
      if: ${{ needs.changes.outputs.plugin == 'true' || needs.changes.outputs.core == 'true' }}
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with:
            node-version: 20
            cache: 'npm'
            cache-dependency-path: paperforge/plugin/package-lock.json
        - run: npm ci
          working-directory: paperforge/plugin
        - run: npx vitest run --reporter=verbose
          working-directory: paperforge/plugin
    ```

    ---

    ### 6. `e2e-tests` — L4: Single config (E2E + audit)
    ```yaml
    e2e-tests:
      name: L4 — E2E + Audit
      needs: changes
      if: ${{ needs.changes.outputs.plugin != 'true' || needs.changes.outputs.core == 'true' }}
      runs-on: ubuntu-latest
      strategy:
        matrix:
          python-version: ["3.11"]
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-python@v5
          with: { python-version: "${{ matrix.python-version }}" }
        - name: Install package with test deps
          run: pip install -e ".[test]"
        - name: Run E2E tests
          run: python -m pytest tests/e2e/ -m e2e -v --tb=short --timeout=120 -x
        - name: Run consistency audit tests
          run: python -m pytest tests/audit/ -m audit -v --tb=short --timeout=120 -x
    ```

    ---

    ### 7. `journey-tests` — L5: Single config (informational, non-blocking)
    ```yaml
    journey-tests:
      name: L5 — Journey Tests
      needs: changes
      if: ${{ needs.changes.outputs.any_changed == 'true' }}
      runs-on: ubuntu-latest
      strategy:
        matrix:
          python-version: ["3.11"]
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-python@v5
          with: { python-version: "${{ matrix.python-version }}" }
        - name: Install package with test deps
          run: pip install -e ".[test]"
        - name: Run journey tests
          run: python -m pytest tests/journey/ -m journey -v --tb=short --timeout=120
    ```
    NOTE: Journey tests run without `-x` (informational) and are NOT included in the alls-green gate. This matches the design intent that L5 is a UX quality signal, not a merge blocker.

    ---

    ### 8. `alls-green` — Single-status aggregator for branch protection
    ```yaml
    alls-green:
      name: All Checks Passed
      if: always()
      needs:
        - version-check
        - unit-tests
        - cli-tests
        - plugin-tests
        - e2e-tests
      runs-on: ubuntu-latest
      steps:
        - uses: re-actors/alls-green@v1
          with:
            allowed-skips: version-check, plugin-tests
            jobs: ${{ toJSON(needs) }}
    ```

    Notes on `allowed-skips`:
    - `version-check` is allowed to be skipped when only non-version files change
    - `plugin-tests` is allowed to be skipped when only Python files change
    - `unit-tests`, `cli-tests`, `e2e-tests` are NOT in allowed-skips — they run on any core change
    - The alls-green action treats skipped jobs (via `if:`) as green when listed in `allowed-skips`

    ---

    **CRITICAL DESIGN RULES:**
    - Do NOT include chaos tests (L6) in this workflow — they have their own scheduled workflow (ci-chaos.yml)
    - All E2E/audit/journey tests must use isolation guards (assert temp dir) — same pattern as tests/e2e/conftest.py
    - Use `actions/checkout@v4` and `actions/setup-python@v5` consistently
    - python-version strings MUST be quoted: `"3.10"`, `"3.11"`, `"3.12"`
    - Each job's `name` should be descriptive (shown in GitHub UI)
    - Use `--junit-xml` for test result artifacts on failure (optional but recommended)
    - All `pip install` commands use `pip install -e ".[test]"` — note the quotes around `.[test]` for Windows PowerShell compatibility
    - Test timeout values: L1=60s, L2=60s, L3=vitest default, L4=120s, L5=120s
    - Do NOT modify ci-chaos.yml — that workflow is intentionally separate
  </action>
  <verify>
    <automated>python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml')); print('YAML valid')"</automated>
  </verify>
  <done>
    - `.github/workflows/ci.yml` is valid YAML
    - Contains 8 jobs: changes, version-check, unit-tests, cli-tests, plugin-tests, e2e-tests, journey-tests, alls-green
    - unit-tests matrix: 3 OS (ubuntu, windows, macos) x 3 Python (3.10, 3.11, 3.12)
    - cli-tests matrix: 2 Python (3.10, 3.12) x 1 OS (ubuntu)
    - plugin-tests: single Node 20 config
    - e2e-tests: single Python 3.11 config + audit tests
    - alls-green aggregates all gate jobs
    - Path-filtered triggers: version-only->L0, plugin-only->L3, core->all
  </done>
</task>

<task type="auto">
  <name>Task 2: Verify CI workflow correctness with dry-run validation</name>
  <files>
    .github/workflows/ci.yml
  </files>
  <action>
    Validate the rewritten ci.yml on multiple dimensions:

    1. **YAML syntax check**: Parse the file with Python's yaml.safe_load to ensure valid YAML.

    2. **Structural validation**: Write a Python validation script that checks:
       - All 8 required jobs exist (changes, version-check, unit-tests, cli-tests, plugin-tests, e2e-tests, journey-tests, alls-green)
       - unit-tests has 9 matrix combinations (3 OS × 3 Python)
       - cli-tests has 2 matrix combinations (2 Python × 1 OS)
       - plugin-tests has `setup-node` with `node-version: 20`
       - e2e-tests runs both `tests/e2e/` and `tests/audit/`
       - alls-green uses `re-actors/alls-green@v1`
       - alls-green `needs` includes all gate jobs except journey-tests
       - Chaos tests NOT included in ci.yml
       - Paths-ignore blocks `**.md` and `docs/**` at the workflow trigger level

    3. **Path filter sanity check**: Verify the dorny/paths-filter paths match actual project file structure. For each filter entry:
       - version: check `paperforge/__init__.py` exists, `paperforge/plugin/manifest.json` exists, `paperforge/plugin/versions.json` exists
       - plugin: check `paperforge/plugin/` directory exists
       - ocr: check `paperforge/worker/ocr.py` exists, `paperforge/ocr_diagnostics.py` exists
       - core: check `paperforge/`, `tests/`, `fixtures/`, `scripts/` directories exist

    4. **Command dry-run**: Optionally run `pip install -e ".[test]"` in a temp dir to confirm the install command works, and `python -m pytest tests/cli/ --co -q` to confirm pytest collects tests.

    Fix any issues found during validation. Ensure the matrix syntax is correct (`${{ matrix.os }}` not `${{ matrix.os }}` escaping issues).

    **Key gotchas to avoid:**
    - In YAML, `${{ }}` expressions don't need escaping inside double-quoted strings on GitHub Actions
    - matrix.os values must match runner labels exactly
    - `pip install -e ".[test]"` needs quotes on Windows
    - `actions/setup-python@v5` requires `with: { python-version: ... }` format
    - `needs.changes.outputs.*` only works if the `changes` job's outputs are declared correctly
  </action>
  <verify>
    <automated>python -c "
import yaml, sys
with open('.github/workflows/ci.yml') as f:
    wf = yaml.safe_load(f)
jobs = wf.get('jobs', {})
required = ['changes', 'version-check', 'unit-tests', 'cli-tests', 'plugin-tests', 'e2e-tests', 'journey-tests', 'alls-green']
for j in required:
    assert j in jobs, f'Missing job: {j}'
# Verify matrix dimensions
ut = jobs['unit-tests']
assert len(ut['strategy']['matrix']['os']) == 3, 'L1 needs 3 OS'
assert len(ut['strategy']['matrix']['python-version']) == 3, 'L1 needs 3 Python'
ct = jobs['cli-tests']
assert len(ct['strategy']['matrix']['python-version']) == 2, 'L2 needs 2 Python'
assert 'alls-green' in jobs
ag = jobs['alls-green']
assert 're-actors/alls-green@v1' in str(ag), 'Missing alls-green action'
assert 'tests/audit' in str(jobs['e2e-tests']), 'Audit not in e2e'
print('All validation checks passed')
"
  </verify>
  <done>
    - CI workflow YAML validated as syntactically correct
    - All 8 jobs verified (changes, version-check, unit-tests, cli-tests, plugin-tests, e2e-tests, journey-tests, alls-green)
    - Plasma matrix dimensions verified (L1: 3 OS x 3 Python, L2: 2 Python x 1 OS, L3-L5: single config)
    - Audit tests wired into e2e-tests job
    - alls-green with re-actors/alls-green@v1 verified
    - Path filter entries verified against project file structure
    - Chaos tests confirmed excluded from ci.yml
  </done>
</task>

</tasks>

<verification>
1. `python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` — YAML is valid
2. CI workflow has 8 jobs matching the design spec
3. Plasma matrix dimensions are correct (L1: 3x3, L2: 2x1, L3-L5: 1x1)
4. Path-filtered triggers version-only->L0, plugin-only->L3, ocr->L1+L2+L4, core->all
5. alls-green aggregator correctly includes version-check, unit-tests, cli-tests, plugin-tests, e2e-tests
6. Chaos tests NOT included (maintained in separate ci-chaos.yml)
7. Audit tests (tests/audit/) wired into e2e-tests job step
</verification>

<success_criteria>
1. `ci.yml` runs L0 through L4 (and L5) on merge to main
2. `re-actors/alls-green` provides single-status check for branch protection
3. L1 unit tests run on 3 OS x 3 Python plasma matrix
4. L2 CLI tests run on 2 Python x 1 OS (ubuntu-latest)
5. L3 plugin tests run on Node 20 single config
6. L4 E2E + audit tests run on Python 3.11 single config
7. L5 journey tests run as informational (not in alls-green gate)
8. Path-filtered triggers prevent unnecessary jobs:
   - Version-only changes trigger L0 only
   - Plugin-only changes trigger L3 only
   - Core/OCR changes trigger all relevant layers
9. Total concurrent job count stays under typical GitHub Actions runner limits
</success_criteria>

<output>
After completion, create `.planning/phases/55-ci-optimization-consistency-audit/55-002-SUMMARY.md`
</output>