Authored in a parallel session alongside the TypeScript migration; committed
separately so the two never share a commit. No TypeScript migration file is
included here.
Candidates becomes the queue of hiring decisions waiting on a person, rather
than a second Talent Pool listing every application the org ever took. Final
selection is DERIVED - there is no `final_selection` value in the
`application_status` enum and none is added. The fact it reads is the existence
of an interview row, a NOT NULL foreign key, rather than
`job_applications.interview_id`, which the schema keeps as an unconstrained soft
reference precisely so it may dangle. `status = 'interview'` is set both when an
interview is arranged and when one is completed, so status alone cannot tell a
queue of people who have been interviewed from a queue of people merely booked
in.
Seats on a position are counted from the employment records instead of a stored
column. A `filled` counter would be a second source of truth, and the day it
disagreed with `staff` nothing could say which was lying. Someone who has left
frees their seat, and over-hiring floors at zero rather than going negative.
`DEMO_FILL` is gone. `hiringRecords.js` padded the hires list with five invented
people so Hired History read as a history rather than as three rows; the padding
reached Analytics too, where "total hires" counted eight against a database
holding three. Hires now come only from `staff`.
Both paths that file an application on somebody's behalf now carry
`worker_profile_id`, the link back to the talent-pool record. The column is
nullable, so omitting it saved cleanly and failed silently: the application
belonged to an email address rather than to a person, and the hire it became
could not be traced back to the profile it came from.
`HiredChronology` used to `return null` with no hires, taking the `chronology`
node identity out of the DOM with it - so on an honest empty dataset the section
could not be addressed by Owliver or the layout editor at all. It now renders an
empty state inside the section it keeps.
Nine new checks cover the above; `npm test` reports 1684/1691.
SIX SSR PARITY CHECKS FAIL ON PURPOSE, and `scripts/__baseline__/README.md`
documents each with verified tag counts. Five are the `DEMO_FILL` removal: the
Hired History and Analytics baselines were captured while the padding was in
effect and, because they render with queries disabled, the padding is all they
contain. The sixth is this change to what Candidates says. Do not regenerate
those baselines to clear them - two of the checks exist to prove the UI node
tree migration added exactly two `<div>`s, and that proof needs the baseline to
be pre-migration markup. Recapturing now would write post-migration markup into
a file named `pre-migration` and the check would compare the current render
against itself forever. The debt is held until the migration work lands, when
both files are recaptured together.
The seventh failure, the stale backend seed fixture, predates all of this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
Phases 0-3 of the JS/JSX -> TS/TSX migration. No runtime behaviour changes:
every converted file emits byte-identical JavaScript, verified file by file.
Phase 0 - harness hardening, before any rename:
- scripts/ssr-resolve.mjs wraps `ssrLoadModule` so the ~170 literal module
paths in the check scripts resolve .js/.jsx/.ts/.tsx. Without it the first
rename would have silently destroyed the 1642-check suite that guards the
Owliver flow.
- eslint.config.js gains a TypeScript block. Its `files` globs listed only
{js,mjs,cjs,jsx}, so a renamed file would have dropped out of the run while
`eslint .` went on exiting 0 - the quietest failure mode available.
- MIGRATION_BASELINE.md records the measured starting point, including the
pre-existing seed-fixture failure and the already-broken standalone
owliver-baseline.mjs, so neither is later mistaken for migration damage.
Phase 1 - tsconfig.json succeeds jsconfig.json, carrying every option across at
its old value. `types` moves from [] to ["vite/client"], which fixes the eight
import.meta errors; @types/node is deliberately excluded so setTimeout stays a
number in browser code. allowJs and checkJs stay on, strict stays off.
Phase 2 - src/types/{api,entities,user}.ts. Transport envelope, error shape,
the entity-name union (the same 18 names are written down twice today, in
httpClient and base44Client, with nothing checking they agree), and the user
record. Every field transcribed from the API contract, the migrations and the
/me projection in me.go - not inferred. Entity record shapes are deliberately
absent: derivable, but nothing consumes them yet.
Phase 3 - nine leaf utilities renamed to .ts with annotations added only where
they could be established from existing usage. Entity records are typed `any`
with a comment naming what they are, rather than a guessed interface.
Verified: tsc 64 errors (65 before; one pre-existing TS2559 genuinely fixed,
none introduced), lint unchanged at 0 errors, skill-check 1641/1642 with only
the known failure, Owliver baseline section 59/59 green, production build
succeeds with the API origin inlined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
`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
The key had two incompatible meanings running at once. CLAUDE.md §3 defines
subagents as "keys of other specs this may DELEGATE to" and §6 as a tool call
from the parent's perspective — the subagent runs its own turn, as the same
caller, out of the parent's budget, and returns an answer. The backend
implements exactly that.
This side did something else: agentSkillIds folded one level of subagent skills
into the parent's carried set, so a parent silently gained everything its
subagents carried, and the UI described it that way — "other agents whose
skills this one may also use", "borrowing them cannot reach data this page does
not hold".
Both are defensible readings. Only one is the specification, and running both
meant krow-workforce-agent carried eight delegation tools AND the flattened
skills of those same eight agents — able to answer a question directly or to
ask an agent that had already lent it the means to answer. Two ways to do one
thing, differing in cost and in what the trajectory records.
So an agent carries what it declares. Reaching another agent is delegation,
which the runtime does with its own budget and its own trajectory.
The blast radius was one check, which is the useful part of the answer: only
"a subagent cycle terminates" depended on the folding, because that traversal
was the only thing that could loop. Nothing walks subagents here now, so the
cycle question moved to where it belongs — refused at publish by
definition.FindSubagentCycle, bounded at run time by the depth cap. The
replacement checks assert the new meaning rather than deleting the old ones,
because the previous behaviour reads as perfectly reasonable and will be
reinvented otherwise.
The `agents` parameter stays on agentSkillIds and is no longer read. Removing
it is a wider edit for no behavioural gain, and agentScopedDisabledWith exists
precisely to pass it.
924/924 checks pass; production build clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g