moai-workflow-ci-autofix)
Quick Reference
Wave 2 CI watch loop(moai-workflow-ci-watch) emits a required check failure (exit 2 + JSON handoff), this skill takes over and performs the following:
scripts/ci-autofix/log-fetch.shcapture failure log + PR diffscripts/ci-autofix/classify.shclassify failure type (mechanical / semantic / unknown)- according to the OQ2 cadence matrix,
manager-qualitydelegate diagnosis to subagent - mechanical failure: propose patch → orchestrator AskUserQuestion → apply → push → re-invoke ci-watch
- semantic failure: immediate AskUserQuestion escalation (no patch attempt)
- after up to 3 iterations, mandatory blocking AskUserQuestion
[HARD] Only the orchestrator (the main session running this skill) calls AskUserQuestion.
manager-quality subagents never call AskUserQuestion.
state file
.moai/state/ci-autofix-<PR번호>.json
Wave 2의 .moai/state/ci-watch-active.flagis located in the same directory as .
Since the filename includes the PR number, duplicate entry for the same PR and conflicts between different PRs are prevented.
Implementation Guide
Wave 2 → 3 Handoff consumption
Wave 2 (scripts/ci-watch/run.sh) exits with exit 2, JSON is output to stdout:
{
"prNumber": 785,
"branch": "feat/my-feature",
"failedChecks": [
{
"name": "Lint",
"runId": "12345678",
"logUrl": "https://github.com/.../actions/runs/12345678",
"conclusionDetail": ""
}
],
"auxiliaryFailCount": 1,
"totalRequired": 6
}
Schema source: internal/ciwatch/handoff.go::Handoff (Wave 2, commit 5d3f6a4c1).
failedChecks[]includes only required failures. auxiliary failures are counted only in auxiliaryFailCount.
[HARD] orchestrator는 위 JSON의 failedChecks[].runId와 logUrl field
scripts/ci-autofix/log-fetch.shas arguments.
When changing schema field names, both Wave 2 + Wave 3 must be modified simultaneously.
state file initialization and management
# Wave 2 handoff JSON을 파싱하여 상태 파일 생성
PR_NUMBER=$(printf '%s\n' "$HANDOFF_JSON" | jq -r '.prNumber')
STATE_FILE=".moai/state/ci-autofix-${PR_NUMBER}.json"
# 상태 파일 초기화 (iteration=1)
cat > "$STATE_FILE" <<EOF
{
"pr_number": $PR_NUMBER,
"iteration": 1,
"max_iterations": 3,
"last_classification": "",
"last_subclass": "",
"last_action": "",
"started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"history": []
}
EOF
crash recovery: stale state files older than 24 hours may be overwritten by a new invocation.
log capture and classification
# failed check의 첫 번째 runId를 추출
RUN_ID=$(printf '%s\n' "$HANDOFF_JSON" | jq -r '.failedChecks[0].runId')
PR_NUMBER=$(printf '%s\n' "$HANDOFF_JSON" | jq -r '.prNumber')
# 로그 + diff 캡처
LOG_AND_DIFF=$(sh scripts/ci-autofix/log-fetch.sh "$RUN_ID" "$PR_NUMBER")
# 분류
CLASSIFICATION=$(printf '%s\n' "$LOG_AND_DIFF" | sh scripts/ci-autofix/classify.sh)
CLASS=$(printf '%s\n' "$CLASSIFICATION" | grep '^classification=' | cut -d= -f2)
SUB=$(printf '%s\n' "$CLASSIFICATION" | grep '^sub_class=' | cut -d= -f2)
OQ2 Cadence Matrix (Authoritative)
This table is the single source of truth for per-iteration behavior:
| iteration | classification | sub_class | action | AskUserQuestion? |
|---|---|---|---|---|
| 1 | mechanical | trivial | confirm + apply | YES (1st option "Apply patch (recommended)") |
| 1 | mechanical | non-trivial | confirm + apply | YES (1st option "Apply patch (recommended)") |
| 1 | semantic / unknown | n/a | escalate (no patch) | YES (includes diagnostic report) |
| 2 | mechanical | trivial | silent + apply + log | NO (automatic, recorded only in report) |
| 2 | mechanical | non-trivial | confirm + apply | YES |
| 2 | semantic / unknown | n/a | escalate (no patch) | YES |
| 3 | mechanical | trivial | silent + apply + log | NO |
| 3 | mechanical | non-trivial | confirm + apply | YES |
| 3 | semantic / unknown | n/a | escalate (no patch) | YES |
| post-iter-3 | any (still failing) | n/a | mandatory escalation | YES (blocking, no timer) |
"trivial" definition: whitespace, gofmt/goimports, import-order. classify.sh의 RX_TRIVIAL_* 1:1 match with patterns.
Orchestrator state machine flow
[wave2-handoff-received]
→ 상태 파일 생성 (iteration=1)
→ log-fetch.sh로 failed log + PR diff 캡처
→ classify.sh로 분류
→ if classification == "semantic" || "unknown" → [escalate-immediate]
→ OQ2 matrix 조회
→ if iteration == 1 → [confirm-required] (iter 1 항상 confirm)
→ if subclass == "trivial" && iteration >= 2 → [silent-apply]
→ else → [confirm-required]
[confirm-required]
→ ToolSearch(query: "select:AskUserQuestion")
→ AskUserQuestion 패치 내용 제시 (1순위 = "패치 적용 (권장)")
→ user accepts → manager-quality patch apply → git add/commit/push (force-push 금지)
→ ci-watch run.sh 재호출
→ user rejects → [escalate-user-choice]
[silent-apply]
→ manager-quality patch apply → git commit/push (force-push 금지)
→ audit log에 iteration 결과 기록 (AskUserQuestion 없음)
→ ci-watch run.sh 재호출
[ci-watch returns]
→ exit 0 (green) → 상태 파일 삭제 + skill 성공 종료
→ exit 2 (still failing) → iteration++ → state file 갱신 → 상태 머신 처음으로
[iteration > 3]
→ ToolSearch(query: "select:AskUserQuestion")
→ AskUserQuestion mandatory (blocking, no timer, 사용자 응답 전까지 무한 대기)
→ 옵션 3개 + Other: (권장) 직접 수동 수정 / SPEC 수정 / PR 포기
→ 모든 iteration 결과를 audit log에 첨부하여 보고
[escalate-immediate / escalate-user-choice]
→ ToolSearch(query: "select:AskUserQuestion")
→ AskUserQuestion 진단 보고서 포함 (patch 시도 없음)
→ audit log에 escalation 이유 기록
manager-quality Subagent invocation pattern
orchestrator가 manager-qualitycontext to inject into the spawn prompt when calling :
## CI Auto-Fix Context
**Wave 2 Handoff JSON:**
<handoff_json>
**Classification Result:**
- classification: <mechanical|semantic|unknown>
- sub_class: <trivial|non-trivial|none>
**Failed CI Log + PR Diff:**
<log_and_diff_content>
**Mode:**
- If classification == "mechanical": propose a patch in unified diff format
- If classification == "semantic" or "unknown": return diagnosis only (NO patch)
[HARD] AskUserQuestion 호출 금지. patch 제안 또는 diagnosis를 Markdown으로 반환만 한다.
manager-quality proposes a patch on mechanical failure and returns only a diagnostic report on semantic failure. After the orchestrator receives the patch, it gets user confirmation via AskUserQuestion.
Audit Log writing
Every iteration is recorded in .moai/reports/ci-autofix/<PR-NNN>-<YYYY-MM-DD>.md:
# CI Auto-Fix Log — PR #785 — 2026-05-07
## Iteration 1
- **classification**: mechanical
- **sub_class**: non-trivial
- **action**: applied
- **patch_sha**: abc1234
- **escalation_reason**: N/A
## Iteration 2
- **classification**: mechanical
- **sub_class**: trivial
- **action**: applied (silent)
- **patch_sha**: def5678
- **escalation_reason**: N/A
Append-only recording via the Bash tool. On the first iteration, create the header + Iteration 1 section.
For subsequent iterations, append the ## Iteration N section.
report directory creation
mkdir -p .moai/reports/ci-autofix/
REPORT_FILE=".moai/reports/ci-autofix/PR-${PR_NUMBER}-$(date +%Y-%m-%d).md"
# 첫 iteration: 헤더 작성
if [ ! -f "$REPORT_FILE" ]; then
printf '# CI Auto-Fix Log — PR #%s — %s\n\n' \
"$PR_NUMBER" "$(date +%Y-%m-%d)" > "$REPORT_FILE"
fi
# 각 iteration: append
printf '\n## Iteration %s\n\n- **classification**: %s\n- **sub_class**: %s\n- **action**: %s\n' \
"$ITER" "$CLASS" "$SUB" "$ACTION" >> "$REPORT_FILE"
Advanced Patterns
escalation path summary
| Trigger | Path |
|---|---|
| classification == semantic / unknown (any iter) | escalate-immediate: manager-quality = diagnosis only; no patch |
| iter 1, mechanical, user rejects patch | escalate-user-choice: record reason + loop halt |
| iter == 3 + still failing (mechanical) | mandatory AskUserQuestion (blocking, no timer) |
| log-fetch.sh failure (gh auth, network) | exit 1 + remediation hint: guide user to manual fallback |
| classify.sh returns unknown twice in a row | "classifier cannot interpret" → user manual takeover |
Force-Push prohibited (Invariant)
[HARD] All auto-fix patches are added as new commits to the PR branch.
git push --force, git push -f, git push --force-with-lease Absolutely prohibited.
This invariant is ci-autofix-protocol.mdis specified as a HARD rule in.
Wave 2 Cross-Reference
- Schema:
internal/ciwatch/handoff.go::Handoff - SKILL:
internal/template/templates/.claude/skills/moai-workflow-ci-watch/SKILL.md§"Wave 3 Handoff Schema" - Protocol:
internal/template/templates/.claude/rules/moai/workflow/ci-watch-protocol.md§"T3 Handoff Format"
Works Well With
moai-workflow-ci-watch→ Wave 2 handoff sourcemanager-quality→ failure log diagnosis + patch suggestionscripts/ci-autofix/classify.sh→ mechanical vs semantic classificationscripts/ci-autofix/log-fetch.sh→ failure log + PR diff capture.claude/rules/moai/workflow/ci-autofix-protocol.md→ HARD invocation contract
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "Let's try auto-patching semantic failures too" | Semantic failures (race condition, test assertion) cannot be auto-patched without context. A wrong patch creates a bigger problem. |
| "It's convenient to time out after iter 3" | AC-CIAUT-008 invariant — no silent timeout. The user must decide. |
| "Cleaning up history with force-push is neat" | Force-pushing to a PR branch makes reviewers lose the diff. Always a new commit. |
| "Let's apply trivial fixes immediately without confirm" | iter 1 always confirms (OQ2 matrix). The user must review the first patch. |
Red Flags
- manager-quality subagent tries to call AskUserQuestion (HARD violation: orchestrator only)
- auto-continue at iteration 4 (no AskUserQuestion) — missing blocking call
- using git push --force or push -f — violates the force-push prohibition invariant
- attempting to generate a patch for a semantic failure — missing the immediate escalate path
- state file not created — iteration counter lost, risk of infinite loop
Verification
- Does Wave 2 exit 2 + JSON trigger state file initialization
- Does every classification in iter 1 raise AskUserQuestion
- Are iter 2-3 trivial mechanical silent apply + log only
- Does semantic/unknown escalate immediately to AskUserQuestion without a patch
- Does entering iter > 3 raise a blocking AskUserQuestion (no timer)
-
.moai/reports/ci-autofix/<PR>-<DATE>.mdAre all iterations recorded in - Is git push --force / -f not used (
grep -r 'push -f\|push --force' scripts/ci-autofix/)