@@ -129,14 +129,12 @@ struct WrittenOpenclawSkill {
129129 manifest_entry : onboard:: ManagedSkillManifestEntry ,
130130}
131131
132- fn write_onboard_openclaw_skill (
133- ob_config : & crate :: onboard:: OnboardConfig ,
132+ /// Write a single ClawShell-managed skill bundle into
133+ /// `<openclaw_root>/skills/<skill_name>/` and return its manifest entry.
134+ fn write_openclaw_skill_bundle (
135+ skill : onboard:: OnboardSkillBundle ,
134136 openclaw_config_path : & Path ,
135- ) -> Result < Option < WrittenOpenclawSkill > , Box < dyn Error > > {
136- let Some ( skill) = onboard:: render_email_messages_skill ( ob_config) else {
137- return Ok ( None ) ;
138- } ;
139-
137+ ) -> Result < WrittenOpenclawSkill , Box < dyn Error > > {
140138 let openclaw_root = onboard:: openclaw_config_root ( openclaw_config_path) ;
141139 let skill_dir = openclaw_root. join ( "skills" ) . join ( skill. name ) ;
142140 std:: fs:: create_dir_all ( & skill_dir) ?;
@@ -163,10 +161,37 @@ fn write_onboard_openclaw_skill(
163161 ) ;
164162 }
165163
166- Ok ( Some ( WrittenOpenclawSkill {
164+ Ok ( WrittenOpenclawSkill {
167165 path : skill_dir,
168166 manifest_entry,
169- } ) )
167+ } )
168+ }
169+
170+ /// Write every ClawShell-managed skill that applies to this onboarding
171+ /// run into OpenClaw's skills directory. The stats skill is always
172+ /// written; the email skill is written only when the onboarding config
173+ /// has email integration enabled. Returns the written skills in
174+ /// installation order.
175+ fn write_onboard_openclaw_skill (
176+ ob_config : & crate :: onboard:: OnboardConfig ,
177+ openclaw_config_path : & Path ,
178+ ) -> Result < Vec < WrittenOpenclawSkill > , Box < dyn Error > > {
179+ let mut written = Vec :: new ( ) ;
180+
181+ let stats_skill = onboard:: render_admin_stats_skill ( ob_config) ;
182+ written. push ( write_openclaw_skill_bundle (
183+ stats_skill,
184+ openclaw_config_path,
185+ ) ?) ;
186+
187+ if let Some ( email_skill) = onboard:: render_email_messages_skill ( ob_config) {
188+ written. push ( write_openclaw_skill_bundle (
189+ email_skill,
190+ openclaw_config_path,
191+ ) ?) ;
192+ }
193+
194+ Ok ( written)
170195}
171196
172197/// Resolve the home directory and uid:gid of the user running onboarding.
@@ -201,23 +226,15 @@ fn resolve_hermes_target_user() -> Result<(PathBuf, u32, u32), Box<dyn Error>> {
201226 Ok ( ( home, 0 , 0 ) )
202227}
203228
204- /// Write the ClawShell-managed email skill into the invoking user's
205- /// `~/.hermes/skills/<skill_name>/` directory. Returns the install path
206- /// when the skill was written, or `None` when the skill render function
207- /// declined (e.g. email integration not configured).
208- ///
209- /// Unlike `write_onboard_openclaw_skill`, this doesn't upsert a
210- /// `managed_skills` manifest entry — Hermes discovers skills from its own
211- /// `~/.hermes/skills/` tree directly and doesn't share OpenClaw's
212- /// manifest bookkeeping.
213- fn write_onboard_hermes_skill (
214- ob_config : & crate :: onboard:: OnboardConfig ,
215- ) -> Result < Option < PathBuf > , Box < dyn Error > > {
216- let Some ( skill) = onboard:: render_email_messages_skill ( ob_config) else {
217- return Ok ( None ) ;
218- } ;
219-
220- let ( home_dir, uid, gid) = resolve_hermes_target_user ( ) ?;
229+ /// Write a single ClawShell-managed skill bundle into
230+ /// `<home>/.hermes/skills/<skill_name>/` and chown the result to the
231+ /// invoking user.
232+ fn write_hermes_skill_bundle (
233+ skill : onboard:: OnboardSkillBundle ,
234+ home_dir : & Path ,
235+ uid : u32 ,
236+ gid : u32 ,
237+ ) -> Result < PathBuf , Box < dyn Error > > {
221238 let skill_dir = home_dir. join ( ".hermes" ) . join ( "skills" ) . join ( skill. name ) ;
222239 std:: fs:: create_dir_all ( & skill_dir) ?;
223240
@@ -240,7 +257,32 @@ fn write_onboard_hermes_skill(
240257 ) ;
241258 }
242259
243- Ok ( Some ( skill_dir) )
260+ Ok ( skill_dir)
261+ }
262+
263+ /// Write every ClawShell-managed skill that applies to this onboarding
264+ /// run into `~/.hermes/skills/`. The stats skill is always written; the
265+ /// email skill is written only when email integration is enabled.
266+ ///
267+ /// Unlike `write_onboard_openclaw_skill`, this doesn't upsert a
268+ /// `managed_skills` manifest entry — Hermes discovers skills from its own
269+ /// `~/.hermes/skills/` tree directly and doesn't share OpenClaw's
270+ /// manifest bookkeeping.
271+ fn write_onboard_hermes_skill (
272+ ob_config : & crate :: onboard:: OnboardConfig ,
273+ ) -> Result < Vec < PathBuf > , Box < dyn Error > > {
274+ let ( home_dir, uid, gid) = resolve_hermes_target_user ( ) ?;
275+
276+ let mut written = Vec :: new ( ) ;
277+
278+ let stats_skill = onboard:: render_admin_stats_skill ( ob_config) ;
279+ written. push ( write_hermes_skill_bundle ( stats_skill, & home_dir, uid, gid) ?) ;
280+
281+ if let Some ( email_skill) = onboard:: render_email_messages_skill ( ob_config) {
282+ written. push ( write_hermes_skill_bundle ( email_skill, & home_dir, uid, gid) ?) ;
283+ }
284+
285+ Ok ( written)
244286}
245287
246288fn resolve_openclaw_owner_spec ( openclaw_path : & Path ) -> Result < Option < String > , Box < dyn Error > > {
@@ -1114,40 +1156,27 @@ fn apply_openclaw_onboarding_steps(
11141156) -> Result < ( ) , Box < dyn Error > > {
11151157 const TOTAL_STEPS : usize = ONBOARD_TOTAL_STEPS ;
11161158
1117- // Step 6: Write OpenClaw skill files
1159+ // Step 6: Write OpenClaw skill files. The stats skill is always
1160+ // installed; the email skill is added on top when email integration
1161+ // is configured for this onboarding.
11181162 tui:: print_step ( 6 , TOTAL_STEPS , "OpenClaw skill setup..." ) ;
11191163 println ! ( ) ;
1120- let openclaw_skill = if onboard:: should_setup_email_skill ( ob_config) {
1121- let openclaw_skill_edit_approved =
1122- tui:: prompt_confirm ( "Write OpenClaw skill files for email integration" , true ) ?;
1123- let openclaw_skill = if openclaw_skill_edit_approved {
1124- write_onboard_openclaw_skill ( ob_config, openclaw_path) ?
1125- } else {
1126- None
1127- } ;
1128- if openclaw_skill_edit_approved {
1129- if let Some ( skill) = openclaw_skill. as_ref ( ) {
1130- onboard:: upsert_managed_skill_manifest_entry ( config_file, & skill. manifest_entry ) ?;
1131- tui:: print_step_done ( 6 , TOTAL_STEPS , "OpenClaw skills written" ) ;
1132- tui:: print_info ( "OpenClaw skill" , & skill. path . display ( ) . to_string ( ) ) ;
1133- } else {
1134- tui:: print_step_done ( 6 , TOTAL_STEPS , "OpenClaw skills skipped" ) ;
1135- }
1136- } else {
1137- tui:: print_step_done (
1138- 6 ,
1139- TOTAL_STEPS ,
1140- "OpenClaw skills skipped (approval not granted)" ,
1141- ) ;
1164+ let openclaw_skill_edit_approved = tui:: prompt_confirm ( "Write OpenClaw skill files" , true ) ?;
1165+ let openclaw_skills: Vec < WrittenOpenclawSkill > = if openclaw_skill_edit_approved {
1166+ let written = write_onboard_openclaw_skill ( ob_config, openclaw_path) ?;
1167+ for skill in & written {
1168+ onboard:: upsert_managed_skill_manifest_entry ( config_file, & skill. manifest_entry ) ?;
1169+ tui:: print_info ( "OpenClaw skill" , & skill. path . display ( ) . to_string ( ) ) ;
11421170 }
1143- openclaw_skill
1171+ tui:: print_step_done ( 6 , TOTAL_STEPS , "OpenClaw skills written" ) ;
1172+ written
11441173 } else {
11451174 tui:: print_step_done (
11461175 6 ,
11471176 TOTAL_STEPS ,
1148- "OpenClaw skills skipped (email integration not configured )" ,
1177+ "OpenClaw skills skipped (approval not granted )" ,
11491178 ) ;
1150- None
1179+ Vec :: new ( )
11511180 } ;
11521181
11531182 // Step 7: Backup OpenClaw configuration file if present.
@@ -1264,17 +1293,18 @@ fn apply_openclaw_onboarding_steps(
12641293 "Temporarily setting `gateway.reload.mode` to `off` during config updates, then restoring `hybrid`." ,
12651294 ) ;
12661295 openclaw_cli:: apply_onboard_openclaw_config ( & mut openclaw_runner, ob_config) ?;
1267- if let Some ( skill) = openclaw_skill . as_ref ( ) {
1296+ for skill in & openclaw_skills {
12681297 align_owner_with_openclaw_path ( & skill. path , openclaw_path) ?;
12691298 }
12701299 tui:: print_step_done ( 8 , TOTAL_STEPS , "OpenClaw config updated" ) ;
12711300 Ok ( ( ) )
12721301}
12731302
12741303/// Steps 6, 7, 8 of the onboarding wizard when the user picked Hermes as
1275- /// the downstream agent target. Installs the email skill (if email is
1276- /// enabled) into the user's `~/.hermes/skills/` tree, then runs
1277- /// `hermes config set` to point Hermes at ClawShell.
1304+ /// the downstream agent target. Installs the ClawShell-managed skill
1305+ /// bundles (stats always, email when configured) into the user's
1306+ /// `~/.hermes/skills/` tree, then runs `hermes config set` to point
1307+ /// Hermes at ClawShell.
12781308///
12791309/// Steps 7 and 8a ("backup OpenClaw" / "preview OpenClaw edits") are
12801310/// rendered as neutral info lines so the overall 9-step numbering stays
@@ -1284,32 +1314,28 @@ fn apply_hermes_onboarding_steps(
12841314) -> Result < ( ) , Box < dyn Error > > {
12851315 const TOTAL_STEPS : usize = ONBOARD_TOTAL_STEPS ;
12861316
1287- // Step 6: Write Hermes skill files into ~/.hermes/skills/ if email enabled.
1317+ // Step 6: Write Hermes skill files into ~/.hermes/skills/. The stats
1318+ // skill is always written; the email skill is written on top when
1319+ // email integration is configured.
12881320 tui:: print_step ( 6 , TOTAL_STEPS , "Hermes skill setup..." ) ;
1289- if onboard:: should_setup_email_skill ( ob_config) {
1290- match write_onboard_hermes_skill ( ob_config) {
1291- Ok ( Some ( path) ) => {
1292- tui:: print_step_done ( 6 , TOTAL_STEPS , "Hermes skill written" ) ;
1321+ match write_onboard_hermes_skill ( ob_config) {
1322+ Ok ( paths) if paths. is_empty ( ) => {
1323+ tui:: print_step_done ( 6 , TOTAL_STEPS , "Hermes skills skipped" ) ;
1324+ }
1325+ Ok ( paths) => {
1326+ for path in & paths {
12931327 tui:: print_info ( "Hermes skill" , & path. display ( ) . to_string ( ) ) ;
12941328 }
1295- Ok ( None ) => {
1296- tui:: print_step_done ( 6 , TOTAL_STEPS , "Hermes skill skipped" ) ;
1297- }
1298- Err ( error) => {
1299- tui:: print_error ( & format ! ( "Failed to write Hermes skill: {error}" ) ) ;
1300- tui:: print_step_done (
1301- 6 ,
1302- TOTAL_STEPS ,
1303- "Hermes skill skipped (write failed — see error above)" ,
1304- ) ;
1305- }
1329+ tui:: print_step_done ( 6 , TOTAL_STEPS , "Hermes skills written" ) ;
1330+ }
1331+ Err ( error) => {
1332+ tui:: print_error ( & format ! ( "Failed to write Hermes skills: {error}" ) ) ;
1333+ tui:: print_step_done (
1334+ 6 ,
1335+ TOTAL_STEPS ,
1336+ "Hermes skills skipped (write failed — see error above)" ,
1337+ ) ;
13061338 }
1307- } else {
1308- tui:: print_step_done (
1309- 6 ,
1310- TOTAL_STEPS ,
1311- "Hermes skills skipped (email integration not configured)" ,
1312- ) ;
13131339 }
13141340
13151341 // Step 7: Not applicable for Hermes (no backup needed — hermes config set
@@ -1740,28 +1766,36 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
17401766 } else {
17411767 None
17421768 } ;
1743- let openclaw_skill_dir = openclaw_path. as_ref ( ) . map ( |path| {
1744- onboard:: openclaw_config_root ( path)
1745- . join ( "skills" )
1746- . join ( onboard:: EMAIL_MESSAGES_SKILL_NAME )
1747- } ) ;
1748- let openclaw_skill_manifest = if clawshell_config_file. exists ( ) {
1749- onboard:: read_managed_skill_manifest_entry (
1750- & clawshell_config_file,
1751- onboard:: EMAIL_MESSAGES_SKILL_NAME ,
1752- )
1753- } else {
1754- None
1755- } ;
1756- let openclaw_skill_inspection = if let Some ( skill_dir) = openclaw_skill_dir. as_ref ( ) {
1757- onboard:: inspect_managed_skill_for_uninstall (
1758- skill_dir,
1759- onboard:: EMAIL_MESSAGES_SKILL_NAME ,
1760- openclaw_skill_manifest. as_ref ( ) ,
1761- )
1762- } else {
1763- onboard:: ManagedSkillInspection :: missing ( )
1764- } ;
1769+ // Build (skill_name, skill_dir, inspection) triples for every managed
1770+ // OpenClaw skill, so the preview and removal blocks can iterate. Stats
1771+ // is listed first so it's still cleaned up even if the email skill is
1772+ // absent (or vice-versa).
1773+ let openclaw_skill_entries: Vec < ( & ' static str , PathBuf , onboard:: ManagedSkillInspection ) > =
1774+ if let Some ( openclaw_path) = openclaw_path. as_ref ( ) {
1775+ let skills_root = onboard:: openclaw_config_root ( openclaw_path) . join ( "skills" ) ;
1776+ [
1777+ onboard:: ADMIN_STATS_SKILL_NAME ,
1778+ onboard:: EMAIL_MESSAGES_SKILL_NAME ,
1779+ ]
1780+ . into_iter ( )
1781+ . map ( |name| {
1782+ let skill_dir = skills_root. join ( name) ;
1783+ let manifest = if clawshell_config_file. exists ( ) {
1784+ onboard:: read_managed_skill_manifest_entry ( & clawshell_config_file, name)
1785+ } else {
1786+ None
1787+ } ;
1788+ let inspection = onboard:: inspect_managed_skill_for_uninstall (
1789+ & skill_dir,
1790+ name,
1791+ manifest. as_ref ( ) ,
1792+ ) ;
1793+ ( name, skill_dir, inspection)
1794+ } )
1795+ . collect ( )
1796+ } else {
1797+ Vec :: new ( )
1798+ } ;
17651799
17661800 tui:: print_warning ( "This will remove the following:" ) ;
17671801 tui:: print_info ( "ClawShell" , "Stop if running" ) ;
@@ -1770,10 +1804,11 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
17701804 if service_exists {
17711805 tui:: print_info ( "Service" , & service_path. display ( ) . to_string ( ) ) ;
17721806 }
1773- if let Some ( skill_dir) = openclaw_skill_dir. as_ref ( )
1774- && skill_dir. exists ( )
1775- {
1776- match openclaw_skill_inspection. state {
1807+ for ( _skill_name, skill_dir, inspection) in & openclaw_skill_entries {
1808+ if !skill_dir. exists ( ) {
1809+ continue ;
1810+ }
1811+ match inspection. state {
17771812 onboard:: ManagedSkillUninstallState :: ManagedUnchanged => {
17781813 tui:: print_info (
17791814 "OpenClaw skill" ,
@@ -1787,7 +1822,7 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
17871822 ) ;
17881823 tui:: print_warning ( & format ! (
17891824 "Managed OpenClaw skill has local modifications: {}" ,
1790- openclaw_skill_inspection . detail
1825+ inspection . detail
17911826 ) ) ;
17921827 }
17931828 onboard:: ManagedSkillUninstallState :: Unmanaged => {
@@ -1797,7 +1832,7 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
17971832 ) ;
17981833 tui:: print_warning ( & format ! (
17991834 "Skill ownership is not verified: {}" ,
1800- openclaw_skill_inspection . detail
1835+ inspection . detail
18011836 ) ) ;
18021837 }
18031838 onboard:: ManagedSkillUninstallState :: Missing => { }
@@ -1848,19 +1883,19 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
18481883 }
18491884 }
18501885
1851- // 0b. Remove ClawShell-managed OpenClaw skill if present.
1852- if let Some ( skill_dir ) = openclaw_skill_dir . as_ref ( )
1853- && skill_dir . exists ( )
1854- {
1855- let remove_skill_dir = | path : & Path | match std :: fs :: remove_dir_all ( path ) {
1856- Ok ( ( ) ) => tui :: print_success ( & format ! ( "OpenClaw skill removed: {}" , path. display( ) ) ) ,
1857- Err ( error ) => tui :: print_warning ( & format ! (
1858- "Failed to remove OpenClaw skill at {}: {error}" ,
1859- path . display ( )
1860- ) ) ,
1861- } ;
1862-
1863- match openclaw_skill_inspection . state {
1886+ // 0b. Remove ClawShell-managed OpenClaw skills if present.
1887+ let remove_skill_dir = | path : & Path | match std :: fs :: remove_dir_all ( path ) {
1888+ Ok ( ( ) ) => tui :: print_success ( & format ! ( "OpenClaw skill removed: {}" , path . display ( ) ) ) ,
1889+ Err ( error ) => tui :: print_warning ( & format ! (
1890+ "Failed to remove OpenClaw skill at {}: {error}" ,
1891+ path. display( )
1892+ ) ) ,
1893+ } ;
1894+ for ( _skill_name , skill_dir , inspection ) in & openclaw_skill_entries {
1895+ if !skill_dir . exists ( ) {
1896+ continue ;
1897+ }
1898+ match inspection . state {
18641899 onboard:: ManagedSkillUninstallState :: ManagedUnchanged => {
18651900 let remove_skill = if skip_confirm {
18661901 true
0 commit comments