leerness 1.9.276 → 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,55 @@
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
+
28
+ ## 1.9.277 — 2026-06-03 — 신규 provider 4종(opencode/qwen/aider/goose) + GPT-5.5 범용 하네스 방향 백로그 등록
29
+
30
+ **🤝 CLI 에이전트 4종을 정식 provider 로 추가 (설치 선택지 + 모델 catalog + dispatch/roles 라우팅). 빌트인 6 → 10종.**
31
+
32
+ ### 신규 provider (사용자 명시)
33
+ 1. **EXTERNAL_AGENTS +4** — `opencode`(오픈소스 터미널 에이전트) · `qwen`(Qwen Code) · `aider`(git-aware 페어프로그래밍) · `goose`(Block 범용 에이전트, MCP 확장). 각 `bin`/`envFlag`/`installCmd` 포함.
34
+ 2. **자동 전파** — provider list(10)/cycle/setup-agents/`agents dispatch`/`roles`/`slash-commands` 가 4종 자동 흡수.
35
+ 3. **install 흐름 10선택지** — 대화형 `_selectMany` + 비대화형 `1~10` 맵 확장.
36
+ 4. **모델 catalog** — opencode(default/anthropic/openai) · qwen(qwen3-coder-plus/max/plus/turbo) · aider(sonnet/gpt-5/deepseek/o1-mini) · goose(default/claude/gpt).
37
+ 5. **`_dispatchCommand`** — `opencode run` · `qwen -p` · `aider --message [--yes]` · `goose run -t` (모델 플래그 주입, best-effort).
38
+ 6. **슬래시 레지스트리** — 4종 세션 슬래시/명령 큐레이션(opencode/qwen/aider/goose).
39
+ 7. **config/nonsecret 키** — `LEERNESS_ENABLE_OPENCODE/QWEN/AIDER/GOOSE`.
40
+
41
+ ### GPT-5.5 "범용 하네스 성장 방향" 판단 → 작업 스케줄 등록
42
+ GPT-5.5: leerness 는 실행 에이전트(Goose 등)와 경쟁하지 말고, 그들이 공통 의존하는 **운영 레이어(Memory+Policy+Handoff+Verification+Audit)**가 되면 범용 하네스로 성장 가능. **판단: 방향 타당 — leerness 는 이미 MCP 75도구/handoff/capabilities 로 상당 부분 정합**. 적용 항목을 백로그 등록:
43
+ - **UR-0030** 운영 레이어 정체성 포지셔닝 (실행기 경쟁 지양)
44
+ - **UR-0031** MCP 시맨틱 verb 정비 (get_context/start_task/record_decision/verify_done/make_handoff …)
45
+ - **UR-0032** `.leerness/` JSON 상태 스키마 (runs/tasks/decisions — 에이전트 간 인수인계 표준)
46
+ - **UR-0033** `leerness adapter <tool>` 도구별 선택 생성 (--minimal 확장)
47
+ - **UR-0034** 권한 등급 enforced (read-only…publish, 기본 보수적) — capabilities 확장
48
+ - **UR-0035** AGENTS.md(정적) vs leerness(동적) 역할 분리 포지셔닝
49
+
50
+ ### 검증
51
+ - **selftest 27/27 PASS** · **E2E 224/224 PASS** (회귀 0) — agents list/quota 10 CLI · provider list 10.
52
+
3
53
  ## 1.9.276 — 2026-06-03 — 설치 부담 완화: init --dry-run / --minimal / --no-env (GPT-5.5 2차 리뷰)
4
54
 
5
55
  **📦 GPT-5.5 2차 외부 평가(7/10)의 핵심 지적 "init이 너무 침투적 + 미리보기 부재"를 직접 해결.**
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.276-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-6-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
  ╔══════════════════════════════════════════════════════════════╗
@@ -28,7 +28,7 @@
28
28
 
29
29
  ## 한눈에 보기
30
30
 
