feat: holodeck skill command (catalog + claude/cursor/codex/gemini installers)#812
Merged
ArangoGutierrez merged 27 commits intoMay 21, 2026
Merged
Conversation
ArangoGutierrez
added a commit
to ArangoGutierrez/holodeck
that referenced
this pull request
May 15, 2026
urfave/cli/v2's default parser stops parsing flags after the first non-flag arg, so 'holodeck skill add using-holodeck --claude' was rejected with 'must specify at least one of --claude/...'. Rewrites the in-binary --help examples and README to put agent flags before the positional skill name, matching actual parser behavior. Caught during Team Lead review of draft PR NVIDIA#812. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
ArangoGutierrez
commented
May 15, 2026
Collaborator
Author
ArangoGutierrez
left a comment
There was a problem hiding this comment.
Principal Engineer review — PR #812
(Posted by Team Lead on behalf of principal-engineer subagent. See ~/.claude/CLAUDE.md PE-review protocol.)
Verdict
APPROVE
Architecture
Rendererinterface (pkg/skill/render.go:27-43) is cleanly factored: four methods, single responsibility, no leaky abstractions. TheSingleFile()discriminator is the right shape for the two install models.Install()(pkg/skill/install.go:54-113) cleanly switches onr.SingleFile()and dispatches toinstallSingleFilevs the per-file atomic flow. Both paths sharewriteFileAtomic.cmd/cli/skill/correctly mirrorscmd/cli/<cmd>/pattern. Dependency direction is right:cmd/cli/skill→pkg/skill, no upward leaks.pkg/skillhas zero non-stdlib deps beyondyaml.v2(parser) andgo-isatty(already in tree). No transitive churn.- Mild scope concern:
commandstruct (cmd/cli/skill/skill.go:30-52) is shared betweenlistandadd;outputFormatonly meaningful forlist, other 10 fields only foradd. Follows existing patterns; acceptable.
Go conventions
- Errors wrapped at every call site (
install.go:126,134,138,149,155,172, etc.) with%w.errors.Is(err, os.ErrNotExist)used correctly. - Receivers correct (value receivers on stateless renderers; consistent).
context.Contextdeliberately absent — appropriate for short synchronous CLI ops on local FS.- "Accept interfaces, return structs" shape held:
Rendererconsumed where defined;*xRendererreturned fromNewXRenderer. - No
init(), no globals, noany.
Security
- gosec G304 (
os.ReadFile(dest)atinstall.go:165): False positive.destis computed fromr.InstallPath(...), which is bounded to known string constants (.codex/AGENTS.md,.gemini/GEMINI.md,~/.codex/...,~/.gemini/...). - gosec G301 (
MkdirAll(..., 0o755)atinstall.go:133): Acceptable. These are config dirs (.claude/skills/,.cursor/rules/, etc.) — user-readable is conventional and matches how Claude Code / Cursor / Codex / Gemini already create them.0o750would satisfy gosec; not worth a behavior change. Consider// #nosec G301 -- config dir conventionannotation for clean lint output. - Path-traversal — defense-in-depth gap (non-blocking):
parseSkill(skill.go:43-75) does not validate thenamefield.s.Nameflows unsanitized intofilepath.Join(".claude", "skills", s.Name, "SKILL.md")andfilepath.Join(".cursor", "rules", s.Name+".mdc"). Safe today because the catalog is//go:embed-only (compile-time, trusted). The moment a future PR adds--file <local-SKILL.md>or a remote catalog, aname: ../../etc/cron.d/evilbecomes a traversal write. Recommend a^[a-z0-9][a-z0-9-]{0,63}$allowlist regex inparseSkill. Cheap, future-proofs the package. writeFileAtomic(install.go:132-159) — correct temp + rename with cleanup-on-err.installSingleFilehas a benign concurrency race (read → splice → write, no flock). Two concurrentholodeck skill addagainst the sameAGENTS.mdwould lose one update. Out of v1 scope; flag for follow-up.
Test quality
Spot-checked every test file. No theater tests detected.
TestParseSkill(skill_test.go): table-driven, six cases (valid + 5 error paths). Literal expected strings derived independently of impl. Fails ifparseSkillbody deleted.TestClaudeRendererRender(render_claude_test.go:36): asserts against fully-derived literal"---\nname: demo\ndescription: A demo skill\n---\n\n# Body\n\nDo the thing.\n".TestSpliceSection_*(install_test.go:27-72): three cases (append, replace, only-touch-named-skill) with literal substrings, count checks (bytes.Count(... == 1), surrounding-content preservation. Catches splice regressions.TestInstall_SingleFile_*/TestInstall_PerFile_*: invoke production functions ont.TempDir()and read back. Real I/O.TestRunAdd_*: validation tests assert specific error substrings;TestRunAdd_DryRun_ClaudeOnlyandTestRunAdd_AllAgents_SetsAllFourgo through the fullCatalog+Installpipeline.TestBuildAddCommand_DescriptionUsesFlagFirstExamples(add_test.go:161-174) is a particularly nice doc-regression guard for the urfave/cli/v2 quirk fixed inad00a640.- Tests are deterministic: no
time.Now, norand, no map-iteration ordering reliance. Catalog is sorted before assertions touch it. - Minor caveat:
TestRunAdd_DryRun_ClaudeOnlyandTestRunAdd_AllAgents_SetsAllFouruseos.Chdir(t.TempDir())and are nott.Parallel()-safe. Currently fine; flag if anyone addst.Parallel()later.
YAGNI / scope
Implementation honored the deferral. No Remove(), no tag filter, no TUI picker, no remote catalog plumbing. Renderer interface is exactly what the four shipped agents need.
SKILL.md content
- Frontmatter
description(SKILL.md:3) is well-shaped for Claude Code's skill-triggering matcher: starts with "Use when...", names concrete user intents, names the trigger noun ("holodeck CLI"), lists scope. Matcher-friendly. - Aspirational content (
list,status,ssh,scp,cleanup,os list) is per Team Lead direction; flagged for separate reconciliation.
Blocking issues
none
Non-blocking suggestions
- Add a
nameregex validator inparseSkillfor defense-in-depth. - Suppress G301 with
// #nosec G301 -- config dir conventionor downgrade to0o750. - Add a brief comment on
installSingleFiledocumenting the concurrent-write race.
Follow-up issues to file
- Parser fix (
ad00a640follow-up): urfave/cli/v2 → v3 migration or interspersion shim. The in-binary--helpshowingholodeck skill add --claude using-holodeckwill confuse users who reach for the naturalholodeck skill add using-holodeck --claudeand silently get the "must specify at least one of --claude/..." error. - SKILL.md aspirational content reconciliation: trim to commands that exist on
main(create,delete,dryrun) OR ship the referenced subcommands before v0.3.0 cut. v0.3.0 is in bug-fix sprint per.claude/CLAUDE.md, so cutting the aspirational sections is the lower-risk path. - Skill-name validation regex: low-priority hardening before opening the catalog source to anything non-embed.
This was referenced May 15, 2026
ArangoGutierrez
added a commit
to ArangoGutierrez/holodeck
that referenced
this pull request
May 16, 2026
urfave/cli/v2's default parser stops parsing flags after the first non-flag arg, so 'holodeck skill add using-holodeck --claude' was rejected with 'must specify at least one of --claude/...'. Rewrites the in-binary --help examples and README to put agent flags before the positional skill name, matching actual parser behavior. Caught during Team Lead review of draft PR NVIDIA#812. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
ad00a64 to
c48bc50
Compare
Coverage Report for CI Build 26209564134Coverage increased (+0.9%) to 48.745%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Pure-function parser that splits a SKILL.md file into Skill{Name,
Description, Body}. Rejects missing name/description, empty body,
and missing frontmatter delimiters. Foundation for the catalog.
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
//go:embed pulls every pkg/skill/data/skills/<name>/SKILL.md into the binary at compile time. Catalog() walks the embedded FS, parses each file, and returns a sorted []Skill. Includes a placeholder using-holodeck skill so the embed has content; final content lands in a later commit. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Renderer is the per-agent translation point. The Claude renderer emits Skill content verbatim as <install-dir>/.claude/skills/<name>/ SKILL.md. Project-local default; --global resolves to $HOME. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Translates SKILL.md frontmatter (name, description) into Cursor's .mdc frontmatter (description, globs, alwaysApply). Project-local default; --global resolves to $HOME. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Renders skill content as a section block delimited by <\!-- BEGIN/END holodeck-skill:<name> --> markers. The installer will use these markers in a later commit to make append/replace idempotent in the shared AGENTS.md file. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Mirrors the Codex section-block output. Installs to GEMINI.md (project-local) or ~/.gemini/GEMINI.md (global). Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Install() ties Renderer to the filesystem. Per-file agents get an atomic write with overwrite prompt (TTY) / error (non-TTY) unless --force is set; --dry-run skips the write entirely. Single-file agents get spliceSection: append when absent, replace when present, malformed-existing falls through to append (never destructive). Parent directories are created with 0755. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Scaffolds cmd/cli/skill following the cmd/cli/os subcommand pattern. 'skill list' reads the embedded catalog and emits table/json/yaml output via pkg/output, matching 'holodeck list -o ...'. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Resolves the (skills x renderers) matrix from flags and calls pkgskill.Install for each pair. Validation: requires an agent flag, requires a skill name (or --all), rejects --stdout with multi-target, rejects --all combined with a positional name. Errors include the available-skill list for discoverability. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Registers the new skill command alongside the existing create/delete/ dryrun subcommands. 'holodeck skill list' and 'holodeck skill add' now reachable from the CLI binary. Adds TestNewCommand_MainGoWiringContract to assert the full surface main.go consumes: non-nil, Name == "skill", non-empty Usage, both 'list' and 'add' subcommands present. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Replaces the placeholder body with a real guide covering env.yaml shape, create/list/status/ssh/scp/delete/cleanup workflows, output flags, and common pitfalls (credentials, region, OS, cache, VPC leak). Source of truth lives next to the code so it versions with CLI changes. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
README gets a quickstart for 'holodeck skill list' and 'holodeck skill add'. CLI top-level --help already lists the new command via urfave/cli's default COMMANDS rendering; no template edits needed because main.go does not define a CustomAppHelpTemplate on this branch. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
pkg/skill imports gopkg.in/yaml.v2 directly to parse SKILL.md frontmatter. 'go mod tidy' reflects this by moving the entry from the indirect-require block to the direct one. No new modules vendored; go.sum and vendor/modules.txt are unchanged. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
urfave/cli/v2's default parser stops parsing flags after the first non-flag arg, so 'holodeck skill add using-holodeck --claude' was rejected with 'must specify at least one of --claude/...'. Rewrites the in-binary --help examples and README to put agent flags before the positional skill name, matching actual parser behavior. Caught during Team Lead review of draft PR NVIDIA#812. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
- Check fmt.Fprintf/Fprintln return values in add.go, list.go, install.go - Tighten MkdirAll permission to 0o750 in install.go (gosec G301) - Annotate os.ReadFile calls where the destination path is bounded by Renderer.InstallPath or t.TempDir() (gosec G304 false positive) - Tighten test WriteFile permission to 0o600 (gosec G306) - Use fmt.Appendf instead of []byte(fmt.Sprintf(...)) (gocritic fmtappendf) - Move Renderer interface satisfaction check to a package-level var declaration so the explicit interface type is preserved without triggering staticcheck ST1023 Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
The shipped skill referenced rhel-9 (no such OS), a flat kubernetesVersion field (actual schema nests under kubernetes.version with installer: kubeadm), and spec.instance.amiID (actual path is spec.instance.image.imageId). Rewrites the env.yaml example against examples/v1alpha1_environment.yaml and verifies every command and flag against the binary's --help output. Also adds workflows for the get and update subcommands, an 'OS image discovery' section covering os list/describe/ami, and corrects the dry-run anti-pattern to reference the dryrun subcommand (create has no --dry-run flag). Closes NVIDIA#814. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
parseSkill now rejects skill names that don't match
^[a-z0-9][a-z0-9-]{0,63}$. The catalog is //go:embed today so this
is defense-in-depth, but it hardens the parser against future
loaders that read SKILL.md from disk or network. Without this,
a SKILL.md with 'name: ../../etc/cron.d/evil' would let renderers
write outside the intended directory via filepath.Join.
Adds table-driven cases for path-traversal, leading-hyphen,
uppercase, underscore, and embedded-space rejections.
Closes NVIDIA#815.
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
The earlier docs commit showed 'holodeck os ami <id> <region>' with region as a positional. The actual command takes --region as a required flag (and --arch as optional). Update the OS image discovery section to match the binary's actual flag set. Caught by re-verifying every command in SKILL.md against the binary's --help output. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Two QA findings caught by the parallel review pass: 1. The env.yaml example nested 'os: ubuntu-22.04' under 'instance.image'. The Image struct in api/holodeck/v1alpha1/types.go has no OS field; the OS field lives directly on Instance. Non-strict YAML unmarshal silently dropped the misplaced key, so users following the example would have been given the default Ubuntu 22.04 AMI regardless of what they set. Move 'os' to be a direct child of 'instance' (sibling of 'type' and 'region') and add a commented-out alternative showing how to set an explicit imageId. 2. The output-flags section grouped 'os list' with the read commands that accept '-o table|json|yaml'. The 'os list' command in cmd/cli/os/os.go has no Flags slice — its output is a hardcoded tabwriter. Note this explicitly instead of implying flag parity. Also align the OS pitfall to reference 'spec.instance.os' rather than the wrong 'spec.instance.image.os'. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Address QA mutation-spot-check gap: the regex caps skill names at 64
characters but no test exercised that bound. Add a 65-char case
('a' repeated) that must be rejected. Mutating '{0,63}' to '{0,}'
would now fail this test.
Also rename the prior 'empty after trim' case to 'embedded space',
matching what it actually exercises (parseSkill does not trim;
the regex rejects the space char).
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
Three more bugs surfaced by a third-pass QA audit against actual
source code:
1. 'dryrun (no AWS calls)' was wrong. pkg/provider/aws/dryrun.go:25-70
makes read-only AWS describe-* API calls (checkInstanceTypes,
checkImages, getInstanceTypeArch) and requires valid credentials.
Reword to 'read-only; no resources created' and note credential
dependency.
2. 'holodeck update <id>' was documented as 'idempotent re-run'. With
no flags it is a no-op: cmd/cli/update/update.go:212 sets
needsProvision := m.reprovision and line 350-352 prints 'No changes
to apply'. Replace with concrete flag-driven examples
(--reprovision, --add-driver, --add-kubernetes, --add-toolkit,
--label) sourced from update.go --help.
3. 'auth.keyName / auth.privateKey are environment-variable names, not
literals' was the opposite of true. pkg/provider/aws/create.go:493
and cluster.go:699 pass Spec.KeyName literally to EC2 RunInstances;
PrivateKey is used literally as a filesystem path. The canonical
example file uses HOLODECK_AWS_ACCESS_KEY_ID-shaped placeholders,
but holodeck never calls os.Getenv on them — they are templates
users must replace. Replace the env.yaml example with realistic
placeholders ('my-aws-keypair', '~/.ssh/my-aws-key.pem'), rewrite
the credentials pitfall to make the literal vs env-var split
explicit, and fix the anti-pattern accordingly.
Also clarify that 'get kubeconfig -o' is an output PATH (not a format
selector) so it isn't confused with 'list -o table|json|yaml'.
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
…egex The new 65-char rejection case (added in c61cff3) catches a {0,63}->{0,} loosening. The matching tightening mutation ({0,63}->{0,62}) would silently break legitimate 64-char names with no test failure. Add a 64-char valid case to lock down the upper bound. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
… description The parent `skill` command's Description has the same urfave/cli/v2 flag-parsing pitfall that add_test.go already guards against on the `add` subcommand: any example shown as `skill add <name> --flag` is unrunnable because urfave/cli/v2 stops parsing flags after the first positional, and the action then fails with 'must specify at least one of --claude/--cursor/--codex/--gemini/--all-agents'. Mirror the regression guard so the Description shown by `holodeck skill --help` cannot drift back to the broken ordering. The regex rejects `skill add <kebab-name> --(claude|cursor|codex|gemini| all-agents)` and a sanity assertion requires the canonical flag-first form `--claude using-holodeck` to be present, so removing all examples doesn't accidentally pass the guard. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
urfave/cli/v2's default parser stops consuming flags after the first positional, so the previous examples shown by `holodeck skill --help` were unrunnable as printed: $ holodeck skill add using-holodeck --claude Error: must specify at least one of --claude/--cursor/--codex/--gemini/--all-agents The flag is silently dropped because `using-holodeck` is treated as the first positional, after which `--claude` is rejected as another positional rather than parsed. Rewrite both copy-pastable examples in the Description so the agent flag precedes the skill-name positional, matching the pattern used by the `add` subcommand's own help and the canonical install commands in the embedded SKILL.md docs: holodeck skill add --claude using-holodeck holodeck skill add --claude --cursor using-holodeck The regression guard added in the prior commit now passes. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
…KILL.md
When an AI agent reads using-holodeck/SKILL.md it copy-pastes the
commands verbatim, so any example in the broken `<id> --flag` form
makes the agent's first attempt fail in a way that's hard to diagnose
without reading urfave/cli/v2 source.
Three classes of fix in this file:
- Rewrite the `update` and `get kubeconfig` examples so flags come
before the positional instance id (e.g. `update --instance-type … <id>`,
`get kubeconfig -o ./kubeconfig <id>`). The `<id>` positional has to
be last for the same urfave/cli/v2 reason.
- Make `cleanup` examples explicit that `--region us-west-2` (or
`AWS_REGION=us-west-2` env) is required — `cleanup` does not infer
the region from the saved instance metadata.
- Two new "Common pitfalls" entries:
* Instance IDs are short hex strings (`a1b2c3d4`), not
EC2-style `i-…` ARNs. Agents routinely confuse the two.
* Flag-first ordering — `urfave/cli/v2` stops parsing flags after
the first positional, with a pointer to which commands this
bites (update, get kubeconfig, cleanup, skill add).
Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
The original comment claimed the skill name came from the parent directory name, but parseSkill ignores that string for the canonical Name (which is read from the YAML frontmatter and validated against skillNameRE). The directory name is only used as an error-message prefix. Reword the comment so a future maintainer doesn't add redundant directory-name validation based on the misleading wording. Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
sigs.k8s.io/yaml is already a direct dependency of this module and is the yaml library used in pkg/jyaml, pkg/utils/kubeconfig, and elsewhere. Routing frontmatter parsing through it avoids introducing a second YAML library and removes ~10k lines of vendored gopkg.in/yaml.v2 code. sigs.k8s.io/yaml converts YAML to JSON internally and then uses encoding/json, so the frontmatter struct tags switch from yaml: to json: to match the rest of the repo (e.g. SkillListItem in cmd/cli/skill/list.go already uses both). Signed-off-by: Carlos Eduardo Arango Gutierrez <eduardoa@nvidia.com>
20766f2 to
5aebea7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Users adopting holodeck via an AI coding agent (Claude Code, Cursor,
Codex, Gemini CLI) have no first-class way to teach the agent the
CLI. Today the only path is "paste --help into the agent."
Approach
Adds a
holodeck skillcommand with two subcommands:holodeck skill list— show the embedded catalog (table/json/yaml).holodeck skill add <name> --claude|--cursor|--codex|--gemini—install the skill in the agent's native format.
--globalwritesto user config;
--allinstalls every skill in the catalog;--all-agentsis a shortcut for all four agent flags.Skills are authored once as Claude Code
SKILL.mdfiles underpkg/skill/data/skills/<name>/, embedded into the binary via//go:embed. ARendererinterface translates per agent. Codexand Gemini share a single-file install path (
AGENTS.md/GEMINI.md); the install is idempotent via section markers.v1 ships one skill:
using-holodeck.Testing
t.TempDir():per-file atomic write, single-file section splice (append /
replace / preserve other sections), overwrite-prompt /
--force interaction.
/tmpdocumented in the plan.Spec / plan
docs/superpowers/specs/2026-05-14-holodeck-skill-cmd-design.mddocs/superpowers/plans/2026-05-14-holodeck-skill-cmd.mdBreaking changes
None. New command, no existing surface modified.