feat(delegate-job): support role aliases mapping and run delegate job safely using isolated temp copy with signal cleanups
This commit is contained in:
@@ -55,6 +55,7 @@ multi-agent-mux-delegate-job <command> [options]
|
||||
[--registry-dir <dir>] [--dry-run] [--role <role_name>]
|
||||
[--type <direct|loop|discuss>] [--reviewer <reviewer_agent>]
|
||||
[--reviewer-session <reviewer_session>] [--max-iterations <count>]
|
||||
[--counterpart-role <role_name>] [--strict-role-check]
|
||||
# The skill is tmux-interactive only; --mode print was removed.
|
||||
status --job <id> [--registry-dir <dir>]
|
||||
list [--registry-dir <dir>]
|
||||
@@ -69,6 +70,11 @@ AGENT="claude-code"; PROMPT=""; WORKDIR="$(pwd)"; AGENT_SESSION="tmux:claude"
|
||||
TIMEOUT=3600; IDLE_TIMEOUT=120; VALIDATE=""; DRY_RUN=0
|
||||
JOB_ID=""; REGISTRY_DIR="$REGISTRY_DIR_DEFAULT"; DELEGATE_ROLE="Worker"
|
||||
TYPE="direct"; REVIEWER="hermes"; REVIEWER_SESSION="tmux:hermes"; MAX_ITERATIONS=5
|
||||
DEFAULT_COUNTERPART_ROLE="Reviewer"
|
||||
COUNTERPART_ROLE="$DEFAULT_COUNTERPART_ROLE"
|
||||
STRICT_ROLE_CHECK=0
|
||||
ROLE_ALIASES_JSON='{"worker": ["worker", "creator"], "planner": ["planner"], "reviewer": ["reviewer"]}'
|
||||
COUNTERPART_ROLE_EXPLICIT=0
|
||||
|
||||
parse_opts() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -88,6 +94,8 @@ parse_opts() {
|
||||
--reviewer) REVIEWER="$2"; shift 2;;
|
||||
--reviewer-session) REVIEWER_SESSION="$2"; shift 2;;
|
||||
--max-iterations) MAX_ITERATIONS="$2"; shift 2;;
|
||||
--counterpart-role) COUNTERPART_ROLE="$2"; COUNTERPART_ROLE_EXPLICIT=1; shift 2;;
|
||||
--strict-role-check) STRICT_ROLE_CHECK=1; shift;;
|
||||
*) echo "unknown option: $1" >&2; usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
@@ -197,7 +205,8 @@ EOF
|
||||
local iteration=1
|
||||
local current_prompt="$PROMPT"
|
||||
local current_session="$AGENT_SESSION"
|
||||
local current_role="worker"
|
||||
local _phase="worker"
|
||||
local display_role="$DELEGATE_ROLE"
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
echo "[dry-run] orchestrator loop would start for job: $JOB_ID type: $TYPE"
|
||||
@@ -209,7 +218,7 @@ EOF
|
||||
|
||||
while true; do
|
||||
echo "=================================================="
|
||||
echo "Iteration $iteration - Role: $current_role"
|
||||
echo "Iteration $iteration - Role: $display_role"
|
||||
echo "Session: $current_session"
|
||||
echo "=================================================="
|
||||
|
||||
@@ -223,7 +232,7 @@ EOF
|
||||
|
||||
- **Job ID**: $JOB_ID
|
||||
- **Target Agent/Session**: $current_session
|
||||
- **Role**: $current_role
|
||||
- **Role**: $display_role
|
||||
- **Iteration**: $iteration
|
||||
- **Output Report Path**: .mam/jobs/$JOB_ID/${clean_session}-reports/report-final.md
|
||||
|
||||
@@ -239,11 +248,11 @@ EOF
|
||||
--agent-session "$current_session" \
|
||||
--prompt "$current_prompt" \
|
||||
--iteration "$iteration" \
|
||||
--role "$current_role" \
|
||||
--role "$display_role" \
|
||||
--status "pending"
|
||||
|
||||
# Start subscriber
|
||||
local logf="$REGISTRY_DIR/${JOB_ID}.iter_${iteration}_${current_role}.subscriber.out"
|
||||
local logf="$REGISTRY_DIR/${JOB_ID}.iter_${iteration}_${display_role}.subscriber.out"
|
||||
"$PY" "$SCRIPT_DIR/scripts/job_subscriber.py" --registry-dir "$REGISTRY_DIR" \
|
||||
--job "$JOB_ID" --timeout "$TIMEOUT" --idle-timeout "$IDLE_TIMEOUT" \
|
||||
>"$logf" 2>&1 &
|
||||
@@ -272,7 +281,12 @@ EOF
|
||||
local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths for iteration $iteration are documented in the task brief file at: .mam/jobs/$JOB_ID/brief.md. Please READ and follow .mam/jobs/$JOB_ID/brief.md to complete your work. Commands: start='$pub --event started', success='$pub --event completed --detail <summary>', error='$pub --event error --detail <reason>'."
|
||||
|
||||
# Trigger agent
|
||||
run_agent "$JOB_ID" "$instructions" "$current_session"
|
||||
local force_warn_only=0
|
||||
if [[ "$_phase" == "reviewer" && "$COUNTERPART_ROLE_EXPLICIT" -eq 1 \
|
||||
&& "${COUNTERPART_ROLE,,}" != "${DEFAULT_COUNTERPART_ROLE,,}" ]]; then
|
||||
force_warn_only=1
|
||||
fi
|
||||
run_agent "$JOB_ID" "$instructions" "$current_session" "$force_warn_only"
|
||||
|
||||
# Wait for subscriber
|
||||
local sub_rc=0
|
||||
@@ -289,21 +303,22 @@ EOF
|
||||
job_status="timeout"
|
||||
fi
|
||||
|
||||
echo "Job role $current_role finished with status: $job_status"
|
||||
echo "Job role $display_role finished with status: $job_status"
|
||||
|
||||
# Retrieve feedback from the last event
|
||||
local feedback
|
||||
feedback="$("$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" get-feedback --job "$JOB_ID")"
|
||||
echo "Feedback/Detail: $feedback"
|
||||
|
||||
if [[ "$current_role" == "worker" ]]; then
|
||||
if [[ "$_phase" == "worker" ]]; then
|
||||
if [[ "$job_status" != "completed" ]]; then
|
||||
echo "Worker did not complete successfully (status: $job_status). Terminating workflow."
|
||||
break
|
||||
fi
|
||||
|
||||
# Worker completed successfully, now switch to reviewer
|
||||
current_role="reviewer"
|
||||
_phase="reviewer"
|
||||
display_role="$COUNTERPART_ROLE"
|
||||
current_session="$REVIEWER_SESSION"
|
||||
# Build reviewer prompt based on type
|
||||
if [[ "$TYPE" == "loop" ]]; then
|
||||
@@ -342,7 +357,8 @@ EOF
|
||||
fi
|
||||
|
||||
iteration=$((iteration + 1))
|
||||
current_role="worker"
|
||||
_phase="worker"
|
||||
display_role="$DELEGATE_ROLE"
|
||||
current_session="$AGENT_SESSION"
|
||||
current_prompt="The reviewer provided the following feedback for job $JOB_ID: $feedback. Please modify the code/artifacts to address these comments. CRITICAL: As the Developer Team Leader, you must thoroughly review the suggested modifications, verify their validity, adopt/implement them if valid, and if you judge any recommendation to be invalid, do NOT implement it but instead explain your reasons clearly in your response and send it back to the reviewer (수정안을 최대한 꼼꼼히 검토하여 타당성을 검증하고, 타당하다면 수렴하여 수정을 진행하되, 타당하지 않다고 판단되는 부분이 있다면 그 이유를 명확히 밝혀 리뷰어에게 전달하십시오)."
|
||||
fi
|
||||
@@ -367,7 +383,7 @@ EOF
|
||||
}
|
||||
|
||||
run_agent() {
|
||||
local job_id="$1"; local instructions="$2"; local target_session="${3:-$AGENT_SESSION}"
|
||||
local job_id="$1"; local instructions="$2"; local target_session="${3:-$AGENT_SESSION}"; local force_warn_only="${4:-0}"
|
||||
# The skill is INTERACTIVE-ONLY. We never invoke `claude -p` or any other
|
||||
# one-shot print mode, because:
|
||||
# - claude -p exits the moment stdin is drained, so there's nothing to
|
||||
@@ -408,6 +424,53 @@ run_agent() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check role suitability
|
||||
source "$SCRIPT_DIR/../lib.sh"
|
||||
local sess_role job_role
|
||||
sess_role=$(SESS_NAME="$sess" MAM_STATE_JSON="$(load_state_json)" "$PY" -c "
|
||||
import os, json
|
||||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||||
name = os.environ.get('SESS_NAME')
|
||||
for s in d.get('tmux_sessions', []):
|
||||
if s.get('name') == name:
|
||||
print(s.get('role', ''))
|
||||
break
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
job_role=$("$PY" -c "
|
||||
import json
|
||||
try:
|
||||
with open('$REGISTRY_DIR/$job_id.json') as f:
|
||||
print(json.load(f).get('role', ''))
|
||||
except Exception:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$job_role" && -n "$sess_role" ]]; then
|
||||
local check_result
|
||||
check_result=$(JOB_ROLE="$job_role" SESS_ROLE="$sess_role" ROLE_ALIASES_JSON="$ROLE_ALIASES_JSON" "$PY" -c "
|
||||
import os, json
|
||||
job = os.environ.get('JOB_ROLE', '').lower()
|
||||
sess = os.environ.get('SESS_ROLE', '').lower()
|
||||
aliases = json.loads(os.environ.get('ROLE_ALIASES_JSON', '{}'))
|
||||
candidates = aliases.get(job, [job])
|
||||
if any(c in sess for c in candidates):
|
||||
print('OK')
|
||||
else:
|
||||
print('MISMATCH')
|
||||
" 2>/dev/null || echo "OK")
|
||||
|
||||
if [[ "$check_result" == "MISMATCH" ]]; then
|
||||
local mismatch_msg="Target session '$sess' has role '$sess_role' which does not match job role '$job_role'."
|
||||
if [[ "$STRICT_ROLE_CHECK" -eq 1 && "$force_warn_only" -ne 1 ]]; then
|
||||
echo "ERROR: role suitability mismatch. $mismatch_msg" >&2
|
||||
return 1
|
||||
else
|
||||
echo "WARNING: $mismatch_msg" >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Before launching the agent, set up error trap to publish error event
|
||||
if [ -n "${job_id:-}" ] && [ -n "${PY:-}" ]; then
|
||||
pub_script="$SCRIPT_DIR/scripts/publish_event.py"
|
||||
@@ -415,7 +478,6 @@ run_agent() {
|
||||
fi
|
||||
|
||||
echo "살아있는 에이전트 세션 '$sess'에 작업을 위임합니다..."
|
||||
source "$SCRIPT_DIR/../lib.sh"
|
||||
if ! send_keys_safe "$sess" "$instructions" "$job_id"; then
|
||||
echo "ERROR: 프롬프트 주입 실패 — 세션 '$sess' (프롬프트 잠금 의심)" >&2
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user