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:
bastien
2026-09-22 03:20:25 +00:00
parent 7c05f75eab
commit cc2f246e65
7 changed files with 221 additions and 61 deletions
+50 -19
View File
@@ -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