leerness 1.9.441 → 1.9.443

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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.443 — 2026-06-08 — evidence-first 완료 게이트 completion_claim_allowed (GPT-5.5 전략리뷰 §6.3/6.4, UR-0153)
4
+
5
+ **🔒 leerness 최대 차별점 강화: 증거로부터 "완료 주장 가능 여부"를 파생·노출.**
6
+
7
+ ### 변경
8
+ - 순수 `_completionClaimAllowed(record)` (pure-utils): run-record 증거 기반 완료 게이트 — **변경 파일 존재 + 검증 실행(tests/commands) + 미해결 errors 0 + verification_result === 'pass'** 일 때만 `allowed:true`. 미검증/실패/증거부족은 사유(reasons)와 함께 불허.
9
+ - `state show`(text + `--json`), `state verify --json`, `state handoff`(latest.json + .md + --json) 에 `completion_claim_allowed` 노출 → 다음 에이전트/PR 리뷰어가 증거 게이트를 직접 읽음.
10
+ - GPT-5.5 전략리뷰가 evidence-first 를 핵심 차별점으로 명시 — run-record 14필드 스키마(기존)에 완료 가능성 판정을 더해 "완료 주장 ↔ 증거" 연결을 표면화.
11
+
12
+ ### 검증 (회귀 0)
13
+ - **selftest 187→188** (순수 6케이스 + state/handoff 와이어), **E2E 신규 B(1.9.443)**: 증거 없음 → no(사유), 증거+verify pass → allowed.
14
+ - 행위 재현: state start→show(no) → record+verify pass → show(--json allowed=true, reasons=[]).
15
+
16
+ ## 1.9.442 — 2026-06-08 — task 계열 positional path 지원 (12th 외부평가 Sonnet P1, UR-0141)
17
+
18
+ **🐛 P1 데이터 오염 수정: `task add "제목" ./경로` / `task list /경로` 가 무시되고 cwd 에 저장되던 문제.**
19
+
20
+ ### 변경
21
+ - `task list/add/update/drop/...` 가 positional path 를 인식 — `--path` > path-like positional > cwd 우선순위. 기존엔 `arg('--path', cwd)` 만 사용해 positional 경로를 무시 → 프로젝트 루트가 아닌 cwd 에서 실행 시 엉뚱한 곳에 메모리 생성(멀티프로젝트 오염).
22
+ - 순수 `_taskPositionalPath(args, startIdx)` (pure-utils): `_parseAddTitle` 과 동일한 path-like 판정(선행 구분자 `/ ./ ../ C:\`)으로 **제목/ID/맨이름은 경로로 오인하지 않음**(`task add "fix src/auth bug"` 의 내부 슬래시 제목 보호). **값-취하는 플래그의 값**(`--evidence /abs/log`)도 경로 후보에서 제외.
23
+
24
+ ### 검증 (회귀 0)
25
+ - **selftest 186→187** (순수 7케이스 + 와이어), **E2E 신규 B(1.9.442)**: 다른 cwd 에서 `task add "x" <ws>` → ws 에 저장 + cwd 오염 0.
26
+ - 행위 재현으로 확인: positional 경로 저장 + cwd `.harness` 미생성. `--path` 최우선·내부슬래시 제목·Windows 경로(`C:\`,`C:/`)·boolean 플래그 뒤 경로 전부 정상.
27
+
3
28
  ## 1.9.441 — 2026-06-08 — README ASCII 배너 추가
4
29
 
5
30
  **🎨 README 최상단에 LEERNESS ASCII 아트 배너 추가(CLI `_banner` 와 동일 아트).**
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.441 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
171
+ 이 프로젝트는 Leerness v1.9.443 하네스를 사용합니다. 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.441는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
225
+ Leerness v1.9.443는 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.441는 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.441)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
246
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.443)** · 매 라운드 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.441: 2026-06-08
284
+ Last synced by Leerness v1.9.443: 2026-06-08
285
285
  <!-- leerness:project-readme:end -->
286
286
 
package/bin/leerness.js CHANGED
@@ -25,13 +25,13 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
25
25
  _withBuiltinSource, _esc, _roadmapTokenStyles, _parseSkillMd,
26
26
  _migrationGuideText, _parseContractSpec, _gitignoreMatch,
27
27
  _featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
28
- _parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle, _parseImplExports } = require('../lib/pure-utils'); // 1.9.318~416 (UR-0025/0053/0075/0086/0087/0104/0122): 순수 유틸 모듈 분리
28
+ _parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed } = require('../lib/pure-utils'); // 1.9.318~443 (UR-0025/0053/0075/0086/0087/0104/0122/0141/0153): 순수 유틸 모듈 분리
29
29
  // 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
30
30
  const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
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.441';
34
+ const VERSION = '1.9.443';
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') 시 호스트 프로세스 오염.
@@ -3312,6 +3312,41 @@ function _selfTestCases() {
3312
3312
  const bannerLine = '███████╗███████╗██╗ ██╗'.slice(0, 0) + '██║ █████╗ █████╗ ██████╔╝'; // LEERNESS 배너 고유 라인(자기참조 회피 split)
3313
3313
  return readme.includes(bannerLine) && read(__filename).includes(bannerLine); // README ↔ CLI _banner 동일 아트
3314
3314
  } },
3315
+ { name: '12th 외부평가 Sonnet P1 (UR-0141): task 계열 positional path 지원 (cwd 오염 차단) (1.9.442)', run: () => {
3316
+ const m = require('../lib/pure-utils');
3317
+ if (typeof m._taskPositionalPath !== 'function' || m._taskPositionalPath !== _taskPositionalPath) return false;
3318
+ const p = (a) => m._taskPositionalPath(a, 2);
3319
+ const pure = p(['task', 'add', '제목', '/abs/p']) === '/abs/p' // add 뒤 절대경로
3320
+ && p(['task', 'add', '제목']) === null // 경로 없으면 null(→cwd)
3321
+ && p(['task', 'add', 'fix src/auth bug']) === null // 내부 슬래시 제목은 경로 아님(보호)
3322
+ && p(['task', 'list', './ws']) === './ws' // list positional
3323
+ && p(['task', 'update', 'T-0001', '/abs']) === '/abs' // id 뒤 경로
3324
+ && p(['task', 'update', 'T-0001', '--evidence', '/abs/log']) === null // 값-플래그 값은 경로 아님
3325
+ && p(['task', 'list', '--json', '/abs']) === '/abs'; // boolean 플래그 뒤 경로 OK
3326
+ const wired = read(__filename).includes("arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd()");
3327
+ return pure && wired;
3328
+ } },
3329
+ { name: 'GPT-5.5 전략리뷰 §6.3 (UR-0153): evidence-first 완료 게이트 _completionClaimAllowed + state/handoff 노출 (1.9.443)', run: () => {
3330
+ const m = require('../lib/pure-utils');
3331
+ if (typeof m._completionClaimAllowed !== 'function' || m._completionClaimAllowed !== _completionClaimAllowed) return false;
3332
+ const f = m._completionClaimAllowed;
3333
+ const ok = f({ files_changed: ['a.js'], tests_run: ['npm test'], errors: [], verification_result: 'pass' });
3334
+ const noFiles = f({ files_changed: [], tests_run: ['t'], verification_result: 'pass' });
3335
+ const noVerifyRun = f({ files_changed: ['a'], tests_run: [], commands_run: [], verification_result: 'pass' });
3336
+ const failed = f({ files_changed: ['a'], tests_run: ['t'], verification_result: 'fail' });
3337
+ const notVerified = f({ files_changed: ['a'], commands_run: ['c'], verification_result: null });
3338
+ const withErr = f({ files_changed: ['a'], tests_run: ['t'], errors: ['boom'], verification_result: 'pass' });
3339
+ const pure = ok.allowed === true && ok.reasons.length === 0
3340
+ && noFiles.allowed === false && noFiles.reasons.includes('no_files_changed')
3341
+ && noVerifyRun.allowed === false && noVerifyRun.reasons.includes('no_verification_run')
3342
+ && failed.allowed === false && failed.reasons.includes('verification_failed')
3343
+ && notVerified.allowed === false && notVerified.reasons.includes('not_verified')
3344
+ && withErr.allowed === false && withErr.reasons.includes('unresolved_errors');
3345
+ const src = read(__filename);
3346
+ const wired = src.includes('completion_claim_allowed: rec ? _completionClaimAllowed(rec) : null') // state show json
3347
+ && src.includes('completion_claim_allowed: _ccaH'); // handoff json + latest.json
3348
+ return pure && wired;
3349
+ } },
3315
3350
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3316
3351
  ];
3317
3352
  }
@@ -16527,7 +16562,7 @@ function stateCmd(root, sub, ...args) {
16527
16562
  if (note) rec.decisions = [...(rec.decisions || []), `verify(${result}): ${note}`];
16528
16563
  });
16529
16564
  if (!rec) return fail(`run 없음: ${curId}`);
16530
- if (json) { log(JSON.stringify({ verified: curId, result, run: rec }, null, 2)); return; }
16565
+ if (json) { log(JSON.stringify({ verified: curId, result, run: rec, completion_claim_allowed: _completionClaimAllowed(rec) }, null, 2)); return; }
16531
16566
  (result === 'pass' ? ok : warn)(`검증 결과: ${curId} → ${result}`);
16532
16567
  return;
16533
16568
  }
@@ -16542,19 +16577,22 @@ function stateCmd(root, sub, ...args) {
16542
16577
  if (!rec) return fail(`run 없음: ${curId}`);
16543
16578
  // 다음 에이전트가 읽을 latest handoff (json + md)
16544
16579
  const hdir = path.join(_leernessStateDir(root), 'handoff'); mkdirp(hdir);
16545
- writeUtf8(path.join(hdir, 'latest.json'), JSON.stringify(rec, null, 2) + '\n');
16580
+ // 1.9.443 (UR-0153): handoff evidence completion_claim_allowed 포함 — 다음 에이전트/PR 이 증거 게이트를 직접 읽음.
16581
+ const _ccaH = _completionClaimAllowed(rec);
16582
+ writeUtf8(path.join(hdir, 'latest.json'), JSON.stringify({ ...rec, completion_claim_allowed: _ccaH }, null, 2) + '\n');
16546
16583
  writeUtf8(path.join(hdir, 'latest.md'),
16547
- `# Handoff — ${rec.run_id}\n\n- task: ${rec.task_id || '-'}\n- agent: ${rec.agent_name || '-'} · model: ${rec.model_name || '-'}\n- goal: ${rec.goal || '-'}\n- files_changed: ${rec.files_changed.join(', ') || '-'}\n- tests_run: ${rec.tests_run.join(', ') || '-'}\n- verification: ${rec.verification_result || '-'}\n\n## Summary\n${rec.handoff_summary}\n`);
16584
+ `# Handoff — ${rec.run_id}\n\n- task: ${rec.task_id || '-'}\n- agent: ${rec.agent_name || '-'} · model: ${rec.model_name || '-'}\n- goal: ${rec.goal || '-'}\n- files_changed: ${rec.files_changed.join(', ') || '-'}\n- tests_run: ${rec.tests_run.join(', ') || '-'}\n- verification: ${rec.verification_result || '-'}\n- completion_claim_allowed: ${_ccaH.allowed ? 'yes' : 'no (' + _ccaH.reasons.join(', ') + ')'}\n\n## Summary\n${rec.handoff_summary}\n`);
16548
16585
  state.currentRunId = null; // 다음 에이전트가 새 run 시작
