leerness 1.9.377 → 1.9.379

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,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.379 — 2026-06-06 — UR-0025 심화: pulse 핸들러 렌더 코어 분리 (수집→렌더 패턴 착수)
4
+
5
+ **🧩 큰 핸들러 모듈화 착수 — display 핸들러의 "수집(I/O) → 렌더(순수)" 분리 패턴을 pulse 에 적용.**
6
+
7
+ ### 배경
8
+ 사용자 선택(큰 핸들러 모듈화/UR-0025 심화). 대형 I/O-결합 핸들러를 안전하게 분해하기 위해, **순수한 렌더/포맷 코어**를 lib/pure-utils 로 분리하고 핸들러는 gather(I/O)+순수 렌더 구조로 정리하는 패턴을 확립. 첫 대상: pulse(집계 display 핸들러).
9
+
10
+ ### 구현
11
+ 1. **`_memorySurface(counts)`**(pure-utils): T/D/R/P/L 카운트 → 문자열. pulse + memory-status 단일출처(중복 포맷 제거).
12
+ 2. **`_renderPulseLine(data)`**(pure-utils): gather 된 data → 한 줄 요약 문자열(마일스톤/비정상종료 조건부 포함).
13
+ 3. **pulseCmd**: 인라인 memory-surface 조합 + 한 줄 조합을 순수 코어 호출로 교체(gather 부분만 I/O 유지).
14
+
15
+ ### 검증 (회귀 0)
16
+ - **selftest 124→125 PASS** (행위: `_memorySurface` 카운트→문자열, `_renderPulseLine` 기본/마일스톤/abnormal 분기 + pulse 와이어).
17
+ - **E2E 323→324 PASS** (행위: 빌더 동작 + pulse CLI 한 줄 출력 정규식 유지).
18
+ - 실측: pulse human/JSON 출력 동일.
19
+
20
+ ## 1.9.378 — 2026-06-06 — UR-0073: 에이전트 팀 MCP 노출 (read-only list/preview)
21
+
22
+ **🧩 신규 team 서브시스템을 MCP 로 외부 AI 에 노출 — 에이전트 팀 비전을 외부 에이전트가 직접 활용.**
23
+
24
+ ### 배경
25
+ leerness 는 MCP-first(80+ 도구)이나 신규 team 서브시스템(UR-0073)이 MCP 미노출이었음. 외부 AI 에이전트가 팀을 조회/미리보기할 수 있어야 비전("에이전트가 팀 관리/실행")이 완성. **읽기 전용만 노출**(deploy 는 CLI 게이트 유지 — MCP 로 배포 트리거 방지).
26
+
27
+ ### 구현
28
+ 1. **`leerness_team_list`**(read-only): 정의된 팀 목록 JSON.
29
+ 2. **`leerness_team_preview`**(read-only, `id` required, `task?`): 팀 실행 계획 dry-run 미리보기 JSON(실행 없음).
30
+ 3. harness MCP dispatch 2 case(`team list/preview --json` 매핑). MCP 도구 83→85(`_mcpToolCount` 자동 반영).
31
+
32
+ ### 검증 (회귀 0)
33
+ - **selftest 123→124 PASS** (MCP 정의 read-only + required id + dispatch 와이어). **E2E 322→323 PASS** (정의 + 매핑 CLI 동작).
34
+ - 실측: mcp-tools 85개, team_list/preview read-only, 매핑 CLI 정상.
35
+
3
36
  ## 1.9.377 — 2026-06-06 — UR-0025 모듈화: workspace reference guide 빌더 분리
4
37
 
