leerness 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.0 — 2026-06-08 — 🛡️ [안정화/Stable] CLI 견고성 + --json 에이전트 계약 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor — 13번째 멀티에이전트 버그헌트로 발견·수정한 patch(1.10.1~1.10.5)를 검증·통합해 npm 에 안정 공개.** R-0011 배포 정책의 2번째 minor 릴리스.
6
+
7
+ ### 이번 minor 에 담긴 개선 (1.10.1~1.10.5, 그동안 GitHub 에만 누적)
8
+ - **시크릿 스캐너 정밀화** (UR-0144): AWS 공식 예제키 `AKIA…EXAMPLE`(접미사) 오탐 차단, 중간 example 실키는 보존(FN 정책 유지).
9
+ - **--json 에이전트 계약 하드닝** (UR-0146/0173/0174): `init --json` 순수 JSON(배너/진행바/대화 메뉴 억제, io quiet 모드) + `scan secrets`/`encoding check`/`lazy detect`/`deps` 잘못된 경로 구조화 에러(`failJson`, exit 1).
10
+ - **정직성·견고성** (UR-0167): `session close <파일>` 가 실패를 성공(exit 0)으로 오판하던 문제 → 구조화 에러 + exit 1.
11
+ - **CLI 인자 견고성** (UR-0169): `arg()` 가 값 없이 끝나면 `true` 누출 / 다음 `--flag` 흡수하던 문제 → def 반환(음수값 보존).
12
+ - **표·출력 안전화** (UR-0170/0168/0171): plan drop 표셀 `_cellSafe`, handoff --json 배너 억제, env encoding-check positional 디렉토리 존중.
13
+
14
+ ### 검증 (회귀 0)
15
+ - **selftest 201 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump(배포) 확인. 13번째 버그헌트 8건 중 7건 수정 + 2건 by-design(UR-0142 intent classify, audit --fix).
16
+
17
+ ### 안정화 표시 (R-0006)
18
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
19
+
20
+ ## 1.10.5 — 2026-06-08 — arg() 플래그 흡수 방지 + handoff --json 배너 억제 (13th 버그헌트 잔여, UR-0168/0169)
21
+
22
+ **🧰 CLI 인자 견고성 + --json 순수성 마무리.**
23
+
24
+ ### 변경
25
+ - **arg() 값 플래그 흡수 방지** (UR-0169, P3): `arg(name)` 가 값 없이 끝나면 boolean `true` 를 누출(`--reason` 가 plan.md 에 'true' 기록)하거나, 다음 토큰이 `--flag` 면 그것을 값으로 흡수(`--target --path` → target='--path')하던 문제 → 다음 토큰이 없거나 `--` 장식 플래그면 `def` 반환. **음수 값(`--x -5` 등 단일 `-`)은 보존**.
26
+ - **handoff --json 미-init 배너 억제** (UR-0168, P2): `handoff --json` 이 미-init 디렉토리에서 사람용 "init 미실행" 경고를 JSON 앞에 출력(비-JSON)하던 문제 → `--json` 시 배너 억제(순수 JSON).
27
+
28
+ ### 검증 (회귀 0)
29
+ - **selftest 199→201** (arg: 흡수방지+음수보존+정상값, handoff 배너 와이어), 행위 재현: `export --target --path` 미흡수, `--reason`(끝) true 누출 없음, `handoff --json` 미-init 순수 JSON.
30
+ - arg() 는 코어 함수 → **E2E 365 전수**가 회귀 안전망. patch(1.10.5, 같은 minor) — R-0011 정책상 npm 미배포. (13th 잔여: UR-0172 시크릿 prefix FP — FN 회귀 위험으로 신중 검토 예정)
31
+
32
+ ## 1.10.4 — 2026-06-08 — 13번째 버그헌트 후속 P2/P3 클러스터 (UR-0167/0170/0171)
33
+
34
+ **🐛 정직성·견고성: 실패 오판 + 표 손상 + 무시되던 인자 수정.**
35
+
36
+ ### 변경
37
+ - **session close 디렉토리 가드** (UR-0167, P2-정직성): `session close <파일경로>` 가 `mkdir <path>/.harness` 에서 ENOTDIR 크래시 + **exit 0(실패를 성공으로 오판)** 하던 문제 → 경로 없음/디렉토리 아님이면 `failJson(code:'path_not_found')` + exit 1.
38
+ - **plan drop 표셀 안전화** (UR-0170, P3): `plan drop` 의 text/`--reason` 에 든 파이프(`|`)·개행이 plan.md 마크다운 표 칼럼을 깨뜨리던 문제 → `_cellSafe`(task/rule UR-0104 와 동일, round-trip 가능) 적용.
39
+ - **env encoding-check positional 디렉토리** (UR-0171, P3): `env encoding-check <dir>` 가 positional 인자를 무시하고 cwd 를 스캔(→ 잘못된 디렉토리 false 'no risk') 하던 문제 → 형제 `env check/sync/detect` 와 동일하게 positional 존중.
40
+
41
+ ### 검증 (회귀 0)
42
+ - **selftest 198→199**, 행위 재현: session close 파일경로 → exit 1 + JSON, plan drop 파이프 → `\|` escape(round-trip OK), env encoding-check `<dir>` → scanned 정상.
43
+ - patch(1.10.4, 같은 minor) — R-0011 정책상 npm 미배포, GitHub 만 갱신. (13th 잔여: UR-0168 handoff 배너 · UR-0169 arg 값흡수 · UR-0172 시크릿 prefix FP)
44
+
45
+ ## 1.10.3 — 2026-06-08 — --json 순수성 하드닝 (13번째 멀티에이전트 버그헌트, UR-0173/0174)
46
+
47
+ **🤖 에이전트 계약 강화: `--json` 이 항상 기계 파싱 가능한 출력만 내도록.**
48
+
49
+ ### 변경
50
+ - **init --json TTY 누출 차단** (UR-0173, 1.10.2 회귀): 인터랙티브 터미널에서 `init --json` 이 배너/진행바/언어선택 메뉴를 JSON 앞에 출력하던 문제 → 배너·진행바(`isTty`)에 `!opts.json` 게이트 + `nonInteractive` 적용. 비-TTY(파이프/CI)는 기존에도 정상.
51
+ - **walk 기반 스캐너 --json 경로 오류 구조화** (UR-0174): `scan secrets` / `encoding check` / `lazy detect` 가 없는/파일 경로에서 `walk()` throw → 사람용 `✗ ENOENT/ENOTDIR` 텍스트(비-JSON)를 뱉던 문제 → `status()` 패턴(path 가드 + `failJson(code:'path_not_found')`, exit 1) 통일.
52
+ - **deps --json 인자누락 구조화** (UR-0174): `deps --json` 의 capability 누락이 사람용 텍스트 → `failJson(code:'missing_arg')`.
53
+
54
+ ### 13번째 버그헌트 — 후속 등록 (이번 미수정, 백로그)
55
+ - UR-0167 session close --json 파일경로 exit 0 오판 · UR-0168 handoff --json 미-init 배너 누출 · UR-0169 arg() 값없는 플래그 흡수 · UR-0170 plan drop 파이프 표 깨짐 · UR-0171 env encoding-check positional 무시 · UR-0172 시크릿 prefix+단어 placeholder FP(검토).
56
+
57
+ ### 검증 (회귀 0)
58
+ - **selftest 196→198** (4종 --json 구조화 에러 spawn 검증 + init 게이트 와이어), 행위 재현: 4종 모두 `{ok:false,code}` exit 1, init --json TTY 순수 JSON.
59
+ - patch(1.10.3, 같은 minor) — R-0011 정책상 npm 미배포, GitHub 만 갱신.
60
+
61
+ ## 1.10.2 — 2026-06-08 — init --json 순수 JSON 출력 (12th 외부평가 Codex P3, UR-0146)
62
+
63
+ **🤖 `init --json` 이 사람용 출력 대신 순수 JSON 요약 1개 — AI 에이전트가 JSON.parse 가능.**
64
+
65
+ ### 변경
66
+ - `leerness init --json` 이 기존엔 `--json` 을 silent ignore 하고 배너+15줄 사람용 로그를 출력 → 이제 **순수 JSON 요약**: `{ ok, action:'init', version, path, harnessFiles, dryRun, minimal }`.
67
+ - io 에 **quiet 모드(`setQuiet`)** 추가 — `log/ok/warn`(사람용)만 묵음, `fail/failJson`(오류)은 항상 노출. init(install) 큰 핸들러 내부 다중 log 게이팅(침투적) 없이 비침투적 묵음.
68
+ - init 디스패치가 `--json` 시 `setQuiet(true)` → install → `finally setQuiet(false)` → JSON 1개 emit (배너/공지 누출 0).
69
+
70
+ ### 검증 (회귀 0)
71
+ - **selftest 195→196** (setQuiet: log/ok 묵음 + fail 노출 + 와이어), 행위 재현: `init --json` → 파싱 가능 JSON, 배너 누출 없음.
72
+ - patch(1.10.2, 같은 minor) — R-0011 정책상 npm 미배포, GitHub 만 갱신.
73
+
74
+ ## 1.10.1 — 2026-06-08 — 시크릿 스캐너 AWS …EXAMPLE 키 오탐 수정 (12th 외부평가 Opus P3, UR-0144)
75
+
76
+ **🔒 AWS 공식 예제키(AKIA…EXAMPLE) 오탐 차단 — 실키 FN 정책은 보존.**
77
+
78
+ ### 변경
79
+ - `_isPlaceholderSecret`: 값이 **'example' 로 끝나면(접미사)** placeholder 로 판정 — AWS 문서 표준 예제키 `AKIAIOSFODNN7EXAMPLE` 등 오탐(FP) 차단.
80
+ - **중간에 'example' 이 있는 실키**(`sk-EXAMPLEab12…`, `sk-proj-realKEYexample…`)는 접미사가 아니라 미해당 → 실키-FN 정책(UR-0105) 보존. 실키가 'example' 로 끝날 확률 0.
81
+ - 직전 시도(1.9.440)에서 'example' 통째 매칭이 FN 정책과 충돌해 보류했던 것을 접미사 한정으로 정밀 해결.
82
+
83
+ ### 함께 종결 (코드 변경 없음)
84
+ - **UR-0142** intent classify 항상 default → **by-design**: intent classify 는 precise/broad scope 분류이며 default 는 scope 신호 없는 입력의 정상 중립 결과(맹신 X 직접 재현: "정확히"→precise, "전체 다양한"→broad). 리뷰의 task-type 기대는 오해.
85
+
86
+ ### 검증 (회귀 0)
87
+ - **selftest 194→195** (AWS EXAMPLE placeholder + 중간 example 실키 real + 진짜키 탐지), 기존 FN 정책 셀프테스트(UR-0106/0109) 유지.
88
+ - patch(1.10.1, 같은 minor) — R-0011 정책상 npm 미배포, GitHub 만 갱신.
89
+
3
90
  ## 1.10.0 — 2026-06-08 — 🛡️ [안정화/Stable] 1.10 안정 minor (배포 정책 첫 minor 릴리스)
