Skip to content

Commit 2163aac

Browse files
committed
feat: ✨ Add cross-surface skill installation for workbench plugins.
Workbench-core now auto-discovers installable skills in dependent @claude-workbench plugins (excluding itself) and surfaces a notice in the SessionStart warmup output when new or updated skills are detected. Users click the highlighted /workbench-core:install-chat-skills slash command to package each skill via skill-creator's package_skill.py and open the .skill files with the Mac app, which handles the install dialog. Skill state is recorded in ~/.claude-workbench/chat-skills-state.json so the notice clears once installed. Detection is engineered to be free in steady state: the SessionStart hook gates to source=startup, then uses a state-file mtime fast-path — if nothing has changed since the last run, the check is a single stat and exits before any JSON parsing. The cold path (actual plugin install/update) is rare and runs synchronously since it only fires when plugins have actually changed. Skills are filtered to those with a `name:` field in frontmatter (required by skill-creator's validator). Workbench-core's own skills are excluded by convention — most depend on Claude-Code-specific infra (session logs, agent dispatch) that doesn't exist in Chat. Adding opt-in support is a v2 concern. Requires the skill-creator@claude-plugins-official plugin and pyyaml; the script will install pyyaml via pip3 --break-system-packages if missing, and prints an install command for skill-creator if absent.
1 parent 16e859c commit 2163aac

5 files changed

Lines changed: 292 additions & 1 deletion

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "workbench-core",
33
"description": "Core infrastructure for Claude Code: persistent agent identity, session lifecycle hooks, operational memory, and meta skills. Provides a customizable framework where users configure the agent name, memory path, and persona files.",
4-
"version": "0.2.0",
4+
"version": "0.3.0",
55
"author": {
66
"name": "Mike Bronner",
77
"url": "https://github.com/mike-bronner"

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,30 @@ Runs on every `startup` warmup:
332332
| `/workbench:summarize-session` | Manually summarize a specific session (or pick from unsummarized) |
333333
| `/workbench:process-pending-summaries` | Dispatch background agents to clear pending summary markers |
334334
| `/workbench:compact-learnings` | Review and compact accumulated skill learnings; integrate into SKILL.md for workbench skills |
335+
| `/workbench-core:install-chat-skills` | Discover skills in `@claude-workbench` plugins and install them into the Claude Mac app's Chat surface via `.skill` packaging |
335336

336337
All skills are **execution-aware** — they check for a `skills/{name}.learnings.md` file in the vault before running and apply any accumulated learnings from prior executions.
337338

339+
### Cross-surface skill installation
340+
341+
workbench-core auto-discovers installable skills in dependent `@claude-workbench` plugins and surfaces a notice in the SessionStart warmup output when new or updated skills are available:
342+
343+
```
344+
## 📦 New Chat-installable skills
345+
346+
The following skills can be installed into Claude Chat (Mac app):
347+
- `develop` (from `workbench-dev-team`)
348+
- `git-commit` (from `workbench-dev-team`)
349+
350+
Click to install: `/workbench-core:install-chat-skills`
351+
```
352+
353+
The detection runs once per session start (`source: startup` only) and uses a state-file mtime fast-path — when nothing has changed since the last run, the check is a single stat. The cold path triggers only after `claude plugin install/update` actually changes `installed_plugins.json`.
354+
355+
The slash command (`/workbench-core:install-chat-skills`) packages each eligible skill via `skill-creator`'s `package_skill.py`, opens the resulting `.skill` files with the Mac app, and updates `~/.claude-workbench/chat-skills-state.json` so the notice clears. Requires the `skill-creator@claude-plugins-official` plugin (the script will tell you to install it if missing).
356+
357+
The notice persists until the user installs — if you ignore it once, it'll appear again on the next session start. Skipping a skill in the install dialog has the same effect.
358+
338359
## Environment variable overrides
339360

340361
All config values can be overridden via environment variables for testing:

hooks/session-warmup.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,72 @@ SYSEOF
9292
mv "$tmp" "$target"
9393
}
9494