5
38
  **🧩 워크스페이스 레퍼런스 가이드 빌더(~57줄)를 lib/pure-utils 로 분리.**
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.377-green)]() [![tests](https://img.shields.io/badge/e2e-322%2F322-success)]() [![selftest](https://img.shields.io/badge/selftest-123-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.379-green)]() [![tests](https://img.shields.io/badge/e2e-324%2F324-success)]() [![selftest](https://img.shields.io/badge/selftest-125-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-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.377 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.379 하네스를 사용합니다. 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.377는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
528
+ Leerness v1.9.379는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
529
529
 
530
530
  ```jsonc
531
531
  // 카테고리별
@@ -538,7 +538,7 @@ Leerness v1.9.377는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
538
538
  // • Workflow: session_close / agents_list / task_export / env_check / usage_stats / reuse_map / whats_new
539
539
 
540
540
  // MCP server 실행: leerness mcp serve
541
- // tools/list 응답: 83 도구
541
+ // tools/list 응답: 85 도구
542
542
  ```
543
543
 
544
544
  ### Autonomous mode (자율 모드)
@@ -546,7 +546,7 @@ Leerness v1.9.377는 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.377)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.379)** · 매 라운드 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.377: 2026-06-06
587
+ Last synced by Leerness v1.9.379: 2026-06-06
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ 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, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _parseSkillsValue, _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
10
+ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _parseSkillsValue, _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
11
11
  PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord,
12
12
  _htmlToText, _extractTitle, _extractLinks,
13
13
  _countDatedBlocks, _extractDecisionBlocks, _classifyIntent,
@@ -28,7 +28,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
28
28
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
29
29
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_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, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
30
30
 
31
- const VERSION = '1.9.377';
31
+ const VERSION = '1.9.379';
32
32
 
33
33
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
34
34
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3014,6 +3014,8 @@ function _selfTestCases() {
3014
3014
  { name: 'UR-0084: _withLock 획득/재진입/해제 + maxWaitMs 하드닝(10s) 행위 (1.9.375)', run: () => { if (typeof _withLock !== 'function') return false; const src = read(__filename); const hardened = /maxWaitMs = opts\.maxWaitMs \|\| 10000/.test(src); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_lock_')); try { const target = path.join(tmp, 'f.md'); let reentrant = false; const lockSeen = _withLock(target, () => { const exists = fs.existsSync(target + '.lock'); reentrant = (_withLock(target, () => 42) === 42); return exists; }); const cleaned = !fs.existsSync(target + '.lock'); return hardened && lockSeen === true && reentrant === true && cleaned; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
3015
3015
  { name: 'UR-0073 Phase D: _teamDeployGate 이중 게이트 (dry-run 기본/env 게이트/실행) 행위 (1.9.376)', run: () => { const m = require('../lib/pure-utils'); if (typeof _teamDeployGate !== 'function' || m._teamDeployGate !== _teamDeployGate) return false; const team = { id: 'd', deployCommand: 'echo hi' }; const noCmd = _teamDeployGate({ id: 'x' }, { yes: true, envOn: true }).mode === 'no-command'; const dry = _teamDeployGate(team, { yes: false, envOn: true }).mode === 'dry-run'; const gated = _teamDeployGate(team, { yes: true, envOn: false }).mode === 'gated'; const exec = _teamDeployGate(team, { yes: true, envOn: true }).mode === 'execute'; return noCmd && dry && gated && exec; } },
3016
3016
  { name: 'UR-0025: _renderWorkspaceReferenceGuide 모듈 분리 + 빌더 동작 (1.9.377)', run: () => { const m = require('../lib/pure-utils'); if (typeof _renderWorkspaceReferenceGuide !== 'function' || m._renderWorkspaceReferenceGuide !== _renderWorkspaceReferenceGuide) return false; const g = _renderWorkspaceReferenceGuide('.leerness', '9.9.9', '2026-01-01T00:00:00.000Z'); const wrapperThin = read(__filename).includes('return _renderWorkspaceReferenceGuide(dirName, VERSION, new Date().toISOString())'); return g.includes('.leerness/progress-tracker.md') && g.includes('9.9.9') && g.includes('자주 묻는 위치') && g.includes('마이그레이션 안내') && wrapperThin; } },
3017
+ { name: 'UR-0073: team MCP 도구 2종(read-only) 정의 + dispatch 와이어 (1.9.378)', run: () => { const tools = require('../lib/mcp-tools'); const src = read(__filename); const tl = tools.find(t => t.name === 'leerness_team_list'); const tp = tools.find(t => t.name === 'leerness_team_preview'); const defsOk = tl && tl.requiredTier === 'read-only' && tp && tp.requiredTier === 'read-only' && tp.inputSchema.required && tp.inputSchema.required.includes('id'); const wired = src.includes("case " + "'leerness_team_list':") && src.includes("case " + "'leerness_team_preview':") && /cliArgs = \['team', 'list'/.test(src) && /cliArgs = \['team', 'preview'/.test(src); return !!defsOk && wired; } },
3018
+ { name: 'UR-0025 심화: pulse 렌더 코어 분리 — _memorySurface + _renderPulseLine 행위 (1.9.379)', run: () => { const m = require('../lib/pure-utils'); if (typeof _memorySurface !== 'function' || typeof _renderPulseLine !== 'function' || m._memorySurface !== _memorySurface || m._renderPulseLine !== _renderPulseLine) return false; const ms = _memorySurface({ tasks: 1, decisions: 2, rules: 3, milestones: 4, lessons: 5 }) === 'T1/D2/R3/P4/L5' && _memorySurface({}) === 'T0/D0/R0/P0/L0'; const base = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'T0/D1/R0/P2/L0' }); const ln = base.includes('v1.0.0') && base.includes('R7') && base.includes('MCP 85') && base.includes('T0/D1/R0/P2/L0') && !base.includes('🎯') && !base.includes('abnormal'); const full = _renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 85, memorySurface: 'x', nextMilestone: 400, etaDays: 6, abnormalShutdown: 'high' }); const ln2 = full.includes('🎯 R400 (6d)') && full.includes('abnormal:high'); const wired = read(__filename).includes('const line = _renderPulseLine(data)') && read(__filename).includes('data.memorySurface = _memorySurface('); return ms && ln && ln2 && wired; } },
3017
3019
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3018
3020
  ];
