makdoong2-team 2.1.0 → 2.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.
@@ -0,0 +1,110 @@
1
+ // verifier-verdict.ts — dispatch_verifier 응답의 순수 분류기.
2
+ //
3
+ // 왜 별도 파일인가: opencode 의 플러그인 로더는 `src/opencode-plugin.ts` 의
4
+ // **모든 named export 를 plugin factory 로 호출**한다. 헬퍼를 거기에 export 하면
5
+ // 로더가 그것을 팩토리로 부르고 downstream `for (S of hooks) { S.auth }` 에서
6
+ // 터진다 (`test/plugin-exports-shape.test.ts` 가 export 집합을 고정한다).
7
+ // 그래서 순수 로직은 항상 별도 모듈에 두고 import 한다.
8
+ //
9
+ // ── 무엇을 고치는가 ────────────────────────────────────────────────────────
10
+ // 종전 dispatch_verifier 는 다섯 갈래의 결과를 `verdictSource` 로 **구분해 로그에
11
+ // 남기면서도** 툴 반환값의 `verdict` 는 전부 `REJECTED` 하나로 눌러 내보냈다.
12
+ // 그래서 호출자(team-leader)는 다음 둘을 구별할 수 없었다:
13
+ //
14
+ // (가) 콘텐츠 반려 — verifier 가 실제로 검사하고 산출물을 물렸다.
15
+ // → 올바른 조치: `.done=false` 로 되돌리고 stage 를 재실행한다.
16
+ // (나) 인프라 실패 — verifier 세션이 판정을 내리지 못하고 죽었다.
17
+ // → 올바른 조치: **verifier 만 재실행한다.** stage 산출물은 멀쩡하다.
18
+ //
19
+ // 실제로 (나)가 (가)로 보고되어, 마커가 전부 정상인 `1_planning.jira` 를
20
+ // team-leader 가 통째로 재실행했다 (planner 200초 재가동). 재검증 결과는
21
+ // VERIFIED 였다 — 원 작업에는 처음부터 결함이 없었다 (GitHub issue #7).
22
+ //
23
+ // 구분에 필요한 정보는 이미 전부 있었다. 계약에 반영하지 않았을 뿐이다.
24
+ const VERDICT_TAG_RE = /<verifier-verdict>\s*(VERIFIED|REJECTED)\s*<\/verifier-verdict>/i;
25
+ const VERDICT_JSON_RE = /"verdict"\s*:\s*"(VERIFIED|REJECTED)"/i;
26
+ const NEXT_ACTION_VERIFIED = "다음 substage 로 진행한다.";
27
+ const NEXT_ACTION_REJECTED = "검증이 산출물을 반려했다. 해당 substage 의 `.done` 을 false 로 되돌리고 " +
28
+ "dispatch_stage 를 재호출한다 (이전 실패 사유는 프롬프트에 자동 주입된다).";
29
+ const NEXT_ACTION_ERROR = "검증이 **수행되지 않았다** (verifier 세션 인프라 실패). 같은 인자로 " +
30
+ "dispatch_verifier 만 재호출한다. stage 를 재실행하지 말고 `.done` 을 false 로 " +
31
+ "되돌리지도 말 것 — 판정을 못 얻었을 뿐 stage 산출물은 그대로다. " +
32
+ "verifier 는 idempotent 하므로 재호출이 안전하다.";
33
+ /**
34
+ * verifier 세션 결과 → 판정 분류.
35
+ *
36
+ * 판정 태그는 세션 실패보다 우선한다: 태그를 이미 뱉은 뒤 세션 꼬리가 죽었다면
37
+ * 그 판정은 실제로 산출된 것이므로 존중한다.
38
+ *
39
+ * `malformed_output_default`(세션은 살아서 끝났는데 태그가 없음)는 `ERROR` 로
40
+ * 올리지 않고 종전대로 `REJECTED` 를 유지한다 — 형식 위반은 verifier 가 관측
41
+ * 가능한 콘텐츠 결함이고, `same_reason_streak` 이 이미 무한루프를 막는다.
42
+ * 여기서 완화 대상은 판정이 물리적으로 존재하지 않는 두 경로뿐이다.
43
+ */
44
+ export function classifyVerifierOutcome(input) {
45
+ const tag = input.raw.match(VERDICT_TAG_RE);
46
+ if (tag) {
47
+ const verdict = tag[1].toUpperCase();
48
+ return {
49
+ verdict,
50
+ source: "verdict_tag",
51
+ retryable: false,
52
+ countsAsRejection: verdict === "REJECTED",
53
+ parsed: "verdict tag found",
54
+ nextAction: verdict === "VERIFIED" ? NEXT_ACTION_VERIFIED : NEXT_ACTION_REJECTED,
55
+ };
56
+ }
57
+ const json = input.raw.match(VERDICT_JSON_RE);
58
+ if (json) {
59
+ const verdict = json[1].toUpperCase();
60
+ return {
61
+ verdict,
62
+ source: "json_fallback",
63
+ retryable: false,
64
+ countsAsRejection: verdict === "REJECTED",
65
+ parsed: `verdict tag missing — extracted from JSON body (${verdict})`,
66
+ nextAction: verdict === "VERIFIED" ? NEXT_ACTION_VERIFIED : NEXT_ACTION_REJECTED,
67
+ };
68
+ }
69
+ if (input.sessionGone || !input.success) {
70
+ const source = input.sessionGone
71
+ ? "session_gone_default"
72
+ : "session_failed_default";
73
+ return {
74
+ verdict: "ERROR",
75
+ source,
76
+ retryable: true,
77
+ countsAsRejection: false,
78
+ parsed: `verifier session failed — 판정 없음 (${source}): ${input.raw.slice(0, 120)}`,
79
+ nextAction: NEXT_ACTION_ERROR,
80
+ };
81
+ }
82
+ return {
83
+ verdict: "REJECTED",
84
+ source: "malformed_output_default",
85
+ retryable: false,
86
+ countsAsRejection: true,
87
+ parsed: "verdict tag missing — defaulted to REJECTED (verifier output malformed)",
88
+ nextAction: NEXT_ACTION_REJECTED,
89
+ };
90
+ }
91
+ /**
92
+ * 인프라 실패로 판정을 못 얻은 횟수의 연속 상한.
93
+ *
94
+ * `same_reason_streak`(REJECTED 무한루프 차단)의 ERROR 판 대응물이다. ERROR 는
95
+ * 집계에서 빠지므로 그쪽 상한이 걸리지 않는다 — 별도 상한이 없으면 verifier 만
96
+ * 재호출하는 루프가 영원히 돈다. 3회로 잡은 것은 재호출이 저렴하고(단일 read-only
97
+ * 세션) 3회 연속 실패면 모델·인프라 문제라 자동 재시도로 풀리지 않기 때문이다.
98
+ */
99
+ export const VERIFIER_ERROR_STREAK_LIMIT = 3;
100
+ /** 이번 결과를 반영한 ERROR 연속 횟수. ERROR 가 아니면 0 으로 리셋된다. */
101
+ export function nextVerifierErrorStreak(previousStreak, classification) {
102
+ if (classification.verdict !== "ERROR")
103
+ return 0;
104
+ const prev = Number.isFinite(previousStreak) && previousStreak > 0 ? previousStreak : 0;
105
+ return prev + 1;
106
+ }
107
+ /** 상한 도달 여부. 도달하면 team-leader 는 재호출을 멈추고 사용자에게 보고한다. */
108
+ export function verifierErrorStreakExceeded(streak, limit = VERIFIER_ERROR_STREAK_LIMIT) {
109
+ return streak >= limit;
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makdoong2-team",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -65,21 +65,21 @@
65
65
  },
