# services/ → apps/ Migration Runbook (audit B-5/R-9)

Prepared 2026-07-18 so the owner's deploy-verified window is an execution, not a
discovery project. **Why this is deploy-gated and not auto-executed:**
`deploy-hetzner.yml` **auto-fires on push to `main`** with path filters that
include `services/psyche/**`, `libs/**`, and its own workflow file — a move
commit both matches the old filters (deleted paths) and rewrites the pipeline it
triggers, unwatched. The Hetzner pipeline is also under active development in a
parallel session (`oshun-lt-2-hetzner`). Execute this in a window where a failed
deploy is observable and reversible.

## Scope

Move `services/concordia` (1 orchestrator project), `services/metis` (Python
FastAPI service: alembic, openapi export, own Dockerfiles), and
`services/psyche` (~14 sub-services with project.json + Dockerfiles) into
`apps/`. Note `apps/metis/*` already exists (admin/api-gateway/mobile/web/
worker are the TS surfaces); `services/metis` is the _Python backend_ — land it
as `apps/metis/service` (recommended) to avoid a name collision, and land
psyche/concordia as `apps/psyche`, `apps/concordia`.

## Complete live-reference inventory (79 tracked files; regenerate with the

command below before executing — parallel sessions add references)

```bash
git grep -l -E "services/(concordia|metis|psyche)" \
  | grep -v -E "^(docs/|evidence/|TODOS/|V[0-9]|WALKTHROUGH)"
```

By category:

1. **Workflows (6):** `deploy-ecs.yml` (dispatch-only; psyche path map lines
   ~177/253), `deploy-hetzner.yml` (**auto-fire**; filter line ~49 + path maps
   ~190/285), `metis-e2e.yml`, `prompt-injection-portfolio.yml` (pip install
   `-e services/metis[dev]`), `psyche-ci.yml` (~10 glob sites),
   `psyche-container-build.yml`.
2. **Build/deploy config:** `infrastructure/terraform/variables.tf` (comment
   ~line 172), `deploy/v1/hetzner/docker-compose.yml` (2 comment references),
   per-service Dockerfiles under the moved trees (check `COPY`/`WORKDIR`
   relative-depth assumptions — psyche Dockerfiles build with own contexts).
3. **Workspace config:** `tsconfig.base.json` — **118 alias path entries**;
   `pnpm-workspace.yaml` — `services/concordia/*` glob (metis/psyche are Python,
   not pnpm members); `nx.json` `workspaceLayout.servicesDir` (drop it);
   `package.json` (1 script reference); `pnpm-lock.yaml` importer keys for
   `services/concordia/*` (regenerated by `pnpm install --lockfile-only`).
4. **Nx projects:** every `project.json` inside the moved trees carries
   `sourceRoot`/target cwds with `services/...` paths (~16 files).
5. **Cross-references (~25 lib/app files):** hardcoded service paths in
   `libs/openapi/scripts/generate.ts` + generated `metis.ts`, `libs/metis/*`
   transports/tests, `libs/calliope/bridge`, `libs/nous/advanced-speech` (2),
   `libs/neith/cloud`, `libs/v9/*` (2), `libs/oshun/*` (2),
   `apps/metis/admin/e2e` fixtures, `apps/oshun/web` Concordia adapter,
   `scripts/verify-phase-25-completion.mjs`, `services/metis` self-references
   (openapi export, admin service, tests), psyche READMEs/self-refs.
6. **Guards to update:** `tools/docs/verify-domains-source-of-truth.mjs`
   `CODE_ROOT_ALLOWLIST` (keys `services/metis`, `services/psyche`; the
   `REQUIRED_PLANNED_ROOTS` needles reference TODO _text_ and stay);
   `tools/env/split-env-examples.mjs` derives per-app env files — moved trees
   become apps and must emit their `.env.example` (run `--check`).
7. **Leave alone:** `docs/`, `evidence/`, `TODOS/`, `V*/`, `DEPENDENCIES.md`,
   `content-coverage/seed-registry.v1.json` (historical/regenerable records —
   regenerate the seed registry rather than editing).

## Execution order (one change, one push)

1. Freeze window: confirm no parallel session is mid-flight on
   `deploy-hetzner.yml` / `oshun-lt-2-*` branches; announce the window.
2. `git mv services/concordia apps/concordia && git mv services/psyche apps/psyche && git mv services/metis apps/metis/service`.
3. Mechanical rewrite over the categories above: `services/psyche` →
   `apps/psyche`, `services/concordia` → `apps/concordia`, `services/metis` →
   `apps/metis/service` (careful: do NOT blind-sed the deploy workflows'
   `apps/metis/*` lines that already exist).
4. Drop `workspaceLayout.servicesDir` from nx.json; update pnpm-workspace glob;
   `pnpm install --lockfile-only`.
5. Static verification, all green before push:
   - `npx nx show project` for each moved project (graph resolves);
   - `pnpm nx affected -t lint --files=<moved>` dry pass;
   - spot-`tsc` two consumers of the 118 aliases;
   - psyche pytest per `reference_devbox_toolchains` (208 baseline) + metis
     pytest; concordia orchestrator suite;
   - glob-test every changed workflow path filter against `git ls-files` (the
     audit G-2 method);
   - `node tools/env/split-env-examples.mjs --check`;
   - `node tools/docs/verify-domains-source-of-truth.mjs`;
   - `act`-style dry parse of deploy-hetzner detection script with a synthetic
     changed-file list covering each service class.
6. Push during the window; watch the auto-fired `deploy-hetzner` run to
   completion (or confirm it no-ops on filter mismatch); dispatch a `deploy-ecs`
   staging run for one psyche service to validate the ECS map.
7. Rollback: single revert commit restores every path; images/deploys are
   immutable-tagged so reverting the repo reverts the pipeline inputs.
