`positionFlow.js` was five per-domain concerns in one file — a field table, an
`@`-token resolver, a sentence extractor, a commit vocabulary and a set of
outcome renderers — and only the control flow between them was general.
Everything else knew it was creating a job posting. Adding a second
conversation meant a second copy of all of it.
So the control flow is now `conversationFlow.js` and each kind of record is a
REGISTRY. Which conversation a skill runs is the skill's own `flow:` line,
resolved through a map: `routing.js` used to say `if (skill.id ===
'create-position')`, which made a second conversational skill a change to the
router rather than a file on disk — the `if agent_key == ...` shape the
platform rules out one level up. The panel's write callback is likewise a map
keyed by flow id instead of an `onCreatePosition` prop, and the outcome wording
comes off the registry, so nothing in the panel names a kind of record any
more.
`create-employee-role` is the second registry. The worker is asked for and
never assumed: a conversation that names nobody re-asks rather than falling
back to the session, because an operator records this on somebody's behalf. Its
`extract` is deliberately narrower than the posting's — "bartender, weekends,
$30/hr" settles three fields and leaves the subject alone, since guessing WHO a
record is about from a fragment is how a role gets filed against the wrong
person.
THE CONFIRMATION STEP ACCEPTED "create position" AND SILENTLY REJECTED "create
positions" — the plural the Positions page itself uses. An anchored regex missed
it, and the reader got the summary back with no indication of what was wrong
with what they said, which is indistinguishable from the screen not having
updated. Matching is now exact membership against a normalized reply, so a
vocabulary is a list of phrases somebody can read rather than an expression
somebody has to parse.
Two bugs in `extractRole`, both of which fabricated a value nobody typed on the
one field a position cannot be created without:
- The phrase pattern marks "new" as the role by the same grammar that marks
"sous chef", so "create new position" opened the conversation titled "New".
The scaffolding is a PHRASE at least as often as a single word, so a
per-word test still produced "Brand New" and "One More". Scaffolding words
are now stripped to DECIDE whether the phrase named anything, and the
ORIGINAL phrase is returned when it did — strip to test, never to rewrite,
or "second chef" becomes "Chef" and the cure is worse than the bug.
- `(?:a|an)?\s*` has no word boundary, so it matched the leading "a" of
"another" and the capture began mid-word. That mangled scaffolding into
"Nother New" and, worse, corrupted every role introduced with "an":
"create an open kitchen lead position" titled the position "N Open Kitchen
Lead". A real role, typed correctly, silently wrong. Found by mutation
testing the first fix.
`@companies` and `@workers` resolve from data the panel already holds — postings
and profiles the API has already scoped to the caller — so neither widens
anybody's view and neither costs a request. The company list is deliberately
unsorted: `useJobPostings` asks for `-created_date`, so the clients staffed for
most recently come first, and the panel does no ranking of its own. That last
part is a rule the suite enforces structurally, and it is the right rule — a
second opinion formed in the panel outranking the server's is exactly the kind
of thing that decays quietly.
`npm test` now refuses a conversation step whose field its registry does not
define. `stepsOf` drops unknown fields, so a typo means the flow asks fewer
questions than the file lists — and a skill whose steps are ALL unknown asks
none, jumps to the summary, and offers to write an empty record. Nothing errors
and the Markdown still reads correctly. 970 checks, up from 924; the new ones
walk both conversations end to end, because a wrong answer at the confirmation
step re-renders the same summary a right answer does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
KROW Demo
AI-powered hiring for event and hospitality staffing — a standalone demo build of KROW Forge.
Runs with no backend, no API keys and no real login. npm install && npm run dev and the app is fully
populated.
npm install
npm run dev # http://localhost:5173 — opens on /admin/login
Stack
Vite 6 · React 19 · React Router 6 · React Query 5 · Tailwind + shadcn/ui · Framer Motion · Recharts + MUI X Charts · react-hot-toast.
Structure
src/
main.jsx entry — mounts <App/>, imports index.css
App.jsx providers + the entire route table
index.css design tokens (HSL CSS variables) + utility layers
api/ the backend seam — see below
base44Client.js the contract the production app talks to
store.js in-memory entity store, mirrored to localStorage
aiEngine.js local, deterministic InvokeLLM
seed.js demo dataset
attendanceSeed.js shift/attendance dataset
lib/ domain logic and data hooks
krowHooks.js React Query hooks over the entity store
krowAi.js AI workflows
krowScore.js score engine
AuthContext.jsx auth provider
admin/ admin session, permissions, position insights
skills/ the Owliver skill system (registry, resolver, flows)
agents/ the agent system (registry, runtime, lifecycle)
skills/ *.md skill definitions — loaded by import.meta.glob
agents/ *.md agent definitions — loaded by import.meta.glob
components/
ui/ 23 shadcn primitives
ds/ 30 design-system components (the app's real vocabulary)
admin/ admin page shell + role glyphs
krow/ domain components (+ talent/, proving/)
charts/ MUI X Charts wrappers and theme
agents/ agent configure / canvas / test panel
forge/ KROW Forge skill + challenge components
skills/ skill authoring surfaces
ai-assistant/ Owliver — panel, routing, capabilities, history
layouts/ AdminLayout — header nav, ⌘K search, assistant panel
pages/ route components (admin/ holds the live product)
hooks/ shared hooks
assets/ brand asset references
Routes
The app opens on /admin/login. Everything else lives under /admin/* behind two guards:
ProtectedRoute (auth) and AdminRoute (admin session), rendered inside AdminLayout.
| Path | Page |
|---|---|
/admin/login |
Admin sign-in — outside the protected boundary |
/admin |
Control Center |
/admin/positions, positions/new, positions/:id |
Positions, Create, Detail |
/admin/candidates, candidates/:id |
Candidates, Candidate Intelligence profile |
/admin/hired · /admin/talent-pool |
Hired History, Talent Pool |
/admin/university, university/:id |
KROW Forge, challenge detail |
/admin/analytics · /admin/activity |
Analytics, Activity |
/admin/profile · /admin/settings |
Profile, Settings |
/admin/workspace |
Workspace hub |
/admin/workspace/agents, agents/new, agents/:id |
Agent list, agent editor |
/admin/workspace/skills, skills/new, skills/:id |
UI skill list and editor |
/admin/workspace/skills/owliver/new, owliver/:id |
Owliver skill editor |
/admin/workspace/skill-development |
Skill Development |
* |
lib/PageNotFound.jsx |
Legacy top-level paths (/overview, /positions, /candidates, /hired, /talent-pool,
/university, /analytics, /activity, /tracking) all Navigate into their /admin/*
equivalent, id preserved. Static route segments are declared before dynamic ones so
workspace/agents/new cannot be read as an agent whose id is "new".
Note.
App.jsxstill imports a set of pre-redesign Employer/Talent pages (Overview,Apply,WorkerProfile,KrowIdentity,EmployeeDashboard,DesignSystem, …) andlayouts/Layout.jsx. None of them are mounted on a route — they are unreachable at runtime and retained pending a decision on whether to re-route or remove them.
The backend seam
The production app talks to a Base44 backend through src/api/base44Client.js. This demo keeps that
module path, export name and full method contract, and swaps only the transport:
| Contract | Demo implementation |
|---|---|
base44.entities.<Name>.list/filter/get/create/update/delete |
api/store.js — in-memory records, mirrored to localStorage |
base44.integrations.Core.InvokeLLM |
api/aiEngine.js — local, deterministic |
base44.integrations.Core.UploadFile |
blob URLs, session-scoped |
base44.auth.* |
seeded employer/admin user, always signed in |
Because the seam did not move, everything above it — lib/krowHooks.js, lib/krowAi.js,
lib/krowScore.js, every page and component — is unchanged from the reference implementation.
The AI engine
Every AI workflow funnels through one call, InvokeLLM({ prompt, response_json_schema }).
api/aiEngine.js reimplements it locally: it recognises each workflow by the phrase its prompt opens
with, reads the structured fields the prompt already carries (Years Experience: 6,
Required Certifications: …, the talent-pool JSON block) and scores them deterministically.
Nine workflows are implemented: candidate screening, job-description generation, résumé building, interview questions, interview evaluation, talent matching, Owliver's conversation, Career DNA building and Proving Ground evaluation. Results respond to the data rather than being canned — the same input always returns the same output.
Skills and agents
Two registries discover their definitions from markdown at build time:
| Registry | Glob | Files |
|---|---|---|
lib/skills/registry.js |
import.meta.glob('/src/skills/**/*.md') |
18 Owliver skills, 5 workforce training skills |
lib/agents/registry.js |
import.meta.glob('/src/agents/**/*.md') |
9 agent definitions |
These globs are absolute paths. Moving or renaming src/skills/ or src/agents/ makes the
registry silently return nothing — no build error, no import failure, just an empty registry. Add
definitions by dropping a new .md file into the right folder; nothing else needs to change.
Demo data
Seeded in src/api/seed.js — positions, applicants, AI-scored candidates, interviews, hires,
talent-pool profiles, Proving Ground courses and an activity log; shift data in
src/api/attendanceSeed.js.
Edits persist to localStorage. To restore the shipped dataset:
// in the browser console
localStorage.removeItem('krow_demo_db'); location.reload();
Bumping STORAGE_VERSION in src/api/store.js (currently 8) invalidates saved snapshots
automatically, so changing the seed does not require a manual clear.
Design system
Tokens live in src/index.css as HSL CSS variables consumed by tailwind.config.js.
| Token | Value |
|---|---|
| Primary — KROW Blue | #0A39DF |
| Accent — KROW Yellow | #F9E547 |
| Pale Yellow | #F8E08E |
| Dark Navy | #333F48 |
| Mint | #D1E0D7 |
| Radius | 0.75rem |
| Headings | Sora |
| Body | Inter |
src/components/ds/ is the vocabulary the app actually writes in — Surface, KpiCard,
DataTable, PageHeader, Modal, toast and the rest, exported through ds/index.js.
src/components/ui/ holds the shadcn primitives those are built on. Surfaces use glass and
glass-card; the page background is the gradient-mesh utility.
Toasts go through ds/toast (a wrapper over react-hot-toast), rendered by <HotToaster> in
App.jsx.
Scripts
| Command | Purpose |
|---|---|
npm run dev |
Dev server (honors PORT, default 5173) |
npm run build |
Production build |
npm run preview |
Serve the build |
npm run lint |
ESLint — currently clean |
npm run lint:fix |
ESLint with --fix |
npm test |
scripts/skill-check.mjs — 835 assertions over the skill and agent systems |
npm run typecheck |
tsc -p ./jsconfig.json with checkJs |
The test harness
npm test boots a real Vite dev server in middleware mode and ssrLoadModules the actual
application modules, so it exercises the real @/ alias and import.meta.glob pipeline rather than
mocks. It also diffs Owliver's behaviour across eleven page contexts against a committed snapshot,
scripts/__baseline__/owliver-baseline.json; regenerate that deliberately with
node scripts/owliver-baseline.mjs --write, never to turn a red check green.
Because it loads modules by absolute path, the harness pins the locations of src/api/, src/lib/,
src/lib/skills/, src/lib/agents/, src/components/ai-assistant/, src/App.jsx,
src/layouts/AdminLayout.jsx and a handful of agent files. Move any of them and update
scripts/owliver-capture.mjs and scripts/skill-check.mjs in the same change.
Known-failing checks
Two checks fail on a clean checkout and are tracked as separate work, not regressions:
npm test— 834/835 pass;the seeded overtime climb is foundfails.npm run typecheck— 59 errors, all pre-existing JSDoc/inference gaps inlib/krowAi.js,lib/skills/*,lib/positionModel.jsand a few components.
Deployment
Dockerfile builds with node:22-alpine (capped at 1 GB heap for small hosts) and serves the
static dist/ from nginx:alpine using nginx.conf — gzip, immutable /assets/ caching and an
SPA fallback to index.html so client-side routes resolve on refresh.