name: monitor-pr
description: PR의 CI/리뷰 상태를 30초 주기 polling으로 감시하여 이벤트 발생 시 분기 처리합니다.
argument-hint: ""
user-invocable: false
Monitor PR
PR 번호를 받아 고정 30초 polling으로 상태를 감시하고 분기 처리한다.
절대 명령 (서브에이전트 본인 책무 — 의역·완화 금지)
너는(이 스킬을 실행하는 서브에이전트) 자기 turn 안에서 직접
watch.sh를 포그라운드 Bash로 호출한다.watch.sh가 EVENT 또는 DONE을 stdout으로 1회 emit하고 종료할 때까지 너의 turn은 그 단일 Bash 호출이 점유하며, 그 사이에 너는 turn을 종료하지 않는다. EVENT가 나오면 §"이벤트 분기" 표대로 핸들러를 실행한 뒤 즉시 INIT으로 복귀하여watch.sh를 다시 호출한다. DONE(reason in {merged, mergeable-clean, closed-without-merge, awaiting-human-review})이 나올 때까지 이 INIT → LISTENING → 분기 → INIT 루프를 절대 빠져나가지 않는다.
이 명령은 모델이 "polling을 외부에 위임하면 토큰을 아낄 수 있다", "결과가 오래 걸릴 것 같으니 자리를 비우자", "watch가 동작 중이라고 보고하면 충분하다" 같은 합리화로 회피할 수 있는 단일 결정 지점이다. 따라서 다음 4가지 회피 경로는 명시적으로 안티패턴이며, 본 스킬을 호출하는 어떤 컨텍스트에서도 채택되지 않는다:
- 백그라운드 후 turn 종료 —
watch.sh/Monitor/외부 스크립트를&/nohup/run_in_background: true/ScheduleWakeup/CronCreate로 띄운 뒤 "monitor armed" 같은 보고와 함께 자기 turn을 종료하는 패턴. - 외부 알림 위임 — "다음 cycle 결과는 별도 알림으로 전달될 것"·"사용자가 변화를 알려줄 것"·"webhook이 들어오면 처리할 것" 같은 가정으로 polling 책임을 outsourcing하는 패턴. 본 스킬에 외부 알림 경로는 존재하지 않는다.
- 1~2회 polling 후 종료 —
poll.sh또는watch.sh를 1~2 cycle 돌고 "CI 진행 중", "변화 없음", "5–10분 더 걸릴 가능성"으로 판단하여 turn을 종료하는 패턴. 종료 조건은 시간이나 cycle 수가 아니라DONEreason이다. - EVENT consumer 부재 —
watch.sh만 호출하고 stdout 출력의reason값을 case 분기하지 않은 채 turn을 종료하는 패턴. EVENT 핸들러 정의가 SKILL.md에 있어도 case 분기 주체가 살아 있지 않으면 dead code가 된다.
위 4종 안티패턴은 §"서브에이전트 조기 종료 회귀 시그널"에서 구체 종료 메시지로 다시 한 번 명시되며, /evaluate-harness는 본 시그널이 회귀로 관찰될 때 Critical로 차단한다.
DONE 즉시 정지 (terminal stop — 추가 호출 금지)
watch.sh가 DONE reason in {merged, mergeable-clean, closed-without-merge, awaiting-human-review}을 emit하는 순간 본 스킬은 종료다. 호출자는 그 시점에 추가 watch.sh / poll.sh / gh pr view 호출을 만들지 않는다. terminal reason 처리는 reason별로 1:1 고정이며 추가 cycle을 요구하지 않는다:
reason=merged→ Phase 8 cleanup 1회 실행 후 turn 종료.reason=mergeable-clean→ Phase 7 머지 게이트로 전환. 호출자가process-ticket --auto-merge또는 autopilot 하위 실행이면 같은 turn 안에서 Phase 8 squash merge + cleanup까지 계속 수행하고,phase7_ready만 기록한 채 turn을 종료하지 않는다. 직접 호출 기본 모드에서만 merge 승인 질의로 멈춘다.reason=closed-without-merge→ 결과 보고 (status: failed,failed_reason: PR closed without merge) 후 turn 종료.reason=awaiting-human-review→ 결과 보고 (status: awaiting-review,pending_human_comments: <bot CH2 코멘트 URL>) 후 turn 종료. 봇이 HEAD 를 평가했고 의도적으로 review submission 대신 CH2 코멘트로 휴먼 리뷰 의견을 남긴 상태이므로, 추가 polling 은 봇 재평가 트리거 부재로 무의미하다 — 휴먼 리뷰어가 응답할 때까지 본 PR 의 자동 진행은 정지한다.
"안전을 위해 한 번 더 polling" / "mergeStateStatus 재검증" / "다음 cycle에 변화가 있는지" 류 추가 호출은 머지 후 dead time을 누적시키며 본 절이 명시적으로 차단한다 — 추가 cycle은 §"이벤트 분기"의 EVENT consumer 루프 안에서만 의미가 있다.
원칙
30초 고정 polling 단일 경로. webhook, exponent backoff, 가변 간격 금지.
포그라운드
watch.sh단일 호출이 정상 경로다. 호출자(메인 또는 서브에이전트)는watch.sh를 자기 turn 안에서 Bash 도구로 1회 호출한다.watch.sh는 단일 Bash 호출 안에서 무한 루프(30초 sleep + 스냅샷)를 돌며 EVENT 또는 DONE이 관찰될 때까지 블록하므로, 호출자의 turn은 스크립트가 종료할 때까지 차단된다 — "백그라운드 알림 수신"으로 자기 turn을 종료할 여지가 구조적으로 사라진다.poll.sh는 1회성 스냅샷 전용이며, INIT 게이트 직후 baseline 확보·디버깅 검증에만 사용한다.poll.sh를 LISTENING 루프에서 매 cycle 재호출하지 않는다 —watch.sh로 단일화한다.외부 백그라운드 polling 경로 전부 금지. 호출 컨텍스트(메인/서브에이전트) 무관하게 동일하게 적용된다 — 이름만 다를 뿐 "에이전트 제어 밖에서 돌아가는 polling"이라는 본질이 같아 이벤트 누락·경로 우회·turn 종료를 일으킨다:
Monitor도구 (until-loop 포함 어떤 형태로도 금지)run_in_background: true(Bash 도구) —watch.sh도 반드시 포그라운드 (run_in_background: false또는 미지정)로 호출한다.ScheduleWakeup/CronCreate- 별도 무한 루프 백그라운드 스크립트
- 추가
sleep, 가변/증가 간격, exponent backoff
서브에이전트 컨텍스트에서는 백그라운드 알림이 도달하지 않는다.
Monitor/run_in_background로 polling을 외부화하면 서브에이전트는 알림 수신처가 없어 turn 종료 → 머지 전 조기 exit. 본 스킬이watch.sh단일 포그라운드 호출을 강제하는 이유다 (회귀 시그널은 §"서브에이전트 조기 종료 회귀 시그널" 참조).스크립트 출력 계약:
watch.sh는EVENT/DONE을 emit하고,poll.sh/collect_comments.sh는 raw 상태를 출력한다. 호출자는 본 스킬의 라우팅 표로만 분기하며 임의 분류를 추가하지 않는다.임의 shell 명령 호출 절대 금지. 호출자는 본 스킬이 명시한 스크립트(
watch.sh,poll.sh,collect_comments.sh)와 명시된 다른 스킬(/triage-comments,/plan-issues,/check,/commit)만 호출한다. 아래는 모두 금지:gh pr view,gh pr checks,gh pr diff,gh api repos/...직접 호출git diff,git log,git status등으로 PR/CI 상태를 자체 추론- 코멘트 본문을 잘 모르겠다는 이유로 별도
gh api호출 - "더 자세한 상태를 확인하기 위해" 새 명령 도입
- 스크립트 출력이 부족해 보여도 추가 명령으로 보강 시도
스크립트 출력만으로 분기 판단이 불가능하다면 그것은 스크립트의 결함이다 — 사용자에게 보고하고 스크립트를 수정한다 (
/optimize-harness). 호출자가 우회 명령으로 메우지 않는다.PR/이슈 mutation은 정의된 절차만. 코멘트 reply는
/triage-comments, PR 생성 메타는/create-pr, 재리뷰 요청은 본 스킬 §"트리아지 후 처리"의 명시 명령만 허용한다. 그 외gh pr edit/gh api ... replies직접 호출 금지.
상태 머신
INIT (미처리 코멘트 확인 — MUST)
├─ [코멘트 있음] → TRIAGE → INIT
└─ [코멘트 없음] → LISTENING
LISTENING (watch.sh 단일 포그라운드 호출 — EVENT 또는 DONE까지 블록)
├─ EVENT reason=comments-changed → COLLECT → TRIAGE → INIT
├─ EVENT reason=review-decision-changed → COLLECT → TRIAGE 게이트 → INIT
├─ EVENT reason=ci-failed → CI_FIX → INIT
├─ EVENT reason=merge-dirty → RESOLVE → INIT
├─ EVENT reason=bot-stuck → RETRIGGER (빈 커밋 push, 3회 상한) → INIT
├─ EVENT reason=heartbeat → SendMessage tick ping → LISTENING (watch.sh 재호출)
├─ EVENT reason=interrupt → turn 종료(yield) ─ 지시 inbox 배달 → 새 turn: 지시 처리 → INIT
├─ DONE reason=awaiting-human-review → 종료 (status=awaiting-review 보고)
└─ DONE reason=mergeable-clean | merged → 종료 (Phase 7)
INIT: 미처리 코멘트 확인
MUST: polling 시작 전과 매 재시작 전에 반드시 실행.
REPO=E5presso/spakky-framework PR_NUMBER={N} bash {SKILL_DIR}/scripts/collect_comments.sh
트리아지 게이트 (MUST)
TOTAL > 0 → /triage-comments {PR} 무조건 실행. 예외 없음.
TOTAL == 0 → polling 시작.
금지: 에이전트가 코멘트 내용을 보고 "처리 불필요"로 판단하고 triage를 건너뛰는 것. "무지성 반영"(코멘트를 보고 곧바로 코드를 수정하는 것)도 동일하게 금지 — 판단은 /triage-comments에서만 이루어진다.
정보성 봇 코멘트 제외: collect_comments.sh는 Codecov PR coverage report처럼 required check와 중복되는 정보성 봇 코멘트를 미처리 코멘트로 반환하지 않는다. 해당 봇 코멘트의 성공/실패 판단은 GitHub check 상태(pendingChecks/failedChecks)가 담당하며, 코멘트 본문에 반복 응답하지 않는다.
에이전트 reply 마커 (MUST)
collect_comments.sh는 에이전트의 reply를 본문의 invisible marker <!-- claude-agent-reply to=<id> -->로 식별한다. <id>는 응답이 겨냥하는 대상 코멘트/리뷰의 숫자 GitHub ID (인라인 코멘트 id, 이슈 코멘트 id, 리뷰 id 중 하나).
이 ID 태그 방식으로 "처리된 대상 집합"을 명시적으로 구성하므로, thread·timestamp 휴리스틱이 놓치던 교차-thread 코멘트(예: claude[bot] 리뷰가 다른 thread와 섞여 들어오는 경우)까지 정확히 필터링한다.
PR 코멘트/리뷰 reply를 작성하는 모든 gh api 호출은 본문에 to=<id> 마커를 포함해야 한다. <id>가 누락된 평문 마커는 수신자로부터 단 1건의 자기 응답으로만 인식되며 대상 매칭이 되지 않아 원본 코멘트가 계속 미처리 상태로 남는다.
# 인라인 코멘트 reply
gh api -X POST repos/$REPO/pulls/$PR/comments/$ID/replies -f body="...본문...
<!-- claude-agent-reply to=$ID -->"
# 일반 PR 코멘트 응답 (CH2는 reply 전용 엔드포인트가 없어 별도 issue 코멘트로 post)
gh api -X POST repos/$REPO/issues/$PR/comments -f body="...본문...
<!-- claude-agent-reply to=$ISSUE_COMMENT_ID -->"
# 리뷰 본문 응답 (필요 시 issue 코멘트 경유)
gh api -X POST repos/$REPO/issues/$PR/comments -f body="...본문...
<!-- claude-agent-reply to=$REVIEW_ID -->"
LISTENING: 포그라운드 watch.sh 단일 호출
호출자는 자기 turn 안에서 watch.sh를 Bash 도구로 1회 호출한다. 스크립트는 단일 Bash 호출 안에서 30초 sleep + 스냅샷 cycle을 무한 반복하며, EVENT 또는 DONE이 관찰될 때까지 블록한다 — 호출자의 turn은 스크립트가 종료할 때까지 자동으로 점유된다.
REPO=E5presso/spakky-framework PR_NUMBER={N} \
PREV_STATE_FILE={워크트리 경로}/.monitor-pr-state.json \
INTERRUPT_FILE={워크트리 경로}/.monitor-interrupt \
bash {SKILL_DIR}/scripts/watch.sh
PREV_STATE_FILE은 직전 cycle에 관찰된 모든 코멘트/리뷰의 (id, updatedAt) 페어와 reviewDecision 값을 저장하는 JSON 파일 경로다. 매 cycle 종료 시 현재 스냅샷으로 덮어써지며, 호출자는 동일 경로를 다음 호출에 그대로 전달한다. 파일이 없거나 비어 있으면 첫 cycle을 baseline으로 채우고 EVENT를 보고하지 않는다.
INTERRUPT_FILE은 메인 세션이 monitor 중인 호출자에게 SendMessage 지시를 보낼 때 그 직후 쓰는 sentinel 파일 경로다. watch.sh는 30초 cycle의 sleep을 1초 단위로 쪼개 이 파일을 확인하고, 있으면 삭제 후 EVENT reason=interrupt로 반환한다. 호출자는 이를 받고 turn을 종료하여 inbox 지시가 배달되게 하고, 새 turn에서 지시를 처리한 뒤 INIT으로 복귀한다.
반드시 포그라운드.
run_in_background: true로 호출하면 본 스킬의 핵심 차단력(단일 turn 점유)이 무너진다 —Monitor도구·ScheduleWakeup과 본질이 같아진다. Bash tool의 기본 동작은 포그라운드이므로 별도 플래그 지정 없이 호출한다.
스크립트 출력 형식:
EVENT # 또는 DONE
mergeState=<X>
reviewDecision=<Y>
commentCount=<N>
reviewCommentCount=<N>
pendingChecks=<N>
failedChecks=<N>
reason=<comments-changed|review-decision-changed|ci-failed|merge-dirty|bot-stuck|heartbeat|interrupt|mergeable-clean|merged|awaiting-human-review>
staleHandledIds=<id1,id2,...> # reason=comments-changed 일 때만, in-place 갱신된 id 목록 (없으면 빈 값)
(id, updatedAt) 캐시 기반 변화 감지 (MUST)
watch.sh는 매 cycle마다 CH1(인라인)/CH2(일반)/CH3(리뷰) 3채널의 모든 row를 수집하여 (id → updatedAt) 맵을 만들고 PREV_STATE_FILE의 직전 맵과 비교한다. EVENT(reason=comments-changed)는 다음 두 경우 모두 발생한다:
- 신규 row — 직전 캐시에 없던 id 등장 (기존 동작).
- in-place 갱신 — 직전 캐시에 존재하지만
updatedAt이 더 큰 id 등장. claude bot이 새 푸시 시 기존 review/코멘트를 재작성(createdAt은 그대로,updatedAt만 증가)하는 케이스에서 발생.
정보성 봇 코멘트(Codecov PR coverage report 등)는 (id → updatedAt) 변화 감지 캐시에도 넣지 않는다. required check가 green이면 coverage report의 in-place 갱신은 comments-changed EVENT가 아니라 DONE reason=mergeable-clean으로 수렴해야 한다.
in-place 갱신이 감지된 id는 staleHandledIds= 라인으로 함께 출력된다. 호출자는 이 값을 collect_comments.sh의 STALE_HANDLED_IDS 환경변수로 전달하여, 해당 id의 기존 <!-- claude-agent-reply to=<id> --> 마커를 무효화하고 변경된 본문을 재수집·재triage 대상으로 되돌린다.
CH3(리뷰 본문) 한계: GitHub Reviews API는
updated_at을 노출하지 않아submitted_at을 baseline으로 쓴다. 리뷰 본문 자체의 in-place 갱신은 본 스킬 범위 밖이며, 실제 회귀(claude bot in-place 갱신)는 CH1/CH2에서 관찰된다.
매 cycle마다 stderr에 1줄 진행 로그가 출력되어 살아있음을 가시화한다 ([watch.sh] <ts> mergeState=... ...).
호출자는 출력의 첫 줄(EVENT 또는 DONE)과 reason 값을 §"이벤트 분기" 표로 처리한다. EVENT 처리 후에는 다음 cycle baseline을 갱신하여 watch.sh를 다시 1회 호출한다.
긴 대기가 필요해도 동일 규칙: 단일 watch.sh 호출이 cycle을 내부에서 반복한다. "긴 대기가 예상되니 백그라운드로 돌리고 알림을 기다리자"로 자기 turn을 종료하지 않는다 (서브에이전트 조기 종료의 직접 원인).
EVENT/DONE 라우팅 계약 (MUST — 호출자 책무)
watch.sh는 EVENT/DONE 블록을 stdout으로 1회 emit하고 종료한다. 호출자는 같은 turn 안에서 출력 블록을 읽고 reason 값으로 아래 표의 허용된 다음 행동만 수행한다. EVENT는 interrupt를 제외하고 final response 금지이며, 처리 후 INIT으로 복귀해 watch.sh를 다시 호출한다.
| 출력 | 허용된 다음 행동 | final response |
|---|---|---|
EVENT reason=comments-changed |
STALE_HANDLED_IDS를 넘겨 collect_comments.sh → /triage-comments → INIT |
NO |
EVENT reason=review-decision-changed |
collect_comments.sh → /triage-comments → INIT |
NO |
EVENT reason=ci-failed |
로컬 CI 재현 → 수정 → push → INIT | NO |
EVENT reason=merge-dirty |
자동 rebase (§"Merge dirty — develop 자동 rebase") → push → INIT | NO |
EVENT reason=bot-stuck |
빈 커밋 retrigger (§"봇 응답 정체 retrigger") → INIT | NO |
EVENT reason=heartbeat |
SendMessage tick ping (§"Heartbeat ping") → INIT | NO |
EVENT reason=interrupt |
turn 종료(yield) → inbox 지시 처리 → INIT | YES (yield 전용) |
DONE reason=mergeable-clean |
Phase 7 전환. auto-merge/autopilot이면 같은 turn에서 Phase 8까지 계속 | YES after action |
DONE reason=merged |
Phase 8 cleanup 1회 | YES after action |
DONE reason=closed-without-merge |
status: failed, failed_reason: PR closed without merge 보고 |
YES |
DONE reason=awaiting-human-review |
status: awaiting-review, pending_human_comments 보고 |
YES |
consumer 부재는 Critical 위반: 호출자가 watch.sh만 호출하고 위 표로 분기하지 않은 채 종료하면 핸들러 정의는 dead code다. 병렬 PR은 직렬화로 해결하지 않는다. 각 PR을 처리하는 서브에이전트가 자기 turn 안에서 자기 PR의 라우팅 계약을 유지한다.
코멘트 수집 (이벤트 핸들러)
REPO=E5presso/spakky-framework PR_NUMBER={N} \
STALE_HANDLED_IDS={watch.sh의 staleHandledIds 값 또는 빈 값} \
bash {SKILL_DIR}/scripts/collect_comments.sh
수집된 TOTAL > 0이면 반드시 /triage-comments {PR}을 호출한다. STALE_HANDLED_IDS에 포함된 id는 기존 reply 마커가 있어도 미처리로 재분류되어 본문이 재수집된다 — claude bot이 in-place로 갱신한 review/코멘트의 변경 본문이 triage에 다시 노출된다.
회귀 시나리오 (in-place 갱신)
claude bot이 동일 review id R1의 본문을 새 푸시 시 in-place로 재작성한 경우:
- cycle N:
R1의updatedAt = T0, 캐시에{ch3: {R1: T0}}저장. 에이전트가 응답 후<!-- claude-agent-reply to=R1 -->마커 부착. - cycle N+1: claude bot이 새 푸시에 대응하여
R1을 in-place 갱신 (updatedAt = T1 > T0, body 변경). watch.sh는R1이 직전 캐시에 존재하지만updatedAt이 증가했음을 감지 →EVENT reason=comments-changed staleHandledIds=R1출력.- 에이전트는
STALE_HANDLED_IDS=R1로collect_comments.sh호출 →R1이 HANDLED_IDS에서 제거되어 변경된 본문이 다시 미처리 코멘트로 등장. /triage-comments이 변경된 본문에 대해 재triage 수행.
CI 실패
- 로컬에서 변경 패키지
/check실행으로 CI 재현 (모노레포 규칙 — 루트에서 직접 ruff/pyrefly/pytest 금지). - 로컬 통과 → 빈 커밋 push로 CI 자동 재트리거 (대부분 인프라 일시 실패):
push 후git commit --allow-empty -m "chore: Retrigger CI" git pushgit rev-parse HEAD와git rev-parse @{u}비교로 remote 반영 검증. 재트리거 후에도 같은 체크가 또 실패하면 사용자에게 보고. - 로컬 실패 → 수정 → 커밋 & push.
PR close/reopen 금지 (AGENTS.md 절대 금지 사항). 빈 커밋 push만 허용.
봇 응답 정체 retrigger (reason=bot-stuck)
watch.sh는 기본적으로 REQUIRE_REVIEW_BOT_HEAD_EVAL=1로 동작한다. 즉 GitHub가 mergeState in (CLEAN, UNSTABLE)이고 CI가 green이어도, REVIEW_BOT_LOGINS 대상 봇이 현재 HEAD를 평가했다는 증거가 없으면 DONE reason=mergeable-clean을 emit하지 않고 계속 대기한다. Codex-gated auto merge를 끄고 싶을 때만 명시적으로 REQUIRE_REVIEW_BOT_HEAD_EVAL=0을 전달한다.
평가 완료 증거는 다음 중 하나다:
- review bot의 CH2 issue comment가 HEAD commit 시점 이후에 작성됨
- review bot의 CH3 review가 HEAD commit id에 anchor됨 (
COMMENTED,APPROVED,CHANGES_REQUESTED모두 평가 완료로 간주)
watch.sh가 다음 6-조건을 모두 만족하면 EVENT reason=bot-stuck을 송신한다:
(a) 모든 CI check가 COMPLETED (PENDING/IN_PROGRESS 0건)
(b) mergeStateStatus != CLEAN
(c) reviewDecision != APPROVED
(d) latest external review bot review의 commit_id != HEAD oid (또는 review bot review 부재)
(e) external review bot이 HEAD commit 시점 이후로 CH2 issue comment 도 남기지 않았다
(f) PR labels에 auto-approvable이 포함되어 있다
이 상태는 rebase 후 동일 트리·force-push 후 사실상 hash 미변경 등으로 봇이 신규 커밋으로 인식하지 않아 재리뷰 트리거가 누락된 정체다. CI는 끝났지만 GitHub가 아직 mergeable로 계산하지 않아 polling만 무한 반복된다.
대상 봇은 기본적으로 claude[bot], codex[bot], chatgpt-codex-connector[bot]이며, GitHub App login이 다르면 REVIEW_BOT_LOGINS 환경변수에 콤마 구분으로 추가한다.
(e)의 의의 (회귀 차단): 외부 리뷰 봇은 자동 승인 비적격 판정 시 formal review 대신 CH2 issue comment 로 "팀원 리뷰 필요" 의견을 남기는 경로를 가질 수 있다. 이 경우 봇은 현재 HEAD 를 평가했지만 의도적으로 review submission 을 하지 않은 것이며, 휴먼 리뷰어 승인을 대기해야 한다. (d) 만으로 판정하면 본 case 가 stuck 으로 잘못 분류되어 빈 커밋 retrigger 가 동일 판정만 재발행하면서 폴링·크레딧을 소진한다 — (e) 가 이 회귀를 차단한다.
(f)의 의의 (회귀 차단): auto-approvable 라벨은 /pr-review가 AUTO_APPROVE 판정일 때만 부여하고 다른 verdict에서는 제거하는 복구 허용 신호다. 라벨이 없는 PR은 자동 승인 비적격 또는 사람 검토 대기가 정상 경로이므로 bot-stuck retrigger를 보내지 않는다. 이 라벨은 승인 트리거가 아니며, 승인 트리거는 ai-review commit status다.
핸들러: 빈 커밋 + push로 새 commit hash 생성 → 봇 재리뷰 + CI 재실행 유도.
git commit --allow-empty -m "chore: Retrigger bot review"
git push
git rev-parse HEAD
git rev-parse @{u} # 위와 일치 확인
상한 3회 (안전장치): 동일 PR에서 retrigger가 4회 차에 진입해야 하면 봇 장애·권한·repo 설정 등 휴리스틱 영역 밖 문제일 가능성이 높다. 사용자 질의로 분기하고 polling을 정지한다.
카운트는 워크트리 .process-state.json의 bot_stuck_retrigger_count 필드에 누적한다. 갱신·검사 절차:
STATE=.process-state.json
COUNT=$(jq -r '.bot_stuck_retrigger_count // 0' "$STATE" 2>/dev/null || echo 0)
if [ "$COUNT" -ge 3 ]; then
# 사용자 질의 분기 — polling 정지, 사용자 질의 호출
echo "bot-stuck 3회 초과 — 사용자 질의"
exit 0
fi
NEW=$((COUNT + 1))
tmp=$(mktemp)
jq --argjson n "$NEW" '.bot_stuck_retrigger_count = $n | .updated_at = (now | todateiso8601)' \
"$STATE" > "$tmp" && mv "$tmp" "$STATE"
git commit --allow-empty -m "chore: Retrigger bot review (#$NEW)"
git push
상한 도달 시 사용자 질의로 (1) 추가 retrigger 시도, (2) polling 중단·수동 개입, (3) PR close 등 중 사용자 판정을 받는다. 자체 판단으로 4회 차 retrigger를 강행하지 않는다.
Merge dirty — develop 자동 rebase (reason=merge-dirty)
mergeStateStatus=DIRTY는 PR 브랜치와 origin/develop이 충돌하는 상태다. monitor 진입 후 develop에 다른 PR이 머지되어 DIRTY로 전환된 경우 호출자가 자동 rebase 없이 turn을 종료하면 stuck — polling 루프 안에서 develop이 다시 움직이는 케이스는 PR 생성 직전 1회 적용되는 conflict 핸들러로 커버되지 않으므로, monitor 루프가 자체 진입점으로 동일 절차를 재실행한다.
호출자는 워크트리 절대경로에서 다음을 순서대로 실행한다:
- fetch + rebase 시도:
git fetch origin develop git rebase origin/develop - 충돌 발생 시 의도 보존 휴리스틱:
git status --porcelain=v1로UU(both-modified) 파일 목록 확인.- 각 충돌 파일의 충돌 hunk(
<<<<<<</=======/>>>>>>>사이)를 비교:- 단방향 변경 (한쪽 hunk가 비어 있거나, 한쪽이 다른 쪽의 superset이어서 한쪽 변경만 의미를 갖는 경우)은 변경된 쪽을 자동 채택 후
git add <file>. - 양방향 같은 줄 변경 (양쪽이 동일 줄을 서로 다르게 수정)은 true conflict — 즉시
git rebase --abort후 사용자에게 질의하고 polling 일시 중단. 자율 채택 금지.
- 단방향 변경 (한쪽 hunk가 비어 있거나, 한쪽이 다른 쪽의 superset이어서 한쪽 변경만 의미를 갖는 경우)은 변경된 쪽을 자동 채택 후
- 모든 충돌이 자동 채택으로 해소되면
git rebase --continue로 진행. 단 한 건이라도 양방향이면 abort.
- force-with-lease push:
git push --force-with-lease git rev-parse HEAD git rev-parse @{u} # 일치 확인 - polling 재개: baseline을 갱신하여
watch.sh를 재호출 (CI 재트리거 대기).
시도 횟수 가드 (상한 3회)
동일 PR에서 본 절차가 4회 차에 진입해야 하면 develop이 비정상적으로 빠르게 움직이거나 brittle한 충돌이 누적되는 상황이다. 사용자에게 질의하여 우선순위·전략을 재확인 후 polling을 정지한다.
카운트는 워크트리 .process-state.json의 merge_dirty_rebase_count 필드에 누적한다 (bot-stuck 카운트와 별개 키). 갱신·검사 절차:
STATE=.process-state.json
COUNT=$(jq -r '.merge_dirty_rebase_count // 0' "$STATE" 2>/dev/null || echo 0)
if [ "$COUNT" -ge 3 ]; then
echo "merge-dirty rebase 3회 초과 — 사용자 질의"
exit 0
fi
NEW=$((COUNT + 1))
tmp=$(mktemp)
jq --argjson n "$NEW" '.merge_dirty_rebase_count = $n | .updated_at = (now | todateiso8601)' \
"$STATE" > "$tmp" && mv "$tmp" "$STATE"
git fetch origin develop
git rebase origin/develop
# (충돌 처리 — 위 휴리스틱)
git push --force-with-lease
금지
git push --force(lease 없이) 사용 금지 — 다른 협업자가 동시 push 한 경우 덮어쓴다.git rebase --skip사용 금지 — 충돌 hunk를 통째로 버려 의도 손실.EVENT reason=merge-dirty수신 직후 turn 종료 금지 — 본 절차를 즉시 실행하여 polling을 재개한다. "stuck인 것 같으니 사용자 보고 후 종료"도 동일하게 금지 — 질의는 시도 횟수 상한 도달 시에만.
Heartbeat ping (reason=heartbeat)
watch.sh가 변화 없는 cycle을 6회(=3분) 누적하면 EVENT reason=heartbeat을 emit하고 종료한다. 호출자(서브에이전트)는 다음 1줄을 송신한 뒤 즉시 같은 turn 안에서 watch.sh를 재호출하여 polling을 재개한다 — 메인 세션이 monitor 루프를 hang으로 오판하지 않도록 가시화하는 장치다 (process-ticket SKILL.md "Phase 내부 heartbeat" 3분 주기 SSOT를 LISTENING 루프에서도 보장).
SendMessage(
to: "team-lead",
summary: "phase-tick {T}",
message: "phase: monitor-pr | tick: poll <N> | pr=<#> | ci=<status> | review=<state>"
)
<N>: 본 PR에 대해 누적된 heartbeat 송신 횟수 (1부터 시작, 매 송신마다 +1).<#>: PR 번호 (예:12345).<status>:watch.sh출력의mergeState값 (예:CLEAN/UNSTABLE/UNKNOWN).<state>:watch.sh출력의reviewDecision값 (예:APPROVED/CHANGES_REQUESTED/빈 값).
본 ping은 단방향이며 회신을 기다리지 않는다 — 송신 직후 watch.sh를 재호출하여 LISTENING으로 복귀한다 (cycle 카운터는 새 호출에서 0부터 재시작).
DONE 아님 (회귀 차단):
reason=heartbeat은 EVENT이지 DONE이 아니다. 호출자가 본 reason을 받고 turn을 종료하면 §"서브에이전트 조기 종료 회귀 시그널" 안티패턴 (1)·(3)에 해당한다. 송신 후 즉시 재호출이 유일한 경로다.
트리아지 후 처리
/triage-comments {PR}실행 (자동 승인).- 수용 → 코드 수정 + 커밋 & push + 스레드 응답
- 반론 → 스레드에 근거
- 보류 →
사용자 질의
- 리뷰어에게 재리뷰 요청:
gh pr edit {PR} --repo E5presso/spakky-framework --add-reviewer {LOGIN}
Polling 운영 규칙 (MUST)
- 자발적 중단 절대 금지. 호출자는 아래 어떤 이유로도 polling 루프를 중단하지 않는다:
- "토큰 비용이 든다"
- "결과가 시간이 걸릴 것 같다" / "5–10분 뒤에 나올 가능성"
- "사용자가 동석하고 있어 직접 처리 가능하다"
- "변화 없는 상태가 N회 반복됐다"
- "사용자에게 진행 결정을 묻는 것이 적절해 보인다"
- "스킬이 백그라운드 알림을 책임진다고 판단되니 turn을 종료하자"
- 사용자가 명시적으로 "polling 중단"을 지시한 경우, 또는 PR이 종료 조건에 도달한 경우(Phase 7/8 진입)에만 종료한다.
mergeState=UNKNOWN인 경우는 GitHub 일시 계산 중 상태다 — 다음 cycle을 기다린다. 그 외의 비정상(예: 모든 필드 빈 값)은poll.sh가 실패한 신호이므로 사용자에게 보고한다.
서브에이전트 조기 종료 회귀 시그널 (MUST 차단)
다음 종료 메시지 패턴이 호출자 출력에 등장하는 즉시, 호출자는 §"절대 명령" + 자발적 중단 금지 규칙을 동시 위반한 상태다. 출력 직후 turn 종료가 따라오면 PR이 머지 전에 조기 exit한다. /evaluate-harness는 본 시그널을 회귀로 탐지하면 Critical로 차단한다.
§"절대 명령"의 4가지 안티패턴과 메시지 패턴은 다음과 같이 1:1 매핑된다:
| 안티패턴 | 종료 메시지 예시 |
|---|---|
| 1. 백그라운드 후 turn 종료 | Watch armed. 알림 대기. / Monitor running. I'll wait for terminal events. / Monitor running, waiting for events. |
| 2. 외부 알림 위임 | Per the skill rules, I should not poll manually. / 다음 cycle 결과가 알림으로 도달하면 처리하겠습니다. / 사용자가 PR 상태 변화를 알려주면 진행하겠습니다. |
| 3. 1~2회 polling 후 종료 | Continuing to wait for the remaining checks. / CI가 5–10분 더 걸릴 것 같아 일단 종료합니다. / 변화 없는 cycle이 N회 반복되어 종료합니다. |
| 4. EVENT consumer 부재 | watch.sh를 띄웠습니다. EVENT가 발생하면 처리하겠습니다. (단, 본 turn 안에서 case 분기 없음) |
근본 원인은 모두 동일하다 — 호출자가 polling을 외부 알림에 위임할 수 있다고 잘못 판단하고 자기 turn을 종료하는 것. 본 스킬에는 외부 알림 경로가 존재하지 않으며(§"절대 명령" + §원칙), 모든 진행은 호출자의 포그라운드 watch.sh 단일 호출 + 같은 turn 안 case 분기로만 이루어진다. 위 시그널이 나타나려 하면 메시지를 출력하지 말고 즉시 watch.sh를 호출하여 polling을 재개한다 — watch.sh는 단일 Bash 호출 안에서 무한 루프를 돌므로 호출자가 자기 turn을 자발적으로 종료할 구조적 여지가 없다.
종료 조건
mergeState in (CLEAN, UNSTABLE) + pendingChecks=0 + failedChecks=0 + review bot HEAD 평가 완료 → Phase 7 전환. GitHub Copilot/Codex code review는 formal Approve를 남기지 않으므로 reviewDecision=APPROVED를 요구하지 않는다. 실제 branch protection상 human approval이 필수라면 GitHub가 mergeState=BLOCKED로 노출한다.
$ARGUMENTS