Below is a “right-size cheat-sheet” you can hand your DevOps engineer so they can
provision the exact VM (or GPUs) for each piece of the stack, wire Oban on a second
lightweight box — and keep the bill from spiking.
________________
1 Model & service memory realities
Component
      Params / binary
      Good quant.
      RAM / VRAM when loaded
      Source
      RAGFlow (service)
      —
      —
      16 GB RAM, 4 vCPU min
      (ragflow.io)
      Qwen 3 Embedding 0.6B
      0.6 B
      Q4_K_M
      ≈5 GB RAM / 0 GPU (CPU ok)
      (huggingface.co)
      Mistral-7B-Instruct
      7 B
      Q4_K_M
      ⇢ 11 GB RAM or ≈8 GB VRAM
      (huggingface.co)
      DeepSeek-R1-dense-6.7B
      6.7 B
      GGUF Q4
      ⇢ 12 GB RAM or ≈10 GB VRAM
      (reddit.com)
      (optional) MagicoderS-CL-7B – great code fixer
      7 B
      Q4_K_S
      ≈11 GB RAM
      model sizing similar to Mistral
      Oban worker service
      Elixir app, no LLM
      —
      512 MB RAM
      —
All three LLMs never run simultaneously – the orchestrator spins one container,
shuts it, starts the next.
________________
2 Recommended GCP layout
VM
      Purpose
      Machine
      Est. $/hr†
      ragbox (all inference)
     RAGFlow + Ollama containers
     e2-standard-8 (8 vCPU / 32 GB)
     $0.30
       • burst GPU
     only when using DeepSeek-R1 full speed
     n1-standard-4 + 1 × T4 (16 GB)
     $0.43
     jobsbox
     Oban + Cron / Trigger.dev runner
     e2-micro (0.25 vCPU / 1 GB)
     $0.01
†US-central, on-demand pricing. You’ll shut the GPU VM after each batch.
________________
3 Auto–stop / auto–start pattern
# /usr/local/bin/idle_check.sh (runs every 5 min via cron)
if ! docker ps --format '{{.Names}}' | grep -qE 'mistral|qwen|deepseek'; then
   if [[ $(cat /proc/uptime | awk '{print int($1)}') -gt 900 ]]; then   # >15 min
idle
      gcloud compute instances stop ragbox --zone=us-central1-a --quiet
   fi
fi
* Cloud Scheduler fires gcloud compute instances start ragbox every weekday 08:00.
* LangGraph first API hit waits ~45-60 s for Ollama to reload a 7 B model.
Internal traffic between VMs in the same VPC & zone is free, so the orchestrator →
Ollama calls cost $0 in egress. Lock firewalls to 10.0.0.0/24 to avoid DDOS attack
surface.
________________
4 Installation steps for DevOps (revised)
1. Create ragbox VM: e2-standard-8 + 100 GB disk, Ubuntu 22.04, private IP only,
ssh via IAP.
2. Docker-compose (same file as previous answer) + add volumes /models/qwen
/models/mistral /models/deepseek to avoid re-download.
3. jobsbox VM: e2-micro, deploy Oban Phoenix app; point it at Cloud SQL PostgreSQL.
4. Install Ollama models
ollama pull mistral:7b-instruct-q4_K_M
ollama pull qwen3-embedding:0.6b-q4_K_M
ollama pull deepseek-r1:6.7b-q4_0
5. Add Cloud Scheduler + startup/idle scripts shown above.
________________
5 Why this sizing works
* RAGFlow 16 GB baseline + one 7 B model (≈11 GB) ⇒ 27 GB → fits 32 GB VM.
* When you switch to DeepSeek-R1 you either:
   * keep CPU mode (12 GB RAM) or
   * spin a short-lived T4 GPU box for ~3× throughput.
