diff --git a/.claude/skills/delphi-new-layout/SKILL.md b/.claude/skills/delphi-new-layout/SKILL.md deleted file mode 100644 index 538975b..0000000 --- a/.claude/skills/delphi-new-layout/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: delphi-new-layout -description: Build a new Delphi layout with the user. Interview them about the work, pick blocks from scope recommendations, draft manifest.yml, and open the layout PR with `delphi layout new --from`. Use when someone wants a new layout or workspace setup in Delphi. ---- - -# Create a Delphi layout - -Drive the CLI. Don't write to `context/` yourself. - -1. **Interview.** Ask what the work is (team, repo, typical tasks) and pick the closest scope - under `context/` (for example `software/application-software/argos`). -2. **Gather candidates.** Read `recommend:` in the scope's `scope.yml` and in each ancestor's, - closest scope first. Browse the scopes' `blocks/`, `docs/`, and `harness/` directories. Run - `delphi layout list` to reuse ideas from existing layouts and avoid name clashes. -3. **Draft** a manifest in a temp file. Use the format in spec §4.3. All paths are relative to - `context/`, and each entry goes under the key that matches its location: - `harness/instructions/*` → `instructions`, `harness/skills/*` → `skills`, - `harness/mcp/*.json` → `mcp`, `harness/settings/*` → `settings` (at most one), - `blocks/*` → `blocks`, `docs/*` → `docs` (a trailing `/*` glob is allowed for both). `name` must equal the layout name - (`[a-z0-9-]+`). `harness: claude-code`. Add `repos:` as `name: git-url`. -4. **Show the draft** and get explicit approval. -5. **Create it:** - `delphi layout new --from --model --effort --yes` - The command runs `check` and prints the branch (`delphi/layout/`). On a check failure, - fix the draft and retry. -6. **Optionally try it** before the PR merges: `delphi workspace new --ref delphi/layout/`. - Once the PR merges, run `delphi workspace refresh --ref main`. diff --git a/.claude/skills/delphi-propose/SKILL.md b/.claude/skills/delphi-propose/SKILL.md deleted file mode 100644 index 40879d0..0000000 --- a/.claude/skills/delphi-propose/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: delphi-propose -description: Propose a Delphi workspace's changes back to the monorepo. Dry-run the routing, help the user resolve each unresolved item inside the workspace, then run `delphi workspace propose`. Use when someone wants to send workspace edits upstream or asks why an edit is unresolved. ---- - -# Propose workspace changes - -Run these inside the workspace. Drive the CLI. Don't edit the Delphi repo directly. - -1. Make sure the work is committed (`git status` is clean). -2. Run `delphi workspace propose --dry-run`. It prints the plan (where each edit routes) and the - **Unresolved** items, each with its path, reason, and diff. -3. For each unresolved item, suggest a fix *in the workspace* and apply it only after the user approves: - - **Edit touches generated/separator lines or spans two blocks:** move the text wholly inside - one block's lines, or split it into two edits. In `CLAUDE.md`, a section set off by blank - lines between fragments routes as a new fragment (`fragment` in the plan). - - **New file outside a recognised place:** move it under `context//blocks/` (an - existing scope), `docs/` for a doc, or `.claude/skills//` for a skill. Otherwise leave it out of the PR - by deleting it or keeping it uncommitted. - - **Deleted file that is still compiled:** drop its entry from `.delphi/manifest.yml` instead. - To replace a block, swap the manifest entry, delete the old file, and add the new one under - `context//blocks/`. - - **Patch did not apply (same block edited in two places):** keep the edit in one place only. - - **Binary file:** Delphi doesn't route it. Remove it from the workspace commit. - Commit the fixes, then run `--dry-run` again until only acceptable items remain. Unresolved - items never block the PR. They are listed in its body. -4. Run `delphi workspace propose --model --effort --yes`. - It refreshes first. On exit code 2 (merge conflicts), help resolve them, commit, and run it again. -5. Report the branch and PR URL. Proposing again later replaces the same PR with the - workspace's full pending diff. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..fa7bd15 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Reviewers for each org folder under software/. +# TODO: replace the placeholder with the real Argos owners (GitHub users or @org/team). +software/application-software/argos/ @argos-owners-placeholder diff --git a/.github/workflows/delphi.yml b/.github/workflows/delphi.yml new file mode 100644 index 0000000..09d7834 --- /dev/null +++ b/.github/workflows/delphi.yml @@ -0,0 +1,40 @@ +# Delphi CI (docs/design.md). This file is at main's root and, identically, at the root of every +# ws/ (GitHub runs a push's workflow from the pushed commit, so ws pushes need their own copy). +# PRs to main: ci/check.sh. Pushes to main or ws/**: ci/sync.sh (refresh + propose), always run from +# main. Pushes and PRs made with GITHUB_TOKEN trigger no workflows: no loops, and CI-opened PRs +# don't run `check` (sync.sh runs it itself). +name: delphi +on: + pull_request: + branches: [main] + push: + branches: [main, 'ws/**'] +permissions: + contents: read +jobs: + check: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - run: ci/check.sh + sync: + if: github.event_name == 'push' + runs-on: ubuntu-latest + concurrency: delphi-sync + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + - env: + GH_TOKEN: ${{ github.token }} + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + ci/sync.sh diff --git a/CLAUDE.md b/CLAUDE.md index 314804f..2646573 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,39 +1,31 @@ -# Delphi — developing the CLI +# Delphi — developing it -Delphi stores NER's AI-harness context under `context/` and ships a small bash CLI that compiles -layouts into workspaces, refreshes them, and proposes edits back as PRs. The design spec -(`context/docs/delphi-design.md`) is the source of truth. +Delphi stores NER's AI-harness context. Each workspace is a folder `software/**/workspaces//` +on `main`; branch `ws/` has that folder as its repo root. `ci/sync.sh` keeps them in sync with +subtree merges: **refresh** (main → `ws/`) and **propose** (`ws/` → PR to main via +`propose/`). 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. ## Layout -- `bin/delphi`: dispatcher. It resolves its own path and sources `lib/core.sh`, then the group's module. -- `lib/core.sh`: messages, `defer` cleanup, prompts, config, `safe_path`, Delphi git access, moves, `parse_args`. -- `lib/parse.sh`: YAML-subset parser (POSIX awk). -- `lib/compile.sh`: layout → files (instructions, blocks, docs, skills, MCP, settings) + `.delphi/lock.tsv`. -- `lib/route.sh` + `lib/route.awk`: pending diff + lock → plan → edits in a PR worktree. -- `lib/pr.sh`: the only write path to Delphi (temp worktree, commit with trailers, push, `gh`). -- `lib/provenance.sh`: harness/model/effort resolution. -- `lib/workspace.sh`, `lib/layout.sh`, `lib/block.sh`, `lib/check.sh`, `lib/setup.sh`: the command groups. -- `lib/harness/.sh`: harness adapters (names + `harness_provenance` + `harness_launch`). -- `dev/sandbox.sh`: throwaway end-to-end playground. It is the only test harness (no automated tests, no CI). -- `.claude/skills/`: LLM workflows that drive the CLI (`delphi-new-layout`, `delphi-propose`). +- `ci/sync.sh [refresh|propose] []`: refresh and/or propose each workspace (both by default). + Runs on pushes to main and `ws/**`. +- `ci/check.sh []`: validates the repo. Runs on PRs to main and on each proposal. +- `tools/new-workspace.sh`: new workspace folder from `templates/workspace/` as a PR. +- `templates/workspace/`: `workspace.yml`, `CLAUDE.md` (`{{name}}`, `{{folder}}`), and the managed + files every workspace carries unchanged: `.delphi/setup.sh`, `.github/workflows/delphi.yml`. +- `software/…/workspaces//`: the workspaces. Org structure is plain directories. +- `.github/workflows/delphi.yml` (identical to the template's copy), `.github/CODEOWNERS`. +- `tests/e2e.sh`: end-to-end test in a throwaway sandbox (bare origin, stub `gh` logging to `gh.log`). ## Rules -- Must run under macOS `/bin/bash` 3.2. Test with `/bin/bash`, never zsh or a newer bash. -- Tools: POSIX awk (no gawk extensions), git, gh. jq is optional. -- Keep it small: fewest lines that implement the spec, terse functions, a short header per file. -- Bash 3.2 pitfalls: - - Functions that `defer` cleanup (`make_tmp`, `delphi_worktree_at`, `pr_begin`) must not run - inside `$(...)`. They return results in `REPLY`. - - errexit is suspended in conditional contexts (`if f`, `f || x`), so critical commands need an explicit `|| die`. - - A dying function inside `$(...)` needs `|| exit 1` at the call site. - - Empty arrays error under `set -u`. Use newline-separated strings. - - A failing command substitution inside a heredoc does not propagate. Assign it to a variable first. - - Use `sed` rather than `grep -v`, which exits 1 on empty input and breaks under pipefail. - - `"$var…"` (a variable followed by a non-ASCII byte) is parsed as a longer name. Write `${var}…`. - An unbound-variable error under the EXIT trap exits with status **0**. -- Every path from config, flags, lock, or `moves.tsv` goes through `safe_path` before use. -- Never test against real GitHub repos. Use the sandbox: - `eval "$(/bin/bash dev/sandbox.sh /tmp/sb1)"`, then `d `. - The sandbox stubs `gh` (log in `$SB/gh.log`), so `--yes` pushes only to its bare origin. +- Keep it small: fewest lines that implement the design; a short header comment per script. + Prefer deleting to adapting. Runtime tools: bash, git, and `gh` (only to open or update PRs). +- Every script: `#!/usr/bin/env bash`, `set -euo pipefail`, `shellcheck`-clean. +- `.delphi/setup.sh` runs on people's machines: bash 3.2 (macOS) and Git Bash safe. No bash-4 + features (associative arrays, `mapfile`, `${x,,}`, `|&`), POSIX awk only. +- Changing a managed file (`setup.sh`, the workflow): update the template, `.github/`, and every + workspace copy in the same PR, or `ci/check.sh` fails. +- `bash tests/e2e.sh` and `ci/check.sh` must pass. Add a test there for every behavior change. +- Never test against real GitHub repos or this checkout's origin; use the sandbox in `tests/e2e.sh`. diff --git a/README.md b/README.md index 31d1692..a87ae5a 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,82 @@ # Delphi -NER's AI-harness context (knowledge, instructions, skills, MCP, settings), stored compressed -by org chart under `context/`, plus a small bash CLI: +NER's AI-harness context (instructions, skills, docs, settings), organized by the org chart under +`software/`. A **workspace** is a folder `software/**/workspaces//` with a `workspace.yml`. +For each one, CI keeps a branch **`ws/`** whose repo root *is* that folder, so people and +agents work on it with plain git. Design: `docs/design.md`. Goals: `docs/goals.md`. -- **compile** a *layout* into a local, git-initialized *workspace* where Claude Code runs, -- **refresh** the workspace when `main` moves, -- **propose** workspace edits back as one PR, routed to the blocks they came from. +## How it works -Design: `context/docs/delphi-design.md`. +``` + ┌───────────────────────────────┐ + │ main │ + │ software/…/workspaces// │ + └───────────────────────────────┘ + │ ▲ + │ refresh │ merge PR + │ (CI) │ (after check.sh) + ▼ │ + ┌──────────────────┐ ┌──────────────────┐ + │ ws/ │ │ propose/ │ + │ root = workspace │─►│ PR to main │ + └──────────────────┘ └──────────────────┘ + │ ▲ propose (CI) + │ branch │ merge PR + ▼ │ + ┌──────────────────┐ + │ your branch │ + │ edit · commit │ + └──────────────────┘ -## Quick start + refresh: main → ws/ (folder becomes root) + propose: ws/ → main PR (root goes back under folder) + CI runs both on every push to main or ws/** +``` -```sh -bin/delphi setup # once: puts `delphi` on PATH (~/.local/bin, or pass a dir) +Both directions are subtree merges (`git merge -Xsubtree=`) done by `ci/sync.sh`, so the +histories stay joined and edits on either side meet in normal three-way merges. -delphi ws new argos-dev # create the workspace -delphi ws open argos-dev # start Claude Code in it -delphi ws propose # send context edits back as a PR (run inside the workspace) -delphi ws refresh # pull in Delphi updates +## 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 # clones workspace.yml's repos into repos/ (git-ignored) +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 ``` -Code changes go in `repos/argos` with its own PRs. Context changes (CLAUDE.md, skills, docs) are -committed in the workspace and sent back with `propose`. +After your PR merges into `ws/`, CI proposes it: a PR `ws/ → main` from +`propose/`. Merge that with a merge commit or squash, never rebase. Never push to `ws/*` or +`main` directly (protect them). Code changes go in `repos/`, through that repo's own PRs. -## Commands +**Conflicts.** If `main` and `ws/` changed the same lines, CI lists the files and skips that +workspace. Fix it in a PR into `ws/`: on a branch cut from `ws/`, run +`git merge -Xsubtree= origin/main`, resolve, commit. +## workspace.yml + +```yaml +harness: claude-code +repos: # cloned into repos/ by .delphi/setup.sh + argos: https://github.com/Northeastern-Electric-Racing/Argos.git ``` -delphi layout new [--from ] create a layout (branch + PR) -delphi layout list list layouts on origin/main -delphi workspace new [--as ] [--ref ] -delphi workspace open|refresh|propose|status (alias: ws) -delphi block mv move a block (branch + PR) -delphi check validate the repo -delphi setup [dir] put `delphi` on PATH -``` -Commands that write to Delphi accept `--model`, `--effort` (provenance) and `--yes`. Without -`--yes`, a non-interactive run fails fast instead of prompting. +The workspace's name is its folder name (unique repo-wide). Every other file in the folder is the +workspace's own, at its normal harness path, except two Delphi manages: `.delphi/setup.sh` and +`.github/workflows/delphi.yml` (copies of the template's). + +## New workspace + +`tools/new-workspace.sh ` copies `templates/workspace/` into +`software//workspaces//` and opens a PR to `main`. Once it merges, CI creates +`ws/`. + +## CI (`.github/workflows/delphi.yml`) + +- PRs to `main`: `ci/check.sh` validates the repo. +- Pushes to `main` or `ws/**`: `ci/sync.sh` refreshes and proposes every workspace. -## Developing Delphi +Repo settings: allow GitHub Actions to create PRs. Don't make `check` a required status check: +PRs opened by CI don't trigger workflows (sync.sh runs `ci/check.sh` itself before proposing). -Test CLI changes in the sandbox, never against GitHub (see `CLAUDE.md`): -`eval "$(/bin/bash dev/sandbox.sh /tmp/delphi-sb)"`, then `d `. +Developing Delphi: see `CLAUDE.md`; `bash tests/e2e.sh` runs everything in a local sandbox. diff --git a/bin/delphi b/bin/delphi deleted file mode 100755 index 2738a08..0000000 --- a/bin/delphi +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# Delphi CLI entry point. Resolves its own location, then dispatches on the command group. -set -euo pipefail - -src=${BASH_SOURCE[0]} -while [ -L "$src" ]; do - dir=$(cd -P "$(dirname "$src")" && pwd) - src=$(readlink "$src") - case $src in /*) ;; *) src="$dir/$src" ;; esac -done -DELPHI_ROOT=$(cd -P "$(dirname "$src")/.." && pwd) -export DELPHI_ROOT - -. "$DELPHI_ROOT/lib/core.sh" - -usage() { - cat <<'EOF' -usage: delphi [args] - - layout new [--from ] create a layout (branch + PR) - layout list list layouts on origin/main - - workspace new [--as ] [--ref ] - workspace open [] [--model m] [--effort e] [--shell] - workspace refresh [] [--ref ] - workspace propose [] [--dry-run] - workspace status [--offline] (alias: ws) - - block mv move/rename a block - - check validate the repo - setup [dir] put `delphi` on PATH (default ~/.local/bin) - -Commands that write to Delphi accept --model, --effort, --yes. -EOF -} - -group=${1:-} -[ $# -gt 0 ] && shift -case $group in - layout) . "$DELPHI_ROOT/lib/layout.sh"; layout_main "$@" ;; - workspace|ws) . "$DELPHI_ROOT/lib/workspace.sh"; workspace_main "$@" ;; - block) . "$DELPHI_ROOT/lib/block.sh"; block_main "$@" ;; - check) . "$DELPHI_ROOT/lib/check.sh"; check_main "$@" ;; - setup) . "$DELPHI_ROOT/lib/setup.sh"; setup_main "$@" ;; - ""|-h|--help|help) usage ;; - *) die "unknown command '$group' (see: delphi help)" ;; -esac diff --git a/ci/check.sh b/ci/check.sh new file mode 100755 index 0000000..3f2c498 --- /dev/null +++ b/ci/check.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# ci/check.sh []: validate Delphi (CI runs it on PRs to main; sync.sh on each proposal). Rules: +# a workspace is software/**/workspaces// with a workspace.yml; names are lowercase letters, +# digits, and '-', unique repo-wide; workspaces never nest; no symlinks under software/; each +# workspace's .delphi/setup.sh and .github/workflows/delphi.yml match templates/workspace/, and the +# template workflow matches .github/workflows/delphi.yml; workspace.yml is `harness: ` plus +# an optional `repos:` map of `: `. Lists every problem; exits 1 if any. +set -euo pipefail +cd "${1:-.}" +problems=0 +bad() { echo "check: $*" >&2 && problems=$((problems + 1)); } +tpl=templates/workspace +managed=".delphi/setup.sh .github/workflows/delphi.yml" + +[ -d software ] || bad "software/ is missing" +cmp -s .github/workflows/delphi.yml $tpl/.github/workflows/delphi.yml || + bad "$tpl/.github/workflows/delphi.yml: differs from .github/workflows/delphi.yml" +while IFS= read -r f; do bad "$f: symlinks are not allowed under software/"; done \ + < <(find software -type l 2>/dev/null) + +folders="" +while IFS= read -r f; do + folder=${f%/workspace.yml} + name=${folder##*/} + case "$(dirname "$folder")" in */workspaces) ;; *) + bad "$f: must be at software/**/workspaces//workspace.yml" && continue ;; + esac + case "$folder" in *[!A-Za-z0-9._/-]*) bad "$folder: path has unsafe characters" && continue ;; esac + case "$name" in -* | *[!a-z0-9-]*) bad "$folder: name must be lowercase letters, digits, and '-'" ;; esac + for other in $folders; do + [ "${other##*/}" != "$name" ] || bad "$folder: name '$name' is also used by $other" + case "$folder/" in "$other"/*) bad "$folder: nested inside workspace $other" ;; esac + case "$other/" in "$folder"/*) bad "$other: nested inside workspace $folder" ;; esac + done + folders="$folders $folder" + for m in $managed; do cmp -s "$tpl/$m" "$folder/$m" || bad "$folder/$m: differs from $tpl/$m"; done + while IFS= read -r msg; do bad "$f: $msg"; done < <(awk ' + /^[[:space:]]*#/ || /^[[:space:]]*$/ { next } + /^harness:/ { v = $0; sub(/^harness:[[:space:]]*/, "", v); sub(/[[:space:]]*#.*$/, "", v) + if (v == "") print "harness is empty"; if (h++) print "duplicate harness"; r = 0; next } + /^repos:[[:space:]]*(#.*)?$/ { r = 1; next } + r && /^[[:space:]]/ { + if ($0 !~ /^[[:space:]]+[A-Za-z0-9._-]+:[[:space:]]+[^[:space:]"#]+[[:space:]]*(#.*)?$/) { + print "bad repos entry (want ` : `): " $0; next } + n = $1; sub(/:$/, "", n) + if (n == "." || n == "..") print "bad repo name: " n + if (seen[n]++) print "duplicate repo: " n + next } + { print "unexpected line: " $0 } + END { if (!h) print "harness is missing" }' "$f") +done < <(find software -name workspace.yml -type f 2>/dev/null | sort) + +[ "$problems" = 0 ] || { echo "check: $problems problem(s)" >&2 && exit 1; } +echo "check: ok" diff --git a/ci/sync.sh b/ci/sync.sh new file mode 100755 index 0000000..2c24d41 --- /dev/null +++ b/ci/sync.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# ci/sync.sh [refresh|propose] []: sync each workspace folder software/**/workspaces// on +# origin/main (or just ) with its branch ws/, whose root is the folder. CI runs it with +# no direction (both) on every push to main or ws/**. +# refresh: merge main into ws/ (-Xsubtree=) and push if the tree changed. A missing +# ws/ is created as one commit: tree = the folder, parent = main. +# propose: build propose/ = main + a subtree merge of ws/; if that changes main and +# isn't already on propose/, check it, force-push it, and open or update its PR. +# A conflict is reported with its files and that workspace skipped; exits 1 if any failed. ws/* +# branches without a folder get a notice, never deleted. Merges run in a temporary worktree. +set -euo pipefail +dir=both +case "${1:-}" in refresh | propose) dir=$1 && shift ;; esac +only=${1:-} +case "$only" in *[!a-z0-9-]*) echo "usage: ci/sync.sh [refresh|propose] []" >&2 && exit 2 ;; esac + +git fetch --quiet --prune origin +main=$(git rev-parse origin/main) +wt=$(mktemp -d) +git worktree add --quiet --detach "$wt" "$main" +trap 'git worktree remove --force "$wt"' EXIT +failed=0 names=" " +tree() { git -C "$wt" rev-parse --quiet --verify "$1^{tree}" || true; } # empty if is missing + +conflict() { # : report the conflicted files, abort the merge, skip this workspace + echo "$1 Files:" >&2 + git -C "$wt" diff --name-only --diff-filter=U | sed 's/^/ /' >&2 + git -C "$wt" merge --abort 2>/dev/null || true # nothing to abort if the merge never started + failed=1 ok=0 +} + +refresh() { # : leave the worktree at ws/ with main merged in + local ws=refs/remotes/origin/ws/$1 + if [ -z "$(tree "$ws")" ]; then + git -C "$wt" checkout --quiet --detach \ + "$(git commit-tree -p "$main" -m "delphi: create ws/$1 from $2" "$main:$2")" + git -C "$wt" push --quiet origin "HEAD:refs/heads/ws/$1" + echo "ws/$1: created from $2" + return + fi + git -C "$wt" checkout --quiet --detach "$ws" + # --no-ff: once ws commits are in main, a fast-forward would put main's whole tree on ws/. + if ! git -C "$wt" merge --quiet --no-ff -Xsubtree="$2" -m "delphi: refresh ws/$1 from main" "$main" >/dev/null; then + conflict "ws/$1: CONFLICT refreshing from main; merge main into ws/$1 in a PR (see README)." + elif [ "$(tree HEAD)" != "$(tree "$ws")" ]; then + git -C "$wt" push --quiet origin "HEAD:refs/heads/ws/$1" + echo "ws/$1: refreshed" + fi +} + +propose() { # : PR the worktree's ws/ into main if it changes anything + local ws title="ws/$1 → main" body pr + ws=$(git -C "$wt" rev-parse HEAD) + git -C "$wt" checkout --quiet --detach "$main" + if ! git -C "$wt" merge --quiet --no-ff -Xsubtree="$2" -m "delphi: propose ws/$1 to main" "$ws" >/dev/null; then + conflict "ws/$1: CONFLICT proposing to main; merge main into ws/$1 in a PR (see README)." + return + fi + case "$(tree HEAD)" in "$(tree "$main")" | "$(tree "refs/remotes/origin/propose/$1")") return ;; esac + "$wt/ci/check.sh" "$wt" || { echo "ws/$1: check failed; not proposed" >&2 && failed=1 && return; } + git -C "$wt" push --quiet --force origin "HEAD:refs/heads/propose/$1" + body="Proposes \`ws/$1\` into \`$2/\` (opened by ci/sync.sh; see README). + +Changed files: +$(git -C "$wt" diff --name-only "$main" HEAD | sed 's/^/- /') + +Workspace commits: +$(git log --no-merges --invert-grep --grep='^delphi: ' --format='- %s (%an)' "$main..$ws")" + pr=$(gh pr list --base main --head "propose/$1" --state open --json number --jq '.[0].number // empty') + if [ -n "$pr" ]; then + gh pr edit "$pr" --title "$title" --body "$body" >/dev/null + else + gh pr create --base main --head "propose/$1" --title "$title" --body "$body" >/dev/null + fi + echo "ws/$1: proposed (PR from propose/$1)" +} + +for folder in $(git ls-tree -r --name-only "$main" -- software | + sed -nE 's#^(software/(.+/)?workspaces/[^/]+)/workspace\.yml$#\1#p'); do + name=${folder##*/} ok=1 + names="$names$name " + [ -z "$only" ] || [ "$only" = "$name" ] || continue + if [ "$dir" != propose ]; then + refresh "$name" "$folder" + elif [ -n "$(tree "refs/remotes/origin/ws/$name")" ]; then + git -C "$wt" checkout --quiet --detach "refs/remotes/origin/ws/$name" # propose ws/ as is + else ok=0; fi + if [ "$ok" = 1 ] && [ "$dir" != refresh ]; then propose "$name" "$folder"; fi +done + +[ -z "$only" ] || case "$names" in *" $only "*) ;; *) echo "no workspace '$only' on main" >&2 && exit 1 ;; esac +for b in $(git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ws/); do + case "$names" in *" ${b#ws/} "*) ;; *) echo "notice: $b has no workspace folder on main (kept)" ;; esac +done +exit "$failed" diff --git a/context/docs/delphi-design.md b/context/docs/delphi-design.md deleted file mode 100644 index 067876d..0000000 --- a/context/docs/delphi-design.md +++ /dev/null @@ -1,473 +0,0 @@ -# Delphi — Context Repository Design - -**Date:** 2026-09-23 -**Status:** Draft for review -**Owner:** Application Software (NER) - -## 1. Purpose - -Delphi is a monorepo that stores Northeastern Electric Racing's AI-harness context — knowledge, instructions, skills, MCP definitions, settings — in a **compressed** form organized by the org chart, plus a small bash CLI that: - -1. **Compiles** a chosen combination of that context (a *layout*) into an **uncompressed**, local, git-initialized *workspace* where a harness (Claude Code first) runs for real development. -2. **Refreshes** that workspace when Delphi's `main` moves. -3. **Proposes** changes made in the workspace back to the monorepo as a PR, routing each edit to the block it came from and recording the model, harness, and effort level used. - -High-level commands stay simple; complexity lives in `lib/`. - -## 2. Terminology - -| Term | Meaning | -|---|---| -| **Scope** | A directory under `context/` representing an org unit (club → area → subteam → team). Identified by its path. | -| **Block** | A tracked unit of context, identified by its path relative to `context/`. | -| **Basic block** | Text/markdown under a scope's `blocks/`. | -| **Doc** | Project documentation (glossary, ADRs) under a scope's `docs/`, compiled into the workspace's `docs/`. | -| **Harness block** | Under a scope's `harness/`: instruction fragments, skills, MCP fragments, settings. | -| **Layout** | A `manifest.yml` under a scope's `layouts//` selecting blocks, a harness, and code repos. Compressed side. | -| **Workspace** | A local git-initialized directory, outside the repo, compiled from a layout. Uncompressed side. Many workspaces may come from one layout. | -| **Compile** | The deterministic function `(delphi commit, layout) → files + lock`. | -| **Lock** | `.delphi/lock.tsv`: segment map from compiled output line ranges to their sources. | -| **Segment** | A contiguous line range of one output file that came from one source. | -| **Pending diff** | `git diff generated-merged HEAD` over routable paths: everything the workspace has changed relative to its latest compile. | - -## 3. Repository structure - -``` -Delphi/ -├── CLAUDE.md # for developing Delphi itself -├── .claude/skills/ -│ ├── delphi-new-layout/ # LLM workflow: build a layout from recommendations -│ └── delphi-propose/ # LLM workflow: resolve unresolved items, then propose -├── delphi.conf # key=value repo config -├── moves.tsv # block rename/move log -├── bin/delphi # dispatcher -├── lib/ -│ ├── core.sh # config, logging, prompts, safe_path, layout lookup, move resolution -│ ├── parse.sh # awk YAML-subset parser -│ ├── compile.sh # layout → files + lock -│ ├── route.sh # pending diff + lock → routing plan -│ ├── pr.sh # temp worktree, commit w/ trailers, push + gh PR -│ ├── provenance.sh # resolve harness/model/effort -│ ├── layout.sh # `delphi layout …` -│ ├── workspace.sh # `delphi workspace …` -│ ├── block.sh # `delphi block …` -│ ├── check.sh # `delphi check` -│ └── harness/ -│ └── claude-code.sh # harness adapter -└── context/ # root scope = NER club-wide - ├── scope.yml - ├── blocks/ docs/ harness/ layouts/ - └── software/ # child scope - ├── scope.yml - ├── blocks/ docs/ harness/ layouts/ - ├── finishline/ - ├── application-software/ - │ ├── argos/ - │ └── nero/ - └── firmware/ - └── … -``` - -### 3.1 Scope shape - -Every scope directory contains a required `scope.yml` and any of these **reserved** subdirectories: - -``` -/ -├── scope.yml -├── blocks/ # basic blocks: *.md, *.txt (subdirs allowed) -├── docs/ # project docs: CONTEXT.md, adr/*.md (subdirs allowed) -├── harness/ -│ ├── instructions/*.md # fragments stacked into the harness instruction file -│ ├── skills//SKILL.md (+ any files) # native skill -│ ├── skills/.skill # built skill spec -│ ├── mcp/.json # MCP server fragment -│ └── settings/.json # harness settings file -└── layouts//manifest.yml -``` - -Any other subdirectory is a child scope. Scope names follow the Fall 2026 roster structure (`Software → FinishLine | Software Product | Application Software → {Argos, NERO} | Firmware → {…}`); scopes are created as content arrives, not up front. - -**No file anywhere under `context/` may be named after a harness instruction file** (`CLAUDE.md`, `AGENTS.md`, …) — enforced by `check`. - -### 3.2 Workspace location - -Workspaces compile **outside** the repo so Delphi's own `CLAUDE.md` is never loaded as an ancestor file: - -``` -/ # default ../Delphi-workspaces (relative to repo root) -├── argos-dev/ -└── argos-dev--bug-612/ -``` - -## 4. File formats - -### 4.1 YAML subset (parsed by `lib/parse.sh`) - -All `.yml` and `.skill` files use this subset: - -- Full-line `#` comments and trailing ` #` comments. -- Top-level `key: value` (scalar; unquoted or `"double-quoted"`). -- Top-level `key:` followed by ` - item` lines (list; two-space indent). -- Top-level `key:` followed by ` subkey: value` lines (one-level map; two-space indent). -- **Not supported** (parse error with `file:line`): tabs, anchors/aliases, flow style (`[a, b]`, `{}`), multi-line strings, deeper nesting. - -Parser output (tab-separated, one record per line): - -| Construct | Output | -|---|---| -| scalar | `keyvalue` | -| list item | `keyvalue` (repeated) | -| map entry | `keysubkeyvalue` | - -Consumers know each key's type from the schemas below. Parser uses POSIX awk only (verified against BSD awk and mawk). - -### 4.2 `scope.yml` - -```yaml -name: Argos -recommend: # paths relative to context/; offered, never auto-included - - software/application-software/argos/blocks/overview.md - - software/application-software/argos/harness/skills/run-tests -``` - -### 4.3 `manifest.yml` (layout) - -```yaml -name: argos-dev # must equal the layout directory name; unique repo-wide -harness: claude-code # required; must match lib/harness/.sh -instructions: # stacked, in order, into $HARNESS_INSTRUCTIONS - - software/harness/instructions/base.md - - software/application-software/argos/harness/instructions/argos.md -blocks: # basic blocks; trailing * glob allowed (non-recursive, sorted) - - software/application-software/argos/blocks/* -docs: # project docs; same entry rules as blocks - - software/application-software/argos/docs/CONTEXT.md - - software/application-software/argos/docs/adr/* -skills: # native skill dir or .skill spec - - software/application-software/argos/harness/skills/run-tests -mcp: - - software/harness/mcp/github.json -settings: software/harness/settings/default.json # optional; at most one in v1 -repos: # name: git URL - argos: git@github.com:Northeastern-Electric-Racing/Argos.git -``` - -All paths are relative to `context/`. Only `name` and `harness` are required. Unknown keys are errors. - -### 4.4 `.skill` (built skill spec) - -```yaml -name: run-tests -description: Run and interpret Argos test suites. -body: # stacked into SKILL.md body - - software/application-software/argos/blocks/testing.md -references: # copied to references/ - - software/application-software/argos/blocks/ci.md -``` - -Lets one basic block serve as plain context in one layout and as a skill in another without duplication. - -### 4.5 MCP fragment - -Each `harness/mcp/.json` contains exactly one member of the `mcpServers` object, no trailing comma: - -```json -"github": { - "command": "gh-mcp", - "args": [] -} -``` - -### 4.6 `delphi.conf` - -``` -workspace_root=../Delphi-workspaces -stale_days=14 -``` - -Parsed line-by-line as `key=value` (never `source`d). Relative paths resolve against the repo root. `DELPHI_WORKSPACE_ROOT` env var overrides `workspace_root`. - -### 4.7 `moves.tsv` - -``` -# oldnewdate -software/blocks/git.md software/blocks/git-conventions.md 2026-10-02 -software/application-software/argos/blocks/old-dir software/application-software/argos/blocks/ops 2026-10-05 -``` - -Append-only. Rows are applied **in order, each once** (exact match, or directory-prefix match for directory entries), and only the rows that are new to the consumer: `block mv` applies the row it appends; a workspace applies the rows added since its `compile_commit`. Paths may therefore be reused, and a block can be moved back. - -### 4.8 `lock.tsv` - -``` -# output start end source sha -CLAUDE.md 1 3 @gen:delphi - -CLAUDE.md 4 4 @glue - -CLAUDE.md 5 44 software/harness/instructions/base.md a1b2c3… -CLAUDE.md 45 45 @glue - -CLAUDE.md 46 92 software/application-software/argos/harness/instructions/argos.md d4e5f6… -.claude/skills/run-tests/SKILL.md 1 4 @gen:software/application-software/argos/harness/skills/run-tests.skill - -.claude/skills/run-tests/SKILL.md 5 60 software/application-software/argos/blocks/testing.md f7a8b9… -.delphi/manifest.yml 1 20 software/application-software/argos/layouts/argos-dev/manifest.yml 0c1d2e… -``` - -`sha` is the git blob id of the source at the compiled commit. Every line of every compiled file except `lock.tsv` itself belongs to exactly one segment. Source kinds: a path under `context/`, `@glue` (separator lines), `@gen:` (generated lines). - -### 4.9 Workspace state - -| Location | In workspace git? | Contents | -|---|---|---| -| `.delphi/manifest.yml` | tracked (compiled) | Editable copy of the layout manifest; edits propose back to the layout. | -| `.delphi/lock.tsv` | tracked (compiled) | Segment map of the compile. Always read from `generated-merged`, never the working copy. | -| `.git/delphi/meta` | not tracked (inside `.git/`) | `key=value`: `layout`, `layout_path`, `ref` (Delphi branch the workspace tracks, default `main`), `created`, `last_proposed`, `last_proposed_hash`, `last_pushed`, `pending_since`, `compile_commit` (the Delphi commit `generated-merged`'s content corresponds to — authoritative; may be newer than that commit's trailer when later compiles were identical). | -| `.git/info/exclude` | not tracked | `repos/`, `worktrees/`, `$HARNESS_IGNORE`. | -| `.git/hooks/commit-msg` | not tracked | Provenance trailer hook (§9). | - -Keeping state inside `.git/` means Delphi bookkeeping never dirties the working tree and never shows up in the pending diff. - -**Refs in the workspace repo:** - -| Ref | Meaning | -|---|---| -| `generated` (branch) | History of compiles only. Each commit's message is `delphi: compile @` with trailer `Delphi-Compile: `. | -| `generated-merged` (tag) | The latest compile that has been **merged into `working`**. Its current Delphi commit is `meta.compile_commit`. | -| `working` | The user's branch, where all development happens. | - -`generated` and `generated-merged` differ only while a refresh is pending (compile committed, merge not yet completed). - -The workspace repo is created with `git init` and shares no history with Delphi; none of these refs are Delphi branches. `generated` holds only Delphi's compiled output (never user work); `working` starts from the first compile and merges each later one. The only links back to Delphi are the `Delphi-Compile` trailer text and `meta.compile_commit`. - -## 5. Compiling (`lib/compile.sh`) - -`compile `: deterministic. Reads sources from a temporary detached worktree of Delphi at ``, writes output files and `/.delphi/lock.tsv`. Each source file's content is emitted with a trailing newline added if missing, so segments never share a line. - -| Manifest key | Output | Segments | -|---|---|---| -| `instructions` | `$HARNESS_INSTRUCTIONS`: delphi header, then fragments in order, one blank line between each. Emitted even when the list is empty (header only). | `@gen:delphi` header, `@glue` blank lines, block per fragment | -| `blocks` | `context/` — mirrors the block's path exactly (e.g. `software/application-software/argos/blocks/ops/x.md` → `context/software/application-software/argos/blocks/ops/x.md`) | one segment per file | -| `docs` | `docs/` (e.g. `software/application-software/argos/docs/adr/0001-x.md` → `docs/adr/0001-x.md`) | one segment per file | -| `skills` (native) | `$HARNESS_SKILLS_DIR//…`, each file (subdirectories included, e.g. `scripts/`) copied 1:1, executable bit kept | one segment per file | -| `skills` (`.skill`) | `$HARNESS_SKILLS_DIR//SKILL.md` = generated frontmatter (`name`, `description`) + body blocks with `@glue` between; references copied to `references/` | `@gen:` frontmatter, block per body/reference | -| `mcp` | `$HARNESS_MCP_FILE`: `{"mcpServers": {` + fragments separated by a `,` line + `}}`. Omitted when the list is empty. | `@gen:delphi` wrapper, block per fragment, `@glue` commas | -| `settings` | `$HARNESS_SETTINGS_FILE`, copied 1:1 | one segment | -| (always) | `.delphi/manifest.yml` copy of the layout manifest | one segment | - -**Delphi header** (top of the instruction file, placed first so appends at the end of the file land in a real block): a short note that this workspace was compiled by Delphi from layout ``, that edits are expected, and that finished work should be committed so `delphi workspace propose` can send it upstream. - -Compile errors (missing path, empty glob, parse error, duplicate output path, unsafe path) abort with no partial output. - -## 6. Commands - -`bin/delphi [args]`. `ws` is an alias for `workspace`. Commands that take `[]` default to the workspace containing the current directory. - -**Common flags** for commands that write to Delphi (`layout new`, `block mv`, `workspace propose`): `--model `, `--effort ` (provenance, §9) and `--yes` (skip the push confirmation; also `DELPHI_YES=1`). Without `--yes`, a non-interactive run (no TTY — e.g. an agent's shell tool) fails fast with a message to pass `--yes` rather than hanging. - -### 6.1 `delphi layout new [--from ]` - -1. Validate: scope has `scope.yml`; `` matches `[a-z0-9-]+` and is unique repo-wide. -2. Without `--from` (basic script): collect `recommend` entries from the scope and each ancestor (closest first, deduped); ask y/n per item; ask harness (choices = `lib/harness/*.sh`); prompt for repos (`name url` lines until blank). Items are placed into manifest keys by path: `*/harness/instructions/*` → `instructions`, `*/harness/skills/*` → `skills`, `*/harness/mcp/*` → `mcp`, `*/harness/settings/*` → `settings` (more than one → re-ask), `*/blocks/*` → `blocks`, `*/docs/*` → `docs`. -3. With `--from`: use the given manifest verbatim (used by the `delphi-new-layout` skill). -4. Via `pr.sh` (§7): write `context//layouts//manifest.yml`, run `check`, commit, PR on branch `delphi/layout/`. Prints the branch name. - -### 6.2 `delphi layout list` - -Prints `namescopeharness` for every layout on `origin/main`. - -### 6.3 `delphi workspace new [--as ] [--ref ]` - -1. `git fetch`. Resolve the layout by name **in the tree of `origin/`** (default `main`), following `moves.tsv` at that commit. Workspace name defaults to the layout name; error if `/` exists. -2. Compile at `origin/` into a temp dir. -3. `git init` the workspace; write `.git/info/exclude`, `.git/delphi/meta` (with `ref`, `pending_since` empty), and the `commit-msg` hook. -4. Copy the compile in, commit on `generated`, tag `generated-merged`, create and check out `working` from it; set `meta.compile_commit`. The working tree is clean. -5. Clone each repo into `repos/`; create empty `worktrees/`. Clone failures warn and are summarized at the end; the workspace is still usable. - -`--ref` lets you try a layout before its PR merges (`workspace new argos-dev --ref delphi/layout/argos-dev`). Such a workspace refreshes from that branch; `propose` refuses until the layout exists on `main` (§6.6), and `refresh --ref main` switches it over once merged. - -### 6.4 `delphi workspace open [] [--model m] [--effort e] [--shell]` - -1. Resolve workspace (argument, current directory, or numbered picker over `status` output). -2. Warn if the compile is behind its ref (suggest `refresh`) or if any workspace is `unproposed` for more than `stale_days`. -3. Export `DELPHI_HARNESS`, `DELPHI_MODEL`, `DELPHI_EFFORT` and `exec harness_launch` in the workspace directory; with `--shell`, `exec $SHELL` there instead. - -### 6.5 `delphi workspace refresh [] [--ref ]` - -Idempotent; re-running always picks up where it left off. No `--continue`. - -1. Require a clean working tree and no merge in progress. `--ref` updates `meta.ref` first. -2. `git fetch` Delphi; resolve the layout path through `moves.tsv` at `origin/`; update `meta.layout_path`. -3. **Compile step** — skipped if `generated` is ahead of `generated-merged` (a pending refresh): compile at `origin/` commit `C` into a temporary worktree of the workspace repo checked out on `generated`, replacing its tracked contents. - - Content changed → commit with trailer `Delphi-Compile: C`. - - Content identical → set `meta.compile_commit = C` (no commits; keeps the recorded commit current, e.g. after a squash-merged layout PR or unrelated `main` activity), report "up to date", exit 0. - If `origin/` no longer exists, fail with "branch `` is gone — run `refresh --ref main`". -4. **Merge step** — skipped if `generated` is already an ancestor of `HEAD`: `git merge --no-edit generated`. On conflict, exit **2**: "resolve conflicts, commit, then re-run `delphi workspace refresh`." -5. **Finalize** (only once `generated` is an ancestor of `HEAD`): move tag `generated-merged` to `generated`; set `meta.compile_commit` from that commit's `Delphi-Compile` trailer. Rewrite any moved paths in `.delphi/manifest.yml`; if changed, commit `delphi: apply moves`. If the pending diff is now empty, set `meta.pending_since = HEAD`. Exit 0. - -All commits Delphi itself makes in a workspace (compiles, merges, `apply moves`) use `--no-verify` so the provenance hook never tags them. - -**Exit codes:** 0 = up to date or finalized; 2 = merge conflict awaiting resolution; 1 = error. - -If a refresh is pending when `--ref` changes, the pending compile is merged and finalized first, then the compile step runs once more against the new ref. Compile and merge commits only appear in workspace history when the compiled content actually changed. - -### 6.6 `delphi workspace propose [] [--dry-run]` - -**One live PR per workspace**, on branch `delphi/propose//` (`` from `gh api user --jq .login`; workspace names are local, so the user part prevents teammates' branches colliding). Each propose rebuilds that branch from scratch with the **entire** current pending diff and force-updates it, so the PR always equals "everything this workspace still differs from `main` by." Proposing twice never duplicates; a merged PR's changes drop out after the next refresh; a closed/rejected change keeps reappearing until it is reverted in the workspace (the documented escape hatch). - -1. `--dry-run`: skip refresh; route against the current `generated-merged` (warn if behind); print the plan and unresolved items; change nothing; stop. -2. Require a clean working tree and `meta.ref = main` (else: "layout not on main yet — merge its PR, then `refresh --ref main`"). Run `refresh`; continue only if it exits 0. -3. Compute the pending diff and route it (§8) into a plan. -4. If the plan has no routed changes and no unresolved items: print "nothing to propose", exit 0. -5. Via `pr.sh`, building from `meta.compile_commit` (so patches apply exactly), in order: - 1. **Layout manifest** — if `.delphi/manifest.yml` changed, replace the layout manifest with it (moved paths resolved). - 2. **Block edits** — apply routed hunks. - 3. **New blocks** — add files; append each to the layout manifest's `blocks:`/`docs:`/`skills:` unless an existing entry or glob already covers it. New instruction fragments (§8) are inserted into `instructions:` at their position. - 4. Run `check`; abort (no push) on failure, printing the violations. - Each step with changes is one commit with provenance trailers. -6. Push (force, with an explicit lease — §7); `gh pr create` if no open PR exists for the branch, otherwise `gh pr edit` to replace the body. Refuse if an open PR on the branch was authored by someone other than the current `gh` user. PR body: routed-change summary, provenance table (§9), **Unresolved** section (each item as a fenced diff with its workspace path and reason). -7. Set `meta.last_proposed` = today and `meta.last_proposed_hash` = pending diff hash. - -**Pending diff hash:** `git hash-object` of the `-U0` pending diff with `@@` hunk headers and `index` lines stripped, so upstream changes that only shift line numbers don't flip a workspace back to `unproposed`. - -### 6.7 `delphi workspace status [--offline]` - -For each directory in `workspace_root` containing `.git/delphi/meta`, print: workspace, layout, ref, dirty flag, **state**, age, behind flag. - -| State | Condition | -|---|---| -| `clean` | pending diff empty | -| `proposed` | pending diff hash = `last_proposed_hash` | -| `unproposed` | otherwise | - -Age = days since `last_proposed` (or `created`) for `unproposed`. Behind = `origin/` has commits since `meta.compile_commit` touching any lock source, the layout manifest, or a directory covered by a manifest glob. `--offline` skips `git fetch`. Status cannot see whether a PR was closed; a rejected workspace stays `proposed` until its change is reverted or re-proposed. - -### 6.8 `delphi block mv ` - -Only paths under a scope's `blocks/`, `docs/`, or `harness/` (layouts cannot be moved in v1). Via `pr.sh`: validate `old` exists and `new` does not, both inside `context/`; `git mv`; append to `moves.tsv`; rewrite exact and directory-prefix references in every `manifest.yml`, `scope.yml`, and `.skill`; run `check`; commit; PR on branch `delphi/mv/-`. Existing workspaces pick up the move on their next `refresh`/`propose`. - -### 6.9 `delphi check` - -Exits non-zero listing every violation: - -- Every scope directory has `scope.yml`; every `.yml`/`.skill` parses. -- Manifests: required keys present, no unknown keys, `name` equals directory, harness adapter exists, at most one `settings`, layout names unique. -- Every referenced path exists (each glob matches ≥1 file) and resolves inside `context/`. -- Entries are under the key matching their location (`skills` entries are a dir with `SKILL.md` or a `.skill` file, etc.). -- No file under `context/` is named any adapter's `$HARNESS_INSTRUCTIONS`. -- `moves.tsv` rows well-formed. -- MCP fragments form valid JSON when wrapped — only if `jq` is installed; otherwise skipped with a note. - -### 6.10 `delphi setup [dir]` - -Writes a two-line `delphi` wrapper (`exec /bin/delphi "$@"`) into `dir` (default `~/.local/bin`) and warns if `dir` is not on `PATH`. A wrapper rather than a symlink, because Git Bash copies on `ln -s` by default. - -## 7. Writing to Delphi (`lib/pr.sh`) - -All commands that modify the monorepo use one path: - -1. `git fetch --prune`; create a temporary worktree of Delphi on the command's branch, starting from a given commit (default `origin/main`). The user's own checkout is never touched. -2. Caller applies changes inside it. -3. Commit with provenance trailers (§9). -4. Confirm `Push and open/update PR? [y/N]` unless `--yes`; then push and `gh pr create` / `gh pr edit` with the caller's body plus the provenance table. Propose branches are force-pushed with an explicit lease: `--force-with-lease=:` where `` is `meta.last_pushed`, the commit this workspace last pushed (empty when the branch is absent on the remote, e.g. auto-deleted after a merge). If the remote branch exists but differs from `last_pushed`, someone else pushed to it: propose stops ("review the PR, then re-run"), records the remote sha, and the next run overwrites it. -5. Remove the temp worktree via `trap` on success or failure. - -## 8. Change routing (`lib/route.sh`) - -Input: pending diff (`git diff --no-renames generated-merged HEAD`), excluding `repos/`, `worktrees/`, `.delphi/lock.tsv`. Lock read from `generated-merged`. Output: plan rows `kindworkspace-pathtarget[reason]`. Checked in this order per file: - -| # | Change | Result | -|---|---|---| -| 1 | `.delphi/manifest.yml` modified | layout manifest replace (whole file) | -| 2 | Binary | unresolved | -| 3 | Deleted | no-op if its source is no longer referenced by the workspace's `.delphi/manifest.yml` (dropped via the manifest); otherwise unresolved — blocks are dropped by editing `.delphi/manifest.yml` | -| 4 | Modified, in lock | per-hunk routing (below) | -| 5 | Added at `context/

