Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -67,7 +66,7 @@ jobs:
assert catalog["skills"] == sorted(catalog["skills"])
PY

for host in codex claude cursor; do
for host in codex claude cursor gemini grok openai; do
test_repo="$test_root/$host"
mkdir -p "$test_repo"
git -C "$test_repo" init -q
Expand All @@ -93,3 +92,77 @@ jobs:
fledge skills uninstall agent-coordination --repo "$test_repo" --host "$host"
test ! -e "$test_repo/.$host/skills/agent-coordination"
done

- name: Prove Grok placement with Let find skills
shell: bash
run: |
# Let’s fledge plugin build hook requires Bun.
curl -fsSL https://bun.sh/install | bash

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pin the Bun release used by the Let build

When the Bun installer’s default release changes, this step begins building the pinned Let v0.2.0 plugin with a different runtime even though the skills commit is unchanged, so an upstream Bun regression or compatibility change can break this integration lane nondeterministically. Install an explicitly tested Bun version rather than executing the installer without a version selection.

Useful? React with 👍 / 👎.

export BUN_INSTALL="${HOME}/.bun"
export PATH="${BUN_INSTALL}/bin:${PATH}"
command -v bun
bun --version
fledge plugins install CorvidLabs/let@v0.2.0 --yes --non-interactive
test_repo="$(mktemp -d)/grok-let"
mkdir -p "$test_repo"
git -C "$test_repo" init -q
fledge skills install agent-coordination --repo "$test_repo" --host grok
fledge skills install fledge-workflows --repo "$test_repo" --host grok
fledge let find skills --scope project --host grok \
--repo "$test_repo" --cwd "$test_repo" --json \
> /tmp/let-find-skills-grok.json
python3 - "$test_repo" <<'PY'
import json
import sys
from pathlib import Path

repo = Path(sys.argv[1]).resolve()
payload = json.loads(Path("/tmp/let-find-skills-grok.json").read_text(encoding="utf-8"))
assert payload.get("ok") is True, payload
project = []
for item in payload.get("data", {}).get("items", []):
if item.get("scope") != "project" or item.get("host") != "grok":
continue
path = Path(item.get("path", "")).resolve()
try:
path.relative_to(repo)
except ValueError:
continue
project.append(item)
names = {item["name"] for item in project}
assert names >= {"agent-coordination", "fledge-workflows"}, names
for item in project:
expected = repo / ".grok" / "skills" / item["name"] / "SKILL.md"
assert Path(item["path"]).resolve() == expected.resolve()
print("let find skills proof ok:", sorted(names))
PY
fledge skills uninstall agent-coordination --repo "$test_repo" --host grok
test ! -e "$test_repo/.grok/skills/agent-coordination"
fledge let find skills --scope project --host grok \
--repo "$test_repo" --cwd "$test_repo" \
--query agent-coordination --json \
> /tmp/let-find-after-uninstall.json
python3 - "$test_repo" <<'PY'
import json
import sys
from pathlib import Path

repo = Path(sys.argv[1]).resolve()
payload = json.loads(Path("/tmp/let-find-after-uninstall.json").read_text(encoding="utf-8"))
assert payload.get("ok") is True, payload
project = []
for item in payload.get("data", {}).get("items", []):
if (
item.get("scope") != "project"
or item.get("host") != "grok"
or item.get("name") != "agent-coordination"
):
continue
try:
Path(item.get("path", "")).resolve().relative_to(repo)
except ValueError:
continue
project.append(item)
assert not project, project
print("let post-uninstall proof ok")
PY
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,14 @@ for these hosts:
| Codex | `.codex/skills/<skill>` | Yes |
| Claude | `.claude/skills/<skill>` | Yes |
| Cursor | `.cursor/skills/<skill>` | Yes |
| Gemini | `.gemini/skills/<skill>` | Yes |
| Grok | `.grok/skills/<skill>` | Yes |
| OpenAI | `.openai/skills/<skill>` | Yes |
| Other agents | Agent-defined | No; use the agent's documented skill path. |

Host placement does not translate private context or product-specific assumptions into a skill.
All catalog content and examples must remain usable from public CorvidLabs sources.
Fledge lifecycle tracking applies to the three automatic placements; a custom agent path remains
Fledge lifecycle tracking applies to the six automatic placements; a custom agent path remains
managed by that agent or by the user.

