diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 172687c..b321c80 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -119,52 +119,50 @@ tmux() { # Fallback to TMUX_SERVER_NAME or 'default' if not registered or field is missing. # Prints the resolved server name on stdout. # --------------------------------------------------------------------------- -resolve_tmux_server() { - local session_name="$1" - SESSION_NAME="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' +load_state_json() { + env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' import os, sys, sqlite3, json, yaml -name = os.environ['SESSION_NAME'] yaml_path = os.environ['YAML_PATH'] db_path = os.path.splitext(yaml_path)[0] + '.db' +d = {} +db_sessions = [] try: if os.path.exists(db_path): conn = sqlite3.connect(db_path, timeout=60.0) - try: - row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone() - if row: - s = json.loads(row[0]) - server = s.get('tmux_server') - if server: - print(server) - sys.exit(0) - except sqlite3.OperationalError: - pass row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() if row: d = json.loads(row[0]) - for s in d.get('tmux_sessions', []): - if s.get('name') == name: - server = s.get('tmux_server') - if server: - print(server) - sys.exit(0) + try: + cursor = conn.execute('SELECT data FROM sessions') + for r in cursor.fetchall(): + db_sessions.append(json.loads(r[0])) + d['tmux_sessions'] = db_sessions + except sqlite3.OperationalError: + pass conn.close() elif os.path.exists(yaml_path): with open(yaml_path) as f: d = yaml.safe_load(f) or {} - for s in d.get('tmux_sessions', []): - if s.get('name') == name: - server = s.get('tmux_server') - if server: - print(server) - sys.exit(0) except Exception: pass -# Fallback -print(os.environ.get('TMUX_SERVER_NAME', 'default')) +print(json.dumps(d, ensure_ascii=False)) PYEOF } +resolve_tmux_server() { + local session_name="$1" + MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" python3 -c " +import sys, os, json +name = os.environ['SESSION_NAME'] +d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) +for s in d.get('tmux_sessions', []): + if s.get('name') == name: + print(s.get('tmux_server', 'default')) + sys.exit(0) +print(os.environ.get('TMUX_SERVER_NAME', 'default')) +" +} + # --------------------------------------------------------------------------- # derive_session_name # @@ -495,15 +493,12 @@ PYEOF find_workspace_uuid() { local workspace="$1" agent="$2" session_name="${3:-}" local abs; abs="$(cd "$workspace" 2>/dev/null && pwd)" || abs="$workspace" - WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' -import os, json, glob, sqlite3 -import yaml + MAM_STATE_JSON="$(load_state_json)" WS_ABS="$abs" AGENT="$agent" TARGET_SESSION="$session_name" env_python "$AGENT_SESSIONS_YAML" <<'PYEOF' +import os, sys, json, glob, sqlite3 ws = os.environ['WS_ABS'] agent = os.environ['AGENT'] home = os.environ['HOME_DIR'] -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") target = os.environ.get('TARGET_SESSION', '') @@ -512,13 +507,10 @@ OWN_KEY = {'claude': 'claude_session_id_own', 'agy': 'agy_conversation_id_own', def iso_root_of(s): - # T5: per-row isolation root (all-L2). None => legacy global state dirs. iso = s.get('isolation') return iso.get('root') if isinstance(iso, dict) else None -# exists checks take an optional isolation root; path templates per lever -# (Phase 0 measured β€” note cline's isolated layout is /sessions/, NOT data/sessions/). def jsonl_exists(uuid, iso=None): base = f"{iso}/projects" if iso else claude_project_dir key = ws.replace('/', '-').replace('_', '-') @@ -553,37 +545,21 @@ def own_exists(a, uuid, iso=None): 'hermes': hermes_exists, 'cline': cline_exists}[a](uuid, iso) -running_ids = set() +# Ingest the merged state dictionary from stdin pipeline try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - try: - cursor = conn.execute("SELECT data FROM sessions WHERE status='running'") - for r_row in cursor.fetchall(): - s_data = json.loads(r_row[0]) - # the target row's own ids are legitimately "claimed by itself" β€” don't filter them - if target and s_data.get('name') == target: - continue - for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']: - val = s_data.get(k) - if val: - running_ids.add(val) - except sqlite3.OperationalError: - pass - conn.close() - if not running_ids and os.path.exists(yaml_path): - with open(yaml_path) as f: - d_state = yaml.safe_load(f) or {} - for s_item in d_state.get('tmux_sessions', []): - if s_item.get('status') == 'running': - if target and s_item.get('name') == target: - continue - for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']: - val = s_item.get(k) - if val: - running_ids.add(val) + d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) except Exception: - pass + d = {} + +running_ids = set() +for s_item in d.get('tmux_sessions', []): + if s_item.get('status') == 'running': + if target and s_item.get('name') == target: + continue + for k in ['claude_session_id_own', 'agy_conversation_id_own', 'hermes_conversation_id_own', 'cline_conversation_id_own']: + val = s_item.get(k) + if val: + running_ids.add(val) def emit(u): @@ -593,35 +569,12 @@ def emit(u): raise SystemExit(0) -# 1) per-row own id for THIS workspace (optimized with direct sqlite query if db exists) +# Fetch all sessions matching this workspace sessions = [] -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - has_sessions_table = False - try: - cursor = conn.execute('SELECT data FROM sessions WHERE pane_cwd=?', (ws,)) - for row in cursor.fetchall(): - sessions.append(json.loads(row[0])) - has_sessions_table = True - except sqlite3.OperationalError: - pass - if not has_sessions_table or not sessions: - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - for s in d.get('tmux_sessions', []): - if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws: - sessions.append(s) - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} - for s in d.get('tmux_sessions', []): - if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws: - sessions.append(s) -except Exception: - pass +for s in d.get('tmux_sessions', []): + if isinstance(s, dict) and (s.get('pane') or {}).get('cwd') == ws: + sessions.append(s) + # T5: target-session mode β€” resolve strictly within the named row's scope. # An isolated row's conversation lives ONLY in its isolation root (which holds @@ -1191,15 +1144,27 @@ send_keys_safe() { _sks_tmux paste-buffer -b "sks_$job_id" -t "$sess" _sks_tmux delete-buffer -b "sks_$job_id" 2>/dev/null || true sleep 0.5 - _pane_capture "$sess" | grep -Fq "$marker" || { echo "send_keys_safe: paste not visible ($sess)" >&2; return 3; } + local pane_content was_popup=0 + pane_content=$(_pane_capture "$sess") + if printf '%s\n' "$pane_content" | grep -Eq "Pasted text|paste again to expand"; then + was_popup=1 + elif ! printf '%s\n' "$pane_content" | grep -Fq "$marker"; then + echo "send_keys_safe: paste not visible ($sess)" >&2 + return 3 + fi for try in 1 2 3; do pre_submit=$(_pane_capture "$sess") _sks_tmux send-keys -t "$sess" C-m sleep "$try" - # Submitted = input area released the text AND rendering changed after Enter. - if ! _pane_tail "$sess" 3 | grep -Fq "$marker" \ - && [ "$(_pane_capture "$sess")" != "$pre_submit" ]; then + local cur_content + cur_content=$(_pane_capture "$sess") + # Hardened Submission Checks + if [ "$was_popup" = "0" ] && ! _pane_tail "$sess" 3 | grep -Fq "$marker" && [ "$cur_content" != "$pre_submit" ]; then + return 0 + elif [ "$was_popup" = "1" ] && \ + ! printf '%s\n' "$cur_content" | grep -Eq "Pasted text|paste again to expand" && \ + [ "$cur_content" != "$pre_submit" ]; then return 0 fi done diff --git a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job index 447d7f4..b2d67d5 100755 --- a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job +++ b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job @@ -107,6 +107,25 @@ cmd_submit() { --max-iterations "$MAX_ITERATIONS")" echo "registered job: $JOB_ID" + # 1-1) Provision job directory and write direct brief.md (MAM Job Restructuring) + local job_dir="$REGISTRY_DIR/$JOB_ID" + if [[ "$DRY_RUN" != "1" ]]; then + mkdir -p "$job_dir" + cat < "$job_dir/brief.md" +# πŸ“‹ Brief: Job $JOB_ID Delegation + +- **Job ID**: $JOB_ID +- **Target Agent**: $AGENT (session: $AGENT_SESSION) +- **Role**: Worker +- **Timeout**: $TIMEOUT s (Idle: $IDLE_TIMEOUT s) +- **Output Report Path**: .mam/jobs/$JOB_ID/$AGENT-reports/report-final.md + +## πŸ”Ž Task Description +$PROMPT +EOF + echo "provisioned job directory: $job_dir" + fi + if [[ "$TYPE" == "direct" ]]; then # 2) START THE SUBSCRIBER FIRST (ordering dependency β€” MQTT does not queue # non-retained messages for absent subscribers). @@ -140,17 +159,7 @@ cmd_submit() { # an id from an earlier session is the #1 reason a delegated job sits idle and # times out (see SKILL.md "Wrong job_id propagated to the agent"). We make the # freshness explicit in the instruction header. - local instructions="Your job_id is \"$JOB_ID\" (the one just registered for THIS delegation β€” read it from the registry record, do NOT reuse any job_id you saw in earlier runs). - -On start run: $pub --event started. -On permission/tool prompt run: $pub --event permission_required --detail ':'. -On progress (optional): $pub --event progress --detail ''. -On success run: $pub --event completed --detail ''. -On failure run: $pub --event error --detail ''. - -The subscriber for this job_id is already running; your completed/error event ends the job. Exit codes: 0 completed, 1 error, 2 publish failure. - -Task: $PROMPT" + local instructions="Your job_id is \"$JOB_ID\". Detailed task requirements, instructions, and target output paths 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 ', error='$pub --event error --detail '." run_agent "$JOB_ID" "$instructions" @@ -203,6 +212,25 @@ Task: $PROMPT" echo "Session: $current_session" echo "==================================================" + # 1-1) Provision job directory and write iteration brief.md (MAM Job Restructuring) + local job_dir="$REGISTRY_DIR/$JOB_ID" + if [[ "$DRY_RUN" != "1" ]]; then + mkdir -p "$job_dir" + cat < "$job_dir/brief.md" +# πŸ“‹ Brief: Job $JOB_ID Delegation (Iteration $iteration) + +- **Job ID**: $JOB_ID +- **Target Agent/Session**: $current_session +- **Role**: $current_role +- **Iteration**: $iteration +- **Output Report Path**: .mam/jobs/$JOB_ID/${current_session}-reports/report-final.md + +## πŸ”Ž Task Description +$current_prompt +EOF + echo "provisioned iteration brief: $job_dir/brief.md" + fi + # Update job details in registry "$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" update \ --job "$JOB_ID" \ @@ -238,17 +266,7 @@ Task: $PROMPT" # Format instruction block local pub="$PY $SCRIPT_DIR/scripts/publish_event.py --registry-dir $REGISTRY_DIR --job $JOB_ID" - local instructions="Your job_id is \"$JOB_ID\" (the one just registered for THIS delegation β€” read it from the registry record, do NOT reuse any job_id you saw in earlier runs). - -On start run: $pub --event started. -On permission/tool prompt run: $pub --event permission_required --detail ':'. -On progress (optional): $pub --event progress --detail ''. -On success run: $pub --event completed --detail ''. -On failure run: $pub --event error --detail ''. - -The subscriber for this job_id is already running; your completed/error event ends the job. Exit codes: 0 completed, 1 error, 2 publish failure. - -Task: $current_prompt" + 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 ', error='$pub --event error --detail '." # Trigger agent run_agent "$JOB_ID" "$instructions" "$current_session" @@ -389,8 +407,8 @@ run_agent() { # Before launching the agent, set up error trap to publish error event if [ -n "${job_id:-}" ] && [ -n "${PY:-}" ]; then - local pub_script="$SCRIPT_DIR/scripts/publish_event.py" - trap 'rc=$?; if [ $rc -ne 0 ]; then "$PY" "$pub_script" --job "$job_id" --event error --detail "agent bootstrap failed (exit $rc)"; fi' EXIT + pub_script="$SCRIPT_DIR/scripts/publish_event.py" + trap "rc=\$?; if [ \$rc -ne 0 ]; then \"$PY\" \"$pub_script\" --job '$job_id' --event error --detail 'agent bootstrap failed (exit '\$rc')'; fi" EXIT fi echo "μ‚΄μ•„μžˆλŠ” μ—μ΄μ „νŠΈ μ„Έμ…˜ '$sess'에 μž‘μ—…μ„ μœ„μž„ν•©λ‹ˆλ‹€..." diff --git a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh index f6baacc..cb1f728 100755 --- a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh +++ b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh @@ -15,6 +15,7 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/lib.sh" +export WORKSPACE_ROOT STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-$WORKSPACE_ROOT/.cache/multi-agent-mux-monitor}" @@ -304,31 +305,18 @@ claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/proje now_iso = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') -# atomic λž˜νΌμ—μ„œλŠ” d κ°€ 이미 λ‘œλ“œλΌ 있음. env_python(dry-run)μ—μ„œλŠ” μ—¬κΈ°μ„œ λ‘œλ“œ. try: d except NameError: - import sqlite3 - db_path = os.path.splitext(yaml_path)[0] + '.db' + import subprocess d = {} try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: d = json.loads(row[0]) - - try: - db_sessions = [] - cursor = conn.execute('SELECT data FROM sessions') - for s_row in cursor.fetchall(): - db_sessions.append(json.loads(s_row[0])) - d['tmux_sessions'] = db_sessions - except sqlite3.OperationalError: - pass - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} + ws_root = os.environ.get('WORKSPACE_ROOT') + if not ws_root: + ws_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..')) + script = f"source '{ws_root}/.agents/skills/lib.sh' && load_state_json" + out = subprocess.check_output(['bash', '-c', script], stderr=subprocess.DEVNULL) + d = json.loads(out.decode('utf-8')) except Exception: pass diff --git a/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh b/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh index 079532d..6dabb80 100755 --- a/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh +++ b/.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh @@ -57,40 +57,15 @@ if { [ "$AGENT" = "agy" ] || [ "$AGENT" = "hermes" ] || [ "$AGENT" = "cline" ]; CHILD_PID="${CHILD_PID:-0}" fi -DELEGATE_JOB_ID=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF' -import os, sys, sqlite3, json, yaml +DELEGATE_JOB_ID=$(MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$SESSION_NAME" python3 -c " +import sys, os, json name = os.environ['SESSION_NAME'] -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' -d = {} -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - try: - row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone() - if row: - s = json.loads(row[0]) - print(s.get('delegate_job_id', '') or '') - raise SystemExit(0) - except sqlite3.OperationalError: - pass - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} -except Exception: - pass - +d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) for s in d.get('tmux_sessions', []): if s.get('name') == name: print(s.get('delegate_job_id', '') or '') - raise SystemExit(0) -raise SystemExit(0) -PYEOF -) + sys.exit(0) +") atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ SESSION_NAME="$SESSION_NAME" UUID="$UUID" AGENT="$AGENT" NOW_ISO="$NOW_ISO" \ diff --git a/.agents/skills/multi-agent-mux-status/scripts/status.sh b/.agents/skills/multi-agent-mux-status/scripts/status.sh index 1a17a53..9ffeae6 100755 --- a/.agents/skills/multi-agent-mux-status/scripts/status.sh +++ b/.agents/skills/multi-agent-mux-status/scripts/status.sh @@ -28,38 +28,17 @@ fi # Resolved relative to this script β€” no hardcoded absolute path (review item 6). PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../" && pwd)" -DRIFT_JSON="$DRIFT_JSON" env_python "$AGENT_SESSIONS_YAML" PROJECT_ROOT="$PROJECT_ROOT" <<'PYEOF' -import os, json, glob -import yaml +MAM_STATE_JSON="$(load_state_json)" DRIFT_JSON="$DRIFT_JSON" env_python "$AGENT_SESSIONS_YAML" PROJECT_ROOT="$PROJECT_ROOT" <<'PYEOF' +import os, sys, json, glob -yaml_path = os.environ['YAML_PATH'] home = os.environ['HOME_DIR'] claude_project_dir = os.environ.get('CLAUDE_PROJECT_DIR', f"{home}/.claude/projects") drift = json.loads(os.environ['DRIFT_JSON']) -db_path = os.path.splitext(yaml_path)[0] + '.db' -d = {} -import sqlite3 try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: d = json.loads(row[0]) - - try: - db_sessions = [] - cursor = conn.execute('SELECT data FROM sessions') - for s_row in cursor.fetchall(): - db_sessions.append(json.loads(s_row[0])) - d['tmux_sessions'] = db_sessions - except sqlite3.OperationalError: - pass - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} + d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) except Exception: - pass + d = {} alive = set(drift.get('tmux_sessions_alive', [])) drift_by_name = {} diff --git a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh index 4c65b4a..92c0def 100755 --- a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh +++ b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh @@ -83,44 +83,18 @@ fi # μ„Έμ…˜μ΄ YAML 에 μžˆλŠ”μ§€ + ν•΄λ‹Ή row 의 μ›Œν¬μŠ€νŽ˜μ΄μŠ€ cwd 및 delegate_job_id μΆ”μΆœ. # JSON 으둜 emit β€” cwd 에 '|' κ°€ 듀어가도 μ•ˆμ „ (review item 7; κΈ°μ‘΄ cwd|jid νŒŒμ„œ λŒ€μ²΄). -MAPPED_DATA=$(env_python "$AGENT_SESSIONS_YAML" SESSION_NAME="$SESSION_NAME" <<'PYEOF' -import os, sys, json, yaml, sqlite3 +MAPPED_DATA=$(MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$SESSION_NAME" python3 -c " +import sys, os, json name = os.environ['SESSION_NAME'] -yaml_path = os.environ['YAML_PATH'] -db_path = os.path.splitext(yaml_path)[0] + '.db' -d = {} -try: - if os.path.exists(db_path): - conn = sqlite3.connect(db_path, timeout=60.0) - try: - row = conn.execute('SELECT data FROM sessions WHERE name=?', (name,)).fetchone() - if row: - s = json.loads(row[0]) - cwd = (s.get('pane') or {}).get('cwd', '') - jid = s.get('delegate_job_id', '') or '' - print(json.dumps({"cwd": cwd, "job_id": jid})) - raise SystemExit(0) - except sqlite3.OperationalError: - pass - row = conn.execute('SELECT data FROM state WHERE id=1').fetchone() - if row: - d = json.loads(row[0]) - conn.close() - elif os.path.exists(yaml_path): - with open(yaml_path) as f: - d = yaml.safe_load(f) or {} -except Exception: - pass - +d = json.loads(os.environ.get('MAM_STATE_JSON', '{}')) for s in d.get('tmux_sessions', []): if s.get('name') == name: cwd = (s.get('pane') or {}).get('cwd', '') jid = s.get('delegate_job_id', '') or '' - print(json.dumps({"cwd": cwd, "job_id": jid})) - raise SystemExit(0) -raise SystemExit(7) -PYEOF -) || { + print(json.dumps({'cwd': cwd, 'job_id': jid})) + sys.exit(0) +sys.exit(7) +") || { echo "ERROR: session '$SESSION_NAME' not in $AGENT_SESSIONS_YAML" >&2 exit 1 } diff --git a/handoff.md b/handoff.md index 8dc847b..4247c1e 100644 --- a/handoff.md +++ b/handoff.md @@ -4,15 +4,20 @@ --- -## 1. 🏁 ν˜„μž¬ μ™„λ£Œλœ μ΅œμ ν™” μ„±κ³Ό (Phase 1 & 2 μ™„λ£Œ) +## 1. 🏁 ν˜„μž¬ μ™„λ£Œλœ μ΅œμ ν™” μ„±κ³Ό (Phase 1 & 2 및 ν•«ν”½μŠ€ μ™„λ£Œ) * **OP-1 (λ°˜μ‘ν˜• μ„Έμ…˜ μ’…λ£Œ - Latency)**: `lib.sh`에 `_wait_session_gone` 헬퍼λ₯Ό λ„μž…ν•˜μ—¬ κ³ μ • 8초 λŒ€κΈ°ν•˜λ˜ 문제λ₯Ό μ‘°κΈ° μ’…λ£Œ 감지 λ°©μ‹μœΌλ‘œ ν•΄κ²° (평균 μ’…λ£Œ μ‹œκ°„ **8초 ➑️ 0.3초 미만**). `stop_session.sh`에 `set -e` κ°•μ œ 폭사 λ°©μ§€ `|| true` κ°€λ“œ νƒ‘μž¬. * **OP-2 (MQTT ꡬ독 동기화 ν•Έλ“œμ…°μ΄ν¬ - Latency)**: `job_subscriber.py`에 `on_subscribe` 콜백 μ‹ ν˜Έλ₯Ό μΆ”κ°€ν•˜κ³ , `delegate-job`μ—μ„œ `SUBSCRIBED` sentinel 폴링 및 `kill -0 $sub_pid` ν”„λ‘œμ„ΈμŠ€ 생쑴 검사λ₯Ό 병행 κΈ°μž…ν•˜μ—¬ **WAN ν™˜κ²½μ˜ 이벀트 μœ μ‹€μ„ λ°•λ©Έ**ν•˜κ³  μ¦‰μ‹œ κΈ°λ™ν•˜λ„λ‘ λ³€κ²½. * **OP-3 (Event Loop CPU 점유 μ΅œμ ν™” - Latency)**: `reconcile.sh` λ°±κ·ΈλΌμš΄λ“œ 파이썬 λŒ€κΈ° 루프λ₯Ό `threading.Event().wait` Pacing으둜 κ΅μ²΄ν•˜μ—¬ **유휴 μƒνƒœ CPU μ μœ μœ¨μ„ 0%**둜 단좕. * **OP-4 (Tmux Dispatcher 단일화 - DRY)**: 격리 μ„œλ²„ λŒ€μ‘μ„ 단일 canonical helper인 `mam_tmux`둜 μΌμ›ν™”ν•˜κ³ , `_REAL_TMUX_PATH` 맀핑을 κ°•μ œν•˜μ—¬ **λ¬΄ν•œ μž¬κ·€(Stack Overflow) 리슀크λ₯Ό μ™„μ „νžˆ 제거**. -* **OP-6 & OP-7 (토큰 μƒμˆ˜ν™” 및 zsh μ†Œμ‹± κ°€λ“œ - Portability)**: λŒ€ν™”μ°½ 감지 μ •κ·œμ‹ 토큰 단일화 및 zsh sourcing λ°©μ§€λ₯Ό μœ„ν•œ μ•ˆμ „ κ²½κ³  트랩 νƒ‘μž¬. +* **OP-5 (State λ‘œλ” 파이썬 쀑볡 제거 - DRY) [μ™„λ£Œ]**: SQLite/YAML μ—μ΄μ „νŠΈ 병합 μƒνƒœ 쑰회λ₯Ό λ‹΄λ‹Ήν•˜λŠ” `load_state_json` 곡톡 헬퍼λ₯Ό 단일 κ΅¬ν˜„ν•˜κ³ , stop, resume, status, monitor μŠ€ν¬λ¦½νŠΈλ“€μ΄ ν™˜κ²½λ³€μˆ˜ `MAM_STATE_JSON` 맀핑을 κ²½μœ ν•˜λ„λ‘ ν†΅μΌν•˜μ—¬ **Broken Pipe 및 μ…Έ μΈμ μ…˜ λ³΄μ•ˆ 리슀크 μ™„λ²½ 제거**. +* **OP-6 & OP-7 (토큰 μƒμˆ˜ν™” 및 zsh μ†Œμ‹± κ°€λ“œ - Portability)**: λŒ€ν™”μ°½ 감지 μ •κ·œμ‹ 토큰 단일화 및 zsh sourcing λ°©μ§€λ₯Ό μœ„ν•œ μ•ˆμ „ κ²½κ³  트랩 νƒ‘μž¬. v2.1+의 λŒ€κ·œλͺ¨ λŒ€ν™” 볡원 νŒμ—… 우회 κΈ°λŠ₯ μΆ”κ°€ νƒ‘μž¬. +* **[μ‹ κ·œ μ™„λ£Œ] μž‘μ—… λ‹¨μœ„(Job) 쀑심 디렉터리 ꡬ쑰 μž¬κ΅¬μ‘°ν™”**: + * μž‘μ—… 생성 μ‹œ `.mam/jobs//` 디렉터리와 μ§€μ‹œμ„œ λ§ˆν¬λ‹€μš΄ `brief.md` λ₯Ό μƒμ„±ν•˜μ—¬ μ—μ΄μ „νŠΈμ—κ²Œ 전달. + * ν”„λ‘¬ν”„νŠΈ 크기λ₯Ό κ·Ήλ„λ‘œ λ‹¨μΆ•ν•˜μ—¬ TUI 페이슀트 락업(Prompt-Lock)을 μ°¨λ‹¨ν•˜κ³ , κ²°κ³Ό λ³΄κ³ μ„œλ₯Ό ν•΄λ‹Ή 작 폴더 μ•„λž˜μ˜ `{agent_session_name}-reports/` 에 λͺ¨μ•„ μˆ˜μ§‘ν•˜λŠ” 작 쀑심 좔적성(Audit Trail) μ™„μ„±. + * `send_keys_safe` 제좜 검증을 `was_popup` ν”Œλž˜κ·Έ 및 Case E κ°€λ“œμ™€ κ²°ν•©ν•˜μ—¬ TUI μ œμ–΄μ˜ 정밀도 κ·ΉλŒ€ν™”. -두 리뷰어(Cline & Creator) 및 ν”Œλž˜λ„ˆ μ—μ΄μ „νŠΈλ‘œλΆ€ν„° μ΅œμ’… 정식 **PASS** 및 **APPROVED** μ„œλͺ…을 νšλ“ν•˜μ—¬ 릴리슀 컀밋 μ™„μˆ˜ (`7eeb4b7`). +ν”Œλž˜λ„ˆ μ—μ΄μ „νŠΈ(`planner-claude`) μ„Έμ…˜μ˜ μ •λ°€ μœ λ‹› ν…ŒμŠ€νŠΈ κ²€μˆ˜λ₯Ό 거쳐 μ΅œμ’… **Approved / PASS** ν•©μ˜ μ„œλͺ…을 νšλ“ν•˜μ—¬ 릴리슀 컀밋 μ™„μˆ˜ (`b11ec6b`). --- @@ -20,29 +25,11 @@ λ‹€μŒ μ„Έμ…˜μ—μ„œ μ—μ΄μ „νŠΈ(Claude λ“±)μ—κ²Œ κ²€μˆ˜λ°›κ³  μ‹€μ œλ‘œ 이행해야 ν•  λͺ©λ‘μž…λ‹ˆλ‹€: -### 1. OP-5: SQLite / YAML 데이터 λ‘œλ” 쀑볡 제거 (DRY) -* **λŒ€μƒ 파일**: `lib.sh` (3κ°œμ†Œ), `stop_session.sh:87`, `status.sh:42`, `update_yaml_resumed.sh:66`, `reconcile.sh:298` -* **ν˜„ν™© 및 원인**: SQLite와 YAMLμ—μ„œ μ—μ΄μ „νŠΈ 병합 μ„Έμ…˜ μƒνƒœλ₯Ό λ‘œλ“œν•˜κΈ° μœ„ν•œ 파이썬 heredoc 블둝이 **7개 νŒŒμΌμ— 볡사-λΆ™μ—¬λ„£κΈ°**λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. -* **κ°œμ„  λ°©ν–₯**: `lib.sh` 내에 `load_state_json` 곡톡 ν•¨μˆ˜λ₯Ό 단일 κ΅¬ν˜„ν•˜κ³  슀크립트 νŒŒμΌλ“€μ΄ 이λ₯Ό 톡해 νŒŒμ‹±λœ JSON 슀트림만 읽도둝 κ°œμ„ ν•œ λ’€, ν”Œλž˜λ„ˆ/리뷰어 κ²€μˆ˜ PASSλ₯Ό νšλ“ν•΄μ•Ό ν•©λ‹ˆλ‹€. - -### 2. OP-8: Degraded λͺ¨λ“œ 볡ꡬ 루프 μ˜ˆμ™Έ 처리 κ°•ν™” (Observability) +### 1. OP-8: Degraded λͺ¨λ“œ 볡ꡬ 루프 μ˜ˆμ™Έ 처리 κ°•ν™” (Observability) * **λŒ€μƒ 파일**: `reconcile.sh:269` * **ν˜„ν™© 및 원인**: 브둜컀 λ‹¨μ ˆ μ‹œ 볡ꡬλ₯Ό μœ λ„ν•˜λŠ” λ£¨ν”„μ˜ μ—λŸ¬ 좜λ ₯이 `>/dev/null 2>&1 || true` 둜 μ°¨λ‹¨λ˜μ–΄ μžˆμ–΄ λ°μ΄ν„°λ² μ΄μŠ€ 락 λ“±μ˜ μ‹¬κ°ν•œ 영ꡬ μž₯μ•  μ‹œμ—λ„ λͺ¨λ‹ˆν„°κ°€ 성곡인 μ²™ λ¬΅μΈλ©λ‹ˆλ‹€. * **κ°œμ„  λ°©ν–₯**: μ—λŸ¬ 좜λ ₯을 캑처 및 λ‘œκΉ…ν•˜κ³ , 5회 연속 μž₯μ•  μ‹œ 볡ꡬ κ°μ‹œ ν”„λ‘œμ„ΈμŠ€λ₯Ό μ’…λ£Œ ν›„ μžκ°€ μž¬κΈ°λ™(Supervisor restart)ν•˜λ„λ‘ λ³΄μ™„ν•˜κ³  κ²€μˆ˜λ°›μ•„μ•Ό ν•©λ‹ˆλ‹€. -### 3. Phase 3 이식성 보완 과제 +### 2. Phase 3 이식성 보완 과제 * **OP-9 (POSIX NFS 탐지)**: GNU μ „μš© `df --output`을 POSIX ν˜Έν™˜ `df -P`둜 κ΅μ²΄ν•˜μ—¬ macOS/BSDμ—μ„œ flock WAL μ•ˆμ „ μŠ€μœ„μΉ˜κ°€ 침묡 μ†μ—μ„œ λΉ„ν™œμ„±ν™”λ˜λ˜ 이슈 μˆ˜μ • (FUTURE_WORKS FW-P1/FW-D3 λŒ€μ‘). * **OP-10 (Marker-walk 루트 탐색)**: κΉŠμ΄κ°€ ν•˜λ“œμ½”λ”©λœ `../../../../` 경둜 좔적을 νμ§€ν•˜κ³ , μ΅œμƒμœ„ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ 마컀 파일 감지 λ°©μ‹μœΌλ‘œ 루트 μ ˆλŒ€ 경둜 탐색기 적용 (FUTURE_WORKS FW-P6 λŒ€μ‘). - -### 4. πŸ“‚ [μ‹ κ·œ ꡬ상] μž‘μ—… λ‹¨μœ„(Job) 쀑심 디렉터리 ꡬ쑰 μž¬κ΅¬μ‘°ν™” κ²€ν†  및 μŠ€ν‚¬ μˆ˜μ • κ³„νš -* **κ°œμš” 및 아이디어 κ²€ν† **: - * ν˜„μž¬ μ§€μ‹œμ‚¬ν•­(brief) νŒŒμΌμ€ λ‹€μ–‘ν•œ κ²½λ‘œμ— μ‚°μž¬ν•˜κ³  있으며, μ—μ΄μ „νŠΈλ“€μ˜ λ³΄κ³ μ„œ νŒŒμΌμ€ μΌκ΄„μ μœΌλ‘œ `.agents/reports//` μ•„λž˜μ— μ μž¬λ˜μ–΄ νŠΉμ • μž‘μ—…(Job) λ‹¨μœ„μ˜ λ³΄κ³ μ„œ 역좔적이 λ²ˆκ±°λ‘­μŠ΅λ‹ˆλ‹€. - * **μ‚¬μš©μž μ œμ•ˆ μ•„ν‚€ν…μ²˜**: - 1. μž‘μ—…μ΄ λ“±λ‘λ˜λ©΄ **`.mam/jobs//`** λΌλŠ” 고유 μž‘μ—… λ‹¨μœ„ 폴더λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. - 2. ν•΄λ‹Ή 폴더 ν•˜μœ„μ— **μ§€μ‹œμ‚¬ν•­ brief 파일**을 기둝해 λ‹€λ₯Έ μ—μ΄μ „νŠΈλ“€μ—κ²Œ μ „λ‹¬ν•©λ‹ˆλ‹€. - 3. μž‘μ—…μ„ μœ„μž„λ°›μ•„ μˆ˜ν–‰ν•œ λ‹€λ₯Έ μ—μ΄μ „νŠΈλ“€μ˜ κ²°κ³Ό λ³΄κ³ μ„œλ“€μ€ κΈ°μ‘΄ 곡톡 폴더 λŒ€μ‹  ν•΄λ‹Ή μž‘μ—… 폴더 내뢀인 **`.mam/jobs//{agent-id}-reports/`** 디렉터리에 격리 μ €μž₯ν•˜λ„λ‘ 경둜λ₯Ό μΉ˜ν™˜ν•©λ‹ˆλ‹€. - * **κ²€ν†  의견 (APPROVED)**: μž‘μ—…(Job)을 μ€‘μ‹¬μœΌλ‘œ ν•œ μ§€μ‹œμ„œμ™€ κ²°κ³Ό λ³΄κ³ μ„œμ˜ 귀속성과 좔적성(Audit Trail)을 κ·ΉλŒ€ν™”ν•  수 μžˆλŠ” 맀우 ν›Œλ₯­ν•œ ꡬ쑰적 κ°œμ„ μ•ˆμž…λ‹ˆλ‹€. -* **μŠ€ν‚¬ μˆ˜μ • 영ν–₯ λ²”μœ„ 및 이행 κ³„νš**: - * `multi-agent-mux-delegate-job` 슀크립트 μˆ˜μ •: 작 등둝 μ‹œ `.mam/jobs//` 디렉터리λ₯Ό μžλ™ μƒμ„±ν•˜κ³  μ§€μ‹œμ„œ λ§ˆν¬λ‹€μš΄μ„ κ·Έ ν•˜μœ„λ‘œ 동적 좜λ ₯ν•˜λ„λ‘ λ³€κ²½. - * 리뷰어 및 크리에이터 μ—μ΄μ „νŠΈμ˜ ꡬ동 인자 및 슀크립트 μˆ˜μ •: λ³΄κ³ μ„œ 좜λ ₯ 경둜λ₯Ό ν™˜κ²½λ³€μˆ˜ λ˜λŠ” 인자둜 전달받은 작 ν•˜μœ„ 디렉터리(`$JOB_DIR/$AGENT_ID-reports/`)둜 μΉ˜ν™˜ν•˜μ—¬ λΌμ΄νŒ…ν•˜λ„λ‘ 패치. -