Neither survived a machine change. CLAUDE.md sat in the directory ABOVE both repositories, which is not a git repository at all, so the governing document for the project existed on exactly one laptop. It is now in this repository; place a copy at the parent level on a new machine, where it covers both. docs/handover.md records what CLAUDE.md does not: what was decided and why, what is deployed and how to verify it, and the conventions that produce confident wrong numbers rather than errors — a score of 0 meaning "not rated", screened_at being vestigial, shift data anchored to today. Written because Claude Code's own memory is per-machine and keyed to the absolute path of the checkout: it does not sync, and a different path on a new machine reads a different folder. A file in the repository travels with the code and is useful to a person besides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186JgqQUCDS8ZwGmyw3ymWu
172 lines
7.8 KiB
Markdown
172 lines
7.8 KiB
Markdown
# Handover
|
||
|
||
Written 2026-08-28, when the machine this was built on was retired.
|
||
|
||
Everything Claude Code "remembers" lives in `~/.claude/projects/<mangled-path>/`
|
||
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@<host> -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 <slug> # publishes agents/ and skills/
|
||
kubectl -n krow exec krow-1 -- ingest --org <slug> # 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 <backend> krow-backend && git clone <frontend> 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.
|