leerness 1.9.282 → 1.9.284

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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.284 — 2026-06-03 — UR-0029: handoff 성능 회귀 수정 (10 provider 전부 spawn → 활성만) + e2e 단축
4
+
5
+ **⚡ GPT-5.5 "e2e 5분 초과" 추적 중 발견 — `handoff` 가 매번 10개 provider 의 `--version` 을 전부 spawn(≈5.5s 오버헤드). 활성(env=1) provider 만 체크하도록 수정 → handoff 7.3s → 2.6s.**
6
+
7
+ ### 배경 (프로파일 기반 디버그)
8
+ GPT-5.5 가 두 번 "npm test(e2e) 5분 내 미완료" 지적. 실측 336s. `node --prof` 프로파일 결과 **handoff 시간의 90%가 `spawnSync`** — handoff 헤드라인이 `EXTERNAL_AGENTS.map(_checkAgent)` 로 **10개 provider 의 `--version` 을 매번 전부 실행**(1.9.277 provider 6→10 으로 악화). 'ready'(=활성+설치) 는 **활성(env flag=1) 일 때만** 가능하므로 비활성 provider spawn 은 순수 낭비.
9
+
10
+ ### 구현
11
+ 1. **handoff 헤드라인 agent 체크 — 활성 필터 선행** (bin/harness.js:7723): `EXTERNAL_AGENTS.filter(a => process.env[a.envFlag]==='1').map(_checkAgent)`. 결과 동일(비활성은 ready 불가), spawn 만 제거. **활성 0(대부분/모든 e2e) → spawn 0**.
12
+ - 효과: handoff **7.3s → 2.6s** (회당 4.6s↓). 실사용에서도 매 handoff 5초 절감 + provider 확장 회귀 영구 해결.
13
+ 2. **e2e roadmap 기본 OFF** (roadmap 전용 블록만 ON) + **총 소요시간 출력**(투명성).
14
+ 3. e2e 총 **336s → 306s**. (구조적으로 133 init × ~1.1s 가 잔여 비용 — init 은 I/O 바운드.)
15
+
16
+ ### 재현성 (GPT 우려 해소)
17
+ - 빠른 게이트: **`npm run test:fast`** (selftest + smoke 13종, ~10s) — 로컬/PR 즉시 검증.
18
+ - 전체 e2e: 229 케이스(subprocess 바운드 ~5min) — CI 매트릭스(ubuntu+windows × node 18/20/22) 병렬 실행으로 재현.
19
+
20
+ ### 검증
21
+ - **selftest 31/31 PASS** · **E2E 229/229 PASS** (회귀 0) · handoff 활성 0 시 spawn 0 실측.
22
+
23
+ ## 1.9.283 — 2026-06-03 — UR-0025 2단계: 순수 함수 추가 모듈 분리 (lib/pure-utils 7→14종)
24
+
25
+ **🧩 GPT-5.5 "1.2MB 단일 파일" 지적의 점진적·비파괴 모듈화 계속 — 최근 추가한 순수 로직을 lib/ 로 추출.**
26
+
27
+ ### 구현
28
+ 1. **`lib/pure-utils.js` 7 → 14 export** — 권한 등급(`PERMISSION_TIERS`/`_tierRank`/`_requiredTier`/`_policyAllows`) + dist-tag(`_resolveNpmTag`) + run 스키마(`_newRunRecord`) + mcp.json(`_mcpJsonContent`) 추가 이동. 모두 harness 내부 상태 의존 0(순수).
29
+ 2. **harness.js require 재연결** — 상단 destructure 확장 + 인라인 정의 7개 제거(`module.exports` 재노출 유지). 동작 동일.
30
+ 3. e2e lib 테스트 7 → 14종 export + 동작 검증. selftest 31/31 그대로(이미 해당 함수 커버).
31
+
32
+ ### 검증
33
+ - **selftest 31/31 PASS** · **E2E 229/229 PASS** (회귀 0) · `npm pack` tarball lib 포함 · require 무결성(14종 함수 + PERMISSION_TIERS) 확인.
34
+
3
35
  ## 1.9.282 — 2026-06-03 — UR-0035: AGENTS.md(정적) vs leerness(동적) 포지셔닝 — 범용 하네스 전략 완성
