diff --git a/.local/bin/skills-update b/.local/bin/skills-update new file mode 100755 index 0000000..29bf721 --- /dev/null +++ b/.local/bin/skills-update @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# skills-update: refresh omo's upstream-tracked skills. On flowstate-remote it runs daily from the +# skills-update.timer systemd user unit; `skills-update --install-timer` writes and enables it. +# +# Vendored in this repo (.config/omo/skills, linked into ~/.omo/agent/skills), committed and pushed: +# impeccable newest pbakaus/impeccable skill-v* release, re-vendored the way +# .config/omo/skills/README.md describes (path rewrite, OmO routing line) +# web-design-guidelines vercel-labs/agent-skills main, when its content changes +# Local to this machine (~/.omo/agent/skills, not in git): +# vercel-react-best-practices vercel-labs/agent-skills skills/react-best-practices +# docx pdf pptx xlsx skill-creator anthropics/skills skills/ +# deep-research came from Claude's skill sync and has no public upstream, so it is not updated. +# +# Only the skill paths are committed, and the push happens only when main was level with +# origin/main, so unrelated local edits and unpushed commits are never published. A skill whose +# update fails is restored to its previous copy, and the run exits non-zero. +# +# Env overrides (for testing): DOTFILES, SKILLS, XDG_STATE_HOME. + +set -uo pipefail + +DOTFILES=${DOTFILES:-$HOME/dotfiles} +VENDORED=$DOTFILES/.config/omo/skills +SKILLS=${SKILLS:-$HOME/.omo/agent/skills} +STATE=${XDG_STATE_HOME:-$HOME/.local/state}/skills-update +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +failures=0 +msg=() + +log() { printf '%s\n' "$*"; } +fail() { + log "FAIL: $*" + failures=$((failures + 1)) +} + +install_timer() { + local dir=${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user + mkdir -p "$dir" + cat >"$dir/skills-update.service" <"$dir/skills-update.timer" </dev/null 2>&1 || log "impeccable: engine $engine download failed; the launcher retries on first use" + msg+=("impeccable: $tag, engine $engine ($sha)") + log "impeccable: vendored $tag (engine $engine)" +} + +update_web_design_guidelines() { + local sha src dst=$VENDORED/web-design-guidelines + sha=$(head_sha vercel-labs/agent-skills main) || return 2 + if [ "$sha" = "$(readme_rev web-design-guidelines)" ]; then + log "web-design-guidelines: up to date" + return 1 + fi + src=$(fetch vercel-labs/agent-skills "$sha") || return 2 + src=$src/skills/web-design-guidelines + [ -f "$src/SKILL.md" ] || return 2 + rsync -a --delete "$src/" "$dst/" + if ! git -C "$DOTFILES" status --porcelain -- "$dst" | grep -q .; then + log "web-design-guidelines: content unchanged at ${sha:0:7}" + return 1 + fi + sed -i -E "/^\| \`web-design-guidelines\` \|/s/\`[0-9a-f]{40}\`/\`$sha\`/" "$VENDORED/README.md" + msg+=("web-design-guidelines: vercel-labs/agent-skills $sha") + log "web-design-guidelines: updated to ${sha:0:7}" +} + +update_vendored() { + cd "$DOTFILES" || { fail "no dotfiles checkout at $DOTFILES"; return; } + [ "$(git branch --show-current)" = main ] || { fail "dotfiles is not on main; vendored skills skipped"; return; } + if git status --porcelain -- .config/omo/skills | grep -q .; then + fail "uncommitted edits under .config/omo/skills; vendored skills skipped" + return + fi + git fetch -q origin || { fail "git fetch failed; vendored skills skipped"; return; } + local ahead behind name rc + read -r ahead behind < <(git rev-list --left-right --count main...origin/main) + if [ "$behind" -gt 0 ] && ! git merge -q --ff-only origin/main; then + fail "dotfiles cannot fast-forward to origin/main; vendored skills skipped" + return + fi + for name in impeccable web-design-guidelines; do + cp "$VENDORED/README.md" "$work/README.md.before" + "update_${name//-/_}" + rc=$? + if [ "$rc" -ge 2 ]; then + git checkout -q -- ".config/omo/skills/$name" + git clean -fdq -- ".config/omo/skills/$name" + cp "$work/README.md.before" "$VENDORED/README.md" + fail "$name: update failed (exit $rc); kept the vendored copy" + fi + done + [ "${#msg[@]}" -gt 0 ] || return + git add -- .config/omo/skills + git commit -q -m "chore(omo): update vendored skills" -m "$(printf '%s\n' "${msg[@]}")" -- .config/omo/skills || + { fail "dotfiles commit failed"; return; } + if [ "$ahead" -gt 0 ]; then + log "dotfiles: main had $ahead unpushed commits, so $(git rev-parse --short HEAD) stays local" + elif git push -q origin main; then + log "dotfiles: pushed $(git rev-parse --short HEAD)" + else + fail "dotfiles push failed; $(git rev-parse --short HEAD) is committed locally" + fi +} + +update_local() { + local name=$1 repo=$2 path=$3 dst=$SKILLS/$1 sha src + if [ -L "$dst" ]; then + log "$name: links into the dotfiles, skipped" + return + fi + sha=$(head_sha "$repo" main) || { fail "$name: cannot read $repo"; return; } + if [ "$sha" = "$(cat "$STATE/$name" 2>/dev/null)" ]; then + log "$name: up to date" + return + fi + src=$(fetch "$repo" "$sha") || { fail "$name: download of $repo failed"; return; } + src=$src/$path + [ -f "$src/SKILL.md" ] || { fail "$name: $repo has no $path/SKILL.md"; return; } + mkdir -p "$dst" + if diff -rq "$src" "$dst" >/dev/null 2>&1; then + log "$name: content unchanged at ${sha:0:7}" + else + rsync -a --delete "$src/" "$dst/" + log "$name: updated to $repo@${sha:0:7}" + fi + printf '%s\n' "$sha" >"$STATE/$name" +} + +mkdir -p "$STATE" +update_vendored +update_local vercel-react-best-practices vercel-labs/agent-skills skills/react-best-practices +for name in docx pdf pptx xlsx skill-creator; do + update_local "$name" anthropics/skills "skills/$name" +done +log "deep-research: no public upstream, not updated" +[ "$failures" -eq 0 ] diff --git a/README.md b/README.md index 94b3645..484ae2d 100644 --- a/README.md +++ b/README.md @@ -235,8 +235,9 @@ Custom scripts and binaries are stored in `.local/bin/`: - `docker-nuke`: Force-restart a wedged Docker Desktop, capturing diagnostics to `~/.local/state/docker-nuke/` first (`--dry-run` to preview, `--no-restart` to leave it down, `--deep` for root helpers) - `impeccable-browser`: The browser for Impeccable's URL scans (`impeccable detect `) on flowstate-remote, set as `IMPECCABLE_BROWSER` in the server's `~/.zshenv`. Public URLs render on Browserless. Its key, `BROWSERLESS_API_KEY` from the Reoclo project `flowstate-tools`, is cached for 12 hours in `~/.impeccable/browserless.token`. Localhost and private addresses reach Browserless through a cloudflared quick tunnel that lasts only for the scan, and reports still name the local URL. Other launches, and any scan whose tunnel or Browserless connection fails, run on the headless Chrome in the Puppeteer cache with `--no-sandbox`, because the server's AppArmor blocks Chrome's sandbox. `IMPECCABLE_TUNNEL=0` keeps private addresses on local Chrome, which pages that call a second local port (a separate API, say) need. Every decision is logged to `~/.impeccable/browser.log` +- `skills-update`: Refreshes omo's skills from upstream every day on flowstate-remote, from a systemd user timer that `skills-update --install-timer` writes and enables (05:00 UTC). The vendored `impeccable` (newest `skill-v*` release) and `web-design-guidelines` are re-vendored as [the skills README](.config/omo/skills/README.md) describes, then committed and pushed. Only the skill paths are committed, and the push happens only when `main` was level with `origin/main`. The machine-local `vercel-react-best-practices`, `docx`, `pdf`, `pptx`, `xlsx` and `skill-creator` copies in `~/.omo/agent/skills` are synced from `vercel-labs/agent-skills` and `anthropics/skills`. A failed update keeps the previous copy and marks the run failed (`journalctl --user -u skills-update`) -The `links` step of `setup-mac.sh` links each script into `~/.local/bin`, which `.zshrc` puts on your PATH. `pbcopy` and `pbpaste` (OSC 52 clipboard shims for headless servers) and `impeccable-browser` (Linux only) are not linked on macOS. +The `links` step of `setup-mac.sh` links each script into `~/.local/bin`, which `.zshrc` puts on your PATH. `pbcopy` and `pbpaste` (OSC 52 clipboard shims for headless servers), `impeccable-browser` and `skills-update` (Linux only) are not linked on macOS. ### Local Servers diff --git a/setup-mac.sh b/setup-mac.sh index 5764e82..3046db0 100755 --- a/setup-mac.sh +++ b/setup-mac.sh @@ -198,7 +198,7 @@ step_links() { name=${script##*/} case $name in pbcopy | pbpaste) continue ;; # OSC 52 clipboard shims for headless servers - impeccable-browser) continue ;; # Linux-only browser shim for flowstate-remote + impeccable-browser | skills-update) continue ;; # Linux-only tools for flowstate-remote esac if [[ -x $script ]]; then link ".local/bin/$name" ".local/bin/$name"; fi done