* Oban and Temporal/Trigger.dev produce HTTP tasks; tiny vCPU suffices.
* Auto-stop after 15 min idle saves 65-75 % of runtime hours.
________________
Ready for hand-off
* Copy the table + scripts into a runbook → DevOps knows exactly which machine
types to click.
* You keep LangGraph prompt work; Dev simply ensures the containers and keys are
live.
Need a Terraform module for the idle-check scheduler or a bash model_switch.sh
script that loads the right LLM container on demand? Tell me and I’ll generate it.
___________________________________________________________________________
0 Prerequisites
* GCP project with the $300 credit.
* gcloud ≥ 462, Docker ≥ 24 on admin laptop.
* A service account ragbox-sa with Compute Admin + IAP-secured Web App User.
gcloud iam service-accounts create ragbox-sa
gcloud projects add-iam-policy-binding $PROJECT --member="serviceAccount:ragbox-
sa@$PROJECT.iam.gserviceaccount.com" --role="roles/compute.admin"
________________
1 Architecture diagram (1-box view)
                  +---------+
                  | IAP LB | https://ragbox.example.com/*
                  +----+----+
                       |
        443 (IAP)      |
    ┌─────────────────┴───────────────────┐
    │      VM 1: ragbox (e2-standard-8) │
    │                                     │
    │ Docker compose stack                │
    │ ┌─────────────┐    8080             │
    │ │ RAGFlow      │<──/                │
    │ └─────────────┘                     │
    │ ┌─────────────┐    11434            │
    │ │ Ollama       │<──/                │
    │ └─────────────┘                     │
    │ ┌─────────────┐    9090             │
    │ │ MCP          │<──/                │
    │ └─────────────┘                     │
    │ ┌─────────────┐    3001             │
    │ │ Flowise UI │<──/                  │
    │ └─────────────┘                     │
    │ ┌─────────────┐    3002             │
    │ │ OpenWebUI    │<──/                │
    │ └─────────────┘                     │
    │ idle-check.service (stops VM if     │
    │ no LLM container >15 min)           │
    └─────────────────────────────────────┘
    ┌─────────────────────────────────────┐
    │ VM 2: jobsbox (e2-micro, 24/7)      │
    │ Oban worker → Temporal / LangGraph │
    └─────────────────────────────────────┘
All traffic stays on internal VPC; external users must pass IAP OAuth.
________________
2 Provision the VMs
# VM 1: 8 vCPU / 32 GB RAM, 100 GB PD
gcloud compute instances create ragbox \
  --zone=us-central1-a \
  --machine-type=e2-standard-8 \
  --service-account=ragbox-sa@$PROJECT.iam.gserviceaccount.com \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --boot-disk-size=100GB \
  --metadata-from-file startup-script=ragbox_startup.sh
# VM 2: tiny always-on box for Oban
gcloud compute instances create jobsbox \
  --zone=us-central1-a \
  --machine-type=e2-micro \
  --service-account=ragbox-sa@$PROJECT.iam.gserviceaccount.com \
  --tags=internal
________________
ragbox_startup.sh
#!/bin/bash
apt-get update -y && apt-get install -y docker.io docker-compose-plugin
usermod -aG docker $USER
mkdir -p /opt/ai-stack && cd /opt/ai-stack
cat > docker-compose.yml <<'YAML'
version: "3.9"
services:
  ragflow:
    image: infiniflow/ragflow:latest
    ports: ["8080:8080"]
    volumes: ["./data/ragflow:/data"]
  ollama:
    image: ollama/ollama:latest
    ports: ["11434:11434"]
    volumes: ["./data/ollama:/root/.ollama"]
    environment:
      - OLLAMA_MODELS=/models
    healthcheck: { test: ["CMD", "curl", "-f", "http://localhost:11434"], interval:
30s, timeout: 5s, retries: 5 }
  mcp:
    image: context7/mcp:latest
    ports: ["9090:9090"]
  flowise:
    image: flowiseai/flowise:latest
    ports: ["3001:3000"]
    environment:
      - PORT=3000
      - DATABASE_TYPE=sqlite
      - SECRETKEY=$(openssl rand -hex 16)
  openwebui:
     image: ghcr.io/open-webui/open-webui:latest
     ports: ["3002:3000"]
     volumes: ["./data/ollama:/root/.ollama"]