31
- `leerness`는 **Claude Code · Cursor · Copilot · Codex · Antigravity · Grok** 같은 AI 코딩 에이전트와 함께 일할 때 생기는 구조적 문제를 자동으로 막아주는 CLI 도구입니다.
31
+ `leerness`는 **Claude Code · Cursor · Copilot · Codex · Antigravity · Grok · opencode · Qwen · Aider · Goose** 같은 AI 코딩 에이전트와 함께 일할 때 생기는 구조적 문제를 자동으로 막아주는 CLI 도구입니다.
32
32
 
33
33
  AI는 코드를 빠르게 쓰지만, 다음 **5가지 함정**에 반복해서 빠집니다 — leerness는 이를 모두 자동으로 감지·차단·회복합니다.
34
34
 
@@ -139,7 +139,7 @@ leerness session close [path] # 세션 마감 + 다음 세션 인계
139
139
  leerness drift check [path] # leerness 미사용 drift 점수
140
140
 
141
141
  # 멀티 AI 에이전트 · 역할
142
- leerness setup-agents # 활성화 (claude/codex/agy/grok/copilot/ollama)
142
+ leerness setup-agents # 활성화 (claude/codex/agy/grok/opencode/qwen/aider/goose/copilot/ollama)
143
143
  leerness agents list | quota # 상태 / 한도
144
144
  leerness agents dispatch "<task>" --role coder # 역할 기반 모델 라우팅
145
145
  leerness agents bench "<task>" # 여러 CLI 동시 호출 + 비교
@@ -215,7 +215,7 @@ leerness release channel # 현재 채널/정책 확인 (--json)
215
215
 
216
216
  ## At a glance
217
217
 
218
- `leerness` is a CLI harness that automatically prevents the structural problems that arise when working with AI coding agents like **Claude Code · Cursor · Copilot · Codex · Antigravity · Grok**.
218
+ `leerness` is a CLI harness that automatically prevents the structural problems that arise when working with AI coding agents like **Claude Code · Cursor · Copilot · Codex · Antigravity · Grok · opencode · Qwen · Aider · Goose**.
219
219
 
220
220
  AI writes code fast, but repeatedly falls into **5 traps** — leerness detects, blocks, and recovers from all of them.
221
221
 
@@ -326,7 +326,7 @@ leerness session close [path] # close session + hand off
326
326
  leerness drift check [path] # leerness-disuse drift score
327
327
 
328
328
  # Multi AI agents · roles
329
- leerness setup-agents # enable (claude/codex/agy/grok/copilot/ollama)
329
+ leerness setup-agents # enable (claude/codex/agy/grok/opencode/qwen/aider/goose/copilot/ollama)
330
330
  leerness agents list | quota # status / limits
331
331
  leerness agents dispatch "<task>" --role coder # role-based model routing
332
332
  leerness agents bench "<task>" # call several CLIs at once + compare
@@ -440,7 +440,7 @@ MIT — © leerness contributors
440
440
  <!-- leerness:project-readme:start -->
441
441
  ## Leerness Project Harness
442
442
 
443
- 이 프로젝트는 Leerness v1.9.276 하네스를 사용합니다. 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.276는 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.276는 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.276)** · 매 라운드 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.276: 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.276';
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 처리).
@@ -866,19 +866,24 @@ async function resolveInstallOptions(root, opts = {}) {
866
866
  { label: 'Codex (OpenAI codex CLI)', description: 'OpenAI 코드 모델', id: 'codex' },
867
867
  { label: 'Antigravity (agy CLI)', description: 'Google Antigravity 멀티모달 에이전트 (1.9.248~)', id: 'agy' },
868
868
  { label: 'Grok (xAI grok-cli)', description: 'xAI Grok CLI — grok-beta 등 (1.9.268~)', id: 'grok' },
869
+ { label: 'opencode', description: '오픈소스 터미널 AI 코딩 에이전트 (1.9.277~)', id: 'opencode' },
870
+ { label: 'Qwen Code (qwen)', description: 'Alibaba Qwen Code CLI (1.9.277~)', id: 'qwen' },
871
+ { label: 'Aider', description: 'git-aware 페어 프로그래밍 CLI (1.9.277~)', id: 'aider' },
872
+ { label: 'Goose (Block)', description: '오픈소스 범용 로컬 AI 에이전트 — MCP 확장 (1.9.277~)', id: 'goose' },
869
873
  { label: 'Copilot (gh extension)', description: 'GitHub Copilot CLI', id: 'copilot' },
870
874
  { label: 'Ollama (로컬 LLM — llama3/qwen 등)', description: 'http://localhost:11434 — 무료/오프라인', id: 'ollama' }
871
875
  ], { defaults: [] });
