ClientUtils.Harness.Onboarding (client_utils v0.1.23)
View SourceWhat onboarding a working copy decides, with none of the doing.
Pure policy, so the two callers can share it without sharing anything else.
Mix.Tasks.Harness.Onboard writes these results to disk for a generated
application; CodeMySpec's Mix.Tasks.Cms.OnboardHarness writes the same results
through its own environment abstraction. Neither this module nor that task
learns what the other is.
That split is the reason onboarding lives here rather than in CodeMySpec: a
generated application depends on client_utils and not on CodeMySpec, so this
is the only placement where both can onboard themselves with one command.
Nothing here touches a disk or a database
Every function returns a value. The commands for creating and migrating
databases are returned as strings — they are handed to whoever ran the
command, never executed. That rule is not stylistic: the version of this that
ran them shelled out to mix with an environment that had MIX_ENV scrubbed
out of it, resolved to the shared development database, and a sibling truncate
emptied it three times on 2026-08-13.
Summary
Functions
The address an agent's model turns relay through.
Whether the copy at root has been onboarded, and what to run if not.
Every database this working copy uses, each with the commands that create and migrate it.
Git settings a working copy needs.
Configure the working copy at root, and report what is left.
The database partition name for a working copy at root.
The settings a working copy needs, as a map ready to be encoded.
The file settings belong in. Untracked, deliberately — see settings/2.
Functions
The address an agent's model turns relay through.
One function, both callers, on purpose. This string was rendered twice — once
here and once in CodeMySpec's own task — and the two disagreed: /api/harnesses/<id>
against /harnesses/<id>/anthropic, of which only the first is a real route in
CmsHarness.Web.Router. A copy onboarded by the wrong one relays every turn to a
404 and its agent's work never reaches the server.
That is the defect this whole story exists to remove — two derivations of one value, disagreeing by accident — reproduced by the refactor meant to fix it, and hidden because each half looked right on its own. Neither half should be able to render this alone.
The caller supplies the base because only it knows one: CodeMySpec reads the port the generated plugin was addressed to.
Whether the copy at root has been onboarded, and what to run if not.
Absence has to report itself. A missing thing that produces no error where it is missing surfaces somewhere else wearing another failure's costume, and stopping that is what onboarding is for.
@spec databases(String.t(), atom() | String.t()) :: [ %{ name: String.t(), partition: String.t(), create: String.t(), migrate: String.t() } ]
Every database this working copy uses, each with the commands that create and migrate it.
Two, not one. An analyzer and an interactive session deliberately use different databases — running a suite and an analyzer concurrently against one produced 13 orphaned rows and 17 phantom failures — so reporting a single database hides the second, and hiding the second is how an agent migrates one and stays blocked on the other for three days.
Each command names its own database for the same reason. The migration guard
printed MIX_ENV=test mix ecto.migrate while checking a partition that command
never touches; an agent followed it correctly and nothing changed.
Git settings a working copy needs.
submodule.recurse because a submodule sits in detached HEAD by design and so
accepts writes with no branch to carry them. Four QA briefs — including the
evidence behind a story shipped with QA recorded complete — existed in exactly
one working copy for a day because of it, and nothing reported a problem.
Configure the working copy at root, and report what is left.
Writes the settings a copy needs, sets the git config it needs, then names the databases it still wants — and does not create them. Returns a report; the caller decides how to show it.
Pass io: to route the writes somewhere other than the real filesystem. The
default is FileIO, which is what a generated application gets without
configuring anything.
No database is created, migrated or dropped. The commands come back as
strings and running them is the operator's job. The rule is not stylistic: the
version that ran them shelled out to mix with an environment that had
MIX_ENV scrubbed out of it, resolved to the shared development database, and a
sibling truncate emptied it three times on 2026-08-13.
No worktree is created. This configures the copy it is pointed at.
The database partition name for a working copy at root.
Assigned here once, at onboarding, and recorded — which is the whole point.
Two mechanisms currently derive this independently and disagree by accident:
config/test.exs builds a name from the worktree path, TestDatabase digests
the cwd. The digest below is not better than either; it is only computed in one
place and written down, and that is what makes it answerable.
The settings a working copy needs, as a map ready to be encoded.
Goes to .claude/settings.local.json, never .claude/settings.json. The base
URL carries the harness id, and settings.json is tracked — so writing it there
stages one machine's identity for commit and the next clone inherits it, looking
onboarded while talking to a harness that is not its own.
@spec settings_path() :: String.t()
The file settings belong in. Untracked, deliberately — see settings/2.