leerness 1.36.31 → 1.36.33

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.33 — 2026-07-15 — 데이터 보존 5종 — migrate --force 상태 보존 · positional path 클래스 소진 · symlink 미추종 · skill 충돌 가드 · settings 손상 중단 (codex 3차 헌트 + 자체 발견)
4
+
5
+ - **P1 — `migrate --force` 가 사용자 상태를 삭제** (codex 3차 #1): --force 가 progress-tracker/plan/task-log/decisions 등 상태 파일까지 새 템플릿으로 덮어써 활성 task 가 archive 에만 남았다(마커 실측 소실). → force 는 가이드/템플릿 문서에만 적용, 상태 파일 14종은 force 여도 보존(`_USER_STATE`). 실측: --force 후 task 잔존 1 · guideline 은 갱신. **1.36.28 의 마이그레이션 유실 전수 검증을 --force 경로까지 확장 완결.**
6
+ - **P2 — positional path 무시 클래스 8종 소진** (자체 발견 — `migrate-workspace-dir <path>` 가 positional 을 무시하고 cwd 를 마이그레이션해 **검증 중 소스 저장소가 실제로 오염**된 사고로 표면화): migrate-workspace-dir/lessons/resume/session-resume/commands/py-check/path-setup/capabilities 에 1.36.21/24 와 동일한 `_taskPositionalPath` 배선. 오염은 사본 제거로 정리(원본 .harness 불변).
7
+ - **P2 — 재귀 복사 symlink 폭주** (1.36.21 이연 #6): 자기참조 junction 이 있으면 copyRec 이 MAX_PATH 에러로 자기종결할 때까지 실복사(63중첩 실측). → `copyRec`/`copyRecursiveSafe` lstat 미추종(+skip 기록). 실측: 중첩 0 · 일반 파일 정상 복사.
8
+ - **P1 — skill install 무경고 덮어쓰기 + raw-name 우회** (codex 3차 #2): 정규화가 `../x` 의 ../ 를 벗겨 traversal 흔적을 지우고 기존 스킬을 조용히 교체. → raw name 의 경로 문자 선검증 + 내용 다른 충돌은 `--force` 요구(교체 전 .bak 백업, 동일 내용 재설치는 멱등).
9
+ - **P2 — auto-update install 이 손상 settings 를 hooks-only 로 교체** (codex 3차 #3, 1.36.28 손상-스토어 클래스): permissions 등 무관 설정 유실. → 파싱 실패 시 원본 보존 + 중단(exit 1). 정상 파일은 종전대로 병합.
10
+ - **검증**: selftest 303/303, 5건 전부 원 재현 before/after + 무회귀(--force 멱등/정상 병합/skill 멱등), exit 전파형 게이트 e2e, 게시본 클린룸.
11
+ - 이연(codex 3차 잔여): #4 tarball 공백경로 · #5 update 캐시 false-fresh + prerelease 비교 · #6 deps --run-tests false-green · #7 주석 트레이스 오인 · #8 policy exit · #9 creds null 스키마 · #10 session close 상태 별칭 — 다음 라운드.
12
+
13
+ ## 1.36.32 — 2026-07-15 — e2e 그린 복원 + 게이트 절차 정직 교정 (자기 감사)
14
+
15
+ **정정(정직 기록)**: 1.36.30/1.36.31 의 CHANGELOG 는 "게이트 e2e" 통과를 주장했지만 **사실이 아니었다** — 1.36.30 의 자동생성 전환(roadmap.html→leerness.html)으로 e2e 의 구식 단언 4건이 실패·크래시했는데, 게이트 러너 래퍼(`npm test > log; echo $?`)의 echo 가 exit code 를 0 으로 가려 두 릴리스가 e2e 실패 상태로 게시됐다(28/29 는 진짜 386/386). selftest 와 라이브 클린룸 실증은 두 릴리스 모두 실제로 통과했으므로 게시된 기능 자체는 검증돼 있었으나, e2e 전체 그린 주장은 과잉이었다.
16
+
17
+ - **e2e 단언 4건 현행화**: 자동생성 대상 단언을 leerness.html 로 (수동 `leerness roadmap` 의 roadmap.html 테스트 3건은 불변 — 여전히 유효한 기능).
18
+ - **게이트 절차 교정**: 러너에서 exit 마스킹 echo 제거 — npm test 의 exit 가 그대로 전파. 성공 판정에 "E2E result: N/N passed" 라인 존재를 필수 조건으로 추가.
19
+ - **검증**: 이번 릴리스는 전체 게이트(selftest 301 + e2e 전체)를 exit 전파형으로 통과한 것만 게시.
20
+
3
21
  ## 1.36.31 — 2026-07-15 — 동시성 직렬화 — 병렬 에이전트의 lost update 차단 (codex 미검토표면 헌트 이연분 #1/#2/#5/#7)
4
22
 
5
23
  1.36.28 헌트에서 이연했던 동시성 P1 계열. 기존 `_withLock`(원자 배타 생성 + stale 회수 + 재진입 안전)을 재사용 — 새 락 발명 아님.
package/README.md CHANGED
@@ -121,7 +121,7 @@ MIT
121
121
  <!-- leerness:project-readme:start -->
122
122
  ## Leerness Project Harness
123
123
 
124
- 이 프로젝트는 Leerness v1.36.31 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
124
+ 이 프로젝트는 Leerness v1.36.33 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
125
 
126
126
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
127
127
 
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
175
175
 
176
176
  ### MCP server (외부 AI 통합)
177
177
 
178
- Leerness v1.36.31는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
178
+ Leerness v1.36.33는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
179
179
 
180
180
  ```jsonc
181
181
  // 카테고리별
@@ -196,7 +196,7 @@ Leerness v1.36.31는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
196
196
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
197
197
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push → 7) session close → 8) 다음 라운드 예약.
198
198
 
199
- 현재 누적: **70 라운드 (1.9.40 → 1.36.31)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
199
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.33)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
200
200
 
201
201
  ### 성능 가이드
202
202
 
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
234
234
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
235
235
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
236
236
 
237
- Last synced by Leerness v1.36.31: 2026-07-15
237
+ Last synced by Leerness v1.36.33: 2026-07-15
238
238
  <!-- leerness:project-readme:end -->
239
239
 
package/bin/leerness.js CHANGED
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
34
34
  const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
35
35
  const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
36
36
 
37
- const VERSION = '1.36.31';
37
+ const VERSION = '1.36.33';
38
38
 
39
39
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
40
40
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -652,7 +652,8 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
652
652
  function copyRecursiveSafe(src, dst) {
653
653
  if (!exists(src)) return;
654
654
  if (src.includes(path.sep + '.harness' + path.sep + 'archive')) return;
655
- const st = fs.statSync(src);
655
+ const st = fs.lstatSync(src); // 1.36.33 (#6): symlink 미추종 — 자기참조 링크 시 MAX_PATH 까지 복제 폭주 방지
656
+ if (st.isSymbolicLink()) return;
656
657
  if (st.isDirectory()) {
657
658
  mkdirp(dst);
658
659
  for (const e of fs.readdirSync(src)) {
@@ -986,16 +987,20 @@ async function install(root, opts = {}) {
986
987
  };
987
988
  drawProgress(0, '');
988
989
  let _done = 0;
990
+ // 1.36.33 (codex 3차 #1, P1 데이터 유실): --force 가 사용자 상태 파일(진행 task/plan/결정/로그/인계 등)까지 새 템플릿으로
991
+ // 덮어써 활성 기록이 archive 에만 남았다. force 는 가이드/템플릿 문서에만 적용 — 상태 파일은 force 여도 보존.
992
+ const _USER_STATE = new Set(['.harness/progress-tracker.md', '.harness/plan.md', '.harness/task-log.md', '.harness/decisions.md', '.harness/current-state.md', '.harness/session-handoff.md', '.harness/rules.md', '.harness/feature-graph.md', '.harness/reuse-map.md', '.harness/feature-contracts.md', '.harness/project-brief.md', '.harness/architecture.md', '.harness/context-map.md', '.harness/design-system.md']);
989
993
  for (const [f, c] of Object.entries(files)) {
990
994
  const existsNow = exists(path.join(root, f));
991
995
  const mergeManaged = managedOverwrite.has(f);
996
+ const effForce = opts.force && !_USER_STATE.has(f);
992
997
  if (opts.dry) {
993
- const action = existsNow ? (mergeManaged || opts.force ? 'merge/update' : 'preserve') : 'create';
998
+ const action = existsNow ? (mergeManaged || effForce ? 'merge/update' : 'preserve') : 'create';
994
999
  log(`[dry-run] ${action}: ${f}`);
995
1000
  actions.push({ file:f, action });
996
1001
  continue;
997
1002
  }
998
- const r = writeIfSafe(root, f, c, { force: opts.force, mergeManaged, archiveDir: backup.archiveDir });
1003
+ const r = writeIfSafe(root, f, c, { force: effForce, mergeManaged, archiveDir: backup.archiveDir });
999
1004
  actions.push(r);
1000
1005
  _done++;
1001
1006
  drawProgress(_done, f);
@@ -1514,11 +1519,19 @@ async function skillInstallCmd(root, source) {
1514
1519
  if (!name) { fail('SKILL.md frontmatter에 `name` 필수'); return process.exit(1); }
1515
1520
  // .harness/skills/<id>/SKILL.md 저장
1516
1521
  // 1.9.350 (UR-0062 외부리뷰): skill id 정규화 — 선행/후행 ./- 제거 + .. 거부 + jail (path traversal 차단)
1522
+ // 1.36.33 (codex 3차 #2): 원문(raw) name 부터 검증 — 정규화가 '../x' 의 ../ 를 벗겨 'x' 로 만들어 traversal 흔적을 지웠다.
1523
+ if (/(^|[\\/])\.\.([\\/]|$)|[\\/]/.test(String(name))) { fail(`유효하지 않은 skill name (path traversal/경로 문자 차단): ${name}`); return process.exit(1); }
1517
1524
  const skillId = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
1518
1525
  if (!skillId || skillId.includes('..')) { fail(`유효하지 않은 skill id (path traversal 차단): ${name}`); return process.exit(1); }
1519
1526
  const _skillsRoot = path.join(root, '.harness', 'skills');
1520
1527
  const dir = path.join(_skillsRoot, skillId);
1521
1528
  if (!path.resolve(dir).startsWith(path.resolve(_skillsRoot) + path.sep)) { fail(`skill id jail 위반: ${name}`); return process.exit(1); }
1529
+ // 1.36.33 (codex 3차 #2): 같은 id 가 이미 설치돼 있고 내용이 다르면 무경고 덮어쓰기 금지 — --force 로만 교체(교체 전 .bak 백업).
1530
+ const _prevMd = path.join(dir, 'SKILL.md');
1531
+ if (exists(_prevMd) && read(_prevMd) !== body) {
1532
+ if (!has('--force')) { fail(`이미 설치된 skill: ${skillId} — 내용이 다릅니다. 교체하려면 --force (기존본은 SKILL.md.bak 백업)`); return process.exit(1); }
1533
+ try { fs.copyFileSync(_prevMd, _prevMd + '.bak'); } catch {}
1534
+ }
1522
1535
  mkdirp(dir);
1523
1536
  writeUtf8(path.join(dir, 'SKILL.md'), body);
1524
1537
  // skill.json도 함께 (자체 catalog 호환)
@@ -3118,6 +3131,25 @@ function _selfTestCases() {
3118
3131
  const pwWired = /_runPreWakeAudit[\s\S]{0,2200}\^\\\|\\s\*T-\\d\{4,\}\\s\*\\\|\\s\*in-progress/.test(s);
3119
3132
  return stateLock && teamLock && teamWired && both && pwWired;
3120
3133
  } },
3134
+ { name: 'positional path 잔존 클래스 소진 (1.36.33): migrate-workspace-dir 외 8종 — cwd 오염 차단 (소스가드, 실제 자기 저장소 오염 사고로 발견)', run: () => {
3135
+ const s = read(__filename);
3136
+ const P = "arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd()";
3137
+ const cmds = ["'migrate-workspace-dir'", "'lessons'", "'resume'", "'session-resume'", "'commands'", "'py-check'", "'path-setup' || cmd === 'path'", "'capabilities' || cmd === 'security-surface'"];
3138
+ return cmds.every(c => {
3139
+ const i = s.indexOf('if (cmd === ' + c + ')');
3140
+ if (i < 0) return false;
3141
+ return s.slice(i, s.indexOf('\n', i)).includes(P);
3142
+ });
3143
+ } },
3144
+ { name: '데이터 보존 3종 (1.36.33, codex 3차 #1/#2/#3): migrate --force 상태파일 보존 + skill 충돌/raw-name 가드 + settings 손상 중단 + symlink 미추종 (소스가드)', run: () => {
3145
+ const s = read(__filename);
3146
+ const forceGuard = s.includes("const effForce = opts.force && !_USER_STATE.has(f);") && s.includes("'.harness/progress-tracker.md', '.harness/plan.md', '.harness/task-log.md'");
3147
+ const skillGuard = s.includes('유효하지 않은 skill name (path traversal/경로 문자 차단)') && s.includes('이미 설치된 skill: ${skillId} — 내용이 다릅니다');
3148
+ const settingsGuard = s.includes('settings.local.json 이 손상돼(JSON 파싱 실패) hook 설치를 중단');
3149
+ const symlinkGuard = s.includes("if (stat.isSymbolicLink()) { report.skippedFiles.push(rel + ' (symlink — 따라가지 않음')") || s.includes('symlink — 따라가지 않음');
3150
+ const copySafe = /function copyRecursiveSafe[\s\S]{0,400}lstatSync/.test(s);
3151
+ return forceGuard && skillGuard && settingsGuard && symlinkGuard && copySafe;
3152
+ } },
3121
3153
  { name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
3122
3154
  const d = LENS_CATALOG.debug;
3123
3155
  if (!d) return false;
@@ -6165,7 +6197,10 @@ function _migrateWorkspaceDir(root, opts = {}) {
6165
6197
  const srcAbs = path.join(srcDir, rel);
6166
6198
  const dstAbs = path.join(dstDir, rel);
6167
6199
  let stat;
6168
- try { stat = fs.statSync(srcAbs); } catch (e) { report.errors.push(`stat ${rel}: ${e.message}`); return; }
6200
+ // 1.36.33 (#6): lstat symlink/junction 따라가지 않음 — 자기참조 링크(.harness →.harness) 있으면
6201
+ // 종전엔 MAX_PATH 에러로 자기종결할 때까지 수십 단계를 실제 복사(63중첩 실측)했다. 링크는 skip 기록.
6202
+ try { stat = fs.lstatSync(srcAbs); } catch (e) { report.errors.push(`stat ${rel}: ${e.message}`); return; }
6203
+ if (stat.isSymbolicLink()) { report.skippedFiles.push(rel + ' (symlink — 따라가지 않음)'); return; }
6169
6204
  if (stat.isDirectory()) {
6170
6205
  if (!dryRun) mkdirp(dstAbs);
6171
6206
  let entries;
@@ -16389,7 +16424,12 @@ function autoUpdateInstall(root) {
16389
16424
  mkdirp(settingsDir);
16390
16425
  const settingsFile = path.join(settingsDir, 'settings.local.json');
16391
16426
  let settings = {};
16392
- if (exists(settingsFile)) { try { settings = JSON.parse(read(settingsFile)); } catch {} }
16427
+ // 1.36.33 (codex 3차 #3, 1.36.28 손상-스토어 클래스): 손상 settings 객체로 오인해 hooks-only 로 덮어쓰면
16428
+ // 사용자의 permissions 등 무관 설정이 유실된다. 파싱 실패 시 원본 보존 + 중단.
16429
+ if (exists(settingsFile)) {
16430
+ try { settings = JSON.parse(read(settingsFile)); }
16431
+ catch { fail(`.claude/settings.local.json 이 손상돼(JSON 파싱 실패) hook 설치를 중단합니다 — 파일을 복구한 뒤 재시도: ${settingsFile}`); process.exitCode = 1; return; }
16432
+ }
16393
16433
  settings.hooks = settings.hooks || {};
16394
16434
  // 1.9.1 P1: legacy 'leerness-plus update' hook 자동 제거 (이전 fork 시절 잔재).
16395
16435
  let removedLegacy = 0, upgradedQuiet = 0;
@@ -21603,7 +21643,7 @@ async function main() {
21603
21643
  if (cmd === 'mcp' && args[1] === 'serve') return mcpServeCmd(absRoot(arg('--path', process.cwd())));
21604
21644
  if (cmd === 'gate') return gate(arg('--path', args[1] || process.cwd()));
21605
21645
  if (cmd === 'verify-code') return verifyCodeCmd(arg('--path', args[1] || process.cwd()));
21606
- if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd()));
21646
+ if (cmd === 'lessons') return lessonsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
21607
21647
  if (cmd === 'retro') return retroCmd(arg('--path', args[1] || process.cwd()));
21608
21648
  if (cmd === 'insights') return insightsCmd(arg('--path', args[1] || process.cwd()));
21609
21649
  if (cmd === 'brainstorm') return brainstormCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')).join(' '));
@@ -21612,14 +21652,14 @@ async function main() {
21612
21652
  // 1.9.201: next-action queue CLI (E축 9.5→10)
21613
21653
  if (cmd === 'next-action') return nextActionCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21614
21654
  // 1.9.203: leerness resume — auto-resume-plan 읽고 다음 라운드 즉시 안내 (사용자 명시)
21615
- if (cmd === 'resume') return resumeCmd(arg('--path', process.cwd()));
21655
+ if (cmd === 'resume') return resumeCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
21616
21656
  // 1.9.207: leerness requests <audit|add|list|complete|drop> — 사용자 요청 누락 확인 절차 (사용자 명시)
21617
21657
  // 1.36.24 (UR-0027 확장, 실피해 재현): requests 계열만 positional 경로 미지원이라 `requests add "text" C:/other/proj` 가
21618
21658
  // 조용히 cwd 프로젝트에 기록하고 성공 보고했다(task/rule 은 _taskPositionalPath 지원 → 불일치가 실수를 유도, 본인 피해 확인).
21619
21659
  // rule 선례와 동일 배선 — path-like(선행 구분자) 만 인정하는 기존 계약이라 텍스트/ID 오인 FP 없음(_parseAddTitle 이 add 텍스트를 path-like 에서 끊는 것도 기존과 동일).
21620
21660
  if (cmd === 'requests') return requestsCmd(arg('--path', null) || _taskPositionalPath(args, 2) || process.cwd(), args[1], ...args.slice(2));
21621
21661
  // 1.9.220: leerness session-resume — 비정상 종료 감지 + 자율 재개 가이드 (사용자 명시)
21622
- if (cmd === 'session-resume') return sessionResumeCmd(arg('--path', process.cwd()));
21662
+ if (cmd === 'session-resume') return sessionResumeCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
21623
21663
  // 1.9.226: leerness round-history — 자율 라운드 통계 + 다음 마일스톤
21624
21664
  if (cmd === 'round-history') return roundHistoryCmd(_resolveRoot(args[1]));
21625
21665
  // 1.9.229: leerness milestones — 도달 마일스톤 + 다음 ETA (1.9.226 확장)
@@ -21629,9 +21669,9 @@ async function main() {
21629
21669
  if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
21630
21670
  if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
21631
21671
  // 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
21632
- if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
21672
+ if (cmd === 'commands') return commandsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
21633
21673
  // 1.9.239: leerness py-check — Python 파일 분석 (사용자 명시 UR-0013)
21634
- if (cmd === 'py-check') return pyCheckCmd(arg('--path', process.cwd()));
21674
+ if (cmd === 'py-check') return pyCheckCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd());
21635
21675
  // 1.9.239: leerness agent-mode <start|tick|stop> — 자율 모드 전용 통합 명령 (사용자 명시 UR-0013)
21636
21676
  if (cmd === 'agent-mode') return agentModeCmd(arg('--path', process.cwd()), args[1]);
21637
21677
  // 1.9.241: leerness env summary|encoding — 환경 종합 + 셸 스크립트 인코딩 위험 (사용자 명시 UR-0014)
@@ -21641,7 +21681,7 @@ async function main() {
21641
21681
  return envCmd((args[2] && !args[2].startsWith('-')) ? args[2] : arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
21642
21682
  // 1.9.254: leerness path-setup [--apply] — CLI PATH 자동 등록 (사용자 명시 UR-0019)
21643
21683
  // npm global bin 경로 감지 + leerness 가 PATH 에서 찾아지는지 확인 → 미등록 시 플랫폼별 등록
21644
- if (cmd === 'path-setup' || cmd === 'path') return pathSetupCmd(arg('--path', process.cwd()), { apply: has('--apply'), json: has('--json') });
21684
+ if (cmd === 'path-setup' || cmd === 'path') return pathSetupCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), { apply: has('--apply'), json: has('--json') });
21645
21685
  // 1.9.258: leerness selftest — 설치된 바이너리 코어 함수 무결성 자가 검증 (CI/사용자 진단)
21646
21686
  if (cmd === 'selftest' || cmd === 'self-test') return selfTestCmd({ json: has('--json') });
21647
21687
  // 1.9.260: leerness shell-guard "<cmd>" — 터미널 명령 셸 호환성 린터 + 실패 메모리 (사용자 명시 UR-0020)
@@ -21658,7 +21698,7 @@ async function main() {
21658
21698
  // 1.9.270: leerness roles <list|set|unset|catalog|suggest|verify> — 모델별 역할 부여 (사용자 명시)
21659
21699
  if (cmd === 'roles' || cmd === 'role') return rolesCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21660
21700
  // 1.9.272: leerness capabilities — 권한/보안 표면 공개 (GPT-5.5 외부 리뷰 반영)
21661
- if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
21701
+ if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), { json: has('--json') });
21662
21702
  // 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
21663
21703
  if (cmd === 'state') return _guardStore(has('--json'), () => stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2))); // 1.36.28 (#3): 손상 스토어 클로버 차단
21664
21704
  if (cmd === 'context') return contextCmd(_resolveRoot(args[1]), { json: has('--json') });
@@ -21700,7 +21740,7 @@ async function main() {
21700
21740
  if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
21701
21741
  if (cmd === 'parent') return parentCmd(arg('--path', process.cwd()), args[1]);
21702
21742
  // 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
21703
- if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', process.cwd()));
21743
+ if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd()); // 1.36.33: positional path 무시 → cwd 오염 (1.36.21/24 계열 잔존 — 실제로 이 버그로 소스 저장소가 오염될 뻔)
21704
21744
  // 1.9.212: leerness idempotency audit — 멱등성 위반 탐지 (사용자 명시)
21705
21745
  if (cmd === 'idempotency') return idempotencyCmd(arg('--path', process.cwd()), args[1]);
21706
21746
  // 1.9.213: leerness intent <classify|expand|domains> — intent inference + scope expansion (사용자 명시)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.31",
3
+ "version": "1.36.33",
4
4
  "description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -2228,8 +2228,8 @@ total++;
2228
2228
  {
2229
2229
  const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-auto1-'));
2230
2230
  cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
2231
- const ok = fs.existsSync(path.join(tmpA, 'roadmap.html'));
2232
- console.log(ok ? '✓ B(1.9.12) install 직후 roadmap.html 자동 생성' : '✗ install 후 roadmap 없음');
2231
+ const ok = fs.existsSync(path.join(tmpA, 'leerness.html'));
2232
+ console.log(ok ? '✓ B(1.9.12) install 직후 leerness.html(온톨로지) 자동 생성 (1.36.30 전환)' : '✗ install 후 leerness.html 없음');
2233
2233
  if (!ok) failed++;
2234
2234
  }
2235
2235
 
@@ -2239,7 +2239,7 @@ total++;
2239
2239
  const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-auto2-'));
2240
2240
  cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
2241
2241
  // 첫 mtime 캡처
2242
- const f = path.join(tmpA, 'roadmap.html');
2242
+ const f = path.join(tmpA, 'leerness.html');
2243
2243
  const mt1 = fs.statSync(f).mtimeMs;
2244
2244
  // 시간 차이 보장
2245
2245
  const wait = Date.now() + 50; while (Date.now() < wait) {}
@@ -2248,7 +2248,7 @@ total++;
2248
2248
  cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpA], { stdio: 'ignore' });
2249
2249
  const mt2 = fs.statSync(f).mtimeMs;
2250
2250
  const ok = mt2 > mt1;
2251
- console.log(ok ? '✓ B(1.9.12) session close 후 roadmap.html 자동 갱신 (mtime 증가)' : `✗ session close 후 갱신 안 됨 mt1=${mt1} mt2=${mt2}`);
2251
+ console.log(ok ? '✓ B(1.9.12) session close 후 leerness.html 자동 갱신 (mtime 증가)' : `✗ session close 후 갱신 안 됨 mt1=${mt1} mt2=${mt2}`);
2252
2252
  if (!ok) failed++;
2253
2253
  }
2254
2254
 
@@ -2258,14 +2258,14 @@ total++;
2258
2258
  const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-auto3-'));
2259
2259
  cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
2260
2260
  cp.spawnSync(process.execPath, [CLI, 'roadmap', 'auto', 'off', '--path', tmpA], { stdio: 'ignore' });
2261
- const f = path.join(tmpA, 'roadmap.html');
2261
+ const f = path.join(tmpA, 'leerness.html');
2262
2262
  const mt1 = fs.statSync(f).mtimeMs;
2263
2263
  const wait = Date.now() + 80; while (Date.now() < wait) {}
2264
2264
  cp.spawnSync(process.execPath, [CLI, 'task', 'add', '비활성 후 추가', '--path', tmpA], { stdio: 'ignore' });
2265
2265
  cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpA], { stdio: 'ignore' });
2266
2266
  const mt2 = fs.statSync(f).mtimeMs;
2267
2267
  const ok = mt2 === mt1;
2268
- console.log(ok ? '✓ B(1.9.12) auto off: roadmap.html 갱신 안 됨' : `✗ auto off 후에도 갱신됨`);
2268
+ console.log(ok ? '✓ B(1.9.12) auto off: leerness.html 갱신 안 됨' : `✗ auto off 후에도 갱신됨`);
2269
2269
  if (!ok) failed++;
2270
2270
  }
2271
2271
 
@@ -2275,7 +2275,7 @@ total++;
2275
2275
  const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-auto4-'));
2276
2276
  cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
2277
2277
  cp.spawnSync(process.execPath, [CLI, 'roadmap', 'auto', 'on', '--on-every-change', '--path', tmpA], { stdio: 'ignore' });
2278
- const f = path.join(tmpA, 'roadmap.html');
2278
+ const f = path.join(tmpA, 'leerness.html');
2279
2279
  const mt1 = fs.statSync(f).mtimeMs;
2280
2280
  const wait = Date.now() + 80; while (Date.now() < wait) {}
2281
2281
  cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'on-every-change 테스트', '--path', tmpA], { stdio: 'ignore' });