makdoong2-team 2.3.2 → 3.0.1

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 (44) 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 +22 -9
  14. package/dist/config.js +61 -11
  15. package/dist/model-fallback-policy.js +0 -4
  16. package/dist/opencode-plugin.js +270 -356
  17. package/dist/poll-sub-session.d.ts +16 -1
  18. package/dist/poll-sub-session.js +76 -11
  19. package/dist/sub-session-registry.d.ts +74 -0
  20. package/dist/sub-session-registry.js +204 -0
  21. package/gates/stage-analysis-verify.sh +63 -5
  22. package/gates/stage4-dev-verify.sh +6 -6
  23. package/gates/verify.sh +0 -2
  24. package/opencode.json.example +0 -1
  25. package/package.json +1 -1
  26. package/scripts/gate-policy-test.sh +15 -14
  27. package/scripts/install-lib.mjs +1 -1
  28. package/scripts/install-lib.mts +1 -1
  29. package/scripts/model-policy.mjs +0 -4
  30. package/scripts/model-policy.mts +0 -4
  31. package/scripts/release.sh +13 -1
  32. package/scripts/run-tests.mts +4 -1
  33. package/scripts/state.sh +24 -2
  34. package/skills/_lib/load-secret.sh +15 -5
  35. package/src/hooks/session-start.sh +0 -1
  36. package/stages/01-planning.md +35 -54
  37. package/stages/02-requirements.md +53 -35
  38. package/stages/04-analysis.md +2 -2
  39. package/stages/05-worktree-dev.md +15 -0
  40. package/agents/makdoong2-researcher.md +0 -68
  41. package/dist/research-fanout.d.ts +0 -165
  42. package/dist/research-fanout.js +0 -341
  43. package/gates/stage3-scope-verify.sh +0 -69
  44. package/stages/03-scope.md +0 -81
