fix: attach_command from one builder; no kill/start_command; stop closes a row-less session (Issue #71)

- attach_command is `herdr --session <server> agent attach <herdr name>`
  (`default` written too), built by lib_py.agents.sanitize.attach_command;
  create, resume and reconcile all use it. kill_command and start_command
  are no longer written, and status --json drops start_command.
- The two raw kill-session hints (create exit 43, id_order's orphan) print a
  stop-skill command with --herdr-session instead.
- stop_session.sh closes a session that has no registry row, herdr only and
  without --agent, when herdr runs it inside this workspace; another
  workspace's session is refused (exit 5), a missing one is exit 1, one that
  survives kill-session is exit 6. The registry is not changed.
- Docs: only the SKILL.md lines these changes made wrong; the skill block
  pins follow. Tests that asserted the removed fields keep their intent
  (issue50: create's own row; issue21: no control-root path in
  attach_command; tier2: the new form and the absent fields). S11 pins the
  shim to 4243da5, which changed it on purpose.

Refs #71

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-25 15:16:53 +00:00
1 parent 9c21fa4e4f
commit e4c7c8fe9b
19 files changed
+605 -109

No files matched your search

@@ -28,9 +28,12 @@
# 구분은 --purge-conversation 유무로 대체되었습니다.
#
# Exit codes:
# 0 = success (or already-stopped no-op) | 1 = YAML not found / not registered
# 0 = success (or already-stopped no-op; or a session with no row closed)
# 1 = YAML not found / not registered and not on the herdr server
# 2 = invalid args | 3 = interactive confirmation required (--yes 누락)
# 4 = purge aborted (herdr session or an agent process holding the conversation survived the kill chain)
# 5 = no row, and herdr runs the session outside this workspace (refused, nothing changed)
# 6 = no row, and its herdr session survived kill-session (registry unchanged)
set -euo pipefail
# shellcheck disable=SC1091
@@ -110,16 +113,50 @@ else
export HERDR_SESSION_NAME
fi
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
# 종전과 동일하게 exit 2 (헤더 :27-30 의 종료 코드 계약 유지).
if [ -z "$AGENT" ]; then
AGENT="$(resolve_agent_type_from_registry "$SESSION_NAME")" || AGENT=""
[ -n "$AGENT" ] || {
echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2
exit 2
}
fi
# Issue #71: a session with no registry row (its create/resume died before
# registering it) is closed here - herdr only, the registry is left alone. It
# has no agent kind, so --agent is not needed. Only a session herdr runs
# inside this workspace is closed; anything else changes nothing.
stop_rowless() {
if [ "$PURGE" = "1" ]; then
echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML; --purge-conversation needs its row. Nothing changed." >&2
exit 1
fi
local info cwd
info=$(herdr agent get "$SESSION_NAME" 2>/dev/null) || info=""
if [ -z "$info" ]; then
echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML, and herdr server '${HERDR_SESSION_NAME:-default}' has no agent of that name. Nothing changed." >&2
exit 1
fi
cwd=$(printf '%s' "$info" | python3 -c '
import sys, json
raw = sys.stdin.read()
try:
print(json.loads(raw[raw.index("{"):]).get("result", {}).get("agent", {}).get("cwd", "") or "")
except Exception:
print("")
')
if ! PANE_CWD="$cwd" python3 -c '
import os, sys
cwd, root = os.environ.get("PANE_CWD", ""), os.environ.get("WORKSPACE_ROOT", "")
if not cwd or not root:
sys.exit(1)
cwd, root = os.path.realpath(cwd), os.path.realpath(root)
sys.exit(0 if cwd == root or cwd.startswith(root + os.sep) else 1)
'; then
echo "ERROR: refusing to stop '$SESSION_NAME': it is not in $AGENT_SESSIONS_YAML, and herdr runs it in '${cwd:-<unknown>}', outside this workspace ($WORKSPACE_ROOT). Nothing changed." >&2
exit 5
fi
echo "session '$SESSION_NAME' has no registry row: closing its herdr session only (registry unchanged)"
herdr kill-session -t "$SESSION_NAME" 2>/dev/null || true
_wait_session_gone "$SESSION_NAME" 8 || true
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "ERROR: herdr session '$SESSION_NAME' is still running after kill-session (registry unchanged)" >&2
exit 6
fi
echo "closed herdr session '$SESSION_NAME' on server '${HERDR_SESSION_NAME:-default}' (no registry row)"
exit 0
}
# 세션이 YAML 에 있는지 + 해당 row 의 워크스페이스 cwd 및 delegate_job_id 추출.
# JSON 으로 emit — cwd 에 '|' 가 들어가도 안전 (review item 7; 기존 cwd|jid 파서 대체).
@@ -136,10 +173,18 @@ for s in d.get('herdr_sessions', []):
'delegate_job_logs_dir': cp.get('delegate_job_logs_dir', '') or ''}))
sys.exit(0)
sys.exit(7)
") || {
echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML" >&2
exit 1
}
") || stop_rowless
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
# 종전과 동일하게 exit 2 (헤더 :27-30 의 종료 코드 계약 유지).
if [ -z "$AGENT" ]; then
AGENT="$(resolve_agent_type_from_registry "$SESSION_NAME")" || AGENT=""
[ -n "$AGENT" ] || {
echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2
exit 2
}
fi
TARGET_CWD=$(printf '%s' "$MAPPED_DATA" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("cwd",""))')
DELEGATE_JOB_ID=$(printf '%s' "$MAPPED_DATA" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("job_id",""))')