makdoong2-team 2.3.2 → 3.0.0

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.
Files changed (42) hide show
  1. package/README.md +4 -12
  2. package/agents/makdoong2-engineer.md +1 -0
  3. package/agents/makdoong2-planner.md +17 -61
  4. package/agents/makdoong2-team-leader.md +14 -4
  5. package/agents/makdoong2-verifier.md +6 -6
  6. package/assets/makdoong2-team.schema.json +0 -18
  7. package/bin/cli.js +27 -1
  8. package/bin/cli.ts +27 -1
  9. package/dist/agent-stage-config.d.ts +1 -1
  10. package/dist/agent-stage-config.js +0 -11
  11. package/dist/apply-patch-paths.d.ts +46 -0
  12. package/dist/apply-patch-paths.js +114 -0
  13. package/dist/config.d.ts +21 -8
  14. package/dist/config.js +34 -0
  15. package/dist/model-fallback-policy.js +0 -4
  16. package/dist/opencode-plugin.js +121 -307
  17. package/dist/poll-sub-session.d.ts +2 -0
  18. package/dist/poll-sub-session.js +37 -9
  19. package/gates/stage-analysis-verify.sh +63 -5
  20. package/gates/stage4-dev-verify.sh +6 -6
  21. package/gates/verify.sh +0 -2
  22. package/opencode.json.example +0 -1
  23. package/package.json +1 -1
  24. package/scripts/gate-policy-test.sh +15 -14
  25. package/scripts/install-lib.mjs +1 -1
  26. package/scripts/install-lib.mts +1 -1
  27. package/scripts/model-policy.mjs +0 -4
  28. package/scripts/model-policy.mts +0 -4
  29. package/scripts/release.sh +13 -1
  30. package/scripts/run-tests.mts +3 -1
  31. package/scripts/state.sh +24 -2
  32. package/skills/_lib/load-secret.sh +15 -5
  33. package/src/hooks/session-start.sh +0 -1
  34. package/stages/01-planning.md +35 -54
  35. package/stages/02-requirements.md +53 -35
  36. package/stages/04-analysis.md +2 -2
  37. package/stages/05-worktree-dev.md +15 -0
  38. package/agents/makdoong2-researcher.md +0 -68
  39. package/dist/research-fanout.d.ts +0 -165
  40. package/dist/research-fanout.js +0 -341
  41. package/gates/stage3-scope-verify.sh +0 -69
  42. package/stages/03-scope.md +0 -81
@@ -314,9 +314,15 @@ export async function pollSubSession(client, sessionId, options = {}) {
314
314
  .catch(() => undefined);
315
315
  }
316
316
  else {
317
+ // 두 실패를 뭉뚱그리지 않는다. "허용 경로 밖" 은 대안 경로를 알려주면
318
+ // 회복 가능하고, "external_directory 가 아닌 권한" 은 경로와 무관한
319
+ // 에이전트 설정 문제라 조치가 정반대다. 종전에는 한 문장이었다.
320
+ const reason = p.permission === "external_directory"
321
+ ? "outside_allowed_roots"
322
+ : "non_external_permission";
317
323
  err(`[pollSubSession] PERMISSION_STALL session=${sessionId} polls=${pollCount}` +
318
324
  ` permissionID=${p.id} type=${p.permission} patterns=${JSON.stringify(p.patterns)}` +
319
- ` — auto-rejecting (outside worktree scope or non-external_directory)`);
325
+ ` reason=${reason} — auto-rejecting`);
320
326
  await client.permission
321
327
  .reply({ path: { requestID: p.id }, body: { reply: "reject" } })
322
328
  .catch(() => undefined);
@@ -329,6 +335,7 @@ export async function pollSubSession(client, sessionId, options = {}) {
329
335
  permissionID: p.id,
330
336
  permissionType: p.permission,
331
337
  permissionPatterns: p.patterns,
338
+ permissionReason: reason,
332
339
  };
333
340
  }
334
341
  }
@@ -379,6 +386,7 @@ export async function pollSubSession(client, sessionId, options = {}) {
379
386
  permissionID: stalledPerm?.id,
380
387
  permissionType: stalledPerm?.permission,
381
388
  permissionPatterns: stalledPerm?.patterns,
389
+ permissionReason: "tool_call_stall",
382
390
  };
383
391
  }
384
392
  // Message stall: sub-agent registered (sessionEverAppeared) and appears
@@ -601,17 +609,37 @@ export function pollOutcomeToLegacy(outcome) {
601
609
  };
602
610
  case "aborted":
603
611
  return { text: `(aborted: ${outcome.reason})`, success: false };
