leerness 1.9.277 → 1.9.278

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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.278 — 2026-06-03 — UR-0032: .leerness/ JSON 상태 스키마 (범용 하네스 substrate 1단계)
4
+
5
+ **🧱 GPT-5.5 가 최우선으로 꼽은 "상태 스키마" — 에이전트 간 구조화 인수인계 substrate. 마크다운(.harness)과 병행, 비파괴.**
6
+
7
+ ### 배경
8
+ GPT-5.5: "범용 하네스가 되려면 파일 생성 도구가 아니라 프로토콜/상태 관리 도구가 돼야 한다. `.leerness/` JSON 스키마(runs/state/decisions)에 task_id/agent/model/files_changed/verification/handoff 가 있어야 Claude Code 작업을 Goose 가 이어받는다." → 전략 백로그 중 substrate 라서 **최우선** 구현.
9
+
10
+ ### 구현
11
+ 1. **`.leerness/` 상태 디렉토리** (신규, 비파괴 — 기존 `.harness` markdown 과 병행).
12
+ 2. **`_newRunRecord()` 순수 스키마 빌더** — GPT-5.5 권고 14필드: `schemaVersion/run_id/task_id/agent_name/model_name/started_at/ended_at/goal/files_read/files_changed/commands_run/tests_run/errors/decisions/verification_result/handoff_summary/status`.
13
+ 3. **`leerness state <show|start|record|verify|handoff>`** CLI (전 서브 `--json`):
14
+ - `start "<goal>" [--agent --model --task]` → `runs/run-NNNN.json` 생성 + `state.json` currentRun 갱신
15
+ - `record --files-changed/--commands/--tests/--decision/...` → 배열 누적(dedup)
16
+ - `verify --result pass|fail` → verification_result + status
17
+ - `handoff "<summary>"` → ended_at/status=handed-off + **`.leerness/handoff/latest.{md,json}`** (다음 에이전트 인수) + currentRun 해제
18
+ - `show [--json]` → 현재 상태 + 진행 run
19
+ 4. **selftest 27→28 + e2e 224→225** — 스키마 14필드 + 전체 라이프사이클(start→record→verify→handoff→show) JSON 인수인계 검증.
20
+
21
+ ### 다음 단계 (전략 백로그 연계)
22
+ - UR-0031: 이 스키마를 MCP 시맨틱 verb(get_context/record_decision/verify_done/make_handoff)로 노출.
23
+ - UR-0033: `leerness adapter` 가 도구별로 이 substrate 를 가리키게.
24
+
25
+ ### 검증
26
+ - **selftest 28/28 PASS** · **E2E 225/225 PASS** (회귀 0) · 라이프사이클 실측(.leerness/state·runs·handoff 생성, 다음 에이전트 JSON 인수).
27
+
3
28
  ## 1.9.277 — 2026-06-03 — 신규 provider 4종(opencode/qwen/aider/goose) + GPT-5.5 범용 하네스 방향 백로그 등록
4
29
 
