leerness 1.36.20 → 1.36.22
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 +31 -0
- package/CONTRIBUTING.md +34 -0
- package/README.ko.md +3 -8
- package/README.md +12 -16
- package/bin/leerness.js +121 -12
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.22 — 2026-07-14 — SessionStart 컨텍스트 주입(compaction 후 컨텍스트 유실 차단) + README 를 소개·사용법·효과로 재구성
|
|
4
|
+
|
|
5
|
+
**① SessionStart 컨텍스트 주입** (obra/superpowers 구조 검토 → 배선). 실측 결함: compaction/resume 후 에이전트가 "세션 시작 handoff" 의례를 건너뛰어 컨텍스트를 잃는다. superpowers 는 hook matcher 를 `startup|clear|compact` 로 걸어 하네스가 컨텍스트를 강제 적재한다(에이전트 자발성에 안 기댐) — leerness 는 hook 플럼빙과 compact 신호를 **이미 둘 다 갖고 있었고 배선만 없었다**.
|
|
6
|
+
|
|
7
|
+
- **`leerness hook session-start [path]` 신설** — hook 전용 표면. Claude Code 규약(`{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"…"}}`)으로 진행상황 2~3줄 주입.
|
|
8
|
+
- ★ **handoff 를 호출하지 않는다**: handoff 는 `last-handoff` stamp 를 찍어 detector 3종(핸드오프 넛지·abnormal shutdown·R-0001 interval)을 오염시킨다. 동일 파일 로더만 재사용 — **subprocess 0 · 쓰기 0 · last-handoff.json 미접촉**(실측 불변 확인).
|
|
9
|
+
- **어떤 예외에서도 stdout 무출력 + exit 0** — 세션 시작을 절대 깨지 않는다. 미초기화/없는 경로도 무음.
|
|
10
|
+
- **자기 라벨링**(1.9.272 투명성): "위는 요약 — 전체 컨텍스트는 `leerness handoff .` 실행" + 끄는 법 동봉.
|
|
11
|
+
- opt-out: `--no-context-inject` / `LEERNESS_NO_CONTEXT_INJECT=1`.
|
|
12
|
+
- **`handoff --no-record` / `LEERNESS_HOOK=1` 가드 신설** — 읽기전용/hook 호출이 stamp 를 찍어 detector 를 오염시키던 경로 차단(기본 동작 불변, 실측 무회귀).
|
|
13
|
+
- **hook 설치 배선** — `auto-update install` 이 2번째 SessionStart hook(`matcher: 'startup|clear|compact'`)을 멱등 push. 기존 설치도 자동 취득. 투명성 문구로 무엇을/언제/왜/끄는법 명시.
|
|
14
|
+
- 채택하지 않음: superpowers 의 SKILL.md 전문 주입(leerness 의 값어치는 정적 문서가 아니라 동적 상태) · bash 래퍼 hook(leerness 는 이미 PATH 실행파일).
|
|
15
|
+
|
|
16
|
+
**② README 재구성** — 소개 / 사용법 / 사용효과만 담도록 정리. 자기평가·성숙도 서술은 README 의 일이 아니다.
|
|
17
|
+
- 제거: "자체 수행 클린룸 평가로 검증됨" **주장과 그 한정어를 함께**(주장이 없으면 한정어도 불필요 — 방법론·결과·한계는 원래부터 `docs/clean-room-evaluations.md` 가 보유, Links 에서 링크) · "성숙도" 섹션(초기 단계/단독 유지보수/자율 AI 개발/외부 채택) · ko 의 문서 출처 메타 노트.
|
|
18
|
+
- 이전(삭제 아님): 기여자 테스트 티어(`test:fast`/`test:core`/`npm test`) → **신규 `CONTRIBUTING.md`**(불변식·판별케이스·릴리스 절차 포함).
|
|
19
|
+
- 재구성: "Guidance vs enforcement (be honest about this)" → **"Make it enforced, not optional"**(ci init + branch protection 사용법). gitleaks 병행은 사과가 아니라 사용 권장으로.
|
|
20
|
+
- 정직성 회귀 가드로 전수 재검 → ko 에 남아있던 **"지속적 외부 검증 — 다중 모델 클린룸 리뷰"** 적발·제거(한정어를 뺀 자리에 제3자 검증 함의가 남아있었음). 최종 확인: 감사/인증/보장 함의 문구 0.
|
|
21
|
+
- **검증**: selftest 292/292(신규 hook 소스가드 — 쓰기0 불변식·JSON 규약·matcher compact·opt-out·자기라벨링), hook 실측 5종(JSON 규약 / last-handoff 불변 / 미초기화 무음 exit0 / opt-out 2종 / `--no-record` + 기본 stamp 무회귀), 게이트 e2e, 게시본 클린룸.
|
|
22
|
+
|
|
23
|
+
## 1.36.21 — 2026-07-14 — 경로/EOL/형태 견고성 4종 — 없는 경로에 트리 생성·CRLF plan tasks 증발·오타 하위명령 전체설치·user-requests 원본 덮어쓰기 (22 에이전트 전수 sweep, 전부 재현확정)
|
|
24
|
+
|
|
25
|
+
codex 이연분(#8/#6) 재현에 그치지 않고 **동일 버그 클래스를 전수 조사**(CRLF 파서 / `--json` 실패경로 / 로더 스키마 / mutation-order 잔여) — 22 에이전트가 실행 기반으로 확정하고 후보마다 독립 재검증. 확정 P2 중 **현실성 있는 4건**만 채택(P3/contrived 는 기각·이연).
|
|
26
|
+
|
|
27
|
+
- **handoff / env detect 가 없는 경로에 디렉토리 트리 + `.harness` 를 생성 (P2, 현실성 high)**: 오타 경로로 `handoff` 하면 쓰레기 트리가 생기고, `--json` 은 `ok`/`error` 없는 **정상 "빈 프로젝트" JSON + exit 0** 이라 기계 소비자가 오타를 감지 못 했음. status/health/drift/lazy detect/scan secrets/session close 는 이미 `path_not_found` + exit 1(UR-0136)인데 **이 둘만 관례 갭**. → 두 진입점에 존재 가드(`--pulse`/`--all-apps` 분기보다 앞). 실측: 없는 경로 → exit 1 + `path_not_found` + **디렉토리 생성 0**, 기존 경로 무회귀.
|
|
28
|
+
- **CRLF `plan.md` 에서 모든 마일스톤 tasks 가 조용히 증발 (P2, codex #8a — 3 에이전트 독립 수렴)**: 체크박스 정규식이 `/m` 없이 후행 `\r` 앞에서 `$` 를 못 잡아 실패. id/title/status/progress 는 `/m` 이라 정상 파싱돼 **부분·무증상 실패**(`plan list` 의 Tasks 줄 소실, MCP `leerness_plan_list` 가 tasks 0건 보고 → 계약 위반). leerness 엔 체크박스를 켜는 명령이 없어 tasks 사용엔 외부 편집이 필수인데 Windows 관용 편집(PowerShell `Set-Content` 등)이 기본 CRLF → 현실 경로. → 코드베이스 기존 관례대로 **읽는 즉시 정규화**(혼합 EOL 도 복구). 실측 LF/CRLF 모두 tasks 2건.
|
|
29
|
+
- **`migrate` 오타/추측 하위명령이 전체 설치를 수행 (P2)**: `migrate aduit` / `migrate check` 가 그 토큰을 **경로로 삼아** 오타명 디렉토리를 만들고 77개 파일을 설치 + `--json` 무시 + exit 0. → **존재 기반 가드**(migrate 의 전제는 기존 프로젝트, 새 트리 생성은 `init` 의 일) + `unknown_subcommand` 구조화 에러. 정당 흐름(`migrate .` / `migrate /existing` / `migrate --path X` / `migrate audit|apply|plan`)은 전부 보존. 실측: 오타 → exit 1 + **디렉토리 생성 0**.
|
|
30
|
+
- **`user-requests.json` 미인식 형태 → 원본 통째 덮어쓰기 (P2, 데이터손실)**: wrapper 가 아닌 유효 JSON 을 조용히 `{requests:[]}` 로 강제한 뒤, mutation 이 **읽지 못한 원본을 새 배열로 대체**(read-modify-write 파괴). → bare array 는 관대 수용해 자기치유(decisions/lessons 관례), 그 외 미인식/파싱실패는 `unrecognized` 로 표시해 **3개 파괴적 쓰기 경로가 모두 bail**. 실측: 미인식 형태에서 `requests add` → 기록 건너뜀 + **파일 INTACT**, bare array → 보존+append, canonical 무회귀. 부수 수정: id 를 `length+1` 로 만들어 200개 truncate 후 충돌하던 것 → **최대 UR 번호+1** 파생.
|
|
31
|
+
- **검증**: selftest 291/291(신규 4수정 소스가드 + CRLF 판별 순수행위), 4건 전부 임시 워크스페이스에서 재현→수정 후 재검(17 체크 중 14→17 통과, 나머지 3은 테스트 스크립트 결함으로 판명 후 정정), 게이트 e2e, 게시본 클린룸.
|
|
32
|
+
- **기각/이연**: `[null]`·필드누락 JSON crash(P3, low — leerness 자체 파서는 null 을 만들지 않아 수동 손상 필요) · `copyRec` 심링크(P2, low — .harness 에 심링크를 둘 이유 없음, Windows 권한상 재현 불가) · `--json` 진행로그 오염 12건 중 P3 다수 · **UR-0027 확장 증거**(`requests` 계열은 positional 경로 미지원 → `requests add "text" /other/project` 가 조용히 cwd 에 기록. `task` 는 지원해 불일치가 실수를 유도 — 실제 피해 재현. 계약 변경 리스크로 별도 라운드).
|
|
33
|
+
|
|
3
34
|
## 1.36.20 — 2026-07-14 — `--apply --json`이 실제로 적용하도록 수정 — mutation-before-output (codex fresh-QA #7, 3커맨드 재현확정)
|
|
4
35
|
|
|
5
36
|
codex fresh-surface QA(1.36.19 이연분)에서 확정한 **silent no-op** 버그 수정: `--json` 분기가 mutation "전"에 return 해, `--apply --json`이 적용을 보고하고도 실제로는 아무 변경도 안 하던 3개 커맨드.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Contributing to leerness
|
|
2
|
+
|
|
3
|
+
## Invariants (non-negotiable)
|
|
4
|
+
|
|
5
|
+
- **0 runtime dependencies** and **0 install scripts** (`preinstall` / `install` / `postinstall`). `leerness install-safety` asserts both; the release gate fails if either is violated.
|
|
6
|
+
- **UTF-8, no BOM** for every source file.
|
|
7
|
+
- Korean-first comments in `bin/` and `lib/` (the codebase's existing convention).
|
|
8
|
+
- Protected harness files are never deleted — merge, archive, or mark deprecated instead.
|
|
9
|
+
|
|
10
|
+
## Test tiers
|
|
11
|
+
|
|
12
|
+
Three tiers, fastest to slowest:
|
|
13
|
+
|
|
14
|
+
| Command | What it runs | Time | Use it for |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `npm run test:fast` | `selftest` + smoke (commands run without crashing) | < 1 min | the dev loop |
|
|
17
|
+
| `npm run test:core` | `selftest` + a flagship behavioral suite — `verify-claim` / `gate` / `contract` / `scan` actually reject bad input and pass honest input | ~20 s | pre-commit, quick CI |
|
|
18
|
+
| `npm test` | `--version` + `selftest` + the entire e2e suite | **10+ minutes by design** | the release gate |
|
|
19
|
+
|
|
20
|
+
`npm test` is the gate that must pass before publishing. It is deliberately slow: the e2e suite drives the published CLI surface end-to-end rather than mocking it.
|
|
21
|
+
|
|
22
|
+
## Adding a check
|
|
23
|
+
|
|
24
|
+
Prefer **behavioral** tests over source-grep guards. A source-grep guard asserts that a string exists, not that the behavior holds — and it can match its own literal in the file it greps (use split literals like `'function ' + 'name'` to anchor past your own line if you must grep).
|
|
25
|
+
|
|
26
|
+
When you add a guard, construct the **discriminating case**: an input where the old code passes and only the new check catches it. If you cannot construct one, the check may not be measuring what you think.
|
|
27
|
+
|
|
28
|
+
## Release
|
|
29
|
+
|
|
30
|
+
`npm test` (gate) → bump `VERSION` in `bin/leerness.js` **and** `version` in `package.json` (selftest asserts they match) → CHANGELOG entry → commit → publish → push.
|
|
31
|
+
|
|
32
|
+
## How releases get tested
|
|
33
|
+
|
|
34
|
+
Methodology, results, and limitations of the clean-room evaluations: [docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md).
|
package/README.ko.md
CHANGED
|
@@ -11,12 +11,10 @@
|
|
|
11
11
|
╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
> **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다.
|
|
14
|
+
> **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다.
|
|
15
15
|
|
|
16
16
|
[](https://www.npmjs.com/package/leerness) ·  · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
|
|
17
17
|
|
|
18
|
-
> 이 문서는 **AI 다중 모델(Codex / Claude Sonnet / Claude Opus)이 README 없이 leerness 를 직접 설치·실행·분석한 클린룸 리뷰**(행위 기반 — 제3자 인간 객관 감사는 아님)를 바탕으로 재구성됐습니다. 매 릴리스마다 자동 갱신됩니다.
|
|
19
|
-
|
|
20
18
|
---
|
|
21
19
|
|
|
22
20
|
## leerness가 뭔가요?
|
|
@@ -45,11 +43,9 @@ leerness는 이 문제들을 해결하는 **외부 운영 substrate**입니다.
|
|
|
45
43
|
|
|
46
44
|
---
|
|
47
45
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
의존하기 전에 솔직히: leerness 는 **초기 단계이고 사실상 단독 유지보수**이며, 대부분 **자율 AI 라운드**로 개발됐습니다 — 따라서 자체 `selftest` + e2e 스위트가 1차 품질 신호이고, 외부 채택은 아직 미미합니다. 신뢰만으로 load-bearing 으로 만들지 마세요: **버전을 핀**하고, 차별화된 슬라이스 — `verify-claim` + CI `gate`(필수 체크) — 만 의존할 부분으로 삼으세요.
|
|
46
|
+
## 도입 부담이 낮은 설계
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
MIT, **런타임 의존성 0**, offline-first, 모든 상태가 *당신의* 저장소 안 평문 파일입니다. lock-in 이 거의 없어 — 값을 못 하면 도구만 빼도 `task`/`decision`/`lesson` 파일은 그대로 남습니다. CI 에서는 **버전을 핀**하세요 — 조용한 업그레이드로 게이트 판정이 바뀌지 않습니다. (시크릿은 gitleaks/trufflehog 같은 전용 스캐너와 병행하세요 — `scan secrets` 는 에이전트가 로컬에서 보는 것과 같은 신호를 주는 편의 레이어입니다.)
|
|
53
49
|
|
|
54
50
|
---
|
|
55
51
|
|
|
@@ -170,7 +166,6 @@ leerness mcp serve # JSON-RPC over stdio, 85 도구
|
|
|
170
166
|
- **통합 게이트** — 보안·인코딩·드리프트·검증을 `gate` 하나로.
|
|
171
167
|
- **한국어 우선 + Windows/인코딩 1급 시민** — CP949/BOM 가드, 한국어 출력 기본(`--language en` 지원).
|
|
172
168
|
- **자기기술(self-describing)** — `about`/`commands`/`capabilities`/`doctor` 로 도구가 스스로 설명.
|
|
173
|
-
- **지속적 외부 검증** — 다중 모델 클린룸 리뷰로 회귀 0 을 유지하며 진화.
|
|
174
169
|
|
|
175
170
|
---
|
|
176
171
|
|
package/README.md
CHANGED
|
@@ -65,13 +65,11 @@ Built-in harnesses remember what the AI **said**. leerness verifies what the AI
|
|
|
65
65
|
| Secrets · encoding · drift guards | none | `scan secrets` · `encoding check` · `drift check --auto-fix` — CI-ready |
|
|
66
66
|
| Lock-in | one vendor | any agent, any language, 0 runtime dependencies |
|
|
67
67
|
|
|
68
|
-
This positioning is checked by **self-administered clean-room evaluations** — AI agents do a fresh `npm install` into temp dirs and drive it by behavior only, including adversarial attacks against the verifier itself (fake tests, comment-only stubs, inflated test counts — all rejected). To be clear: these are *AI* clean-room runs, **not third-party human audits or peer review** — they make the claim *checkable* rather than a marketing line. Methodology, results, and honest limitations: **[docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md)**.
|
|
69
|
-
|
|
70
68
|
---
|
|
71
69
|
|
|
72
|
-
##
|
|
70
|
+
## Make it enforced, not optional
|
|
73
71
|
|
|
74
|
-
By default leerness is **cooperative**: your AI agent runs the commands because CLAUDE.md / AGENTS.md tell it to. A determined agent could skip them. To
|
|
72
|
+
By default leerness is **cooperative**: your AI agent runs the commands because CLAUDE.md / AGENTS.md tell it to. A determined agent could skip them. To turn the guideline into a guardrail:
|
|
75
73
|
|
|
76
74
|
```bash
|
|
77
75
|
leerness ci init # writes .github/workflows/leerness-gate.yml — runs `leerness gate` on every PR
|
|
@@ -81,23 +79,19 @@ The generated workflow is production-grade: it **pins the leerness version** (re
|
|
|
81
79
|
|
|
82
80
|
Then make that check **required** in GitHub branch protection. Now a PR that skips verification (or whose claims fail) **cannot merge** — the gate runs independently of the agent, returns a non-zero exit code, and blocks. That is the difference between a guideline and a guardrail. For exact per-claim enforcement, run `leerness gate --claims` — it adds a 6th check that runs `verify-claim` on **every** completed task and fails the gate if any "done" task's evidence doesn't match reality (the default 5-check gate already blocks false-done via heuristics; `--claims` makes it precise).
|
|
83
81
|
|
|
84
|
-
For secrets, pair the gate with a **dedicated scanner** in the same workflow
|
|
82
|
+
For secrets, pair the gate with a **dedicated scanner** in the same workflow. `scan secrets` gives your agent the same signal locally; a dedicated scanner is the hard-guarantee layer:
|
|
85
83
|
|
|
86
84
|
```yaml
|
|
87
85
|
# add to .github/workflows/leerness-gate.yml (or a separate job)
|
|
88
86
|
- uses: gitleaks/gitleaks-action@v2 # dedicated scanner — the hard-guarantee layer
|
|
89
|
-
- run: npx leerness@<pinned-version> scan secrets . --json #
|
|
87
|
+
- run: npx leerness@<pinned-version> scan secrets . --json # same check your agent runs locally
|
|
90
88
|
```
|
|
91
89
|
|
|
92
90
|
---
|
|
93
91
|
|
|
94
|
-
##
|
|
95
|
-
|
|
96
|
-
Be honest with yourself before you depend on this: leerness is **early and largely solo-maintained**, developed mostly through autonomous AI rounds — so its own `selftest` + e2e suites are the primary quality signal, and external adoption is still small. Don't make it load-bearing on faith: **pin a version**, and treat the differentiated slice — `verify-claim` + the CI `gate` as a required check — as the part worth relying on.
|
|
97
|
-
|
|
98
|
-
(Contributor note — three test tiers, fastest to slowest: `npm run test:fast` = selftest + smoke (commands run without crashing, <1 min, dev loop); `npm run test:core` = selftest + a flagship behavioral suite (verify-claim / gate / contract / scan actually reject bad input and pass honest input, ~20 s, pre-commit / quick CI); `npm test` = selftest + the entire e2e suite (**10+ minutes by design**, the release gate).)
|
|
92
|
+
## Low-risk by design
|
|
99
93
|
|
|
100
|
-
|
|
94
|
+
**MIT · 0 runtime dependencies · offline-first**, and all state is plain files in *your* repo. Lock-in is near zero — if it doesn't earn its place, remove the tool and your `task` / `decision` / `lesson` files stay exactly where they are. **Pin a version** in CI so the gate's verdict can't change from a silent upgrade.
|
|
101
95
|
|
|
102
96
|
---
|
|
103
97
|
|
|
@@ -117,6 +111,8 @@ Full command reference, workflows, and architecture: **[README.ko.md](./README.k
|
|
|
117
111
|
- npm: https://www.npmjs.com/package/leerness
|
|
118
112
|
- Site & release videos: https://leerness.pages.dev
|
|
119
113
|
- Changelog: [CHANGELOG.md](./CHANGELOG.md)
|
|
114
|
+
- How this gets tested — methodology, results, and limitations: [docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md)
|
|
115
|
+
- Contributing & test tiers (`test:fast` / `test:core` / `npm test`): [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
120
116
|
|
|
121
117
|
## License
|
|
122
118
|
|
|
@@ -125,7 +121,7 @@ MIT
|
|
|
125
121
|
<!-- leerness:project-readme:start -->
|
|
126
122
|
## Leerness Project Harness
|
|
127
123
|
|
|
128
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.22 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
129
125
|
|
|
130
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
131
127
|
|
|
@@ -179,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
179
175
|
|
|
180
176
|
### MCP server (외부 AI 통합)
|
|
181
177
|
|
|
182
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.22는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
183
179
|
|
|
184
180
|
```jsonc
|
|
185
181
|
// 카테고리별
|
|
@@ -200,7 +196,7 @@ Leerness v1.36.20는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
200
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
201
197
|
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) 다음 라운드 예약.
|
|
202
198
|
|
|
203
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
199
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.22)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
204
200
|
|
|
205
201
|
### 성능 가이드 (1.9.140 측정)
|
|
206
202
|
|
|
@@ -238,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
238
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
239
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
240
236
|
|
|
241
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.22: 2026-07-15
|
|
242
238
|
<!-- leerness:project-readme:end -->
|
|
243
239
|
|
package/bin/leerness.js
CHANGED
|
@@ -33,7 +33,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
35
35
|
|
|
36
|
-
const VERSION = '1.36.
|
|
36
|
+
const VERSION = '1.36.22';
|
|
37
37
|
|
|
38
38
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
39
39
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -1923,13 +1923,19 @@ function _analyzeWakeupStatus(root) {
|
|
|
1923
1923
|
// 레코드 구조: { id, text, recordedAt, status: 'open'|'in-progress'|'completed'|'dropped',
|
|
1924
1924
|
// linkedTaskIds?, linkedPlanIds?, linkedDecisionIds?, notes?, completedAt?, droppedAt? }
|
|
1925
1925
|
function _userRequestsPath(root) { return path.join(root, '.harness', 'user-requests.json'); }
|
|
1926
|
+
// 1.36.21 (전수 sweep P2, 데이터손실): 종전엔 wrapper 가 아닌 "유효 JSON" 을 조용히 {requests:[]} 로 강제하고, 이후 mutation 이
|
|
1927
|
+
// 파일 전체를 새 배열로 덮어써 원본을 파괴했다(읽지 못한 데이터를 지우는 read-modify-write). 이제 (1) bare array 는 관대 수용해
|
|
1928
|
+
// 다음 쓰기에 자기치유(decisions/lessons 관례와 동일), (2) 그 외 미인식 형태/파싱 실패는 unrecognized 로 표시해 쓰기 경로가 bail.
|
|
1929
|
+
// 읽기 전용 소비자들은 .requests 만 구조분해하므로 하위호환.
|
|
1926
1930
|
function _loadUserRequests(root) {
|
|
1927
1931
|
try {
|
|
1928
1932
|
const fp = _userRequestsPath(root);
|
|
1929
|
-
if (!exists(fp)) return { requests: [] };
|
|
1933
|
+
if (!exists(fp)) return { requests: [] }; // 미존재 = 신규 시작(쓰기 허용)
|
|
1930
1934
|
const j = JSON.parse(read(fp));
|
|
1931
|
-
|
|
1932
|
-
|
|
1935
|
+
if (j && Array.isArray(j.requests)) return { requests: j.requests }; // canonical wrapper
|
|
1936
|
+
if (Array.isArray(j)) return { requests: j }; // bare array → 관대 수용(다음 쓰기에 wrapper 로 치유)
|
|
1937
|
+
return { requests: [], unrecognized: true }; // 미인식 형태 → 파괴적 쓰기 차단
|
|
1938
|
+
} catch { return { requests: [], unrecognized: true }; } // 파싱 실패 → 차단(덮어쓰기 방지)
|
|
1933
1939
|
}
|
|
1934
1940
|
function _writeUserRequests(root, requests) {
|
|
1935
1941
|
try {
|
|
@@ -1941,11 +1947,14 @@ function _writeUserRequests(root, requests) {
|
|
|
1941
1947
|
function _recordUserRequest(root, text, opts = {}) {
|
|
1942
1948
|
if (!text || typeof text !== 'string') return null;
|
|
1943
1949
|
const state = _loadUserRequests(root);
|
|
1950
|
+
if (state.unrecognized) { warn(`user-requests.json 형태 인식 불가 — 기록 건너뜀(원본 덮어쓰기 방지). 파일 확인: ${_userRequestsPath(root)}`); return null; } // 1.36.21: 읽지 못한 데이터 파괴 차단
|
|
1944
1951
|
// 중복 방지: 동일 텍스트 + open 상태 이미 존재 시 skip
|
|
1945
1952
|
const norm = text.trim().slice(0, 500);
|
|
1946
|
-
const dup = state.requests.find(r => r.text === norm && (r.status === 'open' || r.status === 'in-progress'));
|
|
1953
|
+
const dup = state.requests.find(r => r && r.text === norm && (r.status === 'open' || r.status === 'in-progress'));
|
|
1947
1954
|
if (dup) return dup;
|
|
1948
|
-
|
|
1955
|
+
// 1.36.21 (전수 sweep 부수발견): id 를 length+1 로 만들면 200개 초과 truncate(slice(-200)) 후 기존 id 와 충돌 → 최대 UR 번호 +1 로 파생.
|
|
1956
|
+
const _maxUr = state.requests.reduce((mx, r) => { const m = /^UR-(\d{4,})$/.exec(String((r && r.id) || '')); return m ? Math.max(mx, parseInt(m[1], 10)) : mx; }, 0);
|
|
1957
|
+
const id = 'UR-' + String(_maxUr + 1).padStart(4, '0');
|
|
1949
1958
|
const entry = {
|
|
1950
1959
|
id, text: norm, recordedAt: new Date().toISOString(),
|
|
1951
1960
|
status: opts.status || 'open',
|
|
@@ -1962,7 +1971,8 @@ function _recordUserRequest(root, text, opts = {}) {
|
|
|
1962
1971
|
}
|
|
1963
1972
|
function _updateUserRequest(root, id, patch) {
|
|
1964
1973
|
const state = _loadUserRequests(root);
|
|
1965
|
-
|
|
1974
|
+
if (state.unrecognized) { warn(`user-requests.json 형태 인식 불가 — 업데이트 건너뜀(원본 덮어쓰기 방지). 파일 확인: ${_userRequestsPath(root)}`); return null; } // 1.36.21
|
|
1975
|
+
const idx = state.requests.findIndex(r => r && r.id === id);
|
|
1966
1976
|
if (idx < 0) return null;
|
|
1967
1977
|
state.requests[idx] = { ...state.requests[idx], ...patch, updatedAt: new Date().toISOString() };
|
|
1968
1978
|
if (patch.status === 'completed' && !state.requests[idx].completedAt) state.requests[idx].completedAt = new Date().toISOString();
|
|
@@ -2972,6 +2982,42 @@ function _selfTestCases() {
|
|
|
2972
2982
|
const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
|
|
2973
2983
|
return reuseOk && releaseOk && encOk;
|
|
2974
2984
|
} },
|
|
2985
|
+
{ name: 'SessionStart 컨텍스트 주입 (1.36.22, obra/superpowers 구조 검토): hook 전용 표면 + 쓰기0 가드 + matcher 에 compact 포함 + opt-out — 소스가드', run: () => {
|
|
2986
|
+
const s = read(__filename);
|
|
2987
|
+
const surface = typeof hookSessionStartCmd === 'function' && s.includes("cmd === 'hook' && args[1] === 'session-start'");
|
|
2988
|
+
// ★ 핵심 불변식: hook 은 handoff 를 부르지 않는다(부르면 last-handoff stamp 로 detector 3종 오염) + record 는 가드 뒤에만
|
|
2989
|
+
const zeroWrite = s.includes("if (!has('--no-record') && process.env.LEERNESS_HOOK !== '1') { try { _recordLastHandoff(root); } catch {} }");
|
|
2990
|
+
// split-literal: 앵커를 통짜 문자열로 두면 이 selftest 줄 자신이 먼저 매칭돼 43자짜리 자기 슬라이스를 잡는다(자기참조 트랩, UR-0009 계열).
|
|
2991
|
+
const hookBody = s.slice(s.indexOf('function ' + 'hookSessionStartCmd'), s.indexOf('function ' + 'handoffCmd'));
|
|
2992
|
+
const noHandoffCall = hookBody.length > 100 && !/handoffCmd\(|_recordLastHandoff\(|writeUtf8\(|mkdirp\(/.test(hookBody); // 쓰기/handoff 호출 0
|
|
2993
|
+
const jsonContract = hookBody.includes("hookEventName: 'SessionStart'") && hookBody.includes('additionalContext');
|
|
2994
|
+
const optOut = hookBody.includes("has('--no-context-inject')") && hookBody.includes("LEERNESS_NO_CONTEXT_INJECT");
|
|
2995
|
+
const selfLabel = hookBody.includes('위는 요약'); // 1.9.272 투명성: 요약임을 자기 라벨링
|
|
2996
|
+
const matcherCompact = s.includes("{ matcher: 'startup|clear|compact', command: 'leerness hook session-start' }"); // 실측 결함(compaction) 커버
|
|
2997
|
+
const idempotent = s.includes("some(h => h.command && h.command.includes('leerness hook session-start'))");
|
|
2998
|
+
return surface && zeroWrite && noHandoffCall && jsonContract && optOut && selfLabel && matcherCompact && idempotent;
|
|
2999
|
+
} },
|
|
3000
|
+
{ name: '경로/EOL/형태 견고성 4종 (1.36.21, 전수 sweep P2): handoff·env detect 없는경로 가드 + CRLF plan tasks + migrate 오타설치 차단 + user-requests 미인식 쓰기차단 — 소스가드 + 순수 행위', run: () => {
|
|
3001
|
+
const s = read(__filename);
|
|
3002
|
+
// B: 없는 경로에 트리/.harness 를 만들고 빈 프로젝트로 보고하던 것 차단 (UR-0136 관례 정렬)
|
|
3003
|
+
const handoffGuard = s.includes("if (!exists(_hr) || !fs.statSync(_hr).isDirectory()) { failJson(has('--json'), 'path_not_found'");
|
|
3004
|
+
const envGuard = s.includes("if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(has('--json') || opts.json, 'path_not_found'");
|
|
3005
|
+
// A: CRLF plan → tasks 증발 (읽는 즉시 정규화)
|
|
3006
|
+
const planNorm = s.includes("const text = read(pp).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');");
|
|
3007
|
+
// C: migrate 오타/추측 하위명령이 그 토큰을 경로로 삼아 전체 설치하던 것 차단
|
|
3008
|
+
const migrateGuard = s.includes("failJson(has('--json'), 'unknown_subcommand'") && s.includes('_msub');
|
|
3009
|
+
// D: user-requests 미인식 형태를 조용히 비우고 덮어쓰던 데이터손실 차단 + id 충돌
|
|
3010
|
+
const urTolerant = s.includes('if (Array.isArray(j)) return { requests: j };') && s.includes('return { requests: [], unrecognized: true };');
|
|
3011
|
+
const urWriteGuard = (s.match(/state\.unrecognized/g) || []).length >= 2 && s.includes('ur.unrecognized');
|
|
3012
|
+
const urMaxId = s.includes("/^UR-(\\d{4,})$/.exec");
|
|
3013
|
+
// 순수 행위(실 흐름 복제: 정규화 → split('\n') → 줄별 매치). \r 가 남으면 앵커 $ 가 실패해 태스크 0건, 정규화하면 2건.
|
|
3014
|
+
const crlfBlock = '- [ ] one\r\n- [x] two\r\n';
|
|
3015
|
+
const hit = ls => ls.filter(l => /^-\s*\[([\sx])\]\s*(.+)$/.test(l)).length;
|
|
3016
|
+
const rawHit = hit(crlfBlock.split('\n')); // 종전 동작: 0 (후행 \r)
|
|
3017
|
+
const normHit = hit(crlfBlock.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')); // 수정 후: 2
|
|
3018
|
+
const crlfBehavior = rawHit === 0 && normHit === 2;
|
|
3019
|
+
return handoffGuard && envGuard && planNorm && migrateGuard && urTolerant && urWriteGuard && urMaxId && crlfBehavior;
|
|
3020
|
+
} },
|
|
2975
3021
|
{ name: 'ID 리더 5자리+ 대응 (1.36.18, UR-0052 P1-2): \\d{4}→\\d{4,} — 10k+ ID(T-10000) truncation 방지, 날짜 연도는 exactly-4 보존 — 행위검사', run: () => {
|
|
2976
3022
|
// 행위: 5자리 ID 전체 캡처(구 \d{4}는 앞 4자리만 → truncation/충돌), 4자리 ID 무회귀, 날짜 연도 exactly-4 유지.
|
|
2977
3023
|
const taskRe = /\bT-(\d{4,})\b/;
|
|
@@ -3102,7 +3148,7 @@ function _selfTestCases() {
|
|
|
3102
3148
|
{ name: 'UR-0068(외부리뷰 P2): _roadmapParseMilestones 블록 경계 — 다음 milestone status 누출 차단', run: () => { const m = require('../lib/pure-utils'); const r = m._roadmapParseMilestones('### M-0001. A\n\n### M-0002. B\nStatus: done\nProgress: 80%\n'); return r.length === 2 && r[0].status === 'planned' && r[0].progress === 0 && r[1].status === 'done' && r[1].progress === 80; } },
|
|
3103
3149
|
{ name: 'UR-0066(외부리뷰 P2): shell:true 주입 가드 — agents bench task _shellQuoteArg + fetchNpmLatest cmd.exe args', run: () => { const m = require('../lib/pure-utils'); const src = read(__filename); const agentsSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'agents.js')); const benchQuoted = agentsSrc.includes('const qTask = ' + '_shellQuoteArg(task)'); const npmSafe = /'\/d', '\/s', '\/c', 'npm', 'view'/.test(src); const q = m._shellQuoteArg('a & b'); const safe = (process.platform === 'win32' ? q === '"a & b"' : q === "'a & b'"); return benchQuoted && npmSafe && safe; } },
|
|
3104
3150
|
{ name: 'UR-0072(외부리뷰 P3): compareVer pre-release + _classifyCJK 한자 kana 귀속', run: () => { const m = require('../lib/pure-utils'); const verOk = m.compareVer('1.9.0-beta', '1.9.0') === -1 && m.compareVer('1.9.0', '1.9.0-beta') === 1 && m.compareVer('1.9.5', '1.9.5') === 0 && m.compareVer('1.9.6', '1.9.5') === 1; const jp = Buffer.from([0xE3, 0x81, 0x82, 0xE6, 0x97, 0xA5, 0xE6, 0x9C, 0xAC]); const cn = Buffer.from([0xE4, 0xB8, 0xAD, 0xE5, 0x9B, 0xBD]); const rj = m._classifyCJK(jp, jp.length); const rc = m._classifyCJK(cn, cn.length); const cjkOk = rj.japanese > rj.chinese && rc.chinese > 0 && rc.japanese === 0; return verOk && cjkOk; } },
|
|
3105
|
-
{ name: 'UR-0075 Phase A: 마이그레이션 가이드(_migrationGuideText) + migrate --guide 와이어 + init/migrate/update --path', run: () => { const m = require('../lib/pure-utils'); const g = m._migrationGuideText('1.9.355'); const guideOk = typeof g === 'string' && g.includes('마이그레이션 가이드') && g.includes('update --check --path') && g.includes('selftest') && g.includes('canonical JSON') && g.includes('롤백') && g.includes('1.9.355'); const src = read(__filename); const wired = src.includes("has('--guide') || args[1] === " + "'guide'") && src.includes('install(arg(' + "'--path',
|
|
3151
|
+
{ name: 'UR-0075 Phase A: 마이그레이션 가이드(_migrationGuideText) + migrate --guide 와이어 + init/migrate/update --path', run: () => { const m = require('../lib/pure-utils'); const g = m._migrationGuideText('1.9.355'); const guideOk = typeof g === 'string' && g.includes('마이그레이션 가이드') && g.includes('update --check --path') && g.includes('selftest') && g.includes('canonical JSON') && g.includes('롤백') && g.includes('1.9.355'); const src = read(__filename); const wired = src.includes("has('--guide') || args[1] === " + "'guide'") && src.includes('install(arg(' + "'--path', _msub || process.cwd())") && src.includes('updateCmd(arg(' + "'--path', args[1] || process.cwd())"); return guideOk && wired; } }, // 1.36.21: migrate 는 존재가드 도입으로 args[1]→_msub (--path 와이어는 동일)
|
|
3106
3152
|
{ name: 'UR-0075 Phase B: migrate audit(dry-run 스키마 drift) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migrateAuditCmd === 'function' && src.includes('migrateAuditCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'audit'"); } },
|
|
3107
3153
|
{ name: 'UR-0075 Phase C: migrate apply(canonical 백필 비파괴 적용) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migrateApplyCmd === 'function' && src.includes('migrateApplyCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'apply'"); } },
|
|
3108
3154
|
{ name: 'UR-0075 Phase D: migrate plan(임시폴더 설치 후 비교) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migratePlanCmd === 'function' && src.includes('migratePlanCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'plan'"); } },
|
|
@@ -6969,10 +7015,11 @@ function _autoFixIdempotency(root) {
|
|
|
6969
7015
|
// 2) user-requests.json open 중복 — 동일 텍스트 중 최초만 open 유지, 나머지 dropped(보존)
|
|
6970
7016
|
try {
|
|
6971
7017
|
const ur = _loadUserRequests(root);
|
|
7018
|
+
if (ur.unrecognized) throw new Error('user-requests.json 형태 인식 불가 — dedup 건너뜀(원본 덮어쓰기 방지)'); // 1.36.21: 3번째 파괴적 caller 도 차단
|
|
6972
7019
|
const seen = new Map();
|
|
6973
7020
|
let droppedReq = 0;
|
|
6974
7021
|
for (const r of ur.requests) {
|
|
6975
|
-
if (r.status !== 'open' && r.status !== 'in-progress') continue;
|
|
7022
|
+
if (!r || (r.status !== 'open' && r.status !== 'in-progress')) continue;
|
|
6976
7023
|
const k = (r.text || '').trim();
|
|
6977
7024
|
if (seen.has(k)) { r.status = 'dropped'; r.droppedReason = `auto-dedup-1.9.293 (== ${seen.get(k)})`; droppedReq++; }
|
|
6978
7025
|
else seen.set(k, r.id);
|
|
@@ -7674,7 +7721,12 @@ function planListCmd(root, opts = {}) {
|
|
|
7674
7721
|
}
|
|
7675
7722
|
return ok('plan.md 없음 — leerness plan add "<text>" 로 첫 milestone 등록');
|
|
7676
7723
|
}
|
|
7677
|
-
|
|
7724
|
+
// 1.36.21 (codex #8a, 전수 sweep 3 에이전트 수렴): CRLF plan.md 에서 모든 마일스톤의 tasks 가 조용히 [] 로 증발하던 것 수정.
|
|
7725
|
+
// 체크박스 정규식(/^-\s*\[([\sx])\]\s*(.+)$/)이 /m 없이 후행 \r 앞에서 $ 를 못 잡아 실패 — id/title/status/progress 는 /m 이라 정상 파싱되어
|
|
7726
|
+
// 부분·무증상 실패였음(plan list 의 Tasks 줄 소실, MCP leerness_plan_list 가 tasks 0건 보고 → 계약 위반). leerness 는 체크박스를 켜는 명령이
|
|
7727
|
+
// 없어 tasks 사용엔 외부 편집이 필수인데, Windows 관용 편집(PowerShell Set-Content 등)이 기본 CRLF 라 현실 경로. 코드베이스 기존 관례(:4643)대로
|
|
7728
|
+
// 읽는 즉시 정규화 — 혼합 EOL(기존 CRLF + plan add 의 LF append)도 함께 복구되고 이 함수의 향후 정규식 재발도 차단.
|
|
7729
|
+
const text = read(pp).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
7678
7730
|
const milestones = [];
|
|
7679
7731
|
// ### M-XXXX. <title> 블록 추출
|
|
7680
7732
|
const blocks = text.split(/\n(?=### M-\d{4,}\.)/);
|
|
@@ -9038,7 +9090,9 @@ function handoff(root) {
|
|
|
9038
9090
|
// detector 가 prior gap 을 정확히 측정 (overwrite 전 값 필요). 함수 최상단에 선언.
|
|
9039
9091
|
let _priorHandoffGap = { hasLast: false };
|
|
9040
9092
|
try { _priorHandoffGap = _getLastHandoffGap(root); } catch {}
|
|
9041
|
-
|
|
9093
|
+
// 1.36.22: 읽기전용/hook 호출이 last-handoff stamp 를 찍으면 detector 3종(핸드오프 넛지·abnormal shutdown·R-0001 interval)이
|
|
9094
|
+
// "방금 handoff 했다"고 오판해 오염된다. --no-record / LEERNESS_HOOK=1 로 stamp 생략(기본 동작 불변).
|
|
9095
|
+
if (!has('--no-record') && process.env.LEERNESS_HOOK !== '1') { try { _recordLastHandoff(root); } catch {} }
|
|
9042
9096
|
// 1.9.203: auto-resume-plan 자동 로드 (사용자 명시 — 자동 모드 알람 트리거)
|
|
9043
9097
|
let _autoResumePlan = null;
|
|
9044
9098
|
try { _autoResumePlan = _loadAutoResumePlan(root); } catch {}
|
|
@@ -10664,7 +10718,42 @@ function _handoffWorkspace(rootBase) {
|
|
|
10664
10718
|
log(` - 새 패턴 추가 시 \`leerness reuse-map --all-apps\`로 중복 감지${sinceCutoff ? '' : ' / `--since 24h`로 최근 변경 추적'}`);
|
|
10665
10719
|
}
|
|
10666
10720
|
|
|
10721
|
+
// 1.36.22 (obra/superpowers 구조 검토 → 배선): SessionStart 컨텍스트 주입 전용 표면.
|
|
10722
|
+
// 문제(실측): compaction/resume 후 에이전트가 "세션 시작 handoff" 의례를 건너뛰어 컨텍스트를 잃는다.
|
|
10723
|
+
// superpowers 는 hook matcher 를 startup|clear|compact 로 걸어 하네스가 컨텍스트를 강제 적재한다(에이전트 자발성에 안 기댐).
|
|
10724
|
+
// leerness 는 hook 플럼빙(autoUpdateInstall)과 compact 신호(handoff --compact)를 이미 둘 다 갖고 있었고 배선만 없었음 → 그 배선.
|
|
10725
|
+
// ★ 여기서 handoff 를 호출하지 않는다: handoff 는 last-handoff stamp 를 찍어 detector 3종을 오염시킴. 동일 파일 로더만 재사용
|
|
10726
|
+
// (subprocess 0 · 쓰기 0 · last-handoff.json 미접촉). 어떤 예외에서도 stdout 무출력 + exit 0 — 세션 시작을 절대 깨지 않는다.
|
|
10727
|
+
function hookSessionStartCmd(root) {
|
|
10728
|
+
if (has('--no-context-inject') || process.env.LEERNESS_NO_CONTEXT_INJECT === '1') return; // opt-out → 무출력 exit 0
|
|
10729
|
+
try {
|
|
10730
|
+
root = absRoot(root || process.cwd());
|
|
10731
|
+
if (!exists(root) || !exists(path.join(root, '.harness'))) return; // 미초기화/없는 경로 → 조용히 무출력(hook 은 어디서나 실행됨)
|
|
10732
|
+
const rows = readProgressRows(root);
|
|
10733
|
+
const byStatus = {};
|
|
10734
|
+
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
10735
|
+
const done = byStatus['done'] || 0, wip = byStatus['in-progress'] || 0, blocked = byStatus['blocked'] || 0;
|
|
10736
|
+
const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
|
|
10737
|
+
const openReq = (_loadUserRequests(root).requests || []).filter(r => r && (r.status === 'open' || r.status === 'in-progress')).length;
|
|
10738
|
+
const nx = rows.find(r => r.status === 'in-progress') || rows.find(r => r.status === 'planned') || null;
|
|
10739
|
+
const flags = [];
|
|
10740
|
+
if (openReq) flags.push(`📥 미답 요청 ${openReq}`);
|
|
10741
|
+
if (blocked) flags.push(`🚫 blocked ${blocked}`);
|
|
10742
|
+
const lines = [_lineSafe(`[leerness] ${detectProjectName(root)} · ${done}/${rows.length}(${pct}%) done · WIP ${wip}${flags.length ? ' · ' + flags.join(' · ') : ''}`)];
|
|
10743
|
+
if (nx) lines.push(_lineSafe(`다음: ${nx.id} [${nx.status}] ${nx.nextAction || nx.request || ''}`).slice(0, 200));
|
|
10744
|
+
// 자기 라벨링(1.9.272 투명성 불변식): 요약임을 밝히고 전체 컨텍스트 경로와 끄는 법을 함께 준다.
|
|
10745
|
+
lines.push('⚠ 위는 요약 — 전체 컨텍스트는 `leerness handoff .` 실행. (이 주입 끄기: LEERNESS_NO_CONTEXT_INJECT=1)');
|
|
10746
|
+
// Claude Code SessionStart hook 규약. JSON.stringify 가 이스케이프 담당(수동 백슬래시/따옴표 처리 불필요).
|
|
10747
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: lines.join('\n') } }) + '\n');
|
|
10748
|
+
} catch { /* 세션 시작을 절대 깨지 않는다 — 무출력 exit 0 */ }
|
|
10749
|
+
}
|
|
10667
10750
|
function handoffCmd(root) {
|
|
10751
|
+
// 1.36.21 (전수 sweep P2/high): 존재하지 않는 경로에 디렉토리 트리 + .harness 를 "생성"하고 깨끗한 빈 프로젝트로 보고하던 것 차단.
|
|
10752
|
+
// 오타 경로에 handoff 하면 쓰레기 트리가 생기고, --json 은 ok/error 없는 정상 JSON + exit 0 이라 기계 소비자가 오타를 감지 못 했음.
|
|
10753
|
+
// status/health/drift/lazy detect/scan secrets/session close 는 이미 path_not_found + exit 1(UR-0136) — handoff/env detect 만 누락된 관례 갭.
|
|
10754
|
+
// --pulse/--all-apps 분기보다 "앞"에 둬야 전 모드가 커버됨.
|
|
10755
|
+
const _hr = absRoot(root);
|
|
10756
|
+
if (!exists(_hr) || !fs.statSync(_hr).isDirectory()) { failJson(has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${_hr}`); return; }
|
|
10668
10757
|
// 1.9.232: --pulse 옵션 — pulse 1 line 형식으로 출력 (handoff 전체 대신)
|
|
10669
10758
|
if (has('--pulse')) {
|
|
10670
10759
|
return pulseCmd(absRoot(root));
|
|
@@ -15969,6 +16058,11 @@ function autoUpdateInstall(root) {
|
|
|
15969
16058
|
if (!settings.hooks.SessionStart.some(h => h.command && h.command.includes('leerness update'))) {
|
|
15970
16059
|
settings.hooks.SessionStart.push({ matcher: '*', command: 'leerness update --check --quiet' });
|
|
15971
16060
|
}
|
|
16061
|
+
// 1.36.22: 2번째 SessionStart hook — 컨텍스트 주입. matcher 에 compact 를 포함하는 게 핵심(실측 결함: compaction/resume 후
|
|
16062
|
+
// 에이전트가 세션시작 handoff 의례를 건너뜀). 기존 설치는 이 멱등 push 로 자동 취득(1.9.364 업그레이드 패턴 재사용).
|
|
16063
|
+
if (!settings.hooks.SessionStart.some(h => h.command && h.command.includes('leerness hook session-start'))) {
|
|
16064
|
+
settings.hooks.SessionStart.push({ matcher: 'startup|clear|compact', command: 'leerness hook session-start' });
|
|
16065
|
+
}
|
|
15972
16066
|
writeUtf8(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
15973
16067
|
writeUtf8(path.join(root, '.claude/commands/update.md'),
|
|
15974
16068
|
`# /update\n\nleerness 자동 업데이트 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n\n체크만:\n\n\`\`\`\n!leerness update --check\n\`\`\`\n`);
|
|
@@ -15979,6 +16073,8 @@ function autoUpdateInstall(root) {
|
|
|
15979
16073
|
// 1.9.272 (GPT-5.5 외부 리뷰 반영): hook 설치 투명성 — 무엇을/왜/어떻게 끄는지 명시.
|
|
15980
16074
|
// 1.9.364 (4번째 외부평가 9.3): 비침투 — 세션 시작 시 --quiet 로 up-to-date 면 아무 출력 없음, 업데이트 가능 시에만 1줄.
|
|
15981
16075
|
log(` ⓘ 이 hook 은 Claude Code 세션 시작 시 \`leerness update --check --quiet\` 를 1회 실행합니다 (최신이면 무음, 업데이트 가능 시에만 1줄 통지 · 자동 설치는 안 함).`);
|
|
16076
|
+
// 1.36.22: 2번째 hook 투명성 — 무엇을(요약 주입)/언제(startup·clear·compact)/왜(compaction 후 컨텍스트 유실)/어떻게 끄는지.
|
|
16077
|
+
log(` ⓘ 2번째 hook \`leerness hook session-start\` 는 세션 시작·clear·compaction 시 진행상황 요약 2~3줄을 컨텍스트에 주입합니다 (쓰기 0 · 미초기화 프로젝트면 무음 · 끄기: LEERNESS_NO_CONTEXT_INJECT=1).`);
|
|
15982
16078
|
log(` ⓘ 제거: ${rel(root, settingsFile)} 의 hooks.SessionStart 항목 삭제 · 설치 시 끄기: leerness init . --no-auto-update`);
|
|
15983
16079
|
}
|
|
15984
16080
|
|
|
@@ -17393,6 +17489,8 @@ function _diffEnvSnapshots(prev, curr) {
|
|
|
17393
17489
|
}
|
|
17394
17490
|
function envDetectCmd(root, opts = {}) {
|
|
17395
17491
|
root = absRoot(root || process.cwd());
|
|
17492
|
+
// 1.36.21 (전수 sweep P2/high): handoff 와 동일 갭 — 없는 경로에 .harness/environment.json 을 만들며 exit 0. UR-0136 관례로 정렬.
|
|
17493
|
+
if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(has('--json') || opts.json, 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
|
|
17396
17494
|
const jsonMode = has('--json') || opts.json;
|
|
17397
17495
|
const writeMode = !has('--no-write'); // default: 캡처 후 .harness/environment.json 에 저장
|
|
17398
17496
|
const snap = _detectEnvironment(root);
|
|
@@ -21030,7 +21128,17 @@ async function main() {
|
|
|
21030
21128
|
if (cmd === 'migrate' && args[1] === 'apply') return migrateApplyCmd(arg('--path', args[2] && !args[2].startsWith('-') ? args[2] : process.cwd()), { json: has('--json'), yes: has('--yes') }); // 1.9.357 (UR-0075 Phase C): canonical 백필 비파괴 적용 (기본 dry-run)
|
|
21031
21129
|
if (cmd === 'migrate' && args[1] === 'plan') return migratePlanCmd(arg('--path', args[2] && !args[2].startsWith('-') ? args[2] : process.cwd()), { json: has('--json') }); // 1.9.358 (UR-0075 Phase D): 임시폴더 설치 후 비교 마이그레이션 플랜 (읽기 전용)
|
|
21032
21130
|
if (cmd === 'install-safety') return installSafetyCmd({ json: has('--json') }); // 1.9.359 (UR-0074): 설치 안전 프로필 (0 deps / 0 install-script) + 안전 설치 워크플로
|
|
21033
|
-
|
|
21131
|
+
// 1.36.21 (전수 sweep P2): 오타/추측 하위명령(`migrate aduit`, `migrate check`)이 그 토큰을 "경로"로 삼아 새 디렉토리를 만들고
|
|
21132
|
+
// 전체 설치를 수행하던 것 차단(--json 무시 + exit 0 이라 조용했음). migrate 의 전제는 "기존 프로젝트" — 새 트리 생성은 init 의 일.
|
|
21133
|
+
// 존재 기반 가드라 정당 흐름(migrate . / migrate /existing / migrate --path X / migrate)은 전부 보존.
|
|
21134
|
+
if (cmd === 'migrate') {
|
|
21135
|
+
const _msub = args[1];
|
|
21136
|
+
if (_msub && !_msub.startsWith('-') && !has('--path') && !(exists(_msub) && fs.statSync(_msub).isDirectory())) {
|
|
21137
|
+
failJson(has('--json'), 'unknown_subcommand', `알 수 없는 migrate 하위명령/경로: ${_msub} — 사용법: leerness migrate [기존경로] | migrate audit|apply|plan [경로] | migrate --guide (새 프로젝트는 leerness init)`);
|
|
21138
|
+
return;
|
|
21139
|
+
}
|
|
21140
|
+
return await install(arg('--path', _msub || process.cwd()), { force:has('--force'), dry:has('--dry-run'), migration:true }); // 1.9.355 (UR-0075): --path 지원
|
|
21141
|
+
}
|
|
21034
21142
|
if (cmd === 'update') return await updateCmd(arg('--path', args[1] || process.cwd()), { checkOnly: has('--check'), yes: has('--yes'), force: has('--force') }); // 1.9.355 (UR-0075): --path 지원
|
|
21035
21143
|
if (cmd === 'auto-update' && args[1] === 'install') return autoUpdateInstall(arg('--path', args[2] || process.cwd()));
|
|
21036
21144
|
if (cmd === 'status') return status(arg('--path', args[1] || process.cwd()));
|
|
@@ -21042,6 +21150,7 @@ async function main() {
|
|
|
21042
21150
|
if (cmd === 'encoding' && args[1] === 'check') return encodingCheck(arg('--path', args[2] || process.cwd()));
|
|
21043
21151
|
if (cmd === 'lazy' && args[1] === 'detect') return lazyDetect(_resolveRoot(args[2]), { json: has('--json') });
|
|
21044
21152
|
if (cmd === 'memory' && args[1] === 'search') return memorySearch(arg('--path', process.cwd()), args.slice(2).join(' '));
|
|
21153
|
+
if (cmd === 'hook' && args[1] === 'session-start') return hookSessionStartCmd(arg('--path', args[2] || process.cwd())); // 1.36.22: SessionStart 컨텍스트 주입(쓰기 0)
|
|
21045
21154
|
if (cmd === 'handoff') { const _hp = arg('--path', args[1] || process.cwd()); const _hr = handoffCmd(_hp); _maybeAutoGraph(_hp); return _hr; }
|
|
21046
21155
|
if (cmd === 'reuse-map') return reuseMapCmd(arg('--path', args[1] || process.cwd()));
|
|
21047
21156
|
if (cmd === 'verify-claim') { const _p = arg('--path', process.cwd()); if (args[1] === '--all' || has('--all')) return verifyClaimAllCmd(_p); return verifyClaimCmd(_p, args[1]); } // 1.33.2: --all → 모든 done 주장 일괄 검증
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.22",
|
|
4
4
|
"description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"docs",
|
|
44
44
|
"README.md",
|
|
45
45
|
"CHANGELOG.md",
|
|
46
|
+
"CONTRIBUTING.md",
|
|
46
47
|
"SECURITY.md",
|
|
47
48
|
"LICENSE"
|
|
48
49
|
],
|