feat(issue-21): worktree_prepare.sh / worktree_remove.sh (opt-in submodules; judge before any change, single verified --force for submodule worktrees); create starts its reconcile and watchdog from the control root
- worktree_prepare.sh (create skill): idempotent `git worktree add` next to the control root (a path inside it is refused), .venv linked to the control root's, /.venv and .pytest_cache/ in the shared info/exclude, submodules only with --submodules - a failed init removes its own traces (modules/, new submodule.* keys in the shared config) and exits 1, since git refuses a plain `worktree remove` once modules/ exists - and an optional creator registered in the control root's .mam with MAM_NO_SUBSCRIBER_DAEMON=1. Prints BASE= for `run_loop.sh --worktree ... --base ... --review-only`. - worktree_remove.sh (stop skill): every check first, nothing changed on a refusal (exit 3): linked and not locked, running sessions only with --stop, no open job of those sessions except the delegate jobs stop cancels, clean tree including submodules, and inside submodules no modification and no commit on no remote (HEAD included - submodules sit on a detached HEAD). Then stop, re-check jobs, and remove: plain, or a single --force when the worktree holds submodule state. - lib_py/worktree.py: the parsing behind both (stdlib only). - A8: create's immediate reconcile and watchdog use the control root's code and WORKSPACE_ROOT, not the worktree --workspace's copy. Tests W1-W6d (local submodules allowed through GIT_CONFIG_* env only). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
1 parent
2679ddfffd
commit
657b4b70ed
5 files changed
+696
-2
No files matched your search
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
# worktree_prepare.sh — prepare an issue worktree next to the control root
|
||||
# (Issue #21 item 4). Run it from the control root's skills tree: the control
|
||||
# root is lib.sh's WORKSPACE_ROOT, the main checkout that owns .mam.
|
||||
#
|
||||
# Usage:
|
||||
# worktree_prepare.sh --path <WT> --branch <name> [--base <ref>] [--submodules]
|
||||
# [--creator-agent <agent> [--session <name>] [--role <role>]
|
||||
# [--herdr-session <s>] [--herdr-workspace <w>] [--onboard]] [--dry-run]
|
||||
#
|
||||
# Steps: git worktree (idempotent) -> .venv link to the control root's ->
|
||||
# /.venv and .pytest_cache/ in the shared info/exclude -> submodules only with
|
||||
# --submodules (a failed init is rolled back: it would otherwise leave a
|
||||
# modules/ trace that makes a plain `git worktree remove` refuse) -> optional
|
||||
# creator session registered in the control root's .mam -> summary with BASE=.
|
||||
#
|
||||
# Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule.
|
||||
set -euo pipefail
|
||||
|
||||
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$(cd "$_script_dir/../.." && pwd)/lib.sh"
|
||||
|
||||
usage() {
|
||||
sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
WT=""; BRANCH=""; BASE_REF=""; SUBMODULES=0; DRY_RUN=0
|
||||
CREATOR_AGENT=""; SESSION=""; ROLE="creator"; HS=""; HW=""; ONBOARD=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--path) WT="${2:-}"; shift 2 ;;
|
||||
--branch) BRANCH="${2:-}"; shift 2 ;;
|
||||
--base) BASE_REF="${2:-}"; shift 2 ;;
|
||||
--submodules) SUBMODULES=1; shift ;;
|
||||
--creator-agent) CREATOR_AGENT="${2:-}"; shift 2 ;;
|
||||
--session) SESSION="${2:-}"; shift 2 ;;
|
||||
--role) ROLE="${2:-}"; shift 2 ;;
|
||||
--herdr-session) HS="${2:-}"; shift 2 ;;
|
||||
--herdr-workspace) HW="${2:-}"; shift 2 ;;
|
||||
--onboard) ONBOARD=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$WT" ] && [ -n "$BRANCH" ] || { echo "ERROR: --path and --branch are required" >&2; usage >&2; exit 2; }
|
||||
|
||||
MAIN="$(mam_abs_workspace "$WORKSPACE_ROOT")"
|
||||
top="$(git -C "$MAIN" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$top" ] || [ "$(mam_abs_workspace "$top")" != "$MAIN" ]; then
|
||||
echo "ERROR: the control root '$MAIN' (WORKSPACE_ROOT) is not the top of a git checkout" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "$WT" in /*) ;; *) WT="$PWD/$WT" ;; esac
|
||||
WT="${WT%/}"
|
||||
if [ -d "$WT" ]; then
|
||||
WT="$(mam_abs_workspace "$WT")"
|
||||
elif [ -d "$(dirname "$WT")" ]; then
|
||||
WT="$(mam_abs_workspace "$(dirname "$WT")")/$(basename "$WT")"
|
||||
fi
|
||||
case "$WT/" in
|
||||
"$MAIN"/*)
|
||||
echo "ERROR: '$WT' is inside the control root '$MAIN'; a worktree there shows up as untracked files in the main tree. Put it next to the control root instead." >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
|
||||
PY="$(_delegate_py_bin)"
|
||||
wt_py() { PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" "$PY" -m lib_py.worktree "$@"; }
|
||||
|
||||
if [ -z "$SESSION" ] && [ -n "$CREATOR_AGENT" ]; then
|
||||
slug="$(printf '%s' "$BRANCH" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed -E 's/-+/-/g; s/^-//; s/-$//')"
|
||||
SESSION="creator-${CREATOR_AGENT}-${slug}"
|
||||
SESSION="${SESSION:0:60}"
|
||||
SESSION="${SESSION%-}"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "[dry-run] control root: $MAIN"
|
||||
echo "[dry-run] worktree: $WT (branch $BRANCH${BASE_REF:+, base $BASE_REF})"
|
||||
echo "[dry-run] .venv link and shared info/exclude entries; submodules: $([ "$SUBMODULES" = 1 ] && echo init || echo skipped)"
|
||||
[ -n "$CREATOR_AGENT" ] && echo "[dry-run] creator: $CREATOR_AGENT as $SESSION (role $ROLE)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. The worktree (idempotent).
|
||||
if [ "$(wt_py is-linked "$MAIN" "$WT")" = "true" ]; then
|
||||
cur="$(git -C "$WT" branch --show-current)"
|
||||
if [ "$cur" != "$BRANCH" ]; then
|
||||
echo "ERROR: '$WT' is already a worktree, on branch '${cur:-(detached)}', not '$BRANCH'" >&2
|
||||
exit 2
|
||||
fi
|
||||
echo "worktree already prepared: $WT ($BRANCH)"
|
||||
else
|
||||
if [ -e "$WT" ] && [ -n "$(ls -A "$WT" 2>/dev/null)" ]; then
|
||||
echo "ERROR: '$WT' exists and is not a worktree of '$MAIN'" >&2
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "$(dirname "$WT")"
|
||||
if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$BRANCH"; then
|
||||
git -C "$MAIN" worktree add "$WT" "$BRANCH" || exit 1
|
||||
else
|
||||
git -C "$MAIN" worktree add -b "$BRANCH" "$WT" "${BASE_REF:-HEAD}" || exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. .venv: a link to the control root's, never a copy.
|
||||
if [ -e "$MAIN/.venv" ]; then
|
||||
if [ ! -e "$WT/.venv" ] && [ ! -L "$WT/.venv" ]; then
|
||||
ln -s "$MAIN/.venv" "$WT/.venv"
|
||||
elif [ "$(readlink "$WT/.venv" 2>/dev/null || true)" != "$MAIN/.venv" ]; then
|
||||
echo "WARNING: '$WT/.venv' exists and is not a link to '$MAIN/.venv'; left as it is" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Ignore rules. A linked worktree's info/exclude is the shared
|
||||
# $GIT_COMMON_DIR/info/exclude, so these lines apply to the main checkout too
|
||||
# (they name artefacts it ignores as well).
|
||||
excl="$(cd "$WT" && git rev-parse --path-format=absolute --git-path info/exclude)"
|
||||
mkdir -p "$(dirname "$excl")"
|
||||
touch "$excl"
|
||||
if [ -s "$excl" ] && [ -n "$(tail -c1 "$excl")" ]; then echo >> "$excl"; fi
|
||||
for pat in "/.venv" ".pytest_cache/"; do
|
||||
grep -Fxq -- "$pat" "$excl" || printf '%s\n' "$pat" >> "$excl"
|
||||
done
|
||||
|
||||
# 4. Submodules: opt-in only.
|
||||
SUB_STATE="skipped"
|
||||
if [ -f "$WT/.gitmodules" ]; then
|
||||
if [ "$SUBMODULES" = "1" ]; then
|
||||
gd="$(git -C "$WT" rev-parse --absolute-git-dir)"
|
||||
had_modules=0
|
||||
[ -e "$gd/modules" ] && had_modules=1
|
||||
new_keys=()
|
||||
while read -r key _; do
|
||||
[ -n "$key" ] || continue
|
||||
name="${key#submodule.}"; name="${name%.path}"
|
||||
for k in url active; do
|
||||
git -C "$MAIN" config --get "submodule.$name.$k" >/dev/null 2>&1 || new_keys+=("submodule.$name.$k")
|
||||
done
|
||||
done < <(git config --file "$WT/.gitmodules" --get-regexp '^submodule\..*\.path$' 2>/dev/null || true)
|
||||
if ! git -C "$WT" submodule update --init --recursive; then
|
||||
[ "$had_modules" = "1" ] || rm -rf "$gd/modules"
|
||||
for k in ${new_keys[@]+"${new_keys[@]}"}; do
|
||||
git -C "$MAIN" config --unset-all "$k" 2>/dev/null || true
|
||||
done
|
||||
echo "ERROR: submodule initialisation failed; its traces (modules/, new submodule.* config keys) were removed so the worktree can still be removed without --force. Re-run without --submodules to finish preparing." >&2
|
||||
exit 1
|
||||
fi
|
||||
SUB_STATE="initialised"
|
||||
else
|
||||
echo "NOTE: submodules were not initialised (pass --submodules to initialise them)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5. Optional creator, registered in the control root's .mam.
|
||||
if [ -n "$CREATOR_AGENT" ]; then
|
||||
args=(--workspace "$WT" --agent "$CREATOR_AGENT" --role "$ROLE" --session "$SESSION")
|
||||
if [ "$ONBOARD" = "1" ]; then args+=(--onboard); else args+=(--no-onboard); fi
|
||||
if [ -n "$HS" ]; then args+=(--herdr-session "$HS"); fi
|
||||
if [ -n "$HW" ]; then args+=(--herdr-workspace "$HW"); fi
|
||||
env_add=(WORKSPACE_ROOT="$MAIN"
|
||||
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$MAIN/.mam/agent-sessions.yaml}"
|
||||
MAM_JOBS_DIR="${MAM_JOBS_DIR:-$MAIN/.mam/jobs}"
|
||||
DELEGATE_JOB_LOGS_DIR="${DELEGATE_JOB_LOGS_DIR:-$MAIN/.mam/delegate_job_logs}"
|
||||
MAM_NO_SUBSCRIBER_DAEMON=1)
|
||||
if [ -z "${MAM_ENV_FILE:-}" ] && [ -f "$MAIN/.mam.env" ]; then env_add+=(MAM_ENV_FILE="$MAIN/.mam.env"); fi
|
||||
rc=0
|
||||
env "${env_add[@]}" bash "$SKILL_DIR/multi-agent-mux-create/scripts/create_session.sh" "${args[@]}" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "ERROR: create_session.sh failed (rc=$rc); the worktree is kept - remove it with worktree_remove.sh --path '$WT'" >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
fi
|
||||
|
||||
BASE_SHA="$(git -C "$WT" rev-parse HEAD)"
|
||||
echo "WORKTREE=$WT"
|
||||
echo "BRANCH=$BRANCH"
|
||||
echo "BASE=$BASE_SHA"
|
||||
echo "SUBMODULES=$SUB_STATE"
|
||||
[ -n "$CREATOR_AGENT" ] && echo "SESSION=$SESSION"
|
||||
echo "next: bash $SKILL_DIR/multi-agent-mux-loop/scripts/run_loop.sh --worktree '$WT' --base $BASE_SHA --review-only --creator <creator> --reviewer <reviewers> --task '...'"
|
||||
echo "then: bash $SKILL_DIR/multi-agent-mux-stop/scripts/worktree_remove.sh --path '$WT' --stop"
|
||||
Reference in new issue
Block a user