leerness 1.9.291 → 1.9.292

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.292 — 2026-06-03 — UR-0031: get_project_context — MCP 시맨틱 verb (에이전트 온보딩 1콜 집약)
4
+
5
+ **🧭 GPT-5.5 "MCP-first 범용 하네스" 전략의 핵심 — 어떤 에이전트든 leerness 내부 명령을 몰라도 단 1콜로 "지금 무엇을 알아야 하는가"를 파악하는 집약 컨텍스트 verb 신설.**
6
+
7
+ ### 배경
8
+ 1.9.279에서 `leerness_state_*` 도구 설명문에 시맨틱 verb(get_project_context 등)를 표기했으나, 실제 호출 가능한 이름은 여전히 `leerness_state_*` → MCP 클라이언트가 `tools/list`에서 의도 기반 이름을 못 봄. 또한 에이전트가 작업 시작 전 컨텍스트를 모으려면 handoff/state/task/decision/rule 여러 도구를 따로 호출해야 했음.
9
+
10
+ ### 구현 (UR-0031)
11
+ 1. **`leerness context [path] [--json]` CLI 신설** — 현재 작업(in-progress task) / 미답 사용자 요청 / 최근 결정 3건 / 활성 룰 / next-actions / memory surface(진행·결정·룰·교훈) / 프로젝트 의도(project-brief Purpose)를 **단일 구조화 객체**로 집약. 기존 헬퍼(readProgressRows·readRules·_loadUserRequests·_extractDecisionBlocks·_loadNextActionQueue) 재사용 — 신규 가치(집약)이지 단일 도구 중복 아님.
12
+ 2. **MCP `leerness_get_project_context` (80번째 도구)** — `context --json` 호출. read-only. handoff(인간용 장문)와 달리 기계 친화 lean payload. 응답: `{ version, project, currentTask, openRequests, recentDecisions, activeRules, nextActions, memory }`.
13
+ 3. selftest 39→40 (MCP verb 등록 + CLI 디스패치 + `_mcpToolCount()≥80` 소스 정합) · e2e 236→237 (init된 프로젝트에서 집약 JSON 구조 + MCP 80 도구 실측). README MCP 배지 79→80 자동 동기화.
14
+
15
+ ### 검증
16
+ - **selftest 40/40 PASS** · **E2E 237/237 PASS** (회귀 0).
17
+ - B(1.9.288) 도구수 정합 테스트가 배지-live 불일치(79 vs 80)를 즉시 검출 → readme sync로 80 정합(Codex #5 가드 정상 동작 확인).
18
+ - 실측: `context --json` → openRequests 3건(전략 백로그) + activeRules R-0001 + memory 집약. MCP tools/list 80개에 `leerness_get_project_context` 노출.
19
+
3
20
  ## 1.9.291 — 2026-06-03 — UR-0025 (2단계): 외부 에이전트 레지스트리 → lib/agent-registry.js 모듈 분리
4
21
 
5
22
  **🧩 GPT-5.5 + Codex 두 리뷰가 공통 지적한 #1 유지보수 이슈(단일 1.2MB 파일)를 비파괴 모듈 분리로 점진 해소. 1.9.274의 lib/pure-utils.js 패턴 계승.**
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.291-green)]() [![tests](https://img.shields.io/badge/e2e-236%2F236-success)]() [![selftest](https://img.shields.io/badge/selftest-39-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-79-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.292-green)]() [![tests](https://img.shields.io/badge/e2e-237%2F237-success)]() [![selftest](https://img.shields.io/badge/selftest-40-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.291 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.292 하네스를 사용합니다. 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.291는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **79개 도구**를 노출:
516
+ Leerness v1.9.292는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **80개 도구**를 노출:
517
517
 
518
518
  ```jsonc
519
519
  // 카테고리별
@@ -526,7 +526,7 @@ Leerness v1.9.291는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
526
526
  // • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new
527
527
 
528
528
  // MCP server 실행: leerness mcp serve
529
- // tools/list 응답: 79 도구
529
+ // tools/list 응답: 80 도구
530
530
  ```
531
531
 
532
532
  ### Autonomous mode (자율 모드)
@@ -534,7 +534,7 @@ Leerness v1.9.291는 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.291)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
537
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.292)** · 매 라운드 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.291: 2026-06-03
575
+ Last synced by Leerness v1.9.292: 2026-06-03
576
576
  <!-- leerness:project-readme:end -->