YAML
docker compose up -d
# preload models (CPU Q4)
docker exec ollama ollama pull mistral:7b-instruct-q4_K_M
docker exec ollama ollama pull qwen3-embedding:0.6b-q4_K_M
docker exec ollama ollama pull deepseek-r1:6.7b-q4_0
# idle-stop service
cat > /etc/systemd/system/idle-check.service <<'SERVICE'
[Unit]
Description=Stop VM if Ollama idle > 15 min
[Service]
Type=simple
ExecStart=/usr/local/bin/idle-check.sh
Restart=always
RestartSec=300
SERVICE
cat > /usr/local/bin/idle-check.sh <<'SCRIPT'
#!/bin/bash
idle=$(docker ps --format '{{.Names}}' | grep -E 'ollama|mistral|qwen|deepseek' ||
true)
if [ -z "$idle" ]; then
   up=$(awk '{print int($1)}' /proc/uptime)
   if [ "$up" -gt 900 ]; then
      gcloud compute instances stop ragbox --zone=us-central1-a --quiet
   fi
fi
SCRIPT
chmod +x /usr/local/bin/idle-check.sh
systemctl daemon-reload && systemctl enable --now idle-check.service
(Startup script ≈ 5 min on first boot.)
________________
3 Identity-Aware Proxy
1. Create an HTTPS external load balancer that fronts ragbox on ports 8080, 3001,
3002.
2. Enable Cloud IAP on the backend service and add your Google Workspace emails as
IAP-Secured Web App Users. (cloud.google.com)
3. Now the URLs:
https://ragbox.example.com/ragflow     # RAGFlow UI
https://ragbox.example.com/flowise     # prompt playground
https://ragbox.example.com/openwebui   # model switch chat
are gated by Google Sign-in.
________________
4 Oban worker box
Deploy your Elixir project (or a minimal app):
sudo apt-get update && sudo apt-get install -y git elixir erlang-dev
git clone https://github.com/myorg/orchestrator.git
cd orchestrator
mix deps.get
MIX_ENV=prod mix release
_build/prod/rel/orchestrator/bin/orchestrator start
Oban connects to Cloud SQL or Neon; Oban.LanggraphRunner hits the LangGraph Cloud
URL.
________________
5 Start / Stop buttons for power users
From Flowise (Admin > Tools > REST):
* POST /api/util/start – gcloud compute instances start ragbox ...
* POST /api/util/stop – gcloud compute instances stop ragbox ...
Add them as custom endpoints in Flowise; UI shows two buttons. Because Flowise
itself is on ragbox, you can also use the GCP mobile app for emergencies.
________________
6 One login for every UI
* IAP handles OAuth; no local passwords.
* Flowise and OpenWebUI: set BASIC_AUTH_USER / BASIC_AUTH_PASS env variables even
though IAP is in front for defense-in-depth.
* RAGFlow has no auth → rely on IAP.
________________
7 Validation checklist (30 minutes)
Check
      Command / URL
      Docker stack healthy
      docker compose ps
      Ollama model list
      curl http://localhost:11434/api/tags
      Flowise opens (IAP)
      https://ragbox.example.com/flowise
      RAGFlow search
      Upload a PDF → /query returns hits
      OpenWebUI chat
      switch model → ask “Hello”
      Idle-stop trigger
      docker stop ollama; wait 15 min → VM stops automatically