5
30
  **🤝 CLI 에이전트 4종을 정식 provider 로 추가 (설치 선택지 + 모델 catalog + dispatch/roles 라우팅). 빌트인 6 → 10종.**
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.277-green)]() [![tests](https://img.shields.io/badge/e2e-224%2F224-success)]() [![selftest](https://img.shields.io/badge/selftest-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-75-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.278-green)]() [![tests](https://img.shields.io/badge/e2e-225%2F225-success)]() [![selftest](https://img.shields.io/badge/selftest-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-75-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
  ╔══════════════════════════════════════════════════════════════╗
@@ -440,7 +440,7 @@ MIT — © leerness contributors
440
440
  <!-- leerness:project-readme:start -->
441
441
  ## Leerness Project Harness
442
442
 
443
- 이 프로젝트는 Leerness v1.9.277 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
443
+ 이 프로젝트는 Leerness v1.9.278 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
444
444
 
445
445
  ### Core Commands
446
446
 
@@ -482,7 +482,7 @@ leerness memory restore decision <date|title>
482
482
 
483
483
  ### MCP server (외부 AI 통합)
484
484
 
485
- Leerness v1.9.277는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
485
+ Leerness v1.9.278는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
486
486
 
487
487
  ```jsonc
488
488
  // 카테고리별
@@ -503,7 +503,7 @@ Leerness v1.9.277는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
503
503
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
504
504
  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) 다음 라운드 예약.
505
505
 
506
- 현재 누적: **70 라운드 (1.9.40 → 1.9.277)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
506
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.278)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
507
507
 
508
508
  ### 성능 가이드 (1.9.140 측정)
509
509
 
@@ -541,6 +541,6 @@ leerness release pack --close --auto-main-push
541
541
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
542
542
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
543
543
 
544
- Last synced by Leerness v1.9.277: 2026-06-03
544
+ Last synced by Leerness v1.9.278: 2026-06-03
545
545
  <!-- leerness:project-readme:end -->
546
546
 
package/bin/harness.js CHANGED
@@ -9,7 +9,7 @@ const readline = require('readline');
9
9
  // 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
10
10
  const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp } = require('../lib/pure-utils');
11
11
 
12
- const VERSION = '1.9.277';
12
+ const VERSION = '1.9.278';
13
13
 
14
14
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
15
15
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -2960,6 +2960,7 @@ function _selfTestCases() {
2960
2960
  { name: '_pickModel: top/code/fast 등급 선택 (1.9.270)', run: () => { return _pickModel('codex', 'code') === 'gpt-5-codex' && _pickModel('claude', 'top') === 'claude-opus-4-7' && /haiku/.test(_pickModel('claude', 'fast')); } },
2961
2961
  { name: 'CAPABILITY_SURFACE: 6 영역 + risk/optOut + 주의명령 (1.9.272)', run: () => { const ks = Object.keys(CAPABILITY_SURFACE); return ks.length === 6 && ks.includes('automationBridges') && Object.values(CAPABILITY_SURFACE).every(v => ['low','medium','high'].includes(v.risk) && v.optOut && v.desc) && POWERFUL_COMMANDS.length >= 5; } },
2962
2962
  { name: '_resolveNpmTag: latest 기본 + next 허용 + 잘못된 형식 폴백 (1.9.275)', run: () => _resolveNpmTag(null, {}) === 'latest' && _resolveNpmTag('next', {}) === 'next' && _resolveNpmTag(null, { LEERNESS_NPM_TAG: 'next' }) === 'next' && _resolveNpmTag('bad tag!', {}) === 'latest' && _resolveNpmTag('Beta', {}) === 'beta' },
2963
+ { name: '_newRunRecord: GPT-5.5 권고 스키마 14필드 + 기본값 (1.9.278)', run: () => { const r = _newRunRecord({ run_id: 'run-0001', goal: 'g', started_at: '2026-06-03T00:00:00Z' }); return r.schemaVersion === 1 && r.run_id === 'run-0001' && r.started_at === '2026-06-03T00:00:00Z' && Array.isArray(r.files_changed) && Array.isArray(r.commands_run) && Array.isArray(r.tests_run) && Array.isArray(r.decisions) && r.verification_result === null && r.status === 'in-progress' && 'handoff_summary' in r && 'model_name' in r && 'task_id' in r; } },
2963
2964
  { 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]); } },
2964
2965
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
2965
2966
  ];
@@ -3618,6 +3619,7 @@ function commandsCmd(root) {
3618
3619
  { cmd: 'agents list|check|quota|dispatch|multi', desc: '외부 AI CLI 오케스트레이션 (dispatch --role 모델 라우팅 1.9.270)' },
3619
3620
  { cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배) — 1.9.270' },
3620
3621
  { cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272' },
3622
+ { cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278' },
3621
3623
  { cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
3622
3624
  { cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
3623
3625
  { cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
@@ -18020,6 +18022,144 @@ function capabilitiesCmd(root, opts = {}) {
18020
18022
  log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json`));
18021
18023
  }
18022
18024
 
