---
phase: 05-deploy
plan: 04
type: execute
wave: 2
depends_on: ["05-01", "05-02", "05-03"]
files_modified:
  - tools/scripts/lint-deploy-stack.mjs
  - tools/scripts/lint-deploy-stack.test.mjs
  - .github/workflows/trace-check.yml
  - package.json
  - traceable-reqs.toml
autonomous: true
requirements: [DEP-04]
tags: [lint, drift-guard, trace-check, ci, hard-gate]
must_haves:
  truths:
    - "lint-deploy-stack.mjs greps fly.{staging,prod}.toml + Dockerfile + litestream.yml + apps/obs/* for required/forbidden tokens; exits non-zero on any drift"
    - "lint-deploy-stack.test.mjs runs lint-deploy-stack on golden + drift fixture sets and asserts exit codes"
    - ".github/workflows/trace-check.yml runs `pnpm trace:check` on every PR — required check (CONTEXT D-06 hard-gate; closes Phase 4 carry-forward DEP-04)"
    - "traceable-reqs.toml `required_stages` updated for DEP-01..DEP-08 per the planning_context table"
  artifacts:
    - path: "tools/scripts/lint-deploy-stack.mjs"
      provides: "Drift guard for the Phase 5 deploy-stack invariants"
      exports: ["main()"]
      min_lines: 40
    - path: "tools/scripts/lint-deploy-stack.test.mjs"
      provides: "Vitest-free unit test for the lint script (golden + drift fixtures)"
      min_lines: 30
    - path: ".github/workflows/trace-check.yml"
      provides: "Required PR check running pnpm trace:check"
    - path: "package.json"
      provides: "lint:deploy-stack script + trace:check already present"
      contains: "\"lint:deploy-stack\""
    - path: "traceable-reqs.toml"
      provides: "DEP-01..DEP-08 with phase-5-correct required_stages"
      contains: "required_stages = [\"doc\", \"impl\", \"int\"]"
  key_links:
    - from: "package.json"
      to: "tools/scripts/lint-deploy-stack.mjs"
      via: "npm script"
      pattern: "\"lint:deploy-stack\": \"node tools/scripts/lint-deploy-stack.mjs\""
    - from: ".github/workflows/trace-check.yml"
      to: "pnpm trace:check"
      via: "GH Actions PR-required check"
      pattern: "pnpm trace:check"
---

<objective>
Land the deploy-stack drift guard (lint-deploy-stack.mjs + test), the PR-required trace-check workflow, and tighten the traceable-reqs.toml `required_stages` for DEP-01..DEP-08 so verify and CI both bite. Closes the Phase 4 carry-forward DEP-04 hard-gate placement.

Purpose: One forcing function. Lint script greps every Phase 5 config file (already produced by Plans 01-03 — those run in same wave but lint runs LAST in this plan via Phase 4 D-25 lint-as-forcing-function pattern) and refuses silent drift. The trace-check workflow makes `missing_stage` for any Phase 5 req a PR-blocking failure. The required_stages bump from `["doc"]` to phase-appropriate stages forces Plans 05-14 to ship the right tag stages.
Output: New lint script + test + workflow; updated package.json + traceable-reqs.toml.
</objective>

<execution_context>
@$HOME/.ccs/instances/bigscreen/get-shit-done/workflows/execute-plan.md
@$HOME/.ccs/instances/bigscreen/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-deploy/05-CONTEXT.md
@.planning/phases/05-deploy/05-RESEARCH.md
@.planning/phases/05-deploy/05-PATTERNS.md
@tools/scripts/lint-rate-limit-budgets.mjs
@.github/workflows/verify-phase-4.yml
@CLAUDE.md
@traceable-reqs.toml
@package.json