3019
3021
  }
@@ -3500,7 +3502,7 @@ function pulseCmd(root) {
3500
3502
  const planText = exists(planPath(root)) ? read(planPath(root)) : '';
3501
3503
  const milestonesCnt = (planText.match(/^### M-\d{4}\./gm) || []).length;
3502
3504
  const lessonsCount = _loadLessons(root).length;
3503
- data.memorySurface = `T${tasksInProgress}/D${decisionCount}/R${rulesActive}/P${milestonesCnt}/L${lessonsCount}`;
3505
+ data.memorySurface = _memorySurface({ tasks: tasksInProgress, decisions: decisionCount, rules: rulesActive, milestones: milestonesCnt, lessons: lessonsCount });
3504
3506
  } catch {}
3505
3507
  // 마일스톤 + ETA
3506
3508
  try {
@@ -3521,14 +3523,7 @@ function pulseCmd(root) {
3521
3523
  }
3522
3524
  if (has('--json')) { log(JSON.stringify(data, null, 2)); return; }
3523
3525
  // 한 줄 출력
3524
- let line = `📍 v${data.version} · 🔄 R${data.roundCount} · 🔌 MCP ${data.mcpTools} · 🧠 ${data.memorySurface}`;
3525
- if (data.nextMilestone) {
3526
- const eta = data.etaDays != null ? ` (${data.etaDays}d)` : '';
3527
- line += ` · 🎯 R${data.nextMilestone}${eta}`;
3528
- }
3529
- if (data.abnormalShutdown !== 'none') {
3530
- line += ` · 🔌 abnormal:${data.abnormalShutdown}`;
3531
- }
3526
+ const line = _renderPulseLine(data);
3532
3527
  log(cy(`# leerness pulse (1.9.231/1.9.232) — 한 줄 종합 요약`));
3533
3528
  log('');
3534
3529
  log(` ${line}`);
@@ -16676,6 +16671,15 @@ function _mcpToCliArgs(name, args, targetPath) {
16676
16671
  // 1.9.233: 카테고리화된 전체 CLI 명령 목록
16677
16672
  cliArgs = ['commands', '--json'];
16678
16673
  break;
16674
+ case 'leerness_team_list':
16675
+ // 1.9.378 (UR-0073): 정의된 에이전트 팀 목록 (read-only)
16676
+ cliArgs = ['team', 'list', '--path', targetPath, '--json'];
16677
+ break;
16678
+ case 'leerness_team_preview':
16679
+ // 1.9.378 (UR-0073 Phase B): 팀 실행 계획 dry-run 미리보기 (실행 없음)
16680
+ cliArgs = ['team', 'preview', String(args.id || ''), '--path', targetPath, '--json'];
16681
+ if (args.task) cliArgs.push('--task', String(args.task));
16682
+ break;
16679
16683
  case 'leerness_py_check':
16680
16684
  // 1.9.239 (UR-0013): Python 파일 분석
16681
16685
  cliArgs = ['py-check', '--path', targetPath, '--json'];
package/lib/mcp-tools.js CHANGED
@@ -69,6 +69,8 @@ module.exports = [
69
69
  { name: 'leerness_milestones', requiredTier: 'read-only', description: '1.9.229 (1.9.226 확장) — 도달 마일스톤 + 다음 ETA. git tag 순차 분석으로 25/50/75/100/125/150/175/200/250/300/400/500 마일스톤 도달 일자 + 다음 마일스톤 ETA (현재 속도 기준) 계산. 응답: { totalRounds, reached: [{milestone, version, reachedAt, daysFromBaseline}], next: {milestone, roundsRemaining, etaDays, etaDate}, baselineAt, avgRoundsPerDay }. 외부 AI가 "지금까지 어떤 마일스톤을 언제 달성했고 다음 마일스톤 예상 도달일은?"을 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
70
70
  { name: 'leerness_pulse', requiredTier: 'read-only', description: '1.9.231 — 한 줄 종합 요약 (10 핵심 지표). 응답: { version, roundCount, mcpTools, memorySurface, security, health, driftScore, nextMilestone, etaDays, abnormalShutdown }. 외부 AI가 "leerness 상태 한 눈에 보기"를 가벼운 단일 호출로 회수. handoff 보다 5배 빠름 (drift/health 계산 skip). 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
71
71
  { name: 'leerness_commands', requiredTier: 'read-only', description: '1.9.233 — 카테고리화된 전체 CLI 명령 목록 (9 카테고리). 응답: { version, totalCommands, categories: { status, task, memory, audit, workflow, release, skill, bridge, config } }. 외부 AI가 "leerness 가 제공하는 명령이 뭐가 있나"를 직접 회수. 매뉴얼/도움말 동적 생성. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
72
+ { name: 'leerness_team_list', requiredTier: 'read-only', description: '1.9.378 (UR-0073) — 정의된 에이전트 팀 목록 (페르소나 기반, opt-in · 정의 전용). 응답: { version, count, teams: [{ id, name, purpose, personas[], members[], schedule, deployCommand, status }] }. 외부 AI 가 "이 워크스페이스에 정의된 팀이 뭐가 있나"를 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
73
+ { name: 'leerness_team_preview', requiredTier: 'read-only', description: '1.9.378 (UR-0073 Phase B) — 팀 실행 계획 dry-run 미리보기 (실제 실행/배포 없음). 응답: { dryRun: true, teamId, task, schedule, memberCount, steps: [{ member, personas[], dispatchPrompt, suggestedCommand }] }. 외부 AI 가 "이 팀이 무엇을 할지"를 안전하게 회수. 인자: { id (required), task?, path? }', inputSchema: { type: 'object', properties: { id: { type: 'string' }, task: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
72
74
  { name: 'leerness_release_cleanup', requiredTier: 'git-write', description: '1.9.236 (1.9.235 자동 회수) — local release/* branches 정리. main 에 merge된 것만 후보 (unmerged 보호, 현재 branch 보호). 응답: { apply, keep, total, merged, unmerged, deleteCount, toDelete[], recent[], unmergedSample[] }. 외부 AI가 "운영 누적 release branches 정리 가능?"을 회수. 기본 dry-run, apply: true 시에만 실 삭제. 인자: { path?, apply? (default false), keep? (default 5) }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, apply: { type: 'boolean' }, keep: { type: 'number' } } } },
73
75
  { name: 'leerness_py_check', requiredTier: 'read-only', description: '1.9.239 (사용자 명시 UR-0013) — Python 파일 분석. 의존성 0 regex fallback. .md 외 .py 도 leerness 인지. 응답: { totalFiles, totalLOC, totalImports, totalFuncs, totalClasses, totalTodos, biggest: [{ file, loc, funcs, classes }] }. 외부 AI가 "이 프로젝트 Python 표면이 얼마나 되나"를 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
74
76
  { name: 'leerness_agent_mode', requiredTier: 'safe-write', description: '1.9.239 (사용자 명시 UR-0013) — 자율 모드 전용 통합 명령. start: handoff + drift --auto-fix + session-resume --auto-fix (진입). tick: pulse 한 줄 (매 라운드). stop: session close --auto-apply-delivered --auto-cleanup-branches (마감). 외부 AI 가 자율 라운드 진입/매 라운드/마감을 단일 호출로 수행. 인자: { path?, sub (required: "start"|"tick"|"stop"|"help") }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['start', 'tick', 'stop', 'help'] } }, required: ['sub'] } },
package/lib/pure-utils.js CHANGED
@@ -855,11 +855,30 @@ function _renderWorkspaceReferenceGuide(dirName, version, generatedAt) {
855
855
  return lines.join('\n');
856
856
  }
857
857
 
858
+ // 1.9.379 (UR-0025 심화): Memory Surface 포맷 (순수) — T/D/R/P/L 카운트 → 문자열. pulse/memory-status 단일출처.
859
+ function _memorySurface(counts) {
860
+ const c = counts || {};
861
+ return `T${c.tasks || 0}/D${c.decisions || 0}/R${c.rules || 0}/P${c.milestones || 0}/L${c.lessons || 0}`;
862
+ }
863
+ // 1.9.379 (UR-0025 심화): pulse 한 줄 요약 조합 (순수) — gather(I/O)된 data → 한 줄 문자열. pulse 핸들러 렌더 코어.
864
+ function _renderPulseLine(data) {
865
+ const d = data || {};
866
+ let line = `📍 v${d.version} · 🔄 R${d.roundCount} · 🔌 MCP ${d.mcpTools} · 🧠 ${d.memorySurface}`;
867
+ if (d.nextMilestone) {
868
+ const eta = d.etaDays != null ? ` (${d.etaDays}d)` : '';
869
+ line += ` · 🎯 R${d.nextMilestone}${eta}`;
870
+ }
871
+ if (d.abnormalShutdown && d.abnormalShutdown !== 'none') {
872
+ line += ` · 🔌 abnormal:${d.abnormalShutdown}`;
873
+ }
874
+ return line;
875
+ }
876
+
858
877
  module.exports = {
859
878
  _isSecretKey, compareVer, parseHarnessVersion,
860
879
  _isPlaceholderSecret, _looksSecretLike,
861
880
  _mergeLines, _mergeEnvLines, _mergeReadmeSection, _managedMerge, _parseSkillsValue,
862
- _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide,
881
+ _parseArchiveBlocks, _parseSkillCatalog, _renderTeamsMd, _composeTeamPlan, _teamHandoffReminders, _cadenceAssessment, _teamDeployGate, _renderWorkspaceReferenceGuide, _memorySurface, _renderPulseLine,
863
882
  _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
864
883
  // 1.9.283 (UR-0025 2단계)
865
884
  PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.377",
3
+ "version": "1.9.379",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -5209,5 +5209,48 @@ total++;
5209
5209
  if (!ok) failed++;
5210
5210
  }
5211
5211
 
5212
+ // 1.9.378 회귀 (UR-0073): team MCP 도구 2종(read-only) 정의 + 매핑 CLI(team list/preview) 동작
5213
+ total++;
5214
+ {
5215
+ let ok = false;
5216
+ try {
5217
+ const tools = require(path.resolve(__dirname, '..', 'lib', 'mcp-tools'));
5218
+ const tl = tools.find(t => t.name === 'leerness_team_list');
5219
+ const tp = tools.find(t => t.name === 'leerness_team_preview');
5220
+ const defsOk = !!tl && tl.requiredTier === 'read-only' && !!tp && tp.requiredTier === 'read-only' && tp.inputSchema.required.includes('id') && tools.length >= 85;
5221
+ // 매핑 대상 CLI 동작 (MCP dispatch 가 호출하는 것)
5222
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tmcp-'));
5223
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
5224
+ cp.spawnSync(process.execPath, [CLI, 'team', 'add', 'rev', '--members', 'claude', '--path', d], { encoding: 'utf8', timeout: 15000 });
5225
+ const lj = cp.spawnSync(process.execPath, [CLI, 'team', 'list', '--path', d, '--json'], { encoding: 'utf8', timeout: 15000 });
5226
+ let cliOk = false;
5227
+ try { cliOk = JSON.parse(lj.stdout).count === 1; } catch {}
5228
+ fs.rmSync(d, { recursive: true, force: true });
5229
+ ok = defsOk && cliOk;
5230
+ } catch {}
5231
+ console.log(ok ? '✓ B(1.9.378) UR-0073: team MCP 도구(read-only list/preview) 정의 + 매핑 CLI 동작' : '✗ team MCP 도구 실패');
5232
+ if (!ok) failed++;
5233
+ }
5234
+
5235
+ // 1.9.379 회귀 (UR-0025 심화): pulse 렌더 코어(_memorySurface/_renderPulseLine) 분리 — 빌더 동작 + pulse CLI 출력 유지
5236
+ total++;
5237
+ {
5238
+ let ok = false;
5239
+ try {
5240
+ const pu = require(path.resolve(__dirname, '..', 'lib', 'pure-utils'));
5241
+ const msOk = pu._memorySurface({ tasks: 1, decisions: 2, rules: 3, milestones: 4, lessons: 5 }) === 'T1/D2/R3/P4/L5';
5242
+ const lnOk = pu._renderPulseLine({ version: '1.0.0', roundCount: 7, mcpTools: 9, memorySurface: 'T0/D0/R0/P0/L0', nextMilestone: 400, etaDays: 6 }).includes('🎯 R400 (6d)');
5243
+ // pulse CLI 가 한 줄 출력 유지
5244
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pulse-'));
5245
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
5246
+ const r = cp.spawnSync(process.execPath, [CLI, 'pulse', d], { encoding: 'utf8', timeout: 15000 });
5247
+ const cliOk = /📍 v[\d.]+ · 🔄 R\d+ · 🔌 MCP \d+ · 🧠 T\d+\/D\d+\/R\d+\/P\d+\/L\d+/.test(r.stdout || '');
5248
+ fs.rmSync(d, { recursive: true, force: true });
5249
+ ok = msOk && lnOk && cliOk;
5250
+ } catch {}
5251
+ console.log(ok ? '✓ B(1.9.379) UR-0025 심화: pulse 렌더 코어 분리 (_memorySurface/_renderPulseLine + CLI 출력 유지)' : '✗ pulse 렌더 코어 실패');
5252
+ if (!ok) failed++;
5253
+ }
5254
+
5212
5255
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
5213
5256
  if (failed > 0) process.exit(1);