Hatching¶
The HatchingManager class (missy/agent/hatching.py) implements "hatching" — Missy's first-run bootstrap experience, where the agent establishes identity, validates its environment, configures behavior, initializes memory, and becomes usable. It's an idempotent, resumable state machine: each of its 8 steps is tracked individually, so an interrupted or failed hatching can pick up exactly where it left off on the next run. The missy hatch CLI command (documented at Core Commands) is the operator-facing entry point; this page covers the underlying design.
The 8 Steps¶
flowchart TD
A[validate_environment] --> B[initialize_config]
B --> C[verify_providers]
C --> D[initialize_security]
D --> E[generate_persona]
E --> F[check_vision]
F --> G[seed_memory]
G --> H[finalize] validate_environment— Checks the running Python is 3.11+ (hard failure otherwise), ensures~/.missy/can be created and is writable, and checks available disk space against a 50 MiB minimum (_MIN_FREE_BYTES). Low disk space is a non-fatal warning, not a hard failure.initialize_config— Writes a default~/.missy/config.yamlif none exists (network presetanthropic, shell/plugins disabled,workspace_path: ~/workspace). Does nothing if a config already exists.verify_providers— Looks forANTHROPIC_API_KEYorOPENAI_API_KEYin the environment, then falls back to scanningconfig.yaml'sproviderssection for a non-emptyapi_key/api_keys. If nothing is found, this raises a non-fatal warning (hatching still proceeds; credentials can be added later).initialize_security— Creates~/.missy/secrets/with mode0o700. Does not generate the Ed25519 identity keypair itself — that's created lazily byAgentIdentityon first use — but logs whether~/.missy/identity.pemis already present.generate_persona— If~/.missy/persona.yamldoesn't already exist, constructs aPersonaManagerand callssave()to write the default persona. See Persona for the full schema.check_vision— Non-fatal readiness check for the optional vision subsystem: verifiesopencv-python-headlessandnumpyare importable, callsdiscover_cameras(force=True), and checks for screenshot tooling viaVisionDoctor.check_screenshot_tools(). Any missing piece is collected into anissueslist and raised as a single non-fatal warning — vision is optional and never blocks hatching.seed_memory— Opens theSQLiteMemoryStoreat~/.missy/memory.dband writes a single welcomeConversationTurn(session_id="hatching",role="system", content"Missy hatching completed. Ready to assist."). Failure here is also non-fatal.finalize— Setsstatus = HATCHED, stampscompleted_at, clears any priorerror, and persists the final state.
State Machine¶
HatchingStatus is a plain Enum with four values: UNHATCHED, IN_PROGRESS, HATCHED, FAILED. run_hatching():
- Short-circuits immediately if already
HATCHED. - If resuming from
FAILED, clearsstate.errorand proceeds. - Sets status to
IN_PROGRESS, stampsstarted_at(only if not already set, so a resumed run keeps its original start time). - Iterates the 8 steps in order, skipping any step whose name is already in
state.steps_completed— this is what makes hatching resumable. - On success, appends the step name to
steps_completedand saves state after every step (not just at the end). - On a caught
_HatchingStepWarning, logs it as"warn", still marks the step completed, and continues — this is the mechanism steps 1, 3, 6, and 7 use to signal "non-fatal, keep going." - On any other
Exception, logs"error", setsstatus = FAILEDwith a descriptiveerrormessage, saves, and returns immediately — remaining steps do not run until the nextrun_hatching()call.
from missy.agent.hatching import HatchingManager
manager = HatchingManager()
if manager.needs_hatching():
state = manager.run_hatching(interactive=False)
if state.status.name == "HATCHED":
print("Missy is ready.")
else:
print(f"Hatching incomplete: {state.error}")
needs_hatching() returns True when no state file exists, or when the persisted status is UNHATCHED or FAILED. is_hatched() is a convenience check for status is HatchingStatus.HATCHED. reset() deletes the state file (forcing a full re-hatch) but deliberately preserves the hatching log, so the history of all attempts survives a reset.
HatchingState Fields¶
| Field | Type | Description |
|---|---|---|
status | HatchingStatus | Current lifecycle phase |
started_at / completed_at | str \| None | ISO-8601 UTC timestamps |
steps_completed | list[str] | Names of steps that have succeeded (drives resumability) |
persona_generated | bool | Set by step 5 |
environment_validated | bool | Set by step 1 |
provider_verified | bool | Set by step 3 |
security_initialized | bool | Set by step 4 |
memory_seeded | bool | Set by step 7 |
error | str \| None | Description of the last failure, if any |
Persistence¶
State is written atomically to ~/.missy/hatching.yaml: HatchingManager._save_state() writes to a .yaml.tmp sibling file (created with mode 0o600 via os.open) and then Path.replace()s it into place, so a crash mid-write can't corrupt the state file. It's saved after every step transition, not just at the end.
A separate structured HatchingLog (missy/agent/hatching.py) appends one JSON object per line to ~/.missy/hatching_log.jsonl (also mode 0o600) for every step attempt, with timestamp, step, status ("ok"/"warn"/"error"/"info"), message, and optional details. This log is intentionally append-only and survives resets, giving a full audit trail of every hatching attempt across the agent's lifetime. get_hatching_log() returns all entries in chronological order.
Integration with the Runtime¶
Hatching runs before the Agent Runtime is fully operational — it's the bootstrap that ensures the config file, persona file, security directories, and memory store the runtime depends on all exist. The generate_persona step directly constructs a PersonaManager to seed ~/.missy/persona.yaml, and seed_memory writes directly into the same SQLiteMemoryStore the runtime uses at steady state.
Related¶
- Core CLI Commands — the
missy hatchcommand reference - Persona — the persona system seeded by the
generate_personastep - Agent Runtime — the subsystem hatching prepares the ground for