Port NPA psychographic profiling into IronClaw#321
Port NPA psychographic profiling into IronClaw#321jayzalowitz wants to merge 1 commit intonearai:stagingfrom
Conversation
Summary of ChangesHello @jayzalowitz, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the agent's ability to understand and interact with users by porting a sophisticated psychographic profiling system. It introduces a conversational onboarding experience to build rich user profiles, which then dynamically inform the agent's communication style, proactivity, and assistance preferences. This foundational change aims to create a more personalized and effective user experience from the very first interaction. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR ports the complete psychographic profiling system from cami2 into IronClaw, enabling personalized assistant behavior through a conversational onboarding flow and ongoing profile evolution. The system introduces a 9-dimension analysis framework that captures personality traits, communication preferences, behavioral patterns, and relationship dynamics.
Changes:
- Adds a 987-line profile module with 5 new data structures (CohortClassification, FriendshipQualities, RelationshipValues, InteractionPreferences, AnalysisMetadata) and backward-compatible schema migration from v1 to v2
- Implements conversational onboarding engine with 6-intent tracking, one-step-removed questioning technique, and LLM-driven profile generation with retry logic
- Introduces three-tier system prompt augmentation: Tier 1 summary always shown, Tier 2 full personalization gated on confidence > 0.6 and 7-day recency check
- Adds two new skills (delegation, routine-advisor) for passive profiling and proactive automation suggestions
- Configures SKIP_WIZARD environment variable to bypass setup on hosted platforms
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/profile.rs | Core psychographic profile types with 9-dimension framework, custom deserializers for backward compatibility, and rendering methods for USER.md, assistant-directives.md, and HEARTBEAT.md |
| src/setup/onboarding_chat.rs | Conversational onboarding engine with intent tracking, JSON parsing with retry logic, and profile generation from conversation transcript |
| src/setup/profile_evolution.rs | Weekly profile update prompt generation with confidence gating and evolution routine template |
| src/setup/wizard.rs | Step 9 integration: personal onboarding flow with LLM provider initialization and multi-backend profile storage |
| src/workspace/mod.rs | Three-tier system prompt injection with confidence and recency checks, plus first-contact guidance for new users |
| src/workspace/document.rs | New path constants for profile.json and assistant-directives.md |
| src/main.rs | SKIP_WIZARD environment variable check to bypass wizard on hosted platforms |
| src/settings.rs | personal_onboarding_completed flag tracking |
| src/lib.rs | Public profile module export |
| src/setup/mod.rs | Module exports for onboarding_chat, profile_evolution, and SetupError |
| skills/delegation/SKILL.md | Task delegation skill with profile-aware communication and autonomy levels |
| skills/routine-advisor/SKILL.md | Routine suggestion skill with pacing guidelines and user-type-specific ideas |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
8e091fb to
04886e1
Compare
There was a problem hiding this comment.
Code Review
This pull request successfully ports the psychographic profiling system from cami2 into IronClaw. It introduces a comprehensive 9-dimension analysis framework, conversational onboarding, and automated profile evolution. The implementation is robust, including backward compatibility for older profile schemas and integration with the existing workspace and setup wizard. I have identified a few areas for improvement, primarily around type safety by using enums instead of strings for fixed categories, refining the JSON parsing logic to be more resilient, and ensuring explicit error handling for parsing failures, aligning with best practices for clear error messages.
04886e1 to
bacfe08
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4d13201 to
b5f5e32
Compare
|
Instead of doing this during wizard - let's move it into first interaction with the assistant. Onboarding after asking questions can educate user what is possible to do and help setup channels like telegram/whatsapp/etc based on question like "what is your preferred personal/work communication". |
cdc3c8b to
5209483
Compare
|
Done — moved personal onboarding from wizard Step 9 to the first assistant interaction. What changed:
The onboarding chat engine ( |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
5209483 to
c28be23
Compare
c28be23 to
4cd6cd2
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4cd6cd2 to
e98dadd
Compare
e98dadd to
6c1081a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b3e727a to
ef9b0dd
Compare
ef9b0dd to
c6369b4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
c6369b4 to
4d57fa3
Compare
Port the complete psychographic profiling system from NPA into IronClaw, including enriched profile schema, conversational onboarding, profile evolution, and three-tier prompt augmentation. Personal onboarding moved from wizard Step 9 to first assistant interaction per maintainer feedback — the First Contact system prompt block now instructs the LLM to conduct a natural onboarding conversation that builds the psychographic profile via memory_write. Changes: - Enrich profile.rs with 5 new structs, 9-dimension analysis framework, custom deserializers for backward compatibility, and rendering methods - Add conversational onboarding engine with one-step-removed questioning technique, personality framework, and confidence-scored profile generation - Add profile evolution with confidence gating, analysis metadata tracking, and weekly update routine - Replace thin interaction style injection with three-tier system gated on confidence > 0.6 and profile recency - Replace wizard Step 9 with First Contact system prompt block that drives conversational onboarding during the user's first interaction - Add autonomy progression to SOUL.md seed and personality framework to AGENTS.md seed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4d57fa3 to
d22bdef
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Sync derived identity documents when the profile is written. | ||
| let mut synced_docs: Vec<&str> = Vec::new(); | ||
| if path == paths::PROFILE { | ||
| match self.workspace.sync_profile_documents().await { | ||
| Ok(true) => { | ||
| tracing::info!("profile write: synced USER.md + assistant-directives.md"); | ||
| synced_docs.extend_from_slice(&[paths::USER, paths::ASSISTANT_DIRECTIVES]); | ||
|
|
There was a problem hiding this comment.
context/assistant-directives.md is now injected into the system prompt (workspace/mod.rs), but it is not included in PROTECTED_IDENTITY_FILES. This means an attacker (or prompt-injected model) can overwrite it directly via memory_write, persisting malicious system instructions. Add paths::ASSISTANT_DIRECTIVES to the protected list, and keep updates to this file restricted to the trusted sync_profile_documents path.
| if path == paths::PROFILE { | ||
| match self.workspace.sync_profile_documents().await { | ||
| Ok(true) => { | ||
| tracing::info!("profile write: synced USER.md + assistant-directives.md"); | ||
| synced_docs.extend_from_slice(&[paths::USER, paths::ASSISTANT_DIRECTIVES]); | ||
|
|
||
| // Persist the onboarding-completed flag. | ||
| let toml_path = crate::settings::Settings::default_toml_path(); | ||
| if let Ok(Some(mut settings)) = crate::settings::Settings::load_toml(&toml_path) | ||
| && !settings.profile_onboarding_completed | ||
| { | ||
| settings.profile_onboarding_completed = true; | ||
| if let Err(e) = settings.save_toml(&toml_path) { | ||
| tracing::warn!("failed to persist profile_onboarding_completed: {e}"); | ||
| } | ||
| } | ||
| } | ||
| Ok(false) => { | ||
| tracing::debug!("profile not populated, skipping document sync"); | ||
| } | ||
| Err(e) => { | ||
| tracing::warn!("profile document sync failed: {e}"); | ||
| } |
There was a problem hiding this comment.
sync_profile_documents() writes USER.md (a protected identity file) as a side-effect of a memory_write call to the profile. This effectively reintroduces a tool-driven path to modify system-prompt identity content. Consider sanitizing/escaping profile-derived strings before emitting markdown, and/or ensuring the sync happens only after validating the profile JSON parses as PsychographicProfile and was written with append=false (to prevent partial/poisoned writes from generating identity docs).
| if path == paths::PROFILE { | |
| match self.workspace.sync_profile_documents().await { | |
| Ok(true) => { | |
| tracing::info!("profile write: synced USER.md + assistant-directives.md"); | |
| synced_docs.extend_from_slice(&[paths::USER, paths::ASSISTANT_DIRECTIVES]); | |
| // Persist the onboarding-completed flag. | |
| let toml_path = crate::settings::Settings::default_toml_path(); | |
| if let Ok(Some(mut settings)) = crate::settings::Settings::load_toml(&toml_path) | |
| && !settings.profile_onboarding_completed | |
| { | |
| settings.profile_onboarding_completed = true; | |
| if let Err(e) = settings.save_toml(&toml_path) { | |
| tracing::warn!("failed to persist profile_onboarding_completed: {e}"); | |
| } | |
| } | |
| } | |
| Ok(false) => { | |
| tracing::debug!("profile not populated, skipping document sync"); | |
| } | |
| Err(e) => { | |
| tracing::warn!("profile document sync failed: {e}"); | |
| } | |
| if path == paths::PROFILE && !append { | |
| // Only sync derived identity docs when we have a non-append write | |
| // and the new profile content parses as valid JSON. This helps | |
| // prevent partial/poisoned writes from generating identity docs. | |
| if let Err(e) = serde_json::from_str::<serde_json::Value>(&content) { | |
| tracing::warn!( | |
| "skipping profile document sync: profile content is not valid JSON: {e}" | |
| ); | |
| } else { | |
| match self.workspace.sync_profile_documents().await { | |
| Ok(true) => { | |
| tracing::info!( | |
| "profile write: synced USER.md + assistant-directives.md" | |
| ); | |
| synced_docs | |
| .extend_from_slice(&[paths::USER, paths::ASSISTANT_DIRECTIVES]); | |
| // Persist the onboarding-completed flag. | |
| let toml_path = crate::settings::Settings::default_toml_path(); | |
| if let Ok(Some(mut settings)) = | |
| crate::settings::Settings::load_toml(&toml_path) | |
| && !settings.profile_onboarding_completed | |
| { | |
| settings.profile_onboarding_completed = true; | |
| if let Err(e) = settings.save_toml(&toml_path) { | |
| tracing::warn!( | |
| "failed to persist profile_onboarding_completed: {e}" | |
| ); | |
| } | |
| } | |
| } | |
| Ok(false) => { | |
| tracing::debug!("profile not populated, skipping document sync"); | |
| } | |
| Err(e) => { | |
| tracing::warn!("profile document sync failed: {e}"); | |
| } | |
| } |
| /// Reserved flag for whether the conversational profile onboarding has | ||
| /// been completed. | ||
| /// | ||
| /// Intended semantics (not yet wired into the agent loop): this will be | ||
| /// set during the user's first interaction with the running assistant | ||
| /// (not during the setup wizard), after the agent builds a psychographic | ||
| /// profile via `memory_write`. | ||
| /// | ||
| // TODO: Wire into agent loop — currently the system uses `is_populated()` | ||
| // (content check) to gate First Contact. This flag will eventually | ||
| // distinguish "never onboarded" from "onboarded but profile was reset". |
There was a problem hiding this comment.
The doc comment says profile_onboarding_completed is "not yet wired into the agent loop", but this PR now sets it on profile writes in MemoryWriteTool. Update the comment to reflect current behavior (set when context/profile.json is written successfully / profile is populated), and clarify which storage layer (TOML vs DB settings) is considered the source of truth for this flag.
| /// Reserved flag for whether the conversational profile onboarding has | |
| /// been completed. | |
| /// | |
| /// Intended semantics (not yet wired into the agent loop): this will be | |
| /// set during the user's first interaction with the running assistant | |
| /// (not during the setup wizard), after the agent builds a psychographic | |
| /// profile via `memory_write`. | |
| /// | |
| // TODO: Wire into agent loop — currently the system uses `is_populated()` | |
| // (content check) to gate First Contact. This flag will eventually | |
| // distinguish "never onboarded" from "onboarded but profile was reset". | |
| /// Flag for whether the conversational profile onboarding has been | |
| /// completed. | |
| /// | |
| /// Current semantics: this is set once the assistant has successfully | |
| /// created/populated the user's conversational profile (e.g., after | |
| /// `MemoryWriteTool` writes a non-empty `context/profile.json` during | |
| /// the user's first real interaction with the running assistant, not | |
| /// during the setup wizard). | |
| /// | |
| /// Source of truth: this flag is owned by the runtime settings layer | |
| /// represented by this struct (persisted via `~/.ironclaw/settings.json` | |
| /// and any backing DB-based settings), not by static TOML configuration. | |
| /// TOML may provide a default/seed value, but the persisted settings | |
| /// storage is authoritative for this mutable flag. |
| // persisting. First Contact will still fire on next turn | ||
| // because has_rich_profile checks profile content, not existence. |
There was a problem hiding this comment.
This comment is now outdated: First Contact injection in workspace/mod.rs gates on has_profile_doc (any valid profile JSON exists), not on has_rich_profile/is_populated(). Update the note so future readers don’t assume onboarding will continue after a minimal-but-valid profile write.
| // persisting. First Contact will still fire on next turn | |
| // because has_rich_profile checks profile content, not existence. | |
| // persisting it if they want it saved to disk. First Contact | |
| // injection in `workspace/mod.rs` currently gates on `has_profile_doc` | |
| // (i.e., the existence of any valid profile JSON), not on a "rich" profile. |
| schema below. Also use `memory_write` to update `USER.md` with a human-readable markdown \ | ||
| summary of what you've learned. |
There was a problem hiding this comment.
FIRST_CONTACT_INTRO instructs the agent to update USER.md via memory_write, but memory_write explicitly blocks writes to USER.md (protected identity file). This will cause onboarding tool calls to fail. Since USER.md is now derived from context/profile.json via sync_profile_documents, update the prompt to stop instructing memory_write to write USER.md (or introduce a dedicated safe tool for syncing).
| schema below. Also use `memory_write` to update `USER.md` with a human-readable markdown \ | |
| summary of what you've learned. | |
| schema below. `USER.md` will be generated from this JSON (e.g., via `sync_profile_documents`); \ | |
| do not attempt to write `USER.md` directly with `memory_write`. |
| When you learn something notable, silently update `context/profile.json` and \ | ||
| `USER.md` using `memory_write`. Merge new data — don't replace the whole file.\n\ |
There was a problem hiding this comment.
The agent instructions say to "silently update context/profile.json and USER.md using memory_write" and "Merge new data — don't replace the whole file." As written, this conflicts with memory_write protections (cannot write USER.md) and can easily lead to invalid JSON if the model tries to “merge” by appending. Adjust this guidance to: update only context/profile.json (read-modify-write with append=false), and rely on the post-write sync to regenerate derived docs.
| When you learn something notable, silently update `context/profile.json` and \ | |
| `USER.md` using `memory_write`. Merge new data — don't replace the whole file.\n\ | |
| When you learn something notable, use `memory_read` + `memory_write` to update \ | |
| `context/profile.json` only (read-modify-write with `append=false`). Rely on the \ | |
| post-write sync to regenerate any derived docs.\n\ |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let profile: crate::profile::PsychographicProfile = match serde_json::from_str(&doc.content) | ||
| { | ||
| Ok(p) => p, | ||
| Err(_) => return Ok(false), |
There was a problem hiding this comment.
sync_profile_documents treats any JSON parse failure as Ok(false) and drops the error detail. This makes it hard to debug malformed context/profile.json (it will silently skip syncing USER.md/directives). Consider logging the parse error (or returning a distinct error) so operators can see why syncing was skipped.
| Err(_) => return Ok(false), | |
| Err(e) => { | |
| eprintln!( | |
| "sync_profile_documents: failed to parse {} as JSON: {e}", | |
| paths::PROFILE | |
| ); | |
| return Ok(false); | |
| } |
| 5. If updates are needed, write the updated profile to `context/profile.json` using `memory_write`. | ||
| 6. Also update `USER.md` with a refreshed markdown summary if the profile changed. |
There was a problem hiding this comment.
The weekly evolution routine prompt tells the agent to memory_write context/profile.json (and USER.md) but doesn’t mention append=false. Since memory_write defaults to appending, this can corrupt the JSON profile on subsequent updates; and USER.md writes are blocked by the tool. Update the routine instructions to overwrite context/profile.json with append=false and rely on the profile-write sync to update USER.md/assistant-directives.
| 5. If updates are needed, write the updated profile to `context/profile.json` using `memory_write`. | |
| 6. Also update `USER.md` with a refreshed markdown summary if the profile changed. | |
| 5. If updates are needed, write the updated profile to `context/profile.json` using the `memory_write` tool with `append=false` so the file is fully replaced with the new JSON profile. | |
| 6. Do NOT write directly to `USER.md`. The system will automatically sync `USER.md` and assistant directives from `context/profile.json` after a successful profile write. |
| if self.read(paths::HEARTBEAT).await.is_err() { | ||
| self.write(paths::HEARTBEAT, &profile.to_heartbeat_md()) | ||
| .await?; |
There was a problem hiding this comment.
This seeds HEARTBEAT.md when read(paths::HEARTBEAT) returns any error. That can mask real DB/storage errors by treating them as “file missing” and attempting a write. Consider only seeding when the error is specifically WorkspaceError::DocumentNotFound (and propagate other errors).
| if self.read(paths::HEARTBEAT).await.is_err() { | |
| self.write(paths::HEARTBEAT, &profile.to_heartbeat_md()) | |
| .await?; | |
| match self.read(paths::HEARTBEAT).await { | |
| Ok(_) => { | |
| // HEARTBEAT.md already exists; don't overwrite. | |
| } | |
| Err(WorkspaceError::DocumentNotFound) => { | |
| self.write(paths::HEARTBEAT, &profile.to_heartbeat_md()) | |
| .await?; | |
| } | |
| Err(e) => { | |
| // Propagate unexpected storage/DB errors. | |
| return Err(e); | |
| } |
| if std::env::var("SKIP_WIZARD") | ||
| .map(|v| !v.is_empty() && v != "0" && v != "false") | ||
| .unwrap_or(false) | ||
| { | ||
| return None; | ||
| } |
There was a problem hiding this comment.
SKIP_WIZARD parsing is case-sensitive and treats any non-empty value other than "0"/"false" as true, so values like "False" or "FALSE" will unexpectedly skip onboarding. The repo already has a shared parse_bool_env helper that normalizes case and validates values. Consider reusing that helper here or at least lowercasing the env var before comparison for consistent behavior.
| - Weekly reflection prompt (what went well, what to improve) | ||
| - Periodic task/reminder check-in | ||
| - Regular cleanup of stale tasks or notes | ||
| - Weekly profile evolution (if the user has a profile in `context/profile.json`, suggest a Monday routine that reads the profile via `memory_read`, searches recent conversations for new patterns with `memory_search`, and updates the profile via `memory_write` if any fields should change with confidence > 0.6 — be conservative, only update with clear evidence) |
There was a problem hiding this comment.
This skill suggests updating context/profile.json via memory_write, but doesn’t specify using append=false. Since the tool defaults to appending, following this guidance can corrupt the JSON profile after the initial write. Consider explicitly noting that JSON files like context/profile.json should be written with append=false (overwrite) after reading/merging.
| - Weekly profile evolution (if the user has a profile in `context/profile.json`, suggest a Monday routine that reads the profile via `memory_read`, searches recent conversations for new patterns with `memory_search`, and updates the profile via `memory_write` if any fields should change with confidence > 0.6 — be conservative, only update with clear evidence) | |
| - Weekly profile evolution (if the user has a profile in `context/profile.json`, suggest a Monday routine that reads the profile via `memory_read`, searches recent conversations for new patterns with `memory_search`, merges any changes into an in-memory profile object, and then updates the profile via `memory_write` with `append=false` (overwrite) if any fields should change with confidence > 0.6 — be conservative, only update with clear evidence) |
|
Closing in favor of #927 which builds on this work and takes it to completion. The NPA psychographic profiling system has been ported and refined — interactive onboarding replaced with a simpler bootstrap greeting flow, workspace seed files added, and routine advisor integrated. Thank you @jayzalowitz for the original contribution and design work on the profiling schema, analysis framework, and onboarding approach! 🙏 |
Summary
Ports the NPA psychographic profiling system into IronClaw, enabling personalized assistant interactions based on user communication style, personality traits, and behavioral patterns.
What changed
Profile schema (
src/profile.rs): 9-dimension psychographic profile withPsychographicProfile,CommunicationStyle,PersonalityTraits,BehaviorPatterns,InteractionPreferences,FriendshipProfile,ContextProfile,AssistancePreferences,CohortInfo, andAnalysisMetadata. SharedPROFILE_JSON_SCHEMAconstant prevents schema drift between onboarding and workspace. Custom deserializers handle backward compatibility (v1 bare cohort strings, Vec qualities) and LLM resilience (trait scores accept floats/negatives/out-of-range via f64 deserialization with clamping to 0-100, defaulting to 50 on non-numeric input).First-interaction onboarding (
src/workspace/mod.rs): Instead of a wizard step, onboarding happens conversationally during the user's first interaction with the running assistant. TheFirst Contactsystem prompt block injects when no profile document exists (has_profile_docis false) and instructs the LLM to naturally learn about the user using a one-step-removed questioning technique, then write the profile viamemory_write. The block self-disables once any valid profile JSON is written, regardless of which fields are populated.Three-tier prompt augmentation (
src/workspace/mod.rs):is_populated()(preferred_name, profession, or goals present)Onboarding engine (
src/setup/onboarding_chat.rs): Conversational engine with 6 intent tracking, kept for potential futureironclaw onboard --personalCLI command. Generates confidence-scored profiles from conversation transcripts.Profile evolution (
src/setup/profile_evolution.rs): Weekly re-analysis prompt with confidence gating (only update fields when confidence > 0.6), gradual personality trait shifts (max ±10 per update), and schema-version-aware updates. Version field is schema version (1=original, 2=enriched), not a revision counter.Skills:
delegation/SKILL.mdandroutine-advisor/SKILL.mdfor delegated task management and routine setup guidance.Wizard: Unchanged at 8 steps. Personal onboarding moved to first assistant interaction per maintainer feedback.
Design decisions
Stringwith validation at the display layer is more resilient than strict enum deserialization.confidencefield: Quick-access for Tier 2 gating without traversing intoanalysis_metadata. Intentional duplication.has_profile_docvsis_populated(): First Contact gates on document existence (any valid profile written → stop onboarding), while Tier 1/2 augmentation gates onis_populated()(meaningful content present). This prevents onboarding from looping if a profile was written with minimal fields.profile_onboarding_completedkept in Settings: Durable flag distinguishing "never onboarded" from "onboarded but profile was reset", separate from the content-basedis_populated()check. TODO to wire into agent loop.max(message_count, 1)prevents division by zero in all three confidence calculation sites.Test plan
cargo fmt— no formatting issuescargo clippy --all --benches --tests --examples --all-features— zero warningscargo test --lib profile— 22 profile-specific tests passis_populated()tests: default profile is false, profile with name is true🤖 Generated with Claude Code