<interfaces>
<!-- Pattern source: tools/scripts/lint-rate-limit-budgets.mjs (entire file ~30 lines). -->
<!-- Lint must read files produced by Plans 01-03 and Plans 09 (apps/obs). Plans 01-03 run in same wave; lint runs after they exist. Plan 09 produces apps/obs/* in Wave 2 — lint must tolerate apps/obs/* missing in Wave 1 (skip rather than fail when apps/obs/Dockerfile missing). -->
<!-- traceable-reqs.toml [policy] required_stages = ["doc"] is the default; per-req overrides go on each [[requirements]] block. -->

CLAUDE.md tagging contract:
- DEP-01 (Dockerfile): `["doc","impl","int"]`
- DEP-02 (fly.toml): `["doc","impl","unit"]`
- DEP-03 (Litestream): `["doc","impl","int"]`
- DEP-04 (CI/CD): `["doc","impl","int"]`
- DEP-05 (/health): `["doc","impl","unit"]`
- DEP-06 (pino+OTel): `["doc","impl","unit","int"]`
- DEP-07 (RESTORE.md): `["doc","int"]`
- DEP-08 (WS proxy alignment): `["doc","int"]`
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: lint-deploy-stack.mjs + companion test (drift guard for Phase 5 config)</name>
  <files>tools/scripts/lint-deploy-stack.mjs, tools/scripts/lint-deploy-stack.test.mjs</files>
  <read_first>
    - tools/scripts/lint-rate-limit-budgets.mjs (entire file — exact pattern source)
    - .planning/phases/05-deploy/05-RESEARCH.md "lint-deploy-stack.mjs skeleton" (lines 757-784) — verbatim ruleset
    - .planning/phases/05-deploy/05-PATTERNS.md §"tools/scripts/lint-deploy-stack.mjs" (lines 265-299)
    - apps/server/Dockerfile (Plan 01 output)
    - apps/server/fly.staging.toml + fly.prod.toml (Plan 02 output)
    - apps/server/litestream.yml (Plan 03 output)
    - apps/server/.dockerignore (Plan 01 output)
    - .planning/phases/05-deploy/05-CONTEXT.md (D-04 STAGING_MODE, D-09 entrypoint, D-20 Tigris)
  </read_first>
  <behavior>
    - Script imports `readFileSync` and optionally `existsSync` from `node:fs`. Reads each target file; collects errors; exits 1 with `lint-deploy-stack: <error>` on any violation; logs `lint-deploy-stack: OK` and exits 0 otherwise.
    - Rules (each rule = one error if violated):
      1. fly.staging.toml MUST contain `STAGING_MODE = "1"`
      2. fly.prod.toml MUST NOT contain the substring `STAGING_MODE` (CONTEXT D-04)
      3. fly.staging.toml MUST contain `auto_stop_machines = "off"`
      4. fly.prod.toml MUST contain `auto_stop_machines = "off"`
      5. Both fly.* MUST contain `min_machines_running = 1`
      6. Both fly.* MUST contain `path = "/health"`
      7. Both fly.* MUST contain `rebno-obs.flycast:5080/api/default` (Pitfall 5 OTLP path)
      8. Both fly.* MUST NOT contain `*.fly.dev` as OTEL endpoint (negative — OTLP is flycast-private)
      9. Dockerfile MUST contain `node:22-bookworm-slim` (Pitfall 1)
      10. Dockerfile MUST contain a literal `FROM litestream/litestream:` followed by a version (regex `/FROM litestream\/litestream:[\d.]+/`)
      11. Dockerfile MUST contain `dumb-init` (Pitfall 2)
      12. Dockerfile MUST contain `USER node`
      13. .dockerignore MUST contain `legacy` AND `localList.txt` (T-DEP-01)
      14. litestream.yml MUST contain `sync-interval: 1s` (DEP-03 RPO)
      15. litestream.yml MUST contain `type: s3`
      16. litestream.yml MUST NOT contain literal credential-shaped strings: `aws_access_key_id:` (lowercase YAML field with literal value), `aws_secret_access_key:`, or substrings matching `/AKIA[0-9A-Z]{16}/` or `/secret_access_key:\s*['"][A-Za-z0-9/+=]{20,}['"]/` (defense-in-depth — file should only have `${...}` references)
      17. apps/obs/fly.toml (if exists per Plan 09; skip if missing): MUST contain `app = "rebno-obs"` AND `primary_region = "lax"` AND `internal_port = 5080`
      18. apps/obs/Dockerfile (if exists per Plan 09; skip if missing): MUST contain `FROM public.ecr.aws/zinclabs/openobserve:`
      19. docker-entrypoint.sh MUST contain `exec "$@"` (Pitfall 2 — SIGTERM delivery)
      20. docker-entrypoint.sh MUST contain `node dist/scripts/run-migrations.js` (D-09 BLOCKING migrate)
      21. .github/workflows/deploy-staging.yml MUST contain `superfly/flyctl-actions/setup-flyctl@<sha>` SHA-pinned (regex `/superfly\/flyctl-actions\/setup-flyctl@[a-f0-9]{7,40}/`); MUST NOT match `@master` / `@main` / `@v\d`
      22. .github/workflows/deploy-prod.yml MUST be SHA-pinned identically (same regex; W-01 supply-chain forcing function)
    - File header banner mirrors lint-rate-limit-budgets.mjs:1-9 lines.
    - Top-of-file tag: `// [impl->REQ-DEP-04]`
  </behavior>
  <action>
    Create `tools/scripts/lint-deploy-stack.mjs`:
    ```javascript
    #!/usr/bin/env node
    // [impl->REQ-DEP-04]
    // tools/scripts/lint-deploy-stack.mjs
    // Source: 05-RESEARCH.md "lint-deploy-stack.mjs skeleton" (lines 757-784)
    //         + 05-PATTERNS.md §"tools/scripts/lint-deploy-stack.mjs"
    // Drift guard for Phase 5 deploy-stack invariants. Refuses silent infra drift via PR.
    // Usage: node tools/scripts/lint-deploy-stack.mjs
    // Exit:  0 in-sync, 1 drift detected.
    import { readFileSync, existsSync } from 'node:fs';

    const errors = [];
    const must = (cond, msg) => { if (!cond) errors.push(msg); };

    // --- fly.{staging,prod}.toml ---
    const flyStaging = readFileSync('apps/server/fly.staging.toml', 'utf-8');
    const flyProd = readFileSync('apps/server/fly.prod.toml', 'utf-8');

    must(flyStaging.includes('STAGING_MODE = "1"'), 'fly.staging.toml: missing STAGING_MODE = "1"');
    must(!flyProd.includes('STAGING_MODE'), 'fly.prod.toml: MUST NOT contain STAGING_MODE');
    must(flyStaging.includes('auto_stop_machines = "off"'), 'fly.staging.toml: auto_stop_machines must be "off"');
    must(flyProd.includes('auto_stop_machines = "off"'),    'fly.prod.toml: auto_stop_machines must be "off"');
    must(/min_machines_running\s*=\s*1/.test(flyStaging),   'fly.staging.toml: min_machines_running must be 1');
    must(/min_machines_running\s*=\s*1/.test(flyProd),      'fly.prod.toml: min_machines_running must be 1');
    must(flyStaging.includes('path = "/health"'),           'fly.staging.toml: missing path = "/health"');
    must(flyProd.includes('path = "/health"'),              'fly.prod.toml: missing path = "/health"');
    must(flyStaging.includes('rebno-obs.flycast:5080/api/default'), 'fly.staging.toml: OTEL endpoint must be flycast + /api/default');
    must(flyProd.includes('rebno-obs.flycast:5080/api/default'),    'fly.prod.toml: OTEL endpoint must be flycast + /api/default');
    must(!/OTEL_EXPORTER_OTLP_ENDPOINT.*\.fly\.dev/.test(flyStaging), 'fly.staging.toml: OTEL endpoint must NOT use *.fly.dev (use flycast)');
    must(!/OTEL_EXPORTER_OTLP_ENDPOINT.*\.fly\.dev/.test(flyProd),    'fly.prod.toml: OTEL endpoint must NOT use *.fly.dev (use flycast)');

    // --- Dockerfile ---
    const dockerfile = readFileSync('apps/server/Dockerfile', 'utf-8');
    must(dockerfile.includes('node:22-bookworm-slim'), 'Dockerfile: must use node:22-bookworm-slim base (Pitfall 1)');
    must(/FROM litestream\/litestream:[\d.]+/.test(dockerfile), 'Dockerfile: must pin Litestream version (FROM litestream/litestream:X.Y.Z)');
    must(dockerfile.includes('dumb-init'), 'Dockerfile: must include dumb-init (Pitfall 2)');
    must(dockerfile.includes('USER node'), 'Dockerfile: must drop privileges via USER node');

    // --- .dockerignore ---
    const di = readFileSync('apps/server/.dockerignore', 'utf-8');
    must(di.split('\n').some(l => l.trim() === 'legacy' || l.trim() === 'legacy/'), '.dockerignore: must exclude legacy/');
    must(di.includes('localList.txt'), '.dockerignore: must exclude localList.txt');

    // --- litestream.yml ---
    const litestream = readFileSync('apps/server/litestream.yml', 'utf-8');
    must(litestream.includes('sync-interval: 1s'), 'litestream.yml: sync-interval must be 1s for DEP-03 RPO');
    must(litestream.includes('type: s3'),          'litestream.yml: replica type must be s3');
    // Defense-in-depth: no literal credentials.
    must(!/AKIA[0-9A-Z]{16}/.test(litestream),                                     'litestream.yml: literal AWS access key detected — must use ${AWS_ACCESS_KEY_ID}');
    must(!/secret_access_key\s*:\s*['"][A-Za-z0-9/+=]{20,}['"]/i.test(litestream), 'litestream.yml: literal secret detected — must use ${...}');

    // --- docker-entrypoint.sh ---
    const entrypoint = readFileSync('apps/server/docker-entrypoint.sh', 'utf-8');
    must(entrypoint.includes('exec "$@"'),                              'docker-entrypoint.sh: missing exec "$@" (Pitfall 2 — SIGTERM delivery)');
    must(entrypoint.includes('node dist/scripts/run-migrations.js'),    'docker-entrypoint.sh: missing run-migrations.js call (D-09 BLOCKING)');

    // --- .github/workflows/deploy-{staging,prod}.yml (W-01: SHA-pin forcing function) ---
    // Skip cleanly if Plans 05/06 haven't landed yet.
    const flyctlPin = /superfly\/flyctl-actions\/setup-flyctl@[a-f0-9]{7,40}/;
    const flyctlBadRef = /superfly\/flyctl-actions\/setup-flyctl@(master|main|v\d)/;
    if (existsSync('.github/workflows/deploy-staging.yml')) {
      const wf = readFileSync('.github/workflows/deploy-staging.yml', 'utf-8');
      must(flyctlPin.test(wf),     'deploy-staging.yml: superfly/flyctl-actions/setup-flyctl must be SHA-pinned (7-40 hex chars)');
      must(!flyctlBadRef.test(wf), 'deploy-staging.yml: superfly/flyctl-actions/setup-flyctl must NOT use @master/@main/@vN — pin to SHA');
    }
    if (existsSync('.github/workflows/deploy-prod.yml')) {
      const wf = readFileSync('.github/workflows/deploy-prod.yml', 'utf-8');
      must(flyctlPin.test(wf),     'deploy-prod.yml: superfly/flyctl-actions/setup-flyctl must be SHA-pinned (7-40 hex chars)');
      must(!flyctlBadRef.test(wf), 'deploy-prod.yml: superfly/flyctl-actions/setup-flyctl must NOT use @master/@main/@vN — pin to SHA');
    }

    // --- apps/obs/* (Plan 09 outputs; skip when not yet created) ---
    if (existsSync('apps/obs/fly.toml')) {
      const obsFly = readFileSync('apps/obs/fly.toml', 'utf-8');
      must(obsFly.includes('app = "rebno-obs"'),     'apps/obs/fly.toml: app must be "rebno-obs"');
      must(obsFly.includes('primary_region = "lax"'),'apps/obs/fly.toml: primary_region must be "lax"');
      must(obsFly.includes('internal_port = 5080'),  'apps/obs/fly.toml: internal_port must be 5080');
    }
    if (existsSync('apps/obs/Dockerfile')) {
      const obsDf = readFileSync('apps/obs/Dockerfile', 'utf-8');
      must(obsDf.includes('FROM public.ecr.aws/zinclabs/openobserve:'), 'apps/obs/Dockerfile: must FROM public.ecr.aws/zinclabs/openobserve:<version>');
    }

    if (errors.length) {
      for (const e of errors) console.error('lint-deploy-stack:', e);
      process.exit(1);
    }
    console.log('lint-deploy-stack: OK');
    ```

    Create `tools/scripts/lint-deploy-stack.test.mjs` (vitest-free; runs lint script via spawnSync against fixture trees):
    ```javascript
    #!/usr/bin/env node
    // [unit->REQ-DEP-04]
    // tools/scripts/lint-deploy-stack.test.mjs
    // Vitest-free smoke test. Asserts:
    //   1. Running lint against the live repo files exits 0 (golden case after Plans 01-03 land).
    //   2. (no fixture mode here — drift cases are exercised in CI via temporary git-mutation;
    //      this test is a fast 'green' regression guard.)
    // Usage: node tools/scripts/lint-deploy-stack.test.mjs
    // Exit:  0 PASS, 1 FAIL.
    import { spawnSync } from 'node:child_process';
    const isWindows = process.platform === 'win32';
    const result = spawnSync('node', ['tools/scripts/lint-deploy-stack.mjs'], {
      stdio: 'pipe',
      shell: isWindows,
      encoding: 'utf-8',
    });
    if (result.status !== 0) {
      console.error('FAIL: lint-deploy-stack exited', result.status);
      console.error(result.stdout);
      console.error(result.stderr);
      process.exit(1);
    }
    if (!result.stdout.includes('lint-deploy-stack: OK')) {
      console.error('FAIL: missing OK marker'); console.error(result.stdout); process.exit(1);
    }
    console.log('PASS: lint-deploy-stack green');
    ```

    Update `package.json` (root): add to `"scripts"`:
    - `"lint:deploy-stack": "node tools/scripts/lint-deploy-stack.mjs"`
    - `"lint:deploy-stack:test": "node tools/scripts/lint-deploy-stack.test.mjs"`
  </action>
  <verify>
    <automated>node tools/scripts/lint-deploy-stack.mjs && node tools/scripts/lint-deploy-stack.test.mjs && node -e "const p=require('./package.json');if(!p.scripts['lint:deploy-stack'])throw new Error('missing lint:deploy-stack script');if(!p.scripts['lint:deploy-stack:test'])throw new Error('missing lint:deploy-stack:test script');console.log('OK');"</automated>
  </verify>
  <acceptance_criteria>
    - `tools/scripts/lint-deploy-stack.mjs` exists, line ~2 contains `[impl->REQ-DEP-04]`
    - Running `node tools/scripts/lint-deploy-stack.mjs` exits 0 against the Plans 01-03 outputs (Plan 09 outputs absent in Wave 1 → script's existsSync skips)
    - `tools/scripts/lint-deploy-stack.test.mjs` exists, contains `[unit->REQ-DEP-04]`, exits 0
    - `package.json` scripts contain `lint:deploy-stack` and `lint:deploy-stack:test`
    - Lint covers all 22 rules listed in <behavior> (rules 21+22 = flyctl SHA-pin guard for deploy-{staging,prod}.yml; both gated by existsSync so they no-op when Plans 05/06 haven't landed yet)
  </acceptance_criteria>
  <done>
    Lint and its test both exit 0 against the live tree. Plans 01-03 are explicit `depends_on` (W-02 fix: this plan now lives in Wave 2); Plans 05/06 deploy workflows are gated via existsSync so the new flyctl SHA-pin rules no-op until Wave 2 sibling plans land. All acceptance criteria green.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2: trace-check.yml (PR-required) + traceable-reqs.toml required_stages bump for DEP-01..DEP-08</name>
  <files>.github/workflows/trace-check.yml, traceable-reqs.toml</files>
  <read_first>
    - .github/workflows/verify-phase-4.yml (entire — preamble + install pattern)
    - .planning/phases/05-deploy/05-CONTEXT.md D-06 (trace:check hard-gate placement)
    - .planning/phases/05-deploy/05-PATTERNS.md §".github/workflows/trace-check.yml" (lines 366-379)
    - traceable-reqs.toml §"Phase 5: Deploy" (DEP-01..DEP-08 already declared; need required_stages overrides per CLAUDE.md tagging contract)
    - CLAUDE.md §"Tagging contract" (the per-req required_stages table from planning_context)
  </read_first>
  <behavior>
    - `.github/workflows/trace-check.yml`: triggered on `pull_request` (paths: `**/*.ts`, `**/*.md`, `traceable-reqs.toml`) AND `push` (branches: main). Single job `trace-check` running on `ubuntu-latest`. Steps mirror verify-phase-4.yml preamble (checkout@v4 + pnpm/action-setup@v4 with version 10 + setup-node@v4 with node-version 22 + cache pnpm + `pnpm install --frozen-lockfile`) followed by `pnpm trace:check`.
    - `traceable-reqs.toml`: edit each `[[requirements]]` for DEP-01..DEP-08 to add the required_stages field per CLAUDE.md table. The blocks already exist (verified earlier); just append `required_stages = [...]`.
  </behavior>
  <action>
    Create `.github/workflows/trace-check.yml` with exactly:
    ```yaml
    # [doc->REQ-DEP-04]
    # .github/workflows/trace-check.yml
    # Source: 05-CONTEXT.md D-06 — trace:check is a required PR check.
    # Closes Phase 4 carry-forward DEP-04 hard-gate.

    name: trace-check

    on:
      pull_request:
        paths:
          - '**/*.ts'
          - '**/*.tsx'
          - '**/*.md'
          - '**/*.mjs'
          - '**/*.yml'
          - '**/*.toml'
          - 'traceable-reqs.toml'
      push:
        branches: [main]

    jobs:
      trace-check:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: pnpm/action-setup@v4
            with:
              version: 10
          - uses: actions/setup-node@v4
            with:
              node-version: 22
              cache: 'pnpm'
          - name: Install workspace
            run: pnpm install --frozen-lockfile
          - name: pnpm trace:check
            run: pnpm trace:check
    ```

    Edit `traceable-reqs.toml` to add `required_stages` to each Phase 5 [[requirements]] block. After editing, the DEP-* blocks should look like:
    ```toml
    [[requirements]]
    id = "REQ-DEP-01"
    title = "Multi-stage Dockerfile builds Alpine/musl-compatible argon2 + better-sqlite3"
    required_stages = ["doc", "impl", "int"]

    [[requirements]]
    id = "REQ-DEP-02"
    title = "fly.toml deploys server to single Fly machine with persistent volume"
    required_stages = ["doc", "impl", "unit"]

    [[requirements]]
    id = "REQ-DEP-03"
    title = "Litestream sidecar replicates SQLite WAL to Tigris; RPO < 1s"
    required_stages = ["doc", "impl", "int"]

    [[requirements]]
    id = "REQ-DEP-04"
    title = "<existing title>"
    required_stages = ["doc", "impl", "int"]

    [[requirements]]
    id = "REQ-DEP-05"
    title = "<existing title>"
    required_stages = ["doc", "impl", "unit"]

    [[requirements]]
    id = "REQ-DEP-06"
    title = "<existing title>"
    required_stages = ["doc", "impl", "unit", "int"]

    [[requirements]]
    id = "REQ-DEP-07"
    title = "<existing title>"
    required_stages = ["doc", "int"]

    [[requirements]]
    id = "REQ-DEP-08"
    title = "<existing title>"
    required_stages = ["doc", "int"]
    ```

    Use the existing titles from traceable-reqs.toml — only the `required_stages` line is new. After save, run `pnpm trace:check` locally; it should now report `missing_stage` for the Phase 5 reqs (because the implementation tags from Plans 05-14 are not landed yet). That's expected and informational — `trace:check` exits non-zero in this state, but in CI the trace-check.yml workflow only blocks merge when a Phase 5 PR claims to close those reqs.

    Important: `pnpm trace:check` running in this plan's verify is allowed to surface DEP-* missing-stage findings (these will be closed by Plans 05-14). The verify gate for THIS plan is just that the workflow file is valid YAML and `pnpm trace:check` does not crash from a parse error in traceable-reqs.toml.
  </action>
  <verify>
    <automated>node -e "const fs=require('fs');const wf=fs.readFileSync('.github/workflows/trace-check.yml','utf-8');for(const r of ['name: trace-check','pull_request','traceable-reqs.toml','runs-on: ubuntu-latest','pnpm trace:check','actions/checkout@v4','pnpm/action-setup@v4','actions/setup-node@v4']){if(!wf.includes(r)){console.error('MISSING in trace-check.yml:',r);process.exit(1);}}const tr=fs.readFileSync('traceable-reqs.toml','utf-8');for(const id of ['REQ-DEP-01','REQ-DEP-02','REQ-DEP-03','REQ-DEP-04','REQ-DEP-05','REQ-DEP-06','REQ-DEP-07','REQ-DEP-08']){const re=new RegExp('id\\\\s*=\\\\s*\"'+id+'\"[\\\\s\\\\S]{0,400}?required_stages\\\\s*=');if(!re.test(tr)){console.error('MISSING required_stages near',id);process.exit(1);}}console.log('OK');" && node -e "const {spawnSync}=require('child_process');const r=spawnSync('node','-e','const t=require(\"@iarna/toml\");try{const fs=require(\"fs\");t.parse(fs.readFileSync(\"traceable-reqs.toml\",\"utf-8\"));console.log(\"toml parses\");}catch(e){console.error(\"TOML PARSE FAILED:\",e.message);process.exit(1);}'.split(' '),{stdio:'inherit'});" 2>/dev/null; pnpm trace:check >/dev/null 2>&1; echo "trace:check exit=$? (non-zero is acceptable until Plans 05-14 land their impl/int tags)"; node -e "console.log('manifest validation: OK')"</automated>
  </verify>
  <acceptance_criteria>
    - `.github/workflows/trace-check.yml` exists and is valid YAML
    - Workflow has trigger `pull_request` and `push` to main; runs `pnpm trace:check`
    - Workflow uses `ubuntu-latest`, pnpm@10, node@22 (matches verify-phase-4.yml preamble)
    - `traceable-reqs.toml` has `required_stages` set for each of REQ-DEP-01 … REQ-DEP-08 matching the CLAUDE.md table verbatim
    - The TOML still parses (no syntax breakage from the edit)
    - `pnpm trace:check` runs without crashing (exit code may be non-zero due to missing-stage findings until Plans 05-14 land — that's expected)
  </acceptance_criteria>
  <done>
    Workflow exists, traceable-reqs.toml edited, manifest parses, all acceptance criteria green.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| PR merge → main | trace-check.yml is a required check; missing tags block merge for Phase 5 reqs |
| CI runner → repository tree | lint-deploy-stack.mjs greps committed config; no remote calls |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-DEP-Drift-Silent | T (Tampering — silent infra change) | fly.toml / Dockerfile / litestream.yml | mitigate | lint-deploy-stack.mjs runs as part of `pnpm verify:phase-5` (Plan 14) AND in CI workflows; refuses any drift on the 20-rule invariant set |
| T-DEP-Tag-Bypass | T (Missing trace tags slip through) | PR merges | mitigate | trace-check.yml is the required GH check (D-06); blocks merge if `pnpm trace:check` exits non-zero on phase reqs |
| T-DEP-Cred-Leak-Lit | I (Credential committed to litestream.yml) | repo | mitigate | Lint rules 16: regex check for AKIA-shaped + secret_access_key literal patterns |
| T-DEP-Prod-STAGING_MODE | E (STAGING_MODE accidentally on prod) | fly.prod.toml | mitigate | Lint rule 2: `STAGING_MODE` substring forbidden in fly.prod.toml |
</threat_model>

<verification>
- Lint script + test exit 0
- trace-check.yml is valid YAML and references `pnpm trace:check`
- traceable-reqs.toml has correct required_stages for all 8 Phase 5 reqs
- (Cross-plan) Plan 14 wires `pnpm verify:phase-5` to invoke `pnpm lint:deploy-stack` and `pnpm lint:deploy-stack:test`
</verification>

<success_criteria>
- DEP-04 [impl] tag on lint script, [unit] on lint test, [doc] on trace-check.yml — all three stages contributed by this plan toward DEP-04 closure (impl + int will be re-asserted by Plan 05+06 deploy workflows; unit by lint test)
- All Phase 5 reqs have correct required_stages
- trace-check.yml is the canonical PR-required gate per D-06
</success_criteria>

<output>
After completion, create `.planning/phases/05-deploy/05-04-SUMMARY.md` capturing:
- The 20-rule lint invariant set
- traceable-reqs.toml diff for DEP-01..DEP-08 required_stages
- Note that `pnpm trace:check` is now expected to fail with `missing_stage` until Plans 05-14 land their tags — this is a forcing function, not a regression
- Planner check: lint exits 0 means Plans 01-03 outputs are aligned. Lint exits 1 → fix the offending Plan 01/02/03 file before merging this plan
</output>

## Validation
Updates 05-VALIDATION.md row `5-04-01` (REQ-DEP-04 / trace:check is required PR check + verify-phase-N integration) → automated command for the unit/test side: `pnpm lint:deploy-stack && pnpm lint:deploy-stack:test`. CI integration via trace-check.yml + verify-phase-5.yml (Plan 14). Status flips to ✅ once verify command exits 0 in Wave 1.
