Split Claude plugin from agents.md CLI#56
Conversation
…ter with disk Phase 0 (bug fixes): - Fix license-header-check workflow placeholder SHA (@874b1c... -> @main). GitHub Actions could not resolve the placeholder; the workflow had never produced a single check run. Match the org-canonical pattern (~22/30 LF repos pin to @main, including the lfx-backstage scaffold template). - Add `with: copyright_line + include_files: "*.md"` to extend scanning to markdown (not in default filetypes). - Restructure all 15 SKILL.md license headers as YAML comments inside the frontmatter (line 2-3, after opening `---`). Required because the upstream workflow runs `head -4 | grep` and the previous HTML comments landed at line ~12, after frontmatter. YAML comments are silently ignored by the frontmatter parser; verified all 15 files still parse correctly with `name`, `description`, `allowed-tools` keys intact. - Add license header to README.md (top of file). - Fix wrong clone URL in 3 places: linuxfoundation/skills.git was working via GitHub redirect but is fragile; updated to linuxfoundation/lfx-skills.git in README.md (lines 8, 41) and docs/platform-install.md (line 15). Phase 1 (README/router coverage): - Update README's three skill lists (Verify autocomplete, Skill Overview table, Project Structure tree) to include all 15 skills. Previously missing: /lfx-pr-resolve, /lfx-git-setup, /lfx-snowflake-access, plus partial coverage of /lfx-cdp-snowflake-connectors. Also added the two undocumented coordinator references (fga-protected-types.md, indexed-data-types.md) and pr-resolve's evals/ directory to the tree. - Update /lfx routing table (lfx/SKILL.md) to cover the 5 unrouted skills: /lfx-pr-catchup, /lfx-git-setup, /lfx-intercom, /lfx-snowflake-access, /lfx-cdp-snowflake-connectors. Added matching Routing sections with Skill() invocation examples. /lfx-backend-builder and /lfx-ui-builder remain intentionally unrouted (invoked via /lfx-coordinator, not directly). - Add "Defer to specialized skills" section to /lfx-coordinator so it hands off Intercom and CDP-Snowflake-connector requests instead of attempting full-stack coordination on them. - Cross-link /lfx-pr-catchup → /lfx-pr-resolve in the drill-down step: if a drilled PR has unresolved comments, suggest invoking pr-resolve. - Document MCP prerequisites at the top of lfx-cdp-snowflake-connectors (lists the 5 mcp__claude_ai_LFX_BI_Layer__* tools the skill calls). Verification: - All 42 tracked .md files now have copyright in `head -4` (simulated the upstream workflow locally — it would PASS). - All 15 SKILL.md frontmatter still parses correctly via Ruby's YAML. - README's 3 lists exactly match `ls -d lfx*/`. - /lfx routing covers 12 user-facing skills (excludes builders by design). Skipped from the planned scope: - Standardizing skill descriptions with trigger phrases (deferred per reviewer direction — keep current descriptions). - .gitignore cleanup items (deferred). - install.sh "Available skills" output bug (will be made obsolete by the Phase 3 CLI rewrite). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Two of the three Copilot comments on PR #52: 1. Remove duplicate license header (the YAML frontmatter `# Copyright` from this branch + the pre-existing `<!-- Copyright -->` HTML comment after the closing `---` were both kept). Removed the redundant HTML `<!-- Copyright -->` and `<!-- SPDX-License-Identifier -->` lines across all 14 affected SKILL.md files. Kept the `<!-- Tool names ... -->` comment where present (different content, still useful as cross-platform guidance to readers). Collapsed the leftover blank lines so the post-frontmatter section stays clean. 2. Replace ambiguous `[number]` placeholder in lfx-pr-catchup's drill-down suggestion with `<pr-number>` and a concrete `#142` example to make it clear users shouldn't paste it verbatim. The third Copilot comment (SHA-pin the reusable workflow) is being addressed in a reply on the PR thread — sticking with @main per the org-canonical pattern (~22/30 LF repos including the lfx-backstage scaffold template pin to @main; supply chain risk is bounded since both repos are linuxfoundation-controlled). Verification: - All 15 SKILL.md still parse as YAML with required keys present. - All 15 still have copyright in `head -4` (license-header check passes). - No remaining `<!-- Copyright -->` HTML comments anywhere. - 13 files retain the tool-names comment (the 2 without it never had one). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Skills hardcoded ~/lf/ as the LFX clone location, locking out users
who keep their repos elsewhere. Replace with ${LFX_DEV_ROOT:-$HOME/lf}
across the three skills that scan the dev root, document the env var
in README Prerequisites, and update the test-journey usage example.
Backwards-compatible: with the var unset, behavior is identical to
the previous ~/lf/ default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
The original install.sh was a 65-line one-shot that only knew how to
symlink skills into ~/.claude/skills/. It can't probe the user's setup,
support agents.md tools alongside Claude Code, install per-repo,
diagnose problems, or persist what it did. This commit replaces it with
a structured CLI that does all of that, while keeping ./install.sh as a
thin shim so the documented entry point still works.
Architecture:
bin/lfx-skills Subcommand dispatcher and all command handlers.
install (interactive wizard + non-interactive
flags), uninstall, update, check, doctor,
list, info, config, repos, version, help.
lib/ui.sh Color-aware prompts, multi-select, confirm.
LFX_SKILLS_YES=1 short-circuits to defaults.
lib/probe.sh Pure detection: installed CLIs, ~/.claude*/
and ~/.agents*/ config dirs, dev root
candidates, lf* git repos under a dev root,
shell rc files. No side effects.
lib/platforms.sh Per-platform target paths for the 4 (platform,
scope) shapes: claude/agents x global/repo.
lib/config.sh jq-backed I/O over ~/.config/lfx-skills/
config.json (manifest of every symlink the CLI
has installed) and env.sh (exports
LFX_DEV_ROOT and prepends bin/ to PATH).
lib/symlinks.sh The 3-way create logic from the original
install.sh, lifted unchanged, plus the
eligible-skills filter that excludes
clone-only meta-skills (lfx-install,
lfx-new-skill — added in Phase 6).
lib/doctor.sh Eight diagnostic categories (symlinks,
source clone, dev root, platforms,
frontmatter, routing, MCP deps, license
headers). Records flow as pipe-delimited
lines through stdin/stdout; doctor_format_human
renders a colourised report and
doctor_format_json emits a JSON array. No
temp files.
Hard dependency on jq (universally available on dev machines, one-line
install everywhere). Targets bash 3.2+ (stock macOS) — no associative
arrays, no mapfile.
install.sh is now a 7-line shim that execs bin/lfx-skills install
"\$@", preserving the documented entry point with no behavioural
regression for existing users.
.gitignore picks up the legacy .install-manifest.json (which older
clones may have lying around) and leaves a comment about the negation
block Phase 6 will need under .claude/skills/.
The doctor's --fix flag is intentionally narrow: it handles the
mechanical repairs only (recreate a symlink from the manifest, write
env.sh from current config). Content-level issues (a skill directory
with no SKILL.md) are flagged as fixable=no for the CLI but stay in
the JSON output for the upcoming Phase 4 /lfx-doctor skill, which can
exercise judgment the bash CLI can't.
Spot-tested end-to-end on an isolated tmp env: install of all 15
skills across both global and per-repo targets for both platforms
produces 90 manifest entries, doctor reports 0 errors, --fix recreates
a deliberately-broken symlink, uninstall cleans up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Two new conversational skills layered on top of the lfx-skills CLI.
Both ship to every install location alongside the user-facing skills
(only /lfx-install and /lfx-new-skill remain clone-only — those land
in Phase 6).
/lfx-doctor — diagnostic surface
Wraps `lfx-skills doctor --json` with a conversational fix flow:
parse the JSON, render a human report grouped by severity, ask the
user per-issue whether to apply the available auto-fix, then
re-verify. For content-level issues that the bash CLI can't repair
(missing SKILL.md, license header gaps, routing-table coverage,
draft-able description fields), the skill applies judgment the CLI
can't — reading files, drafting fixes, asking before editing.
references/fix-recipes.md gives per-issue narrative for every
doctor check id, with copy-pasteable templates for the recipes the
/lfx-doctor skill walks through.
/lfx-skills-helper — CLI front-end
Conversational layer over the management subcommands: install,
uninstall, update, list, info, config, repos. Maps natural-language
management intents to CLI invocations, confirms before any
state-changing command, and reformats CLI output for human reading.
Strict separation of responsibilities:
- "Which skill should I use for X?" → hand off to /lfx (the
plain-language router is the right surface; the helper does
not invent recommendations).
- Diagnostics → hand off to /lfx-doctor.
- Authoring a new skill → hand off to /lfx-new-skill (Phase 6).
references/intents.md documents the intent → CLI mapping for the
management surface only. No backend/frontend/data taxonomy: that
distinction isn't in the skill metadata, and forcing one on the
user would mislead them about what each skill actually does.
The CLI's lfx-skills list --available now picks both skills up
automatically (eligible-skills filter checks for SKILL.md existence
and excludes only the clone-only meta-skills).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
The "fresh-clone, what now?" UX was the missing piece. After this commit
a user can clone the repo, open Claude Code (or Codex / Gemini CLI /
OpenCode via AGENTS.md) inside the clone, and ask for help installing,
diagnosing, managing, or authoring skills — without anything being
installed yet. The four meta-skills are auto-discovered via committed
relative symlinks under .claude/skills/ and .agents/skills/.
New skill directories:
lfx-install/SKILL.md
Conversational wrapper over the install wizard. Asks platform,
scope, dev root, config dirs, repos in plain language; composes
the non-interactive bin/lfx-skills install invocation; verifies
via doctor; prints the rc snippet for the user to add manually.
Strictly clone-only: never gets installed elsewhere.
lfx-new-skill/SKILL.md + references/conventions-quickref.md
Scaffolds a new lfx-<name>/ directory with valid frontmatter, the
YAML license-header lines on rows 2-3, optional references/, then
chains into `lfx-skills update` and `lfx-skills doctor` so the
contributor can try /lfx-<name> immediately. Also clone-only.
Auto-discovery wiring:
.claude/skills/{lfx-install,lfx-new-skill,lfx-doctor,lfx-skills-helper}
.agents/skills/{lfx-install,lfx-new-skill,lfx-doctor,lfx-skills-helper}
Eight committed relative symlinks (../../lfx-foo). Survive any
`git clone <repo> <any-path>` because they're relative.
CLAUDE.md and AGENTS.md
Repo-root agent guidance pointing at the four meta-skills, when
to use which, and the layout. Same content; CLAUDE.md is for
Claude Code's discovery, AGENTS.md for the agentskills.io
ecosystem (Codex, Gemini CLI, OpenCode).
.gitignore negation block:
The repo's `.claude` was previously fully gitignored, which would
block the four committed symlinks. Switched to `.claude/*` (the
trailing-slash variant prevents negation) and added explicit
un-ignore rules for the four meta-skill paths. Mirror block for
.agents/. Other Claude/agents local state (settings.local.json,
per-repo install symlinks created by the CLI in user repos) remains
ignored.
IFS-leak bug fix in cmd_install / cmd_uninstall:
Three sites used `local IFS=','` to split comma-separated flag
values. Bash's `local` is dynamically scoped, so the changed IFS
leaked into nested calls — including `for ex in $SYMLINKS_CLONE_ONLY`
inside symlinks_eligible_skills, which then failed to split the
exclusion list on spaces and never excluded anything. Result:
`lfx-skills install` symlinked lfx-install and lfx-new-skill into
every target, defeating the "clone-only" boundary.
Replaced `local IFS=','; for x in $var` with
`while IFS= read -r x; do ... done < <(printf '%s\n' "$var" | tr ',' '\n')`.
The trailing `\n` on printf is load-bearing: without it, `read`
would silently drop the last comma-separated value because it had
no terminator.
Caught by the Phase 6 fresh-clone integration test, which expected
17 symlinks per target and got 19.
End-to-end verification: 7-target install (2 Claude config dirs + 1
agents + 2 repos × 2 platforms) creates 119 manifest entries
(7 × 17), with lfx-install and lfx-new-skill correctly excluded
from every target.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
…cope
Three cleanups landed after the final review pass:
1. Drop the MCP-deps doctor category entirely.
Per direct user instruction: remove the mcp_deps check from doctor
and any references to it. The category was a false-positive magnet
(any skill that even MENTIONED an mcp__* tool name in prose got
flagged), and there's no clear v1 contract for the inverse: a skill
genuinely depending on an MCP server is the user's setup problem,
not a doctor concern. Removed:
- check_mcp_deps() function and its call in doctor_run_all
- the mcp-undocumented row from /lfx-doctor's per-issue table
- the mcp-undocumented entry from references/fix-recipes.md
- the "doctor warns" note in /lfx-new-skill (kept the recommendation
to write a Prerequisites section — that's still good practice,
it's just not enforced)
Doctor is now 7 categories instead of 8 (help text updated).
2. Routing-uncovered: skip management surfaces and internal builders.
The check used to flag /lfx-doctor and /lfx-skills-helper for not
being in /lfx's routing table — but they're management surfaces
reached by name, not via plain-language routing. Same for
/lfx-backend-builder and /lfx-ui-builder, which are only invoked
internally by /lfx-coordinator. Added a routing_exempt list in
check_routing covering all four.
3. README autocomplete sample.
The skill list under "Restart and verify" omitted /lfx-doctor and
/lfx-skills-helper. Both auto-install globally as of this PR, so
they'll appear in users' autocomplete; added them with one-line
labels so the list is accurate.
Doctor on the working tree post-fix: 19 pass / 6 warn / 0 fail
(remaining warnings are real signal — uncommitted clone, env not
sourced in current shell, etc.).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
… TUI
The numbered comma-separated multiselect was bad UX: the user had to
read a numbered list, decide which numbers to type, and rely on a
free-text prompt that could mis-parse. Replaced with a real
checkbox menu navigated by arrow keys.
New helper in lib/ui.sh:
ui_checkbox_select PROMPT DEFAULT_SPEC MIN_REQUIRED OPT1 OPT2 ...
Renders an interactive checkbox menu on stderr; emits selected options
on stdout (one per line). Navigation: ↑/↓ (or k/j) to move, space to
toggle, enter to confirm, q or Esc to cancel. The cursor wraps top-to-
bottom. Confirm is refused if fewer than MIN_REQUIRED items are
selected (with an inline warning).
Implementation notes:
- Bash 3.2 compatible (no associative arrays, no mapfile, no
process-substitution-only idioms in the inner loop).
- Saves stty state up-front and restores via trap on EXIT/INT/TERM
so a Ctrl-C never leaves the user's terminal in raw mode with the
cursor hidden.
- Reads keys one byte at a time via `read -rsn1`; arrow keys are
detected as ESC '[' 'A'/'B' with a 50ms timeout to distinguish a
plain Esc keypress from an arrow-key escape sequence.
- Falls back to ui_multiselect (the legacy numbered prompt) when
stdin or stderr is not a TTY (CI, piped input).
- Honours LFX_SKILLS_YES=1 by emitting the resolved defaults
immediately, no rendering.
Wired into all four multiselect sites in cmd_install:
Platform: 2 options ("Claude Code", "agents.md (Codex / Gemini CLI
/ OpenCode)"), prompt "Which AI platform(s)? (select one or both)".
The old 3-option "claude / agents / both" was replaced — "both" was
just selecting both checkboxes.
Scope: 2 options ("global", "per-repo"), prompt "Install scope?
(select one or both)". Same simplification.
Claude config dirs: previously defaulted to "1" or asked for
comma-separated numbers when more than one was probed; now shows
a checkbox per dir.
Repo picker: previously defaulted to "all" with comma-separated
numbers; now a checkbox per lf* git repo found under LFX_DEV_ROOT.
Internal refactor: cmd_install no longer threads $platform/$scope as
single-value enums. Both can now be comma-joined ("claude,agents",
"global,repo"). Compute boolean include_claude / include_agents /
include_global / include_repo flags once after the picker, use those
throughout the apply phase. The `--platform=both` and `--scope=both`
flags still work as syntactic sugar for the comma-joined form.
Manifest schema: agents.config_dir (singular) → agents.config_dirs
(array), mirroring claude. The doctor's check_platforms iterates the
array. This is a schema change from the v1-shape spec but lands in v1
since nothing has been released yet.
Cancel handling: each picker returns an empty list on Esc/q. The
caller checks `${#_picked[@]}` post-loop — process substitution
exit codes don't propagate, so a `||` after `< <(picker)` would not
fire. Caught during testing.
End-to-end verified non-interactively:
- --platform=both --scope=global with claude+agents multi-config
(4 dirs total) creates 68 symlinks, manifest has parallel
config_dirs arrays
- --platform=both legacy sugar still works
- --agents-config=a,b multi-value parses correctly
- --yes skips all pickers and uses defaults
The TUI itself is interactive-only; nothing in this change affects
LFX_SKILLS_YES=1 or the piped-input fallback paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
…compat) Bash 3.2 (stock macOS) rejects fractional `read -t` values with "invalid timeout specification". Arrow-key navigation in the new checkbox TUI was bombing out at the first keystroke as a result. Fixed by using `-t 1` instead of `-t 0.05`. Arrow keys still respond instantly because the terminal delivers the full escape sequence in one burst — the read returns as soon as the next two bytes are in the buffer (microseconds, not the full second). Only a bare Esc keypress now has a one-second debounce before registering as cancel; users have `q` as an instant alternative. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Three real issues from interactive testing: 1. Items came pre-selected, which was confusing. The user expected to start from a blank slate and explicitly opt in. 2. Enter confirmed the picker outright — the user expected enter on a checkbox to TOGGLE that item. There was no separate way to explicitly proceed. 3. As a knock-on of #2: pressing enter on the per-repo line confirmed the picker with whatever was pre-selected (i.e., global), so the wizard then asked for global config dirs even when the user had meant to switch to per-repo. Redesign: - Interactive path starts with EVERY checkbox unchecked. The user must explicitly toggle what they want. - Enter (and space) on a checkbox toggles that item. Neither confirms. - A new [continue] line appears at the bottom of every picker. Enter on it proceeds (refused with an inline warning if fewer than MIN_REQUIRED items are checked). - Added 'a' / 'n' shortcuts for select-all / clear-all. Helpful when the picker has 8+ items (e.g. 8 lf* repos under LFX_DEV_ROOT). - Added blank lines between wizard steps so the flow has visual breathing room. YES_DEFAULT_SPEC parameter (renamed from DEFAULT_SPEC to make intent explicit) now ONLY governs the LFX_SKILLS_YES=1 auto-pick path. Interactive callers ignore it. Non-interactive install / CI behaviour is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
The install summary previously showed each per-skill line as
'installed <name> → <source-path>' with no indication of which target
directory the symlink was being created in. When the user picked
multiple targets (e.g. claude + agents on the same repo), the same
17 skill names appeared twice in a row and looked like the loop had
double-run.
Three changes to make the structure obvious:
1. Per-skill output (lib/symlinks.sh) drops the source path. The
source is always the canonical clone — repeating it on every line
adds noise. Lines now read 'installed lfx-coordinator'.
2. Apply phase (cmd_install) prints a header before each
symlinks_install_all call:
==> Claude · per-repo · /path/to/repo/.claude/skills/
installed lfx-coordinator
...
so the per-skill lines have unambiguous context.
3. Preview shows the real symlink count, not just the count of unique
skills. With 17 unique skills, 1 repo, and platform=both, the
preview now reads:
Skills: 17 unique
Targets: 2
Symlinks: 34 total (skills × targets)
instead of the misleading 'Skills to install: 17'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
…work
Mirrors the pattern Claude Code and Codex use: each tool owns its own
config dir under $HOME and reads from it directly. The user never has
to teach their shell about LFX.
Previously, the install ended with a "Next steps" instruction to add a
sourcing line to ~/.zshrc so that LFX_DEV_ROOT and the lfx-skills CLI
would be on PATH. That had two problems:
1. It was annoying — most users skipped it and then wondered why
skills couldn't find their repos.
2. It was fragile — env from shell rc only flows to Claude Code if
Claude was launched from a terminal. GUI launches (Spotlight,
Dock) don't source the rc, so LFX_DEV_ROOT wasn't there even
when the user had done the rc edit.
New architecture:
- env.sh becomes idempotent. Uses `: "${LFX_DEV_ROOT:=...}"` instead
of `export LFX_DEV_ROOT=...`, so an existing user-set value wins
and the file is safe to source repeatedly.
- The 3 skills that use LFX_DEV_ROOT (test-journey, coordinator,
research) source ~/.config/lfx-skills/env.sh themselves at the top
of their bash blocks. The user's shell is never involved.
- Install "Next steps" simplified to "restart your AI tool, type /lfx".
The rc-edit instruction is demoted to an optional follow-up for
users who want to type `lfx-skills` directly from a terminal
(rather than via /lfx-skills-helper from inside the AI tool).
- Verified: skills find the configured dev root in a clean shell with
no LFX_DEV_ROOT set; user override (`export LFX_DEV_ROOT=...`)
still wins.
Same pattern Claude Code uses with `~/.claude/`: the tool reads from
its own config dir, the shell stays out of it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Replaces the env.sh-sourced-from-shell-rc approach with two cleaner
mechanisms that match what other dev tools (pipx, gh, brew) actually
do:
1. ~/.lfx-skills/dev-root — single-line text file the 3 dev-root-aware
skills read with `cat` to resolve LFX_DEV_ROOT. No env vars, no
shell rc edits, no per-bash-block sourcing boilerplate.
2. ~/.local/bin/lfx-skills (or ~/bin, /opt/homebrew/bin, /usr/local/bin)
— symlink the installer creates so `lfx-skills` is on PATH from
any terminal. Probe picks the best writable PATH dir:
~/.local/bin → ~/bin → /opt/homebrew/bin → /usr/local/bin
/opt/homebrew/bin specifically added per audit — Apple Silicon
stock setup has no other user-writable PATH dir by default.
env.sh, write_env_sh(), env_sh_path(), and rc_snippet() are gone.
The doctor's env-sh-missing / env-sh-not-sourced / dev-root-not-in-
session / dev-root-session-drift checks are gone — they were measuring
something that no longer matters. New checks: dev-root-file-missing
(fail; CLI auto-fixes) and dev-root-file-mismatch (warn; CLI rewrites
from manifest).
Safety holes plugged after a focused review subagent pass:
- install_cli_symlink REFUSES to overwrite an existing symlink that
doesn't already point into the canonical clone. Previously it
silently `rm`-ed any pre-existing link, which would have wiped a
user's own lfx-skills script. Now: idempotent re-install of our
own link is fine; another lfx-skills clone gets a loud warning;
any other foreign symlink is refused with instructions.
- remove_cli_symlink refuses to act when canonical_clone is empty
in the config (corrupted manifest). Previously the empty-prefix
strip silently passed the safety check.
- POSIX-correct quoting in `${actual#"$clone/"}` — protects against
glob interpretation if a path ever contains * or ?.
- Captured-stdout fix: install call no longer redirects stderr into
the path variable, so warnings can flow to the user without
contaminating the recorded value.
- config_set_cli_symlink failure is detected and reported (was set -e
fall-through previously).
CLI-on-PATH is now installed by default; --no-cli-symlink opts out.
If the probe finds no writable PATH dir, the install prints a
copy-pasteable alias snippet pointing at <clone>/bin/lfx-skills, so
the user always has a one-line escape hatch.
End-to-end verified: foreign symlink survives a fresh install (with
warn + alias fallback); corrupt-config uninstall refuses to remove
the CLI symlink; multi-clone clobber emits "Replacing existing
symlink…" warning instead of silent overwrite.
Doc cascade: CLAUDE.md, AGENTS.md, lfx-doctor/SKILL.md,
lfx-doctor/references/fix-recipes.md, lfx-install/SKILL.md,
lfx-skills-helper/SKILL.md, and the 3 LFX_DEV_ROOT-using skills
(test-journey, coordinator, research) all updated to point at the
new shape.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
andrest50
left a comment
There was a problem hiding this comment.
The PR looks good. Great job streamlining the installation process skills in not only Claude Code but other agents - it's definitely a big improvement! Please fix the merge conflicts and then I'll re-approve it if needed.
dealako
left a comment
There was a problem hiding this comment.
Hey @josep-reyero — thanks for this. It's a big-looking diff, but you called it: most of the churn is the skills/ reorg, and the real new surface is the ~2,400-line bash CLI under cli/lfx-skills + lib/*.sh. I reviewed it with a security pass and a code-quality pass running in parallel alongside my own read, and consolidated everything below.
Overall impression: This is genuinely careful bash. The bash-3.2 discipline is real (no associative arrays, no mapfile, integer-only read -t, the FD-3 stdin trick for doctor --fix), the safety-critical symlink logic consistently refuses to clobber non-symlinks or foreign targets before any rm, and path-with-spaces / jq --arg quoting is right almost everywhere. The security pass found no critical or high-severity issues — every symlink/CLI-symlink removal verifies readlink against an owned source first. The findings below are robustness and polish, plus one process gate.
Security reconciliation
The security audit surfaced only low-severity items, all of which I've folded in: the cmd_list jq string-interpolation (inline above), predictable temp-file names (folded into the config temp-file comment), and the workflow @main pin (acknowledged below). No unresolved critical findings — the approval gate is clear on the security side.
AI / prior-review reconciliation
No CodeRabbit, Cursor, or Copilot comments exist on this PR to reconcile (the earlier Copilot round was on #52 and its two actionable items were already addressed in commit 65fc245). One prior human review: @andrest50 approved on 2026-05-11. I'm not seeing regressions versus that state — my notes are additive.
Issue count
- 🔴 Blocking: 0 code defects. (See the merge-state note below — that is what blocks merge.)
- 🟡 Minor: 5
lib/symlinks.sh—failedoutcome silently dropped from the install summarycli/lfx-skills—uninstalldoesn't validate--scope(--scope=bothremoves nothing yet prints success)lib/ui.sh— Ctrl-C restores the terminal but doesn't actually cancel the menulib/config.sh— manifest temp-file leaks on failure underset -e; predictable namecli/lfx-skills—cmd_listsplices flag values into the jq program instead of--arg
- ⚪ Nit: 4
lib/symlinks.sh:331— dead:no-op + misleading commentcli/lfx-skills:905—grep -qxtreats the name as a regex (use-qxF)lib/doctor.sh—_emituses a|-delimited / newline record format; a symlink target path containing|or a newline could shift the--jsonfields (low-likelihood data-integrity, not a security boundary).github/workflows/license-header-check.yml:15— reusable workflow pinned to mutable@main. You've already documented the org-canonical rationale (~22/30 LF repos pin@main, same-org source,pull_requesttrigger so no fork-secret exposure) in77998b6— flagging only for completeness; fine to leave as-is.
Recommendation (not blocking)
There are no automated tests for ~1,500 lines of state-mutating bash (symlink create/remove, manifest JSON I/O, CLI-symlink-on-PATH) — exactly the \u201crefuse to clobber\u201d logic that most benefits from regression coverage. The code is already structured for it (pure probe_*, env-overridable config paths, a text-record doctor pipeline), so a small bats suite over a temp HOME/LFX_SKILLS_CONFIG_DIR sandbox would be high-value as a fast follow-up. I'm not gating the PR on it given the repo has no harness today, but it's the highest-leverage next step.
Merge state
gh reports this PR as CONFLICTING against main. That has to be resolved before merge regardless of the review.
Decision
🔴 Needs changes before approval — driven by the merge conflict, not by any code defect. Please rebase/resolve against main; the 5 minors are worth a quick pass while you're in there. Once conflicts are cleared, none of the remaining items are approval-blockers and I'm happy to re-review and approve. Nice work on this.
| skipped) | ||
| n_skipped=$((n_skipped + 1)) | ||
| ;; | ||
| esac |
There was a problem hiding this comment.
[minor] Correctness — failed outcome is silently dropped.
symlinks_create_one can echo failed (lib/symlinks.sh:80-81, 91-92 — e.g. an ln -s error), but this case only handles installed|updated|skipped. A failure still increments n_total (line 146) with no sub-bucket, so the install summary under-counts and the failure never surfaces in the installed/updated/skipped/total line. Add a failed) arm that warns (and ideally a *) catch-all):
failed)
n_failed=$((n_failed + 1))
;;
*) ui_warn " unknown outcome '$outcome' for $skill" ;;| while [ $# -gt 0 ]; do | ||
| case "$1" in | ||
| --yes) LFX_SKILLS_YES=1; export LFX_SKILLS_YES ;; | ||
| --scope=*) scope="${1#*=}" ;; |
There was a problem hiding this comment.
[minor] Correctness — uninstall never validates --scope.
cmd_install validates/desugars scope (both → global,repo, rejects unknown values at cli/lfx-skills:514-521), but cmd_uninstall passes $scope straight through to symlinks_uninstall_all → config_list_symlinks (lib/config.sh:169-178), whose if/elif chain silently matches nothing for any value other than global/repo/empty. So lfx-skills uninstall --scope=both (a natural thing to type, since install accepts it) removes 0 symlinks and still prints “Uninstall complete.” Validate scope here the same way install does, or desugar both.
| # Save terminal state and ensure restore on any exit. | ||
| local _stty_saved | ||
| _stty_saved="$(stty -g 2>/dev/null || true)" | ||
| trap 'stty '"'$_stty_saved'"' 2>/dev/null; printf "\033[?25h" >&2' EXIT INT TERM |
There was a problem hiding this comment.
[minor] Correctness — Ctrl-C does not actually cancel the menu.
The INT/TERM trap restores stty and re-shows the cursor, but after the handler returns bash resumes the read loop, so pressing Ctrl-C mid-selection shows the cursor reappear while the menu keeps running. Split the handler so INT/TERM also exit:
_ui_cb_restore() { [ -n "$_stty_saved" ] && stty "$_stty_saved" 2>/dev/null; printf '\033[?25h' >&2; }
trap '_ui_cb_restore' EXIT
trap '_ui_cb_restore; exit 130' INT TERMThis also fixes the fragile pattern of baking $_stty_saved into the trap string (a single quote in the value would break out of the quoting).
| local cfg now tmp | ||
| cfg="$(config_json_path)" | ||
| now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | ||
| tmp="$cfg.tmp.$$" |
There was a problem hiding this comment.
[minor] Robustness — temp file leaks on failure + predictable name.
This (and the same pattern at lines ~104, 125, 156, 207) uses tmp="$cfg.tmp.$$" then jq ... > "$tmp" && mv "$tmp" "$cfg". If jq fails, or mv fails, $tmp is left behind and — because this runs under the caller's set -e — the function aborts without cleanup. This is the single source of truth for the install manifest, so it's worth hardening:
local tmp; tmp="$(mktemp "$cfg.XXXXXX")"
trap 'rm -f "$tmp"' RETURN
jq ... > "$tmp" && mv "$tmp" "$cfg"mktemp also removes the predictable-name race if LFX_SKILLS_CONFIG_DIR is ever pointed somewhere shared.
| jq_filter="$jq_filter and .repo == \"$repo\"" | ||
| fi | ||
| fi | ||
| config_read | jq -r --argjson _ 0 "(.symlinks // [])[] | select($jq_filter) | \"\(.scope)\t\(.skill)\t\(.link)\"" | sort -u |
There was a problem hiding this comment.
[minor] Robustness — cmd_list interpolates flag values into the jq program.
jq_filter is string-built from $scope/$repo (lines 319-324) and spliced into the jq program here, rather than passed via --arg the way cmd_info/config_* correctly do. It's not a real exploit (local user, own flags — jq can't exec a shell), but a value containing " silently breaks the filter. Prefer:
config_read | jq -r --arg scope "$scope" --arg repo "$repo" '
(.symlinks // [])[]
| select(($scope == "") or (.scope == $scope))
| select(($repo == "") or (.repo == $repo))
| "\(.scope)\t\(.skill)\t\(.link)"' | sort -u| # user-managed, from another checkout, or from another tool. | ||
| local actual; actual="$(readlink "$target" 2>/dev/null || true)" | ||
| if [ "$actual" = "$source" ]; then | ||
| : # already correct — fall through to ln (will recreate identically) |
There was a problem hiding this comment.
[nit] Dead no-op + misleading comment. The : does nothing; only rm "$target" matters, and the comment says “fall through to ln (will recreate identically)” when the code actually removes-then-recreates. Drop the : and reword the comment.
| installed_set="$(config_read | jq -r '(.symlinks // [])[] | .skill' | sort -u)" | ||
| new_skills="" | ||
| while IFS= read -r skill; do | ||
| if ! printf '%s\n' "$installed_set" | grep -qx "$skill"; then |
There was a problem hiding this comment.
[nit] grep -qx "$skill" treats the skill name as a regex. Harmless today (names are lfx-[a-z0-9-]+), but a . would match unintendedly. Use grep -qxF "$skill" for a literal compare.
|
Made obsolete by PR #59 |
Summary
This PR looks large in the file diff, but most of the churn is structural: the CLI work adds agents.md support, and the published LFX Skills suite is organized under
skills/.This PR splits LFX Skills into two clear distribution paths:
It also makes the repo helper skills available locally from this repository so contributors can work immediately after cloning, without first installing the runtime suite globally or into another repo.
The README has the detailed install and authoring guidance.
Claude Code Plugin
Once merged, Claude Code users install from the in-repo marketplace:
The marketplace lives at
.claude-plugin/marketplace.jsonand points to the plugin in this same repo with:The plugin is skills-only:
.claude-plugin/plugin.jsonexposes the runtime suite with"skills": ["./skills/"].skills/is part of the published LFX Skills suite.lfx-skillsCLI, as not needed.Versioning is intentionally simple for now:
.claude-plugin/plugin.jsonowns the pluginversion..claude-plugin/plugin.json.Legacy Claude symlink installs are handled separately. Users who previously installed LFX Skills into Claude Code via symlinks can remove only those old links with:
agents.md Support
agents.md-compatible tools use the
lfx-skillsCLI:The setup is agent-first: after cloning, users can start their coding agent in this repo and ask it to set up LFX Skills. The README covers the manual fallback too.
The CLI now focuses on agents.md installs only:
skills//lfx-skills-doctorand/lfx-skills-helperfrom.agents/skills//lfx-installand/lfx-new-skilllocal to this source repo~/.lfx-skills/config.json~/.lfx-skills/dev-rootlfx-skillsCLI symlink onPATHwhen possibleAfter installation, users update agents.md installs with:
Contributing
New LFX Skills suite skills should be created from this repo with
/lfx-new-skilland placed underskills/lfx-<name>/.For a new suite skill or plugin-visible behavior change, contributors should:
skills/.claude-plugin/plugin.jsonversionwhen Claude Code users need to receive the changeclaude plugin validate .git commit -s -SFor agents.md installs, contributors can test by running:
Dev Root Support
Both distribution paths support the same LFX dev root convention for skills that need to find local LFX repositories.
~/.lfx-skills/dev-rootthrough the CLI installer.~/.lfx-skills/dev-rootfile when present.~/lfand can ask the user whether to set a different path.Repo-Local Helper Skills
The repo now includes committed helper skills under both:
.agents/skills/.claude/skills/These make the repo helpers available out of the box when working inside this repository:
/lfx-install/lfx-skills-doctor/lfx-skills-helper/lfx-new-skillThese helpers are for working on and contributing to this repository locally. They are not part of the published LFX Skills suite.