leerness 1.9.443 → 1.9.445

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.445 — 2026-06-08 — add-family positional path 일관화 (UR-0151, UR-0141 후속)
4
+
5
+ **🐛 데이터 오염 일관화: `decision/lesson/rule add` 도 positional path 인식 (task 와 동일).**
6
+
7
+ ### 변경
8
+ - `decision add`, `lesson save`, `rule add` 가 positional path 를 인식 — `--path` > path-like positional > cwd. 1.9.442(task)에서 도입한 순수 `_taskPositionalPath` 재사용 → 제목/내용은 경로로 오인 안 함(선행 구분자 only), 값-플래그값(`--reason`/`--trigger`/`--tag` 등) 제외.
9
+ - `_TASK_VALUE_FLAGS` 에 add-family 값-플래그(`--trigger`/`--tag`) 추가.
10
+ - 기존엔 블록 root 가 `arg('--path', cwd)` 만 사용 → 비-루트 cwd 에서 add 시 cwd 오염(멀티프로젝트). 이제 task 와 일관.
11
+
12
+ ### 검증 (회귀 0)
13
+ - **selftest 189→190** (와이어 3종 + --trigger 값 제외), **E2E 신규 B(1.9.445)**: 다른 cwd 에서 decision/lesson/rule add `<ws>` → ws 저장 + cwd 오염 0.
14
+
15
+ ## 1.9.444 — 2026-06-08 — CI/PR 턴키: leerness ci init (GPT-5.5 전략리뷰 §6.7, UR-0152)
16
+
17
+ **⚙️ leerness 를 팀/CI 품질 게이트로 확장 — PR 마다 `leerness gate` 를 실행하는 GitHub Actions 워크플로 1-커맨드 생성.**
18
+
19
+ ### 변경
20
+ - 신규 `leerness ci init [path] [--force] [--json]`: `.github/workflows/leerness-gate.yml` 생성 — `on: pull_request` 에서 `npx -y leerness gate .` 실행. gate = verify + audit + scan secrets + encoding + lazy. **exit 1 시 PR 체크 실패** → 증거 없는 완료·커밋 시크릿·인코딩 깨짐·규칙 위반을 CI 에서 차단.
21
+ - 워크플로 상단에 **exit-code 정책 주석** 명시(통과=0 / 실패=1). 멱등(이미 존재 시 경고, `--force` 로 덮어쓰기). `--json` 구조화 출력.
22
+
23
+ ### 검증 (회귀 0)
24
+ - **selftest 188→189** (워크플로 콘텐츠 + 와이어), **E2E 신규 B(1.9.444)**: `ci init` → 파일 생성 + 핵심 라인(name/pull_request/gate) 포함 + 재실행 멱등.
25
+ - 행위 재현: 임시 워크스페이스에 워크플로 생성·내용 검증·멱등 확인.
26
+
3
27
  ## 1.9.443 — 2026-06-08 — evidence-first 완료 게이트 completion_claim_allowed (GPT-5.5 전략리뷰 §6.3/6.4, UR-0153)
4
28
 
5
29
  **🔒 leerness 최대 차별점 강화: 증거로부터 "완료 주장 가능 여부"를 파생·노출.**
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.9.443 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
171
+ 이 프로젝트는 Leerness v1.9.445 하네스를 사용합니다. 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.9.443는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
225
+ Leerness v1.9.445는 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.9.443는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
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.9.443)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
246
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.445)** · 매 라운드 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.9.443: 2026-06-08
284
+ Last synced by Leerness v1.9.445: 2026-06-08
285
285
  <!-- leerness:project-readme:end -->
286
286
 
package/bin/leerness.js CHANGED
@@ -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.9.443';
34
+ const VERSION = '1.9.445';
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') 시 호스트 프로세스 오염.
@@ -3096,7 +3096,7 @@ function _selfTestCases() {
3096
3096
  } },