18025
+ // ===== 1.9.278 (UR-0032, GPT-5.5 범용 하네스 방향): .leerness/ JSON 상태 스키마 =====
18026
+ // 목적: 마크다운(.harness/)과 병행하는 "구조화 상태 substrate". 에이전트 간 인수인계 표준 —
18027
+ // Claude Code 가 한 작업을 Goose/Codex 가 JSON 으로 정확히 이어받음. 비파괴 (신규 .leerness/ 디렉토리).
18028
+ // 향후 UR-0031(MCP verb)/UR-0033(adapter) 가 이 스키마를 공통 호출 표면으로 사용.
18029
+ function _leernessStateDir(root) { return path.join(absRoot(root), '.leerness'); }
18030
+ // 순수 함수: run 레코드 빌더 (모든 필드 기본값 — GPT-5.5 권고 스키마). startedAt 주입 가능(selftest).
18031
+ function _newRunRecord(opts = {}) {
18032
+ return {
18033
+ schemaVersion: 1,
18034
+ run_id: opts.run_id || null,
18035
+ task_id: opts.task_id || null,
18036
+ agent_name: opts.agent_name || null,
18037
+ model_name: opts.model_name || null,
18038
+ started_at: opts.started_at || new Date().toISOString(),
18039
+ ended_at: opts.ended_at || null,
18040
+ goal: opts.goal || '',
18041
+ files_read: Array.isArray(opts.files_read) ? opts.files_read : [],
18042
+ files_changed: Array.isArray(opts.files_changed) ? opts.files_changed : [],
18043
+ commands_run: Array.isArray(opts.commands_run) ? opts.commands_run : [],
18044
+ tests_run: Array.isArray(opts.tests_run) ? opts.tests_run : [],
18045
+ errors: Array.isArray(opts.errors) ? opts.errors : [],
18046
+ decisions: Array.isArray(opts.decisions) ? opts.decisions : [],
18047
+ verification_result: opts.verification_result || null, // 'pass' | 'fail' | null
18048
+ handoff_summary: opts.handoff_summary || null,
18049
+ status: opts.status || 'in-progress' // in-progress | verified | handed-off
18050
+ };
18051
+ }
18052
+ function _loadLeernessState(root) {
18053
+ const f = path.join(_leernessStateDir(root), 'state.json');
18054
+ if (!exists(f)) return { schemaVersion: 1, project: detectProjectName(root), currentRunId: null, runCounter: 0, updatedAt: null };
18055
+ try { return JSON.parse(read(f)); } catch { return { schemaVersion: 1, project: detectProjectName(root), currentRunId: null, runCounter: 0, updatedAt: null }; }
18056
+ }
18057
+ function _saveLeernessState(root, state) {
18058
+ const dir = _leernessStateDir(root); mkdirp(dir);
18059
+ state.updatedAt = new Date().toISOString();
18060
+ writeUtf8(path.join(dir, 'state.json'), JSON.stringify(state, null, 2) + '\n');
18061
+ }
18062
+ function _runFile(root, id) { return path.join(_leernessStateDir(root), 'runs', `${id}.json`); }
18063
+ function _loadRun(root, id) { const f = _runFile(root, id); if (!exists(f)) return null; try { return JSON.parse(read(f)); } catch { return null; } }
18064
+ function _saveRun(root, rec) { const f = _runFile(root, rec.run_id); mkdirp(path.dirname(f)); writeUtf8(f, JSON.stringify(rec, null, 2) + '\n'); }
18065
+ function _splitList(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
18066
+
18067
+ // leerness state <show|start|record|verify|handoff>
18068
+ function stateCmd(root, sub, ...args) {
18069
+ root = absRoot(root || process.cwd());
18070
+ const json = has('--json');
18071
+ sub = sub || 'show';
18072
+
18073
+ if (sub === 'start') {
18074
+ const goal = (args.find(a => a && !a.startsWith('-')) || arg('--goal', '')).trim();
18075
+ const state = _loadLeernessState(root);
18076
+ state.runCounter = (state.runCounter || 0) + 1;
18077
+ const run_id = `run-${String(state.runCounter).padStart(4, '0')}`;
18078
+ const rec = _newRunRecord({
18079
+ run_id, goal,
18080
+ task_id: arg('--task', null),
18081
+ agent_name: arg('--agent', null),
18082
+ model_name: arg('--model', null)
18083
+ });
18084
+ _saveRun(root, rec);
18085
+ state.currentRunId = run_id;
18086
+ _saveLeernessState(root, state);
18087
+ if (json) { log(JSON.stringify({ started: run_id, run: rec }, null, 2)); return; }
18088
+ ok(`run 시작: ${run_id}${rec.agent_name ? ' · agent=' + rec.agent_name : ''}${rec.model_name ? ' · model=' + rec.model_name : ''}`);
18089
+ if (goal) log(` goal: ${goal}`);
18090
+ log(` → 기록: leerness state record --files-changed "a,b" --commands "npm test" --decision "..."`);
18091
+ return;
18092
+ }
18093
+
18094
+ const state = _loadLeernessState(root);
18095
+ const curId = state.currentRunId;
18096
+ if ((sub === 'record' || sub === 'verify' || sub === 'handoff') && !curId) {
18097
+ return fail('진행 중 run 없음 — 먼저 leerness state start "<goal>"');
18098
+ }
18099
+
18100
+ if (sub === 'record') {
18101
+ const rec = _loadRun(root, curId); if (!rec) return fail(`run 없음: ${curId}`);
18102
+ for (const [flag, key] of [['--files-read', 'files_read'], ['--files-changed', 'files_changed'], ['--commands', 'commands_run'], ['--tests', 'tests_run'], ['--errors', 'errors'], ['--decision', 'decisions']]) {
18103
+ const v = arg(flag, null); if (v) rec[key] = Array.from(new Set([...(rec[key] || []), ..._splitList(v)]));
18104
+ }
18105
+ _saveRun(root, rec);
18106
+ if (json) { log(JSON.stringify({ recorded: curId, run: rec }, null, 2)); return; }
18107
+ ok(`기록: ${curId} (files_changed ${rec.files_changed.length} · commands ${rec.commands_run.length} · tests ${rec.tests_run.length} · decisions ${rec.decisions.length})`);
18108
+ return;
18109
+ }
18110
+
18111
+ if (sub === 'verify') {
18112
+ const rec = _loadRun(root, curId); if (!rec) return fail(`run 없음: ${curId}`);
18113
+ const result = (arg('--result', '') || (args.find(a => a && !a.startsWith('-')) || '')).toLowerCase();
18114
+ if (result !== 'pass' && result !== 'fail') return fail('--result pass|fail 필요');
18115
+ rec.verification_result = result;
18116
+ rec.status = result === 'pass' ? 'verified' : 'in-progress';
18117
+ const note = arg('--note', null); if (note) rec.decisions = [...(rec.decisions || []), `verify(${result}): ${note}`];
18118
+ _saveRun(root, rec);
18119
+ if (json) { log(JSON.stringify({ verified: curId, result, run: rec }, null, 2)); return; }
18120
+ (result === 'pass' ? ok : warn)(`검증 결과: ${curId} → ${result}`);
18121
+ return;
18122
+ }
18123
+
18124
+ if (sub === 'handoff') {
18125
+ const rec = _loadRun(root, curId); if (!rec) return fail(`run 없음: ${curId}`);
18126
+ const summary = (args.find(a => a && !a.startsWith('-')) || arg('--summary', '')).trim();
18127
+ rec.handoff_summary = summary || rec.handoff_summary || '(요약 없음)';
18128
+ rec.ended_at = new Date().toISOString();
18129
+ rec.status = 'handed-off';
18130
+ _saveRun(root, rec);
18131
+ // 다음 에이전트가 읽을 latest handoff (json + md)
18132
+ const hdir = path.join(_leernessStateDir(root), 'handoff'); mkdirp(hdir);
18133
+ writeUtf8(path.join(hdir, 'latest.json'), JSON.stringify(rec, null, 2) + '\n');
18134
+ writeUtf8(path.join(hdir, 'latest.md'),
18135
+ `# Handoff — ${rec.run_id}\n\n- task: ${rec.task_id || '-'}\n- agent: ${rec.agent_name || '-'} · model: ${rec.model_name || '-'}\n- goal: ${rec.goal || '-'}\n- files_changed: ${rec.files_changed.join(', ') || '-'}\n- tests_run: ${rec.tests_run.join(', ') || '-'}\n- verification: ${rec.verification_result || '-'}\n\n## Summary\n${rec.handoff_summary}\n`);
18136
+ state.currentRunId = null; // 다음 에이전트가 새 run 시작
18137
+ _saveLeernessState(root, state);
18138
+ if (json) { log(JSON.stringify({ handoff: rec.run_id, run: rec }, null, 2)); return; }
18139
+ ok(`handoff 작성: ${rec.run_id} → .leerness/handoff/latest.{md,json}`);
18140
+ return;
18141
+ }
18142
+
18143
+ // default: show
18144
+ const rec = curId ? _loadRun(root, curId) : null;
18145
+ if (json) { log(JSON.stringify({ state, currentRun: rec }, null, 2)); return; }
18146
+ log(`# leerness state (1.9.278, UR-0032) — .leerness/ 구조화 상태`);
18147
+ log(`project: ${state.project} · runs 누적: ${state.runCounter || 0} · 현재 run: ${curId || '(없음)'}`);
18148
+ if (rec) {
18149
+ log('');
18150
+ log(`## ${rec.run_id} [${rec.status}]`);
18151
+ log(` goal: ${rec.goal || '-'}`);
18152
+ log(` agent/model: ${rec.agent_name || '-'} / ${rec.model_name || '-'}`);
18153
+ log(` files_changed: ${rec.files_changed.join(', ') || '-'}`);
18154
+ log(` commands_run: ${rec.commands_run.join(', ') || '-'}`);
18155
+ log(` tests_run: ${rec.tests_run.join(', ') || '-'} · verification: ${rec.verification_result || '-'}`);
18156
+ if (rec.decisions.length) log(` decisions: ${rec.decisions.length}`);
18157
+ } else {
18158
+ log(` → 시작: leerness state start "<goal>" [--agent claude --model claude-opus-4-7 --task T-0001]`);
18159
+ }
18160
+ return;
18161
+ }
18162
+
18023
18163
  // 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
18024
18164
  async function _agentRepl(root, opts) {
18025
18165
  // 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
@@ -20946,6 +21086,8 @@ async function main() {
20946
21086
  if (cmd === 'roles' || cmd === 'role') return rolesCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
20947
21087
  // 1.9.272: leerness capabilities — 권한/보안 표면 공개 (GPT-5.5 외부 리뷰 반영)
20948
21088
  if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
21089
+ // 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
21090
+ if (cmd === 'state') return stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
20949
21091
  // 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
20950
21092
  if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
20951
21093
  // 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
@@ -21126,5 +21268,7 @@ module.exports = {
21126
21268
  // 1.9.275: 릴리스 채널 (npm dist-tag, GPT-5.5 리뷰) — 단위 테스트
21127
21269
  _resolveNpmTag, releaseChannelCmd,
21128
21270
  // 1.9.276: init --minimal 파일 필터 (GPT-5.5 2차 리뷰) — 단위 테스트
21129
- MINIMAL_SKIP_KEYS, coreFiles
21271
+ MINIMAL_SKIP_KEYS, coreFiles,
21272
+ // 1.9.278: .leerness/ 상태 스키마 (UR-0032, GPT-5.5 범용 하네스) — 단위 테스트
21273
+ _newRunRecord, _leernessStateDir, _loadLeernessState, _saveLeernessState, _loadRun, _saveRun, stateCmd
21130
21274
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.277",
3
+ "version": "1.9.278",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -2843,5 +2843,37 @@ total++;
2843
2843
  if (!ok) { failed++; console.log((rVerify.stdout || '').slice(0, 300)); }
2844
2844
  }
2845
2845
 
2846
+ // 1.9.278 회귀 (UR-0032): .leerness/ 상태 스키마 라이프사이클 (start→record→verify→handoff→show)
2847
+ total++;
2848
+ {
2849
+ const sDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-state-'));
2850
+ const run = (a) => cp.spawnSync(process.execPath, [CLI, 'state', ...a, '--path', sDir], { encoding: 'utf8', timeout: 15000 });
2851
+ run(['start', 'API 구현', '--agent', 'claude', '--model', 'claude-opus-4-7', '--task', 'T-0001']);
2852
+ run(['record', '--files-changed', 'src/api.js,src/db.js', '--commands', 'npm test', '--tests', '12 passed', '--decision', 'retry 3']);
2853
+ run(['verify', '--result', 'pass']);
2854
+ run(['handoff', 'API 완료, db 마이그레이션은 다음 에이전트']);
2855
+ // 다음 에이전트가 JSON 으로 인수
2856
+ const rShow = run(['show', '--json']);
2857
+ let recOk = false, handoffOk = false;
2858
+ try {
2859
+ const j = JSON.parse(rShow.stdout);
2860
+ // handoff 후 currentRun 은 null, 누적 runs=1
2861
+ recOk = j.state && j.state.runCounter === 1 && j.state.currentRunId === null;
2862
+ } catch {}
2863
+ const runFile = path.join(sDir, '.leerness', 'runs', 'run-0001.json');
2864
+ const hJson = path.join(sDir, '.leerness', 'handoff', 'latest.json');
2865
+ try {
2866
+ const rec = JSON.parse(fs.readFileSync(runFile, 'utf8'));
2867
+ const hf = JSON.parse(fs.readFileSync(hJson, 'utf8'));
2868
+ handoffOk = rec.status === 'handed-off' && rec.verification_result === 'pass'
2869
+ && rec.files_changed.length === 2 && rec.model_name === 'claude-opus-4-7'
2870
+ && rec.task_id === 'T-0001' && hf.handoff_summary && hf.run_id === 'run-0001'
2871
+ && fs.existsSync(path.join(sDir, '.leerness', 'handoff', 'latest.md'));
2872
+ } catch {}
2873
+ const ok = recOk && handoffOk;
2874
+ console.log(ok ? '✓ B(1.9.278) state substrate: start→record→verify→handoff→show (.leerness JSON 인수인계)' : `✗ state 실패 (show=${recOk} handoff=${handoffOk})`);
2875
+ if (!ok) { failed++; console.log((rShow.stdout || '').slice(0, 300)); }
2876
+ }
2877
+
2846
2878
  console.log(`\nE2E result: ${total - failed}/${total} passed`);
2847
2879
  if (failed > 0) process.exit(1);