## Manage installed skills
Expand Down Expand Up @@ -108,9 +111,9 @@ fledge skills update --all --dry-run
fledge skills update --all
```

Use `--host codex`, `--host claude`, or `--host cursor` to select one placement when a skill
is installed for multiple hosts. `--host auto` is available for install only when exactly one
supported host directory already exists.
Use `--host codex`, `--host claude`, `--host cursor`, `--host gemini`, `--host grok`, or
`--host openai` to select one placement when a skill is installed for multiple hosts. `--host auto`
is available for install only when exactly one supported host directory already exists.

## Spec Sync placement

Expand Down
55 changes: 39 additions & 16 deletions bin/corvid-skills
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ Manage shared CorvidLabs agent skills in one repository.

Usage:
fledge skills list [--json]
fledge skills install <skill> [--repo <path>] [--host <auto|codex|claude|cursor>] [--link] [--dry-run]
fledge skills install <skill> [--repo <path>] [--host <auto|codex|claude|cursor|gemini|grok|openai>] [--link] [--dry-run]
fledge skills status [--repo <path>] [--json]
fledge skills update <skill>|--all [--repo <path>] [--host <codex|claude|cursor>] [--dry-run]
fledge skills uninstall <skill> [--repo <path>] [--host <codex|claude|cursor>] [--dry-run]
fledge skills update <skill>|--all [--repo <path>] [--host <codex|claude|cursor|gemini|grok|openai>] [--dry-run]
fledge skills uninstall <skill> [--repo <path>] [--host <codex|claude|cursor|gemini|grok|openai>] [--dry-run]

Install copies a selected skill into a repository-local agent directory and
records the exact source revision and content digest in .corvid-skills.json.
Expand All @@ -38,6 +38,9 @@ host_directory() {
codex) printf '.codex/skills' ;;
claude) printf '.claude/skills' ;;
cursor) printf '.cursor/skills' ;;
gemini) printf '.gemini/skills' ;;
grok) printf '.grok/skills' ;;
openai) printf '.openai/skills' ;;
*) fail "unsupported host: $1" ;;
esac
}
Expand Down Expand Up @@ -82,13 +85,19 @@ ensure_safe_managed_paths() {
[[ ! -L "$repo/$manifest_name" ]] || fail "refusing symlinked manifest: $repo/$manifest_name"
}

has_only_regular_entries() {
local directory="$1"
[[ -d "$directory" && ! -L "$directory" ]] || return 1
[[ -z "$(find "$directory" -mindepth 1 ! -type d ! -type f -print -quit)" ]]
}

validate_skill_source() {
local skill="$1"
[[ "$skill" =~ ^[a-z0-9][a-z0-9-]*$ ]] || fail "invalid skill name: $skill"
[[ -d "$skills_dir/$skill" && ! -L "$skills_dir/$skill" && -f "$skills_dir/$skill/SKILL.md" ]] ||
fail "unknown skill: $skill"
[[ -z "$(find "$skills_dir/$skill" -type l -print -quit)" ]] ||
fail "skill contains a symlink: $skill"
has_only_regular_entries "$skills_dir/$skill" ||
fail "skill contains unsupported filesystem entries: $skill"
}

resolve_host() {
Expand All @@ -102,28 +111,31 @@ resolve_host() {

local candidates=()
local host
for host in codex claude cursor; do
for host in codex claude cursor gemini grok openai; do
[[ -d "$repo/$(host_directory "$host")" ]] && candidates+=("$host")
done
if [[ ${#candidates[@]} -eq 1 ]]; then
printf '%s' "${candidates[0]}"
return
fi
if [[ ${#candidates[@]} -eq 0 ]]; then
fail "cannot infer an agent host; pass --host codex, --host claude, or --host cursor"
fail "cannot infer an agent host; pass --host codex, --host claude, --host cursor, --host gemini, --host grok, or --host openai"
fi
fail "multiple agent hosts are present (${candidates[*]}); pass --host explicitly"
}

directory_digest() {
local directory="$1"
has_only_regular_entries "$directory" || return 1
(
cd "$directory"
find . -type f -print0 | LC_ALL=C sort -z | while IFS= read -r -d '' file; do
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file"
find . -mindepth 1 -print0 | LC_ALL=C sort -z | while IFS= read -r -d '' entry; do
if [[ -d "$entry" ]]; then
printf 'directory\0%s\0' "$entry"
elif command -v shasum >/dev/null 2>&1; then
printf 'file\0%s\0%s\0' "$entry" "$(shasum -a 256 "$entry" | awk '{print $1}')"
else
sha256sum "$file"
printf 'file\0%s\0%s\0' "$entry" "$(sha256sum "$entry" | awk '{print $1}')"
Comment on lines +132 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve compatibility with recorded content digests

When a repository contains an install created by the previous catalog version, its manifest stores the old file-only digest, but this new algorithm always produces a different path/type-aware digest for the unchanged directory. After upgrading the catalog, status therefore reports every existing install as modified, and the lifecycle guards refuse both update and uninstall; retain legacy-digest verification or version/migrate the digest format before switching algorithms.

Useful? React with 👍 / 👎.

fi
done
) | if command -v shasum >/dev/null 2>&1; then
Expand Down Expand Up @@ -235,6 +247,9 @@ expected_destination() {
codex) printf '.codex/skills/%s' "$skill" ;;
claude) printf '.claude/skills/%s' "$skill" ;;
cursor) printf '.cursor/skills/%s' "$skill" ;;
gemini) printf '.gemini/skills/%s' "$skill" ;;
grok) printf '.grok/skills/%s' "$skill" ;;
openai) printf '.openai/skills/%s' "$skill" ;;
*) return 1 ;;
esac
}
Expand Down Expand Up @@ -265,16 +280,24 @@ entry_state() {

if [[ "$install_mode" == "link" && -L "$destination" ]]; then
if [[ "$(readlink "$destination")" == "$skills_dir/$skill" ]]; then
if has_only_regular_entries "$skills_dir/$skill"; then
local actual_digest
actual_digest="$(directory_digest "$skills_dir/$skill")"
[[ "$actual_digest" == "$expected_digest" ]] && printf 'current' || printf 'modified'
else
printf 'modified'
fi
else
printf 'modified'
fi
elif [[ "$install_mode" == "copy" && -d "$destination" && ! -L "$destination" ]]; then
if has_only_regular_entries "$destination"; then
local actual_digest
actual_digest="$(directory_digest "$skills_dir/$skill")"
actual_digest="$(directory_digest "$destination")"
[[ "$actual_digest" == "$expected_digest" ]] && printf 'current' || printf 'modified'
else
printf 'modified'
fi
elif [[ "$install_mode" == "copy" && -d "$destination" && ! -L "$destination" ]]; then
local actual_digest
actual_digest="$(directory_digest "$destination")"
[[ "$actual_digest" == "$expected_digest" ]] && printf 'current' || printf 'modified'
elif [[ -e "$destination" || -L "$destination" ]]; then
printf 'modified'
else
Expand Down
Loading