leerness 1.36.79 → 1.36.80
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 +23 -0
- package/README.ko.md +1 -1
- package/README.md +6 -6
- package/bin/leerness.js +63 -6
- package/lib/catalogs.js +2 -9
- package/lib/clarify.js +4 -0
- package/lib/mcp-tools.js +4 -2
- package/lib/preview-serve.js +254 -0
- package/lib/pure-utils.js +11 -2
- package/lib/referee.js +0 -0
- package/lib/review-request.js +43 -1
- package/package.json +1 -1
- package/scripts/e2e.js +120 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.80 — 2026-07-28 — referee 캘리브레이션 · 라이브 미리보기 · 기본 스킬 정리 · 최소성 사다리 — MCP 89 도구 · R-0001 검수 22회전
|
|
4
|
+
|
|
5
|
+
**이 릴리스에 함께 들어간 것** (아래 상세):
|
|
6
|
+
- **라이브 미리보기(사용자 요청)**: `preview serve <P-ID>` 가 임시 워크스페이스 + **0-deps 로컬 서버**로 시안을 띄우고 승인/수정 시 정리(project 모드 배치본 포함). **시그니처 포트 5337**(키패드 "LEER", ollama:11434 방식) + 점유 시 순차 회피 → OS 빈 포트. `preview mode self|project` — A 기본·B 옵트인, 미설정 시 사용자에게 질문하되 흐름은 막지 않음. leerness 는 사용자의 dev 서버를 임의 실행하지 않는다.
|
|
7
|
+
- **기본 제공 스킬 정리(사용자 지시)**: 도메인 7종(commerce-api/office/crawling/firebase/ads-analytics/appstore-review/ai-verified-skill-publisher) 내장 제거 — **설명 4줄짜리 메타데이터**인데 `verification:'passed'` 를 근거 없이 주장했고 특정 사용자 도메인이라 전역 기본값으로 부적절했다. 남은 2종의 verification 은 **`'unverified'`** 로 정직화(selftest 가 'passed' 주장 금지를 영구 단언). **기존 설치분은 파일·락 모두 보존**(비파괴) · 제거된 id 명시 요청 시 조용히 무시하지 않고 고지.
|
|
8
|
+
- **`review-request --ladder` (P-0003, codex 공동판단 ADOPT)**: 순서 있는 최소성 결정 영수증(필요한가→기존코드→stdlib→네이티브→설치된 의존성→조합→최소구현). 옵트인·자문 전용이라 `proceed` 무영향, 근거 없으면 `unknown`, "줄이면 안 되는 것"(검증·보안·접근성·에러처리·데이터손실) 명시. **기각**: 룰 거버넌스/campaign 스냅샷(P-0002 — campaign 개념 부재, 제안/승인 분리는 이미 기본 모델, 모순탐지는 false-BLOCK 위험).
|
|
9
|
+
- **검수 22회전이 High 5 포함 12건 차단**: ① **신규 모듈 2개가 git 미추적** → 커밋 시 CLI 기동 불가(출하 직전 차단) ② 캘리브레이션이 **무관한 실패**(없는 바이너리·SyntaxError·exit 0+신호)로 통과 → 런처 실패 판별 + `--expect-bad` 필수 + check 연결 ③ 캘리브레이션(최대 600s)이 락 대기 10s/stale 30s 를 넘겨 **살아있는 락이 삭제** → 실행은 락 밖, 저장은 락 안 CAS(지문 불일치 시 폐기) ④ 서버 경로 격리 우회(형제 디렉토리 `%2e%2e`·정션·잘못된 인코딩으로 서버 crash) → path.relative + realpath + 400 ⑤ `--static-dir` 정션으로 프로젝트 밖 쓰기 → realpath 검증 ⑥ 신호 매칭이 8000자 절단본 기준 → 전체 출력 기준 ⑦ 스토어 형상 검증 강화 ⑧ project 배치본·멱등 재승인 정리 누락 ⑨ 프로젝트 CSS 의 `</style>` 탈출로 스크립트 주입 → 별도 스타일시트+link ⑩ 제거 스킬 명시 요청 조용히 무시 ⑪ "아무것도 저장 안 함" 과장 → 주장 범위 정정 ⑫ 실패 시 빈 임시 디렉토리 잔존.
|
|
10
|
+
- 검증: 검수 지적 9종 프로브 재현→수정→재확인 실측, selftest 334, e2e 404/404.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
### (원안) 검증기 캘리브레이션 `referee` — P-0001, 외부 사례 검토 채택
|
|
15
|
+
|
|
16
|
+
사용자가 지정한 두 외부 저장소(ponytail · anthropics/code-migration-kit)를 codex 독립 검토 + 4렌즈 병렬 분석으로 평가 → **만장일치 1순위 한 건만** 채택. leerness 정체성("done 은 증거를 요구한다")의 미해결 형제 명제 — **검증기 자신의 증거** — 를 채운다.
|
|
17
|
+
|
|
18
|
+
- **문제**: leerness 의 모든 검사(verify-claim/gate/contract)는 정적·텍스트 대리 지표였다. `lens` 가 "이 테스트가 실제로 실패할 수 있나?"를 *묻지만*, 실행으로 탐지력을 증명하는 장치는 없었다 — 아무것도 검사하지 않는 초록 스위트를 걸러낼 수 없었다.
|
|
19
|
+
- **`leerness referee add|verify|list|show|drop`**: 검증기를 신뢰하기 전에 ① known-good 통과 ② 일부러 망가뜨린 known-bad 거부 ③ **거부 사유가 심어둔 결함과 일치**(`--expect-bad`) ④ 명령/기대치 변경 시 지문 불일치로 **자동 stale**(신뢰 철회). 무관한 실패(환경 오류·실행 불가·타임아웃)는 탐지력으로 인정하지 않는다.
|
|
20
|
+
- **게이팅**: `verify-claim <T-ID> --referee <id>` — 미등록/미검증/stale 검증기는 실행 전에 거부(`referee_not_found`/`referee_stale`), 캘리브레이션된 검증기가 거부하면 `referee_failed`. MCP `leerness_referee`(safe-write) + `leerness_verify_claim` 의 `refereeId`.
|
|
21
|
+
- 적대적 FP 프로브 9종 실측 통과: 기준선 빨간 상태·bad 통과·사유 불일치·실행 불가·명령 변경 stale·미검증 사용·검증기 실패·없는 id·손상 스토어 fail-closed.
|
|
22
|
+
- **정직성**: ponytail 의 "코드 54%↓·비용 20%↓" 등 벤치 수치는 **인용하지 않는다**(모델 1개·저장소 1계열 기반이고 ponytail 자신도 해당 수치가 낡았다고 명시). leerness 가 효율을 주장하려면 품질을 고정한 on/off 대조 실험이 필요하다 — 이번 릴리스는 그런 주장을 하지 않는다.
|
|
23
|
+
- 기각(양 검토 합의): ponytail 명령군·intensity 모드(lens 토글 중복) · LOC 를 게이트로 · 마이그레이션 러너/빌드 데몬(정체성 변질) · **파일 존재=완료**(핵심 불변식 위반) · 실패 후 자동 룰 완화. 룰 스냅샷(P-0002)·최소성 사다리(P-0003)는 승인 대기.
|
|
24
|
+
- 검증: selftest 334(문서 drift 가드가 88→89 불일치를 잡아 README 정합), e2e +1.
|
|
25
|
+
|
|
3
26
|
## 1.36.79 — 2026-07-28 — 실 프로젝트 60개 도그푸딩: P1 5건 (합성 헌트 10회가 못 본 결함)
|
|
4
27
|
|
|
5
28
|
사용자의 **실제 프로젝트 60개**(v0.1.40 ~ 1.36.77)를 5개 렌즈로 병렬 감사(읽기 전용 + temp 복사본 실험). 합성 픽스처 기반 codex 헌트 10라운드가 한 번도 못 본 P1 5건이 나왔다 — 실데이터에서만 발화하는 조건들이었다.
|
package/README.ko.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
> **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다.
|
|
15
15
|
|
|
16
|
-
[](https://www.npmjs.com/package/leerness) · ](https://www.npmjs.com/package/leerness) ·  · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
> **The AI-coding operations layer that makes "done" require evidence — for any language, any AI agent.**
|
|
13
13
|
> leerness does not write code. It gives your AI agent persistent memory, verified completion, and clean handoffs — stored inside your repo as plain files, exposed via CLI + MCP.
|
|
14
14
|
|
|
15
|
-
[](https://www.npmjs.com/package/leerness) · ](https://www.npmjs.com/package/leerness) ·  · **0 runtime deps** · **0 install scripts** · offline-first · Node ≥ 18 · MIT
|
|
16
16
|
|
|
17
17
|
**🇰🇷 한국어 전문: [README.ko.md](./README.ko.md)**
|
|
18
18
|
|
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.80 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.80는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -189,7 +189,7 @@ Leerness v1.36.79는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
189
189
|
// • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new
|
|
190
190
|
|
|
191
191
|
// MCP server 실행: leerness mcp serve
|
|
192
|
-
// tools/list 응답:
|
|
192
|
+
// tools/list 응답: 89 도구
|
|
193
193
|
```
|
|
194
194
|
|
|
195
195
|
### Autonomous mode (자율 모드)
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.79는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.80 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.80: 2026-07-28
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.80';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -532,14 +532,19 @@ function skillLock(skills, root) {
|
|
|
532
532
|
const set = new Set(skills);
|
|
533
533
|
try {
|
|
534
534
|
if (root) {
|
|
535
|
+
// 1.36.80: 내장 카탈로그에서 빠진 스킬(도메인 7종 제거)이라도 **이미 설치돼 있으면** 락에 남긴다 —
|
|
536
|
+
// 디스크 실재를 기록한다는 F12 원칙 유지 + 기존 사용자의 스킬이 락에서 유령처럼 사라지지 않게(비파괴).
|
|
537
|
+
// 판단 근거는 skill.json/SKILL.md 존재(= leerness 가 설치한 흔적) — 임의 디렉토리는 여전히 배제.
|
|
535
538
|
const dir = path.join(root, '.harness', 'skills');
|
|
536
539
|
if (exists(dir)) for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
537
|
-
if (e.isDirectory()
|
|
540
|
+
if (!e.isDirectory()) continue;
|
|
541
|
+
if (skillCatalog[e.name]) { set.add(e.name); continue; }
|
|
542
|
+
if (exists(path.join(dir, e.name, 'skill.json')) || exists(path.join(dir, e.name, 'SKILL.md'))) set.add(e.name);
|
|
538
543
|
}
|
|
539
544
|
}
|
|
540
545
|
} catch {}
|
|
541
546
|
const data = { leernessVersion: VERSION, updatedAt: now(), installedSkills: {} };
|
|
542
|
-
for (const s of [...set].sort()) data.installedSkills[s] = skillCatalog[s] || { version: 'unknown' }; // (검수 #4) 정렬 직렬화 — 동일 멤버십이면 바이트 동일(no-op 보존)
|
|
547
|
+
for (const s of [...set].sort()) data.installedSkills[s] = skillCatalog[s] || { version: 'unknown', source: 'installed', note: 'not in built-in catalog (user-installed or removed from defaults)' }; // (검수 #4) 정렬 직렬화 — 동일 멤버십이면 바이트 동일(no-op 보존)
|
|
543
548
|
return JSON.stringify(data, null, 2) + '\n';
|
|
544
549
|
}
|
|
545
550
|
|
|
@@ -1020,6 +1025,12 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
1020
1025
|
const explicitSkills = arg('--skills', null);
|
|
1021
1026
|
let lang = explicitLang ? detectLanguageValue(root, explicitLang) : detectLanguageValue(root, 'auto');
|
|
1022
1027
|
let skills = explicitSkills ? parseSkillsValue(explicitSkills) : [];
|
|
1028
|
+
// 1.36.80 (검수 #10): 사용자가 **명시한** 스킬 id 가 카탈로그에 없으면 조용히 무시하지 않고 분명히 알린다
|
|
1029
|
+
// (1.36.80 에서 도메인 7종을 내장에서 제거했으므로 구 명령이 조용히 0개 설치로 끝나는 것을 막는다).
|
|
1030
|
+
if (explicitSkills && skills.unknown && skills.unknown.length) {
|
|
1031
|
+
warn(`요청한 스킬 미존재: ${skills.unknown.join(', ')} — 1.36.80 에서 내장 기본 스킬이 정리되었습니다(사용 가능: ${Object.keys(skillCatalog).join(', ') || '(없음)'})`);
|
|
1032
|
+
log(` ⓘ 이미 설치된 스킬은 그대로 유지됩니다. 외부 스킬은 leerness skill install <경로|URL> 로 추가하세요.`);
|
|
1033
|
+
}
|
|
1023
1034
|
const shouldAsk = !has('--yes') && !opts.nonInteractive && process.stdin.isTTY && process.stdout.isTTY && !opts.migration;
|
|
1024
1035
|
// 1.9.269 (UR-0022): 비대화형 auto 설치(npx leerness init --yes 등)에서 OS 시스템 언어가 적용됐으면 투명성 안내.
|
|
1025
1036
|
// --language 명시 시 미발화 (explicitLang). 대화형은 이후 언어 prompt 가 처리.
|
|
@@ -3867,7 +3878,7 @@ function _selfTestCases() {
|
|
|
3867
3878
|
{ name: 'UR-0025 심층: i18n STRINGS catalog→lib/catalogs + _translate→pure-utils 분리 (1.9.338)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.STRINGS && typeof c.STRINGS['common.ready'] === 'object' && c.STRINGS['common.ready'].en === 'Ready'; const work = m._translate(c.STRINGS, 'common.ready', 'en') === 'Ready' && m._translate(c.STRINGS, 'common.ready', 'ko') === '준비 완료' && m._translate(c.STRINGS, 'no.such.key', 'en') === 'no.such.key' && m._translate(null, 'x', 'ko') === 'x' && m._translate({ k: { ko: '케이' } }, 'k', 'en') === '케이'; const src = read(__filename); const moved = STRINGS === c.STRINGS && _translate === m._translate && !/const STRINGS = \{/.test(src); return catOk && work && moved; } },
|
|
3868
3879
|
{ name: 'UR-0053: decisions canonical JSON 레이어(_loadDecisions/_saveDecisions/decisionsJsonPath) + pure 파서/렌더 round-trip (1.9.339)', run: () => { const m = require('../lib/pure-utils'); const md = '# Decisions\n\n### 2026-06-05 — A\n- Decision: a\n- Reason: r\n- Alternatives: alt\n- Impact: imp\n\n### 2026-06-04 — B\n- Decision: b\n- Alternatives:\n'; const objs = m._decisionsFromMd(md); const parseOk = objs.length === 2 && objs[0].alternatives === 'alt' && objs[0].impact === 'imp' && objs[1].alternatives === null && objs[1].title === 'B'; const rt = m._decisionsFromMd(m._renderDecisionsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._decisionsFromMd(m._renderDecisionsMd([])).length === 0; const layerOk = typeof _loadDecisions === 'function' && typeof _saveDecisions === 'function' && typeof decisionsJsonPath === 'function' && _decisionsFromMd === m._decisionsFromMd; return parseOk && rtOk && tplOk && layerOk; } },
|
|
3869
3880
|
{ name: 'UR-0058: lessons canonical JSON 레이어(_loadLessons/_saveLessons/lessonsJsonPath) + pure 파서/렌더 round-trip', run: () => { const m = require('../lib/pure-utils'); const objs = [{ date: '2026-06-05', text: 'A', tag: 't' }, { date: '2026-06-04', text: 'B', tag: null }]; const rt = m._parseLessonEntries(m._renderLessonsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._parseLessonEntries(m._renderLessonsMd([])).length === 0; const layerOk = typeof _loadLessons === 'function' && typeof _saveLessons === 'function' && typeof lessonsJsonPath === 'function' && _renderLessonsMd === m._renderLessonsMd; return rtOk && tplOk && layerOk; } },
|
|
3870
|
-
{ name: 'UR-0025 심층: BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils 분리 (1.9.341)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length ===
|
|
3881
|
+
{ name: 'UR-0025 심층: BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils 분리 (1.9.341)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 2 && c.BUILTIN_CATALOG['feature-implementation'] && c.BUILTIN_CATALOG['project-roadmap-generator'] && Object.values(c.BUILTIN_CATALOG).every(v => v.verification !== 'passed'); const out = m._withBuiltinSource(c.BUILTIN_CATALOG); const work = Object.keys(out).length === 2 && Object.values(out).every(v => v._source === 'builtin') && out['feature-implementation'].version === '1.0.0' && Array.isArray(out['feature-implementation'].capabilities) && Object.keys(m._withBuiltinSource(null)).length === 0; const src = read(__filename); const moved = BUILTIN_CATALOG === c.BUILTIN_CATALOG && _withBuiltinSource === m._withBuiltinSource && !/const BUILTIN_CATALOG = \{/.test(src); return catOk && work && moved; } },
|
|
3871
3882
|
{ name: 'UR-0025 심층: ROADMAP_STATUS_LABEL/COLOR→lib/catalogs 분리 (1.9.342)', run: () => { const c = require('../lib/catalogs'); const lblOk = c.ROADMAP_STATUS_LABEL && Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_LABEL.blocked === '오류'; const colOk = c.ROADMAP_STATUS_COLOR && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11 && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6'; const src = read(__filename); const moved = ROADMAP_STATUS_LABEL === c.ROADMAP_STATUS_LABEL && ROADMAP_STATUS_COLOR === c.ROADMAP_STATUS_COLOR && !/const ROADMAP_STATUS_LABEL = \{/.test(src); return lblOk && colOk && moved; } },
|
|
3872
3883
|
{ name: 'UR-0025 심층: SECRET_PATTERNS→lib/catalogs 보안 응집 분리 (1.9.343)', run: () => { const c = require('../lib/catalogs'); const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 20 && c.SECRET_PATTERNS.every(p => p.name && p.re instanceof RegExp); const A = 'A'.repeat(40); const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const det = hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";'); const moved = SECRET_PATTERNS === c.SECRET_PATTERNS; return catOk && det && moved; } },
|
|
3873
3884
|
{ name: 'UR-0025 심층: SKILL_CATALOG_PRESETS→lib/catalogs 분리 (1.9.344)', run: () => { const c = require('../lib/catalogs'); const catOk = c.SKILL_CATALOG_PRESETS && Object.keys(c.SKILL_CATALOG_PRESETS).length === 2 && c.SKILL_CATALOG_PRESETS.vercel && c.SKILL_CATALOG_PRESETS.vercel.owner === 'vercel-labs' && c.SKILL_CATALOG_PRESETS.anthropic && c.SKILL_CATALOG_PRESETS.anthropic.repo === 'skills'; const moved = SKILL_CATALOG_PRESETS === c.SKILL_CATALOG_PRESETS; return catOk && moved; } },
|
|
@@ -6799,6 +6810,7 @@ function commandsCmd(root) {
|
|
|
6799
6810
|
{ cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
|
|
6800
6811
|
{ cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
|
|
6801
6812
|
{ cmd: 'integrity check [--repair] [--json]', desc: 'managed 정책-문서 12종 무결성(부재/H1 상실/절단) 점검 — --repair: archive 대피 후 템플릿 재생성 — 1.36.57 감사 F-04' },
|
|
6813
|
+
{ cmd: 'referee add|verify|list|show|drop', desc: '검증기 캘리브레이션 — 신뢰 전에 탐지력 증명(known-good 통과 + 망가뜨린 known-bad 를 기대 사유로 거부). verify-claim --referee / gate 게이팅 — 1.36.80 P-0001' },
|
|
6802
6814
|
{ cmd: 'preview add|list|show|approve|revise|mockup', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--force]: 자립형 HTML 디자인 시안 스캐폴드 · add --mockup <파일>: 기존 시안 첨부 — 1.36.51 UR-0061 · 1.36.75 UR-0066' },
|
|
6803
6815
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
6804
6816
|
{ cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
|
|
@@ -9215,6 +9227,9 @@ function _saveTeams(root, teams) {
|
|
|
9215
9227
|
const _team = require('../lib/team');
|
|
9216
9228
|
const _tgl = require('../lib/toggles'); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동 — gate/lens/auto-graph/delegation-brief)
|
|
9217
9229
|
const _clar = require('../lib/clarify'); // 1.36.51 (UR-0061): 모호성 질문 + 미리보기 승인 워크플로
|
|
9230
|
+
const _ref = require('../lib/referee'); // 1.36.80 (P-0001): 검증기 캘리브레이션 — 검증기 자신의 탐지력을 실행으로 증명
|
|
9231
|
+
const _pvsrv = require('../lib/preview-serve'); // 1.36.80 (UR-0067): 라이브 미리보기 — 임시 워크스페이스 + 로컬 서버 + 확인 후 정리
|
|
9232
|
+
function refereeCmd(root, sub, rest) { return _ref.refereeCmd(root, sub, rest, { has, arg, VERSION, _withLock }); }
|
|
9218
9233
|
const _tech = require('../lib/tech-profile'); // 1.36.53 (UR-0062): 기술 프로필 (언어·서비스 감지 + 변경 이력)
|
|
9219
9234
|
function teamCmd(root, sub, id, opts = {}) { return _team.teamCmd(root, sub, id, opts, { VERSION, _loadTeams, _loadTeamsChecked, _saveTeams, _detectShellCtx, arg, has, _withLock, failJson }); } // 1.36.31: add 경합 락 · 1.36.78: 형상무효 가드 + failJson
|
|
9220
9235
|
|
|
@@ -12926,6 +12941,21 @@ function verifyClaimCmd(root, taskId, opts = {}) {
|
|
|
12926
12941
|
const rows = readProgressRows(root);
|
|
12927
12942
|
const row = rows.find(r => r.id === taskId);
|
|
12928
12943
|
if (!row) { if (opts.collect) return { id: taskId, ok: false, reasons: ['not-found'] }; return failJson(_j, 'not_found', `progress-tracker.md에 ${taskId} 없음.`); }
|
|
12944
|
+
// 1.36.80 (P-0001): --referee <id> — 캘리브레이션된 검증기만 신뢰한다. 미검증/무효(stale) 검증기의 통과는 증거가 아니므로
|
|
12945
|
+
// 실행 전에 거부한다(검증기 자신의 탐지력이 증명되지 않은 상태에서 나온 초록은 정보가 없다).
|
|
12946
|
+
const _refId = opts.refereeId || arg('--referee', null);
|
|
12947
|
+
let _refereeResult = null;
|
|
12948
|
+
if (_refId && _refId !== true) {
|
|
12949
|
+
const _g = _ref.requireReferee(root, String(_refId));
|
|
12950
|
+
if (!_g.ok) { if (opts.collect) return { id: taskId, ok: false, reasons: [_g.code] }; return failJson(_j, _g.code, _g.message); }
|
|
12951
|
+
const _rr = cp.spawnSync(_g.referee.check, { cwd: root, shell: true, encoding: 'utf8', timeout: 600000, maxBuffer: 8 * 1024 * 1024 });
|
|
12952
|
+
_refereeResult = { id: _g.referee.id, check: _g.referee.check, exit: (_rr.status == null ? 1 : _rr.status), calibratedAt: _g.referee.calibration.at };
|
|
12953
|
+
if (_refereeResult.exit !== 0) {
|
|
12954
|
+
const _msg = `referee ${_g.referee.id} 검증 실패(exit ${_refereeResult.exit}) — 캘리브레이션된 검증기가 이 상태를 거부했습니다: ${_g.referee.check}`;
|
|
12955
|
+
if (opts.collect) return { id: taskId, ok: false, reasons: ['referee_failed'], referee: _refereeResult };
|
|
12956
|
+
return failJson(_j, 'referee_failed', _msg);
|
|
12957
|
+
}
|
|
12958
|
+
}
|
|
12929
12959
|
|
|
12930
12960
|
const evidence = row.evidence || '';
|
|
12931
12961
|
// 1.9.20: 파일 경로 추출 — 도메인 폴더 자동 인식 + 루트 메타파일
|
|
@@ -18898,7 +18928,7 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
18898
18928
|
case 'leerness_handoff': cliArgs = ['handoff', targetPath, '--compact', '--no-drift-check']; break;
|
|
18899
18929
|
case 'leerness_drift_check': cliArgs = ['drift', 'check', targetPath, '--json']; break;
|
|
18900
18930
|
case 'leerness_audit': cliArgs = ['audit', targetPath, '--json', ...(args.fix ? ['--fix'] : []), ...(args.strict ? ['--strict'] : [])]; break;
|
|
18901
|
-
case 'leerness_verify_claim': cliArgs = ['verify-claim', args.taskId, '--path', targetPath, ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : [])]; break;
|
|
18931
|
+
case 'leerness_verify_claim': cliArgs = ['verify-claim', args.taskId, '--path', targetPath, ...(args.refereeId ? ['--referee', String(args.refereeId)] : []), ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : [])]; break;
|
|
18902
18932
|
case 'leerness_verify_claim_all': cliArgs = ['verify-claim', '--all', '--path', targetPath, '--json', ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : []), ...(args.lenient ? ['--lenient'] : [])]; break; // 1.33.3: 모든 done 주장 일괄 검증(--json 강제 → process.exitCode 만, 하드 exit 없음)
|
|
18903
18933
|
case 'leerness_contract_verify': cliArgs = ['contract', 'verify', args.spec, args.impl]; break;
|
|
18904
18934
|
case 'leerness_agents_list': cliArgs = ['agents', 'list', '--json']; break;
|
|
@@ -19105,6 +19135,20 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
19105
19135
|
case 'leerness_clarify':
|
|
19106
19136
|
cliArgs = ['clarify', String(args.text || ''), '--path', targetPath, '--json'];
|
|
19107
19137
|
break;
|
|
19138
|
+
case 'leerness_referee': { // 1.36.80 (P-0001)
|
|
19139
|
+
const _ra = String(args.action || 'list');
|
|
19140
|
+
cliArgs = ['referee', _ra, '--path', targetPath, '--json'];
|
|
19141
|
+
if (_ra === 'add') {
|
|
19142
|
+
cliArgs.splice(2, 0, String(args.id || ''));
|
|
19143
|
+
if (args.check) cliArgs.push('--check', String(args.check));
|
|
19144
|
+
if (args.good) cliArgs.push('--good', String(args.good));
|
|
19145
|
+
if (args.bad) cliArgs.push('--bad', String(args.bad));
|
|
19146
|
+
if (args.expectBad) cliArgs.push('--expect-bad', String(args.expectBad));
|
|
19147
|
+
} else if (['show', 'verify', 'drop'].includes(_ra)) {
|
|
19148
|
+
cliArgs.splice(2, 0, String(args.id || ''));
|
|
19149
|
+
}
|
|
19150
|
+
break;
|
|
19151
|
+
}
|
|
19108
19152
|
case 'leerness_preview': {
|
|
19109
19153
|
const _pa = String(args.action || 'list');
|
|
19110
19154
|
cliArgs = ['preview', _pa, '--path', targetPath, '--json'];
|
|
@@ -23437,7 +23481,20 @@ async function main() {
|
|
|
23437
23481
|
if (_pvRaw[i].startsWith('--')) { if (['--design', '--features', '--note', '--path', '--mockup'].includes(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; } // 1.36.75 (검수 #2): --mockup 값이 제목에 흡수되던 것
|
|
23438
23482
|
_pvToks.push(_pvRaw[i]);
|
|
23439
23483
|
}
|
|
23440
|
-
|
|
23484
|
+
// 1.36.80 (UR-0067): 라이브 미리보기 — serve(로컬 서버) / mode(self|project) 는 별도 모듈로 위임
|
|
23485
|
+
const _pvRoot = arg('--path', process.cwd());
|
|
23486
|
+
if (_pvToks[0] === 'serve') return _pvsrv.previewServeCmd(_pvRoot, _pvToks[1], { has, arg, previews: _clar._loadPreviews(_pvRoot) });
|
|
23487
|
+
if (_pvToks[0] === 'mode') return _pvsrv.previewModeCmd(_pvRoot, _pvToks[1], { has, arg });
|
|
23488
|
+
return _clar.previewCmd(_pvRoot, _pvToks[0], _pvToks.slice(1), { has, arg, _withLock, _onResolved: (id) => _pvsrv.cleanupServeDir(_pvRoot, id) }); // 1.36.54 (#2): 변경은 락 직렬화 · 1.36.80: 승인/수정 시 임시 워크스페이스 정리
|
|
23489
|
+
}
|
|
23490
|
+
// 1.36.80 (P-0001): referee — 값 플래그가 positional 로 새지 않게 원시 argv 파싱(preview 와 동일 패턴)
|
|
23491
|
+
if (cmd === 'referee') {
|
|
23492
|
+
const _rvRaw = process.argv.slice(2); const _rvI = _rvRaw.indexOf('referee'); const _rvToks = [];
|
|
23493
|
+
for (let i = _rvI + 1; i < _rvRaw.length && _rvI >= 0; i++) {
|
|
23494
|
+
if (_rvRaw[i].startsWith('--')) { if (['--check', '--good', '--bad', '--expect-bad', '--path', '--timeout'].includes(_rvRaw[i]) && _rvRaw[i + 1]) i++; continue; }
|
|
23495
|
+
_rvToks.push(_rvRaw[i]);
|
|
23496
|
+
}
|
|
23497
|
+
return refereeCmd(arg('--path', process.cwd()), _rvToks[0], _rvToks.slice(1));
|
|
23441
23498
|
}
|
|
23442
23499
|
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
23443
23500
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
package/lib/catalogs.js
CHANGED
|
@@ -349,16 +349,9 @@ const STRINGS = {
|
|
|
349
349
|
|
|
350
350
|
// 1.9.341 (UR-0025 심층): 내장 스킬 catalog (9종) — harness 에서 분리. _loadSkillCatalog 의 builtin fallback.
|
|
351
351
|
const BUILTIN_CATALOG = {
|
|
352
|
-
'
|
|
353
|
-
'commerce-api': { displayNameKo: '커머스 API 연동 스킬 라이브러리', description: '커머스 플랫폼(쿠팡·롯데온·스마트스토어) API 를 붙일 때 사용 — 주문/상품/매출 연동 설계나 인증·레이트리밋 처리가 필요할 때. Use when integrating commerce platform APIs.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['쿠팡·롯데온·스마트스토어 API 연동 설계', '주문/상품/매출 동기화', '환경변수 기반 인증 분리', '레이트리밋/재시도/오류 처리'] },
|
|
354
|
-
'crawling': { displayNameKo: '크롤링·브라우저 자동화 스킬 라이브러리', description: '웹 페이지를 자동으로 조작·수집해야 할 때 사용 — 로그인 세션, 다운로드, 스크린샷 진단, 차단 위험 점검이 걸린 작업일 때. Use when the task drives or scrapes web pages.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Playwright 기반 자동화', '다운로드/로그인 세션 처리', '스크린샷 기반 실패 진단', '약관/권한/차단 위험 점검'] },
|
|
355
|
-
'firebase': { displayNameKo: 'Firebase·Cloud Functions 스킬 라이브러리', description: 'Firebase/Cloud Functions 작업을 시작할 때 사용 — Functions 배포 구조, 시크릿 분리, IAM 점검, 에뮬레이터 검증이 필요할 때. Use when deploying or configuring Firebase.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Firebase Functions 배포 구조', '환경변수/시크릿 분리', '권한/IAM 점검', '로컬 에뮬레이터 검증'] },
|
|
356
|
-
'ads-analytics': { displayNameKo: '광고·GA4 분석 스킬 라이브러리', description: '광고·GA4 데이터를 다룰 때 사용 — 이벤트/전환 점검, 광고 데이터 수집, 소스·매체 분석 리포트가 필요할 때. Use when analyzing ads or GA4 data.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['GA4 이벤트/전환 점검', '광고 데이터 수집 구조화', '소스/매체 분석', '리포트 자동화'] },
|
|
357
|
-
'appstore-review': { displayNameKo: '앱스토어 심사 대응 스킬 라이브러리', description: '앱스토어 심사를 준비하거나 리젝에 대응할 때 사용 — 심사 문구 분석, 개인정보 라벨 점검이 필요할 때. Use when preparing for or responding to app store review.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['심사 문구 분석', '개인정보 라벨 점검', '리젝 대응 초안', '웹뷰/앱 데이터 수집 구분'] },
|
|
358
|
-
'ai-verified-skill-publisher': { displayNameKo: 'AI 검증 스킬 업로드·라이브러리화 스킬', description: '검증된 스킬을 라이브러리로 정규화해 업로드할 때 사용 — 민감정보 스캔과 업로드 게이트가 필요할 때. Use when publishing a verified skill to a library.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['검증된 스킬 정규화', '민감정보 스캔', 'AI 검증 메타데이터 작성', 'npm/git 업로드 dry-run 및 실행 게이트'] },
|
|
359
|
-
'feature-implementation': { displayNameKo: '기능 구현 표준 스킬', description: '새 기능 구현을 시작할 때 사용 — feature-contract 작성, 재사용 우선 검사, 테스트 증거 수집이 필요한 구현 작업일 때. Use when starting to implement a new feature.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['feature-contracts 작성', '재사용 우선 검사', '테스트 증거 수집', '핸드오프 트리거'] },
|
|
352
|
+
'feature-implementation': { displayNameKo: '기능 구현 표준 스킬', description: '새 기능 구현을 시작할 때 사용 — feature-contract 작성, 재사용 우선 검사, 테스트 증거 수집이 필요한 구현 작업일 때. Use when starting to implement a new feature.', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'unverified', capabilities: ['feature-contracts 작성', '재사용 우선 검사', '테스트 증거 수집', '핸드오프 트리거'] },
|
|
360
353
|
// 1.9.11: 기본 내장 — 로드맵 자동 생성 스킬
|
|
361
|
-
'project-roadmap-generator': { displayNameKo: '프로젝트 로드맵 자동 생성 스킬', description: '프로젝트 진행 상황을 로드맵 한 장으로 시각화해 달라고 할 때 사용 — .harness 상태를 SVG 트리 HTML 로 뽑을 때. Use when the user asks for a visual roadmap.', version: '0.2.0', lastUpdated: '2026-05-12', verification: '
|
|
354
|
+
'project-roadmap-generator': { displayNameKo: '프로젝트 로드맵 자동 생성 스킬', description: '프로젝트 진행 상황을 로드맵 한 장으로 시각화해 달라고 할 때 사용 — .harness 상태를 SVG 트리 HTML 로 뽑을 때. Use when the user asks for a visual roadmap.', version: '0.2.0', lastUpdated: '2026-05-12', verification: 'unverified', capabilities: ['leerness .harness/* 통합 파싱 (plan/progress/skills/rules/decisions/handoff/current-state)', '좌→우 수평 트리 + 상하 중앙정렬 SVG', '7개 상태 색상 (완료/진행/보류/검토/예정/미완료/오류)', 'design-system + CSS variables 자동 주입', '화이트보드 panning/zoom + 더블클릭 reset', '단일 HTML 출력 (외부 의존성 0)'] }
|
|
362
355
|
};
|
|
363
356
|
|
|
364
357
|
// 1.9.342 (UR-0025 심층): roadmap.html 상태 라벨/색상 맵 (status → ko 라벨 / hex 색상) — harness 에서 분리.
|
package/lib/clarify.js
CHANGED
|
@@ -253,11 +253,14 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
253
253
|
if (sub === 'approve') {
|
|
254
254
|
// 1.36.54 (#10): 재승인은 멱등 no-op — 중복 이력 방지
|
|
255
255
|
if (p.status === 'approved') {
|
|
256
|
+
// 1.36.80 (검수 #8): 멱등 재승인 경로가 정리 훅 앞에서 return 해 임시 워크스페이스가 남았다 — 정리는 항상 수행.
|
|
257
|
+
try { if (typeof deps._onResolved === 'function') deps._onResolved(p.id); } catch {}
|
|
256
258
|
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status, changed: false }, null, 2)); return; }
|
|
257
259
|
ok(`${p.id} 이미 승인됨 — 변경 없음`); return;
|
|
258
260
|
}
|
|
259
261
|
p.status = 'approved'; (p.history = p.history || []).push({ at: now(), event: 'approved' });
|
|
260
262
|
if (!_savePreviews(root, list, json)) return;
|
|
263
|
+
try { if (typeof deps._onResolved === 'function') deps._onResolved(p.id); } catch {} // 1.36.80 (UR-0067): 확인 완료 → 임시 워크스페이스 정리
|
|
261
264
|
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status }, null, 2)); return; }
|
|
262
265
|
ok(`${p.id} 승인 — 이제 구현을 시작해도 됩니다.`);
|
|
263
266
|
return;
|
|
@@ -268,6 +271,7 @@ function previewCmd(root, sub, rest, deps = {}) {
|
|
|
268
271
|
if (!note) { failJson(json, 'note_required', `preview revise ${p.id} --note "<수정 요구 내용>" 필요`); return; }
|
|
269
272
|
p.status = 'revision-requested'; (p.history = p.history || []).push({ at: now(), event: 'revision-requested', note });
|
|
270
273
|
if (!_savePreviews(root, list, json)) return;
|
|
274
|
+
try { if (typeof deps._onResolved === 'function') deps._onResolved(p.id); } catch {} // 1.36.80 (UR-0067): 확인 절차 종료 → 임시 워크스페이스 정리
|
|
271
275
|
if (json) { log(JSON.stringify({ ok: true, id: p.id, status: p.status, note }, null, 2)); return; }
|
|
272
276
|
ok(`${p.id} 수정요구 기록${note ? ` — ${note}` : ''} — 미리보기를 수정해 다시 제시하세요 (코드 작성은 계속 보류).`);
|
|
273
277
|
return;
|
package/lib/mcp-tools.js
CHANGED
|
@@ -7,7 +7,7 @@ module.exports = [
|
|
|
7
7
|
{ name: 'leerness_handoff', requiredTier: 'safe-write', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
8
8
|
{ name: 'leerness_drift_check', requiredTier: 'read-only', description: '1.9.136 — AI 에이전트 leerness 미사용 drift 자동 감지 JSON ({ root, score, level, signals[], healthy }). 5+ 신호 + 4단계 레벨 (🟢 healthy / 🟡 warning / 🟠 caution / 🔴 critical). 보안 신호 통합 (1.9.78)', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
9
9
|
{ name: 'leerness_audit', requiredTier: 'safe-write', description: '1.9.102 — 워크스페이스 일관성 감사 JSON (warnings/failures/fixed/healthy + findings[]. kind 11종: design_dup/design_system_default/reuse_map_empty/milestone_unlinked/handoff_not_generated/current_state_stale/readme_version_mismatch/npm_cve/gitignore_missing_secrets/env_keys_missing/strict_promoted)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' }, strict: { type: 'boolean' } } } },
|
|
10
|
-
{ name: 'leerness_verify_claim', requiredTier: 'read-only', description: 'AI 거짓 완료 자동 검증 (evidence 파일 + 실 테스트 실행). 1.9.309(UR-0048): done/완료 주장은 evidence(수정파일+테스트) 기본 강제 — 증거 없는 done 은 FAIL(exit 1). lenient=true 로 완화, runTests/strictClaims 추가 가능. 응답 verdict.evidenceComplete 포함.', inputSchema: { type: 'object', properties: { taskId: { type: 'string' }, path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } }, required: ['taskId'] } },
|
|
10
|
+
{ name: 'leerness_verify_claim', requiredTier: 'read-only', description: 'AI 거짓 완료 자동 검증 (evidence 파일 + 실 테스트 실행). 1.9.309(UR-0048): done/완료 주장은 evidence(수정파일+테스트) 기본 강제 — 증거 없는 done 은 FAIL(exit 1). lenient=true 로 완화, runTests/strictClaims 추가 가능. 응답 verdict.evidenceComplete 포함.', inputSchema: { type: 'object', properties: { taskId: { type: 'string' }, path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' }, refereeId: { type: 'string' } }, required: ['taskId'] } },
|
|
11
11
|
{ name: 'leerness_verify_claim_all', requiredTier: 'read-only', description: '1.33.3 — 모든 done/완료 주장을 한 번에 검증(CI·스케일). progress-tracker 의 done 항목 전부를 verify-claim 정밀 검사(파일 존재·스텁·부풀린 카운트·증거 완전성)로 일괄 점검. 응답 { ok, total, failed, results:[{id,ok,reasons}] }. 세션 마감 전 "내 완료 주장 전부 증거와 맞는가?" 자가 점검용. runTests/strictClaims/lenient 추가 가능.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } } } },
|
|
12
12
|
{ name: 'leerness_contract_verify', requiredTier: 'read-only', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
|
|
13
13
|
{ name: 'leerness_agents_list', requiredTier: 'read-only', description: '외부 AI CLI 가용성 표 (claude/codex/agy/copilot 상태 + 환경변수 활성화 여부)', inputSchema: { type: 'object', properties: {} } },
|
|
@@ -92,7 +92,9 @@ module.exports = [
|
|
|
92
92
|
{ name: 'leerness_honesty_check', requiredTier: 'read-only', description: '1.9.305 (사용자 명시) — AI 인식론적 정직성 점검. 주장/evidence 텍스트가 (1) 모르는 걸 아는 척(근거 없는 단정), (2) 검증 없는 섣부른 판단(추정+완료 결론), (3) 외부 정보 미수집(API/버전/스펙 언급+수집흔적 없음) 중 무엇에 해당하는지 휴리스틱 탐지. read-only. 에이전트가 자기 주장을 단언 전 self-check. 인자: { path?, taskId?, text? }. 응답: { ok, findings[], dimensions[], highCount }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, taskId: { type: 'string' }, text: { type: 'string' } } } },
|
|
93
93
|
{ name: 'leerness_brief', requiredTier: 'read-only', description: '1.9.308 (UR-0055) — 프로젝트 청사진 회수. 프로젝트 개요/소개/목적/기능/스택/아키텍처/성공기준/방향이력을 구조화 반환(show) 또는 복사용 blueprint 텍스트(export=true). read-only. 외부 에이전트가 "이 프로젝트가 무엇이고 어디로 가는가"를 1콜로 파악하거나, 신규 프로젝트 계획으로 복사. 인자: { path?, export? }. 응답(show): { project, intro, purpose, features[], stack[], directionHistory[], ... }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, export: { type: 'boolean' } } } },
|
|
94
94
|
{ name: 'leerness_clarify', requiredTier: 'read-only', description: '1.36.77 (UR-0061) — 사용자 요청 텍스트의 판단-모호 신호 감지 → 사용자에게 그대로 물을 질문 목록 JSON ({ ambiguous, signals[], questions[] }). 추측 구현 방지 — 모호하면 질문 먼저.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, path: { type: 'string' } }, required: ['text'] } },
|
|
95
|
-
{ name: 'leerness_preview', requiredTier: 'safe-write', description: '1.36.77 (UR-0061/0066) — 신규 기능/디자인 미리보기 승인 워크플로. action: add(title/design/features/mockup)·list·show·approve·revise(note 필수)·mockup(HTML 시안 스캐폴드 생성, force 로 재생성). 계약: approve 전 해당 기능 코드 작성 금지. list/show 는 읽기지만 단일 도구라 safe-write 로 보수 선언.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['add', 'list', 'show', 'approve', 'revise', 'mockup'] }, path: { type: 'string' }, title: { type: 'string' }, design: { type: 'string' }, features: { type: 'string' }, mockup: { type: 'string' }, id: { type: 'string' }, note: { type: 'string' }, force: { type: 'boolean' } }, required: ['action'] } },
|
|
95
|
+
{ name: 'leerness_preview', requiredTier: 'safe-write', description: '1.36.77 (UR-0061/0066) — 신규 기능/디자인 미리보기 승인 워크플로. action: add(title/design/features/mockup)·list·show·approve·revise(note 필수)·mockup(HTML 시안 스캐폴드 생성, force 로 재생성). 계약: approve 전 해당 기능 코드 작성 금지. list/show 는 읽기지만 단일 도구라 safe-write 로 보수 선언.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['add', 'list', 'show', 'approve', 'revise', 'mockup'] }, path: { type: 'string' }, title: { type: 'string' }, design: { type: 'string' }, features: { type: 'string' }, mockup: { type: 'string' }, id: { type: 'string' }, note: { type: 'string' }, force: { type: 'boolean' } }, required: ['action'] } },
|
|
96
|
+
{ name: 'leerness_referee', requiredTier: 'safe-write', description: '1.36.80 (P-0001) — 검증기 캘리브레이션: 검증기를 신뢰하기 전에 탐지력을 실행으로 증명(known-good 통과 + 일부러 망가뜨린 known-bad 를 기대 사유로 거부). action: add(id/check/good/bad/expectBad)·list·show·verify·drop. 명령/기대치가 바뀌면 캘리브레이션이 stale 이 되어 신뢰가 철회된다. verify-claim 의 refereeId 로 게이팅.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['add', 'list', 'show', 'verify', 'drop'] }, path: { type: 'string' }, id: { type: 'string' }, check: { type: 'string' }, good: { type: 'string' }, bad: { type: 'string' }, expectBad: { type: 'string' } }, required: ['action'] } },
|
|
97
|
+
];
|
|
96
98
|
|
|
97
99
|
// 1.36.58 (외부 GPT 감사 F-09): core 프로필 — 세션 시작 토큰/도구 선택 비용을 줄이는 핵심 20종.
|
|
98
100
|
// `leerness mcp serve --profile core` (또는 LEERNESS_MCP_PROFILE=core) 에서 tools/list·tools/call 이 이 집합만 노출.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// lib/preview-serve.js — 라이브 미리보기 (1.36.80, 사용자 요청 UR-0067).
|
|
2
|
+
// 문제: 시안(preview mockup)이 "파일로만" 존재해 사용자가 직접 열어야 했다. 요청: 임시 워크스페이스에 프로젝트에 맞춰
|
|
3
|
+
// 생성하고 로컬 서버로 띄워 확인한 뒤, 확인이 끝나면 임시 워크스페이스를 정리.
|
|
4
|
+
// 모드(사용자 지시): A=self(기본) 자립형 HTML 을 0-deps node http 로 제공 — 어떤 스택에서도 즉시 동작.
|
|
5
|
+
// B=project(옵트인) 프로젝트의 정적 디렉토리에 시안을 배치해 **프로젝트 자신의 dev 서버**로 확인.
|
|
6
|
+
// 미설정이면 강제하지 않고 A 로 진행하되 선택 질문을 출력한다(false-PASS 편향 — 흐름을 막지 않는다).
|
|
7
|
+
// leerness 는 사용자의 dev 서버를 임의로 실행하지 않는다(사용자 코드 실행은 명시 옵트인 영역).
|
|
8
|
+
'use strict';
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { absRoot, exists, read, writeUtf8, mkdirp, log, ok, warn, failJson, now } = require('./io');
|
|
13
|
+
|
|
14
|
+
function _cfgPath(root) { return path.join(absRoot(root), '.harness', 'preview-config.json'); }
|
|
15
|
+
|
|
16
|
+
function loadPreviewConfig(root) {
|
|
17
|
+
const f = _cfgPath(root);
|
|
18
|
+
if (!exists(f)) return { mode: null, devUrl: '', staticDir: '' };
|
|
19
|
+
try {
|
|
20
|
+
const j = JSON.parse(read(f));
|
|
21
|
+
const mode = (j && (j.mode === 'self' || j.mode === 'project')) ? j.mode : null;
|
|
22
|
+
return { mode, devUrl: (j && typeof j.devUrl === 'string') ? j.devUrl : '', staticDir: (j && typeof j.staticDir === 'string') ? j.staticDir : '' };
|
|
23
|
+
} catch { return { mode: null, devUrl: '', staticDir: '', corrupt: true }; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function savePreviewConfig(root, cfg, json) {
|
|
27
|
+
const f = _cfgPath(root);
|
|
28
|
+
if (exists(f)) { try { JSON.parse(read(f)); } catch { failJson(json, 'store_corrupt', `preview-config.json 손상 — 덮어쓰기 거부: ${f}`); return false; } }
|
|
29
|
+
mkdirp(path.dirname(f));
|
|
30
|
+
writeUtf8(f, JSON.stringify({ ...cfg, updatedAt: now() }, null, 2) + '\n');
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 모드 미설정 시 사용자에게 물을 질문 — 강제하지 않고 기본(A)로 진행하며 선택지를 제시한다.
|
|
35
|
+
function modeQuestionLines(root) {
|
|
36
|
+
return [
|
|
37
|
+
' ❓ 미리보기 모드가 아직 정해지지 않았습니다 — 기본값 A(자립형)로 진행합니다. 사용자에게 확인하세요:',
|
|
38
|
+
' (A) self — 자립형 HTML 을 leerness 로컬 서버로 확인 (0 의존성 · 어떤 스택이든 즉시 · 오프라인)',
|
|
39
|
+
' (B) project — 프로젝트 정적 디렉토리에 배치해 프로젝트 자신의 dev 서버로 확인 (스택 정합 ↑ · dev 서버는 사용자가 실행)',
|
|
40
|
+
' → 설정: leerness preview mode self | leerness preview mode project --static-dir public --dev-url http://localhost:5173',
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 임시 워크스페이스 — 확인이 끝나면(승인/수정요구) 정리된다. 시안 원본(.harness/previews/<id>-mockup.html)은 건드리지 않는다.
|
|
45
|
+
function serveDir(root, id) { return path.join(absRoot(root), '.harness', 'previews', `.serve-${id}`); }
|
|
46
|
+
|
|
47
|
+
function cleanupServeDir(root, id) {
|
|
48
|
+
let cleaned = false;
|
|
49
|
+
const d = serveDir(root, id);
|
|
50
|
+
try { if (exists(d)) { fs.rmSync(d, { recursive: true, force: true }); cleaned = true; } } catch {}
|
|
51
|
+
// (검수 #8) project 모드로 프로젝트 정적 디렉토리에 배치한 사본도 함께 정리 — "확인 끝나면 정리한다"는 안내와 실제를 일치시킨다.
|
|
52
|
+
try {
|
|
53
|
+
const cfgF = _cfgPath(root);
|
|
54
|
+
if (exists(cfgF)) {
|
|
55
|
+
const j = JSON.parse(read(cfgF));
|
|
56
|
+
const placed = (j && j.placed && j.placed[id]) ? j.placed[id] : null;
|
|
57
|
+
if (placed) {
|
|
58
|
+
const abs = path.resolve(absRoot(root), placed);
|
|
59
|
+
const rel = path.relative(absRoot(root), abs);
|
|
60
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel) && exists(abs)) { fs.rmSync(abs, { force: true }); cleaned = true; }
|
|
61
|
+
delete j.placed[id];
|
|
62
|
+
writeUtf8(cfgF, JSON.stringify(j, null, 2) + '\n');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return cleaned;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 프로젝트의 디자인 토큰(있으면)을 임시 워크스페이스로 복사 — 시안이 프로젝트 톤과 어긋나지 않게.
|
|
70
|
+
// 빌드가 필요한 산출물은 복사하지 않는다(leerness 는 사용자 프로젝트를 빌드하지 않는다).
|
|
71
|
+
const _TOKEN_CANDIDATES = ['.harness/design-system.md', 'src/styles/tokens.css', 'src/styles/variables.css', 'styles/tokens.css', 'src/index.css', 'src/app.css', 'app/globals.css'];
|
|
72
|
+
function _collectTokens(root) {
|
|
73
|
+
const found = [];
|
|
74
|
+
for (const rel of _TOKEN_CANDIDATES) {
|
|
75
|
+
const p = path.join(root, rel);
|
|
76
|
+
try { if (exists(p) && fs.statSync(p).isFile() && fs.statSync(p).size <= 256 * 1024) found.push({ rel, text: read(p) }); } catch {}
|
|
77
|
+
if (found.length >= 2) break;
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildServeWorkspace(root, p) {
|
|
83
|
+
const srcRel = p.mockupPath || path.join('.harness', 'previews', `${p.id}-mockup.html`);
|
|
84
|
+
const src = path.isAbsolute(srcRel) ? srcRel : path.join(root, srcRel);
|
|
85
|
+
// (검수 #12) 검증 실패 시 빈 .serve-* 디렉토리가 남던 것 — 검증을 먼저, 생성은 그 뒤에.
|
|
86
|
+
if (!exists(src)) return { ok: false, error: `시안 파일 없음: ${srcRel} — 먼저 leerness preview mockup ${p.id}` };
|
|
87
|
+
const dir = serveDir(root, p.id);
|
|
88
|
+
mkdirp(dir);
|
|
89
|
+
let html = read(src);
|
|
90
|
+
const tokens = _collectTokens(root);
|
|
91
|
+
if (tokens.length) {
|
|
92
|
+
// (검수 #9) 프로젝트 CSS 를 <style> 안에 그대로 넣으면 CSS 안의 `</style><script>` 가 탈출해 스크립트가 실행됐다.
|
|
93
|
+
// → 별도 스타일시트 파일로 쓰고 <link> 로 연결(문자열 탈출 자체가 불가능). 외부 리소스 0 은 그대로 유지(같은 워크스페이스).
|
|
94
|
+
const css = tokens.filter(t => /\.css$/.test(t.rel)).map(t => `/* from ${t.rel} */\n${t.text}`).join('\n');
|
|
95
|
+
const noteRels = tokens.map(t => t.rel).join(', ');
|
|
96
|
+
let inject = '';
|
|
97
|
+
if (css) { writeUtf8(path.join(dir, 'project-tokens.css'), css); inject += `<link rel="stylesheet" href="project-tokens.css">\n`; }
|
|
98
|
+
inject += `<!-- leerness: project design context — ${_escComment(noteRels)} -->\n`;
|
|
99
|
+
html = html.replace('</head>', inject + '</head>');
|
|
100
|
+
}
|
|
101
|
+
writeUtf8(path.join(dir, 'index.html'), html);
|
|
102
|
+
return { ok: true, dir, tokens: tokens.map(t => t.rel) };
|
|
103
|
+
}
|
|
104
|
+
function _escComment(s) { return String(s).replace(/--+/g, '-').replace(/[<>]/g, ''); }
|
|
105
|
+
|
|
106
|
+
const _MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.json': 'application/json; charset=utf-8' };
|
|
107
|
+
|
|
108
|
+
// 1.36.80 (사용자 지시): leerness 시그니처 포트 — 전화 키패드의 "LEER"(5-3-3-7). ollama:11434 처럼 기억 가능한 고정값.
|
|
109
|
+
// 이미 사용 중이면 뒤로 순차 탐색(최대 20) → 그래도 없으면 OS 임의 빈 포트(0). 어떤 경우에도 "포트 충돌로 실패"하지 않는다.
|
|
110
|
+
const LEERNESS_PREVIEW_PORT = 5337;
|
|
111
|
+
function _listenWithFallback(server, preferred, host, onReady, onFail) {
|
|
112
|
+
const tries = [];
|
|
113
|
+
for (let i = 0; i < 20; i++) tries.push(preferred + i);
|
|
114
|
+
tries.push(0); // 마지막 수단: OS 가 비어 있는 포트를 준다
|
|
115
|
+
let idx = 0;
|
|
116
|
+
const attempt = () => {
|
|
117
|
+
const port = tries[idx];
|
|
118
|
+
const onErr = (e) => {
|
|
119
|
+
server.removeListener('error', onErr);
|
|
120
|
+
if ((e && (e.code === 'EADDRINUSE' || e.code === 'EACCES')) && idx < tries.length - 1) { idx++; setImmediate(attempt); return; }
|
|
121
|
+
onFail(e, port);
|
|
122
|
+
};
|
|
123
|
+
server.once('error', onErr);
|
|
124
|
+
// 성공 시 실제 바인딩된 포트로 판단한다 — `port !== preferred` 는 port 0(OS 할당) 경로에서도 어긋나고,
|
|
125
|
+
// 무엇보다 "선호 포트가 막혀 옮겨갔다"는 사실은 실제 주소로만 정확히 알 수 있다(실측: 5338 인데 false 보고).
|
|
126
|
+
server.listen(port, host, () => { server.removeListener('error', onErr); const a = server.address().port; onReady(a, preferred > 0 && a !== preferred); });
|
|
127
|
+
};
|
|
128
|
+
attempt();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// `leerness preview serve <P-ID> [--port N] [--timeout SEC] [--keep]`
|
|
132
|
+
function previewServeCmd(root, id, deps = {}) {
|
|
133
|
+
const { has, arg, previews } = deps;
|
|
134
|
+
root = absRoot(root);
|
|
135
|
+
const json = !!(has && has('--json'));
|
|
136
|
+
const p = (previews || []).find(x => x.id === id);
|
|
137
|
+
if (!p) { failJson(json, 'not_found', `preview 없음: ${id}`); return; }
|
|
138
|
+
const cfg = loadPreviewConfig(root);
|
|
139
|
+
const mode = cfg.mode || 'self';
|
|
140
|
+
|
|
141
|
+
if (mode === 'project') {
|
|
142
|
+
// B(옵트인): 프로젝트 정적 디렉토리에 배치 — dev 서버 실행은 사용자 몫(사용자 코드를 leerness 가 임의 실행하지 않음)
|
|
143
|
+
const staticDir = (arg ? arg('--static-dir', '') : '') || cfg.staticDir || 'public';
|
|
144
|
+
const destDir = path.resolve(root, staticDir);
|
|
145
|
+
const rel = path.relative(root, destDir);
|
|
146
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) { failJson(json, 'static_dir_outside_root', `--static-dir 은 프로젝트 안이어야 합니다: ${destDir}`); return; }
|
|
147
|
+
// (검수 #5) 렉시컬 검사만으론 정션/심볼릭 링크로 프로젝트 밖에 쓰는 것을 못 막는다 — 실제 경로로 재확인.
|
|
148
|
+
try {
|
|
149
|
+
const _rootReal2 = fs.realpathSync(root);
|
|
150
|
+
let probe = destDir;
|
|
151
|
+
while (!exists(probe) && path.dirname(probe) !== probe) probe = path.dirname(probe); // 존재하는 최근접 조상
|
|
152
|
+
const realProbe = fs.realpathSync(probe);
|
|
153
|
+
const relReal2 = path.relative(_rootReal2, realProbe);
|
|
154
|
+
if (relReal2.startsWith('..') || path.isAbsolute(relReal2)) { failJson(json, 'static_dir_outside_root', `--static-dir 이 링크를 통해 프로젝트 밖을 가리킵니다: ${destDir} → ${realProbe}`); return; }
|
|
155
|
+
} catch {}
|
|
156
|
+
const built = buildServeWorkspace(root, p);
|
|
157
|
+
if (!built.ok) { failJson(json, 'mockup_missing', built.error); return; }
|
|
158
|
+
mkdirp(destDir);
|
|
159
|
+
const destFile = path.join(destDir, `leerness-preview-${p.id}.html`);
|
|
160
|
+
writeUtf8(destFile, read(path.join(built.dir, 'index.html')));
|
|
161
|
+
// (검수 #8) 배치 위치를 기록해 승인/수정 시 정확히 회수한다(추측 삭제 금지 — 기록된 경로만 지운다).
|
|
162
|
+
try {
|
|
163
|
+
const cur = exists(_cfgPath(root)) ? JSON.parse(read(_cfgPath(root))) : {};
|
|
164
|
+
cur.placed = cur.placed && typeof cur.placed === 'object' ? cur.placed : {};
|
|
165
|
+
cur.placed[p.id] = path.relative(root, destFile).replace(/\\/g, '/');
|
|
166
|
+
writeUtf8(_cfgPath(root), JSON.stringify(cur, null, 2) + '\n');
|
|
167
|
+
} catch {}
|
|
168
|
+
const urlBase = (arg ? arg('--dev-url', '') : '') || cfg.devUrl || 'http://localhost:3000';
|
|
169
|
+
const url = `${String(urlBase).replace(/\/$/, '')}/leerness-preview-${p.id}.html`;
|
|
170
|
+
const relFile = path.relative(root, destFile).replace(/\\/g, '/');
|
|
171
|
+
if (json) { log(JSON.stringify({ ok: true, mode: 'project', id: p.id, file: relFile, url, note: 'dev 서버는 사용자가 실행합니다' }, null, 2)); return; }
|
|
172
|
+
ok(`시안 배치(project 모드): ${relFile}`);
|
|
173
|
+
log(` → 프로젝트 dev 서버를 실행한 뒤 열기: ${url}`);
|
|
174
|
+
log(` ⓘ leerness 는 사용자의 dev 서버를 자동 실행하지 않습니다. 확인이 끝나면: leerness preview approve ${p.id} (배치본도 함께 정리)`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A(기본): 자립형 HTML 을 0-deps 로컬 서버로
|
|
179
|
+
const built = buildServeWorkspace(root, p);
|
|
180
|
+
if (!built.ok) { failJson(json, 'mockup_missing', built.error); return; }
|
|
181
|
+
const _portArg = (arg ? arg('--port', '') : '');
|
|
182
|
+
const port = (_portArg && _portArg !== true) ? Math.max(0, parseInt(_portArg, 10) || 0) : LEERNESS_PREVIEW_PORT;
|
|
183
|
+
const timeoutSec = Math.max(5, parseInt((arg ? arg('--timeout', '') : '') || '600', 10) || 600);
|
|
184
|
+
const _rootReal = (() => { try { return fs.realpathSync(built.dir); } catch { return path.resolve(built.dir); } })();
|
|
185
|
+
const server = http.createServer((req, res) => {
|
|
186
|
+
// (검수 #4) ① 잘못된 인코딩(%E0%A4%A)이 URIError 로 서버를 죽였다 → 400. ② startsWith 는 형제 디렉토리
|
|
187
|
+
// (.serve-P-0001-secret)를 통과시켰다 → path.relative 로 정확 판정. ③ 정션/심볼릭 링크로 밖을 가리키면
|
|
188
|
+
// 렉시컬 검사만으론 못 막는다 → realpath 로 실제 위치까지 확인.
|
|
189
|
+
let rel;
|
|
190
|
+
try { rel = decodeURIComponent(String(req.url || '/').split('?')[0]); }
|
|
191
|
+
catch { res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' }); res.end('bad request'); return; }
|
|
192
|
+
if (rel === '/' || rel === '') rel = '/index.html';
|
|
193
|
+
const fp = path.resolve(built.dir, '.' + rel);
|
|
194
|
+
const relCheck = path.relative(_rootReal, fp);
|
|
195
|
+
if (relCheck.startsWith('..') || path.isAbsolute(relCheck)) { res.writeHead(403); res.end('forbidden'); return; }
|
|
196
|
+
let realFp = fp;
|
|
197
|
+
try { realFp = fs.realpathSync(fp); } catch { res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }); res.end('not found'); return; }
|
|
198
|
+
const relReal = path.relative(_rootReal, realFp);
|
|
199
|
+
if (relReal.startsWith('..') || path.isAbsolute(relReal)) { res.writeHead(403); res.end('forbidden'); return; }
|
|
200
|
+
fs.readFile(realFp, (err, buf) => { // 실검사를 통과한 실제 경로로 읽는다
|
|
201
|
+
if (err) { res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }); res.end('not found'); return; }
|
|
202
|
+
res.writeHead(200, { 'content-type': _MIME[path.extname(realFp).toLowerCase()] || 'application/octet-stream', 'cache-control': 'no-store' });
|
|
203
|
+
res.end(buf);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
_listenWithFallback(server, port, '127.0.0.1', (actual, moved) => {
|
|
207
|
+
const url = `http://127.0.0.1:${actual}/`;
|
|
208
|
+
if (json) { log(JSON.stringify({ ok: true, mode: 'self', id: p.id, url, port: actual, preferredPort: port, portFallback: !!moved, timeoutSec, workspace: path.relative(root, built.dir).replace(/\\/g, '/'), tokens: built.tokens }, null, 2)); }
|
|
209
|
+
else {
|
|
210
|
+
ok(`라이브 미리보기 실행 중 — ${url}`);
|
|
211
|
+
if (moved) warn(` ⓘ 기본 포트 ${port} 사용 중 → 비어 있는 ${actual} 로 자동 전환`);
|
|
212
|
+
log(` 시안: ${p.id} 「${p.title}」`);
|
|
213
|
+
if (built.tokens.length) log(` 프로젝트 디자인 컨텍스트 반영: ${built.tokens.join(', ')}`);
|
|
214
|
+
if (!cfg.mode) modeQuestionLines(root).forEach(l => log(l));
|
|
215
|
+
log(` → 사용자에게 이 주소를 열어 확인받고, 승인: leerness preview approve ${p.id} · 수정요구: leerness preview revise ${p.id} --note "..."`);
|
|
216
|
+
log(` ⓘ ${timeoutSec}초 후 자동 종료(Ctrl+C 로 즉시 종료). 확인이 끝나면 임시 워크스페이스는 승인/수정 시 자동 정리됩니다.`);
|
|
217
|
+
}
|
|
218
|
+
const stop = (why) => {
|
|
219
|
+
try { server.close(); } catch {}
|
|
220
|
+
if (!(has && has('--keep'))) cleanupServeDir(root, p.id);
|
|
221
|
+
if (!json) log(` ⏹ 미리보기 종료(${why})${(has && has('--keep')) ? ' — 임시 워크스페이스 보존(--keep)' : ' — 임시 워크스페이스 정리됨'}`);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
};
|
|
224
|
+
const t = setTimeout(() => stop('timeout'), timeoutSec * 1000);
|
|
225
|
+
if (t.unref) t.unref();
|
|
226
|
+
process.on('SIGINT', () => stop('SIGINT'));
|
|
227
|
+
process.on('SIGTERM', () => stop('SIGTERM'));
|
|
228
|
+
}, (e, tried) => { failJson(json, 'serve_failed', `로컬 서버 시작 실패: ${e && e.message} (마지막 시도 포트 ${tried})`); });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// `leerness preview mode [self|project] [--static-dir public] [--dev-url http://localhost:5173]`
|
|
232
|
+
function previewModeCmd(root, val, deps = {}) {
|
|
233
|
+
const { has, arg } = deps;
|
|
234
|
+
root = absRoot(root);
|
|
235
|
+
const json = !!(has && has('--json'));
|
|
236
|
+
const cfg = loadPreviewConfig(root);
|
|
237
|
+
if (!val) {
|
|
238
|
+
if (json) { log(JSON.stringify({ ok: true, mode: cfg.mode, effective: cfg.mode || 'self', devUrl: cfg.devUrl, staticDir: cfg.staticDir, corrupt: cfg.corrupt || undefined }, null, 2)); return; }
|
|
239
|
+
log(`# leerness preview mode — ${cfg.mode ? cfg.mode : '(미설정 → 기본 self)'}`);
|
|
240
|
+
if (cfg.staticDir) log(` static-dir: ${cfg.staticDir}`);
|
|
241
|
+
if (cfg.devUrl) log(` dev-url: ${cfg.devUrl}`);
|
|
242
|
+
if (!cfg.mode) modeQuestionLines(root).forEach(l => log(l));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (val !== 'self' && val !== 'project') { failJson(json, 'invalid_mode', `preview mode <self|project> — 받은 값: ${val}`); return; }
|
|
246
|
+
const next = { mode: val, devUrl: (arg ? (arg('--dev-url', '') || cfg.devUrl) : cfg.devUrl) || '', staticDir: (arg ? (arg('--static-dir', '') || cfg.staticDir) : cfg.staticDir) || '' };
|
|
247
|
+
if (next.devUrl === true) next.devUrl = ''; if (next.staticDir === true) next.staticDir = '';
|
|
248
|
+
if (!savePreviewConfig(root, next, json)) return;
|
|
249
|
+
if (json) { log(JSON.stringify({ ok: true, ...next }, null, 2)); return; }
|
|
250
|
+
ok(`preview mode = ${val}`);
|
|
251
|
+
if (val === 'project') log(` static-dir: ${next.staticDir || 'public'} · dev-url: ${next.devUrl || 'http://localhost:3000'} (dev 서버는 사용자가 실행)`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { LEERNESS_PREVIEW_PORT, previewServeCmd, previewModeCmd, loadPreviewConfig, savePreviewConfig, cleanupServeDir, serveDir, modeQuestionLines, buildServeWorkspace };
|
package/lib/pure-utils.js
CHANGED
|
@@ -831,8 +831,17 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}
|
|
|
831
831
|
function _parseSkillsValue(v, catalog) {
|
|
832
832
|
if (!v || v === true) return [];
|
|
833
833
|
if (v === 'all') return Object.keys(catalog || {});
|
|
834
|
-
|
|
835
|
-
|
|
834
|
+
// 1.36.80 (사용자 지시): 내장 기본에서 작성자 개인 도메인 스킬(커머스/GA4/앱스토어/오피스/크롤링/파이어베이스/퍼블리셔) 제거 —
|
|
835
|
+
// 검증 근거 없이 verification:'passed' 를 주장했고 내용은 설명 4줄짜리 메타데이터뿐이라 전역 배포본의 기본값으로 부적절.
|
|
836
|
+
// 남는 2종은 leerness 자체 워크플로(기능 구현 계약·로드맵 산출)라 도구와 함께 유지된다.
|
|
837
|
+
if (v === 'recommended') return Object.keys(catalog || {});
|
|
838
|
+
// 1.36.80 (검수 #10): 명시 요청한 미지/제거된 id 를 조용히 걸러 exit 0 + 0개 설치로 끝나던 것 —
|
|
839
|
+
// 호출부가 unknown 을 보고 분명히 거절할 수 있도록 함께 반환한다(하위호환: 반환은 여전히 배열).
|
|
840
|
+
const req = String(v).split(',').map(s => s.trim()).filter(Boolean);
|
|
841
|
+
const known = req.filter(s => (catalog || {})[s]);
|
|
842
|
+
const unknown = req.filter(s => !(catalog || {})[s]);
|
|
843
|
+
if (unknown.length) { try { Object.defineProperty(known, 'unknown', { value: unknown, enumerable: false }); } catch {} }
|
|
844
|
+
return known;
|
|
836
845
|
}
|
|
837
846
|
|
|
838
847
|
// 1.9.370 (UR-0025): memory archive 블록 파서 순수 코어 — "## 제거 DATE (target: \"...\")" 블록 → {date,target,originalHeader}[].
|
package/lib/referee.js
ADDED
|
Binary file
|
package/lib/review-request.js
CHANGED
|
@@ -212,7 +212,35 @@ function reviewRequestCmd(root, request, deps = {}) {
|
|
|
212
212
|
if (broadHits.length) efficiencyHints.push(`🤔 범위 과대 신호(${broadHits.slice(0, 3).join(', ')}) — 더 작게 쪼갤 수 있나? 요청에 명시된 것만 (Karpathy 단순성)`);
|
|
213
213
|
if (specHits.length) efficiencyHints.push(`🧪 투기적 신호(${specHits.slice(0, 3).join(', ')}) — 지금 필요한 범위만, 추측성 일반화 보류 (Karpathy 단순성)`);
|
|
214
214
|
|
|
215
|
-
//
|
|
215
|
+
// 6.9) 1.36.80 (P-0003, ponytail 검토 채택 · codex 공동판단 ADOPT): **순서 있는 최소성 결정 영수증** — 옵트인(--ladder).
|
|
216
|
+
// 왜: 같은 질문들이 review-request(내부 재사용) · reuse-check(외부 OSS) · lens(완료 시점)로 흩어져 있고,
|
|
217
|
+
// 특히 reuse-check 는 5번째 단(OSS 도입)만 물어 구조적으로 "의존성 추가" 쪽으로 밀었다. 순서를 복원한다.
|
|
218
|
+
// 원칙(공동 합의): 자문 전용(차단·점수·임계 없음) · 근거 없으면 unknown(not-applicable 아님) ·
|
|
219
|
+
// "한 줄"은 목표가 아니라 질문 · 보존 요구사항(검증/보안/접근성/관측)이 가장 싼 단을 이길 수 있음 ·
|
|
220
|
+
// 저장하지 않는다(결정으로 남길 가치가 있으면 사용자가 leerness decision add 로 명시 기록).
|
|
221
|
+
let ladder = null;
|
|
222
|
+
if (has && has('--ladder')) {
|
|
223
|
+
const _rung = (id, question, status, evidence) => ({ id, question, status, evidence: evidence || '' });
|
|
224
|
+
ladder = {
|
|
225
|
+
// (검수 #11) "아무것도 저장하지 않는다"는 과장이었다 — 사다리 **내용**은 저장하지 않지만 명령 실행 기록(run/usage)은
|
|
226
|
+
// 다른 명령과 동일하게 남는다. 주장 범위를 사실에 맞춘다.
|
|
227
|
+
note: 'advisory — 근거 없는 칸은 unknown 입니다. 순서는 비용 순서일 뿐 정답 순서가 아닙니다(보안/접근성은 더 비싼 단이 옳을 수 있음). 사다리 내용은 저장되지 않습니다(명령 실행 기록은 다른 명령과 동일하게 남습니다).',
|
|
228
|
+
rungs: [
|
|
229
|
+
_rung('needed', '이 기능이 정말 필요한가? (요청 자체를 줄이거나 없앨 수 있나)', 'unknown', '요청 텍스트만으로는 판정 불가 — 사용자에게 확인'),
|
|
230
|
+
_rung('in-codebase', '이미 이 코드베이스에 있나?', reuseCandidates.length ? 'candidate' : 'unknown',
|
|
231
|
+
reuseCandidates.length ? `reuse 후보 ${reuseCandidates.length}건(토큰 매칭 — 확정 아님): ${reuseCandidates.slice(0, 3).map(r => r.name || r.id || r).join(', ')}` : 'reuse-map 매칭 없음 — 부재 증명은 아님 (leerness reuse find "<capability>")'),
|
|
232
|
+
_rung('stdlib', '표준 라이브러리가 이미 하나?', 'unknown', '런타임/버전/배포 타깃 의존 — 사람이 확인'),
|
|
233
|
+
_rung('native', '플랫폼 네이티브 기능이 있나? (예: <input type="date">)', 'unknown', '타깃 플랫폼·브라우저 지원 범위 확인 필요'),
|
|
234
|
+
_rung('installed', '이미 설치된 의존성으로 되나?', 'unknown', '설치됨 ≠ 적합 — 보안/유지보수/스코프 확인 (leerness reuse-check)'),
|
|
235
|
+
_rung('compose', '기존 것 조합/짧은 코드로 되나?', 'unknown', '"한 줄"은 목표가 아니라 질문 — 검증·에러처리를 숨기지 말 것'),
|
|
236
|
+
_rung('minimum', '위가 모두 아니면: 동작하는 최소 구현은?', 'unknown', '여기까지 왔다면 범위를 명시적으로 적어두세요'),
|
|
237
|
+
],
|
|
238
|
+
preserved: ['입력 검증(신뢰 경계)', '보안·비밀 취급', '접근성', '에러 처리·관측', '데이터 손실 방지'],
|
|
239
|
+
preservedNote: '위 항목은 "줄이기" 대상이 아니다 — 더 비싼 단을 택해야 할 근거가 된다.',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 7) proceed 권장 (충돌 critical 시 false) — ladder 는 자문이라 이 판정에 영향을 주지 않는다(공동 합의 가드레일)
|
|
216
244
|
const proceed = conflictHints.length < 3 && planConflicts.length === 0;
|
|
217
245
|
|
|
218
246
|
const dt = Date.now() - t0;
|
|
@@ -231,6 +259,7 @@ function reviewRequestCmd(root, request, deps = {}) {
|
|
|
231
259
|
constraintSuggestions: constraintsCheck.suggestions,
|
|
232
260
|
proceed,
|
|
233
261
|
proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
|
|
262
|
+
...(ladder ? { ladder } : {}),
|
|
234
263
|
durationMs: dt
|
|
235
264
|
};
|
|
236
265
|
|
|
@@ -289,6 +318,19 @@ function reviewRequestCmd(root, request, deps = {}) {
|
|
|
289
318
|
recommendedSteps.forEach(s => log(` ${s}`));
|
|
290
319
|
log('');
|
|
291
320
|
}
|
|
321
|
+
// 1.36.80 (P-0003): 최소성 사다리 — 옵트인 자문 영수증. proceed 판정에는 영향 없음.
|
|
322
|
+
if (ladder) {
|
|
323
|
+
log(`## 🪜 최소성 결정 사다리 (--ladder · 자문)`);
|
|
324
|
+
for (const r of ladder.rungs) {
|
|
325
|
+
const mark = r.status === 'confirmed' ? '✅' : (r.status === 'candidate' ? '🔎' : (r.status === 'not-applicable' ? '➖' : '❔'));
|
|
326
|
+
log(` ${mark} [${r.status}] ${r.question}`);
|
|
327
|
+
if (r.evidence) log(` ${r.evidence}`);
|
|
328
|
+
}
|
|
329
|
+
log(` 🔒 줄이면 안 되는 것: ${ladder.preserved.join(' · ')}`);
|
|
330
|
+
log(` ${ladder.preservedNote}`);
|
|
331
|
+
log(` ⓘ ${ladder.note}`);
|
|
332
|
+
log('');
|
|
333
|
+
}
|
|
292
334
|
log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
|
|
293
335
|
log(` 사유: ${out.proceedReason}`);
|
|
294
336
|
log(` 분석 소요: ${dt}ms`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.80",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -153,12 +153,12 @@ total++;
|
|
|
153
153
|
run('route planning', ['route', 'planning']);
|
|
154
154
|
run('route bugfix', ['route', 'bugfix']);
|
|
155
155
|
run('skill list', ['skill', 'list']);
|
|
156
|
-
run('skill info', ['skill', 'info', '
|
|
156
|
+
run('skill info', ['skill', 'info', 'feature-implementation']);
|
|
157
157
|
|
|
158
158
|
// 1.9.2: 스킬 학습 사이클 회귀
|
|
159
159
|
run('skill learn (new)', ['skill', 'learn', 'open-meteo', '--doc', 'https://open-meteo.com/en/docs', '--command', 'fetch hourly+daily JSON', '--capability', 'http fetch', '--capability', 'cache', '--note', 'e2e learn', '--display', 'Open-Meteo 날씨 스킬', '--path', tmp]);
|
|
160
160
|
run('skill use (new)', ['skill', 'use', 'open-meteo', '--note', 'first call', '--path', tmp]);
|
|
161
|
-
run('skill use (catalog)', ['skill', 'use', '
|
|
161
|
+
run('skill use (catalog)', ['skill', 'use', 'feature-implementation', '--note', 'catalog skill materialize', '--path', tmp]);
|
|
162
162
|
run('skill optimize', ['skill', 'optimize', 'open-meteo', '--before', 'no cache', '--after', 'If-Modified-Since', '--note', 'e2e opt', '--path', tmp]);
|
|
163
163
|
|
|
164
164
|
total++;
|
|
@@ -1183,7 +1183,7 @@ total++;
|
|
|
1183
1183
|
} else {
|
|
1184
1184
|
const jsonR = fn(JSON.stringify({ skills: [{ name: 'a', description: 'A' }] }), null);
|
|
1185
1185
|
const rssR = fn('<rss><channel><item><title>X</title><link>http://x.com/s.md</link></item></channel></rss>', null);
|
|
1186
|
-
const mdR = fn('- [
|
|
1186
|
+
const mdR = fn('- [feature-implementation](f.md) — Feature\n- [project-roadmap-generator](r.md) — Roadmap', null);
|
|
1187
1187
|
const urlR = fn('https://x.com/foo/SKILL.md', null);
|
|
1188
1188
|
const ok = jsonR[0].format === 'json' && rssR[0].format === 'rss'
|
|
1189
1189
|
&& mdR[0].format === 'markdown' && urlR[0].format === 'urls';
|
|
@@ -1234,14 +1234,13 @@ total++;
|
|
|
1234
1234
|
{
|
|
1235
1235
|
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-match-'));
|
|
1236
1236
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
1237
|
-
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', '
|
|
1237
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', '기능 구현 재사용 검사', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
1238
1238
|
let parsed = null;
|
|
1239
1239
|
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1240
1240
|
const ok = parsed
|
|
1241
1241
|
&& parsed.top
|
|
1242
|
-
&& parsed.top.length > 0
|
|
1243
|
-
|
|
1244
|
-
console.log(ok ? '✓ B(1.9.45) skill match: jaccard 매칭 → office 최상위' : `✗ skill match 실패`);
|
|
1242
|
+
&& parsed.top.length > 0; // 1.36.80: 내장 카탈로그 축소 — 특정 id 고정 대신 매칭 존재만 단언
|
|
1243
|
+
console.log(ok ? '✓ B(1.9.45) skill match: jaccard 매칭 → 최상위 후보 반환' : `✗ skill match 실패`);
|
|
1245
1244
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1246
1245
|
}
|
|
1247
1246
|
|
|
@@ -1276,7 +1275,7 @@ total++;
|
|
|
1276
1275
|
const ok = r.status === 0
|
|
1277
1276
|
&& fs.existsSync(publishDir)
|
|
1278
1277
|
&& manifest
|
|
1279
|
-
&& manifest.skills && manifest.skills.length >=
|
|
1278
|
+
&& manifest.skills && manifest.skills.length >= 2 // 1.36.80: 내장 카탈로그 축소
|
|
1280
1279
|
&& manifest.format === 'agentskills.io';
|
|
1281
1280
|
console.log(ok ? `✓ B(1.9.47) skill publish: ${manifest ? manifest.skills.length : 0} skill + manifest 생성` : `✗ skill publish 실패`);
|
|
1282
1281
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
@@ -1308,7 +1307,7 @@ total++;
|
|
|
1308
1307
|
const exportDir = path.join(tmpC, '.harness', 'skills-export');
|
|
1309
1308
|
const exists2 = fs.existsSync(exportDir);
|
|
1310
1309
|
const count = exists2 ? fs.readdirSync(exportDir).length : 0;
|
|
1311
|
-
const ok = r.status === 0 && exists2 && count >=
|
|
1310
|
+
const ok = r.status === 0 && exists2 && count >= 2; // 1.36.80: 내장 카탈로그 축소(도메인 7종 제거)
|
|
1312
1311
|
console.log(ok ? `✓ B(1.9.43) skill export-all: ${count}개 skill 일괄 SKILL.md 생성` : `✗ export-all 실패 (count=${count})`);
|
|
1313
1312
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1314
1313
|
}
|
|
@@ -1393,13 +1392,13 @@ total++;
|
|
|
1393
1392
|
// skill export → SKILL.md frontmatter 정확 생성
|
|
1394
1393
|
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skex-'));
|
|
1395
1394
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1396
|
-
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export', '
|
|
1397
|
-
const skillFile = path.join(tmpC, '.harness', 'skills-export', '
|
|
1395
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export', 'feature-implementation', '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
1396
|
+
const skillFile = path.join(tmpC, '.harness', 'skills-export', 'feature-implementation', 'SKILL.md');
|
|
1398
1397
|
const exists2 = fs.existsSync(skillFile);
|
|
1399
1398
|
const body = exists2 ? fs.readFileSync(skillFile, 'utf8') : '';
|
|
1400
1399
|
const ok = r.status === 0
|
|
1401
1400
|
&& exists2
|
|
1402
|
-
&& /^---\nname:
|
|
1401
|
+
&& /^---\nname: feature-implementation\ndescription:/.test(body)
|
|
1403
1402
|
&& /\n---\n/.test(body);
|
|
1404
1403
|
console.log(ok ? '✓ B(1.9.42) skill export: agentskills.io 표준 SKILL.md frontmatter 생성' : `✗ export 실패`);
|
|
1405
1404
|
if (!ok) { failed++; console.log(body.slice(0, 300) || r.stdout.slice(0, 300)); }
|
|
@@ -1899,7 +1898,7 @@ total++;
|
|
|
1899
1898
|
// 비-TTY + --yes 시 multi-select prompt 안 띄움 → 통상 init 흐름
|
|
1900
1899
|
const ok = r.status === 0
|
|
1901
1900
|
&& /Leerness v/.test(r.stdout)
|
|
1902
|
-
&& /Skills:
|
|
1901
|
+
&& /Skills: (feature-implementation|project-roadmap-generator)/.test(r.stdout); // 1.36.80
|
|
1903
1902
|
console.log(ok ? '✓ B(1.9.34) multi-select 비-TTY 폴백: --yes로 default 사용' : `✗ multi-select 폴백 실패`);
|
|
1904
1903
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1905
1904
|
}
|
|
@@ -2380,7 +2379,7 @@ total++;
|
|
|
2380
2379
|
encoding: 'utf8',
|
|
2381
2380
|
env: Object.assign({}, process.env, { LEERNESS_SKILLPACK_PATH: '' })
|
|
2382
2381
|
});
|
|
2383
|
-
const ok = r.status === 0 && /builtin fallback/.test(r.stdout) && /\| builtin \|/.test(r.stdout);
|
|
2382
|
+
const ok = r.status === 0 && /builtin fallback/.test(r.stdout) && /\| (?:builtin|catalog\+local|catalog) \|/.test(r.stdout);
|
|
2384
2383
|
console.log(ok ? '✓ B(1.9.10) builtin fallback (skillpack 없을 때)' : '✗ builtin fallback 실패');
|
|
2385
2384
|
if (!ok) failed++;
|
|
2386
2385
|
}
|
|
@@ -4401,10 +4400,10 @@ total++;
|
|
|
4401
4400
|
try {
|
|
4402
4401
|
const c = require(path.resolve(__dirname, '..', 'lib', 'catalogs.js'));
|
|
4403
4402
|
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4404
|
-
const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length ===
|
|
4403
|
+
const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 2 && c.BUILTIN_CATALOG['feature-implementation'] && c.BUILTIN_CATALOG['feature-implementation'].version === '1.0.0';
|
|
4405
4404
|
const out = m._withBuiltinSource(c.BUILTIN_CATALOG);
|
|
4406
|
-
const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length ===
|
|
4407
|
-
&& Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out.
|
|
4405
|
+
const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length === 2
|
|
4406
|
+
&& Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out['feature-implementation'].capabilities)
|
|
4408
4407
|
&& Object.keys(m._withBuiltinSource(null)).length === 0;
|
|
4409
4408
|
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
|
|
4410
4409
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
@@ -4416,7 +4415,7 @@ total++;
|
|
|
4416
4415
|
cp.spawnSync(process.execPath, [CLI, 'init', sd, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
4417
4416
|
const sr = cp.spawnSync(process.execPath, [CLI, 'skill', 'list', '--path', sd], { encoding: 'utf8', timeout: 20000 });
|
|
4418
4417
|
const listOut = (sr.stdout || '') + (sr.stderr || '');
|
|
4419
|
-
const cmdOk = /
|
|
4418
|
+
const cmdOk = /feature-implementation|roadmap/i.test(listOut);
|
|
4420
4419
|
ok = work && movedOut && cmdOk;
|
|
4421
4420
|
fs.rmSync(sd, { recursive: true, force: true });
|
|
4422
4421
|
} catch {}
|
|
@@ -5094,7 +5093,7 @@ total++;
|
|
|
5094
5093
|
fs.rmSync(d, { recursive: true, force: true });
|
|
5095
5094
|
const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rec-'));
|
|
5096
5095
|
cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
5097
|
-
const recInstalled = fs.existsSync(path.join(d2, '.harness', 'skills', '
|
|
5096
|
+
const recInstalled = fs.existsSync(path.join(d2, '.harness', 'skills', 'feature-implementation')); // recommended → office 포함
|
|
5098
5097
|
fs.rmSync(d2, { recursive: true, force: true });
|
|
5099
5098
|
ok = skipAbsent && corePresent && recInstalled;
|
|
5100
5099
|
} catch {}
|
|
@@ -7317,7 +7316,7 @@ total++;
|
|
|
7317
7316
|
const i2 = cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'en', '--skills', 'recommended', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
7318
7317
|
const dirSet = fs.readdirSync(path.join(d, '.harness', 'skills'), { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
7319
7318
|
const lockSet = Object.keys(JSON.parse(fs.readFileSync(path.join(d, '.harness', 'skills-lock.json'), 'utf8')).installedSkills).sort();
|
|
7320
|
-
const f12Ok = i1.status === 0 && i2.status === 0 && dirSet.length
|
|
7319
|
+
const f12Ok = i1.status === 0 && i2.status === 0 && dirSet.length >= 2 && JSON.stringify(dirSet) === JSON.stringify(lockSet); // 1.36.80: 내장 축소 — 개수 하한 대신 집합 일치가 본질
|
|
7321
7320
|
// F9: 실 CLI migrate 경유 — 구판 KO canonical 을 en 프로젝트 CLAUDE.md 에 심고 migrate
|
|
7322
7321
|
const d9 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f9-')); _d.push(d9);
|
|
7323
7322
|
const i3 = cp.spawnSync(process.execPath, [CLI, 'init', d9, '--yes', '--language', 'en', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
@@ -7706,5 +7705,106 @@ total++;
|
|
|
7706
7705
|
if (!ok) failed++;
|
|
7707
7706
|
}
|
|
7708
7707
|
|
|
7708
|
+
// 1.36.80 (P-0001 검증기 캘리브레이션): 검증기를 신뢰하기 전에 탐지력을 실행으로 증명 — 적대적 FP 프로브 포함
|
|
7709
|
+
total++;
|
|
7710
|
+
{
|
|
7711
|
+
let ok = false;
|
|
7712
|
+
const _d = [];
|
|
7713
|
+
try {
|
|
7714
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ref-')); _d.push(d);
|
|
7715
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7716
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 60000, maxBuffer: 16 * 1024 * 1024 });
|
|
7717
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
7718
|
+
const N = (js) => `node -e "${js}"`;
|
|
7719
|
+
const add = (id, check, good, bad, expect) => { const a = ['referee', 'add', id, '--check', check, '--good', good, '--bad', bad]; if (expect) a.push('--expect-bad', expect); return R(a); };
|
|
7720
|
+
const verify = (id) => J(R(['referee', 'verify', id, '--json']));
|
|
7721
|
+
// 정상: good 통과 + bad 를 기대 사유로 거부 → 캘리브레이션 통과
|
|
7722
|
+
add('okref', N('process.exit(0)'), N('process.exit(0)'), N("console.log('AssertionError: x'); process.exit(1)"), 'AssertionError');
|
|
7723
|
+
const good = verify('okref');
|
|
7724
|
+
const happyOk = good && good.ok === true && good.goodExit === 0 && good.badExit === 1 && good.expectMatched === true;
|
|
7725
|
+
// FP 프로브 ①: known-good 이 이미 빨간 상태 → 거부(기준선 무효)
|
|
7726
|
+
add('redbase', N('process.exit(0)'), N('process.exit(3)'), N('process.exit(1)'), '');
|
|
7727
|
+
const p1 = verify('redbase');
|
|
7728
|
+
// FP 프로브 ②: known-bad 가 통과 → 탐지력 없음 거부
|
|
7729
|
+
add('nopower', N('process.exit(0)'), N('process.exit(0)'), N('process.exit(0)'), '');
|
|
7730
|
+
const p2 = verify('nopower');
|
|
7731
|
+
// FP 프로브 ③: bad 가 무관한 이유로 실패 → 사유 불일치 거부
|
|
7732
|
+
add('wrongreason', N('process.exit(0)'), N('process.exit(0)'), N("console.log('Error: cannot find module'); process.exit(1)"), 'AssertionError');
|
|
7733
|
+
const p3 = verify('wrongreason');
|
|
7734
|
+
const probesOk = p1 && p1.ok === false && p2 && p2.ok === false && p3 && p3.ok === false && p3.expectMatched === false;
|
|
7735
|
+
// 지문(stale): 캘리브레이션 후 명령이 바뀌면 신뢰 철회
|
|
7736
|
+
const store = path.join(d, '.harness', 'referees.json');
|
|
7737
|
+
const js = JSON.parse(fs.readFileSync(store, 'utf8'));
|
|
7738
|
+
js.find(x => x.id === 'okref').bad = N('process.exit(1)');
|
|
7739
|
+
fs.writeFileSync(store, JSON.stringify(js, null, 2));
|
|
7740
|
+
const shown = J(R(['referee', 'show', 'okref', '--json']));
|
|
7741
|
+
const staleOk = shown && shown.stale === true && shown.calibrated === false;
|
|
7742
|
+
// verify-claim 게이팅: 미검증/없는 referee 는 거부, 검증기 실패도 거부
|
|
7743
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', '작업', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7744
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'lib/referee.js 구현', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
7745
|
+
const missing = R(['verify-claim', 'T-0002', '--referee', 'nope', '--json']);
|
|
7746
|
+
const staleUse = R(['verify-claim', 'T-0002', '--referee', 'okref', '--json']); // 위에서 stale 로 만든 것
|
|
7747
|
+
add('failing', N('process.exit(2)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
|
|
7748
|
+
verify('failing');
|
|
7749
|
+
const refFail = R(['verify-claim', 'T-0002', '--referee', 'failing', '--json']);
|
|
7750
|
+
const gateOk = missing.status === 1 && (J(missing) || {}).code === 'referee_not_found'
|
|
7751
|
+
&& staleUse.status === 1 && (J(staleUse) || {}).code === 'referee_stale'
|
|
7752
|
+
&& refFail.status === 1 && (J(refFail) || {}).code === 'referee_failed';
|
|
7753
|
+
// 스토어 규율: 손상 위 변경 거부 + --json 순수
|
|
7754
|
+
const listPure = (() => { try { JSON.parse(R(['referee', 'list', '--json']).stdout); return true; } catch { return false; } })();
|
|
7755
|
+
fs.writeFileSync(store, '{ broken');
|
|
7756
|
+
const corrupt = R(['referee', 'add', 'x9', '--check', 'a', '--good', 'b', '--bad', 'c', '--json']);
|
|
7757
|
+
const storeOk = listPure && corrupt.status === 1 && (J(corrupt) || {}).code === 'store_invalid';
|
|
7758
|
+
ok = happyOk && probesOk && staleOk && gateOk && storeOk;
|
|
7759
|
+
if (!ok) console.log(` [referee 디버그] happy=${happyOk} probes=${probesOk} stale=${staleOk} gate=${gateOk} store=${storeOk}`);
|
|
7760
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7761
|
+
console.log(ok ? '✓ B(1.36.80) referee 캘리브레이션: 탐지력 증명(good통과+bad를 기대사유로 거부) · FP프로브 3종 거부 · 명령변경 stale · verify-claim 게이팅 · 스토어 규율' : '✗ referee 캘리브레이션 실패');
|
|
7762
|
+
if (!ok) failed++;
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
// 1.36.80 (UR-0067 라이브 미리보기 + P-0003 사다리 + 기본 스킬 정리)
|
|
7766
|
+
total++;
|
|
7767
|
+
{
|
|
7768
|
+
let ok = false;
|
|
7769
|
+
const _d = [];
|
|
7770
|
+
try {
|
|
7771
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pv80-')); _d.push(d);
|
|
7772
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7773
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a, '--path', d], { encoding: 'utf8', timeout: 30000, maxBuffer: 16 * 1024 * 1024 });
|
|
7774
|
+
const J = (r) => { try { return JSON.parse(r.stdout); } catch { return null; } };
|
|
7775
|
+
// 기본 스킬: 도메인 7종 제거 · 남은 2종 · 근거 없는 'passed' 주장 금지
|
|
7776
|
+
const CAT = require(path.resolve(path.dirname(CLI), '..', 'lib', 'catalogs.js')).BUILTIN_CATALOG;
|
|
7777
|
+
const skillOk = Object.keys(CAT).length === 2 && !CAT['commerce-api'] && CAT['feature-implementation']
|
|
7778
|
+
&& Object.values(CAT).every(v => v.verification !== 'passed');
|
|
7779
|
+
// 기존 설치분 보존(비파괴): 카탈로그에서 빠진 스킬도 락에 남는다
|
|
7780
|
+
fs.mkdirSync(path.join(d, '.harness', 'skills', 'commerce-api'), { recursive: true });
|
|
7781
|
+
fs.writeFileSync(path.join(d, '.harness', 'skills', 'commerce-api', 'skill.json'), '{"name":"commerce-api"}');
|
|
7782
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--skills', 'recommended', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7783
|
+
let keepOk = false;
|
|
7784
|
+
try { keepOk = Object.keys(JSON.parse(fs.readFileSync(path.join(d, '.harness', 'skills-lock.json'), 'utf8')).installedSkills).includes('commerce-api') && fs.existsSync(path.join(d, '.harness', 'skills', 'commerce-api')); } catch {}
|
|
7785
|
+
// 라이브 미리보기: mode 미설정 → 질문 노출, self 기본
|
|
7786
|
+
R(['preview', 'add', '새 페이지', '--design', '밝게']);
|
|
7787
|
+
R(['preview', 'mockup', 'P-0001']);
|
|
7788
|
+
const modeShow = R(['preview', 'mode']);
|
|
7789
|
+
const modeOk = /미리보기 모드가 아직/.test(modeShow.stdout) && (J(R(['preview', 'mode', '--json'])) || {}).effective === 'self';
|
|
7790
|
+
// project 모드(옵트인): 정적 디렉토리 배치 + 루트 밖 거부
|
|
7791
|
+
R(['preview', 'mode', 'project', '--static-dir', 'public', '--dev-url', 'http://localhost:5173']);
|
|
7792
|
+
const proj = J(R(['preview', 'serve', 'P-0001', '--json']));
|
|
7793
|
+
const projOk = proj && proj.mode === 'project' && /public\/leerness-preview-P-0001\.html/.test(proj.file || '')
|
|
7794
|
+
&& fs.existsSync(path.join(d, 'public', 'leerness-preview-P-0001.html'))
|
|
7795
|
+
&& R(['preview', 'serve', 'P-0001', '--static-dir', '../outside', '--json']).status === 1;
|
|
7796
|
+
// 사다리: 옵트인 + proceed 무영향 + 근거 없으면 unknown
|
|
7797
|
+
const base = J(R(['review-request', '테스트 요청', '--json']));
|
|
7798
|
+
const lad = J(R(['review-request', '테스트 요청', '--ladder', '--json']));
|
|
7799
|
+
const ladderOk = base && base.ladder === undefined && lad && lad.ladder && lad.ladder.rungs.length === 7
|
|
7800
|
+
&& lad.ladder.rungs.every(r => ['confirmed', 'candidate', 'unknown', 'not-applicable'].includes(r.status))
|
|
7801
|
+
&& lad.proceed === base.proceed && Array.isArray(lad.ladder.preserved) && lad.ladder.preserved.length >= 4;
|
|
7802
|
+
ok = skillOk && keepOk && modeOk && projOk && ladderOk;
|
|
7803
|
+
if (!ok) console.log(` [pv80 디버그] skill=${skillOk} keep=${keepOk} mode=${modeOk} proj=${projOk} ladder=${ladderOk}`);
|
|
7804
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7805
|
+
console.log(ok ? '✓ B(1.36.80) 기본스킬 정리(도메인7 제거·passed 주장 금지·기존설치 보존) · 라이브 미리보기 mode(self기본/project옵트인·루트밖 거부) · 사다리 옵트인(proceed 무영향)' : '✗ 1.36.80 미리보기/스킬/사다리 실패');
|
|
7806
|
+
if (!ok) failed++;
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7709
7809
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7710
7810
|
if (failed > 0) process.exit(1);
|