872
876
  agentsOptIn = picked.length ? picked.map(p => p.id) : 'none';
873
877
  } else {
874
878
  log('\nCLI 에이전트 활성화 (복수 선택 — 콤마로 구분, opt-in):');
875
- log(' 1) claude 2) codex 3) agy (Antigravity) 4) grok (xAI) 5) copilot 6) ollama (예: 1,6 또는 all 또는 none)');
879
+ log(' 1) claude 2) codex 3) agy 4) grok 5) opencode 6) qwen 7) aider 8) goose 9) copilot 10) ollama (예: 1,8 또는 all 또는 none)');
876
880
  const a = (await ask('선택 [none]: ')).trim().toLowerCase();
877
- if (a === 'all') agentsOptIn = ['claude', 'codex', 'agy', 'grok', 'copilot', 'ollama'];
881
+ const ALL_IDS = ['claude', 'codex', 'agy', 'grok', 'opencode', 'qwen', 'aider', 'goose', 'copilot', 'ollama'];
882
+ if (a === 'all') agentsOptIn = ALL_IDS.slice();
878
883
  else if (!a || a === 'none' || a === '0') agentsOptIn = 'none';
879
884
  else {
880
- const map = { '1': 'claude', '2': 'codex', '3': 'agy', '4': 'grok', '5': 'copilot', '6': 'ollama' };
881
- const picks = a.split(/[,\s]+/).map(t => map[t] || (['claude','codex','agy','grok','copilot','ollama'].includes(t) ? t : null)).filter(Boolean);
885
+ const map = { '1': 'claude', '2': 'codex', '3': 'agy', '4': 'grok', '5': 'opencode', '6': 'qwen', '7': 'aider', '8': 'goose', '9': 'copilot', '10': 'ollama' };
886
+ const picks = a.split(/[,\s]+/).map(t => map[t] || (ALL_IDS.includes(t) ? t : null)).filter(Boolean);
882
887
  agentsOptIn = picks.length ? picks : 'none';
883
888
  }
884
889
  }
