leerness 1.36.183 → 1.36.184
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 +4 -0
- package/LICENSE +21 -21
- package/README.md +4 -4
- package/SECURITY.md +66 -66
- package/bin/leerness.js +97 -28
- package/lib/diagnostics.js +184 -184
- package/lib/health.js +350 -350
- package/lib/mcp-tools.js +104 -104
- package/lib/referee.js +270 -270
- package/lib/review-request.js +339 -339
- package/package.json +1 -1
- package/scripts/e2e.js +27 -8
- package/scripts/i18n-next-cluster-probe.js +436 -5
- package/scripts/lint.js +73 -73
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.184 — 2026-09-01
|
|
4
|
+
|
|
5
|
+
- T-0164: English-mode mode output now emits zero Hangul across stored, environment, and explicit locale paths, including pre-dispatch and stale-version diagnostics, while preserving Korean and locale-independent JSON contracts. Lock-time manifest rereads fail closed on corruption and normalize concurrent valid mode values before writing, with deterministic real-EEXIST regressions; the 39-command leakage ratchet tightens exactly from 30 to 27 lines.
|
|
6
|
+
|
|
3
7
|
## 1.36.183 — 2026-08-31
|
|
4
8
|
|
|
5
9
|
- T-0163: English skill list now emits zero Hangul across stored, environment, and explicit locale paths while preserving Korean bytes and locale-independent JSON values. Adversarial external-skill metadata and injective ID rendering are covered, and the 39-command leakage ratchet tightens exactly from 34 to 30 lines.
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 leerness contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 leerness contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ MIT
|
|
|
138
138
|
<!-- leerness:project-readme:start -->
|
|
139
139
|
## Leerness Project Harness
|
|
140
140
|
|
|
141
|
-
이 프로젝트는 Leerness v1.36.
|
|
141
|
+
이 프로젝트는 Leerness v1.36.184 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
142
142
|
|
|
143
143
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
144
144
|
|
|
@@ -192,7 +192,7 @@ leerness memory restore decision <date|title>
|
|
|
192
192
|
|
|
193
193
|
### MCP server (외부 AI 통합)
|
|
194
194
|
|
|
195
|
-
Leerness v1.36.
|
|
195
|
+
Leerness v1.36.184는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
|
|
196
196
|
|
|
197
197
|
```jsonc
|
|
198
198
|
// 카테고리별
|
|
@@ -213,7 +213,7 @@ Leerness v1.36.183는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
213
213
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
214
214
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
215
215
|
|
|
216
|
-
현재 누적: **v1.9.x → 1.36.
|
|
216
|
+
현재 누적: **v1.9.x → 1.36.184 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
217
217
|
|
|
218
218
|
### 성능 가이드
|
|
219
219
|
|
|
@@ -251,5 +251,5 @@ leerness release pack --close --auto-main-push
|
|
|
251
251
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
252
252
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
253
253
|
|
|
254
|
-
Last synced by Leerness v1.36.
|
|
254
|
+
Last synced by Leerness v1.36.184: 2026-09-01
|
|
255
255
|
<!-- leerness:project-readme:end -->
|
package/SECURITY.md
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
# Security & Transparency / 보안·투명성
|
|
2
|
-
|
|
3
|
-
> `leerness capabilities` 로 언제든 아래 표면을 CLI 에서 확인할 수 있습니다 (`--json` 지원).
|
|
4
|
-
> Run `leerness capabilities` anytime to view this surface from the CLI (`--json` supported).
|
|
5
|
-
|
|
6
|
-
leerness 는 **권한이 큰 CLI 하네스**입니다 (child_process · git · 외부 AI CLI · 자동화 브리지 · hook 설치). 신뢰를 위해 **할 수 있는 전부와 각 항목의 끄는 법(opt-out)** 을 공개합니다.
|
|
7
|
-
|
|
8
|
-
leerness is a **powerful CLI harness** (child_process, git, external AI CLIs, automation bridges, hook install). For trust, we disclose **everything it can do and how to opt out of each**.
|
|
9
|
-
|
|
10
|
-
## 설계 원칙 / Design principles
|
|
11
|
-
|
|
12
|
-
- **런타임 의존성 0** — `npm audit` 취약점 0. / Zero runtime dependencies.
|
|
13
|
-
- **postinstall 없음** — 설치만으로 코드가 실행되지 않음. / No postinstall scripts.
|
|
14
|
-
- **변경 전 자동 백업** — 모든 메타파일 변경 전 `.leerness/archive/` 에 백업. / Auto-backup before any change.
|
|
15
|
-
- **동의 없는 자동 호출 금지** — 사용자 동의 없이 외부 LLM/API/CLI 를 자동 호출하지 않음. / Never auto-calls external LLMs/APIs/CLIs without consent.
|
|
16
|
-
|
|
17
|
-
## 권한 표면 / Capability surface
|
|
18
|
-
|
|
19
|
-
| 영역 / Area | 위험 / Risk | 설명 / What | opt-out |
|
|
20
|
-
|---|---|---|---|
|
|
21
|
-
| filesystem | 🟢 low | `.leerness/` 메타파일 생성·갱신, 변경 전 백업. 소스코드 직접 수정 안 함. | 핵심 동작 (백업으로 보호) |
|
|
22
|
-
| network | 🟢 low | `update --check` 의 npm 최신 버전 비교만. 외부 URL 자동 fetch 안 함. | `LEERNESS_OFFLINE=1` |
|
|
23
|
-
| childProcess | 🟡 medium | git(명시 명령 시), `npm test`(verify-code), 외부 CLI `--version` 감지. | verify 계열 한정 · 외부 CLI 는 opt-in |
|
|
24
|
-
| externalAgents | 🟡 medium | `agents dispatch/multi` — 기본은 명령 텍스트만 생성, `multi --execute` 시 실제 spawn. | `LEERNESS_ENABLE_*` 미설정 시 비활성 (기본 off) |
|
|
25
|
-
| automationBridges | 🔴 high | `web`(playwright)/`pc`(robotjs)/`lsp`(typescript) — opt-in 의존성. `pc` 는 마우스/키보드 제어. | 의존성 미설치 시 비활성 (기본 off) |
|
|
26
|
-
| claudeHook | 🟢 low | `init` 시 `.claude/settings.local.json` 에 SessionStart hook(`update --check`) 설치. | `leerness init . --no-auto-update` |
|
|
27
|
-
|
|
28
|
-
## ⚠ 주의해서 쓸 명령 / Use with caution
|
|
29
|
-
|
|
30
|
-
회사·운영 코드에서는 아래 명령을 먼저 검토하세요. / Review these before using on company/production code.
|
|
31
|
-
|
|
32
|
-
| 명령 / Command | 동작 / Action |
|
|
33
|
-
|---|---|
|
|
34
|
-
| `init` | `.leerness/` 50+ 파일 + `.claude` hook 생성 (변경 전 백업) |
|
|
35
|
-
| `update --yes` | 자동 마이그레이션 — 메타파일 갱신 |
|
|
36
|
-
| `agents multi --execute` | 외부 AI CLI 실제 spawn (병렬) |
|
|
37
|
-
| `release publish` / `sync-main` | git push + npm publish + GitHub release |
|
|
38
|
-
| `pc <click|type>` | 마우스/키보드 제어 (robotjs, full 권한) |
|
|
39
|
-
| `web <...>` | 헤드리스 브라우저 자동화 (playwright) |
|
|
40
|
-
| `setup-agents` | 외부 CLI 활성화 + 자동 설치 시도 |
|
|
41
|
-
|
|
42
|
-
## 권장 도입 방식 / Recommended adoption
|
|
43
|
-
|
|
44
|
-
전역 자동 적용보다, 먼저 한 프로젝트에서 확인 후 커밋하세요.
|
|
45
|
-
Prefer per-project trial over global auto-adoption.
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npx leerness@latest init . --yes --language ko # 또는 --language en
|
|
49
|
-
git diff && git status # 생성 파일 검토 후 커밋
|
|
50
|
-
leerness capabilities # 권한 표면 확인
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## 취약점 보고 / Reporting
|
|
54
|
-
|
|
55
|
-
보안 취약점은 **공개 이슈로 올리지 마세요** — 공개 이슈는 내용이 모두에게 노출됩니다.
|
|
56
|
-
**Do NOT open a public issue for security vulnerabilities** — public issues are visible to everyone.
|
|
57
|
-
|
|
58
|
-
대신 아래 **비공개 채널** 중 하나로 알려주세요 / Use one of these **private** channels instead:
|
|
59
|
-
|
|
60
|
-
1. **GitHub Private Vulnerability Reporting** (권장 / preferred):
|
|
61
|
-
https://github.com/gugu9999gu/leerness/security/advisories/new
|
|
62
|
-
— GitHub 의 비공개 보안 권고 폼. 저장소 Security 탭 → "Report a vulnerability".
|
|
63
|
-
2. **보안 이메일 / Security email**: adstore3869@gmail.com (제목에 `[leerness security]` 표기 / prefix the subject with `[leerness security]`)
|
|
64
|
-
|
|
65
|
-
48 시간 내 접수 확인을 목표로 합니다. 수정 전까지 세부 내용은 공개하지 말아 주세요 (책임 있는 공개).
|
|
66
|
-
We aim to acknowledge within 48 hours. Please keep details private until a fix ships (responsible disclosure).
|
|
1
|
+
# Security & Transparency / 보안·투명성
|
|
2
|
+
|
|
3
|
+
> `leerness capabilities` 로 언제든 아래 표면을 CLI 에서 확인할 수 있습니다 (`--json` 지원).
|
|
4
|
+
> Run `leerness capabilities` anytime to view this surface from the CLI (`--json` supported).
|
|
5
|
+
|
|
6
|
+
leerness 는 **권한이 큰 CLI 하네스**입니다 (child_process · git · 외부 AI CLI · 자동화 브리지 · hook 설치). 신뢰를 위해 **할 수 있는 전부와 각 항목의 끄는 법(opt-out)** 을 공개합니다.
|
|
7
|
+
|
|
8
|
+
leerness is a **powerful CLI harness** (child_process, git, external AI CLIs, automation bridges, hook install). For trust, we disclose **everything it can do and how to opt out of each**.
|
|
9
|
+
|
|
10
|
+
## 설계 원칙 / Design principles
|
|
11
|
+
|
|
12
|
+
- **런타임 의존성 0** — `npm audit` 취약점 0. / Zero runtime dependencies.
|
|
13
|
+
- **postinstall 없음** — 설치만으로 코드가 실행되지 않음. / No postinstall scripts.
|
|
14
|
+
- **변경 전 자동 백업** — 모든 메타파일 변경 전 `.leerness/archive/` 에 백업. / Auto-backup before any change.
|
|
15
|
+
- **동의 없는 자동 호출 금지** — 사용자 동의 없이 외부 LLM/API/CLI 를 자동 호출하지 않음. / Never auto-calls external LLMs/APIs/CLIs without consent.
|
|
16
|
+
|
|
17
|
+
## 권한 표면 / Capability surface
|
|
18
|
+
|
|
19
|
+
| 영역 / Area | 위험 / Risk | 설명 / What | opt-out |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| filesystem | 🟢 low | `.leerness/` 메타파일 생성·갱신, 변경 전 백업. 소스코드 직접 수정 안 함. | 핵심 동작 (백업으로 보호) |
|
|
22
|
+
| network | 🟢 low | `update --check` 의 npm 최신 버전 비교만. 외부 URL 자동 fetch 안 함. | `LEERNESS_OFFLINE=1` |
|
|
23
|
+
| childProcess | 🟡 medium | git(명시 명령 시), `npm test`(verify-code), 외부 CLI `--version` 감지. | verify 계열 한정 · 외부 CLI 는 opt-in |
|
|
24
|
+
| externalAgents | 🟡 medium | `agents dispatch/multi` — 기본은 명령 텍스트만 생성, `multi --execute` 시 실제 spawn. | `LEERNESS_ENABLE_*` 미설정 시 비활성 (기본 off) |
|
|
25
|
+
| automationBridges | 🔴 high | `web`(playwright)/`pc`(robotjs)/`lsp`(typescript) — opt-in 의존성. `pc` 는 마우스/키보드 제어. | 의존성 미설치 시 비활성 (기본 off) |
|
|
26
|
+
| claudeHook | 🟢 low | `init` 시 `.claude/settings.local.json` 에 SessionStart hook(`update --check`) 설치. | `leerness init . --no-auto-update` |
|
|
27
|
+
|
|
28
|
+
## ⚠ 주의해서 쓸 명령 / Use with caution
|
|
29
|
+
|
|
30
|
+
회사·운영 코드에서는 아래 명령을 먼저 검토하세요. / Review these before using on company/production code.
|
|
31
|
+
|
|
32
|
+
| 명령 / Command | 동작 / Action |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `init` | `.leerness/` 50+ 파일 + `.claude` hook 생성 (변경 전 백업) |
|
|
35
|
+
| `update --yes` | 자동 마이그레이션 — 메타파일 갱신 |
|
|
36
|
+
| `agents multi --execute` | 외부 AI CLI 실제 spawn (병렬) |
|
|
37
|
+
| `release publish` / `sync-main` | git push + npm publish + GitHub release |
|
|
38
|
+
| `pc <click|type>` | 마우스/키보드 제어 (robotjs, full 권한) |
|
|
39
|
+
| `web <...>` | 헤드리스 브라우저 자동화 (playwright) |
|
|
40
|
+
| `setup-agents` | 외부 CLI 활성화 + 자동 설치 시도 |
|
|
41
|
+
|
|
42
|
+
## 권장 도입 방식 / Recommended adoption
|
|
43
|
+
|
|
44
|
+
전역 자동 적용보다, 먼저 한 프로젝트에서 확인 후 커밋하세요.
|
|
45
|
+
Prefer per-project trial over global auto-adoption.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx leerness@latest init . --yes --language ko # 또는 --language en
|
|
49
|
+
git diff && git status # 생성 파일 검토 후 커밋
|
|
50
|
+
leerness capabilities # 권한 표면 확인
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 취약점 보고 / Reporting
|
|
54
|
+
|
|
55
|
+
보안 취약점은 **공개 이슈로 올리지 마세요** — 공개 이슈는 내용이 모두에게 노출됩니다.
|
|
56
|
+
**Do NOT open a public issue for security vulnerabilities** — public issues are visible to everyone.
|
|
57
|
+
|
|
58
|
+
대신 아래 **비공개 채널** 중 하나로 알려주세요 / Use one of these **private** channels instead:
|
|
59
|
+
|
|
60
|
+
1. **GitHub Private Vulnerability Reporting** (권장 / preferred):
|
|
61
|
+
https://github.com/gugu9999gu/leerness/security/advisories/new
|
|
62
|
+
— GitHub 의 비공개 보안 권고 폼. 저장소 Security 탭 → "Report a vulnerability".
|
|
63
|
+
2. **보안 이메일 / Security email**: adstore3869@gmail.com (제목에 `[leerness security]` 표기 / prefix the subject with `[leerness security]`)
|
|
64
|
+
|
|
65
|
+
48 시간 내 접수 확인을 목표로 합니다. 수정 전까지 세부 내용은 공개하지 말아 주세요 (책임 있는 공개).
|
|
66
|
+
We aim to acknowledge within 48 hours. Please keep details private until a fix ships (responsible disclosure).
|
package/bin/leerness.js
CHANGED
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
migrateLegacyWorkspace,
|
|
50
50
|
} = require('../lib/workspace-dir');
|
|
51
51
|
|
|
52
|
-
const VERSION = '1.36.
|
|
52
|
+
const VERSION = '1.36.184';
|
|
53
53
|
|
|
54
54
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
55
55
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -851,6 +851,24 @@ function _uiLang(root) {
|
|
|
851
851
|
} catch {}
|
|
852
852
|
return 'ko';
|
|
853
853
|
}
|
|
854
|
+
// `mode` accepts both `--path` and path-like positionals after get/set. Keep
|
|
855
|
+
// every pre-dispatch diagnostic, stale check, and handler branch on the same
|
|
856
|
+
// target instead of letting each layer guess a different argv index.
|
|
857
|
+
function _modeRoot(args = nonFlagArgs(), fallback = process.cwd()) {
|
|
858
|
+
return absRoot(arg('--path', null) || _taskPositionalPath(args, 1) || fallback);
|
|
859
|
+
}
|
|
860
|
+
// T-0164: mode machine errors predate localization and use Korean canonical
|
|
861
|
+
// text. Preserve that byte contract even when an error is raised by a shared
|
|
862
|
+
// pre-dispatch guard before the mode handler can select its own renderer.
|
|
863
|
+
function _errorUiLang(root, command) {
|
|
864
|
+
// Global flags may legally precede the command. Use the same parser as main
|
|
865
|
+
// instead of assuming argv[2] is the command, or `--language en mode ...`
|
|
866
|
+
// makes shared pre-dispatch JSON errors language-dependent again.
|
|
867
|
+
const parsed = nonFlagArgs();
|
|
868
|
+
const resolvedCommand = command || parsed[0];
|
|
869
|
+
if (resolvedCommand === 'mode' && has('--json')) return 'ko';
|
|
870
|
+
return _uiLang(resolvedCommand === 'mode' ? _modeRoot(parsed, root) : root);
|
|
871
|
+
}
|
|
854
872
|
// ko/en 쌍에서 해석된 UI 언어로 선택 (Phase 1: 첫 화면 한정 사용).
|
|
855
873
|
function _tx(lang, ko, en) { return lang === 'en' ? en : ko; }
|
|
856
874
|
function fm(role, readWhen, updateWhen, body, lang = 'ko') {
|
|
@@ -1123,7 +1141,11 @@ function skillLock(skills, root) {
|
|
|
1123
1141
|
// 생성물도 동작도 standard 와 **바이트 동일**이었다 — 이름만 있는 등급이었다. 없는 것을 광고하지 않는다.
|
|
1124
1142
|
// 두 등급이 정직하게 도는 편이 셋 중 하나가 거짓말하는 것보다 낫다. strict 는 실제 게이트를 갖출 때 다시 연다.
|
|
1125
1143
|
const _MODES = ['minimal', 'standard'];
|
|
1126
|
-
function _normMode(v) {
|
|
1144
|
+
function _normMode(v) {
|
|
1145
|
+
let s = '';
|
|
1146
|
+
try { s = String(v || '').toLowerCase(); } catch { s = ''; }
|
|
1147
|
+
return _MODES.includes(s) ? s : 'standard';
|
|
1148
|
+
}
|
|
1127
1149
|
// 1.36.107: 재init 이 사용자의 선택을 되돌리지 않게 하는 두 술어. 우선순위는 **명시 플래그 > 기존 저장값 > 기본값**.
|
|
1128
1150
|
// opts.mode 는 호출부에서 정규화하지 않고 원본을 넘겨야 한다 — 미리 _normMode 를 걸면 '미지정' 과 '--mode standard'
|
|
1129
1151
|
// 가 구분되지 않아 명시적으로 standard 로 올리는 것과 그냥 재init 한 것이 같아진다.
|
|
@@ -9973,7 +9995,8 @@ function _p0015ModeOk() {
|
|
|
9973
9995
|
const normOk = _normMode('minimal') === 'minimal' && _normMode('standard') === 'standard'
|
|
9974
9996
|
&& _normMode('') === 'standard' && _normMode('MINIMAL') === 'minimal'
|
|
9975
9997
|
&& _normMode('banana') === 'standard' && _normMode(null) === 'standard'
|
|
9976
|
-
&& _normMode('strict') === 'standard' &&
|
|
9998
|
+
&& _normMode('strict') === 'standard' && _normMode({ toString: null }) === 'standard'
|
|
9999
|
+
&& _MODES.length === 2;
|
|
9977
10000
|
// ② 생성물이 실제로 갈린다 — 이름만 바뀌면 이 기능은 무의미하다
|
|
9978
10001
|
const full = coreFiles('.', 'ko', [], {});
|
|
9979
10002
|
const min = coreFiles('.', 'ko', [], { mode: 'minimal' });
|
|
@@ -15256,7 +15279,7 @@ function _rejectUnknownFlags(allowed, usageHint, options = {}) {
|
|
|
15256
15279
|
const dym = (u) => { const c = [...all].find(k => u.startsWith(k) || k.startsWith(u)); return c ? ` — 혹시 ${c}?` : ''; };
|
|
15257
15280
|
const detail = unknown.map(u => u + dym(u)).join(', ');
|
|
15258
15281
|
const supported = [...all].join(' ');
|
|
15259
|
-
const en =
|
|
15282
|
+
const en = _errorUiLang(process.cwd()) === 'en';
|
|
15260
15283
|
failJson(has('--json'), 'unknown_flag', en
|
|
15261
15284
|
? `Unsupported option for this command: ${detail} (supported: ${supported}${usageHint ? ' · ' + usageHint : ''}) — rejected instead of silently discarding its value`
|
|
15262
15285
|
: `이 명령에서 지원하지 않는 옵션: ${detail} (지원: ${supported}${usageHint ? ' · ' + usageHint : ''}) — 값이 조용히 버려지는 것을 방지하기 위해 거부`);
|
|
@@ -15273,7 +15296,7 @@ function _argvLongFlagNames() {
|
|
|
15273
15296
|
function _rejectUnregisteredFlags() {
|
|
15274
15297
|
const unknown = _argvLongFlagNames().filter(name => !_VALUE_FLAGS.has(name) && !_BOOL_FLAGS.has(name));
|
|
15275
15298
|
if (!unknown.length) return true;
|
|
15276
|
-
const en =
|
|
15299
|
+
const en = _errorUiLang(process.cwd()) === 'en';
|
|
15277
15300
|
const human = en
|
|
15278
15301
|
? `Unknown flag: ${unknown.join(' ')} — no such flag in leerness. Its value could otherwise be absorbed into text or a path. Check: leerness --help`
|
|
15279
15302
|
: `알 수 없는 플래그: ${unknown.join(' ')} — leerness 에 없는 이름입니다. 값이 본문이나 경로 인자에 섞이지 않도록 거부했습니다. 확인: leerness --help`;
|
|
@@ -15326,7 +15349,7 @@ function _rejectMalformedFlagForms(cmd, args) {
|
|
|
15326
15349
|
if (!errors.length) return true;
|
|
15327
15350
|
const onlyMissing = errors.every(e => e.kind === 'missing_value');
|
|
15328
15351
|
const onlyInvalidValue = errors.every(e => e.kind === 'invalid_value');
|
|
15329
|
-
const en =
|
|
15352
|
+
const en = _errorUiLang(process.cwd(), cmd) === 'en';
|
|
15330
15353
|
const detail = errors.map(e => {
|
|
15331
15354
|
if (e.kind === 'missing_value') {
|
|
15332
15355
|
if (e.name === '--path') return en
|
|
@@ -30986,7 +31009,7 @@ async function main() {
|
|
|
30986
31009
|
if (_k !== null) {
|
|
30987
31010
|
const _kn = Number(_k);
|
|
30988
31011
|
if (!Number.isInteger(_kn) || _kn < 0) {
|
|
30989
|
-
const _enK =
|
|
31012
|
+
const _enK = _errorUiLang(process.cwd(), cmd) === 'en';
|
|
30990
31013
|
failJson(has('--json'), 'invalid_keep', _enK
|
|
30991
31014
|
? `--keep must be an integer >= 0 (got: ${_k})`
|
|
30992
31015
|
: `--keep 은 0 이상의 정수여야 합니다 (받은 값: ${_k})`);
|
|
@@ -31071,7 +31094,7 @@ async function main() {
|
|
|
31071
31094
|
// 플래그를 적었는데 값이 없으면 그건 오타다. 조용히 cwd 로 떨어뜨리지 않는다.
|
|
31072
31095
|
const _pathTyped = process.argv.slice(2).some((t) => t === '--path' || (typeof t === 'string' && t.startsWith('--path=')));
|
|
31073
31096
|
if (_pathTyped && (typeof _p !== 'string' || !_p.trim())) {
|
|
31074
|
-
const _en =
|
|
31097
|
+
const _en = _errorUiLang(process.cwd(), cmd) === 'en';
|
|
31075
31098
|
failJson(has('--json'), 'path_missing_value', _en
|
|
31076
31099
|
? '--path was given without a value — nothing was done'
|
|
31077
31100
|
: '--path 에 값이 없습니다 — 아무것도 하지 않았습니다');
|
|
@@ -31080,7 +31103,7 @@ async function main() {
|
|
|
31080
31103
|
if (_p && !_PATH_MAY_BE_ABSENT.has(cmd)) {
|
|
31081
31104
|
const _abs = absRoot(_p);
|
|
31082
31105
|
if (!exists(_abs)) {
|
|
31083
|
-
const _en =
|
|
31106
|
+
const _en = _errorUiLang(process.cwd(), cmd) === 'en';
|
|
31084
31107
|
failJson(has('--json'), 'path_not_found', _lineSafe(_en
|
|
31085
31108
|
? `path not found: ${_abs} — no directory was created`
|
|
31086
31109
|
: `경로 없음: ${_abs} — 디렉토리를 새로 만들지 않았습니다`));
|
|
@@ -31091,7 +31114,7 @@ async function main() {
|
|
|
31091
31114
|
// 프로젝트 root 는 디렉토리다. 파일이면 그렇다고 말한다(스캔처럼 파일이 정상 입력인 경로는 위 _strict 가 따로 다룬다).
|
|
31092
31115
|
try {
|
|
31093
31116
|
if (!fs.statSync(_abs).isDirectory()) {
|
|
31094
|
-
const _en2 =
|
|
31117
|
+
const _en2 = _errorUiLang(process.cwd(), cmd) === 'en';
|
|
31095
31118
|
failJson(has('--json'), 'path_not_a_directory', _lineSafe(_en2
|
|
31096
31119
|
? `--path is not a directory: ${_abs}`
|
|
31097
31120
|
: `--path 가 디렉토리가 아닙니다: ${_abs}`));
|
|
@@ -31124,15 +31147,23 @@ async function main() {
|
|
|
31124
31147
|
// init/migrate 는 이미 _warnIfStale 호출하므로 제외. mcp/version/help 등 출력 민감 명령도 제외.
|
|
31125
31148
|
// 24h 캐시 (.leerness/cache/update-check.json) 활용 — 네트워크 비차단.
|
|
31126
31149
|
const _staleSkip = new Set(['init', 'migrate', 'usage', 'mcp', 'release', 'session-close', '--version', '--help', 'help', 'update', 'whats-new']);
|
|
31127
|
-
if (!_staleSkip.has(cmd) && process.env.LEERNESS_NO_STALE_CHECK !== '1' && !has('--no-stale-check')) {
|
|
31150
|
+
if (!_staleSkip.has(cmd) && !has('--json') && process.env.LEERNESS_NO_STALE_CHECK !== '1' && !has('--no-stale-check')) {
|
|
31128
31151
|
try {
|
|
31129
|
-
const root =
|
|
31152
|
+
const root = cmd === 'mode'
|
|
31153
|
+
? _modeRoot(args)
|
|
31154
|
+
: absRoot(arg('--path', args[1] && !args[1].startsWith('-') ? args[1] : process.cwd()));
|
|
31130
31155
|
// 캐시 fresh 시에만 즉시 비교 (네트워크 호출 X — 비차단). offline + cache 없으면 skip.
|
|
31131
31156
|
const cached = readUpdateCache(root);
|
|
31132
31157
|
if (cacheFresh(cached, 24) && cached.nextLeerness && compareVer(cached.nextLeerness, VERSION) > 0) {
|
|
31133
31158
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
31134
31159
|
const C = isTty ? { y: s => `\x1b[33m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`, d: s => `\x1b[2m${s}\x1b[0m` } : { y: s => s, b: s => s, d: s => s };
|
|
31135
|
-
|
|
31160
|
+
const _staleEn = _uiLang(root) === 'en';
|
|
31161
|
+
process.stderr.write(C.y(' ⚠ ') + C.b(_staleEn
|
|
31162
|
+
? `leerness v${VERSION} → v${cached.nextLeerness} available`
|
|
31163
|
+
: `leerness v${VERSION} → v${cached.nextLeerness} 사용 가능`)
|
|
31164
|
+
+ C.d(_staleEn
|
|
31165
|
+
? ` · ${C.b('npm i leerness@latest')} recommended (set LEERNESS_NO_STALE_CHECK=1 to disable)`
|
|
31166
|
+
: ` · ${C.b('npm i leerness@latest')} 권장 (LEERNESS_NO_STALE_CHECK=1 로 끄기)`) + '\n');
|
|
31136
31167
|
}
|
|
31137
31168
|
} catch {}
|
|
31138
31169
|
}
|
|
@@ -31141,9 +31172,23 @@ async function main() {
|
|
|
31141
31172
|
if (cmd === 'mode') {
|
|
31142
31173
|
const _tty = process.stdout && process.stdout.isTTY;
|
|
31143
31174
|
const _cy = s => _tty ? `\x1b[36m${s}\x1b[0m` : s, _dm = s => _tty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
31144
|
-
const _mRoot =
|
|
31175
|
+
const _mRoot = _modeRoot(args);
|
|
31145
31176
|
const _mj = has('--json');
|
|
31146
|
-
|
|
31177
|
+
const _mEn = _uiLang(_mRoot) === 'en';
|
|
31178
|
+
// Machine errors keep their established canonical text; language selection
|
|
31179
|
+
// is presentation-only, as with the other T-0092 priority surfaces.
|
|
31180
|
+
const _mt = (ko, en) => (_mEn && !_mj ? en : ko);
|
|
31181
|
+
const _mReason = chk => (_mEn
|
|
31182
|
+
? ({ read_failed: 'read failed', json_parse_failed: 'JSON parse failed', not_an_object: 'top level is not an object' }[chk.code] || 'unknown manifest error')
|
|
31183
|
+
: chk.reason);
|
|
31184
|
+
const _mRejectCorrupt = chk => failJson(_mj, 'manifest_corrupt', _mt(
|
|
31185
|
+
`manifest.json 손상(${chk.reason}) — 덮어쓰기 거부: ${chk.path}\n 복구 후 재시도하세요(지금 쓰면 project/language 등 남은 필드가 사라집니다)`,
|
|
31186
|
+
`manifest.json is corrupt (${_mReason(chk)}) — refusing to overwrite: ${chk.path}\n Repair it and retry (writing now would remove remaining fields such as project/language)`,
|
|
31187
|
+
));
|
|
31188
|
+
if (!exists(path.join(_mRoot, '.leerness'))) {
|
|
31189
|
+
failJson(_mj, 'harness_missing', _mt(`leerness 미설치: ${_mRoot} — 먼저 leerness init`, `leerness is not installed: ${_mRoot} — run leerness init first`));
|
|
31190
|
+
return;
|
|
31191
|
+
}
|
|
31147
31192
|
const sub = args[1] && !args[1].startsWith('-') && !_MODES.includes(args[1]) && !/^[.\/\\]|^[A-Za-z]:/.test(args[1]) ? args[1] : (_MODES.includes(args[1]) ? 'set' : 'get');
|
|
31148
31193
|
const cur = _projectMode(_mRoot);
|
|
31149
31194
|
// 1.36.107: 손상을 조용히 'standard' 로 보고하지 않는다 — 사용자는 설정이 날아간 걸 알아야 한다.
|
|
@@ -31151,44 +31196,65 @@ async function main() {
|
|
|
31151
31196
|
if (sub === 'get') {
|
|
31152
31197
|
if (_mj) { log(JSON.stringify({ ok: !_mChk.corrupt, root: _mRoot, mode: cur, modes: _MODES, corrupt: !!_mChk.corrupt, corruptReason: _mChk.reason || null }, null, 2)); if (_mChk.corrupt) process.exitCode = 1; return; }
|
|
31153
31198
|
log(_cy(`# leerness mode — ${cur}`));
|
|
31154
|
-
if (_mChk.corrupt) log(
|
|
31155
|
-
|
|
31156
|
-
|
|
31199
|
+
if (_mChk.corrupt) log(_mt(
|
|
31200
|
+
` ⚠ manifest.json 손상(${_mChk.reason}) — 위 값은 **저장값이 아니라 기본값**입니다: ${_mChk.path}`,
|
|
31201
|
+
` ⚠ manifest.json is corrupt (${_mReason(_mChk)}) — the value above is the default, not the stored value: ${_mChk.path}`,
|
|
31202
|
+
));
|
|
31203
|
+
log(_mt(
|
|
31204
|
+
` minimal 핵심 3종만 읽힌다 (handoff · verify-claim · session close)`,
|
|
31205
|
+
` minimal loads only the core three (handoff · verify-claim · session close)`,
|
|
31206
|
+
));
|
|
31207
|
+
log(_mt(
|
|
31208
|
+
` standard 현재 기본 — 전체 지침/상태 문서`,
|
|
31209
|
+
` standard current default — all instruction/state documents`,
|
|
31210
|
+
));
|
|
31157
31211
|
// 목록은 _MODES 에서 유도한다 — 하드코딩하면 등급을 뺐을 때 안내만 남아 없는 등급을 계속 광고한다
|
|
31158
31212
|
// (실제로 strict 제거 후 이 줄만 살아남아 게이트가 잡았다).
|
|
31159
|
-
log(_dm(
|
|
31213
|
+
log(_dm(_mt(
|
|
31214
|
+
` 변경: leerness mode set <${_MODES.join('|')}> · 적재량: leerness context budget`,
|
|
31215
|
+
` Change: leerness mode set <${_MODES.join('|')}> · Load: leerness context budget`,
|
|
31216
|
+
)));
|
|
31160
31217
|
return;
|
|
31161
31218
|
}
|
|
31162
31219
|
if (sub === 'set') {
|
|
31163
31220
|
const want = _normMode(args.find(a => _MODES.includes(a)));
|
|
31164
|
-
if (!args.some(a => _MODES.includes(a))) {
|
|
31221
|
+
if (!args.some(a => _MODES.includes(a))) {
|
|
31222
|
+
failJson(_mj, 'invalid_mode', _mt(`mode 는 ${_MODES.join('|')} 중 하나입니다`, `mode must be one of ${_MODES.join('|')}`));
|
|
31223
|
+
return;
|
|
31224
|
+
}
|
|
31165
31225
|
// 1.36.107 (손상 스토어 사냥): 종전엔 `catch { m = {} }` 후 덮어썼다 — 손상 매니페스트 위에 새로 쓰면서
|
|
31166
31226
|
// project·language·installedAt 등 **남아 있던 필드까지 통째로 날렸다**(실측 3/3 손상 유형에서 재현).
|
|
31167
31227
|
// 1.36.49 가 토글에 세운 규율과 같다: 변경 진입점은 fail-closed. 읽지 못하면 쓰지 않는다.
|
|
31168
31228
|
if (_mChk.corrupt) {
|
|
31169
|
-
|
|
31229
|
+
_mRejectCorrupt(_mChk);
|
|
31170
31230
|
return;
|
|
31171
31231
|
}
|
|
31172
31232
|
const mf = _mChk.path;
|
|
31173
31233
|
// 1.36.108 (T-0097): 락 안에서 **다시 읽는다**. 1.36.107 은 손상 판정과 필드 보존을 넣었지만 그 사이 창은 남아 있었다 —
|
|
31174
31234
|
// codex 검수가 rename 직전에 writer 를 주입해 그 writer 의 project/language/installedAt 이 전부 사라지는 것을 재현했다.
|
|
31175
31235
|
// 락 밖에서 읽은 _mChk.data 로 쓰면 "보존" 하는 것이 **낡은 스냅샷**이라 옆 프로세스의 변경을 지운다.
|
|
31176
|
-
_withLock(mf, () => {
|
|
31236
|
+
const _modeWrite = _withLock(mf, () => {
|
|
31177
31237
|
const fresh = _readManifestChecked(_mRoot);
|
|
31178
|
-
if (fresh.corrupt) return;
|
|
31238
|
+
if (fresh.corrupt) return { ok: false, check: fresh }; // 호출부도 성공/재생성을 중단해야 fail-closed 다
|
|
31179
31239
|
// 1.36.107 (codex 검수, 재현됨): Object.assign 은 [[Set]] 을 쓰므로 `__proto__` 키에서 setter 가 걸려
|
|
31180
31240
|
// 그 필드를 **조용히 버린다**(실측: 주입 후 mode set → 파일에서 사라짐). 스프레드는 CreateDataProperty 라 보존한다.
|
|
31181
31241
|
// 병적인 입력이긴 하나 이 수정이 내건 계약이 "남은 필드를 전부 보존" 이므로, 예외를 두면 계약이 거짓이 된다.
|
|
31242
|
+
// T-0164 재검수 P2: 이전 값을 쓰기 **전에** total 정규화한다. 쓰고 나서 hostile object 변환이 throw 하면
|
|
31243
|
+
// manifest 만 바뀌고 지침 재생성은 빠지는 부분 커밋이 된다.
|
|
31244
|
+
const previous = _normMode(fresh.data.mode);
|
|
31182
31245
|
const m = { ...fresh.data };
|
|
31183
31246
|
m.mode = want; writeUtf8(mf, JSON.stringify(m, null, 2) + '\n');
|
|
31247
|
+
return { ok: true, previous };
|
|
31184
31248
|
});
|
|
31249
|
+
if (!_modeWrite.ok) { _mRejectCorrupt(_modeWrite.check); return; }
|
|
31250
|
+
const previous = _modeWrite.previous;
|
|
31185
31251
|
// 지침 파일은 등급의 산출물이므로 즉시 재생성한다 — 안 그러면 mode 는 이름만 바뀌고 읽히는 양은 그대로다.
|
|
31186
31252
|
let regenerated = 0;
|
|
31187
31253
|
try {
|
|
31188
31254
|
const _l = _uiLang(_mRoot);
|
|
31189
31255
|
const gen = coreFiles(_mRoot, _l, [], { mode: want });
|
|
31190
31256
|
let prevGen = null;
|
|
31191
|
-
try { prevGen = coreFiles(_mRoot, _l, [], { mode:
|
|
31257
|
+
try { prevGen = coreFiles(_mRoot, _l, [], { mode: previous }); } catch { prevGen = null; } // 직전 등급 템플릿(차감용)
|
|
31192
31258
|
for (const k of ['AGENTS.md', 'CLAUDE.md']) {
|
|
31193
31259
|
if (!gen[k]) continue;
|
|
31194
31260
|
const p = path.join(_mRoot, k);
|
|
@@ -31205,12 +31271,15 @@ async function main() {
|
|
|
31205
31271
|
regenerated++;
|
|
31206
31272
|
}
|
|
31207
31273
|
} catch {}
|
|
31208
|
-
if (_mj) { log(JSON.stringify({ ok: true, root: _mRoot, mode: want, previous
|
|
31209
|
-
ok(
|
|
31210
|
-
|
|
31274
|
+
if (_mj) { log(JSON.stringify({ ok: true, root: _mRoot, mode: want, previous, regenerated }, null, 2)); return; }
|
|
31275
|
+
ok(_mt(
|
|
31276
|
+
`운영 등급: ${previous} → ${want} (지침 ${regenerated}개 재생성)`,
|
|
31277
|
+
`Operating mode: ${previous} → ${want} (${regenerated} instruction file${regenerated === 1 ? '' : 's'} regenerated)`,
|
|
31278
|
+
));
|
|
31279
|
+
log(_dm(_mt(` 적재량 확인: leerness context budget`, ` Check load: leerness context budget`)));
|
|
31211
31280
|
return;
|
|
31212
31281
|
}
|
|
31213
|
-
failJson(_mj, 'unknown_subcommand', `알 수 없는 mode 하위명령: ${sub} (가능: get, set)`);
|
|
31282
|
+
failJson(_mj, 'unknown_subcommand', _mt(`알 수 없는 mode 하위명령: ${sub} (가능: get, set)`, `Unknown mode subcommand: ${sub} (valid: get, set)`));
|
|
31214
31283
|
return;
|
|
31215
31284
|
}
|
|
31216
31285
|
if (cmd === 'context' && args[1] === 'budget') {
|