feat(worktree): onboard creator by default in worktree_prepare.sh (Issue #68)
This commit is contained in:
1 parent
3fd2416a30
commit
c0e9f5c8a5
5 files changed
+77
-10
No files matched your search
@@ -202,15 +202,15 @@ Run from the control root's skills tree (lib.sh's `WORKSPACE_ROOT` = the main ch
|
|||||||
```bash
|
```bash
|
||||||
bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \
|
bash .agents/skills/multi-agent-mux-create/scripts/worktree_prepare.sh \
|
||||||
--path ../multi-agent-mux-wt/issue-42 --branch fix/issue-42 [--base <ref>] [--submodules] \
|
--path ../multi-agent-mux-wt/issue-42 --branch fix/issue-42 [--base <ref>] [--submodules] \
|
||||||
[--creator-agent claude [--session <name>] [--role creator] [--herdr-session <s>] [--herdr-workspace <w>] [--onboard]] [--dry-run]
|
[--creator-agent claude [--session <name>] [--role creator] [--herdr-session <s>] [--herdr-workspace <w>] [--no-onboard]] [--dry-run]
|
||||||
```
|
```
|
||||||
|
|
||||||
- Creates the worktree next to the control root (a path **inside** it is refused), idempotently; creates the worktree's **own** `.venv` from the control root's `.venv/bin/python` and installs the delegate-job skill's `requirements.txt` (paho-mqtt, PyYAML - what `deploy/install.sh` installs) plus `requirements.txt` / `requirements-dev.txt` when present (needs network access to the package index; a failed install removes the half-made venv and exits 1 so a re-run starts over; never a link - a leftover link is refused with exit 1; no control-root interpreter is a warning and `VENV=skipped`); adds `/.venv` and `.pytest_cache/` to the shared `info/exclude` (a linked worktree's exclude file is `$GIT_COMMON_DIR/info/exclude`, so it applies to the main checkout too).
|
- Creates the worktree next to the control root (a path **inside** it is refused), idempotently; creates the worktree's **own** `.venv` from the control root's `.venv/bin/python` and installs the delegate-job skill's `requirements.txt` (paho-mqtt, PyYAML - what `deploy/install.sh` installs) plus `requirements.txt` / `requirements-dev.txt` when present (needs network access to the package index; a failed install removes the half-made venv and exits 1 so a re-run starts over; never a link - a leftover link is refused with exit 1; no control-root interpreter is a warning and `VENV=skipped`); adds `/.venv` and `.pytest_cache/` to the shared `info/exclude` (a linked worktree's exclude file is `$GIT_COMMON_DIR/info/exclude`, so it applies to the main checkout too).
|
||||||
- **Submodules are opt-in** (`--submodules`). Initialisation writes `submodule.<name>.url` to the shared `.git/config`, and any attempt - even a failed one - leaves a `modules/` directory that makes a plain `git worktree remove` refuse. A failed `--submodules` undoes only what that attempt created - new submodule checkouts are deinitialised before their git dirs are deleted, so the worktree stays a usable checkout; submodules initialised earlier and pre-existing `submodule.*` values are kept - and exits 1; re-run without `--submodules` to finish.
|
- **Submodules are opt-in** (`--submodules`). Initialisation writes `submodule.<name>.url` to the shared `.git/config`, and any attempt - even a failed one - leaves a `modules/` directory that makes a plain `git worktree remove` refuse. A failed `--submodules` undoes only what that attempt created - new submodule checkouts are deinitialised before their git dirs are deleted, so the worktree stays a usable checkout; submodules initialised earlier and pre-existing `submodule.*` values are kept - and exits 1; re-run without `--submodules` to finish.
|
||||||
- `--creator-agent` creates the creator through this skill with the control root's binding and `MAM_NO_SUBSCRIBER_DAEMON=1`; the row's `pane.cwd` is the worktree.
|
- `--creator-agent` creates the creator through this skill with the control root's binding and `MAM_NO_SUBSCRIBER_DAEMON=1` (the row's `pane.cwd` is the worktree) and onboards it (the onboarding job is sent; `--no-onboard` skips it, `--onboard` is the default and still accepted).
|
||||||
- Prints `BASE=<sha>`: record it **before** delegating the implementation, then review with `run_loop.sh --worktree <WT> --base "$BASE" --review-only`.
|
- Prints `BASE=<sha>`: record it **before** delegating the implementation, then review with `run_loop.sh --worktree <WT> --base "$BASE" --review-only`.
|
||||||
- A submodule path that is not initialised yet but already holds files refuses `--submodules` up front (exit 3, nothing changed): the clone would fail on it and a rollback could not tell those files from its own.
|
- A submodule path that is not initialised yet but already holds files refuses `--submodules` up front (exit 3, nothing changed): the clone would fail on it and a rollback could not tell those files from its own.
|
||||||
- Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule, 3 refused to protect existing content (nothing changed); 44/45 are create's (onboarding error / timeout with `--onboard`; session and worktree kept).
|
- Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule, 3 refused to protect existing content (nothing changed); 44/45 are create's (onboarding error / timeout; onboarding is the default, `--no-onboard` skips it; session and worktree kept).
|
||||||
- create itself: a control-plane binding that cannot be computed is refused (exit 2) for a `--workspace` other than the control root and warned about for the control root; the immediate reconcile and the watchdog run the control root's code.
|
- create itself: a control-plane binding that cannot be computed is refused (exit 2) for a `--workspace` other than the control root and warned about for the control root; the immediate reconcile and the watchdog run the control root's code.
|
||||||
|
|
||||||
## Pitfalls
|
## Pitfalls
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# worktree_prepare.sh --path <WT> --branch <name> [--base <ref>] [--submodules]
|
# worktree_prepare.sh --path <WT> --branch <name> [--base <ref>] [--submodules]
|
||||||
# [--creator-agent <agent> [--session <name>] [--role <role>]
|
# [--creator-agent <agent> [--session <name>] [--role <role>]
|
||||||
# [--herdr-session <s>] [--herdr-workspace <w>] [--onboard]] [--dry-run]
|
# [--herdr-session <s>] [--herdr-workspace <w>] [--no-onboard]] [--dry-run]
|
||||||
#
|
#
|
||||||
# Steps: git worktree (idempotent) -> the worktree's own .venv (control root
|
# Steps: git worktree (idempotent) -> the worktree's own .venv (control root
|
||||||
# interpreter + requirements*.txt) -> /.venv and .pytest_cache/ in the shared info/exclude -> submodules only with
|
# interpreter + requirements*.txt) -> /.venv and .pytest_cache/ in the shared info/exclude -> submodules only with
|
||||||
# --submodules (a failed init is rolled back: it would otherwise leave a
|
# --submodules (a failed init is rolled back: it would otherwise leave a
|
||||||
# modules/ trace that makes a plain `git worktree remove` refuse) -> optional
|
# modules/ trace that makes a plain `git worktree remove` refuse) -> optional
|
||||||
# creator session registered in the control root's .mam -> summary with BASE=.
|
# creator session (onboarded unless --no-onboard) registered in the control root's .mam -> summary with BASE=.
|
||||||
#
|
#
|
||||||
# Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule,
|
# Exit codes: 0 ok, 1 git / submodule / create failure, 2 usage or path rule,
|
||||||
# 3 refused to protect existing content (nothing changed); 44/45 are create's
|
# 3 refused to protect existing content (nothing changed); 44/45 are create's
|
||||||
# (onboarding error / timeout; session and worktree kept, Issue #50).
|
# (onboarding error / timeout - onboarding is the default, --no-onboard skips it; session and worktree kept, Issue #50).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -27,7 +27,7 @@ usage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WT=""; BRANCH=""; BASE_REF=""; SUBMODULES=0; DRY_RUN=0
|
WT=""; BRANCH=""; BASE_REF=""; SUBMODULES=0; DRY_RUN=0
|
||||||
CREATOR_AGENT=""; SESSION=""; ROLE="creator"; HS=""; HW=""; ONBOARD=0
|
CREATOR_AGENT=""; SESSION=""; ROLE="creator"; HS=""; HW=""; ONBOARD=1
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--path) WT="${2:-}"; shift 2 ;;
|
--path) WT="${2:-}"; shift 2 ;;
|
||||||
@@ -39,7 +39,8 @@ while [ $# -gt 0 ]; do
|
|||||||
--role) ROLE="${2:-}"; shift 2 ;;
|
--role) ROLE="${2:-}"; shift 2 ;;
|
||||||
--herdr-session) HS="${2:-}"; shift 2 ;;
|
--herdr-session) HS="${2:-}"; shift 2 ;;
|
||||||
--herdr-workspace) HW="${2:-}"; shift 2 ;;
|
--herdr-workspace) HW="${2:-}"; shift 2 ;;
|
||||||
--onboard) ONBOARD=1; shift ;;
|
--onboard) ONBOARD=1; shift ;; # the default; still accepted
|
||||||
|
--no-onboard) ONBOARD=0; shift ;;
|
||||||
--dry-run) DRY_RUN=1; shift ;;
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) echo "ERROR: unknown arg: $1" >&2; usage >&2; exit 2 ;;
|
*) echo "ERROR: unknown arg: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
"start": 202,
|
"start": 202,
|
||||||
"end": 206,
|
"end": 206,
|
||||||
"lang": "bash",
|
"lang": "bash",
|
||||||
"sha": "38836d31bf38",
|
"sha": "8bfabafb0328",
|
||||||
"class": "c",
|
"class": "c",
|
||||||
"kind": "invocation",
|
"kind": "invocation",
|
||||||
"evidence": [
|
"evidence": [
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ def test_w6_remove_plain_and_with_stop(ctl, mock_herdr, mock_agents):
|
|||||||
assert os.path.realpath(wt1) not in _worktrees(main)
|
assert os.path.realpath(wt1) not in _worktrees(main)
|
||||||
assert _git(main, "show-ref", "--verify", "--quiet", f"refs/heads/b-{wt1.name}", check=False).returncode == 0
|
assert _git(main, "show-ref", "--verify", "--quiet", f"refs/heads/b-{wt1.name}", check=False).returncode == 0
|
||||||
|
|
||||||
res = _prepare(main, wt2, "--creator-agent", "claude")
|
res = _prepare(main, wt2, "--creator-agent", "claude", "--no-onboard")
|
||||||
assert res.returncode == 0, res.stdout + res.stderr
|
assert res.returncode == 0, res.stdout + res.stderr
|
||||||
name = [r for r in _rows(main) if os.path.realpath(r["pane"]["cwd"]) == os.path.realpath(wt2)][0]["name"]
|
name = [r for r in _rows(main) if os.path.realpath(r["pane"]["cwd"]) == os.path.realpath(wt2)][0]["name"]
|
||||||
res = _run(main, REM, "--path", str(wt2), "--stop")
|
res = _run(main, REM, "--path", str(wt2), "--stop")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ if str(SKILLS) not in sys.path:
|
|||||||
|
|
||||||
from lib_py.agents.base import POST_DISCOVERED # noqa: E402
|
from lib_py.agents.base import POST_DISCOVERED # noqa: E402
|
||||||
from lib_py.agents.registry import get_adapter # noqa: E402
|
from lib_py.agents.registry import get_adapter # noqa: E402
|
||||||
|
from test_issue21_phase2_loop_a import _prepare, _rows, _wt, ctl # noqa: E402,F401
|
||||||
from test_issue63_same_cwd_ids import _mock_herdr_agent, _read_rows, _row, _write_rows # noqa: E402
|
from test_issue63_same_cwd_ids import _mock_herdr_agent, _read_rows, _row, _write_rows # noqa: E402
|
||||||
|
|
||||||
ALL_AGENTS = ("claude", "agy", "hermes", "grok", "opencode", "codex")
|
ALL_AGENTS = ("claude", "agy", "hermes", "grok", "opencode", "codex")
|
||||||
@@ -180,3 +181,68 @@ def test_no_onboard_warning_precedes_a_later_refusal(mam_sandbox, mock_herdr, mo
|
|||||||
finally:
|
finally:
|
||||||
holder.kill()
|
holder.kill()
|
||||||
holder.wait()
|
holder.wait()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"opts,expect_onboard",
|
||||||
|
[
|
||||||
|
([], True),
|
||||||
|
(["--onboard"], True),
|
||||||
|
(["--no-onboard"], False),
|
||||||
|
(["--no-onboard", "--onboard"], True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_prepare_forwards_onboarding_choice(ctl, opts, expect_onboard):
|
||||||
|
main = ctl
|
||||||
|
args_file = main / "captured_create_args.txt"
|
||||||
|
stub = main / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
stub.write_text(f'#!/usr/bin/env bash\nprintf "%s\\n" "$*" > "{args_file}"\nexit 0\n')
|
||||||
|
wt = _wt(main, "fwd")
|
||||||
|
try:
|
||||||
|
res = _prepare(main, wt, "--creator-agent", "claude", *opts)
|
||||||
|
assert res.returncode == 0, res.stdout + res.stderr
|
||||||
|
tokens = args_file.read_text().split()
|
||||||
|
if expect_onboard:
|
||||||
|
assert "--onboard" in tokens, tokens
|
||||||
|
assert "--no-onboard" not in tokens, tokens
|
||||||
|
else:
|
||||||
|
assert "--no-onboard" in tokens, tokens
|
||||||
|
assert "--onboard" not in tokens, tokens
|
||||||
|
finally:
|
||||||
|
subprocess.run(["git", "worktree", "remove", "--force", str(wt)], cwd=str(main), capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_onboards_by_default_end_to_end(ctl, mock_herdr, mock_agents):
|
||||||
|
main = ctl
|
||||||
|
wt = _wt(main, "onboard-def")
|
||||||
|
try:
|
||||||
|
res = _prepare(main, wt, "--creator-agent", "claude")
|
||||||
|
assert res.returncode == 0, res.stdout + res.stderr
|
||||||
|
(row,) = _rows(main)
|
||||||
|
jid = row["delegate_job_id"]
|
||||||
|
assert jid, row
|
||||||
|
job_path = main / ".mam" / "jobs" / f"{jid}.json"
|
||||||
|
assert job_path.is_file(), f"missing job record {job_path}"
|
||||||
|
rec = json.loads(job_path.read_text())
|
||||||
|
assert rec["status"] == "completed", rec
|
||||||
|
assert f"onboarding completed: job {jid}" in res.stdout, res.stdout
|
||||||
|
finally:
|
||||||
|
subprocess.run(["git", "worktree", "remove", "--force", str(wt)], cwd=str(main), capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_no_onboard_sends_none(ctl, mock_herdr, mock_agents):
|
||||||
|
main = ctl
|
||||||
|
wt = _wt(main, "no-onboard")
|
||||||
|
try:
|
||||||
|
res = _prepare(main, wt, "--creator-agent", "claude", "--no-onboard")
|
||||||
|
assert res.returncode == 0, res.stdout + res.stderr
|
||||||
|
(row,) = _rows(main)
|
||||||
|
assert row["delegate_job_id"] is None, row
|
||||||
|
jobs_dir = main / ".mam" / "jobs"
|
||||||
|
if jobs_dir.is_dir():
|
||||||
|
for jf in jobs_dir.glob("*.json"):
|
||||||
|
rec = json.loads(jf.read_text())
|
||||||
|
assert rec.get("agent_session") != f"herdr:{row['name']}", rec
|
||||||
|
finally:
|
||||||
|
subprocess.run(["git", "worktree", "remove", "--force", str(wt)], cwd=str(main), capture_output=True)
|
||||||
|
|
||||||
Reference in new issue
Block a user