leerness 1.9.321 → 1.9.322
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 +17 -0
- package/README.md +5 -5
- package/bin/harness.js +55 -44
- package/package.json +1 -1
- package/scripts/e2e.js +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.322 — 2026-06-05 — UR-0044(완료): MCP handler 통합 (_mcpToCliArgs 추출)
|
|
4
|
+
|
|
5
|
+
**🧩 mcpServeCmd 의 인라인 83-case switch(name→cliArgs)를 단일 함수 `_mcpToCliArgs` 로 통합 — ToolRegistry handler 통합 완료.**
|
|
6
|
+
|
|
7
|
+
### 배경 (UR-0044)
|
|
8
|
+
mcpServeCmd 의 tools/call 핸들러에 **230줄 인라인 switch**(83 도구 name→cliArgs 매핑)가 있어 mcpServeCmd 가 비대하고 매핑이 dispatch 로직과 뒤섞임. 1.9.319 에서 일치성 가드(모든 def↔case)를 먼저 깔아 안전망 확보 → 본 라운드에서 물리 이전.
|
|
9
|
+
|
|
10
|
+
### 구현 (UR-0044 완료)
|
|
11
|
+
1. **`_mcpToCliArgs(name, args, targetPath)` 모듈레벨 함수 추출** — 83-case switch 를 그대로 이전(미지 도구는 `null` 반환). mcpServeCmd 는 `cliArgs = _mcpToCliArgs(...)` 호출 + `null` 시 `-32601` 응답.
|
|
12
|
+
2. **마이그레이션 스크립트(`scripts/_migrate-mcp-tocli.js`)로 프로그램적 splice** — 수기 전사 위험 0, 실행 후 삭제.
|
|
13
|
+
3. 안전망: 일치성 가드(selftest: 83 def↔case) + e2e(MCP 16+ 테스트 + 신규 B(1.9.322) 인자매핑/미지도구) 가 검증.
|
|
14
|
+
4. selftest 69→70 · e2e 266→267.
|
|
15
|
+
|
|
16
|
+
### 검증
|
|
17
|
+
- **selftest 70/70 PASS** · **E2E 267/267 PASS** (회귀 0 — 전체 MCP 테스트 green).
|
|
18
|
+
- 실측: about/state_start(멀티인자)/feature_add/task_add(status push) dispatch 정상 · 미지 도구 → `-32601 Unknown tool` · `switch (name)` 소스 1곳(_mcpToCliArgs)만.
|
|
19
|
+
|
|
3
20
|
## 1.9.321 — 2026-06-05 — UR-0053(2단계): decision/lesson 빈 필드 오파싱 수정
|
|
4
21
|
|
|
5
22
|
**🔧 빈 필드(Alternatives 등)가 다음 줄(Impact)을 캡처하던 `Alternatives→Impact 오파싱` 버그 수정 (UR-0053 의 두 번째 구체 결함).**
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.322 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.322는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.321는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
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) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.322)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.322: 2026-06-04
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -15,7 +15,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
15
15
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
16
16
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST } = require('../lib/catalogs');
|
|
17
17
|
|
|
18
|
-
const VERSION = '1.9.
|
|
18
|
+
const VERSION = '1.9.322';
|
|
19
19
|
|
|
20
20
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
21
21
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3121,6 +3121,7 @@ function _selfTestCases() {
|
|
|
3121
3121
|
{ name: 'MCP ToolRegistry 일치성: 모든 도구 def 가 dispatch case 보유 + 고아 case 0 + requiredTier 완비 (UR-0044 1.9.319)', run: () => { const tools = require('../lib/mcp-tools'); const src = read(__filename); const missing = tools.filter(t => !src.includes("case '" + t.name + "':")); const cases = [...src.matchAll(/case '(leerness_[a-z_]+)':/g)].map(m => m[1]); const defNames = new Set(tools.map(t => t.name)); const orphans = [...new Set(cases)].filter(c => !defNames.has(c)); const tierOk = tools.every(t => typeof t.requiredTier === 'string' && PERMISSION_TIERS.includes(t.requiredTier)); return tools.length >= 83 && missing.length === 0 && orphans.length === 0 && tierOk; } },
|
|
3122
3122
|
{ name: 'count drift 수정: _countDatedBlocks 코드펜스(템플릿) 제외 + 카운트 사이트 단일화 (UR-0053 1.9.320)', run: () => { const f = _countDatedBlocks; const withTpl = '# D\n\n```md\n### 2026-01-01 — Decision 제목\n- Decision:\n```\n\n### 2026-06-04 — 실제\n- Decision: 실제\n'; const c1 = f(withTpl) === 1; const c0 = f('```md\n### 2026-01-01 — x\n```\n') === 0; const c2 = f('### 2026-01-01 — A\n### 2026-02-02 — B\n') === 2; const src = read(__filename); const wired = src.includes('_countDatedBlocks(read(decisionsPath(root)))') && src.includes('_countDatedBlocks(read(lessonsPath(root)))') && src.includes('_countDatedBlocks(dtext)') && !src.includes('read(decisionsPath(root)).' + 'match(/^### '); return typeof f === 'function' && c1 && c0 && c2 && wired; } },
|
|
3123
3123
|
{ name: 'decision/lesson 필드 파싱: 빈 필드가 다음 줄로 안 샘 ([ \\t]* 사용) (UR-0053 1.9.321)', run: () => { const block = '### 2026-06-05 — X\n- Decision: X\n- Reason: r\n- Alternatives: \n- Impact: 보안\n'; const alt = block.match(/- Alternatives:[ \t]*(.+)/); const imp = block.match(/- Impact:[ \t]*(.+)/); const altNoBleed = !alt || !/Impact/.test(alt[1]); const impOk = !!imp && imp[1].trim() === '보안'; const src = read(__filename); const fixedOk = src.includes('- Alternatives:[ \\t]*(.+)') && src.includes('- Lesson:[ \\t]*(.+)') && src.includes('- Impact:[ \\t]*(.+)'); return altNoBleed && impOk && fixedOk; } },
|
|
3124
|
+
{ name: 'MCP handler 통합: _mcpToCliArgs 단일 함수 + mcpServeCmd 호출 + 인라인 switch 단일화 (UR-0044 1.9.322)', run: () => { const src = read(__filename); const fnDef = /function _mcpToCliArgs\(name, args, targetPath\) \{/.test(src); const called = src.includes('cliArgs = _mcpToCliArgs(name, args, targetPath)'); const switchCount = (src.match(/switch \(name\) \{/g) || []).length; const nullPath = src.includes('if (cliArgs === null) return send('); return fnDef && called && switchCount === 1 && nullPath; } },
|
|
3124
3125
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3125
3126
|
];
|
|
3126
3127
|
}
|
|
@@ -16742,49 +16743,11 @@ function skillExportAllCmd(root) {
|
|
|
16742
16743
|
}
|
|
16743
16744
|
|
|
16744
16745
|
// 1.9.43: MCP server — stdio JSON-RPC로 leerness 도구 노출 (Claude Code/Hermes 등이 호출)
|
|
16745
|
-
// 프로토콜: MCP 표준 (JSON-RPC 2.0). 메서드: initialize, tools/list, tools/call
|
|
16746
|
-
function mcpServeCmd(root) {
|
|
16747
|
-
root = absRoot(root || process.cwd());
|
|
16748
|
-
// 노출할 leerness 도구 목록
|
|
16749
|
-
// 1.9.297 (UR-0025 5단계): MCP 도구 정의를 lib/mcp-tools.js 단일출처로 분리 (Codex #5 regex self-count 영구 해소).
|
|
16750
|
-
const TOOLS = require('../lib/mcp-tools');
|
|
16751
16746
|
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
const r = cp.spawnSync(process.execPath, [__filename, ...cliArgs], {
|
|
16757
|
-
encoding: 'utf8',
|
|
16758
|
-
timeout: 60000,
|
|
16759
|
-
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_STALE_CHECK: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_WORKFLOW_GUIDE: '1' }
|
|
16760
|
-
});
|
|
16761
|
-
return { ok: r.status === 0, exit: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
16762
|
-
}
|
|
16763
|
-
function handleRequest(req) {
|
|
16764
|
-
const id = req.id;
|
|
16765
|
-
// 1.9.313 (UR-0049, 설치리뷰 3중수렴): JSON-RPC 2.0 / MCP notification 준수.
|
|
16766
|
-
// notification = id 없는 요청 (또는 notifications/* 메서드) → spec "MUST NOT reply" → 절대 응답 금지.
|
|
16767
|
-
// 이전: notifications/initialized 에 -32601 + id 없는 응답 → 엄격한 MCP 클라이언트가 프로토콜 위반 로깅/중단.
|
|
16768
|
-
const isNotification = !('id' in req) || (typeof req.method === 'string' && req.method.startsWith('notifications/'));
|
|
16769
|
-
if (isNotification) return; // initialized/cancelled/progress 등 조용히 수용 (무응답)
|
|
16770
|
-
if (req.method === 'initialize') {
|
|
16771
|
-
send({ jsonrpc: '2.0', id, result: {
|
|
16772
|
-
protocolVersion: '2024-11-05',
|
|
16773
|
-
capabilities: { tools: {} },
|
|
16774
|
-
serverInfo: { name: 'leerness', version: VERSION }
|
|
16775
|
-
} });
|
|
16776
|
-
} else if (req.method === 'ping') {
|
|
16777
|
-
// 1.9.313 (UR-0049): MCP 표준 ping → 빈 결과 응답 (연결 확인 — 이전엔 -32601 오류)
|
|
16778
|
-
send({ jsonrpc: '2.0', id, result: {} });
|
|
16779
|
-
} else if (req.method === 'tools/list') {
|
|
16780
|
-
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
|
|
16781
|
-
} else if (req.method === 'tools/call') {
|
|
16782
|
-
const { name, arguments: args = {} } = req.params || {};
|
|
16783
|
-
const targetPath = args.path || root;
|
|
16784
|
-
// 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지 가시화
|
|
16785
|
-
try { _bumpMcpUsage(targetPath, name); } catch {}
|
|
16786
|
-
let cliArgs;
|
|
16787
|
-
try {
|
|
16747
|
+
// 1.9.322 (UR-0044, 설치리뷰): MCP tools/call name->cliArgs 매핑을 단일 함수로 통합 (이전: mcpServeCmd 인라인 83-case switch).
|
|
16748
|
+
// 일치성 가드(selftest: 모든 도구 def 가 case 보유) + e2e(대표 도구 dispatch) 가 안전망. 미지 도구는 null 반환.
|
|
16749
|
+
function _mcpToCliArgs(name, args, targetPath) {
|
|
16750
|
+
let cliArgs;
|
|
16788
16751
|
switch (name) {
|
|
16789
16752
|
case 'leerness_handoff': cliArgs = ['handoff', targetPath, '--compact', '--no-drift-check']; break;
|
|
16790
16753
|
case 'leerness_drift_check': cliArgs = ['drift', 'check', targetPath, '--json']; break;
|
|
@@ -17017,8 +16980,56 @@ function mcpServeCmd(root) {
|
|
|
17017
16980
|
cliArgs = ['brief', args.export === true ? 'export' : 'show', '--path', targetPath, ...(args.export === true ? [] : ['--json'])];
|
|
17018
16981
|
break;
|
|
17019
16982
|
default:
|
|
17020
|
-
return
|
|
16983
|
+
return null;
|
|
17021
16984
|
}
|
|
16985
|
+
return cliArgs;
|
|
16986
|
+
}
|
|
16987
|
+
|
|
16988
|
+
// 프로토콜: MCP 표준 (JSON-RPC 2.0). 메서드: initialize, tools/list, tools/call
|
|
16989
|
+
function mcpServeCmd(root) {
|
|
16990
|
+
root = absRoot(root || process.cwd());
|
|
16991
|
+
// 노출할 leerness 도구 목록
|
|
16992
|
+
// 1.9.297 (UR-0025 5단계): MCP 도구 정의를 lib/mcp-tools.js 단일출처로 분리 (Codex #5 regex self-count 영구 해소).
|
|
16993
|
+
const TOOLS = require('../lib/mcp-tools');
|
|
16994
|
+
|
|
16995
|
+
function send(obj) {
|
|
16996
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
16997
|
+
}
|
|
16998
|
+
function callLeerness(cliArgs) {
|
|
16999
|
+
const r = cp.spawnSync(process.execPath, [__filename, ...cliArgs], {
|
|
17000
|
+
encoding: 'utf8',
|
|
17001
|
+
timeout: 60000,
|
|
17002
|
+
env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_STALE_CHECK: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_WORKFLOW_GUIDE: '1' }
|
|
17003
|
+
});
|
|
17004
|
+
return { ok: r.status === 0, exit: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
17005
|
+
}
|
|
17006
|
+
function handleRequest(req) {
|
|
17007
|
+
const id = req.id;
|
|
17008
|
+
// 1.9.313 (UR-0049, 설치리뷰 3중수렴): JSON-RPC 2.0 / MCP notification 준수.
|
|
17009
|
+
// notification = id 없는 요청 (또는 notifications/* 메서드) → spec "MUST NOT reply" → 절대 응답 금지.
|
|
17010
|
+
// 이전: notifications/initialized 에 -32601 + id 없는 응답 → 엄격한 MCP 클라이언트가 프로토콜 위반 로깅/중단.
|
|
17011
|
+
const isNotification = !('id' in req) || (typeof req.method === 'string' && req.method.startsWith('notifications/'));
|
|
17012
|
+
if (isNotification) return; // initialized/cancelled/progress 등 조용히 수용 (무응답)
|
|
17013
|
+
if (req.method === 'initialize') {
|
|
17014
|
+
send({ jsonrpc: '2.0', id, result: {
|
|
17015
|
+
protocolVersion: '2024-11-05',
|
|
17016
|
+
capabilities: { tools: {} },
|
|
17017
|
+
serverInfo: { name: 'leerness', version: VERSION }
|
|
17018
|
+
} });
|
|
17019
|
+
} else if (req.method === 'ping') {
|
|
17020
|
+
// 1.9.313 (UR-0049): MCP 표준 ping → 빈 결과 응답 (연결 확인 — 이전엔 -32601 오류)
|
|
17021
|
+
send({ jsonrpc: '2.0', id, result: {} });
|
|
17022
|
+
} else if (req.method === 'tools/list') {
|
|
17023
|
+
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
|
|
17024
|
+
} else if (req.method === 'tools/call') {
|
|
17025
|
+
const { name, arguments: args = {} } = req.params || {};
|
|
17026
|
+
const targetPath = args.path || root;
|
|
17027
|
+
// 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지 가시화
|
|
17028
|
+
try { _bumpMcpUsage(targetPath, name); } catch {}
|
|
17029
|
+
let cliArgs;
|
|
17030
|
+
try {
|
|
17031
|
+
cliArgs = _mcpToCliArgs(name, args, targetPath);
|
|
17032
|
+
if (cliArgs === null) return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
17022
17033
|
// 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
|
|
17023
17034
|
// 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
|
|
17024
17035
|
// cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3854,5 +3854,27 @@ total++;
|
|
|
3854
3854
|
if (!ok) failed++;
|
|
3855
3855
|
}
|
|
3856
3856
|
|
|
3857
|
+
// 1.9.322 회귀 (UR-0044): _mcpToCliArgs 추출 후 인자 매핑(push) 보존 + 미지 도구 -32601
|
|
3858
|
+
total++;
|
|
3859
|
+
{
|
|
3860
|
+
let ok = false;
|
|
3861
|
+
try {
|
|
3862
|
+
const md = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tocli-'));
|
|
3863
|
+
cp.spawnSync(process.execPath, [CLI, 'init', md, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
3864
|
+
const call = (req) => { const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: JSON.stringify(req) + '\n' }); try { return JSON.parse(r.stdout.split('\n').filter(Boolean)[0]); } catch { return null; } };
|
|
3865
|
+
// task_add with status arg (멀티 인자 push 경로) → task list 반영 확인
|
|
3866
|
+
call({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_task_add', arguments: { path: md, text: 'TOCLI 테스트', status: 'in-progress' } } });
|
|
3867
|
+
const listed = call({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'leerness_task_list', arguments: { path: md } } });
|
|
3868
|
+
const t = (listed && listed.result && listed.result.content && listed.result.content[0].text) || '';
|
|
3869
|
+
const argMapOk = /TOCLI 테스트/.test(t) && /in-progress/.test(t);
|
|
3870
|
+
const unk = call({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'leerness_nope', arguments: {} } });
|
|
3871
|
+
const unkOk = !!unk && !!unk.error && unk.error.code === -32601;
|
|
3872
|
+
ok = argMapOk && unkOk;
|
|
3873
|
+
fs.rmSync(md, { recursive: true, force: true });
|
|
3874
|
+
} catch {}
|
|
3875
|
+
console.log(ok ? '✓ B(1.9.322) _mcpToCliArgs 추출: 인자 매핑(task_add status) 보존 + 미지 도구 -32601 (UR-0044)' : '✗ _mcpToCliArgs 추출 실패');
|
|
3876
|
+
if (!ok) failed++;
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3857
3879
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3858
3880
|
if (failed > 0) process.exit(1);
|