leerness 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +4 -4
- package/bin/leerness.js +22 -51
- package/lib/review-request.js +297 -289
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.15.0 — 2026-06-09 — 🛡️ [안정화/Stable] Karpathy 가이드라인 정렬 3부작 안정 minor
|
|
4
|
+
|
|
5
|
+
**🛡️ 안정화(Stable) minor.** Andrej Karpathy 코딩 가이드라인(생각하고 코딩 / 단순성 / 외과적 변경 / 목표 주도) 대비 외부 에이전트 검토에서 도출한 정렬 작업(1.14.1~1.14.3)을 검증·통합해 npm 공개. R-0011 정책의 6번째 minor. 영상은 HyperFrames 파이프라인 제작.
|
|
6
|
+
|
|
7
|
+
### 이번 minor 통합 (1.14.1~1.14.3) — Karpathy 4원칙 정렬
|
|
8
|
+
- **원칙1·2 (생각하고 코딩 / 단순성)**: `review-request` 가 요청의 **범위 과대 신호**(전체·모두·리팩토링·재구성·rewrite/refactor everything)와 **투기적 신호**(나중에·확장 가능·유연하게·범용화·추상화·future-proof)를 탐지해 "더 작게 쪼갤 수 있나? 요청 범위만" 환기(advisory). `--json simplicitySignals`.
|
|
9
|
+
- **원칙4 (목표 주도 — 검증가능한 완료)**: `plan add --done-when "<조건>"` 로 milestone 에 성공 기준을 1급 필드(`Done-When`)로 저장 · `plan list` 표시 + `--json doneWhen` · 미정 시 환기. (부수: `--done-when` 값이 제목에 흡수되던 잠복 버그 수정.)
|
|
10
|
+
- **원칙2 (자기적용)**: 정적 분석으로 호출 0·동적참조 0 인 **죽은 함수 5개 제거**(무행위변경). 함수 476→471.
|
|
11
|
+
|
|
12
|
+
### 검증 (회귀 0)
|
|
13
|
+
- **selftest 213 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. 각 기능 행위 재현(신호 탐지/오탐0, done-when 저장·표시, dead 제거 후 재스캔 0).
|
|
14
|
+
|
|
15
|
+
### 안정화 표시 (R-0006)
|
|
16
|
+
CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
|
|
17
|
+
|
|
18
|
+
## 1.14.3 — 2026-06-09 — Karpathy 정렬④(완결): 자체 단순화 — 죽은 코드 제거 (원칙2)
|
|
19
|
+
|
|
20
|
+
**🧹 leerness 가 자신의 원칙2(단순성)를 자신에게 적용.** 정적 분석으로 "정의됐으나 호출 0 + 동적(문자열) 참조 0" 인 함수만 골라 제거 — 무행위변경(검증가능) 단순화.
|
|
21
|
+
|
|
22
|
+
### 변경 (UR-0033)
|
|
23
|
+
- **죽은 함수 5개 제거**: `_isAutoLoopActive` · `_invalidateSkillsCache` · `_currentLang` · `_typewrite` · `_writeDomainCatalog` (전부 호출처 0, 과거 리팩토링/대체로 고립된 잔재). 함수 476→471, bin 19561줄.
|
|
24
|
+
- 제거 후 **재스캔 = 새 고아 0** (각 함수가 쓰던 헬퍼는 다른 곳에서도 사용 중 — 연쇄 dead 없음).
|
|
25
|
+
- 막연한 "큰 파일 줄이기"가 아닌, **검증가능·무행위변경** 범위로 한정(Karpathy 원칙3 외과적 변경 준수).
|
|
26
|
+
|
|
27
|
+
### 검증 (회귀 0)
|
|
28
|
+
- **selftest 213 PASS** · **E2E 365/365 PASS** (행위 동일 — dead 코드라 출력/동작 불변). 제거 전 각 후보의 전체 참조(bin+lib+scripts+test)=정의 1줄만 직접 확인(맹신 X).
|
|
29
|
+
- patch(1.14.3) — npm 미배포(R-0011, GitHub). **Karpathy 백로그(UR-0031/0032/0033) 완결.**
|
|
30
|
+
|
|
31
|
+
## 1.14.2 — 2026-06-09 — Karpathy 정렬③: plan --done-when 검증가능 완료조건 (원칙4)
|
|
32
|
+
|
|
33
|
+
**🎯 milestone 에 "성공 기준"을 1급 필드로.** Karpathy 원칙4(목표 주도 실행 — 검증가능한 완료 정의)의 빠진 절반. plan 에 done-criteria 개념이 없어 "언제 끝인지" 가 모호했던 것을 보강.
|
|
34
|
+
|
|
35
|
+
### 변경 (UR-0032)
|
|
36
|
+
- **plan add `--done-when "<조건>"`**: milestone 에 `Done-When:` 라인으로 검증가능 완료조건 저장. 미지정 시 `(미정)`.
|
|
37
|
+
- **plan list 표시 + `--json doneWhen`**: 각 milestone 의 완료기준 노출. 누락(legacy/미정) 시 `⚠ 미정 — --done-when 권장 (Karpathy 원칙4)` 환기.
|
|
38
|
+
- **🐛 잠복 버그 수정(맹신 X)**: `nonFlagArgs()` 의 value-flag 집합(`withValue`)에 `--done-when` 이 없어, 값이 positional 로 누출돼 **milestone 제목에 흡수**(예: "결제 연동" → "결제 연동 Stripe e2e…")되던 것 차단. 행위 재현으로 발견·수정.
|
|
39
|
+
|
|
40
|
+
### 검증 (회귀 0)
|
|
41
|
+
- **selftest 212→213**, 행위: `plan add "결제 연동" --done-when "Stripe e2e 통과"` → 제목="결제 연동"(흡수 없음)·Done-When 분리 저장·plan list/json 노출; 미지정 → (미정)+환기.
|
|
42
|
+
- patch(1.14.2) — npm 미배포(R-0011, GitHub). 잔여 Karpathy: UR-0033(자체 단순화).
|
|
43
|
+
|
|
44
|
+
## 1.14.1 — 2026-06-09 — Karpathy 정렬②: review-request 범위과대/투기적 신호 (원칙1+2)
|
|
45
|
+
|
|
46
|
+
**🤔 사전 검토 게이트에 "생각하고 코딩" + "단순성 우선" 신호 추가.** Karpathy 리뷰가 가장 약한 원칙으로 꼽은 1(트레이드오프 표면화)·2(단순성)를, 가장 많이 쓰는 `review-request`(작업 전 자동 호출)에서 보강.
|
|
47
|
+
|
|
48
|
+
### 변경 (UR-0031)
|
|
49
|
+
- **review-request 단순성/범위 신호**: 요청 텍스트에서 **범위 과대 동사**(전체·모두·리팩토링·재구성·rewrite/refactor everything 등)와 **투기적 신호**(나중에·확장 가능·유연하게·범용화·추상화·future-proof 등)를 탐지해 `efficiencyHints` + `--json simplicitySignals` 로 표면화. "더 작게 쪼갤 수 있나? 요청 범위만" 환기. advisory(차단 X — 표면화만, Karpathy 원칙1).
|
|
50
|
+
- 신규 명령 0 — 기존 명령 확장(leerness 자신의 원칙2 준수).
|
|
51
|
+
|
|
52
|
+
### 검증 (회귀 0)
|
|
53
|
+
- **selftest 211→212**, 행위 재현: "전체 코드베이스 리팩토링 + 나중에 유연하게 확장 가능하게 추상화" → broad=[전체,리팩토링] spec=[나중에,유연하게,확장 가능,추상화]; 단순 요청 → 0(오탐 없음).
|
|
54
|
+
- patch(1.14.1) — R-0011 정책상 npm 미배포(GitHub). 잔여 Karpathy 백로그: UR-0032(plan --done-when 성공기준), UR-0033(자체 단순화).
|
|
55
|
+
|
|
3
56
|
## 1.14.0 — 2026-06-09 — 🛡️ [안정화/Stable] 블라인드 리뷰 수정 + Karpathy 정렬 안정 minor
|
|
4
57
|
|
|
5
58
|
**🛡️ 안정화(Stable) minor.** 블라인드 3-모델 리뷰(codex/Sonnet/Opus) 수정 + Karpathy 가이드라인 정렬(1.13.1~1.13.2)을 검증·통합해 npm 공개. R-0011 정책의 5번째 minor. (이 릴리스의 소개 영상부터 **HyperFrames 파이프라인**으로 자동 제작됩니다.)
|
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ MIT
|
|
|
186
186
|
<!-- leerness:project-readme:start -->
|
|
187
187
|
## Leerness Project Harness
|
|
188
188
|
|
|
189
|
-
이 프로젝트는 Leerness v1.
|
|
189
|
+
이 프로젝트는 Leerness v1.15.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
190
190
|
|
|
191
191
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
192
192
|
|
|
@@ -240,7 +240,7 @@ leerness memory restore decision <date|title>
|
|
|
240
240
|
|
|
241
241
|
### MCP server (외부 AI 통합)
|
|
242
242
|
|
|
243
|
-
Leerness v1.
|
|
243
|
+
Leerness v1.15.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
244
244
|
|
|
245
245
|
```jsonc
|
|
246
246
|
// 카테고리별
|
|
@@ -261,7 +261,7 @@ Leerness v1.14.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
|
|
|
261
261
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
262
262
|
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) 다음 라운드 예약.
|
|
263
263
|
|
|
264
|
-
현재 누적: **70 라운드 (1.9.40 → 1.
|
|
264
|
+
현재 누적: **70 라운드 (1.9.40 → 1.15.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
265
265
|
|
|
266
266
|
### 성능 가이드 (1.9.140 측정)
|
|
267
267
|
|
|
@@ -299,6 +299,6 @@ leerness release pack --close --auto-main-push
|
|
|
299
299
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
300
300
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
301
301
|
|
|
302
|
-
Last synced by Leerness v1.
|
|
302
|
+
Last synced by Leerness v1.15.0: 2026-06-09
|
|
303
303
|
<!-- leerness:project-readme:end -->
|
|
304
304
|
|
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
|
|
35
|
-
const VERSION = '1.
|
|
35
|
+
const VERSION = '1.15.0';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -190,12 +190,6 @@ function _withLock(targetPath, fn, opts = {}) {
|
|
|
190
190
|
}
|
|
191
191
|
// 1.9.327 (UR-0025): _getLocalTz / _formatLocal → lib/pure-utils.js 로 이동 (순수 TZ/날짜 포맷, require 사용).
|
|
192
192
|
// 자동 모드 활성 여부 (R-XXXX every-round 룰 존재 시 true)
|
|
193
|
-
function _isAutoLoopActive(root) {
|
|
194
|
-
try {
|
|
195
|
-
const rules = readRules(root);
|
|
196
|
-
return rules.some(r => r.status === 'active' && /every-round|every-session/i.test(r.trigger || ''));
|
|
197
|
-
} catch { return false; }
|
|
198
|
-
}
|
|
199
193
|
function _getAutoLoopRule(root) {
|
|
200
194
|
try {
|
|
201
195
|
return readRules(root).find(r => r.status === 'active' && /every-round/i.test(r.trigger || '')) || null;
|
|
@@ -222,7 +216,7 @@ function _resolveRoot(positional) {
|
|
|
222
216
|
}
|
|
223
217
|
function nonFlagArgs() {
|
|
224
218
|
const out = [];
|
|
225
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version']);
|
|
219
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단
|
|
226
220
|
const a = process.argv.slice(2);
|
|
227
221
|
for (let i = 0; i < a.length; i++) {
|
|
228
222
|
const x = a[i];
|
|
@@ -1257,10 +1251,6 @@ function _buildAllSkills(root) {
|
|
|
1257
1251
|
return out;
|
|
1258
1252
|
}
|
|
1259
1253
|
// 1.9.66: skill 추가/제거 시 캐시 invalidate (외부 helper)
|
|
1260
|
-
function _invalidateSkillsCache(root) {
|
|
1261
|
-
try { _SKILLS_LIST_CACHE.delete(absRoot(root)); } catch {}
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
1254
|
function skillList(root) {
|
|
1265
1255
|
const all = listAllSkills(root);
|
|
1266
1256
|
// 1.9.84: --json 옵션 (MCP 통합용)
|
|
@@ -1862,42 +1852,11 @@ async function skillAutoCacheCmd(root, sub) {
|
|
|
1862
1852
|
// lang 결정: explicit > .harness/LANGUAGE > LEERNESS_LANG env > 'ko' (default)
|
|
1863
1853
|
// 1.9.338 (UR-0025 심층): STRINGS (i18n ko/en catalog) 는 lib/catalogs.js 로 이전 (import). _t 는 _translate(STRINGS,..) 박막.
|
|
1864
1854
|
// 현재 사용 언어 결정 (env > config > 'ko')
|
|
1865
|
-
function _currentLang(root) {
|
|
1866
|
-
if (process.env.LEERNESS_LANG) return process.env.LEERNESS_LANG === 'en' ? 'en' : 'ko';
|
|
1867
|
-
try {
|
|
1868
|
-
if (root) {
|
|
1869
|
-
const fp = path.join(root, '.harness', 'LANGUAGE');
|
|
1870
|
-
if (exists(fp)) {
|
|
1871
|
-
const v = read(fp).trim().toLowerCase();
|
|
1872
|
-
if (v === 'en' || v === 'english') return 'en';
|
|
1873
|
-
if (v === 'ko' || v === 'korean' || v === 'kr') return 'ko';
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
} catch {}
|
|
1877
|
-
return 'ko'; // default
|
|
1878
|
-
}
|
|
1879
1855
|
// 1.9.338 (UR-0025 심층): 순수 _translate(STRINGS, key, lang) (lib/pure-utils) 박막 — STRINGS catalog 주입.
|
|
1880
1856
|
function _t(key, lang) {
|
|
1881
1857
|
return _translate(STRINGS, key, lang);
|
|
1882
1858
|
}
|
|
1883
1859
|
|
|
1884
|
-
// 1.9.206: UI/UX 개선 — typewriter / fade-in 효과 (opt-in via LEERNESS_TYPEWRITER=1)
|
|
1885
|
-
function _typewrite(text, delayMs) {
|
|
1886
|
-
delayMs = delayMs || 15;
|
|
1887
|
-
if (process.env.LEERNESS_TYPEWRITER !== '1' || !process.stdout.isTTY) {
|
|
1888
|
-
process.stdout.write(text);
|
|
1889
|
-
return Promise.resolve();
|
|
1890
|
-
}
|
|
1891
|
-
return new Promise((resolve) => {
|
|
1892
|
-
let i = 0;
|
|
1893
|
-
const step = () => {
|
|
1894
|
-
if (i >= text.length) return resolve();
|
|
1895
|
-
process.stdout.write(text[i++]);
|
|
1896
|
-
setTimeout(step, delayMs);
|
|
1897
|
-
};
|
|
1898
|
-
step();
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
1860
|
// 색상 helper (TTY 시 ANSI, 비-TTY 시 plain)
|
|
1902
1861
|
const _ui = {
|
|
1903
1862
|
bold: s => process.stdout.isTTY ? `\x1b[1m${s}\x1b[0m` : s,
|
|
@@ -3566,6 +3525,20 @@ function _selfTestCases() {
|
|
|
3566
3525
|
const src = read(__filename);
|
|
3567
3526
|
return src.includes('const changedNotClaimed = gitApplicable') && src.includes('files.some(f => _claimFileInGit(f, new Set([g])))') && src.includes('scopeCreep:') && src.includes('외과적 변경 점검');
|
|
3568
3527
|
} },
|
|
3528
|
+
{ name: 'Karpathy 가이드라인1+2 (UR-0031): review-request 범위과대/투기적 신호 표면화 (1.14.1)', run: () => {
|
|
3529
|
+
const m = require('../lib/review-request');
|
|
3530
|
+
const rr = read(path.join(path.dirname(__filename), '..', 'lib', 'review-request.js'));
|
|
3531
|
+
const wired = rr.includes('const simplicitySignals = { broad: broadHits, speculative: specHits }') && rr.includes('범위 과대 신호') && rr.includes('투기적 신호') && rr.includes('simplicitySignals,');
|
|
3532
|
+
return typeof m.reviewRequestCmd === 'function' && wired;
|
|
3533
|
+
} },
|
|
3534
|
+
{ name: 'Karpathy 가이드라인4 (UR-0032): plan --done-when 검증가능 완료조건 저장/파싱/표시 (1.14.2)', run: () => {
|
|
3535
|
+
const src = read(__filename);
|
|
3536
|
+
const wired = src.includes("const doneWhen = _lineSafe(arg('--done-when', '') || '(미정)')") && src.includes('Done-When: ${doneWhen}') && src.includes("const doneWhenMatch = b.match(/^Done-When:") && src.includes('doneWhen: doneWhenMatch ? doneWhenMatch[1].trim() : null')
|
|
3537
|
+
&& src.includes("'--ps-version','--done-when'"); // 잠복버그 회귀가드: nonFlagArgs withValue 에 --done-when (제목 흡수 차단)
|
|
3538
|
+
const b = '### M-0001. 로그인\nStatus: planned\nProgress: 0%\nDone-When: 로그인 e2e 테스트 통과\n\nTasks:\n- [ ] x\n';
|
|
3539
|
+
const dw = (b.match(/^Done-When:\s*(.+)$/m) || [])[1];
|
|
3540
|
+
return wired && dw === '로그인 e2e 테스트 통과';
|
|
3541
|
+
} },
|
|
3569
3542
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3570
3543
|
];
|
|
3571
3544
|
}
|
|
@@ -4810,13 +4783,6 @@ function _loadDomainCatalog(root) {
|
|
|
4810
4783
|
return merged;
|
|
4811
4784
|
} catch { return _DEFAULT_DOMAIN_CATALOG; }
|
|
4812
4785
|
}
|
|
4813
|
-
function _writeDomainCatalog(root, catalog) {
|
|
4814
|
-
try {
|
|
4815
|
-
mkdirp(path.join(root, '.harness'));
|
|
4816
|
-
writeUtf8(_domainCatalogPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
|
|
4817
|
-
return true;
|
|
4818
|
-
} catch { return false; }
|
|
4819
|
-
}
|
|
4820
4786
|
// 1.9.325 (UR-0025): _classifyIntent → lib/pure-utils.js 로 이동 (순수 intent 분류, require 사용).
|
|
4821
4787
|
function _detectDomain(text, root) {
|
|
4822
4788
|
return _matchDomain(_loadDomainCatalog(root), text);
|
|
@@ -6467,6 +6433,7 @@ function planListCmd(root, opts = {}) {
|
|
|
6467
6433
|
const title = headerMatch[2].trim();
|
|
6468
6434
|
const statusMatch = b.match(/^Status:\s*(.+)$/m);
|
|
6469
6435
|
const progressMatch = b.match(/^Progress:\s*(.+)$/m);
|
|
6436
|
+
const doneWhenMatch = b.match(/^Done-When:\s*(.+)$/m); // 1.14.2 (Karpathy 원칙4, UR-0032): 검증가능 완료조건
|
|
6470
6437
|
// Tasks 블록 (- [ ] 또는 - [x])
|
|
6471
6438
|
const tasks = [];
|
|
6472
6439
|
const tasksSection = b.match(/Tasks:\s*\n([\s\S]+?)(?=\n###|\n## |$)/);
|
|
@@ -6481,6 +6448,7 @@ function planListCmd(root, opts = {}) {
|
|
|
6481
6448
|
title,
|
|
6482
6449
|
status: statusMatch ? statusMatch[1].trim() : null,
|
|
6483
6450
|
progress: progressMatch ? progressMatch[1].trim() : null,
|
|
6451
|
+
doneWhen: doneWhenMatch ? doneWhenMatch[1].trim() : null,
|
|
6484
6452
|
tasks,
|
|
6485
6453
|
});
|
|
6486
6454
|
}
|
|
@@ -6495,6 +6463,7 @@ function planListCmd(root, opts = {}) {
|
|
|
6495
6463
|
log(`\n[${m.id}] ${m.title}`);
|
|
6496
6464
|
if (m.status) log(` Status: ${m.status}`);
|
|
6497
6465
|
if (m.progress) log(` Progress: ${m.progress}`);
|
|
6466
|
+
log(` 완료기준(Done-When): ${m.doneWhen || '⚠ 미정 — plan add ... --done-when "<검증가능 조건>" 권장 (Karpathy 원칙4)'}`);
|
|
6498
6467
|
if (m.tasks.length) log(` Tasks: ${m.tasks.length}개 (${m.tasks.filter(t => t.done).length} 완료)`);
|
|
6499
6468
|
}
|
|
6500
6469
|
}
|
|
@@ -6503,10 +6472,12 @@ function planAdd(root, text) {
|
|
|
6503
6472
|
if (!_requireInit(root, 'plan add')) return; // 1.9.311 (UR-0047): init 가드
|
|
6504
6473
|
if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'plan status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
|
|
6505
6474
|
const status = arg('--status','planned'), progress = arg('--progress','0'), nextAction = arg('--next', '다음 액션 작성');
|
|
6475
|
+
// 1.14.2 (Karpathy 원칙4 "성공기준 정의", UR-0032): --done-when 으로 검증가능 완료조건을 milestone 에 기록. 미지정 시 (미정) — plan show/audit 가 환기.
|
|
6476
|
+
const doneWhen = _lineSafe(arg('--done-when', '') || '(미정)');
|
|
6506
6477
|
// 1.9.303 (UR-0043): M-id append + T-id upsert 를 하나의 락으로 — 동시 plan add ID 충돌 방지.
|
|
6507
6478
|
const { id, tid } = _withLock(progressPath(root), () => {
|
|
6508
6479
|
const id = nextId(root, 'M');
|
|
6509
|
-
append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\n\nTasks:\n- [ ] ${text}\n`);
|
|
6480
|
+
append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\nDone-When: ${doneWhen}\n\nTasks:\n- [ ] ${text}\n`);
|
|
6510
6481
|
const tid = nextId(root, 'T');
|
|
6511
6482
|
upsertProgress(root, { id: tid, status, request: text, evidence: `plan:${id}`, nextAction });
|
|
6512
6483
|
return { id, tid };
|
package/lib/review-request.js
CHANGED
|
@@ -1,289 +1,297 @@
|
|
|
1
|
-
// lib/review-request.js — review-request 핸들러 (UR-0125 큰 핸들러 모듈화, 1.9.420)
|
|
2
|
-
// bin/leerness.js 에서 reviewRequestCmd(277줄) 분리. DI: harness 고유 의존(has · harnessPath · _checkRequestConstraints · _recordRun) 주입.
|
|
3
|
-
// io 프리미티브는 ./io, cp/path 는 빌트인. 동작/출력 무변경(thin wrapper 위임).
|
|
4
|
-
'use strict';
|
|
5
|
-
const cp = require('child_process');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { absRoot, exists, read, log, fail, failJson } = require('./io');
|
|
8
|
-
|
|
9
|
-
function reviewRequestCmd(root, request, deps = {}) {
|
|
10
|
-
const { has, harnessPath, _checkRequestConstraints, _recordRun } = deps;
|
|
11
|
-
root = absRoot(root || process.cwd());
|
|
12
|
-
if (!request || !String(request).trim()) {
|
|
13
|
-
// 1.9.428 (10th 외부평가 UR-0128): --json 오류 경로도 순수 JSON (failJson 이 모드 분기)
|
|
14
|
-
return failJson(!!(has && has('--json')), 'review_request_empty', 'leerness review-request "<request>" — 사용자 요청 텍스트 필요');
|
|
15
|
-
}
|
|
16
|
-
const t0 = Date.now();
|
|
17
|
-
const text = String(request).trim();
|
|
18
|
-
|
|
19
|
-
// 1) 작업 유형 추정 (route 기반 키워드 매핑)
|
|
20
|
-
const lower = text.toLowerCase();
|
|
21
|
-
const routeKw = {
|
|
22
|
-
bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
|
|
23
|
-
refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
|
|
24
|
-
feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
|
|
25
|
-
research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
|
|
26
|
-
planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
|
|
27
|
-
release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
|
|
28
|
-
consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
|
|
29
|
-
};
|
|
30
|
-
let estimatedType = 'feature'; // default
|
|
31
|
-
let maxScore = 0;
|
|
32
|
-
for (const [type, kws] of Object.entries(routeKw)) {
|
|
33
|
-
const score = kws.filter(k => lower.includes(k)).length;
|
|
34
|
-
if (score > maxScore) { maxScore = score; estimatedType = type; }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
|
|
38
|
-
const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
|
|
39
|
-
const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
|
|
40
|
-
const lessonsRecall = []; // 🧠 과거 lesson
|
|
41
|
-
const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
|
|
42
|
-
|
|
43
|
-
// brainstorm 호출 (1.9.13~) — JSON 결과 회수
|
|
44
|
-
try {
|
|
45
|
-
const r = cp.spawnSync(process.execPath, [harnessPath, 'brainstorm', text, '--path', root, '--json'], {
|
|
46
|
-
encoding: 'utf8', timeout: 12000,
|
|
47
|
-
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
48
|
-
});
|
|
49
|
-
if (r.stdout) {
|
|
50
|
-
const j = JSON.parse(r.stdout);
|
|
51
|
-
const hits = j.hits || {};
|
|
52
|
-
// decisions — 과거 결정 후보
|
|
53
|
-
(hits.decisions || []).slice(0, 5).forEach(d => {
|
|
54
|
-
lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
|
|
55
|
-
});
|
|
56
|
-
// lessons — 과거 교훈 (특히 실패 키워드)
|
|
57
|
-
(hits.lessons || []).slice(0, 5).forEach(l => {
|
|
58
|
-
const preview = (l.text || l.preview || '').slice(0, 100);
|
|
59
|
-
const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
|
|
60
|
-
if (isFailure) {
|
|
61
|
-
conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
|
|
62
|
-
} else {
|
|
63
|
-
lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
// skills — 기존 skill 후보
|
|
67
|
-
(hits.skills || []).slice(0, 3).forEach(s => {
|
|
68
|
-
reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
|
|
69
|
-
});
|
|
70
|
-
// tasks — 진행 중 task 충돌
|
|
71
|
-
(hits.tasks || []).slice(0, 3).forEach(tsk => {
|
|
72
|
-
if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
|
|
73
|
-
conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
// plan milestones — 진행 중 milestone
|
|
77
|
-
(hits.planMilestones || []).slice(0, 3).forEach(m => {
|
|
78
|
-
if (m.status && /in-progress|진행/.test(String(m.status))) {
|
|
79
|
-
planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
// taskLogFails — 과거 같은 키워드 실패 흔적
|
|
83
|
-
(hits.taskLogFails || []).slice(0, 3).forEach(f => {
|
|
84
|
-
conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
} catch {}
|
|
88
|
-
|
|
89
|
-
// 3) reuse-map 매칭 — 기존 capability 등록 후보
|
|
90
|
-
try {
|
|
91
|
-
const reusePath = path.join(root, '.harness/reuse-map.md');
|
|
92
|
-
if (exists(reusePath)) {
|
|
93
|
-
const reuseLines = read(reusePath).split('\n');
|
|
94
|
-
const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
|
|
95
|
-
for (const line of reuseLines) {
|
|
96
|
-
if (!/^\| /.test(line)) continue; // 테이블 row만
|
|
97
|
-
const ll = line.toLowerCase();
|
|
98
|
-
const matched = tokens.filter(t => ll.includes(t)).length;
|
|
99
|
-
if (matched > 0) {
|
|
100
|
-
const cols = line.split('|').map(s => s.trim());
|
|
101
|
-
if (cols[1]) {
|
|
102
|
-
reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch {}
|
|
108
|
-
|
|
109
|
-
// 4) feature_graph — 같은 영역 변경 가능성
|
|
110
|
-
const featureConflicts = [];
|
|
111
|
-
try {
|
|
112
|
-
const fgPath = path.join(root, '.harness/feature_graph.md');
|
|
113
|
-
if (exists(fgPath)) {
|
|
114
|
-
const fg = read(fgPath);
|
|
115
|
-
const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
|
|
116
|
-
// F-XXXX 노드 라인 추출
|
|
117
|
-
const nodeBlocks = fg.split(/\n### /);
|
|
118
|
-
for (const blk of nodeBlocks.slice(1)) {
|
|
119
|
-
const bl = blk.toLowerCase();
|
|
120
|
-
const matched = tokens.filter(t => bl.includes(t)).length;
|
|
121
|
-
if (matched > 0) {
|
|
122
|
-
const titleMatch = blk.match(/^([^\n]+)/);
|
|
123
|
-
const idMatch = blk.match(/F-\d+/);
|
|
124
|
-
if (titleMatch && idMatch) {
|
|
125
|
-
featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} catch {}
|
|
131
|
-
|
|
132
|
-
// 5) 권장 단계 (작업 유형별)
|
|
133
|
-
const recommendedSteps = {
|
|
134
|
-
feature: [
|
|
135
|
-
'1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
|
|
136
|
-
'2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
|
|
137
|
-
'3) leerness plan add "<milestone>" — 진행 추적',
|
|
138
|
-
'4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
|
|
139
|
-
'5) verify-claim --run-tests 로 evidence 의무화'
|
|
140
|
-
],
|
|
141
|
-
bugfix: [
|
|
142
|
-
'1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
|
|
143
|
-
'2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
|
|
144
|
-
'3) verify-code --run-tests — 재현 + fix 검증',
|
|
145
|
-
'4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
|
|
146
|
-
],
|
|
147
|
-
refactor: [
|
|
148
|
-
'1) leerness reuse-map — 영향 범위 파악',
|
|
149
|
-
'2) leerness impact <file> — 강한/약한 참조 분리',
|
|
150
|
-
'3) leerness contract verify — 외부 인터페이스 보존 확인',
|
|
151
|
-
'4) verify-code --run-tests + 회귀 테스트'
|
|
152
|
-
],
|
|
153
|
-
research: [
|
|
154
|
-
'1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
|
|
155
|
-
'2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
|
|
156
|
-
'3) leerness review <file> --persona research — 깊이 검토',
|
|
157
|
-
'4) leerness decision add "<결론>" — 회수 가능하게 영구화'
|
|
158
|
-
],
|
|
159
|
-
planning: [
|
|
160
|
-
'1) leerness plan add "<milestone>" — 분해 시작',
|
|
161
|
-
'2) leerness reuse-map — 기존 자원 인벤토리',
|
|
162
|
-
'3) leerness agents recommend planning — sub-agent 분배',
|
|
163
|
-
'4) leerness session close — 결정 영구화'
|
|
164
|
-
],
|
|
165
|
-
release: [
|
|
166
|
-
'1) leerness health — production-ready 확인',
|
|
167
|
-
'2) leerness audit + verify-code — 보안 + 검수',
|
|
168
|
-
'3) leerness release bump + note + publish'
|
|
169
|
-
],
|
|
170
|
-
consistency: [
|
|
171
|
-
'1) leerness audit — design/reuse/handoff 일관성 검사',
|
|
172
|
-
'2) leerness consistency check — 잠재 일관성 위반',
|
|
173
|
-
'3) leerness drift check --auto-fix — 자동 회복'
|
|
174
|
-
]
|
|
175
|
-
}[estimatedType] || [];
|
|
176
|
-
|
|
177
|
-
// 6) 효율 제안 (적용 가능한 sub-agent + skill)
|
|
178
|
-
const efficiencyHints = [];
|
|
179
|
-
if (reuseCandidates.length > 0) {
|
|
180
|
-
efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
|
|
181
|
-
}
|
|
182
|
-
if (conflictHints.length > 0) {
|
|
183
|
-
efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
|
|
184
|
-
}
|
|
185
|
-
if (planConflicts.length > 0) {
|
|
186
|
-
efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
|
|
187
|
-
}
|
|
188
|
-
if (featureConflicts.length > 0) {
|
|
189
|
-
efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
|
|
190
|
-
}
|
|
191
|
-
// 다중 에이전트 분배 추천
|
|
192
|
-
if (estimatedType === 'feature' || estimatedType === 'planning') {
|
|
193
|
-
efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
|
|
194
|
-
}
|
|
195
|
-
if (efficiencyHints.length === 0) {
|
|
196
|
-
efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
|
|
200
|
-
let constraintsCheck = { matched: [], suggestions: [] };
|
|
201
|
-
try {
|
|
202
|
-
constraintsCheck = _checkRequestConstraints(root, text);
|
|
203
|
-
if (constraintsCheck.matched.length > 0) {
|
|
204
|
-
efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
|
|
205
|
-
}
|
|
206
|
-
} catch {}
|
|
207
|
-
|
|
208
|
-
// 7)
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
251
|
-
log('');
|
|
252
|
-
}
|
|
253
|
-
if (
|
|
254
|
-
log(`##
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
log(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
log(
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
log(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1
|
+
// lib/review-request.js — review-request 핸들러 (UR-0125 큰 핸들러 모듈화, 1.9.420)
|
|
2
|
+
// bin/leerness.js 에서 reviewRequestCmd(277줄) 분리. DI: harness 고유 의존(has · harnessPath · _checkRequestConstraints · _recordRun) 주입.
|
|
3
|
+
// io 프리미티브는 ./io, cp/path 는 빌트인. 동작/출력 무변경(thin wrapper 위임).
|
|
4
|
+
'use strict';
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { absRoot, exists, read, log, fail, failJson } = require('./io');
|
|
8
|
+
|
|
9
|
+
function reviewRequestCmd(root, request, deps = {}) {
|
|
10
|
+
const { has, harnessPath, _checkRequestConstraints, _recordRun } = deps;
|
|
11
|
+
root = absRoot(root || process.cwd());
|
|
12
|
+
if (!request || !String(request).trim()) {
|
|
13
|
+
// 1.9.428 (10th 외부평가 UR-0128): --json 오류 경로도 순수 JSON (failJson 이 모드 분기)
|
|
14
|
+
return failJson(!!(has && has('--json')), 'review_request_empty', 'leerness review-request "<request>" — 사용자 요청 텍스트 필요');
|
|
15
|
+
}
|
|
16
|
+
const t0 = Date.now();
|
|
17
|
+
const text = String(request).trim();
|
|
18
|
+
|
|
19
|
+
// 1) 작업 유형 추정 (route 기반 키워드 매핑)
|
|
20
|
+
const lower = text.toLowerCase();
|
|
21
|
+
const routeKw = {
|
|
22
|
+
bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
|
|
23
|
+
refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
|
|
24
|
+
feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
|
|
25
|
+
research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
|
|
26
|
+
planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
|
|
27
|
+
release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
|
|
28
|
+
consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
|
|
29
|
+
};
|
|
30
|
+
let estimatedType = 'feature'; // default
|
|
31
|
+
let maxScore = 0;
|
|
32
|
+
for (const [type, kws] of Object.entries(routeKw)) {
|
|
33
|
+
const score = kws.filter(k => lower.includes(k)).length;
|
|
34
|
+
if (score > maxScore) { maxScore = score; estimatedType = type; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
|
|
38
|
+
const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
|
|
39
|
+
const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
|
|
40
|
+
const lessonsRecall = []; // 🧠 과거 lesson
|
|
41
|
+
const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
|
|
42
|
+
|
|
43
|
+
// brainstorm 호출 (1.9.13~) — JSON 결과 회수
|
|
44
|
+
try {
|
|
45
|
+
const r = cp.spawnSync(process.execPath, [harnessPath, 'brainstorm', text, '--path', root, '--json'], {
|
|
46
|
+
encoding: 'utf8', timeout: 12000,
|
|
47
|
+
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
48
|
+
});
|
|
49
|
+
if (r.stdout) {
|
|
50
|
+
const j = JSON.parse(r.stdout);
|
|
51
|
+
const hits = j.hits || {};
|
|
52
|
+
// decisions — 과거 결정 후보
|
|
53
|
+
(hits.decisions || []).slice(0, 5).forEach(d => {
|
|
54
|
+
lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
|
|
55
|
+
});
|
|
56
|
+
// lessons — 과거 교훈 (특히 실패 키워드)
|
|
57
|
+
(hits.lessons || []).slice(0, 5).forEach(l => {
|
|
58
|
+
const preview = (l.text || l.preview || '').slice(0, 100);
|
|
59
|
+
const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
|
|
60
|
+
if (isFailure) {
|
|
61
|
+
conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
|
|
62
|
+
} else {
|
|
63
|
+
lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// skills — 기존 skill 후보
|
|
67
|
+
(hits.skills || []).slice(0, 3).forEach(s => {
|
|
68
|
+
reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
|
|
69
|
+
});
|
|
70
|
+
// tasks — 진행 중 task 충돌
|
|
71
|
+
(hits.tasks || []).slice(0, 3).forEach(tsk => {
|
|
72
|
+
if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
|
|
73
|
+
conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// plan milestones — 진행 중 milestone
|
|
77
|
+
(hits.planMilestones || []).slice(0, 3).forEach(m => {
|
|
78
|
+
if (m.status && /in-progress|진행/.test(String(m.status))) {
|
|
79
|
+
planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// taskLogFails — 과거 같은 키워드 실패 흔적
|
|
83
|
+
(hits.taskLogFails || []).slice(0, 3).forEach(f => {
|
|
84
|
+
conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
|
|
89
|
+
// 3) reuse-map 매칭 — 기존 capability 등록 후보
|
|
90
|
+
try {
|
|
91
|
+
const reusePath = path.join(root, '.harness/reuse-map.md');
|
|
92
|
+
if (exists(reusePath)) {
|
|
93
|
+
const reuseLines = read(reusePath).split('\n');
|
|
94
|
+
const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
|
|
95
|
+
for (const line of reuseLines) {
|
|
96
|
+
if (!/^\| /.test(line)) continue; // 테이블 row만
|
|
97
|
+
const ll = line.toLowerCase();
|
|
98
|
+
const matched = tokens.filter(t => ll.includes(t)).length;
|
|
99
|
+
if (matched > 0) {
|
|
100
|
+
const cols = line.split('|').map(s => s.trim());
|
|
101
|
+
if (cols[1]) {
|
|
102
|
+
reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
// 4) feature_graph — 같은 영역 변경 가능성
|
|
110
|
+
const featureConflicts = [];
|
|
111
|
+
try {
|
|
112
|
+
const fgPath = path.join(root, '.harness/feature_graph.md');
|
|
113
|
+
if (exists(fgPath)) {
|
|
114
|
+
const fg = read(fgPath);
|
|
115
|
+
const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
|
|
116
|
+
// F-XXXX 노드 라인 추출
|
|
117
|
+
const nodeBlocks = fg.split(/\n### /);
|
|
118
|
+
for (const blk of nodeBlocks.slice(1)) {
|
|
119
|
+
const bl = blk.toLowerCase();
|
|
120
|
+
const matched = tokens.filter(t => bl.includes(t)).length;
|
|
121
|
+
if (matched > 0) {
|
|
122
|
+
const titleMatch = blk.match(/^([^\n]+)/);
|
|
123
|
+
const idMatch = blk.match(/F-\d+/);
|
|
124
|
+
if (titleMatch && idMatch) {
|
|
125
|
+
featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
|
|
132
|
+
// 5) 권장 단계 (작업 유형별)
|
|
133
|
+
const recommendedSteps = {
|
|
134
|
+
feature: [
|
|
135
|
+
'1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
|
|
136
|
+
'2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
|
|
137
|
+
'3) leerness plan add "<milestone>" — 진행 추적',
|
|
138
|
+
'4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
|
|
139
|
+
'5) verify-claim --run-tests 로 evidence 의무화'
|
|
140
|
+
],
|
|
141
|
+
bugfix: [
|
|
142
|
+
'1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
|
|
143
|
+
'2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
|
|
144
|
+
'3) verify-code --run-tests — 재현 + fix 검증',
|
|
145
|
+
'4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
|
|
146
|
+
],
|
|
147
|
+
refactor: [
|
|
148
|
+
'1) leerness reuse-map — 영향 범위 파악',
|
|
149
|
+
'2) leerness impact <file> — 강한/약한 참조 분리',
|
|
150
|
+
'3) leerness contract verify — 외부 인터페이스 보존 확인',
|
|
151
|
+
'4) verify-code --run-tests + 회귀 테스트'
|
|
152
|
+
],
|
|
153
|
+
research: [
|
|
154
|
+
'1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
|
|
155
|
+
'2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
|
|
156
|
+
'3) leerness review <file> --persona research — 깊이 검토',
|
|
157
|
+
'4) leerness decision add "<결론>" — 회수 가능하게 영구화'
|
|
158
|
+
],
|
|
159
|
+
planning: [
|
|
160
|
+
'1) leerness plan add "<milestone>" — 분해 시작',
|
|
161
|
+
'2) leerness reuse-map — 기존 자원 인벤토리',
|
|
162
|
+
'3) leerness agents recommend planning — sub-agent 분배',
|
|
163
|
+
'4) leerness session close — 결정 영구화'
|
|
164
|
+
],
|
|
165
|
+
release: [
|
|
166
|
+
'1) leerness health — production-ready 확인',
|
|
167
|
+
'2) leerness audit + verify-code — 보안 + 검수',
|
|
168
|
+
'3) leerness release bump + note + publish'
|
|
169
|
+
],
|
|
170
|
+
consistency: [
|
|
171
|
+
'1) leerness audit — design/reuse/handoff 일관성 검사',
|
|
172
|
+
'2) leerness consistency check — 잠재 일관성 위반',
|
|
173
|
+
'3) leerness drift check --auto-fix — 자동 회복'
|
|
174
|
+
]
|
|
175
|
+
}[estimatedType] || [];
|
|
176
|
+
|
|
177
|
+
// 6) 효율 제안 (적용 가능한 sub-agent + skill)
|
|
178
|
+
const efficiencyHints = [];
|
|
179
|
+
if (reuseCandidates.length > 0) {
|
|
180
|
+
efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
|
|
181
|
+
}
|
|
182
|
+
if (conflictHints.length > 0) {
|
|
183
|
+
efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
|
|
184
|
+
}
|
|
185
|
+
if (planConflicts.length > 0) {
|
|
186
|
+
efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
|
|
187
|
+
}
|
|
188
|
+
if (featureConflicts.length > 0) {
|
|
189
|
+
efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
|
|
190
|
+
}
|
|
191
|
+
// 다중 에이전트 분배 추천
|
|
192
|
+
if (estimatedType === 'feature' || estimatedType === 'planning') {
|
|
193
|
+
efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
|
|
194
|
+
}
|
|
195
|
+
if (efficiencyHints.length === 0) {
|
|
196
|
+
efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
|
|
200
|
+
let constraintsCheck = { matched: [], suggestions: [] };
|
|
201
|
+
try {
|
|
202
|
+
constraintsCheck = _checkRequestConstraints(root, text);
|
|
203
|
+
if (constraintsCheck.matched.length > 0) {
|
|
204
|
+
efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
|
|
208
|
+
// 6.7) 1.14.1 (Karpathy 가이드라인 1+2, UR-0031): 범위 과대 / 투기적 신호 표면화 — "생각하고 코딩"(트레이드오프 표면화) + "단순성 우선"(요청 범위만, 추측성 일반화 보류). advisory(차단 X).
|
|
209
|
+
const broadHits = [...new Set((text.match(/(전체|싹\s*다|모두|전부|모든\s*(?:코드|파일|것)|리팩토링|리팩터|재구성|재작성|갈아\s*엎|overhaul|rewrite\s+everything|refactor\s+everything)/gi) || []).map(s => s.trim()))];
|
|
210
|
+
const specHits = [...new Set((text.match(/(나중에|추후|미래\s*대비|확장\s*가능|유연하게|범용화|일반화|추상화|future[-\s]?proof|extensible|pluggable|일단\s*만들)/gi) || []).map(s => s.trim()))];
|
|
211
|
+
const simplicitySignals = { broad: broadHits, speculative: specHits };
|
|
212
|
+
if (broadHits.length) efficiencyHints.push(`🤔 범위 과대 신호(${broadHits.slice(0, 3).join(', ')}) — 더 작게 쪼갤 수 있나? 요청에 명시된 것만 (Karpathy 단순성)`);
|
|
213
|
+
if (specHits.length) efficiencyHints.push(`🧪 투기적 신호(${specHits.slice(0, 3).join(', ')}) — 지금 필요한 범위만, 추측성 일반화 보류 (Karpathy 단순성)`);
|
|
214
|
+
|
|
215
|
+
// 7) proceed 권장 (충돌 critical 시 false)
|
|
216
|
+
const proceed = conflictHints.length < 3 && planConflicts.length === 0;
|
|
217
|
+
|
|
218
|
+
const dt = Date.now() - t0;
|
|
219
|
+
const out = {
|
|
220
|
+
request: text,
|
|
221
|
+
estimatedType,
|
|
222
|
+
conflicts: conflictHints,
|
|
223
|
+
reuseCandidates,
|
|
224
|
+
lessonsRecall,
|
|
225
|
+
planConflicts,
|
|
226
|
+
featureConflicts,
|
|
227
|
+
recommendedSteps,
|
|
228
|
+
efficiencyHints,
|
|
229
|
+
simplicitySignals,
|
|
230
|
+
platformConstraints: constraintsCheck.matched,
|
|
231
|
+
constraintSuggestions: constraintsCheck.suggestions,
|
|
232
|
+
proceed,
|
|
233
|
+
proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
|
|
234
|
+
durationMs: dt
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
|
|
238
|
+
|
|
239
|
+
if (has('--json')) {
|
|
240
|
+
log(JSON.stringify(out, null, 2));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
log(`# leerness review-request (1.9.176 사전 검토)`);
|
|
245
|
+
log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
|
|
246
|
+
log(`추정 작업 유형: ${estimatedType}`);
|
|
247
|
+
log('');
|
|
248
|
+
if (conflictHints.length) {
|
|
249
|
+
log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
|
|
250
|
+
conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
|
|
251
|
+
log('');
|
|
252
|
+
}
|
|
253
|
+
if (reuseCandidates.length) {
|
|
254
|
+
log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
|
|
255
|
+
reuseCandidates.slice(0, 5).forEach(r => {
|
|
256
|
+
if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
|
|
257
|
+
else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
|
|
258
|
+
});
|
|
259
|
+
log('');
|
|
260
|
+
}
|
|
261
|
+
if (lessonsRecall.length) {
|
|
262
|
+
log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
|
|
263
|
+
lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
|
|
264
|
+
log('');
|
|
265
|
+
}
|
|
266
|
+
if (planConflicts.length || featureConflicts.length) {
|
|
267
|
+
log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
|
|
268
|
+
planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
|
|
269
|
+
featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
|
|
270
|
+
log('');
|
|
271
|
+
}
|
|
272
|
+
log(`## 💡 효율 제안`);
|
|
273
|
+
efficiencyHints.forEach(h => log(` ${h}`));
|
|
274
|
+
log('');
|
|
275
|
+
// 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
|
|
276
|
+
if (constraintsCheck.matched.length > 0) {
|
|
277
|
+
log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
|
|
278
|
+
for (const m of constraintsCheck.matched) {
|
|
279
|
+
log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
|
|
280
|
+
for (const c of (m.constraints || []).slice(0, 3)) {
|
|
281
|
+
log(` • [${c.kind}] ${c.detail}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
log(` → leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
|
|
285
|
+
log('');
|
|
286
|
+
}
|
|
287
|
+
if (recommendedSteps.length) {
|
|
288
|
+
log(`## 📍 권장 단계 (${estimatedType})`);
|
|
289
|
+
recommendedSteps.forEach(s => log(` ${s}`));
|
|
290
|
+
log('');
|
|
291
|
+
}
|
|
292
|
+
log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
|
|
293
|
+
log(` 사유: ${out.proceedReason}`);
|
|
294
|
+
log(` 분석 소요: ${dt}ms`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = { reviewRequestCmd };
|