Publish skills through the agent-neutral skill home
This commit is contained in:
@@ -9,7 +9,7 @@ You are editing a running desktop, not a codebase that gets deployed later. Ever
|
||||
repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact
|
||||
drives every rule below.
|
||||
|
||||
Read before large work, in this order:
|
||||
Before large work, read these in order:
|
||||
|
||||
- `README.md` — layout, the `panama` command, how installing and updating work
|
||||
- `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one
|
||||
@@ -62,7 +62,8 @@ Every executable contract under `tests/` is classified in `tests/contracts.manif
|
||||
`PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr.
|
||||
Successful stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||
- Contracts run directly too: `tests/setup/interview-contract`.
|
||||
- After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
|
||||
- After changing `PreferenceSchema.qml` or
|
||||
`config/dot/quickshell/services/SettingsRoutes.qml`, regenerate:
|
||||
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
|
||||
`config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links).
|
||||
Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/panama
|
||||
@@ -53,7 +53,7 @@ in order, without stopping again:
|
||||
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
|
||||
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
|
||||
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
|
||||
| `link-skills` | Links the agent skills in `skills/` into `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||||
| `link-skills` | Links the agent skills in `skills/` into `~/.agents/skills` and `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||||
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
|
||||
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
|
||||
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
||||
@@ -186,7 +186,7 @@ server/ The server role: compose services (one directory per
|
||||
service), the nightly image updater, and its units. See
|
||||
server/README.md
|
||||
skills/ Agent skills for operating this desktop, linked into
|
||||
~/.claude/skills
|
||||
~/.agents/skills and ~/.claude/skills
|
||||
setup/
|
||||
apps/ Applications built from source, one file each
|
||||
keys/ Pinned signing keys, for publishers that ship no fetchable
|
||||
|
||||
+21
-19
@@ -11,12 +11,11 @@
|
||||
# this repository, not anybody's personal content, so a stranger who clones
|
||||
# Panama wants it for exactly the same reason its author does.
|
||||
#
|
||||
# ~/.claude/skills was a single symlink into user/agents/skills until now, and
|
||||
# a directory cannot be two things at once. So the destination becomes a real
|
||||
# directory and every skill -- shipped here, personal from user/ -- is linked
|
||||
# into it one at a time. link-user runs after this stage on purpose: it links
|
||||
# last, so a personal skill named like a shipped one wins, which is the
|
||||
# precedence Claude Code itself uses.
|
||||
# ~/.agents/skills and ~/.claude/skills may each start as a single symlink into
|
||||
# user/agents/skills, but a directory cannot point at personal and shipped
|
||||
# skills at once. Both destinations become real directories with one link per
|
||||
# skill. link-user runs after this stage on purpose, so a personal skill named
|
||||
# like a shipped one wins in every agent runtime.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -25,7 +24,7 @@ log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||
SKILLS_DIR="$PANAMA_PATH/skills"
|
||||
PANAMA_OLD="$PANAMA_PATH/config/old"
|
||||
DESTINATION="$HOME/.claude/skills"
|
||||
DESTINATIONS=("$HOME/.agents/skills" "$HOME/.claude/skills")
|
||||
|
||||
[[ -d "$SKILLS_DIR" ]] || { log "No skills/ in this checkout; nothing to link."; exit 0; }
|
||||
|
||||
@@ -51,27 +50,30 @@ displace() {
|
||||
log "Moved existing $destination to $backup"
|
||||
}
|
||||
|
||||
# The destination itself has to be a real directory before anything can be
|
||||
# linked into it. An old whole-directory symlink is removed; a regular file
|
||||
# somebody left at this path is kept, in config/old/.
|
||||
mkdir -p "$(dirname "$DESTINATION")"
|
||||
if [[ -L "$DESTINATION" ]]; then
|
||||
rm -f "$DESTINATION"
|
||||
log "Removed the old $DESTINATION symlink; skills are linked one by one now"
|
||||
elif [[ -e "$DESTINATION" && ! -d "$DESTINATION" ]]; then
|
||||
displace "$DESTINATION"
|
||||
# Each destination has to be a real directory before anything can be linked
|
||||
# into it. An old whole-directory symlink is removed; a regular file somebody
|
||||
# left at the path is kept in config/old/.
|
||||
for destination in "${DESTINATIONS[@]}"; do
|
||||
mkdir -p "$(dirname "$destination")"
|
||||
if [[ -L "$destination" ]]; then
|
||||
rm -f "$destination"
|
||||
log "Removed the old $destination symlink; skills are linked one by one now"
|
||||
elif [[ -e "$destination" && ! -d "$destination" ]]; then
|
||||
displace "$destination"
|
||||
fi
|
||||
mkdir -p "$DESTINATION"
|
||||
mkdir -p "$destination"
|
||||
done
|
||||
|
||||
linked=0
|
||||
for skill in "$SKILLS_DIR"/*; do
|
||||
[[ -e "$skill" ]] || continue
|
||||
name="$(basename "$skill")"
|
||||
target="$DESTINATION/$name"
|
||||
|
||||
for destination in "${DESTINATIONS[@]}"; do
|
||||
target="$destination/$name"
|
||||
displace "$target"
|
||||
ln -s "$skill" "$target"
|
||||
log "Linked skills/$name → $target"
|
||||
done
|
||||
linked=$(( linked + 1 ))
|
||||
done
|
||||
|
||||
|
||||
+41
-20
@@ -2,7 +2,7 @@
|
||||
|
||||
# The manual this machine hands an agent.
|
||||
#
|
||||
# skills/ and .claude/skills/panama exist because an agent asked to do anything
|
||||
# skills/ and .agents/skills/panama exist because an agent asked to do anything
|
||||
# on a Panama desktop will otherwise infer it from the source and get half of it
|
||||
# wrong. That only helps if what the skills say is true -- and a skill is worse
|
||||
# than no skill when it is stale, because an agent believes it verbatim and does
|
||||
@@ -16,7 +16,7 @@
|
||||
# That is a convention on the prose -- name things exactly, in backticks --
|
||||
# and it is how they should be written anyway.
|
||||
# 3. The delivery works: link-skills is a stage, in the right place, and the
|
||||
# personal manifest hands ~/.claude/skills over to the linkdir kind.
|
||||
# personal manifest hands both shared skill homes to the linkdir kind.
|
||||
#
|
||||
# Sections 1 and 2 report clearly and keep going when a skill is not written
|
||||
# yet, so this contract is useful while the skills are still being authored.
|
||||
@@ -32,9 +32,21 @@ manifest="$repo_dir/user/manifest"
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
# The three, and where each is delivered from. The two under skills/ are shipped
|
||||
# to every machine; the third is project-level and needs no delivery at all.
|
||||
SKILL_DIRS=(skills/panama-desktop skills/panama-sudo .claude/skills/panama)
|
||||
# Every directory under skills/ ships to every machine. The project-level
|
||||
# panama skill stays in this repository and needs no home-directory delivery.
|
||||
SKILL_DIRS=()
|
||||
for directory in "$repo_dir"/skills/*; do
|
||||
[[ -d "$directory" ]] && SKILL_DIRS+=("${directory#"$repo_dir"/}")
|
||||
done
|
||||
SKILL_DIRS+=(.agents/skills/panama)
|
||||
|
||||
# The project skill has one agent-neutral source. Claude gets a compatibility
|
||||
# symlink, while Codex and other Agent Skills readers use .agents directly.
|
||||
[[ -L "$repo_dir/.claude/skills/panama" ]] \
|
||||
|| note '.claude/skills/panama is not a compatibility symlink to the agent-neutral source'
|
||||
[[ "$(readlink -f "$repo_dir/.claude/skills/panama" 2>/dev/null)" == \
|
||||
"$(readlink -f "$repo_dir/.agents/skills/panama" 2>/dev/null)" ]] \
|
||||
|| note '.claude and .agents resolve the project panama skill differently'
|
||||
|
||||
# ── 1. Each skill loads ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -54,7 +66,7 @@ for relative in "${SKILL_DIRS[@]}"; do
|
||||
present+=("$directory")
|
||||
|
||||
# Frontmatter is the first --- delimited block, and a skill without one is
|
||||
# not a skill: Claude Code skips the directory entirely.
|
||||
# not a skill: agent loaders skip the directory entirely.
|
||||
frontmatter="$(awk 'NR==1 { if ($0 != "---") exit 1; next } $0 == "---" { exit } { print }' "$file")"
|
||||
if [[ -z "$frontmatter" ]]; then
|
||||
note "$relative/SKILL.md does not open with a --- frontmatter block"
|
||||
@@ -207,12 +219,12 @@ else
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# A checkout and a home of its own. Never the real ones: ~/.claude/skills on
|
||||
# this machine is somebody's live agent setup, and a contract that broke it
|
||||
# mid-session would be worse than the bug it was looking for.
|
||||
# A checkout and a home of its own. Never the real ones: these are somebody's
|
||||
# live agent setup, and a contract that broke them mid-session would be worse
|
||||
# than the bug it was looking for.
|
||||
checkout="$work/Panama"
|
||||
home="$work/home"
|
||||
mkdir -p "$checkout/setup/scripts" "$checkout/skills/shipped" "$home/.claude"
|
||||
mkdir -p "$checkout/setup/scripts" "$checkout/skills/shipped" "$home/.claude" "$home/.agents"
|
||||
cp "$linker" "$checkout/setup/scripts/link-skills"
|
||||
printf 'a shipped skill\n' >"$checkout/skills/shipped/SKILL.md"
|
||||
|
||||
@@ -220,6 +232,7 @@ else
|
||||
# symlink, which is what link-user used to leave here.
|
||||
mkdir -p "$work/personal"
|
||||
ln -s "$work/personal" "$home/.claude/skills"
|
||||
ln -s "$work/personal" "$home/.agents/skills"
|
||||
|
||||
run() { HOME="$home" PANAMA_PATH="$checkout" "$checkout/setup/scripts/link-skills" >"$work/log" 2>&1; }
|
||||
|
||||
@@ -231,22 +244,30 @@ else
|
||||
|| note 'link-skills left ~/.claude/skills a symlink, so nothing else can be linked into it'
|
||||
[[ -L "$home/.claude/skills/shipped" ]] \
|
||||
|| note 'link-skills did not link each shipped skill as a child of ~/.claude/skills'
|
||||
[[ -d "$home/.agents/skills" && ! -L "$home/.agents/skills" ]] \
|
||||
|| note 'link-skills left ~/.agents/skills a symlink, so shipped and personal skills cannot coexist'
|
||||
[[ -L "$home/.agents/skills/shipped" ]] \
|
||||
|| note 'link-skills did not link each shipped skill as a child of ~/.agents/skills'
|
||||
grep -q 'Agent skills: 1 linked' "$work/log" \
|
||||
|| note 'link-skills does not report how many skills it linked'
|
||||
|
||||
# A real directory at a shipped skill's name is somebody's work: it moves to
|
||||
# config/old rather than being deleted, the same promise the other stages
|
||||
# make. A symlink is not, and must not accumulate there.
|
||||
rm "$home/.claude/skills/shipped"
|
||||
mkdir -p "$home/.claude/skills/shipped"
|
||||
printf 'installed by hand\n' >"$home/.claude/skills/shipped/SKILL.md"
|
||||
mkdir -p "$home/.claude/skills/untouched"
|
||||
for skill_home in "$home/.claude/skills" "$home/.agents/skills"; do
|
||||
rm -f "$skill_home/shipped"
|
||||
mkdir -p "$skill_home/shipped"
|
||||
printf 'installed by hand\n' >"$skill_home/shipped/SKILL.md"
|
||||
mkdir -p "$skill_home/untouched"
|
||||
done
|
||||
|
||||
run
|
||||
grep -rq 'installed by hand' "$checkout/config/old" 2>/dev/null \
|
||||
|| note 'link-skills destroyed a real skill instead of moving it to config/old'
|
||||
[[ -d "$home/.claude/skills/untouched" ]] \
|
||||
|| note 'link-skills removed a skill it does not ship'
|
||||
[[ "$(grep -rl 'installed by hand' "$checkout/config/old" 2>/dev/null | wc -l)" == 2 ]] \
|
||||
|| note 'link-skills did not preserve real skills from both agent homes'
|
||||
for skill_home in "$home/.claude/skills" "$home/.agents/skills"; do
|
||||
[[ -d "$skill_home/untouched" ]] \
|
||||
|| note "link-skills removed an unshipped skill from $skill_home"
|
||||
done
|
||||
|
||||
before="$(find "$checkout/config/old" | wc -l)"
|
||||
run
|
||||
@@ -287,8 +308,8 @@ fi
|
||||
|
||||
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.claude/skills\s*$' "$manifest" \
|
||||
|| note 'the manifest does not use linkdir for ~/.claude/skills, so personal skills would replace the directory'
|
||||
grep -qE '^\s*link\s+agents/skills\s+~/\.agents/skills\s*$' "$manifest" \
|
||||
|| note '~/.agents/skills is no longer a whole-directory link, and nothing else claims that path'
|
||||
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.agents/skills\s*$' "$manifest" \
|
||||
|| note 'the manifest does not use linkdir for ~/.agents/skills, so personal skills would replace shipped skills'
|
||||
grep -q 'linkdir)' "$user_linker" \
|
||||
|| note 'link-user does not implement the linkdir kind the manifest asks for'
|
||||
grep -q 'linkdir' "$repo_dir/user/README.md" \
|
||||
|
||||
@@ -73,7 +73,7 @@ cat >"$checkout/user/manifest" <<'FIXTURE'
|
||||
# a comment, and a blank line follow
|
||||
|
||||
link agents/AGENTS.md ~/.claude/CLAUDE.md
|
||||
link agents/skills ~/.agents/skills
|
||||
linkdir agents/skills ~/.agents/skills
|
||||
linkdir agents/skills ~/.claude/skills
|
||||
copy plain.txt ~/.config/plain.txt
|
||||
link missing.txt ~/.config/missing.txt
|
||||
@@ -102,11 +102,12 @@ PANAMA_USER_CONTENT=no run
|
||||
|
||||
# ── 2. Saying yes links, and keeps what was there ───────────────────────────
|
||||
|
||||
# ~/.claude/skills is shared now: link-skills has already made it a real
|
||||
# directory and linked Panama's own skills into it. A linkdir entry has to land
|
||||
# beside those rather than replace the directory holding them.
|
||||
mkdir -p "$home/.claude/skills"
|
||||
# Both skill homes are shared now: link-skills has already made them real
|
||||
# directories and linked Panama's own skills into each. Personal linkdir
|
||||
# entries land beside those rather than replacing either directory.
|
||||
mkdir -p "$home/.claude/skills" "$home/.agents/skills"
|
||||
ln -s "$checkout/skills/shipped" "$home/.claude/skills/shipped"
|
||||
ln -s "$checkout/skills/shipped" "$home/.agents/skills/shipped"
|
||||
|
||||
PANAMA_USER_CONTENT=yes run
|
||||
|
||||
@@ -114,8 +115,12 @@ PANAMA_USER_CONTENT=yes run
|
||||
|| note 'CLAUDE.md was not replaced with a symlink into the checkout'
|
||||
[[ "$(cat "$home/.claude/CLAUDE.md")" == "tracked instructions" ]] \
|
||||
|| note 'the CLAUDE.md link does not resolve to the tracked file'
|
||||
[[ -L "$home/.agents/skills" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|
||||
|| note 'the skills directory was not linked as a directory'
|
||||
[[ -d "$home/.agents/skills" && ! -L "$home/.agents/skills" ]] \
|
||||
|| note 'a linkdir entry replaced ~/.agents/skills with a symlink'
|
||||
[[ -L "$home/.agents/skills/example" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|
||||
|| note 'a linkdir entry did not publish the personal skill for agent-neutral readers'
|
||||
[[ -L "$home/.agents/skills/shipped" ]] \
|
||||
|| note 'a linkdir entry removed the shipped skill from ~/.agents/skills'
|
||||
[[ -d "$home/.claude/skills" && ! -L "$home/.claude/skills" ]] \
|
||||
|| note 'a linkdir entry replaced its destination directory with a symlink'
|
||||
[[ -L "$home/.claude/skills/example" && -f "$home/.claude/skills/example/SKILL.md" ]] \
|
||||
|
||||
+11
-11
@@ -13,7 +13,7 @@ and one file says where each piece goes.
|
||||
| Path | Goes to | Why |
|
||||
| --- | --- | --- |
|
||||
| `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` | Two tools, two names, one file. These were byte-identical copies before this, waiting to disagree. |
|
||||
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | A skill installed on any machine lands in the checkout. The Claude path is `linkdir` — see below. |
|
||||
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | Personal skills live once in the checkout. Both destinations use `linkdir` so Panama's shipped skills can live beside them. |
|
||||
| `agents/rules/` | `~/.claude/rules` | |
|
||||
| `ssh/config` | `~/.ssh/config` | Host aliases only. Keys are per-machine and are never tracked. |
|
||||
| `espanso/identity.yml` | `~/.config/espanso/match/identity.yml` | Copied, not linked, because a machine may add its own triggers. |
|
||||
@@ -27,22 +27,22 @@ destination is empty. `linkdir` is the third, and it exists because one
|
||||
destination is no longer only ours.
|
||||
|
||||
Panama ships its own agent skills now (`skills/`, linked by
|
||||
`setup/scripts/link-skills`), and they go to `~/.claude/skills` — the same
|
||||
directory the personal ones went to as a single symlink. A directory cannot be
|
||||
a symlink to two places, so that entry became `linkdir`: the destination is a
|
||||
real directory, and each child of `user/agents/skills/` is linked into it
|
||||
individually. `~/.agents/skills` is still a whole-directory `link`, because
|
||||
nothing else claims it.
|
||||
`setup/scripts/link-skills`), and they go to both skill homes. A directory
|
||||
cannot be a symlink to two places, so both personal entries use `linkdir`: each
|
||||
destination is a real directory, and each child of `user/agents/skills/` is
|
||||
linked into it individually.
|
||||
|
||||
Two consequences, recorded rather than fixed:
|
||||
|
||||
- **A personal skill named like a shipped one shadows it.** `link-user` runs
|
||||
after `link-skills` and displaces what it finds, so the personal one wins.
|
||||
That is the intent, and it is the precedence Claude Code uses anyway.
|
||||
after `link-skills` and displaces what it finds, so the personal one wins in
|
||||
both skill homes.
|
||||
- **A new personal skill needs a re-link to appear.** The whole-directory link
|
||||
showed a newly created skill instantly; per-child links do not know about a
|
||||
child that did not exist when they were made. Run `panama update` or
|
||||
`setup/scripts/link-user` after adding one.
|
||||
child that did not exist when they were made. Put new shared skills in
|
||||
`user/agents/skills/`, then run `panama update` or `setup/scripts/link-user`.
|
||||
A tool that installs directly into a home skill directory creates a
|
||||
machine-local skill until it is moved into the checkout.
|
||||
|
||||
## It is off unless you say yes
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ ln -s ../../.agents/skills/<name> ~/.claude/skills/<name>
|
||||
Writing a second copy into `~/.claude/skills/` gives you two files that drift, and the drift is
|
||||
silent because each harness only ever reads its own. One home, one symlink per harness that needs it.
|
||||
|
||||
A skill that only makes sense inside one repo belongs in that repo, at `<repo>/.claude/skills/`.
|
||||
A skill that only makes sense inside one repo lives once at
|
||||
`<repo>/.agents/skills/<name>/`. Point Claude Code at that source with a
|
||||
`<repo>/.claude/skills/<name>` symlink.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
|
||||
+6
-7
@@ -28,14 +28,13 @@
|
||||
link agents/AGENTS.md ~/.claude/CLAUDE.md
|
||||
link agents/AGENTS.md ~/.codex/AGENTS.md
|
||||
|
||||
# Skills, at both paths that look for them. Linked so that a skill installed by
|
||||
# any tool lands in the checkout and `panama update` offers to commit it.
|
||||
# Skills, at both paths that look for them. Each tracked skill is linked from
|
||||
# this checkout so edits stay shared and `panama update` offers to commit them.
|
||||
#
|
||||
# ~/.agents/skills is this directory and nothing else, so it stays one link.
|
||||
# ~/.claude/skills also holds the skills Panama itself ships, put there by the
|
||||
# link-skills stage, so it is a real directory with one link per skill -- and
|
||||
# this stage runs after that one, so a personal skill wins a name collision.
|
||||
link agents/skills ~/.agents/skills
|
||||
# Both destinations also hold skills Panama itself ships, put there by the
|
||||
# link-skills stage, so both are real directories with one link per skill. This
|
||||
# stage runs after that one, so a personal skill wins a name collision.
|
||||
linkdir agents/skills ~/.agents/skills
|
||||
linkdir agents/skills ~/.claude/skills
|
||||
|
||||
link agents/rules ~/.claude/rules
|
||||
|
||||
Reference in New Issue
Block a user