@@ -1071,6 +1076,10 @@ async function install(root, opts = {}) {
1071
1076
  LEERNESS_ENABLE_CODEX: enable('codex') ? '1' : '0',
1072
1077
  LEERNESS_ENABLE_AGY: enable('agy') ? '1' : '0',
1073
1078
  LEERNESS_ENABLE_GROK: enable('grok') ? '1' : '0',
1079
+ LEERNESS_ENABLE_OPENCODE: enable('opencode') ? '1' : '0',
1080
+ LEERNESS_ENABLE_QWEN: enable('qwen') ? '1' : '0',
1081
+ LEERNESS_ENABLE_AIDER: enable('aider') ? '1' : '0',
1082
+ LEERNESS_ENABLE_GOOSE: enable('goose') ? '1' : '0',
1074
1083
  LEERNESS_ENABLE_COPILOT: enable('copilot') ? '1' : '0',
1075
1084
  LEERNESS_ENABLE_OLLAMA: enable('ollama') ? '1' : '0',
1076
1085
  LEERNESS_SKILL_DISCOVER_URL: '',
@@ -2944,13 +2953,14 @@ function _selfTestCases() {
2944
2953
  { name: '_agentSlashHint: grok 슬래시 요약 + copilot 하위명령 라벨', run: () => { const g = _agentSlashHint('.', 'grok'); const c = _agentSlashHint('.', 'copilot'); return !!g && g.count > 0 && /Grok/.test(g.summary) && !!c && c.invoke === 'subcommand' && /하위명령/.test(c.summary); } },
2945
2954
  { name: '_parseSlashFromHelp: 슬래시 검출 + 플래그 제외 (1.9.267)', run: () => { const r = _parseSlashFromHelp(' /help show help\n /model switch model\n --version print version\n', 'slash'); return r.length === 2 && r[0].cmd === '/help' && /show help/.test(r[0].desc) && !r.some(c => /version/.test(c.cmd)); } },
2946
2955
  { name: '_parseSlashFromHelp: subcommand 모드 들여쓰기 파싱 (1.9.267)', run: () => { const r = _parseSlashFromHelp('Commands:\n suggest 제안\n explain 설명\n', 'subcommand'); return r.length >= 2 && r.some(c => c.cmd === 'suggest') && r.some(c => c.cmd === 'explain'); } },
2947
- { name: 'EXTERNAL_AGENTS: grok 정식 provider 승격 (1.9.268)', run: () => { const g = EXTERNAL_AGENTS.find(a => a.id === 'grok'); return !!g && g.bin === 'grok' && g.envFlag === 'LEERNESS_ENABLE_GROK' && EXTERNAL_AGENTS.length === 6; } },
2956
+ { name: 'EXTERNAL_AGENTS: 10종 (grok + opencode/qwen/aider/goose 포함) (1.9.277)', run: () => { const ids = EXTERNAL_AGENTS.map(a => a.id); return EXTERNAL_AGENTS.length === 10 && ['grok','opencode','qwen','aider','goose'].every(id => ids.includes(id)) && EXTERNAL_AGENTS.find(a => a.id === 'goose').envFlag === 'LEERNESS_ENABLE_GOOSE'; } },
2948
2957
  { name: '_detectSystemLang: POSIX LANG ko_KR/en_US 판별 (1.9.269)', run: () => _detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko' && _detectSystemLang({ LANG: 'en_US.UTF-8' }) === 'en' },
2949
2958
  { name: '_detectSystemLang: LC_ALL 우선 + LANGUAGE 폴백 (1.9.269)', run: () => _detectSystemLang({ LC_ALL: 'ko_KR.UTF-8', LANG: 'en_US.UTF-8' }) === 'ko' && _detectSystemLang({ LANGUAGE: 'en_GB' }) === 'en' },
2950
2959
  { name: 'ROLE_CATALOG + _normalizeRole: 7종 + 한국어 별칭 (1.9.270)', run: () => { const keys = Object.keys(ROLE_CATALOG); return keys.length === 7 && keys.includes('coder') && keys.includes('reviewer') && _normalizeRole('코딩') === 'coder' && _normalizeRole('검수자') === 'reviewer' && _normalizeRole('지휘관') === 'commander'; } },
2951
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')); } },
2952
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; } },
2953
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; } },
2954
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]); } },
2955
2965
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
2956
2966
  ];
