From 243787e6a92e31a2f7a8b5078025048d184f32c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:21:10 +0000 Subject: [PATCH 01/14] delphi: sync .delphi/ from main --- .delphi/.gitattributes | 2 + .delphi/.gitignore | 3 + .delphi/manifest.yml | 20 +++ .../0.0.1-linked-worktrees-to-clones.sh | 20 +++ .delphi/migrations/0.1.0-path-names.sh | 29 +++++ .delphi/new-worktree.sh | 22 ++++ .delphi/park.sh | 31 +++++ .delphi/setup.sh | 120 ++++++++++++++++++ .delphi/switch.sh | 28 ++++ .delphi/version.sh | 23 ++++ .delphi/versions | 6 + 11 files changed, 304 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 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..1e37303 --- /dev/null +++ b/.delphi/manifest.yml @@ -0,0 +1,20 @@ +# .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.1.1 +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/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..0ba0c89 --- /dev/null +++ b/.delphi/new-worktree.sh @@ -0,0 +1,22 @@ +#!/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 +root=$(cd "$(dirname "$0")/.." && pwd) +repo=${1:?usage: new-worktree.sh []} branch=${2:?usage: new-worktree.sh []} +base=${3:-origin/HEAD} dest="$root/worktrees/$repo/$branch" +g() { git -C "$root/repos/$repo" "$@"; } +[ -d "$root/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..7d56429 --- /dev/null +++ b/.delphi/park.sh @@ -0,0 +1,31 @@ +#!/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 +# repos/, worktrees/, references/ and linked/ at .delphi/parked// (git-ignored by +# .delphi/.gitignore) and brings the new one's back. 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 +for d in repos worktrees references linked; do + if [ -n "$old" ] && [ -e "$top/$d" ]; then + 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 + fi + src=$park/$new/$d + if [ -n "$new" ] && [ -e "$src" ]; then + 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 + 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/setup.sh b/.delphi/setup.sh new file mode 100755 index 0000000..03568d6 --- /dev/null +++ b/.delphi/setup.sh @@ -0,0 +1,120 @@ +#!/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 /repos/, /worktrees/, /references/ and +# /linked/ 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 them. 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). 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 diff --git a/.delphi/switch.sh b/.delphi/switch.sh new file mode 100755 index 0000000..3a25001 --- /dev/null +++ b/.delphi/switch.sh @@ -0,0 +1,28 @@ +#!/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. +set -euo pipefail +{ # one block, so bash has read all of it before the switch replaces this file + cd "$(dirname "$0")/.." + 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..b2d2d33 --- /dev/null +++ b/.delphi/versions @@ -0,0 +1,6 @@ +# .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 From ecb1dd00908e974dafe861f66c9cba81d9841ff1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:54:59 +0000 Subject: [PATCH 02/14] delphi: sync .delphi/ from main --- .delphi/manifest.yml | 3 ++- .delphi/new-worktree.sh | 9 +++++---- .delphi/paths.sh | 15 +++++++++++++++ .delphi/versions | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 .delphi/paths.sh diff --git a/.delphi/manifest.yml b/.delphi/manifest.yml index 1e37303..dfd4283 100644 --- a/.delphi/manifest.yml +++ b/.delphi/manifest.yml @@ -6,7 +6,7 @@ # 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.1.1 +version: 0.2.0 files: # repo-relative paths; a trailing / means everything under it - .delphi/manifest.yml - .delphi/.gitattributes @@ -14,6 +14,7 @@ files: # repo-relative paths; a trailing / means everythi - .delphi/version.sh - .delphi/setup.sh - .delphi/new-worktree.sh + - .delphi/paths.sh - .delphi/switch.sh - .delphi/park.sh - .delphi/migrations/ diff --git a/.delphi/new-worktree.sh b/.delphi/new-worktree.sh index 0ba0c89..7760f90 100755 --- a/.delphi/new-worktree.sh +++ b/.delphi/new-worktree.sh @@ -4,11 +4,12 @@ # (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 -root=$(cd "$(dirname "$0")/.." && pwd) +# 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="$root/worktrees/$repo/$branch" -g() { git -C "$root/repos/$repo" "$@"; } -[ -d "$root/repos/$repo" ] || +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 : 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/versions b/.delphi/versions index b2d2d33..ca660ad 100644 --- a/.delphi/versions +++ b/.delphi/versions @@ -4,3 +4,4 @@ 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 From 28f6b6168937247497d1e7aa041b377951af1e16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:36:43 +0000 Subject: [PATCH 03/14] delphi: sync .delphi/ from main --- .delphi/manifest.yml | 2 +- .delphi/versions | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.delphi/manifest.yml b/.delphi/manifest.yml index dfd4283..540845c 100644 --- a/.delphi/manifest.yml +++ b/.delphi/manifest.yml @@ -6,7 +6,7 @@ # 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.2.0 +version: 0.3.0 files: # repo-relative paths; a trailing / means everything under it - .delphi/manifest.yml - .delphi/.gitattributes diff --git a/.delphi/versions b/.delphi/versions index ca660ad..4825fb4 100644 --- a/.delphi/versions +++ b/.delphi/versions @@ -5,3 +5,4 @@ 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/ From 4872d5c1a1ca5456799f710b71cf53910659f030 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:36:43 +0000 Subject: [PATCH 04/14] delphi: sync inherited files from main --- .claude/skills/journal/SKILL.md | 22 ++++++++++++++++++++++ .claude/skills/journal/scripts/capture.sh | 17 +++++++++++++++++ .claude/skills/new-worktree/SKILL.md | 15 +++++++++++++++ .claude/skills/setup-workspace/SKILL.md | 18 ++++++++++++++++++ .claude/skills/switch-workspace/SKILL.md | 20 ++++++++++++++++++++ 5 files changed, 92 insertions(+) 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/setup-workspace/SKILL.md create mode 100644 .claude/skills/switch-workspace/SKILL.md diff --git a/.claude/skills/journal/SKILL.md b/.claude/skills/journal/SKILL.md new file mode 100644 index 0000000..4917259 --- /dev/null +++ b/.claude/skills/journal/SKILL.md @@ -0,0 +1,22 @@ +--- +name: journal +description: Park rough notes locally mid-task ("note to self", "remember to", "journal this"), and later export them to their permanent home (issue, spec, ADR, glossary, docs) through the skill that owns it. Use when the user wants to jot a thought without derailing, or to file parked notes. +--- + +Notes live in `.journal/` at the workspace root (git-excluded, outside worktrees so they survive removal). An **entry** is one file, a **category** a subfolder. The journal never pushes, files, or contacts GitHub itself. + +**Capture** (no git, no other skills): + +``` +bash scripts/capture.sh [-c ] ["note text"] +``` + +Omit `-c` for a loose entry, omit the text for an empty one to edit. Report the path it prints. + +**Export** (only when asked): + +1. Read the entries and propose a grouping: one entry → one output, a category → one merged output, or one entry split across several. Let the user adjust. +2. For each output, pick the workspace skill whose description fits its permanent home, preferring an existing docs area. A new section always needs approval. +3. Show the destination and reshaped text and wait for approval. Nothing lands silently. +4. Delegate to that skill; it owns formatting, conventions, and any GitHub contact. +5. Ask whether to delete the exported entry (default: keep). diff --git a/.claude/skills/journal/scripts/capture.sh b/.claude/skills/journal/scripts/capture.sh new file mode 100755 index 0000000..1e90d7b --- /dev/null +++ b/.claude/skills/journal/scripts/capture.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# capture.sh [-c ] [note text]: write a timestamped journal entry to +# .journal/[/] at the workspace root (adding /.journal/ to .git/info/exclude once) and print +# its path. An empty entry is created when no text is given. Never touches git history, GitHub, or +# other skills. +set -euo pipefail +usage="usage: capture.sh [-c ] [note text]" +# shellcheck source=/dev/null +. "${DELPHI_ROOT:?unset: start the session at the workspace root, on Delphi 0.2.0+ (pull its ws/ branch), or source /.delphi/paths.sh}/.delphi/paths.sh" +dir=$DELPHI_ROOT/.journal +[ "${1:-}" = -c ] && dir=$dir/${2:?$usage} && shift 2 +exclude=$(git -C "$DELPHI_ROOT" rev-parse --path-format=absolute --git-path info/exclude 2>/dev/null || true) +[ -z "$exclude" ] || grep -qxF /.journal/ "$exclude" 2>/dev/null || { mkdir -p "${exclude%/*}" && echo /.journal/ >>"$exclude"; } +mkdir -p "$dir" +file=$dir/$(date +%Y-%m-%d-%H%M%S).md +{ echo ""; [ $# -eq 0 ] || echo "$*"; } >"$file" +echo "$file" diff --git a/.claude/skills/new-worktree/SKILL.md b/.claude/skills/new-worktree/SKILL.md new file mode 100644 index 0000000..9fd3ac7 --- /dev/null +++ b/.claude/skills/new-worktree/SKILL.md @@ -0,0 +1,15 @@ +--- +name: new-worktree +description: Create or reuse a worktree for a branch of a code repo (worktrees//). Use before any branch work, such as starting a ticket or reviewing or fixing a PR branch, unless the user says not to use worktrees. +--- + +Run from the workspace root: + +``` +bash .delphi/new-worktree.sh [] +``` + +- An existing branch (local or on origin, e.g. a PR head) is checked out; anything else is created from `` (default: the repo's default branch). Safe to re-run. +- Stay at the workspace root and reach into the worktree by the printed path. Never switch branches in an existing worktree or work in the bare store `repos/`. +- Keep `worktrees///` a clean reference: fetch and fast-forward only. +- After the branch merges: `git -C repos/ worktree remove ../../worktrees//`. diff --git a/.claude/skills/setup-workspace/SKILL.md b/.claude/skills/setup-workspace/SKILL.md new file mode 100644 index 0000000..6e73fb3 --- /dev/null +++ b/.claude/skills/setup-workspace/SKILL.md @@ -0,0 +1,18 @@ +--- +name: setup-workspace +description: Change what this workspace checks out and re-run its setup. workspace.yml lists code repos (repos:), read-only dependency sources (references-git:), and read-only copies of other Delphi workspaces or main (workspaces:). Use when the user wants to read or compare another workspace's or main's instructions, skills, or docs (link it), add or remove a repo or dependency reference, or run setup after cloning or pulling. +--- + +`workspace.yml` at the workspace root lists what `.delphi/setup.sh` checks out, all git-ignored: code repos (`repos:`), dependency sources (`references-git:`), and other Delphi workspaces or main (`workspaces:`, entries `- ws/` or `- main`; a workspace's name is its place in the tree, like `software/application-software/argos/argos-dev`). The comment on each section gives its entry format and where it lands; read it, then edit the file. + +Run from the workspace root after any change, and to refresh references and workspace copies to their latest: + +``` +bash .delphi/setup.sh +``` + +- The copies under `linked/` are for reading: never edit, branch, or push them. To change one of those workspaces or main, switch to it (`switch-workspace` skill). +- This workspace's project folder (shared context, `AUTHORING.md`, `workspace-defaults/`) is `linked/main/` plus this workspace's name without its last part: for `software/application-software/argos/argos-dev`, `linked/main/software/application-software/argos/`. List workspaces: `git branch -r --list 'origin/ws/*'`. +- `workspace.yml` is shared: change it on a branch and open a PR into `ws/` (the `name:` in `workspace.yml`). For a one-off look, add the entry, run setup, then `git restore workspace.yml`. +- Setup never deletes what you'd lose. A removed entry's `linked//`, `references//`, or repo store and worktrees stay until you delete them (ask first). +- When Delphi releases a version that needs cleanup in clones, setup runs its migration first (`.delphi/migrations/`), once per workspace. A migration removes only what nothing is lost from, and otherwise stops setup with what to do: tell the user, do it with their go-ahead, and re-run. diff --git a/.claude/skills/switch-workspace/SKILL.md b/.claude/skills/switch-workspace/SKILL.md new file mode 100644 index 0000000..412016f --- /dev/null +++ b/.claude/skills/switch-workspace/SKILL.md @@ -0,0 +1,20 @@ +--- +name: switch-workspace +description: Switch this clone to another Delphi workspace, or to Delphi's main, to work in it. Use when the user wants to check out, switch to, open, or work in another workspace or project, or to change another workspace's or main's files. +--- + +Run from the workspace root: + +``` +bash .delphi/switch.sh # ws/: its place in the tree, e.g. software/application-software/argos/argos-dev +bash .delphi/switch.sh argos-dev # its last part alone, if only one workspace ends in it +bash .delphi/switch.sh main # Delphi's main: project folders, templates, CI +``` + +- List workspaces: `git branch -r --list 'origin/ws/*'`. A workspace's name is its folder path on main without `workspaces/`. +- It refuses while tracked files have changes: commit them on a branch (and PR them) first. Then it fetches, switches, fast-forwards, and runs the new workspace's `.delphi/setup.sh`. +- The post-checkout hook parks this workspace's `repos/`, `worktrees/`, `references/` and `linked/` in `.delphi/parked//` (git-ignored; `` as in its `workspace.yml`) and brings the other's back at the same paths, so worktrees and the work in them return as they were. Switch back the same way. Never run `git clean -X` or `git stash --all` at the workspace root: they take ignored files, parked work included. +- Before switching, stop anything running from `worktrees/` (dev servers, Docker builds): its folder moves. +- This session keeps the old workspace's `CLAUDE.md` and skills. After switching, tell the user to restart Claude Code in the same folder, and don't start work in the new workspace until they have. +- To change the workspace you switched to: `git switch -c `, commit, push, and open a PR into `ws/` (or `main`). +- To only read another workspace or main without leaving this one, list it under `workspaces:` instead (`setup-workspace` skill). From f773ada0602d0f9b1b20f3941065ac722c076833 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:08:17 +0000 Subject: [PATCH 05/14] delphi: sync .delphi/ from main --- .delphi/manifest.yml | 3 ++- .delphi/plugins.sh | 21 +++++++++++++++++++++ .delphi/setup.sh | 4 +++- .delphi/versions | 1 + 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100755 .delphi/plugins.sh diff --git a/.delphi/manifest.yml b/.delphi/manifest.yml index 540845c..5b26d6c 100644 --- a/.delphi/manifest.yml +++ b/.delphi/manifest.yml @@ -6,7 +6,7 @@ # 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.3.0 +version: 0.4.0 files: # repo-relative paths; a trailing / means everything under it - .delphi/manifest.yml - .delphi/.gitattributes @@ -15,6 +15,7 @@ files: # repo-relative paths; a trailing / means everythi - .delphi/setup.sh - .delphi/new-worktree.sh - .delphi/paths.sh + - .delphi/plugins.sh - .delphi/switch.sh - .delphi/park.sh - .delphi/migrations/ diff --git a/.delphi/plugins.sh b/.delphi/plugins.sh new file mode 100755 index 0000000..2ad077e --- /dev/null +++ b/.delphi/plugins.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# .delphi/plugins.sh: setup.sh runs it to warn about the plugins this workspace's generated harness +# settings enable but this machine hasn't installed, so their skills are missing (CLAUDE.md says how to +# install them). Prints nothing when all are installed, or when it can't tell. Only `harness: +# claude-code` so far: installed means at user scope, or at project or local scope for this root. +# 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 +enabled=$(sed -nE 's/^[[:space:]]*"([^"]+@[^"]+)": true,?$/\1/p' .claude/settings.json 2>/dev/null || true) +[ -n "$enabled" ] && command -v claude >/dev/null || exit 0 +root=$(pwd -W 2>/dev/null || pwd) # Git Bash: the Windows path Claude Code records +installed=$(claude plugin list --json 2>/dev/null | awk -v root="$root" ' + function val(l) { sub(/^[^:]*:[[:space:]]*"/, "", l); sub(/"[^"]*$/, "", l); gsub(/\\\\/, "/", l); return l } + /"id":/ { id = val($0); scope = path = "" } + /"scope":/ { scope = val($0) } + /"projectPath":/ { path = val($0) } + /^[[:space:]]*}/ && id != "" { if (scope == "user" || tolower(path) == tolower(root)) print id; id = "" }') || exit 0 +missing=$(for p in $enabled; do echo "$installed" | grep -qxF "$p" || echo "$p"; done | 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" diff --git a/.delphi/setup.sh b/.delphi/setup.sh index 03568d6..704b122 100755 --- a/.delphi/setup.sh +++ b/.delphi/setup.sh @@ -13,7 +13,8 @@ # to another workspace or main parks them. 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). Safe to re-run. Keep it bash 3.2 +# .delphi/ isn't exactly the version it says (.delphi/version.sh). Last, it warns about plugins the workspace +# enables that this machine hasn't installed (.delphi/plugins.sh). 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")/.." @@ -118,3 +119,4 @@ 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 # a warning, never a failure diff --git a/.delphi/versions b/.delphi/versions index 4825fb4..f046913 100644 --- a/.delphi/versions +++ b/.delphi/versions @@ -6,3 +6,4 @@ 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 From 8626de3df19090a44062d3a0b11834dca40c8cde Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:08:17 +0000 Subject: [PATCH 06/14] 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/create-ticket/SKILL.md | 52 +++++++++++++++ .claude/skills/implement/SKILL.md | 13 ++++ .claude/skills/open-pr/SKILL.md | 20 ++++++ .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/run-local/SKILL.md | 14 ++++ .claude/skills/run-local/scripts/run-local.sh | 66 +++++++++++++++++++ .claude/skills/run-local/scripts/serve.sh | 43 ++++++++++++ .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 ++++++++ 19 files changed, 464 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/create-ticket/SKILL.md create mode 100644 .claude/skills/implement/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/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/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): + + ``` + : — · ·