66
66
  "scripts": {
67
67
  "build": "tsc -p tsconfig.build.json",
68
- "build:entry": "tsc -p tsconfig.entry.json && node -e \"import('node:fs').then(({chmodSync})=>{for(const f of ['bin/cli.js','postinstall.mjs','scripts/run-tests.mjs','scripts/test-postinstall.mjs']) chmodSync(f, 0o755)})\"",
69
68
  "typecheck": "tsc -p tsconfig.json",
70
69
  "clean": "node -e \"import('node:fs').then(({rmSync})=>rmSync('dist',{recursive:true,force:true}))\"",
71
- "prepack": "npm run clean && npm run build && node --test test/entry-artifacts.test.ts",
70
+ "prepack": "npm run clean && npm run build && npm run build:entry",
72
71
  "postinstall": "node postinstall.mjs",
73
72
  "prepare": "husky install",
74
73
  "test": "XDG_CONFIG_HOME=\"$(mktemp -d)\" XDG_CACHE_HOME=\"$(mktemp -d)\" npm run test:all",
75
- "test:all": "npm run build && node scripts/run-tests.mjs",
74
+ "test:all": "npm run build && node scripts/run-tests.mts",
76
75
  "test:install": "node test/install-lib.test.ts",
77
- "test:postinstall": "node scripts/test-postinstall.mjs",
76
+ "test:postinstall": "node scripts/test-postinstall.mts",
78
77
  "test:ubuntu": "bash scripts/test-ubuntu.sh",
79
78
  "release": "bash scripts/release.sh",
80
79
  "release:patch": "bash scripts/release.sh patch",
81
80
  "release:minor": "bash scripts/release.sh minor",
82
- "release:major": "bash scripts/release.sh major"
81
+ "release:major": "bash scripts/release.sh major",
82
+ "build:entry": "tsc -p tsconfig.entry.json && node -e \"import('node:fs').then(({chmodSync})=>{chmodSync('bin/cli.js',0o755)})\""
83
83
  },
84
84
  "homepage": "https://github.com/y00njinuk/makdoong2-team#readme",