Once these pass, you—the prompt engineer—can log in, upload docs, craft LangGraph
prompts and never bother DevOps again.
(VM cost ≈ $12–$17/mo at 1¾ h/day runtime; idle-stop prevents surprises.)
(cloud.google.com, hub.docker.com)
Below are three self-contained artifacts you can hand to your DevOps teammate:
1. model_switch.sh – on-VM utility that Flowise or OpenWebUI can trigger to pull /
load / tag a model on demand.
2. Terraform stack (main.tf + variables.tf) – provisions both VMs, firewall,
startup-script injection, and a weekday Cloud Scheduler “auto-start” job (idle-stop
is already handled by the system-unit in ragbox_startup.sh).
3. Flowise “REST Tool” snippet – so the Start/Stop buttons and model-switch call
show up in its UI without extra coding.
Nothing else is required for the POC.
________________
1 model_switch.sh
/usr/local/bin/model_switch.sh — place on ragbox and chmod +x.
#!/bin/bash
#
# model_switch.sh <model-name>
# Pulls model if missing, then sets it as the CURRENT tag for
# OpenWebUI & default LangChain calls.
# Usage: model_switch.sh mistral:7b-instruct-q4_K_M
set -e
MODEL=$1
if [[ -z "$MODEL" ]]; then
   echo "Usage: $0 <model-name-in-ollama>"
   exit 1
fi
echo "🔄 Pulling $MODEL (if needed)…"
docker exec ollama ollama pull "$MODEL" >/dev/null
echo "🔄 Tagging $MODEL as 'current'…"
docker exec ollama ollama tag "$MODEL" current
echo "✅ Model '$MODEL' is now the default (tag = current)."
How to expose in Flowise
* In Flowise UI → Tools → REST → create endpoint:
   * Method: POST
   * URL: http://localhost:3001/api/run/model_switch
   * Body: { "model": "{{input}}" }
Add a node “Switch Model” in any Flow and you’ll get a button; the input box lets
you paste deepseek-r1:6.7b-q4_0 etc.
(OpenWebUI already detects new tags automatically.)
________________
2 Terraform (“one-click”) stack
variables.tf
variable "project"        { type = string }
variable "region"         { type = string default = "us-central1" }
variable "zone"           { type = string default = "us-central1-a" }
variable "service_account_email" { type = string }
variable "ssh_key"        { type = string } # ~/.ssh/id_rsa.pub
main.tf
terraform {
  required_providers {
    google = { source = "hashicorp/google", version = "~> 5.21" }
  }
}
provider "google" {
  project = var.project
  region = var.region
  zone    = var.zone
}
# ------------ ragbox ---------------
resource "google_compute_instance" "ragbox" {
  name         = "ragbox"
  machine_type = "e2-standard-8"
  zone         = var.zone
  tags         = ["iap-allowed"]
    boot_disk {
      initialize_params { image = "ubuntu-os-cloud/ubuntu-2204-lts" size = 100 }
    }
    metadata_startup_script = file("ragbox_startup.sh")
    service_account { email = var.service_account_email scopes = ["cloud-platform"] }
    network_interface { network = "default" access_config {} }
    metadata = { ssh-keys = "ubuntu:${file(var.ssh_key)}" }
}
# ------------ jobsbox --------------
resource "google_compute_instance" "jobsbox" {
  name         = "jobsbox"
  machine_type = "e2-micro"
  zone         = var.zone
  tags         = ["internal"]
    boot_disk { initialize_params { image = "ubuntu-os-cloud/ubuntu-2204-lts" } }
    network_interface { network = "default" access_config {} }
    service_account { email = var.service_account_email scopes = ["cloud-platform"] }
    metadata = { ssh-keys = "ubuntu:${file(var.ssh_key)}" }
}
# ---------- firewall for IAP LB ----
resource "google_compute_firewall" "iap_allow" {
  name    = "allow-iap"
  network = "default"
  allow { protocol = "tcp" ports = ["8080","9090","11434","3001","3002"] }
  source_ranges = ["35.235.240.0/20"] # IAP fixed IP range
  target_tags   = ["iap-allowed"]
}
# ---------- Cloud Scheduler: auto-start weekdays ------
resource "google_cloud_scheduler_job" "start_ragbox" {
  name             = "start-ragbox-weekdays"
  schedule         = "50 13 * * 1-5"         # 07:50 America/Chicago = 13:50 UTC
  time_zone        = "America/Chicago"
  attempt_deadline = "60s"
  http_target {
    http_method = "POST"
    uri          = "https://compute.googleapis.com/compute/v1/projects/$
{var.project}/zones/${var.zone}/instances/ragbox/start"
    oidc_token {
      service_account_email = var.service_account_email
    }
  }
}
Run terraform init && terraform apply. Total deploy time ≈ 5 min.
________________
3 Flowise “Start / Stop ragbox” buttons
In Flowise → Tools → REST, add two endpoints:
Action
      Method
      URL
      Body
      Start ragbox
      POST
      https://compute.googleapis.com/compute/v1/projects/${PROJECT}/zones/${ZONE}/
