leerness 1.9.287 → 1.9.288
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/CHANGELOG.md +19 -0
- package/README.md +6 -6
- package/bin/harness.js +56 -18
- package/package.json +1 -1
- package/scripts/e2e.js +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.288 — 2026-06-03 — Codex gpt-5.5 코드 리뷰 수렴: MCP policy enforce + release dry-run + 도구수 정합
|
|
4
|
+
|
|
5
|
+
**🔒 Codex(gpt-5.5, xhigh)가 실제 코드 라인 근거로 제시한 5건 중 검증된 high/med 3건 수렴 + 나머지 2건 백로그.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
codex CLI(gpt-5.5)로 leerness@1.9.287 을 직접 리뷰(샌드박스 우회로 파일 접근, 285k 토큰). 5건 모두 코드 라인 근거의 타당한 지적. 이번 라운드에 검증·테스트 가능한 3건 반영.
|
|
9
|
+
|
|
10
|
+
### 구현 (Codex 지적 → 수렴)
|
|
11
|
+
1. **#1 (high) MCP policy enforce 우회** — `_policyEnforce` 가 `agents multi --execute` 한 곳뿐이라 MCP `state_start` 등 write 도구가 정책을 우회. → MCP `tools/call` 의 `callLeerness` 직전에 **중앙 정책 게이트** 추가. enforce ON 시 허용 등급 초과 도구는 JSON-RPC `isError` 로 차단(실행 안 함). read-only enforce 에서 MCP state_start 차단 + state.json 미생성 실측.
|
|
12
|
+
2. **#2 (high) `release publish --dry-run` 이 실제 push + 실패도 성공 처리** — dry-run 인데 npm pack/git push 실행, status 미검사로 실패 시에도 exit 0. → dry-run 은 모든 외부 side effect(pack/push/gh/pages)를 **계획 출력만**, live 는 git push status 실패 시 **non-zero 종료**.
|
|
13
|
+
3. **#5 (med) 도구 수 불일치** — 배지 80 / 관리블록 42 / capability 카운트 자기-매칭 80 vs 실제 tools/list 79. → `_mcpToolCount()` 단일 출처(정확한 도구 정의 패턴)로 배지·관리블록·capability·CHANGELOG 카운트 통일. syncReadme 가 MCP 배지 자동 동기화(stale 방지).
|
|
14
|
+
|
|
15
|
+
### 백로그 (Codex #3/#4)
|
|
16
|
+
- **UR-0036** (#3): REPL agy/copilot prompt 가 shell:true args 전달(셸 주입 위험) → stdin/shell-safe 통일.
|
|
17
|
+
- **UR-0037** (#4): require.main 가드에도 top-level side effect(warning listener/NODE_OPTIONS/chcp) 가 require 시 실행 → main() 내부 이동.
|
|
18
|
+
|
|
19
|
+
### 검증
|
|
20
|
+
- **selftest 36/36 PASS** · **E2E 233/233 PASS** (회귀 0) · MCP 정책 차단(write)/허용(enforce off) + release dry-run 무push exit 0 + 배지==tools/list(79) 실측.
|
|
21
|
+
|
|
3
22
|
## 1.9.287 — 2026-06-03 — Codex 외부 리뷰 수렴: 허위 완료 차단 강화 + handoff 펜스 + status minimal
|
|
4
23
|
|
|
5
24
|
**🛡️ 다른 AI(Codex)가 실측한 핵심 한계 "테스트만 통과하면 미구현도 done 통과"를 보강 + 발견된 품질 버그 2건 수정.**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.288 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### Core Commands
|
|
477
477
|
|
|
@@ -513,7 +513,7 @@ leerness memory restore decision <date|title>
|
|
|
513
513
|
|
|
514
514
|
### MCP server (외부 AI 통합)
|
|
515
515
|
|
|
516
|
-
Leerness v1.9.
|
|
516
|
+
Leerness v1.9.288는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **79개 도구**를 노출:
|
|
517
517
|
|
|
518
518
|
```jsonc
|
|
519
519
|
// 카테고리별
|
|
@@ -526,7 +526,7 @@ Leerness v1.9.287는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
526
526
|
// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new
|
|
527
527
|
|
|
528
528
|
// MCP server 실행: leerness mcp serve
|
|
529
|
-
// tools/list 응답:
|
|
529
|
+
// tools/list 응답: 79 도구
|
|
530
530
|
```
|
|
531
531
|
|
|
532
532
|
### Autonomous mode (자율 모드)
|
|
@@ -534,7 +534,7 @@ Leerness v1.9.287는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
534
534
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
535
535
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
536
536
|
|
|
537
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
537
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.288)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
538
538
|
|
|
539
539
|
### 성능 가이드 (1.9.140 측정)
|
|
540
540
|
|
|
@@ -572,6 +572,6 @@ leerness release pack --close --auto-main-push
|
|
|
572
572
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
573
573
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
574
574
|
|
|
575
|
-
Last synced by Leerness v1.9.
|
|
575
|
+
Last synced by Leerness v1.9.288: 2026-06-03
|
|
576
576
|
<!-- leerness:project-readme:end -->
|
|
577
577
|
|
package/bin/harness.js
CHANGED
|
@@ -10,7 +10,7 @@ const readline = require('readline');
|
|
|
10
10
|
const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
11
11
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
|
|
12
12
|
|
|
13
|
-
const VERSION = '1.9.
|
|
13
|
+
const VERSION = '1.9.288';
|
|
14
14
|
|
|
15
15
|
// 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
|
|
16
16
|
// leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
|
|
@@ -152,6 +152,11 @@ function fail(s) { log('✗ ' + s); }
|
|
|
152
152
|
// 1.9.287 (Codex 리뷰 수렴): 임베딩 텍스트의 코드펜스(```)를 중립화해 외부 마크다운(session-handoff)이 깨지지 않게.
|
|
153
153
|
// ``` → ''' (펜스가 아닌 표시), 인라인 백틱은 보존. 순수 함수.
|
|
154
154
|
function _sanitizeFences(s) { return String(s || '').replace(/```+/g, "'''"); }
|
|
155
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #5 수렴): MCP 도구 수 단일 출처 — tools 정의 패턴만 카운트(설명/자기-매칭 오탐 제외).
|
|
156
|
+
// 배지/관리블록/capability 가 모두 이 값을 써서 tools/list 실제 수와 일치.
|
|
157
|
+
function _mcpToolCount() {
|
|
158
|
+
try { return (read(__filename).match(/\{ name: 'leerness_[a-z_]+', description:/g) || []).length; } catch { return 0; }
|
|
159
|
+
}
|
|
155
160
|
function absRoot(p) { return path.resolve(p || process.cwd()); }
|
|
156
161
|
function exists(p) { return fs.existsSync(p); }
|
|
157
162
|
function read(p) {
|
|
@@ -317,7 +322,7 @@ function managedReadmeBlock(project) {
|
|
|
317
322
|
'',
|
|
318
323
|
'### MCP server (외부 AI 통합)',
|
|
319
324
|
'',
|
|
320
|
-
`Leerness v${VERSION}는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에
|
|
325
|
+
`Leerness v${VERSION}는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **${_mcpToolCount()}개 도구**를 노출:`,
|
|
321
326
|
'',
|
|
322
327
|
'```jsonc',
|
|
323
328
|
'// 카테고리별',
|
|
@@ -330,7 +335,7 @@ function managedReadmeBlock(project) {
|
|
|
330
335
|
'// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new',
|
|
331
336
|
'',
|
|
332
337
|
'// MCP server 실행: leerness mcp serve',
|
|
333
|
-
|
|
338
|
+
`// tools/list 응답: ${_mcpToolCount()} 도구`,
|
|
334
339
|
'```',
|
|
335
340
|
'',
|
|
336
341
|
'### Autonomous mode (자율 모드)',
|
|
@@ -813,6 +818,11 @@ function syncReadme(root) {
|
|
|
813
818
|
const stCount = _selfTestCases().length;
|
|
814
819
|
if (stCount > 0) updated = updated.replace(/badge\/selftest-\d+(?:%2F\d+)?/g, `badge/selftest-${stCount}`);
|
|
815
820
|
} catch {}
|
|
821
|
+
// 1.9.288 (Codex #5): MCP 도구 배지 자동 동기화 — tools/list 실제 수와 일치 (badge 80↔실제 79 불일치 해소).
|
|
822
|
+
try {
|
|
823
|
+
const mcpN = _mcpToolCount();
|
|
824
|
+
if (mcpN > 0) updated = updated.replace(/badge\/MCP--tools-\d+/g, `badge/MCP--tools-${mcpN}`);
|
|
825
|
+
} catch {}
|
|
816
826
|
} catch {}
|
|
817
827
|
if (updated !== existing) writeUtf8(p, updated);
|
|
818
828
|
ok('README.md Leerness section synced');
|
|
@@ -2972,6 +2982,7 @@ function _selfTestCases() {
|
|
|
2972
2982
|
{ name: 'CAPABILITY_SURFACE: 6 영역 + risk/optOut + 주의명령 (1.9.272)', run: () => { const ks = Object.keys(CAPABILITY_SURFACE); return ks.length === 6 && ks.includes('automationBridges') && Object.values(CAPABILITY_SURFACE).every(v => ['low','medium','high'].includes(v.risk) && v.optOut && v.desc) && POWERFUL_COMMANDS.length >= 5; } },
|
|
2973
2983
|
{ name: '_resolveNpmTag: latest 기본 + next 허용 + 잘못된 형식 폴백 (1.9.275)', run: () => _resolveNpmTag(null, {}) === 'latest' && _resolveNpmTag('next', {}) === 'next' && _resolveNpmTag(null, { LEERNESS_NPM_TAG: 'next' }) === 'next' && _resolveNpmTag('bad tag!', {}) === 'latest' && _resolveNpmTag('Beta', {}) === 'beta' },
|
|
2974
2984
|
{ name: '_sanitizeFences: 코드펜스 중립화 (Codex 수렴 1.9.287)', run: () => { const out = _sanitizeFences('text\n```js\ncode\n```\nmore'); return !/```/.test(out) && /'''/.test(out) && /code/.test(out); } },
|
|
2985
|
+
{ name: '_mcpToolCount: 실제 도구 정의 수 (자기-매칭 오탐 없음) (Codex #5 1.9.288)', run: () => { const n = _mcpToolCount(); return n >= 75 && n < 200; } },
|
|
2975
2986
|
{ name: '_evidenceQuality: 파일+테스트 근거 강제 (Codex 수렴 1.9.287)', run: () => { const good = _evidenceQuality('src/api.js 수정, npm test 12/12 통과 (Exit: 0)'); const weak = _evidenceQuality('테스트 통과함'); const noFile = _evidenceQuality('12 tests passed'); return good.ok === true && good.hasFile && good.hasTest && weak.ok === false && weak.missing.includes('수정 파일 경로') && noFile.ok === false; } },
|
|
2976
2987
|
{ name: '_parseEvidenceStats: review-evidence pass/fail 집계 (1.9.286)', run: () => { const s = _parseEvidenceStats('## 2026-06-03\nCommand: npm test\nExit: 0\n\n## 2026-06-03\nCommand: build\nExit: 1\n\n## 2026-06-03\nverify PASS 통과\n'); return s.entries === 3 && s.pass === 2 && s.fail === 1 && s.rate === 67; } },
|
|
2977
2988
|
{ name: '_reuseDetect: 키워드→OSS 카테고리 + 체크리스트 (1.9.285)', run: () => { const a = _reuseDetect('JWT 인증 구현'); const b = _reuseDetect('날짜 포맷 date'); const c = _reuseDetect('전혀무관한xyzzy'); return a.some(x => x.key === 'auth') && b.some(x => x.key === 'date') && c.length === 0 && REUSE_CHECKLIST.length >= 5 && REUSE_CATEGORIES.length >= 12; } },
|
|
@@ -14562,20 +14573,31 @@ function releasePublish(root) {
|
|
|
14562
14573
|
if (remote) log(`Git remote (origin): ${remote.host === 'github' ? `${remote.owner}/${remote.repo}` : remote.url}`);
|
|
14563
14574
|
else log('Git remote: 없음');
|
|
14564
14575
|
|
|
14576
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #2 수렴): dry-run 은 모든 외부 side effect 를 계획 출력으로만 + 각 단계 status 실패 시 non-zero.
|
|
14577
|
+
let pubFail = false;
|
|
14565
14578
|
// 2. npm pack (필요한 경우 — pack-only도 의미 있음)
|
|
14566
14579
|
if (has('--pack') || has('--npm-publish') || (!has('--git-push') && !has('--gh-release') && !has('--gh-pages'))) {
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14580
|
+
if (dryRun) { log('(dry-run) npm pack 생략 (실행 안 함)'); }
|
|
14581
|
+
else {
|
|
14582
|
+
const packR = cp.spawnSync('npm', ['pack'], { cwd: root, encoding: 'utf8', shell: true });
|
|
14583
|
+
if (packR.status !== 0) { fail('npm pack 실패'); log(packR.stderr); process.exitCode = 1; return; }
|
|
14584
|
+
ok('npm pack 완료');
|
|
14585
|
+
}
|
|
14570
14586
|
}
|
|
14571
14587
|
|
|
14572
14588
|
// 3. git push (--git-push 또는 --auto + remote 있을 때)
|
|
14573
14589
|
if (has('--git-push') || (has('--auto') && remote)) {
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14590
|
+
if (dryRun) {
|
|
14591
|
+
log('(dry-run) git push / git push --tags 생략 (실행 안 함)');
|
|
14592
|
+
} else {
|
|
14593
|
+
log('git push:');
|
|
14594
|
+
const r1 = cp.spawnSync('git', ['push'], { cwd: root, encoding: 'utf8', shell: true });
|
|
14595
|
+
log((r1.stdout || r1.stderr || '').slice(-200) || '(no output)');
|
|
14596
|
+
if (r1.status !== 0) { fail('git push 실패'); pubFail = true; }
|
|
14597
|
+
const r2 = cp.spawnSync('git', ['push', '--tags'], { cwd: root, encoding: 'utf8', shell: true });
|
|
14598
|
+
log((r2.stdout || r2.stderr || '').slice(-200) || '(no output)');
|
|
14599
|
+
if (r2.status !== 0) { fail('git push --tags 실패'); pubFail = true; }
|
|
14600
|
+
}
|
|
14579
14601
|
}
|
|
14580
14602
|
|
|
14581
14603
|
// 4. GitHub Release (--gh-release, gh CLI 사용)
|
|
@@ -14584,6 +14606,7 @@ function releasePublish(root) {
|
|
|
14584
14606
|
else {
|
|
14585
14607
|
const v = getCurrentVersion(root);
|
|
14586
14608
|
if (!v) { warn('--gh-release: package.json#version 없음 — 스킵'); }
|
|
14609
|
+
else if (dryRun) { log(`(dry-run) gh release create v${v} 생략 (실행 안 함)`); }
|
|
14587
14610
|
else {
|
|
14588
14611
|
const tag = `v${v}`;
|
|
14589
14612
|
const ghArgs = ['release', 'create', tag, '--generate-notes', '--title', `${remote.repo} ${tag}`];
|
|
@@ -14600,8 +14623,11 @@ function releasePublish(root) {
|
|
|
14600
14623
|
|
|
14601
14624
|
// 5. gh-pages 배포 (--gh-pages)
|
|
14602
14625
|
if (has('--gh-pages')) {
|
|
14603
|
-
|
|
14604
|
-
|
|
14626
|
+
if (dryRun) { log('(dry-run) gh-pages 배포 생략 (실행 안 함)'); }
|
|
14627
|
+
else {
|
|
14628
|
+
const src = arg('--gh-pages-src', null) || arg('--roadmap', null) || 'roadmap.html';
|
|
14629
|
+
deployGhPages(root, src);
|
|
14630
|
+
}
|
|
14605
14631
|
}
|
|
14606
14632
|
|
|
14607
14633
|
// 6. npm publish (--npm-publish)
|
|
@@ -14614,7 +14640,9 @@ function releasePublish(root) {
|
|
|
14614
14640
|
log((r.stdout || '').split('\n').slice(-5).join('\n'));
|
|
14615
14641
|
if (r.status !== 0) { fail('npm publish 실패'); process.exitCode = 1; return; }
|
|
14616
14642
|
}
|
|
14617
|
-
|
|
14643
|
+
// 1.9.288 (Codex #2): 단계 실패가 있으면 완료를 성공으로 출력하지 않고 non-zero 종료.
|
|
14644
|
+
if (pubFail) { fail('release publish: 일부 단계 실패 (위 로그 확인)'); process.exitCode = 1; return; }
|
|
14645
|
+
ok(`release publish 완료${dryRun ? ' (dry-run — 실제 변경 없음)' : ''}`);
|
|
14618
14646
|
}
|
|
14619
14647
|
|
|
14620
14648
|
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
@@ -16887,6 +16915,15 @@ function mcpServeCmd(root) {
|
|
|
16887
16915
|
default:
|
|
16888
16916
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
16889
16917
|
}
|
|
16918
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
|
|
16919
|
+
// 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
|
|
16920
|
+
// cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
|
|
16921
|
+
try {
|
|
16922
|
+
const pol = _policyEnforce(targetPath, cliArgs.join(' '));
|
|
16923
|
+
if (!pol.allowed) {
|
|
16924
|
+
return send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `정책 차단(policy): ${pol.reason}` }], isError: true } });
|
|
16925
|
+
}
|
|
16926
|
+
} catch {}
|
|
16890
16927
|
const r = callLeerness(cliArgs);
|
|
16891
16928
|
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
16892
16929
|
const fullText = r.stdout || r.stderr || '(no output)';
|
|
@@ -19976,9 +20013,8 @@ function healthCmd(root) {
|
|
|
19976
20013
|
cap.replMultiProvider = (hasRepl && hasCliChat)
|
|
19977
20014
|
? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
|
|
19978
20015
|
: { score: 30, status: '⚠', evidence: 'REPL 미완성' };
|
|
19979
|
-
// (5) MCP 도구 — tools array 카운트
|
|
19980
|
-
const
|
|
19981
|
-
const toolCount = toolsMatch ? toolsMatch.length : 0;
|
|
20016
|
+
// (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
|
|
20017
|
+
const toolCount = _mcpToolCount();
|
|
19982
20018
|
cap.mcpTools = toolCount >= 50
|
|
19983
20019
|
? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
|
|
19984
20020
|
: { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
|
|
@@ -21611,5 +21647,7 @@ module.exports = {
|
|
|
21611
21647
|
// 1.9.286: 스킬 영향 상관추적 (UR-0024) — 단위 테스트
|
|
21612
21648
|
_parseEvidenceStats, skillImpactCmd,
|
|
21613
21649
|
// 1.9.287: evidence 완전성 + 코드펜스 sanitize (Codex 리뷰 수렴) — 단위 테스트
|
|
21614
|
-
_evidenceQuality, _sanitizeFences
|
|
21650
|
+
_evidenceQuality, _sanitizeFences,
|
|
21651
|
+
// 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
|
|
21652
|
+
_mcpToolCount
|
|
21615
21653
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3042,5 +3042,42 @@ total++;
|
|
|
3042
3042
|
if (!ok) { failed++; console.log((rFake.stdout || '').slice(-300)); }
|
|
3043
3043
|
}
|
|
3044
3044
|
|
|
3045
|
+
// 1.9.288 회귀 (Codex gpt-5.5 리뷰 수렴): MCP policy enforce + release dry-run + 도구수 정합
|
|
3046
|
+
total++;
|
|
3047
|
+
{
|
|
3048
|
+
const env = { ...process.env, LEERNESS_OFFLINE: '1' };
|
|
3049
|
+
// #1 MCP policy: read-only enforce → state_start(write) 차단 + state.json 미생성
|
|
3050
|
+
const pDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mcppol-'));
|
|
3051
|
+
cp.spawnSync(process.execPath, [CLI, 'init', pDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000, env });
|
|
3052
|
+
cp.spawnSync(process.execPath, [CLI, 'policy', 'set', 'read-only', '--enforce', '--path', pDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3053
|
+
const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_state_start', arguments: { path: pDir, goal: 'x' } } });
|
|
3054
|
+
const rMcp = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: req + '\n', env });
|
|
3055
|
+
let blocked = false;
|
|
3056
|
+
try { const j = JSON.parse(rMcp.stdout.split('\n').filter(Boolean)[0]); blocked = j.result.isError === true && /정책 차단/.test(j.result.content[0].text); } catch {}
|
|
3057
|
+
const noWrite = !fs.existsSync(path.join(pDir, '.leerness', 'state.json'));
|
|
3058
|
+
// enforce 해제 시 통과(회귀 방지 — 정상 동작 보존)
|
|
3059
|
+
cp.spawnSync(process.execPath, [CLI, 'policy', 'set', 'project-write', '--no-enforce', '--path', pDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3060
|
+
const rMcp2 = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: req + '\n', env });
|
|
3061
|
+
let allowed = false;
|
|
3062
|
+
try { const j = JSON.parse(rMcp2.stdout.split('\n').filter(Boolean)[0]); allowed = j.result.isError !== true && /run-0001/.test(j.result.content[0].text); } catch {}
|
|
3063
|
+
// #2 release publish --dry-run --git-push: push 시도 안 함(생략 출력) + exit 0
|
|
3064
|
+
const rDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-reldry-'));
|
|
3065
|
+
fs.writeFileSync(path.join(rDir, 'package.json'), JSON.stringify({ name: 'x', version: '0.0.1' }) + '\n');
|
|
3066
|
+
const rRel = cp.spawnSync(process.execPath, [CLI, 'release', 'publish', rDir, '--dry-run', '--git-push'], { encoding: 'utf8', timeout: 20000, env });
|
|
3067
|
+
const dryOk = rRel.status === 0 && /\(dry-run\)/.test(rRel.stdout) && /생략/.test(rRel.stdout) && !/git push:/.test(rRel.stdout);
|
|
3068
|
+
// #5 도구수 정합: 배지 == 관리블록 == tools/list
|
|
3069
|
+
let countOk = false;
|
|
3070
|
+
try {
|
|
3071
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 10000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n', env });
|
|
3072
|
+
const live = JSON.parse(rl.stdout.split('\n').filter(Boolean)[0]).result.tools.length;
|
|
3073
|
+
const readme = fs.readFileSync(path.resolve(__dirname, '..', 'README.md'), 'utf8');
|
|
3074
|
+
const badge = (readme.match(/MCP--tools-(\d+)/) || [])[1];
|
|
3075
|
+
countOk = String(live) === badge;
|
|
3076
|
+
} catch {}
|
|
3077
|
+
const ok = blocked && noWrite && allowed && dryOk && countOk;
|
|
3078
|
+
console.log(ok ? '✓ B(1.9.288) Codex 수렴: MCP policy 차단/허용 + release dry-run 무push + 도구수 정합' : `✗ Codex 수렴 실패 (blocked=${blocked} noWrite=${noWrite} allowed=${allowed} dry=${dryOk} count=${countOk})`);
|
|
3079
|
+
if (!ok) { failed++; console.log((rRel.stdout || '').slice(0, 300)); }
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3045
3082
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3046
3083
|
if (failed > 0) process.exit(1);
|