3097
3097
  { name: '10th 외부평가 Sonnet P2: rule add flag/경로 break(_parseAddTitle) — trigger 값/경로 흡수 차단 (1.9.426)', run: () => {
3098
3098
  const src = read(__filename);
3099
- const wired = src.includes("ruleAdd(arg('--path', process.cwd()), _parseAddTitle(args, 2))");
3099
+ const wired = src.includes("ruleAdd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), _parseAddTitle(args, 2))");
3100
3100
  const m = require('../lib/pure-utils');
3101
3101
  const u = m._parseAddTitle(['rule', 'add', '세션', '점검', '--trigger', 'every-session', '/p'], 2) === '세션 점검';
3102
3102
  return wired && u;
@@ -3347,6 +3347,25 @@ function _selfTestCases() {
3347
3347
  && src.includes('completion_claim_allowed: _ccaH'); // handoff json + latest.json
3348
3348
  return pure && wired;
3349
3349
  } },
3350
+ { name: 'GPT-5.5 전략리뷰 §6.7 (UR-0152): ci init — PR gate 워크플로 생성 + exit-code 정책 (1.9.444)', run: () => {
3351
+ if (typeof ciInitCmd !== 'function') return false;
3352
+ const wf = LEERNESS_GATE_WORKFLOW;
3353
+ const contentOk = /name:\s*leerness-gate/.test(wf) && /on:\s*\n\s*pull_request:/.test(wf) && /leerness gate \./.test(wf) && /exit code 정책/.test(wf) && /actions\/checkout@v4/.test(wf);
3354
+ const src = read(__filename);
3355
+ const wired = src.includes("cmd === 'ci' && (args[1] === 'init'") && src.includes('ciInitCmd(absRoot(_resolveRoot(args[2]))');
3356
+ return contentOk && wired;
3357
+ } },
3358
+ { name: 'UR-0151: decision/lesson/rule add positional path 지원(_taskPositionalPath 재사용, cwd 오염 차단) (1.9.445)', run: () => {
3359
+ const src = read(__filename);
3360
+ const rule = src.includes("ruleAdd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), _parseAddTitle(args, 2))");
3361
+ const lesson = src.includes("if (cmd === 'lesson') {\n const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());");
3362
+ const decision = src.includes("if (cmd === 'decision') {\n const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd());");
3363
+ // rule add 의 --trigger 값은 경로 아님(path-like 아님) + 값-플래그 제외
3364
+ const m = require('../lib/pure-utils');
3365
+ const trig = m._taskPositionalPath(['rule', 'add', '룰', '--trigger', 'every-update', '/p'], 2) === '/p'
3366
+ && m._taskPositionalPath(['rule', 'add', '룰', '--trigger', 'every-update'], 2) === null;
3367
+ return rule && lesson && decision && trig;
3368
+ } },
3350
3369
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3351
3370
  ];
3352
3371
  }