instances/ragbox/start
      empty
      Stop ragbox
      POST
      https://compute.googleapis.com/compute/v1/projects/${ZONE}/instances/ragbox/
stop
      empty
Auth tab → “OAuth 2 (GCP)” → pick the same ragbox-sa service-account.
Users who sign in via IAP will see two buttons in the Flowise canvas.
________________
That’s all DevOps needs
* ragbox_startup.sh (from previous message) in repo root.
* the Terraform files above.
* model_switch.sh copied onto ragbox.
* Quick doc: how to log in via IAP and where the buttons live.
Everything else—prompt crafting, doc uploads, LangGraph runs—stays yours.
_____________________________________________________________
1 Core LLM & Retrieval Stack
Component
      Purpose
      Repo / Docs
      RAGFlow
      Chunk, embed, search documents (20 % overlap, Qwen 3 embed).
      https://github.com/infiniflow/ragflow
      Ollama
      Local model runner (Mistral-7B, DeepSeek-R1, Qwen-3-Embed).
      https://github.com/ollama/ollama
      Context7 MCP
      Code-snippet retriever via /search endpoint.
      https://github.com/upstash/context7
      Qwen 3 Embedding 0.6 B
      Embedding model binary for RAGFlow.
      https://github.com/QwenLM/Qwen3-Embedding
________________
2 Orchestration & Workflows
Component
      Purpose
      Repo / Docs
      LangGraph Cloud
      Multi-agent state-machine service (Draft → Test → Fix).
      https://www.langchain.com/langgraph
      LangSmith
      Prompt playground & run tracing.
      https://www.langchain.com/langsmith
      Temporal Cloud
      Long-running workflow engine (optional at POC).
      https://github.com/temporalio
      Trigger.dev
     Event / cron glue; calls Temporal or LangGraph.
     https://github.com/triggerdotdev/trigger.dev
     Oban
     Job queue inside Elixir app that calls LangGraph.
     https://github.com/sorentwo/oban
________________
3 “Ops” Services & Datastores
Component
      Purpose
      Repo / Docs
     Docker + Docker-Compose
     Container runtime on VMs.
     https://docs.docker.com/compose/
     Terraform
     Infra-as-code for VMs, firewall, scheduler.
     https://github.com/hashicorp/terraform
     GCP Compute Engine
     VMs (e2-standard-8, e2-micro).
     https://cloud.google.com/compute
     GCP Cloud Scheduler
     Auto-start ragbox weekdays 07:50.
     https://cloud.google.com/scheduler
     GCP Identity-Aware Proxy (IAP)
     OAuth-gate every UI port.
     https://cloud.google.com/iap
________________
4 Local Admin & Testing UIs
Component
      Purpose
      Repo / Docs
      Flowise AI
      Drag-drop doc upload, retriever & chat playground.
      https://github.com/FlowiseAI/Flowise
     Open WebUI
     Ollama model switcher & side-by-side chat.
     https://github.com/open-webui/open-webui
