Files
krow_backend/docs/handover.md
Aravind d190fc8ee9
Some checks failed
CI / test (push) Has been cancelled
CI / fixture (push) Has been cancelled
Preserve CLAUDE.md and add a handover document
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
2026-08-28 13:56:50 +05:30

172 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.