Private local-dev configuration for running QM as a full stack on this machine — core (with the in-process Slack bot) plus the web, admin, and portal surfaces, backed by Postgres.
This repo holds only configuration and scripts. The QM source (core) is a
separate checkout at $QM_DIR and is not stored here. No real secret ever
lands in git — everything sensitive lives in the gitignored .env.
.env.example # template; copied to .env by setup.sh
.env # real config + secrets (gitignored)
docker-compose.yml # local Postgres (qm-pg)
scripts/
setup.sh # one-time: .env + secrets, Postgres, deps, web-ui build
up.sh # boot core + web + admin + portal
down.sh # stop them (--pg also stops Postgres)
logs.sh # tail logs
build-sandbox.sh # build the local Docker sandbox image (slow, optional)
lib.sh # shared env loading + Node 24 on PATH
deploy/layers/hbd/ # placeholder for a future real Fly/AWS deploy layer
- Node ≥ 24 —
brew install node@24(scripts put it on PATH automatically) - Docker running (Postgres + the agent sandbox)
- An
ANTHROPIC_API_KEY(default model isclaude-opus-5; theclaudeharness is Anthropic-only)
# 1. Clone the QM core next to this repo (once)
git clone https://github.com/yc-software/qm ../qm # -> $QM_DIR
# 2. Setup: creates .env, generates secrets, starts Postgres, installs deps
scripts/setup.sh
# 3. Put your key in .env
# ANTHROPIC_API_KEY=sk-ant-...
# 4. Boot the stack
scripts/up.shThen open http://localhost:8088 (the portal front door). On localhost the
portal signs you in automatically as DEV_ADMIN_PRINCIPAL (no email login).
Admin is at http://localhost:8088/admin.
HARNESS=pi in .env is the org default. To make both selectable per
person/room:
- Open /admin → the runtime/harness governance card.
- Set approved harnesses to
piandclaude. - In the Web UI each scope can then pick
piorclaude(and its model); Slack uses whatever that scope resolves to.
Quick alternative for a one-off test: set HARNESS=claude in .env and
restart (scripts/down.sh && scripts/up.sh).
patches/0001-pi-qwen-local.patch (applied to the core by setup.sh via
scripts/apply-patches.sh) registers a selectable model id qwen-local that
the pi harness calls over the OpenAI-compatible chat-completions API — no
translation proxy. Verified working end-to-end (generation and tool calls)
against Qwen3.6-35B-A3B-local.
Requirements & config (.env):
- llama.cpp serving on
LLAMACPP_BASE_URL(defaulthttp://127.0.0.1:8888/v1), started with tool support (--jinja+ a tool-capable chat template) so the agent's tool calls work. OPENROUTER_API_KEYset to any placeholder — pi needs a key in the model's provider slot; llama.cpp ignores it.- Optional
PI_MODEL=qwen-localto make it the default model (needed for a local-only run with no Anthropic key). Otherwise Claude stays default and you pickqwen-localper-scope in the Web UI.
Start llama-server with a tool-capable chat template and thinking disabled.
Qwen3.6 otherwise emits a long reasoning trace before every answer (~300 tokens/turn)
that dominates local latency and bloats the stored history — --reasoning-budget 0
alone does not stop it; use --chat-template-kwargs:
llama-server --model <Qwen3.6-35B-A3B-…-.gguf> --alias Qwen3.6-35B-A3B-local \
--host 127.0.0.1 --port 8888 -ngl 99 --flash-attn on --mlock \
-c 131072 -b 2048 -ub 2048 --cache-type-k q8_0 --cache-type-v q4_0 \
--cache-ram 4096 --ctx-checkpoints 24 \
--jinja --chat-template-kwargs '{"enable_thinking":false}' \
--parallel 1 --spec-draft-n-max 6 --spec-type draft-mtp,ngram-mod \
--override-kv qwen35moe.expert_used_count=int:6With this plus patches/0002-pi-stable-cwd.patch, warm turns run ~1–3s: llama.cpp
reuses the whole KV prefix and only the new message is prefilled. The first turn of
a session is still a cold full prefill (~30s).
qwen-local shows up alongside the Claude/GPT models. Switch by:
- Web UI model picker (per person/room) — pick
qwen-localor aclaude-*model. This is the normal, live way; Slack follows the scope's selection. - /admin — set the org base model, the Web-UI model list, or approved harnesses.
.env—PI_MODEL=qwen-localsets the pi default; unset it (or set aclaude-*id) to default back. Restart after editing.
Note: qwen-local runs only under the pi harness. Caveats: the first turn of a
session is slow (cold prefill of the ~6–7k-token system prompt + tools), and tool-calling
quality depends on the model. Heavy local-only use can skip the inbound content classifier
with HARNESS_SECURITY_POSTURE=dangerous (dev only — it otherwise also runs on the local
model). See the server-flags block above for the latency-critical settings; aux calls
(title/memory) run on the local model too as long as no Anthropic-provider base model is
set (keep the org base unset or qwen-local).
To update these customizations, edit the core and regenerate the affected patch, e.g.
git -C ../qm diff -- src/model/pi-models.ts > patches/0001-pi-qwen-local.patch or
git -C ../qm diff -- src/harness/pi-harness.ts > patches/0002-pi-stable-cwd.patch,
then commit it here.
Slack runs in-process with core — set the two tokens and restart, no public URL needed (Socket Mode).
- api.slack.com/apps → Create New App → From an app manifest → your
workspace. Paste
$QM_DIR/cli/templates/slack-manifest.json(Socket Mode on, all needed scopes/events already declared). - Install to Workspace → copy the Bot User OAuth Token (
xoxb-…). - Basic Information → App-Level Tokens → create one with scope
connections:write→ that is thexapp-…. - Put both in
.env:SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... scripts/down.sh && scripts/up.sh. Invite the bot to a channel and@-mention it, or DM it. (DM = your personal scope; channel = a shared scope.)
The agent's execute tool runs commands in a per-scope Docker sandbox. Build
the image once (slow under Apple-Silicon emulation):
scripts/build-sandbox.shUntil it exists, everything works except execute turns.
Session/run state is in Postgres (qm-pg), so it survives restarts. down.sh
leaves Postgres running; down.sh --pg stops it too. Data persists in the
qm-pgdata Docker volume.