577
577
 
package/bin/harness.js CHANGED
@@ -10,7 +10,7 @@ const readline = require('readline');
10
10
  const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
11
11
  PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
12
12
 
13
- const VERSION = '1.9.291';
13
+ const VERSION = '1.9.292';
14
14
 
15
15
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
16
16
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -2998,6 +2998,7 @@ function _selfTestCases() {
2998
2998
  { name: 'coreFiles --minimal: 핵심 유지 + 비핵심 제외 + verify 필수 보존 (1.9.276)', run: () => { const full = coreFiles('.', 'ko', []); const min = coreFiles('.', 'ko', [], { minimal: true }); const keep = ['.harness/plan.md','.harness/progress-tracker.md','.harness/session-handoff.md','AGENTS.md','CLAUDE.md','.harness/consistency-policy.md','.harness/reuse-map.md','.harness/encoding-policy.md','.harness/secret-policy.md']; const drop = ['.cursor/rules/leerness.mdc','.harness/skill-index.md','.harness/architecture.md']; const verifyReq = ['.harness/design-system.md','.harness/protected-files.md','.harness/current-state.md']; if (!verifyReq.every(k => min[k])) return false; return Object.keys(min).length < Object.keys(full).length && keep.every(k => min[k]) && drop.every(k => !min[k]); } },
2999
2999
  { name: '_cliBootstrap: CLI 부작용 require.main 가드 격리 (Codex #4 UR-0037 1.9.290)', run: () => { if (typeof _cliBootstrap !== 'function' || typeof _ensureStdoutEncoding !== 'function') return false; const src = read(__filename); const guarded = /if \(require\.main === module\) _cliBootstrap\(\);/.test(src); const inFn = /function _cliBootstrap\(\)\s*\{[\s\S]*?removeAllListeners\('warning'\)[\s\S]*?NODE_OPTIONS[\s\S]*?_ensureStdoutEncoding\(\);[\s\S]*?\n\}/.test(src); const noTopIife = !/\}\)\(\);\s*\n\n\/\/ 1\.9\.184/.test(src); return guarded && inFn && noTopIife; } },
3000
3000
  { name: 'lib/agent-registry: EXTERNAL_AGENTS/AGENT_SLASH_COMMANDS 모듈 단일출처 분리 (UR-0025 1.9.291)', run: () => { const m = require('../lib/agent-registry'); return m.EXTERNAL_AGENTS === EXTERNAL_AGENTS && m.AGENT_SLASH_COMMANDS === AGENT_SLASH_COMMANDS && m.EXTERNAL_AGENTS.length === 10 && Object.keys(m.AGENT_SLASH_COMMANDS).length === 9 && !/const EXTERNAL_AGENTS = \[/.test(read(__filename)); } },
3001
+ { name: 'get_project_context: MCP 시맨틱 verb 등록 + CLI context 디스패치 (UR-0031 1.9.292)', run: () => { const src = read(__filename); const mcpDef = /name: 'leerness_get_project_context'/.test(src); const mcpCase = /case 'leerness_get_project_context':[\s\S]*?cliArgs = \['context'/.test(src); const cliDisp = /if \(cmd === 'context'\)\s+return contextCmd/.test(src); return typeof contextCmd === 'function' && mcpDef && mcpCase && cliDisp && _mcpToolCount() >= 80; } },
3001
3002
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3002
3003
  ];
3003
3004
  }