95+
detect_chat_skill_changes() {
96+
# Print a warmup notice when workbench-* plugins (claude-workbench
97+
# marketplace, excluding workbench-core itself) have skills that aren't yet
98+
# installed in Claude Chat at their current versions. The notice points the
99+
# user at the /workbench-core:install-chat-skills slash command.
100+
#
101+
# Cheap fast-path: if the state file is newer than installed_plugins.json,
102+
# nothing has changed since our last run — exit before any JSON parsing.
103+
local plugins_file="$HOME/.claude/plugins/installed_plugins.json"
104+
local state_file="$HOME/.claude-workbench/chat-skills-state.json"
105+
106+
[ ! -f "$plugins_file" ] && return 0
107+
command -v jq >/dev/null 2>&1 || return 0
108+
109+
if [ -f "$state_file" ] && [ "$state_file" -nt "$plugins_file" ]; then
110+
return 0
111+
fi
112+
113+
# For each eligible plugin, find skills with `name:` frontmatter and check
114+
# whether the recorded version in state file matches the current version.
115+
local new_or_updated=()
116+
while IFS=$'\t' read -r plugin_path plugin_version; do
117+
[ -z "$plugin_path" ] && continue
118+
[ ! -d "$plugin_path/skills" ] && continue
119+
local plugin_name
120+
plugin_name="$(echo "$plugin_path" | awk -F/ '{print $(NF-1)}')"
121+
122+
for skill_dir in "$plugin_path/skills"/*/; do
123+
skill_dir="${skill_dir%/}"
124+
[ ! -f "$skill_dir/SKILL.md" ] && continue
125+
grep -q '^name:' "$skill_dir/SKILL.md" 2>/dev/null || continue
126+
127+
local skill_name
128+
skill_name="$(basename "$skill_dir")"
129+
130+
local recorded_version=""
131+
if [ -f "$state_file" ]; then
132+
recorded_version=$(jq -r --arg p "$plugin_name" --arg s "$skill_name" '
133+
.installed[]? | select(.plugin == $p and .skill == $s) | .version
134+
' "$state_file" 2>/dev/null)
135+
fi
136+
137+
if [ "$recorded_version" != "$plugin_version" ]; then
138+
new_or_updated+=("$plugin_name|$skill_name")
139+
fi
140+
done
141+
done < <(jq -r '
142+
.plugins | to_entries[]
143+
| select(.key | endswith("@claude-workbench"))
144+
| select(.key | startswith("workbench-core@") | not)
145+
| .value[0]
146+
| "\(.installPath)\t\(.version)"
147+
' "$plugins_file" 2>/dev/null)
148+
149+
if [ ${#new_or_updated[@]} -gt 0 ]; then
150+
printf '## 📦 New Chat-installable skills\n\n'
151+
printf 'The following skills can be installed into Claude Chat (Mac app):\n\n'
152+
for entry in "${new_or_updated[@]}"; do
153+
local plugin_name="${entry%|*}"
154+
local skill_name="${entry#*|}"
155+
printf -- '- `%s` (from `%s`)\n' "$skill_name" "$plugin_name"
156+
done
157+
printf '\nClick to install: `/workbench-core:install-chat-skills`\n\n'
158+
fi
159+
}
160+
95161
collect_session_warmup_contributions() {
96162
# Concatenate `session-warmup.md` from every installed workbench-* plugin
97163
# in the claude-workbench marketplace. Source of truth is
@@ -342,4 +408,12 @@ NOTICE
342408
fi
343409
fi
344410

411+
# ──────────── Chat-installable skills check (startup only) ────────────
412+
# Detect new or updated skills in workbench-* plugins that haven't been
413+
# installed into Claude Chat yet. Cheap fast-path via state-file mtime
414+
# comparison — only does real work when plugins have actually changed.
415+
if [ "$SOURCE" = "startup" ]; then
416+
detect_chat_skill_changes
417+
fi
418+
345419
exit 0

scripts/install-chat-skills.sh

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env bash
2+
#
3+
# install-chat-skills.sh — package and install workbench-* plugin skills into
4+
# the Claude Mac app's Chat surface via .skill files.
5+
#
6+
# Discovers eligible skills in installed @claude-workbench plugins
7+
# (excluding workbench-core itself), packages each via the skill-creator's
8+
# package_skill.py, and opens each .skill file with the Mac app to trigger
9+
# the install dialog. Updates a state file so the SessionStart notice clears.
10+
#
11+
# Triggered by the /workbench-core:install-chat-skills slash command, OR
12+
# run directly: `bash scripts/install-chat-skills.sh`
13+
#
14+
# Exit codes:
15+
# 0 — success or nothing to do
16+
# 1 — pre-flight failure (jq missing, skill-creator missing, etc.)
17+
18+
set -euo pipefail
19+
20+
PLUGINS_FILE="$HOME/.claude/plugins/installed_plugins.json"
21+
STATE_FILE="$HOME/.claude-workbench/chat-skills-state.json"
22+
DIST_DIR="/tmp/workbench-chat-skills"
23+
24+
# ──────────── Pre-flight ────────────
25+
26+
if [ ! -f "$PLUGINS_FILE" ]; then
27+
echo "❌ Plugin registry not found at $PLUGINS_FILE"
28+
exit 1
29+
fi
30+
31+
if ! command -v jq >/dev/null 2>&1; then
32+
echo "❌ jq not installed. Install via: brew install jq"
33+
exit 1
34+
fi
35+
36+
# Find skill-creator's package_skill.py — the install path layout differs
37+
# slightly between plugin versions, so check both common shapes.
38+
SKILL_CREATOR_PATH=""
39+
for candidate_path in $(jq -r '
40+
.plugins | to_entries[]
41+
| select(.key | startswith("skill-creator@"))
42+
| .value[0].installPath // empty
43+
' "$PLUGINS_FILE" 2>/dev/null); do
44+
if [ -f "$candidate_path/skills/skill-creator/scripts/package_skill.py" ]; then
45+
SKILL_CREATOR_PATH="$candidate_path/skills/skill-creator"
46+
break
47+
elif [ -f "$candidate_path/scripts/package_skill.py" ]; then
48+
SKILL_CREATOR_PATH="$candidate_path"
49+
break
50+
fi
51+
done
52+
53+
if [ -z "$SKILL_CREATOR_PATH" ]; then
54+
echo "❌ skill-creator plugin not found. Install it first:"
55+
echo " /plugin install skill-creator@claude-plugins-official"
56+
exit 1
57+
fi
58+
59+
# pyyaml is required by skill-creator's quick_validate.py.
60+
if ! python3 -c "import yaml" 2>/dev/null; then
61+
echo "📦 Installing pyyaml (required by skill-creator's validator)..."
62+
if ! pip3 install pyyaml --break-system-packages --quiet 2>&1; then
63+
echo "❌ Failed to install pyyaml. Install manually:"
64+
echo " pip3 install pyyaml --break-system-packages"
65+
exit 1
66+
fi
67+
fi
68+
69+
# ──────────── Discovery ────────────
70+
71+
echo "🔍 Scanning workbench plugins for installable skills..."
72+
73+
SKILLS=()
74+
SKILL_NAMES=()
75+
PLUGIN_NAMES=()
76+
PLUGIN_VERSIONS=()
77+
78+
while IFS=$'\t' read -r plugin_path plugin_version; do
79+
[ -z "$plugin_path" ] && continue
80+
[ ! -d "$plugin_path/skills" ] && continue
81+
82+
# Plugin name = the directory two levels up from skills/ (cache layout:
83+
# cache/<marketplace>/<plugin_name>/<version>/skills/...).
84+
plugin_name="$(echo "$plugin_path" | awk -F/ '{print $(NF-1)}')"
85+
86+
for skill_dir in "$plugin_path/skills"/*/; do
87+
skill_dir="${skill_dir%/}"
88+
[ ! -f "$skill_dir/SKILL.md" ] && continue
89+
90+
# `name:` is required by the skill-creator validator. Skip with a notice
91+
# so the user knows why a skill they expected isn't being installed.
92+
if grep -q '^name:' "$skill_dir/SKILL.md" 2>/dev/null; then
93+
SKILLS+=("$skill_dir")
94+
SKILL_NAMES+=("$(basename "$skill_dir")")
95+
PLUGIN_NAMES+=("$plugin_name")
96+
PLUGIN_VERSIONS+=("$plugin_version")
97+
else
98+
echo "⚠️ Skipping $plugin_name/$(basename "$skill_dir") — missing 'name:' in frontmatter"
99+
fi
100+
done
101+
done < <(jq -r '
102+
.plugins | to_entries[]
103+
| select(.key | endswith("@claude-workbench"))
104+
| select(.key | startswith("workbench-core@") | not)
105+
| .value[0]
106+
| "\(.installPath)\t\(.version)"
107+
' "$PLUGINS_FILE")
108+
109+
if [ ${#SKILLS[@]} -eq 0 ]; then
110+
echo "ℹ️ No installable skills found in dependent plugins."
111+
# Clear the state file so the warmup notice clears too.
112+
mkdir -p "$(dirname "$STATE_FILE")"
113+
echo '{"installed": []}' > "$STATE_FILE"
114+
exit 0
115+
fi
116+
117+
# ──────────── Plan ────────────
118+
119+
echo ""
120+
echo "Found ${#SKILLS[@]} skill(s) to install into Claude Chat:"
121+
for i in "${!SKILLS[@]}"; do
122+
echo "${PLUGIN_NAMES[$i]} / ${SKILL_NAMES[$i]}"
123+
done
124+
echo ""
125+
126+
# ──────────── Package and open ────────────
127+
128+
mkdir -p "$DIST_DIR"
129+
rm -f "$DIST_DIR"/*.skill
130+
131+
INSTALLED_RECORDS=()
132+
133+
for i in "${!SKILLS[@]}"; do
134+
skill_dir="${SKILLS[$i]}"
135+
name="${SKILL_NAMES[$i]}"
136+
plugin="${PLUGIN_NAMES[$i]}"
137+
version="${PLUGIN_VERSIONS[$i]}"
138+
139+
echo "📦 Packaging $plugin / $name..."
140+
cd "$SKILL_CREATOR_PATH"
141+
if python3 -m scripts.package_skill "$skill_dir" "$DIST_DIR" 2>&1 | tail -3 | grep -q ""; then
142+
echo "🚀 Opening $name.skill — confirm install in the Mac app dialog..."
143+
open -a "Claude" "$DIST_DIR/$name.skill" 2>&1 || {
144+
echo "⚠️ Failed to open Claude.app for $name.skill — is the Mac app installed?"
145+
continue
146+
}
147+
INSTALLED_RECORDS+=("$plugin|$name|$version")
148+
sleep 1.5 # let dialog appear before queueing next
149+
else
150+
echo "❌ Packaging failed for $name. Skipping."
151+
fi
152+
done
153+
154+
# ──────────── State file update ────────────
155+
156+
mkdir -p "$(dirname "$STATE_FILE")"
157+
158+
# Build state JSON via jq for safe escaping.
159+
state_json='{"installed":[]}'
160+
for record in "${INSTALLED_RECORDS[@]}"; do
161+
IFS='|' read -r plugin skill version <<< "$record"
162+
state_json=$(echo "$state_json" | jq \
163+
--arg plugin "$plugin" \
164+
--arg skill "$skill" \
165+
--arg version "$version" \
166+
'.installed += [{plugin: $plugin, skill: $skill, version: $version}]')
167+
done
168+
echo "$state_json" > "$STATE_FILE"
169+
170+
echo ""
171+
echo "✅ Done. Verify in Claude Chat that the skills appear."
172+
echo " State recorded at: $STATE_FILE"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
name: install-chat-skills
3+
description: Install workbench-* plugin skills into Claude Chat (Mac app) via .skill packaging. Discovers all eligible skills in installed @claude-workbench plugins (excluding workbench-core itself), packages them with skill-creator's package_skill.py, and opens each .skill file with the Mac app to trigger the install dialog. Use this skill whenever the SessionStart warmup output mentions new Chat-installable skills, or to manually re-sync Chat skills after installing or updating a workbench plugin.
4+
---
5+
6+
# Install Chat Skills
7+
8+
Run the install-chat-skills script. It handles the full discover → package → open flow and updates the state file so the SessionStart notice clears.
9+
10+
```bash
11+
bash "${CLAUDE_PLUGIN_ROOT}/scripts/install-chat-skills.sh"
12+
```
13+
14+
What it does:
15+
16+
1. Scans `~/.claude/plugins/installed_plugins.json` for `@claude-workbench` plugins (excluding workbench-core itself).
17+
2. For each plugin, finds skills under `skills/<name>/SKILL.md` that have `name:` in their frontmatter (the skill-creator validator requires it).
18+
3. Packages each skill as a `.skill` file in `/tmp/workbench-chat-skills/` via `python3 -m scripts.package_skill` from the skill-creator plugin.
19+
4. Opens each `.skill` with `open -a Claude` — the Mac app handles the file extension and shows an install dialog.
20+
5. Records what was installed (with versions) in `~/.claude-workbench/chat-skills-state.json` so the SessionStart notice clears.
21+
22+
The user confirms each install dialog as it appears. After the script finishes, verify in Claude Chat that the skills appear and trigger correctly.
23+
24+
If `skill-creator` isn't installed, the script will print a one-line install command — run that first, then re-invoke this skill.

0 commit comments

Comments
 (0)