feat(delegate-job): support role aliases mapping and run delegate job safely using isolated temp copy with signal cleanups

This commit is contained in:
2026-07-17 17:07:30 +09:00
parent 5a6cb91fb0
commit b45649de76
4 changed files with 196 additions and 21 deletions
+1 -1
View File
@@ -1192,7 +1192,7 @@ send_keys_safe() {
local cur_content
cur_content=$(_pane_capture "$sess")
# Hardened Submission Checks
if printf '%s\n' "$cur_content" | grep -Eq "● |Twisting|Thinking"; then
if printf '%s\n' "$cur_content" | grep -Eq "● |✽ |[A-Za-z]+ing…|[A-Za-z]+ing\.\.\.|esc to interrupt"; then
return 0
fi
if [ "$was_popup" = "0" ] && ! _pane_tail "$sess" 3 | grep -Fq "$marker" && [ "$cur_content" != "$pre_submit" ]; then
@@ -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
@@ -67,6 +67,18 @@ if [ -z "$TARGET_AGENT" ] || [ -z "$TASK" ]; then
echo "ERROR: --target-agent and --task are mandatory fields."
usage
fi
delegate_job_safe() {
local orig_script="$REPO_ROOT/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job"
local tmp_script
tmp_script="${orig_script}.${RANDOM}_$$.tmp"
cp "$orig_script" "$tmp_script"
trap 'rm -f "$tmp_script"' EXIT INT TERM HUP
local rc=0
bash "$tmp_script" "$@" || rc=$?
rm -f "$tmp_script"
trap - EXIT INT TERM HUP
return $rc
}
log_info() {
echo -e "\033[1;34m[*]\033[0m $1"
@@ -221,7 +233,7 @@ if [ "$PLAN_MODE" = true ]; then
# Step 1.1: Request initial plan from Planner
log_info "Requesting initial implementation plan from Planner..."
PLAN_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
PLAN_JOB_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$PLANNER_SESSION" \
--agent "$(resolve_agent_type "$PLANNER_SESSION")" \
--type "direct" \
@@ -255,7 +267,7 @@ if [ "$PLAN_MODE" = true ]; then
log_info "Discussion Turn $turn/$PLAN_TALK_TURNS: Creator challenging the plan..."
# Creator critique job
DEBATE_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
DEBATE_JOB_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$TARGET_AGENT" \
--agent "$(resolve_agent_type "$TARGET_AGENT")" \
--type "direct" \
@@ -283,7 +295,7 @@ if [ "$PLAN_MODE" = true ]; then
CRITIQUE_TEXT=$(cat "$CRITIQUE_FILE")
log_info "Planner refining plan with Creator's feedback..."
REFINE_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
REFINE_JOB_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$PLANNER_SESSION" \
--agent "$(resolve_agent_type "$PLANNER_SESSION")" \
--type "direct" \
@@ -339,7 +351,7 @@ if [ -n "$CURRENT_PLAN" ]; then
EXECUTION_PROMPT="이미 수립된 다음 계획서에 입각하여 작업자의 판단하에 코드를 구현하고 작업 목표를 완성해주세요. 계획서:\n$CURRENT_PLAN\n\n작업 목표: $TASK"
fi
EXEC_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
EXEC_JOB_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$TARGET_AGENT" \
--agent "$(resolve_agent_type "$TARGET_AGENT")" \
--type "direct" \
@@ -383,7 +395,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
if [ "${#REVIEWERS[@]}" -eq 0 ]; then
log_warn "No reviewers specified. Conducting Creator Self-Review..."
SELF_REV_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
SELF_REV_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$TARGET_AGENT" \
--agent "$(resolve_agent_type "$TARGET_AGENT")" \
--type "direct" \
@@ -427,7 +439,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
CHANGES_DIFF=$(git diff 2>/dev/null || echo "No git diff available")
fi
REV_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
REV_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$rev" \
--agent "$(resolve_agent_type "$rev")" \
--type "direct" \
@@ -498,7 +510,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
if [ "$PLAN_MODE" = true ] && [ "$COMPLEX_FIX" = true ]; then
log_warn "Feedback involves complex code modifications. Diverting to Planner to revise plan..."
REFINE_PLAN_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
REFINE_PLAN_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$PLANNER_SESSION" \
--agent "$(resolve_agent_type "$PLANNER_SESSION")" \
--type "direct" \
@@ -527,7 +539,7 @@ while [ "$loop_count" -le "$MAX_LOOP" ]; do
fi
# Creator execution corrective job
CORRECT_JOB_OUTPUT=$(bash .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job submit \
CORRECT_JOB_OUTPUT=$(delegate_job_safe submit \
--agent-session "tmux:$TARGET_AGENT" \
--agent "$(resolve_agent_type "$TARGET_AGENT")" \
--type "direct" \