Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .delphi/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# MINOR something new that existing clones need nothing for
# MAJOR clones need cleanup (MINOR while 0.x): add .delphi/migrations/<version>-<slug>.sh, which
# setup.sh runs once per workspace in each clone (rules in Delphi's CLAUDE.md)
version: 0.0.1
version: 0.1.0
files: # repo-relative paths; a trailing / means everything under it
- .delphi/manifest.yml
- .delphi/.gitattributes
Expand Down
29 changes: 29 additions & 0 deletions .delphi/migrations/0.1.0-path-names.sh
Original file line number Diff line number Diff line change
@@ -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/<name>` or `- main`), copied to linked/<branch>.
# Moves what this clone keeps under old names to the new ones, for every workspace on main: parked
# slots (.delphi/parked/<old> and, from before that, .git/delphi/<old>) to .delphi/parked/<name>, and
# read-only copies linked/<old> to linked/ws/<name>, here and in each parked slot. Only renames; a
# name taken at both places is reported and left. Your local ws/<old> branch stays; delete it once
# you're on ws/<name>.
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() { # <from> <to>
[ -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
17 changes: 10 additions & 7 deletions .delphi/park.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@
# .delphi/park.sh <prev> <new> <branch-flag>: the post-checkout hook; .delphi/setup.sh symlinks
# .git/hooks/post-checkout to it. .delphi/ is on every branch, main included, so the link always
# resolves, to the version just checked out. When a checkout moves this clone to another workspace
# (the `name:` in workspace.yml; none on main), it parks the old workspace's repos/, worktrees/,
# references/ and linked/ at .delphi/parked/<name>/ (git-ignored by .delphi/.gitignore) and brings the
# new one's back, from there or from .git/delphi/<name>/, where parking used to be. Moves are renames,
# so every path is the same again once its workspace is checked out. Runs on bash 3.2 and Git Bash.
# (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/<name>/ (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 legacy=$(git rev-parse --absolute-git-dir)/delphi
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
[ -e "$src" ] || src=$legacy/$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" ] || rmdir "$park/$new" "$legacy/$new" 2>/dev/null || true # once all back, drop the empty slot
[ -z "$new" ] || (cd "$park" 2>/dev/null && rmdir -p "$new" 2>/dev/null) || true # once all back, drop the empty slot
21 changes: 10 additions & 11 deletions .delphi/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
# out its default branch as the first worktree, worktrees/<name>/<default-branch>. For each repo under
# `references-git:` (read-only dependency sources, `<name>: <git-url> [<ref>]`), checks out a shallow,
# detached copy of <ref> (default: origin's default branch) at references/<name>, refreshed on every
# run, with pushes disabled. Each entry under `links:` (`- <workspace>` or `- main`) gets the same
# kind of copy of this clone's origin (Delphi) at ws/<workspace> (or main), at linked/<name>; to edit
# one, switch this clone to it (.delphi/switch.sh). Adds /repos/, /worktrees/, /references/ and
# run, with pushes disabled. Each entry under `workspaces:` (`- ws/<name>` 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/<branch>; 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 symlinks its post-checkout hook to .delphi/park.sh,
# which is on every branch (unless another hook is there) so switching to another workspace or main parks them. First
# it runs the .delphi/migrations/<version>-*.sh this workspace hasn't (oldest first; the last one done
Expand All @@ -18,7 +18,7 @@ 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
case "$self" in "" | [-/]* | */ | *//* | *[!a-z0-9/-]*) echo "setup: workspace.yml needs a valid name:" >&2 && exit 1 ;; esac

older() { # <a> <b>: 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" ]
Expand All @@ -44,7 +44,7 @@ list=$(awk '
/^[[:space:]]*#/ || /^[[:space:]]*$/ { next }
/^repos:/ { s = "repo"; next }
/^references-git:/ { s = "ref"; next }
/^links:/ { s = "link"; next }
/^workspaces:/ { s = "link"; next }
/^[^[:space:]]/ { s = "" }
s == "link" { sub(/^[[:space:]]*-[[:space:]]*/, ""); sub(/[[:space:]]*#.*$/, ""); print s, $0; next }
s { sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+#.*$/, ""); n = index($0, ":")
Expand All @@ -64,17 +64,16 @@ readonly_copy() { # <dir> <url> [<ref>]: shallow, detached, push-disabled checko
fi
}

link() { # <workspace>|main: a read-only copy of Delphi's ws/<workspace> (or main) at linked/<name>
case "$1" in -* | *[!a-z0-9-]*) echo "setup: skipping bad link '$1'" >&2 && return ;; "$self") return ;; esac
local ref=ws/$1
[ "$1" != main ] || ref=main
readonly_copy "linked/$1" "$(git remote get-url origin)" "$ref"
link() { # ws/<name>|main: a read-only copy of that branch of Delphi at linked/<branch>
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
case "$name" in . | .. | *[!A-Za-z0-9._-]*) echo "setup: skipping unsafe repo name '$name'" >&2 && continue ;; esac
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
Expand Down
13 changes: 11 additions & 2 deletions .delphi/switch.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
# .delphi/switch.sh <workspace>|main: move this clone to Delphi's ws/<workspace> (or main). Refuses if
# .delphi/switch.sh <workspace>|main: move this clone to Delphi's ws/<workspace> (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
Expand All @@ -8,7 +10,14 @@ 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 <workspace>|main}
case "$name" in main) ref=main ;; -* | *[!a-z0-9-]*) echo "switch: bad workspace name '$name'" >&2 && exit 1 ;; *) ref=ws/$name ;; esac
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/<name>: 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"
Expand Down
1 change: 1 addition & 0 deletions .delphi/versions
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# 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/.../<name>); links: is workspaces: (- ws/<name> | - main) (migration)
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Delphi — developing it

Delphi stores NER's AI-harness context. Each workspace is a folder `software/**/workspaces/<name>/`
on `main`; branch `ws/<name>` has that folder as its repo root. `ci/sync.sh` keeps them in sync with
Delphi stores NER's AI-harness context. Each workspace is a folder `software/**/workspaces/<leaf>/`
on `main`, named by its place in the tree (the path without `workspaces/`:
`software/application-software/argos/argos-dev`); branch `ws/<name>` has that folder as its repo root. `ci/sync.sh` keeps them in sync with
subtree merges: **refresh** (main → `ws/<name>`) and **propose** (`ws/<name>` → PR to main via
`propose/<name>`). There is no CLI, just a few shell scripts run by CI. `docs/design.md` is the
source of truth; `docs/goals.md` lists what any change must keep.
Expand Down
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Delphi

NER's AI-harness context (instructions, skills, docs, settings), organized by the org chart under
`software/`. A **workspace** is a folder `software/**/workspaces/<name>/` with a `workspace.yml`.
For each one, CI keeps a branch **`ws/<name>`** whose repo root *is* that folder, so people and
agents work on it with plain git. Design: `docs/design.md`. Goals: `docs/goals.md`.
`software/`. A **workspace** is a folder `software/**/workspaces/<leaf>/` with a `workspace.yml`,
named by its place in the tree: the folder's path without `workspaces/`
(`software/application-software/argos/argos-dev`). For each one, CI keeps a branch **`ws/<name>`**
whose repo root *is* that folder, so people and agents work on it with plain git. Design: `docs/design.md`. Goals: `docs/goals.md`.

## How it works

Expand Down Expand Up @@ -39,11 +40,11 @@ histories stay joined and edits on either side meet in normal three-way merges.
## Using a workspace

```sh
git clone -b ws/argos-dev https://github.com/Northeastern-Electric-Racing/Delphi.git argos-dev
cd argos-dev && .delphi/setup.sh # repos/ stores + default-branch worktrees in worktrees/ (git-ignored)
.delphi/switch.sh nero-experimental # switch freely (or to main): the post-checkout hook parks them in .delphi/parked/
git switch -c my-change # edit, commit, push, open a PR into ws/argos-dev
git fetch origin && git merge origin/ws/argos-dev # update your branch any time
git clone -b ws/software/application-software/argos/argos-dev https://github.com/Northeastern-Electric-Racing/Delphi.git delphi
cd delphi && .delphi/setup.sh # repos/ stores + default-branch worktrees in worktrees/ (git-ignored)
.delphi/switch.sh nero-experimental # switch freely (or to main; the full name works too): the hook parks them in .delphi/parked/
git switch -c my-change # edit, commit, push, open a PR into ws/software/application-software/argos/argos-dev
git fetch origin && git merge origin/ws/software/application-software/argos/argos-dev # update your branch any time
```

After your PR merges into `ws/<name>`, CI proposes it: a PR `ws/<name> → main` from
Expand All @@ -57,25 +58,26 @@ workspace. Fix it in a PR into `ws/<name>`: on a branch cut from `ws/<name>`, ru
## workspace.yml

```yaml
name: argos-dev # the folder name
name: software/application-software/argos/argos-dev # its place in the tree
harness: claude-code
repos: # stored in repos/<name>, checked out in worktrees/<name>/ by .delphi/setup.sh
argos: https://github.com/Northeastern-Electric-Racing/Argos.git
references-git: # read-only dependency sources, shallow-cloned into references/<name>/
socketioxide: https://github.com/Totodore/socketioxide.git v0.18.7 # optional tag or branch
links: # Delphi workspaces or main, read-only, shallow-cloned into linked/<name>/
workspaces: # Delphi workspaces (ws/<name>) or main, read-only, shallow-cloned into linked/<branch>/
- main
```

`repos:` are the code you change. `references-git:` are open-source dependencies the agent reads for
context: `setup.sh` checks each out shallow and detached at its pin (or the default branch), with
pushes disabled, and refreshes them on every run. They get no store and no worktrees. Names are
unique across both maps. The `-git` suffix leaves room for other kinds of references later.
`links:` are other workspaces (or `main`) to read: the same kind of read-only copy, of Delphi itself.
`workspaces:` are other workspaces (`- ws/<name>`) or `main` to read: the same kind of read-only copy, of Delphi itself.
To change one, switch to it instead.

The workspace's name is its folder name (unique repo-wide), repeated in `name:` so a checkout knows
which workspace it is. Every other file in the folder is the workspace's own, at its normal harness
The workspace's name is its place in the tree, repeated in `name:` so a checkout knows which
workspace it is. Workspaces in different places may end in the same name; but since git can't hold
both `ws/a` and `ws/a/b`, no workspace may be named like a project folder beside it. Every other file in the folder is the workspace's own, at its normal harness
path, except `.github/workflows/delphi.yml` (a copy of the template's). The scripts, `.delphi/*.sh`
(`setup.sh`, `new-worktree.sh`, `switch.sh`, `park.sh`, `version.sh`), live once at main's root: sync puts
`.delphi/` on every `ws/*` branch, so change them only through PRs to main.
Expand All @@ -92,11 +94,11 @@ a version needs cleanup in people's clones, it ships `.delphi/migrations/<versio

`tools/new-workspace.sh <org-path under software/> <name>` copies `templates/workspace/` into
`software/<org-path>/workspaces/<name>/`, adds the project's `defaults/` if it has one, and opens a
PR to `main`. Once it merges, CI creates `ws/<name>`.
PR to `main`. Once it merges, CI creates `ws/software/<org-path>/<name>`.

**Project folders.** The folder above `workspaces/` (e.g. `software/application-software/argos/`)
can hold shared context (`README.md`), a guide to its workspaces (`AUTHORING.md`), and `defaults/`.
A workspace that lists `main` under `links:` reads them at `linked/main/` (recommended for every
A workspace that lists `main` under `workspaces:` reads them at `linked/main/` (recommended for every
new workspace); to change them, switch to `main`.

## CI (`.github/workflows/delphi.yml`)
Expand Down
Loading