________________
5 Utility / Support Scripts (included in run-book)
Script
      Path on ragbox
      Notes
      ragbox_startup.sh
      metadata-startup-script
      Installs Docker, pulls containers, preloads models, sets idle-stop service.
      idle-check.service + idle-check.sh
      /etc/systemd/system/ & /usr/local/bin/
      Stops VM after 15 min with no LLM containers.
      model_switch.sh
      /usr/local/bin/
      model_switch.sh mistral:7b-instruct-q4_K_M tags model current.
      Terraform files
      infra/main.tf, infra/variables.tf
      Provisions ragbox & jobsbox, firewall, Cloud Scheduler.
      Flowise REST endpoints
      Created in Flowise UI
      POST /model_switch, /start, /stop hit scripts & GCP API.
________________
6 Credentials / Keys to hand over
Key
      For
      Where to generate
      LANGGRAPH_API_KEY
      LangGraph Cloud
      LangGraph console → “Settings → API Keys”.
      ragbox-sa service-account key
      Terraform + idle-stop
      GCP IAM → JSON key (store in Secret Manager).
     BASIC_AUTH_USER/PASS
     Flowise, Open WebUI
     Put in docker-compose.yml env.
Everything else (docs upload, prompt editing, model comparison) you can handle
through Flowise / OpenWebUI once DevOps deploys.
Hand this inventory plus the run-book from the previous message, and your DevOps
engineer has every repo, credential pointer, and script needed to spin up the full
POC in a single work-day.
________________________________________________________________________
Below is a copy-pastable run-book you can hand to your DevOps teammate. It wires
together RAGFlow → CTM → Oban → LangGraph and exposes progress on a Focalboard
Kanban board. An optional snippet shows how to switch CTM to OpenAI o3 instead of a
local model.
https://github.com/eyaltoledano/claude-task-master/tree/main
https://github.com/mattermost-community/focalboard
________________
flowchart TD
  subgraph Ingestion
     A[RAGFlow (Qdrant + Qwen 3)]--/query-->C
     B[Context7 (code MCP)]--/search-->C
  end
  subgraph Planning
     C[Claude-Task-Master CLI]-->.task.yaml-->D
     D[Focalboard card "Backlog"]-.REST .->|
       status:"Backlog"|Focal
  end
  subgraph Execution
     D--enqueue-->E[Oban TaskRunner]
     E--spec_yaml-->G[LangGraph Pipeline]
     G--green-->H(Git PR + Focal "Done")
     G--fail-->E
  end