604
- case "permission_stall":
612
+ case "permission_stall": {
613
+ // 사유별로 **처방이 다르다.** 종전에는 셋을 한 문장으로 보고해서, 워크스페이스
614
+ // 밖 접근(대안 경로를 알려주면 끝날 일)과 부분 설치(재설치가 필요한 일)와
615
+ // 에이전트 오설정이 전부 같은 메시지로 나왔다.
616
+ const detail = outcome.permissionType
617
+ ? `${outcome.permissionType} permission (id=${outcome.permissionID}` +
618
+ `${outcome.permissionPatterns ? `, patterns=${JSON.stringify(outcome.permissionPatterns)}` : ""})`
619
+ : "an unidentified permission request";
620
+ const remedy = (() => {
621
+ switch (outcome.permissionReason) {
622
+ case "outside_allowed_roots":
623
+ return "워크스페이스 밖 경로 접근이라 차단됐다. " +
624
+ "임시 파일이 필요하면 `/tmp` 이 아니라 worktree 안의 " +
625
+ "`.makdoong2-team/<이슈키>/tmp/` 에 만들어라 — 그 경로는 cwd 안이라 승인이 필요 없고, " +
626
+ "git exclude 와 worktree 동기화 대상이다. `/tmp` 에 쓴 것은 동기화도 커밋도 되지 않아 조용히 사라진다.";
627
+ case "non_external_permission":
628
+ return "external_directory 가 아닌 권한 요청이 서브에이전트에 도달했다 — 경로 문제가 아니라 " +
629
+ "에이전트 permission 설정 문제다. 해당 에이전트의 frontmatter `permission:` 블록을 점검하라 " +
630
+ "(정식 키는 `edit`; `write` 키는 존재하지 않아 조용히 무시된다).";
631
+ default:
632
+ return "대기 대상을 특정하지 못했다. 점검: opencode.json 의 permission.external_directory 시드 존재 여부 " +
633
+ "(`npx makdoong2-team doctor`).";
634
+ }
635
+ })();
605
636
  return {
606
- text: outcome.permissionType
607
- ? `(permission_stall: sub-agent blocked on ${outcome.permissionType} permission ` +
608
- `(id=${outcome.permissionID}${outcome.permissionPatterns ? `, patterns=${JSON.stringify(outcome.permissionPatterns)}` : ""}) ` +
609
- `— cannot be answered in subagent context; aborted after ${outcome.stalledMs}ms)`
610
- : `(permission_stall: sub-agent tool call stalled for ${outcome.stalledMs}ms — likely waiting for a permission approval ` +
611
- `that cannot be answered in subagent context; pending permission could not be identified (permission.list unavailable or empty). ` +
612
- `점검: opencode.json 의 permission.external_directory 시드 존재 여부 (npx makdoong2-team doctor))`,
637
+ text: `(permission_stall: sub-agent blocked on ${detail} — cannot be answered in subagent context; ` +
638
+ `aborted after ${outcome.stalledMs}ms. reason=${outcome.permissionReason ?? "unknown"})\n` +
639
+ `→ ${remedy}`,
613
640
  success: false,
614
641
  };
642
+ }
615
643
  case "session_gone":
