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.
- package/README.md +4 -12
- package/agents/makdoong2-engineer.md +1 -0
- package/agents/makdoong2-planner.md +17 -61
- package/agents/makdoong2-team-leader.md +14 -4
- package/agents/makdoong2-verifier.md +6 -6
- package/assets/makdoong2-team.schema.json +0 -18
- package/bin/cli.js +27 -1
- package/bin/cli.ts +27 -1
- package/dist/agent-stage-config.d.ts +1 -1
- package/dist/agent-stage-config.js +0 -11
- package/dist/apply-patch-paths.d.ts +46 -0
- package/dist/apply-patch-paths.js +114 -0
- package/dist/config.d.ts +21 -8
- package/dist/config.js +34 -0
- package/dist/model-fallback-policy.js +0 -4
- package/dist/opencode-plugin.js +121 -307
- package/dist/poll-sub-session.d.ts +2 -0
- package/dist/poll-sub-session.js +37 -9
- package/gates/stage-analysis-verify.sh +63 -5
- package/gates/stage4-dev-verify.sh +6 -6
- package/gates/verify.sh +0 -2
- package/opencode.json.example +0 -1
- package/package.json +1 -1
- package/scripts/gate-policy-test.sh +15 -14
- package/scripts/install-lib.mjs +1 -1
- package/scripts/install-lib.mts +1 -1
- package/scripts/model-policy.mjs +0 -4
- package/scripts/model-policy.mts +0 -4
- package/scripts/release.sh +13 -1
- package/scripts/run-tests.mts +3 -1
- package/scripts/state.sh +24 -2
- package/skills/_lib/load-secret.sh +15 -5
- package/src/hooks/session-start.sh +0 -1
- package/stages/01-planning.md +35 -54
- package/stages/02-requirements.md +53 -35
- package/stages/04-analysis.md +2 -2
- package/stages/05-worktree-dev.md +15 -0
- package/agents/makdoong2-researcher.md +0 -68
- package/dist/research-fanout.d.ts +0 -165
- package/dist/research-fanout.js +0 -341
- package/gates/stage3-scope-verify.sh +0 -69
- package/stages/03-scope.md +0 -81
package/dist/poll-sub-session.js
CHANGED
|
@@ -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
|
|
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:
|
|
607
|
-
|
|
608
|
-
|
|
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.
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
#
|
|
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."
|
|
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.
|
|
35
|
-
[ "$(q '.stages."1_planning".substages."
|
|
36
|
-
|| fail "
|
|
37
|
-
if [ "$(q '.stages."1_planning".substages."
|
|
38
|
-
fail "
|
|
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)
|
package/opencode.json.example
CHANGED
package/package.json
CHANGED
|
@@ -51,44 +51,45 @@ expect(){
|
|
|
51
51
|
echo ""
|
|
52
52
|
echo "[gate-policy-test] 조건부 auto-approve 게이트 분기"
|
|
53
53
|
echo ""
|
|
54
|
-
echo "──
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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."
|
|
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."
|
|
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."
|
|
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."
|
|
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."
|
|
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."
|
|
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."
|
|
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."
|
|
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 ""
|
package/scripts/install-lib.mjs
CHANGED
|
@@ -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", "
|
|
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/
|
package/scripts/install-lib.mts
CHANGED
|
@@ -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", "
|
|
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/
|
package/scripts/model-policy.mjs
CHANGED
|
@@ -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" }],
|
package/scripts/model-policy.mts
CHANGED
|
@@ -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" }],
|
package/scripts/release.sh
CHANGED
|
@@ -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
|
|
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
|
package/scripts/run-tests.mts
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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 "$
|
|
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)),
|