@@ -3609,6 +3619,7 @@ function commandsCmd(root) {
3609
3619
  { cmd: 'agents list|check|quota|dispatch|multi', desc: '외부 AI CLI 오케스트레이션 (dispatch --role 모델 라우팅 1.9.270)' },
3610
3620
  { cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배) — 1.9.270' },
3611
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' },
3612
3623
  { cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
3613
3624
  { cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
3614
3625
  { cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
@@ -9464,6 +9475,10 @@ const _LEERNESS_NONSECRET_KEYS = new Set([
9464
9475
  'LEERNESS_ENABLE_CODEX',
9465
9476
  'LEERNESS_ENABLE_AGY',
9466
9477
  'LEERNESS_ENABLE_GROK',
9478
+ 'LEERNESS_ENABLE_OPENCODE',
9479
+ 'LEERNESS_ENABLE_QWEN',
9480
+ 'LEERNESS_ENABLE_AIDER',
9481
+ 'LEERNESS_ENABLE_GOOSE',
9467
9482
  'LEERNESS_ENABLE_COPILOT',
9468
9483
  'LEERNESS_ENABLE_OLLAMA',
9469
9484
  'LEERNESS_SKILL_DISCOVER_URL', // 공개 URL
@@ -10122,6 +10137,15 @@ const EXTERNAL_AGENTS = [
10122
10137
  // → provider cycle / setup-agents / dispatch / slash-commands --refresh probe 가 grok 도 자동 처리.
10123
10138
  { id: 'grok', bin: 'grok', envFlag: 'LEERNESS_ENABLE_GROK', versionArgs: ['--version'], desc: 'xAI Grok CLI (커뮤니티 grok-cli, /model grok-beta 등)',
10124
10139
  installCmd: 'npm i -g @vibe-kit/grok-cli', installHint: 'https://github.com/superagent-ai/grok-cli (xAI API 키 필요 — /login 또는 GROK_API_KEY)' },
10140
+ // 1.9.277 (사용자 명시): 신규 CLI 에이전트 4종 — opencode/qwen/aider/goose. 활성 시 sub-agent dispatch/roles 라우팅 대상.
10141
+ { id: 'opencode', bin: 'opencode', envFlag: 'LEERNESS_ENABLE_OPENCODE', versionArgs: ['--version'], desc: 'opencode — 오픈소스 터미널 AI 코딩 에이전트 (provider-agnostic)',
10142
+ installCmd: 'npm i -g opencode-ai', installHint: 'https://opencode.ai (또는 curl -fsSL https://opencode.ai/install | bash)' },
10143
+ { id: 'qwen', bin: 'qwen', envFlag: 'LEERNESS_ENABLE_QWEN', versionArgs: ['--version'], desc: 'Qwen Code CLI (Alibaba qwen3-coder 등)',
10144
+ installCmd: 'npm i -g @qwen-code/qwen-code', installHint: 'https://github.com/QwenLM/qwen-code (DASHSCOPE/OpenAI 호환 키)' },
10145
+ { id: 'aider', bin: 'aider', envFlag: 'LEERNESS_ENABLE_AIDER', versionArgs: ['--version'], desc: 'Aider — git-aware 페어 프로그래밍 CLI (--model 임의 지정)',
10146
+ installCmd: 'python -m pip install aider-install && aider-install', installHint: 'https://aider.chat (pip install aider-chat · API 키 env)' },
10147
+ { id: 'goose', bin: 'goose', envFlag: 'LEERNESS_ENABLE_GOOSE', versionArgs: ['--version'], desc: 'Goose — Block 오픈소스 범용 로컬 AI 에이전트 (MCP 확장)',
10148
+ installCmd: 'curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash', installHint: 'https://block.github.io/goose (goose configure 로 provider/model 설정)' },
10125
10149
  { id: 'copilot', bin: 'gh', envFlag: 'LEERNESS_ENABLE_COPILOT', versionArgs: ['copilot', '--version'], desc: 'GitHub Copilot CLI (gh copilot)',
10126
10150
  installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' },
10127
10151
  // 1.9.146: Ollama 추가 (사용자 명시 요청 #3) — 로컬 LLM, HTTP API 11434
@@ -10200,6 +10224,40 @@ const AGENT_SLASH_COMMANDS = {
10200
10224
  { cmd: 'explain', desc: '명령 설명 (gh copilot explain "...")' },
10201
10225
  { cmd: 'config', desc: 'gh copilot 설정' }
10202
10226
  ]
10227
+ },
10228
+ // 1.9.277: 신규 4종 슬래시/세션 명령 (best-effort 큐레이션 — 배포판마다 차이 가능, 사용자 override 권장)
10229
+ opencode: {
10230
+ label: 'opencode', asOf: '1.9.277', invoke: 'slash',
10231
+ note: 'TUI 세션 슬래시 — 배포판마다 차이 가능.',
10232
+ commands: [
10233
+ { cmd: '/help', desc: '명령 목록' }, { cmd: '/new', desc: '새 세션' }, { cmd: '/models', desc: '모델 선택' },
10234
+ { cmd: '/init', desc: '프로젝트 컨텍스트' }, { cmd: '/undo', desc: '되돌리기' }, { cmd: '/share', desc: '세션 공유' }, { cmd: '/exit', desc: '종료' }
10235
+ ]
10236
+ },
10237
+ qwen: {
10238
+ label: 'Qwen Code CLI', asOf: '1.9.277', invoke: 'slash',
10239
+ note: 'Gemini CLI 계열 슬래시(qwen-code) 기준 큐레이션.',
10240
+ commands: [
10241
+ { cmd: '/help', desc: '명령 목록' }, { cmd: '/clear', desc: '컨텍스트 초기화' }, { cmd: '/memory', desc: '메모리 관리' },
10242
+ { cmd: '/tools', desc: '도구 목록' }, { cmd: '/model', desc: '모델 전환' }, { cmd: '/quit', desc: '종료' }
10243
+ ]
10244
+ },
10245
+ aider: {
10246
+ label: 'Aider', asOf: '1.9.277', invoke: 'slash',
10247
+ note: 'aider 세션 내 슬래시 명령 (in-chat commands).',
10248
+ commands: [
10249
+ { cmd: '/add', desc: '파일 컨텍스트 추가' }, { cmd: '/drop', desc: '파일 제거' }, { cmd: '/ask', desc: '코드 수정 없이 질문' },
10250
+ { cmd: '/run', desc: '셸 명령 실행' }, { cmd: '/test', desc: '테스트 실행' }, { cmd: '/diff', desc: '변경 diff' },
10251
+ { cmd: '/commit', desc: '커밋' }, { cmd: '/undo', desc: '마지막 커밋 취소' }, { cmd: '/help', desc: '도움말' }
10252
+ ]
10253
+ },
10254
+ goose: {
10255
+ label: 'Goose (Block)', asOf: '1.9.277', invoke: 'slash',
10256
+ note: 'goose 세션 내 명령 (배포판마다 차이 가능).',
10257
+ commands: [
10258
+ { cmd: '/?', desc: '도움말' }, { cmd: '/mode', desc: '모드 전환(auto/approve/chat)' }, { cmd: '/extension', desc: 'MCP 확장 관리' },
10259
+ { cmd: '/builtin', desc: '빌트인 확장' }, { cmd: '/exit', desc: '종료' }
10260
+ ]
10203
10261
  }
10204
10262
  };
10205
10263
  function _agentSlashFile(root) { return path.join(absRoot(root), '.harness', 'agent-slash-commands.json'); }
@@ -10411,7 +10469,7 @@ function slashCommandsCmd(root, agentArg, opts = {}) {
10411
10469
  }
10412
10470
 
10413
10471
  // 1.9.157: Provider Registry — 사용자 정의 provider 동적 추가 (.harness/providers.json)
10414
- // 빌트인 6종 (EXTERNAL_AGENTS: claude/codex/agy/grok/copilot/ollama) + 사용자 정의를 merge. OpenRouter / Bedrock 등 새 CLI 즉시 흡수 가능.
10472
+ // 빌트인 10종 (EXTERNAL_AGENTS: claude/codex/agy/grok/opencode/qwen/aider/goose/copilot/ollama) + 사용자 정의를 merge. OpenRouter / Bedrock 등 새 CLI 즉시 흡수 가능.
10415
10473
  // 파일 형식: { "schemaVersion": 1, "providers": [{ id, bin, envFlag, versionArgs, desc, installHint }] }
10416
10474
  function _providersFile(root) { return path.join(absRoot(root), '.harness', 'providers.json'); }
10417
10475
  function _readUserProviders(root) {
@@ -11291,6 +11349,11 @@ function _dispatchCommand(agentId, task, writeMode, model) {
11291
11349
  if (agentId === 'codex') return `codex ${writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check'}${m ? ` -m ${m}` : ''} "${q}"`;
11292
11350
  if (agentId === 'agy') return `agy ${writeMode ? '-p --yolo' : '-p'}${m ? ` --model ${m}` : ''} "${q}"`;
11293
11351
  if (agentId === 'grok') return `grok${writeMode ? ' --yolo' : ''}${m ? ` --model ${m}` : ''} "${q}"`;
11352
+ // 1.9.277: 신규 provider 4종 — best-effort 비대화형 호출 (실제 실행은 사용자/메인)
11353
+ if (agentId === 'opencode') return `opencode run${m ? ` --model ${m}` : ''} "${q}"`;
11354
+ if (agentId === 'qwen') return `qwen -p "${q}"${m ? ` # --model ${m} (env/설정)` : ''}`;
11355
+ if (agentId === 'aider') return `aider --message "${q}"${writeMode ? ' --yes' : ' --no-auto-commits'}${m ? ` --model ${m}` : ''}`;
11356
+ if (agentId === 'goose') return `goose run -t "${q}"${m && m !== 'default' ? ` # provider/model: goose configure` : ''}`;
11294
11357
  if (agentId === 'copilot') return `gh copilot suggest "${q}"`;
11295
11358
  if (agentId === 'ollama') return `# ollama — leerness agent "${q}" --provider ollama${m ? ` --model ${m}` : ''} 로 직접 호출 (REPL: leerness agent)`;
11296
11359
  return `# ${agentId}: 명령 빌더 미정의`;
@@ -17678,6 +17741,29 @@ const _PROVIDER_MODEL_CATALOG = {
17678
17741
  { id: 'grok-2', note: 'xAI Grok 2' },
17679
17742
  { id: 'grok-2-mini', note: '빠른 응답' }
17680
17743
  ],
17744
+ // 1.9.277: 신규 provider 4종 모델 (provider-agnostic 인 도구는 default + 설정 안내)
17745
+ opencode: [
17746
+ { id: 'default', note: 'opencode 설정 모델 (opencode auth/models 로 provider별 지정)' },
17747
+ { id: 'anthropic/claude-sonnet', note: 'Anthropic 경유' },
17748
+ { id: 'openai/gpt-5', note: 'OpenAI 경유' }
17749
+ ],
17750
+ qwen: [
17751
+ { id: 'qwen3-coder-plus', note: '코드 특화 최신' },
17752
+ { id: 'qwen-max', note: '최고 성능' },
17753
+ { id: 'qwen-plus', note: '균형' },
17754
+ { id: 'qwen-turbo', note: '빠름' }
17755
+ ],
17756
+ aider: [
17757
+ { id: 'sonnet', note: 'Anthropic Sonnet (aider --model)' },
17758
+ { id: 'gpt-5', note: 'OpenAI' },
17759
+ { id: 'deepseek', note: 'DeepSeek (가성비)' },
17760
+ { id: 'o1-mini', note: '빠른 reasoning' }
17761
+ ],
17762
+ goose: [
17763
+ { id: 'default', note: 'goose configure 로 설정한 provider/model' },
17764
+ { id: 'claude-sonnet', note: 'Anthropic 경유' },
17765
+ { id: 'gpt-5', note: 'OpenAI 경유' }
17766
+ ],
17681
17767
  copilot: [
17682
17768
  { id: 'default', note: 'gh copilot 기본 (모델 선택 불가)' }
17683
17769
  ],
@@ -17690,7 +17776,7 @@ const _PROVIDER_MODEL_CATALOG = {
17690
17776
  };
17691
17777
 
17692
17778
  // 1.9.170: provider cycle 순서 (Tab) — 빌트인 6종(1.9.268 grok 추가). user provider는 동적으로 뒤에 추가.
17693
- const _PROVIDER_CYCLE_ORDER = ['ollama', 'claude', 'codex', 'agy', 'grok', 'copilot'];
17779
+ const _PROVIDER_CYCLE_ORDER = ['ollama', 'claude', 'codex', 'agy', 'grok', 'opencode', 'qwen', 'aider', 'goose', 'copilot'];
17694
17780
 
17695
17781
  // 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
17696
17782
  const _AGENT_ROLE_PROMPTS = {
@@ -17936,6 +18022,144 @@ function capabilitiesCmd(root, opts = {}) {
17936
18022
  log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json`));
17937
18023
  }
17938
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
+
17939
18163
  // 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
17940
18164
  async function _agentRepl(root, opts) {
17941
18165
  // 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
@@ -20862,6 +21086,8 @@ async function main() {
20862
21086
  if (cmd === 'roles' || cmd === 'role') return rolesCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
20863
21087
  // 1.9.272: leerness capabilities — 권한/보안 표면 공개 (GPT-5.5 외부 리뷰 반영)
20864
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));
20865
21091
  // 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
20866
21092
  if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
20867
21093
  // 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
@@ -21042,5 +21268,7 @@ module.exports = {
21042
21268
  // 1.9.275: 릴리스 채널 (npm dist-tag, GPT-5.5 리뷰) — 단위 테스트
21043
21269
  _resolveNpmTag, releaseChannelCmd,
21044
21270
  // 1.9.276: init --minimal 파일 필터 (GPT-5.5 2차 리뷰) — 단위 테스트
21045
- 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
21046
21274
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.276",
3
+ "version": "1.9.278",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -807,14 +807,16 @@ total++;
807
807
  && /\| grok \|/.test(r1.stdout)
808
808
  && /\| copilot \|/.test(r1.stdout);
809
809
  // env 모두 0 → 비활성
810
- // 1.9.146: Ollama 추가 → 5 CLI · 1.9.268: grok 정식 승격6 CLI
811
- const env2 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_GROK: '0', LEERNESS_ENABLE_COPILOT: '0', LEERNESS_ENABLE_OLLAMA: '0' };
810
+ // 1.9.146: Ollama → 5 · 1.9.268: grok 6 · 1.9.277: opencode/qwen/aider/goose 10 CLI
811
+ const offAll = { LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_GROK: '0', LEERNESS_ENABLE_OPENCODE: '0', LEERNESS_ENABLE_QWEN: '0', LEERNESS_ENABLE_AIDER: '0', LEERNESS_ENABLE_GOOSE: '0', LEERNESS_ENABLE_COPILOT: '0', LEERNESS_ENABLE_OLLAMA: '0' };
812
+ const env2 = { ...process.env, ...offAll };
812
813
  const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'list', '--json'], { encoding: 'utf8', timeout: 15000, env: env2 });
813
814
  let parsed = null;
814
815
  try { parsed = JSON.parse(r2.stdout); } catch {}
815
- const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length === 6 && parsed.agents.every(a => a.status !== 'ready');
816
- const ok = okList && okJson;
817
- console.log(ok ? '✓ B(1.9.30+1.9.268) agents list: 6 CLI 정의 (claude/codex/agy/grok/copilot/ollama)' : `✗ agents list 실패 (list=${okList} json=${okJson})`);
816
+ const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length === 10 && parsed.agents.every(a => a.status !== 'ready');
817
+ const okNew = /\| opencode \|/.test(r1.stdout) && /\| qwen \|/.test(r1.stdout) && /\| aider \|/.test(r1.stdout) && /\| goose \|/.test(r1.stdout);
818
+ const ok = okList && okJson && okNew;
819
+ console.log(ok ? '✓ B(1.9.30+1.9.277) agents list: 10 CLI (claude/codex/agy/grok/opencode/qwen/aider/goose/copilot/ollama)' : `✗ agents list 실패 (list=${okList} json=${okJson} new=${okNew})`);
818
820
  if (!ok) { failed++; console.log(r1.stdout.slice(0, 500)); }
819
821
  }
820
822
 
@@ -853,11 +855,11 @@ total++;
853
855
  const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'quota', '--json'], { encoding: 'utf8', timeout: 15000, env });
854
856
  let parsed = null;
855
857
  try { parsed = JSON.parse(r2.stdout); } catch {}
856
- // 1.9.146: Ollama 추가 5 CLI · 1.9.268: grok 정식 승격 → 6 CLI
857
- const okJson = parsed && Array.isArray(parsed.quota) && parsed.quota.length === 6
858
+ // 1.9.277: opencode/qwen/aider/goose10 CLI
859
+ const okJson = parsed && Array.isArray(parsed.quota) && parsed.quota.length === 10
858
860
  && parsed.quota.every(q => typeof q.id === 'string' && typeof q.status === 'string' && (q.hint === null || typeof q.hint === 'string'));
859
861
  const ok = okText && okJson;
860
- console.log(ok ? '✓ B(1.9.31+1.9.268) agents quota: 6 CLI 사용량/안내' : `✗ quota 실패 (text=${okText} json=${okJson})`);
862
+ console.log(ok ? '✓ B(1.9.31+1.9.277) agents quota: 10 CLI 사용량/안내' : `✗ quota 실패 (text=${okText} json=${okJson})`);
861
863
  if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
862
864
  }
863
865
 
@@ -2841,5 +2843,37 @@ total++;
2841
2843
  if (!ok) { failed++; console.log((rVerify.stdout || '').slice(0, 300)); }
2842
2844
  }
2843
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
+
2844
2878
  console.log(`\nE2E result: ${total - failed}/${total} passed`);
2845
2879
  if (failed > 0) process.exit(1);