616
644
  return {
617
645
  text: outcome.reason === "message_stall"
@@ -5,7 +5,8 @@
5
5
  #
6
6
  # 진입 조건:
7
7
  # 1. state.json 존재
8
- # 2. 1_planning.scope substage done=true
8
+ # 2. 1_planning.requirements substage done=true + 승인 + 품질 게이트
9
+ # (scope 흡수 전 stage3-scope-verify.sh 가 하던 검사를 그대로 이어받는다)
9
10
  #
10
11
  # SKIP 조건:
11
12
  # 프로젝트 루트에 build tool 마커 파일이 하나도 없음
@@ -41,11 +42,68 @@ q() {
41
42
  ANALYSIS_DONE="$(q '.stages."2_implementation".substages."analysis".done')"
42
43
  [ "$ANALYSIS_DONE" != "true" ] || fail "이미 done=true 완료됨 — auto_advance_stage 로 다음 단계 진행"
43
44
 
44
- # 3. scope substage 완료 확인
45
- SCOPE_DONE="$(q '.stages."1_planning".substages."scope".done')"
46
- [ "$SCOPE_DONE" = "true" ] || fail "1_planning.scope 미완료 (done=$SCOPE_DONE)"
45
+ # 3. requirements substage 완료 + 승인 + 품질 확인
46
+ #
47
+ # 종전에는 `1_planning.scope` substage 의 done 만 봤다. scope 가 requirements 로
48
+ # 흡수되면서(범위 확정은 요구사항 확정과 같은 판단의 연속이다) 그 자리를 지키던
49
+ # stage3-scope-verify.sh 의 검사가 통째로 이 게이트로 넘어왔다. 검사를 옮기지 않고
50
+ # 게이트만 지우면 승인·모호성·spec drift 검증이 파이프라인에서 사라진다.
51
+ REQ_DONE="$(q '.stages."1_planning".substages."requirements".done')"
52
+ [ "$REQ_DONE" = "true" ] || fail "1_planning.requirements 미완료 (done=$REQ_DONE)"
53
+
54
+ if [ "$(q '.policy.auto_approve."1_planning.requirements"')" != "true" ]; then
55
+ [ "$(q '.stages."1_planning".substages."requirements".approved_by_user')" = "true" ] \
56
+ || fail "requirements substage 사용자 승인 없음 (또는 .policy.auto_approve.\"1_planning.requirements\" 미설정)"
57
+ if [ "$(q '.stages."1_planning".substages."requirements".verification_pending')" = "true" ]; then
58
+ fail "requirements substage 검증 대기 중 (verification_pending) — 사용자 승인 후 approved_by_user를 기록하라"
59
+ fi
60
+ fi
61
+
62
+ if [ "$(q '.stages."1_planning".substages."requirements".interview_required')" = "true" ]; then
63
+ [ "$(q '.stages."1_planning".substages."requirements".interview_completed')" = "true" ] \
64
+ || fail "requirements substage 인터뷰 미완료 — 모든 미결 항목 해소 후 interview_completed=true 기록 필요"
65
+ fi
66
+
67
+ # --- 요구사항 품질 게이트 (조건부 — 마커 존재 시에만 검사, 구형 state 호환) ---
68
+ # 1) Ambiguity Score 수렴: 기록된 값이 0.2 초과면 요구사항 미수렴 (stages/02-requirements.md)
69
+ AMB="$(q '.stages."1_planning".substages."requirements".ambiguity_score')"
70
+ if [ "$AMB" != "__MISSING__" ] && [ "$AMB" != "null" ] && [ -n "$AMB" ]; then
71
+ awk -v a="$AMB" 'BEGIN { exit (a + 0 <= 0.2) ? 0 : 1 }' \
72
+ || fail "ambiguity_score=$AMB > 0.2 — 요구사항 미수렴. 인터뷰로 미결 항목 해소 후 재산정하라"
73
+ fi
74
+
75
+ # 2) 명세 동결(spec drift) 검증: spec_hash 기록 시 draft 파일 해시 재계산 일치 필요
76
+ SPEC_HASH="$(q '.stages."1_planning".substages."requirements".spec_hash')"
77
+ if [ "$SPEC_HASH" != "__MISSING__" ] && [ "$SPEC_HASH" != "null" ] && [ -n "$SPEC_HASH" ]; then
78
+ DRAFT="$(q '.stages."1_planning".substages."requirements".draft_path')"
79
+ ROOT="$("$SCRIPTS/state.sh" root)"
80
+ # 마커 누락과 파일 부재를 구분해서 알린다. 둘 다 "확정 명세 파일 없음" 으로
81
+ # 뭉뚱그리면 파일은 멀쩡하고 마커만 빠진 흔한 경우(issue #6-①)에 무엇을 해야
82
+ # 하는지 알 수 없다.
83
+ DEFAULT_DRAFT=".makdoong2-team/${ISSUE}/requirements-draft.md"
84
+ if [ "$DRAFT" = "__MISSING__" ] || [ "$DRAFT" = "null" ] || [ -z "$DRAFT" ]; then
85
+ if [ -f "$ROOT/$DEFAULT_DRAFT" ]; then
86
+ fail "spec_hash 는 기록됐는데 draft_path 마커가 없다 — 파일은 ${DEFAULT_DRAFT} 에 있다.
87
+ 복구: (1) sha256sum \"${ROOT}/${DEFAULT_DRAFT}\" 가 spec_hash(${SPEC_HASH}) 와 같은지 대조하고,
88
+ (2) 같으면 state.sh set 으로 requirements.draft_path 에 \"${DEFAULT_DRAFT}\" 를 기록한다,
89
+ (3) 다르면 명세가 동결 후 변경된 것이므로 requirements substage 를 재작업한다."
90
+ fi
91
+ fail "spec_hash 는 기록됐는데 draft_path 마커도 확정 명세 파일도 없다 — requirements substage 를 재작업하라 (stages/02-requirements.md)"
92
+ fi
93
+ # "생성된 적 없음" 을 안내에서 빼면 복구 방향을 잘못 잡는다 — 실제로 가장 흔한
94
+ # 경우인데 종전 메시지는 동기화 누락·삭제만 언급해 리더가 동기화 문제부터
95
+ # 의심했다 (issue #8).
96
+ [ -f "$ROOT/$DRAFT" ] \
97
+ || fail "draft_path=${DRAFT} 마커는 있으나 파일이 없다 (기준 경로 ${ROOT}). 가능한 원인 순서대로:
98
+ (1) 애초에 생성된 적 없음 — planner 가 마커만 기록하고 파일 생성에 실패한 경우. requirements substage 를 재작업해 쓰기 툴로 초안부터 생성하라 (stages/02-requirements.md),
99
+ (2) worktree 동기화 누락 — 다른 cwd(main repo/worktree)의 같은 상대경로에 파일이 있는지 확인,
100
+ (3) 파일이 삭제됨 — 삭제 경위 확인 후 requirements 재작업"
101
+ ACTUAL="$(sha256sum "$ROOT/$DRAFT" | cut -d' ' -f1)"
102
+ [ "$ACTUAL" = "$SPEC_HASH" ] \
103
+ || fail "확정 명세 무단 변경 감지 (spec drift) — 동결 후 변경은 사용자 재승인 + spec_hash 재기록 절차만 허용 (stages/02-requirements.md)"
104
+ fi
47
105
 
48
- # 3. SKIP 판정: worktree 루트에서 build tool 마커 파일 존재 여부
106
+ # 4. SKIP 판정: worktree 루트에서 build tool 마커 파일 존재 여부
49
107
  WT="$(q '.worktree' | tr -d '"')"
50
108
  [ -d "$WT" ] || WT="$(pwd)"
51
109
 
@@ -23,7 +23,7 @@ assert_worktree_sibling(){
23
23
  [ "$pwt" = "$pmain" ] || fail "worktree가 메인 repo의 형제 디렉토리가 아님: $wt (메인: $main)"
24
24
  }
25
25
  [ "$(q '.stages."2_implementation".substages."dev".done')" != "true" ] || fail "이미 done=true 완료됨 — auto_advance_stage 로 다음 단계 진행"
26
- [ "$(q '.stages."1_planning".substages."scope".done')" = "true" ] || fail "scope substage 미완료"
26
+ [ "$(q '.stages."1_planning".substages."requirements".done')" = "true" ] || fail "requirements substage 미완료"
27
27
  # analysis substage 는 게이트가 SKIP 시 done=true 로 마킹하므로 skipped 여부와 무관하게 done=true 검사만 하면 된다.
28
28
  [ "$(q '.stages."2_implementation".substages."analysis".done')" = "true" ] || fail "analysis substage 미완료 (skipped 도 done=true 로 처리됨)"
29
29
 
@@ -31,11 +31,11 @@ WT="$(q '.worktree')"
31
31
  [ -n "$WT" ] && [ "$WT" != "__MISSING__" ] || fail "worktree 경로 미설정 — state.json의 .worktree 필드 확인"
32
32
  assert_worktree_sibling "$WT"
33
33
 
34
- if [ "$(q '.policy.auto_approve."1_planning.scope"')" != "true" ]; then
35
- [ "$(q '.stages."1_planning".substages."scope".approved_by_user')" = "true" ] \
36
- || fail "scope substage 사용자 승인 없음 (또는 .policy.auto_approve.\"1_planning.scope\" 미설정)"
37
- if [ "$(q '.stages."1_planning".substages."scope".verification_pending')" = "true" ]; then
38
- fail "scope substage 검증 대기 중 (verification_pending) — 사용자 승인 후 approved_by_user를 기록하라"
34
+ if [ "$(q '.policy.auto_approve."1_planning.requirements"')" != "true" ]; then
35
+ [ "$(q '.stages."1_planning".substages."requirements".approved_by_user')" = "true" ] \
36
+ || fail "requirements substage 사용자 승인 없음 (또는 .policy.auto_approve.\"1_planning.requirements\" 미설정)"
37
+ if [ "$(q '.stages."1_planning".substages."requirements".verification_pending')" = "true" ]; then
38
+ fail "requirements substage 검증 대기 중 (verification_pending) — 사용자 승인 후 approved_by_user를 기록하라"
39
39
  fi
40
40
  fi
41
41
  echo "MAKDOONG2-GATE OK: 2_implementation.dev"
package/gates/verify.sh CHANGED
@@ -23,8 +23,6 @@ case "$TARGET" in
23
23
  echo "MAKDOONG2-GATE OK: $TARGET (jira validation handled by extension gate)" ;;
24
24
  1_planning.requirements)
25
25
  "$HERE/stage2-requirements-verify.sh" "$ISSUE" ;;
26
- 1_planning.scope)
27
- "$HERE/stage3-scope-verify.sh" "$ISSUE" ;;
28
26
  2_implementation.analysis)