` where `

` is `/blocks/…` and `` is an existing scope at `meta.compile_commit` | new block at `

` | -| 5a | Added at `docs//` | new doc beside the compiled docs already in `docs//` (their source directory), else at `/docs//` | -| 6 | Added under `$HARNESS_SKILLS_DIR//` where `` is a native skill in the lock | new file in that skill's source directory | -| 7 | Added under `$HARNESS_SKILLS_DIR//` where `` is not in the lock | new native skill at `/harness/skills//` | -| 8 | Anything else | unresolved | - -**Hunk routing** (from `git diff -U0`; base-side range `a,n`; patches applied with `git apply --unidiff-zero`): - -| Hunk | Result | -|---|---| -| Insertion (`n = 0`) in `$HARNESS_INSTRUCTIONS` at a segment boundary (`a = 0` or `a = seg.end`) | **new fragment**, except the leading/trailing lines touching a neighbouring block with no blank line between, which extend that block (rows below). The rest, trimmed of blank lines, goes to `/harness/instructions/.md` (slug from its first line; `-2`, `-3`, … when taken) and is listed in the layout's `instructions:` after the preceding fragment (first when none) | -| All base lines `a..a+n-1` inside one block segment | patch that block (`block_line = base_line − seg.start + 1`) | -| Insertion (`n = 0`) after line `a`, where `seg.start ≤ a < seg.end` of a block segment | patch that block | -| Insertion after the last line of a block segment (`a = seg.end`), when the next line is end-of-file or a non-block segment | append to that block | -| Insertion at file start (`a = 0`), when the first segment is a block | prepend to that block | -| Insertion right after a `@glue`/`@gen` segment (`a = seg.end`), when the next segment is a block | prepend to that next block | -| Inside `@gen:.skill`, touching only `name:`/`description:` lines | rewrite those keys in the `.skill` spec | -| Anything else: in `@glue`/other `@gen`, spanning segments, or `git apply` fails (e.g. the same block edited in two outputs) | unresolved | - -**Replacing a block** needs no special case: swap the entry in `.delphi/manifest.yml` (old path → new path), delete the old compiled file, and add the new one under `context//blocks/`. The deletion is a no-op (rule 3), the manifest edit routes by rule 1, and the new file by rule 5. - -**Placement is strict by design:** a file not in a recognised location stays unresolved; Delphi never guesses where it belongs. - -All routed hunks for one (output file, block) pair are combined into a single patch so line offsets stay consistent. - -Unresolved items never block the PR; they are listed in it. They are resolved by changing the **workspace** so the change becomes routable (move text into a block's region, move a new file under `context//blocks/`, edit `.delphi/manifest.yml`, or revert it), then proposing again. - -## 9. Provenance - -Every commit Delphi writes to the monorepo carries trailers; every PR body repeats them as a table. - -``` -Delphi-Harness: claude-code 2.4.1 -Delphi-Model: claude-opus-5-5 -Delphi-Effort: high -Delphi-Layout: software/application-software/argos/layouts/argos-dev -Delphi-Workspace: argos-dev--bug-612 # propose only -Delphi-Base: 0cdd375 # propose only: meta.compile_commit -``` - -**Resolution order** (`lib/provenance.sh`), per field: CLI flag → `DELPHI_*` env (set by `workspace open`) → adapter's `harness_provenance` fallback. If still missing: prompt when interactive (accepting `none` for runs without an LLM); error when non-interactive. Never written as "unknown". - -**Workspace commit hook:** `.git/hooks/commit-msg` appends `Delphi-Harness/Model/Effort` trailers from `DELPHI_*` env vars when set and not already present. - -**Propose aggregation:** the PR table lists the proposing session's values plus each distinct trailer combination from non-merge workspace commits in `pending_since..HEAD` (all of `working` if `pending_since` is empty). - -**Known gap:** if the model changes mid-session (e.g. `/model`), the env vars from `workspace open` go stale; flags on `propose` override. - -## 10. Harness adapter contract - -`lib/harness/.sh` is sourced and must define: - -| Name | Kind | claude-code value | -|---|---|---| -| `HARNESS_INSTRUCTIONS` | var | `CLAUDE.md` | -| `HARNESS_SKILLS_DIR` | var | `.claude/skills` | -| `HARNESS_MCP_FILE` | var | `.mcp.json` | -| `HARNESS_SETTINGS_FILE` | var | `.claude/settings.json` | -| `HARNESS_IGNORE` | var | `.claude/settings.local.json` | -| `harness_provenance` | fn | prints 3 lines: `claude-code `, model, effort (from `CLAUDE_CODE_EFFORT_LEVEL` when set) | -| `harness_launch ` | fn | `exec claude` with `--model` when given and `CLAUDE_CODE_EFFORT_LEVEL` set | - -The generic compiler does all file work; adapters only supply names and two functions. New harnesses = new adapter file. - -## 11. LLM workflows (Delphi repo skills) - -- **`delphi-new-layout`** (primary way to create layouts): interviews the user about the work, reads `scope.yml` recommendations up the scope chain and browses available blocks, drafts a manifest, shows it for approval, then runs `delphi layout new --from --model … --effort … --yes`. Optionally follows with `delphi workspace new --ref delphi/layout/` to try it. -- **`delphi-propose`**: runs `delphi workspace propose --dry-run`, walks the user through each unresolved item with a suggested fix in the workspace (per §8's resolution list), applies and commits approved fixes, then runs `delphi workspace propose --model … --effort … --yes`. - -Both are optional conveniences; every operation is available through the scripts alone. - -## 12. Cross-cutting rules - -- **Locating Delphi:** `bin/delphi` resolves its own real path (a `readlink` loop, no `readlink -f`) and uses its parent as the repo root, both in the Delphi checkout and inside workspaces. Install = put `Delphi/bin` on `PATH` or symlink `bin/delphi`. -- **Bash 3.2 compatible** (macOS default): no associative arrays, `mapfile`, `${x,,}`, etc. `set -euo pipefail` in every entry point. -- **Dependencies:** `git`, `awk` (POSIX features only), `sed`, `gh` (only for opening PRs). `jq` optional (check only). Blob ids and diff hashes via `git hash-object`. -- **Path safety:** every path from config, flags, lock, or `moves.tsv` goes through `safe_path ` (rejects absolute paths, `..` escapes, and symlinks resolving outside ``) before any read, write, or delete. Deletes happen only via `git` or inside `mktemp -d` dirs. (Prior prototypes all failed this.) -- **Errors:** actionable messages naming the file/line or command to run next; temp worktrees cleaned via `trap`; multi-step operations are idempotent on re-run, never half-applied silently. - -## 13. Out of scope for v1 - -- Automated tests and CI (deferred by decision until the working version has been used). -- Seed content (Argos scope and layout come next, separately). -- Harness adapters other than claude-code. -- Merging multiple settings files; recursive globs; automatic routing of deletions; moving/renaming layouts. -- Managing `worktrees/` beyond creating the directory (use `git worktree` directly). -- Mechanical, Electrical, and Business scopes. - -## 14. Resolved decisions - -- Default `workspace_root` is `../Delphi-workspaces` (sibling of the Delphi clone); configurable via `delphi.conf` or `DELPHI_WORKSPACE_ROOT`. diff --git a/context/harness/instructions/workspace.md b/context/harness/instructions/workspace.md deleted file mode 100644 index d2d5306..0000000 --- a/context/harness/instructions/workspace.md +++ /dev/null @@ -1,13 +0,0 @@ -# Delphi workspace - -You are in a Delphi workspace: a folder that pairs team context with the code repos it's about. It has two kinds of git repo, and every change belongs to exactly one: - -| Path | What it is | Changes go | -|---|---|---| -| `CLAUDE.md`, `.claude/`, `context/`, `docs/` | Team context and docs pulled from Delphi | Workspace git (`working` branch); `delphi workspace propose` opens the Delphi PR | -| `repos//` | A normal clone of a code repo, with its own remote | That repo's git, branches, and PRs, per its conventions | -| `worktrees/` | Empty; for extra checkouts of a repo (`git -C repos/ worktree add ../../worktrees/ `) | Same as the repo it came from | - -- Run a repo's git, `gh`, build, and test commands from inside that repo (`cd repos/`), never from the workspace root: at the root, `git` is the workspace repo. -- Code changes never go in the workspace git (`repos/` and `worktrees/` are git-ignored there). Context changes (instructions, skills, blocks) never go in a code repo. -- `.delphi/` is Delphi bookkeeping. Don't edit it by hand. diff --git a/context/scope.yml b/context/scope.yml deleted file mode 100644 index f04ee4c..0000000 --- a/context/scope.yml +++ /dev/null @@ -1 +0,0 @@ -name: NER diff --git a/context/software/application-software/argos/blocks/open-pr.md b/context/software/application-software/argos/blocks/open-pr.md deleted file mode 100644 index 42f8a98..0000000 --- a/context/software/application-software/argos/blocks/open-pr.md +++ /dev/null @@ -1 +0,0 @@ -Stop if the working tree is dirty. Check the commits since `develop` follow the commit format, lint the frontend if it changed, and check that `origin/develop` merges without conflicts. Then push and run `gh pr create --draft --base develop --head --title "#{ticket} title" --body-file /tmp/-pr-body.md --assignee @me`, and report the URL. diff --git a/context/software/application-software/argos/blocks/update-pr.md b/context/software/application-software/argos/blocks/update-pr.md deleted file mode 100644 index 22ae612..0000000 --- a/context/software/application-software/argos/blocks/update-pr.md +++ /dev/null @@ -1 +0,0 @@ -Rewrite the current branch's PR body (`gh pr view`) to match `git diff develop...HEAD`. Keep human-written text that's still accurate, `user-attachments` screenshots, and `Closes`/`Fixes` refs. Apply it with `gh pr edit --body-file`. diff --git a/context/software/application-software/argos/harness/skills/open-pr.skill b/context/software/application-software/argos/harness/skills/open-pr.skill deleted file mode 100644 index d293975..0000000 --- a/context/software/application-software/argos/harness/skills/open-pr.skill +++ /dev/null @@ -1,5 +0,0 @@ -name: open-pr -description: Run pre-PR checks, push the branch, and open a draft pull request -body: - - software/application-software/argos/blocks/open-pr.md - - software/application-software/argos/blocks/pr-body.md diff --git a/context/software/application-software/argos/harness/skills/update-pr.skill b/context/software/application-software/argos/harness/skills/update-pr.skill deleted file mode 100644 index 94c7dbd..0000000 --- a/context/software/application-software/argos/harness/skills/update-pr.skill +++ /dev/null @@ -1,5 +0,0 @@ -name: update-pr -description: Update the current branch's PR description to reflect the latest changes -body: - - software/application-software/argos/blocks/update-pr.md - - software/application-software/argos/blocks/pr-body.md diff --git a/context/software/application-software/argos/layouts/argos-dev/manifest.yml b/context/software/application-software/argos/layouts/argos-dev/manifest.yml deleted file mode 100644 index e4857c5..0000000 --- a/context/software/application-software/argos/layouts/argos-dev/manifest.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: argos-dev -harness: claude-code -instructions: - - harness/instructions/workspace.md - - software/harness/instructions/base.md - - software/application-software/argos/harness/instructions/argos.md -docs: - - software/application-software/argos/docs/CONTEXT.md -skills: - - software/application-software/argos/harness/skills/grill-with-docs - - software/application-software/argos/harness/skills/to-spec - - software/application-software/argos/harness/skills/to-tickets - - software/application-software/argos/harness/skills/implement - - software/application-software/argos/harness/skills/code-review - - software/application-software/argos/harness/skills/commit - - software/application-software/argos/harness/skills/open-pr.skill - - software/application-software/argos/harness/skills/update-pr.skill - - software/application-software/argos/harness/skills/run-local - - software/application-software/argos/harness/skills/new-worktree -repos: - argos: https://github.com/Northeastern-Electric-Racing/Argos.git diff --git a/context/software/application-software/argos/scope.yml b/context/software/application-software/argos/scope.yml deleted file mode 100644 index df18e9a..0000000 --- a/context/software/application-software/argos/scope.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: Argos -recommend: - - software/application-software/argos/harness/skills/grill-with-docs - - software/application-software/argos/harness/skills/to-spec - - software/application-software/argos/harness/skills/to-tickets - - software/application-software/argos/harness/skills/implement - - software/application-software/argos/harness/skills/commit - - software/application-software/argos/harness/skills/open-pr.skill - - software/application-software/argos/harness/skills/run-local diff --git a/context/software/application-software/scope.yml b/context/software/application-software/scope.yml deleted file mode 100644 index ad43ee6..0000000 --- a/context/software/application-software/scope.yml +++ /dev/null @@ -1 +0,0 @@ -name: Application Software diff --git a/context/software/harness/instructions/base.md b/context/software/harness/instructions/base.md deleted file mode 100644 index 98d668c..0000000 --- a/context/software/harness/instructions/base.md +++ /dev/null @@ -1,12 +0,0 @@ -# NER Software conventions - -## Branch & Commit Conventions - -- Branch from `develop` (not `main`) unless told otherwise. Branch name format: `{issue-number}-{kebab-case-title}` (e.g. `533-csv-upload-download-rules`). -- Commit message format: `#{ticket-number} - {concise description}` (e.g. `#533 - add CSV upload endpoint`). - -## Safety Rules - -- Never modify `.env` or secret files without explicit confirmation. -- Never delete files without explicit confirmation. -- Explain reasoning before making architectural changes. diff --git a/context/software/scope.yml b/context/software/scope.yml deleted file mode 100644 index d98d663..0000000 --- a/context/software/scope.yml +++ /dev/null @@ -1 +0,0 @@ -name: Software diff --git a/delphi.conf b/delphi.conf deleted file mode 100644 index 8131a5c..0000000 --- a/delphi.conf +++ /dev/null @@ -1,3 +0,0 @@ -# Delphi repo config (key=value). Relative paths resolve against the repo root. -workspace_root=../Delphi-workspaces -stale_days=14 diff --git a/dev/sandbox.sh b/dev/sandbox.sh deleted file mode 100755 index b970a7f..0000000 --- a/dev/sandbox.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# dev/sandbox.sh — throwaway playground for trying Delphi end to end without GitHub. -# -# eval "$(dev/sandbox.sh)" # prints exports; then use: d … -# -# Creates, in a fresh temp dir ($SB): -# origin.git bare "remote" for Delphi (stands in for GitHub) -# bin/gh stub gh: logs to gh.log, remembers PRs in gh.prs (so --yes works end to end) -# Delphi/ clone with this checkout's bin/ lib/ delphi.conf moves.tsv + sample content -# argos.git bare code repo referenced by the sample layout -# Delphi-workspaces/ where workspaces land (the default ../Delphi-workspaces) -# `d` runs the sandbox CLI under /bin/bash (macOS bash 3.2) to catch bash-4-isms. -set -euo pipefail - -here=$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -SB=${1:-$(mktemp -d "${TMPDIR:-/tmp}/delphi-sb.XXXXXX")} -mkdir -p "$SB" -SB=$(cd -P "$SB" && pwd) - -git init -q --bare -b main "$SB/origin.git" -git clone -q "$SB/origin.git" "$SB/Delphi" 2>/dev/null -cp -R "$here/bin" "$here/lib" "$SB/Delphi/" -cp "$here/delphi.conf" "$here/moves.tsv" "$SB/Delphi/" - -C="$SB/Delphi/context" -A="$C/software/application-software/argos" -mkdir -p "$C/software/harness/instructions" "$C/software/harness/mcp" \ - "$A/blocks" "$A/harness/instructions" "$A/harness/skills/run-tests" "$A/layouts/argos-dev" - -printf 'name: NER\n' > "$C/scope.yml" -printf 'name: Software\nrecommend:\n - software/harness/instructions/base.md\n' > "$C/software/scope.yml" -printf 'name: Application Software\n' > "$C/software/application-software/scope.yml" -cat > "$A/scope.yml" <<'EOF' -name: Argos -recommend: - - software/application-software/argos/blocks/overview.md - - software/application-software/argos/harness/skills/run-tests -EOF -printf '# Software conventions\n\n- Use conventional commits.\n- Open PRs against develop.\n' \ - > "$C/software/harness/instructions/base.md" -printf '"github": {\n "command": "gh-mcp",\n "args": []\n}\n' > "$C/software/harness/mcp/github.json" -printf '# Argos\n\nArgos is the telemetry dashboard.\nIt has an Angular client and a Rust server.\n' \ - > "$A/harness/instructions/argos.md" -printf '# Argos overview\n\nLine one.\nLine two.\nLine three.\n' > "$A/blocks/overview.md" -printf '# Testing\n\nRun `npm test` in angular-client.\nRun `cargo test` in the server.\n' > "$A/blocks/testing.md" -printf -- '---\nname: run-tests\ndescription: Run the Argos test suites.\n---\n\nRun both suites and summarize failures.\n' \ - > "$A/harness/skills/run-tests/SKILL.md" -mkdir -p "$A/harness/skills/run-tests/scripts" -printf '#!/bin/sh\necho running tests\n' > "$A/harness/skills/run-tests/scripts/run.sh" -chmod +x "$A/harness/skills/run-tests/scripts/run.sh" -cat > "$A/harness/skills/triage.skill" <<'EOF' -name: triage -description: Triage a failing Argos test. -body: - - software/application-software/argos/blocks/testing.md -EOF - -git init -q -b main "$SB/argos-src" -printf '# Argos code\n' > "$SB/argos-src/README.md" -git -C "$SB/argos-src" add -A && git -C "$SB/argos-src" commit -qm init -git clone -q --bare "$SB/argos-src" "$SB/argos.git" - -cat > "$A/layouts/argos-dev/manifest.yml" </dev/null -git -C "$SB/Delphi" branch -q -u origin/main 2>/dev/null || true - -mkdir -p "$SB/bin"; : > "$SB/gh.prs" -{ printf '#!/bin/sh\nSB=%q\n' "$SB"; cat <<'EOF'; } > "$SB/bin/gh" -# stub gh for the Delphi sandbox: never contacts GitHub -echo "gh $*" >> "$SB/gh.log" -n=$(awk -v b="$4" '$1 == b { print $2 }' "$SB/gh.prs") # args: pr list|create --head … -case "$1 $2" in - "api user") echo sandbox-user ;; - "pr list") [ -z "$n" ] || { [ "$8" = author ] && echo sandbox-user || echo "$n"; } ;; - "pr create") n=$(($(wc -l < "$SB/gh.prs") + 1)); echo "$4 $n" >> "$SB/gh.prs"; echo "https://github.invalid/pr/$n" ;; -esac -exit 0 -EOF -chmod +x "$SB/bin/gh" - -cat </`, with its files at their +normal harness paths (`CLAUDE.md`, `.claude/…`, `docs/…`). For each workspace, branch +**`ws/`** has **that folder as its repo root**. Two directions keep them in sync, both subtree +merges (`git merge -Xsubtree=`), so the histories stay joined and edits on either side meet +in normal three-way merges. Nothing is shared or generated between workspaces. + +``` + ┌───────────────────────────────┐ + │ main │ + │ software/…/workspaces// │ + └───────────────────────────────┘ + │ ▲ + │ refresh │ merge PR + │ (CI) │ (after check.sh) + ▼ │ + ┌──────────────────┐ ┌──────────────────┐ + │ ws/ │ │ propose/ │ + │ root = workspace │─►│ PR to main │ + └──────────────────┘ └──────────────────┘ + │ ▲ propose (CI) + │ branch │ merge PR + ▼ │ + ┌──────────────────┐ + │ your branch │ + │ edit · commit │ + └──────────────────┘ + + refresh: main → ws/ (folder becomes root) + propose: ws/ → main PR (root goes back under folder) + CI runs both on every push to main or ws/** +``` + +## 2. Repository (main) + +``` +software//…/workspaces// workspace.yml CLAUDE.md .claude/… docs/… + .delphi/setup.sh .github/workflows/delphi.yml .gitattributes +ci/sync.sh ci/check.sh CI scripts +tools/new-workspace.sh new workspace as a PR +templates/workspace/ what new-workspace copies +.github/workflows/delphi.yml CI +tests/e2e.sh sandbox test of all of the above +``` + +Org folders under `software/` are plain directories. `workspace.yml` is tiny YAML: `harness: +` and an optional `repos:` map of `: `. The name is the folder name: +lowercase letters, digits, `-`; unique repo-wide. Workspaces never nest; no symlinks under +`software/`. Each workspace's `.delphi/setup.sh` and `.github/workflows/delphi.yml` equal the +template's, and the template's workflow equals main's. `ci/check.sh` enforces all of this and lists +every problem. `.gitattributes` (`eol=lf`) keeps `setup.sh` runnable in Git Bash clones. +`.github/CODEOWNERS` assigns reviewers per org folder. + +## 3. Sync (`ci/sync.sh [refresh|propose] []`; no direction = both) + +For every workspace on `origin/main` (or just ``): + +1. **Refresh** (main → `ws/`). If `ws/` is missing, create it as one commit + (`git commit-tree

