Skip to content

Split Claude plugin from agents.md CLI#56

Closed
josep-reyero wants to merge 21 commits into
mainfrom
feat/claude-plugin-and-cli
Closed

Split Claude plugin from agents.md CLI#56
josep-reyero wants to merge 21 commits into
mainfrom
feat/claude-plugin-and-cli

Conversation

@josep-reyero

@josep-reyero josep-reyero commented May 6, 2026

Copy link
Copy Markdown
Contributor

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:

  • Claude Code plugin for Claude Code users.
  • agents.md CLI install for Codex, Gemini CLI, OpenCode, and other agents.md-compatible tools.

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:

/plugin marketplace add linuxfoundation/lfx-skills
/plugin install lfx-skills@lfx-skills

The marketplace lives at .claude-plugin/marketplace.json and points to the plugin in this same repo with:

"source": "./"

The plugin is skills-only:

  • .claude-plugin/plugin.json exposes the runtime suite with "skills": ["./skills/"].
  • Everything under skills/ is part of the published LFX Skills suite.
  • The plugin does not install or use the lfx-skills CLI, as not needed.

Versioning is intentionally simple for now:

  • .claude-plugin/plugin.json owns the plugin version.
  • Claude Code uses that version for cache/update detection.
  • If plugin content changes but the version does not change, Claude keeps using the cached plugin.
  • Every Claude-facing plugin change should include a SemVer bump in .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:

lfx-skills uninstall --legacy-claude-only

agents.md Support

agents.md-compatible tools use the lfx-skills CLI:

./install.sh

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:

  • installs runtime skill symlinks from skills/
  • installs /lfx-skills-doctor and /lfx-skills-helper from .agents/skills/
  • keeps /lfx-install and /lfx-new-skill local to this source repo
  • records the install in ~/.lfx-skills/config.json
  • records the dev root in ~/.lfx-skills/dev-root
  • installs a safe lfx-skills CLI symlink on PATH when possible
  • removes agents.md installs and legacy Claude symlinks with explicit uninstall modes

After installation, users update agents.md installs with:

lfx-skills update --pull
lfx-skills doctor

Contributing

New LFX Skills suite skills should be created from this repo with /lfx-new-skill and placed under skills/lfx-<name>/.

For a new suite skill or plugin-visible behavior change, contributors should:

  • create or update the skill under skills/
  • bump .claude-plugin/plugin.json version when Claude Code users need to receive the change
  • validate locally with claude plugin validate .
  • commit with DCO and cryptographic signing: git commit -s -S
  • avoid co-author trailers

For agents.md installs, contributors can test by running:

./cli/lfx-skills update
./cli/lfx-skills doctor

Dev Root Support

Both distribution paths support the same LFX dev root convention for skills that need to find local LFX repositories.

  • agents.md installs set and update ~/.lfx-skills/dev-root through the CLI installer.
  • Claude Code plugin skills can read the same ~/.lfx-skills/dev-root file when present.
  • If no dev root is configured, dev-root-aware skills fall back to ~/lf and 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-skill

These helpers are for working on and contributing to this repository locally. They are not part of the published LFX Skills suite.

josep-reyero and others added 15 commits May 4, 2026 14:31
…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>
Copilot AI review requested due to automatic review settings May 6, 2026 15:52
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>
@josep-reyero josep-reyero marked this pull request as ready for review May 7, 2026 10:50
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>
Signed-off-by: Josep Garcia-Reyero Sais <josepreyero@gmail.com>

@andrest50 andrest50 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dealako left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.shfailed outcome silently dropped from the install summary
    • cli/lfx-skillsuninstall doesn't validate --scope (--scope=both removes nothing yet prints success)
    • lib/ui.sh — Ctrl-C restores the terminal but doesn't actually cancel the menu
    • lib/config.sh — manifest temp-file leaks on failure under set -e; predictable name
    • cli/lfx-skillscmd_list splices flag values into the jq program instead of --arg
  • Nit: 4
    • lib/symlinks.sh:331 — dead : no-op + misleading comment
    • cli/lfx-skills:905grep -qx treats the name as a regex (use -qxF)
    • lib/doctor.sh_emit uses a |-delimited / newline record format; a symlink target path containing | or a newline could shift the --json fields (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_request trigger so no fork-secret exposure) in 77998b6 — 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.

Comment thread lib/symlinks.sh
skipped)
n_skipped=$((n_skipped + 1))
;;
esac

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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" ;;

Comment thread cli/lfx-skills
while [ $# -gt 0 ]; do
case "$1" in
--yes) LFX_SKILLS_YES=1; export LFX_SKILLS_YES ;;
--scope=*) scope="${1#*=}" ;;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Correctness — uninstall never validates --scope.

cmd_install validates/desugars scope (bothglobal,repo, rejects unknown values at cli/lfx-skills:514-521), but cmd_uninstall passes $scope straight through to symlinks_uninstall_allconfig_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.

Comment thread lib/ui.sh
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 TERM

This 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).

Comment thread lib/config.sh
local cfg now tmp
cfg="$(config_json_path)"
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
tmp="$cfg.tmp.$$"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment thread cli/lfx-skills
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment thread lib/symlinks.sh
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment thread cli/lfx-skills
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@josep-reyero

Copy link
Copy Markdown
Contributor Author

Made obsolete by PR #59

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants