makdoong2-team 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -147,6 +147,8 @@ permission:
147
147
  2. **REJECTED 사유는 dispatch_verifier 가 자동으로 state.json 에 기록**한다 (`last_verdict_reason` / `last_verdict_reason_hash` / `same_reason_streak` / `rejected_count`). 부장님이 별도 기록할 필요 없다.
148
148
  3. **재-dispatch 시 dispatch_stage 가 자동으로 이전 사유를 프롬프트에 재주입**한다. 부장님은 그냥 `dispatch_stage(issue, target_stage, worktree)` 를 다시 호출하면 된다.
149
149
  4. **동일 REJECTED 사유 연속 5회 감지 시 dispatch_verifier 응답에 `same_reason_streak_exceeded: true`** 가 포함된다. 이때는 재시도를 중단하고 사용자에게 상황을 보고한다 (해시 기반 자동 무한루프 방지장치).
150
+ 5. **`.done=false` 재설정은 당신의 cwd(main repo)에서 그대로 실행한다.** worktree 로 옮겨가지 않는다 — `dispatch_stage` 가 서브세션 생성 전에 main→worktree 정방향 동기화를 하므로 그 값이 전달된다. worktree 사본을 직접 고치면 그 동기화가 덮어쓴다.
151
+ 6. 그런데도 `already_done: true` 가 오면 응답의 `state_copy_mismatch` / `main_repo_done` / `worktree_done` 을 먼저 읽는다. `state_copy_mismatch: true` 는 **자동 동기화가 실패했다**는 뜻이다 — 동기화를 손으로 다시 실행하지 말고 `next_action` 이 지시하는 `state.sh status` 로 확인한 뒤, 해소되지 않으면 사용자에게 에스컬레이션한다.
150
152
 
151
153
  ### 응답 처리 순서
152
154
 
@@ -87,6 +87,8 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
87
87
 
88
88
  기대: 모든 항목(boolean)이 `true`인 JSON 객체. 누락·`false`·문법 오류 = **REJECTED**.
89
89
 