29
27
  "$HERE/stage-analysis-verify.sh" "$ISSUE" ;;
30
28
  2_implementation.dev)
@@ -34,7 +34,6 @@
34
34
  "verify_stage": true,
35
35
  "dispatch_stage": true,
36
36
  "dispatch_verifier": true,
37
- "dispatch_research": true,
38
37
  "auto_advance_stage": true,
39
38
  "get_fallback_model": true
40
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makdoong2-team",
3
- "version": "2.3.2",
3
+ "version": "3.0.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -51,44 +51,45 @@ expect(){
51
51
  echo ""
52
52
  echo "[gate-policy-test] 조건부 auto-approve 게이트 분기"
53
53
  echo ""
54
- echo "── stage3-scope-verify (2→3) ──"
54
+ echo "── stage-analysis-verify (requirements→analysis) ──"
55
+ # scope substage 흡수로 stage-analysis-verify.sh 의 승인·정합성 검사가 이 게이트로 이관됐다.
55
56
  rm -f "$REPORT"
56
57
 
57
58
  # minor (기본): auto_approve true → 승인/pending 무시하고 통과
58
59
  mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".verification_pending=true | .policy={category:"minor",auto_approve:{"1_planning.requirements":true}}'
59
- expect "minor 기본 auto-approve (승인 없이 통과)" pass stage3-scope-verify.sh
60
+ expect "minor 기본 auto-approve (승인 없이 통과)" pass stage-analysis-verify.sh
60
61
 
