fix(impeccable): global-scope install with agents, pin fallback, output-read failure check
make plugin never installed impeccable. The 3.2.0 pin had rotted upstream
(the CLI fetches its skill dist at install time; that release's zip is
gone), the --scope=project staging moved the skill dir alone and dropped
the 4 impeccable-* subagents, and /impeccable init was never announced.
Step 8d now installs at --scope=global straight through the
~/.claude/{skills,agents} symlinks into the repo (both paths gitignored),
guards on those symlinks existing, keeps a profile-parked copy parked,
falls back to @latest on a pin failure with a bump-the-lock warning, and
prints the per-project init hint. update-all.sh mirrors the shape.
Found while probing: with a copy already installed a rotted pin exits 0
("Could not check for skill updates ... left unchanged"), byte-identical
on disk to an up-to-date rerun, so imp_install reads the installer output
instead of trusting the exit code. Harness 4/4 in a sandbox HOME with the
real installer.
plugins.lock.json: impeccable 3.2.0 -> 4.1.0 (CLI only). link.sh drops
impeccable from EXTERNAL_SKILLS. lib/design-gate.md section 5: suggest-only
/impeccable init check when a frontend project has no PRODUCT.md.
This commit is contained in:
+50
-19
@@ -379,11 +379,34 @@ else
|
||||
info "design-motion-principles not installed — skipping"
|
||||
fi
|
||||
|
||||
# ── Impeccable (design anti-pattern detector + skill) ──
|
||||
# ── Impeccable (design detector + skill + subagents) ──
|
||||
# Global scope: the installer writes through the ~/.claude/{skills,agents}
|
||||
# symlinks straight into this repo (install-plugins.sh Step 8d explains why
|
||||
# staging + project scope was wrong). The pin can rot upstream, so a pinned
|
||||
# failure falls back to @latest rather than leaving the tool stale forever.
|
||||
#
|
||||
# One install attempt. $1 = "latest" or an exact version. On failure, IMP_FAIL
|
||||
# holds the reason. Same helper as Step 8d: with a copy already in place a
|
||||
# rotted pin exits 0 and says "Could not check for skill updates … left
|
||||
# unchanged", so the exit code cannot tell it from an up-to-date no-op.
|
||||
imp_install() {
|
||||
local pkg="impeccable" out rc=0
|
||||
[ "$1" != "latest" ] && pkg="impeccable@$1"
|
||||
out=$(npx -y "$pkg" skills install -y --providers=claude --scope=global \
|
||||
--no-hooks 2>&1) || rc=$?
|
||||
IMP_FAIL=$(printf '%s\n' "$out" \
|
||||
| grep -E 'Download failed|Could not check for skill updates' \
|
||||
| head -1 || true)
|
||||
if [ "$rc" -ne 0 ] && [ -z "$IMP_FAIL" ]; then
|
||||
IMP_FAIL="installer exited $rc"
|
||||
fi
|
||||
[ -z "$IMP_FAIL" ]
|
||||
}
|
||||
echo ""
|
||||
echo "── Updating impeccable..."
|
||||
IMP_DIR="$REPO/skills-external/impeccable"
|
||||
if [ ! -f "$IMP_DIR/SKILL.md" ]; then
|
||||
IMP_SKILL_DIR="$HOME/.claude/skills/impeccable"
|
||||
IMP_PARKED="$REPO/skills-disabled/impeccable"
|
||||
if [ ! -f "$IMP_SKILL_DIR/SKILL.md" ] && [ ! -f "$IMP_PARKED/SKILL.md" ]; then
|
||||
info "impeccable not installed — skipping (run: make plugin)"
|
||||
else
|
||||
IMP_VER=""
|
||||
@@ -397,26 +420,34 @@ print(d.get('impeccable',{}).get('version','latest'))
|
||||
fi
|
||||
IMP_NODE=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||
if [ -z "${IMP_NODE:-}" ] || [ "$IMP_NODE" -lt 24 ]; then
|
||||
info "impeccable update skipped — needs Node >= 24 (found ${IMP_NODE:-none}); existing dist kept"
|
||||
info "impeccable update skipped — needs Node >= 24 (found ${IMP_NODE:-none}); existing copy kept"
|
||||
else
|
||||
IMP_PKG="impeccable"
|
||||
# Pin honored (LRN-077 class: a silent rules update changes audit
|
||||
# output on unchanged code) — bump the pin deliberately, then update.
|
||||
[ -n "$IMP_VER" ] && [ "$IMP_VER" != "latest" ] && IMP_PKG="impeccable@${IMP_VER}"
|
||||
IMP_STAGE=$(mktemp -d)
|
||||
if (cd "$IMP_STAGE" && npx -y "$IMP_PKG" skills install -y --providers=claude --scope=project --no-hooks >/dev/null 2>&1); then
|
||||
IMP_SRC=$(find "$IMP_STAGE" -type d -name impeccable -path "*skills*" 2>/dev/null | head -1)
|
||||
if [ -n "$IMP_SRC" ] && [ -f "$IMP_SRC/SKILL.md" ]; then
|
||||
rm -rf "$IMP_DIR"
|
||||
mv "$IMP_SRC" "$IMP_DIR"
|
||||
ok "impeccable refreshed (CLI ${IMP_VER:-latest})"
|
||||
else
|
||||
warn "impeccable: installer produced no dist — existing kept"
|
||||
IMP_WAS_PARKED=false
|
||||
[ -d "$IMP_PARKED" ] && IMP_WAS_PARKED=true
|
||||
# Pin honored (LRN-077 class: a silent rules update changes audit output
|
||||
# on unchanged code) — bump the pin deliberately, then update.
|
||||
IMP_PIN="${IMP_VER:-latest}"
|
||||
IMP_OK=false
|
||||
if imp_install "$IMP_PIN"; then
|
||||
IMP_OK=true
|
||||
elif [ "$IMP_PIN" != "latest" ]; then
|
||||
warn "impeccable@${IMP_PIN} did not install (${IMP_FAIL}) — that release's skill dist is gone upstream; trying @latest"
|
||||
if imp_install latest; then
|
||||
IMP_OK=true
|
||||
warn "refreshed from @latest, not the pin — bump \"impeccable\".version in plugins.lock.json"
|
||||
fi
|
||||
fi
|
||||
if [ "$IMP_OK" = true ] && [ -f "$IMP_SKILL_DIR/SKILL.md" ]; then
|
||||
IMP_SKILL_VER=$(sed -n 's/^version:[[:space:]]*//p' "$IMP_SKILL_DIR/SKILL.md" | head -1)
|
||||
ok "impeccable refreshed (CLI ${IMP_VER:-latest}, skill ${IMP_SKILL_VER:-?})"
|
||||
if [ "$IMP_WAS_PARKED" = true ]; then
|
||||
rm -rf "${IMP_PARKED:?}"
|
||||
mv "$IMP_SKILL_DIR" "$IMP_PARKED"
|
||||
info "impeccable was parked by a profile — refreshed copy returned to skills-disabled/"
|
||||
fi
|
||||
else
|
||||
warn "impeccable refresh failed — existing dist kept"
|
||||
warn "impeccable refresh failed (${IMP_FAIL:-no SKILL.md written}) — existing copy kept"
|
||||
fi
|
||||
rm -rf "$IMP_STAGE"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user