1 Prerequisites
Host
       Purpose
       ragbox VM
       Qdrant + RAGFlow REST (http://ragbox:8080)
       langgraph-runner (container or Fly app)
       Executes Draft → Test → Refine chain
       Postgres
       Oban jobs + Focalboard DB (single instance is fine)
________________
2 Install Focalboard (UI)
docker run -d \
  --name focalboard \
  -p 8000:8000 \
  mattermost/focalboard # official image :contentReference[oaicite:0]{index=0}
Open http://SERVER:8000, create an admin user, then a board named “Phase-1
Backlog”. From “Profile → Security” copy the Personal Access Token — we’ll call it
FOCAL_TOKEN.
________________
3 Install & configure CTM
python3 -m venv ~/.ctm
source ~/.ctm/bin/activate
pip install "claude_task_master @ git+https://github.com/eyaltoledano/claude-task-
master"
# ~/.ctmrc
contextProviders:
  - name: ragflow
    url: http://ragbox:8080/query
    weight: 3
  - name: context7
    url: http://context7:9090/search
    weight: 1
postTaskValidators:
  - complete_file_paths
  - compile_elixir           # see
docs/configuration.md :contentReference[oaicite:1]{index=1}
maxParallelTasks: 3
Optional – use OpenAI o3 for planning:
export OPENAI_API_KEY=sk-prod-*********
export OPENAI_BASE_URL=https://api.openai.com/v1
ctm plan specs/epic.md --model gpt-4o
(If those env vars are unset, CTM falls back to the --local --ollama-url flags you
already use.)
________________
4 Bridge CTM → Focalboard {scripts/ctm_to_focal.exs}
Mix.install([{:httpoison, "~> 2.2"}, {:yaml_elixir, "~> 2.9"}])
[focal_url, token, board_id] = System.argv()
for path <- Path.wildcard("tasks/*.task.yaml") do
  spec = YamlElixir.read_from_file!(path)
  # create card
  body = %{
    board_id: board_id,
    title: spec["title"],
    description: "```yaml\n#{File.read!(path)}\n```",
    fields: %{status: "Backlog"}
  }
  HTTPoison.post!(
    "#{focal_url}/api/v1/cards",
    Jason.encode!(body),
    [{"Authorization", "Bearer #{token}"}, {"Content-Type", "application/json"}]
  )
end
Run after ctm plan:
mix run scripts/ctm_to_focal.exs \
   http://focalboard:8000 \
   $FOCAL_TOKEN \
   $BOARD_ID
Cards land in “Backlog” column. Devs drag to “In Progress” to trigger the pipeline.
________________
5 Oban worker → LangGraph
# mix.exs
defp deps, do: [
  {:oban, "~> 2.16"},
  {:yaml_elixir, "~> 2.9"},
  {:httpoison, "~> 2.2"} # reuse for focal patch
]
# lib/task_runner.ex
defmodule TaskRunner do
  use Oban.Worker, queue: :tasks
  def perform(%Oban.Job{args: %{"yaml" => path, "card_id" => card_id}}) do
    spec = File.read!(path)
    model = if YAML.get(spec, "difficulty") == 3, do: "gpt-4o", else: "mistral:7b"
    result = Langgraph.run(
      template: YAML.get(spec, "promptTemplate", "draft.j2"),
      spec_yaml: spec,
      context: Retriever.merged(top_k: 8),
      model: model
    )
    status = if result.status == :ok, do: "Done", else: "UnitTest:Fail"
    patch_card(card_id, status)
  end
  defp patch_card(card_id, status) do
    HTTPoison.put!(
      "#{System.fetch_env!("FOCAL_URL")}/api/v1/cards/#{card_id}",
      Jason.encode!(%{fields: %{status: status}}),
      [{"Authorization", "Bearer #{System.get_env("FOCAL_TOKEN")}"},
       {"Content-Type", "application/json"}])
  end
end
Webhook (Focalboard → Oban):
post "/focalhook", FocalController, :move
# In :move detect column change to "In Progress" → enqueue Oban job with card_id +
yaml path
________________
6 CI helper (GitHub Actions)
name: CTM Plan & Board
on:
  push:
    paths: ["specs/**", "docs/**"]
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Plan tasks
        run: |
           source ~/.ctm/bin/activate
           ctm plan specs/epic.md --local --ollama-url http://ragbox:11434
      - name: Populate Focalboard
        env:
           FOCAL_TOKEN: ${{ secrets.FOCAL_TOKEN }}
           BOARD_ID: ${{ secrets.BOARD_ID }}
        run: |
           mix run scripts/ctm_to_focal.exs http://focalboard:8000 $FOCAL_TOKEN
$BOARD_ID
Now every doc change refreshes tasks & backlog cards.
________________
7 Developer workflow
Action
      Who
      Result
      Drag card → In Progress
      Dev
      Focal webhook enqueues Oban job.
      LangGraph pipeline finishes
      TaskRunner updates card status to Done or UnitTest:Fail.
     Merge PR
     GitHub Action moves card to Released lane.
Jira eliminated.   All activity surfaces in Focalboard, and CTM + LangGraph run
head-less in the background.