61
62
  # major (기본): auto_approve true → 통과 (minor 와 동일 흐름 — category 는 라벨용)
62
63
  mk '.stages."1_planning".substages."requirements".done=true | .policy={category:"major",auto_approve:{"1_planning.requirements":true}}'
63
- expect "major 기본도 요구사항 게이트는 무인 통과" pass stage3-scope-verify.sh
64
+ expect "major 기본도 요구사항 게이트는 무인 통과" pass stage-analysis-verify.sh
64
65
 
65
66
  # legacy(no policy) + 미승인 → 차단
66
67
  mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".approved_by_user=false'
67
- expect "구형 state 미승인 → 차단" block stage3-scope-verify.sh
68
+ expect "구형 state 미승인 → 차단" block stage-analysis-verify.sh
68
69
 
69
70
  # legacy(no policy) + 승인 → 통과
70
71
  mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".approved_by_user=true | .stages."1_planning".substages."requirements".verification_pending=false'
71
- expect "구형 state 사용자 승인 → 통과" pass stage3-scope-verify.sh
72
+ expect "구형 state 사용자 승인 → 통과" pass stage-analysis-verify.sh
72
73
 
73
74
  # auto-approve여도 인터뷰 미완료면 차단 (정합성 게이트는 독립)
74
75
  mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".interview_required=true | .stages."1_planning".substages."requirements".interview_completed=false | .policy={category:"minor",auto_approve:{"1_planning.requirements":true}}'
75
- expect "인터뷰 미완료 → 차단 (auto와 무관)" block stage3-scope-verify.sh
76
+ expect "인터뷰 미완료 → 차단 (auto와 무관)" block stage-analysis-verify.sh
76
77
 
77
78
  echo ""
78
79
  echo "── stage4-dev-verify (3→4) ──"
79
80
  # worktree 경로 검증을 위해 실제 git worktree 생성 (메인 repo의 형제)
80
81
  WT="$(dirname "$TMP")/$(basename "$TMP")-TEST-1"
81
82
  (cd "$ROOT" && git worktree add "$WT" -b test-branch 2>/dev/null || true)
82
- mk '.stages."1_planning".substages."scope".done=true | .stages."1_planning".substages."scope".verification_pending=true | .stages."2_implementation".substages."analysis".done=true | .policy={category:"minor",auto_approve:{"1_planning.scope":true}} | .worktree="'"$WT"'"'
83
+ mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".verification_pending=true | .stages."2_implementation".substages."analysis".done=true | .policy={category:"minor",auto_approve:{"1_planning.requirements":true}} | .worktree="'"$WT"'"'
83
84
  expect "minor auto-approve → 통과" pass stage4-dev-verify.sh
84
- mk '.stages."1_planning".substages."scope".done=true | .stages."1_planning".substages."scope".approved_by_user=false | .stages."2_implementation".substages."analysis".done=true | .worktree="'"$WT"'"'
85
+ mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".approved_by_user=false | .stages."2_implementation".substages."analysis".done=true | .worktree="'"$WT"'"'
85
86
  expect "구형 state 미승인 → 차단" block stage4-dev-verify.sh
86
- mk '.stages."1_planning".substages."scope".done=true | .stages."1_planning".substages."scope".approved_by_user=true | .stages."2_implementation".substages."analysis".done=true | .worktree="'"$WT"'"'
87
+ mk '.stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."requirements".approved_by_user=true | .stages."2_implementation".substages."analysis".done=true | .worktree="'"$WT"'"'
87
88
  expect "구형 state 승인 → 통과" pass stage4-dev-verify.sh
88
89
 
89
90
  echo ""
90
91
  echo "── stage6-commit-verify (5→6) ──"
91
- TESTS='.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."scope".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."2_implementation".substages."test".integration="skip" | .stages."2_implementation".substages."test".coverage="exempt"'
92
+ TESTS='.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."2_implementation".substages."test".integration="skip" | .stages."2_implementation".substages."test".coverage="exempt"'
92
93
 
93
94
  # minor (기본): commit auto_approve true → 보고서/승인 없이 통과
94
95
  rm -f "$REPORT"
@@ -130,20 +131,20 @@ expect "구형 state → 휴먼 게이트 없이 통과" pass stage6-commit-veri
130
131
 
131
132
  echo ""
132
133
  echo "── stage8-review-verify (7→8) ──"
133
- BV='.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."scope".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:true,template_match:true,section_content_match:true} | .stages."3_delivery".substages."pr".reviewer_added=true | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true'
134
+ BV='.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:true,template_match:true,section_content_match:true} | .stages."3_delivery".substages."pr".reviewer_added=true | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true'
134
135
  mk "$BV | .policy={category:\"minor\",auto_approve:{\"3_delivery.pr\":true}}"
135
136
  expect "minor auto-approve → 통과" pass stage8-review-verify.sh
136
137
  mk "$BV | .stages.\"3_delivery\".substages.\"pr\".approved_by_user=false"