4
91
 
5
92
  **🛡️ 안정화(Stable) minor — patch 누적(1.9.446~449)을 검증·통합해 npm 에 안정 버전으로 공개.** R-0011 배포 정책(patch 는 누적, minor 만 npm 공개)의 첫 minor 릴리스.
package/README.md CHANGED
@@ -168,7 +168,7 @@ MIT
168
168
  <!-- leerness:project-readme:start -->
169
169
  ## Leerness Project Harness
170
170
 
171
- 이 프로젝트는 Leerness v1.10.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
171
+ 이 프로젝트는 Leerness v1.11.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
172
172
 
173
173
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
174
174
 
@@ -222,7 +222,7 @@ leerness memory restore decision <date|title>
222
222
 
223
223
  ### MCP server (외부 AI 통합)
224
224
 
225
- Leerness v1.10.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
225
+ Leerness v1.11.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
226
226
 
227
227
  ```jsonc
228
228
  // 카테고리별
@@ -243,7 +243,7 @@ Leerness v1.10.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
243
243
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
244
244
  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) 다음 라운드 예약.
245
245
 
246
- 현재 누적: **70 라운드 (1.9.40 → 1.10.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
246
+ 현재 누적: **70 라운드 (1.9.40 → 1.11.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
247
247
 
248
248
  ### 성능 가이드 (1.9.140 측정)
249
249
 
@@ -281,6 +281,6 @@ leerness release pack --close --auto-main-push
281
281
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
282
282
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
283
283
 
284
- Last synced by Leerness v1.10.0: 2026-06-08
284
+ Last synced by Leerness v1.11.0: 2026-06-08
285
285
  <!-- leerness:project-readme:end -->
286
286
 
package/bin/leerness.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const cp = require('child_process');
7
- const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('../lib/io'); // 1.9.382/383 (UR-0025): 출력/시간/파일 프리미티브 공유 모듈
7
+ const { log, ok, warn, fail, failJson, setQuiet, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('../lib/io'); // 1.9.382/383 (UR-0025): 출력/시간/파일 프리미티브 공유 모듈 · 1.10.2 (UR-0146): setQuiet
8
8
  const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
9
9
  const readline = require('readline');
10
10
  // 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
31
31
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
32
32
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_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 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
33
33
 
34
- const VERSION = '1.10.0';
34
+ const VERSION = '1.11.0';
35
35
 
36
36
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
37
37
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -202,7 +202,11 @@ function _getAutoLoopRule(root) {
202
202
  }
203
203
  function arg(name, def = null) {
204
204
  const i = process.argv.indexOf(name);
205
- if (i >= 0) return process.argv[i + 1] || true;
205
+ if (i >= 0) {
206
+ // 1.10.5 (13th 버그헌트 P3, UR-0169): 값 없음(끝) 또는 다음 토큰이 --장식플래그면 def 반환 — 기존 `argv[i+1] || true` 는 다음 --flag 를 값으로 흡수하거나 boolean true 를 누출(예: --reason 가 'true' 기록)했음. 음수값(-5 등 단일 -)은 값으로 보존.
207
+ const next = process.argv[i + 1];
208
+ return (next != null && !next.startsWith('--')) ? next : def;
209
+ }
206
210
  const eq = process.argv.find(a => a.startsWith(name + '=')); // --name=value 형태 (외부리뷰 CV-1/UR-0076)
207
211
  return eq ? eq.slice(name.length + 1) : def;
208
212
  }
@@ -930,7 +934,7 @@ async function install(root, opts = {}) {
930
934
  }
931
935
  } catch {}
932
936
  // 1.9.32: init 시 ASCII 배너 + 빠른 시작 가이드 (migrate는 quiet)
933
- if (!opts.migration && !has('--no-banner')) _banner({ quickStart: !opts.dry });
937
+ if (!opts.migration && !has('--no-banner') && !opts.json) _banner({ quickStart: !opts.dry }); // 1.10.3 (UR-0173): --json 시 배너 억제(순수 JSON)
934
938
  // 1.9.33: npx 캐시로 옛 버전이 실행될 때 경고 (migrate/--no-stale-check 시 스킵)
935
939
  // 1.9.276: dry-run 시 스킵 — 캐시 파일(.harness/cache) 생성 방지 (dry = 부작용 0 보장)
936
940
  if (!opts.migration && !has('--no-stale-check') && !opts.nonInteractive && !opts.dry) {
@@ -973,7 +977,7 @@ async function install(root, opts = {}) {
973
977
  // 1.9.184 (사용자 명시): 파일 설치 — 생성 목록 나열 X, 로딩바 + 단일 완료 메시지.
974
978
  const actions = [];
975
979
  const totalFiles = Object.keys(files).length;
976
- const isTty = process.stdout.isTTY && !opts.dry;
980
+ const isTty = process.stdout.isTTY && !opts.dry && !opts.json; // 1.10.3 (UR-0173): --json 시 진행바/완료메시지 억제(순수 JSON)
977
981
  const drawProgress = (done, file) => {
978
982
  if (!isTty) return;
979
983
  const pct = Math.round(done * 100 / totalFiles);
@@ -3405,6 +3409,66 @@ function _selfTestCases() {
3405
3409
  const dropsToPlan = src.includes("ok(`plan dropped: ${id} → .harness/plan.md (Out of Scope / Dropped)`);");
3406
3410
  return noTaskRow && dropsToPlan;
3407
3411
  } },
3412
+ { name: '12th 외부평가 Opus P3 (UR-0144): AWS …EXAMPLE 접미사 placeholder + 중간 example 실키 FN 보존 (1.10.1)', run: () => {
3413
+ const m = require('../lib/pure-utils'); const f = m._isPlaceholderSecret;
3414
+ return f('AKIAIOSFODNN7EXAMPLE') === true
3415
+ && f('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false
3416
+ && f('sk-proj-realKEYexample9988776655') === false
3417
+ && f('AKIAJQXMP7RZ2KL9WXYZ') === false
3418
+ && read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js')).includes('if (/example$/.test(v)) return true;');
3419
+ } },
3420
+ { name: '12th 외부평가 Codex P3 (UR-0146): init --json 순수 JSON(quiet 모드) + 오류는 quiet 무시 (1.10.2)', run: () => {
3421
+ const io = require('../lib/io');
3422
+ if (typeof io.setQuiet !== 'function') return false;
3423
+ // quiet 면 log/ok/warn 묵음, fail/failJson 은 노출
3424
+ let out = ''; const _w = process.stdout.write; process.stdout.write = s => { out += s; return true; };
3425
+ let quietLogSuppressed, failShown;
3426
+ try { io.setQuiet(true); io.log('HUMAN'); io.ok('OK'); quietLogSuppressed = !/HUMAN|OK/.test(out); out = ''; io.fail('ERR'); failShown = /ERR/.test(out); }
3427
+ finally { io.setQuiet(false); process.stdout.write = _w; process.exitCode = 0; }
3428
+ const src = read(__filename);
3429
+ const wired = src.includes("setQuiet(true);") && src.includes("action: 'init', version: VERSION, path: _initRoot, harnessFiles");
3430
+ return quietLogSuppressed && failShown && wired;
3431
+ } },
3432
+ { name: '13th 버그헌트 P2 (UR-0174): scan secrets/encoding check/lazy detect/deps --json 잘못된경로 구조화 에러 (1.10.3)', run: () => {
3433
+ const cp = require('child_process'); const osm = require('os');
3434
+ const bad = path.join(osm.tmpdir(), '__leerness_nope_' + process.pid);
3435
+ const isJson = (a) => { const r = cp.spawnSync(process.execPath, [__filename, ...a], { encoding: 'utf8' }); if (r.status !== 1) return false; try { const j = JSON.parse((r.stdout || '').trim()); return j.ok === false && !!j.code; } catch { return false; } };
3436
+ return isJson(['scan', 'secrets', '--path', bad, '--json'])
3437
+ && isJson(['lazy', 'detect', '--path', bad, '--json'])
3438
+ && isJson(['encoding', 'check', '--path', __filename, '--json']); // 파일 경로(디렉토리 아님)
3439
+ } },
3440
+ { name: '13th 버그헌트 P2 (UR-0173): init --json 배너/진행바/대화 억제 와이어 (1.10.3)', run: () => {
3441
+ const src = read(__filename);
3442
+ return src.includes("!has('--no-banner') && !opts.json")
3443
+ && src.includes('process.stdout.isTTY && !opts.dry && !opts.json')
3444
+ && src.includes('{ ..._initOpts, json: true, nonInteractive: true }');
3445
+ } },
3446
+ { name: '13th 버그헌트 P2/P3 (UR-0167/0170/0171): session close 디렉토리 가드 + plan drop _cellSafe + env encoding-check positional (1.10.4)', run: () => {
3447
+ const scSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
3448
+ const sc = scSrc.includes("!fs.statSync(root).isDirectory()") && scSrc.includes("'path_not_found'");
3449
+ const src = read(__filename);
3450
+ const planSafe = src.includes('const safeText = _cellSafe(text); const safeReason = _cellSafe(reason);') && src.includes('| ${id} | ${safeText} | ${safeReason} |');
3451
+ const envPos = src.includes("(args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check')");
3452
+ return sc && planSafe && envPos;
3453
+ } },
3454
+ { name: '13th 버그헌트 P3 (UR-0169): arg() 값없음/다음--플래그 흡수 방지 (음수값 보존) (1.10.5)', run: () => {
3455
+ const save = process.argv;
3456
+ try {
3457
+ process.argv = ['node', 'h', '--target', '--path', '/x'];
3458
+ if (arg('--target', null) !== null) return false; // 다음 --flag 흡수 X → def
3459
+ process.argv = ['node', 'h', '--reason'];
3460
+ if (arg('--reason', 'D') !== 'D') return false; // 끝(값없음) → def (true 누출 X)
3461
+ process.argv = ['node', 'h', '--x', '-5'];
3462
+ if (arg('--x', null) !== '-5') return false; // 음수값(단일 -)은 보존
3463
+ process.argv = ['node', 'h', '--target', 'claude'];
3464
+ if (arg('--target', null) !== 'claude') return false; // 정상 값
3465
+ return true;
3466
+ } finally { process.argv = save; }
3467
+ } },
3468
+ { name: '13th 버그헌트 P2 (UR-0168): handoff 미-init 배너 --json 억제 와이어 (1.10.5)', run: () => {
3469
+ const src = read(__filename);
3470
+ return src.includes("!has('--no-init-check') && !has('--json')) {");
3471
+ } },
3408
3472
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3409
3473
  ];
3410
3474
  }
@@ -6352,14 +6416,16 @@ function planAdd(root, text) {
6352
6416
  function planDrop(root, text) {
6353
6417
  const id = nextId(root, 'D');
6354
6418
  const reason = arg('--reason', '사용자 요청으로 제외');
6419
+ // 1.10.4 (13th 버그헌트 P3, UR-0170): text/reason 의 파이프(|)·개행이 plan.md 마크다운 표 칼럼을 깨뜨림 → _cellSafe 셀 안전화(task/rule UR-0104 와 동일).
6420
+ const safeText = _cellSafe(text); const safeReason = _cellSafe(reason);
6355
6421
  const planFile = planPath(root); let p = exists(planFile) ? read(planFile) : '';
6356
6422
  const droppedHeader = '## Out of Scope / Dropped';
6357
6423
  if (p.includes(droppedHeader)) {
6358
6424
  p = p.replace(droppedHeader + '\n| ID | Item | Reason | Date |\n|---|---|---|---|\n',
6359
- droppedHeader + '\n| ID | Item | Reason | Date |\n|---|---|---|---|\n' + `| ${id} | ${text} | ${reason} | ${today()} |\n`);
6425
+ droppedHeader + '\n| ID | Item | Reason | Date |\n|---|---|---|---|\n' + `| ${id} | ${safeText} | ${safeReason} | ${today()} |\n`);
6360
6426
  writeUtf8(planFile, p);
6361
6427
  } else {
6362
- append(planFile, `\n${droppedHeader}\n| ID | Item | Reason | Date |\n|---|---|---|---|\n| ${id} | ${text} | ${reason} | ${today()} |\n`);
6428
+ append(planFile, `\n${droppedHeader}\n| ID | Item | Reason | Date |\n|---|---|---|---|\n| ${id} | ${safeText} | ${safeReason} | ${today()} |\n`);
6363
6429
  }
6364
6430
  // 1.9.449 (12th 외부평가 Sonnet P3, UR-0143): progress-tracker 에 dropped task(T-) 행 생성 제거 — plan drop 은 plan.md "Out of Scope / Dropped"(D-) 에만 기록.
6365
6431
  // 기존엔 scope 드랍이 phantom T- task 로도 생겨 plan↔progress 역할 혼선 + task list 노이즈. 드랍 기록의 단일 출처 = plan.md.
@@ -7260,6 +7326,8 @@ function _collectSecretFindings(root) {
7260
7326
 
7261
7327
  function scanSecrets(root, opts = {}) {
7262
7328
  root = absRoot(root);
7329
+ // 1.10.3 (13th 버그헌트 P2, UR-0174): 없는 경로에서 --json 비-JSON(ENOENT) 차단 → 구조화 에러. 단 파일 경로는 지원(UR-0072: _collectSecretFindings 가 파일 root 단일 스캔) → 존재성만 검사(디렉토리 강제 X).
7330
+ if (!exists(root)) { failJson(has('--json') || opts.json, 'path_not_found', `경로 없음: ${root}`); return; }
7263
7331
  const { committed, ignored } = _collectSecretFindings(root);
7264
7332
  // 1.9.415 (9th 외부평가 Opus/Codex): --json 일관성 — 기존엔 --json 무시하고 사람용 텍스트만 출력하던 FN.
7265
7333
  if (has('--json') || opts.json) {
@@ -7281,6 +7349,8 @@ function scanSecrets(root, opts = {}) {
7281
7349
 
7282
7350
  function encodingCheck(root, opts = {}) {
7283
7351
  root = absRoot(root);
7352
+ // 1.10.3 (13th 버그헌트 P2, UR-0174): 잘못된 경로(없음/파일)에서 walk() throw → --json 비-JSON 차단.
7353
+ if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(has('--json') || opts.json, 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
7284
7354
  let warnings = 0; const findings = [];
7285
7355
  for (const file of walk(root)) {
7286
7356
  const ext = path.extname(file).toLowerCase();
@@ -7327,6 +7397,8 @@ function encodingCheck(root, opts = {}) {
7327
7397
  function lazyDetect(root, opts = {}) {
7328
7398
  root = absRoot(root);
7329
7399
  const jsonMode = !!opts.json || has('--json');
7400
+ // 1.10.3 (13th 버그헌트 P2, UR-0174): 잘못된 경로(없음/파일)에서 walk() throw → --json 비-JSON 차단.
7401
+ if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(jsonMode, 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
7330
7402
  let issues = 0;
7331
7403
  const findings = []; // 1.9.101: { kind, severity, message, ...details }
7332
7404
  const _warn = (msg, finding) => { if (finding) findings.push(finding); if (!jsonMode) warn(msg); };
@@ -9180,7 +9252,7 @@ function handoffCmd(root) {
9180
9252
  // 1.9.35 개선 #1: .harness 부재 시 즉시 경고 (자동 init 권장)
9181
9253
  // 사용자가 신규 디렉토리에서 handoff 호출 시 sub-agent 작업이 길을 잃지 않도록.
9182
9254
  const absR = absRoot(root || process.cwd());
9183
- if (!exists(path.join(absR, '.harness')) && !has('--no-init-check')) {
9255
+ if (!exists(path.join(absR, '.harness')) && !has('--no-init-check') && !has('--json')) { // 1.10.5 (13th 버그헌트 P2, UR-0168): --json 시 사람용 미-init 배너 억제(순수 JSON, 앞부분 비-JSON 누출 차단)
9184
9256
  const isTty = process.stdout && process.stdout.isTTY;
9185
9257
  const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
9186
9258
  const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
@@ -9985,7 +10057,7 @@ async function orchestrateCmd(root, goalParts) {
9985
10057
  // → 회귀 발생 시 어느 프로젝트인지 즉시 보고
9986
10058
  function depsImpactCmd(root, targetCapability) {
9987
10059
  root = absRoot(root || process.cwd());
9988
- if (!targetCapability) { fail('impact <capability> 필요. 예: leerness impact Character'); return process.exit(1); }
10060
+ if (!targetCapability) { failJson(has('--json'), 'missing_arg', 'impact <capability> 필요. 예: leerness impact Character'); return; } // 1.10.3 (UR-0174): --json 구조화 에러
9989
10061
  const paths = _collectWorkspacePaths(root);
9990
10062
  if (!paths.length) {
9991
10063
  // --all-apps 자동
@@ -18905,7 +18977,20 @@ async function main() {
18905
18977
  } catch {}
18906
18978
  }
18907
18979
  // 1.9.276 (GPT-5.5 2차 리뷰): init --dry-run(미리보기) / --minimal(핵심 파일만) / --no-env(.env 생략)
18908
- if (cmd === 'init') return await install(arg('--path', args[1] || process.cwd()), { force:has('--force'), dry:has('--dry-run'), migration:false, minimal:has('--minimal'), noEnv:has('--no-env') }); // 1.9.355 (UR-0075): --path 지원
18980
+ if (cmd === 'init') {
18981
+ const _initRoot = absRoot(arg('--path', args[1] && !args[1].startsWith('-') ? args[1] : process.cwd()));
18982
+ const _initOpts = { force:has('--force'), dry:has('--dry-run'), migration:false, minimal:has('--minimal'), noEnv:has('--no-env') };
18983
+ if (has('--json')) {
18984
+ // 1.10.2 (UR-0146): init --json — 사람용 출력 묵음(setQuiet) + 순수 JSON 요약 1개. 기존엔 --json 을 silent ignore(배너만 출력).
18985
+ // 1.10.3 (UR-0173): json:true → 배너/진행바 억제(TTY 누출 차단), nonInteractive:true → 대화 메뉴 차단(머신 계약).
18986
+ setQuiet(true);
18987
+ try { await install(_initRoot, { ..._initOpts, json: true, nonInteractive: true }); } finally { setQuiet(false); }
18988
+ let harnessFiles = 0; try { harnessFiles = exists(path.join(_initRoot, '.harness')) ? fs.readdirSync(path.join(_initRoot, '.harness')).length : 0; } catch {}
18989
+ log(JSON.stringify({ ok: true, action: 'init', version: VERSION, path: _initRoot, harnessFiles, dryRun: !!_initOpts.dry, minimal: !!_initOpts.minimal }, null, 2));
18990
+ return;
18991
+ }
18992
+ return await install(_initRoot, _initOpts); // 1.9.355 (UR-0075): --path 지원
18993
+ }
18909
18994
  // 1.9.64: install <skill-id-or-url> 별칭 (= skill install). 자주 쓰는 명령 단축형.
18910
18995
  // 단, init이 leerness install . 같은 형태로도 동작하던 옛 호환은 유지 — args[1]이 디렉토리면 init으로 라우팅.
18911
18996
  if (cmd === 'install') {
@@ -19068,7 +19153,8 @@ async function main() {
19068
19153
  // 1.9.241: leerness env summary|encoding — 환경 종합 + 셸 스크립트 인코딩 위험 (사용자 명시 UR-0014)
19069
19154
  // 기존 env check/sync/detect (1.9.71) 와 충돌하지 않음 — summary/encoding 신규 서브
19070
19155
  if (cmd === 'env' && (args[1] === 'summary' || args[1] === 'encoding' || args[1] === 'encoding-check'))
19071
- return envCmd(arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
19156
+ // 1.10.4 (13th 버그헌트 P3, UR-0171): positional 디렉토리 인자 존중(형제 env check/sync/detect 와 통일) 기존엔 무시하고 cwd 스캔 잘못된 디렉토리 false 'no risk'.
19157
+ return envCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
19072
19158
  // 1.9.254: leerness path-setup [--apply] — CLI PATH 자동 등록 (사용자 명시 UR-0019)
19073
19159
  // npm global bin 경로 감지 + leerness 가 PATH 에서 찾아지는지 확인 → 미등록 시 플랫폼별 등록
19074
19160
  if (cmd === 'path-setup' || cmd === 'path') return pathSetupCmd(arg('--path', process.cwd()), { apply: has('--apply'), json: has('--json') });
package/lib/io.js CHANGED
@@ -6,15 +6,19 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
 
9
- function log(s = '') { console.log(s); }
9
+ // 1.10.2 (UR-0146): quiet 모드 사람용 출력(log/ok/warn) 억제. init --json 등에서 큰 핸들러의 다수 log 를 비침투적으로 묵음 → 순수 JSON 1개만 출력.
10
+ // fail/failJson(오류)은 묵음 대상 아님(에러는 항상 노출). setQuiet 로 토글, 호출부 finally 에서 반드시 복구.
11
+ let _quiet = false;
12
+ function setQuiet(v) { _quiet = !!v; }
13
+ function log(s = '') { if (_quiet) return; console.log(s); }
10
14
  function ok(s) { log('✓ ' + s); }
11
15
  function warn(s) { log('⚠ ' + s); }
12
16
  // fail() 은 오류 신호 → exit code 1 설정 (CI/MCP/에이전트가 실패를 성공으로 오판 방지, UR-0045).
13
- function fail(s) { log('✗ ' + s); process.exitCode = 1; }
17
+ function fail(s) { console.log('✗ ' + s); process.exitCode = 1; } // quiet 무시(오류는 항상 노출)
14
18
  // 1.9.398 (6번째 외부평가/codex P1-C, UR-0099): --json 모드 에러는 구조화 출력 — AI 에이전트가 에러 경로에서 JSON.parse 실패하지 않도록.
15
19
  // jsonMode 면 {ok:false,error,code} + exit1, 아니면 사람용 fail(). 양쪽 exit code 1 일관.
16
20
  function failJson(jsonMode, code, msg) {
17
- if (jsonMode) { log(JSON.stringify({ ok: false, error: msg, code }, null, 2)); process.exitCode = 1; }
21
+ if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg, code }, null, 2)); process.exitCode = 1; } // quiet 무시(오류 JSON 항상 노출)
18
22
  else fail(msg);
19
23
  }
20
24
  function today() { return new Date().toISOString().slice(0, 10); }
@@ -48,4 +52,4 @@ function writeUtf8(p, s) {
48
52
  function append(p, s) { mkdirp(path.dirname(p)); fs.appendFileSync(p, s, 'utf8'); }
49
53
  function rel(root, p) { return path.relative(root, p).replace(/\\/g, '/') || '.'; }
50
54
 
51
- module.exports = { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel };
55
+ module.exports = { log, ok, warn, fail, failJson, setQuiet, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel };
package/lib/pure-utils.js CHANGED
@@ -652,7 +652,9 @@ function _isPlaceholderSecret(value) {
652
652
  const hasRealPrefix = /^(?:sk-|sk-proj-|pk_|rk_|akia|ghp_|gho_|ghs_|ghr_|github_pat_|xox[baprs]-|aiza|ya29\.|glpat-|-----begin)/.test(v);
653
653
  // 1.9.436 (11th 외부평가 Opus P3): prefix 가 있어도 본문이 동일문자 8+연속(AKIAXXXX…/…00000000…)이면 명백한 더미 → placeholder. 실키는 고엔트로피라 무영향.
654
654
  if (/(.)\1{7,}/.test(alnum)) return true;
655
- // (12th 외부평가 Opus P3 'AWS …EXAMPLE 키'는 보류 — 'example' 통째 placeholder 화는 기존 FN 정책(sk-EXAMPLE… 실키 유지, UR-0105)과 충돌. UR-0144 분리.)
655
+ // 1.10.1 (12th 외부평가 Opus P3, UR-0144): 'example' 끝나면(접미사) placeholder AWS 공식 예제키 AKIAIOSFODNN7EXAMPLE 등.
656
+ // 중간에 'example' 이 있는 실키(sk-EXAMPLEab12…, sk-proj-realKEYexample…)는 접미사 아니라 미해당 → 기존 FN 정책(UR-0105) 보존. 실키는 'example' 로 끝날 확률 0.
657
+ if (/example$/.test(v)) return true;
656
658
  // 실키 prefix → 항상 실키(마커 무시). 그 외 마커 단어 있으면 placeholder(고엔트로피여도). prefix 없고 마커 없고 고엔트로피 → 실키.
657
659
  if (hasRealPrefix) return false;
658
660
  if (hasMarker) return true;
@@ -13,6 +13,8 @@ const { _sanitizeFences, _parseArchiveBlocks } = require('./pure-utils');
13
13
  function sessionClose(root, opts = {}, deps = {}) {
14
14
  const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest } = deps;
15
15
  root = absRoot(root);
16
+ // 1.10.4 (13th 버그헌트 P2, UR-0167): 경로 없음/디렉토리 아님 → 구조화 에러 + exit 1. mkdir <path>/.harness ENOTDIR 크래시 & 실패를 성공(exit 0)으로 오판하던 문제 차단.
17
+ if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(!!opts.json || has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
16
18
  // 1.9.103: --json 모드 — stdout 억제 후 구조화 출력
17
19
  const jsonMode = !!opts.json || has('--json');
18
20
  const _origWrite = process.stdout.write.bind(process.stdout);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",