From mst
Accepts Phase 3 reviewed results by squash-merging worktree into base branch, cleaning up branches/worktrees, updating request status to done, and advancing to Phase 5. Manual /mst:accept or auto after approve.
npx claudepluginhub myrtlepn/gran-maestro --plugin mstThis skill uses the workspace's default tool permissions.
Phase 3 리뷰를 통과한 결과물을 최종 수락하여 감지된 base 브랜치에 머지하고 정리합니다.
Approves PM-written implementation specs and starts Phase 2 execution in Gran Maestro workflow. Supports single/batch approvals, dependency checks, DAG chaining, and non-stop execution rules.
Merges the feature branch PR, removes the git worktree, and deletes the flow state file to finalize the development workflow. Supports auto/manual modes and self-invocation.
Guides finishing implementation work after tests pass: verifies full suite, presents git options (local merge, PR creation, defer, discard), executes chosen workflow.
Share bugs, ideas, or general feedback.
Phase 3 리뷰를 통과한 결과물을 최종 수락하여 감지된 base 브랜치에 머지하고 정리합니다.
auto_accept_result=true(기본) 시 /mst:approve에서 Phase 3 PASS 후 자동 실행auto_accept_result=false 시 /mst:approve가 Phase 3 PASS 후 멈추고 사용자가 명시적 호출source_plan 존재 여부를 확인해 Step 6 동기화 대상(plan.json)을 결정한다.request.json이 done 상태로 갱신되고 후속 의존 REQ 처리(해당 시)가 반영되어야 한다.source_plan이 있으면 Plan 상태 동기화 시도 결과(완료/스킵)를 남긴다.git branch -d만 사용해 정리 실패를 방치한다.source_plan이 있는데도 Step 6 동기화 확인을 생략하고 완료 처리한다.source_plan 조회 및 sync 실행(또는 skip 사유)을 명시한다.경로 규칙 (MANDATORY): 이 스킬의 모든
.gran-maestro/경로는 절대경로로 사용합니다. 스킬 실행 시작 시PROJECT_ROOT를 취득하고, 이후 모든 경로에{PROJECT_ROOT}/접두사를 붙입니다.PROJECT_ROOT=$(pwd)
{PLUGIN_ROOT}는 이 스킬의 "Base directory"에서skills/{스킬명}/을 제거한 절대경로입니다. 상대경로(.claude/...)는 절대 사용하지 않습니다.
-a 또는 --auto 존재 여부 검사:
AUTO_MODE=true (args 어느 위치든 허용)read_workflow_state_auto_mode("mst:accept", REQ_ID) 호출 (helper는 T01에서 추가됨)
AUTO_MODE에 채택None → 3번 단계로 진행Bash(python3 {PLUGIN_ROOT}/scripts/mst.py config get auto_mode.accept)로 config.auto_mode.accept 확인
AUTO_MODE=trueAUTO_MODE=false (기본)AUTO_MODE=true이면 수락 AskUserQuestion을 전부 생략하고 최종 머지 단계까지 무정지 진행한다.AUTO_MODE는 "이 accept 호출의 무정지 실행"만 제어합니다. dependencies.blocks를 기반으로 한 DAG 연쇄 재기동 여부는 기존 정책 플래그만 참조합니다:
workflow.auto_approve_on_unblock (config)request.json.auto_approve (해당 REQ 속성)따라서 /mst:accept -a REQ-N이 workflow.auto_approve_on_unblock=false 환경에서 호출되어도 후속 REQ로의 자동 연쇄는 발생하지 않습니다. AUTO_MODE와 DAG 연쇄를 같은 신호로 취급하지 마십시오.
또한 approve의 auto-accept guard 차단은 AUTO_MODE와 별개이며, 차단된 건은 approve 단계에서 연쇄 호출이 멈춘 뒤 수동 /mst:accept로만 진입합니다.
사용자가 대기 중 "auto로", "자율 모드로", "-a로", "지금부터 자동으로" 등 입력 시 즉시 AUTO_MODE=true로 전환하고 [자율 모드 전환] 이제부터 -a 모드로 진행합니다. 출력 후 현재 Step부터 재개합니다.
requests/의 모든 request.json 스캔 → current_phase==3 + phase3_review/PASS 상태 필터링 → REQ 번호 오름차순 첫 번째 선택 (없으면 "대기 중 요청 없음" 알림)
/mst:feedback 완료 필요)summary.md 작성
2.5. Evidence Verification Gate (PAC 증거 검증):
source_plan 기반 PAC 검증 증거가 최신 review 산출물에 모두 첨부되었는지 확인한다.request.json.source_plan 확인.
"[INFO] Evidence gate skip (source_plan 없음)" 출력 후 다음 단계 진행 (하위 호환).source_plan이 있으면 {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.ids.json Read.
"[INFO] Evidence gate skip (plan.ids.json 없음)" 출력 후 다음 단계 진행 (하위 호환).plan.ids.json에서 PAC ID 목록(PAC-N)을 로드한다.
3.5. PAC 범위 필터링 (분리 실행 플랜 대응):
request.json의 모든 태스크에서 covers_ac 배열을 수집한다.spec.md의 ## 3.3 PAC Mapping에서 Coverage == "COVERED"인 PAC ID만 추출하여 in_scope_pacs로 설정한다.in_scope_pacs가 비어있으면 (하위 호환: PAC Mapping 미존재) plan.ids.json의 전체 PAC를 대상으로 한다.in_scope_pacs가 있으면 해당 PAC만 검증 대상으로 한정한다 (OTHER_REQ PAC 제외).request.json.review_iterations의 최신 rv_id)을 식별하고
{PROJECT_ROOT}/.gran-maestro/requests/{REQ_ID}/reviews/{RV_ID}/evidence-ledger.md를 Read한다.
"[INFO] Evidence gate skip (evidence-ledger.md 없음 — 레거시 review)" 출력 후 다음 단계 진행 (하위 호환).in_scope_pacs 범위 내에서만).
evidence-ledger.md에 해당 PAC ID 레코드가 존재해야 한다.증거 미첨부 PAC: {PAC-ID 목록}request.json.source_plan 값이 있으면 {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json을 Read하고 type 필드를 확인한다.plan_type = plan.json.type (type 누락 또는 Read 실패 시 "code" fallback)type_strategies = Read({PLUGIN_ROOT}/templates/defaults/type-strategies.json) 시도strategy = type_strategies[plan_type] || type_strategies["code"]type-strategies.json Read 실패/파싱 실패/키 누락 시 strategy = {"template":"templates/impl-request.md","worktree_policy":"required","review_mode":"code","accept_mode":"squash-merge"}로 fallback해 기존 수락 경로를 유지한다.strategy.accept_mode != "file-placement")
if strategy.accept_mode == "file-placement":
else (strategy.accept_mode != "file-placement"):
request.json.detected_base가 있으면 해당 값을 최우선 base로 사용한다.request.json.detected_base가 없으면 fallback: config.worktree.base_branch → master 순서로 사용한다.BASE_SLUG는 base 이름의 /를 -로 치환한 값이다. (T03 helper가 있는 환경에서는 동일 규칙의 helper를 재사용한다.){BASE_BRANCH} checkout 실패 시 감지 base와 실제 git 상태가 불일치한 것으로 보고 명시적 오류를 출력한 뒤 중단한다.REQUEST_JSON="{PROJECT_ROOT}/.gran-maestro/requests/{REQ_ID}/request.json"
DETECTED_BASE=$(python3 -c 'import json, sys; data=json.load(open(sys.argv[1], encoding="utf-8")); print(str(data.get("detected_base") or "").strip())' "$REQUEST_JSON")
CONFIG_BASE_BRANCH=$(python3 {PLUGIN_ROOT}/scripts/mst.py config get worktree.base_branch 2>/dev/null || true)
CONFIG_BASE_BRANCH=$(printf "%s" "$CONFIG_BASE_BRANCH" | head -n 1 | xargs)
BASE_BRANCH="${DETECTED_BASE:-${CONFIG_BASE_BRANCH:-master}}"
BASE_SLUG=$(python3 -c 'import sys; print(sys.argv[1].replace("/", "-"))' "$BASE_BRANCH")
REQ_BRANCH="gran-maestro/${BASE_SLUG}/REQ-NNN"
TASK_BRANCH_PREFIX="gran-maestro/${BASE_SLUG}/REQ-NNN-T"
echo "[accept] squash base: ${BASE_BRANCH}"
echo "[accept] request branch: ${REQ_BRANCH}"
# 각 태스크 브랜치를 REQ 브랜치에 머지 (커밋 이력 보존)
git -C {PROJECT_ROOT} checkout "${REQ_BRANCH}"
git -C {PROJECT_ROOT} merge --no-ff "${TASK_BRANCH_PREFIX}01"
git -C {PROJECT_ROOT} merge --no-ff "${TASK_BRANCH_PREFIX}02"
# ... (태스크 수만큼 반복)
git -C {PROJECT_ROOT} checkout master
git -C {PROJECT_ROOT} merge --squash gran-maestro/REQ-NNN
하위 호환 snapshot 계약은 위 flat REQ branch 예시를 유지한다. 실제 실행은 아래 감지 base 변수를 사용한다.
git -C {PROJECT_ROOT} checkout "${BASE_BRANCH}"
git -C {PROJECT_ROOT} merge --squash "${REQ_BRANCH}"
예: request.json.detected_base="feature/branch-rules"이면 git checkout feature/branch-rules 후 git merge --squash gran-maestro/feature-branch-rules/REQ-NNN를 실행한다.
[커밋 양식 감지]
git -C {PROJECT_ROOT} log --pretty=format:"%s" -10을 실행해 최근 10개 커밋 subject를 수집한다.[REQ-로 시작하는 항목을 우선 분석 대상으로 사용하고, 없으면 전체 10개를 분석 대상으로 사용한다.[REQ-NNN] 형태와 뒤따르는 설명 구조를 식별한다.(...) 형태의 파일목록/부록 유무를 식별한다.git log 실행 실패, 커밋 히스토리 부재, 또는 분석 대상에서 일관된 패턴을 추출할 수 없는 경우 subject 폴백은 [REQ-NNN] {REQ 제목}으로 고정한다.{DETECTED_SUBJECT}를 만들고, 예를 들어 [REQ-NNN] 한국어 설명 (파일목록)이 우세하면 동일한 접두사/언어/괄호 부록 구조를 유지한다.git -C {PROJECT_ROOT} commit -m "{DETECTED_SUBJECT}
Base branch: ${BASE_BRANCH}
태스크 요약:
- T01: {태스크 1 제목}
- T02: {태스크 2 제목}"
-D 강제 삭제):
# Step 4의 cleanup helper를 먼저 정의한 뒤 사용한다.
# 실패 시 각 태스크 meta를 clean_failed로 기록하고 accept 흐름은 정리 단계로 계속 진행한다.
delete_req_branch_safely "${REQ_BRANCH}" "T01" "T02"
3.5. Implementation Decision 기록 (비차단):
source_plan이 존재하면 {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json의 linked_intent 필드를 읽어 INTENT_ID 취득{PROJECT_ROOT}/.gran-maestro/intents/{INTENT_ID}.md)의 ## Implementation Decision 섹션 끝에 아래 형식으로 직접 Edit(append):
[YYYY-MM-DD] [REQ-NNN] {spec §1 요약}
linked_intent 미존재 시 skip (비차단); 파일 Edit 실패 시 warn만 출력, 워크플로우 차단 금지⚠️ squash merge 후 브랜치 삭제 규칙: REQ 브랜치를
{BASE_BRANCH}에 squash merge하면 merge ancestor가 생성되지 않으므로git branch -d(soft delete)는 "not fully merged" 오류로 실패합니다. 브랜치 삭제는git branch -D를 사용하세요.\|\| true로 정리 실패를 숨기지 않습니다. 각 호출은 exit code를 수집하고 실패 시.gran-maestro/worktrees/{REQ_ID}-{taskId}.meta.json의 기존 필드를 보존하면서state="clean_failed"와clean_failed.{command,exit_code,message,timestamp}를 기록합니다.
strategy.accept_mode == "file-placement"이면 worktree가 없을 수 있으므로 worktree 제거 단계는 "없으면 skip"으로 처리한다 (graceful skip, 비차단).&& 연결 금지 — 하나 실패 시 나머지 미실행됨)
record_clean_failed() { local task_id="$1" local exit_code="$2" local failed_command="$3" local error_message="$4"
python3 - "$PROJECT_ROOT" "$REQ_ID" "$task_id" "$exit_code" "$failed_command" "$error_message" <<'PY' import json import os import sys from datetime import datetime, timezone from pathlib import Path
project_root = Path(sys.argv[1]) req_id = sys.argv[2] task_id = sys.argv[3] exit_code = int(sys.argv[4]) failed_command = sys.argv[5] error_message = sys.argv[6].strip() or "cleanup command failed" now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") meta_path = project_root / ".gran-maestro" / "worktrees" / f"{req_id}-{task_id}.meta.json"
try: payload = json.loads(meta_path.read_text(encoding="utf-8")) except Exception: payload = {} if not isinstance(payload, dict): payload = {}
payload.setdefault("taskId", f"{req_id}-{task_id}") payload["state"] = "clean_failed" payload["last_activity_at"] = now payload["clean_failed"] = { "command": failed_command, "exit_code": exit_code, "message": error_message, "timestamp": now, } payload["error_message"] = error_message payload["exit_code"] = exit_code payload["failed_command"] = failed_command payload["failed_at"] = now
meta_path.parent.mkdir(parents=True, exist_ok=True) tmp_path = Path(str(meta_path) + ".tmp") tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") os.replace(tmp_path, meta_path) PY }
remove_worktree_safely() { local task_id="$1" local worktree_path="$2" local failed_command output status
failed_command="python3 ${PLUGIN_ROOT}/scripts/mst.py worktree remove --path ${worktree_path} --force" set +e output=$(python3 "${PLUGIN_ROOT}/scripts/mst.py" worktree remove --path "$worktree_path" --force 2>&1) status=$? set -e if [ "$status" -ne 0 ]; then echo "[mst:accept][WARN] worktree cleanup failed: task=${task_id} status=${status} path=${worktree_path}" >&2 record_clean_failed "$task_id" "$status" "$failed_command" "$output" fi return 0 }
delete_task_branch_safely() { local task_id="$1" local branch="$2" local failed_command output status
failed_command="git -C ${PROJECT_ROOT} branch -D ${branch}" set +e output=$(git -C "$PROJECT_ROOT" branch -D "$branch" 2>&1) status=$? set -e if [ "$status" -ne 0 ]; then echo "[mst:accept][WARN] task branch cleanup failed: task=${task_id} status=${status} branch=${branch}" >&2 record_clean_failed "$task_id" "$status" "$failed_command" "$output" fi return 0 }
delete_req_branch_safely() { local branch="$1" shift local failed_command output status task_id
failed_command="git -C ${PROJECT_ROOT} branch -D ${branch}" set +e output=$(git -C "$PROJECT_ROOT" branch -D "$branch" 2>&1) status=$? set -e if [ "$status" -ne 0 ]; then echo "[mst:accept][WARN] request branch cleanup failed: status=${status} branch=${branch}" >&2 for task_id in "$@"; do record_clean_failed "$task_id" "$status" "$failed_command" "$output" done fi return 0 }
cleanup_task_safely() { local task_id="$1" local worktree_path="$2" local task_branch="$3"
remove_worktree_safely "$task_id" "$worktree_path" delete_task_branch_safely "$task_id" "$task_branch" }
- 태스크별 실행 예시:
```bash
cleanup_task_safely "T01" "{PROJECT_ROOT}/.gran-maestro/worktrees/REQ-NNN-T01" "${TASK_BRANCH_PREFIX}01"
cleanup_task_safely "T02" "{PROJECT_ROOT}/.gran-maestro/worktrees/REQ-NNN-T02" "${TASK_BRANCH_PREFIX}02"
delete_req_branch_safely "${REQ_BRANCH}" "T01" "T02"
```
4.5. **Pending Stitch 화면 재확인**:
- `request.json`의 `stitch_screens` 배열에서 `status: "pending"` 항목 확인
- 없으면 이 단계 스킵
- 있으면: `mcp__stitch__list_screens(projectId)` 호출 (projectId는 `config.stitch.project_id`)
- **`baseline_screen_ids` 있는 경우**:
- 현재 screen IDs = `screens[].name`에서 마지막 `/` 이후 값 추출
- 차집합 = 현재 screen IDs - pending 항목의 `baseline_screen_ids`
- 차집합 비어있지 않으면: 첫 번째 ID로 `get_screen` 호출 → URL 확보 → 발견 처리
- 차집합 비어있으면: 미발견 처리
- **`baseline_screen_ids` 없는 경우** (구버전 pending 호환):
- 타임스탬프 비교 불가 → 미발견 처리
- **발견 시**: `get_screen`으로 URL 확보 →
`stitch_screens`의 pending 항목을 아래 필드로 업데이트:
`stitch_screen_id`, `url` (`https://stitch.withgoogle.com/projects/{project_id}`),
`image_url` (`screenshot.downloadUrl` 또는 null), `status: "active"`
→ "[Stitch] 화면 확인 완료 — {screen title}" 출력
- **미발견 시**: pending 유지 →
"[Stitch] 화면 미확인 — /mst:stitch --list로 수동 확인 가능합니다." 출력
5. **Phase 5 완료 처리**: `stitch_screens`의 `active` 항목 → `archived`로 변경; **스크립트 우선**: `python3 {PLUGIN_ROOT}/scripts/mst.py request set-phase {REQ_ID} 5 done`; 실패 시 fallback으로 `current_phase`=5, `status`=`done` 직접 업데이트; 완료 알림
> ⚠️ **CONTINUATION GUARD**: 서브스킬 반환 후 즉시 다음 Step 진행 (hook이 자동 강제).
5.1. **워크플로우 상태 정리 (MANDATORY)**: Phase 5 완료 처리 직후, `python3 {PLUGIN_ROOT}/scripts/mst.py state get --json` 결과의 `agile_loop_active`를 먼저 확인한 뒤 아래 분기로 실행한다.
- `agile_loop_active=true`이면, 워크플로우를 끄지 말고 agile 복귀 상태로 복원:
```bash
MST_STATE_PPID="${PPID}" python3 {PLUGIN_ROOT}/scripts/mst.py state set-workflow \
--active true \
--skill mst:agile \
--req "{ACTIVE_REQ}" \
--auto {AUTO_MODE} \
|| echo "[mst:accept] warning: failed to restore agile workflow state" >&2
--auto 값은 현재 accept의 AUTO_MODE(= Step 0.1 판정 결과)를 그대로 전달하여
retrospective → 새 sprint 진입 시 AUTO_MODE가 false로 덮이지 않도록 한다.
기존 --auto false 고정 호출(clear 경로, :186)은 변경하지 않는다
(active=false로 state를 닫을 때는 auto_mode도 의미가 없으므로 false 유지).
agile_loop_active!=true이면, 기존처럼 워크플로우 비활성:
MST_STATE_PPID="${PPID}" python3 {PLUGIN_ROOT}/scripts/mst.py state set-workflow \
--active false \
--auto false \
|| echo "[mst:accept] warning: failed to clear workflow state" >&2
이 호출은 workflow 종료용이며 --auto false 고정을 유지한다.
두 호출 모두 비차단(non-blocking)으로 처리한다: 실패 시 경고만 출력하고 워크플로우를 계속 진행한다.
approve의 state set-workflow --active true 호출과 대칭을 이룬다.
5.5. 후속 REQ 활성화 (Dependency Unblock):
request.json의 dependencies.blocks 배열 확인{PROJECT_ROOT}/.gran-maestro/requests/{BLOCKED-REQ-ID}/request.json Read
b. status가 pending_dependency인지 확인 (아니면 스킵)
c. dependencies.blockedBy 배열에서 현재 완료된 REQ-ID를 제거
d. blockedBy 배열이 비어지면:
request.json의 status를 "phase1_analysis"로 변경[활성화] {BLOCKED-REQ-ID} 의존성 해소 — Phase 1 분석 시작status == "spec_ready") 후:
workflow.auto_approve_on_unblock == true이면:
[자동 실행] {BLOCKED-REQ-ID} 의존성 해소 완료 → approve 자동 실행 중...Skill(skill: "mst:approve", args: "{BLOCKED-REQ-ID}") 호출false이면: 기존과 동일하게 /mst:approve {BLOCKED-REQ-ID} 안내
e. blockedBy가 아직 남아있으면 현재 REQ-ID만 제거하고 pending_dependency 유지
5.6. DAG 자동 연쇄 실행 게이트 (수동 수락 경로 지원):workflow.auto_accept_result=false로 /mst:accept를 수동 호출한 경로에서도 DAG 연쇄를 동일하게 보장workflow.auto_accept_result == falserequest.json.source_plan이 "PLN-NNN" 형태로 존재request.json.dag_auto_chain == truedone 또는 completed 또는 accepted{PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json Read 후 linked_requests 전체를 재평가한다 (after=current 방식 금지)pending_dependency, phase1_analysis, spec_ready를 모두 포함한다done, completed, accepted, cancelled)는 제외한다dependencies.blockedBy가 모두 해소된 경우에만 실행한다Skill(skill: "mst:request", args: "--plan {source_plan} --resume {next_req.id} -a")mst:request는 기존 REQ 재개 모드로 동작해야 하며 신규 REQ를 생성하면 안 된다done/completed/accepted가 아니면 즉시 중단[DAG 연쇄 중단] {REQ-ID} 실패. 후속 REQ: {REQ-ID 목록}linked_requests 전체가 done/completed/accepted면 완료 보고:
[DAG 연쇄 완료] PLN-NNN의 모든 REQ가 완료되었습니다. ...source_plan(예: PLN-NNN) 있으면: {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json Read → linked_requests 내 모든 REQ 상태 확인done/completed/아카이브 시: 스크립트 우선 python3 {PLUGIN_ROOT}/scripts/mst.py plan sync {source_plan}; 실패 시 fallback으로 plan.json의 status="completed" + completed_at 직접 업데이트source_plan 없으면 스킵[MST skill={name} step={N}/{M} return_to={parent_skill/step | null}]skill: 현재 실행 중인 스킬 이름step: 현재 단계(N/M) 또는 서브스킬 종료 시 returnedreturn_to: 최상위 스킬이면 null, 서브스킬이면 {parent_skill}/{step_number}[MST skill={subskill} step=returned return_to={parent/step}][MST skill={name} step=1/3 return_to=null][MST skill={subskill} step=returned return_to={parent_skill}/{step_number}]/mst:accept # 최종 수락 대기 중인 첫 번째 요청 자동 선택
/mst:accept REQ-001 # 명시적으로 REQ-001 최종 수락
workflow.auto_accept_result (기본: true): true → 자동 수락; false → 수동 호출 필요
/mst:settings workflow.auto_accept_result false
/mst:inspect {REQ-ID}로 Phase 3 PASS 상태 확인/mst:feedback으로 피드백 루프 먼저 완료/mst:inspect {REQ-ID} 확인