leerness 1.9.286 → 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 +38 -0
- package/README.md +6 -6
- package/bin/harness.js +108 -25
- package/package.json +1 -1
- package/scripts/e2e.js +72 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
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
|
+
|
|
22
|
+
## 1.9.287 — 2026-06-03 — Codex 외부 리뷰 수렴: 허위 완료 차단 강화 + handoff 펜스 + status minimal
|
|
23
|
+
|
|
24
|
+
**🛡️ 다른 AI(Codex)가 실측한 핵심 한계 "테스트만 통과하면 미구현도 done 통과"를 보강 + 발견된 품질 버그 2건 수정.**
|
|
25
|
+
|
|
26
|
+
### 배경 (Codex 직접 테스트 결과 수렴)
|
|
27
|
+
Codex 가 별도 프로젝트에 설치해 평가: 메모리/인계/추적은 쓸 만하나, (1) **허위 완료 태스크가 `verify-claim --strict-claims`/`gate` 를 통과**(테스트 통과 = 구현 인정 아님), (2) `session-handoff.md` 가 evidence 코드펜스를 끌어와 **마크다운 깨짐**, (3) `--minimal` 설치인데 `status` 가 전체 기준 누락 파일을 경고(UX 혼란). 타당한 지적을 수렴.
|
|
28
|
+
|
|
29
|
+
### 구현
|
|
30
|
+
1. **`verify-claim --require-evidence`** (허위 완료 갭) — `_evidenceQuality()` 로 done 주장의 evidence 완전성 강제: **수정 파일 경로 + 테스트명/개수** 가 없으면 FAIL (테스트 통과만으로 불충분). Codex 의 명시 권고("evidence 에 수정 파일/테스트명/개수/로그 강제") 반영. + **정직한 한계 고지**("테스트 통과 ≠ 의미적 구현 정확성").
|
|
31
|
+
2. **handoff 코드펜스 sanitize** — `_sanitizeFences()`: session-handoff 임베딩 시 inner ``` → `'''` 중립화 → wrapper 펜스 균형 유지, 마크다운 안 깨짐.
|
|
32
|
+
3. **`status` minimal 인지** — `manifest.json` 에 `minimal` 플래그 기록 + `status` 가 minimal 설치 시 생략 파일을 missing 으로 경고 안 함 ("minimal set" 표기).
|
|
33
|
+
4. selftest 33→35 (evidence 완전성 + 펜스 sanitize) + e2e 231→232.
|
|
34
|
+
|
|
35
|
+
### 검증
|
|
36
|
+
- **selftest 35/35 PASS** · **E2E 232/232 PASS** (회귀 0) · 허위("테스트 통과함")=exit 1 차단 / 완전(파일+테스트+실제파일)=exit 0 통과 / 펜스 균형 / status "minimal set" 실측.
|
|
37
|
+
|
|
38
|
+
### Codex 평가 중 인지 (즉시 조치 외)
|
|
39
|
+
- 단일 대형 파일 → UR-0025 진행 중(lib/ 14종 추출). 잦은 릴리스 → dist-tag(1.9.275). e2e 시간 → handoff 회귀 수정(1.9.284) + test:fast.
|
|
40
|
+
|
|
3
41
|
## 1.9.286 — 2026-06-03 — UR-0024: 스킬 설치 영향 경량 상관추적 + HuggingFace 안내 (백로그 완전 소진)
|
|
4
42
|
|
|
5
43
|
**📊 "설치 스킬이 코딩 성능/정확도에 도움?" — 무거운 벤치 없이 기존 데이터로 정직한 상관 advisory (GPT-5.5 / 사용자 요청). 마지막 추적 백로그.**
|
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.286는 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.286는 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 처리).
|
|
@@ -149,6 +149,14 @@ function log(s = '') { console.log(s); }
|
|
|
149
149
|
function ok(s) { log('✓ ' + s); }
|
|
150
150
|
function warn(s) { log('⚠ ' + s); }
|
|
151
151
|
function fail(s) { log('✗ ' + s); }
|
|
152
|
+
// 1.9.287 (Codex 리뷰 수렴): 임베딩 텍스트의 코드펜스(```)를 중립화해 외부 마크다운(session-handoff)이 깨지지 않게.
|
|
153
|
+
// ``` → ''' (펜스가 아닌 표시), 인라인 백틱은 보존. 순수 함수.
|
|
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
|
+
}
|
|
152
160
|
function absRoot(p) { return path.resolve(p || process.cwd()); }
|
|
153
161
|
function exists(p) { return fs.existsSync(p); }
|
|
154
162
|
function read(p) {
|
|
@@ -314,7 +322,7 @@ function managedReadmeBlock(project) {
|
|
|
314
322
|
'',
|
|
315
323
|
'### MCP server (외부 AI 통합)',
|
|
316
324
|
'',
|
|
317
|
-
`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()}개 도구**를 노출:`,
|
|
318
326
|
'',
|
|
319
327
|
'```jsonc',
|
|
320
328
|
'// 카테고리별',
|
|
@@ -327,7 +335,7 @@ function managedReadmeBlock(project) {
|
|
|
327
335
|
'// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new',
|
|
328
336
|
'',
|
|
329
337
|
'// MCP server 실행: leerness mcp serve',
|
|
330
|
-
|
|
338
|
+
`// tools/list 응답: ${_mcpToolCount()} 도구`,
|
|
331
339
|
'```',
|
|
332
340
|
'',
|
|
333
341
|
'### Autonomous mode (자율 모드)',
|
|
@@ -415,7 +423,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
415
423
|
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
|
|
416
424
|
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
417
425
|
'.harness/LANGUAGE': lang + '\n',
|
|
418
|
-
'.harness/manifest.json': JSON.stringify({ project, leernessVersion: VERSION, language: lang, installedAt: now() }, null, 2) + '\n',
|
|
426
|
+
'.harness/manifest.json': JSON.stringify({ project, leernessVersion: VERSION, language: lang, installedAt: now(), minimal: !!opts.minimal }, null, 2) + '\n',
|
|
419
427
|
'.harness/skills-lock.json': skillLock(selectedSkills),
|
|
420
428
|
'.harness/project-brief.md': fm('project-brief', ['프로젝트 목적 확인','신규 기능 판단','계획 수립'], ['프로젝트 목적 변경','사용자/범위 변경'], `# Project Brief\n\n## Project\n${project}\n\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n\n## Users\n-\n\n## Success Criteria\n-\n`),
|
|
421
429
|
'.harness/plan.md': fm('plan', ['작업 시작 전','새 요청 접수','범위 변경','신규 프로젝트 감지'], ['계획 추가/수정/드랍','milestone 변경','목표 변경'], `# Plan\n\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n\n## Scope\n- 포함 범위를 기록합니다.\n\n## Out of Scope / Dropped\n| ID | Item | Reason | Date |\n|---|---|---|---|\n\n## Milestones\n\n### M-0001. 프로젝트 계획 정리\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] project-brief.md를 실제 프로젝트 목적에 맞게 작성\n- [ ] context-map.md를 실제 파일 구조에 맞게 작성\n`),
|
|
@@ -805,6 +813,16 @@ function syncReadme(root) {
|
|
|
805
813
|
updated = updated.replace(/badge\/e2e-(\d+)%2F(\d+)-success/g, `badge/e2e-${total}%2F${total}-success`);
|
|
806
814
|
}
|
|
807
815
|
}
|
|
816
|
+
// 1.9.287: selftest 배지 자동 동기화 — _selfTestCases() 실제 케이스 수 직접 카운트(정확). 배지 stale 방지(GPT 지적).
|
|
817
|
+
try {
|
|
818
|
+
const stCount = _selfTestCases().length;
|
|
819
|
+
if (stCount > 0) updated = updated.replace(/badge\/selftest-\d+(?:%2F\d+)?/g, `badge/selftest-${stCount}`);
|
|
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 {}
|
|
808
826
|
} catch {}
|
|
809
827
|
if (updated !== existing) writeUtf8(p, updated);
|
|
810
828
|
ok('README.md Leerness section synced');
|
|
@@ -2963,6 +2981,9 @@ function _selfTestCases() {
|
|
|
2963
2981
|
{ name: '_pickModel: top/code/fast 등급 선택 (1.9.270)', run: () => { return _pickModel('codex', 'code') === 'gpt-5-codex' && _pickModel('claude', 'top') === 'claude-opus-4-7' && /haiku/.test(_pickModel('claude', 'fast')); } },
|
|
2964
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; } },
|
|
2965
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' },
|
|
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; } },
|
|
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; } },
|
|
2966
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; } },
|
|
2967
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; } },
|
|
2968
2989
|
{ name: 'AGENTS.md: 정적 vs 동적 leerness 역할 경계 (1.9.282)', run: () => { const a = coreFiles('.', 'ko', [])['AGENTS.md']; return /정적 vs 동적/.test(a) && /대체하지 않고 \*\*보완\*\*|대체하지 않고 보완/.test(a) && /leerness state/.test(a) && /\.leerness\//.test(a); } },
|
|
@@ -3614,7 +3635,7 @@ function commandsCmd(root) {
|
|
|
3614
3635
|
{ cmd: 'scan secrets [path]', desc: '시크릿 탐지' },
|
|
3615
3636
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
3616
3637
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
3617
|
-
{ cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims]', desc: '주장 검증 (1.9.18~26)' },
|
|
3638
|
+
{ cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287)' },
|
|
3618
3639
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
3619
3640
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
3620
3641
|
{ cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
|
|
@@ -6793,12 +6814,15 @@ function status(root) {
|
|
|
6793
6814
|
const verF = path.join(root,'.harness/HARNESS_VERSION');
|
|
6794
6815
|
const ver = exists(verF) ? read(verF).trim() : 'not installed';
|
|
6795
6816
|
const lang = exists(path.join(root,'.harness/LANGUAGE')) ? read(path.join(root,'.harness/LANGUAGE')).trim() : 'ko';
|
|
6796
|
-
|
|
6817
|
+
// 1.9.287 (Codex 리뷰 수렴): --minimal 설치는 manifest.minimal=true → 의도적으로 생략된 파일을 missing 으로 경고하지 않음.
|
|
6818
|
+
let isMinimal = false;
|
|
6819
|
+
try { const mf = path.join(root, '.harness/manifest.json'); if (exists(mf)) isMinimal = !!JSON.parse(read(mf)).minimal; } catch {}
|
|
6820
|
+
const files = Object.keys(coreFiles(root, lang, [], { minimal: isMinimal }));
|
|
6797
6821
|
const missing = files.filter(f => !exists(path.join(root,f)));
|
|
6798
|
-
log(`Leerness: ${ver}`);
|
|
6822
|
+
log(`Leerness: ${ver}${isMinimal ? ' (minimal)' : ''}`);
|
|
6799
6823
|
log(`Files: ${files.length - missing.length}/${files.length}`);
|
|
6800
6824
|
if (missing.length) missing.forEach(x => warn('missing: ' + x));
|
|
6801
|
-
else ok(
|
|
6825
|
+
else ok(`required files present${isMinimal ? ' (minimal set)' : ''}`);
|
|
6802
6826
|
}
|
|
6803
6827
|
function verify(root) {
|
|
6804
6828
|
root = absRoot(root);
|
|
@@ -9243,6 +9267,19 @@ function reuseMapCmd(root) {
|
|
|
9243
9267
|
|
|
9244
9268
|
// 1.9.18: verify-claim — progress-tracker의 evidence 컬럼 자동 검증
|
|
9245
9269
|
// "src/foo.js + 5개 테스트 (54/54 통과)" 같은 주장을 파싱해 실제 파일/카운트 확인
|
|
9270
|
+
// 1.9.287 (Codex 리뷰 수렴): evidence 완전성 평가 — "테스트 통과만으로 done" 차단.
|
|
9271
|
+
// done 주장은 (a) 수정 파일 경로 (b) 테스트명/개수 가 evidence 에 있어야 신뢰 가능. 순수 함수(selftest).
|
|
9272
|
+
function _evidenceQuality(evidence) {
|
|
9273
|
+
const e = String(evidence || '');
|
|
9274
|
+
const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|sh)\b/i.test(e);
|
|
9275
|
+
const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
|
|
9276
|
+
const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
|
|
9277
|
+
const missing = [];
|
|
9278
|
+
if (!hasFile) missing.push('수정 파일 경로');
|
|
9279
|
+
if (!hasTest) missing.push('테스트명/개수');
|
|
9280
|
+
if (!hasLog) missing.push('실행 로그(Command/Exit)');
|
|
9281
|
+
return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
|
|
9282
|
+
}
|
|
9246
9283
|
function verifyClaimCmd(root, taskId) {
|
|
9247
9284
|
root = absRoot(root);
|
|
9248
9285
|
if (!taskId) return fail('verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
|
|
@@ -9434,7 +9471,23 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9434
9471
|
for (const s of optimismSuspects) log(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`);
|
|
9435
9472
|
}
|
|
9436
9473
|
}
|
|
9437
|
-
|
|
9474
|
+
// 1.9.287 (Codex 리뷰 수렴): --require-evidence — done 주장의 evidence 완전성(파일+테스트) 강제.
|
|
9475
|
+
// "테스트 통과만으로 done" 차단 — Codex 가 발견한 허위 완료 통과 갭 보강.
|
|
9476
|
+
const reqEvidence = has('--require-evidence');
|
|
9477
|
+
const isDoneClaim = /done|완료/i.test(row.status || '');
|
|
9478
|
+
let evidenceQualityOk = true;
|
|
9479
|
+
if (reqEvidence) {
|
|
9480
|
+
const evq = _evidenceQuality(evidence);
|
|
9481
|
+
evidenceQualityOk = !isDoneClaim || evq.ok;
|
|
9482
|
+
log(` - evidence 완전성 (--require-evidence): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
|
|
9483
|
+
if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분)`);
|
|
9484
|
+
}
|
|
9485
|
+
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (has('--strict-claims') && !strictOk) || !evidenceQualityOk;
|
|
9486
|
+
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
9487
|
+
if (has('--strict-claims') || reqEvidence) {
|
|
9488
|
+
log('');
|
|
9489
|
+
log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
|
|
9490
|
+
}
|
|
9438
9491
|
if (overallFail) {
|
|
9439
9492
|
log('');
|
|
9440
9493
|
log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
|
|
@@ -11997,7 +12050,8 @@ function sessionClose(root, opts = {}) {
|
|
|
11997
12050
|
return arr.map(r => `- ${r.id} ${r.request} → next: ${r.nextAction}`).join('\n');
|
|
11998
12051
|
}
|
|
11999
12052
|
|
|
12000
|
-
|
|
12053
|
+
// 1.9.287 (Codex 리뷰 수렴): evidence 임베딩 시 코드펜스(```) 가 session-handoff.md 마크다운을 깨뜨리는 품질 버그 수정.
|
|
12054
|
+
const evidenceSummary = _sanitizeFences(exists(evidencePath(root)) ? (read(evidencePath(root)).split('\n').slice(-30).join('\n')) : '(no review-evidence.md)');
|
|
12001
12055
|
const block = [
|
|
12002
12056
|
`# Session Handoff`,
|
|
12003
12057
|
``,
|
|
@@ -14519,20 +14573,31 @@ function releasePublish(root) {
|
|
|
14519
14573
|
if (remote) log(`Git remote (origin): ${remote.host === 'github' ? `${remote.owner}/${remote.repo}` : remote.url}`);
|
|
14520
14574
|
else log('Git remote: 없음');
|
|
14521
14575
|
|
|
14576
|
+
// 1.9.288 (Codex gpt-5.5 리뷰 #2 수렴): dry-run 은 모든 외부 side effect 를 계획 출력으로만 + 각 단계 status 실패 시 non-zero.
|
|
14577
|
+
let pubFail = false;
|
|
14522
14578
|
// 2. npm pack (필요한 경우 — pack-only도 의미 있음)
|
|
14523
14579
|
if (has('--pack') || has('--npm-publish') || (!has('--git-push') && !has('--gh-release') && !has('--gh-pages'))) {
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
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
|
+
}
|
|
14527
14586
|
}
|
|
14528
14587
|
|
|
14529
14588
|
// 3. git push (--git-push 또는 --auto + remote 있을 때)
|
|
14530
14589
|
if (has('--git-push') || (has('--auto') && remote)) {
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
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
|
+
}
|
|
14536
14601
|
}
|
|
14537
14602
|
|
|
14538
14603
|
// 4. GitHub Release (--gh-release, gh CLI 사용)
|
|
@@ -14541,6 +14606,7 @@ function releasePublish(root) {
|
|
|
14541
14606
|
else {
|
|
14542
14607
|
const v = getCurrentVersion(root);
|
|
14543
14608
|
if (!v) { warn('--gh-release: package.json#version 없음 — 스킵'); }
|
|
14609
|
+
else if (dryRun) { log(`(dry-run) gh release create v${v} 생략 (실행 안 함)`); }
|
|
14544
14610
|
else {
|
|
14545
14611
|
const tag = `v${v}`;
|
|
14546
14612
|
const ghArgs = ['release', 'create', tag, '--generate-notes', '--title', `${remote.repo} ${tag}`];
|
|
@@ -14557,8 +14623,11 @@ function releasePublish(root) {
|
|
|
14557
14623
|
|
|
14558
14624
|
// 5. gh-pages 배포 (--gh-pages)
|
|
14559
14625
|
if (has('--gh-pages')) {
|
|
14560
|
-
|
|
14561
|
-
|
|
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
|
+
}
|
|
14562
14631
|
}
|
|
14563
14632
|
|
|
14564
14633
|
// 6. npm publish (--npm-publish)
|
|
@@ -14571,7 +14640,9 @@ function releasePublish(root) {
|
|
|
14571
14640
|
log((r.stdout || '').split('\n').slice(-5).join('\n'));
|
|
14572
14641
|
if (r.status !== 0) { fail('npm publish 실패'); process.exitCode = 1; return; }
|
|
14573
14642
|
}
|
|
14574
|
-
|
|
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 — 실제 변경 없음)' : ''}`);
|
|
14575
14646
|
}
|
|
14576
14647
|
|
|
14577
14648
|
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
@@ -16844,6 +16915,15 @@ function mcpServeCmd(root) {
|
|
|
16844
16915
|
default:
|
|
16845
16916
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
16846
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 {}
|
|
16847
16927
|
const r = callLeerness(cliArgs);
|
|
16848
16928
|
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
16849
16929
|
const fullText = r.stdout || r.stderr || '(no output)';
|
|
@@ -19933,9 +20013,8 @@ function healthCmd(root) {
|
|
|
19933
20013
|
cap.replMultiProvider = (hasRepl && hasCliChat)
|
|
19934
20014
|
? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
|
|
19935
20015
|
: { score: 30, status: '⚠', evidence: 'REPL 미완성' };
|
|
19936
|
-
// (5) MCP 도구 — tools array 카운트
|
|
19937
|
-
const
|
|
19938
|
-
const toolCount = toolsMatch ? toolsMatch.length : 0;
|
|
20016
|
+
// (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
|
|
20017
|
+
const toolCount = _mcpToolCount();
|
|
19939
20018
|
cap.mcpTools = toolCount >= 50
|
|
19940
20019
|
? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
|
|
19941
20020
|
: { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
|
|
@@ -21566,5 +21645,9 @@ module.exports = {
|
|
|
21566
21645
|
// 1.9.285: 외부 OSS 재사용 게이트 (UR-0023) — 단위 테스트
|
|
21567
21646
|
REUSE_CATEGORIES, REUSE_CHECKLIST, _reuseDetect, reuseCheckCmd,
|
|
21568
21647
|
// 1.9.286: 스킬 영향 상관추적 (UR-0024) — 단위 테스트
|
|
21569
|
-
_parseEvidenceStats, skillImpactCmd
|
|
21648
|
+
_parseEvidenceStats, skillImpactCmd,
|
|
21649
|
+
// 1.9.287: evidence 완전성 + 코드펜스 sanitize (Codex 리뷰 수렴) — 단위 테스트
|
|
21650
|
+
_evidenceQuality, _sanitizeFences,
|
|
21651
|
+
// 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
|
|
21652
|
+
_mcpToolCount
|
|
21570
21653
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3007,5 +3007,77 @@ total++;
|
|
|
3007
3007
|
if (!ok) failed++;
|
|
3008
3008
|
}
|
|
3009
3009
|
|
|
3010
|
+
// 1.9.287 회귀 (Codex 리뷰 수렴): verify-claim --require-evidence + handoff 펜스 sanitize + status minimal
|
|
3011
|
+
total++;
|
|
3012
|
+
{
|
|
3013
|
+
const cDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-codex-'));
|
|
3014
|
+
const env = { ...process.env, LEERNESS_OFFLINE: '1' };
|
|
3015
|
+
cp.spawnSync(process.execPath, [CLI, 'init', cDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000, env });
|
|
3016
|
+
// A) 허위 완료(테스트 통과만) → --require-evidence FAIL (exit 1)
|
|
3017
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'fake done', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3018
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', '테스트 통과함', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3019
|
+
const rFake = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--require-evidence', '--path', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3020
|
+
const fakeBlocked = rFake.status === 1 && /evidence 완전성.*FAIL/.test(rFake.stdout);
|
|
3021
|
+
// 완전한 evidence(파일+테스트) + 실제 파일 존재 → pass (exit 0)
|
|
3022
|
+
fs.mkdirSync(path.join(cDir, 'src'), { recursive: true });
|
|
3023
|
+
fs.writeFileSync(path.join(cDir, 'src', 'api.js'), 'module.exports = {};\n');
|
|
3024
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--evidence', 'src/api.js 구현, npm test 5/5 통과 (Exit: 0)', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3025
|
+
const rGood = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--require-evidence', '--path', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3026
|
+
const goodPass = rGood.status === 0 && /evidence 완전성.*pass/.test(rGood.stdout);
|
|
3027
|
+
// B) handoff 펜스 sanitize — review-evidence 에 ``` 넣고 session close → session-handoff 에 ``` 불균형 없음
|
|
3028
|
+
fs.appendFileSync(path.join(cDir, '.harness', 'review-evidence.md'), '\n## 2026-06-03\n```js\nconst x=1;\n```\n');
|
|
3029
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3030
|
+
// 펜스 균형: 감싸는 wrapper ``` 는 허용하되, review-evidence 의 inner ``` 는 '''로 sanitize 되어 불균형이 없어야 함.
|
|
3031
|
+
let fenceOk = false;
|
|
3032
|
+
try {
|
|
3033
|
+
const sh = fs.readFileSync(path.join(cDir, '.harness', 'session-handoff.md'), 'utf8');
|
|
3034
|
+
const bareFences = sh.split('\n').filter(l => l.trim() === '`'.repeat(3)).length;
|
|
3035
|
+
fenceOk = (bareFences % 2 === 0) && sh.includes("'''"); // 균형(짝수) + inner sanitize 적용 확인
|
|
3036
|
+
} catch {}
|
|
3037
|
+
// C) status minimal 인지 — minimal set 표기 + missing 경고 없음
|
|
3038
|
+
const rStat = cp.spawnSync(process.execPath, [CLI, 'status', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3039
|
+
const statOk = /minimal/.test(rStat.stdout) && !/missing:/.test(rStat.stdout);
|
|
3040
|
+
const ok = fakeBlocked && goodPass && fenceOk && statOk;
|
|
3041
|
+
console.log(ok ? '✓ B(1.9.287) Codex 수렴: require-evidence(허위차단/완전통과) + 펜스 sanitize + status minimal' : `✗ Codex 수렴 실패 (fake=${fakeBlocked} good=${goodPass} fence=${fenceOk} stat=${statOk})`);
|
|
3042
|
+
if (!ok) { failed++; console.log((rFake.stdout || '').slice(-300)); }
|
|
3043
|
+
}
|
|
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
|
+
|
|
3010
3082
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3011
3083
|
if (failed > 0) process.exit(1);
|