Skip to content

PLAN — On-prem deployment pivot: B1 co-locate cocoindex + pullmd on one host (ID-66.4)

PLAN — On-prem deployment pivot: B1 co-locate cocoindex + pullmd on one host (ID-66.4)

Section titled “PLAN — On-prem deployment pivot: B1 co-locate cocoindex + pullmd on one host (ID-66.4)”

Task: ID-66 — On-prem deployment pivot — B1 co-locate cocoindex + pullmd on one host. Subtask: {66.4} PLAN (decomposition into {66.5+} implementation Subtasks). Author date: 29/05/2026. Predecessors (ratified): RESEARCH.md ({66.1}), PRODUCT.md ({66.2}, 25 numbered behaviour invariants), TECH.md ({66.3}, 9 Proposed changes + 10-step migration ordering + invariant coverage map), OQ-CLOSURE.md (OQ-66-1/2/3/4/6 closed). Authored by a fresh Planner instance per Q-PLANNER-2 — PRODUCT.md and TECH.md were read in full as input, not assumed.

Filename convention (ID-48.4): canonical {N.4} artefact is PLAN.md. This is the decomposition only — the Subtask records below are drafted inside this document for the parent to transcribe into docs/reference/task-list.json verbatim. PLAN does not edit task-list.json, the roadmap, the backlog, or any code.


B1 re-platforms the ingestion pipeline only (cocoindex + pullmd + Playwright/Trafilatura sidecars) off Google Cloud Run onto one IONOS VPS managed by Coolify. The product app (Vercel + managed Supabase) is untouched — there is no app cutover and no KH application code change anywhere. The whole decomposition is infrastructure/ops work: Compose YAML, a thin GitHub Action, persistent volumes, secret migration, a backup cron, a datapath monitor, and host/DNS config. The only KH-corpus seam is the single inbound pipeline-runs/record webhook, which is repointed by env var only (no source change, OQ-66-2).

This PLAN maps the TECH 10-step migration ordering into 14 implementation Subtasks ({66.5}{66.18}), comfortably under the 25-Subtask soft ceiling. The heaviest area (TECH change 4 — the thin GH Action + ghcr.io + Coolify deploy webhook + workflow retirement) is split into four discrete slices ({66.7} build-and-push, {66.11} Coolify webhook wiring, {66.12} rollback-by-tag, {66.17} workflow retirement). The kpf dead-manifest cleanup (change 9) is a clearly-labelled standalone side-task ({66.18}).

Code-intelligence orientation (cited verbatim per the planner-block)

Section titled “Code-intelligence orientation (cited verbatim per the planner-block)”

This is an infrastructure/ops artefact. The orientation tool was run against the KH corpus per .gitnexus/CLAUDE.md; the gitnexus index is STALE (last indexed b8d9f89), so file paths are authoritative and exact line numbers are treated as approximate.

  • gitnexus_query({query: "cocoindex pipeline deploy cloudrun", repo: "knowledge-hub"})"processes": [] (no execution flows indexed for this deploy surface — the pipeline has never run end-to-end, consistent with PRODUCT §Problem). It returned only definitions[]: pipeline + server symbols start_cocoindex_thread (scripts/cocoindex_pipeline/server.py:168-225), _target (scripts/cocoindex_pipeline/server.py:203-217), _build_db_ctx (scripts/cocoindex_pipeline/flow.py:651-673), _build_dsn (scripts/cocoindex_pipeline/flow.py:969-1005), app_main (scripts/cocoindex_pipeline/flow.py:1523-1867), plus a cluster of pipeline test modules under scripts/tests/. No execution process binds these — confirming the deploy/co-location surface is greenfield infra plumbing, not a re-wire of an indexed call graph.
  • External-library grounding (OQ-3 / Q-EX2): this artefact asserts no importable third-party library symbol shape as load-bearing, so no import-and-call check is required. The cocoindex→pullmd repoint is env-only (PULLMD_SERVICE_URL, scripts/cocoindex_pipeline/adapters.py:125, OQ-66-2). cocoindex[postgres]==1.0.3 is already empirically pinned (requirements.txt); no cocoindex symbol shape is asserted. mdb_copy is an LMDB CLI tool, not an importable symbol (MVCC hot-copy safety grounded in OQ-CLOSURE OQ-66-4). Coolify is a hosted-platform capability set, not an importable API (Compose build pack, per-app encrypted env, S3 DB backup, deploy webhook, tag-edit rollback, auto-Let’s-Encrypt — grounded in RESEARCH §2 + OQ-CLOSURE OQ-66-3/4/6).

Code-touching discipline note (for the Executor)

Section titled “Code-touching discipline note (for the Executor)”

These Subtasks are largely NON-code-touching in the KH sense — they create/edit Compose YAML, host config, Coolify env, a backup script, a monitor script, and documentation. The gitnexus impact-analysis discipline (gitnexus_impact before edit) is therefore mostly N/A because no KH Python/TS symbol is modified.

