leerness 1.9.279 → 1.9.281
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 +39 -0
- package/README.md +14 -5
- package/bin/harness.js +175 -3
- package/package.json +1 -1
- package/scripts/e2e.js +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.281 — 2026-06-03 — UR-0034: 권한 등급(permission tiers) — opt-in enforced
|
|
4
|
+
|
|
5
|
+
**🛡️ GPT-5.5 "안전성 핵심" — capabilities(1.9.272 공개)를 8단계 enforced 등급으로 확장. 기본 OFF(advisory)라 기존 동작 불변.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
GPT-5.5: "범용 하네스가 되려면 안전성이 핵심. read-only/safe-write/.../publish 등급 + 기본 보수적(위험 명령 차단)." 1.9.272 capabilities 가 표면을 *공개*만 했던 것을 *등급 기반 차단*으로 승격하되, 사용자 워크플로(릴리스 등)를 깨지 않도록 **opt-in**.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **`PERMISSION_TIERS` 8등급** (위험 오름차순): read-only < safe-write < project-write < shell-read < shell-write < git-write < network < publish.
|
|
12
|
+
2. **`_requiredTier(cmd)` 순수 매핑** — release publish→publish, agents multi --execute/pc→shell-write, sync-main/git push→git-write, web→network, init/adapter→project-write, state/decision→safe-write, handoff/audit→read-only.
|
|
13
|
+
3. **`leerness policy <show|set|check>`** — `set <tier> [--enforce]`(.leerness/policy.json) · `check "<command>"` allow/deny · `show` 등급+주의명령 매핑. 기본 허용 `project-write`, enforce OFF.
|
|
14
|
+
4. **opt-in 차단** — `_policyEnforce()` 가 enforce ON(또는 `LEERNESS_ENFORCE_POLICY=1`) 시 허용 등급 초과 명령 차단. **`agents multi --execute`** 진입점에 게이트(기본 OFF → 동작 불변, advisory 경고만).
|
|
15
|
+
5. selftest 29→30 + e2e 227→228.
|
|
16
|
+
|
|
17
|
+
### 검증
|
|
18
|
+
- **selftest 30/30 PASS** · **E2E 228/228 PASS** (회귀 0).
|
|
19
|
+
- 실측: policy set read-only --enforce → `release publish` 🔴 차단 / `handoff` 🟢 허용 · enforce OFF → advisory(통과).
|
|
20
|
+
|
|
21
|
+
### 범용 하네스 로드맵 (GPT 5단계) — 거의 완성
|
|
22
|
+
✅ 최소 하네스(--minimal) · ✅ 어댑터(UR-0033) · ✅ MCP verb(UR-0031) · ✅ 상태 스키마(UR-0032) · ✅ **권한 등급(UR-0034)**. 남음: UR-0035 AGENTS.md(정적) vs leerness(동적) 포지셔닝.
|
|
23
|
+
|
|
24
|
+
## 1.9.280 — 2026-06-03 — UR-0033: leerness adapter <tool> — 도구별 선택 설치 + .mcp.json
|
|
25
|
+
|
|
26
|
+
**🔌 GPT-5.5 로드맵 2단계 "어댑터" — init 전체 대신 특정 도구의 지침/연결 파일만 생성. `--minimal` + `adapter` 로 침투성↓·범용성↑.**
|
|
27
|
+
|
|
28
|
+
### 배경
|
|
29
|
+
GPT-5.5: "leerness adapter claude/cursor/codex/goose/gemini — 각 도구별로 필요한 지침 파일만 선택 생성." 범용 하네스는 모든 에이전트 환경을 한꺼번에 심지 말고 사용자가 쓰는 도구만 연결해야 함.
|
|
30
|
+
|
|
31
|
+
### 구현
|
|
32
|
+
1. **`ADAPTERS` 레지스트리 9종** — claude/cursor/copilot/codex/goose/gemini/opencode/aider/qwen. 각 도구가 소유하는 파일 키(coreFiles 재사용 — 단일 출처) + MCP 지원 여부.
|
|
33
|
+
2. **`leerness adapter <tool> [path] [--dry-run]`** — 해당 도구의 지침 파일만 `writeIfSafe`(비파괴 mergeManaged) 로 생성. `--dry-run` 미리보기(0 변경). `adapter list` 레지스트리 표.
|
|
34
|
+
3. **`.mcp.json` 자동 등록** — MCP 지원 도구(claude/cursor/codex/goose/opencode)는 `.mcp.json` 에 `leerness mcp serve` 를 병합 등록(기존 mcpServers 보존) → 그 도구가 **UR-0031 상태 verb(state_show/start/record/verify/handoff)를 즉시 호출**.
|
|
35
|
+
4. **권장 흐름** — `leerness init . --minimal --no-env` → `leerness adapter <내 도구>` (최소 설치 + 내 에이전트만 연결).
|
|
36
|
+
5. selftest 28→29 + e2e 226→227.
|
|
37
|
+
|
|
38
|
+
### 검증
|
|
39
|
+
- **selftest 29/29 PASS** · **E2E 227/227 PASS** (회귀 0).
|
|
40
|
+
- 디버그: adapter 가 `--path` 만 읽어 위치 인자 path 무시 → CWD 오염 버그 발견·수정(위치 args[2] 우선) + stray 정리. 실측: `adapter cursor` → .cursor + .mcp.json, list 9종, dry-run 0파일.
|
|
41
|
+
|
|
3
42
|
## 1.9.279 — 2026-06-03 — UR-0031: 상태 substrate MCP 시맨틱 verb 노출 (모든 에이전트 공통 호출 표면)
|
|
4
43
|
|
|
5
44
|
**🔌 1.9.278 의 `.leerness/` 상태 substrate 를 MCP 도구 5종으로 노출 — Claude Code/Goose/Codex/Cursor 가 동일한 verb 로 작업 상태를 읽고 쓴다 (GPT-5.5 범용 하네스 핵심).**
|
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
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -196,6 +196,15 @@ leerness init . --minimal # 핵심 파일만 (에디터 통합/가이드/roa
|
|
|
196
196
|
leerness init . --no-env # .env/.env.example 자동 생성만 생략
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
**도구별 어댑터 (1.9.280) — 내 에이전트만 연결:**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
leerness init . --minimal --no-env # 1) 최소 설치 / minimal install
|
|
203
|
+
leerness adapter cursor # 2) 내 도구만 (.cursor + .mcp.json) / only my tool
|
|
204
|
+
leerness adapter list # 가능 어댑터 (claude/cursor/codex/goose/opencode/aider/qwen/...)
|
|
205
|
+
# MCP 지원 도구는 .mcp.json 에 leerness 등록 → 상태 verb(state_*) 직접 호출
|
|
206
|
+
```
|
|
207
|
+
|
|
199
208
|
### 릴리스 채널 (안정 vs 실험)
|
|
200
209
|
|
|
201
210
|
leerness는 활발히 개발됩니다(잦은 1.9.x). 채널을 골라 안정성을 제어하세요:
|
|
@@ -440,7 +449,7 @@ MIT — © leerness contributors
|
|
|
440
449
|
<!-- leerness:project-readme:start -->
|
|
441
450
|
## Leerness Project Harness
|
|
442
451
|
|
|
443
|
-
이 프로젝트는 Leerness v1.9.
|
|
452
|
+
이 프로젝트는 Leerness v1.9.281 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
444
453
|
|
|
445
454
|
### Core Commands
|
|
446
455
|
|
|
@@ -482,7 +491,7 @@ leerness memory restore decision <date|title>
|
|
|
482
491
|
|
|
483
492
|
### MCP server (외부 AI 통합)
|
|
484
493
|
|
|
485
|
-
Leerness v1.9.
|
|
494
|
+
Leerness v1.9.281는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
|
|
486
495
|
|
|
487
496
|
```jsonc
|
|
488
497
|
// 카테고리별
|
|
@@ -503,7 +512,7 @@ Leerness v1.9.279는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
503
512
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
504
513
|
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
514
|
|
|
506
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
515
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.281)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
507
516
|
|
|
508
517
|
### 성능 가이드 (1.9.140 측정)
|
|
509
518
|
|
|
@@ -541,6 +550,6 @@ leerness release pack --close --auto-main-push
|
|
|
541
550
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
542
551
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
543
552
|
|
|
544
|
-
Last synced by Leerness v1.9.
|
|
553
|
+
Last synced by Leerness v1.9.281: 2026-06-03
|
|
545
554
|
<!-- leerness:project-readme:end -->
|
|
546
555
|
|
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.
|
|
12
|
+
const VERSION = '1.9.281';
|
|
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,8 @@ 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: '권한 등급: _requiredTier + _policyAllows 순서 (1.9.281)', run: () => { return _requiredTier('release publish') === 'publish' && _requiredTier('agents multi --execute') === 'shell-write' && _requiredTier('handoff') === 'read-only' && _requiredTier('init') === 'project-write' && _policyAllows('project-write', 'safe-write') === true && _policyAllows('project-write', 'publish') === false && _tierRank('read-only') < _tierRank('publish'); } },
|
|
2964
|
+
{ name: 'ADAPTERS + _mcpJsonContent: 도구 매핑 + .mcp.json 등록 (1.9.280)', run: () => { const ids = Object.keys(ADAPTERS); const okMap = ['claude','cursor','codex','goose','opencode'].every(id => ADAPTERS[id] && Array.isArray(ADAPTERS[id].keys) && ADAPTERS[id].keys.length); const m = JSON.parse(_mcpJsonContent()); return ids.length >= 9 && okMap && ADAPTERS.claude.mcp === true && ADAPTERS.copilot.mcp === false && m.mcpServers.leerness.command === 'npx' && m.mcpServers.leerness.args.join(' ') === 'leerness mcp serve'; } },
|
|
2963
2965
|
{ 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; } },
|
|
2964
2966
|
{ 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]); } },
|
|
2965
2967
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
@@ -3620,6 +3622,8 @@ function commandsCmd(root) {
|
|
|
3620
3622
|
{ cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배) — 1.9.270' },
|
|
3621
3623
|
{ cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272' },
|
|
3622
3624
|
{ cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278' },
|
|
3625
|
+
{ cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280' },
|
|
3626
|
+
{ cmd: 'policy show|set|check', desc: '권한 등급 (read-only…publish) — opt-in enforced (위험 명령 차단) — 1.9.281' },
|
|
3623
3627
|
{ cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
|
|
3624
3628
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
3625
3629
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
@@ -11427,6 +11431,12 @@ function agentsCmd(root, sub, ...args) {
|
|
|
11427
11431
|
fail('활성 (ready) 에이전트 없음 — `leerness agents list` 로 확인. 1.9.151 install 흐름에서 복수 선택 후 .env 활성화 필요.');
|
|
11428
11432
|
return process.exit(1);
|
|
11429
11433
|
}
|
|
11434
|
+
// 1.9.281 (UR-0034): 권한 등급 게이트 — enforce ON 시 shell-write 초과 차단 (기본 OFF, 동작 불변)
|
|
11435
|
+
if (execute) {
|
|
11436
|
+
const pol = _policyEnforce(root, 'agents multi --execute');
|
|
11437
|
+
if (!pol.allowed) { fail(pol.reason); return process.exit(1); }
|
|
11438
|
+
if (pol.advisory) warn(`정책 advisory: 'agents multi --execute' 요구 등급 ${pol.required} > 허용 ${pol.allowedTier} (enforce OFF — 진행). leerness policy 로 등급 확인`);
|
|
11439
|
+
}
|
|
11430
11440
|
// 1.9.156: --execute 모드 — 실제 spawn + 결과 수집 + multi-signal consensus
|
|
11431
11441
|
if (execute) {
|
|
11432
11442
|
return (async () => {
|
|
@@ -18051,7 +18061,85 @@ function capabilitiesCmd(root, opts = {}) {
|
|
|
18051
18061
|
log(`## ⚠ 주의해서 쓸 명령 (회사/운영 코드)`);
|
|
18052
18062
|
for (const c of POWERFUL_COMMANDS) log(` • ${c.cmd.padEnd(22)} ${dm(c.note)}`);
|
|
18053
18063
|
log('');
|
|
18054
|
-
log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json`));
|
|
18064
|
+
log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json · 권한 등급: leerness policy`));
|
|
18065
|
+
}
|
|
18066
|
+
|
|
18067
|
+
// ===== 1.9.281 (UR-0034, GPT-5.5 범용 하네스): 권한 등급(permission tiers) =====
|
|
18068
|
+
// capabilities(1.9.272 공개)를 enforced 등급으로 확장. 기본 OFF(opt-in) — enforce 켤 때만 차단.
|
|
18069
|
+
// 등급 오름차순(왼→오 위험↑). 허용 등급 이하의 명령만 통과. 기본 허용 'project-write'(파일 작업까지, 셸/git/publish 차단).
|
|
18070
|
+
const PERMISSION_TIERS = ['read-only', 'safe-write', 'project-write', 'shell-read', 'shell-write', 'git-write', 'network', 'publish'];
|
|
18071
|
+
function _tierRank(t) { const i = PERMISSION_TIERS.indexOf(String(t || '')); return i < 0 ? PERMISSION_TIERS.length : i; }
|
|
18072
|
+
// 명령/capability → 요구 등급 (순수 함수, selftest)
|
|
18073
|
+
function _requiredTier(cmd) {
|
|
18074
|
+
const c = String(cmd || '').toLowerCase();
|
|
18075
|
+
if (/release\s+publish|npm\s+publish|\bpublish\b/.test(c)) return 'publish';
|
|
18076
|
+
if (/\bweb\b/.test(c)) return 'network';
|
|
18077
|
+
if (/git\s+push|sync-main/.test(c)) return 'git-write';
|
|
18078
|
+
if (/multi\s+--execute|dispatch\s+--write|--yolo|\bpc\b/.test(c)) return 'shell-write';
|
|
18079
|
+
if (/agents\s+(list|quota|bench)|--run-tests/.test(c)) return 'shell-read';
|
|
18080
|
+
if (/\binit\b|\badapter\b|update\s+--yes|\bmigrate\b/.test(c)) return 'project-write';
|
|
18081
|
+
if (/state\s+(start|record|verify|handoff)|decision|lesson|plan\s+add|task\s+add|rule\s+add/.test(c)) return 'safe-write';
|
|
18082
|
+
return 'read-only';
|
|
18083
|
+
}
|
|
18084
|
+
function _policyAllows(allowedTier, requiredTier) { return _tierRank(requiredTier) <= _tierRank(allowedTier); }
|
|
18085
|
+
function _policyFile(root) { return path.join(_leernessStateDir(root), 'policy.json'); }
|
|
18086
|
+
function _loadPolicy(root) {
|
|
18087
|
+
const f = _policyFile(root);
|
|
18088
|
+
const def = { schemaVersion: 1, allowedTier: 'project-write', enforce: false };
|
|
18089
|
+
if (!exists(f)) return def;
|
|
18090
|
+
try { const j = JSON.parse(read(f)); return { schemaVersion: 1, allowedTier: PERMISSION_TIERS.includes(j.allowedTier) ? j.allowedTier : def.allowedTier, enforce: !!j.enforce }; } catch { return def; }
|
|
18091
|
+
}
|
|
18092
|
+
function _savePolicy(root, p) { const d = _leernessStateDir(root); mkdirp(d); writeUtf8(_policyFile(root), JSON.stringify({ schemaVersion: 1, allowedTier: p.allowedTier, enforce: !!p.enforce, updatedAt: new Date().toISOString() }, null, 2) + '\n'); }
|
|
18093
|
+
// 위험 진입점이 호출 — enforce ON 이고 요구등급 > 허용등급이면 {allowed:false}. 기본 OFF → 항상 allowed:true(+advisory).
|
|
18094
|
+
// env LEERNESS_ENFORCE_POLICY=1 로 강제 ON 가능.
|
|
18095
|
+
function _policyEnforce(root, cmd) {
|
|
18096
|
+
const p = _loadPolicy(root);
|
|
18097
|
+
const enforce = p.enforce || process.env.LEERNESS_ENFORCE_POLICY === '1';
|
|
18098
|
+
const required = _requiredTier(cmd);
|
|
18099
|
+
const allowed = _policyAllows(p.allowedTier, required);
|
|
18100
|
+
if (enforce && !allowed) return { allowed: false, required, allowedTier: p.allowedTier, reason: `정책 차단: '${cmd}' 는 '${required}' 등급 필요 (허용 '${p.allowedTier}'). 해제: leerness policy set ${required} 또는 LEERNESS_ENFORCE_POLICY 해제` };
|
|
18101
|
+
return { allowed: true, required, allowedTier: p.allowedTier, advisory: !allowed };
|
|
18102
|
+
}
|
|
18103
|
+
// leerness policy <show|set|check>
|
|
18104
|
+
function policyCmd(root, sub, ...args) {
|
|
18105
|
+
root = absRoot(root || process.cwd());
|
|
18106
|
+
const json = has('--json');
|
|
18107
|
+
sub = sub || 'show';
|
|
18108
|
+
if (sub === 'set') {
|
|
18109
|
+
const tier = (args.find(a => a && !a.startsWith('-')) || arg('--tier', '')).trim();
|
|
18110
|
+
if (!PERMISSION_TIERS.includes(tier)) return fail(`등급 필요: ${PERMISSION_TIERS.join(' < ')}`);
|
|
18111
|
+
const p = _loadPolicy(root);
|
|
18112
|
+
p.allowedTier = tier;
|
|
18113
|
+
if (has('--enforce')) p.enforce = true;
|
|
18114
|
+
if (has('--no-enforce')) p.enforce = false;
|
|
18115
|
+
_savePolicy(root, p);
|
|
18116
|
+
if (json) { log(JSON.stringify({ set: tier, enforce: p.enforce, file: _policyFile(root) }, null, 2)); return; }
|
|
18117
|
+
ok(`정책: 허용 등급 ${tier} · enforce ${p.enforce ? 'ON' : 'OFF'}`);
|
|
18118
|
+
return;
|
|
18119
|
+
}
|
|
18120
|
+
if (sub === 'check') {
|
|
18121
|
+
const cmd = (args.find(a => a && !a.startsWith('-')) || arg('--cmd', '')).trim();
|
|
18122
|
+
if (!cmd) return fail('policy check "<command>" 필요');
|
|
18123
|
+
const res = _policyEnforce(root, cmd);
|
|
18124
|
+
if (json) { log(JSON.stringify({ cmd, ...res }, null, 2)); return; }
|
|
18125
|
+
log(`# policy check: ${cmd}`);
|
|
18126
|
+
log(` 요구 등급: ${res.required} · 허용 등급: ${res.allowedTier} · ${res.allowed ? '🟢 허용' : '🔴 차단'}${res.advisory ? ' (advisory — enforce OFF)' : ''}`);
|
|
18127
|
+
if (!res.allowed && !res.advisory) log(` ${res.reason}`);
|
|
18128
|
+
return;
|
|
18129
|
+
}
|
|
18130
|
+
// show
|
|
18131
|
+
const p = _loadPolicy(root);
|
|
18132
|
+
if (json) { log(JSON.stringify({ tiers: PERMISSION_TIERS, allowedTier: p.allowedTier, enforce: p.enforce, powerfulCommands: POWERFUL_COMMANDS.map(c => ({ cmd: c.cmd, requiredTier: _requiredTier(c.cmd) })) }, null, 2)); return; }
|
|
18133
|
+
log(`# leerness policy (1.9.281, UR-0034) — 권한 등급`);
|
|
18134
|
+
log(`등급(위험 오름차순): ${PERMISSION_TIERS.join(' < ')}`);
|
|
18135
|
+
log(`허용 등급: ${p.allowedTier} · enforce: ${p.enforce ? '🔴 ON (초과 명령 차단)' : '🟢 OFF (advisory only, 기본)'}`);
|
|
18136
|
+
log('');
|
|
18137
|
+
log(`## 주의 명령 요구 등급`);
|
|
18138
|
+
for (const c of POWERFUL_COMMANDS) { const rt = _requiredTier(c.cmd); const al = _policyAllows(p.allowedTier, rt); log(` ${al ? '🟢' : (p.enforce ? '🔴' : '🟡')} ${c.cmd.padEnd(22)} → ${rt}`); }
|
|
18139
|
+
log('');
|
|
18140
|
+
log(` 설정: leerness policy set <tier> [--enforce] · 확인: leerness policy check "<command>"`);
|
|
18141
|
+
log(` 강제 ON(임시): LEERNESS_ENFORCE_POLICY=1`);
|
|
18142
|
+
return;
|
|
18055
18143
|
}
|
|
18056
18144
|
|
|
18057
18145
|
// ===== 1.9.278 (UR-0032, GPT-5.5 범용 하네스 방향): .leerness/ JSON 상태 스키마 =====
|
|
@@ -18192,6 +18280,78 @@ function stateCmd(root, sub, ...args) {
|
|
|
18192
18280
|
return;
|
|
18193
18281
|
}
|
|
18194
18282
|
|
|
18283
|
+
// ===== 1.9.280 (UR-0033, GPT-5.5 범용 하네스): leerness adapter <tool> =====
|
|
18284
|
+
// 목적: 도구별로 "필요한 지침/연결 파일만" 선택 생성 (전체 init 대신). --minimal + adapter 조합으로 침투성↓·범용성↑.
|
|
18285
|
+
// MCP 지원 도구는 .mcp.json (leerness mcp serve 등록) 도 생성 → UR-0031 상태 verb 를 즉시 호출 가능.
|
|
18286
|
+
// 파일 템플릿은 coreFiles() 재사용(단일 출처). 비파괴(writeIfSafe + mergeManaged).
|
|
18287
|
+
const ADAPTERS = {
|
|
18288
|
+
claude: { label: 'Anthropic Claude Code', keys: ['CLAUDE.md', '.claude/commands/handoff.md', '.claude/commands/session-close.md', '.claude/commands/audit.md', '.claude/commands/lazy-detect.md', '.claude/commands/update.md', '.claude/skills/leerness.md'], mcp: true },
|
|
18289
|
+
cursor: { label: 'Cursor', keys: ['.cursor/rules/leerness.mdc'], mcp: true },
|
|
18290
|
+
copilot: { label: 'GitHub Copilot', keys: ['.github/copilot-instructions.md'], mcp: false },
|
|
18291
|
+
codex: { label: 'OpenAI Codex CLI', keys: ['AGENTS.md'], mcp: true },
|
|
18292
|
+
goose: { label: 'Goose (Block)', keys: ['AGENTS.md'], mcp: true },
|
|
18293
|
+
gemini: { label: 'Gemini CLI / Antigravity', keys: ['AGENTS.md'], mcp: false },
|
|
18294
|
+
opencode: { label: 'opencode', keys: ['AGENTS.md'], mcp: true },
|
|
18295
|
+
aider: { label: 'Aider', keys: ['AGENTS.md'], mcp: false },
|
|
18296
|
+
qwen: { label: 'Qwen Code', keys: ['AGENTS.md'], mcp: false }
|
|
18297
|
+
};
|
|
18298
|
+
function _mcpJsonContent() {
|
|
18299
|
+
return JSON.stringify({ mcpServers: { leerness: { command: 'npx', args: ['leerness', 'mcp', 'serve'] } } }, null, 2) + '\n';
|
|
18300
|
+
}
|
|
18301
|
+
// .mcp.json 병합 — 기존 mcpServers 보존하고 leerness 항목만 추가/갱신.
|
|
18302
|
+
function _mergeMcpJson(root) {
|
|
18303
|
+
const f = path.join(absRoot(root), '.mcp.json');
|
|
18304
|
+
let obj = {};
|
|
18305
|
+
if (exists(f)) { try { obj = JSON.parse(read(f)); } catch { obj = {}; } }
|
|
18306
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
18307
|
+
const already = !!obj.mcpServers.leerness;
|
|
18308
|
+
obj.mcpServers.leerness = { command: 'npx', args: ['leerness', 'mcp', 'serve'] };
|
|
18309
|
+
writeUtf8(f, JSON.stringify(obj, null, 2) + '\n');
|
|
18310
|
+
return { file: '.mcp.json', action: already ? 'updated' : 'created' };
|
|
18311
|
+
}
|
|
18312
|
+
function adapterCmd(root, tool, opts = {}) {
|
|
18313
|
+
root = absRoot(root || process.cwd());
|
|
18314
|
+
const json = has('--json');
|
|
18315
|
+
const dry = has('--dry-run') || opts.dryRun;
|
|
18316
|
+
if (!tool || tool === 'list') {
|
|
18317
|
+
if (json) { log(JSON.stringify({ adapters: Object.fromEntries(Object.entries(ADAPTERS).map(([k, v]) => [k, { label: v.label, files: v.keys, mcp: v.mcp }])) }, null, 2)); return; }
|
|
18318
|
+
log(`# leerness adapter (1.9.280, UR-0033) — 도구별 선택 설치`);
|
|
18319
|
+
log(` init 전체 대신 특정 도구의 지침/연결 파일만 생성합니다. (권장: leerness init . --minimal 후 adapter)`);
|
|
18320
|
+
log('');
|
|
18321
|
+
for (const [id, a] of Object.entries(ADAPTERS)) {
|
|
18322
|
+
log(` ${id.padEnd(9)} ${a.label}${a.mcp ? ' [+.mcp.json]' : ''}`);
|
|
18323
|
+
log(` ${a.keys.join(', ')}`);
|
|
18324
|
+
}
|
|
18325
|
+
log(`\n 사용: leerness adapter <tool> [--dry-run]`);
|
|
18326
|
+
return;
|
|
18327
|
+
}
|
|
18328
|
+
const a = ADAPTERS[tool];
|
|
18329
|
+
if (!a) return fail(`알 수 없는 adapter: ${tool} (가능: ${Object.keys(ADAPTERS).join(', ')})`);
|
|
18330
|
+
const lang = exists(path.join(root, '.harness/LANGUAGE')) ? read(path.join(root, '.harness/LANGUAGE')).trim() : 'ko';
|
|
18331
|
+
const files = coreFiles(root, lang === 'en' ? 'en' : 'ko', []);
|
|
18332
|
+
const managedOverwrite = new Set(['AGENTS.md', 'CLAUDE.md', '.cursor/rules/leerness.mdc', '.github/copilot-instructions.md']);
|
|
18333
|
+
const planned = a.keys.filter(k => files[k] != null);
|
|
18334
|
+
if (a.mcp) planned.push('.mcp.json');
|
|
18335
|
+
if (dry) {
|
|
18336
|
+
if (json) { log(JSON.stringify({ adapter: tool, dryRun: true, files: planned }, null, 2)); return; }
|
|
18337
|
+
log(`# leerness adapter ${tool} [dry-run] — 생성/갱신 예정 ${planned.length}개 (실제 변경 0)`);
|
|
18338
|
+
planned.forEach(f => log(` - ${f}`));
|
|
18339
|
+
log(` → 실제 적용: leerness adapter ${tool}`);
|
|
18340
|
+
return;
|
|
18341
|
+
}
|
|
18342
|
+
const results = [];
|
|
18343
|
+
for (const k of a.keys) {
|
|
18344
|
+
if (files[k] == null) continue;
|
|
18345
|
+
results.push(writeIfSafe(root, k, files[k], { mergeManaged: managedOverwrite.has(k) }));
|
|
18346
|
+
}
|
|
18347
|
+
if (a.mcp) results.push(_mergeMcpJson(root));
|
|
18348
|
+
if (json) { log(JSON.stringify({ adapter: tool, files: results }, null, 2)); return; }
|
|
18349
|
+
ok(`adapter ${tool} (${a.label}) 적용 — ${results.length}개 파일`);
|
|
18350
|
+
for (const r of results) log(` ${r.action}: ${r.file}`);
|
|
18351
|
+
if (a.mcp) log(` ℹ .mcp.json 등록 — 이 도구가 leerness MCP verb(state_show/start/record/verify/handoff)를 직접 호출 가능`);
|
|
18352
|
+
return;
|
|
18353
|
+
}
|
|
18354
|
+
|
|
18195
18355
|
// 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
|
|
18196
18356
|
async function _agentRepl(root, opts) {
|
|
18197
18357
|
// 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
|
|
@@ -21120,6 +21280,14 @@ async function main() {
|
|
|
21120
21280
|
if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
|
|
21121
21281
|
// 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
|
|
21122
21282
|
if (cmd === 'state') return stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21283
|
+
// 1.9.281 (UR-0034): leerness policy <show|set|check> — 권한 등급 (opt-in enforced)
|
|
21284
|
+
if (cmd === 'policy') return policyCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21285
|
+
// 1.9.280 (UR-0033): leerness adapter <tool> [path] — 도구별 지침/.mcp.json 선택 생성 (path: 위치 args[2] 또는 --path)
|
|
21286
|
+
if (cmd === 'adapter') {
|
|
21287
|
+
const _aTool = args[1] && !args[1].startsWith('-') ? args[1] : 'list';
|
|
21288
|
+
const _aRoot = (args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd());
|
|
21289
|
+
return adapterCmd(_aRoot, _aTool);
|
|
21290
|
+
}
|
|
21123
21291
|
// 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
|
|
21124
21292
|
if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
|
|
21125
21293
|
// 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
|
|
@@ -21302,5 +21470,9 @@ module.exports = {
|
|
|
21302
21470
|
// 1.9.276: init --minimal 파일 필터 (GPT-5.5 2차 리뷰) — 단위 테스트
|
|
21303
21471
|
MINIMAL_SKIP_KEYS, coreFiles,
|
|
21304
21472
|
// 1.9.278: .leerness/ 상태 스키마 (UR-0032, GPT-5.5 범용 하네스) — 단위 테스트
|
|
21305
|
-
_newRunRecord, _leernessStateDir, _loadLeernessState, _saveLeernessState, _loadRun, _saveRun, stateCmd
|
|
21473
|
+
_newRunRecord, _leernessStateDir, _loadLeernessState, _saveLeernessState, _loadRun, _saveRun, stateCmd,
|
|
21474
|
+
// 1.9.280: adapter (UR-0033) — 단위 테스트
|
|
21475
|
+
ADAPTERS, _mcpJsonContent, _mergeMcpJson, adapterCmd,
|
|
21476
|
+
// 1.9.281: 권한 등급 (UR-0034) — 단위 테스트
|
|
21477
|
+
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _loadPolicy, _savePolicy, _policyEnforce, policyCmd
|
|
21306
21478
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -2897,5 +2897,50 @@ total++;
|
|
|
2897
2897
|
if (!ok) failed++;
|
|
2898
2898
|
}
|
|
2899
2899
|
|
|
2900
|
+
// 1.9.280 회귀 (UR-0033): leerness adapter — 도구별 선택 생성 + .mcp.json
|
|
2901
|
+
total++;
|
|
2902
|
+
{
|
|
2903
|
+
const aDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-adapter-'));
|
|
2904
|
+
cp.spawnSync(process.execPath, [CLI, 'init', aDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
2905
|
+
// adapter cursor: .cursor + .mcp.json 생성, .claude 미생성(minimal이라 commands는 있으나 cursor adapter는 안 건드림)
|
|
2906
|
+
const rCur = cp.spawnSync(process.execPath, [CLI, 'adapter', 'cursor', aDir], { encoding: 'utf8', timeout: 15000 });
|
|
2907
|
+
const cursorOk = rCur.status === 0 && fs.existsSync(path.join(aDir, '.cursor', 'rules', 'leerness.mdc'));
|
|
2908
|
+
let mcpOk = false;
|
|
2909
|
+
try { const m = JSON.parse(fs.readFileSync(path.join(aDir, '.mcp.json'), 'utf8')); mcpOk = m.mcpServers && m.mcpServers.leerness && m.mcpServers.leerness.args.join(' ') === 'leerness mcp serve'; } catch {}
|
|
2910
|
+
// adapter list --json: 9종
|
|
2911
|
+
const rList = cp.spawnSync(process.execPath, [CLI, 'adapter', 'list', '--json', '--path', aDir], { encoding: 'utf8', timeout: 15000 });
|
|
2912
|
+
let listOk = false;
|
|
2913
|
+
try { const j = JSON.parse(rList.stdout); listOk = Object.keys(j.adapters).length >= 9 && j.adapters.claude.mcp === true && j.adapters.copilot.mcp === false; } catch {}
|
|
2914
|
+
// adapter --dry-run: 파일 미생성
|
|
2915
|
+
const dDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-adapterdry-'));
|
|
2916
|
+
const rDry = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--dry-run', '--path', dDir], { encoding: 'utf8', timeout: 15000 });
|
|
2917
|
+
const dryOk = /\[dry-run\]/.test(rDry.stdout) && fs.readdirSync(dDir).length === 0;
|
|
2918
|
+
const ok = cursorOk && mcpOk && listOk && dryOk;
|
|
2919
|
+
console.log(ok ? '✓ B(1.9.280) adapter: cursor(.cursor+.mcp.json)/list 9종/dry-run(0파일)' : `✗ adapter 실패 (cursor=${cursorOk} mcp=${mcpOk} list=${listOk} dry=${dryOk})`);
|
|
2920
|
+
if (!ok) { failed++; console.log((rCur.stdout || '').slice(0, 200)); }
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// 1.9.281 회귀 (UR-0034): 권한 등급 policy (show/set/check + enforce)
|
|
2924
|
+
total++;
|
|
2925
|
+
{
|
|
2926
|
+
const pDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-policy-'));
|
|
2927
|
+
const run = (a) => cp.spawnSync(process.execPath, [CLI, 'policy', ...a, '--path', pDir], { encoding: 'utf8', timeout: 15000 });
|
|
2928
|
+
// 기본: enforce OFF + 8 등급
|
|
2929
|
+
let showOk = false;
|
|
2930
|
+
try { const j = JSON.parse(run(['show', '--json']).stdout); showOk = j.tiers.length === 8 && j.enforce === false && j.allowedTier === 'project-write'; } catch {}
|
|
2931
|
+
// read-only + enforce → publish 차단, handoff 허용
|
|
2932
|
+
run(['set', 'read-only', '--enforce']);
|
|
2933
|
+
let blockOk = false, allowOk = false;
|
|
2934
|
+
try { blockOk = JSON.parse(run(['check', 'release publish', '--json']).stdout).allowed === false; } catch {}
|
|
2935
|
+
try { allowOk = JSON.parse(run(['check', 'handoff', '--json']).stdout).allowed === true; } catch {}
|
|
2936
|
+
// enforce OFF 로 되돌리면 advisory(allowed true)
|
|
2937
|
+
run(['set', 'read-only', '--no-enforce']);
|
|
2938
|
+
let advisoryOk = false;
|
|
2939
|
+
try { const j = JSON.parse(run(['check', 'release publish', '--json']).stdout); advisoryOk = j.allowed === true && j.advisory === true; } catch {}
|
|
2940
|
+
const ok = showOk && blockOk && allowOk && advisoryOk;
|
|
2941
|
+
console.log(ok ? '✓ B(1.9.281) policy: 8등급 + set/check + enforce 차단/advisory' : `✗ policy 실패 (show=${showOk} block=${blockOk} allow=${allowOk} advisory=${advisoryOk})`);
|
|
2942
|
+
if (!ok) failed++;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2900
2945
|
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
2901
2946
|
if (failed > 0) process.exit(1);
|