# Handover Written 2026-08-28, when the machine this was built on was retired. Everything Claude Code "remembers" lives in `~/.claude/projects//` on one machine, keyed to the absolute path of the checkout. It does not sync, and a different path on a new machine reads a different folder. So the durable record is this file, in the repository, where git carries it and any path works. Read `CLAUDE.md` first — it is the governing document. This file is what it does not say: what was decided, what is deployed, and which parts bite. --- ## Where things stand **Deployed.** Backend at `https://mcp.krowforce.com`, frontend at `https://platform.krowforce.com`, Kubernetes statefulset `krow` in namespace `krow`, pods named `krow-1` and `krow-2` (they start at 1, not 0). ssh root@ -p 4422 "kubectl -n krow rollout restart statefulset/krow && \ kubectl -n krow rollout status statefulset/krow --timeout=180s" **Verify a deployment** — 55 checks including a real agent run: KROW_EMAIL=... KROW_PASSWORD=... make verify-deploy BASE=https://mcp.krowforce.com Auth runs BEFORE routing, so an unauthenticated probe answers 401 for every path including ones that do not exist. `curl` cannot tell a missing endpoint from a guarded one; only an authenticated check can. **Two runtime steps a deploy does not do**, both easy to forget because the API looks healthy without them: kubectl -n krow exec krow-1 -- importagents --org # publishes agents/ and skills/ kubectl -n krow exec krow-1 -- ingest --org # ingests knowledge/ Without the first, every Owliver question answers 404. Without the second, retrieval finds nothing. The org slug is `krow-dev` — a hardcoded constant (`internal/orgctx.DevOrgSlug`), not configuration. **The endpoint count is a signal.** `GET /api/v1/version` reports it. Agent run routes are not registered without a model credential, so 56 means no `ANTHROPIC_API_KEY` and 58 means there is one. A keyless deployment boots cleanly under `APP_ENV=staging` and refuses under `production`. --- ## Decisions taken, so they are not relitigated **§12, who authors agents: self-serve, split by visibility.** `personal` agents are authored in the UI, POST to `/api/v1/agent-definitions`, and are runnable immediately; evals are not required for them. `organization` agents stay as files published by `importagents` on deploy — that deploy step *is* the approval workflow, and §9's eval requirement still applies. §12 warned self-serve needs "3× the platform"; it does not here, because I1 means an agent runs as its caller and cannot exceed their access, I4 means writes still need a human, and the spec format has no `limits` block so budgets cannot be raised by an author. **A rejected candidate counts as screened.** It ranks with `ai_screened`: rejection overwrites the stage it came from, so shortlisted can never be claimed. **An assigned candidate counts as hired**, matching the backend's existing `status IN ('hired','assigned')`. **Seeded profile scores are the formula's output**, not hand-authored narrative. Recalculating a seeded profile is a no-op, and a skill-check enforces it. --- ## Conventions the schema actively contradicts These are the ones that produce confident, wrong numbers rather than an error. **`job_applications.screened_at` is vestigial.** Nothing writes it. "Screened" means `status <> 'applied'`, in about eight places in the frontend. Reading the column reported 1 screened of 24 where the truth was 14. **A score of `0` means "not rated", never "rated zero".** Every score column is NOT NULL, so there is no null to distinguish it — that is the trap. Applies to `ai_score`, `client_rating`, `krow_score`, `reliability_score`, `attendance_score`, `performance_score`, `experience_years`. Aggregates need `FILTER (WHERE col > 0)` and a stated basis count. Counting zeros once reported "16 weak candidates averaging 28" for a pool that was 1 weak averaging 76. Anchors to check against: applications are **9 scored averaging 76**; workers are **5 rated of 9, client rating 4.70**. **Genuine zeros, do not filter these:** `overtime_hours`, `minutes_late`, `xp`, `profile_completion`, and `actual_hours` (0 only on absent/no_show shifts). **`absent` and `no_show` are both missed shifts, but only one is a no-show.** `attendance.js` is canonical. **`attendance_score` defaults to 100 for display and must never be a scoring input.** A profile with no evidence otherwise scores 12 and leaves the "not yet scored" band. **The stage ladder lives once**, in `krow-demo/src/lib/hiringRecords.js`. It was three byte-identical private copies, all missing `rejected` and `assigned`, which `indexOf` scored -1 and dropped from every bucket including `applied`. --- ## Things that will waste your afternoon **A space in the checkout path breaks path derivation.** This repo lives under `Krow Project /`, and it has bitten three times: an unquoted `$(CURDIR)` in the Makefile, `` `file://${process.argv[1]}` `` in a script guard, and `new URL(...).pathname` in `scripts/oracle.mjs` (use `fileURLToPath`). Any new path derivation is guilty until tested there. **`seed.json` is generated from `krow-demo/src/api/seed.js`.** Never edit it. `npm run seed:fixture` writes it, `npm run seed:check` verifies, and the skill-check compares byte-for-byte. `ShiftRecord` is excluded on purpose: the Go seeder generates it against *now*. **Shift data is anchored to today**, so anything asserting against it is date-dependent unless the anchor is pinned. `buildShiftsAt(anchor)` exists for that. One detector had a Friday-and-Saturday blind spot for exactly this reason. **Database tests skip when PostgreSQL is unreachable** (`testutil` calls `t.Skipf`). `go test` then exits 0 having run almost nothing. CI has a guard that fails on any skip other than `TestLive*`; keep it. **The eval suites use a scripted model.** They prove the permission boundary, not answer quality. `make eval-live` uses the real model and costs tokens; it has never been run. --- ## Still outstanding - The knowledge corpus is 8 documents locally; production still has the 2 seeded ones until `ingest` runs there. - `ANTHROPIC_API_KEY` was pasted into a chat transcript and is live in a Kubernetes Secret. Rotate it. - Deployments report `version=dev`: the image is built without `--build-arg VERSION`. `make docker-build` passes it. - `APP_ENV=staging` on the deployment, so the production config guards are off. - Skills are still stored in `user_preferences`; agents were moved to the registry and skills were deliberately left for their own pass. - `definition_versions` is empty — immutable versioning is built, trigger-proven, and nothing has gone through it because `importagents` republishes in place. - CI tests but does not deploy. The README's claim that migrations are "run by CI against the target database" is still aspirational. - The fixture-drift CI jobs need `FRONTEND_REPO_TOKEN` to see the sibling repo, and fail rather than pass quietly without it. - The remote is Gitea. These are GitHub Actions workflows; they do nothing until a compatible runner exists. --- ## Setting up a new machine git clone krow-backend && git clone krow-demo cp krow-backend/CLAUDE.md ./claude.md # the governing doc lives above both repos Needs: Go (see `go-api/go.mod`), Node 20, PostgreSQL, Docker, and Ollama with `nomic-embed-text` if you want semantic retrieval locally. Then: cd krow-backend && cp .env.example .env # fill it in; .env is gitignored make migrate-up && make seed make import-agents ORG=krow-dev make ingest ORG=krow-dev go run ./go-api/cmd/setpassword -email demo@krow.app cd ../krow-demo && npm ci && cp .env.example .env # VITE_AGENT_API=/api/v1 for local dev (vite proxies it); # production passes an absolute URL as a Docker build arg instead. `.env` files are not in git and must be carried across by hand.