137
138
  expect "구형 state 미승인 → 차단" block stage8-review-verify.sh
138
139
  mk "$BV | .stages.\"3_delivery\".substages.\"pr\".approved_by_user=true | .stages.\"3_delivery\".substages.\"pr\".verification_pending=false"
139
140
  expect "구형 state 승인 → 통과" pass stage8-review-verify.sh
140
- mk '.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."scope".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:false,template_match:true,section_content_match:true} | .stages."3_delivery".substages."pr".reviewer_added=true | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true | .policy={category:"minor",auto_approve:{"3_delivery.pr":true}}'
141
+ mk '.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:false,template_match:true,section_content_match:true} | .stages."3_delivery".substages."pr".reviewer_added=true | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true | .policy={category:"minor",auto_approve:{"3_delivery.pr":true}}'
141
142
  expect "body_validation false → 차단 (auto와 무관)" block stage8-review-verify.sh
142
143
 
143
- BSS='.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."scope".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:true,template_match:true,section_content_match:true} | .stages."3_delivery".substages."pr".reviewer_self_skipped=true | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true'
144
+ BSS='.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:true,template_match:true,section_content_match:true} | .stages."3_delivery".substages."pr".reviewer_self_skipped=true | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true'
144
145
  mk "$BSS | .policy={category:\"minor\",auto_approve:{\"3_delivery.pr\":true}}"
145
146
  expect "reviewer_self_skipped=true → reviewer 없어도 통과" pass stage8-review-verify.sh
146
- mk '.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."1_planning".substages."scope".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:true,template_match:true,section_content_match:true} | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true | .policy={category:"minor",auto_approve:{"3_delivery.pr":true}}'
147
+ mk '.stages."1_planning".substages."jira".done=true | .stages."1_planning".substages."requirements".done=true | .stages."2_implementation".substages."dev".done=true | .stages."2_implementation".substages."test".unit="pass" | .stages."3_delivery".substages."commit".done=true | .stages."3_delivery".substages."pr".draft_url="http://pr/1" | .stages."3_delivery".substages."pr".body_validation={no_orphan_scenarios:true,template_match:true,section_content_match:true} | .stages."3_delivery".substages."review".comments=1 | .stages."3_delivery".substages."review".all_comments_inline=true | .policy={category:"minor",auto_approve:{"3_delivery.pr":true}}'
147
148
  expect "reviewer_added·reviewer_self_skipped 모두 없음 → 차단" block stage8-review-verify.sh
148
149
 
149
150
  echo ""
@@ -134,7 +134,7 @@ const LEGACY_RELATIVE_PLUGIN_REF = "./plugins/makdoong2-team/src/opencode-plugin
134
134
  const LEGACY_ABSOLUTE_TS_SUFFIX = "/src/opencode-plugin.ts";
135
135
  const LEGACY_SCOPED_NAME = "@local/makdoong2-team";
136
136
  const PLUGIN_PACKAGE_NAME = "makdoong2-team";
137
- const TOOLS = ["verify_stage", "dispatch_stage", "dispatch_verifier", "dispatch_research", "auto_advance_stage", "get_fallback_model"];
137
+ const TOOLS = ["verify_stage", "dispatch_stage", "dispatch_verifier", "auto_advance_stage", "get_fallback_model"];
138
138
  const TOOL_SEARCH_PLUGIN_PREFIX = "opencode-tool-search";
139
139
  // UTIL_SCRIPTS is no longer deployed (scripts live inside the npm module).
140
140
  // Retained for cleanup: older installs copied these into ~/.config/opencode/scripts/
@@ -164,7 +164,7 @@ const LEGACY_RELATIVE_PLUGIN_REF = "./plugins/makdoong2-team/src/opencode-plugin
164
164
  const LEGACY_ABSOLUTE_TS_SUFFIX = "/src/opencode-plugin.ts";
165
165
  const LEGACY_SCOPED_NAME = "@local/makdoong2-team";
166
166
  const PLUGIN_PACKAGE_NAME = "makdoong2-team";
167
- const TOOLS = ["verify_stage", "dispatch_stage", "dispatch_verifier", "dispatch_research", "auto_advance_stage", "get_fallback_model"];
167
+ const TOOLS = ["verify_stage", "dispatch_stage", "dispatch_verifier", "auto_advance_stage", "get_fallback_model"];
168
168
  const TOOL_SEARCH_PLUGIN_PREFIX = "opencode-tool-search";
169
169
  // UTIL_SCRIPTS is no longer deployed (scripts live inside the npm module).
170
170
  // Retained for cleanup: older installs copied these into ~/.config/opencode/scripts/
