feat(cli): add nemoclaw <sandbox> skill remove command#1962
feat(cli): add nemoclaw <sandbox> skill remove command#1962sinha-abhash wants to merge 13 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds CLI documentation and public-display metadata for ChangesSkill Removal Feature
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/nemoclaw.ts (1)
1340-1568: Splitskill installandskill removeinto dedicated handlers.
sandboxSkillInstall()now owns subcommand help, argument validation, two SSH-config lifecycles, and both install/remove workflows. That pushes this dispatcher well past the repo's complexity target and makes the next skill subcommand harder to change safely. Extract the remove flow and install flow into separate helpers, and leave this function to parse the subcommand and delegate.As per coding guidelines,
Limit cyclomatic complexity to 20 in JavaScript/TypeScript files, with target of 15.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/nemoclaw.ts` around lines 1340 - 1568, Current sandboxSkillInstall is doing parsing plus both install and remove workflows; extract the remove and install workflows into two small helpers (e.g. handleSkillRemove(ctxNameOrSandbox, args) and handleSkillInstall(ctxNameOrSandbox, args) or similar) and leave sandboxSkillInstall to only parse subcommand, validate arguments, and delegate. Move the SSH-config lifecycle (captureOpenshell(...), tmpSshConfig creation, fs.writeFileSync, and fs.unlinkSync cleanup) into the helpers so each has its own try/finally and uses the same ctx shape ({ configFile, sandboxName }); move code that validates skillName (skillInstall.validateSkillName), checkExisting/remove/verifyRemove into the remove helper (use agentRuntime.getSessionAgent and skillInstall.resolveSkillPaths there) and move frontmatter parsing, collectFiles, uploadDirectory, postInstall, and verifyInstall into the install helper (again resolving agent and paths inside it). Ensure sandboxSkillInstall still performs top-level argument/usage printing and only calls the appropriate helper with sandboxName and the remaining args.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/skill-install.ts`:
- Around line 423-428: validateSkillName currently allows "." and ".." because
the regex matches them; update validateSkillName(name: string) to explicitly
reject a single "." and a double ".." (e.g., check name !== "." && name !==
"..") while keeping the existing /^[A-Za-z0-9._-]+$/ and length > 0 checks so
behavior remains the same for all other names; also ensure this change is
mirrored where the earlier frontmatter/skill-name validation is implemented so
both checks stay aligned.
- Around line 379-420: The removal currently reports success and verifyRemove()
checks only paths.uploadDir so if paths.mirrorDir (OpenClaw mirror) wasn't
removed the CLI can lie; update the removal return value and verification to
require the mirror deletion when paths.isOpenClaw is true: in the remover
function make success combine removedUploadDir && (if paths.isOpenClaw ?
removedMirrorDir : true) and in verifyRemove(ctx, paths) have sshExec test both
shellQuote(paths.uploadDir) and, when paths.isOpenClaw && paths.mirrorDir,
shellQuote(paths.mirrorDir) (both must be GONE); also update the existence check
logic used by checkExisting() to probe the mirror SKILL.md
(paths.mirrorDir/SKILL.md) in addition to uploadDir/SKILL.md so retries won't be
blocked.
---
Nitpick comments:
In `@src/nemoclaw.ts`:
- Around line 1340-1568: Current sandboxSkillInstall is doing parsing plus both
install and remove workflows; extract the remove and install workflows into two
small helpers (e.g. handleSkillRemove(ctxNameOrSandbox, args) and
handleSkillInstall(ctxNameOrSandbox, args) or similar) and leave
sandboxSkillInstall to only parse subcommand, validate arguments, and delegate.
Move the SSH-config lifecycle (captureOpenshell(...), tmpSshConfig creation,
fs.writeFileSync, and fs.unlinkSync cleanup) into the helpers so each has its
own try/finally and uses the same ctx shape ({ configFile, sandboxName }); move
code that validates skillName (skillInstall.validateSkillName),
checkExisting/remove/verifyRemove into the remove helper (use
agentRuntime.getSessionAgent and skillInstall.resolveSkillPaths there) and move
frontmatter parsing, collectFiles, uploadDirectory, postInstall, and
verifyInstall into the install helper (again resolving agent and paths inside
it). Ensure sandboxSkillInstall still performs top-level argument/usage printing
and only calls the appropriate helper with sandboxName and the remaining args.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3f679918-19f5-4eff-b916-7665b1b7d101
📒 Files selected for processing (3)
src/lib/skill-install.test.tssrc/lib/skill-install.tssrc/nemoclaw.ts
|
✨ Thanks for submitting this PR that proposes an enhancement to add a skill remove command, which could help improve the user experience and provide a complete skill lifecycle from the CLI. Possibly related open PRs: Possibly related open issues: |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/lib/skill-install.ts (1)
405-420:⚠️ Potential issue | 🟠 MajorRequire mirror deletion for OpenClaw before reporting removal success.
On Line [406],
successignores mirror deletion; and Lines [417-420] verify onlyuploadDir. In OpenClaw mode, this can report “removed” while$HOME/.openclaw/skills/<name>still exists.Proposed fix
export function removeSkill( ctx: SshContext, paths: SkillPaths, ): RemoveResult { @@ return { - success: removedUploadDir, + success: removedUploadDir && (!paths.isOpenClaw || removedMirrorDir), removedUploadDir, removedMirrorDir, clearedSessions, messages, }; } @@ export function verifyRemove(ctx: SshContext, paths: SkillPaths): boolean { - const target = shellQuote(paths.uploadDir); - const result = sshExec(ctx, `test -e ${target} && echo EXISTS || echo GONE`); + const checks = [`test ! -e ${shellQuote(paths.uploadDir)}`]; + if (paths.isOpenClaw && paths.mirrorDir) { + checks.push(`test ! -e "${paths.mirrorDir}"`); + } + const result = sshExec(ctx, `${checks.join(" && ")} && echo GONE || echo EXISTS`); return result !== null && result.stdout === "GONE"; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/skill-install.ts` around lines 405 - 420, The removal success currently ignores mirror deletion; update the code so success requires the mirror to be removed when applicable and make verifyRemove check both upload and mirror dirs: modify the caller that builds the result object to set success = removedUploadDir && (paths.mirrorDir ? removedMirrorDir : true) (use the existing removedUploadDir and removedMirrorDir flags), and change the verifyRemove(ctx: SshContext, paths: SkillPaths) function to shell-quote and test both paths.uploadDir and paths.mirrorDir (if paths.mirrorDir is provided) — return true only when both are gone (i.e., test each with sshExec and ensure both echo GONE).
🧹 Nitpick comments (1)
src/lib/skill-install.ts (1)
66-70: Consolidate skill-name validation into one shared helper.The same rule is duplicated in
parseFrontmatterandvalidateSkillName. Centralizing it reduces drift risk and keeps install/remove behavior consistent.Refactor sketch
+const SKILL_NAME_RE = /^[A-Za-z0-9._-]+$/; +function isValidSkillName(name: string): boolean { + return name.length > 0 && name !== "." && name !== ".." && SKILL_NAME_RE.test(name); +} @@ - if (nameValue === "." || nameValue === ".." || !/^[A-Za-z0-9._-]+$/.test(nameValue)) { + if (!isValidSkillName(nameValue)) { @@ export function validateSkillName(name: string): boolean { - return ( - name.length > 0 && - name !== "." && - name !== ".." && - /^[A-Za-z0-9._-]+$/.test(name) - ); + return isValidSkillName(name); }Also applies to: 427-433
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/skill-install.ts` around lines 66 - 70, Extract the skill-name validation logic into a single helper (e.g., isValidSkillName or validateSkillNameFormat) and use it from both parseFrontmatter and validateSkillName instead of duplicating the regex and "."/".." checks; implement the helper to return boolean or throw with the same error text (`SKILL.md name '${name}' contains invalid characters. Only [A-Za-z0-9._-] allowed.`) and replace the inline checks in parseFrontmatter and the existing validateSkillName to call the new helper so install/remove behavior is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/lib/skill-install.ts`:
- Around line 405-420: The removal success currently ignores mirror deletion;
update the code so success requires the mirror to be removed when applicable and
make verifyRemove check both upload and mirror dirs: modify the caller that
builds the result object to set success = removedUploadDir && (paths.mirrorDir ?
removedMirrorDir : true) (use the existing removedUploadDir and removedMirrorDir
flags), and change the verifyRemove(ctx: SshContext, paths: SkillPaths) function
to shell-quote and test both paths.uploadDir and paths.mirrorDir (if
paths.mirrorDir is provided) — return true only when both are gone (i.e., test
each with sshExec and ensure both echo GONE).
---
Nitpick comments:
In `@src/lib/skill-install.ts`:
- Around line 66-70: Extract the skill-name validation logic into a single
helper (e.g., isValidSkillName or validateSkillNameFormat) and use it from both
parseFrontmatter and validateSkillName instead of duplicating the regex and
"."/".." checks; implement the helper to return boolean or throw with the same
error text (`SKILL.md name '${name}' contains invalid characters. Only
[A-Za-z0-9._-] allowed.`) and replace the inline checks in parseFrontmatter and
the existing validateSkillName to call the new helper so install/remove behavior
is consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7a79c72d-a216-4456-a527-90d7ce0e709b
📒 Files selected for processing (2)
src/lib/skill-install.test.tssrc/lib/skill-install.ts
… OpenClaw mirror - removeSkill(): success now requires both uploadDir and mirrorDir to be removed for OpenClaw sandboxes (previously only checked uploadDir, so a surviving mirror would still show the skill as removed) - verifyRemove(): checks both uploadDir and mirrorDir via a compound SSH test before returning GONE (previously only checked uploadDir) - checkExisting(): probes both uploadDir/SKILL.md and mirrorDir/SKILL.md so a partial removal (uploadDir gone, mirrorDir surviving) no longer blocks a reinstall All 37 skill-install tests pass. Fixes address the Major issue flagged by CodeRabbit on PR NVIDIA#1962. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lib/skill-install.ts (1)
66-70: Consolidate skill-name validation into a single helper.Line [66] and Line [436] duplicate the same rules. Keeping one source of truth avoids future drift between install-time and CLI-time validation.
♻️ Proposed refactor
+function isValidSkillName(name: string): boolean { + return ( + name.length > 0 && + name !== "." && + name !== ".." && + /^[A-Za-z0-9._-]+$/.test(name) + ); +} + export function parseFrontmatter(content: string): SkillFrontmatter { ... - if (nameValue === "." || nameValue === ".." || !/^[A-Za-z0-9._-]+$/.test(nameValue)) { + if (!isValidSkillName(nameValue)) { throw new Error( `SKILL.md name '${nameValue}' contains invalid characters. Only [A-Za-z0-9._-] allowed.`, ); } ... } ... export function validateSkillName(name: string): boolean { - return ( - name.length > 0 && - name !== "." && - name !== ".." && - /^[A-Za-z0-9._-]+$/.test(name) - ); + return isValidSkillName(name); }Also applies to: 436-442
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/skill-install.ts` around lines 66 - 70, Extract the duplicated validation into a single helper called e.g. validateSkillName(name: string) (or isValidSkillName returning boolean) that encapsulates the checks against "." and ".." and the /^[A-Za-z0-9._-]+$/ regex and the error message (`SKILL.md name '${name}' contains invalid characters. Only [A-Za-z0-9._-] allowed.`); replace the inline check in the install flow that uses nameValue and the duplicate check at the CLI side (the block around the other duplicate) to call this helper instead (either throw from validateSkillName or check boolean and throw at the call site) so both install-time and CLI-time use the same validation logic and message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/lib/skill-install.ts`:
- Around line 66-70: Extract the duplicated validation into a single helper
called e.g. validateSkillName(name: string) (or isValidSkillName returning
boolean) that encapsulates the checks against "." and ".." and the
/^[A-Za-z0-9._-]+$/ regex and the error message (`SKILL.md name '${name}'
contains invalid characters. Only [A-Za-z0-9._-] allowed.`); replace the inline
check in the install flow that uses nameValue and the duplicate check at the CLI
side (the block around the other duplicate) to call this helper instead (either
throw from validateSkillName or check boolean and throw at the call site) so
both install-time and CLI-time use the same validation logic and message.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 68e25d3e-e4b7-4326-9fe4-4d96807d3b6e
📒 Files selected for processing (2)
src/lib/skill-install.test.tssrc/lib/skill-install.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/lib/skill-install.test.ts
Hi @wscurran, thanks for the review! #1845 and #1844 are related but not duplicates — #1845 (merged) implements skill install, and #1844 is the issue it closed. Skill removal was explicitly listed as out of scope in #1844 (see the "Out of scope" section: "Skill removal or update commands (follow-up)"). This PR is that follow-up, completing the skill lifecycle: Happy to add a reference to #1845 in the PR description to make the relationship clearer if that helps. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/skill-install.ts`:
- Around line 81-84: The error thrown when validateSkillName(nameValue) fails
should explicitly mention reserved-name restrictions (e.g., "." and "..") as
distinct from invalid character rules; update the message in the thrown Error
inside the SKILL.md name check to state both: allowed characters [A-Za-z0-9._-]
and that names cannot be "." or ".." (or other reserved names if applicable),
referencing validateSkillName and nameValue so maintainers can locate and adjust
the error string accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7afe3706-6ea6-456a-a029-99ba7cd28a93
📒 Files selected for processing (1)
src/lib/skill-install.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/nemoclaw.ts`:
- Around line 1380-1382: skillInstall.checkExisting currently conflates “absent”
and “probe failed” (see src/lib/skill-install.ts:349-356); change checkExisting
to return a tri-state/result object (for example {exists: boolean|null, error?:
Error}) or throw on probe failure, and then update the caller here to
distinguish outcomes: if exists === false report the skill is not installed, if
exists === null or an error was returned/logged then surface the probe error
(console.error with the error) and exit (or propagate) instead of printing “not
installed”. Reference: skillInstall.checkExisting, variables ctx, paths,
skillName, sandboxName.
- Around line 1394-1399: The current message assumes verifyRemove succeeded in
proving deletion, but skillInstall.verifyRemove(ctx, paths) can return false on
SSH/verification failure; update the error path where gone is false (the block
using gone, skillName and paths.uploadDir) to log that removal is unverified
rather than asserting the dir still exists—e.g. change the console.error message
to indicate "could not verify removal" and include skillName and paths.uploadDir
(or include the underlying error if available from verifyRemove) so callers know
verification failed rather than deletion was proven to have failed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 798ae3cd-318f-4d60-800e-b93fbfb611b6
📒 Files selected for processing (1)
src/nemoclaw.ts
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/lib/skill-install.ts (1)
354-363:⚠️ Potential issue | 🟠 MajorPreserve the tri-state on non-zero SSH exits.
sshExec()only returnsnullwhenspawnSync()throws. A normal SSH transport/auth failure still comes back with a non-zerostatus, so this path still collapses probe failures intofalseinstead ofnull. That means install can still treat an unreachable sandbox as a fresh install, and remove can still say the skill is not installed when the probe never completed.🩹 Proposed fix
export function checkExisting(ctx: SshContext, paths: SkillPaths): boolean | null { const checks = [`test -f ${shellQuote(`${paths.uploadDir}/SKILL.md`)}`]; if (paths.isOpenClaw && paths.mirrorDir) { checks.push(`test -f "${paths.mirrorDir}/SKILL.md"`); } const result = sshExec(ctx, `{ ${checks.join(" || ")}; } && echo EXISTS || echo ABSENT`); - if (result === null) { + if (result === null || result.status !== 0) { return null; } return result.stdout === "EXISTS"; }Verify by comparing
sshExec()andcheckExisting(): if SSH failures can produce a non-zero exit without throwing, this function should map that case tonull.#!/bin/bash sed -n '168,200p' src/lib/skill-install.ts printf '\n---\n' sed -n '344,364p' src/lib/skill-install.ts🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/skill-install.ts` around lines 354 - 363, The checkExisting function currently treats any non-null sshExec() result as a definitive false/true, but sshExec can return a non-zero exit/status for SSH transport or auth failures (not just throw), so update checkExisting (referencing checkExisting, sshExec, SshContext, SkillPaths) to detect sshExec results where result.status !== 0 (or an error indicated in result.stderr) and return null in that case; otherwise keep the existing stdout === "EXISTS" logic. Ensure you surface the tri-state by returning null for SSH failures, true when stdout is "EXISTS", and false otherwise.src/nemoclaw.ts (1)
1353-1356:⚠️ Potential issue | 🟡 MinorMention reserved names in the CLI validation error.
validateSkillName()also rejects.and.., so this message is still misleading for those inputs. Surface the reserved-name rule here too so the CLI explains the actual failure.✏️ Suggested tweak
if (!skillInstall.validateSkillName(skillName)) { console.error(` Invalid skill name: '${skillName}'`); - console.error(" Skill names must match [A-Za-z0-9._-]."); + console.error(" Skill names must use [A-Za-z0-9._-] and cannot be '.' or '..'."); process.exit(1); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/nemoclaw.ts` around lines 1353 - 1356, The CLI error for invalid skill names is misleading because validateSkillName(skillName) also rejects the reserved names "." and ".."; update the messages in the block that checks validateSkillName(skillName) so the output mentions both the allowed pattern and the reserved-name rule (e.g., state that "." and ".." are reserved and not allowed), keeping the existing descriptive first line (`Invalid skill name: '${skillName}'`) and replacing the second line to include the regex constraint and the reserved names; locate this logic around the skillInstall.validateSkillName call and adjust the console.error strings accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/lib/skill-install.ts`:
- Around line 354-363: The checkExisting function currently treats any non-null
sshExec() result as a definitive false/true, but sshExec can return a non-zero
exit/status for SSH transport or auth failures (not just throw), so update
checkExisting (referencing checkExisting, sshExec, SshContext, SkillPaths) to
detect sshExec results where result.status !== 0 (or an error indicated in
result.stderr) and return null in that case; otherwise keep the existing stdout
=== "EXISTS" logic. Ensure you surface the tri-state by returning null for SSH
failures, true when stdout is "EXISTS", and false otherwise.
In `@src/nemoclaw.ts`:
- Around line 1353-1356: The CLI error for invalid skill names is misleading
because validateSkillName(skillName) also rejects the reserved names "." and
".."; update the messages in the block that checks validateSkillName(skillName)
so the output mentions both the allowed pattern and the reserved-name rule
(e.g., state that "." and ".." are reserved and not allowed), keeping the
existing descriptive first line (`Invalid skill name: '${skillName}'`) and
replacing the second line to include the regex constraint and the reserved
names; locate this logic around the skillInstall.validateSkillName call and
adjust the console.error strings accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1df3f5b4-0338-409f-b1c7-2d29d57409ef
📒 Files selected for processing (2)
src/lib/skill-install.tssrc/nemoclaw.ts
f51e324 to
e1a7db7
Compare
… OpenClaw mirror - removeSkill(): success now requires both uploadDir and mirrorDir to be removed for OpenClaw sandboxes (previously only checked uploadDir, so a surviving mirror would still show the skill as removed) - verifyRemove(): checks both uploadDir and mirrorDir via a compound SSH test before returning GONE (previously only checked uploadDir) - checkExisting(): probes both uploadDir/SKILL.md and mirrorDir/SKILL.md so a partial removal (uploadDir gone, mirrorDir surviving) no longer blocks a reinstall All 37 skill-install tests pass. Fixes address the Major issue flagged by CodeRabbit on PR NVIDIA#1962. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/skill-install.ts`:
- Around line 365-375: checkExisting currently only returns null if sshExec
returns null, but sshExec can also return a non-null object with status !== 0 on
probe failure; update checkExisting to treat any non-successful sshExec result
(result === null OR result.status !== 0) as a probe failure and return null.
Locate the checkExisting function and change the result handling for the sshExec
call (the variable named result from sshExec) so that you return null when
result is null or result.status !== 0, and keep the existing EXISTS/ABSENT
stdout check (result.stdout === "EXISTS") for successful probes; ensure you
still build the same checks array using paths.uploadDir and paths.mirrorDir.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: b7f6d165-6392-4fc7-b38c-24713addfb0f
📒 Files selected for processing (12)
nemoclaw-blueprint/policies/presets/telegram.yamlnemoclaw/src/blueprint/runner.tsnemoclaw/src/lib/subprocess-env.tspackage.jsonsrc/lib/command-registry.tssrc/lib/http-probe.test.tssrc/lib/skill-install.test.tssrc/lib/skill-install.tssrc/lib/validation.tssrc/nemoclaw.tstest/check-docs-links.test.tstest/credential-exposure.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/lib/skill-install.test.ts
- src/nemoclaw.ts
e1a7db7 to
8ea0d86
Compare
… OpenClaw mirror - removeSkill(): success now requires both uploadDir and mirrorDir to be removed for OpenClaw sandboxes (previously only checked uploadDir, so a surviving mirror would still show the skill as removed) - verifyRemove(): checks both uploadDir and mirrorDir via a compound SSH test before returning GONE (previously only checked uploadDir) - checkExisting(): probes both uploadDir/SKILL.md and mirrorDir/SKILL.md so a partial removal (uploadDir gone, mirrorDir surviving) no longer blocks a reinstall All 37 skill-install tests pass. Fixes address the Major issue flagged by CodeRabbit on PR NVIDIA#1962. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/nemoclaw.ts (1)
2496-2499:⚠️ Potential issue | 🟡 MinorInvalid-name message is incomplete for reserved names.
Line 2498 says only regex constraints, but validation also rejects
"."and"..”. Please align the CLI error to avoid misleading users.✏️ Suggested tweak
if (!skillInstall.validateSkillName(skillName)) { console.error(` Invalid skill name: '${skillName}'`); - console.error(" Skill names must match [A-Za-z0-9._-]."); + console.error(" Skill names must match [A-Za-z0-9._-] and must not be '.' or '..'."); process.exit(1); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/nemoclaw.ts` around lines 2496 - 2499, Update the CLI error output in the block that checks skillInstall.validateSkillName(skillName) to mention reserved names in addition to the regex constraint: change the two console.error lines so they state that names must match [A-Za-z0-9._-] and that the single-segment reserved names "." and ".." are disallowed (reference skillInstall.validateSkillName and the variable skillName to locate the check). Ensure the message remains concise and printed before process.exit(1).
🧹 Nitpick comments (1)
src/nemoclaw.ts (1)
2710-2728: Deduplicate skill help output viaprintSkillInstallUsage().The dispatcher reprints the same usage/help block already provided by
printSkillInstallUsage(), which risks drift.♻️ Suggested refactor
async function sandboxSkillInstall(sandboxName, args = []) { const sub = args[0]; if (!sub || sub === "help" || sub === "--help" || sub === "-h") { - console.log(""); - console.log(" Usage: nemoclaw <sandbox> skill install <path>"); - console.log(" nemoclaw <sandbox> skill remove <name>"); - console.log(""); - console.log(" Deploy or remove a skill in a running sandbox."); - console.log(""); - console.log(" install <path> Deploy a skill directory to the sandbox."); - console.log( - " <path> must be a skill directory containing a SKILL.md (with 'name:' frontmatter),", - ); - console.log( - " or a direct path to a SKILL.md file. All non-dot files in the directory are uploaded.", - ); - console.log(""); - console.log(" remove <name> Remove an installed skill from the sandbox by name."); - console.log(" <name> is the skill name from SKILL.md frontmatter (e.g. my-skill)."); - console.log(""); + printSkillInstallUsage(); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/nemoclaw.ts` around lines 2710 - 2728, The large duplicated help/usage block for the "skill install/remove" command should be replaced by a single call to the existing helper printSkillInstallUsage(); locate the conditional that checks sub (the block starting with if (!sub || sub === "help" || sub === "--help" || sub === "-h")) and remove the console.log calls, then call printSkillInstallUsage() instead so the help text is maintained in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/nemoclaw.ts`:
- Around line 2496-2499: Update the CLI error output in the block that checks
skillInstall.validateSkillName(skillName) to mention reserved names in addition
to the regex constraint: change the two console.error lines so they state that
names must match [A-Za-z0-9._-] and that the single-segment reserved names "."
and ".." are disallowed (reference skillInstall.validateSkillName and the
variable skillName to locate the check). Ensure the message remains concise and
printed before process.exit(1).
---
Nitpick comments:
In `@src/nemoclaw.ts`:
- Around line 2710-2728: The large duplicated help/usage block for the "skill
install/remove" command should be replaced by a single call to the existing
helper printSkillInstallUsage(); locate the conditional that checks sub (the
block starting with if (!sub || sub === "help" || sub === "--help" || sub ===
"-h")) and remove the console.log calls, then call printSkillInstallUsage()
instead so the help text is maintained in one place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: b7c91822-cb2c-42fb-aca7-3f37d704762c
📒 Files selected for processing (14)
nemoclaw-blueprint/policies/presets/telegram.yamlnemoclaw/src/blueprint/runner.tsnemoclaw/src/lib/subprocess-env.tspackage.jsonsrc/lib/command-registry.tssrc/lib/config-io.tssrc/lib/http-probe.test.tssrc/lib/inventory-commands.tssrc/lib/skill-install.test.tssrc/lib/skill-install.tssrc/lib/validation.tssrc/nemoclaw.tstest/check-docs-links.test.tstest/credential-exposure.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/lib/command-registry.ts
- src/lib/skill-install.test.ts
8ea0d86 to
8adfe48
Compare
Adds `nemoclaw <sandbox> skill remove <name>` to complement the existing `skill install` command, providing a complete skill lifecycle from the CLI. - Removes the skill from the immutable upload dir: /sandbox/.openclaw/skills/<name>/ - Removes the OpenClaw mirror dir: $HOME/.openclaw/skills/<name>/ - Clears sessions.json so the agent re-discovers remaining skills on the next session (same mechanism as a fresh install) - Verifies the directory is gone after removal - Rejects invalid/unsafe skill names before any SSH operation - Prints a clear error when the named skill is not installed src/lib/skill-install.ts - Updated module-level comment to mention skill remove - Added removeSkill() - removes upload dir, mirror dir, clears sessions - Added verifyRemove() - confirms directory is gone post-removal - Added validateSkillName() - rejects shell metacharacters in CLI input src/nemoclaw.ts - Extended sandboxSkillInstall() to dispatch install vs remove - Updated --help output to document both subcommands - Updated global help text to list skill remove - Validates skill name with validateSkillName() before any SSH ops - Checks skill exists before attempting removal (clear error if missing) - SSH config temp file always cleaned up in finally block src/lib/skill-install.test.ts - Added describe(validateSkillName) - valid names, empty, spaces, metacharacters - Added describe(removeSkill) - SSH-failure path returns success=false + warning - Added describe(verifyRemove) - SSH-failure returns false (conservative) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… parseFrontmatter Both functions accepted single-dot and double-dot names because the regex /^[A-Za-z0-9._-]+$/ allows '.'. In the remove path this would expand to rm -rf '/sandbox/.openclaw/skills/..' which deletes the parent .openclaw or skills/ directory instead of a single skill. Fix: add explicit name !== '.' && name !== '..' guards in both validateSkillName (CLI input) and parseFrontmatter (SKILL.md name field), keeping the two checks aligned as CodeRabbit flagged. Add test cases for '.' and '..' in both validateSkillName and parseFrontmatter describe blocks. All 33 skill-install tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… OpenClaw mirror - removeSkill(): success now requires both uploadDir and mirrorDir to be removed for OpenClaw sandboxes (previously only checked uploadDir, so a surviving mirror would still show the skill as removed) - verifyRemove(): checks both uploadDir and mirrorDir via a compound SSH test before returning GONE (previously only checked uploadDir) - checkExisting(): probes both uploadDir/SKILL.md and mirrorDir/SKILL.md so a partial removal (uploadDir gone, mirrorDir surviving) no longer blocks a reinstall All 37 skill-install tests pass. Fixes address the Major issue flagged by CodeRabbit on PR NVIDIA#1962. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move validateSkillName before parseFrontmatter so parseFrontmatter can call it instead of duplicating the same dot/double-dot and regex checks inline. Removes the second definition at the bottom of the file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract skill remove logic into sandboxSkillRemove() and skill install logic into sandboxSkillDeploy(). sandboxSkillInstall() is now a thin dispatcher that handles help and routes to the two handlers. Each function has a single responsibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Clarify error message for invalid skill names to mention '.' and '..' - Change checkExisting() to return boolean|null (null = SSH probe failed) to distinguish absent from unreachable sandbox - Update both callers: remove exits with clear error on null; install treats null as fresh install with a warning - Fix verifyRemove failure message to say 'could not be verified' instead of asserting the upload dir still exists Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sshExec() can return a non-null result with status !== 0 when the SSH transport fails (connection refused, auth error, etc.). Previously, checkExisting() only returned null when sshExec() itself returned null (i.e. spawnSync threw), so a transport-layer failure was silently collapsed into false — causing callers to report 'skill not installed' instead of 'could not reach sandbox'. Fix: treat result === null OR result.status !== 0 as a probe failure and return null. Also tighten the stdout check so any unexpected output (neither EXISTS nor ABSENT) returns null rather than false. Update the two unit tests that expected false on an unreachable SSH config to expect null, matching the corrected contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…strictions The error message in sandboxSkillRemove previously said only '[A-Za-z0-9._-]' but validateSkillName also rejects '.' and '..' as reserved names. Aligns the CLI error with the parseFrontmatter error which already mentions both rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/nemoclaw.ts (1)
2483-2737: Please run the sandbox lifecycle E2E set for this CLI-path change.Given this PR updates
src/nemoclaw.tssandbox skill command flow, run the recommended targeted nightly jobs before merge:
sandbox-survival-e2esandbox-operations-e2eskip-permissions-e2eAs per coding guidelines:
src/nemoclaw.tschanges should run the listed E2E jobs viagh workflow run nightly-e2e.yaml --ref <branch> -f jobs=sandbox-survival-e2e,sandbox-operations-e2e,skip-permissions-e2e.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/nemoclaw.ts` around lines 2483 - 2737, The change touches the sandbox skill CLI flow in src/nemoclaw.ts (see sandboxSkillRemove, sandboxSkillDeploy and sandboxSkillInstall), so before merging run the targeted nightly E2E jobs: invoke the GitHub Actions workflow nightly-e2e.yaml for this branch and pass the three jobs sandbox-survival-e2e,sandbox-operations-e2e,skip-permissions-e2e (e.g. use gh workflow run nightly-e2e.yaml --ref <branch> -f jobs=sandbox-survival-e2e,sandbox-operations-e2e,skip-permissions-e2e) and attach the run results to the PR.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/skill-install.test.ts`:
- Around line 84-87: The test in skill-install.test.ts that checks
parseFrontmatter should be updated to match the parser's current error message:
change the expect(...).toThrow("invalid characters") assertions in the "rejects
dot and double-dot as skill names in frontmatter" test to expect the new message
thrown by parseFrontmatter ("is invalid. Use [A-Za-z0-9._-] and do not use '.'
or '..'."); update both cases (name: . and name: ..) to assert that exact string
from parseFrontmatter.
In `@src/nemoclaw.ts`:
- Around line 2496-2499: The validation error message for invalid skill names is
misleading because validateSkillName(skillName) also rejects the reserved names
"." and ".."; update the error output around the validateSkillName check to
mention that names must match the allowed pattern [A-Za-z0-9._-] and that the
single-dot "." and double-dot ".." are reserved/invalid (i.e., change the
console.error lines that reference skillName to include an explicit note that
"." and ".." are not allowed).
---
Nitpick comments:
In `@src/nemoclaw.ts`:
- Around line 2483-2737: The change touches the sandbox skill CLI flow in
src/nemoclaw.ts (see sandboxSkillRemove, sandboxSkillDeploy and
sandboxSkillInstall), so before merging run the targeted nightly E2E jobs:
invoke the GitHub Actions workflow nightly-e2e.yaml for this branch and pass the
three jobs sandbox-survival-e2e,sandbox-operations-e2e,skip-permissions-e2e
(e.g. use gh workflow run nightly-e2e.yaml --ref <branch> -f
jobs=sandbox-survival-e2e,sandbox-operations-e2e,skip-permissions-e2e) and
attach the run results to the PR.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 1be63b75-1ccd-4fc7-ac19-1d943b776a1c
📒 Files selected for processing (3)
src/lib/skill-install.test.tssrc/lib/skill-install.tssrc/nemoclaw.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/nemoclaw.ts (1)
2695-2771: Please run the recommended sandbox lifecycle E2Es on this new command path.
skill removenow drives live sandbox SSH, remote deletion, session refresh, and reconnect-on-next-session behavior. I'd want the recommendedsandbox-survival-e2eandsandbox-operations-e2ecoverage on this PR before merge.As per coding guidelines
src/nemoclaw.ts: “Changes affect sandbox lifecycle commands. E2E test recommendation:sandbox-survival-e2e— gateway restart recovery;sandbox-operations-e2e— process recovery after gateway kill.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/nemoclaw.ts` around lines 2695 - 2771, The new sandboxSkillRemove implementation in src/nemoclaw.ts modifies live sandbox lifecycle (SSH removal, session refresh, reconnect behavior) but lacks E2E verification; run the recommended E2Es (sandbox-survival-e2e and sandbox-operations-e2e) exercising the new command path (sandboxSkillRemove / "sandbox <name> skill remove <name>") and update or add tests to cover SSH-driven remote deletion, session refresh/reconnect-on-next-session, and gateway restart/process-kill recovery; if any test fails, adjust sandboxSkillRemove or its helpers (agentRuntime.getSessionAgent, skillInstall.removeSkill, verifyRemove, and ensureLiveSandboxOrExit usage) until the E2Es pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/skill-install.ts`:
- Around line 330-335: The SkillPaths type is missing the mirrorDir property and
resolveSkillPaths() never sets it, causing the accesses in checkExisting, and
other places (uses of paths.mirrorDir) to be invalid; add mirrorDir?: string to
the SkillPaths interface and modify resolveSkillPaths() to compute and assign
mirrorDir (e.g., when isOpenClaw is true or when a mirror path is present) so
that functions like checkExisting and any callers safely read paths.mirrorDir
(ensure it is optional or set to undefined when not applicable).
---
Nitpick comments:
In `@src/nemoclaw.ts`:
- Around line 2695-2771: The new sandboxSkillRemove implementation in
src/nemoclaw.ts modifies live sandbox lifecycle (SSH removal, session refresh,
reconnect behavior) but lacks E2E verification; run the recommended E2Es
(sandbox-survival-e2e and sandbox-operations-e2e) exercising the new command
path (sandboxSkillRemove / "sandbox <name> skill remove <name>") and update or
add tests to cover SSH-driven remote deletion, session
refresh/reconnect-on-next-session, and gateway restart/process-kill recovery; if
any test fails, adjust sandboxSkillRemove or its helpers
(agentRuntime.getSessionAgent, skillInstall.removeSkill, verifyRemove, and
ensureLiveSandboxOrExit usage) until the E2Es pass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 45a6b89f-8131-4b44-b32d-509b09e99fec
📒 Files selected for processing (14)
nemoclaw-blueprint/policies/presets/telegram.yamlnemoclaw/src/blueprint/runner.tsnemoclaw/src/lib/subprocess-env.tspackage.jsonsrc/lib/command-registry.tssrc/lib/config-io.tssrc/lib/http-probe.test.tssrc/lib/inventory-commands.tssrc/lib/skill-install.test.tssrc/lib/skill-install.tssrc/lib/validation.tssrc/nemoclaw.tstest/check-docs-links.test.tstest/credential-exposure.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/lib/skill-install.test.ts
…hs() SkillPaths was missing the mirrorDir property, causing TypeScript to flag accesses to paths.mirrorDir in checkExisting(), removeSkill(), and verifyRemove() as undefined property references. - Add mirrorDir: string | null to SkillPaths interface - Populate it in resolveSkillPaths() as isOpenClaw ? '$HOME/.openclaw/skills/<name>' : null - Add mirrorDir assertions to resolveSkillPaths() unit tests - Update stale 'invalid characters' assertions in parseFrontmatter tests to 'is invalid' to match the updated error message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/skill-install.test.ts`:
- Around line 334-339: The non-OpenClaw tests use the wrong agent config shape:
update the resolveSkillPaths/resolvePaths call sites in the tests to pass the
current agent shape by replacing configPaths.immutableDir and
configPaths.writableDir with a single configPaths.dir (e.g., configPaths: { dir:
"/sandbox/.hermes" }) so resolveSkillPaths/resolvePaths computes the correct
skill paths; apply this same change to the three occurrences around the failing
assertions (the calls that currently pass immutableDir/writableDir).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 4baf4ffa-8edd-4886-af07-0edb45e9f8f2
📒 Files selected for processing (2)
src/lib/skill-install.test.tssrc/lib/skill-install.ts
…pers
resolveSkillPaths() reads configPaths.dir, but the three non-OpenClaw
test call sites were passing configPaths.immutableDir/writableDir (the
old agent shape). This caused the computed uploadDir to be
'undefined/skills/test-skill', weakening what the tests validate.
Replace all three occurrences with configPaths: { dir: '/sandbox/.hermes' }.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
|
@sinha-abhash could you add a DCO to this PR, please? We can't merge it otherwise. |
|
@sinha-abhash can you add a DCO, please? |
|
Thanks for the contribution. Since this branch remains DCO-blocked, I opened a maintainer-signed salvage PR with the same feature work here: #4563. We'll continue review on the replacement PR. |
|
Closing as superseded by the maintainer-signed salvage PR: #4563. |
## Summary Salvages the skill removal work from [#1962](#1962) onto a maintainer-owned, signed-off branch so the DCO-blocked contribution can proceed. Adds `nemoclaw <sandbox> skill remove <skill>` for removing installed agent skills from a running sandbox. ## Related Issue Supersedes [#1962](#1962). ## Changes - Add the native `sandbox skill remove` oclif command and public argv translation. - Add removal logic for upload directories, OpenClaw mirror directories, and OpenClaw session refresh. - Validate skill names before constructing remote removal paths, including rejecting `.` and `..`. - Update command reference docs and CLI display metadata. - Add unit and command tests for remove behavior and safety validation. ## Type of Change - [x] Code change with doc updates - [ ] Code change (feature, bug fix, or refactor) - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification - [ ] `npx prek run --all-files` passes - [x] `npm test` passes - [x] Tests added or updated for new or changed behavior - [x] No secrets, API keys, or credentials committed - [x] Docs updated for user-facing behavior changes - [ ] `npm run docs` builds without warnings (doc changes only) - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) Validation run: - `npm run build:cli` - `npm test -- src/lib/skill-install.test.ts src/commands/sandbox/skill.test.ts src/lib/cli/public-argv-translation.test.ts src/lib/cli/command-registry.test.ts` - `npm run typecheck:cli` - `npm run docs:strict` - commit/push hooks completed successfully, including CLI test and pre-push TypeScript checks. - `npm run docs:strict` completed with no errors (Fern reported existing upgrade warnings). --- Signed-off-by: Carlos Villela <cvillela@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a CLI command to remove an installed skill from a running sandbox by SKILL.md name, with validation and verification. * **Documentation** * CLI reference, help text, usage and examples updated to document the remove subcommand and agent-specific behavior (non-OpenClaw agents may require gateway restart). * **Tests** * Test suites expanded to cover removal flows, validation, and error cases. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/NVIDIA/NemoClaw/pull/4563?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Carlos Villela <cvillela@nvidia.com>
Summary
Adds
nemoclaw <sandbox> skill remove <name>to complement the existingskill installcommand (#1844 / #1845), providing a complete skill lifecycle from the CLI.Motivation
After
skill installwas added, there was no way to cleanly remove a skill from a running sandbox. Users had to manually SSH in, delete two directories, and clearsessions.json— mirroring the same problemskill installwas created to solve.What this does
/sandbox/.openclaw/skills/<name>/$HOME/.openclaw/skills/<name>/sessions.jsonso the agent re-discovers remaining skills on the next session[A-Za-z0-9._-]before any SSH operation — shell injection rejected client-sideUsage
Run
nemoclaw <sandbox> skill --helpfor full usage.Edge cases tested
Changes
src/lib/skill-install.tsremoveSkill()— removes upload dir, mirror dir, clears sessionsverifyRemove()— confirms directory is gone post-removalvalidateSkillName()— rejects shell metacharacters in CLI name inputsrc/nemoclaw.tssandboxSkillInstall()to dispatchinstallvsremove--helpoutput and global help textfinallyblocksrc/lib/skill-install.test.tsvalidateSkillNametests — valid names, empty, spaces, metacharactersremoveSkillunit test — SSH-failure path returnssuccess=false+ warningverifyRemoveunit test — SSH-failure returnsfalse(conservative/safe)All 1611 existing tests continue to pass.
Summary by CodeRabbit
New Features
Documentation
Tests