90
+ > **유일한 예외**: `2_implementation.dev` 의 `new_tests_added` 는 `1_planning.requirements` 가 테스트 범위 제외를 선언한 경우(`test_scope.new_tests_required == false`) `false` 가 정상이다. 판정 규칙은 §2 의 `2_implementation.dev` 항목에 있다. 이 예외를 모르는 채 "모든 항목 true" 만 적용하면, 승인된 스코프 아웃이 무한 반려로 되돌아온다 (issue #11).
91
+
90
92
  > **⚠️ 스키마 규약:** state.sh 의 모든 jq path 는 `.stages."<PHASE>".substages."<SUBSTAGE>".<field>` 형태를 따른다. 참조: CLAUDE.md "워크플로우 상태 & 위임 규약".
91
93
 
92
94
  ### 2. 단계 명세 재대조
@@ -119,7 +121,15 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."2_implementation".substage
119
121
  git status --porcelain | grep -v '\.makdoong2-team/' | grep -v 'workspace-analysis\.json'
120
122
  ```
121
123
  출력이 비어 있어야 한다. `.makdoong2-team/` 은 플러그인이 작업 트리 안에 만드는 **자기 상태**이지 analyzer 의 부산물이 아니다. 이것을 위반으로 세면 해당 패턴이 git exclude 에 없는 저장소에서 **항상 REJECTED** 가 나고, 그 시점에 exclude 를 고칠 권한을 가진 역할이 파이프라인에 없어(analyzer 는 산출물 1개만 쓰기 가능 · team-leader 는 하드룰 2 로 차단 · engineer 는 analysis 통과 후 단계) 동일 사유 무한 루프가 된다 (issue #6-②).
122
- - 2_implementation.dev: `done=true` + sub-agent output에 "테스트 추가" 명시 / 5체크
124
+ - 2_implementation.dev: `done=true` + `self_check` 6항목 + **테스트 동반 원칙은 requirements 의 선언을 따른다** (issue #11)
125
+ - 판정 근거는 **state.json 마커와 게이트 재실행**이다. sub-agent output 은 보조 근거이며, **output 이 비어 있거나 특정 문구가 없다는 사실만으로 REJECTED 하지 않는다** — 마커가 충족되면 VERIFIED 다. (`dispatch_stage` 는 텍스트 없이 종료한 세션도 `.done=true` 만으로 성공 처리한다. 그 경로를 verifier 가 문구 검색으로 뒤집으면 재작업 루프만 남는다.)
126
+ - `REQ` = `.stages."1_planning".substages."requirements".test_scope.new_tests_required` 를 읽는다. **마커 부재·`null` 은 `true` 로 간주한다 (fail-closed).**
127
+ ```bash
128
+ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".test_scope'
129
+ ```
130
+ - `REQ == true` → `self_check.new_tests_added == true` 여야 한다. 추가로 staged 변경(`git diff --cached --name-only`)에 실제 테스트 파일이 있는지 확인한다 — 판단 기준은 `workspace-analysis.json` 의 `test_conventions` 다. 마커는 true 인데 테스트 파일이 하나도 없으면 **REJECTED**.
131
+ - `REQ == false` → 테스트 범위 제외가 **요구사항 단계에서 이미 승인·동결된 것**이다. `self_check.new_tests_added` 가 `false` 여도 정상이며, 이때 `.stages."2_implementation".substages."dev".new_tests_waived == true` 만 확인한다. **테스트가 없다는 이유로 REJECTED 하지 않는다** — 그 반려가 승인된 스코프 밖의 테스트 코드를 유입시킨 사고의 직접 원인이었다.
132
+ - 어느 경우든 `gates/stage4-dev-post-verify.sh <이슈키>` 를 재실행해 exit 0 을 확인한다.
123
133
  - 2_implementation.test: `.stages."2_implementation".substages."test"` 의 각 필드가 아래 조건을 모두 충족해야 함
124
134
  - `.unit` ∈ `{"pass", "fail", "skip"}` — `none`/`null` 은 미기록 → REJECTED
125
135
  - `.integration` ∈ `{"pass", "fail", "skip"}` — `none`/`null` 은 미기록 → REJECTED
@@ -1711,22 +1711,73 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
1711
1711
  effectiveWorktree = storedWt;
1712
1712
  }
1713
1713
  }
1714
+ // ── main repo → worktree 정방향 동기화 ──
1715
+ // dispatch_verifier 는 서브세션 생성 전에 이 동기화를 하는데 dispatch_stage
1716
+ // 에는 없었다. 그 비대칭이 REJECTED 재작업 규약을 구조적으로 깨뜨린다:
1717
+ // 규약은 team-leader 에게 `.done=false` 재설정을 시키는데, 리더의 cwd 는
1718
+ // main repo 이고 아래 done 검사는 worktree 사본을 본다. 리더가 규약대로
1719
+ // 했는데도 `already_done: true` 로 차단되고, 리더는 오류 문구만으로는
1720
+ // 원인을 알 수 없어 같은 실수를 반복했다 (issue #11).
1721
+ // 동기화 방향은 파이프라인 불변식과 같다 — main 이 durable 사본, worktree
1722
+ // 는 forward-seed / reverse-merge 되는 작업 사본이다 (finally 의 REVERSE).
1723
+ if (effectiveWorktree !== cwd) {
1724
+ logger.debug(`[wt-sync] FORWARD issue=${args.issue} worktree=${effectiveWorktree} ` +
1725
+ `caller=dispatch_stage stage=${args.target_stage}`);
1726
+ const fwdSync = await $ `bash ${SCRIPTS_DIR}/wt-sync-ignored.sh ${effectiveWorktree} ${args.issue}`
1727
+ .cwd(cwd).quiet().nothrow();
1728
+ if (fwdSync.exitCode !== 0) {
1729
+ logger.warn(`[wt-sync] FORWARD FAIL issue=${args.issue} caller=dispatch_stage exit=${fwdSync.exitCode} ` +
1730
+ `stderr=${redactAndTruncate(fwdSync.stderr?.toString() ?? "", 200)}`);
1731
+ }
1732
+ }
1714
1733
  // done=true stage 재-dispatch 방지 (sub-agent tool-call loop → timeout/empty output).
1715
1734
  // 3_delivery.* 는 hybrid stage (publisher = spec provider) 로 재-진입이 정상 흐름이라 제외.
1716
1735
  const isHybridDelivery = args.target_stage.startsWith("3_delivery.");
1717
1736
  if (!isHybridDelivery) {
1718
- const doneR = await $ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} ${stageJqPath(args.target_stage) + ".done"}`
1737
+ const donePath = stageJqPath(args.target_stage) + ".done";
1738
+ const doneR = await $ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} ${donePath}`
1719
1739
  .cwd(effectiveWorktree).quiet().nothrow();
1720
1740
  if (doneR.exitCode === 0 && doneR.stdout?.toString().trim() === "true") {
1741
+ // 위 정방향 동기화가 성공했다면 두 사본은 같아야 한다. 그래도 다르면
1742
+ // 동기화가 실패한 것이고, 그 사실을 추측이 아니라 관측으로 알린다 —
1743
+ // 종전 문구는 "이미 done=true" 만 말해서, 사본 불일치라는 실제 원인을
1744
+ // 리더가 스스로 추론해야 했다 (state_unreadable 은 이미 안내한다).
1745
+ let mainRepoDone = null;
1746
+ if (effectiveWorktree !== cwd) {
1747
+ const mainDoneR = await $ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} ${donePath}`
1748
+ .cwd(cwd).quiet().nothrow();
1749
+ if (mainDoneR.exitCode === 0)
1750
+ mainRepoDone = mainDoneR.stdout?.toString().trim() ?? null;
1751
+ }
1752
+ const copyMismatch = mainRepoDone !== null && mainRepoDone !== "true";
1721
1753
  return JSON.stringify({
1722
1754
  ok: false,
1723
1755
  gate: args.target_stage,
1724
1756
  stage: args.target_stage,
1725
1757
  agent: spec.id,
1726
1758
  already_done: true,
1727
- reason: `Stage '${args.target_stage}' is already done=true. ` +
1728
- `Re-dispatching a completed stage causes sub-agent tool-call loops (timeout/empty output). ` +
1729
- `Call auto_advance_stage to obtain the correct next stage instead.`,
1759
+ state_copy_mismatch: copyMismatch,
1760
+ worktree_done: "true",
1761
+ main_repo_done: mainRepoDone,
1762
+ reason: copyMismatch
1763
+ ? `Stage '${args.target_stage}' is done=true in the worktree state.json copy but ` +
1764
+ `done=${mainRepoDone} in the main repo copy. state.sh 는 호출 cwd 의 git toplevel 을 ` +
1765
+ `쓰므로 두 사본은 분리돼 있고, 이 검사는 worktree 사본을 본다. ` +
1766
+ `main repo cwd 에서 '.done' 을 되돌렸다면 그 변경은 worktree 사본에 반영되지 않은 것이다 ` +
1767
+ `(정방향 동기화를 이미 1회 자동 시도했고 실패했다).`
1768
+ : `Stage '${args.target_stage}' is already done=true. ` +
1769
+ `Re-dispatching a completed stage causes sub-agent tool-call loops (timeout/empty output). ` +
1770
+ `Call auto_advance_stage to obtain the correct next stage instead. ` +
1771
+ `참고: 방금 '.done=false' 로 되돌렸는데도 이 응답이 왔다면, 다른 cwd(main repo)에서 ` +
1772
+ `state.json 을 조작해 worktree 사본과 불일치한 경우다 — state_unreadable 과 같은 메커니즘이다.`,
1773
+ next_action: copyMismatch
1774
+ ? `동기화는 이미 자동 시도했고 실패했습니다 — 직접 재실행하지 마세요. ` +
1775
+ `'bash ${SCRIPTS_DIR}/state.sh status ${args.issue}' 로 사본 상태를 확인하고, ` +
1776
+ `해소되지 않으면 사용자에게 에스컬레이션하세요.`
1777
+ : `auto_advance_stage(issue: "${args.issue}") 로 올바른 다음 단계를 받으세요. ` +
1778
+ `REJECTED 재작업 중이라면 '.done' 재설정이 실제로 반영됐는지 ` +
1779
+ `bash ${SCRIPTS_DIR}/state.sh get ${args.issue} <done jq-path> 로 먼저 확인하세요 ` +
1780
+ `(jq-path: ${donePath}).`,
1730
1781
  });
