From a458bc222ca75d84045a0effafececefea9586fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:59:04 +0000 Subject: [PATCH 01/19] delphi: sync .delphi/ from main --- .delphi/.gitattributes | 2 + .delphi/.gitignore | 3 + .delphi/manifest.yml | 22 ++++ .../0.0.1-linked-worktrees-to-clones.sh | 20 +++ .delphi/migrations/0.1.0-path-names.sh | 29 +++++ .delphi/new-worktree.sh | 23 ++++ .delphi/park.sh | 38 ++++++ .delphi/paths.sh | 15 +++ .delphi/plugins.sh | 78 +++++++++++ .delphi/setup.sh | 123 ++++++++++++++++++ .delphi/switch.sh | 38 ++++++ .delphi/version.sh | 23 ++++ .delphi/versions | 14 ++ 13 files changed, 428 insertions(+) create mode 100644 .delphi/.gitattributes create mode 100644 .delphi/.gitignore create mode 100644 .delphi/manifest.yml create mode 100644 .delphi/migrations/0.0.1-linked-worktrees-to-clones.sh create mode 100644 .delphi/migrations/0.1.0-path-names.sh create mode 100755 .delphi/new-worktree.sh create mode 100755 .delphi/park.sh create mode 100644 .delphi/paths.sh create mode 100755 .delphi/plugins.sh create mode 100755 .delphi/setup.sh create mode 100755 .delphi/switch.sh create mode 100644 .delphi/version.sh create mode 100644 .delphi/versions diff --git a/.delphi/.gitattributes b/.delphi/.gitattributes new file mode 100644 index 0000000..49724b5 --- /dev/null +++ b/.delphi/.gitattributes @@ -0,0 +1,2 @@ +# LF, so these scripts run in Git Bash even with core.autocrlf=true +* text eol=lf diff --git a/.delphi/.gitignore b/.delphi/.gitignore new file mode 100644 index 0000000..918f4a9 --- /dev/null +++ b/.delphi/.gitignore @@ -0,0 +1,3 @@ +# Parked workspaces (.delphi/park.sh): this clone's own, never committed. Ignored files are +# what `git clean -X` and `git stash --all` take, so don't run those at the workspace root. +/parked/ diff --git a/.delphi/manifest.yml b/.delphi/manifest.yml new file mode 100644 index 0000000..aa8eb88 --- /dev/null +++ b/.delphi/manifest.yml @@ -0,0 +1,22 @@ +# .delphi/manifest.yml: the files that make up a Delphi version, identical on main and on every ws/* +# branch (sync puts main's .delphi/ on each). Change any of them, this file included, and you make a +# new version: bump `version` below, then add the line `bash .delphi/version.sh` prints, plus what +# changed, to .delphi/versions (ci/check.sh fails until you do, and says so). MAJOR.MINOR.PATCH: +# PATCH fixes and wording +# MINOR something new that existing clones need nothing for +# MAJOR clones need cleanup (MINOR while 0.x): add .delphi/migrations/-.sh, which +# setup.sh runs once per workspace in each clone (rules in Delphi's CLAUDE.md) +version: 0.7.0 +files: # repo-relative paths; a trailing / means everything under it + - .delphi/manifest.yml + - .delphi/.gitattributes + - .delphi/.gitignore + - .delphi/version.sh + - .delphi/setup.sh + - .delphi/new-worktree.sh + - .delphi/paths.sh + - .delphi/plugins.sh + - .delphi/switch.sh + - .delphi/park.sh + - .delphi/migrations/ + - .github/workflows/delphi.yml diff --git a/.delphi/migrations/0.0.1-linked-worktrees-to-clones.sh b/.delphi/migrations/0.0.1-linked-worktrees-to-clones.sh new file mode 100644 index 0000000..fa54838 --- /dev/null +++ b/.delphi/migrations/0.0.1-linked-worktrees-to-clones.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# 0.0.1: linked/ used to be a worktree of this clone (.delphi/link.sh); setup.sh now makes it a +# read-only copy. Deletes each old one, and its registration in .git/worktrees/, if nothing in it can +# be lost: no uncommitted or untracked files, and its commit is on a branch. setup.sh then makes it a +# copy. Stops, changing nothing more, at the first one that isn't safe to delete. +set -euo pipefail +for d in linked/*/; do + d=${d%/} + [ -f "$d/.git" ] || continue # a read-only copy already, or no checkout at all + admin=$(git -C "$d" rev-parse --absolute-git-dir 2>/dev/null) || admin= + case "$admin" in */.git/worktrees/*) ;; *) + echo "migrate: $d is a broken worktree; move out anything you need, delete it, re-run setup" >&2 && exit 1 ;; + esac + [ -z "$(git -C "$d" status --porcelain)" ] || + { echo "migrate: $d has uncommitted changes; commit or move them, then re-run setup" >&2 && exit 1; } + [ -n "$(git -C "$d" for-each-ref --count=1 --contains HEAD refs/heads refs/remotes)" ] || + { echo "migrate: $d has commits on no branch; git -C $d switch -c , then re-run setup" >&2 && exit 1; } + rm -rf "$d" "$admin" + echo "migrate: removed $d, an old link.sh worktree (nothing lost); setup.sh makes it a read-only copy" +done diff --git a/.delphi/migrations/0.1.0-path-names.sh b/.delphi/migrations/0.1.0-path-names.sh new file mode 100644 index 0000000..fe8fa4f --- /dev/null +++ b/.delphi/migrations/0.1.0-path-names.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# 0.1.0: workspaces are named by their place in the tree (ws/software/application-software/argos/argos-dev, +# was ws/argos-dev), and `links:` is `workspaces:` (`- ws/` or `- main`), copied to linked/. +# Moves what this clone keeps under old names to the new ones, for every workspace on main: parked +# slots (.delphi/parked/ and, from before that, .git/delphi/) to .delphi/parked/, and +# read-only copies linked/ to linked/ws/, here and in each parked slot. Only renames; a +# name taken at both places is reported and left. Your local ws/ branch stays; delete it once +# you're on ws/. +set -euo pipefail +git fetch -q origin main 2>/dev/null || echo "migrate: could not fetch main; using the last fetched" >&2 +git rev-parse -q --verify refs/remotes/origin/main >/dev/null || + { echo "migrate: no origin/main here; git fetch origin main, then re-run setup" >&2 && exit 1; } +names=$(git ls-tree -r --name-only origin/main -- software | + sed -nE 's#^(software/(.+/)?)workspaces/([^/]+)/workspace\.yml$#\1\3#p') +move() { # + [ -e "$1" ] || return 0 + if [ -e "$2" ]; then echo "migrate: $1 and $2 both exist; left $1 (move what you need by hand)" >&2 && return 0; fi + mkdir -p "$(dirname "$2")" && mv "$1" "$2" && echo "migrate: moved $1 to $2" +} +git_dir=$(git rev-parse --absolute-git-dir) +for n in $names; do + move ".delphi/parked/${n##*/}" ".delphi/parked/$n" + move "$git_dir/delphi/${n##*/}" ".delphi/parked/$n" + git config --remove-section "delphi.${n##*/}" 2>/dev/null || true # its old version record +done +rmdir "$git_dir/delphi" 2>/dev/null || true +for slot in . $(for n in $names; do [ ! -d ".delphi/parked/$n" ] || echo ".delphi/parked/$n"; done); do + for n in $names; do move "$slot/linked/${n##*/}" "$slot/linked/ws/$n"; done +done diff --git a/.delphi/new-worktree.sh b/.delphi/new-worktree.sh new file mode 100755 index 0000000..7760f90 --- /dev/null +++ b/.delphi/new-worktree.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# .delphi/new-worktree.sh []: create or reuse worktrees// from +# the store .delphi/setup.sh made at repos/, and print its path. An existing branch +# (local or on origin) is checked out; a new one starts from (default: origin's default +# branch). Runs on bash 3.2 and Git Bash. +set -euo pipefail +# shellcheck source-path=SCRIPTDIR source=paths.sh +. "$(dirname "$0")/paths.sh" +repo=${1:?usage: new-worktree.sh []} branch=${2:?usage: new-worktree.sh []} +base=${3:-origin/HEAD} dest="$DELPHI_WORKTREES/$repo/$branch" +g() { git -C "$DELPHI_REPOS/$repo" "$@"; } +[ -d "$DELPHI_REPOS/$repo" ] || + { echo "new-worktree: no repos/$repo (list it under repos: and run .delphi/setup.sh; references are read-only)" >&2 && exit 1; } +g fetch -q origin +if [ -d "$dest" ]; then : +elif g rev-parse -q --verify "refs/heads/$branch" >/dev/null; then + g worktree add -q "$dest" "$branch" +elif g rev-parse -q --verify "refs/remotes/origin/$branch" >/dev/null; then + g worktree add -q --track -b "$branch" "$dest" "origin/$branch" +else + g worktree add -q --no-track -b "$branch" "$dest" "$base" +fi +echo "$dest" diff --git a/.delphi/park.sh b/.delphi/park.sh new file mode 100755 index 0000000..cafb387 --- /dev/null +++ b/.delphi/park.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# .delphi/park.sh : run by the post-checkout hook .delphi/setup.sh writes, +# from the worktree that checked out. .delphi/ is on every branch, main included, so the hook always +# finds it, the version just checked out. When a checkout moves this clone to another workspace +# (the `name:` in workspace.yml, its place in the tree; none on main), it parks the old workspace's +# root folders (the ones .delphi/paths.sh names, .journal/, and those under `park:` in the workspace.yml +# of the branch it leaves) at .delphi/parked// (git-ignored by .delphi/.gitignore), and brings +# back everything parked for the new one. A name from before workspaces were named by +# path (argos-dev) is the same workspace as the path ending in it, so moving onto the renamed branch +# moves nothing. Moves are renames, so every path is the same again once its workspace is checked +# out. Runs on bash 3.2 and Git Bash. +set -euo pipefail +[ "${3:-0}" = 1 ] || exit 0 +name() { git show "$1:workspace.yml" 2>/dev/null | awk '/^name:/ { print $2; exit }' || true; } +old=$(name "$1") new=$(name "$2") +[ "$old" != "$new" ] || exit 0 +case "$old" in */*) ;; ?*) [ "$old" != "${new##*/}" ] || exit 0 ;; esac # renamed by path: still the same workspace +case "$new" in */*) ;; ?*) [ "$new" != "${old##*/}" ] || exit 0 ;; esac +top=$(git rev-parse --show-toplevel) +park=$top/.delphi/parked +parks() { # : the root folders its workspace parks + echo repos worktrees references linked .journal + git show "$1:workspace.yml" 2>/dev/null | awk '/^[^[:space:]#]/ { s = /^park:/; next } + s && /^[[:space:]]+-/ { sub(/^[[:space:]]+-[[:space:]]+/, ""); sub(/[[:space:]]*#.*$/, ""); print }' || true +} +[ -z "$old" ] || for d in $(parks "$1"); do + case "$d" in */* | . | .. | .git | .delphi) echo "delphi: won't park '$d' (park: takes folder names at the root)" >&2 && continue ;; esac + [ -e "$top/$d" ] || continue + if [ -e "$park/$old/$d" ]; then echo "delphi: $park/$old/$d exists; left $d/ in place" >&2 + else mkdir -p "$park/$old" && mv "$top/$d" "$park/$old/$d" || echo "delphi: could not park $d/" >&2; fi +done +shopt -s nullglob dotglob +[ -z "$new" ] || for src in "$park/$new"/*; do + d=${src##*/} + if [ -e "$top/$d" ]; then echo "delphi: $d/ is in the way; $new's is still at $src" >&2 + else mv "$src" "$top/$d" || echo "delphi: could not restore $d/ from $src" >&2; fi +done +[ -z "$new" ] || (cd "$park" 2>/dev/null && rmdir -p "$new" 2>/dev/null) || true # once all back, drop the empty slot diff --git a/.delphi/paths.sh b/.delphi/paths.sh new file mode 100644 index 0000000..1c0c27e --- /dev/null +++ b/.delphi/paths.sh @@ -0,0 +1,15 @@ +# .delphi/paths.sh: the workspace layout, in one place. Sourced, it sets and exports DELPHI_ROOT (this +# clone's top, from this file's place, never by searching) and the folders under it; run, it prints +# them as one export line. Each harness's session-start hook writes that line into every shell (Claude +# Code's is in the generated .claude/settings.json), so skill scripts read the workspace from +# $DELPHI_ROOT and take only names; in a terminal, `. .delphi/paths.sh` from the root. Bash 3.2, Git +# Bash and zsh safe. +# shellcheck shell=bash +DELPHI_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd) +DELPHI_REPOS=$DELPHI_ROOT/repos # bare stores: repos/ +DELPHI_WORKTREES=$DELPHI_ROOT/worktrees # checkouts: worktrees// +DELPHI_REFERENCES=$DELPHI_ROOT/references # read-only dependency sources: references/ +DELPHI_LINKED=$DELPHI_ROOT/linked # read-only Delphi copies: linked/ +export DELPHI_ROOT DELPHI_REPOS DELPHI_WORKTREES DELPHI_REFERENCES DELPHI_LINKED +[ "${BASH_SOURCE[0]:-}" != "$0" ] || printf 'export DELPHI_ROOT=%q DELPHI_REPOS=%q DELPHI_WORKTREES=%q DELPHI_REFERENCES=%q DELPHI_LINKED=%q\n' \ + "$DELPHI_ROOT" "$DELPHI_REPOS" "$DELPHI_WORKTREES" "$DELPHI_REFERENCES" "$DELPHI_LINKED" diff --git a/.delphi/plugins.sh b/.delphi/plugins.sh new file mode 100755 index 0000000..7449048 --- /dev/null +++ b/.delphi/plugins.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# .delphi/plugins.sh: setup.sh runs it to keep this clone's Delphi plugins in step with this +# workspace, so a plugin change on main reaches everyone on their next setup, switch or pull, with no +# migration of its own. Of the `@delphi` plugins (the Delphi marketplace's; others are only warned +# about), this workspace's generated .claude/settings.json is the list: it installs the ones it +# enables but this clone hasn't (claude plugin install --scope local, for this root), uninstalls this +# root's local installs it doesn't enable (a switch leaves the last workspace's behind, still enabled +# in the git-ignored .claude/settings.local.json), and uninstalls installed ones the marketplace no +# longer has (Claude Code marks them plugin-not-found), at any scope. Other clones' installs and +# user-scope ones the marketplace still has are never touched. It refreshes the marketplace before +# installing or uninstalling what it dropped, only then; if it can't (offline), it does only the +# local uninstalls and warns about the rest. Installed means at user scope, or at project or local +# scope for this root. Says what it did in one line; prints nothing when all is in order or it can't +# tell. Only `harness: claude-code` so far. Finds the workspace from its own place, never by +# searching. Bash 3.2, Git Bash safe. +set -euo pipefail +cd "$(dirname "$0")/.." +[ -f workspace.yml ] && [ "$(awk '/^harness:/ { print $2; exit }' workspace.yml)" = claude-code ] || exit 0 +command -v claude >/dev/null || exit 0 +enabled=$(sed -nE 's/^[[:space:]]*"([^"]+@[^"]+)": true,?$/\1/p' .claude/settings.json 2>/dev/null || true) +root=$(pwd -W 2>/dev/null || pwd) # Git Bash: the Windows path Claude Code records + +# ` ` for each plugin installed for this root (gone: 1 if its marketplace lost it) +installed() { + claude plugin list --json 2>/dev/null | awk -v root="$root" ' + function val(l) { sub(/^[^:]*:[[:space:]]*"/, "", l); sub(/"[^"]*$/, "", l); gsub(/\\\\/, "/", l); return l } + depth == 1 && /"id":/ { id = val($0); scope = path = ""; gone = 0 } + depth == 1 && /"scope":/ { scope = val($0) } + depth == 1 && /"projectPath":/ { path = val($0) } + /"type":[[:space:]]*"plugin-not-found"/ { gone = 1 } + { opened = gsub(/\{/, "{"); closed = gsub(/\}/, "}"); depth += opened - closed } + closed && depth == 0 && id != "" { if (scope == "user" || tolower(path) == tolower(root)) print id, scope, gone; id = "" }' +} +# What's left to do: `install `, `drop ` (a local install here this workspace doesn't enable), +# `uninstall ` (gone from the marketplace), `missing ` (another marketplace's) +todo() { + local have + have=$(installed) || return 1 + for p in $enabled; do + echo "$have" | awk -v p="$p" '$1 == p { f = 1 } END { exit !f }' && continue + case $p in *@delphi) echo "install $p" ;; *) echo "missing $p" ;; esac + done + echo "$have" | awk -v on=" ${enabled//$'\n'/ } " '$1 ~ /@delphi$/ { + if ($2 == "local" && !index(on, " " $1 " ")) print "drop", $1; else if ($3 == 1) print "uninstall", $1, $2 }' +} +work=$(todo) || exit 0 +[ -n "$work" ] || exit 0 +online="" +if echo "$work" | grep -qE '^(un)?install ' && claude plugin marketplace update delphi >/dev/null 2>&1; then + online=1 + work=$(todo) || exit 0 # again, against the marketplace as it is now +fi +did="" left="" +while read -r verb id scope; do + case $verb in + drop) claude plugin uninstall "$id" --scope local >/dev/null 2>&1 && + did="$did; uninstalled $id (this workspace doesn't enable it)" || left="$left +drop $id" ;; + install) [ -n "$online" ] && claude plugin install "$id" --scope local >/dev/null 2>&1 && + did="$did; installed $id" || left="$left +missing $id" ;; + uninstall) [ -n "$online" ] && claude plugin uninstall "$id" --scope "$scope" >/dev/null 2>&1 && + did="$did; uninstalled $id (gone from the Delphi marketplace)" || left="$left +uninstall $id" ;; + *) left="$left +$verb $id" ;; + esac +done <<<"$work" +[ -z "$did" ] || echo "delphi: plugins: ${did#; }; run /reload-plugins in Claude Code (or restart it) to load the change" +missing=$(echo "$left" | awk '$1 == "missing" || $1 == "install" { print $2 }' | paste -sd " " -) +stale=$(echo "$left" | awk '$1 == "uninstall" { print $2 }' | paste -sd " " -) +extra=$(echo "$left" | awk '$1 == "drop" { print $2 }' | paste -sd " " -) +[ -z "$missing" ] || + echo "delphi: this workspace's plugins $missing aren't installed here, so their skills are missing; install each with: claude plugin install --scope local" +[ -z "$stale" ] || + echo "delphi: plugins $stale are gone from the Delphi marketplace; uninstall each with: claude plugin uninstall --scope " +[ -z "$extra" ] || + echo "delphi: plugins $extra are installed here but this workspace doesn't enable them; uninstall each with: claude plugin uninstall --scope local" diff --git a/.delphi/setup.sh b/.delphi/setup.sh new file mode 100755 index 0000000..ecb2a62 --- /dev/null +++ b/.delphi/setup.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# .delphi/setup.sh: run in a checkout of ws/ (the workspace root; .delphi/ comes from Delphi's +# main, which sync keeps on every ws/* branch, so edit it only there; on main this does nothing). For each repo listed under +# `repos:` in workspace.yml, fetches it into a bare store at repos/ (unless present) and checks +# out its default branch as the first worktree, worktrees//. For each repo under +# `references-git:` (read-only dependency sources, `: []`), checks out a shallow, +# detached copy of (default: origin's default branch) at references/, refreshed on every +# run, with pushes disabled. Each entry under `workspaces:` (`- ws/` or `- main`, a workspace +# being named by its place in the tree) gets the same kind of copy of that branch of this clone's +# origin (Delphi), at linked/; to edit one, switch this clone to it (.delphi/switch.sh). Adds the +# folders park.sh parks and macOS's .DS_Store to this clone's .git/info/exclude, and writes its post-checkout hook, which runs .delphi/park.sh (on +# every branch) as just checked out in the worktree that checked out (unless another hook is there), so switching +# to another workspace or main parks those folders. First +# it runs the .delphi/migrations/-*.sh this workspace hasn't (oldest first; the last one done +# is `git config delphi..version`, 0.0.0 if unset), stopping at one that fails, and warns if +# .delphi/ isn't exactly the version it says (.delphi/version.sh). Last, it brings this machine's Delphi plugins in +# step with the workspace (.delphi/plugins.sh: installs what it enables, uninstalls local ones it doesn't and what the marketplace dropped). Safe to re-run. Keep it bash 3.2 +# (macOS) and Git Bash safe: no bash-4 features, POSIX awk only. +set -euo pipefail +cd "$(dirname "$0")/.." +[ -f workspace.yml ] || { echo "setup: no workspace.yml here (on main?); nothing to set up" >&2 && exit 0; } +self=$(awk '/^name:/ { print $2; exit }' workspace.yml) +case "$self" in "" | [-/]* | */ | *//* | *[!a-z0-9/-]*) echo "setup: workspace.yml needs a valid name:" >&2 && exit 1 ;; esac + +older() { # : a is a lower MAJOR.MINOR.PATCH than b + [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | head -n 1)" = "$1" ] +} +here=$(bash .delphi/version.sh) released=$(grep -v '^#' .delphi/versions | grep . | tail -n 1 | cut -d' ' -f1,2 || true) +[ "$here" = "$released" ] || + echo "setup: .delphi/ is not Delphi ${released%% *} as released (edited here? change it only through main)" >&2 +have=$(git config "delphi.$self.version" || echo 0.0.0) +pending=$(for f in .delphi/migrations/*.sh; do [ ! -e "$f" ] || echo "${f##*/}"; done | sort -t. -k1,1n -k2,2n -k3,3n) +while IFS= read -r f; do + v=${f%%-*} + [ -n "$f" ] || continue + older "$have" "$v" || continue + echo "setup: migrating $self to $v (.delphi/migrations/$f)" + bash ".delphi/migrations/$f" &2 && exit 1; } + git config "delphi.$self.version" "$v" && have=$v +done < []: shallow, detached, push-disabled checkout at + local dir=$1 + [ ! -f "$dir/.git" ] || { echo "setup: $dir is a worktree, not a read-only copy; skipping it" >&2 && return; } + [ -d "$dir/.git" ] || { git init -q "$dir" && git -C "$dir" remote add origin "$2"; } + git -C "$dir" remote set-url origin "$2" + git -C "$dir" remote set-url --push origin DISABLED-read-only-copy + if git -C "$dir" fetch -q --depth 1 origin "${3:-HEAD}" &2 + fi +} + +link() { # ws/|main: a read-only copy of that branch of Delphi at linked/ + case "$1" in main | ws/[a-z0-9]*) ;; *) echo "setup: skipping bad workspaces: entry '$1'" >&2 && return ;; esac + case "$1" in "ws/$self") return ;; */ | *//* | *[!a-z0-9/-]*) echo "setup: skipping bad workspaces: entry '$1'" >&2 && return ;; esac + readonly_copy "linked/$1" "$(git remote get-url origin)" "$1" +} + +while read -r kind name url ref; do + [ -n "$name" ] || continue + if [ "$kind" = link ]; then link "$name"; continue; fi + case "$name" in . | .. | *[!A-Za-z0-9._-]*) echo "setup: skipping unsafe repo name '$name'" >&2 && continue ;; esac + if [ "$kind" = ref ]; then readonly_copy "references/$name" "$url" ${ref:+"$ref"} && continue; fi + store=repos/$name + if [ -e "$store" ]; then + echo "repos/$name: already present" + else + git init -q --bare "$store" + git -C "$store" remote add origin "$url" + git -C "$store" fetch -q origin /dev/null &2 && continue; } + def=${def#origin/} + dest=worktrees/$name/$def + if [ -e "$dest" ]; then continue + elif git -C "$store" rev-parse -q --verify "refs/heads/$def" >/dev/null; then + git -C "$store" worktree add -q "$PWD/$dest" "$def" || + echo "setup: could not check out $dest (is $def checked out in repos/$name?)" >&2 + else + git -C "$store" worktree add -q --track -b "$def" "$PWD/$dest" "origin/$def" + fi +done </dev/null || echo "$d" >>"$exclude" +done +hook=$(git rev-parse --git-path hooks/post-checkout) +link=$(readlink "$hook" 2>/dev/null || true) # ours: this script, or an old link to (or copy of) a park.sh +if [ "${link%/.delphi/park.sh}" != "$link" ] || { [ -z "$link" ] && { [ ! -e "$hook" ] || grep -qF '.delphi/park.sh' "$hook"; }; }; then + # A script, not a link: the clone's worktrees share one hook, and a link would run one checkout's park.sh for all + mkdir -p "$(dirname "$hook")" && rm -f "$hook" && cat >"$hook" <<'EOF3' && chmod +x "$hook" +#!/bin/sh +# Delphi's post-checkout hook, written by .delphi/setup.sh: runs .delphi/park.sh as just checked out, +# in the worktree that checked out (git runs hooks at its root), so each runs its own branch's. +[ ! -f .delphi/park.sh ] || exec bash .delphi/park.sh "$@" +EOF3 +else + echo "setup: $hook is another hook; not installing .delphi/park.sh, so checkouts won't park repos/" >&2 +fi +bash .delphi/plugins.sh >&2 || true # never a failure diff --git a/.delphi/switch.sh b/.delphi/switch.sh new file mode 100755 index 0000000..0e6afb7 --- /dev/null +++ b/.delphi/switch.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# .delphi/switch.sh |main: move this clone to Delphi's ws/ (or main). A workspace +# is named by its place in the tree (software/application-software/argos/argos-dev; ws/ in front is +# fine), or by its last part alone (argos-dev) if only one workspace ends in it. Refuses if +# tracked files have changes (they would follow the switch), fetches the branch, switches to it (the +# local branch, else a new one tracking origin), fast-forwards it to origin, then runs the new +# workspace's .delphi/setup.sh (none for main). The post-checkout hook (.delphi/park.sh) parks this workspace's +# repos/, worktrees/, references/ and linked/ and restores the new one's. Runs on bash 3.2 and Git Bash. +# .delphi/switch.sh --list: fetch the ws/* branches, then print main and each workspace's name, one a +# line, `* ` before this clone's and two spaces before the others (the delphi mod's /ws-delphi pane reads it). +set -euo pipefail +{ # one block, so bash has read all of it before the switch replaces this file + cd "$(dirname "$0")/.." + if [ "${1:-}" = --list ]; then + git fetch -q --prune origin '+refs/heads/ws/*:refs/remotes/origin/ws/*' || + echo "switch: could not fetch the ws/* branches; listing the ones last fetched" >&2 + { echo main && git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ws/; } | + awk -v here="$(git branch --show-current)" '$0 == "main" || /^ws\/.*\// { # not ws/: an old name + n = $0; sub(/^ws\//, "", n); print ($0 == here ? "* " : " ") n }' + exit + fi + name=${1:?usage: switch.sh |main} + name=${name#ws/} ref=ws/${name#ws/} + case "$name" in main) ref=main ;; "" | [-/]* | */ | *//* | *[!a-z0-9/-]*) echo "switch: bad workspace name '$1'" >&2 && exit 1 ;; esac + if [ "$ref" != main ] && [ "$name" = "${name##*/}" ]; then # its last part alone: find the one workspace it names + git fetch -q origin '+refs/heads/ws/*:refs/remotes/origin/ws/*' || { echo "switch: could not fetch the ws/* branches" >&2 && exit 1; } + ref=$(git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ws/ | grep "^ws/.*/$name\$" || true) # not ws/: an old name + [ -n "$ref" ] && [ "$(echo "$ref" | wc -l)" -eq 1 ] || + { echo "switch: no one workspace ends in '$name' (found: $(echo "${ref:-none}" | tr '\n' ' ')); give its full name" >&2 && exit 1; } + fi + git diff --quiet HEAD -- || { echo "switch: commit or set aside your changes first (git status)" >&2 && exit 1; } + git fetch -q origin "+refs/heads/$ref:refs/remotes/origin/$ref" || { echo "switch: could not fetch $ref" >&2 && exit 1; } + git switch -q "$ref" + git merge -q --ff-only "origin/$ref" 2>/dev/null || echo "switch: $ref has diverged from origin; not fast-forwarded" >&2 + [ ! -f workspace.yml ] || bash .delphi/setup.sh + echo "switch: on $ref" + exit +} diff --git a/.delphi/version.sh b/.delphi/version.sh new file mode 100644 index 0000000..90d982a --- /dev/null +++ b/.delphi/version.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# .delphi/version.sh [--files]: prints this checkout's Delphi version, ` `: the +# `version:` in .delphi/manifest.yml and a hash of the files it lists (their paths and contents as git +# stores them, so line endings don't change it; tracked files only). The last line of .delphi/versions +# must match it (ci/check.sh); setup.sh warns when it doesn't. --files: just the listed files, one per +# line (a listed path git doesn't track is printed as is). Keep it bash 3.2 (macOS) and Git Bash safe. +set -euo pipefail +cd "$(dirname "$0")/.." +m=.delphi/manifest.yml +files() { + awk '/^[[:space:]]*#/ { next } /^files:/ { s = 1; next } /^[^[:space:]]/ { s = 0 } + s && /^[[:space:]]+-/ { sub(/^[[:space:]]+-[[:space:]]+/, ""); sub(/[[:space:]]+#.*$/, ""); print }' "$m" | + while IFS= read -r p; do + l=$(git ls-files -- "$p") + if [ -n "$l" ]; then echo "$l"; else echo "$p"; fi + done | LC_ALL=C sort -u +} +[ "${1:-}" != --files ] || { files && exit; } +version=$(awk '/^version:/ { print $2; exit }' "$m") +hash=$(files | while IFS= read -r f; do + echo "$(git hash-object -- "$f" 2>/dev/null || echo missing) $f" +done | git hash-object --stdin | cut -c1-12) +echo "${version:-none} $hash" diff --git a/.delphi/versions b/.delphi/versions new file mode 100644 index 0000000..cab1212 --- /dev/null +++ b/.delphi/versions @@ -0,0 +1,14 @@ +# .delphi/versions: one line per Delphi version, oldest first: ` `, the +# hash being that of the files .delphi/manifest.yml lists. Only add lines; `bash .delphi/version.sh` +# prints the first two fields for the files as they are now. +0.0.1 1fc1890f7d00 versions and migrations; linked/ are read-only copies, not link.sh worktrees (migration) +0.1.0 6522fea335e8 workspaces named by their place in the tree (ws/software/.../); links: is workspaces: (- ws/ | - main) (migration) +0.1.1 4829c508fd00 the post-checkout hook is a script running each worktree's own .delphi/park.sh, not a link to one checkout's +0.2.0 311f7b23606c .delphi/paths.sh is the workspace layout (DELPHI_ROOT, DELPHI_WORKTREES, …): sourced, it exports it; run, it prints the export line a session hook puts in every shell +0.3.0 2c870f633c30 workspace-inherit/ folders at any level of software/ reach every workspace below on each sync (opt-out: in workspace.yml); defaults/ is workspace-defaults/ +0.4.0 ba662e754219 setup.sh warns (.delphi/plugins.sh) about enabled plugins not installed here; CLAUDE.md says how to install them +0.4.1 c5e413632e1c setup.sh also excludes what tools leave at the root: /.playwright-mcp/, /pictures/ and .DS_Store +0.5.0 6088d535fdd9 switch.sh --list prints main and the workspaces, marking this clone's; the delphi mod's /ws pane reads it +0.5.1 3fd96f222b77 the delphi mod's command is /ws-delphi (was /ws); switch.sh's comment says so +0.6.0 c65ffc578bfa setup.sh (.delphi/plugins.sh) makes each clone's @delphi plugins what its workspace enables (installs them, uninstalls local ones it doesn't) and uninstalls ones the marketplace dropped, instead of warning; plugin changes need no migration +0.7.0 84df5ce802db park.sh also parks .journal/ and the root folders under a workspace's park: in workspace.yml (each workspace its own), restores everything parked for a workspace, and skips bad entries; setup.sh excludes them From d041fd92521fca9124c8139f9ab20892d6e14938 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:59:04 +0000 Subject: [PATCH 02/19] delphi: sync inherited files from main --- .claude/skills/address-pr-comments/SKILL.md | 20 ++++++ .../scripts/fetch-feedback.sh | 48 ++++++++++++++ .../address-pr-comments/scripts/github.sh | 19 ++++++ .claude/skills/commit/SKILL.md | 6 ++ .claude/skills/consolidate-spikes/SKILL.md | 13 ++++ .claude/skills/create-ticket/SKILL.md | 66 +++++++++++++++++++ .claude/skills/implement/SKILL.md | 13 ++++ .claude/skills/journal/SKILL.md | 22 +++++++ .claude/skills/journal/scripts/capture.sh | 17 +++++ .claude/skills/new-worktree/SKILL.md | 15 +++++ .claude/skills/open-pr/SKILL.md | 21 ++++++ .claude/skills/open-pr/scripts/pr-checks.sh | 23 +++++++ .claude/skills/pr-conventions/SKILL.md | 15 +++++ .claude/skills/pr-conventions/scripts/lint.sh | 15 +++++ .claude/skills/report-spike/SKILL.md | 22 +++++++ .claude/skills/run-local/SKILL.md | 14 ++++ .claude/skills/run-local/scripts/run-local.sh | 66 +++++++++++++++++++ .claude/skills/run-local/scripts/serve.sh | 43 ++++++++++++ .claude/skills/setup-workspace/SKILL.md | 18 +++++ .claude/skills/switch-workspace/SKILL.md | 21 ++++++ .claude/skills/ticket-conventions/SKILL.md | 15 +++++ .claude/skills/update-pr/SKILL.md | 6 ++ .claude/skills/verify-graph/SKILL.md | 21 ++++++ .claude/skills/verify-graph/helpers.js | 24 +++++++ .claude/skills/verify-telemetry/SKILL.md | 16 +++++ .../verify-telemetry/scripts/check-topic.sh | 28 ++++++++ 26 files changed, 607 insertions(+) create mode 100644 .claude/skills/address-pr-comments/SKILL.md create mode 100755 .claude/skills/address-pr-comments/scripts/fetch-feedback.sh create mode 100644 .claude/skills/address-pr-comments/scripts/github.sh create mode 100644 .claude/skills/commit/SKILL.md create mode 100644 .claude/skills/consolidate-spikes/SKILL.md create mode 100644 .claude/skills/create-ticket/SKILL.md create mode 100644 .claude/skills/implement/SKILL.md create mode 100644 .claude/skills/journal/SKILL.md create mode 100755 .claude/skills/journal/scripts/capture.sh create mode 100644 .claude/skills/new-worktree/SKILL.md create mode 100644 .claude/skills/open-pr/SKILL.md create mode 100755 .claude/skills/open-pr/scripts/pr-checks.sh create mode 100644 .claude/skills/pr-conventions/SKILL.md create mode 100644 .claude/skills/pr-conventions/scripts/lint.sh create mode 100644 .claude/skills/report-spike/SKILL.md create mode 100644 .claude/skills/run-local/SKILL.md create mode 100755 .claude/skills/run-local/scripts/run-local.sh create mode 100755 .claude/skills/run-local/scripts/serve.sh create mode 100644 .claude/skills/setup-workspace/SKILL.md create mode 100644 .claude/skills/switch-workspace/SKILL.md create mode 100644 .claude/skills/ticket-conventions/SKILL.md create mode 100644 .claude/skills/update-pr/SKILL.md create mode 100644 .claude/skills/verify-graph/SKILL.md create mode 100644 .claude/skills/verify-graph/helpers.js create mode 100644 .claude/skills/verify-telemetry/SKILL.md create mode 100755 .claude/skills/verify-telemetry/scripts/check-topic.sh diff --git a/.claude/skills/address-pr-comments/SKILL.md b/.claude/skills/address-pr-comments/SKILL.md new file mode 100644 index 0000000..30083fa --- /dev/null +++ b/.claude/skills/address-pr-comments/SKILL.md @@ -0,0 +1,20 @@ +--- +name: address-pr-comments +description: Fetch review comments on a PR (Argos, Nero), judge whether each (including outdated ones) still applies, and walk through fixes in the PR's worktree +allowed-tools: Bash(git:*), Bash(gh issue list:*), Bash(bash ${CLAUDE_SKILL_DIR}/scripts/fetch-feedback.sh *) +user-invocable: true +--- + +Propose and apply fixes for a PR's unresolved review feedback. **Never reply to threads**; the user does. + +1. **Fetch** from the workspace root: `bash scripts/fetch-feedback.sh |` (`` as under `repos/`, e.g. `argos`; `--all` first for unfiltered history). It checks out the PR head's worktree and prints JSON: `worktree` (target it by path from the root from now on), unresolved `threads`, `comments`, `reviews`. Any non-zero exit: use the GitHub MCP server's `pull_request_read` instead. +2. **Judge.** Skip bot noise and anything already fixed at HEAD. For an `outdated` thread, find its `diffHunk` code by content, then label it **addressed**, **applies**, **moved**, **obsolete**, or **unclear**. +3. **Present** before editing, grouped by file, and ask to confirm, skip, or change each (offer to batch clear ones past 8): + + ``` + : — · ·