@@ -16531,7 +16532,8 @@ function mcpServeCmd(root) {
16531
16532
  { name: 'leerness_state_start', description: '1.9.279 (UR-0031) — start_task. 새 작업 run 시작 (.leerness/runs/run-NNNN.json 생성). 인자: { path?, goal (required), agent?, model?, task? }. 응답: { started, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, goal: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string' }, task: { type: 'string' } }, required: ['goal'] } },
16532
16533
  { name: 'leerness_state_record', description: '1.9.279 (UR-0031) — record_file_change / record_decision. 진행 중 run 에 변경 파일/명령/테스트/결정을 누적. 인자: { path?, filesChanged? (콤마), filesRead?, commands?, tests?, errors?, decision? }. 응답: { recorded, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, filesChanged: { type: 'string' }, filesRead: { type: 'string' }, commands: { type: 'string' }, tests: { type: 'string' }, errors: { type: 'string' }, decision: { type: 'string' } } } },
16533
16534
  { name: 'leerness_state_verify', description: '1.9.279 (UR-0031) — request_verification / verify_done. 현재 run 의 검증 결과 기록. 인자: { path?, result (required: "pass"|"fail"), note? }. 응답: { verified, result, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, result: { type: 'string', enum: ['pass', 'fail'] }, note: { type: 'string' } }, required: ['result'] } },
16534
- { name: 'leerness_state_handoff', description: '1.9.279 (UR-0031) — create_handoff / make_handoff. 현재 run 을 마감하고 다음 에이전트용 .leerness/handoff/latest.{md,json} 작성 (Claude→Goose→Codex 인수인계 표준). 인자: { path?, summary (required) }. 응답: { handoff, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, summary: { type: 'string' } }, required: ['summary'] } }
16535
+ { name: 'leerness_state_handoff', description: '1.9.279 (UR-0031) — create_handoff / make_handoff. 현재 run 을 마감하고 다음 에이전트용 .leerness/handoff/latest.{md,json} 작성 (Claude→Goose→Codex 인수인계 표준). 인자: { path?, summary (required) }. 응답: { handoff, run }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, summary: { type: 'string' } }, required: ['summary'] } },
16536
+ { name: 'leerness_get_project_context', description: '1.9.292 (UR-0031) — get_project_context. 외부 에이전트가 작업 시작 전 단 1콜로 "지금 무엇을 알아야 하는가"를 파악하는 집약 컨텍스트. 여러 소스(현재 작업/미답 사용자 요청/최근 결정/활성 룰/next-actions/memory surface/프로젝트 의도)를 한 번에 구조화 회수. read-only. 인자: { path? }. 응답: { version, project, currentTask, openRequests, recentDecisions, activeRules, nextActions, memory }.', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } }
16535
16537
  ];
16536
16538
 
16537
16539
  function send(obj) {
@@ -16781,6 +16783,9 @@ function mcpServeCmd(root) {
16781
16783
  case 'leerness_state_handoff':
16782
16784
  cliArgs = ['state', 'handoff', String(args.summary || ''), '--path', targetPath, '--json'];
16783
16785
  break;
16786
+ case 'leerness_get_project_context':
16787
+ cliArgs = ['context', '--path', targetPath, '--json'];
16788
+ break;
16784
16789
  default:
16785
16790
  return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
16786
16791
  }
@@ -18105,6 +18110,77 @@ function _saveRun(root, rec) { const f = _runFile(root, rec.run_id); mkdirp(path
18105
18110
  function _splitList(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
18106
18111
 
18107
18112
  // leerness state <show|start|record|verify|handoff>
18113
+ // 1.9.292 (UR-0031): get_project_context — 외부 에이전트 온보딩용 단일 집약 컨텍스트.
18114
+ // 1콜로 현재 작업/미답 요청/최근 결정/활성 룰/next-actions/memory/프로젝트 의도를 구조화 회수.
18115
+ // MCP leerness_get_project_context 가 이 명령(--json)을 호출. handoff(인간용 장문)와 달리 기계 친화 lean payload.
18116
+ function contextCmd(root, opts = {}) {
18117
+ root = absRoot(root);
18118
+ const rows = readProgressRows(root);
18119
+ const active = rows.find(r => r.status === 'in-progress' || r.status === '[진행]');
18120
+ const activeRules = readRules(root).filter(r => r.status === 'active');
18121
+ const openReqs = (_loadUserRequests(root).requests || []).filter(r => r.status === 'open' || r.status === 'in-progress');
18122
+ // 최근 결정 (last 3, 최신순) — decisionListCmd 와 동일 파서 재사용
18123
+ const decFile = decisionsPath(root);
18124
+ let recentDecisions = [];
18125
+ let decisionCount = 0;
18126
+ if (exists(decFile)) {
18127
+ const dtext = read(decFile);
18128
+ decisionCount = (dtext.match(/^### \d{4}-\d{2}-\d{2}/gm) || []).length;
18129
+ const blocks = _extractDecisionBlocks(dtext);
18130
+ recentDecisions = blocks.slice(-3).reverse().map(block => {
18131
+ const tm = (block.match(/^### (.+)$/m) || [])[1] || '';
18132
+ const dt = tm.match(/^(\d{4}-\d{2}-\d{2})\s*—\s*(.+)$/);
18133
+ return { date: dt ? dt[1] : null, title: (dt ? dt[2] : tm).trim().slice(0, 100) };
18134
+ });
18135
+ }
18136
+ const queueState = _loadNextActionQueue(root);
18137
+ const nextActions = (queueState.queue || []).slice(-3).reverse().map(a => ({ title: a.title, command: a.command || null }));
18138
+ const memory = {
18139
+ tasksInProgress: rows.filter(r => r.status === 'in-progress').length,
18140
+ decisions: decisionCount,
18141
+ rulesActive: activeRules.length,
18142
+ lessons: exists(lessonsPath(root)) ? (read(lessonsPath(root)).match(/^### \d{4}-\d{2}-\d{2}/gm) || []).length : 0
18143
+ };
18144
+ // 프로젝트 의도 (best-effort) — .harness/project-brief.md 의 ## Purpose 첫 줄
18145
+ let intent = null;
18146
+ const briefPath = path.join(root, '.harness', 'project-brief.md');
18147
+ if (exists(briefPath)) {
18148
+ const m = read(briefPath).match(/##\s*Purpose\s*\n+\s*[-*]?\s*([^\n#].*)/);
18149
+ if (m && !/업데이트하세요/.test(m[1])) intent = m[1].trim().slice(0, 200);
18150
+ }
18151
+ const ctx = {
18152
+ schemaVersion: 1,
18153
+ version: VERSION,
18154
+ project: { root, intent },
18155
+ currentTask: active ? { id: active.id, request: (active.request || '').slice(0, 200), status: active.status, nextAction: (active.nextAction || '').slice(0, 160) || null } : null,
18156
+ openRequests: { count: openReqs.length, items: openReqs.slice(0, 5).map(r => ({ id: r.id, text: (r.text || '').slice(0, 120) })) },
18157
+ recentDecisions,
18158
+ activeRules: activeRules.map(r => ({ id: r.id, trigger: r.trigger, rule: r.rule })),
18159
+ nextActions,
18160
+ memory
18161
+ };
18162
+ if (opts.json || has('--json')) { process.stdout.write(JSON.stringify(ctx, null, 2) + '\n'); return ctx; }
18163
+ const isTty = process.stdout && process.stdout.isTTY;
18164
+ const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
18165
+ const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
18166
+ const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
18167
+ log(cy(`# leerness context (1.9.292) — 에이전트 온보딩 컨텍스트 (v${VERSION})`));
18168
+ if (intent) log(` 🎯 의도: ${intent}`);
18169
+ log('');
18170
+ if (ctx.currentTask) {
18171
+ log(gr(`▶ 현재 작업: ${ctx.currentTask.id} — ${ctx.currentTask.request}`));
18172
+ if (ctx.currentTask.nextAction) log(dm(` 다음: ${ctx.currentTask.nextAction}`));
18173
+ } else log(dm('▶ 현재 진행 중 작업 없음'));
18174
+ log('');
18175
+ log(`📥 미답 요청: ${ctx.openRequests.count}건`);
18176
+ ctx.openRequests.items.forEach(r => log(dm(` • [${r.id}] ${r.text}`)));
18177
+ log('');
18178
+ log(`🧠 메모리: 진행 ${memory.tasksInProgress} / 결정 ${memory.decisions} / 룰 ${memory.rulesActive} / 교훈 ${memory.lessons}`);
18179
+ if (recentDecisions.length) { log(''); log('🗂 최근 결정:'); recentDecisions.forEach(d => log(dm(` • ${d.date || '?'} — ${d.title}`))); }
18180
+ if (ctx.activeRules.length) { log(''); log('⚡ 활성 룰:'); ctx.activeRules.forEach(r => log(dm(` • [${r.id}] (${r.trigger}) ${r.rule}`))); }
18181
+ if (nextActions.length) { log(''); log('👉 다음 액션:'); nextActions.forEach(a => log(dm(` • ${a.title}${a.command ? ' → ' + a.command : ''}`))); }
18182
+ return ctx;
18183
+ }
18108
18184
  function stateCmd(root, sub, ...args) {
18109
18185
  root = absRoot(root || process.cwd());
18110
18186
  const json = has('--json');
@@ -21315,6 +21391,7 @@ async function main() {
21315
21391
  if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
21316
21392
  // 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
21317
21393
  if (cmd === 'state') return stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21394
+ if (cmd === 'context') return contextCmd(arg('--path', process.cwd()), { json: has('--json') });
21318
21395
  // 1.9.281 (UR-0034): leerness policy <show|set|check> — 권한 등급 (opt-in enforced)
21319
21396
  if (cmd === 'policy') return policyCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21320
21397
  // 1.9.285 (UR-0023): leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 결정 게이트 (오프라인)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.291",
3
+ "version": "1.9.292",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3136,5 +3136,23 @@ total++;
3136
3136
  if (!ok) failed++;
3137
3137
  }
3138
3138
 
3139
+ // 1.9.292 회귀 (UR-0031): leerness context — get_project_context 집약 시맨틱 verb
3140
+ total++;
3141
+ {
3142
+ let ok = false;
3143
+ try {
3144
+ const r = cp.spawnSync(process.execPath, [CLI, 'context', tmp, '--json'], { cwd: tmp, encoding: 'utf8', timeout: 30000 });
3145
+ const j = JSON.parse(r.stdout);
3146
+ const structOk = r.status === 0 && j.schemaVersion === 1 && !!j.version && !!j.project && ('currentTask' in j) &&
3147
+ j.openRequests && typeof j.openRequests.count === 'number' && Array.isArray(j.recentDecisions) &&
3148
+ Array.isArray(j.activeRules) && Array.isArray(j.nextActions) && j.memory && typeof j.memory.rulesActive === 'number';
3149
+ const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3150
+ const mcpOk = h._mcpToolCount && h._mcpToolCount() >= 80;
3151
+ ok = structOk && mcpOk;
3152
+ } catch {}
3153
+ console.log(ok ? '✓ B(1.9.292) leerness context: get_project_context 집약 JSON 구조 + MCP 80 도구 (UR-0031)' : '✗ context 집약 verb 실패');
3154
+ if (!ok) failed++;
3155
+ }
3156
+
3139
3157
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
3140
3158
  if (failed > 0) process.exit(1);