@@ -3925,6 +3944,7 @@ function commandsCmd(root) {
3925
3944
  { cmd: 'capabilities [--json]', desc: '권한·보안 표면 공개 (무엇을 하는지 + opt-out + 주의 명령) — 1.9.272' },
3926
3945
  { cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278' },
3927
3946
  { cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280' },
3947
+ { cmd: 'ci init [path] [--force]', desc: 'PR 마다 leerness gate 실행하는 GitHub Actions 워크플로 생성 (.github/workflows/leerness-gate.yml) — 1.9.444' },
3928
3948
  { cmd: 'policy show|set|check', desc: '권한 등급 (read-only…publish) — opt-in enforced (위험 명령 차단) — 1.9.281' },
3929
3949
  { cmd: 'reuse-check "<기능>"', desc: '외부 OSS 빌드 vs 재사용 결정 게이트 (오프라인 카테고리+체크리스트) — 1.9.285' },
3930
3950
  { cmd: 'skill impact', desc: '스킬 설치 영향 경량 상관추적 (사용 빈도 ↔ 검증 통과율, advisory) — 1.9.286' },
@@ -16004,6 +16024,44 @@ function runsShowCmd(root, id) {
16004
16024
  if (!exists(fp)) return fail(`run 없음: ${id}`);
16005
16025
  log(read(fp));
16006
16026
  }
16027
+
16028
+ // 1.9.444 (GPT-5.5 전략리뷰 §6.7, UR-0152): CI/PR 턴키 — PR 마다 leerness gate 를 실행하는 GitHub Actions 워크플로 생성.
16029
+ // gate = verify + audit + scan secrets + encoding + lazy. exit 1 시 PR 체크 실패 → 증거 없는 완료/시크릿/규칙 위반을 CI 에서 차단.
16030
+ const LEERNESS_GATE_WORKFLOW = [
16031
+ '# leerness gate — AI 코딩 작업 증거/규칙/검증 게이트 (PR CI). leerness ci init 로 생성.',
16032
+ '# exit code 정책: 통과=0, 실패(테스트 실패 / 커밋 시크릿 / 인코딩 깨짐 / 게으름·증거 누락 / 필수 규칙 파일 없음)=1 → PR 체크 실패.',
16033
+ 'name: leerness-gate',
16034
+ 'on:',
16035
+ ' pull_request:',
16036
+ ' workflow_dispatch:',
16037
+ 'jobs:',
16038
+ ' gate:',
16039
+ ' runs-on: ubuntu-latest',
16040
+ ' steps:',
16041
+ ' - uses: actions/checkout@v4',
16042
+ ' - uses: actions/setup-node@v4',
16043
+ ' with:',
16044
+ " node-version: '20'",
16045
+ ' - name: leerness gate',
16046
+ ' run: npx -y leerness gate .',
16047
+ '',
16048
+ ].join('\n');
16049
+ function ciInitCmd(root, opts = {}) {
16050
+ root = absRoot(root || process.cwd());
16051
+ const relPath = '.github/workflows/leerness-gate.yml';
16052
+ const fp = path.join(root, '.github', 'workflows', 'leerness-gate.yml');
16053
+ const json = !!opts.json;
16054
+ if (exists(fp) && !opts.force) {
16055
+ if (json) { log(JSON.stringify({ ok: true, created: false, path: relPath, reason: 'exists' })); return; }
16056
+ warn(`이미 존재: ${relPath} (덮어쓰려면 --force)`);
16057
+ return;
16058
+ }
16059
+ mkdirp(path.join(root, '.github', 'workflows'));
16060
+ writeUtf8(fp, LEERNESS_GATE_WORKFLOW);
16061
+ if (json) { log(JSON.stringify({ ok: true, created: true, path: relPath })); return; }
16062
+ ok(`생성: ${relPath} — PR 마다 leerness gate 실행 (exit 1 시 PR 체크 실패)`);
16063
+ log(' gate = verify + audit + scan secrets + encoding + lazy. 증거 없는 완료·시크릿·규칙 위반을 CI 에서 차단.');
16064
+ }
16007
16065
  // 1.9.294 (UR-0025 3단계): 역할/모델 카탈로그(_PROVIDER_MODEL_CATALOG + _AGENT_ROLE_PROMPTS + ROLE_CATALOG + _ROLE_ALIASES) 데이터 모듈 분리 (비파괴, require-based).
16008
16066
  const { _PROVIDER_MODEL_CATALOG, _AGENT_ROLE_PROMPTS, ROLE_CATALOG, _ROLE_ALIASES } = require('../lib/role-catalog');
16009
16067
  function _normalizeRole(name) {
@@ -18879,6 +18937,7 @@ async function main() {
18879
18937
  if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (arg('--path', args[2] || process.cwd())));
18880
18938
  if (cmd === 'session' && args[1] === 'close') return sessionClose(_resolveRoot(args[2]), { json: has('--json') });
18881
18939
  // 1.9.151: viewwork 명령 제거 (사용자 명시 — leerness 와 무관). session close 의 viewworkEmit 콜도 함께 제거.
18940
+ if (cmd === 'ci' && (args[1] === 'init' || args[1] == null)) return ciInitCmd(absRoot(_resolveRoot(args[2])), { force: has('--force'), json: has('--json') }); // 1.9.444 (UR-0152): CI gate 워크플로 생성
18882
18941
  if (cmd === 'route') return route(args[1] || 'planning');
18883
18942
  if (cmd === 'self' && args[1] === 'check') return await selfCheck(absRoot(arg('--path', args[2] || process.cwd())));
18884
18943
  if (cmd === 'self' && args[1] === 'migrate') return log('Run: npx --yes leerness@latest migrate . --dry-run, then migrate without --dry-run after review.');
@@ -18996,7 +19055,7 @@ async function main() {
18996
19055
  if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
18997
19056
  // 1.9.213: leerness intent <classify|expand|domains> — intent inference + scope expansion (사용자 명시)
18998
19057
  if (cmd === 'intent') return intentCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
18999
- if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), _parseAddTitle(args, 2)); // 1.9.426 (10th 외부평가 Sonnet P2): flag/경로 break 기존 filter 경로 + --trigger 값까지 설명에 흡수
19058
+ if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), _parseAddTitle(args, 2)); // 1.9.426: flag/경로 break(_parseAddTitle) · 1.9.445 (UR-0151): positional path 지원(제목과 분리)
19000
19059
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
19001
19060
  if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
19002
19061
  if (cmd === 'rule' && args[1] === 'pause') return rulePause(arg('--path', process.cwd()), args[2]);
@@ -19077,7 +19136,7 @@ async function main() {
19077
19136
  // 1.9.112: lesson save — lessons.md에 새 lesson 추가
19078
19137
  // 1.9.117: lesson list — lessons.md 조회 + --tag 필터 + --json
19079
19138
  if (cmd === 'lesson') {
19080
- const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || '';
19139
+ const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd()); const sub = args[1] || ''; // 1.9.445 (UR-0151): positional path 지원
19081
19140
  if (sub === 'save') {
19082
19141
  const textParts = [];
19083
19142
  for (let i = 2; i < args.length; i++) {
@@ -19104,7 +19163,7 @@ async function main() {
19104
19163
  // 1.9.108: decision add — decisions.md에 새 설계 결정 추가
19105
19164
  // 1.9.118: decision list — decisions.md 전체 조회 + --json
19106
19165
  if (cmd === 'decision') {
19107
- const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || '';
19166
+ const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd()); const sub = args[1] || ''; // 1.9.445 (UR-0151): positional path 지원
19108
19167
  if (sub === 'add') {
19109
19168
  // args[2..] 가 title (단, --flag 또는 경로형 positional 이 시작되기 전까지)
19110
19169
  // 1.9.351 (UR-0064) → 1.9.416 (UR-0122): 공유 헬퍼 _parseAddTitle 로 단일화(flag/경로 break) + 빈 입력 거부
package/lib/pure-utils.js CHANGED
@@ -1305,7 +1305,7 @@ function _parseAddTitle(args, startIdx = 0) {
1305
1305
  // 1.9.442 (12th 외부평가 Sonnet UR-0141): task 계열 positional path 안전 추출.
1306
1306
  // _parseAddTitle 과 동일한 path-like 판정(선행 구분자 / ./ ../ C:\)으로 제목/ID/맨이름은 경로로 오인 안 함(src/auth 같은 내부 슬래시 제목 보호).
1307
1307
  // 값-취하는 플래그(--evidence /abs/log 등)의 값은 root 후보에서 제외(직전 토큰이 값-플래그면 skip) → 오탐 차단. 첫 path-like positional 만 반환, 없으면 null.
1308
- const _TASK_VALUE_FLAGS = new Set(['--status', '--evidence', '--priority', '--note', '--reason', '--title', '--desc', '--summary', '--id', '--limit', '--from', '--to']);
1308
+ const _TASK_VALUE_FLAGS = new Set(['--status', '--evidence', '--priority', '--note', '--reason', '--title', '--desc', '--summary', '--id', '--limit', '--from', '--to', '--trigger', '--tag']); // 1.9.445 (UR-0151): rule/lesson add 값-플래그(--trigger/--tag) 포함
1309
1309
  function _taskPositionalPath(args, startIdx = 2) {
1310
1310
  const a = args || [];
1311
1311
  for (let i = startIdx; i < a.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.443",
3
+ "version": "1.9.445",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -6153,5 +6153,52 @@ total++;
6153
6153
  if (!ok) failed++;
6154
6154
  }
6155
6155
 
6156
+ // 1.9.444 (GPT-5.5 전략리뷰 §6.7, UR-0152): ci init — PR gate 워크플로 생성(멱등).
6157
+ total++;
6158
+ {
6159
+ let ok = false;
6160
+ try {
6161
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ci-'));
6162
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'ci', 'init', d], { encoding: 'utf8', timeout: 15000 });
6163
+ const wfPath = path.join(d, '.github', 'workflows', 'leerness-gate.yml');
6164
+ const created = fs.existsSync(wfPath);
6165
+ const content = created ? fs.readFileSync(wfPath, 'utf8') : '';
6166
+ const contentOk = /name:\s*leerness-gate/.test(content) && /pull_request:/.test(content) && /leerness gate \./.test(content);
6167
+ // 멱등: 재실행 시 경고(덮어쓰기 X, exit 0)
6168
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'ci', 'init', d], { encoding: 'utf8', timeout: 15000 });
6169
+ const idempotent = r2.status === 0 && /이미 존재|exists/.test((r2.stdout || '') + (r2.stderr || ''));
6170
+ fs.rmSync(d, { recursive: true, force: true });
6171
+ ok = r1.status === 0 && created && contentOk && idempotent;
6172
+ } catch {}
6173
+ console.log(ok ? '✓ B(1.9.444) UR-0152: ci init — PR gate 워크플로 생성 + 멱등' : '✗ ci init 실패');
6174
+ if (!ok) failed++;
6175
+ }
6176
+
6177
+ // 1.9.445 (UR-0151): add-family(decision/lesson/rule) positional path — 다른 cwd 에서 실행해도 positional 경로에 저장(cwd 오염 차단).
6178
+ total++;
6179
+ {
6180
+ let ok = false;
6181
+ try {
6182
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-addpos-'));
6183
+ const cwd3 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cwd3-'));
6184
+ cp.spawnSync(process.execPath, [CLI, 'init', ws, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6185
+ cp.spawnSync(process.execPath, [CLI, 'decision', 'add', '결정E2E', ws, '--reason', 'r'], { encoding: 'utf8', timeout: 15000, cwd: cwd3 });
6186
+ cp.spawnSync(process.execPath, [CLI, 'lesson', 'save', '교훈E2E', ws], { encoding: 'utf8', timeout: 15000, cwd: cwd3 });
6187
+ cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '룰E2E', '--trigger', 'every-update', ws], { encoding: 'utf8', timeout: 15000, cwd: cwd3 });
6188
+ const hdir = path.join(ws, '.harness');
6189
+ let savedAll = false;
6190
+ try {
6191
+ const blob = fs.readdirSync(hdir).map(f => { try { return fs.readFileSync(path.join(hdir, f), 'utf8'); } catch { return ''; } }).join('\n');
6192
+ savedAll = blob.includes('결정E2E') && blob.includes('교훈E2E') && blob.includes('룰E2E');
6193
+ } catch {}
6194
+ const cwdClean = !fs.existsSync(path.join(cwd3, '.harness'));
6195
+ fs.rmSync(ws, { recursive: true, force: true });
6196
+ fs.rmSync(cwd3, { recursive: true, force: true });
6197
+ ok = savedAll && cwdClean;
6198
+ } catch {}
6199
+ console.log(ok ? '✓ B(1.9.445) UR-0151: decision/lesson/rule add positional path 저장 + cwd 오염 차단' : '✗ add-family positional path 실패');
6200
+ if (!ok) failed++;
6201
+ }
6202
+
6156
6203
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
6157
6204
  if (failed > 0) process.exit(1);