1731
1782
  }
1732
1783
  }
@@ -1842,7 +1893,7 @@ export const Makdoong2TeamPlugin = async ({ $, client, directory, worktree }) =>
1842
1893
  `Stage 명세 파일은 위 Stages directory 경로에서 읽으시오. \`<SCRIPTS_DIR>/../stages/\` 상대경로를 사용하지 마시오.`,
1843
1894
  ];
1844
1895
  if (args.target_stage === "2_implementation.dev") {
1845
- base.push(`\n=== dev substage 요구사항 소스 우선순위 ===`, `구현 착수 전 요구사항은 반드시 다음 순서로 참조한다:`, ` a) FIRST — requirements-draft.md 를 우선 확인 (아래 bash 스니펫 그대로 실행):`, buildDraftPathReadSnippet(args.issue, " "), ` 파일이 존재하고 비어있지 않으면 이 파일이 요구사항의 진실의 원천이다.`, ` b) FALLBACK — draft_path 미기록/파일 부재/빈 파일 인 경우 Jira 이슈 조회:`, ` skill(name="jira-research") 로 works MCP 로드 →`, ` skill_mcp(mcp_name="works", tool_name="getIssue", arguments={"issueKey":"${args.issue}"})`, ` c) 두 소스 모두 접근 불가하면 사용자에게 상황을 보고하고 즉시 종료.`, `주의: requirements-draft.md 없이 Jira 이슈만으로 구현 범위를 재결정하지 말 것 — draft 가 없다는 것은 planning 이 부적절하게 진행된 신호이므로 사용자에게 보고하고 종료하시오.`);
1896
+ base.push(`\n=== dev substage 요구사항 소스 우선순위 ===`, `구현 착수 전 요구사항은 반드시 다음 순서로 참조한다:`, ` a) FIRST — requirements-draft.md 를 우선 확인 (아래 bash 스니펫 그대로 실행):`, buildDraftPathReadSnippet(args.issue, " "), ` 파일이 존재하고 비어있지 않으면 이 파일이 요구사항의 진실의 원천이다.`, ` b) FALLBACK — draft_path 미기록/파일 부재/빈 파일 인 경우 Jira 이슈 조회:`, ` skill(name="jira-research") 로 works MCP 로드 →`, ` skill_mcp(mcp_name="works", tool_name="getIssue", arguments={"issueKey":"${args.issue}"})`, ` c) 두 소스 모두 접근 불가하면 사용자에게 상황을 보고하고 즉시 종료.`, `주의: requirements-draft.md 없이 Jira 이슈만으로 구현 범위를 재결정하지 말 것 — draft 가 없다는 것은 planning 이 부적절하게 진행된 신호이므로 사용자에게 보고하고 종료하시오.`, `\n=== 테스트 동반 원칙은 requirements 의 선언을 따른다 (issue #11) ===`, `테스트 추가 여부는 스스로 정하지 않는다. 아래를 실행해 1_planning.requirements 가 승인·동결한 선언을 먼저 읽는다:`, ` bash ${SCRIPTS_DIR}/state.sh get ${args.issue} '.stages."1_planning".substages."requirements".test_scope'`, ` - new_tests_required=true (마커 부재·null 도 true 로 간주 — fail-closed) → 변경에 대한 테스트를 함께 추가하고 self_check.new_tests_added=true 로 기록한다.`, ` - new_tests_required=false → 테스트 추가는 이번 이슈의 범위 밖이다. 추가하지 않는 것이 정답이며, self_check.new_tests_added=false 와 함께 dev.new_tests_waived=true 마커를 남긴다.`, `이 마커는 읽기 전용이다 — engineer 가 test_scope 를 쓰거나 고치지 않는다. 선언과 실제 작업이 맞지 않으면 임의로 면제·추가하지 말고 최종 출력에 적어 보고한다.`);
1846
1897
  }