16549
16586
  _saveLeernessState(root, state);
16550
- if (json) { log(JSON.stringify({ handoff: rec.run_id, run: rec }, null, 2)); return; }
16587
+ if (json) { log(JSON.stringify({ handoff: rec.run_id, run: rec, completion_claim_allowed: _ccaH }, null, 2)); return; }
16551
16588
  ok(`handoff 작성: ${rec.run_id} → .leerness/handoff/latest.{md,json}`);
16552
16589
  return;
16553
16590
  }
16554
16591
 
16555
16592
  // default: show
16556
16593
  const rec = curId ? _loadRun(root, curId) : null;
16557
- if (json) { log(JSON.stringify({ state, currentRun: rec }, null, 2)); return; }
16594
+ // 1.9.443 (UR-0153): evidence-first 완료 게이트 파생 노출.
16595
+ if (json) { log(JSON.stringify({ state, currentRun: rec, completion_claim_allowed: rec ? _completionClaimAllowed(rec) : null }, null, 2)); return; }
16558
16596
  log(`# leerness state (1.9.278, UR-0032) — .leerness/ 구조화 상태`);
16559
16597
  log(`project: ${state.project} · runs 누적: ${state.runCounter || 0} · 현재 run: ${curId || '(없음)'}`);
16560
16598
  if (rec) {
@@ -16566,6 +16604,8 @@ function stateCmd(root, sub, ...args) {
16566
16604
  log(` commands_run: ${rec.commands_run.join(', ') || '-'}`);
16567
16605
  log(` tests_run: ${rec.tests_run.join(', ') || '-'} · verification: ${rec.verification_result || '-'}`);
16568
16606
  if (rec.decisions.length) log(` decisions: ${rec.decisions.length}`);
16607
+ const _cca = _completionClaimAllowed(rec); // 1.9.443 (UR-0153): 증거 기반 완료 주장 가능 여부
16608
+ log(` completion_claim_allowed: ${_cca.allowed ? 'yes ✓' : 'no ✗ (' + _cca.reasons.join(', ') + ')'}`);
16569
16609
  } else {
16570
16610
  log(` → 시작: leerness state start "<goal>" [--agent claude --model claude-opus-4-7 --task T-0001]`);
16571
16611
  }
@@ -19002,7 +19042,9 @@ async function main() {
19002
19042
  if (sub==='list') return planListCmd(absRoot(_resolveRoot(args[2])), { json: has('--json') }); // 1.9.412 (UR-0100): positional path 지원
19003
19043
  }
19004
19044
  if (cmd === 'task') {
19005
- const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'list';
19045
+ // 1.9.442 (UR-0141): positional path 지원 --path > path-like positional(_taskPositionalPath) > cwd.
19046
+ // 기존엔 arg('--path',cwd) 만 사용해 `task add "제목" ./경로` / `task list /경로` 가 무시되고 cwd 오염. 제목/ID/값-플래그값은 경로로 오인 안 함.
19047
+ const root = absRoot(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd()); const sub = args[1] || 'list';
19006
19048
  if (sub==='list') return taskList(root);
19007
19049
  if (sub==='add') {
19008
19050
  // 1.9.416 (UR-0122): flag/경로 break + 빈 입력 거부 (기존: args.slice(2).join(' ') 가 경로 흡수 + 빈 task 생성)
package/lib/pure-utils.js CHANGED
@@ -162,6 +162,22 @@ function _newRunRecord(opts = {}) {
162
162
  };
163
163
  }
164
164
 
165
+ // 1.9.443 (GPT-5.5 전략리뷰 §6.3/6.4, UR-0153): evidence-first 완료 게이트 — run-record 증거로 "완료 주장 가능" 여부 파생.
166
+ // 허용 조건: 변경 파일 존재 + 검증 실행(tests/commands) + 미해결 errors 0 + verification_result === 'pass'.
167
+ // verification 미실행/실패는 불허(증거 없는 완료 차단). reasons 로 불허 사유 명시. 순수 함수(저장 X, 읽을 때 계산).
168
+ function _completionClaimAllowed(rec) {
169
+ const r = rec || {};
170
+ const A = (x) => (Array.isArray(x) ? x : []);
171
+ const reasons = [];
172
+ if (A(r.files_changed).length === 0) reasons.push('no_files_changed');
173
+ if (A(r.tests_run).length === 0 && A(r.commands_run).length === 0) reasons.push('no_verification_run');
174
+ if (A(r.errors).length > 0) reasons.push('unresolved_errors');
175
+ const vr = String(r.verification_result || '').toLowerCase();
176
+ if (vr === 'fail') reasons.push('verification_failed');
177
+ else if (vr !== 'pass') reasons.push('not_verified');
178
+ return { allowed: reasons.length === 0, reasons };
179
+ }
180
+
165
181
  // 1.9.318 (UR-0025): 순수 HTML 파싱 유틸 (api-skill 문서 수집용) — fs/네트워크 의존 0, URL/regex 만 사용.
166
182
  function _htmlToText(html) {
167
183
  if (!html) return '';
@@ -1005,7 +1021,11 @@ module.exports = {
1005
1021
  // 1.9.407 (8번째 버그헌트, UR-0111): --limit 안전 파싱(NaN/음수/0 → 기본값)
1006
1022
  _parseLimit,
1007
1023
  // 1.9.416 (9th 외부평가, UR-0122): add 류 제목 파싱(flag/경로 break) 단일 출처
1008
- _parseAddTitle
1024
+ _parseAddTitle,
1025
+ // 1.9.442 (12th 외부평가, UR-0141): task 계열 positional path 안전 추출
1026
+ _taskPositionalPath,
1027
+ // 1.9.443 (GPT-5.5 전략리뷰 §6.3, UR-0153): evidence-first 완료 게이트
1028
+ _completionClaimAllowed
1009
1029
  };
1010
1030
 
1011
1031
  // 1.9.355 (UR-0075 Phase A): AI 에이전트용 크로스버전 마이그레이션 안전 워크플로 가이드 (순수 텍스트). 임시설치 + --path + 백업 + diff 검증.
@@ -1281,3 +1301,18 @@ function _parseAddTitle(args, startIdx = 0) {
1281
1301
  }
1282
1302
  return parts.join(' ').trim();
1283
1303
  }
1304
+
1305
+ // 1.9.442 (12th 외부평가 Sonnet UR-0141): task 계열 positional path 안전 추출.
1306
+ // _parseAddTitle 과 동일한 path-like 판정(선행 구분자 / ./ ../ C:\)으로 제목/ID/맨이름은 경로로 오인 안 함(src/auth 같은 내부 슬래시 제목 보호).
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']);
1309
+ function _taskPositionalPath(args, startIdx = 2) {
1310
+ const a = args || [];
1311
+ for (let i = startIdx; i < a.length; i++) {
1312
+ if (typeof a[i] !== 'string') continue;
1313
+ if (_TASK_VALUE_FLAGS.has(a[i - 1])) continue; // 값-플래그의 값(예: --evidence /abs) 은 경로 아님
1314
+ if (a[i].startsWith('-')) continue; // 플래그 자체 제외
1315
+ if (/^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(a[i])) return a[i]; // 선행 구분자 path-like 만
1316
+ }
1317
+ return null;
1318
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.441",
3
+ "version": "1.9.443",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -6111,5 +6111,47 @@ total++;
6111
6111
  if (!ok) failed++;
6112
6112
  }
6113
6113
 
6114
+ // 1.9.442 (12th 외부평가 Sonnet P1, UR-0141): task 계열 positional path — 다른 cwd 에서 실행해도 positional 경로에 저장(cwd 오염 차단).
6115
+ total++;
6116
+ {
6117
+ let ok = false;
6118
+ try {
6119
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-taskpos-'));
6120
+ const cwd2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cwd-'));
6121
+ cp.spawnSync(process.execPath, [CLI, 'init', ws, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6122
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', '포지셔널e2e', ws, '--no-review'], { encoding: 'utf8', timeout: 15000, cwd: cwd2 });
6123
+ const tracker = path.join(ws, '.harness', 'progress-tracker.md');
6124
+ const savedToWs = fs.existsSync(tracker) && fs.readFileSync(tracker, 'utf8').includes('포지셔널e2e');
6125
+ const cwdClean = !fs.existsSync(path.join(cwd2, '.harness'));
6126
+ fs.rmSync(ws, { recursive: true, force: true });
6127
+ fs.rmSync(cwd2, { recursive: true, force: true });
6128
+ ok = savedToWs && cwdClean;
6129
+ } catch {}
6130
+ console.log(ok ? '✓ B(1.9.442) UR-0141: task positional path 저장 + cwd 오염 차단' : '✗ task positional path 실패');
6131
+ if (!ok) failed++;
6132
+ }
6133
+
6134
+ // 1.9.443 (GPT-5.5 전략리뷰 §6.3, UR-0153): evidence-first 완료 게이트 — state 워크플로에서 completion_claim_allowed 파생.
6135
+ total++;
6136
+ {
6137
+ let ok = false;
6138
+ try {
6139
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cca-'));
6140
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6141
+ cp.spawnSync(process.execPath, [CLI, 'state', 'start', '목표', '--path', d], { encoding: 'utf8', timeout: 15000 });
6142
+ const before = cp.spawnSync(process.execPath, [CLI, 'state', 'show', '--json', '--path', d], { encoding: 'utf8', timeout: 15000 });
6143
+ cp.spawnSync(process.execPath, [CLI, 'state', 'record', '--files-changed', 'a.js', '--tests', 'npm test', '--path', d], { encoding: 'utf8', timeout: 15000 });
6144
+ cp.spawnSync(process.execPath, [CLI, 'state', 'verify', 'pass', '--path', d], { encoding: 'utf8', timeout: 15000 });
6145
+ const after = cp.spawnSync(process.execPath, [CLI, 'state', 'show', '--json', '--path', d], { encoding: 'utf8', timeout: 15000 });
6146
+ const bj = JSON.parse(before.stdout);
6147
+ const aj = JSON.parse(after.stdout);
6148
+ fs.rmSync(d, { recursive: true, force: true });
6149
+ ok = bj.completion_claim_allowed && bj.completion_claim_allowed.allowed === false
6150
+ && aj.completion_claim_allowed && aj.completion_claim_allowed.allowed === true;
6151
+ } catch {}
6152
+ console.log(ok ? '✓ B(1.9.443) UR-0153: evidence-first completion_claim_allowed (증거없음=no, 증거+pass=yes)' : '✗ completion_claim_allowed 실패');
6153
+ if (!ok) failed++;
6154
+ }
6155
+
6114
6156
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
6115
6157
  if (failed > 0) process.exit(1);