makdoong2-team 1.3.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/agents/makdoong2-analyzer.md +135 -0
  4. package/agents/makdoong2-engineer.md +165 -0
  5. package/agents/makdoong2-planner.md +267 -0
  6. package/agents/makdoong2-publisher.md +481 -0
  7. package/agents/makdoong2-team-leader.md +249 -0
  8. package/agents/makdoong2-verifier.md +353 -0
  9. package/assets/makdoong2-team.default.json +46 -0
  10. package/assets/makdoong2-team.schema.json +357 -0
  11. package/bin/cli.js +404 -0
  12. package/dist/agent-stage-config.d.ts +15 -0
  13. package/dist/agent-stage-config.js +119 -0
  14. package/dist/config.d.ts +79 -0
  15. package/dist/config.js +96 -0
  16. package/dist/logger.d.ts +14 -0
  17. package/dist/logger.js +131 -0
  18. package/dist/mcp-secret-injector.d.ts +56 -0
  19. package/dist/mcp-secret-injector.js +89 -0
  20. package/dist/model-chain-cli.d.ts +1 -0
  21. package/dist/model-chain-cli.js +21 -0
  22. package/dist/model-fallback-policy.d.ts +69 -0
  23. package/dist/model-fallback-policy.js +211 -0
  24. package/dist/opencode-plugin.d.ts +8 -0
  25. package/dist/opencode-plugin.js +2457 -0
  26. package/dist/poll-sub-session.d.ts +139 -0
  27. package/dist/poll-sub-session.js +494 -0
  28. package/dist/redact-secrets.d.ts +3 -0
  29. package/dist/redact-secrets.js +68 -0
  30. package/dist/session-index.d.ts +11 -0
  31. package/dist/session-index.js +71 -0
  32. package/dist/skill-mcp-registry.d.ts +59 -0
  33. package/dist/skill-mcp-registry.js +178 -0
  34. package/dist/stall-escalation.d.ts +1 -0
  35. package/dist/stall-escalation.js +22 -0
  36. package/dist/tmux-monitor.d.ts +193 -0
  37. package/dist/tmux-monitor.js +694 -0
  38. package/dist/verdict-hash.d.ts +1 -0
  39. package/dist/verdict-hash.js +62 -0
  40. package/gates/stage-analysis-verify.sh +84 -0
  41. package/gates/stage2-requirements-verify.sh +13 -0
  42. package/gates/stage3-scope-verify.sh +45 -0
  43. package/gates/stage4-dev-post-verify.sh +64 -0
  44. package/gates/stage4-dev-verify.sh +36 -0
  45. package/gates/stage5-coverage-verify.sh +36 -0
  46. package/gates/stage5-test-verify.sh +24 -0
  47. package/gates/stage6-commit-verify.sh +41 -0
  48. package/gates/stage6-post-commit-verify.sh +131 -0
  49. package/gates/stage7-post-pr-verify.sh +53 -0
  50. package/gates/stage7-pr-verify.sh +48 -0
  51. package/gates/stage8-post-review-verify.sh +84 -0
  52. package/gates/stage8-review-verify.sh +45 -0
  53. package/gates/verify.sh +44 -0
  54. package/opencode.json.example +40 -0
  55. package/package.json +84 -0
  56. package/postinstall.mjs +56 -0
  57. package/references/commit-convention.md +130 -0
  58. package/references/jira-issue-templates.md +203 -0
  59. package/references/pr-template.md +381 -0
  60. package/scripts/config.sh +46 -0
  61. package/scripts/coverage-record.sh +67 -0
  62. package/scripts/gate-policy-test.sh +152 -0
  63. package/scripts/install-lib.mjs +1029 -0
  64. package/scripts/lint-agent-prompts.sh +74 -0
  65. package/scripts/log-event.sh +44 -0
  66. package/scripts/model-policy.mjs +183 -0
  67. package/scripts/publish-if-changed.sh +207 -0
  68. package/scripts/release.sh +276 -0
  69. package/scripts/rollback-commits.sh +35 -0
  70. package/scripts/smoke-test.mjs +194 -0
  71. package/scripts/state.sh +192 -0
  72. package/scripts/test-postinstall.mjs +141 -0
  73. package/scripts/with-fallback.sh +56 -0
  74. package/scripts/wt-sync-ignored.sh +193 -0
  75. package/skills/_lib/load-secret.sh +149 -0
  76. package/skills/bamboo-ci/SKILL.md +81 -0
  77. package/skills/bamboo-ci/run-bamboo.sh +23 -0
  78. package/skills/bitbucket-research/SKILL.md +87 -0
  79. package/skills/bitbucket-research/run-repos.sh +23 -0
  80. package/skills/confluence-research/SKILL.md +75 -0
  81. package/skills/confluence-research/run-docs.sh +23 -0
  82. package/skills/github-oss-research/SKILL.md +59 -0
  83. package/skills/jira-research/SKILL.md +75 -0
  84. package/skills/jira-research/run-works.sh +23 -0
  85. package/src/hooks/guard-bash.sh +67 -0
  86. package/src/hooks/session-start.sh +96 -0
  87. package/src/hooks/sync-state.sh +47 -0
  88. package/stages/01-jira.md +43 -0
  89. package/stages/01-planning.md +229 -0
  90. package/stages/02-requirements.md +298 -0
  91. package/stages/03-scope.md +81 -0
  92. package/stages/04-analysis.md +281 -0
  93. package/stages/05-worktree-dev.md +124 -0
  94. package/stages/06-test.md +161 -0
  95. package/stages/07-commit.md +229 -0
  96. package/stages/08-pr.md +177 -0
  97. package/stages/09-review-comments.md +277 -0
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env bash
2
+ # release.sh - makdoong2-team npm 패키지 릴리스 자동화
3
+ #
4
+ # 사용법:
5
+ # scripts/release.sh <patch|minor|major> [--yes]
6
+ #
7
+ # 워크플로우:
8
+ # 1. Pre-flight 체크 (git 상태, 브랜치, 원격 동기화)
9
+ # 2. 테스트 실행 (build + smoke + gate policy + install-lib)
10
+ # 3. 버전 bump 미리보기
11
+ # 4. **사용자 승인 게이트 #1** (버전 bump 확인)
12
+ # 5. npm version <bump> 실행 (커밋 + 태그 생성)
13
+ # 6. Publish 미리보기 (npm publish --dry-run)
14
+ # 7. **사용자 승인 게이트 #2** (npm 공개 배포 확인)
15
+ # 8. npm publish → public npm registry (registry.npmjs.org)
16
+ # 9. git push --follow-tags
17
+ #
18
+ # 옵션:
19
+ # --yes: 모든 확인 프롬프트 자동 승인 (CI 전용, 대화형에서는 사용 금지)
20
+
21
+ set -euo pipefail
22
+
23
+ RED=$'\033[0;31m'
24
+ GREEN=$'\033[0;32m'
25
+ YELLOW=$'\033[0;33m'
26
+ BLUE=$'\033[0;34m'
27
+ CYAN=$'\033[0;36m'
28
+ BOLD=$'\033[1m'
29
+ RESET=$'\033[0m'
30
+
31
+ log() { printf "%s[release]%s %s\n" "$CYAN" "$RESET" "$*"; }
32
+ info() { printf "%s[info]%s %s\n" "$BLUE" "$RESET" "$*"; }
33
+ ok() { printf "%s[ok]%s %s\n" "$GREEN" "$RESET" "$*"; }
34
+ warn() { printf "%s[warn]%s %s\n" "$YELLOW" "$RESET" "$*"; }
35
+ err() { printf "%s[error]%s %s\n" "$RED" "$RESET" "$*" >&2; }
36
+ prompt() { printf "%s[confirm]%s %s" "$BOLD" "$RESET" "$*"; }
37
+
38
+ # ---------- 인자 파싱 ----------
39
+ BUMP="${1:-}"
40
+ AUTO_YES=false
41
+
42
+ for arg in "$@"; do
43
+ case "$arg" in
44
+ --yes|-y) AUTO_YES=true ;;
45
+ esac
46
+ done
47
+
48
+ if [[ ! "$BUMP" =~ ^(patch|minor|major)$ ]]; then
49
+ err "usage: $0 <patch|minor|major> [--yes]"
50
+ err " patch: 0.2.3 → 0.2.4 (버그 수정)"
51
+ err " minor: 0.2.3 → 0.3.0 (기능 추가, backward-compatible)"
52
+ err " major: 0.2.3 → 1.0.0 (breaking change)"
53
+ exit 1
54
+ fi
55
+
56
+ # ---------- 프로젝트 루트 이동 ----------
57
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
58
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
59
+ cd "$PROJECT_ROOT"
60
+
61
+ log "프로젝트 루트: $PROJECT_ROOT"
62
+ log "Bump 유형: $BUMP"
63
+
64
+ # ---------- 사용자 확인 헬퍼 ----------
65
+ confirm() {
66
+ local message="$1"
67
+ if [ "$AUTO_YES" = true ]; then
68
+ info "[--yes] 자동 승인: $message"
69
+ return 0
70
+ fi
71
+
72
+ local reply
73
+ prompt "$message [y/N]: "
74
+ read -r reply </dev/tty
75
+ case "$reply" in
76
+ [yY]|[yY][eE][sS]) return 0 ;;
77
+ *) return 1 ;;
78
+ esac
79
+ }
80
+
81
+ # ---------- STEP 1: Pre-flight 체크 ----------
82
+ log "=========================================="
83
+ log "STEP 1: Pre-flight 체크"
84
+ log "=========================================="
85
+
86
+ # working tree clean 확인
87
+ if [ -n "$(git status --porcelain)" ]; then
88
+ err "작업 디렉토리에 커밋되지 않은 변경사항이 있음:"
89
+ git status --short
90
+ err "먼저 변경사항을 커밋하거나 stash 하세요."
91
+ exit 1
92
+ fi
93
+ ok "Working tree clean"
94
+
95
+ # 현재 브랜치 확인
96
+ CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
97
+ if [ "$CURRENT_BRANCH" != "master" ] && [ "$CURRENT_BRANCH" != "main" ]; then
98
+ warn "현재 브랜치가 master/main이 아님: $CURRENT_BRANCH"
99
+ confirm "이 브랜치에서 릴리스를 진행하시겠습니까?" || {
100
+ err "사용자가 취소함"
101
+ exit 1
102
+ }
103
+ fi
104
+ ok "브랜치: $CURRENT_BRANCH"
105
+
106
+ # 원격과 동기화 확인
107
+ log "원격 저장소 fetch..."
108
+ git fetch origin --quiet
109
+ UPSTREAM="origin/$CURRENT_BRANCH"
110
+ if git rev-parse --verify "$UPSTREAM" >/dev/null 2>&1; then
111
+ LOCAL="$(git rev-parse HEAD)"
112
+ REMOTE="$(git rev-parse "$UPSTREAM")"
113
+ BASE="$(git merge-base HEAD "$UPSTREAM")"
114
+
115
+ if [ "$LOCAL" = "$REMOTE" ]; then
116
+ ok "원격과 동기화됨"
117
+ elif [ "$LOCAL" = "$BASE" ]; then
118
+ err "로컬이 원격보다 뒤처져 있음. 먼저 pull 하세요."
119
+ exit 1
120
+ elif [ "$REMOTE" = "$BASE" ]; then
121
+ info "로컬에 push되지 않은 커밋 있음 (release 후 함께 push됨)"
122
+ git log --oneline "$UPSTREAM..HEAD"
123
+ else
124
+ err "로컬과 원격이 diverge됨. 먼저 정리하세요."
125
+ exit 1
126
+ fi
127
+ else
128
+ warn "원격 브랜치 $UPSTREAM 없음 (새 브랜치)"
129
+ fi
130
+
131
+ # ---------- STEP 2: 테스트 실행 ----------
132
+ log "=========================================="
133
+ log "STEP 2: 전체 테스트 실행"
134
+ log "=========================================="
135
+ log "npm test 실행 중... (build + smoke + gate policy + install-lib)"
136
+
137
+ if ! npm test; then
138
+ err "테스트 실패. 릴리스 중단."
139
+ exit 1
140
+ fi
141
+ ok "모든 테스트 통과"
142
+
143
+ # ---------- STEP 3: 버전 bump 미리보기 ----------
144
+ log "=========================================="
145
+ log "STEP 3: 버전 bump 미리보기"
146
+ log "=========================================="
147
+
148
+ CURRENT_VERSION="$(node -p "require('./package.json').version")"
149
+ # 다음 버전 계산 (npm version --dry-run 대체)
150
+ NEXT_VERSION="$(node -e "
151
+ const semver = '$CURRENT_VERSION'.split('.').map(Number);
152
+ const bump = '$BUMP';
153
+ if (bump === 'patch') semver[2]++;
154
+ else if (bump === 'minor') { semver[1]++; semver[2] = 0; }
155
+ else if (bump === 'major') { semver[0]++; semver[1] = 0; semver[2] = 0; }
156
+ console.log(semver.join('.'));
157
+ ")"
158
+
159
+ info "현재 버전: $CURRENT_VERSION"
160
+ info "다음 버전: $NEXT_VERSION"
161
+
162
+ # 변경 요약 출력
163
+ log "이번 릴리스에 포함될 커밋:"
164
+ LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || echo '')"
165
+ if [ -n "$LAST_TAG" ]; then
166
+ git log --oneline "${LAST_TAG}..HEAD" | head -20
167
+ else
168
+ git log --oneline -10
169
+ fi
170
+
171
+ # ---------- STEP 4: 승인 게이트 #1 - 버전 bump ----------
172
+ log "=========================================="
173
+ log "STEP 4: 버전 bump 승인 (게이트 #1)"
174
+ log "=========================================="
175
+
176
+ confirm "${BOLD}${YELLOW}버전을 $CURRENT_VERSION → $NEXT_VERSION 로 올리시겠습니까?${RESET}" || {
177
+ err "사용자가 버전 bump를 거부함. 릴리스 중단."
178
+ exit 1
179
+ }
180
+
181
+ # ---------- STEP 5: 버전 bump 실행 ----------
182
+ log "=========================================="
183
+ log "STEP 5: npm version $BUMP 실행"
184
+ log "=========================================="
185
+
186
+ VERSION_OUTPUT="$(npm version "$BUMP")"
187
+ NEW_TAG="$VERSION_OUTPUT"
188
+ ok "버전 bump 완료: $NEW_TAG"
189
+ ok "커밋 및 태그 생성됨"
190
+
191
+ # rollback 함수 (publish 실패 시)
192
+ rollback_version() {
193
+ err "롤백 중..."
194
+ git tag -d "$NEW_TAG" 2>/dev/null || true
195
+ git reset --hard HEAD~1
196
+ warn "버전 bump 커밋 및 태그 롤백 완료. 현재 버전: $CURRENT_VERSION"
197
+ }
198
+
199
+ # ---------- STEP 6: Publish 미리보기 (dry-run) ----------
200
+ log "=========================================="
201
+ log "STEP 6: Publish 미리보기 (dry-run)"
202
+ log "=========================================="
203
+
204
+ info "npm publish --dry-run 실행..."
205
+ if ! npm publish --dry-run 2>&1 | tail -40; then
206
+ err "Dry-run 실패. 롤백 필요."
207
+ rollback_version
208
+ exit 1
209
+ fi
210
+ ok "Dry-run 통과 - 패키지 구조 이상 없음"
211
+
212
+ # 패키지 크기 및 파일 수 표시
213
+ info "패키지 정보:"
214
+ node -e "
215
+ const pkg = require('./package.json');
216
+ console.log(' name: ' + pkg.name);
217
+ console.log(' version: ' + pkg.version);
218
+ console.log(' target: ' + (pkg.publishConfig?.registry || 'default'));
219
+ "
220
+
221
+ # ---------- STEP 7: 승인 게이트 #2 - npm 공개 배포 ----------
222
+ log "=========================================="
223
+ log "STEP 7: npm 공개 배포 승인 (게이트 #2)"
224
+ log "=========================================="
225
+
226
+ warn "이제 공개 npm registry (registry.npmjs.org) 에 $NEW_TAG 를 배포합니다."
227
+ warn "공개 배포이므로 누구나 설치할 수 있게 됩니다."
228
+ warn "배포 후에는 동일 버전 재-publish 가 registry에 의해 거부됩니다."
229
+ warn "돌이킬 수 없는 작업이므로 신중히 확인하세요."
230
+
231
+ confirm "${BOLD}${RED}공개 npm registry에 $NEW_TAG 를 배포하시겠습니까?${RESET}" || {
232
+ warn "사용자가 배포를 거부함. 버전 bump는 유지됩니다."
233
+ info "나중에 배포하려면: npm publish && git push origin HEAD --follow-tags"
234
+ info "취소하려면: git tag -d $NEW_TAG && git reset --hard HEAD~1"
235
+ exit 0
236
+ }
237
+
238
+ # ---------- STEP 8: npm publish ----------
239
+ log "=========================================="
240
+ log "STEP 8: npm publish (공개 npm registry 배포)"
241
+ log "=========================================="
242
+
243
+ if ! npm publish; then
244
+ err "npm publish 실패. 롤백 필요."
245
+ err "수동 확인 필요:"
246
+ err " - npm 로그인 상태 (npm whoami)"
247
+ err " - 동일 버전 중복 배포 여부"
248
+ err ""
249
+ err "롤백 여부는 상황에 따라 결정하세요."
250
+ err " 롤백: git tag -d $NEW_TAG && git reset --hard HEAD~1"
251
+ err " 재시도: npm publish"
252
+ exit 1
253
+ fi
254
+ ok "npm 배포 완료: $NEW_TAG"
255
+
256
+ # ---------- STEP 9: git push ----------
257
+ log "=========================================="
258
+ log "STEP 9: git push --follow-tags"
259
+ log "=========================================="
260
+
261
+ if ! git push origin "$CURRENT_BRANCH" --follow-tags; then
262
+ err "git push 실패. 수동으로 push 하세요:"
263
+ err " git push origin $CURRENT_BRANCH --follow-tags"
264
+ exit 1
265
+ fi
266
+ ok "커밋 및 태그 원격 push 완료"
267
+
268
+ # ---------- 완료 ----------
269
+ log "=========================================="
270
+ ok "${BOLD}${GREEN}릴리스 완료: $NEW_TAG${RESET}"
271
+ log "=========================================="
272
+ info ""
273
+ info "다음 단계 (사용자 환경 업데이트):"
274
+ info " npm install -g makdoong2-team@${NEXT_VERSION}"
275
+ info " makdoong2-team install"
276
+ info ""
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # rollback-commits.sh <issue>
3
+ # 6단계 시작점(base_sha)으로 worktree HEAD를 soft reset 하여
4
+ # 이번 단계에서 만든 모든 커밋을 취소한다. working tree와 index의 변경 내용은 보존되므로
5
+ # 동일 변경을 다시 쪼개어 1 commit = 1 change로 재커밋할 수 있다.
6
+ # 사용: rollback-commits.sh <issue>
7
+ set -euo pipefail
8
+ ISSUE="${1:?usage: $0 <issue>}"
9
+ HERE="$(cd "$(dirname "$0")" && pwd)"
10
+
11
+ WT="$("$HERE/state.sh" get "$ISSUE" '.worktree' 2>/dev/null | tr -d '"')"
12
+ [ -n "$WT" ] && [ -d "$WT" ] || { echo "[rollback] worktree 확인 실패: $WT" >&2; exit 1; }
13
+
14
+ BASE="$("$HERE/state.sh" get "$ISSUE" '.stages."3_delivery".substages."commit".base_sha' 2>/dev/null | tr -d '"')"
15
+ { [ -n "$BASE" ] && [ "$BASE" != "null" ] && [ "$BASE" != "__MISSING__" ]; } || {
16
+ echo "[rollback] base_sha 미기록 — rollback할 기준점이 없다. 3_delivery.commit 단계에서 base_sha를 먼저 기록하라." >&2
17
+ exit 1
18
+ }
19
+
20
+ # 안전 확인: BASE가 HEAD의 조상이어야 함 (임의 sha로 reset되지 않게)
21
+ git -C "$WT" merge-base --is-ancestor "$BASE" HEAD 2>/dev/null || {
22
+ echo "[rollback] base_sha($BASE)가 현재 HEAD의 조상이 아님 — 수동 확인 필요" >&2
23
+ exit 1
24
+ }
25
+
26
+ # 커밋만 취소, 변경은 보존 (soft reset)
27
+ N=$(git -C "$WT" rev-list --count "$BASE..HEAD" 2>/dev/null || echo 0)
28
+ git -C "$WT" reset --soft "$BASE"
29
+ echo "[rollback] HEAD를 $BASE 로 soft-reset 완료 (취소된 커밋 $N개, working tree/index 변경은 보존됨)"
30
+
31
+ # 3_delivery.commit 관련 state 초기화 (base_sha는 보존 — 동일 기준점으로 재커밋)
32
+ "$HERE/state.sh" set "$ISSUE" '.stages."3_delivery".substages."commit".done' 'false' >/dev/null
33
+ "$HERE/state.sh" set "$ISSUE" '.stages."3_delivery".substages."commit".atomic_review' 'null' >/dev/null
34
+ "$HERE/state.sh" set "$ISSUE" '.stages."3_delivery".substages."commit".head_sha' 'null' >/dev/null
35
+ echo "[rollback] 3_delivery.commit state 초기화 완료. commit 단계를 처음부터 다시 진행하라."
@@ -0,0 +1,194 @@
1
+ // smoke-test.mjs — Node 18+ compatible smoke test for the model-fallback-policy logic.
2
+ // Mirrors src/model-fallback-policy.ts via scripts/model-policy.mjs so we can
3
+ // validate without bun/typescript installed.
4
+ //
5
+ // Run: node scripts/smoke-test.mjs
6
+ // Exits 0 on all-pass, 1 on any failure.
7
+
8
+ import {
9
+ DEFAULT_ALLOWED_PRIMARIES,
10
+ DEFAULT_POLICIES,
11
+ nextModel,
12
+ validatePolicies,
13
+ buildPoliciesFromConfig,
14
+ } from "./model-policy.mjs";
15
+
16
+ let passed = 0, failed = 0;
17
+ const t = (name, fn) => {
18
+ try { fn(); console.log(` ✓ ${name}`); passed++; }
19
+ catch (e) { console.error(` ✗ ${name}: ${e.message}`); failed++; }
20
+ };
21
+ const eq = (a, b, msg) => {
22
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
23
+ throw new Error(`${msg}\n expected: ${JSON.stringify(b)}\n got: ${JSON.stringify(a)}`);
24
+ }
25
+ };
26
+ const assertThrows = (fn, matcher, msg) => {
27
+ let thrown = null;
28
+ try { fn(); } catch (e) { thrown = e; }
29
+ if (!thrown) throw new Error(`${msg}: expected throw, got none`);
30
+ if (matcher && !matcher.test(thrown.message)) {
31
+ throw new Error(`${msg}: message did not match ${matcher}\n got: ${thrown.message}`);
32
+ }
33
+ };
34
+
35
+ console.log("\n[smoke-test] model-fallback-policy chain logic\n");
36
+
37
+ // ── Default chain semantics ──
38
+
39
+ t("makdoong2-team-leader: qwen → haiku on first failure", () => {
40
+ const r = nextModel({ agent: "makdoong2-team-leader", current: "local/qwen3.6-27b", reason: "rate_limit" });
41
+ eq(r.exhausted, false, "should not be exhausted");
42
+ eq(r.next.id, "github-copilot/claude-haiku-4.5", "next should be haiku-4.5");
43
+ });
44
+
45
+ t("makdoong2-team-leader: haiku → exhausted after second failure", () => {
46
+ const r = nextModel({ agent: "makdoong2-team-leader", current: "github-copilot/claude-haiku-4.5" });
47
+ eq(r.exhausted, true, "should be exhausted");
48
+ eq(r.next, null, "next should be null");
49
+ });
50
+
51
+ t("makdoong2-planner: qwen → haiku", () => {
52
+ const r = nextModel({ agent: "makdoong2-planner", current: "local/qwen3.6-27b", reason: "5xx" });
53
+ eq(r.next.id, "github-copilot/claude-haiku-4.5", "planner fallback should be haiku");
54
+ });
55
+
56
+ t("makdoong2-engineer: qwen → haiku (lower-tier degrade)", () => {
57
+ const r = nextModel({ agent: "makdoong2-engineer", current: "local/qwen3.6-27b", reason: "context_exceeded" });
58
+ eq(r.next.id, "github-copilot/claude-haiku-4.5", "engineer fallback should be haiku");
59
+ });
60
+
61
+ t("makdoong2-publisher: qwen → haiku", () => {
62
+ const r = nextModel({ agent: "makdoong2-publisher", current: "local/qwen3.6-27b" });
63
+ eq(r.next.id, "github-copilot/claude-haiku-4.5", "publisher fallback should be haiku");
64
+ });
65
+
66
+ t("makdoong2-verifier: qwen → haiku", () => {
67
+ const r = nextModel({ agent: "makdoong2-verifier", current: "local/qwen3.6-27b" });
68
+ eq(r.next.id, "github-copilot/claude-haiku-4.5", "verifier fallback should be haiku");
69
+ });
70
+
71
+ t("unknown agent returns exhausted", () => {
72
+ const r = nextModel({ agent: "stage99-fictional", current: "any" });
73
+ eq(r.exhausted, true, "unknown agent should be exhausted");
74
+ });
75
+
76
+ t("unknown current model starts from primary", () => {
77
+ const r = nextModel({ agent: "makdoong2-team-leader", current: "anthropic/claude-fictional" });
78
+ eq(r.next.id, "local/qwen3.6-27b", "unknown current should start at primary");
79
+ });
80
+
81
+ t("all default primaries are in DEFAULT_ALLOWED_PRIMARIES", () => {
82
+ for (const [agent, policy] of Object.entries(DEFAULT_POLICIES)) {
83
+ if (!DEFAULT_ALLOWED_PRIMARIES.has(policy.primary.id)) {
84
+ throw new Error(`'${agent}' primary '${policy.primary.id}' is not allowed`);
85
+ }
86
+ }
87
+ });
88
+
89
+ t("all fallback tiers are strictly lower than primary tier", () => {
90
+ validatePolicies(DEFAULT_POLICIES, DEFAULT_ALLOWED_PRIMARIES);
91
+ });
92
+
93
+ // ── Config-driven overrides ──
94
+
95
+ t("agents block: primary + fallback override (string form, backward-compat)", () => {
96
+ const { policies } = buildPoliciesFromConfig({
97
+ agents: {
98
+ "makdoong2-engineer": {
99
+ model: "github-copilot/claude-sonnet-4.6",
100
+ fallback_models: ["github-copilot/claude-haiku-4.5"],
101
+ },
102
+ },
103
+ });
104
+ eq(policies["makdoong2-engineer"].primary.id, "github-copilot/claude-sonnet-4.6", "primary updated");
105
+ eq(policies["makdoong2-engineer"].fallbacks.map(f => f.id), ["github-copilot/claude-haiku-4.5"], "fallbacks set");
106
+ eq(policies["makdoong2-engineer"].fallbacks[0].tier, "low", "string-form fallback defaults to tier=low");
107
+ });
108
+
109
+ t("agents block: fallback object form {id, tier} accepted", () => {
110
+ const { policies } = buildPoliciesFromConfig({
111
+ agents: {
112
+ "makdoong2-planner": {
113
+ model: "github-copilot/claude-sonnet-4.6",
114
+ fallback_models: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
115
+ },
116
+ },
117
+ });
118
+ eq(policies["makdoong2-planner"].fallbacks[0], { id: "github-copilot/claude-haiku-4.5", tier: "low" }, "object-form preserved");
119
+ });
120
+
121
+ t("model_policy.allowed_primaries: extends default allow-list", () => {
122
+ const { policies, allowed } = buildPoliciesFromConfig({
123
+ model_policy: { allowed_primaries: ["custom-provider/custom-model"] },
124
+ agents: {
125
+ "makdoong2-engineer": {
126
+ model: "custom-provider/custom-model",
127
+ fallback_models: ["github-copilot/claude-haiku-4.5"],
128
+ },
129
+ },
130
+ });
131
+ eq(allowed.has("custom-provider/custom-model"), true, "extra primary allowed");
132
+ eq(allowed.has("local/qwen3.6-27b"), true, "built-in defaults preserved");
133
+ eq(policies["makdoong2-engineer"].primary.id, "custom-provider/custom-model", "custom primary set");
134
+ });
135
+
136
+ t("validation rejects primary outside allow-list (no model_policy extension)", () => {
137
+ assertThrows(
138
+ () => buildPoliciesFromConfig({
139
+ agents: { "makdoong2-engineer": { model: "rogue-provider/rogue-model" } },
140
+ }),
141
+ /violates policy/,
142
+ "should reject unlisted primary",
143
+ );
144
+ });
145
+
146
+ t("validation rejects fallback tier >= primary tier", () => {
147
+ assertThrows(
148
+ () => buildPoliciesFromConfig({
149
+ agents: {
150
+ "makdoong2-engineer": {
151
+ model: "local/qwen3.6-27b",
152
+ fallback_models: [{ id: "github-copilot/claude-haiku-4.5", tier: "medium" }],
153
+ },
154
+ },
155
+ }),
156
+ /not strictly lower/,
157
+ "fallback at primary tier must be rejected",
158
+ );
159
+ });
160
+
161
+ t("validation rejects invalid tier label", () => {
162
+ assertThrows(
163
+ () => buildPoliciesFromConfig({
164
+ agents: {
165
+ "makdoong2-engineer": {
166
+ model: "local/qwen3.6-27b",
167
+ fallback_models: [{ id: "github-copilot/claude-haiku-4.5", tier: "ultra" }],
168
+ },
169
+ },
170
+ }),
171
+ /not a valid tier/,
172
+ "unknown tier must be rejected",
173
+ );
174
+ });
175
+
176
+ t("build is pure: invalid config throws but does not mutate DEFAULT_POLICIES", () => {
177
+ const before = JSON.stringify(DEFAULT_POLICIES);
178
+ try {
179
+ buildPoliciesFromConfig({
180
+ agents: { "makdoong2-engineer": { model: "rogue-provider/rogue-model" } },
181
+ });
182
+ } catch { /* expected */ }
183
+ const after = JSON.stringify(DEFAULT_POLICIES);
184
+ eq(after, before, "DEFAULT_POLICIES must remain untouched after a failed override");
185
+ });
186
+
187
+ t("empty config returns defaults verbatim", () => {
188
+ const { policies, allowed } = buildPoliciesFromConfig({});
189
+ eq(Object.keys(policies).sort(), Object.keys(DEFAULT_POLICIES).sort(), "same agent set");
190
+ eq(allowed.size, DEFAULT_ALLOWED_PRIMARIES.size, "no extra primaries added");
191
+ });
192
+
193
+ console.log(`\n[smoke-test] ${passed} passed, ${failed} failed\n`);
194
+ process.exit(failed === 0 ? 0 : 1);