1847
1898
  if (attemptNum > 1) {
1848
1899
  base.push(`\n=== 재개(resume) 지시 — 이전 세션 중단됨 ===`, `이전 세션 ID: ${priorSessionIds.join(", ")} (attempt ${attemptNum - 1})`, `중단 원인: 이전 sub-session이 stall/gone 감지되어 새 세션으로 이어서 진행합니다.`, `context 승계 방식: opencode API는 세션 간 대화 이력을 옮기지 못하므로 state.json 을 진실의 원천으로 사용합니다.`, `첫 번째 필수 작업:`, ` 1) bash ${SCRIPTS_DIR}/state.sh get ${args.issue} '.' 로 현재 상태 전량 조회`, ` 2) 이미 done=true 로 기록된 substage / 마커는 재실행하지 말고 skip`, ` 3) 미완료 substage 부터 stage spec 순서대로 이어서 진행`, ` 4) 완료 시 관례대로 요약 출력 후 종료`, `주의: state.json 마커가 이미 target substage 완료를 나타내면 즉시 요약만 출력하고 종료하시오 (재작업 금지).`);
@@ -5,6 +5,7 @@
5
5
  # 1. worktree 가 존재하는가
6
6
  # 2. dev-written-files.txt 에 기록된 모든 파일이 staging(index) 혹은 HEAD tree 에 존재하는가
7
7
  # 3. .gitignore 를 존중한 untracked 파일이 0인가
8
+ # 4. 테스트 동반 원칙이 1_planning.requirements 의 test_scope 선언과 일치하는가
8
9
  #
9
10
  # 불변식: 3_delivery.commit 는 untracked 를 자동 제외하므로,
10
11
  # 본 게이트를 통과한 파일만이 커밋 대상에 진입한다.
@@ -66,4 +67,40 @@ $(echo "$UNTRACKED" | sed 's/^/ - /')"
66
67
  fail "$MSG"
67
68
  fi
68
69
 
70
+ # ── 4. 테스트 동반 원칙 — requirements 의 선언을 따른다 (issue #11) ──────────
71
+ #
72
+ # 종전에는 이 검사가 게이트에 없고 verifier 만 알고 있었으며, 그 verifier 기준은
73
+ # "sub-agent output 에 '테스트 추가' 명시" 라는 무조건 요구였다. requirements 가
74
+ # 테스트 범위 제외를 승인·동결해도 그 결정을 참조하는 경로가 없어서, 순수 설정·
75
+ # 인프라 전환 작업마다 REJECTED 가 반복되고 결국 engineer 가 승인된 스코프 밖의
76
+ # 테스트를 추가했다. 게이트·stage spec·verifier 세 곳이 같은 선언을 보게 한다.
77
+ #
78
+ # 판정 규칙 (fail-closed):
79
+ # REQ = requirements.test_scope.new_tests_required — 부재/null = true 로 간주
80
+ # REQ=true → self_check.new_tests_added 는 true 여야 한다
81
+ # REQ=false → new_tests_added 가 false 여도 되지만, 슬립과 구분하기 위해
82
+ # dev.new_tests_waived=true 마커가 함께 있어야 한다
83
+ #
84
+ # self_check 자체가 없는 구형 state 는 검사하지 않는다 (기존 동작 보존).
85
+ q(){ local __v; if __v="$("$HERE/../scripts/state.sh" get "$ISSUE" "$1" 2>/dev/null)"; then printf "%s" "$__v"; else printf "__MISSING__"; fi; }
86
+
87
+ SELF_CHECK="$(q '.stages."2_implementation".substages."dev".self_check')"
88
+ if [ "${SELF_CHECK}" != "__MISSING__" ] && [ "${SELF_CHECK}" != "null" ] && [ -n "${SELF_CHECK}" ]; then
89
+ NEW_TESTS_ADDED="$(q '.stages."2_implementation".substages."dev".self_check.new_tests_added')"
90
+ if [ "${NEW_TESTS_ADDED}" != "true" ]; then
91
+ REQ="$(q '.stages."1_planning".substages."requirements".test_scope.new_tests_required')"
92
+ if [ "${REQ}" != "false" ]; then
93
+ fail "테스트 동반 원칙 미충족: self_check.new_tests_added=${NEW_TESTS_ADDED} 인데
94
+ requirements 의 테스트 범위 선언이 테스트 추가를 요구한다 (test_scope.new_tests_required=${REQ}; 부재/null 은 true 로 간주).
95
+ → 조치 A: 변경에 대한 테스트를 추가하고 new_tests_added=true 로 다시 기록한다.
96
+ → 조치 B: 이 이슈가 테스트를 붙일 수 없는 성질이라면 임의로 면제하지 말고 부장님에게 보고한다 —
97
+ 테스트 범위 제외는 1_planning.requirements 에서만 승인·기록할 수 있다 (stages/02-requirements.md §2-6a)."
98
+ fi
99
+ WAIVED="$(q '.stages."2_implementation".substages."dev".new_tests_waived')"
100
+ [ "${WAIVED}" = "true" ] || fail "테스트 면제 마커 누락: requirements 가 테스트 추가를 제외했지만(test_scope.new_tests_required=false)
101
+ dev.new_tests_waived 마커가 없다 — new_tests_added=false 가 의도된 면제인지 기록 누락인지 구분할 수 없다.
102
+ → 조치: bash <SCRIPTS_DIR>/state.sh set ${ISSUE} '.stages.\"2_implementation\".substages.\"dev\".new_tests_waived' 'true'"
103
+ fi
104
+ fi
105
+
69
106
  echo "MAKDOONG2-GATE OK: 2_implementation.dev_post"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makdoong2-team",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -60,6 +60,7 @@ const STEPS = [
60
60
  "node --test test/gate-post-pr-verify.test.ts",
61
61
  "node --test test/gate-post-review-verify.test.ts",
62
62
  "node --test test/gate-requirements-quality.test.ts",
63
+ "node --test test/dev-test-scope-declaration.test.ts",
63
64
  "node --test test/worktree-sync-gate.test.ts",
64
65
  "node --test test/planner-prompt-early-exit.test.ts",
65
66
  "node --test test/plugin-bug-fixes.test.ts",
package/scripts/state.sh CHANGED
@@ -61,6 +61,37 @@ validate_issue() {
61
61
 
62
62
  sp() { validate_issue "$1"; echo "$(root)/.makdoong2-team/$1/state.json"; }
63
63
 
64
+ # ── 사본 불일치 경고 ────────────────────────────────────────────────────────
65
+ # state.json 사본은 cwd(git toplevel)마다 하나씩이다. 전용 worktree 가 이미 있는데
66
+ # main repo cwd 에서 `set` 을 실행하면, 갱신되는 것은 main 사본이고 dispatch_stage /
67
+ # 게이트가 보는 것은 worktree 사본이다 — 쓴 사람은 반영됐다고 믿는데 파이프라인은
68
+ # 옛 값을 본다. 실제로 REJECTED 재작업 규약(`.done=false` 재설정)이 이 경로에서
69
+ # 두 번 연속 `already_done: true` 오차단으로 되돌아왔고, 오류 문구도 원인을 알려주지
70
+ # 않아 같은 실수가 반복됐다 (issue #11).
71
+ #
72
+ # 쓰기 자체는 막지 않는다 — main 사본을 고치는 것이 옳은 상황도 있다. 어느 사본을
73
+ # 건드렸는지 stderr 로 알리기만 한다 (stdout 계약·종료 코드 불변).
74
+ norm_path() { if [ -d "$1" ]; then (cd "$1" && pwd -P); else printf '%s' "$1"; fi; }
75
+
76
+ warn_if_copy_split() {
77
+ local file="$1" here there
78
+ [ -f "${file}" ] || return 0
79
+ there="$(jq -r '.worktree // ""' "${file}" 2>/dev/null || true)"
80
+ { [ -n "${there}" ] && [ "${there}" != "null" ] && [ -d "${there}" ]; } || return 0
81
+ here="$(norm_path "$(root)")"
82
+ there="$(norm_path "${there}")"
83
+ [ "${here}" != "${there}" ] || return 0
84
+ cat >&2 <<WARN
85
+ [state.sh] 경고: 지금 갱신한 것은 이 cwd 의 사본이지, 이 이슈의 전용 worktree 사본이 아니다.
86
+ 갱신한 사본 : ${here}
87
+ worktree : ${there}
88
+ state.json 사본은 cwd(git toplevel)마다 하나씩이고, dispatch_stage 와 dev 이후 게이트는
89
+ worktree 사본을 본다. 자동 동기화(wt-sync-ignored.sh)는 서브에이전트 세션 앞뒤에서만 돌므로
90
+ 이 쓰기는 다음 dispatch 까지 worktree 사본에 반영되지 않을 수 있다.
91
+ 의도한 것이 worktree 사본이면 그 경로를 cwd 로 하여 다시 실행한다.
92
+ WARN
93
+ }
94
+
64
95
  # usage_die <시그니처> [부연 설명...]
65
96
  # `${2:?}` 가 뱉는 raw bash 에러(`line 127: 2: parameter null or not set`)는 복구
66
97
  # 작업 중인 에이전트에게 아무것도 알려주지 못한다. 대신 무엇을 어떻게 부를지 적는다.
@@ -294,6 +325,7 @@ JSON
294
325
  P="$(sp "${ISSUE}")"
295
326
  check_flat_stage_notation "$Q"
296
327
  write_json_atomic "$P" "set ${Q}" "$Q = $V"
328
+ warn_if_copy_split "$P"
297
329
  echo "state[$ISSUE] $Q = $V" ;;
298
330
  append)
299
331
  ISSUE="${1:-}"; Q="${2:-}"; V="${3:-}"
@@ -225,6 +225,21 @@ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.scope_size' '"large"'
225
225
  bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_by' '"1_planning.requirements"'
226
226
  ```
227
227
 
228
+ **테스트 범위 선언 (`test_scope` — 기계 판독 마커, 필수)**: 위 `**테스트 범위**` 서술은 사람이 읽는 문장이라 dev 게이트·verifier 가 해석할 수 없다. 같은 결정을 마커로 한 번 더 기록한다 — 없으면 `2_implementation.dev` 의 테스트 동반 원칙이 승인된 스코프 아웃을 보지 못하고 무조건 적용된다 (issue #11). 상세: `stages/02-requirements.md` §2-6a.
229
+
230
+ ```bash
231
+ # 테스트를 동반하는 일반적인 경우
232
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
233
+ '{"new_tests_required": true, "unit": "<대상 클래스/메서드>", "integration": "<빌드 플랜명/시나리오>", "rationale": "<한 줄 근거>"}'
234
+
235
+ # 테스트 추가를 이번 이슈 범위에서 제외하기로 승인한 경우 (스코프 아웃에도 함께 명시)
236
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
237
+ '{"new_tests_required": false, "unit": null, "integration": "<기존 통합 테스트로만 검증>", "rationale": "<왜 제외가 타당한지>"}'
238
+ ```
239
+
240
+ - 마커 부재는 `new_tests_required: true` 로 간주된다 (fail-closed) — 선언 누락이 테스트 면제로 둔갑하지 않는다.
241
+ - `test_scope_defined` self_check 항목은 **이 마커를 실제로 기록했다는 뜻**이다. 기록 후 값을 읽어 확인한다.
242
+
228
243
  ### 2-6. Requirements 완료 기록
229
244
 
230
245
  ```bash
@@ -352,3 +352,25 @@ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_by' '"1_plannin
352
352
  ```
353
353
 
354
354
  self_check 에 `paths_explicit` / `test_scope_defined` / `atomic_units` / `scope_out_listed` 4항목을 함께 기록한다.
355
+
356
+ ### 2-6a. 테스트 범위 선언 (`test_scope` — 기계 판독 마커, 필수)
357
+
358
+ 위 `**테스트 범위**` 서술은 사람이 읽는 문장이라 dev 단계의 게이트·verifier 가 해석할 수 없다. **같은 결정을 기계가 읽는 마커로 한 번 더 기록한다** — 이 마커가 없으면 `2_implementation.dev` 의 "테스트 동반 원칙" 이 승인된 스코프 아웃을 보지 못한 채 무조건 적용되어, 요구사항 밖의 테스트 코드가 강제로 추가된다 (issue #11).
359
+
360
+ ```bash
361
+ # 테스트를 동반하는 일반적인 경우
362
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
363
+ '{"new_tests_required": true, "unit": "<대상 클래스/메서드>", "integration": "<빌드 플랜명/시나리오>", "rationale": "<한 줄 근거>"}'
364
+
365
+ # 테스트 추가를 이번 이슈의 범위에서 제외하기로 승인한 경우
366
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".test_scope' \
367
+ '{"new_tests_required": false, "unit": null, "integration": "<기존 통합 테스트로만 검증>", "rationale": "<왜 제외가 타당한지 — 예: 애플리케이션 코드가 아닌 배포 설정 전환이라 단위 테스트 대상이 없음>"}'
368
+ ```
369
+
370
+ - `new_tests_required` 는 **`true` 가 기본값**이다. 마커를 기록하지 않으면 하류(게이트·verifier)는 `true` 로 간주한다 (fail-closed) — 선언 누락이 "테스트 면제" 로 둔갑하지 않는다.
371
+ - `false` 로 선언하려면 **스코프 아웃 항목에 그 사실이 함께 적혀 있어야 하고**, §2-3-2 의 "스코프 아웃 항목은 항상 명시적으로 확인한다" 절차를 거쳐야 한다. `rationale` 은 빈 문자열·`null` 금지.
372
+ - `test_scope_defined` self_check 항목은 **이 마커를 실제로 기록했다는 뜻**이다. 자기선언이 아니라 값을 읽어 확인한다:
373
+ ```bash
374
+ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".test_scope'
375
+ ```
376
+ - 이 선언은 요구사항 명세의 일부다 — 동결(2-4a) 이후 변경은 §2-4a 3번의 재승인 절차만 허용한다. **engineer / verifier 는 이 마커를 쓰지 않는다. 읽기만 한다.**
@@ -99,6 +99,25 @@ bash <SCRIPTS_DIR>/wt-sync-ignored.sh "$WT" "<이슈키>"
99
99
  - **outer-world 에이전트 위임 금지** — engineer 프론트매터에 `Task` 툴이 없으므로 물리적으로 스폰 불가. 구현·조사·리팩토링 모두 본 에이전트가 직접 수행한다. 조사가 필요하면 `skill_mcp` 로 makdoong2 스킬(`bitbucket-research` 등)만 사용.
100
100
  - 3단계 작업 단위 순서대로 구현한다. 한 단위가 끝나면 커밋 가능 상태로 만들어 둔다(실제 커밋은 6단계).
101
101
 
102
+ ## 4-4-pre. 테스트 범위 선언 조회 (필수 — 자가 검증 전)
103
+
104
+ **테스트 동반 원칙은 무조건 적용되지 않는다.** `1_planning.requirements` 가 승인·동결한 테스트 범위 선언(`test_scope`)을 먼저 읽고, 그 선언이 정한 대로만 적용한다. 이 조회를 건너뛰면 "단위 테스트는 이번 변경 대상에 적용하지 않는다" 고 **이미 승인된** 이슈에서도 테스트 추가가 강제되어, 승인된 스코프 밖의 코드가 유입된다 (issue #11).
105
+
106
+ ```bash
107
+ NEW_TESTS_REQUIRED="$(bash <SCRIPTS_DIR>/state.sh get <이슈키> \
108
+ '.stages."1_planning".substages."requirements".test_scope.new_tests_required' 2>/dev/null || echo 'true')"
109
+ # 마커 부재("null")·조회 실패는 true 로 간주한다 (fail-closed).
110
+ [ "$NEW_TESTS_REQUIRED" = "false" ] || NEW_TESTS_REQUIRED=true
111
+ echo "new_tests_required=$NEW_TESTS_REQUIRED"
112
+
113
+ # 근거(왜 제외됐는지)도 함께 읽어 최종 출력에 인용한다.
114
+ bash <SCRIPTS_DIR>/state.sh get <이슈키> \
115
+ '.stages."1_planning".substages."requirements".test_scope.rationale' 2>/dev/null || true
116
+ ```
117
+
118
+ - **이 마커를 engineer 가 쓰는 것은 금지다 (hardrule).** 읽기 전용이다. 테스트가 불필요해 보인다는 자체 판단으로 `test_scope` 를 기록·수정하면 요구사항 동결(§2-4a)을 우회하는 것이다. 선언이 실제 작업과 맞지 않으면 `done` 을 기록하지 말고 부장님에게 보고한다.
119
+ - `new_tests_required=true` 인데 대상이 테스트를 붙일 수 없는 성질(순수 설정 파일 등)이라고 판단되면 — **임의로 면제하지 말고** 그 사실을 최종 출력에 적어 부장님이 requirements 재작업 여부를 결정하게 한다.
120
+
102
121
  ## 4-4. 최종 자가 검증 (Pre-Completion Checklist)
103
122
 
104
123
  `done=true` 직전, 아래 6체크를 자가 검증한다.
@@ -108,18 +127,35 @@ bash <SCRIPTS_DIR>/wt-sync-ignored.sh "$WT" "<이슈키>"
108
127
  |---|---|
109
128
  | 1 | 3단계에서 합의한 모든 수정/추가 파일이 구현되었다 (스코프 100% 충족) |
110
129
  | 2 | 기존 테스트(`sbt test` / `./gradlew test` / `mvn test` 등)가 모두 통과한다 |
111
- | 3 | 새 기능·버그 수정에 대한 테스트가 함께 추가되었다 (테스트 동반 원칙) |
130
+ | 3 | **`new_tests_required=true` 인 경우에만** — 새 기능·버그 수정에 대한 테스트가 함께 추가되었다 (테스트 동반 원칙). `false` 면 이 항목은 면제되며 테스트를 추가하지 않는 것이 정답이다 |
112
131
  | 4 | 타입/린트/컴파일 에러가 0이다 |
113
132
  | 5 | `.env` / secrets / API 키 / 하드코딩된 비밀이 코드·테스트·로그에 노출되지 않았다 |
114
133
  | 6 | `write`/`edit`/`patch`/`multiedit` 로 편집한 모든 파일이 staging area 에 반영되었다 (`git ls-files --others --exclude-standard` 결과 0) |
115
134
 
116
135
  > 항목 6은 `tool.execute.after` 훅이 매 write 완료 시 자동으로 `git add`를 수행하므로 기본적으로 자동 충족된다. 훅 실패로 untracked 가 남으면 §4-5 exit gate 가 BLOCK 하여 재작업을 요구한다.
117
136
 
137
+ **`new_tests_required=true` (기본)** — 테스트를 추가하고 `new_tests_added: true` 로 기록한다:
138
+
118
139
  ```bash
