From mst
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.
npx claudepluginhub myrtlepn/gran-maestro --plugin mstThis skill uses the workspace's default tool permissions.
PM์ด ์์ฑํ ๊ตฌํ ์คํ์ ์น์ธํ๊ณ Phase 2 ์คํ์ ์์ํฉ๋๋ค. ๋จ๊ฑด/๋ฐฐ์น ์น์ธ ๋ชจ๋ ์ง์. Phase 3 PASS ํ ์ต์ข ์๋ฝ์ `workflow.auto_accept_result` ์ค์ ์ ๋ฐ๋ผ ์๋ ์คํ.
Analyzes user requirements and generates implementation specs (spec.md) for Gran Maestro workflow tasks. Bootstraps project directories/configs and enforces separate approval via /mst:approve.
Orchestrates 4-phase execution loop (IMPLEMENT, VALIDATE, ADVERSARIAL REVIEW, COMMIT) for complex work units with specs. Verifies outputs adversarially in multi-agent setups.
Share bugs, ideas, or general feedback.
PM์ด ์์ฑํ ๊ตฌํ ์คํ์ ์น์ธํ๊ณ Phase 2 ์คํ์ ์์ํฉ๋๋ค. ๋จ๊ฑด/๋ฐฐ์น ์น์ธ ๋ชจ๋ ์ง์. Phase 3 PASS ํ ์ต์ข
์๋ฝ์ workflow.auto_accept_result ์ค์ ์ ๋ฐ๋ผ ์๋ ์คํ.
[TRACE_SAVED] ๋ฑ)๋ฅผ ์ข
๋ฃ ์ ํธ๋ก ์คํดํด ๋ค์ Step ํธ์ถ ์์ด ๋ฉ์ถ๋ค.NEXT_ACTION ์ถ๋ ฅ ์งํ ๋ค์ Step ๋๊ตฌ ํธ์ถ์ ์คํํ๋ค.mst:request --resume ... -a๋ฅผ ์ฆ์ ํธ์ถํ๋ค.๊ฒฝ๋ก ๊ท์น (MANDATORY): ์ด ์คํฌ์ ๋ชจ๋
.gran-maestro/๊ฒฝ๋ก๋ ์ ๋๊ฒฝ๋ก๋ก ์ฌ์ฉํฉ๋๋ค. ์คํฌ ์คํ ์์ ์PROJECT_ROOT๋ฅผ ์ทจ๋ํ๊ณ , ์ดํ ๋ชจ๋ ๊ฒฝ๋ก์{PROJECT_ROOT}/์ ๋์ฌ๋ฅผ ๋ถ์ ๋๋ค.PROJECT_ROOT=$(pwd)
{PLUGIN_ROOT}๋ ์ด ์คํฌ์ "Base directory"์์skills/{์คํฌ๋ช }/์ ์ ๊ฑฐํ ์ ๋๊ฒฝ๋ก์ ๋๋ค. ์๋๊ฒฝ๋ก(.claude/...)๋ ์ ๋ ์ฌ์ฉํ์ง ์์ต๋๋ค.
~/.claude/user-profile.json (AskUserQuestion ์ปจํ
์คํธ, ๋น์ฐจ๋จ)~/.claude/user-profile.json์ Readํ๋ค.
user_profile_context = null๋ก ์ฒ๋ฆฌํ๊ณ ๊ธฐ์กด ๋์์ ์ ์งํ๋ค (graceful fallback).role (string)experience_level (string)domain_knowledge (string[])communication_style (string)user_profile_context = null๋ก ์ฒ๋ฆฌํ๋ค (์ํฌํ๋ก์ฐ ์ฐจ๋จ ๊ธ์ง).AskUserQuestion๊ณผ ์ฌ์ฉ์ ์ค๋ช
ํ
์คํธ ์์ฑ ์:
communication_style์ ์ต์ฐ์ ๋ฐ์ํ๋ค.experience_level/domain_knowledge์ ๋ง์ถฐ ์ฉ์ด ์์ค๊ณผ ์ค๋ช
๊น์ด๋ฅผ ์กฐ์ ํ๋ค.์ธ๋ถ ์์กด์ฑ(๋ผ์ด๋ธ๋ฌ๋ฆฌ/API/ํ๋ ์์ํฌ/๋ฒ์ /ํ๋กํ ์ฝ) ํ๋จ์ ์๋ ๊ณตํต ํ๋กํ ์ฝ์ ๋ฐ๋ฅธ๋ค.
Bash(python3 {PLUGIN_ROOT}/scripts/mst.py config get reference.auto_search)๋ก reference.auto_search๋ฅผ ํ์ธํ๋ค. true์ผ ๋๋ง ์๋ WebSearch๋ฅผ ํ์ฉํ๋ค. ์ค์ ๋ฏธ์กด์ฌ ์ ๊ธฐ๋ณธ๊ฐ์ cache_ttl_days=2, cutoff_threshold_months=0.5, max_searches_per_step=5, llm_auto_trigger=true, auto_fact_check=true.reference.llm_auto_trigger == true์ด๋ฉด PM์ด ์ต์ ์ ๋ณด๊ฐ ํ์ํ๋ค๊ณ ํ๋จํ ๋๋ WebSearch๋ฅผ ํธ๋ฆฌ๊ฑฐํ๋ค. false์ด๋ฉด ํค์๋ ๋งค์นญ ๊ธฐ๋ฐ ๋์๋ง ์ ์งํ๋ค..gran-maestro/references/ ์บ์๋ฅผ reference search --keyword "{keyword}" --json์ผ๋ก ํ์ธ, (b) searched_at + cache_ttl_days ๊ธฐ์ค fresh/stale ํ์ , (c) ํ์ฌ ์๊ฐ ๋๋น cutoff_threshold_months ์ด๊ณผ ์ expired ํ์ .stale/expired์ผ ๋๋ง ๊ฒ์ํ๋ค. reference.auto_search == true์ผ ๋๋ง ์คํํ๊ณ Step๋น max_searches_per_step์ ๋์ง ์๋๋ค. reference.auto_fact_check == true์ด๋ฉด ํต์ฌ claim์ 1ํ์ฑ ๊ต์ฐจ WebSearch๋ก ๊ฒฝ๋ ๊ฒ์ฆํ๋ค.Bash๋ก mst.py reference add๋ฅผ ํธ์ถํ๋ค. ํ/ํ
์คํธ ๊ฒฐ๋ก ์์ฝ๋ง์ผ๋ก๋ ์ ์ฅ ์๋ฃ๊ฐ ์๋๋ฉฐ content.md๋ raw ๋ฐ์ท(์๋ฌธ ๊ทผ๊ฑฐ) ์ค์ฌ์ผ๋ก ๋จ๊ธด๋ค.
python3 {PLUGIN_ROOT}/scripts/mst.py reference add --topic "{topic}" --url "{url}" --summary "{summary}" --content "{raw ๋ฐ์ท ๋ณธ๋ฌธ}"summary๋ ํ ์ค ์ธ๋ฑ์ค ์ ์ง).> ์ธ์ฉ: "{์๋ฌธ ํต์ฌ ๋ฌธ์ฅ}" (์ถ์ฒ: {URL}, ๋ ์ง: {YYYY-MM-DD})| ์ด | ๊ฐ | ํํ์ raw markdown table๊ณผ ์ถ์ฒ URL์ ๋ณด์กดํ๋ค.content.md Read ํ์): summary๋ง์ผ๋ก ๋ถ์กฑํ๊ฑฐ๋ ํ/์ฝ๋/์๋ฌธ ๋์์ค๊ฐ ๊ฒฐ๋ก ์ ์ํฅ์ ์ฃผ๋ฉด ๋ฐ๋์ content.md๋ฅผ Readํ๋ค.[REFERENCE_CONTEXT]๋ฅผ ์ฃผ์
ํ๋ค. ํ์: current_date, model_cutoff, references: REF-001 (fresh|stale|expired) {topic} | {url}. ์ฐธ์กฐ๊ฐ ์์ผ๋ฉด references: none์ผ๋ก ๋ช
์ํ๋ค.$ARGUMENTS๋ฅผ ํ์ฑํ์ฌ ์น์ธ ๋์ REQ ๋ฆฌ์คํธ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. ์๋ ๊ท์น์ ์์๋๋ก ์ ์ฉํฉ๋๋ค.
$ARGUMENTS๊ฐ ๋จ์ผ REQ ํจํด(REQ-NNN)์ด๋ฉด ๋จ๊ฑด ์น์ธ ํ๋กํ ์ฝ์ ์ง์ ์คํํฉ๋๋ค.
๊ณต๋ฐฑ ๊ตฌ๋ถ REQ ํจํด์ด 2๊ฐ ์ด์์ด๋ฉด ํ ๊ธ UI ์์ด ์ง์ ๋ฐฐ์น ์คํํฉ๋๋ค.
์ฝค๋ง(,)๋ ๋ฒ์(..) ํฌํจ ์ธ์๋ฅผ ํ์ฑํฉ๋๋ค. ์์:
/mst:approve REQ-001,REQ-003,REQ-005 โ [REQ-001, REQ-003, REQ-005]
/mst:approve REQ-001..005 โ [REQ-001, REQ-002, REQ-003, REQ-004, REQ-005]
๋ฒ์ ์ง์ ์ ์น์ธ ๊ฐ๋ฅ ์ํ์ธ REQ๋ง ๊ฒฐ๊ณผ ๋ฆฌ์คํธ์ ํฌํจ.
--priority ํํฐ๋ง--priority <level> ํ๋๊ทธ๊ฐ ์์ผ๋ฉด ํด๋น ์ฐ์ ์์์ ์น์ธ ๊ฐ๋ฅ REQ๋ง ํํฐ๋งํฉ๋๋ค. request.json์ priority ํ๋ ๊ธฐ์ค. ํ๋ ์๋ REQ๋ normal๋ก ์ทจ๊ธ. REQ ํจํด/๋ฒ์์ ์กฐํฉ ๊ฐ๋ฅ.
$ARGUMENTS์ REQ ํจํด์ด ์๊ณ ํ๋๊ทธ๋ง ์๊ฑฐ๋ ์์ ํ ๋น์ด ์๋ ๊ฒฝ์ฐ:
์คํฌ๋ฆฝํธ ์ฐ์ : python3 {PLUGIN_ROOT}/scripts/mst.py request filter --phase 1 --format json ์คํ ํ status๊ฐ phase1_analysis ๋๋ pending_dependency๊ฐ ์๋ ๊ฒ ํํฐ๋ง. ์คํจ ์ fallback.
Fallback:
{PROJECT_ROOT}/.gran-maestro/requests/ ๋๋ ํ ๋ฆฌ์ ๋ชจ๋ request.json ์ค์บcurrent_phase == 1 ์ด๊ณ status๊ฐ phase1_analysis ๋๋ pending_dependency๊ฐ ์๋ ๊ฒ, ๋๋ status๊ฐ phase2_spec_review์ธ ๊ฒ--priority ํํฐ ์์ผ๋ฉด ์ถ๊ฐ ์ ์ฉ, REQ ๋ฒํธ ์ค๋ฆ์ฐจ์ ์ ๋ ฌ| ์น์ธ ๋๊ธฐ REQ ์ | ํ๊ฒฝ | ๋์ |
|---|---|---|
| 0๊ฐ | โ | "์น์ธ ๋๊ธฐ ์ค์ธ ์์ฒญ์ด ์์ต๋๋ค" ๋ฉ์์ง ํ ์ข ๋ฃ |
| 1๊ฐ | โ | ๊ธฐ์กด ๋จ๊ฑด ๋์ ๊ทธ๋๋ก (์คํ ์์ฝ โ ์น์ธ โ Phase 2) |
| 2๊ฐ+ | ๋ํํ (TTY) | ํ ๊ธ ์ ํ UI ์ง์ (์๋ ์ฐธ์กฐ) |
| 2๊ฐ+ | ๋น๋ํํ | ๊ธฐ์กด ๋์ ์ ์ง (์ฒซ ๋ฒ์งธ REQ ์๋ ์ ํ, ๋จ๊ฑด ์คํ) |
์น์ธ ๋๊ธฐ REQ๊ฐ 2๊ฐ ์ด์์ด๊ณ ๋ํํ(TTY) ํ๊ฒฝ์ผ ๋:
๋ฐฐ์ง ์์ฑ ๊ท์น: dependencies.blockedBy โ [โREQ-MMM], dependencies.blocks โ [โREQ-PPP], ๋ณตํฉ [โMMM โPPP]. ์์ผ๋ฉด ์๋ต.
AskUserQuestion์ multiSelect ์ต์
์ฌ์ฉ:
label: "REQ-NNN โ {title} [โREQ-MMM โREQ-PPP]" (๋ฐฐ์ง ์์ ๋)description: "Phase 1 ์๋ฃ, ํ์คํฌ N๊ฐ | ์ ํ: REQ-MMM | ํํ: REQ-PPP" (์์กด์ฑ ์์ ๋)REQ-NNN โ {title} [๋ฐฐ์ง] [ํ์คํฌ M๊ฐ] ํ์AUTO_MODE ์ด๊ธฐํ (๋จ๊ฑด ํ๋กํ ์ฝ ์ง์ ์ฆ์):
--auto ๋๋ -a๊ฐ ์์ผ๋ฉด AUTO_MODE=true (์ต์ฐ์ ){PLUGIN_ROOT}/scripts ๊ฒฝ์ ๋ก read_workflow_state_auto_mode("mst:approve", "{REQ-ID}") ํธ์ถ
AUTO_MODE์ ์ฑํNone์ด๋ฉด request.json.auto_approve == true ์ฌ๋ถ๋ฅผ ํ์ธconfig.auto_mode.approve ํ์ธAUTO_MODE=false์ฐ์ ์์: args > state(guarded, expected_source_id=REQ_ID) > request.json.auto_approve > config > false
์ดํ ๋ชจ๋ Step์์ ์ด ๋ณ์๋ฅผ ์ฌ์ฉํ๋ค.
AUTO_MODE=true์ด๋ฉด ๋จ๊ฑด ํ๋กํ ์ฝ ์ง์
์งํ workflow state๋ฅผ ๊ธฐ๋กํ๋ค (non-blocking):
# RESOLVED(PLN-509): agile_loop_active ๋ณด์กด โ plan/agile ๋งฅ๋ฝ์ Step 4b ๋ธ๋ฆฌํ ๋ณ์(PLAN_JSON_META/PAC_LIST/OBJECTIVE_SECTION)๋ก ์ฃผ์
๋จ (PLN-469 โ PLN-509)
MST_STATE_PPID="${PPID}" python3 {PLUGIN_ROOT}/scripts/mst.py state set-workflow \
--active true \
--skill mst:approve \
--req "{REQ-ID}" \
--next-skill mst:accept \
--next-source "{REQ-ID}" \
--source-skill mst:approve \
--auto true \
|| echo "[mst:approve] warning: failed to update workflow state" >&2
AUTO_MODE=false์์๋ ์ด ํธ์ถ์ ์คํํ์ง ์๋๋ค.
์ธ์
์ค ์์จ ๋ชจ๋ ์ ํ: AskUserQuestion ๋๊ธฐ ์ค ์ฌ์ฉ์๊ฐ "auto๋ก ํด์ค", "์์จ ๋ชจ๋๋ก", "-a๋ก", "์ง๊ธ๋ถํฐ ์๋์ผ๋ก" ๋ฑ์ ์
๋ ฅํ๋ฉด ์ฆ์ AUTO_MODE=true๋ก ์ ํํฉ๋๋ค. ์ ํ ์ฆ์ [์์จ ๋ชจ๋ ์ ํ] ์ด์ ๋ถํฐ -a ๋ชจ๋๋ก ์งํํฉ๋๋ค. ์ถ๋ ฅ ํ ํ์ฌ Step๋ถํฐ AUTO_MODE=true ์ ์ฉํ์ฌ ์ฌ๊ฐ.
REQ ๋ฆฌ์คํธ๊ฐ 1๊ฑด์ด๊ฑฐ๋ ๋ช ์์ ๋จ๊ฑด ์ธ์ ํธ์ถ ์ ์ด ํ๋กํ ์ฝ์ ์คํํฉ๋๋ค.
{PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/ ํ์ spec.md ํ์ธ
dependencies.blocks ๋น์ด์์ง ์์ AND workflow.auto_approve_on_unblock == false):AUTO_MODE=true ๋๋ request.json.auto_approve=true์ด๋ฉด AskUserQuestion ์์ด ๊ธฐ๋ณธ๊ฐ("์๋์ค, ๊ฐ ๋จ๊ณ๋ง๋ค ์๋ approve") ์ ์ฉ ํ ์ฆ์ Step 2.5๋ก ์งํ์ด REQ๊ฐ ์๋ฃ๋๋ฉด ์๋ REQ๋ค์ด ์์๋๋ก ์คํ ๊ฐ๋ฅํด์ง๋๋ค:
REQ-NNN โ {title} (๋๊ธฐ ์ค)
REQ-MMM โ {title} (๋๊ธฐ ์ค) โ REQ-NNN ์๋ฃ ํ
(blocks ๋ฐฐ์ด์ ์ง์ ํ์ REQ๋ง ํ์; ์ฌ๊ท ์กฐํ๋ 1๋จ๊ณ๋ง)config.json์ workflow.auto_approve_on_unblock์ true๋ก ์
๋ฐ์ดํธ
์๋ฆผ: "โ ์ดํ ๋ชจ๋ ์ฒด์ธ์์ ์์กด์ฑ ํด์ ์ ์๋ approve๊ฐ ์คํ๋ฉ๋๋ค. (/mst:settings workflow.auto_approve_on_unblock false๋ก ๋๋๋ฆด ์ ์์ต๋๋ค)"๊ตฌํ์ ์์ํ๊ธฐ ์ ์๋ ๊ฒ์ฌ๋ฅผ ์ํํ๋ค:
"Test Scenarios (Pre-Impl)" ๋ฌธ์์ด ํฌํจ ๊ฒ์ฌ(contains) โ "## Test Scenarios (Pre-Impl)" (๋ฒํธ ์์) ๋๋ "## N.N Test Scenarios (Pre-Impl)" (๋ฒํธ ์์) ๋ชจ๋ ํ์ฉ.Test: ํญ๋ชฉ(์คํ ๋ช
๋ น ๋๋ ํ์ธ ๋ฐฉ๋ฒ) ๊ธฐ์
์ฌ๋ถ ํ์ธํต๊ณผ ์กฐ๊ฑด: ์น์ ์กด์ฌ + ๋ชจ๋ automatable AC์ Test ํญ๋ชฉ ๊ธฐ์ ์คํจ ์: ๊ตฌํ ์ฐฉ์ ์ค๋จ โ "Pre-Impl Test Scenarios ๋ฏธ์์ฑ" ์ค๋ฅ ๋ฐํ
์์ธ: manual AC๋ง ์๋ spec์ Test Scenarios ์น์ ์ด ๋น์ด์์ด๋ ํต๊ณผ ํ์ฉ
preflight ๊ฒ์ฌ๊ฐ ํต๊ณผ๋ ๊ฒฝ์ฐ์๋ง ์๋ base ๊ฐ์ง/protected ๊ฒ์ฌ๋ฅผ ์คํํ๊ณ , ์ด ๊ฒ์ฌ๊ฐ ํต๊ณผ๋ ๊ฒฝ์ฐ์๋ง Step 3(worktree ์์ฑ ๋ฐ ๊ตฌํ ์ฐฉ์)๋ก ์งํ.
HEAD base ์๋ ๊ฐ์ง + protected block (์ฐจ๋จ ๊ฒ์ฌ, preflight ํต๊ณผ ์ดํ ์คํ):
Bash(python3 {PLUGIN_ROOT}/scripts/mst.py worktree resolve-base --req {REQ-ID})๋ฅผ ์คํํ๋ค.
DETECTED_BASE๋ก ์ฌ์ฉํ๋ค.{PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/request.json์ detected_base ํ๋๊ฐ ๊ฐ์ ๊ฐ์ผ๋ก ์ ์ฅ๋์ด์ผ ํ๋ค.resolve-base๋ ํ์ฌ git HEAD ๋ธ๋์น๋ฅผ base๋ก ๊ฐ์งํ๊ณ , worktree.protected_branches ํจํด(release/* glob ํฌํจ)๊ณผ ๋งค์นญ๋๋ฉด non-zero๋ก ์ข
๋ฃํ๋ค. ์คํจ ์ stderr ์๋ด("๋ค๋ฅธ ๋ธ๋์น๋ก ์ด๋" ํฌํจ)๋ฅผ ์ ๋ฌํ๊ณ approve๋ฅผ ์ฐจ๋จํ๋ค.worktree.base_branch๋ ํ์ ํธํ ์ค์ ์ผ๋ก๋ง ๋จ๊ธฐ๋ฉฐ approve์ ์ ๊ท base ๊ฒฐ์ ์๋ ์ฌ์ฉํ์ง ์๋๋ค.python3 {PLUGIN_ROOT}/scripts/mst.py request set-phase {REQ_ID} 2 phase2_execution; ์คํจ ์ fallback์ผ๋ก request.json์ current_phase=2, status=phase2_execution ์ง์ ์
๋ฐ์ดํธstrategy.worktree_policy == "skip"์ด๋ฉด worktree ์์ฑ์ ์คํตํ๊ณ {PROJECT_ROOT}์์ ์ง์ ์์
, ๊ทธ๋ ์ง ์์ผ๋ฉด ๊ฐ ํ์คํฌ์ ๋ํด git worktree ์์ฑREQ ๋ฆฌ์คํธ๊ฐ 2๊ฑด ์ด์์ผ ๋, ๋ฐฐ์น ์คํ ๋ฃจํ ์ง์ ์ ์ ํ๋ REQ ์งํฉ์ ์์กด์ฑ ์๋ฐ์ ๊ฒ์ฌํฉ๋๋ค.
violations = []
for req_id in selected:
req = read_request_json(req_id)
for dep in req.dependencies.blockedBy:
if dep not in selected:
violations.append({ req: req_id, missing_prereq: dep })
if violations:
์ถ๋ ฅ: "โ ๏ธ ์์กด์ฑ ์๋ฐ ๊ฐ์ง:"
for v in violations:
์ถ๋ ฅ: " - {v.req}์ {v.missing_prereq}์ด ๋จผ์ ์๋ฃ๋์ด์ผ ํ๋ ์ ํ ๋ชฉ๋ก์ ์์"
AskUserQuestion:
- "๋๋ฝ๋ ์ ํ REQ ์ถ๊ฐํ์ฌ ์ ์ฒด ์ฒด์ธ ์คํ" โ ๋๋ฝ REQ๋ฅผ selected์ ์ถ๊ฐ ํ ์ฌ์งํ
- "ํํ REQ ์ ์ธํ๊ณ ์ ํ๋ ๊ฒ๋ง ์คํ" โ violations์ ํํ REQ๋ฅผ selected์์ ์ ๊ฑฐ ํ ์ฌ์งํ
- "์ทจ์" โ ์ข
๋ฃ
์๋ฐ์ด ์๊ฑฐ๋ ์ฌ์ฉ์ ์ ํ ํ ์ฌ์งํ ์, ์๋ ๋ฐฐ์น ์คํ ๋ฃจํ๋ก ์ง์ ํฉ๋๋ค.
REQ ๋ฆฌ์คํธ๊ฐ 2๊ฑด ์ด์์ผ ๋ ์คํํฉ๋๋ค.
| ํ๋๊ทธ | ๋์ |
|---|---|
| (๊ธฐ๋ณธ, ํ๋๊ทธ ์์) | ์์ฐจ ์คํ โ ๊ฐ REQ์ ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด(Phase 2 โ 3 โ 5) ์๋ฃ ํ ๋ค์ REQ |
--parallel | ๋ณ๋ ฌ ์คํ โ concurrency.batch_max_parallel_reqs๋งํผ REQ๋ฅผ ๋์ ์คํ |
์์กด์ฑ ํ ํด๋ก์ง ์ ๋ ฌ์ ์ํํ์ฌ Wave ๋จ์๋ก ์คํํฉ๋๋ค. ์์กด์ฑ์ด ์๋ REQ๋ ๋จ์ผ Wave๋ก ๋ฌถ์ ๋๋ค.
topological_sort_into_waves ์๊ณ ๋ฆฌ์ฆ:
def topological_sort_into_waves(req_ids):
in_degree = {r: 0 for r in req_ids}
for r in req_ids:
for dep in read_request_json(r).dependencies.blockedBy:
if dep in req_ids:
in_degree[r] += 1
waves = []
remaining = set(req_ids)
while remaining:
wave = [r for r in remaining if in_degree[r] == 0]
if not wave: # ์ฌ์ดํด ๊ฐ์ง
๊ฒฝ๊ณ : "์์กด์ฑ ์ฌ์ดํด ๊ฐ์ง, ๋จ์ REQ๋ ๋
๋ฆฝ ์คํ"
wave = list(remaining)
waves.append(sorted(wave))
for r in wave:
remaining.remove(r)
for s in remaining:
if r in read_request_json(s).dependencies.blockedBy:
in_degree[s] -= 1
return waves
Wave ์บ์ค์ผ์ด๋ ์คํ:
waves = topological_sort_into_waves(req_list)
์ถ๋ ฅ: "์คํ ๊ณํ:"
for i, wave in enumerate(waves):
์ถ๋ ฅ: " Wave {i+1}: {wave} (์์ฐจ ์คํ)"
all_results = []
outer: for wave_num, wave in enumerate(waves):
์ถ๋ ฅ: "โโ Wave {wave_num+1}/{len(waves)} ์์ โโ"
wave_results = []
for req_id in wave:
result = ๋จ๊ฑด ์น์ธ ํ๋กํ ์ฝ ์คํ(req_id, AUTO_MODE=ํ์ฌ AUTO_MODE ๊ฐ)
wave_results.append(result)
if result == FAILED:
์ค๋ฅ ์ฒ๋ฆฌ ๊ท์น ์ ์ฉ (ยง ๋ฐฐ์น ์ค๋ฅ ์ฒ๋ฆฌ)
if ์ค๋จ ๊ฒฐ์ :
๋จ์ REQ (ํ์ฌ Wave ๋ฏธ์คํ + ์ดํ Wave ์ ์ฒด) โ skipped
break outer
all_results.extend(wave_results)
failed_in_wave = [r.req_id for r in wave_results if r.status == FAILED]
if failed_in_wave:
์ดํ Wave์์ failed REQ๋ฅผ blockedBy๋ก ๊ฐ์ง REQ๋ค โ ์๋ Skip ๋งํน
์ถ๋ ฅ: "์์กด REQ N๊ฐ๋ฅผ Skipํฉ๋๋ค" ์๋ฆผ
์ต์ข
์์ฝ ์ถ๋ ฅ(all_results)
--parallel)--parallel ํ๋๊ทธ ์ฌ์ฉ ์์๋ Wave ๊ฒฝ๊ณ๋ ์ค์ํฉ๋๋ค. Wave ๋ด REQ๋ค์ ๋ณ๋ ฌ ์คํํ๊ณ , Wave ๊ฐ์๋ ์์ฐจ ์ ์ง.
config.concurrency.batch_max_parallel_reqs ๊ฐ์ผ๋ก ๋์ ์คํ REQ ์๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
max_concurrent = config.concurrency.batch_max_parallel_reqs # ๊ธฐ๋ณธ 1
queue = req_list.copy()
running = {}
results = []
while queue ๋๋ running:
while len(running) < max_concurrent and queue:
req_id = queue.pop(0)
if has_failed_dependency(req_id, results):
results.append({req_id, status: "skipped", reason: "์์กด REQ ์คํจ"})
continue
์ถ๋ ฅ: "[์งํ] {req_id} โ ์น์ธ ์์..."
task = ๋น๋๊ธฐ๋ก ๋จ๊ฑด ์น์ธ ํ๋กํ ์ฝ ์คํ(req_id) # run_in_background
running[req_id] = task
for req_id, task in running:
if task.completed:
results.append(task.result)
running.remove(req_id)
์ถ๋ ฅ: "[์๋ฃ] {req_id} โ {status}"
sleep(backoff)
์ต์ข
์์ฝ ์ถ๋ ฅ(results)
์ฌ๋กฏ ๊ด๋ฆฌ: ์ ์ญ ๋์ ํ์คํฌ ์๋
min(batch_max_parallel_reqs ร max_tasks_per_req, worktree.max_active)๋ก ์ ํ.
์์ฐจ: [1/3] REQ-013 "JWT ๋ฏธ๋ค์จ์ด" โ ์น์ธ ์ค... โ ์คํ ์ค... โ ์๋ฃ
๋ณ๋ ฌ: [๋ณ๋ ฌ 2/3] REQ-013 ์์ | REQ-014 ์์
์ต์ข ์์ฝ:
โโโ ๋ฐฐ์น ์น์ธ ์๋ฃ โโโ
์ฑ๊ณต: 2 | ์คํจ: 1 | ๊ฑด๋๋: 0
REQ-015: Phase 2 ์ฌ์ ๊ฒ์ฆ ์คํจ (tsc error) โ /mst:approve REQ-015 ๋ก ์ฌ์๋
| ํ๊ฒฝ | ๊ธฐ๋ณธ ๋์ | ์ธ๋ถ |
|---|---|---|
| ๋ํํ (TTY) | Prompt | Continue / Skip / Retry / Abort 4์ง์ ๋ค ์ ์. ๊ธฐ๋ณธ ์ปค์ ์์น: Continue |
| ๋น๋ํํ (CI) | Continue | ์คํจ REQ๋ failed ๋งํน ํ ๋๋จธ์ง ๊ณ์ ์งํ. ์ต์ข
exit code: ์คํจ 1๊ฑด ์ด์์ด๋ฉด non-zero |
์์กด์ฑ ๊ธฐ๋ฐ ์์ธ: dependencies.blockedBy ๊ด๊ณ์์ ์ ํ REQ ์คํจ ์ ํ์ REQ ์๋ Skip (ํ๊ฒฝ ๋ถ๋ฌธ). blockedBy ๋ฏธ๊ธฐ์ฌ ์ ๋
๋ฆฝ REQ๋ก ์ทจ๊ธ.
ํ๋ ์์ ์: --stop-on-fail โ ์ฒซ ์คํจ ์ ์ฆ์ ์ค๋จ. --continue โ ์คํจ ๋ฌด์ ํ ๊ณ์. (์์กด์ฑ Skip์ ์์ชฝ ๋ชจ๋ ์ ์ง)
์คํจํ REQ์ status๋ฅผ failed๋ก ๋งํน. ์ฌ์ง์
: /mst:approve REQ-NNN ๋จ๊ฑด ํธ์ถ ๋๋ ๋ค์ ๋ฐฐ์น ์ ํ ๊ธ UI ์ฌ์ ํ.
OMX_AUTOPILOT = (config.omx.enabled == true && config.omx.autopilot == true) โ config.omx ํค ๋ฏธ์กด์ฌ ์ false๋ก ์ฒ๋ฆฌ (fallback)
์ด ๊ฐ์ Step 4c / Fix / Escalation์์ ์ฐธ์กฐํ๋ค.
Phase 2์์ Claude(PM)๋ ์ ๋ ์ฝ๋๋ฅผ ์ง์ ์์ฑํ์ง ์์ต๋๋ค. ๋ชจ๋ ๊ตฌํ์ /mst:codex ๋๋ /mst:gemini๋ก ์ธ์ฃผํฉ๋๋ค.
request.json.source_plan -> plan.json.type -> type-strategies.json ์ฒด์ธ์ผ๋ก ์คํ ์ ๋ต์ ๊ฒฐ์ ํ๋ค.
source_plan = request.json.source_plan
plan_type = "code"
if source_plan exists:
plan = Read({PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json)
plan_type = plan.type if plan.type exists else "code"
type_strategies = Read({PLUGIN_ROOT}/templates/defaults/type-strategies.json)
strategy = type_strategies[plan_type] || type_strategies["code"]
if Read/parse/key lookup failed:
strategy = {
"template": "templates/impl-request.md",
"worktree_policy": "required",
"review_mode": "code",
"accept_mode": "squash-merge"
} # ํ์ ํธํ
strategy.worktree_policy == "skip"์ด๋ฉด DocExecutor ์ ๋ต(๋ฌธ์ ์ด์ ์์ฑ โ ๊ตฌ์กฐ ๊ฒ์ฆ โ ํฉํธ์ฒดํฌ)์ ์ฌ์ฉํ๋ค.๊ฐ ํ์คํฌ์ spec.md๋ฅผ Readํ๊ธฐ ์ ๊ฒฝ๋ก ์ ํจ์ฑ์ ํ์ธํฉ๋๋ค:
"spec.md ์ฝ๊ธฐ ์คํจ (๊ฒฝ๋ก: {spec_path}) โ ์ํฌํธ๋ฆฌ ๊ตฌ์กฐ ํ์ธ ํ์" ์ค๋ฅ๋ฅผ ๋ฐํํ๊ณ ํด๋น ํ์คํฌ์ ๊ตฌํ ์ฐฉ์๋ฅผ ์ฐจ๋จํฉ๋๋ค.๋ชจ๋ ํ์คํฌ์ spec.md๋ฅผ ์ผ๊ด ๊ฒ์ฆํฉ๋๋ค. ๋ค์ ํญ๋ชฉ์ด ๋ช ํํ์ง ํ์ธ, ๋ถ์กฑํ๋ฉด ๋ณด์:
Ideation ์๋ ํธ๋ฆฌ๊ฑฐ (LLM ํ๋จ): ์๋ ์ํฉ ๊ฐ์ง ์ /mst:ideation ํธ์ถํ์ฌ ์คํ ๋ณด์:
spec.md ยง7์์ blockedBy ๋ฐฐ์ด ์ฝ๊ธฐblockedBy ๋น์ด์์ โ ์ฆ์ ์คํblockedBy ์์ โ ์ ํ ์๋ฃ ํ ์คํWave 1: {๋
๋ฆฝ ํ์คํฌ ๋ชฉ๋ก} (๋ณ๋ ฌ ์คํ)
Wave 2: {Wave 1 ์๋ฃ ํ ์คํ ๊ฐ๋ฅํ ํ์คํฌ} (๋ณ๋ ฌ ์คํ)
spec.md ํค๋์ Assigned Agent ํ๋๋ฅผ ์ฝ์ด ์์ด์ ํธ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
| ํ์คํฌ ์ ํ | ์์ด์ ํธ | capabilities |
|---|---|---|
| ๋ฐฑ์๋, ๋ฆฌํฉํ ๋ง, ํ ์คํธ | codex-dev โ /mst:codex | code, refactor, test |
์ ๊ท .ts ํ์ผ ์์ฑ, ๋จ์ ๋ฆฌํฉํ ๋งยท๋ณด์ผ๋ฌํ๋ ์ดํธ, ๋
๋ฆฝ ํ
์คํธ ์์ฑ, ์๊ท๋ชจ .ts ์ธ๋ผ์ธ ์์ | codex-dev โ /mst:codex | code, refactor, test |
| ํ๋ก ํธ์๋, ๋ฌธ์, ๋์ฉ๋ ์ปจํ ์คํธ | gemini-dev โ /mst:gemini | frontend, docs, large-context |
.md ๋ฌธ์, .json/.env config, *.config.ts, ๊ธฐ์กด .ts ์ธ๋ผ์ธ ์์ (์ ๊ท .ts ์์ฑ ์์) | claude-dev โ /mst:claude | code, docs, config, small-inline |
๊ฒฝ๊ณ ์ผ์ด์ค ๊ธฐ๋ณธ๊ฐ: ํ์คํฌ ์ ํ์ด ๋ชจํธํ ๊ฒฝ์ฐ โ
Bash(python3 {PLUGIN_ROOT}/scripts/mst.py config get workflow.default_agent)๊ฐ ์ฌ์ฉ (claude-devํ๋์ฝ๋ฉ ๊ธ์ง). CLI guard:codex-dev๋ฐฐ์ ์codex๋ช ๋ น์ด ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ์ฌ์ ํ์ธํ ๊ฒ.
claude์ claude-dev๋ ๋์ผํ๊ฒ ์ฒ๋ฆฌ๋ฉ๋๋ค (ํ์ ํธํ).
Assigned Agent ํ๋ ์ฝ๊ธฐ: (1) ์ต์ข
: ํจํด์ด ์์ผ๋ฉด ์ต์ข
: ์ดํ ๊ฐ์ ์์ด์ ํธ๋ช
์ผ๋ก ์ฌ์ฉ. (2) ์ต์ข
: ํจํด์ด ์์ผ๋ฉด ํ๋ ๊ฐ ์ ์ฒด๋ฅผ ์ฌ์ฉ. (3) ํ๋๊ฐ ์๊ฑฐ๋ ๋น์ด์์ผ๋ฉด workflow.default_agent๋ฅผ fallback์ผ๋ก ์ฌ์ฉ.
Assigned Agent: claude/claude-dev์ธ ๊ฒฝ์ฐ: Step 4 ์ธ์ฃผ ๋์คํจ์น๋ฅผ ํตํด /mst:claude ์๋ธ์์ด์ ํธ์๊ฒ ์์. PM์ ์ง์ ๊ตฌํํ์ง ์์ต๋๋ค.
REQ ๋ธ๋์น ์์ฑ (ํ์คํฌ ์์ ๋ฌด๊ดํ ๊ณตํต ์ ํ ๋จ๊ณ):
DETECTED_BASE="{Step 2.7์์ ์ ์ฅํ request.json.detected_base}"
REQ_BRANCH=$(python3 {PLUGIN_ROOT}/scripts/mst.py worktree branch-name --req REQ-NNN --base "$DETECTED_BASE")
git show-ref --verify --quiet "refs/heads/${REQ_BRANCH}" \
|| git checkout -b "$REQ_BRANCH" "$DETECTED_BASE"
ํ์ ํธํ snapshot ๊ณ์ฝ ์์:
git show-ref --verify --quiet refs/heads/gran-maestro/REQ-NNN \
|| git checkout -b gran-maestro/REQ-NNN {config.worktree.base_branch}
ํ์คํฌ worktree ์์ฑ ์ flat REQ branch๋ฅผ base๋ก ์ ๋ฌํ๋ ๊ฒฝ์ฐ --base gran-maestro/REQ-NNN ํ์์ ์ ์งํ๋ค.
REQ ๋ธ๋์น๋ช
์ gran-maestro/{base_slug}/REQ-NNN ํ์์ด๋ค. base_slug๋ ๊ฐ์ง๋ base์ /๋ง -๋ก ์นํํ๋ค.
์ด ๋ธ๋์น๋ ๋ชจ๋ ํ์คํฌ ์ปค๋ฐ์ ์งํฉ์ ์ด ๋๋ฉฐ, accept ๋จ๊ณ๋ request.json.detected_base๋ฅผ ์ฌ์ฉํด ์ค์ base๋ก squash-mergeํ๋ค(T04).
๋จ์ผ ํ์คํฌ REQ์์๋ ๋ฐ๋์ ์ด ๋จ๊ณ๋ฅผ ์คํํด์ผ accept์ 3๋จ๊ณ ํ๋ก์ฐ๊ฐ ์ ์ ์๋ํ๋ค.
ํ์คํฌ๊ฐ 1๊ฐ์ธ ๊ฒฝ์ฐ: ๊ธฐ์กด ์์ฐจ ์คํ๊ณผ ๋์ผ ์ฒ๋ฆฌ.
์คํ ํ์ ๋ถ๊ธฐ (if 1๊ฐ, MANDATORY):
if strategy.worktree_policy == "skip":
{PROJECT_ROOT}์์ ์ง์ ์์
ํ๋ค.templates/doc-request.md ํ
ํ๋ฆฟ์ ์ฌ์ฉํ๋ค.else (strategy.worktree_policy != "skip"):
ํ์คํฌ๊ฐ 2๊ฐ ์ด์์ด๊ณ ๋
๋ฆฝ ํ์คํฌ๊ฐ ์กด์ฌํ๋ ๊ฒฝ์ฐ (strategy.worktree_policy != "skip"):
๋ ๋ฆฝ ํ์คํฌ๋ค์ git worktree๋ฅผ ๋ฏธ๋ฆฌ ์์ฑํฉ๋๋ค. ํ์คํฌ worktree๋ REQ ์ค๊ฐ ๋ธ๋์น๋ฅผ ๊ธฐ์ค์ผ๋ก ์์ฑ:
TASK_BRANCH=$(python3 {PLUGIN_ROOT}/scripts/mst.py worktree branch-name --req REQ-NNN --task T01 --base "$DETECTED_BASE")
python3 {PLUGIN_ROOT}/scripts/mst.py worktree create --path {worktree_path} --branch "$TASK_BRANCH" --base "$REQ_BRANCH"
๋ ๋ฆฝ ํ์คํฌ๋ค์ ๋ธ๋ฆฌํ ํ์ผ์ ํ๋์ ๋ฉ์์ง์์ ๋์์ Write ํธ์ถํฉ๋๋ค.
Write -> {PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/{NN}/prompts/phase2-impl.md
๋ธ๋ฆฌํ๋ templates/impl-request.md ํ
ํ๋ฆฟ ์ฌ์ฉ. (strategy.worktree_policy != "skip" ๊ฒฝ๋ก)
{{IMPL_CONTEXT}}: PM ์์ฑ โ 3~5์ค ์์ ํ์ (๋ฌด์์, ์, ์ด๋ป๊ฒ + ์ฃผ์์ฌํญ)
Reference Lookup Protocol์ ๋จผ์ ์คํํ๊ณ , ์์ฑ๋ [REFERENCE_CONTEXT] ๋ธ๋ก์ {{IMPL_CONTEXT}} ๋์ ์ฃผ์
ํ๋ค.reference.auto_search != true์ด๋ฉด ์๋ WebSearch ์์ด ๊ธฐ์กด REF ์บ์ ์กฐํ ๊ฒฐ๊ณผ๋ง ์ฃผ์
ํ๋ค.request.json์ linked_designs๊ฐ ์กด์ฌํ๊ณ ๋น์ด์์ง ์์ผ๋ฉด, {{IMPL_CONTEXT}} ๋์ "spec.md ยง10์ Stitch HTML ํ์ผ์ ์ฐธ์กฐํ๋ ๊ธฐ์ ์คํ์ ๋ง๊ฒ ๊ตฌํํ์ธ์." ์๋ ์ถ๊ฐ.{{SPEC_PATH}}, {{WORKTREE_PATH}}, {{REQ_ID}}, {{TASK_ID}}: ์๋ ์ฃผ์
{{PLAN_PATH}}: request.json.source_plan ์กด์ฌ ์ {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.md, ๋ฏธ์กด์ฌ ์ "N/A"{{PREV_FEEDBACK_PATH}}: ์ฒซ ์คํ ์ "N/A", ์ฌ์คํ ์ feedback ํ์ผ ๊ฒฝ๋ก{{PLAN_JSON_META}}: resolve ์์ request.json โ plan.json โ plan.ids.json โ objective.md. request.json.source_plan์ด ์กด์ฌํ๋ฉด {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json์ Readํ์ฌ cynefin_domain, linked_objective, linked_intent, linked_captures ํ๋์ ๊ฒฝ๋ก๋ฅผ 3~5์ค ์์ฝ์ผ๋ก ์ฃผ์
. ๋ฏธ์กด์ฌ ์ warn ๋ก๊ทธ + "N/A" ์นํ (graceful skip).{{PAC_LIST}}: source_plan์ด ์กด์ฌํ๋ฉด {PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.ids.json์ Readํ์ฌ ๊ฐ ํญ๋ชฉ์ id, grade, tags, text ํ๋๋ฅผ ๋ชฉ๋ก์ผ๋ก ์ฃผ์
. ๋ฏธ์กด์ฌ ์ warn ๋ก๊ทธ + "N/A" ์นํ (graceful skip).{{OBJECTIVE_SECTION}}: plan.json.linked_objective๊ฐ ์กด์ฌํ๋ฉด {PROJECT_ROOT}/.gran-maestro/agile/{AGI-NNN}/objective/objective.md๋ฅผ Readํ์ฌ JTBD ์์ฝ + ํ๋ก์ ํธ DoD ํญ๋ชฉ + ์ฑ๊ณต ์งํ๋ฅผ 3~5์ค ์์ฝ์ผ๋ก ์ฃผ์
. linked_objective/linked_intent/plan.ids.json ๊ฐ๊ฐ ๋ฏธ์กด์ฌ ์ warn ๋ก๊ทธ + "N/A" ์นํ (graceful skip, ์ํฌํ๋ก์ฐ ์ฐจ๋จ ๊ธ์ง).run_in_background: true ๊ธฐ๋ฐ Bash ์คํ ์ฌ์ฉ. (Skill ํธ์ถ์ ์ง๋ ฌ์ด๋ฏ๋ก ๋ณ๋ ฌ ์คํ ์ CLI ์ง์ ํธ์ถ ํ์)
{task_dir} = {PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/{TASK-NUM}/
โ ๏ธ gemini-dev Bash ๊ฐ์ (MANDATORY): gemini-dev๋ ๋จ๊ฑด/๋ณ๋ ฌ ๋ฌด๊ดํ๊ฒ ํญ์
Bash(run_in_background: true)๋ก ์คํํ๋ค.Skill(mst:gemini)์ ํ ๋ถ๊ฐ. trace๋running.log๋ก ๋์ฒด๋๋ค.
# codex-dev์ธ ๊ฒฝ์ฐ (OMX_AUTOPILOT=true ์ \$autopilot ํ๋ฆฌํฝ์ค ์ฝ์
)
Bash(
MODEL=$(python3 {PLUGIN_ROOT}/scripts/mst.py resolve-model codex default 2>/dev/null || echo "gpt-5.3-codex");
command: 'set -o pipefail; codex exec --full-auto -m "$MODEL" -C {worktree_path} "\$autopilot $(cat {prompt_file})" < /dev/null 2>&1 | tee {task_dir}/running.log', # OMX_AUTOPILOT=true
# ๋๋:
command: 'set -o pipefail; codex exec --full-auto -m "$MODEL" -C {worktree_path} "$(cat {prompt_file})" < /dev/null 2>&1 | tee {task_dir}/running.log', # OMX_AUTOPILOT=false
run_in_background: true,
timeout: {config.timeouts.cli_large_task_ms}
)
# gemini-dev์ธ ๊ฒฝ์ฐ
Bash(
MODEL=$(python3 {PLUGIN_ROOT}/scripts/mst.py resolve-model gemini default 2>/dev/null);
command: 'set -o pipefail && cd {worktree_path} && gemini -p "$(cat {prompt_file})"${MODEL:+ --model "$MODEL"} --approval-mode yolo --sandbox=false < /dev/null 2>&1 | tee {task_dir}/running.log',
run_in_background: true,
timeout: {config.timeouts.cli_large_task_ms}
)
# claude-dev (๋๋ claude)์ธ ๊ฒฝ์ฐ
if (wave_claude_task_count > 1):
Task(subagent_type: "general-purpose", prompt: {prompt_file ๋ด์ฉ}, run_in_background: true)
else:
Skill(skill: "mst:claude", args: "--trace {REQ-ID}/{TASK-NUM}/phase2-impl")
๊ฐ ์คํ์ task_id๋ฅผ request.json์ ์๊ตฌ ์ ์ฅ:
{ "background_task_ids": [{ "task_id": "{bg_task_id}", "task_num": "01", "agent": "codex-dev", "status": "running" }] }
์ธ์ ๊ฐ ์ถ์ : task_id๋ฅผ ๊ธฐ๋กํ์ฌ ์ธ์ ์ ํ ํ์๋ ์ถ์ ๊ฐ๋ฅ. ํ์ ์
TaskStop(task_id)๋ก ์ทจ์.
โ ๏ธ CONTINUATION GUARD: ์๋ธ์คํฌ ๋ฐํ ํ ์ฆ์ ๋ค์ Step ์งํ (hook์ด ์๋ ๊ฐ์ ). approve๋ Phase 5 ์๋ฃ ์์๋ง ์ข ๋ฃ.
๋ชจ๋ ๋ณ๋ ฌ ํ์คํฌ๊ฐ ์๋ฃ๋ ๋๊น์ง ํด๋งํฉ๋๋ค.
backoff = 2์ด
max_backoff = 30์ด
while (์คํ ์ค์ธ ํ์คํฌ๊ฐ ์์):
for task_id in running_tasks:
result = TaskOutput(task_id, block: false, timeout: 5000)
if result.status == 'completed':
remove task_id from running_tasks
if result.exit_code == 0:
Step 5๋ก ์ด๋(์ฌ์ ๊ฒ์ฆ)
else:
mark task status = failed
Step 4e ์คํจ ์ ํ ๊ท์น ์ ์ฉ
Fallback ๊ท์น ์ ์ฉ(ํ์ ์ ์ฌ์คํ)
ํด๋น ํ์คํฌ๊ฐ ์ ํ์ธ ํ์ํ์คํฌ๊ฐ ์์ผ๋ฉด 4e๋ก ์ด๋
if running_tasks ๋จ์์์:
sleep(backoff)
backoff = min(backoff * 2, max_backoff)
์ ํ ํ์คํฌ ์๋ฃ ์ blockedBy ํด์:
blockedBy์์ ์๋ฃ ํ์คํฌ ์ ๊ฑฐblockedBy ๋น๋ฉด 4c์ ์ถ๊ฐํด ๋ณ๋ ฌ ์คํfailed์ด๋ฉด ํ์ ํ์คํฌ๋ฅผ cancelled๋ก ์ ์ด, ์ฆ์ ์ฌ์ฉ์ ์๋ฆผโ ๏ธ NON-STOP EXECUTION RULE (CRITICAL): Step 5 ~ Step 6 ~ Phase 3 ~ ์ต์ข ์๋ฝ๊น์ง ์ค๊ฐ ๋ฉ์ถค ์์ด ์ฐ์ ์คํํ๋ค. ํ ์คํธ๋ฅผ ์ถ๋ ฅํ์ผ๋ฉด ๋ฐ๋์ ํด๋น ๋จ๊ณ์ ๋๊ตฌ ํธ์ถ(Bash/Edit/Skill ๋ฑ)์ ์ฆ์ ์คํํ๋ค. ์๋ธ์คํฌ ๋ฐํ ์งํ ๊ฐ์ ํจํด: ์๋ธ์คํฌ(mst:claude, mst:codex, mst:gemini, mst:review ๋ฑ)์ด ๋ฐํ๋๋ฉด ๋ฐํ ํ ์คํธ์ ๋ฌด๊ดํ๊ฒ ์ฆ์
NEXT_ACTION: <๋ค์ Step ์ค๋ช >ํจํด์ ์ถ๋ ฅํ๊ณ ํด๋น Step์ ๋๊ตฌ ํธ์ถ์ ์คํํ๋ค. ์๋ธ์คํฌ ๋ฐํ์ ์ข ๋ฃ๊ฐ ์๋๋ผ ๋ค์ ๋จ๊ณ ์ ํ ์ ํธ๋ค. ์ปจํ ์คํธ ๊ธธ์ด/๋ํ ๊ธธ์ด/ํ ํฐ ์๋น๋์ ์ด์ ๋ก ํ ์๋ฐ์ ์ค๋จ์ ๊ธ์งํ๋ค. Claude Code๋ ์๋ ๋ํ ์์ถ์ผ๋ก ์ค์ ํ๊ณ๋ฅผ ๊ด๋ฆฌํ๋ฏ๋ก, LLM์ด ์ด๋ฅผ ๊ทผ๊ฑฐ๋ก ์ค๋จ ์ฌ๋ถ๋ฅผ ์ง์ ํ๋จํ์ง ์๋๋ค. ์ด ๊ท์น์ ์ด approve ์คํฌ์ ๋ชจ๋ ํ์ Step์ ์ ์ฉ๋๋ค.
๊ฐ ํ์คํฌ ์๋ฃ ์ฆ์ ์ฌ์ ๊ฒ์ฆ ์คํ:
test_output ์บก์ฒ + exit code ํ๋ณด)tsc_output ์บก์ฒ + exit code ํ๋ณด)self_check ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ request.json์ ํ์ฌ ํ์คํฌ์ ๊ธฐ๋ก
self_check = {
tsc: (tsc_exit_code == 0 ? "PASS" : "FAIL"),
test: (test_exit_code == 0 ? "PASS" : "FAIL"),
ran_at: now_in_iso8601_utc(),
tsc_output: tsc_output,
test_output: test_output,
retry_round: (request_json.pre_check_retries or 0)
}
try:
req = Read({PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/request.json)
task = find(req.tasks, id == {TASK_ID})
if task exists:
task.self_check = self_check
Write(request.json, req)
else:
warn("[non-blocking] self_check ์ ์ฅ ๋์ task๋ฅผ ์ฐพ์ง ๋ชปํจ: {TASK_ID}")
except err:
warn("[non-blocking] self_check ์ ์ฅ ์คํจ: {err}")
์ ์ฅ ์คํจ๋ non-blocking: ๊ฒฝ๊ณ ๋ง ์ถ๋ ฅํ๊ณ ๋ค์ ๋ถ๊ธฐ๋ก ์งํ.status โ review โ ์ฆ์ Step 5.5 ์คํ (PM ์ปค๋ฐ)status โ pre_check_failed โ ์ฆ์ Step 5b ์คํ (์ฌ์ธ์ฃผ)Step 5 PASS ํ PM์ด ์ง์ ์ปค๋ฐํฉ๋๋ค (์ธ์ฃผ ์์ด์ ํธ์ index.lock ๋ฌธ์ ๋ฐฉ์ง).
์ด์ค ์ปค๋ฐ ๋ฐฉ์ง: git -C {worktree_path} status --porcelain โ ์ถ๋ ฅ ์์ผ๋ฉด ์ด๋ฏธ clean. status โ committed ์ ํ ํ Step 5.7 ์งํ.
์ ์ฒด ๋ณ๊ฒฝ ์คํ
์ด์ง: git -C {worktree_path} add -A
frontend/ ๋ณ๊ฒฝ ์๋ ๊ฐ์ง ํ ๋น๋:
FRONTEND_CHANGED=$(git -C {worktree_path} diff --cached --name-only | grep "^frontend/" | head -1)
if [ -n "$FRONTEND_CHANGED" ]; then
cd {worktree_path}/frontend && npm install --prefer-offline && npm run build
git -C {worktree_path} add dist/
fi
PM์ด ์ปค๋ฐ:
git -C {worktree_path} commit -m "[{REQ_ID}/{TASK_ID}] {spec ยง1 ์์ฝ}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
์ปค๋ฐ hash/message ์ ์ฅ (์คํจ ์ ๊ฒฝ๊ณ ํ ๊ณ์):
COMMIT_HASH=$(git -C {worktree_path} log -1 --format="%H")
COMMIT_MSG=$(git -C {worktree_path} log -1 --format="%s")
python3 {PLUGIN_ROOT}/scripts/mst.py task set-commit {REQ_ID}-T{TASK_ID_PAD} "$COMMIT_HASH" "$COMMIT_MSG"
ํ์คํฌ status โ committed, background_task_ids status โ "completed" ์
๋ฐ์ดํธ โ Step 5.7 ์งํ.
์ด Step์ Step 5 ~ Step 6 ์ฌ์ด์ NON-STOP EXECUTION RULE ์ ์ฉ ๋ฒ์ ๋ด๋ถ๋ค. ๊ฒ์ฆ ์์ด์ ํธ ๋ฐํ ํ ์ฆ์ ํ์ /๋ณด์/์ฌ๊ฒ์ฆ ๋๋ Step 6 ์ ํ์ ์ํํ๋ค.
req = Read({PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/request.json)
source_plan = req.source_plan
intent_cfg = JsonParse(Bash(`python3 {PLUGIN_ROOT}/scripts/mst.py config get intent_verification`)) or {}
intent_enabled = intent_cfg.enabled if boolean else true
max_iterations = intent_cfg.max_iterations if positive_integer else 5
all_committed = every(req.tasks[].status in ["committed", "done"])
if all_committed == false:
goto Step 6
if source_plan is null or source_plan is empty:
echo "[Step 5.7 skip] source_plan ์์ (--plan ์๋ REQ) โ Step 6 ์งํ"
goto Step 6
if intent_enabled == false:
echo "[Step 5.7 skip] intent_verification.enabled=false โ Step 6 ์งํ"
goto Step 6
plan.md(AD ๋ฐ ๊ตฌ์กฐ ๋ช
์ธ ์น์
์ถ์ถ), plan.ids.json(PAC ๋ชฉ๋ก ์ถ์ถ)intent-verification/๊ฐ iteration๋ง๋ค ์๋ (a)~(d)๋ฅผ ์์๋๋ก ์คํํ๋ค.
{PROJECT_ROOT}/templates/intent-verification.md{REQ_ID}, {PLN_ID}, {ITERATION}, {WORKTREE_PATH}, {AD_LIST}, {PAC_LIST}, {STRUCTURE_SPEC}intent-verification/prompt-iteration-{iteration}.mdintent-verification/iteration-{iteration}.md๋ถ๋ถ๋ฐ์ + ๋ฏธ๋ฐ์ == 0์ด๋ฉด ์๋ ด์ผ๋ก ๊ฐ์ฃผํ๊ณ ๋ฃจํ ์ข
๋ฃ: "[Step 5.7 converged] ๋ฏธ๋ฐ์ ํญ๋ชฉ 0๊ฑด โ Step 6 ์งํ"๋ณด์ ํ์ ํญ๋ชฉ ๋ชฉ๋ก์ ๊ธฐ๋ฐ์ผ๋ก ๋จ์ผ ๋ณด์ ํ์คํฌ๋ฅผ ์์ฑํ๋ค.AUTO_MODE=true: PM ์์จ ํ๋จ์ผ๋ก ์ฆ์ ๋ณด์ ๋์คํจ์นAUTO_MODE=false: AskUserQuestion์ผ๋ก ๋ฏธ๋ฐ์ ๋ชฉ๋ก ์ ์ โ "๋ณด์ํ๊ณ ์ฌ๊ฒ์ฆ" ๋๋ "๋จ์ ํญ๋ชฉ ๋ฌด์ํ๊ณ ์งํ"๋ณด์ ์ปค๋ฐ ์๋ฃ ์ฆ์ iteration += 1 ํ Step 5.7-2 (a)๋ก ์ฌ์ง์
ํ๋ค. iteration > max_iterations์ด๋ฉด ๋ฃจํ ์ข
๋ฃ.
๋ถ๋ถ๋ฐ์ + ๋ฏธ๋ฐ์ == 0 โ ์ฆ์ Step 6 ์งํiteration > max_iterations โ ์์ฌ ๋ฏธ๋ฐ์์ด ์์ด๋ Step 6 ์งํretry.max_cli_retries) ์ ์ฉ๋ฃจํ ์ข
๋ฃ ์ intent-verification/summary.md์ ์ ์ฅ. ํฌํจ ํญ๋ชฉ: ์ด iteration ์, ์๋ ด ์ฌ๋ถ, ์์ฌ ๋ฏธ๋ฐ์ ํญ๋ชฉ ๋ชฉ๋ก.
Step 5.7 ์ข
๋ฃ ์งํ ์ฆ์ Step 6: Phase 3 ์ ํ์ผ๋ก ์งํํ๋ค.
Step 5 FAIL ์, PM์ด ์ง์ ์ฝ๋๋ฅผ ์์ ํ์ง ์๊ณ ์ธ์ฃผ ์์ด์ ํธ์๊ฒ ์๋ฌ ์ปจํ ์คํธ์ ํจ๊ป ์ฌ์์ฒญํฉ๋๋ค. ์ต๋ ์ฌ์๋ ์์ง ํ PM ์ง์ ๊ฐ์ .
์คํ ํ์ ๋ถ๊ธฐ (if 1๊ฐ, MANDATORY):
if strategy.worktree_policy == "skip":
2ํ, ๋ฃจํ๋ ํฉํธ์ฒดํฌ ์คํจ โ ์์ค ์ฌํ์ธ ํ๋กฌํํธ ์์ฑ โ ์ฌ์์ฑ ์์.์ง์ ๋ฌธ์ ๊ฒ์ฆ ๊ฒฐ๊ณผ์์ ์คํจ claim ๋ชฉ๋ก(failed_claims)๊ณผ ๊ทผ๊ฑฐ ๋ถ์กฑ ํญ๋ชฉ(unverified_claims)์ ์ถ์ถ. ๊ฐ ํญ๋ชฉ์ ๋ํด "ํ์ฌ ์์ / ์คํจ ์ฌ์ / ํ์ํ ๊ทผ๊ฑฐ(source)"๋ฅผ ์ ๋ฆฌํ๋ค.
Write โ {PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/{NN}/prompts/phase2-doc-fix-R{N}.md
ํฌํจ ๋ด์ฉ: spec.md ยง3 ์๋ฝ ์กฐ๊ฑด, ์คํจ/๋ฏธ๊ฒ์ฆ claim ๋ชฉ๋ก + ์คํจ ์ฌ์ , ยง0 Context Manifest ์ฌํ์ธ ์ง์, "์คํจ claim ์น์
๋ง ์ฌ์์ฑ ํ ๊ตฌ์กฐ ๊ฒ์ฆ + ํฉํธ์ฒดํฌ ๋ค์ ์คํ" ์ง์.
๋์ผ ์์ด์ ํธ๋ก ์ฌ์ธ์ฃผ ์คํ. request.json์ doc_factcheck_retries(์์ผ๋ฉด 0)๋ฅผ +1 ์ ์ฅ. ์ฌ์์ฑ ์๋ฃ ํ ์ฆ์ Step 5๋ก ๋ณต๊ท.
doc_factcheck_retries >= 2์ด๋ฉด ๋ฃจํ ์ข
๋ฃ. PM์ด ์์ค ์๋ฌธ์ ์ฌํ์ธํด ๋ฌธ์๋ฅผ ์ง์ ๋ณด์ ํ ๋ค ๊ฒ์ฆ๋ง ์ฌ์คํํ๋ค.
else (strategy.worktree_policy != "skip"):
5b-1 ~ 5b-5 ๊ธฐ์กด ์ฝ๋ ๊ฒฝ๋ก๋ฅผ ๊ทธ๋๋ก ์ํํ๋ค. (๋ณ๊ฒฝ ๊ธ์ง)5b-1์ ํธ๋ฆฌ๋ฐ๋ ์๋ฌ ์ถ๋ ฅ(TRIMMED_ERROR_OUTPUT)์ ์๋ ํฌ๋งทํฐ ์ ์ฉ:
python3 {PLUGIN_ROOT}/scripts/format-precheck-errors.pyํ์ผ๊ฒฝ๋ก(์ค,์ด): error TSNNNN: ๋ฉ์์ง โ ํ์ผ๊ฒฝ๋ก:์ค โ TSNNNN โ ๋ฉ์์งTRIMMED_ERROR_OUTPUT์ ๊ทธ๋๋ก ์ฌ์ฉ (passthrough). ์ต์ข
์ถ๋ ฅ ๋ณ์๋ช
: FORMATTED_ERROR_OUTPUTpre_check_retries ํ๋ ํ์ธ (์์ผ๋ฉด 0)config.retry.max_cli_retries (๊ธฐ๋ณธ 2) ๋ฏธ๋ง โ 5b-3 (์ฌ์ธ์ฃผ)Write โ {PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/{NN}/prompts/phase2-fix-R{N}.md
ํฌํจ ๋ด์ฉ: spec.md ยง3 ์๋ฝ ์กฐ๊ฑด, ํฌ๋งท๋ ์๋ฌ ์ถ๋ ฅ(FORMATTED_ERROR_OUTPUT), "์๋ฌ ์์ ํ ๊ฒ์ฆ ๋ช
๋ น์ด ์คํ ํ์ธ" ์ง์นจ, spec ยง5 ํ
์คํธ/ํ์
์ฒดํฌ ๋ช
๋ น์ด. <error_context>์ {ERROR_OUTPUT}์ FORMATTED_ERROR_OUTPUT ๋ฐ์ธ๋ฉ.
if OMX_AUTOPILOT:
fix_content = Read({PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/{NN}/prompts/phase2-fix-R{N}.md)
fix_omx_path = {PROJECT_ROOT}/.gran-maestro/requests/{REQ-ID}/tasks/{NN}/prompts/phase2-fix-omx-R{N}.md
Write(fix_omx_path, "$autopilot\n\n" + fix_content)
Skill(skill: "mst:codex", args: "--prompt-file {fix_omx_path} --dir {worktree_path} --trace {REQ-ID}/{TASK-NUM}/phase2-fix-R{N}")
else:
Skill(skill: "mst:codex", args: "--prompt-file {fix_path} --dir {worktree_path} --trace {REQ-ID}/{TASK-NUM}/phase2-fix-R{N}")
pre_check_retries +1, tasks[].retry_count +1, request.json ์ ์ฅstatus โ executing. ์ฌ์ธ์ฃผ ์๋ฃ ํ ์ฆ์ Step 5 ๋ณต๊ทmax_cli_retries ์์ง ํ, PM ์ง์ ๊ฐ์
์ Codex ์์ค์ปฌ๋ ์ด์
1ํ ์๋:
codex_fallback_retries ํ์ธ: >= 1์ด๋ฉด โ ์ฆ์ 5b-5๋ก ์ด๋ (์ต๋ 1ํ ํ๋)git -C {worktree_path} stash, ์์ค์ปฌ๋ ์ด์
ํ๋กฌํํธ ์ค๋น (phase2-fix-R{N}.md + ## ์์ค์ปฌ๋ ์ด์
ํํธ ์น์
). Step 4c์ ๋์ผ ํจํด์ผ๋ก ์คํ (running-fallback.log ์ถ๋ ฅ).codex_fallback_retries = 1 ์
๋ฐ์ดํธ โ Step 5 ์ฌ์ง์
. ์คํจ ์: stash pop โ 5b-5 ์ด๋.background_task_ids์์ status: "running" ํญ๋ชฉ์ TaskStop(task_id)๋ก ์ทจ์ โ "cancelled" ์
๋ฐ์ดํธ.status: review / ์ฌ์ ํ FAIL โ ์ฌ์ฉ์ ๊ฐ์
์์ฒญ๋ชจ๋ ํ์คํฌ๊ฐ committed ์ํ์ ๋๋ฌํ๋ฉด:
์คํฌ๋ฆฝํธ ์ฐ์ : python3 {PLUGIN_ROOT}/scripts/mst.py request set-phase {REQ_ID} 3 phase3_review; ์คํจ ์ fallback์ผ๋ก current_phase=3, status=phase3_review ์ง์ ์
๋ฐ์ดํธ โ Phase 3 ์ง์
.
๋ชจ๋ ํ์คํฌ๊ฐ committed ์ํ์ ๋๋ฌํ๊ณ current_phase๊ฐ 3์ผ๋ก ์ ํ๋ ํ:
review.auto_review ์ค์ ํ์ธ (Bash(python3 {PLUGIN_ROOT}/scripts/mst.py config get review.auto_review)):
AUTO_MODE๋ ๋จ๊ฑด ํ๋กํ ์ฝ ์ง์
์ ๋จ์ผ ์ด๊ธฐํ๋ ๊ฐ์ ๊ทธ๋๋ก ์ฌ์ฉํ๋ค (์ด์ค ํ๋จ ๊ธ์ง).false (๊ธฐ๋ณธ): ์๋ ํ์คํฌ ์ํ ๊ฒ์ฆ ํ ์ต์ข
์๋ฝ ์คํ (mst:review ๋ฏธํธ์ถ):
request.json.tasks ์ ์ฒด ํ์ธ: ๋ชจ๋ ํ์คํฌ๊ฐ committed ์ด์ ์ํ์ธ์ง ๊ฒ์ฆ
committed ๋ฏธ๋ง ํ์คํฌ ์กด์ฌ ์: "ํ์คํฌ {TASK_ID}๊ฐ ์์ง committed ์ํ๊ฐ ์๋๋๋ค" ๊ฒฝ๊ณ ํ ๋๊ธฐworkflow.auto_accept_result ์ค์ ์ ๋ฐ๋ผ ์ฆ์ ์คํ:
true (๊ธฐ๋ณธ): Skill(skill: "mst:accept", args: "{REQ_ID}") ํธ์ถ โ accept ์๋ฃ ํ DAG ์ฐ์ ์คํ ํ๋จfalse: Phase 3 ๋ฆฌ๋ทฐ PASS๋ก ๊ฐ์ฃผํ๊ณ ๋ฉ์ถ๊ณ , ์ฌ์ฉ์์๊ฒ /mst:accept {REQ_ID} ์๋ ํธ์ถ ์๋ดtrue ๋๋ AUTO_MODE=true์ด๋ฉด mst:review ํธ์ถ ์งํiteration_num >= 2์ธ ๊ฒฝ์ฐ: iteration-decisions/iteration-{iteration_num - 1}.md Read โ ์ปจํ
์คํธ ๋ณด๊ด. ํ์ผ ์์ผ๋ฉด skip.
AUTO_MODE=true -> Skill(skill: "mst:review", args: "{REQ_ID} --auto")
AUTO_MODE=false -> Skill(skill: "mst:review", args: "{REQ_ID}")
(AUTO_MODE=true์์๋ review.auto_review=false์ด๋๋ผ๋ ํญ์ ํธ์ถ)
โ ๏ธ ๋ฐํ ํ ์ฆ์ 3๋ฒ์ผ๋ก ์งํ โ
[TRACE_SAVED]ํ ์คํธ ํฌํจ ์ฌ๋ถ ๋ฌด๊ด. approve๋ Phase 5(mst:accept) ์๋ฃ ์์๋ง ์ข ๋ฃ.
mst:review ๋ฐํ ํ, review ๊ฒฐ๊ณผ ์ฒ๋ฆฌ(3๋ฒ) ์ง์
์ ์ ์คํ. iteration-decisions/ ๋๋ ํ ๋ฆฌ ์์ฑ ํ iteration-{iteration_num}.md Write:
# Iteration {iteration_num} ๊ฒฐ์ ๋ก๊ทธ
## AC ์ํ
{๊ฐ AC์ ๋ํด: AC-NNN: PASS/FAIL + ํ๋จ ๊ทผ๊ฑฐ 1์ค}
## ํต์ฌ ํ๋จ
{๋ฆฌ๋ทฐ์์ ๋ฐ๊ฒฌ๋ ์ฃผ์ ์ด์์ ๋ํ PM์ severity ๋์/์ด์ + ๊ฒฐ์ ์ด์ }
## ๋ค์ iteration ๋ฐฉํฅ
{๋ค์ iteration์์ ์ง์คํ AC ๋ชฉ๋ก + ์ถ๊ฐ ํ์คํฌ ๋ฐฉํฅ}
Write ์คํจ ์ warn๋ง ์ถ๋ ฅํ๊ณ ์ํฌํ๋ก์ฐ๋ฅผ ์ฐจ๋จํ์ง ์๋๋ค (graceful).
review ๊ฒฐ๊ณผ ์ฒ๋ฆฌ:
review_issues_summary ๋ก๋: ์ต์ reviews/RV-NNN/review.json์ Read โ review_issues_summary ํ์ฑ (critical/major/minor ์นด์ดํธ + auto_fixed/skipped ๋ฐฐ์ด)
auto_accept_guard ๋ฉํ ํ์ฑ:
skipped_minor_count = review_issues_summary.auto_accept_guard.skipped_minor_count (์์ผ๋ฉด 0)
protection_flags_count = review_issues_summary.auto_accept_guard.protection_flags_count (์์ผ๋ฉด 0)
guard_blocked = review_issues_summary.auto_accept_guard.blocked == true OR skipped_minor_count > 0 OR protection_flags_count > 0
auto_accept_guard.blocked_reasons๊ฐ ์์ผ๋ฉด ์ฐจ๋จ ์ฌ์ ๋ก ๊ทธ๋๋ก ๋ณด๊ณ ํ๋ค.
status: "passed": review_summary.status โ "passed" ์ดํ ์๋ ๊ท์น์ผ๋ก ๋ถ๊ธฐํ๋ค:
workflow.auto_accept_result == true AND guard_blocked == false:
AUTO_MODE=true -> Skill(skill: "mst:accept", args: "-a {REQ_ID}")
AUTO_MODE=false -> Skill(skill: "mst:accept", args: "{REQ_ID}")
workflow.auto_accept_result == true AND guard_blocked == true:
auto_accept_guard.blocked_reasons์ ํจ๊ป ๋ณดํธ ์ฐจ๋จ ์ํ๋ฅผ ๋ณด๊ณ ํ๊ณ , ์ฌ์ฉ์์๊ฒ /mst:accept {REQ_ID} ์๋ ํธ์ถ ๊ฒฝ๋ก๋ฅผ ์๋ดํ๋ค.workflow.auto_accept_result == false:
/mst:accept {REQ_ID}๋ฅผ ์๋์ผ๋ก ํธ์ถํ๋ผ๊ณ ์๋ดํ๋ค. ์ค์ ๋ณ๊ฒฝ: /mst:settings workflow.auto_accept_result falseDAG ์๋ ์ฐ์ ์คํ (accept ์๋ฃ ์งํ, auto_accept_result == true์ธ ๊ฒฝ์ฐ์๋ง ์คํ):
์๋ ์กฐ๊ฑด์ ๋ชจ๋ ์ถฉ์กฑํ๋ฉด ๊ฐ์ plan์ ํ์ REQ๋ฅผ ์๋ ์ฐ์ ์คํํ๋ค.
(auto_accept_result == false์ธ ๊ฒฝ์ฐ์ DAG ์ฐ์ ๊ท์น์ mst:accept(Step 5.6)์์ ์คํ)
์คํ ์กฐ๊ฑด:
request.json์์ source_plan์ด "PLN-NNN" ํํ๋ก ์กด์ฌrequest.json์์ dag_auto_chain == truedone ๋๋ completed ๋๋ acceptedํ๋๋ผ๋ ๋ถ์ถฉ์กฑ์ด๋ฉด DAG ์ฐ์ ์คํ ๋จ๊ณ๋ skip.
๋ค์ REQ ํ์ ๊ท์น:
plan.json Read ํ linked_requests ์ ์ฒด๋ฅผ plan ์ ์ ์์๋๋ก ์ฌํ๊ฐpending_dependency, phase1_analysis, spec_ready)๋ง ํ๋ณด.blockedBy ํด์ ํ์ : ๋ชจ๋ ์ ํ REQ๊ฐ done/completed/accepted์ด๋ฉด "์คํ ๊ฐ๋ฅ"์ผ๋ก ํ๋จ์๋ ์ฐ์ ์คํ ๋ฃจํ:
์ปจํ ์คํธ ๊ธธ์ด ๊ธฐ๋ฐ ์ค๋จ ๊ธ์ง (MANDATORY): ์๋ ๋ฃจํ๋ ์ปจํ ์คํธ ๊ธธ์ด/๋ํ ๊ธธ์ด/ํ ํฐ ์๋น๋์ ์ด์ ๋ก ์ค๋จํ์ง ์๋๋ค. ์ ์ผํ ์์ธ๋ ์ฌ์ฉ์์ ๋ช ์์ ์ทจ์ ์ง์๋ค.
chain_results = [{ req_id: CURRENT_REQ_ID, status: "completed" }]
while true:
plan = Read({PROJECT_ROOT}/.gran-maestro/plans/{source_plan}/plan.json)
next_req = first runnable req from plan.linked_requests (full scan each loop)
if not next_req:
break
์ถ๋ ฅ: "[DAG ์ฐ์] ๋ค์ ์คํ: {next_req.id} ({next_req.title})"
Skill(skill: "mst:request", args: "--plan {source_plan} --resume {next_req.id} -a")
refreshed = Read({PROJECT_ROOT}/.gran-maestro/requests/{next_req.id}/request.json)
if refreshed.status in ["done", "completed", "accepted"]:
chain_results.append({ req_id: next_req.id, status: "completed" })
continue
pending_tail = remaining non-terminal req ids in same plan
์ถ๋ ฅ: "[DAG ์ฐ์ ์ค๋จ] {next_req.id} ์คํจ. ํ์ REQ: {pending_tail.join(', ')}"
์ข
๋ฃ
if all linked_requests are done/completed/accepted:
์ถ๋ ฅ: "[DAG ์ฐ์ ์๋ฃ] {source_plan}์ ๋ชจ๋ REQ๊ฐ ์๋ฃ๋์์ต๋๋ค. ..."
else:
์ถ๋ ฅ: "[DAG ์ฐ์ ์ข
๋ฃ] ์คํ ๊ฐ๋ฅํ ๋ค์ REQ๊ฐ ์์ด ์ข
๋ฃํ์ต๋๋ค."
status: "gap_found":
a. CRITICAL ์ด์ ์กด์ฌ ์: CRITICAL์ PM ์ง์ ์์ ๋ถ๊ฐ, ํญ์ ์ฌ์ธ์ฃผ.
a-2. MINOR ์ด์: review_issues_summary.skipped์ ๊ธฐ๋ก๋ ๋๋ก ์คํต, ์ฌ์ธ์ฃผ ๋์์ ํฌํจํ์ง ์์.
b. MAJOR ์ด์ โ PM ์ง์ ์์ ๋ถ๊ธฐ:
CRITICAL์ PM ์ง์ ์์ ๋ถ๊ฐ, ํญ์ ์ฌ์ธ์ฃผ
MAJOR ์ด์ ์ค ์๋ ๋ชจ๋ ์กฐ๊ฑด์ ์ถฉ์กฑํ๋ฉด PM์ด worktree์์ ์ง์ ์์ :
config.review.severity_auto_fix.pm_direct_fix_enabled == truepm_direct_fix_max_filespm_direct_fix_max_diff_linesPM ์ง์ ์์ ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ ์ โ ์ฌ์ธ์ฃผ ๊ฒฝ๋ก(์๋ c.)๋ก ์ ํ.
PM ์ง์ ์์ ์ ์ฐจ: MAJOR ์ด์ ์ค ์กฐ๊ฑด ์ถฉ์กฑ ์ด์๋ง PM์ด ์ง์ ์์ ํ๊ณ ๋๋จธ์ง๋ ์ฌ์ธ์ฃผ.
git checkout -- .) โ ํด๋น MAJOR ์ด์ ํ์คํฌ๋ฅผ request.json.tasks์ ์ ๊ท ์์ฑ(generated_by: "review", status: "pending") โ c. ๊ฒฝ๋ก๋ก ์ง์
.review-report.md์ pm_direct_fix: true, ์์ ํ์ผ ๋ชฉ๋ก, ์์ ๋ด์ฉ ์์ฝ ๊ธฐ๋ก.c. MAJOR ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ ๋๋ ์ฌ์ธ์ฃผ ๊ฒฝ๋ก:
โ ๏ธ AUTO_MODE=true์ผ ๋ ์ฌ์ธ์ฃผ๋ ๋ฌด์ ์ง ์คํ: AskUserQuestion ์์ด ์ฆ์ ์๋ ์ ์ฐจ๋ฅผ ์คํํ๋ค.
request.json.tasks์์ generated_by: "review" + status: "pending" ํ์คํฌ๋ง ์ ๋ณcurrent_phase โ 3 ์ฌ์ ํ โ ์ด ๋ฃจํ ๋ฐ๋ณตstatus: "pass_a_failed":
โ ๏ธ CRITICAL:
pass-a-result.md์คํค๋ง ํ์ ํ๋๊ฐ ํ๋๋ผ๋ ๋๋ฝ๋๋ฉด ์ฌ์ธ์ฃผ ์ ๋ณ์ ์ฆ์ ์ค๋จํ๊ณ review ์ฌ์คํ์ ์๊ตฌํ๋ค.
| ์กฐ๊ฑด | ๋์ | ๋ค์ ๋จ๊ณ |
|---|---|---|
pass-a-result.md ์คํค๋ง ๊ฒ์ฆ ์คํจ (ํ์ ํ๋ ๋๋ฝ) | "์คํค๋ง ๋ถ์ผ์น" ์ถ๋ ฅ + review ์ฌ์คํ ์๋ด | ์ฌ์ธ์ฃผ ์ ๋ณ ์ค๋จ |
์คํค๋ง ํต๊ณผ + covers_ac ๋น์ด์์ง ์์ ํ์คํฌ ์์ | failed_ac_ids โฉ covers_ac ๊ต์งํฉ ๊ธฐ์ค ์ ๋ณ | ์ ๋ณ ํ์คํฌ๋ก ์ฌ์ธ์ฃผ |
์คํค๋ง ํต๊ณผ + ๋ชจ๋ committed ํ์คํฌ์ covers_ac๊ฐ ์๊ฑฐ๋ ๋น ๋ฐฐ์ด | ํ์ ํธํ fallback โ ์ ์ฒด committed ํ์คํฌ ์ ๋ณ | ์ฌ์ธ์ฃผ ์งํ |
์คํค๋ง ํต๊ณผ + covers_ac ์์ผ๋ ๊ต์งํฉ ์์ | fallback ์์ด ๋น ์ ๋ณ ์ ์ง | ์ฌ์ธ์ฃผ ๋์ ์์ |
์คํค๋ง ํต๊ณผ + ์ผ๋ถ ํ์คํฌ๋ง covers_ac ์กด์ฌ | ๊ต์งํฉ ๊ธฐ์ค ์ ๋ณ + ๋๋จธ์ง fallback ํฌํจ | ์ ๋ณ ํ์คํฌ๋ก ์ฌ์ธ์ฃผ |
์ฌ์ธ์ฃผ ํ์คํฌ ์ ๋ณ:
reviews/RV-NNN/pass-a-result.md Read โ failed_ac_ids ํ์ฑpass_a_result, failed_ac_ids, failure_class, evidence) ํ๋๋ผ๋ ๋๋ฝ ์ ์ค๋จcommitted ํ์คํฌ ์ค covers_ac ๋น์ด์์ง ์์ ํ์คํฌ: failed_ac_ids โฉ covers_ac ๊ต์งํฉ >= 1์ธ ํ์คํฌ ์ ์ covers_ac ์๊ฑฐ๋ ๋น ๋ฐฐ์ด์ธ committed ํ์คํฌ๋ fallback์ผ๋ก ํฌํจ์ฌ์ธ์ฃผ ์ ์ฐจ:
โ ๏ธ AUTO_MODE=true์ผ ๋ ์ฌ์ธ์ฃผ๋ ๋ฌด์ ์ง ์คํ.
generated_by: "review", status: "pending")current_phase 3 ์ฌ์ ํ โ mst:review ์ฌํธ์ถstatus: "limit_reached":
--auto ๋ชจ๋: review_summary.status = "limit_reached" ๊ธฐ๋ก ํ workflow.auto_accept_result ์ค์ ์ ๋ฐ๋ผ ์ฆ์ ์คํ๋จ, --auto ํ๋๊ทธ ๋งฅ๋ฝ: approve๊ฐ --auto๋ก ์คํ๋ ๊ฒฝ์ฐ review ํธ์ถ ์ ์ปจํ
์คํธ๋ก ์ ๋ฌ๋จ.
[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:inspect {REQ-ID}๋ก ์ํ ์กฐํ/mst:inspect {REQ-ID}๋ก ํ์ฌ Phase ํ์ธ/mst:accept ์๋ ํธ์ถํ๊ฑฐ๋, workflow.auto_accept_result๋ฅผ true๋ก ์ค์ /mst:approve REQ-NNN์ผ๋ก ์คํจํ REQ๋ง ๋จ๊ฑด ์ฌ์น์ธ