leerness 1.9.313 → 1.9.315

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 CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.315 — 2026-06-04 — UR-0054(부분): doc/surface 정합 + doctor 명령 (설치리뷰)
4
+
5
+ **📋 stale 현재상태 표시 수정 + `leerness doctor` 설치 진단 명령 추가 (Codex#2·Sonnet#2·Opus#2 공통 지적).**
6
+
7
+ ### 배경 (설치리뷰)
8
+ "문서/표면 drift — init 'MCP 46'(실제83), .harness/.leerness 혼재, --help 중복, --compact 미압축, doctor no-op, init후 gate 실패". 6개 항목 중 **검증 후** 실제/현행 상태:
9
+ - ✅ **stale MCP 카운트**: `commands` 요약(하드코딩 65) + `_banner`(하드코딩 46) — 실제 83. 현재상태 주장이 stale.
10
+ - ✅ **doctor 부재**: `leerness doctor` 명령 없음(unknown command).
11
+ - ⏸ `--help 중복`/`--compact`: 현재 재현 안 됨(중복 0, --compact 200→185줄 압축 동작) — 이미 정상.
12
+ - ⏸ `init후 gate 실패`/`.harness·.leerness 혼재`: 의미적 결정/구조 변경 필요 → 후속(별도 라운드).
13
+
14
+ ### 구현 (UR-0054 부분)
15
+ 1. **stale MCP 카운트 → 동적 `_mcpToolCount()`**: `commands` 요약 + `_banner` 외부 AI 섹션. (역사적 changelog/What's-new 박스의 숫자는 그 시점 기록이라 보존.)
16
+ 2. **`leerness doctor` 신규** — 설치/환경 1콜 진단: version·node·경로·MCP 수·**selftest 코어 무결성(N/N)**·셸/PowerShell. `--json` + 실패 시 exit 1(CI 친화). health(프로젝트)와 구분되는 "도구 자체" 진단.
17
+ 3. selftest 62→63 · e2e 259→260.
18
+
19
+ ### 검증
20
+ - **selftest 63/63 PASS** · **E2E 260/260 PASS** (회귀 0).
21
+ - 실측: `commands`/`_banner` → "MCP 83" · `doctor` → `version 1.9.315 · MCP 83 · selftest 63/63 · 셸` exit 0 · `doctor --json` 구조화 출력 · `about`/`pulse` mcpTools 이미 동적(확인).
22
+
23
+ ## 1.9.314 — 2026-06-04 — UR-0052: PowerShell 감지 정확화 (설치리뷰)
24
+
25
+ **🪟 shell-guard 가 PowerShell 7(pwsh)을 cmd/ps5.1 로 오판해 `&&` 에 잘못된 ps5-chain 경고를 내던 결함 수정 (Opus#2 지적, Windows/한국어 타깃 핵심).**
26
+
27
+ ### 배경 (설치리뷰)
28
+ - `_collectRuntimeEnv` 가 항상 `powershell.exe`(=Windows PowerShell 5.1)만 probe → pwsh7 환경도 버전 `5` 로 오판.
29
+ - `_detectShellCtx` 가 Windows `ComSpec`(항상 cmd.exe)에 의존 → pwsh7/ps5.1 도 `cmd` 로 오판.
30
+ - 결과: pwsh7(=`&&`/`||` 지원)에서 `a && b` 에 **거짓 ps5-chain 에러**(exit 1) 발생 가능.
31
+
32
+ ### 구현 (UR-0052)
33
+ 1. **`_detectPwshFromEnv(e)` 순수 헬퍼** — pwsh 6/7 신뢰 마커로 판별: `POWERSHELL_DISTRIBUTION_CHANNEL`(pwsh 런타임 전용, cmd/ps5 미상속) · pwsh 전용 경로(`\PowerShell\7\`, `Documents\PowerShell\`). pwsh 오검출은 안전(pwsh 는 `&&` 지원 → ps5-chain 무발생).
34
+ 2. **ps5.1 자동 판별 안 함** — `Documents\WindowsPowerShell` 은 영구 user env(bash/cmd 도 상속)라 신뢰 불가 → **과경고 방지**. ps5-chain 규칙은 ctx 가 명시적 powershell+v5 일 때만(직접 호출/probe) 발화.
35
+ 3. `_collectRuntimeEnv`/`_detectShellCtx` 둘 다 헬퍼 사용 + cross-platform psVersion 폴백 + `psEdition` 필드 추가.
36
+ 4. selftest 61→62 · e2e 258→259.
37
+
38
+ ### 검증
39
+ - **selftest 62/62 PASS** · **E2E 259/259 PASS** (회귀 0).
40
+ - 실측: pwsh7(channel) → `shell=powershell, psVersion=7, ps5-chain 없음` · 영구 ps5.1경로+bash → `shell=bash, ps5-chain 없음`(과경고 0) · 직접 `{powershell, v5}` → ps5-chain 발화(규칙 보존).
41
+ - 회귀 수정: B(1.9.304) 가 OLD 오판(shell=cmd)에 의존하던 `shell-guard "a && b"` exit 0 가정 → 정확 감지 후에도 통과(이 환경은 bash).
42
+
3
43
  ## 1.9.313 — 2026-06-04 — UR-0049: MCP notification 프로토콜 준수 (설치리뷰 3중수렴 high)
4
44
 
5
45
  **🔌 MCP 서버가 JSON-RPC notification(`notifications/*`)에 에러 응답을 보내 프로토콜을 위반하던 결함 수정 — 엄격한 MCP 클라이언트의 핸드셰이크 중단 위험.**
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
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.313-green)]() [![tests](https://img.shields.io/badge/e2e-258%2F258-success)]() [![selftest](https://img.shields.io/badge/selftest-61-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-83-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.315-green)]() [![tests](https://img.shields.io/badge/e2e-260%2F260-success)]() [![selftest](https://img.shields.io/badge/selftest-63-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-83-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
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.313 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.315 하네스를 사용합니다. 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.313는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
528
+ Leerness v1.9.315는 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.313는 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.313)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.315)** · 매 라운드 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.313: 2026-06-04
587
+ Last synced by Leerness v1.9.315: 2026-06-04
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/harness.js CHANGED
@@ -14,7 +14,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
14
14
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
15
15
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST } = require('../lib/catalogs');
16
16
 
17
- const VERSION = '1.9.313';
17
+ const VERSION = '1.9.315';
18
18
 
19
19
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
20
20
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -2604,6 +2604,22 @@ function _computeRecentChanges(root, limit = 5) {
2604
2604
  // 사용자 보고: Windows + 한국어 PowerShell 에서 .ps1 UTF-8 (no BOM) 파일이 CP949 로 잘못 인식되어 파싱 오류
2605
2605
  // 해결: leerness 가 환경 정보 + 셸 스크립트 인코딩을 사전 감지 → handoff body 경고
2606
2606
  // 주의: 기존 _detectEnvironment (1.9.145) 와 명명 충돌 → _collectRuntimeEnv 로 분리
2607
+ // 1.9.314 (UR-0052, 설치리뷰 3중수렴): Windows PowerShell 실제 실행 셸/버전 감지 — env 마커 우선(순수 함수).
2608
+ // 배경: 이전엔 powershell.exe(=5.1)만 probe + ComSpec(항상 cmd.exe) 의존 → pwsh7 을 ps5/cmd 로 오판 → && 에 ps5-chain 오탐.
2609
+ // 신뢰 마커: POWERSHELL_DISTRIBUTION_CHANNEL(pwsh 런타임 전용, cmd/ps5 미상속) · PSModulePath 의 사용자 모듈 경로
2610
+ // (Documents\PowerShell=pwsh7 / Documents\WindowsPowerShell=ps5.1). 시스템 경로는 cmd 도 가져 비신뢰 → 사용자 경로만 판별.
2611
+ function _detectPwshFromEnv(e) {
2612
+ e = e || process.env;
2613
+ const channel = e.POWERSHELL_DISTRIBUTION_CHANNEL || '';
2614
+ const pmp = e.PSModulePath || '';
2615
+ // pwsh 6/7 (Core) 신뢰 마커: POWERSHELL_DISTRIBUTION_CHANNEL(pwsh 런타임 전용, cmd/ps5 미상속) · pwsh 전용 경로(\PowerShell\7|6\, Documents\PowerShell\).
2616
+ // pwsh 오검출은 안전 — pwsh 는 &&/|| 지원 → ps5-chain 오탐을 만들지 않음.
2617
+ // ⚠ ps5.1 은 신뢰 가능한 런타임 마커가 없음(Documents\WindowsPowerShell 은 영구 user env → bash/cmd 도 상속) → 자동 판별하지 않음(과경고 방지).
2618
+ if (channel || /[\\/]PowerShell[\\/][67][\\/]/i.test(pmp) || /Documents[\\/]+PowerShell[\\/]/i.test(pmp)) {
2619
+ return { isPowerShell: true, version: '7', edition: 'Core' };
2620
+ }
2621
+ return { isPowerShell: false, version: null, edition: null };
2622
+ }
2607
2623
  function _collectRuntimeEnv() {
2608
2624
  const env = {
2609
2625
  os: { platform: os.platform(), release: os.release(), arch: os.arch() },
@@ -2628,7 +2644,8 @@ function _collectRuntimeEnv() {
2628
2644
  isTTY: !!(process.stdout && process.stdout.isTTY),
2629
2645
  term: process.env.TERM || null,
2630
2646
  shell: process.env.SHELL || process.env.ComSpec || null,
2631
- powershell: null
2647
+ powershell: null,
2648
+ psEdition: null
2632
2649
  },
2633
2650
  tools: { git: null, npm: null, python: null, python3: null }
2634
2651
  };
@@ -2661,11 +2678,21 @@ function _collectRuntimeEnv() {
2661
2678
  }
2662
2679
  }
2663
2680
  } catch {}
2664
- // PowerShell 감지
2681
+ // PowerShell 감지 (1.9.314 UR-0052, 설치리뷰): 실제 부모 셸 env 마커 우선 — 이전엔 powershell.exe(=5.1)만 probe 해 pwsh7 을 '5' 로 오판.
2665
2682
  try {
2666
2683
  if (env.os.platform === 'win32') {
2667
- const r = cp.spawnSync('powershell', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.Major'], { encoding: 'utf8', timeout: 5000 });
2668
- if (r.status === 0) env.terminal.powershell = (r.stdout || '').trim().split('\n')[0];
2684
+ const fromEnv = _detectPwshFromEnv();
2685
+ if (fromEnv.version) {
2686
+ env.terminal.powershell = fromEnv.version;
2687
+ env.terminal.psEdition = fromEnv.edition;
2688
+ } else {
2689
+ // env 마커 불충분 → 보수적으로 Windows PowerShell(5.1) probe (미확정 시 && 경고 유지가 안전)
2690
+ const r = cp.spawnSync('powershell', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.Major'], { encoding: 'utf8', timeout: 5000 });
2691
+ if (r.status === 0 && (r.stdout || '').trim()) {
2692
+ env.terminal.powershell = (r.stdout || '').trim().split(/\r?\n/)[0].trim();
2693
+ env.terminal.psEdition = parseInt(env.terminal.powershell, 10) >= 6 ? 'Core' : 'Desktop';
2694
+ }
2695
+ }
2669
2696
  }
2670
2697
  } catch {}
2671
2698
  // 도구 버전 감지
@@ -3085,6 +3112,8 @@ function _selfTestCases() {
3085
3112
  { name: 'init 가드: 미초기화 write 차단 + 다중마커 판별 + --force 우회 (UR-0047 설치리뷰 1.9.311)', run: () => { const src = read(__filename); const fnOk = typeof _isInitialized === 'function' && typeof _requireInit === 'function'; const liveOk = _isInitialized('.') === true; const emptyOk = _isInitialized(path.join(os.tmpdir(), '__leerness_noinit_marker__')) === false; const wired = ["task add", "task update", "plan add", "decision add", "rule add", "lesson save", "brief set"].every(l => src.includes(`_requireInit(root, '${l}')`)) && !src.includes("_requireInit(root, 'state " + "start')"); const force = /if \(_isInitialized\(root\) \|\| has\('--force'\)\) return true/.test(src); return fnOk && liveOk && emptyOk && wired && force; } },
3086
3113
  { name: 'secret 스캐너 현대 키: OpenAI proj/svcacct·Anthropic api03(_)·GitHub 변종·Stripe·npm 검출 + 오탐 가드 (UR-0050 설치리뷰 1.9.312)', run: () => { const hit = (s) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const named = (s, nm) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s) && p.name === nm; }); const A = 'A'.repeat(40); const projKey = 'sk-' + 'proj-' + A + '_' + A; const svcKey = 'sk-' + 'svcacct-' + A; const antKey = 'sk-' + 'ant-api03-' + A + '_' + A; const ghoKey = 'gho_' + 'a1B2'.repeat(9); const stripeKey = 'sk_' + 'live_' + A; const npmKey = 'npm_' + 'a1B2'.repeat(9); const asiaKey = 'ASIA' + 'ABCD1234EFGH5678'; const legacy = 'sk-' + A; const hits = hit(projKey) && hit(svcKey) && hit(antKey) && hit(ghoKey) && hit(stripeKey) && hit(npmKey) && hit(asiaKey) && hit(legacy); const names = named(projKey, 'OpenAI project/service key') && named(antKey, 'Anthropic API key') && named(stripeKey, 'Stripe secret key') && named(npmKey, 'npm token'); const clean = !hit('const userName = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return hits && names && clean; } },
3087
3114
  { name: 'MCP notification 준수: id없는 요청 무응답 가드 + ping {} (UR-0049 설치리뷰 1.9.313)', run: () => { const src = read(__filename); const guard = src.includes("const isNotification = !('id' in req)") && src.includes("req.method.startsWith('notifications/')") && src.includes('if (isNotification) return;'); const ping = src.includes("req.method === 'ping'") && /ping[\s\S]{0,140}result: \{\} \}/.test(src); return guard && ping; } },
3115
+ { name: 'PowerShell 감지: pwsh7(channel/Documents\\PowerShell/install) + ps5.1 영구경로 과경고 안함 (UR-0052 설치리뷰 1.9.314)', run: () => { const f = _detectPwshFromEnv; const pwsh7a = f({ POWERSHELL_DISTRIBUTION_CHANNEL: 'MSI:Windows 10' }).version === '7'; const pwsh7b = f({ PSModulePath: 'C:\\Users\\me\\Documents\\PowerShell\\Modules' }).version === '7'; const pwsh7c = f({ PSModulePath: 'C:\\Program Files\\PowerShell\\7\\Modules' }).version === '7'; const noFalsePs5 = f({ PSModulePath: 'C:\\Users\\me\\Documents\\WindowsPowerShell\\Modules' }).isPowerShell === false; const cmdSys = f({ PSModulePath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules' }).isPowerShell === false; const empty = f({}).isPowerShell === false; const src = read(__filename); const wired = src.includes('const fromEnv = _detectPwshFromEnv()') && src.includes('const pwshEnv = _detectPwshFromEnv()'); return pwsh7a && pwsh7b && pwsh7c && noFalsePs5 && cmdSys && empty && wired; } },
3116
+ { name: 'doc/surface 정합: doctor 명령 + stale MCP 카운트 동적화(commands/banner) (UR-0054 설치리뷰 1.9.315)', run: () => { const src = read(__filename); const doctorOk = typeof doctorCmd === 'function' && /cmd === 'doctor'/.test(src) && /# leerness doctor/.test(src); const dynCount = /MCP 도구: \$\{_mcpToolCount\(\)\}/.test(src) && /외부 AI 통합 \(MCP \$\{_mcpToolCount\(\)\} 도구\)/.test(src); return doctorOk && dynCount; } },
3088
3117
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3089
3118
  ];
3090
3119
  }
@@ -3146,13 +3175,17 @@ function _detectShellCtx() {
3146
3175
  let env; try { env = _collectRuntimeEnv(); } catch { env = null; }
3147
3176
  let shell = 'unknown';
3148
3177
  const sh = (env && env.terminal && env.terminal.shell) || process.env.SHELL || process.env.ComSpec || '';
3149
- if (/powershell|pwsh/i.test(sh)) shell = 'powershell';
3150
- else if (/cmd\.exe/i.test(sh)) shell = 'cmd';
3178
+ // 1.9.314 (UR-0052, 설치리뷰): PowerShell env 마커 우선 — Windows ComSpec 은 항상 cmd.exe 라 pwsh/ps5 를 cmd 로 오판하던 문제 수정.
3179
+ // psVersion _collectRuntimeEnv(win32 probe) env 마커 순 폴백 (cross-platform pwsh 도 정확).
3180
+ const pwshEnv = _detectPwshFromEnv();
3181
+ const psVersion = (env && env.terminal && env.terminal.powershell) || pwshEnv.version || null;
3182
+ if (/powershell|pwsh/i.test(sh) || pwshEnv.isPowerShell) shell = 'powershell';
3151
3183
  else if (/bash/i.test(sh)) shell = 'bash';
3152
3184
  else if (/\bsh\b|zsh/i.test(sh)) shell = 'sh';
3153
- else if (process.platform === 'win32') shell = (env && env.terminal && env.terminal.powershell) ? 'powershell' : 'cmd';
3154
- const psVersion = env && env.terminal && env.terminal.powershell ? env.terminal.powershell : null;
3155
- return { shell, psVersion, raw: sh };
3185
+ else if (/cmd\.exe/i.test(sh)) shell = 'cmd';
3186
+ else if (process.platform === 'win32') shell = 'cmd';
3187
+ else shell = 'sh';
3188
+ return { shell, psVersion, raw: sh, psEdition: (env && env.terminal && env.terminal.psEdition) || pwshEnv.edition || null };
3156
3189
  }
3157
3190
  // 환경 버전 변동 감지 (environment.json 스냅샷 vs 현재 — 사용자 명시: 과거↔현재 버전 업데이트)
3158
3191
  function _shellEnvDrift(root) {
@@ -3759,7 +3792,7 @@ function commandsCmd(root) {
3759
3792
  log('');
3760
3793
  }
3761
3794
  log(dm(` → 총 ${Object.values(cats).reduce((s, a) => s + a.length, 0)} 명령 (9 카테고리)`));
3762
- log(dm(` → MCP 도구: 65 (외부 AI 노출)`));
3795
+ log(dm(` → MCP 도구: ${_mcpToolCount()} (외부 AI 노출)`)); // 1.9.315 (UR-0054): 하드코딩 65 → 동적
3763
3796
  log(dm(` → 빠른 시작: leerness pulse | handoff | health`));
3764
3797
  }
3765
3798
 
@@ -11253,7 +11286,7 @@ function _banner(opts = {}) {
11253
11286
  cmd('leerness env sync .', '.env ↔ .env.example 동기화');
11254
11287
  cmd('leerness health . --json', '종합 헬스 (drift+보안+skill+feature)');
11255
11288
 
11256
- section('🤖 외부 AI 통합 (MCP 46 도구)');
11289
+ section(`🤖 외부 AI 통합 (MCP ${_mcpToolCount()} 도구)`); // 1.9.315 (UR-0054): 하드코딩 46 → 동적
11257
11290
  cmd('npx leerness mcp serve', 'stdio JSON-RPC server');
11258
11291
  cmd('leerness memory status . --json', '5 surface + featureGraph 한 호출');
11259
11292
  cmd('leerness memory archive list --query "kw"', 'DELETE 5종 archive 검색');
@@ -21354,6 +21387,46 @@ function reviewRequestCmd(root, request) {
21354
21387
 
21355
21388
  // 1.9.164: leerness which — 진단 도구 (구버전 충돌 / npx 캐시 / PATH 충돌 해결)
21356
21389
  // 사용자가 "최신 버전 작동 안 함" 의심 시: 실제 실행 중인 leerness 의 경로 / 버전 / npm 캐시 / PATH 후보 표시.
21390
+ // 1.9.315 (UR-0054, 설치리뷰): leerness doctor — 설치/환경 1콜 진단 (selftest 코어 무결성 + 버전/경로 + 셸/PowerShell).
21391
+ // 배경: 리뷰에서 doctor 명령 부재 지적("doctor no-op"). health(프로젝트 상태)와 달리 doctor 는 "도구 자체"의 정상성 진단.
21392
+ function doctorCmd(opts = {}) {
21393
+ const json = opts.json || has('--json');
21394
+ // 1) 코어 무결성: selftest 케이스 인라인 실행
21395
+ let pass = 0; const failNames = [];
21396
+ try {
21397
+ for (const c of _selfTestCases()) {
21398
+ let okc = false;
21399
+ try { okc = !!c.run(); } catch { okc = false; }
21400
+ if (okc) pass++; else failNames.push(c.name);
21401
+ }
21402
+ } catch {}
21403
+ const total = pass + failNames.length;
21404
+ // 2) 셸/PowerShell 컨텍스트 (UR-0052)
21405
+ let shell = null, psVersion = null;
21406
+ try { const ctx = _detectShellCtx(); shell = ctx.shell; psVersion = ctx.psVersion; } catch {}
21407
+ const mcpCount = (() => { try { return _mcpToolCount(); } catch { return null; } })();
21408
+ const report = {
21409
+ version: VERSION, node: process.version, platform: process.platform + '/' + process.arch,
21410
+ runningFrom: __filename, mcpTools: mcpCount,
21411
+ selftest: { pass, total, ok: failNames.length === 0, failed: failNames },
21412
+ shell, psVersion, healthy: failNames.length === 0
21413
+ };
21414
+ if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
21415
+ const isTty = process.stdout && process.stdout.isTTY;
21416
+ const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s, rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s, cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s, dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
21417
+ log(cy(`# leerness doctor — 설치/환경 진단`));
21418
+ log('');
21419
+ log(` ${gr('✓')} version ${VERSION} · node ${process.version} · ${process.platform}/${process.arch}`);
21420
+ log(` ${gr('✓')} 설치 경로: ${dm(__filename)}`);
21421
+ log(` ${gr('✓')} MCP 도구: ${mcpCount}`);
21422
+ log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total} ${report.selftest.ok ? '통과' : '실패'}`);
21423
+ if (!report.selftest.ok) report.selftest.failed.slice(0, 5).forEach(n => log(rd(` ✗ ${n}`)));
21424
+ log(` ${gr('✓')} 셸: ${shell || 'unknown'}${psVersion && shell === 'powershell' ? ` (PowerShell ${psVersion})` : ''}`);
21425
+ log('');
21426
+ log(report.healthy ? gr(' ✓ leerness 설치 정상') : rd(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which'));
21427
+ if (!report.healthy) process.exitCode = 1;
21428
+ return report;
21429
+ }
21357
21430
  function whichCmd() {
21358
21431
  const out = {
21359
21432
  version: VERSION,
@@ -21527,6 +21600,8 @@ async function main() {
21527
21600
  if (cmd === 'provider') return providerCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21528
21601
  // 1.9.164: leerness which — 진단 도구 (구버전 충돌 / npx 캐시 / PATH 후보)
21529
21602
  if (cmd === 'which') return whichCmd();
21603
+ // 1.9.315 (UR-0054): leerness doctor — 설치/환경 1콜 진단 (selftest + 버전/경로 + 셸)
21604
+ if (cmd === 'doctor') return doctorCmd({ json: has('--json') });
21530
21605
  // 1.9.165: leerness web — playwright bridge (opt-in 의존성)
21531
21606
  if (cmd === 'web') return webCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21532
21607
  // 1.9.166: leerness pc — robotjs/nut-tree bridge (opt-in 의존성)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.313",
3
+ "version": "1.9.315",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3694,5 +3694,39 @@ total++;
3694
3694
  if (!ok) failed++;
3695
3695
  }
3696
3696
 
3697
+ // 1.9.314 회귀 (UR-0049→UR-0052 설치리뷰): PowerShell 감지 — pwsh7(channel) ps5-chain 오탐 제거 + ps5.1 정상 발화
3698
+ // env 마커로 셸/버전 판별(cross-platform) — POWERSHELL_DISTRIBUTION_CHANNEL=pwsh7, Documents\WindowsPowerShell=ps5.1
3699
+ total++;
3700
+ {
3701
+ let ok = false;
3702
+ try {
3703
+ const sg = (extra) => { const r = cp.spawnSync(process.execPath, [CLI, 'shell-guard', 'a && b', '--json'], { encoding: 'utf8', timeout: 15000, env: { ...process.env, ...extra } }); try { return JSON.parse(r.stdout); } catch { return null; } };
3704
+ const j7 = sg({ POWERSHELL_DISTRIBUTION_CHANNEL: 'MSI:Windows 10 Pro', PSModulePath: '', SHELL: '' });
3705
+ const jb = sg({ POWERSHELL_DISTRIBUTION_CHANNEL: '', PSModulePath: 'C:\\Users\\u\\Documents\\WindowsPowerShell\\Modules', SHELL: '/usr/bin/bash' });
3706
+ const pwsh7Ok = j7 && String(j7.psVersion) === '7' && j7.shell === 'powershell' && !(j7.issues || []).some(i => i.rule === 'ps5-chain'); // pwsh7(channel) → ps5-chain 오탐 없음
3707
+ const noFalsePs5 = jb && jb.shell !== 'powershell' && !(jb.issues || []).some(i => i.rule === 'ps5-chain'); // 영구 ps5.1 PSModulePath + bash → ps5 오판/과경고 없음
3708
+ ok = pwsh7Ok && noFalsePs5;
3709
+ } catch {}
3710
+ console.log(ok ? '✓ B(1.9.314) PowerShell 감지: pwsh7(channel) ps5-chain 오탐 제거 + 영구 ps5.1경로 과경고 없음 (UR-0052)' : '✗ PowerShell 감지 실패');
3711
+ if (!ok) failed++;
3712
+ }
3713
+
3714
+ // 1.9.315 회귀 (UR-0054 설치리뷰): doc/surface 정합 — doctor 진단 명령 + stale MCP 카운트 동적화(commands/banner)
3715
+ total++;
3716
+ {
3717
+ let ok = false;
3718
+ try {
3719
+ const r = cp.spawnSync(process.execPath, [CLI, 'doctor', '--json'], { encoding: 'utf8', timeout: 30000 });
3720
+ let j = null; try { j = JSON.parse(r.stdout); } catch {}
3721
+ const doctorOk = j && j.version && typeof j.mcpTools === 'number' && j.mcpTools >= 80 && j.selftest && j.selftest.total > 0 && j.healthy === true && r.status === 0;
3722
+ // commands 요약 + banner 가 실제 MCP 수 노출 (하드코딩 65/46 아님)
3723
+ const rc = cp.spawnSync(process.execPath, [CLI, 'commands'], { encoding: 'utf8', timeout: 15000 });
3724
+ const dynOk = j && new RegExp('MCP 도구: ' + j.mcpTools).test(rc.stdout || '') && !/MCP 도구: 65\b/.test(rc.stdout || '');
3725
+ ok = doctorOk && dynOk;
3726
+ } catch {}
3727
+ console.log(ok ? '✓ B(1.9.315) doc/surface: doctor 진단(selftest+버전+셸) + commands MCP 카운트 동적 (UR-0054)' : '✗ doc/surface 실패');
3728
+ if (!ok) failed++;
3729
+ }
3730
+
3697
3731
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
3698
3732
  if (failed > 0) process.exit(1);