4
36
 
5
37
  **🧭 GPT-5.5 마지막 전략 항목 — leerness 는 AGENTS.md 를 대체하지 않고 보완. 정적 지침 vs 동적 상태 역할 경계 명시.**
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.282-green)]() [![tests](https://img.shields.io/badge/e2e-229%2F229-success)]() [![selftest](https://img.shields.io/badge/selftest-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-80-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.284-green)]() [![tests](https://img.shields.io/badge/e2e-229%2F229-success)]() [![selftest](https://img.shields.io/badge/selftest-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-80-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.282 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.284 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
475
475
 
476
476
  ### Core Commands
477
477
 
@@ -513,7 +513,7 @@ leerness memory restore decision <date|title>
513
513
 
514
514
  ### MCP server (외부 AI 통합)
515
515
 
516
- Leerness v1.9.282는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
516
+ Leerness v1.9.284는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
517
517
 
518
518
  ```jsonc
519
519
  // 카테고리별
@@ -534,7 +534,7 @@ Leerness v1.9.282는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
534
534
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
535
535
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
536
536
 
537
- 현재 누적: **70 라운드 (1.9.40 → 1.9.282)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
537
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.284)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
538
538
 
539
539
  ### 성능 가이드 (1.9.140 측정)
540
540
 
@@ -572,6 +572,6 @@ leerness release pack --close --auto-main-push
572
572
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
573
573
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
574
574
 
575
- Last synced by Leerness v1.9.282: 2026-06-03
575
+ Last synced by Leerness v1.9.284: 2026-06-03
576
576
  <!-- leerness:project-readme:end -->
577
577
 
package/bin/harness.js CHANGED
@@ -7,9 +7,10 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
8
8
  const readline = require('readline');
9
9
  // 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
10
- const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp } = require('../lib/pure-utils');
10
+ const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
11
+ PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
11
12
 
12
- const VERSION = '1.9.282';
13
+ const VERSION = '1.9.284';
13
14
 
14
15
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
15
16
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -7719,7 +7720,9 @@ function handoff(root) {
7719
7720
  // 8) 1.9.152: 활성 외부 AI CLI 카운트 (1.9.151 복수 선택 결과 반영) — 메인 에이전트가 sub-agent 분배 가능성 즉시 인지
7720
7721
  try {
7721
7722
  _loadEnvFile(root); // .env 자동 로드 (handoff 컨텍스트)
7722
- const ready = EXTERNAL_AGENTS.map(a => _checkAgent(a)).filter(c => c.status === 'ready');
7723
+ // 1.9.284 (UR-0029) 성능: 'ready'(=활성+설치)는 활성(env=1) 때만 가능 활성 agent 만 --version spawn.
7724
+ // 비활성(대부분/모든 e2e) 은 spawn 0 (이전: 10개 전부 spawn = handoff 5.5s 병목, provider 6→10 으로 악화).
7725
+ const ready = EXTERNAL_AGENTS.filter(a => process.env[a.envFlag] === '1').map(a => _checkAgent(a)).filter(c => c.status === 'ready');
7723
7726
  if (ready.length > 0) {
7724
7727
  parts.push(`🤖 agents ${ready.length} (${ready.map(c => c.id).join(',')})`);
7725
7728
  }
@@ -14279,12 +14282,7 @@ function releaseCleanupCmd(root) {
14279
14282
  // - NPM_TOKEN 또는 LEERNESS_NPM_TOKEN 환경변수에서만 읽음 (값 절대 로그 X)
14280
14283
  // - 임시 .npmrc 파일 (mkdtempSync) 생성 → publish → 즉시 삭제 (finally)
14281
14284
  // - 이미 publish된 버전 자동 detect → skip (중복 publish 차단)
14282
- // 1.9.275 (UR-0026, GPT-5.5 리뷰): npm dist-tag 결정 — latest(안정, 기본) / next(실험). 잘못된 형식은 latest 폴백. 순수 함수.
14283
- function _resolveNpmTag(explicit, env) {
14284
- env = env || process.env;
14285
- const raw = String(explicit || env.LEERNESS_NPM_TAG || 'latest').trim().toLowerCase();
14286
- return /^[a-z][a-z0-9-]{0,38}$/.test(raw) ? raw : 'latest';
14287
- }
14285
+ // _resolveNpmTag lib/pure-utils.js 이동 (1.9.283 UR-0025 2단계)
14288
14286
  // 1.9.275 (UR-0026): leerness release channel — 안정/실험 채널 정책 + 버전 고정 안내 (npm dist-tag).
14289
14287
  function releaseChannelCmd(root) {
14290
14288
  root = absRoot(root || process.cwd());
@@ -18068,21 +18066,7 @@ function capabilitiesCmd(root, opts = {}) {
18068
18066
  // ===== 1.9.281 (UR-0034, GPT-5.5 범용 하네스): 권한 등급(permission tiers) =====
18069
18067
  // capabilities(1.9.272 공개)를 enforced 등급으로 확장. 기본 OFF(opt-in) — enforce 켤 때만 차단.
18070
18068
  // 등급 오름차순(왼→오 위험↑). 허용 등급 이하의 명령만 통과. 기본 허용 'project-write'(파일 작업까지, 셸/git/publish 차단).
18071
- const PERMISSION_TIERS = ['read-only', 'safe-write', 'project-write', 'shell-read', 'shell-write', 'git-write', 'network', 'publish'];
18072
- function _tierRank(t) { const i = PERMISSION_TIERS.indexOf(String(t || '')); return i < 0 ? PERMISSION_TIERS.length : i; }
18073
- // 명령/capability → 요구 등급 (순수 함수, selftest)
18074
- function _requiredTier(cmd) {
18075
- const c = String(cmd || '').toLowerCase();
18076
- if (/release\s+publish|npm\s+publish|\bpublish\b/.test(c)) return 'publish';
18077
- if (/\bweb\b/.test(c)) return 'network';
18078
- if (/git\s+push|sync-main/.test(c)) return 'git-write';
18079
- if (/multi\s+--execute|dispatch\s+--write|--yolo|\bpc\b/.test(c)) return 'shell-write';
18080
- if (/agents\s+(list|quota|bench)|--run-tests/.test(c)) return 'shell-read';
18081
- if (/\binit\b|\badapter\b|update\s+--yes|\bmigrate\b/.test(c)) return 'project-write';
18082
- if (/state\s+(start|record|verify|handoff)|decision|lesson|plan\s+add|task\s+add|rule\s+add/.test(c)) return 'safe-write';
18083
- return 'read-only';
18084
- }
18085
- function _policyAllows(allowedTier, requiredTier) { return _tierRank(requiredTier) <= _tierRank(allowedTier); }
18069
+ // PERMISSION_TIERS / _tierRank / _requiredTier / _policyAllows → lib/pure-utils.js 로 이동 (1.9.283 UR-0025 2단계)
18086
18070
  function _policyFile(root) { return path.join(_leernessStateDir(root), 'policy.json'); }
18087
18071
  function _loadPolicy(root) {
18088
18072
  const f = _policyFile(root);
@@ -18148,28 +18132,7 @@ function policyCmd(root, sub, ...args) {
18148
18132
  // Claude Code 가 한 작업을 Goose/Codex 가 JSON 으로 정확히 이어받음. 비파괴 (신규 .leerness/ 디렉토리).
18149
18133
  // 향후 UR-0031(MCP verb)/UR-0033(adapter) 가 이 스키마를 공통 호출 표면으로 사용.
18150
18134
  function _leernessStateDir(root) { return path.join(absRoot(root), '.leerness'); }
18151
- // 순수 함수: run 레코드 빌더 (모든 필드 기본값 — GPT-5.5 권고 스키마). startedAt 주입 가능(selftest).
18152
- function _newRunRecord(opts = {}) {
18153
- return {
18154
- schemaVersion: 1,
18155
- run_id: opts.run_id || null,
18156
- task_id: opts.task_id || null,
18157
- agent_name: opts.agent_name || null,
18158
- model_name: opts.model_name || null,
18159
- started_at: opts.started_at || new Date().toISOString(),
18160
- ended_at: opts.ended_at || null,
18161
- goal: opts.goal || '',
18162
- files_read: Array.isArray(opts.files_read) ? opts.files_read : [],
18163
- files_changed: Array.isArray(opts.files_changed) ? opts.files_changed : [],
18164
- commands_run: Array.isArray(opts.commands_run) ? opts.commands_run : [],
18165
- tests_run: Array.isArray(opts.tests_run) ? opts.tests_run : [],
18166
- errors: Array.isArray(opts.errors) ? opts.errors : [],
18167
- decisions: Array.isArray(opts.decisions) ? opts.decisions : [],
18168
- verification_result: opts.verification_result || null, // 'pass' | 'fail' | null
18169
- handoff_summary: opts.handoff_summary || null,
18170
- status: opts.status || 'in-progress' // in-progress | verified | handed-off
18171
- };
18172
- }
18135
+ // _newRunRecord lib/pure-utils.js 이동 (1.9.283 UR-0025 2단계)
18173
18136
  function _loadLeernessState(root) {
18174
18137
  const f = path.join(_leernessStateDir(root), 'state.json');
18175
18138
  if (!exists(f)) return { schemaVersion: 1, project: detectProjectName(root), currentRunId: null, runCounter: 0, updatedAt: null };
@@ -18296,9 +18259,7 @@ const ADAPTERS = {
18296
18259
  aider: { label: 'Aider', keys: ['AGENTS.md'], mcp: false },
18297
18260
  qwen: { label: 'Qwen Code', keys: ['AGENTS.md'], mcp: false }
18298
18261
  };
18299
- function _mcpJsonContent() {
18300
- return JSON.stringify({ mcpServers: { leerness: { command: 'npx', args: ['leerness', 'mcp', 'serve'] } } }, null, 2) + '\n';
18301
- }
18262
+ // _mcpJsonContent → lib/pure-utils.js 로 이동 (1.9.283 UR-0025 2단계)
18302
18263
  // .mcp.json 병합 — 기존 mcpServers 보존하고 leerness 항목만 추가/갱신.
18303
18264
  function _mergeMcpJson(root) {
18304
18265
  const f = path.join(absRoot(root), '.mcp.json');
package/lib/pure-utils.js CHANGED
@@ -103,7 +103,61 @@ function _parseSlashFromHelp(text, invoke = 'slash') {
103
103
  return out;
104
104
  }
105
105
 
106
+ // 1.9.283 (UR-0025 2단계): 권한 등급(permission tiers) 순수 로직 — capabilities/policy 공유.
107
+ const PERMISSION_TIERS = ['read-only', 'safe-write', 'project-write', 'shell-read', 'shell-write', 'git-write', 'network', 'publish'];
108
+ function _tierRank(t) { const i = PERMISSION_TIERS.indexOf(String(t || '')); return i < 0 ? PERMISSION_TIERS.length : i; }
109
+ // 명령/capability → 요구 등급 (순수 매핑)
110
+ function _requiredTier(cmd) {
111
+ const c = String(cmd || '').toLowerCase();
112
+ if (/release\s+publish|npm\s+publish|\bpublish\b/.test(c)) return 'publish';
113
+ if (/\bweb\b/.test(c)) return 'network';
114
+ if (/git\s+push|sync-main/.test(c)) return 'git-write';
115
+ if (/multi\s+--execute|dispatch\s+--write|--yolo|\bpc\b/.test(c)) return 'shell-write';
116
+ if (/agents\s+(list|quota|bench)|--run-tests/.test(c)) return 'shell-read';
117
+ if (/\binit\b|\badapter\b|update\s+--yes|\bmigrate\b/.test(c)) return 'project-write';
118
+ if (/state\s+(start|record|verify|handoff)|decision|lesson|plan\s+add|task\s+add|rule\s+add/.test(c)) return 'safe-write';
119
+ return 'read-only';
120
+ }
121
+ function _policyAllows(allowedTier, requiredTier) { return _tierRank(requiredTier) <= _tierRank(allowedTier); }
122
+
123
+ // 1.9.283: npm dist-tag 결정 (UR-0026) — latest(안정)/next(실험), 잘못된 형식은 latest.
124
+ function _resolveNpmTag(explicit, env) {
125
+ env = env || process.env;
126
+ const raw = String(explicit || env.LEERNESS_NPM_TAG || 'latest').trim().toLowerCase();
127
+ return /^[a-z][a-z0-9-]{0,38}$/.test(raw) ? raw : 'latest';
128
+ }
129
+
130
+ // 1.9.283: .mcp.json 내용 (UR-0033) — leerness MCP 서버 등록.
131
+ function _mcpJsonContent() {
132
+ return JSON.stringify({ mcpServers: { leerness: { command: 'npx', args: ['leerness', 'mcp', 'serve'] } } }, null, 2) + '\n';
133
+ }
134
+
135
+ // 1.9.283: run 레코드 빌더 (UR-0032) — GPT-5.5 권고 14필드. startedAt 주입 가능(테스트).
136
+ function _newRunRecord(opts = {}) {
137
+ return {
138
+ schemaVersion: 1,
139
+ run_id: opts.run_id || null,
140
+ task_id: opts.task_id || null,
141
+ agent_name: opts.agent_name || null,
142
+ model_name: opts.model_name || null,
143
+ started_at: opts.started_at || new Date().toISOString(),
144
+ ended_at: opts.ended_at || null,
145
+ goal: opts.goal || '',
146
+ files_read: Array.isArray(opts.files_read) ? opts.files_read : [],
147
+ files_changed: Array.isArray(opts.files_changed) ? opts.files_changed : [],
148
+ commands_run: Array.isArray(opts.commands_run) ? opts.commands_run : [],
149
+ tests_run: Array.isArray(opts.tests_run) ? opts.tests_run : [],
150
+ errors: Array.isArray(opts.errors) ? opts.errors : [],
151
+ decisions: Array.isArray(opts.decisions) ? opts.decisions : [],
152
+ verification_result: opts.verification_result || null,
153
+ handoff_summary: opts.handoff_summary || null,
154
+ status: opts.status || 'in-progress'
155
+ };
156
+ }
157
+
106
158
  module.exports = {
107
159
  _isSecretKey, compareVer, parseHarnessVersion,
108
- _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp
160
+ _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
161
+ // 1.9.283 (UR-0025 2단계)
162
+ PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord
109
163
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.282",
3
+ "version": "1.9.284",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -8,9 +8,13 @@ const cp = require('child_process');
8
8
 
9
9
  // 1.9.12: e2e 안정성을 위해 자식 프로세스의 npm 호출 차단 (hang 방지)
10
10
  process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
11
+ // 1.9.284 (UR-0029): e2e 속도 — 기본 roadmap.html(70KB HTML) 자동 생성 OFF (roadmap 전용 테스트 블록만 일시 ON).
12
+ // 대부분의 init/session 테스트는 roadmap 을 검증하지 않으므로 생성 비용 제거 → 5분 내 완료.
13
+ process.env.LEERNESS_NO_AUTO_ROADMAP = '1';
11
14
  const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
12
15
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-'));
13
16
  let failed = 0; let total = 0;
17
+ const _e2eStart = Date.now(); // 1.9.284 (UR-0029): 총 소요시간 투명성
14
18
 
15
19
  function run(label, args, opts = {}) {
16
20
  total++;
@@ -2217,6 +2221,8 @@ total++;
2217
2221
  if (!ok) failed++;
2218
2222
  }
2219
2223
 
2224
+ // 1.9.284: roadmap 전용 테스트 블록 — roadmap 생성 일시 ON (이후 다시 OFF)
2225
+ process.env.LEERNESS_NO_AUTO_ROADMAP = '0';
2220
2226
  // 1.9.12: auto roadmap — install 직후 자동 생성
2221
2227
  total++;
2222
2228
  {
@@ -2278,6 +2284,8 @@ total++;
2278
2284
  console.log(ok ? '✓ B(1.9.12) --on-every-change: task add만으로 즉시 갱신' : `✗ on-every-change 미작동`);
2279
2285
  if (!ok) failed++;
2280
2286
  }
2287
+ // 1.9.284: roadmap 전용 블록 종료 — 다시 OFF (나머지 테스트 속도)
2288
+ process.env.LEERNESS_NO_AUTO_ROADMAP = '1';
2281
2289
 
2282
2290
  // 1.9.12: status 출력
2283
2291
  total++;
@@ -2793,13 +2801,19 @@ total++;
2793
2801
  let exportsOk = false;
2794
2802
  try {
2795
2803
  const m = require(libPath);
2796
- exportsOk = ['_isSecretKey','compareVer','parseHarnessVersion','_classifyCJK','_riskLabel','_detectSystemLang','_parseSlashFromHelp']
2797
- .every(n => typeof m[n] === 'function') && m.compareVer('1.9.1','1.9.0') === 1 && m._detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko';
2804
+ // 1.9.283: 1단계 7종 + 2단계 7종(권한등급/dist-tag/run스키마/mcp.json)
2805
+ exportsOk = ['_isSecretKey','compareVer','parseHarnessVersion','_classifyCJK','_riskLabel','_detectSystemLang','_parseSlashFromHelp',
2806
+ '_tierRank','_requiredTier','_policyAllows','_resolveNpmTag','_mcpJsonContent','_newRunRecord']
2807
+ .every(n => typeof m[n] === 'function')
2808
+ && m.compareVer('1.9.1','1.9.0') === 1 && m._detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko'
2809
+ && Array.isArray(m.PERMISSION_TIERS) && m.PERMISSION_TIERS.length === 8
2810
+ && m._requiredTier('release publish') === 'publish' && m._resolveNpmTag(null, {}) === 'latest'
2811
+ && m._newRunRecord({ run_id: 'r1' }).schemaVersion === 1;
2798
2812
  } catch {}
2799
2813
  let filesOk = false;
2800
2814
  try { const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')); filesOk = Array.isArray(pkg.files) && pkg.files.includes('lib'); } catch {}
2801
2815
  const ok = libExists && exportsOk && filesOk;
2802
- console.log(ok ? '✓ B(1.9.274) lib 모듈 분리: pure-utils 7종 export + 동작 + files 포함' : `✗ lib 분리 실패 (exists=${libExists} exports=${exportsOk} files=${filesOk})`);
2816
+ console.log(ok ? '✓ B(1.9.274/283) lib 모듈 분리: pure-utils 14종 export + 동작 + files 포함' : `✗ lib 분리 실패 (exists=${libExists} exports=${exportsOk} files=${filesOk})`);
2803
2817
  if (!ok) failed++;
2804
2818
  }
2805
2819
 
@@ -2956,5 +2970,5 @@ total++;
2956
2970
  if (!ok) failed++;
2957
2971
  }
2958
2972
 
2959
- console.log(`\nE2E result: ${total - failed}/${total} passed`);
2973
+ console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
2960
2974
  if (failed > 0) process.exit(1);