@@ -69,10 +69,6 @@ export const DEFAULT_POLICIES = Object.freeze({
69
69
  fallbacks: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
70
70
  },
71
71
  // Research fan-out worker — one session per source, spawned in parallel.
72
- "makdoong2-researcher": {
73
- primary: { id: "github-copilot/gpt-5.6-luna", variant: "xhigh", tier: "medium" },
74
- fallbacks: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
75
- },
76
72
  "makdoong2-planner": {
77
73
  primary: { id: "github-copilot/gpt-5.6-luna", variant: "xhigh", tier: "medium" },
78
74
  fallbacks: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
@@ -130,10 +130,6 @@ export const DEFAULT_POLICIES: Readonly<Record<string, AgentModelPolicy>> = Obje
130
130
  fallbacks: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
131
131
  },
132
132
  // Research fan-out worker — one session per source, spawned in parallel.
133
- "makdoong2-researcher": {
134
- primary: { id: "github-copilot/gpt-5.6-luna", variant: "xhigh", tier: "medium" },
135
- fallbacks: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
136
- },
137
133
  "makdoong2-planner": {
138
134
  primary: { id: "github-copilot/gpt-5.6-luna", variant: "xhigh", tier: "medium" },
139
135
  fallbacks: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
@@ -160,7 +160,19 @@ info "다음 버전: $NEXT_VERSION"
160
160
  log "이번 릴리스에 포함될 커밋:"
161
161
  LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || echo '')"
162
162
  if [ -n "$LAST_TAG" ]; then
163
- git log --oneline "${LAST_TAG}..HEAD" | head -20
163
+ # `git log … | head -N` 을 쓰지 않는다. set -o pipefail 아래서 head 가 N 줄을
164
+ # 읽고 먼저 종료하면 git 이 SIGPIPE(141)로 죽고, 그 코드가 파이프라인 전체의
165
+ # 상태가 되어 errexit 가 **릴리스를 통째로 중단**시킨다. 커밋이 20개를 넘은
166
+ # 적이 없어 지금까지 잠복해 있었고, 48개 릴리스에서 실제로 발화했다.
167
+ # "1 파일 = 1 commit" 규약을 지키면 커밋 수는 앞으로도 쉽게 20을 넘는다.
168
+ # 개수 제한은 git 에게 맡긴다 — 파이프가 없어지면 발화 조건 자체가 사라진다.
169
+ COMMIT_TOTAL="$(git rev-list --count "${LAST_TAG}..HEAD")"
170
+ git log --oneline -20 "${LAST_TAG}..HEAD"
171
+ # `[ … ] && info …` 형태로 쓰지 않는다. 조건이 거짓이면 AND 리스트 전체가 1을
172
+ # 반환하고 errexit 가 그 자리에서 스크립트를 끝낸다 (CLAUDE.md 게이트 사례와 동형).
173
+ if [ "$COMMIT_TOTAL" -gt 20 ]; then
174
+ info "... 외 $((COMMIT_TOTAL - 20))개 (총 ${COMMIT_TOTAL}개)"
175
+ fi
164
176
  else
165
177
  git log --oneline -10
166
178
  fi
@@ -41,6 +41,9 @@ const STEPS = [
41
41
  "node test/install-lib.test.ts",
42
42
  "node test/skill-mcp-registry.test.ts",
43
43
  "node --test test/state-write-guard.test.ts",
44
+ "node --test test/apply-patch-paths.test.ts",
45
+ "node --test test/pipeline-shape.test.ts",
46
+ "node --test test/permission-scope-hardening.test.ts",
44
47
  "node --test test/state-access-guard.test.ts",
45
48
  "node --test test/git-exclude-registration.test.ts",
46
49
  "node --test test/issue-reporter-guard.test.ts",
@@ -82,7 +85,6 @@ const STEPS = [
82
85
  "node --test test/with-fallback-no-bun.test.ts",
83
86
  "node --test test/doctor-exit-code.test.ts",
84
87
  "node --test test/example-config-portability.test.ts",
85
- "node --test test/research-fanout.test.ts",
86
88
  "node --test test/stage-completion.test.ts",
87
89
  "node --test test/state-sh-write-atomicity.test.ts",
88
90
  "node --test test/gate-locale-and-path.test.ts",
package/scripts/state.sh CHANGED
@@ -185,8 +185,7 @@ case "$cmd" in
185
185
  "done": false,
186
186
  "substages": {
187
187
  "jira": {"done": false},
188
- "requirements": {"done": false, "approved_by_user": false},
189
- "scope": {"done": false, "approved_by_user": false}
188
+ "requirements": {"done": false, "approved_by_user": false}
190
189
  }
191
190
  },
192
191
  "2_implementation": {
@@ -333,6 +332,29 @@ JSON
333
332
  | move("3_delivery.commit"; "3_delivery"; "commit")
334
333
  | move("3_delivery.pr"; "3_delivery"; "pr")
335
334
  | move("3_delivery.review"; "3_delivery"; "review")
335
+
336
+ # scope substage 흡수 (범위 확정 → requirements).
337
+ # 진행 중이던 워크플로우가 남긴 scope 마커를 버리지 않고 requirements 로
338
+ # 접는다. done / approved_by_user 는 **AND** 로 접는다 — scope 가 아직
339
+ # 끝나지 않았는데 requirements 만 보고 통과시키면, 흡수 전이라면 막혔을
340
+ # 지점을 마이그레이션이 열어주는 셈이 된다. 접은 뒤 scope 키는 지운다.
341
+ | (if (.stages."1_planning".substages? // {} | has("scope")) then
342
+ .stages."1_planning".substages.requirements =
343
+ ((.stages."1_planning".substages.requirements // {})
344
+ + {"done": ((.stages."1_planning".substages.requirements.done // false)
345
+ and (.stages."1_planning".substages.scope.done // false)),
346
+ "approved_by_user":
347
+ ((.stages."1_planning".substages.requirements.approved_by_user // false)
348
+ and (.stages."1_planning".substages.scope.approved_by_user // false))})
349
+ | del(.stages."1_planning".substages.scope)
350
+ else . end)
351
+ # auto_approve 는 flat 표기를 유지하는 유일한 예외라 여기서 같이 옮긴다.
352
+ | (if (.policy.auto_approve? // {} | has("1_planning.scope")) then
353
+ .policy.auto_approve = (.policy.auto_approve
354
+ | ."1_planning.requirements" =
355
+ ((."1_planning.requirements" // false) and ."1_planning.scope")
356
+ | del(."1_planning.scope"))
357
+ else . end)
336
358
  '
337
359
  echo "migrated[$ISSUE] $P" ;;
338
360
  *) echo "usage: state.sh {root|issue|init|status|get|set|append|migrate} ..." >&2; exit 64 ;;
@@ -42,16 +42,26 @@ EOF
42
42
 
43
43
  # `jq -e` fails when the file cannot be parsed; capture stderr for diagnostics.
44
44
  local token jq_err
45
- jq_err=$(mktemp 2>/dev/null || echo "/tmp/load-secret.$$.err")
46
- token=$(jq -r --arg k "$var_name" '.secrets[$k] // empty' "$cfg_file" 2>"$jq_err")
45
+ # 임시 파일은 **설정 파일과 같은 디렉토리**에 만든다. 종전 폴백은 `/tmp` 을
46
+ # 명령문에 노출시켰는데, opencode 1.18 의 ShellTool 은 bash 명령이 참조하는
47
+ # 디렉토리마다 external_directory 승인을 묻고, 서브에이전트 세션에서 그 요청은
48
+ # 자동 거부 + 세션 abort 로 이어진다 (워크스페이스 밖 경로이므로).
49
+ # mktemp 실패 시에도 워크스페이스 밖으로 새지 않게 한다.
50
+ jq_err=""
51
+ jq_err=$(mktemp "$(dirname "$cfg_file")/.load-secret.XXXXXX" 2>/dev/null) || jq_err=""
52
+ # mktemp 실패 시 /dev/null 로 흘린다. 이때 `jq_err` 는 빈 문자열로 남으므로
53
+ # 아래 정리 단계가 **실제로 만든 파일만** 지운다 — `rm -f /dev/null` 은
54
+ # root 로 도는 환경(이슈 #8 로그의 /root/.nvm)에서 진짜로 장치 노드를 지운다.
55
+ local jq_err_sink="${jq_err:-/dev/null}"
56
+ token=$(jq -r --arg k "$var_name" '.secrets[$k] // empty' "$cfg_file" 2>"$jq_err_sink")
47
57
  local jq_status=$?
48
58
  if [ "$jq_status" -ne 0 ]; then
49
59
  printf '[%s] makdoong2-team.json is not valid JSON: %s\n' "$tag" "$cfg_file" >&2
50
- if [ -s "$jq_err" ]; then sed -e "s/^/[$tag] jq: /" "$jq_err" >&2; fi
51
- rm -f "$jq_err"
60
+ if [ -s "$jq_err_sink" ]; then sed -e "s/^/[$tag] jq: /" "$jq_err_sink" >&2; fi
61
+ if [ -n "$jq_err" ]; then rm -f -- "$jq_err"; fi
52
62
  return 67
53
63
  fi
54
- rm -f "$jq_err"
64
+ if [ -n "$jq_err" ]; then rm -f -- "$jq_err"; fi
55
65
 
56
66
  if [ -z "$token" ] || [ "$token" = "null" ]; then
57
67
  cat >&2 <<EOF
@@ -55,7 +55,6 @@ jq -r '
55
55
  # Planning substages
56
56
  ($p1.substages.jira // {} | " 1_planning.jira: done=" + ((.done // false) | tostring) + (if .approved_by_user then " (approved)" else "" end)),
57
57
  ($p1.substages.requirements // {} | " 1_planning.requirements: done=" + ((.done // false) | tostring) + (if .approved_by_user then " (approved)" else "" end)),
58
- ($p1.substages.scope // {} | " 1_planning.scope: done=" + ((.done // false) | tostring) + (if .approved_by_user then " (approved)" else "" end)),
59
58
 
60
59
  # Implementation substages
61
60
  ($p2.substages.analysis // {} | " 2_implementation.analysis: done=" + ((.done // false) | tostring) + (if .skipped then " (skipped)" else "" end)),