The one exception: any edit to .github/workflows/*.yml on the deploy path ({66.7}, {66.11}, {66.17}, {66.18}) — the Executor should still run gitnexus_detect_changes() before committing those, to confirm the change scope is the deploy workflow and nothing else.

The standing guard repeated in every Subtask details: NO KH application code changes — scripts/cocoindex_pipeline/adapters.py, scripts/cocoindex_pipeline/flow.py, and the Vercel route app/api/internal/pipeline-runs/record/route.ts are unchanged; every repoint is env-only (OQ-66-2). Do NOT “fix” the env-only repoint in source.


Cross-Task hand-off deliverable to ID-62 (UNMISSABLE — read this section)

Section titled “Cross-Task hand-off deliverable to ID-62 (UNMISSABLE — read this section)”

ID-62 (fixture-staging-infra) has Task.dependencies including ID-66. This dependency is a Task-level dependency (correct per §3.3 — cross-Task deps live at the Task level, NOT the Subtask level). ID-62 cannot begin its live-verification implementation until ID-66 delivers a reachable, datapath-proven host.

The Subtask that carries this hand-off deliverable is {66.16} — “First end-to-end ingest smoke + datapath proof + ID-62 reachability hand-off”. Its acceptance is the gate ID-62 depends on:

  1. The co-located host is running — all four containers up on one Coolify-managed compose network (invariant 1).
  2. The datapath is proven end-to-end — a real ingestion runs and a genuine pipeline_runs row lands in Supabase via the repointed webhook (invariants 8, 9, 21), proving the {62.1} reachability defect is dissolved (invariant 3) — the cocoindex→pullmd hop that could not route on Cloud Run no longer exists because the two share a host-local network.
  3. A host-facing HTTPS ingress surface is reachable — Coolify auto-Let’s-Encrypt serves the host over HTTPS, and an external client (e.g. a GitHub-hosted runner) can reach a host-facing endpoint over HTTPS — i.e. the ingress:internal/403 problem that blocked ID-62 from CI (its defect (2)) is gone because the host is publicly reachable.

The /stage boundary (OQ-66-8 — assumption documented, pending confirmation). Per the analysis appended to OQ-pending.md as ## OQ-66-8, ID-62’s /stage is a fixture-drop HTTP server that ID-62 itself builds (its {62.2}/{62.3} are PRODUCT/TECH for the /stage co-resident route; ID-62 found NO /stage handler exists yet — ID-49.10 built only the client helper). ID-66’s hand-off is therefore the reachable HTTPS host + the proven datapath + the provisioned ingress surface onto which ID-62 will later mount its own /stage route — ID-66 does NOT implement /stage (that would be KH/pipeline code, out of B1 scope per OQ-66-2).

Assumption adopted (OQ-66-8): “host reachable over HTTPS + datapath proven + ingress surface provisioned” is the ID-66→ID-62 hand-off. If Liam/parent confirms ID-66 must instead pre-provision something more specific (a reserved hostname/subdomain, a Coolify app slot, or a published COCOINDEX_FIXTURE_STAGING_URL base value), {66.16}’s acceptance widens accordingly. PLAN proceeds on the documented assumption; this is flagged in {66.16}’s details.


Subtask records (TM-shape — for the parent to transcribe into task-list.json)

Section titled “Subtask records (TM-shape — for the parent to transcribe into task-list.json)”

All id values are local to parent Task 66 (the ledger renders them as {66.5}{66.18}). dependencies are sibling-only integer arrays referencing other Subtasks of THIS Task. description ≤250 chars HARD, testStrategy ≤300 chars HARD (parent’s CLI budget gate REJECTS over-budget records). status initialises to pending.

{66.5} — Stand up IONOS VPS + base hardening

Section titled “{66.5} — Stand up IONOS VPS + base hardening”
  • id: 5
  • title: Stand up IONOS VPS + base hardening
  • description: Provision the IONOS VPS (8 vCPU / 16 GB / 480 GB NVMe, ~£21/mo per OQ-66-1) and apply base hardening — SSH key-only auth, firewall, OS updates. The host for the co-located pipeline.
  • details: TECH migration step 1; satisfies the host substrate for invariant 1 (single-host co-location). Provision the IONOS VPS per OQ-66-1 (8 vCPU / 16 GB / 480 GB NVMe). Harden: SSH key-only (disable password auth), firewall (allow only SSH + the HTTPS/HTTP ports Coolify needs), apply OS security updates. Record host IP + access in the new B1 runbook (proposed docs/runbooks/onprem-b1-deploy.md). NO KH application code changes — this is host provisioning only; adapters.py/flow.py/the Vercel route are untouched. No code-intel impact discipline (no KH symbol touched).
  • testStrategy: Operator SSHes into the IONOS VPS with key-only auth (password auth refused); nproc/free -g/df -h confirm 8 vCPU / 16 GB / 480 GB NVMe; firewall denies all but the intended ports; OS is patched.
  • dependencies: []
  • priority: high

{66.6} — Install Coolify + verify compose build pack

Section titled “{66.6} — Install Coolify + verify compose build pack”
  • id: 6
  • title: Install Coolify + verify compose build pack
  • description: Run the Coolify one-script install on the VPS; verify the dashboard loads and the Docker Compose build pack is available. Coolify is the PaaS layer replacing the Cloud Run scaffolding.
  • details: TECH migration step 2. Run the Coolify one-script install on the {66.5} host. Verify: the Coolify dashboard is reachable; the Docker Compose build pack is selectable (RESEARCH §2.3 — the direct fit for the existing pullmd compose unit); Coolify’s per-app encrypted env, S3-backup, deploy-webhook, and auto-Let’s-Encrypt capabilities are present (grounded OQ-66-3/4/6). Record dashboard URL + admin access in the B1 runbook. NO KH application code changes. No code-intel impact discipline (no KH symbol touched).
  • testStrategy: Coolify dashboard loads over HTTPS; an operator can create a new application using the Docker Compose build pack; per-app env, S3-backup, and deploy-webhook features are visible in the UI.
  • dependencies: [5]
  • priority: high

{66.7} — Thin GH Action: build cocoindex image off-host → push pinned to ghcr.io

Section titled “{66.7} — Thin GH Action: build cocoindex image off-host → push pinned to ghcr.io”
  • id: 7
  • title: GH Action — build off-host, push pinned image to ghcr.io
  • description: Author a thin GitHub Action that builds the ~9 GB cocoindex image on GH-hosted runners and pushes a pinned (sha/semver, never :latest) image to ghcr.io. The heavy build never runs on the 16 GB host.
  • details: TECH change 4 slice A; TECH migration step 6 (part). Satisfies invariants 13, 14 (push half). Create .github/workflows/onprem-deploy.yml. On push to the deploy branch (paths scripts/**/*.py, requirements.txt, deploy/onprem/**), build the cocoindex image on GitHub-hosted runners (the ~9 GB two-image build — cocoindex ~5.3 GB + Playwright ~3.7 GB — MUST NOT run on the 16 GB host; OQ-66-6). Push to ghcr.io under a pinned content-addressable tag (:sha-<gitsha> or :<semver>, never :latest — mirrors the existing pullmd no-:latest discipline). This Subtask covers build-and-push ONLY; the Coolify pull/deploy webhook is {66.11}. .yml edit → Executor MUST run gitnexus_detect_changes() before commit to confirm scope. NO KH application code changes — the build packages existing scripts/cocoindex_pipeline/*.py unchanged; do not edit source. NEXT_PUBLIC_CLIENT_ID is NOT a B1-host concern (OQ-66-6 — Vercel-side bake).
  • testStrategy: A push to the deploy branch triggers the Action; the build runs on a GH-hosted runner (not the host); a pinned, non-:latest image tag appears in ghcr.io and is pullable; the host CPU/RAM is uncontended during the build.
  • dependencies: []
  • priority: high

{66.8} — Author + deploy the co-location docker-compose

Section titled “{66.8} — Author + deploy the co-location docker-compose”
  • id: 8
  • title: Author + deploy the co-location docker-compose
  • description: Author deploy/onprem/docker-compose.yml declaring cocoindex + pullmd + Playwright + Trafilatura on one Coolify compose network; set PULLMD_SERVICE_URL=http://pullmd:3000 (env-only repoint). Deploy via Coolify.
  • details: TECH change 1; TECH migration step 3. Satisfies invariants 1, 2, 3, 4, 11. Create deploy/onprem/docker-compose.yml with four services on one Coolify-managed compose network: cocoindex (ghcr.io/<org>/kh-cocoindex-pipeline:<pinned-tag> from {66.7}), env PULLMD_SERVICE_URL=http://pullmd:3000 (compose alias — repoint is env-only, no source change per OQ-66-2), retain PULLMD_API_TOKEN Bearer as defence-in-depth, carry over COCOINDEX_SOURCE_PATH/COCOINDEX_DB/COCOINDEX_DB_DSN boot-required discipline; pullmd (aeternalabshq/pullmd:2.0.0 pinned, port 3000, PULLMD_AUTH_MODE=single-admin, PLAYWRIGHT_URL=http://playwright:8002/render, TRAFILATURA_URL=http://trafilatura:8001/extract); playwright (aeternalabshq/pullmd-playwright:2 — resolve :2→digest before deploy); trafilatura (aeternalabshq/pullmd-trafilatura:2 — same digest-pin). The LMDB/corpus volumes are mounted by {66.9}; secrets are injected by {66.10}. A /health healthcheck MAY be kept for container-restart only — it is explicitly NOT the datapath signal ({66.15}). .yml/compose only — NO KH application code changes; do not edit adapters.py.
  • testStrategy: Operator sees all four containers on one compose network; a Bearer-authed GET http://pullmd:3000/api?url=… issued from inside the cocoindex container returns a pullmd response over the alias (never a *.run.app URL); no ingress:internal/VPC/NAT in the path.
  • dependencies: [6, 7]
  • priority: high

{66.9} — Create + mount the LMDB persistent volume + corpus volume

Section titled “{66.9} — Create + mount the LMDB persistent volume + corpus volume”
  • id: 9
  • title: Create + mount LMDB persistent volume + corpus volume
  • description: Replace the Cloud Run in-RAM tmpfs with a disk-backed Docker named volume mounted at the cocoindex COCOINDEX_DB parent path on local NVMe; add the corpus volume. Set a generous COCOINDEX_LMDB_MAP_SIZE.
  • details: TECH change 2; TECH migration step 4. Satisfies invariants 5, 6, 7. In deploy/onprem/docker-compose.yml (from {66.8}), declare a Docker named volume / Compose volumes: mount on the host’s local NVMe, mounted at the cocoindex COCOINDEX_DB parent path (/cocoindex-state, so the engine store lands at /cocoindex-state/lmdb). The volume is disk-backed, NOT tmpfs (local NVMe mmaps + locks LMDB natively — LMDB’s hard requirement; GCS-FUSE never could). Add the corpus volume for COCOINDEX_SOURCE_PATH. Set COCOINDEX_LMDB_MAP_SIZE generously (a few GiB — sparse virtual reservation, not disk pre-alloc) to avoid MDB_MAP_FULL re-tune. Preserve the engine-LMDB-vs-asyncpg-DSN distinction (cocoindex-deploy.md §0): this volume backs COCOINDEX_DB (engine store), NOT COCOINDEX_DB_DSN (Supabase pool). Document in the B1 runbook that LMDB loss forces a memo-rebuild only, never KH data loss (Supabase canonical, inv 7). NO KH application code changes.
  • testStrategy: An operator inspecting the mount sees a disk-backed volume, not tmpfs; writing memo state, restarting the cocoindex container without destroying the volume, then observing the state intact confirms persistence; the runbook documents LMDB loss = memo-rebuild only, never KH data loss.
  • dependencies: [8]
  • priority: high

{66.10} — Migrate secrets into Coolify per-app encrypted env

Section titled “{66.10} — Migrate secrets into Coolify per-app encrypted env”
  • id: 10
  • title: Migrate secrets into Coolify per-app encrypted env
  • description: One-time manual copy of the ~8–10 runtime secret values out of GCP Secret Manager into Coolify per-app encrypted env (OQ-66-3). Drop, not migrate, the GCP deploy-SA/WIF creds. PULLMD_SERVICE_URL becomes a non-secret.
  • details: TECH change 3; TECH migration step 5. Satisfies invariants 10, 11, 12. For each value run gcloud secrets versions access latest --secret=<name> and paste into Coolify’s per-app encrypted env (hidden-after-save; Infisical deferred to B2). Base set (cocoindex): ANTHROPIC_API_KEY, OPENAI_API_KEY, SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, NEXT_PUBLIC_APP_URL, CRON_SECRET, SENTRY_AUTH_TOKEN. cocoindex-specific: COCOINDEX_DB_DSN (Supabase pooler, boot-required), PIPELINE_RUN_WEBHOOK_URL (set in {66.13}), PULLMD_API_TOKEN. pullmd: PULLMD_ADMIN_EMAIL, PULLMD_ADMIN_PASSWORD, PULLMD_API_TOKEN. PULLMD_SERVICE_URL is NOT a secret — it is the plain http://pullmd:3000 (inv 11). DROPPED, not migrated: the GCP deploy-SA / WIF credentials — no deploy-SA JSON ever lands on the host (inv 12). NO KH application code changes.
  • testStrategy: Auditing Coolify env shows the runtime secret set stored encrypted/hidden, not read from GCP Secret Manager at runtime; PULLMD_SERVICE_URL is a plain non-secret value; a host secrets audit finds NO GCP deploy-SA JSON — runtime secrets only.
  • dependencies: [6]
  • priority: high

{66.11} — Wire Coolify deploy webhook → pull pinned image

Section titled “{66.11} — Wire Coolify deploy webhook → pull pinned image”
  • id: 11
  • title: Wire Coolify deploy webhook → pull pinned image
  • description: Extend onprem-deploy.yml to call Coolify’s deploy webhook/API after the ghcr.io push so the host pulls and deploys the pinned image. The host pulls finished images; it never builds.
  • details: TECH change 4 slice B; TECH migration step 6 (part). Satisfies invariant 14 (pull half). Extend .github/workflows/onprem-deploy.yml (from {66.7}): after the pinned-image push to ghcr.io, call Coolify’s deploy webhook/API to pull and deploy that pinned tag onto the {66.8} compose app. Store the Coolify webhook URL/token as a GitHub secret (NOT on the host). Confirm Coolify is configured to pull from ghcr.io (registry credentials in Coolify if the package is private). The host pulls finished images; it does not build (OQ-66-6). .yml edit → Executor MUST run gitnexus_detect_changes() before commit. NO KH application code changes.
  • testStrategy: A push that builds+pushes a pinned image (via {66.7}) then triggers the Coolify deploy webhook; Coolify pulls the exact pinned tag from ghcr.io and redeploys the compose app; the running cocoindex container reports the new image digest; the host performed no build.
  • dependencies: [6, 7]
  • priority: high

{66.12} — Rollback-by-tag mechanics + runbook

Section titled “{66.12} — Rollback-by-tag mechanics + runbook”
  • id: 12
  • title: Rollback-by-tag mechanics + runbook
  • description: Document and verify the rollback path: re-point the deployed image tag to a prior pinned tag via Coolify’s API/UI and redeploy — near-instant pull, no host rebuild. Roll-forward is the same op with a newer tag.
  • details: TECH change 4 slice C; TECH migration step 6 (part). Satisfies invariant 15. Document in the B1 runbook (docs/runbooks/onprem-b1-deploy.md) the rollback = re-point image tag, no rebuild procedure: set the deployed image tag to a prior pinned ghcr.io tag via Coolify’s API/UI and redeploy (near-instant pull; OQ-66-6 confirms Coolify supports tag-edit rollback). Roll-forward is the identical op with a newer tag. Verify by deploying tag A, then re-pointing to a prior tag B and confirming the container runs B without any rebuild. This is documentation + a verified manual procedure — no workflow change (the webhook from {66.11} already supports it). NO KH application code changes.
  • testStrategy: With two pinned tags in ghcr.io, an operator re-points the Coolify deployed tag from the newer to the prior tag and redeploys; the container runs the prior image within seconds and no build is triggered on the host; the runbook documents the exact steps.
  • dependencies: [11]
  • priority: medium

{66.13} — Repoint inbound webhook env + DNS/SSL (auto-Let’s-Encrypt)

Section titled “{66.13} — Repoint inbound webhook env + DNS/SSL (auto-Let’s-Encrypt)”
  • id: 13
  • title: Repoint inbound webhook env + DNS/SSL
  • description: Set the cocoindex host env PIPELINE_RUN_WEBHOOK_URL to the same Vercel app URL with the same CRON_SECRET (env-only, zero app change), and configure DNS + Coolify auto-Let’s-Encrypt HTTPS for any host-facing endpoint.
  • details: TECH change 7; TECH migration step 7. Satisfies invariants 8, 9. Set the cocoindex container’s Coolify env PIPELINE_RUN_WEBHOOK_URL to the same Vercel app URL the Cloud Run deploy used, with the same CRON_SECRET (from {66.10}). _emit_pipeline_run_webhook (scripts/cocoindex_pipeline/flow.py:552-645) reads both from os.environNO source change. ZERO changes to app code, proxy.ts, the route handler (app/api/internal/pipeline-runs/record/route.ts), or Vercel config — the endpoint, its verifyCronAuth, and its recordPipelineRunpipeline_runs write are byte-for-byte as today (OQ-66-2). Also configure DNS + Coolify auto-Let’s-Encrypt to serve the host over HTTPS for any host-facing endpoint (this provisions the ingress surface ID-62 will later use — see {66.16} + OQ-66-8). NO KH application code changes — the webhook repoint is two host env vars only; do NOT edit flow.py.
  • testStrategy: The only change is two host env vars (PIPELINE_RUN_WEBHOOK_URL, CRON_SECRET); the app/route/proxy.ts/Vercel config are untouched; the host serves HTTPS via Coolify auto-Let’s-Encrypt with a valid cert; a git diff shows no change to flow.py or the route handler.
  • dependencies: [8, 10]
  • priority: high

{66.14} — LMDB mdb_copy → S3 backup job

Section titled “{66.14} — LMDB mdb_copy → S3 backup job”
  • id: 14
  • title: LMDB mdb_copy → S3 backup job
  • description: Author a scheduled job (deploy/onprem/backup/lmdb-backup.sh) that runs mdb_copy -c for an LMDB-consistent hot snapshot, then tars/ships it to the same S3 bucket Coolify uses. Document the restore path.
  • details: TECH change 5; TECH migration step 8. Satisfies invariants 17, 18, 19, 20. Create deploy/onprem/backup/lmdb-backup.sh (run as a Coolify scheduled task / host cron / tiny sidecar sharing the LMDB volume — Coolify’s native S3 backups are database-only and do NOT cover a raw named volume, OQ-66-4). The job: (1) run mdb_copy -c /cocoindex-state/lmdb /snapshot (-c compacts + drops free pages) — mdb_copy copies through an LMDB read transaction (MVCC read-snapshot immutable for the txn lifetime) so it produces a consistent hot snapshot with no quiesce; a naive cp/tar of live data.mdb is NEVER used (torn-page risk). (2) tar/restic the snapshot to the same S3 bucket Coolify targets for DB backups. Schedule: daily or twice-daily (low write volume; LMDB ≤256–512 MiB historically). Document the restore path in the B1 runbook: fetch latest S3 snapshot → untar onto the persistent volume at /cocoindex-state/lmdb → resume the cocoindex container; fallback is memo-rebuild (inv 7). The backup path touches Supabase not at all (inv 20). NO KH application code changes.
  • testStrategy: A scheduled job lands an LMDB snapshot in the S3 bucket separate from Coolify’s DB backup; a snapshot taken DURING active writes restores to a consistent, openable LMDB; the documented restore path resumes the engine; the backup path touches Supabase not at all.
  • dependencies: [9]
  • priority: medium

{66.15} — Datapath monitor — pipeline_runs row-arrival watch

Section titled “{66.15} — Datapath monitor — pipeline_runs row-arrival watch”
  • id: 15
  • title: Datapath monitor — pipeline_runs row-arrival watch
  • description: Author a datapath monitor (deploy/onprem/monitor/datapath-watch.ts or a scheduled query) that watches pipeline_runs row arrival in Supabase against a stall threshold — NOT /health (which is a liveness no-op).
  • details: TECH change 6; TECH migration step 9 (part — the monitor; the smoke is {66.16}). Satisfies invariants 21, 22. Create deploy/onprem/monitor/datapath-watch.ts (or a small scheduled Supabase query). The monitor: treats “container up + /health green” as insufficient (the cocoindex /health probe is a liveness no-op on a separate thread — green ≠ ingesting; RESEARCH §4-i, scripts/cocoindex_pipeline/server.py:131-144); queries pipeline_runs for the most recent row (filtered to active-ingestion windows) and compares newest created_at/run timestamp against a configurable stall threshold (default: no terminal-status row within the expected window of a triggered ingestion → flag a datapath stall); routes the alert via Coolify notification channels (Discord/Telegram/email) or Sentry. Threshold/window values are operator-tunable and recorded in the runbook. A successful ingestion is confirmed by row arrival, not the probe. NO KH application code changes — this is a standalone monitor reading Supabase; do not modify the pipeline.
  • testStrategy: With containers up and /health green but no pipeline_runs rows in the expected window, the monitor flags a datapath stall (NOT “healthy”); when a real ingestion lands a row, the monitor reports healthy on row arrival, not on the probe.
  • dependencies: [8]
  • priority: high

{66.16} — First end-to-end ingest smoke + datapath proof + ID-62 reachability hand-off

Section titled “{66.16} — First end-to-end ingest smoke + datapath proof + ID-62 reachability hand-off”
  • id: 16
  • title: First end-to-end ingest smoke + ID-62 reachability hand-off
  • description: Run a real end-to-end ingestion on the host; confirm a genuine pipeline_runs row lands (proving {62.1} dissolved), and confirm a host-facing HTTPS endpoint is reachable externally. This is the gating hand-off ID-62 depends on.
  • details: TECH change 6 + TECH migration step 9 (the smoke). THIS IS THE CROSS-TASK HAND-OFF DELIVERABLE TO ID-62 (ID-62.dependencies includes ID-66 — Task-level dep). Satisfies invariants 3, 8, 9, 21. Run a real ingestion through the co-located pipeline ({66.8} compose, {66.9} volume, {66.10} secrets, {66.13} webhook). Confirm via ROW ARRIVAL (NOT /health): a genuine pipeline_runs row lands in Supabase with the same shape it would have had from Cloud Run (run status, stage counts, items created) — proving the {62.1} reachability defect is DISSOLVED (inv 3): the cocoindex→pullmd hop that 403’d on Cloud Run no longer exists (host-local network). Also confirm the host-facing HTTPS ingress surface ({66.13} Coolify auto-Let’s-Encrypt) is reachable by an EXTERNAL client (e.g. a GH-hosted runner over HTTPS) — i.e. the ingress:internal/403 barrier that blocked ID-62 from CI is gone. OQ-66-8 BOUNDARY (assumption, pending Liam/parent confirm): ID-66 hands ID-62 a reachable HTTPS host + proven datapath + provisioned ingress surface onto which ID-62 mounts its OWN /stage route — ID-66 does NOT build /stage (that is KH/pipeline code, ID-62’s job, out of B1 scope per OQ-66-2). If Liam confirms ID-66 must pre-provision more (reserved hostname / Coolify app slot / published COCOINDEX_FIXTURE_STAGING_URL base), widen this acceptance. NO KH application code changes — running an ingestion through unchanged source; do not edit the pipeline.
  • testStrategy: A real ingestion lands a genuine pipeline_runs row in Supabase (proving inv 3 / {62.1} dissolved), confirmed by row arrival not /health; an external client reaches a host-facing endpoint over HTTPS (no 403). ID-62 hand-off gate met; OQ-66-8 boundary noted.
  • dependencies: [8, 9, 10, 11, 13, 15]
  • priority: high

{66.17} — Retire cloud-run-deploy.yml + WIF + reversibility doc

Section titled “{66.17} — Retire cloud-run-deploy.yml + WIF + reversibility doc”
  • id: 17
  • title: Retire cloud-run-deploy.yml + WIF + reversibility doc
  • description: Make B1 the active deploy path: stop using cloud-run-deploy.yml/WIF (file stays in git for reversibility), and document the one-env-var revert path + cost-floor removal in the B1 runbook.
  • details: TECH change 4 slice D + TECH change 8; TECH migration step 6 (retirement) + step 10 prerequisite. Satisfies invariants 16, 23, 24, 25. Only AFTER {66.16} proves the new path works: retire .github/workflows/cloud-run-deploy.yml + its WIF auth as the active deploy path (disable its triggers; the file + Cloud Run manifests under cloudrun/** REMAIN in git for reversibility — inv 24). WIF + deploy-SA are retired, NOT migrated. Document in the B1 runbook (docs/runbooks/onprem-b1-deploy.md): (a) cost-floor removed (inv 23) — no Filestore ($200/mo), one host vs two warm minScale=maxScale=1 Services, no per-deploy two-image Cloud Build; standing cost = the single IONOS VPS line (£21/mo); (b) revert path (inv 24) — cocoindex→pullmd reverts with one env var (PULLMD_SERVICE_URL), webhook reverts with one env var (PIPELINE_RUN_WEBHOOK_URL), secrets move back to Secret Manager, deploy reverts to the WIF workflow; (c) LMDB is the single genuinely stateful artefact (inv 25) — its loss costs a rebuild, never KH data loss. .yml edit → Executor MUST run gitnexus_detect_changes() before commit. NO KH application code changes.
  • testStrategy: A push no longer triggers cloud-run-deploy.yml (B1 path active); the workflow + cloudrun/** manifests remain in git; the runbook documents one-env-var reverts per B1 item, the cost-floor removal (one VPS line, no Filestore/warm-×2), and the LMDB as the one stateful artefact.
  • dependencies: [16]
  • priority: high

{66.18} — kpf dead-manifest cleanup (B1-cutover SIDE-TASK)

Section titled “{66.18} — kpf dead-manifest cleanup (B1-cutover SIDE-TASK)”
  • id: 18
  • title: kpf dead-manifest cleanup (side-task)
  • description: Standalone side-task riding the cutover: delete the two kpf Job manifests and edit the residual kpf refs in cloudrun files so grep -rl kpf cloudrun/ returns zero. Must not block or bloat the core migration.
  • details: TECH change 9; TECH migration step 10. Clearly-labelled standalone SIDE-TASK — NOT a core B1 step. It rides the cutover because retiring cloud-run-deploy.yml ({66.17}/inv 16) is the natural moment to drop the retired-but-still-deployed kpf tenant (RESEARCH §4-iii). Exact set (file-grounded): (1) DELETE cloudrun/jobs/prod-kpf.yaml; (2) DELETE cloudrun/jobs/staging-kpf.yaml; (3) EDIT cloudrun/jobs/staging-phew.yaml — drop kpf from the ^(phew|kpf)$ slug pattern in the bake-assert comment; (4) EDIT cloudrun/services/prod-pullmd.yaml — update/drop the “kpf decommissioned” comment; (5) EDIT cloudrun/services/staging-pullmd.yaml — same comment; (6) EDIT cloudrun/services/README.md — drop the kpf SA-mapping line + the two kh-cocoindex-pipeline-kpf smoke-verify blocks (target a Service with no manifest). The for TENANT in phew kpf loop in cloud-run-deploy.yml is mooted by {66.17} retiring that workflow — items 1–6 are the residual cleanup of files the retirement does not delete. NO KH application code changes — only Cloud Run YAML/README/comment edits; no .py/.ts touched.
  • testStrategy: grep -rl kpf cloudrun/ returns zero files (down from 6); the two kpf Job manifests are deleted; no kpf tenant is referenced by any active deploy path; no KH application code (.py/.ts) is changed.
  • dependencies: [17]
  • priority: low

{66.5} VPS stand-up + hardening ─────────────┐
{66.6} Install Coolify ──────────────┬───────────────┐
│ │
{66.7} GH Action build+push (ghcr) ──┤ (parallel) │
(no in-host dep — off-host build) │ ▼
│ {66.10} Secrets → Coolify env
▼ │
{66.8} Co-location compose │
deploy [6,7] │
│ │ │
┌───────────────┘ └──────────┐ │
▼ ▼ │
{66.9} LMDB + corpus {66.15} Datapath │
volume [8] monitor [8] │
│ │ │
│ {66.11} Coolify deploy │ │
│ webhook [6,7] │ │
│ │ │ │
│ ▼ │ │
│ {66.12} Rollback-by-tag │ │
│ [11] │ │
│ │ {66.13} Webhook │ │
│ │ repoint + DNS/SSL [8,10] ◄──┘
│ │ │ │
▼ │ ▼ ▼
{66.14} LMDB │ ┌────────────────────┐
backup [9] └──►│ {66.16} E2E smoke + │
│ ID-62 hand-off │
│ [8,9,10,11,13,15] │
└─────────┬───────────┘
{66.17} Retire cloud-run-deploy.yml
+ WIF + reversibility doc [16]
{66.18} kpf cleanup (side-task) [17]

Parallelisation (multi-agent / multi-session safe):

  • Wave 1 (sequential foundation): {66.5}{66.6}. {66.7} (off-host GH Action build) has no in-host dependency and can run in parallel with {66.5}/{66.6} — it only needs the repo, not the host.
  • Wave 2 (after {66.6}+{66.7}): {66.8} (compose deploy) and {66.10} (secrets, needs only {66.6}) and {66.11} (deploy webhook, needs {66.6}+{66.7}) can largely proceed together; {66.8} consumes the {66.7} image.
  • Wave 3 (after {66.8}): {66.9} (volume), {66.15} (monitor) parallelise — both depend only on {66.8}. {66.13} (webhook repoint) needs {66.8}+{66.10}. {66.12} (rollback) needs {66.11}. {66.14} (backup) needs {66.9}.
  • Wave 4 (serialised convergence): {66.16} (E2E smoke + ID-62 hand-off) is the convergence point — it depends on {66.8, 9, 10, 11, 13, 15}. It must wait for the datapath substrate to be complete.
  • Wave 5 (serialised teardown): {66.17} (retire Cloud Run path) depends on {66.16} (don’t tear down the old path until the new one is proven). {66.18} (kpf cleanup side-task) depends on {66.17}.

All dependencies are sibling-only (other Subtasks of Task 66). No cross-Task Subtask dependency is expressed. The ID-62→ID-66 dependency is correctly a Task-level dependency (ID-62.dependencies includes 66), satisfied by the {66.16} hand-off deliverable — not a Subtask-level cross-Task dep.


TECH Proposed changes 1–9 → Subtask mapping (all 9 covered):

TECH changeSubtask(s)PRODUCT invariants
1 — Co-location compose{66.8}1, 2, 3, 4, 11
2 — LMDB persistent volume{66.9}5, 6, 7
3 — Secrets → Coolify env{66.10}10, 11, 12
4 — Thin GH Action / ghcr.io / Coolify pull / retirement{66.7} (build+push), {66.11} (webhook pull), {66.12} (rollback), {66.17} (retirement)13, 14, 15, 16
5 — mdb_copy → S3 backup{66.14}17, 18, 19, 20
6 — Datapath monitor + smoke{66.15} (monitor), {66.16} (smoke)21, 22 (+ 3, 8, 9 proven at smoke)
7 — Webhook repoint + DNS/SSL{66.13}8, 9
8 — Cost-floor removal + reversibility{66.17} (folded with retirement)23, 24, 25
9 — kpf dead-manifest cleanup (side-task){66.18}(in-scope-adjacent — no core invariant)

TECH migration steps 1–10 → Subtask mapping (all 10 covered):

StepSubtask
1 — VPS stand-up + hardening{66.5}
2 — Install Coolify{66.6}
3 — Co-location compose deploy{66.8}
4 — LMDB + corpus volume{66.9}
5 — Secrets migration{66.10}
6 — Thin GH Action + retirement{66.7} + {66.11} + {66.12} + {66.17}
7 — Webhook repoint + DNS/SSL{66.13}
8 — LMDB backup job{66.14}
9 — E2E smoke + datapath monitor{66.15} + {66.16}
10 — kpf cleanup{66.18}

All 25 PRODUCT invariants are covered via the TECH change → invariant map (carried forward from TECH’s coverage map), and all 9 TECH changes + all 10 migration steps map to at least one Subtask.


Risks and open items (carried from TECH; for parent/Liam awareness)

Section titled “Risks and open items (carried from TECH; for parent/Liam awareness)”
ItemNote
OQ-66-8 (NEW, this PLAN)The exact ID-66→ID-62 /stage hand-off contract. Assumption adopted (host reachable + datapath proven + ingress provisioned; ID-66 does NOT build /stage). Appended to OQ-pending.md. {66.16} acceptance widens if Liam confirms more pre-provisioning is needed.
OQ-66-5(pre-known, deferred) GCP billing export to harden the cost baseline. Not blocking.
OQ-66-7(pre-known, deferred) B2 self-host-Supabase + AGPL-pullmd conveyance. Not blocking.
16 GB RAM ceilingThe one dimension neither VPS exceeds (OQ-66-1). Builds run off-host ({66.7}) so the host RAM is never build-contended; watch host memory in Coolify monitoring.
Sidecar image-tag driftPlaywright/Trafilatura :2 floating tags — resolve :2→digest before first deploy ({66.8} details).

  • No Subtask touches KH application code. Every record’s details repeats the guard. The only .github/workflows/*.yml edits are {66.7}, {66.11}, {66.17}, {66.18} — the Executor should run gitnexus_detect_changes() before committing those four.
  • {66.16} is the ID-62 gate. It is the named cross-Task hand-off deliverable; ID-62’s Task-level dependency on ID-66 is satisfied by {66.16}’s acceptance.
  • {66.18} is a labelled side-task — keep it last; it must not block or bloat the core migration.
  • 14 Subtasks ({66.5}{66.18}) — within the 25 soft ceiling; no Task-split signal.