feat(create,resume,stop): verify and standardize --herdr-session option with full peer review
- Standardize --herdr-session as primary flag with --herdr-server alias across create_session.sh, resume_session.sh, update_yaml_resumed.sh, and stop_session.sh - Guard HERDR_SESSION_NAME in create_session.sh from being overwritten by workspace slug defaults when explicitly provided - Forward explicit --herdr-session from resume_session.sh to update_yaml_resumed.sh and force-update row metadata - Add 5 new Tier 2 component tests covering CLI dry-run parsing, usage matching, default preservation, YAML serialization, and resume propagation - Update multi-agent-mux-create/SKILL.md documentation - Verified by autonomous multi-agent loop with unanimous PASS verdicts from Claude and Cline
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
# 🔍 교차 코드 리뷰 (2차) — Job `1b18eb9a`
|
||||||
|
|
||||||
|
- **역할**: Reviewer
|
||||||
|
- **대상**: `--herdr-session` / `--herdr-server` 표준화 구현분 — 워킹 트리 6파일 (`+245 / −56`)
|
||||||
|
- **기준 커밋**: `f7e1513` / 미추적 파일 0건
|
||||||
|
- **직전 판정**: `2d3fef82` **NOT PASS** (차단 2건 B-1·B-2, 권고 2건)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 결론
|
||||||
|
|
||||||
|
직전 리뷰의 차단 2건과 권고 2건이 **전부 해소**됐고, 각각에 **뮤테이션으로 감도가 확인되는 회귀 가드**가 붙었습니다. 전체 스위트 **346 passed / 실패 0**.
|
||||||
|
|
||||||
|
P3 관찰 2건만 남습니다. 어느 쪽도 결함을 가리지 않아 통과 처리합니다(최종 태그는 보고서 마지막 줄).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 직전 지적 대비 이행
|
||||||
|
|
||||||
|
| 직전 항목 | 이행 | 가드 |
|
||||||
|
|---|---|---|
|
||||||
|
| **B-1** resume 주 경로가 `--herdr-session` 미전달 | ✅ `resume_session.sh:136-138` 이 조기 종료 분기(`:72-74`)와 동일하게 전달 | **H1 검출** |
|
||||||
|
| **B-2** `setdefault` 로 기존 행에 무효 | ✅ `HERDR_SERVER_OPT_EXPLICIT` 로 명시/백필 구분, `start`/`attach`/`kill_command` 까지 갱신 | **H2 검출** |
|
||||||
|
| **§4.1** dry-run 이 플래그 무시를 구분 못 함 | ✅ 출력에 `herdr_session=${HERDR_SESSION_NAME:-default}` 추가 + 두 플래그 각각 단언 | **H4 검출** |
|
||||||
|
| **§4.2** 감사 지시된 세 가드에 커버리지 0 | ✅ `test_comp_create_herdr_session_default_preserved` 신설 | **H3 검출** |
|
||||||
|
| **N-2** create usage 테스트가 파서 미실행 | ✅ 수용 경로(`--herdr-session` + dry-run rc=0)와 거부 경로(`--invalid-flag-xyz` rc=2) 양방향 추가 | — |
|
||||||
|
| **N-3** `HERDR_SERVER_NAME` 지원 여부 모호 | ✅ SKILL.md 에 *"legacy env alias: `HERDR_SERVER_NAME`"* 명기 | — |
|
||||||
|
|
||||||
|
B-2 의 처방은 제가 제안한 것보다 낫습니다. `if is_explicit or not target.get('herdr_session')` 는 **`herdr_session: null` 인 행까지 백필**합니다 — `setdefault` 는 키가 존재하기만 하면 `None` 도 보존해 버리던 구멍이었는데, 이 형태가 그것도 함께 닫습니다. 또 `start_command`/`attach_command`/`kill_command` 3종을 함께 갱신해, 행의 라우팅 정보가 부분적으로만 갱신되는 상태를 만들지 않습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 검증 결과
|
||||||
|
|
||||||
|
| 검증 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 전체 스위트 | **346 passed / 462.39s / 실패 0** |
|
||||||
|
| 수집 수 | 341 → **346** (신설 5건) |
|
||||||
|
| `bash -n` 4개 변경 스크립트 | 4/4 OK |
|
||||||
|
| 신설 5건 대조군 | 5 passed |
|
||||||
|
| 뮤테이션 | **H1~H5 전부 지정 테스트 검출** |
|
||||||
|
|
||||||
|
1차 리뷰에서 관찰됐던 `test_o2_18_orphan_steal_lock_recovered` 플레이크는 이번 실행에서 재현되지 않았습니다(선재 부하 민감 이슈, §5 N-1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 뮤테이션 매트릭스
|
||||||
|
|
||||||
|
격리 `rsync` 사본. 대조군 5/5 통과.
|
||||||
|
|
||||||
|
| # | 뮤테이션 | 결과 |
|
||||||
|
|---|---|---|
|
||||||
|
| **H1** | `resume_session.sh:136-138` 에서 `--herdr-session` 제거 (B-1 되돌림) | `resume_herdr_session_propagation` **FAILED** |
|
||||||
|
| **H2** | `is_explicit or not target.get(...)` → `setdefault` (B-2 되돌림) | `resume_herdr_session_propagation` **FAILED** |
|
||||||
|
| **H3** | `create_session.sh` 의 `HERDR_SERVER_OPT` 가드 되돌림 | `default_preserved` **FAILED** / 나머지 2건 PASSED |
|
||||||
|
| **H4** | dry-run 출력에서 `herdr_session=` 제거 | `cli_parsing_dry_run` **FAILED** |
|
||||||
|
| **H5** | create 파서가 값을 버림 (`shift 2` 만) | `cli_parsing_dry_run` · `default_preserved` · `yaml_propagation` **3건 FAILED** |
|
||||||
|
|
||||||
|
**H3 이 정확히 하나만 깨는 것**이 중요합니다. 직전 리뷰에서 실측했듯 그 가드들의 행동 변화는 리터럴 값 `default` **하나뿐**이므로, `default_preserved` 만 실패하고 `cli_parsing_dry_run`·`yaml_propagation` 이 통과하는 것이 **정확한 감도**입니다. 과잉 결합 없이 딱 그 성질만 잡습니다.
|
||||||
|
|
||||||
|
**H1 은 제가 차단했던 바로 그 회귀**입니다. 이제 잡힙니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 플래그 없는 resume 경로 확인 (신규 검토)
|
||||||
|
|
||||||
|
`resume_session.sh` 는 이제 `--herdr-session "$HERDR_SESSION_NAME"` 을 **무조건** 전달합니다. 사용자가 플래그를 주지 않아도 값이 `resolve_herdr_session(...)` 결과로 채워져 넘어가므로, 자식에서 `HERDR_SERVER_OPT_EXPLICIT` 이 **항상 1** 이 됩니다. 명시/백필 구분이 이 호출자에서는 무의미해지는 셈이라, 잘못된 기록을 만드는지 실측했습니다.
|
||||||
|
|
||||||
|
| 행의 `herdr_session` | 플래그 없이 resume 후 | 판정 |
|
||||||
|
|---|---|---|
|
||||||
|
| `RECORDED-X` | `RECORDED-X` (attach_command 도 일치) | 멱등 ✅ |
|
||||||
|
| `default` | `mam-<ws-slug>` | **정정** ✅ |
|
||||||
|
| (키 없음) | `mam-<ws-slug>` | 백필 ✅ |
|
||||||
|
|
||||||
|
2행이 유일한 행동 변화입니다. 행이 `default` 를 기록하고 있으면 `resolve_herdr_session` 은 (`val != 'default'` 조건 때문에) 그 값을 건너뛰고 폴백으로 내려가므로, **실제 스폰은 이미 `mam-<ws-slug>` 로 이뤄집니다**. 즉 기록을 `mam-<ws-slug>` 로 바꾸는 것은 행을 **현실과 일치시키는 정정**이지 오작동이 아닙니다. 세 경우 모두 문제없습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 관찰 사항 (P3 — 비차단)
|
||||||
|
|
||||||
|
### 🟡 O-1: `update_yaml_resumed.sh` 신규 행 분기의 `herdr_server` 추가에 가드가 없다
|
||||||
|
|
||||||
|
`target is None` 분기에 `'herdr_server': server_name` 이 추가됐는데, 이 줄을 제거해도 검출되지 않습니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
H6 (신규 행 dict 에서 'herdr_server' 제거)
|
||||||
|
tier2 33 passed
|
||||||
|
tier1 + tier3 + tier4 + uuid_target + ws_scope 69 passed
|
||||||
|
→ 102건 전부 통과, 미검출
|
||||||
|
```
|
||||||
|
|
||||||
|
신설 resume 테스트가 **기존 행**을 심어 놓고 시작하므로 신규 행 경로를 타지 않습니다. 레지스트리에 없는 세션을 resume 할 때만 도달하는 좁은 경로이고, 추가된 필드는 기존 필드 옆에 별칭을 하나 더 두는 **순수 가산 변경**이라 회귀 위험이 낮습니다. 차단하지 않습니다.
|
||||||
|
|
||||||
|
처방이 필요하면 기존 resume 테스트에서 `herdr_sessions: []` 로 시작하는 케이스 1건이면 충분합니다.
|
||||||
|
|
||||||
|
### 🟡 O-2: `create_session.sh:216` 가드는 여전히 무동작
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
||||||
|
RESOLVED_SERVER="$(resolve_herdr_workspace "$SESSION_NAME" "$WORKSPACE")"
|
||||||
|
export HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-$RESOLVED_SERVER}"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
`--herdr-session` 이 주어지면 `:78-79` 에서 이미 `HERDR_SESSION_NAME` 이 비어 있지 않으므로 `${VAR:-...}` 가 발동하지 않습니다. 즉 이 가드의 유일한 실효는 **`resolve_herdr_workspace` 서브프로세스 호출 1회를 건너뛰는 것**입니다. 브리프가 감사를 지시한 세 지점 중 하나이므로 "감사했고 무동작임을 확인했다" 는 사실 자체가 기록될 가치가 있습니다 — 다만 방어적으로 남겨 두는 것이 해롭지 않고, 미래에 `:78-79` 가 바뀌면 실효가 생길 수 있으므로 제거를 권하지는 않습니다.
|
||||||
|
|
||||||
|
### 이월 (범위 밖, 선재)
|
||||||
|
|
||||||
|
| ID | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| **N-1** | `test_o2_18_orphan_steal_lock_recovered` 부하 민감 플레이크 — `acquire_bg()` 의 고정 `time.sleep(0.3)` 을 마커 폴링으로 교체하면 해소. 이번 변경과 무관 |
|
||||||
|
| **N-4** | `README.md:98,100` / `README.ko.md:80,82` 가 구 `herdr -L <server>` 메커니즘을 서술 — `IMPROVEMENTS.md:304` 기준 이미 완료된 전환이므로 선재 드리프트. SKILL.md 만 정리되어 문서 표면 간 불일치가 남아 있음 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 총평
|
||||||
|
|
||||||
|
차단 2건이 모두 닫혔고, 더 중요하게는 **각각에 감도가 실증된 가드가 붙었습니다**. 직전 리뷰에서 "신설 3건 중 실효는 1건" 이었던 상태가 이번에는 신설 5건 중 4건이 뮤테이션을 잡습니다(usage 테스트는 성격상 문서 단언이라 제외).
|
||||||
|
|
||||||
|
B-2 의 처방은 제가 제안한 형태보다 낫습니다 — `herdr_session: null` 구멍과 라우팅 명령 3종의 부분 갱신 문제를 함께 닫았습니다.
|
||||||
|
|
||||||
|
남은 2건은 P3 입니다. O-1 은 좁은 경로의 가산 변경이라 회귀 위험이 낮고, O-2 는 방어적 무동작 코드입니다. 다음 커밋에 정리하거나 그대로 둬도 무방합니다.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# Cross-Code Review — Job 40944efc
|
||||||
|
|
||||||
|
- **Reviewer**: cline
|
||||||
|
- **Target**: `--herdr-session` (alias `--herdr-server`) standardization across 6 files (`+245 / −56`)
|
||||||
|
- **Base commit**: working tree (unstaged diff)
|
||||||
|
- **Date**: 2026-08-24
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0. Verdict Summary
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| `bash -n` (4 changed shell scripts) | 4/4 OK |
|
||||||
|
| Changeset-specific tests (6) | 6/6 PASS |
|
||||||
|
| Full pytest suite (parallel run) | 346 passed, 0 failed (462.39s) |
|
||||||
|
| `--herdr-session` parsing consistency (4 scripts) | Consistent |
|
||||||
|
| `HERDR_SESSION_NAME` not clobbered when explicit | Verified (3 guard sites) |
|
||||||
|
| Companion script forwarding (resume → update_yaml) | Both call sites forward |
|
||||||
|
| Backward compat (`--herdr-server`, `HERDR_SERVER_NAME`) | Retained as alias/fallback |
|
||||||
|
| SKILL.md documentation | Updated, duplicate block removed |
|
||||||
|
|
||||||
|
**Previous N-1 (resume post-spawn not forwarding `--herdr-session`): FIXED.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1. create_session.sh — Guard Hardening
|
||||||
|
|
||||||
|
### 1.1 Three guard sites verified
|
||||||
|
|
||||||
|
All three sites now wrap the clobbering logic in `if [ -z "$HERDR_SERVER_OPT" ]; then … fi`, so an explicitly provided `--herdr-session` value is never overwritten:
|
||||||
|
|
||||||
|
| Site | Location | Behavior when `--herdr-session` explicit |
|
||||||
|
|---|---|---|
|
||||||
|
| ① ws_slug default | ~line 135 | Skipped — `HERDR_SESSION_NAME` preserved |
|
||||||
|
| ② spawn() internal | ~line 174 | Skipped — `HERDR_SESSION_NAME` preserved |
|
||||||
|
| ③ post-spawn resolve | ~line 211 | Skipped — `resolve_herdr_workspace` not called |
|
||||||
|
|
||||||
|
**Line 78-80** (pre-guard): `export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"` is set immediately after arg parsing, before any guard can interfere. ✅
|
||||||
|
|
||||||
|
### 1.2 Dry-run output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "[dry-run] would spawn: herdr session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT, herdr_session=${HERDR_SESSION_NAME:-default})"
|
||||||
|
```
|
||||||
|
Correctly surfaces the resolved `herdr_session` value. ✅
|
||||||
|
|
||||||
|
### 1.3 YAML serialization
|
||||||
|
|
||||||
|
`atomic_dump_yaml` receives `HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-default}"` as an env var (line ~289). Inside the Python heredoc:
|
||||||
|
|
||||||
|
```python
|
||||||
|
server_name = os.environ.get('HERDR_SESSION_NAME', 'default') # line ~297
|
||||||
|
…
|
||||||
|
'herdr_session': server_name, # line ~309
|
||||||
|
'herdr_server': server_name, # line ~310
|
||||||
|
'start_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
||||||
|
'attach_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
||||||
|
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
|
||||||
|
```
|
||||||
|
|
||||||
|
All 5 fields (`herdr_session`, `herdr_server`, `start_command`, `attach_command`, `kill_command`) are derived from `server_name`. ✅
|
||||||
|
|
||||||
|
### 1.4 `--herdr-session default` edge case
|
||||||
|
|
||||||
|
Test `test_comp_create_herdr_session_default_preserved` passes `--herdr-session default` and asserts `herdr_session == "default"` in YAML. The guard `if [ -z "$HERDR_SERVER_OPT" ]` is false (since `HERDR_SERVER_OPT="default"` is non-empty), so the ws_slug override is skipped and the literal `"default"` is preserved. ✅
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2. resume_session.sh — Both Call Sites Forward `--herdr-session`
|
||||||
|
|
||||||
|
### 2.1 Argument parsing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
|
```
|
||||||
|
Consistent with create/stop. `HERDR_SERVER_OPT=""` initialized → `set -u` safe. ✅
|
||||||
|
|
||||||
|
### 2.2 Export logic (lines 57-62)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||||
|
else
|
||||||
|
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "$WORKSPACE")"
|
||||||
|
export HERDR_SESSION_NAME
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
Explicit value takes priority; otherwise resolves from registry. ✅
|
||||||
|
|
||||||
|
### 2.3 Forwarding to update_yaml_resumed.sh — BOTH call sites
|
||||||
|
|
||||||
|
| Call site | Lines | Forwards `--herdr-session`? |
|
||||||
|
|---|---|---|
|
||||||
|
| Already-running path | 72-74 | ✅ `--herdr-session "$HERDR_SESSION_NAME"` |
|
||||||
|
| Post-spawn path | 136-138 | ✅ `--herdr-session "$HERDR_SESSION_NAME"` |
|
||||||
|
|
||||||
|
**This fixes the N-1 from the prior review (job f03021cf)** where the post-spawn call at line 136 did not forward the flag. Both paths now propagate the resolved session name to the YAML updater. ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3. update_yaml_resumed.sh — `HERDR_SERVER_OPT_EXPLICIT` Mechanism
|
||||||
|
|
||||||
|
### 3.1 Explicit-tracking env var (lines 42-49)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||||
|
export HERDR_SERVER_OPT_EXPLICIT="1" # explicit flag passed
|
||||||
|
else
|
||||||
|
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "${WORKSPACE:-}")"
|
||||||
|
export HERDR_SESSION_NAME
|
||||||
|
export HERDR_SERVER_OPT_EXPLICIT="0" # resolved, not explicit
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a new, clean mechanism that distinguishes "user explicitly passed `--herdr-session`" from "value was resolved from registry/env". ✅
|
||||||
|
|
||||||
|
### 3.2 Propagation to Python heredoc (line 95)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
||||||
|
… PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" \
|
||||||
|
HERDR_SERVER_OPT_EXPLICIT="${HERDR_SERVER_OPT_EXPLICIT:-0}" <<'PYEOF'
|
||||||
|
```
|
||||||
|
The env var is forwarded to the Python subprocess. ✅
|
||||||
|
|
||||||
|
### 3.3 Else-branch conditional overwrite (lines 130-139)
|
||||||
|
|
||||||
|
For an **existing** target row:
|
||||||
|
|
||||||
|
```python
|
||||||
|
sn = os.environ.get('HERDR_SESSION_NAME')
|
||||||
|
is_explicit = os.environ.get('HERDR_SERVER_OPT_EXPLICIT') == '1'
|
||||||
|
if sn:
|
||||||
|
if is_explicit or not target.get('herdr_session'):
|
||||||
|
target['herdr_session'] = sn
|
||||||
|
target['herdr_server'] = sn
|
||||||
|
target['start_command'] = f'HERDR_SESSION_NAME={sn} herdr agent attach {name}'
|
||||||
|
target['attach_command'] = f'HERDR_SESSION_NAME={sn} herdr agent attach {name}'
|
||||||
|
target['kill_command'] = f'HERDR_SESSION_NAME={sn} herdr kill-session -t {name}'
|
||||||
|
```
|
||||||
|
|
||||||
|
| Scenario | `is_explicit` | `target['herdr_session']` exists | Action |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Explicit `--herdr-session NEW` | 1 | yes (OLD) | **Overwrites** to NEW ✅ |
|
||||||
|
| Explicit `--herdr-session NEW` | 1 | no | Overwrites to NEW ✅ |
|
||||||
|
| Resolved (no flag) | 0 | yes | **Preserves** existing ✅ |
|
||||||
|
| Resolved (no flag) | 0 | no | **Backfills** from resolved sn ✅ |
|
||||||
|
| Resolved, sn absent | 0 | — | Skips (no-op) ✅ |
|
||||||
|
|
||||||
|
This is a significant improvement over the previous `setdefault`-only approach. When explicit, it always overwrites (fixing the orphan-registry edge case). When not explicit, it preserves the existing value and only backfills if missing. ✅
|
||||||
|
|
||||||
|
### 3.4 New-target path (lines 111-129)
|
||||||
|
|
||||||
|
When the target row doesn't exist, a new entry is created with `server_name = os.environ.get('HERDR_SESSION_NAME', default_server)` and all 5 fields populated. ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4. stop_session.sh — Consistent Parsing
|
||||||
|
|
||||||
|
### 4.1 Argument parsing (line 77)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
|
```
|
||||||
|
Identical pattern to the other 3 scripts. ✅
|
||||||
|
|
||||||
|
### 4.2 Export logic (lines 104-109)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||||
|
else
|
||||||
|
HERDR_SESSION_NAME="$(resolve_herdr_workspace "$SESSION_NAME" "${WORKSPACE:-$WORKSPACE_ROOT}")"
|
||||||
|
export HERDR_SESSION_NAME
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
Consistent with resume's pattern. ✅
|
||||||
|
|
||||||
|
### 4.3 Usage/docs updated
|
||||||
|
|
||||||
|
Both the header comment (line 4, 19) and `usage()` (lines 44-45, 52) document `--herdr-session` with the `--herdr-server` alias. ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5. SKILL.md — Documentation
|
||||||
|
|
||||||
|
- **Title**: Renamed "Herdr Server Isolation (격리 서버)" → "Herdr Session Isolation (격리 세션)" ✅
|
||||||
|
- **Primary names**: `HERDR_SESSION_NAME` env var and `--herdr-session` flag documented as standard ✅
|
||||||
|
- **Alias note**: "(opt-in; alias: `--herdr-server`; legacy env alias: `HERDR_SERVER_NAME`)" ✅
|
||||||
|
- **Duplicate "Recommended Alias" block removed**: The previous version had a misplaced/repeated paragraph. This is now cleaned up. ✅
|
||||||
|
- **Wording fix**: "this now maps to" → "this maps to" (removed erroneous "now") ✅
|
||||||
|
- **Migration examples**: Updated to use `HERDR_SESSION_NAME` and `--herdr-session` ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6. Backward Compatibility
|
||||||
|
|
||||||
|
| Legacy mechanism | Status | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| `--herdr-server` flag | Retained as alias in all 4 scripts | `--herdr-session\|--herdr-server)` parser case |
|
||||||
|
| `HERDR_SERVER_NAME` env var | Retained as fallback in `lib.sh` | `resolve_herdr_workspace`: `os.environ.get('HERDR_SESSION_NAME', '') or os.environ.get('HERDR_SERVER_NAME', '')` (line 1043) |
|
||||||
|
| `reconcile.sh` env fallback | Retained | `elif 'HERDR_SERVER_NAME' in os.environ:` (line 386-387) |
|
||||||
|
| Existing tests using `HERDR_SERVER_NAME` | Still pass | `test_tier1_unit.py`, `test_tier3_integration.py`, `test_workspace_scope.py` — all in the 346 passed |
|
||||||
|
|
||||||
|
Zero functionality loss. A user who has `HERDR_SERVER_NAME` exported or uses `--herdr-server` will see identical behavior. ✅
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7. Test Coverage
|
||||||
|
|
||||||
|
### 7.1 Changeset-specific tests (6 total: 5 new + 1 modified)
|
||||||
|
|
||||||
|
| Test | Feature | Status | Runtime |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `test_comp_create_usage_matches_parser` | Create: usage docs + parser | PASS | 2.09s |
|
||||||
|
| `test_comp_create_herdr_session_cli_parsing_dry_run` | Create: `--herdr-session` + `--herdr-server` dry-run | PASS | 2.34s |
|
||||||
|
| `test_comp_create_herdr_session_default_preserved` | Create: `--herdr-session default` preserved | PASS | (batch 24.20s) |
|
||||||
|
| `test_comp_create_herdr_session_yaml_propagation` | Create: YAML field propagation (5 fields) | PASS | (batch 24.20s) |
|
||||||
|
| `test_comp_resume_herdr_session_propagation` | Resume: NEW overwrites OLD (N-1 fix) | PASS | 5.35s |
|
||||||
|
| `test_comp_stop_usage_matches_parser` (modified) | Stop: `--herdr-session` parser acceptance | PASS | 1.16s |
|
||||||
|
|
||||||
|
### 7.2 Coverage assessment
|
||||||
|
|
||||||
|
- **CLI parsing**: Both `--herdr-session` and `--herdr-server` tested in dry-run mode ✅
|
||||||
|
- **Usage/parser matching**: Create + stop both verify usage() advertises flags that the parser accepts ✅
|
||||||
|
- **YAML propagation**: `herdr_session`, `herdr_server`, `start_command`, `attach_command`, `kill_command` all asserted ✅
|
||||||
|
- **Default preservation**: `--herdr-session default` edge case covered ✅
|
||||||
|
- **Resume overwrite**: Explicit `--herdr-session NEW` overwriting `OLD` in existing row — directly tests the N-1 fix ✅
|
||||||
|
|
||||||
|
### 7.3 Full suite
|
||||||
|
|
||||||
|
A parallel full-suite run (by the claude reviewer) completed: **346 passed, 0 failed** (462.39s). This includes all changeset-specific tests plus tier1/tier3/tier4/integration/e2e suites. ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8. Non-blocking Observations
|
||||||
|
|
||||||
|
### N-1 (FIXED — no longer an issue)
|
||||||
|
|
||||||
|
The previous review (job f03021cf) noted that `resume_session.sh` line 136-137 (post-spawn `update_yaml_resumed.sh` call) did not forward `--herdr-session`. **This is now fixed**: both call sites (already-running at line 72 and post-spawn at line 136) forward `--herdr-session "$HERDR_SESSION_NAME"`. Additionally, `update_yaml_resumed.sh` now uses the `HERDR_SERVER_OPT_EXPLICIT` mechanism to force-overwrite the existing row's `herdr_session`/`herdr_server`/commands when the flag is explicit. The new test `test_comp_resume_herdr_session_propagation` directly verifies this. ✅
|
||||||
|
|
||||||
|
### N-2 (pre-existing, out of scope)
|
||||||
|
|
||||||
|
`deploy/install_mam.sh` (line 330), `deploy/install.sh` (line 524), and potentially `README.ko.md` still use `HERDR_SERVER_NAME` as the primary env var name in user-facing instructions. These are pre-existing references not introduced by this changeset and are out of scope. The legacy alias still works via `lib.sh`'s fallback, so there is no functional impact — only documentation consistency.
|
||||||
|
|
||||||
|
### N-3 (environmental, not a changeset defect)
|
||||||
|
|
||||||
|
The environment has dozens of orphaned `reconcile.sh --subscribe --idle-timeout 0` daemon processes from prior test runs, plus a live herdr server. This slowed independent test execution but did not affect results — the parallel full suite (346 passed) and all individually-run changeset tests confirm correctness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §9. Conclusion
|
||||||
|
|
||||||
|
This changeset is a clean, well-tested standardization of `--herdr-session` across the multi-agent-mux skill scripts. Key strengths:
|
||||||
|
|
||||||
|
1. **Correctness**: All 3 guard sites in `create_session.sh` properly protect explicit values from being clobbered.
|
||||||
|
2. **Consistency**: All 4 scripts use the same `--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"` parsing pattern and the same `if [ -n "$HERDR_SERVER_OPT" ]` export logic.
|
||||||
|
3. **N-1 fix**: The previous review's blocking observation (resume post-spawn not forwarding `--herdr-session`) is fully addressed — both call sites now forward, and `update_yaml_resumed.sh` uses `HERDR_SERVER_OPT_EXPLICIT` to force-overwrite when explicit.
|
||||||
|
4. **Backward compatibility**: `--herdr-server` flag and `HERDR_SERVER_NAME` env var are retained as aliases/fallbacks with zero functionality loss.
|
||||||
|
5. **Test coverage**: 6 changeset-specific tests (5 new + 1 modified) cover CLI parsing, usage/parser matching, default preservation, YAML field propagation, and resume overwrite. Full suite: 346 passed, 0 failed.
|
||||||
|
|
||||||
|
No blocking issues found. No design-level rework needed.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -63,23 +63,23 @@ If any check fails → abort with a non-zero exit and report the reason (automat
|
|||||||
- contents: herdr new-session with `claude` inside, auto-handles trust/bypass dialogs
|
- contents: herdr new-session with `claude` inside, auto-handles trust/bypass dialogs
|
||||||
- see `<workdir>/agent_sessions.md` for the canonical wrapper template
|
- see `<workdir>/agent_sessions.md` for the canonical wrapper template
|
||||||
|
|
||||||
## Herdr Server Isolation (격리 서버)
|
## Herdr Session Isolation (격리 세션)
|
||||||
|
|
||||||
When running multiple agent sessions alongside other workflows (e.g., cmux, background workers, manual herdr sessions), sharing the default herdr server can lead to session name conflicts, monitoring clutter, and accidental destruction of user sessions via global commands.
|
When running multiple agent sessions alongside other workflows (e.g., cmux, background workers, manual herdr sessions), sharing the default herdr server can lead to session name conflicts, monitoring clutter, and accidental destruction of user sessions via global commands.
|
||||||
|
|
||||||
To prevent this, you can run this skill inside an **isolated herdr server** using the `HERDR_SERVER_NAME` environment variable or the `--herdr-server <name>` flag (opt-in).
|
To prevent this, you can run this skill inside an **isolated herdr session** using the `HERDR_SESSION_NAME` environment variable or the `--herdr-session <name>` flag (opt-in; alias: `--herdr-server`; legacy env alias: `HERDR_SERVER_NAME`).
|
||||||
|
|
||||||
Under the hood this now maps to a real, separate herdr **session** (`herdr --session <name>` — its own socket, its own `agent list`/`workspace list`, completely invisible to the default session and vice versa), not just a workspace label inside the same server. `lib.sh`'s shim bootstraps the named session's server headlessly (`herdr --session <name> server`, backgrounded) the first time it's needed, and scopes every subsequent herdr call to it automatically — this headless bootstrap is what lets it work even when the skill itself is running from inside another herdr-managed pane (a plain interactive `herdr --session <name>` launch is blocked there by herdr's "nested herdr is disabled" guard; headless `server` mode isn't).
|
Under the hood this maps to a real, separate herdr **session** (`herdr --session <name>` — its own socket, its own `agent list`/`workspace list`, completely invisible to the default session and vice versa), not just a workspace label inside the same server. `lib.sh`'s shim bootstraps the named session's server headlessly (`herdr --session <name> server`, backgrounded) the first time it's needed, and scopes every subsequent herdr call to it automatically — this headless bootstrap is what lets it work even when the skill itself is running from inside another herdr-managed pane (a plain interactive `herdr --session <name>` launch is blocked there by herdr's "nested herdr is disabled" guard; headless `server` mode isn't).
|
||||||
|
|
||||||
### How to use
|
### How to use
|
||||||
1. **Via Environment Variable**:
|
1. **Via Environment Variable**:
|
||||||
```bash
|
```bash
|
||||||
export HERDR_SERVER_NAME=multi-agent-canary
|
export HERDR_SESSION_NAME=multi-agent-canary
|
||||||
# All subsequent commands (create, status, stop, etc.) will run in the isolated 'multi-agent-canary' herdr server.
|
# All subsequent commands (create, status, stop, etc.) will run in the isolated 'multi-agent-canary' herdr session.
|
||||||
```
|
```
|
||||||
2. **Via Option Flag**:
|
2. **Via Option Flag**:
|
||||||
```bash
|
```bash
|
||||||
bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --herdr-server multi-agent-canary
|
bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --herdr-session multi-agent-canary
|
||||||
```
|
```
|
||||||
3. **Submit Job Integration**:
|
3. **Submit Job Integration**:
|
||||||
You can automatically register a delegated job with a prompt when creating a session:
|
You can automatically register a delegated job with a prompt when creating a session:
|
||||||
@@ -92,25 +92,10 @@ Under the hood this now maps to a real, separate herdr **session** (`herdr --ses
|
|||||||
bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --onboard
|
bash scripts/create_session.sh --workspace /path/to/project --agent claude --role developer --onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
### Recommended Alias
|
|
||||||
You can set an alias in your shell to easily query sessions on the isolated server:
|
|
||||||
To prevent this, you can run this skill inside an **isolated herdr session** using the `HERDR_SESSION_NAME` environment variable or the `--herdr-session <name>` flag (opt-in).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Explicit custom session
|
|
||||||
export HERDR_SESSION_NAME=multi-agent-canary
|
|
||||||
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
|
|
||||||
--workspace /path/to/project --agent claude --role Developer
|
|
||||||
|
|
||||||
# Or via flag
|
|
||||||
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
|
|
||||||
--workspace /path/to/project --agent claude --role Developer --herdr-session multi-agent-canary
|
|
||||||
```
|
|
||||||
|
|
||||||
Why use `--herdr-session`?
|
Why use `--herdr-session`?
|
||||||
|
|
||||||
- By default, all skills target `default` herdr session socket — fine for single-workspace use.
|
- By default, all skills target `default` herdr session socket — fine for single-workspace use.
|
||||||
- By using an isolated session via `HERDR_SESSION_NAME`, your agent sessions are completely separated from your default user workspace, ensuring 0% interference — this is now backed by a genuinely separate `herdr` session/socket, not merely a workspace label.
|
- By using an isolated session via `HERDR_SESSION_NAME` (or `--herdr-session`), your agent sessions are completely separated from your default user workspace, ensuring 0% interference — this is backed by a genuinely separate `herdr` session/socket, not merely a workspace label.
|
||||||
- To deliberately tear down an *entire* isolated group at once (all its workspaces and agents), use `herdr session stop <HERDR_SESSION_NAME>` followed by `herdr session delete <HERDR_SESSION_NAME>` — this only affects that named session, never the default one.
|
- To deliberately tear down an *entire* isolated group at once (all its workspaces and agents), use `herdr session stop <HERDR_SESSION_NAME>` followed by `herdr session delete <HERDR_SESSION_NAME>` — this only affects that named session, never the default one.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -192,7 +177,8 @@ After spawn, append a new `herdr_sessions[]` entry to `.mam/agent-sessions.yaml`
|
|||||||
status: running
|
status: running
|
||||||
herdr_session_created_at: 2026-06-17T...Z # ISO 8601 UTC
|
herdr_session_created_at: 2026-06-17T...Z # ISO 8601 UTC
|
||||||
herdr_session_epoch: <HERDR_EPOCH>
|
herdr_session_epoch: <HERDR_EPOCH>
|
||||||
herdr_server: <HERDR_SERVER_NAME> # Isolated server name (default: 'default')
|
herdr_session: <HERDR_SESSION_NAME> # Isolated session name (default: 'mam-<ws-slug>')
|
||||||
|
herdr_server: <HERDR_SESSION_NAME> # Alias for herdr_session
|
||||||
pane:
|
pane:
|
||||||
index: 0
|
index: 0
|
||||||
pid: <PANE_PID>
|
pid: <PANE_PID>
|
||||||
@@ -205,13 +191,13 @@ After spawn, append a new `herdr_sessions[]` entry to `.mam/agent-sessions.yaml`
|
|||||||
plan: <from TUI status>
|
plan: <from TUI status>
|
||||||
account: <from TUI status>
|
account: <from TUI status>
|
||||||
version: <from TUI status>
|
version: <from TUI status>
|
||||||
start_command: "HERDR_SERVER_NAME=<herdr_server> herdr new-session -d -s <SESSION_NAME> -x 140 -y 40 -c <WORKSPACE> <CMD_FULL>"
|
start_command: "HERDR_SESSION_NAME=<herdr_session> herdr new-session -d -s <SESSION_NAME> -x 140 -y 40 -c <WORKSPACE> <CMD_FULL>"
|
||||||
attach_command: "HERDR_SERVER_NAME=<herdr_server> herdr agent attach <SESSION_NAME>"
|
attach_command: "HERDR_SESSION_NAME=<herdr_session> herdr agent attach <SESSION_NAME>"
|
||||||
kill_command: "HERDR_SERVER_NAME=<herdr_server> herdr kill-session -t <SESSION_NAME>"
|
kill_command: "HERDR_SESSION_NAME=<herdr_session> herdr kill-session -t <SESSION_NAME>"
|
||||||
# All three require `source .agents/skills/lib.sh` first — `new-session`/`kill-session`
|
# All three require `source .agents/skills/lib.sh` first — `new-session`/`kill-session`
|
||||||
# are tmux-compat pseudo-commands the shim translates, and `HERDR_SERVER_NAME` is what
|
# are tmux-compat pseudo-commands the shim translates, and `HERDR_SESSION_NAME` is what
|
||||||
# the shim reads to route to the right isolated herdr *session* (real `herdr` has no
|
# the shim reads to route to the right isolated herdr *session* (real `herdr` has no
|
||||||
# env-var-based scoping of its own; `herdr_server: default` needs no prefix at all).
|
# env-var-based scoping of its own; `herdr_session: default` needs no prefix at all).
|
||||||
```
|
```
|
||||||
|
|
||||||
`cmd_full` per agent (this is the actual command line in the pane, not the resume command):
|
`cmd_full` per agent (this is the actual command line in the pane, not the resume command):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# create_session.sh — multi-agent-mux-create 의 부속 스크립트
|
# create_session.sh — multi-agent-mux-create 의 부속 스크립트
|
||||||
# Usage:
|
# Usage:
|
||||||
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|cline> --role <role> [--session <name>] [--wrapper]
|
# bash create_session.sh --workspace <path> --agent <claude|agy|hermes|cline> --role <role> [--session <name>] [--herdr-session <name>] [--wrapper]
|
||||||
#
|
#
|
||||||
# 동작:
|
# 동작:
|
||||||
# 1) preflight: herdr/claude/agy 가용성, workspace 존재
|
# 1) preflight: herdr/claude/agy 가용성, workspace 존재
|
||||||
@@ -35,7 +35,8 @@ Options:
|
|||||||
--session NAME herdr session name (default: derived from workspace)
|
--session NAME herdr session name (default: derived from workspace)
|
||||||
--wrapper force use of ~/.local/bin/<session> wrapper even if not present
|
--wrapper force use of ~/.local/bin/<session> wrapper even if not present
|
||||||
--dry-run print commands without executing
|
--dry-run print commands without executing
|
||||||
--herdr-server NAME specify isolated herdr server name
|
--herdr-session NAME specify isolated herdr session name (alias: --herdr-server)
|
||||||
|
--herdr-server NAME specify isolated herdr session name (legacy alias)
|
||||||
--submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt
|
--submit-job PROMPT submit a job to multi-agent-mux-delegate-job registry with the given prompt
|
||||||
--onboard automatically submit a project alignment/orientation job to the new agent
|
--onboard automatically submit a project alignment/orientation job to the new agent
|
||||||
--no-onboard disable automatic onboarding job submission
|
--no-onboard disable automatic onboarding job submission
|
||||||
@@ -136,8 +137,10 @@ LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
|
|||||||
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
WRAPPER="$LOCAL_BIN/$SESSION_NAME"
|
||||||
|
|
||||||
ws_slug="$(derive_workspace_slug "$WORKSPACE")"
|
ws_slug="$(derive_workspace_slug "$WORKSPACE")"
|
||||||
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
||||||
export HERDR_SESSION_NAME="$ws_slug"
|
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
||||||
|
export HERDR_SESSION_NAME="$ws_slug"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
|
# Resolve absolute path of the agent command to prevent herdr PATH inheritance issues (especially on macOS)
|
||||||
@@ -170,8 +173,10 @@ if [ -z "$CMD_FULL" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
spawn() {
|
spawn() {
|
||||||
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
||||||
export HERDR_SESSION_NAME="$ws_slug"
|
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
|
||||||
|
export HERDR_SESSION_NAME="$ws_slug"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
case "$AGENT" in
|
case "$AGENT" in
|
||||||
claude)
|
claude)
|
||||||
@@ -192,7 +197,7 @@ spawn() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ "$DRY_RUN" = "1" ]; then
|
if [ "$DRY_RUN" = "1" ]; then
|
||||||
echo "[dry-run] would spawn: herdr session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT)"
|
echo "[dry-run] would spawn: herdr session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT, herdr_session=${HERDR_SESSION_NAME:-default})"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -208,8 +213,10 @@ cleanup_herdr_on_error() {
|
|||||||
}
|
}
|
||||||
trap cleanup_herdr_on_error EXIT
|
trap cleanup_herdr_on_error EXIT
|
||||||
|
|
||||||
RESOLVED_SERVER="$(resolve_herdr_workspace "$SESSION_NAME" "$WORKSPACE")"
|
if [ -z "$HERDR_SERVER_OPT" ]; then
|
||||||
export HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-$RESOLVED_SERVER}"
|
RESOLVED_SERVER="$(resolve_herdr_workspace "$SESSION_NAME" "$WORKSPACE")"
|
||||||
|
export HERDR_SESSION_NAME="${HERDR_SESSION_NAME:-$RESOLVED_SERVER}"
|
||||||
|
fi
|
||||||
|
|
||||||
# TUI 준비 대기
|
# TUI 준비 대기
|
||||||
if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then
|
if ! wait_for_tui_ready "$SESSION_NAME" "$AGENT"; then
|
||||||
|
|||||||
@@ -9,17 +9,19 @@ source "$LIB_SH"
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --session <name> [--dry-run]
|
Usage: $0 --workspace <path> --agent <claude|agy|hermes|cline> --session <name> [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dry-run Simulates resume flow (resolves binary, environment) without writing
|
--herdr-session NAME specify isolated herdr session name (alias: --herdr-server)
|
||||||
any updates to YAML or DB. Safe to execute inside active write transactions.
|
--dry-run Simulates resume flow (resolves binary, environment) without writing
|
||||||
|
any updates to YAML or DB. Safe to execute inside active write transactions.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
WORKSPACE=""
|
WORKSPACE=""
|
||||||
AGENT=""
|
AGENT=""
|
||||||
SESSION_NAME=""
|
SESSION_NAME=""
|
||||||
|
HERDR_SERVER_OPT=""
|
||||||
|
|
||||||
DRY_RUN=0
|
DRY_RUN=0
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--workspace) WORKSPACE="$2"; shift 2 ;;
|
--workspace) WORKSPACE="$2"; shift 2 ;;
|
||||||
--agent) AGENT="$2"; shift 2 ;;
|
--agent) AGENT="$2"; shift 2 ;;
|
||||||
--session) SESSION_NAME="$2"; shift 2 ;;
|
--session) SESSION_NAME="$2"; shift 2 ;;
|
||||||
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
--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; exit 2 ;;
|
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||||
@@ -51,8 +54,12 @@ if [ -z "$UUID" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "$WORKSPACE")"
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
export HERDR_SESSION_NAME
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||||
|
else
|
||||||
|
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "$WORKSPACE")"
|
||||||
|
export HERDR_SESSION_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
# 2. If herdr is alive, print warning or attach.
|
# 2. If herdr is alive, print warning or attach.
|
||||||
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||||
@@ -63,7 +70,8 @@ if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|||||||
echo "herdr '$SESSION_NAME' already running."
|
echo "herdr '$SESSION_NAME' already running."
|
||||||
# Just update YAML to make sure it's set to running
|
# Just update YAML to make sure it's set to running
|
||||||
bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_resumed.sh" \
|
bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_resumed.sh" \
|
||||||
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT" --workspace "$WORKSPACE"
|
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT" --workspace "$WORKSPACE" \
|
||||||
|
--herdr-session "$HERDR_SESSION_NAME"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -126,6 +134,7 @@ sleep 2
|
|||||||
|
|
||||||
# 5. Update agent-sessions.yaml: status running, last_visible_status
|
# 5. Update agent-sessions.yaml: status running, last_visible_status
|
||||||
bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_resumed.sh" \
|
bash "$(dirname "${BASH_SOURCE[0]}")/update_yaml_resumed.sh" \
|
||||||
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT" --workspace "$WORKSPACE"
|
--session "$SESSION_NAME" --uuid "$UUID" --agent "$AGENT" --workspace "$WORKSPACE" \
|
||||||
|
--herdr-session "$HERDR_SESSION_NAME"
|
||||||
|
|
||||||
echo "Successfully resumed $SESSION_NAME ($AGENT)"
|
echo "Successfully resumed $SESSION_NAME ($AGENT)"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh"
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|cline]
|
Usage: $0 --session <name> --uuid <id> [--agent claude|agy|hermes|cline] [--herdr-session <name>]
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ UUID=""
|
|||||||
AGENT=""
|
AGENT=""
|
||||||
WORKSPACE=""
|
WORKSPACE=""
|
||||||
ROLE=""
|
ROLE=""
|
||||||
|
HERDR_SERVER_OPT=""
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -28,6 +29,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--agent) AGENT="$2"; shift 2 ;;
|
--agent) AGENT="$2"; shift 2 ;;
|
||||||
--workspace) WORKSPACE="$2"; shift 2 ;;
|
--workspace) WORKSPACE="$2"; shift 2 ;;
|
||||||
--role) ROLE="$2"; shift 2 ;;
|
--role) ROLE="$2"; shift 2 ;;
|
||||||
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
@@ -37,8 +39,14 @@ done
|
|||||||
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
[ -n "$UUID" ] || { echo "ERROR: --uuid required" >&2; exit 2; }
|
||||||
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
[ -f "$AGENT_SESSIONS_YAML" ] || { echo "ERROR: $AGENT_SESSIONS_YAML not found" >&2; exit 1; }
|
||||||
|
|
||||||
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "${WORKSPACE:-}")"
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
export HERDR_SESSION_NAME
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||||
|
export HERDR_SERVER_OPT_EXPLICIT="1"
|
||||||
|
else
|
||||||
|
HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "${WORKSPACE:-}")"
|
||||||
|
export HERDR_SESSION_NAME
|
||||||
|
export HERDR_SERVER_OPT_EXPLICIT="0"
|
||||||
|
fi
|
||||||
|
|
||||||
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
|
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
|
||||||
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
|
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
|
||||||
@@ -84,7 +92,7 @@ for s in d.get('herdr_sessions', []):
|
|||||||
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
|
||||||
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \
|
||||||
NOW_EPOCH="$NOW_EPOCH" TARGET_WORKSPACE="${WORKSPACE:-$WORKSPACE_ROOT}" ROLE="$ROLE" \
|
NOW_EPOCH="$NOW_EPOCH" TARGET_WORKSPACE="${WORKSPACE:-$WORKSPACE_ROOT}" ROLE="$ROLE" \
|
||||||
PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" <<'PYEOF'
|
PANE_PID="$PANE_PID" CHILD_PID="$CHILD_PID" HERDR_SERVER_OPT_EXPLICIT="${HERDR_SERVER_OPT_EXPLICIT:-0}" <<'PYEOF'
|
||||||
name = os.environ['SESSION_NAME']
|
name = os.environ['SESSION_NAME']
|
||||||
uuid = os.environ['UUID']
|
uuid = os.environ['UUID']
|
||||||
agent = os.environ['AGENT']
|
agent = os.environ['AGENT']
|
||||||
@@ -111,6 +119,7 @@ if target is None:
|
|||||||
'herdr_session_created_at': now,
|
'herdr_session_created_at': now,
|
||||||
'herdr_session_epoch': epoch,
|
'herdr_session_epoch': epoch,
|
||||||
'herdr_session': server_name,
|
'herdr_session': server_name,
|
||||||
|
'herdr_server': server_name,
|
||||||
'delegate_job_id': None,
|
'delegate_job_id': None,
|
||||||
'pane': {'index': 0, 'pid': int(pane_pid) if pane_pid.isdigit() else 0, 'cmd': agent, 'cwd': ws_root},
|
'pane': {'index': 0, 'pid': int(pane_pid) if pane_pid.isdigit() else 0, 'cmd': agent, 'cwd': ws_root},
|
||||||
'start_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
'start_command': f'HERDR_SESSION_NAME={server_name} herdr agent attach {name}',
|
||||||
@@ -118,6 +127,16 @@ if target is None:
|
|||||||
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
|
'kill_command': f'HERDR_SESSION_NAME={server_name} herdr kill-session -t {name}',
|
||||||
}
|
}
|
||||||
d.setdefault('herdr_sessions', []).append(target)
|
d.setdefault('herdr_sessions', []).append(target)
|
||||||
|
else:
|
||||||
|
sn = os.environ.get('HERDR_SESSION_NAME')
|
||||||
|
is_explicit = os.environ.get('HERDR_SERVER_OPT_EXPLICIT') == '1'
|
||||||
|
if sn:
|
||||||
|
if is_explicit or not target.get('herdr_session'):
|
||||||
|
target['herdr_session'] = sn
|
||||||
|
target['herdr_server'] = sn
|
||||||
|
target['start_command'] = f'HERDR_SESSION_NAME={sn} herdr agent attach {name}'
|
||||||
|
target['attach_command'] = f'HERDR_SESSION_NAME={sn} herdr agent attach {name}'
|
||||||
|
target['kill_command'] = f'HERDR_SESSION_NAME={sn} herdr kill-session -t {name}'
|
||||||
|
|
||||||
target['status'] = 'running'
|
target['status'] = 'running'
|
||||||
target.pop('terminated_at', None)
|
target.pop('terminated_at', None)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
|
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
|
||||||
# Usage:
|
# Usage:
|
||||||
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|cline] \
|
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|cline] [--herdr-session <name>] \
|
||||||
# [--reason <reason>] [--purge-conversation] [--yes]
|
# [--reason <reason>] [--purge-conversation] [--yes]
|
||||||
#
|
#
|
||||||
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
|
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
# (권장: 항상 명시. 미지정 시 레지스트리 기록으로
|
# (권장: 항상 명시. 미지정 시 레지스트리 기록으로
|
||||||
# 해석 — agent 필드 → 세션명 접미사 → pane.cmd;
|
# 해석 — agent 필드 → 세션명 접미사 → pane.cmd;
|
||||||
# 셋 다 실패하면 exit 2)
|
# 셋 다 실패하면 exit 2)
|
||||||
|
# --herdr-session <name> — isolated herdr session name (alias: --herdr-server)
|
||||||
# --reason <reason> — 상태 전이 사유 (stop_reason). 기본값 manual_stop
|
# --reason <reason> — 상태 전이 사유 (stop_reason). 기본값 manual_stop
|
||||||
# --purge-conversation — 디스크의 conversation artifact 까지 삭제.
|
# --purge-conversation — 디스크의 conversation artifact 까지 삭제.
|
||||||
# status=terminated, resumable=false 로 전이하며
|
# status=terminated, resumable=false 로 전이하며
|
||||||
@@ -40,14 +41,15 @@ source "$_lib_sh"
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 --session <name> [--agent claude|agy|hermes|cline] [--reason <reason>]
|
Usage: $0 --session <name> [--agent claude|agy|hermes|cline] [--herdr-session <name>]
|
||||||
[--purge-conversation] [--yes]
|
[--reason <reason>] [--purge-conversation] [--yes]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
--session <name> — target session name (required)
|
--session <name> — target session name (required)
|
||||||
--agent <type> — claude | agy | hermes | cline (recommended: always pass it)
|
--agent <type> — claude | agy | hermes | cline (recommended: always pass it)
|
||||||
(falls back to the registry record: agent field ->
|
(falls back to the registry record: agent field ->
|
||||||
session-name suffix -> pane.cmd)
|
session-name suffix -> pane.cmd)
|
||||||
|
--herdr-session <name> — specify isolated herdr session name (alias: --herdr-server)
|
||||||
--reason <reason> — stop_reason field (default: manual_stop)
|
--reason <reason> — stop_reason field (default: manual_stop)
|
||||||
--purge-conversation — also delete on-disk conversation artifacts;
|
--purge-conversation — also delete on-disk conversation artifacts;
|
||||||
status becomes terminated and resume is impossible
|
status becomes terminated and resume is impossible
|
||||||
@@ -60,6 +62,7 @@ EOF
|
|||||||
|
|
||||||
SESSION_NAME=""
|
SESSION_NAME=""
|
||||||
AGENT=""
|
AGENT=""
|
||||||
|
HERDR_SERVER_OPT=""
|
||||||
PURGE=0
|
PURGE=0
|
||||||
YES=0
|
YES=0
|
||||||
CAPTURE_ID=1
|
CAPTURE_ID=1
|
||||||
@@ -71,6 +74,7 @@ while [ $# -gt 0 ]; do
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--session) SESSION_NAME="$2"; shift 2 ;;
|
--session) SESSION_NAME="$2"; shift 2 ;;
|
||||||
--agent) AGENT="$2"; shift 2 ;;
|
--agent) AGENT="$2"; shift 2 ;;
|
||||||
|
--herdr-session|--herdr-server) HERDR_SERVER_OPT="$2"; shift 2 ;;
|
||||||
--purge-conversation) PURGE=1; shift ;;
|
--purge-conversation) PURGE=1; shift ;;
|
||||||
--yes) YES=1; shift ;;
|
--yes) YES=1; shift ;;
|
||||||
--reason) REASON="$2"; shift 2 ;;
|
--reason) REASON="$2"; shift 2 ;;
|
||||||
@@ -97,8 +101,12 @@ if [ "$PURGE" = "1" ]; then
|
|||||||
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
|
trap 'rm -f "$WORKSPACE_ROOT/.mam/purging-$SESSION_NAME"' EXIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HERDR_SESSION_NAME="$(resolve_herdr_workspace "$SESSION_NAME" "${WORKSPACE:-$WORKSPACE_ROOT}")"
|
if [ -n "$HERDR_SERVER_OPT" ]; then
|
||||||
export HERDR_SESSION_NAME
|
export HERDR_SESSION_NAME="$HERDR_SERVER_OPT"
|
||||||
|
else
|
||||||
|
HERDR_SESSION_NAME="$(resolve_herdr_workspace "$SESSION_NAME" "${WORKSPACE:-$WORKSPACE_ROOT}")"
|
||||||
|
export HERDR_SESSION_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
|
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
|
||||||
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
|
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def get_mqtt_common(mam_sandbox):
|
|||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# FEATURE 1: Create Session (5 Test Cases)
|
# FEATURE 1: Create Session (9 Test Cases)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
def test_comp_create_schema_validation(mam_sandbox):
|
def test_comp_create_schema_validation(mam_sandbox):
|
||||||
@@ -133,8 +133,122 @@ def test_comp_create_sqlite_tables_created(mam_sandbox, mock_herdr, mock_agents)
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_comp_create_usage_matches_parser(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
"""Verify that create_session.sh usage documents --herdr-session and parser accepts it."""
|
||||||
|
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res = subprocess.run(["bash", str(script), "--help"], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 0
|
||||||
|
assert "--herdr-session" in res.stdout
|
||||||
|
assert "--herdr-server" in res.stdout
|
||||||
|
for agent in ("claude", "agy", "hermes", "cline"):
|
||||||
|
assert agent in res.stdout
|
||||||
|
|
||||||
|
# Test parser acceptance of valid flags vs unknown arg rejection
|
||||||
|
r1 = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "claude",
|
||||||
|
"--role", "Creator",
|
||||||
|
"--herdr-session", "test-sess",
|
||||||
|
"--dry-run"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert r1.returncode == 0
|
||||||
|
assert "unknown arg" not in r1.stderr
|
||||||
|
|
||||||
|
r2 = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--invalid-flag-xyz"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert r2.returncode == 2
|
||||||
|
assert "unknown arg" in r2.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_comp_create_herdr_session_cli_parsing_dry_run(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
"""Verify that --herdr-session and --herdr-server are parsed cleanly in --dry-run mode."""
|
||||||
|
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res1 = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "claude",
|
||||||
|
"--role", "Creator",
|
||||||
|
"--herdr-session", "test-isolated-sess",
|
||||||
|
"--dry-run"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert res1.returncode == 0
|
||||||
|
assert "[dry-run] would spawn:" in res1.stdout
|
||||||
|
assert "herdr_session=test-isolated-sess" in res1.stdout
|
||||||
|
|
||||||
|
res2 = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "claude",
|
||||||
|
"--role", "Creator",
|
||||||
|
"--herdr-server", "test-isolated-srv",
|
||||||
|
"--dry-run"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert res2.returncode == 0
|
||||||
|
assert "[dry-run] would spawn:" in res2.stdout
|
||||||
|
assert "herdr_session=test-isolated-srv" in res2.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_comp_create_herdr_session_default_preserved(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
"""Verify --herdr-session default is honored and not overwritten by workspace slug."""
|
||||||
|
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "claude",
|
||||||
|
"--role", "Creator",
|
||||||
|
"--session", "custom-proj-default-claude",
|
||||||
|
"--herdr-session", "default"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
|
||||||
|
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||||
|
import yaml
|
||||||
|
with open(yaml_path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
sessions = data.get("herdr_sessions", [])
|
||||||
|
assert len(sessions) == 1
|
||||||
|
s = sessions[0]
|
||||||
|
assert s["name"] == "custom-proj-default-claude"
|
||||||
|
assert s["herdr_session"] == "default"
|
||||||
|
assert s["herdr_server"] == "default"
|
||||||
|
assert "HERDR_SESSION_NAME=default" in s["start_command"]
|
||||||
|
assert "HERDR_SESSION_NAME=default" in s["attach_command"]
|
||||||
|
assert "HERDR_SESSION_NAME=default" in s["kill_command"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_comp_create_herdr_session_yaml_propagation(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
"""Verify HERDR_SESSION_NAME propagation into start_command / herdr_session YAML field."""
|
||||||
|
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "claude",
|
||||||
|
"--role", "Creator",
|
||||||
|
"--session", "custom-proj-creator-claude",
|
||||||
|
"--herdr-session", "isolated-suite-01"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
|
||||||
|
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||||
|
import yaml
|
||||||
|
with open(yaml_path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
sessions = data.get("herdr_sessions", [])
|
||||||
|
assert len(sessions) == 1
|
||||||
|
s = sessions[0]
|
||||||
|
assert s["name"] == "custom-proj-creator-claude"
|
||||||
|
assert s["herdr_session"] == "isolated-suite-01"
|
||||||
|
assert s["herdr_server"] == "isolated-suite-01"
|
||||||
|
assert "HERDR_SESSION_NAME=isolated-suite-01" in s["start_command"]
|
||||||
|
assert "HERDR_SESSION_NAME=isolated-suite-01" in s["attach_command"]
|
||||||
|
assert "HERDR_SESSION_NAME=isolated-suite-01" in s["kill_command"]
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# FEATURE 2: Resume Session (5 Test Cases)
|
# FEATURE 2: Resume Session (6 Test Cases)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
def test_comp_resume_config_restore(mam_sandbox):
|
def test_comp_resume_config_restore(mam_sandbox):
|
||||||
@@ -293,6 +407,51 @@ d['herdr_sessions'] = [{
|
|||||||
assert res.stdout.strip() == "scanned-uuid"
|
assert res.stdout.strip() == "scanned-uuid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_comp_resume_herdr_session_propagation(mam_sandbox, mock_herdr, mock_agents):
|
||||||
|
"""Verify that resume_session.sh with --herdr-session updates existing row's herdr_session."""
|
||||||
|
conv_id = "11111111-2222-3333-4444-555555555555"
|
||||||
|
key = str(mam_sandbox).replace('/', '-').replace('_', '-')
|
||||||
|
proj_dir = mam_sandbox / ".claude" / "projects" / key
|
||||||
|
proj_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(proj_dir / f"{conv_id}.jsonl").write_text(f'{{"sessionId": "{conv_id}"}}')
|
||||||
|
|
||||||
|
# Seed a stopped session with OLD herdr_session
|
||||||
|
mutation = f"""
|
||||||
|
d['herdr_sessions'] = [{{
|
||||||
|
'name': 'test-proj-creator-claude',
|
||||||
|
'status': 'stopped',
|
||||||
|
'herdr_session': 'OLD-HERDR-SESSION',
|
||||||
|
'herdr_server': 'OLD-HERDR-SESSION',
|
||||||
|
'claude_session_id_own': '{conv_id}',
|
||||||
|
'pane': {{'cwd': '{str(mam_sandbox)}', 'cmd': 'claude'}}
|
||||||
|
}}]
|
||||||
|
"""
|
||||||
|
run_mutation(mam_sandbox, mutation)
|
||||||
|
|
||||||
|
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-resume" / "scripts" / "resume_session.sh"
|
||||||
|
res = subprocess.run([
|
||||||
|
"bash", str(script),
|
||||||
|
"--workspace", str(mam_sandbox),
|
||||||
|
"--agent", "claude",
|
||||||
|
"--session", "test-proj-creator-claude",
|
||||||
|
"--herdr-session", "NEW-HERDR-SESSION"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
assert res.returncode == 0, res.stderr
|
||||||
|
|
||||||
|
yaml_path = mam_sandbox / ".mam" / "agent-sessions.yaml"
|
||||||
|
import yaml
|
||||||
|
with open(yaml_path) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
sessions = data.get("herdr_sessions", [])
|
||||||
|
assert len(sessions) == 1
|
||||||
|
s = sessions[0]
|
||||||
|
assert s["status"] == "running"
|
||||||
|
assert s["herdr_session"] == "NEW-HERDR-SESSION"
|
||||||
|
assert s["herdr_server"] == "NEW-HERDR-SESSION"
|
||||||
|
assert "HERDR_SESSION_NAME=NEW-HERDR-SESSION" in s["attach_command"]
|
||||||
|
assert "HERDR_SESSION_NAME=NEW-HERDR-SESSION" in s["kill_command"]
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# FEATURE 3: Stop Session (7 Test Cases)
|
# FEATURE 3: Stop Session (7 Test Cases)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -780,7 +939,8 @@ def test_comp_stop_usage_matches_parser(mam_sandbox):
|
|||||||
for flag, args in (("--reason", ["--reason", "x"]),
|
for flag, args in (("--reason", ["--reason", "x"]),
|
||||||
("--purge-conversation", ["--purge-conversation"]),
|
("--purge-conversation", ["--purge-conversation"]),
|
||||||
("--yes", ["--yes"]),
|
("--yes", ["--yes"]),
|
||||||
("--agent", ["--agent", "hermes"])):
|
("--agent", ["--agent", "hermes"]),
|
||||||
|
("--herdr-session", ["--herdr-session", "isolated-sess"])):
|
||||||
r = subprocess.run(["bash", str(script), "--session", VALID] + args,
|
r = subprocess.run(["bash", str(script), "--session", VALID] + args,
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
assert "unknown arg" not in r.stderr, f"usage() advertises {flag} but parser rejects it: {r.stderr}"
|
assert "unknown arg" not in r.stderr, f"usage() advertises {flag} but parser rejects it: {r.stderr}"
|
||||||
|
|||||||
Reference in New Issue
Block a user