85
85
  "bugs": {
package/postinstall.mjs CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // postinstall.mts — Auto-install on npm install -g
3
3
  //
4
- // 산출물은 같은 자리의 postinstall.mjs 다 (`npm run build:entry`). 그 .mjs 를
5
- // 직접 편집하지 말 것 — 다음 빌드가 덮어쓴다.
6
4
  //
7
5
  // **이 파일은 반드시 패키지 루트에 있어야 한다.** pkgRoot = 자기 디렉토리
8
6
  // (`..` 없음)가 계약이라, 한 단계라도 깊어지면 agents/skills 복사가 ENOENT 로
@@ -83,7 +83,8 @@
83
83
  ## 4. PR 생성 규칙
84
84
 
85
85
  1. 반드시 **Draft 로 생성**한다 → `bitbucket_createPullRequest(..., draft=true)`.
86
- 2. **리뷰어를 추가하지 않는다** → `reviewers` 파라미터 자체를 생략한다 (`[]` 도 아니라 미전달).
86
+ 2. **토큰 소유자를 리뷰어로 추가한다** → §4-1 로 식별한 username 을 `reviewers` 에 전달한다. 자기 자신이 PR 작성자여서 Bitbucket DC 가 거부하면 `reviewer_self_skipped=true` 로 기록한다 (`stages/08-pr.md` §7-5).
87
+ > 종전 이 항목은 "리뷰어를 추가하지 않는다 (`reviewers` 파라미터 자체를 생략)" 였다. 같은 문서의 §4-1·§4-2 와도, `stages/08-pr.md` 와도, `gates/stage7-post-pr-verify.sh` 의 reviewer 마커 검사(`reviewer_added` XOR `reviewer_self_skipped`, 미기록 시 fail)와도 정면으로 어긋나 있었다. 게이트가 마커를 요구하는 이상 리뷰어는 추가하는 것이 맞다.
87
88
  3. 생성 후 각 커밋별 diff 를 분석하여 **인라인 코멘트** 를 **개별 live 코멘트** 로 작성한다 (아래 §5–6).
88
89
 
89
90
  ### 4-1. 토큰 소유자 식별 (리뷰어 추가용)
@@ -1,7 +1,5 @@
1
1
  // scripts/install-lib.mts — Reusable install logic for CLI and postinstall.
2
2
  //
3
- // 산출물은 같은 자리의 scripts/install-lib.mjs 다 (`npm run build:entry`).
4
- // 그 .mjs 를 직접 편집하지 말 것 — 다음 빌드가 덮어쓴다.
5
3
  //
6
4
  // Extracted from bin/cli doInstall() to enable both:
7
5
  // 1) Manual install via `makdoong2-team install` (CLI)
@@ -1,7 +1,5 @@
1
1
  // scripts/install-lib.mts — Reusable install logic for CLI and postinstall.
2
2
  //
3
- // 산출물은 같은 자리의 scripts/install-lib.mjs 다 (`npm run build:entry`).
4
- // 그 .mjs 를 직접 편집하지 말 것 — 다음 빌드가 덮어쓴다.
5
3
  //
6
4
  // Extracted from bin/cli doInstall() to enable both:
7
5
  // 1) Manual install via `makdoong2-team install` (CLI)
@@ -1,7 +1,5 @@
1
1
  // model-policy.mts — pure mirror of src/model-fallback-policy.ts.
2
2
  //
3
- // 산출물은 같은 자리의 scripts/model-policy.mjs 다 (`npm run build:entry`).
4
- // 그 .mjs 를 직접 편집하지 말 것 — 다음 빌드가 덮어쓴다.
5
3
  //
6
4
  // 왜 미러인가: bin/cli.js 는 dist/ 없이 돌아야 한다. doctor/validate 는 설치가
7
5
  // 깨졌을 때 실행하는 진단 도구인데, dist/ 를 import 하면 (a) .gitignore 대상이라
@@ -1,7 +1,5 @@
1
1
  // model-policy.mts — pure mirror of src/model-fallback-policy.ts.
2
2
  //
3
- // 산출물은 같은 자리의 scripts/model-policy.mjs 다 (`npm run build:entry`).
4
- // 그 .mjs 를 직접 편집하지 말 것 — 다음 빌드가 덮어쓴다.
5
3
  //
6
4
  // 왜 미러인가: bin/cli.js 는 dist/ 없이 돌아야 한다. doctor/validate 는 설치가
7
5
  // 깨졌을 때 실행하는 진단 도구인데, dist/ 를 import 하면 (a) .gitignore 대상이라
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/run-tests.mts — 전체 테스트를 순차 실행하고 실패를 모아서 보고한다.
3
3
  //
4
- // 산출물은 같은 자리의 scripts/run-tests.mjs 다 (`npm run build:entry`).
5
4
  // **새 테스트를 등록할 때는 이 .mts 의 STEPS 를 고친다** — .mjs 는 산출물이라
6
5
  // 다음 빌드에 날아간다.
7
6
  //
@@ -37,7 +36,7 @@ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
37
36
  const STEPS = [
38
37
  "bash scripts/lint-agent-prompts.sh",
39
38
  "node --test test/shell-portability.test.ts",
40
- "node scripts/smoke-test.mjs",
39
+ "node scripts/smoke-test.mts",
41
40
  "bash scripts/gate-policy-test.sh",
42
41
  "node test/install-lib.test.ts",
43
42
  "node test/skill-mcp-registry.test.ts",
@@ -51,6 +50,7 @@ const STEPS = [
51
50
  "node --test test/doctor-phantom-scan.test.ts",
52
51
  "node --test test/mcp-secret-injector.test.ts",
53
52
  "node --test test/poll-sub-session.test.ts",
53
+ "node --test test/verifier-verdict.test.ts",
54
54
  "node --test test/tmux-monitor.test.ts",
55
55
  "node --test test/gate-already-done-block.test.ts",
56
56
  "node --test test/gate-hybrid-first-entry.test.ts",
@@ -83,7 +83,6 @@ const STEPS = [
83
83
  "node --test test/doctor-exit-code.test.ts",
84
84
  "node --test test/example-config-portability.test.ts",
85
85
  "node --test test/research-fanout.test.ts",
86
- "node --test test/entry-artifacts.test.ts",
87
86
  "node --test test/state-sh-write-atomicity.test.ts",
88
87
  "node --test test/gate-locale-and-path.test.ts",
89
88
  "node --test test/model-policy-parity.test.ts",
@@ -96,7 +95,7 @@ const STEPS = [
96
95
  "node --test test/sync-state-commit-detection.test.ts",
97
96
  "node --test test/skill-tls-config.test.ts",
98
97
  "node --test test/opencode-json-read.test.ts",
99
- "node scripts/test-postinstall.mjs"
98
+ "node scripts/test-postinstall.mts"
100
99
  ];
101
100
 
102
101
  const CYAN = "\u001b[36m", GREEN = "\u001b[32m", RED = "\u001b[31m", YELLOW = "\u001b[33m", OFF = "\u001b[0m";
@@ -1,6 +1,5 @@
1
1
  // smoke-test.mts — smoke test for the model-fallback-policy chain logic.
2
2
  //
3
- // 산출물은 같은 자리의 scripts/smoke-test.mjs 다 (`npm run build:entry`).
4
3
  //
5
4
  // 주의: 이 파일은 **미러(scripts/model-policy.mts)만** 검증한다. 정본
6
5
  // src/model-fallback-policy.ts 와의 동치는 test/model-policy-parity.test.ts 가
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/test-postinstall.mts — integration test for npm install -g postinstall hook
3
3
  //
4
- // 산출물은 같은 자리의 scripts/test-postinstall.mjs 다 (`npm run build:entry`).
5
4
  //
6
5
  // Simulates global install in isolated HOME directory:
7
6
  // 1) npm pack → produces tarball
package/stages/08-pr.md CHANGED
@@ -175,3 +175,30 @@ post-verify 실패 시:
175
175
 
176
176
 
177
177
 
178
+
179
+ ## 7-7. publisher 세션이 반복 실패할 때 (대체 경로)
180
+
181
+ `dispatch_stage(3_delivery.pr)` 가 **연속 3회 이상** 실패 반환하면 (empty / session_gone / timeout — 즉 산출물 문제가 아니라 세션 자체가 결과를 못 낸 경우), 부장님(team-leader)은 아래 조건을 **모두** 확인한 뒤 남은 작업을 직접 완결할 수 있다.
182
+
183
+ > 근거: 실제로 이 단계가 4회 연속 empty 반환했고, 그 사이 `git push` 는 실제로 수행됐는데 PR 생성과 마커 기록만 미완으로 남았다. 재-dispatch 를 반복해도 같은 지점에서 멈춘다 — 남은 일에 git 명령이 없기 때문이다 (GitHub issue #7).
184
+
185
+ **진입 조건 (하나라도 false 면 진입 금지 — 사용자에게 보고하고 지시를 기다린다)**
186
+
187
+ | 항목 | 확인 방법 |
188
+ |---|---|
189
+ | 1 | `dispatch_stage(3_delivery.pr)` 가 3회 이상 `ok:false` 로 반환됐다 |
190
+ | 2 | `origin/<브랜치>` 가 이미 존재한다 (**push 가 끝나 있다**) — `git ls-remote` 는 부장님 권한 밖이므로 직전 publisher 세션 로그 또는 `hang_history` 로 확인 |
191
+ | 3 | 남은 작업에 **git 명령이 하나도 없다** — PR 생성(MCP)·마커 기록(`state.sh`)뿐이다 |
192
+ | 4 | `escalate: true` 를 받지 않았다 (받았으면 재디스패치·대체 실행 모두 금지, 사용자 에스컬레이션만) |
193
+
194
+ **허용 범위 (이것만 한다)**
195
+
196
+ - `bitbucket_createPullRequest` / `bitbucket_getPullRequest` / `bitbucket_updatePullRequest` — MCP 호출이며 git 명령이 아니다.
197
+ - `state.sh set` 으로 §완료 기록의 마커 기록.
198
+ - `gates/stage7-post-pr-verify.sh` 실행.
199
+
200
+ **금지 (조건 2 가 거짓이면 그냥 막힌다)**
201
+
202
+ - `git push` 를 비롯한 모든 git 명령. 부장님은 permission 이 deny 이고, 우회하지 않는다. push 가 남아 있으면 이 대체 경로를 쓸 수 없다 — 사용자에게 보고한다.
203
+
204
+ **보고**: `[3_delivery.pr PUBLISHER FALLBACK] publisher <N>회 실패 → 남은 작업(PR 생성·마커)에 git 없음을 확인하고 부장님이 직접 완결. push 는 <세션 ID> 에서 이미 완료됨`
@@ -1,155 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/run-tests.mts — 전체 테스트를 순차 실행하고 실패를 모아서 보고한다.
3
- //
4
- // 산출물은 같은 자리의 scripts/run-tests.mjs 다 (`npm run build:entry`).
5
- // **새 테스트를 등록할 때는 이 .mts 의 STEPS 를 고친다** — .mjs 는 산출물이라
6
- // 다음 빌드에 날아간다.
7
- //
8
- // 왜 `&&` 체인을 대체하는가:
9
- // 기존 test:all 은 42개 단계를 `&&` 로 이어 붙였다. 앞 단계가 하나라도 실패하면
10
- // 뒤 단계는 아예 실행되지 않는다. 실제로 tmux-monitor 실패가 그 뒤의
11
- // rollback-commits / worktree-sync-gate 실패를 오래 가려왔고, gate-policy-test 가
12
- // 깨져 있던 동안에는 사실상 전 구간이 미실행 상태였다.
13
- //
14
- // 순차 실행은 유지한다 (병렬화하지 않는다). npm test 가 XDG_CONFIG_HOME 을 단일
15
- // 임시 디렉토리로 고정하므로, 설치 계열 테스트를 동시에 돌리면 서로의 config 를
16
- // 덮어써 경합이 난다.
17
- //
18
- // 2단계 구성:
19
- // 1) 호스트 단계 — STEPS 를 순차 실행한다.
20
- // 2) Linux 교차 검증 — 호스트가 Linux 가 아니면 같은 스위트를 Ubuntu 컨테이너에서
21
- // 한 번 더 돌린다. 별도 명령을 기억할 필요 없이 npm test 만으로 두 플랫폼이
22
- // 함께 검증된다.
23
- //
24
- // 호스트 단계를 컨테이너 실행으로 "대체" 하지 않는 이유: 실제로 잡힌 결함 중
25
- // 하나(rollback-commits.sh 의 unbraced 변수)는 Darwin libc 에서만 재현된다.
26
- // 컨테이너로 갈아타면 그 부류가 영구히 은폐된다. 두 번 돌려야 둘 다 잡힌다.
27
- //
28
- // 종료 코드: 실패 단계가 하나라도 있으면 1.
29
- import { spawnSync } from "node:child_process";
30
- import { existsSync } from "node:fs";
31
- import { dirname, join } from "node:path";
32
- import { fileURLToPath } from "node:url";
33
- const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
34
- const STEPS = [
35
- "bash scripts/lint-agent-prompts.sh",
36
- "node --test test/shell-portability.test.ts",
37
- "node scripts/smoke-test.mjs",
38
- "bash scripts/gate-policy-test.sh",
39
- "node test/install-lib.test.ts",
40
- "node test/skill-mcp-registry.test.ts",
41
- "node --test test/state-write-guard.test.ts",
42
- "node --test test/state-access-guard.test.ts",
43
- "node --test test/git-exclude-registration.test.ts",
44
- "node --test test/issue-reporter-guard.test.ts",
45
- "node --test test/release-confirm.test.ts",
46
- "node --test test/state-sh-schema.test.ts",
47
- "node --test test/state-sh-init-review-shape.test.ts",
48
- "node --test test/doctor-phantom-scan.test.ts",
49
- "node --test test/mcp-secret-injector.test.ts",
50
- "node --test test/poll-sub-session.test.ts",
51
- "node --test test/tmux-monitor.test.ts",
52
- "node --test test/gate-already-done-block.test.ts",
53
- "node --test test/gate-hybrid-first-entry.test.ts",
54
- "node --test test/gate-post-pr-verify.test.ts",
55
- "node --test test/gate-post-review-verify.test.ts",
56
- "node --test test/gate-requirements-quality.test.ts",
57
- "node --test test/worktree-sync-gate.test.ts",
58
- "node --test test/planner-prompt-early-exit.test.ts",
59
- "node --test test/plugin-bug-fixes.test.ts",
60
- "node --test test/omo-tmux-detection.test.ts",
61
- "node --test test/empty-output-done-override.test.ts",
62
- "node --test test/tmux-monitor-orphan.test.ts",
63
- "node --test test/poll-permission-scope.test.ts",
64
- "node --test test/dispatch-review-fixes.test.ts",
65
- "node --test test/dispatch-stage-redispatch.test.ts",
66
- "node --test test/dispatch-stage-dev-resume-prompt.test.ts",
67
- "node --test test/logger.test.ts",
68
- "node --test test/redact-secrets.test.ts",
69
- "node --test test/commit-atomicity-verify.test.ts",
70
- "node --test test/verdict-reason-injection.test.ts",
71
- "node --test test/verdict-hash-normalize.test.ts",
72
- "node --test test/session-index-fallback.test.ts",
73
- "node --test test/plugin-exports-shape.test.ts",
74
- "node --test test/stale-worktree-recovery.test.ts",
75
- "node --test test/rollback-commits.test.ts",
76
- "node --test test/config-dir-home-fallback.test.ts",
77
- "node --test test/state-path-migration.test.ts",
78
- "node --test test/state-path-relative.test.ts",
79
- "node --test test/with-fallback-no-bun.test.ts",
80
- "node --test test/doctor-exit-code.test.ts",
81
- "node --test test/example-config-portability.test.ts",
82
- "node --test test/research-fanout.test.ts",
83
- "node --test test/entry-artifacts.test.ts",
84
- "node --test test/state-sh-write-atomicity.test.ts",
85
- "node --test test/gate-locale-and-path.test.ts",
86
- "node --test test/model-policy-parity.test.ts",
87
- // npm install -g 전 구간(pack → 전역 설치 → postinstall → doctor)을 검증하는
88
- // 유일한 테스트다. 종전에는 STEPS 에 없어서 `npm test` 가 한 번도 실행하지
89
- // 않았고, 그 사이 내용이 현행 구현과 반대로 썩어 있었다 (plugin ref 형식,
90
- // secrets.env 처리). 등록하지 않으면 같은 일이 반복된다.
91
- "node --test test/agent-permission-keys.test.ts",
92
- "node --test test/gate-missing-state-sentinel.test.ts",
93
- "node --test test/sync-state-commit-detection.test.ts",
94
- "node --test test/skill-tls-config.test.ts",
95
- "node --test test/opencode-json-read.test.ts",
96
- "node scripts/test-postinstall.mjs"
97
- ];
98
- const CYAN = "\u001b[36m", GREEN = "\u001b[32m", RED = "\u001b[31m", YELLOW = "\u001b[33m", OFF = "\u001b[0m";
99
- const say = (s) => { process.stdout.write(s); };
100
- // ── 1단계: 호스트 ──
101
- const results = [];
102
- for (const [i, step] of STEPS.entries()) {
103
- const [cmd, ...args] = step.split(" ");
104
- say(`\n${CYAN}[${String(i + 1).padStart(2)}/${STEPS.length}]${OFF} ${step}\n`);
105
- const r = spawnSync(cmd, args, { stdio: "inherit" });
106
- results.push({ step, code: r.status === null ? 1 : r.status });
107
- }
108
- // ── 2단계: Linux 교차 검증 ──
109
- /**
110
- * 컨테이너 교차 검증을 돌려야 하는지 판단한다.
111
- * @returns {{run: true} | {run: false, reason: string, loud: boolean}}
112
- */
113
- function linuxCheckPlan() {
114
- // 컨테이너 안에서 다시 컨테이너를 띄우지 않는다 (무한 재귀 방지).
115
- if (process.env.MAKDOONG2_IN_TEST_CONTAINER === "1") {
116
- return { run: false, reason: "이미 테스트 컨테이너 안", loud: false };
117
- }
118
- // 호스트가 Linux 면 방금 돌린 것이 곧 Linux 결과다.
119
- if (process.platform === "linux") {
120
- return { run: false, reason: "호스트가 이미 Linux", loud: false };
121
- }
122
- if (process.env.MAKDOONG2_SKIP_LINUX_CHECK === "1") {
123
- return { run: false, reason: "MAKDOONG2_SKIP_LINUX_CHECK=1", loud: true };
124
- }
125
- if (!existsSync(join(REPO_ROOT, "Dockerfile.test"))) {
126
- return { run: false, reason: "Dockerfile.test 없음 (배포 tarball 등)", loud: false };
127
- }
128
- if (spawnSync("docker", ["--version"], { stdio: "ignore" }).status !== 0) {
129
- return { run: false, reason: "docker 없음 — Linux 전용 회귀를 놓칠 수 있다", loud: true };
130
- }
131
- return { run: true };
132
- }
133
- const plan = linuxCheckPlan();
134
- let linuxResult = null;
135
- if (plan.run) {
136
- say(`\n${CYAN}[linux]${OFF} Ubuntu 컨테이너에서 교차 검증 (${process.platform} 호스트)\n`);
137
- const r = spawnSync("bash", [join(REPO_ROOT, "scripts", "test-ubuntu.sh")], { stdio: "inherit" });
138
- linuxResult = { step: "linux 교차 검증 (scripts/test-ubuntu.sh)", code: r.status === null ? 1 : r.status };
139
- results.push(linuxResult);
140
- }
141
- else if (plan.loud) {
142
- say(`\n${YELLOW}[linux] 교차 검증 건너뜀 — ${plan.reason}${OFF}\n`);
143
- }
144
- // ── 요약 ──
145
- const failed = results.filter((r) => r.code !== 0);
146
- say(`\n${"=".repeat(70)}\n`);
147
- if (failed.length === 0) {
148
- const suffix = linuxResult ? " (호스트 + Ubuntu 교차 검증)" : "";
149
- say(`${GREEN}[run-tests] ${results.length}개 단계 전부 통과${suffix}${OFF}\n`);
150
- process.exit(0);
151
- }
152
- say(`${RED}[run-tests] ${failed.length}/${results.length} 단계 실패:${OFF}\n`);
153
- for (const f of failed)
154
- say(` ${RED}\u2716${OFF} ${f.step} (exit ${f.code})\n`);
155
- process.exit(1);
@@ -1,178 +0,0 @@
1
- // smoke-test.mts — smoke test for the model-fallback-policy chain logic.
2
- //
3
- // 산출물은 같은 자리의 scripts/smoke-test.mjs 다 (`npm run build:entry`).
4
- //
5
- // 주의: 이 파일은 **미러(scripts/model-policy.mts)만** 검증한다. 정본
6
- // src/model-fallback-policy.ts 와의 동치는 test/model-policy-parity.test.ts 가
7
- // 따로 강제한다 — 예전 주석은 이 파일이 정본까지 검증한다고 적었으나 거짓이었다.
8
- //
9
- // Run: node scripts/smoke-test.mjs
10
- // Exits 0 on all-pass, 1 on any failure.
11
- import { DEFAULT_ALLOWED_PRIMARIES, DEFAULT_POLICIES, nextModel, validatePolicies, buildPoliciesFromConfig, } from "./model-policy.mjs";
12
- let passed = 0, failed = 0;
13
- const t = (name, fn) => {
14
- try {
15
- fn();
16
- console.log(` ✓ ${name}`);
17
- passed++;
18
- }
19
- catch (e) {
20
- console.error(` ✗ ${name}: ${e instanceof Error ? e.message : String(e)}`);
21
- failed++;
22
- }
23
- };
24
- const eq = (a, b, msg) => {
25
- if (JSON.stringify(a) !== JSON.stringify(b)) {
26
- throw new Error(`${msg}\n expected: ${JSON.stringify(b)}\n got: ${JSON.stringify(a)}`);
27
- }
28
- };
29
- const assertThrows = (fn, matcher, msg) => {
30
- let thrown = null;
31
- try {
32
- fn();
33
- }
34
- catch (e) {
35
- thrown = e;
36
- }
37
- if (!thrown)
38
- throw new Error(`${msg}: expected throw, got none`);
39
- const message = thrown instanceof Error ? thrown.message : String(thrown);
40
- if (matcher && !matcher.test(message)) {
41
- throw new Error(`${msg}: message did not match ${matcher}\n got: ${message}`);
42
- }
43
- };
44
- /** `next` 가 null 이 아님을 단언하고 좁혀서 돌려준다. */
45
- const expectNext = (r) => {
46
- if (!r.next)
47
- throw new Error("expected a next model, got null (chain exhausted)");
48
- return r.next;
49
- };
50
- console.log("\n[smoke-test] model-fallback-policy chain logic\n");
51
- // ── Default chain semantics ──
52
- t("makdoong2-team-leader: gpt-5.6-luna → haiku on first failure", () => {
53
- const r = nextModel({ agent: "makdoong2-team-leader", current: "github-copilot/gpt-5.6-luna", reason: "rate_limit" });
54
- eq(r.exhausted, false, "should not be exhausted");
55
- eq(expectNext(r).id, "github-copilot/claude-haiku-4.5", "next should be haiku-4.5");
56
- });
57
- t("makdoong2-team-leader: haiku → exhausted after second failure", () => {
58
- const r = nextModel({ agent: "makdoong2-team-leader", current: "github-copilot/claude-haiku-4.5" });
59
- eq(r.exhausted, true, "should be exhausted");
60
- eq(r.next, null, "next should be null");
61
- });
62
- t("makdoong2-planner: gpt-5.6-luna → haiku", () => {
63
- const r = nextModel({ agent: "makdoong2-planner", current: "github-copilot/gpt-5.6-luna", reason: "5xx" });
64
- eq(expectNext(r).id, "github-copilot/claude-haiku-4.5", "planner fallback should be haiku");
65
- });
66
- t("makdoong2-engineer: gpt-5.6-luna → haiku (lower-tier degrade)", () => {
67
- const r = nextModel({ agent: "makdoong2-engineer", current: "github-copilot/gpt-5.6-luna", reason: "context_exceeded" });
68
- eq(expectNext(r).id, "github-copilot/claude-haiku-4.5", "engineer fallback should be haiku");
69
- });
70
- t("makdoong2-publisher: gpt-5.6-luna → haiku", () => {
71
- const r = nextModel({ agent: "makdoong2-publisher", current: "github-copilot/gpt-5.6-luna" });
72
- eq(expectNext(r).id, "github-copilot/claude-haiku-4.5", "publisher fallback should be haiku");
73
- });
74
- t("makdoong2-verifier: gpt-5.6-luna → haiku", () => {
75
- const r = nextModel({ agent: "makdoong2-verifier", current: "github-copilot/gpt-5.6-luna" });
76
- eq(expectNext(r).id, "github-copilot/claude-haiku-4.5", "verifier fallback should be haiku");
77
- });
78
- t("unknown agent returns exhausted", () => {
79
- const r = nextModel({ agent: "stage99-fictional", current: "any" });
80
- eq(r.exhausted, true, "unknown agent should be exhausted");
81
- });
82
- t("unknown current model starts from primary", () => {
83
- const r = nextModel({ agent: "makdoong2-team-leader", current: "anthropic/claude-fictional" });
84
- eq(expectNext(r).id, "github-copilot/gpt-5.6-luna", "unknown current should start at primary");
85
- });
86
- t("all default primaries are in DEFAULT_ALLOWED_PRIMARIES", () => {
87
- for (const [agent, policy] of Object.entries(DEFAULT_POLICIES)) {
88
- if (!DEFAULT_ALLOWED_PRIMARIES.has(policy.primary.id)) {
89
- throw new Error(`'${agent}' primary '${policy.primary.id}' is not allowed`);
90
- }
91
- }
92
- });
93
- t("all fallback tiers are strictly lower than primary tier", () => {
94
- validatePolicies(DEFAULT_POLICIES, DEFAULT_ALLOWED_PRIMARIES);
95
- });
96
- // ── Config-driven overrides ──
97
- t("agents block: primary + fallback override (string form, backward-compat)", () => {
98
- const { policies } = buildPoliciesFromConfig({
99
- agents: {
100
- "makdoong2-engineer": {
101
- model: "github-copilot/claude-sonnet-4.6",
102
- fallback_models: ["github-copilot/claude-haiku-4.5"],
103
- },
104
- },
105
- });
106
- eq(policies["makdoong2-engineer"].primary.id, "github-copilot/claude-sonnet-4.6", "primary updated");
107
- eq(policies["makdoong2-engineer"].fallbacks.map(f => f.id), ["github-copilot/claude-haiku-4.5"], "fallbacks set");
108
- eq(policies["makdoong2-engineer"].fallbacks[0].tier, "low", "string-form fallback defaults to tier=low");
109
- });
110
- t("agents block: fallback object form {id, tier} accepted", () => {
111
- const { policies } = buildPoliciesFromConfig({
112
- agents: {
113
- "makdoong2-planner": {
114
- model: "github-copilot/claude-sonnet-4.6",
115
- fallback_models: [{ id: "github-copilot/claude-haiku-4.5", tier: "low" }],
116
- },
117
- },
118
- });
119
- eq(policies["makdoong2-planner"].fallbacks[0], { id: "github-copilot/claude-haiku-4.5", tier: "low" }, "object-form preserved");
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
- t("validation rejects primary outside allow-list (no model_policy extension)", () => {
136
- assertThrows(() => buildPoliciesFromConfig({
137
- agents: { "makdoong2-engineer": { model: "rogue-provider/rogue-model" } },
138
- }), /violates policy/, "should reject unlisted primary");
139
- });
140
- t("validation rejects fallback tier >= primary tier", () => {
141
- assertThrows(() => buildPoliciesFromConfig({
142
- agents: {
143
- "makdoong2-engineer": {
144
- model: "local/qwen3.6-27b",
145
- fallback_models: [{ id: "github-copilot/claude-haiku-4.5", tier: "medium" }],
146
- },
147
- },
148
- }), /not strictly lower/, "fallback at primary tier must be rejected");
149
- });
150
- t("validation rejects invalid tier label", () => {
151
- assertThrows(() => buildPoliciesFromConfig({
152
- agents: {
153
- "makdoong2-engineer": {
154
- model: "local/qwen3.6-27b",
155
- // 의도적으로 잘못된 tier 라벨 — 런타임 검증이 잡아야 한다.
156
- fallback_models: [{ id: "github-copilot/claude-haiku-4.5", tier: "ultra" }],
157
- },
158
- },
159
- }), /not a valid tier/, "unknown tier must be rejected");
160
- });
161
- t("build is pure: invalid config throws but does not mutate DEFAULT_POLICIES", () => {
162
- const before = JSON.stringify(DEFAULT_POLICIES);
163
- try {
164
- buildPoliciesFromConfig({
165
- agents: { "makdoong2-engineer": { model: "rogue-provider/rogue-model" } },
166
- });
167
- }
168
- catch { /* expected */ }
169
- const after = JSON.stringify(DEFAULT_POLICIES);
170
- eq(after, before, "DEFAULT_POLICIES must remain untouched after a failed override");
171
- });
172
- t("empty config returns defaults verbatim", () => {
173
- const { policies, allowed } = buildPoliciesFromConfig({});
174
- eq(Object.keys(policies).sort(), Object.keys(DEFAULT_POLICIES).sort(), "same agent set");
175
- eq(allowed.size, DEFAULT_ALLOWED_PRIMARIES.size, "no extra primaries added");
176
- });
177
- console.log(`\n[smoke-test] ${passed} passed, ${failed} failed\n`);
178
- process.exit(failed === 0 ? 0 : 1);