: -p
`, "delphi: create ws/ from "): root = + folder, parent = main, so no unrelated histories. Otherwise merge `origin/main` into `ws/` + with `--no-ff -Xsubtree=` and push if the tree changed. (`--no-ff` matters: once ws + commits are in main, a fast-forward would put main's whole tree on the ws branch.) +2. **Propose** (`ws/` → PR to main). Build `propose/` = `origin/main` + + `git merge --no-ff -Xsubtree= ws/`. Stop if its tree equals main's or the current + `propose/`'s (nothing new). Otherwise run `ci/check.sh` on it, force-push it, and create or + update the PR `ws/ → main` with `gh` (body: changed files, ws commit subjects and authors). + +A conflict or failed check is reported (conflicts with their files); that workspace is skipped, the +others continue, and the script exits 1. A `ws/*` branch without a folder on main gets a notice and +is never deleted. Merges happen in a temporary worktree, so the caller's checkout is untouched. + +## 4. Working on a workspace + +1. Clone or check out `ws/` (it is the workspace root); run `.delphi/setup.sh` once. +2. Branch, commit, push, open a PR into `ws/`. Update with `git merge origin/ws/`. +3. After it merges, CI proposes it; merge that PR with a merge commit or squash, never rebase + (rebasing replays folder-rooted commits onto main). CI then refreshes `ws/`. +4. On a sync conflict: on a branch cut from `ws/`, `git merge -Xsubtree= + origin/main`, resolve, and PR it into `ws/`. + +Nobody pushes to `ws/*` or `main` directly (branch protection; CI's token is the exception). + +## 5. `.delphi/setup.sh` (in every workspace) + +Reads `repos:` from `workspace.yml`, clones each into `repos/` unless present, and adds +`/repos/` to the clone's `.git/info/exclude` once. Nothing else. Must run on macOS `/bin/bash` 3.2 +and Git Bash: no bash-4 features, POSIX awk only. + +## 6. New workspaces (`tools/new-workspace.sh `) + +Validates the name (format, not on main, no leftover `ws/`), copies `templates/workspace/` +into `software//workspaces//` (filling `{{name}}`/`{{folder}}` in `CLAUDE.md`) in a +temporary worktree, runs `ci/check.sh`, pushes branch `new-workspace/`, and opens a PR. Sync +creates `ws/` once it merges. + +## 7. CI (`.github/workflows/delphi.yml`) + +- `check`: PRs to main, read-only token, runs `ci/check.sh`. +- `sync`: pushes to main or `ws/**`; checks out main (scripts never come from a ws branch) with full + history, sets the bot identity, runs `ci/sync.sh` (contents + pull-requests write; one run at a + time). + +GitHub runs a push's workflow from the pushed commit, so the same file sits at the root of every +`ws/` (via the workspace folder). Pushes and PRs made with `GITHUB_TOKEN` trigger no +workflows: no loops, and CI-opened PRs don't run `check`, which is why sync runs `ci/check.sh` +itself (so `check` can't be a required status check). + +## 8. Testing + +`tests/e2e.sh` builds a sandbox (temp dir, bare origin, stub `gh` logging to `gh.log`, two +workspaces in different org folders) and drives the scripts as CI and people would. Never test +against GitHub. diff --git a/docs/goals.md b/docs/goals.md new file mode 100644 index 0000000..bfce84a --- /dev/null +++ b/docs/goals.md @@ -0,0 +1,30 @@ +# Delphi — Goals + +What Delphi must achieve, not how. Any change must keep these. Details: `docs/design.md`. + +## Purpose + +Store NER's AI context (instructions, skills, docs, settings) once, organized by the org chart. Let +anyone, person or agent, work on a workspace with plain git and send improvements back. + +## Goals + +- **G1. Workspaces live in Delphi.** Each workspace is a folder on `main` with every file at its + normal harness location. +- **G2. The workspace is the repo root.** Checking out `ws/` gives exactly that workspace at + the root. `.delphi/setup.sh` clones the code repos it lists. +- **G3. Both directions, automatically.** Refresh: changes on `main` reach `ws/`. Propose: + merged workspace changes reach `main` as a PR. +- **G4. Conflicts are reported, never guessed.** A conflicting workspace is skipped with its files + listed and resolved by a person in a normal PR; other workspaces keep syncing. +- **G5. Keep Delphi valid.** `ci/check.sh` catches bad manifests and structure before merge. New + workspaces arrive through PRs too. +- **G6. Simple.** No CLI to install: git, `gh`, and a few short shell scripts. + +## Invariants + +- **I1.** Changes reach `main` and `ws/*` only through PRs (CI's refreshes excepted). +- **I2.** `ws/` history is joined to `main`; no unrelated histories, no force-pushes to `ws/*`. +- **I3.** Code repos in `repos/` never appear as workspace changes. +- **I4.** `setup.sh` works on macOS bash 3.2 and Git Bash. +- **I5.** Everything is testable end to end without GitHub (`tests/e2e.sh`). diff --git a/lib/block.sh b/lib/block.sh deleted file mode 100644 index 7341e0b..0000000 --- a/lib/block.sh +++ /dev/null @@ -1,39 +0,0 @@ -# block.sh — `delphi block mv `: move a block, log it in moves.tsv, rewrite references. -. "$DELPHI_ROOT/lib/parse.sh" -. "$DELPHI_ROOT/lib/provenance.sh" -. "$DELPHI_ROOT/lib/pr.sh" -. "$DELPHI_ROOT/lib/check.sh" - -block_main() { - local verb=${1:-} - [ $# -gt 0 ] && shift - parse_args "--model --effort --yes" "$@"; eval "set -- $ARGS" - [ "$verb" = mv ] && [ $# -eq 2 ] || die "usage: delphi block mv " - block_mv "${1%/}" "${2%/}" -} - -block_mv() { - local old=$1 new=$2 p src dst f row - for p in "$old" "$new"; do - path_ok "$p" || die "unsafe path: '$p'" - case "/$p" in */blocks/?*|*/docs/?*|*/harness/?*) ;; *) die "'$p' is not under a scope's blocks/, docs/, or harness/" ;; esac - done - delphi_fetch - p=$(delphi_commit main) || exit 1 - provenance_resolve "$OPT_MODEL" "$OPT_EFFORT" claude-code - pr_begin "delphi/mv/${old##*/}-$(date +%Y%m%d)" "$p" - src=$(safe_path "$PR_WT/context" "$old") || exit 1 - dst=$(safe_path "$PR_WT/context" "$new") || exit 1 - [ -e "$src" ] || die "no such block on origin/main: $old" - [ -e "$dst" ] && die "already exists on origin/main: $new" - mkdir -p "$(dirname "$dst")" && git -C "$PR_WT" mv "context/$old" "context/$new" || die "git mv failed" - make_tmp; row="$REPLY/row" - printf '%s\t%s\t%s\n' "$old" "$new" "$(date +%Y-%m-%d)" | tee -a "$PR_WT/moves.tsv" > "$row" - while IFS= read -r f; do rewrite_moves "$row" "$f" || true; done < $new" || die "nothing to commit" - pr_finish "delphi: move $old -> $new" "Moves \`$old\` to \`$new\` and rewrites references. Workspaces follow on their next refresh." - printf '%s\n' "$PR_BRANCH" -} diff --git a/lib/check.sh b/lib/check.sh deleted file mode 100644 index 9ba5048..0000000 --- a/lib/check.sh +++ /dev/null @@ -1,148 +0,0 @@ -# check.sh — repo-wide validation. `check_tree ` prints every violation, returns 1 if any. -# Layouts are validated by compiling them; the rules here cover what compile doesn't enforce. -. "$DELPHI_ROOT/lib/parse.sh" -. "$DELPHI_ROOT/lib/compile.sh" - -check_main() { - [ $# -eq 0 ] || die "usage: delphi check" - if check_tree "$DELPHI_ROOT"; then info "check: ok"; else exit 1; fi -} - -_ce() { printf '%s\n' "$*" >> "$_CHECK_ERRS"; } - -# _ck_entry : placement rule for the kind (instructions|skills|mcp| -# settings|body); `body` (skill specs) and `any` (recommendations) must also exist. -_ck_entry() { - local f=$1 key=$2 p=$3 want='*' - path_ok "${p%/\*}" || { _ce "$f: $key: unsafe path '$p'"; return 0; } - case $4 in - instructions) want='*/harness/instructions/*' ;; skills) want='*/harness/skills/*' ;; - mcp) want='*/harness/mcp/*.json' ;; settings) want='*/harness/settings/*' ;; body) want='*/blocks/*' ;; - esac - case "/$p" in $want) ;; *) _ce "$f: $key: '$p' is not under ${want#\*/}" ;; esac - case $4 in body|any) [ -e "$_CK_ROOT/context/$p" ] || _ce "$f: $key: missing '$p'" ;; esac - return 0 -} - -check_tree() { - _CK_ROOT=$1 - local ctx="$1/context" f rel recs k p n names dup name hfile h i=0 - make_tmp; _CHECK_ERRS="$REPLY/errs"; : > "$_CHECK_ERRS" - [ -d "$ctx" ] || { _ce "missing context/ directory"; cat "$_CHECK_ERRS" >&2; return 1; } - - # scopes: every non-reserved directory needs scope.yml - while IFS= read -r d; do - [ -f "$d/scope.yml" ] || _ce "${d#$1/}: scope directory has no scope.yml" - for f in "$d"/*; do - [ -f "$f" ] && [ "${f##*/}" != scope.yml ] && _ce "${f#$1/}: stray file (scopes hold only scope.yml, blocks/, docs/, harness/, layouts/, child scopes)" - done - done < /dev/null 2>> "$_CHECK_ERRS" || true - done </dev/null) || continue - while IFS= read -r p; do [ -n "$p" ] && _ck_entry "${f#$1/}" recommend "$p" any; done </dev/null) || continue - [ -n "$(yaml_get "$recs" name)" ] || _ce "$rel: missing name" - [ -n "$(yaml_get "$recs" description)" ] || _ce "$rel: missing description" - for k in $(yaml_keys "$recs"); do - case $k in name|description|body|references) ;; *) _ce "$rel: unknown key '$k'" ;; esac - done - while IFS= read -r p; do [ -n "$p" ] && _ck_entry "$rel" body "$p" body; done </dev/null) || continue - name=$(yaml_get "$recs" name) - dup=${f%/manifest.yml}; dup=${dup##*/} - [ -n "$name" ] || _ce "$rel: missing name" - [ "$name" = "$dup" ] || _ce "$rel: name '$name' must equal its directory '$dup'" - in_list "$name" "$names" && _ce "$rel: layout name '$name' is not unique" - names="$names -$name" - h=$(yaml_get "$recs" harness) - if [ -z "$h" ]; then _ce "$rel: missing harness" - elif [ ! -f "$DELPHI_ROOT/lib/harness/$h.sh" ]; then _ce "$rel: unknown harness '$h'" - else # compile it: catches missing paths, empty globs, duplicate outputs, bad skill specs - i=$((i + 1)); mkdir "$_CHECK_ERRS.$i" - ( compile "$1" "$(dirname "${rel#context/}")" "$_CHECK_ERRS.$i" ) 2>&1 > /dev/null | sed "s#^delphi: #$rel: #" >> "$_CHECK_ERRS" || true - fi - for k in $(yaml_keys "$recs"); do - case $k in name|harness|instructions|blocks|docs|skills|mcp|settings|repos) ;; *) _ce "$rel: unknown key '$k'" ;; esac - done - n=$(yaml_list "$recs" settings | awk 'NF' | awk 'END { print NR }') - [ "$n" -le 1 ] || _ce "$rel: at most one settings file" - for k in instructions skills mcp settings; do - while IFS= read -r p; do [ -n "$p" ] && _ck_entry "$rel" "$k" "$p" "$k"; done <newdate\n", NR }' "$1/moves.tsv" >> "$_CHECK_ERRS" - - # MCP fragments are valid JSON (only if jq is installed) - if command -v jq > /dev/null 2>&1; then - while IFS= read -r f; do - [ -z "$f" ] && continue - { printf '{'; cat "$f"; printf '}'; } | jq empty > /dev/null 2>&1 || _ce "${f#$1/}: not a valid mcpServers member" - done <&2; return 1; fi - return 0 -} diff --git a/lib/compile.sh b/lib/compile.sh deleted file mode 100644 index 7fabfef..0000000 --- a/lib/compile.sh +++ /dev/null @@ -1,182 +0,0 @@ -# compile.sh — layout -> workspace files + .delphi/lock.tsv (segment map). Deterministic. -# -# compile -# a checked-out Delphi tree (usually a temp worktree at a specific commit) -# layout directory relative to context/ (…/layouts/) -# empty directory to write into -# Requires parse.sh. Sets R_HARNESS to the layout's harness name. - -_r_count() { if [ -f "$1" ]; then wc -l < "$1" | tr -d ' '; else echo 0; fi; } - -# _r_seg -_r_seg() { printf '%s\t%s\t%s\t%s\t%s\n' "$1" "$2" "$3" "$4" "$5" >> "$R_LOCK"; } - -# _r_file : append a source file (relative to context/) to an output file. -_r_file() { - local out="$R_OUT/$1" src start end - src=$(safe_path "$R_SRC/context" "$2") || exit 1 - [ -f "$src" ] || die "compile: missing file context/$2" - [ -s "$src" ] || die "compile: empty file context/$2" - mkdir -p "$(dirname "$out")" - start=$(( $(_r_count "$out") + 1 )) - cat "$src" >> "$out" - [ -n "$(tail -c 1 "$src")" ] && printf '\n' >> "$out" - end=$(_r_count "$out") - _r_seg "$1" "$start" "$end" "$2" "$(git hash-object "$src")" -} - -# _r_copy : 1:1 copy (keeps the executable bit); the output path must not already exist. -_r_copy() { - [ -e "$R_OUT/$1" ] && die "compile: two sources map to the same output '$1'" - _r_file "$1" "$2" - if [ -x "$R_SRC/context/$2" ]; then chmod +x "$R_OUT/$1" || die "compile: cannot chmod $1"; fi -} - -# _r_text