119
140
  bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."2_implementation".substages."dev".self_check' \
120
141
  '{"scope_met": true, "existing_tests_pass": true, "new_tests_added": true, "type_lint_clean": true, "no_secrets": true, "all_writes_staged": true}'
121
142
  ```
122
143
 
144
+ **`new_tests_required=false` (승인된 스코프 아웃)** — `new_tests_added` 를 `false` 로 기록하고, 그것이 슬립이 아니라 선언에 따른 면제임을 나타내는 `new_tests_waived` 마커를 함께 남긴다. **두 기록은 한 쌍이다** — `new_tests_waived` 없이 `new_tests_added: false` 만 있으면 §4-5 exit gate 가 BLOCK 한다:
145
+
146
+ ```bash
147
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."2_implementation".substages."dev".self_check' \
148
+ '{"scope_met": true, "existing_tests_pass": true, "new_tests_added": false, "type_lint_clean": true, "no_secrets": true, "all_writes_staged": true}'
149
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."2_implementation".substages."dev".new_tests_waived' 'true'
150
+ ```
151
+
152
+ ### 최종 출력에 반드시 포함할 것
153
+
154
+ verifier 는 state.json 마커로 판정하지만, 사람이 스코프 이탈을 조기에 발견할 수 있도록 출력에 한 줄을 남긴다:
155
+
156
+ - `new_tests_required=true` → 추가한 테스트 파일 목록과 실행 결과
157
+ - `new_tests_required=false` → `테스트 추가 없음 — requirements 의 test_scope.new_tests_required=false (사유: <rationale>)`
158
+
123
159
  ## 4-5. Exit Gate 실행 (staging 강제)
124
160
 
125
161
  ```bash