@@ -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",
@@ -73,6 +76,7 @@ const STEPS = [
73
76
  "node --test test/verdict-reason-injection.test.ts",
74
77
  "node --test test/verdict-hash-normalize.test.ts",
75
78
  "node --test test/session-index-fallback.test.ts",
79
+ "node --test test/sub-session-registry.test.ts",
76
80
  "node --test test/plugin-exports-shape.test.ts",
77
81
  "node --test test/stale-worktree-recovery.test.ts",
78
82
  "node --test test/rollback-commits.test.ts",
@@ -82,7 +86,6 @@ const STEPS = [
82
86
  "node --test test/with-fallback-no-bun.test.ts",
83
87
  "node --test test/doctor-exit-code.test.ts",
84
88
  "node --test test/example-config-portability.test.ts",
85
- "node --test test/research-fanout.test.ts",
86
89
  "node --test test/stage-completion.test.ts",
87
90
  "node --test test/state-sh-write-atomicity.test.ts",
88
91
  "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)),
@@ -1,4 +1,4 @@
1
- # 1–3단계 통합 Planning — `1_planning.{jira,requirements,scope}` 연속 처리
1
+ # 통합 Planning — `1_planning.{jira,requirements}` 연속 처리
2
2
 
3
3
  **목적**: Jira 조회 → 요구사항 확정 → 개발 범위 파악을 **단일 세션**에서 완료한다.
4
4
  **속도**: 3개 substage dispatch(모델 호출 3회)를 1회로 단축.
@@ -15,12 +15,10 @@
15
15
  ```bash
16
16
  bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."jira".done'
17
17
  bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".done'
18
- bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."scope".done'
19
18
  ```
20
19
 
21
20
  - `jira.done=true` → Phase 1 건너뛰고 Phase 2로 직행
22
- - `jira.done=true` + `requirements.done=true` → Phase 3으로 직행
23
- - 셋 다 `true` → 모두 완료. 완료 메시지 출력 후 종료
21
+ - 둘 다 `true` → 모두 완료. 완료 메시지 출력 후 종료
24
22
 
25
23
  ---
26
24
 
@@ -89,7 +87,11 @@ mkdir -p .makdoong2-team/<이슈키>
89
87
 
90
88
  파일 (repo/worktree root 기준 상대경로): `.makdoong2-team/<이슈키>/requirements-draft.md`
91
89
 
92
- **초안 파일은 반드시 `write` 툴(filePath 인자)로 생성·갱신한다.** bash 리디렉션(`cat > …`, `printf > …` 등)과 `apply_patch` 는 같은 경로라도 훅이 차단한다 — planner 의 유일한 파일 쓰기 수단은 `write` 다. 이 경로의 `write` 는 planner READ-ONLY 원칙의 명시적 예외이며 훅이 허용한다 (issue #8).
90
+ **초안 파일은 반드시 쓰기 *툴* 로 생성·갱신한다 — bash 리디렉션(`cat > …`, `printf > …`, `tee`, `sed -i` 등)은 같은 경로라도 훅이 차단한다.**
91
+ - 세션에 `write` 툴이 있으면 `write(filePath=…)` 를 쓴다.
92
+ - **`write` 가 툴 목록에 없으면 `apply_patch` 를 쓴다.** opencode 는 모델에 따라 쓰기 툴을 바꿔 끼운다 — `gpt-5` 계열 세션에는 `write`·`edit` 가 아예 없고 `apply_patch` 만 있다. 이때 패치 본문은 `*** Begin Patch` / `*** Add File: .makdoong2-team/<이슈키>/requirements-draft.md` / `*** End Patch` 형식으로 정확히 작성한다 — 훅이 본문에서 대상 경로를 파싱해 허용 경로면 통과시킨다.
93
+ - **툴이 없다는 이유로 초안 생성을 포기하지 않는다.** 둘 중 실제로 존재하는 툴을 골라 즉시 실행하고, 차단 메시지를 받으면 그 메시지가 지시하는 형식으로 재시도한다.
94
+ - 이 경로의 파일 쓰기는 planner READ-ONLY 원칙의 명시적 예외이며 훅이 허용한다 (issue #8).
93
95
 
94
96
  초안 구조:
95
97
  ```markdown
@@ -104,36 +106,29 @@ mkdir -p .makdoong2-team/<이슈키>
104
106
  ```bash
105
107
  # state.json 에는 반드시 상대경로만 저장한다 (절대경로 저장 시 다른 cwd 에서 접근 불가 → Read hang 유발).
106
108
  # 이 마커는 §2-5b 의 spec_hash 와 **한 쌍**이다 — spec_hash 만 기록되고 이것이 빠지면
107
- # stage3-scope-verify.sh 가 1_planning.scope 진입을 하드 차단한다 (issue #6-①).
109
+ # stage-analysis-verify.sh 가 2_implementation.analysis 진입을 하드 차단한다 (issue #6-①).
108
110
  bash <SCRIPTS_DIR>/state.sh set <이슈키> \
109
111
  '.stages."1_planning".substages."requirements".draft_path' '".makdoong2-team/<이슈키>/requirements-draft.md"'
110
112
  ```
111
113
 
112
- ### 2-3. 다출처 교차 조사 (병렬)
114
+ ### 2-3. 다출처 교차 조사
113
115
 
114
- **`dispatch_research` 툴 1회 호출로 소스별 조사를 병렬 실행한다.** `skill_mcp` 를 직접 순차 호출하지 않는다 — 플러그인이 소스마다 별도 세션을 동시에 띄우므로 대기 시간이 가장 느린 소스 하나로 수렴하고, 각 소스의 원자료가 이 세션의 컨텍스트를 잠식하지 않는다. **outer-world 에이전트 위임 금지.**
116
+ **당신의 세션에서 직접 조사한다.** 소스마다 `skill(name=...)` 로 스킬을 먼저 로드한 뒤 그 스킬의 MCP 를 호출한다. 로드 전에 `skill_mcp` 를 부르면 `MCP server "<name>" not found` 로 실패한다. **outer-world 에이전트 위임 금지.**
115
117
 
116
- ```
117
- dispatch_research(
118
- issue = "<이슈키>",
119
- worktree = "<Working directory 절대경로>",
120
- context = "<Phase 1 에서 요약한 Jira 핵심 3~5줄>",
121
- queries = [
122
- {source: "jira", focus: "에픽/상위 이슈, 링크 이슈, 관련 코멘트에서 구체화된 요구"},
123
- {source: "confluence", focus: "관련 설계 문서, ADR, API 스펙, 운영 가이드"},
124
- {source: "bitbucket", focus: "수정 대상 파일/클래스 현재 구현, 관련 PR 이력, 테스트 패턴"}
125
- ]
126
- )
127
- ```
128
-
129
- 조사 세션은 서로를 보지 못한다. **한 focus 가 다른 조사 결과에 의존하면 안 된다** — 의존이 필요하면 라운드를 나눠 두 번 호출한다.
118
+ | 소스 | skill | mcp_name | 조사 범위 |
119
+ |---|---|---|---|
120
+ | Jira | `jira-research` | `works` | 에픽·상위 이슈·링크 이슈·서브태스크·코멘트에서 구체화된 요구와 결정 사항 |
121
+ | Confluence | `confluence-research` | `docs` | 설계 문서·ADR·API 스펙·운영 가이드의 제약과 합의된 규약 |
122
+ | Bitbucket | `bitbucket-research` | `repos` | 수정 대상 파일·클래스의 현재 구현, 관련 PR 이력, 기존 테스트 패턴 |
123
+ | GitHub OSS | `github-oss-research` | (없음 — WebFetch) | 외부 오픈소스의 사용 예시·알려진 이슈·업스트림 변경 |
130
124
 
131
- Simple 이슈는 조사 A + C만으로 축소 가능. 외부 라이브러리가 쟁점이면 `{source: "github-oss", ...}` 를 추가한다.
125
+ 기본 조사는 **Jira → Bitbucket** 두 소스다. 설계·규약이 쟁점이면 Confluence 를, 외부 라이브러리가 쟁점이면 GitHub OSS 를 더한다. Simple 이슈는 Jira 하나로 끝낼 수 있다.
132
126
 
133
- **결과 읽기**: 반환 JSON 의 `artifact_path` (`.makdoong2-team/<이슈키>/research-findings.json`) 를 Read 로 읽는다. `failed` 가 있어도 **부분 성공이 정상**이므로 나머지 결과로 진행하고, 실패 소스가 요구사항 확정에 필수인 경우에만 사유를 사용자에게 보고한다. 전 소스 실패(`ok: false`)면 추측으로 채우지 말고 보고한다.
127
+ **조사 예산은 소스당 최대 5회 호출이다.** 초과하면 그 소스는 거기서 멈추고 미확인 항목을 `미결 사항` 에 적는다.
134
128
 
135
- - **`status: "partial"` 은 그 자체로 정상 종료다.** 실패한 소스를 당신이 직접 조사해 메우려 하지 말 것 — `skill_mcp` 순차 호출로 결손을 메우려다 세션 예산을 전부 소진하고 **마커를 하나도 남기지 못한 채** 종료한 사고가 이틀 연속 재현됐다 (GitHub issue #9, 각 27분·17분 소모). 결손을 더 좁히고 싶으면 focus 를 좁혀 `dispatch_research` 를 **1회만** 다시 호출한다.
136
- - **조사 완결성을 이유로 마커 기록을 미루지 않는다 (hardrule).** 조사가 부분적이면 `gaps` 에 미확인 항목을 남기고 그 상태 그대로 산출물과 substage 마커를 기록한 뒤 종료한다. 마커가 없는 종료는 상위에서 `completion: "incomplete"` 로 분류되어 substage 전체가 재실행된다 — 부분 결과까지 함께 버려진다.
129
+ - **부분 조사는 그 자체로 정상 종료다.** 한 소스가 응답하지 않으면 그 소스를 포기하고 나머지로 진행한다. 결손을 메우려 같은 소스를 계속 두드리다 세션 예산을 전부 소진하고 **마커를 하나도 남기지 못한 채** 종료한 사고가 이틀 연속 재현됐다 (GitHub issue #9, 각 27분·17분 소모).
130
+ - **조사 완결성을 이유로 마커 기록을 미루지 않는다 (hardrule).** 조사가 부분적이면 미확인 항목을 초안의 `미결 사항` 에 남기고 **그 상태 그대로** 산출물과 substage 마커를 기록한 뒤 종료한다. 마커가 없는 종료는 상위에서 `completion: "incomplete"` 로 분류되어 substage 전체가 재실행된다 — 부분 결과까지 함께 버려진다.
131
+ - 조사 결과는 별도 파일로 만들지 않는다. `requirements-draft.md` 의 `## 수집된 정보` 에 소스별로 정리한다 (출처 URL 포함).
137
132
 
138
133
  ### 2-4. 요구사항 체크리스트 확인
139
134
 
@@ -176,7 +171,7 @@ category = (criticality == "critical" OR scope_size == "large") ? "major" : base
176
171
 
177
172
  ```bash
178
173
  bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy' \
179
- '{"intent_type":"Standard","change_type":"bugfix","scope_size":"small","criticality":"normal","category":"minor","auto_approve":{"1_planning.requirements":true,"1_planning.scope":true,"3_delivery.commit":true,"3_delivery.pr":true},"rationale":"<한 줄 근거>","categorized_by":"1_planning.requirements"}'
174
+ '{"intent_type":"Standard","change_type":"bugfix","scope_size":"small","criticality":"normal","category":"minor","auto_approve":{"1_planning.requirements":true,"3_delivery.commit":true,"3_delivery.pr":true},"rationale":"<한 줄 근거>","categorized_by":"1_planning.requirements"}'
180
175
  bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_at' "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
181
176
  ```
182
177
 
@@ -184,7 +179,7 @@ major 로 판정된 경우에도 `auto_approve` 맵은 **모두 true** 로 두
184
179
 
185
180
  ### 2-5b. 요구사항 품질 마커 (ambiguity_score · spec_hash)
186
181
 
187
- `stage3-scope-verify.sh` 의 요구사항 품질 게이트는 **이 두 마커가 있을 때만** 검사한다
182
+ `stage-analysis-verify.sh` 의 요구사항 품질 게이트는 **이 두 마커가 있을 때만** 검사한다
188
183
  (구형 state 호환을 위한 조건부 검사). 즉 여기서 기록하지 않으면 통합 경로에서는
189
184
  품질 게이트가 통째로 사문화된다 — `02-requirements.md` 를 거치는 분리 경로에서는
190
185
  검사되는데 이 경로에서만 안 되는 비대칭이 생긴다. 반드시 기록한다.
@@ -206,23 +201,9 @@ bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."req
206
201
  bash <SCRIPTS_DIR>/state.sh get <이슈키> '.stages."1_planning".substages."requirements".spec_hash'
207
202
  ```
208
203
 
209
- ### 2-6. Requirements 완료 기록
204
+ ### 2-5c. 개발 범위 확정 (구 `1_planning.scope` — 흡수됨)
210
205
 
211
- ```bash
212
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".self_check' \
213
- '{"checklist_complete":true,"conflicts_resolved":true,"user_confirmed":true,"scope_clean":true,"draft_synced":true,"categorized":true,"ambiguity_scored":true,"spec_frozen":true,"draft_recorded":true}'
214
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".verification_pending' 'false'
215
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".done' 'true'
216
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".done_at' "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
217
- ```
218
-
219
- ---
220
-
221
- ## Phase 3: 개발 범위 파악 (`1_planning.scope`)
222
-
223
- 2단계 조사 결과로 코드 수정 계획을 수립한다. 2단계 `bitbucket-research` 탐색을 이어서 사용한다.
224
-
225
- ### 3-1. 범위 출력
206
+ 범위 확정은 요구사항 확정과 **같은 판단의 연속**이라 별도 substage 를 두지 않는다. 조사 결과로 코드 수정 계획을 여기서 함께 확정한다. §2-3 의 `bitbucket-research` 탐색을 이어서 사용한다.
226
207
 
227
208
  ```
228
209
  ### 개발 범위
@@ -234,27 +215,27 @@ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."req
234
215
  **스코프 아웃**: <이번 이슈에서 다루지 않는 것>
235
216
  ```
236
217
 
237
- ### 3-2. 범주 재평가 (escalation — 하향 금지)
218
+ 이 블록은 `requirements-draft.md` 에도 그대로 남긴다 — dev 단계가 초안을 읽고 작업 단위를 잡는다.
238
219
 
239
- 실제 수정/추가 파일과 작업 단위가 확정된 뒤, `scope_size`·`criticality`를 재평가한다.
220
+ **범주 재평가 (escalation — 하향 금지)**: 실제 수정/추가 파일과 작업 단위가 확정된 뒤 `scope_size`·`criticality` 를 재평가한다. minor → major 상향인 경우에만 갱신하며, `auto_approve` 맵은 건드리지 않고 **모두 true 로 유지**한다 — 상향은 위험도 라벨 정정에 그치고 흐름은 무인 진행을 유지한다:
240
221
 
241
- minor → major로 상향될 경우에만 아래 항목들을 갱신한다. `auto_approve` 맵은 건드리지 않고 **모두 true 로 유지**한다 — 상향은 위험도 라벨 정정에 그치며 흐름 자체는 무인 진행으로 유지된다:
242
222
  ```bash
243
223
  bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.category' '"major"'
244
224
  bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.scope_size' '"large"'
245
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_by' '"1_planning.scope"'
225
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.policy.categorized_by' '"1_planning.requirements"'
246
226
  ```
247
227
 
248
- ### 3-3. Scope 완료 기록
228
+ ### 2-6. Requirements 완료 기록
249
229
 
250
230
  ```bash
251
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."scope".self_check' \
252
- '{"paths_explicit":true,"test_scope_defined":true,"atomic_units":true,"scope_out_listed":true,"user_approved":true}'
253
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."scope".verification_pending' 'false'
254
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."scope".done' 'true'
255
- bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."scope".done_at' "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
231
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".self_check' \
232
+ '{"checklist_complete":true,"conflicts_resolved":true,"user_confirmed":true,"scope_clean":true,"draft_synced":true,"categorized":true,"ambiguity_scored":true,"spec_frozen":true,"draft_recorded":true,"paths_explicit":true,"test_scope_defined":true,"atomic_units":true,"scope_out_listed":true}'
233
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".verification_pending' 'false'
234
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".done' 'true'
235
+ bash <SCRIPTS_DIR>/state.sh set <이슈키> '.stages."1_planning".substages."requirements".done_at' "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
256
236
  ```
257
237
 
238
+
258
239
  ---
259
240
 
260
241
  ## 최종 출력