leerness 1.9.280 → 1.9.282
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 +41 -0
- package/README.md +27 -5
- package/bin/harness.js +95 -4
- package/package.json +1 -1
- package/scripts/e2e.js +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.282 — 2026-06-03 — UR-0035: AGENTS.md(정적) vs leerness(동적) 포지셔닝 — 범용 하네스 전략 완성
|
|
4
|
+
|
|
5
|
+
**🧭 GPT-5.5 마지막 전략 항목 — leerness 는 AGENTS.md 를 대체하지 않고 보완. 정적 지침 vs 동적 상태 역할 경계 명시.**
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
GPT-5.5: "AGENTS.md = 정적 프로젝트 지침, leerness = 동적 작업 상태/기억/검증/인수인계. 이 구분이 명확해야 한다 — 대체가 아니라 보완." 전략 백로그(UR-0030~0035)의 마지막.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **AGENTS.md 템플릿에 "정적 vs 동적 — leerness 역할 경계" 섹션** — 모든 init/adapter 생성 AGENTS.md 상단(워크플로 직후)에 포함: 규칙/명령/금지는 AGENTS.md, 진행 상태/검증/인수인계는 `leerness state`/MCP `leerness_state_*` → `.leerness/`. "대체하지 않고 보완" 명시.
|
|
12
|
+
2. **README 포지셔닝 섹션(한/영)** — 정적 vs 동적 비교표 + "모든 에이전트 공통 운영 레이어".
|
|
13
|
+
3. selftest 30→31 + e2e 228→229.
|
|
14
|
+
|
|
15
|
+
### 검증
|
|
16
|
+
- **selftest 31/31 PASS** · **E2E 229/229 PASS** (회귀 0) · init AGENTS.md 경계 섹션 포함 실측.
|
|
17
|
+
|
|
18
|
+
### 🎉 범용 하네스 전략 백로그(UR-0030~0035) 완전 소진
|
|
19
|
+
GPT-5.5 "모든 AI 에이전트에 적용되는 범용 하네스" 방향 6항목 전부 구현:
|
|
20
|
+
- UR-0030 운영 레이어 정체성 · UR-0031 MCP verb · UR-0032 상태 스키마 · UR-0033 어댑터 · UR-0034 권한 등급 · **UR-0035 포지셔닝**.
|
|
21
|
+
- GPT 로드맵 5단계(최소 하네스/어댑터/MCP 서버/검증/권한) 충족.
|
|
22
|
+
|
|
23
|
+
## 1.9.281 — 2026-06-03 — UR-0034: 권한 등급(permission tiers) — opt-in enforced
|
|
24
|
+
|
|
25
|
+
**🛡️ GPT-5.5 "안전성 핵심" — capabilities(1.9.272 공개)를 8단계 enforced 등급으로 확장. 기본 OFF(advisory)라 기존 동작 불변.**
|
|
26
|
+
|
|
27
|
+
### 배경
|
|
28
|
+
GPT-5.5: "범용 하네스가 되려면 안전성이 핵심. read-only/safe-write/.../publish 등급 + 기본 보수적(위험 명령 차단)." 1.9.272 capabilities 가 표면을 *공개*만 했던 것을 *등급 기반 차단*으로 승격하되, 사용자 워크플로(릴리스 등)를 깨지 않도록 **opt-in**.
|
|
29
|
+
|
|
30
|
+
### 구현
|
|
31
|
+
1. **`PERMISSION_TIERS` 8등급** (위험 오름차순): read-only < safe-write < project-write < shell-read < shell-write < git-write < network < publish.
|
|
32
|
+
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.
|
|
33
|
+
3. **`leerness policy <show|set|check>`** — `set <tier> [--enforce]`(.leerness/policy.json) · `check "<command>"` allow/deny · `show` 등급+주의명령 매핑. 기본 허용 `project-write`, enforce OFF.
|
|
34
|
+
4. **opt-in 차단** — `_policyEnforce()` 가 enforce ON(또는 `LEERNESS_ENFORCE_POLICY=1`) 시 허용 등급 초과 명령 차단. **`agents multi --execute`** 진입점에 게이트(기본 OFF → 동작 불변, advisory 경고만).
|
|
35
|
+
5. selftest 29→30 + e2e 227→228.
|
|
36
|
+
|
|
37
|
+
### 검증
|
|
38
|
+
- **selftest 30/30 PASS** · **E2E 228/228 PASS** (회귀 0).
|
|
39
|
+
- 실측: policy set read-only --enforce → `release publish` 🔴 차단 / `handoff` 🟢 허용 · enforce OFF → advisory(통과).
|
|
40
|
+
|
|
41
|
+
### 범용 하네스 로드맵 (GPT 5단계) — 거의 완성
|
|
42
|
+
✅ 최소 하네스(--minimal) · ✅ 어댑터(UR-0033) · ✅ MCP verb(UR-0031) · ✅ 상태 스키마(UR-0032) · ✅ **권한 등급(UR-0034)**. 남음: UR-0035 AGENTS.md(정적) vs leerness(동적) 포지셔닝.
|
|
43
|
+
|
|
3
44
|
## 1.9.280 — 2026-06-03 — UR-0033: leerness adapter <tool> — 도구별 선택 설치 + .mcp.json
|
|
4
45
|
|
|
5
46
|
**🔌 GPT-5.5 로드맵 2단계 "어댑터" — init 전체 대신 특정 도구의 지침/연결 파일만 생성. `--minimal` + `adapter` 로 침투성↓·범용성↑.**
|
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
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -107,6 +107,17 @@ leerness drift check .
|
|
|
107
107
|
→ 권장: leerness session close . (1회 실행으로 🟠 attention 회복)
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### AGENTS.md(정적) vs leerness(동적) — 보완 관계
|
|
111
|
+
|
|
112
|
+
leerness는 [AGENTS.md](https://agents.md)를 **대체하지 않고 보완**합니다.
|
|
113
|
+
|
|
114
|
+
| 정적 — AGENTS.md | 동적 — leerness |
|
|
115
|
+
|---|---|
|
|
116
|
+
| 코딩 규칙·테스트 명령·금지·배포 절차 (자주 안 변함) | 현재 목표·수정 파일·실패 시도·검증 결과·다음 인수인계 (매 작업 변함) |
|
|
117
|
+
| 사람이 작성 | `leerness state`/MCP `leerness_state_*` 가 `.leerness/` 에 기록 |
|
|
118
|
+
|
|
119
|
+
→ 규칙은 AGENTS.md, **작업 상태/기억/검증/인수인계는 leerness** (모든 에이전트 공통 운영 레이어).
|
|
120
|
+
|
|
110
121
|
---
|
|
111
122
|
|
|
112
123
|
## 📊 적용 효과 (정량)
|
|
@@ -303,6 +314,17 @@ leerness drift check .
|
|
|
303
314
|
→ recommended: leerness session close . (one run recovers to 🟠 attention)
|
|
304
315
|
```
|
|
305
316
|
|
|
317
|
+
### AGENTS.md (static) vs leerness (dynamic) — complementary
|
|
318
|
+
|
|
319
|
+
leerness **complements, not replaces** [AGENTS.md](https://agents.md).
|
|
320
|
+
|
|
321
|
+
| Static — AGENTS.md | Dynamic — leerness |
|
|
322
|
+
|---|---|
|
|
323
|
+
| coding rules, test commands, prohibitions, deploy steps (rarely change) | current goal, files changed, failed attempts, verification, next handoff (change every task) |
|
|
324
|
+
| written by humans | recorded to `.leerness/` via `leerness state` / MCP `leerness_state_*` |
|
|
325
|
+
|
|
326
|
+
→ Rules live in AGENTS.md; **work state/memory/verification/handoff live in leerness** (the shared operating layer for any agent).
|
|
327
|
+
|
|
306
328
|
---
|
|
307
329
|
|
|
308
330
|
## 📊 Measured impact
|
|
@@ -449,7 +471,7 @@ MIT — © leerness contributors
|
|
|
449
471
|
<!-- leerness:project-readme:start -->
|
|
450
472
|
## Leerness Project Harness
|
|
451
473
|
|
|
452
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.282 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
453
475
|
|
|
454
476
|
### Core Commands
|
|
455
477
|
|
|
@@ -491,7 +513,7 @@ leerness memory restore decision <date|title>
|
|
|
491
513
|
|
|
492
514
|
### MCP server (외부 AI 통합)
|
|
493
515
|
|
|
494
|
-
Leerness v1.9.
|
|
516
|
+
Leerness v1.9.282는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
|
|
495
517
|
|
|
496
518
|
```jsonc
|
|
497
519
|
// 카테고리별
|
|
@@ -512,7 +534,7 @@ Leerness v1.9.280는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
512
534
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
513
535
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
514
536
|
|
|
515
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
537
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.282)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
516
538
|
|
|
517
539
|
### 성능 가이드 (1.9.140 측정)
|
|
518
540
|
|
|
@@ -550,6 +572,6 @@ leerness release pack --close --auto-main-push
|
|
|
550
572
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
551
573
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
552
574
|
|
|
553
|
-
Last synced by Leerness v1.9.
|
|
575
|
+
Last synced by Leerness v1.9.282: 2026-06-03
|
|
554
576
|
<!-- leerness:project-readme:end -->
|
|
555
577
|
|
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.282';
|
|
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 처리).
|
|
@@ -408,7 +408,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
408
408
|
const project = detectProjectName(root);
|
|
409
409
|
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
410
410
|
const _files = {
|
|
411
|
-
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (1.9.39+ 6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증합니다.\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍 (1.9.13)\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리 (1.9.8)\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용 (1.9.8)\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
|
|
411
|
+
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계 (1.9.282, UR-0035)\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n- 진행 상태/결정/검증/인수인계는 \`leerness state show|start|record|verify|handoff\` (또는 MCP \`leerness_state_*\`) 로 \`.leerness/\` 에 기록한다.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (1.9.39+ 6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증합니다.\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍 (1.9.13)\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리 (1.9.8)\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용 (1.9.8)\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
|
|
412
412
|
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Always run \`leerness handoff .\` at the start and \`leerness session close .\` before ending a session.\n\n**⭐ 매 세션 첫 행동 (1.9.39+)**: \`.harness/session-workflow.md\`의 6단계 워크플로(요청분석→계획→분배→sub-agent→종합검증→마감)를 따라야 함. drift critical 시 \`leerness drift check --auto-fix\`로 자동 회복.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## 자연어 영구 룰 (1.9.8)\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
|
|
413
413
|
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .harness/context-routing.md.\nRun: \`leerness handoff .\` at session start.\nRun: \`leerness session close .\` at session end.\nPreserve Leerness protected files.\n`,
|
|
414
414
|
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
|
|
@@ -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: 'AGENTS.md: 정적 vs 동적 leerness 역할 경계 (1.9.282)', run: () => { const a = coreFiles('.', 'ko', [])['AGENTS.md']; return /정적 vs 동적/.test(a) && /대체하지 않고 \*\*보완\*\*|대체하지 않고 보완/.test(a) && /leerness state/.test(a) && /\.leerness\//.test(a); } },
|
|
2964
|
+
{ 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'); } },
|
|
2963
2965
|
{ 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'; } },
|
|
2964
2966
|
{ 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; } },
|
|
2965
2967
|
{ 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]); } },
|
|
@@ -3622,6 +3624,7 @@ function commandsCmd(root) {
|
|
|
3622
3624
|
{ cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272' },
|
|
3623
3625
|
{ cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278' },
|
|
3624
3626
|
{ cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280' },
|
|
3627
|
+
{ cmd: 'policy show|set|check', desc: '권한 등급 (read-only…publish) — opt-in enforced (위험 명령 차단) — 1.9.281' },
|
|
3625
3628
|
{ cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
|
|
3626
3629
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
3627
3630
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
@@ -11429,6 +11432,12 @@ function agentsCmd(root, sub, ...args) {
|
|
|
11429
11432
|
fail('활성 (ready) 에이전트 없음 — `leerness agents list` 로 확인. 1.9.151 install 흐름에서 복수 선택 후 .env 활성화 필요.');
|
|
11430
11433
|
return process.exit(1);
|
|
11431
11434
|
}
|
|
11435
|
+
// 1.9.281 (UR-0034): 권한 등급 게이트 — enforce ON 시 shell-write 초과 차단 (기본 OFF, 동작 불변)
|
|
11436
|
+
if (execute) {
|
|
11437
|
+
const pol = _policyEnforce(root, 'agents multi --execute');
|
|
11438
|
+
if (!pol.allowed) { fail(pol.reason); return process.exit(1); }
|
|
11439
|
+
if (pol.advisory) warn(`정책 advisory: 'agents multi --execute' 요구 등급 ${pol.required} > 허용 ${pol.allowedTier} (enforce OFF — 진행). leerness policy 로 등급 확인`);
|
|
11440
|
+
}
|
|
11432
11441
|
// 1.9.156: --execute 모드 — 실제 spawn + 결과 수집 + multi-signal consensus
|
|
11433
11442
|
if (execute) {
|
|
11434
11443
|
return (async () => {
|
|
@@ -18053,7 +18062,85 @@ function capabilitiesCmd(root, opts = {}) {
|
|
|
18053
18062
|
log(`## ⚠ 주의해서 쓸 명령 (회사/운영 코드)`);
|
|
18054
18063
|
for (const c of POWERFUL_COMMANDS) log(` • ${c.cmd.padEnd(22)} ${dm(c.note)}`);
|
|
18055
18064
|
log('');
|
|
18056
|
-
log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json`));
|
|
18065
|
+
log(dm(` 전체 정책: SECURITY.md · 기계 판독: leerness capabilities --json · 권한 등급: leerness policy`));
|
|
18066
|
+
}
|
|
18067
|
+
|
|
18068
|
+
// ===== 1.9.281 (UR-0034, GPT-5.5 범용 하네스): 권한 등급(permission tiers) =====
|
|
18069
|
+
// capabilities(1.9.272 공개)를 enforced 등급으로 확장. 기본 OFF(opt-in) — enforce 켤 때만 차단.
|
|
18070
|
+
// 등급 오름차순(왼→오 위험↑). 허용 등급 이하의 명령만 통과. 기본 허용 'project-write'(파일 작업까지, 셸/git/publish 차단).
|
|
18071
|
+
const PERMISSION_TIERS = ['read-only', 'safe-write', 'project-write', 'shell-read', 'shell-write', 'git-write', 'network', 'publish'];
|
|
18072
|
+
function _tierRank(t) { const i = PERMISSION_TIERS.indexOf(String(t || '')); return i < 0 ? PERMISSION_TIERS.length : i; }
|
|
18073
|
+
// 명령/capability → 요구 등급 (순수 함수, selftest)
|
|
18074
|
+
function _requiredTier(cmd) {
|
|
18075
|
+
const c = String(cmd || '').toLowerCase();
|
|
18076
|
+
if (/release\s+publish|npm\s+publish|\bpublish\b/.test(c)) return 'publish';
|
|
18077
|
+
if (/\bweb\b/.test(c)) return 'network';
|
|
18078
|
+
if (/git\s+push|sync-main/.test(c)) return 'git-write';
|
|
18079
|
+
if (/multi\s+--execute|dispatch\s+--write|--yolo|\bpc\b/.test(c)) return 'shell-write';
|
|
18080
|
+
if (/agents\s+(list|quota|bench)|--run-tests/.test(c)) return 'shell-read';
|
|
18081
|
+
if (/\binit\b|\badapter\b|update\s+--yes|\bmigrate\b/.test(c)) return 'project-write';
|
|
18082
|
+
if (/state\s+(start|record|verify|handoff)|decision|lesson|plan\s+add|task\s+add|rule\s+add/.test(c)) return 'safe-write';
|
|
18083
|
+
return 'read-only';
|
|
18084
|
+
}
|
|
18085
|
+
function _policyAllows(allowedTier, requiredTier) { return _tierRank(requiredTier) <= _tierRank(allowedTier); }
|
|
18086
|
+
function _policyFile(root) { return path.join(_leernessStateDir(root), 'policy.json'); }
|
|
18087
|
+
function _loadPolicy(root) {
|
|
18088
|
+
const f = _policyFile(root);
|
|
18089
|
+
const def = { schemaVersion: 1, allowedTier: 'project-write', enforce: false };
|
|
18090
|
+
if (!exists(f)) return def;
|
|
18091
|
+
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; }
|
|
18092
|
+
}
|
|
18093
|
+
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'); }
|
|
18094
|
+
// 위험 진입점이 호출 — enforce ON 이고 요구등급 > 허용등급이면 {allowed:false}. 기본 OFF → 항상 allowed:true(+advisory).
|
|
18095
|
+
// env LEERNESS_ENFORCE_POLICY=1 로 강제 ON 가능.
|
|
18096
|
+
function _policyEnforce(root, cmd) {
|
|
18097
|
+
const p = _loadPolicy(root);
|
|
18098
|
+
const enforce = p.enforce || process.env.LEERNESS_ENFORCE_POLICY === '1';
|
|
18099
|
+
const required = _requiredTier(cmd);
|
|
18100
|
+
const allowed = _policyAllows(p.allowedTier, required);
|
|
18101
|
+
if (enforce && !allowed) return { allowed: false, required, allowedTier: p.allowedTier, reason: `정책 차단: '${cmd}' 는 '${required}' 등급 필요 (허용 '${p.allowedTier}'). 해제: leerness policy set ${required} 또는 LEERNESS_ENFORCE_POLICY 해제` };
|
|
18102
|
+
return { allowed: true, required, allowedTier: p.allowedTier, advisory: !allowed };
|
|
18103
|
+
}
|
|
18104
|
+
// leerness policy <show|set|check>
|
|
18105
|
+
function policyCmd(root, sub, ...args) {
|
|
18106
|
+
root = absRoot(root || process.cwd());
|
|
18107
|
+
const json = has('--json');
|
|
18108
|
+
sub = sub || 'show';
|
|
18109
|
+
if (sub === 'set') {
|
|
18110
|
+
const tier = (args.find(a => a && !a.startsWith('-')) || arg('--tier', '')).trim();
|
|
18111
|
+
if (!PERMISSION_TIERS.includes(tier)) return fail(`등급 필요: ${PERMISSION_TIERS.join(' < ')}`);
|
|
18112
|
+
const p = _loadPolicy(root);
|
|
18113
|
+
p.allowedTier = tier;
|
|
18114
|
+
if (has('--enforce')) p.enforce = true;
|
|
18115
|
+
if (has('--no-enforce')) p.enforce = false;
|
|
18116
|
+
_savePolicy(root, p);
|
|
18117
|
+
if (json) { log(JSON.stringify({ set: tier, enforce: p.enforce, file: _policyFile(root) }, null, 2)); return; }
|
|
18118
|
+
ok(`정책: 허용 등급 ${tier} · enforce ${p.enforce ? 'ON' : 'OFF'}`);
|
|
18119
|
+
return;
|
|
18120
|
+
}
|
|
18121
|
+
if (sub === 'check') {
|
|
18122
|
+
const cmd = (args.find(a => a && !a.startsWith('-')) || arg('--cmd', '')).trim();
|
|
18123
|
+
if (!cmd) return fail('policy check "<command>" 필요');
|
|
18124
|
+
const res = _policyEnforce(root, cmd);
|
|
18125
|
+
if (json) { log(JSON.stringify({ cmd, ...res }, null, 2)); return; }
|
|
18126
|
+
log(`# policy check: ${cmd}`);
|
|
18127
|
+
log(` 요구 등급: ${res.required} · 허용 등급: ${res.allowedTier} · ${res.allowed ? '🟢 허용' : '🔴 차단'}${res.advisory ? ' (advisory — enforce OFF)' : ''}`);
|
|
18128
|
+
if (!res.allowed && !res.advisory) log(` ${res.reason}`);
|
|
18129
|
+
return;
|
|
18130
|
+
}
|
|
18131
|
+
// show
|
|
18132
|
+
const p = _loadPolicy(root);
|
|
18133
|
+
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; }
|
|
18134
|
+
log(`# leerness policy (1.9.281, UR-0034) — 권한 등급`);
|
|
18135
|
+
log(`등급(위험 오름차순): ${PERMISSION_TIERS.join(' < ')}`);
|
|
18136
|
+
log(`허용 등급: ${p.allowedTier} · enforce: ${p.enforce ? '🔴 ON (초과 명령 차단)' : '🟢 OFF (advisory only, 기본)'}`);
|
|
18137
|
+
log('');
|
|
18138
|
+
log(`## 주의 명령 요구 등급`);
|
|
18139
|
+
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}`); }
|
|
18140
|
+
log('');
|
|
18141
|
+
log(` 설정: leerness policy set <tier> [--enforce] · 확인: leerness policy check "<command>"`);
|
|
18142
|
+
log(` 강제 ON(임시): LEERNESS_ENFORCE_POLICY=1`);
|
|
18143
|
+
return;
|
|
18057
18144
|
}
|
|
18058
18145
|
|
|
18059
18146
|
// ===== 1.9.278 (UR-0032, GPT-5.5 범용 하네스 방향): .leerness/ JSON 상태 스키마 =====
|
|
@@ -21194,6 +21281,8 @@ async function main() {
|
|
|
21194
21281
|
if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
|
|
21195
21282
|
// 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
|
|
21196
21283
|
if (cmd === 'state') return stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21284
|
+
// 1.9.281 (UR-0034): leerness policy <show|set|check> — 권한 등급 (opt-in enforced)
|
|
21285
|
+
if (cmd === 'policy') return policyCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21197
21286
|
// 1.9.280 (UR-0033): leerness adapter <tool> [path] — 도구별 지침/.mcp.json 선택 생성 (path: 위치 args[2] 또는 --path)
|
|
21198
21287
|
if (cmd === 'adapter') {
|
|
21199
21288
|
const _aTool = args[1] && !args[1].startsWith('-') ? args[1] : 'list';
|
|
@@ -21384,5 +21473,7 @@ module.exports = {
|
|
|
21384
21473
|
// 1.9.278: .leerness/ 상태 스키마 (UR-0032, GPT-5.5 범용 하네스) — 단위 테스트
|
|
21385
21474
|
_newRunRecord, _leernessStateDir, _loadLeernessState, _saveLeernessState, _loadRun, _saveRun, stateCmd,
|
|
21386
21475
|
// 1.9.280: adapter (UR-0033) — 단위 테스트
|
|
21387
|
-
ADAPTERS, _mcpJsonContent, _mergeMcpJson, adapterCmd
|
|
21476
|
+
ADAPTERS, _mcpJsonContent, _mergeMcpJson, adapterCmd,
|
|
21477
|
+
// 1.9.281: 권한 등급 (UR-0034) — 단위 테스트
|
|
21478
|
+
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _loadPolicy, _savePolicy, _policyEnforce, policyCmd
|
|
21388
21479
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -2920,5 +2920,41 @@ total++;
|
|
|
2920
2920
|
if (!ok) { failed++; console.log((rCur.stdout || '').slice(0, 200)); }
|
|
2921
2921
|
}
|
|
2922
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
|
+
|
|
2945
|
+
// 1.9.282 회귀 (UR-0035): AGENTS.md 정적 vs 동적(leerness) 역할 경계 포지셔닝
|
|
2946
|
+
total++;
|
|
2947
|
+
{
|
|
2948
|
+
const gDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pos-'));
|
|
2949
|
+
cp.spawnSync(process.execPath, [CLI, 'init', gDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000 });
|
|
2950
|
+
let ok = false;
|
|
2951
|
+
try {
|
|
2952
|
+
const a = fs.readFileSync(path.join(gDir, 'AGENTS.md'), 'utf8');
|
|
2953
|
+
ok = /정적 vs 동적/.test(a) && /leerness state/.test(a) && /\.leerness\//.test(a) && /대체하지 않고/.test(a);
|
|
2954
|
+
} catch {}
|
|
2955
|
+
console.log(ok ? '✓ B(1.9.282) AGENTS.md: 정적(지침) vs 동적(leerness 상태) 역할 경계 포지셔닝' : `✗ AGENTS.md 포지셔닝 실패`);
|
|
2956
|
+
if (!ok) failed++;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2923
2959
|
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
2924
2960
|
if (failed > 0) process.exit(1);
|