Disciplines · Audits

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

Move services/concordia (1 orchestrator project), services/metis (Python FastAPI service: alembic, openapi export, own Dockerfiles), and

3sections3 minread

On this page

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.json118 alias path entries; pnpm-workspace.yamlservices/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/psycheapps/psyche, services/concordiaapps/concordia, services/metisapps/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.