leerness 1.9.349 → 1.9.351
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 +27 -0
- package/README.md +5 -5
- package/bin/harness.js +27 -9
- package/lib/catalogs.js +7 -0
- package/lib/pure-utils.js +3 -1
- package/package.json +1 -1
- package/scripts/e2e.js +56 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.351 — 2026-06-05 — 외부리뷰 UR-0064/0065: 제목 오염 차단 + 문서 정합
|
|
4
|
+
|
|
5
|
+
**🧩 외부 리뷰 남은 P1 2건 — decision/lesson 제목 오염 차단 + 문서 정합(AGENTS.md/--help). 외부리뷰 P0/P1 전량 해소.**
|
|
6
|
+
|
|
7
|
+
### 구현
|
|
8
|
+
1. **UR-0064 — 제목/텍스트 오염 차단** (Sonnet F-02): `decision add "T" --reason "R" /path` 처럼 `--flag value` 가 nonFlagArgs 로 제거된 뒤 남는 **경로형 positional** 이 title/text 에 흡수되던 문제. titleParts/textParts 루프가 절대/상대 경로 패턴(`C:\`, `/`, `./`, `../`)에서 break.
|
|
9
|
+
2. **UR-0065 — 문서 정합** (Sonnet F-04 + Codex#7):
|
|
10
|
+
- **AGENTS.md** `.leerness/` 모호성 해소 — 동적 상태(결정/교훈/계획/진행/인수인계)는 **기본 워크스페이스 `.harness/`** 에 기록됨을 명시. `leerness state` JSON substrate(`.leerness/`)는 별개(선택)임을 구분.
|
|
11
|
+
- **--help** 누락 명령 보강: context/health/doctor/pulse/milestones/round-history/decision/lesson/intent/constraints/requests/skill install/discover.
|
|
12
|
+
|
|
13
|
+
### 검증
|
|
14
|
+
- **selftest 98/98 PASS** · **E2E 295→296 PASS** (회귀 0).
|
|
15
|
+
- 실측: `decision add "PG 채택" --reason "관계형" /abs/leak` → title="PG 채택"(경로 미흡수) · lesson save 동일 · 생성 AGENTS.md ".harness 메인 + state substrate 별개" 명시 · `--help` 가 이전 누락 10+ 명령 노출.
|
|
16
|
+
|
|
17
|
+
## 1.9.350 — 2026-06-05 — 외부리뷰 P1 보안 하드닝 3종 (UR-0060/0061/0062)
|
|
18
|
+
|
|
19
|
+
**🔒 외부 멀티모델 리뷰 P1 보안 3건 일괄 수정 — 자율 팀(UR-0073) 전 보안 기반 다지기.**
|
|
20
|
+
|
|
21
|
+
### 구현
|
|
22
|
+
1. **UR-0062 — skill install path traversal 차단** (Codex#2): `SKILL.md name: ..` 가 `.harness/skills/<id>` 밖에 쓰이던 문제. id 정규화에 선행/후행 `./-` 제거 + `..` 거부 + jail(`dir.startsWith(skillsRoot+sep)`).
|
|
23
|
+
2. **UR-0061 — roadmap CSS 인젝션 차단** (Codex#3+Opus P3-1, 2모델): design token 값이 `_roadmapTokenStyles` 의 `:root{}` 에 raw 보간 → `}`/`</style>` breakout. CSS 값 whitelist 살균(`[^#a-zA-Z0-9(),.%\s_-]` 제거)으로 규칙/태그 탈출 차단, 색상/길이 보존.
|
|
24
|
+
3. **UR-0060 — 시크릿 스캐너 보강** (3모델): (a) 파일명 substring(`harness.js`/`secret-policy.md`) skip → **정확 경로** self-exclusion(`path.resolve(file)===__filename` + 생성 secret-policy 템플릿만) — 사용자 파일명 false-negative 제거. (b) SECRET_PATTERNS 13→**19**: GitLab PAT / JWT / DB-URI(임베디드 비밀번호) / SendGrid / AWS Secret Access Key(context) / Bearker 토큰 보강.
|
|
25
|
+
|
|
26
|
+
### 검증
|
|
27
|
+
- **selftest 98/98 PASS** · **E2E 295/295 PASS** (회귀 0).
|
|
28
|
+
- 실측: skill `name: ..` → "path traversal 차단"(`.harness/SKILL.md` 미생성) · 사용자 `harness.js` 의 GitLab PAT **이제 탐지**(false-neg 해소) · CSS `red;}</style><script>` → `<`/`>`/`}` 제거(breakout 차단)·`#2563eb` 보존 · 신규 6패턴 탐지 + clean(john_doe/example.com URL) 오탐 0.
|
|
29
|
+
|
|
3
30
|
## 1.9.349 — 2026-06-05 — 외부리뷰 UR-0063: selftest/doctor 위치독립화 (trust-killer)
|
|
4
31
|
|
|
5
32
|
**🔴 신규 사용자 첫인상 신뢰 직결 버그 수정 — `selftest`/`doctor` 가 비초기화 디렉토리에서 거짓 "설치 손상 → 재설치" 오보고하던 문제 해소.** (Opus 4.8 P1-1 + 웹 GPT-5.5 "가장 치명적" + 하네스 재현 = **3모델 교차검증**)
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
> **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.351 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.351는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.349는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
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) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.351)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.351: 2026-06-05
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -27,7 +27,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
27
27
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
28
28
|
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, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344 (UR-0025): SKILL_CATALOG_PRESETS 분리
|
|
29
29
|
|
|
30
|
-
const VERSION = '1.9.
|
|
30
|
+
const VERSION = '1.9.351';
|
|
31
31
|
|
|
32
32
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
33
33
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -443,7 +443,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
443
443
|
const project = detectProjectName(root);
|
|
444
444
|
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
445
445
|
const _files = {
|
|
446
|
-
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계 (1.9.282, UR-0035)\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n-
|
|
446
|
+
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계 (1.9.282, UR-0035)\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n- 동적 상태(결정/교훈/계획/진행/검증/인수인계)는 leerness 가 **기본 워크스페이스 \`.harness/\`** 에 기록한다 (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md). \`leerness handoff\` · \`decision add\` · \`lesson save\` 등이 여기에 쓴다.\n- (선택) \`leerness state show|start|record|verify|handoff\` (또는 MCP \`leerness_state_*\`) 의 JSON 상태 substrate 는 \`.leerness/\` (에이전트 간 인수인계 표준, 1.9.278 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (1.9.39+ 6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증합니다.\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍 (1.9.13)\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리 (1.9.8)\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용 (1.9.8)\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
|
|
447
447
|
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Always run \`leerness handoff .\` at the start and \`leerness session close .\` before ending a session.\n\n**⭐ 매 세션 첫 행동 (1.9.39+)**: \`.harness/session-workflow.md\`의 6단계 워크플로(요청분석→계획→분배→sub-agent→종합검증→마감)를 따라야 함. drift critical 시 \`leerness drift check --auto-fix\`로 자동 회복.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## 자연어 영구 룰 (1.9.8)\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
|
|
448
448
|
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .harness/context-routing.md.\nRun: \`leerness handoff .\` at session start.\nRun: \`leerness session close .\` at session end.\nPreserve Leerness protected files.\n`,
|
|
449
449
|
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
|
|
@@ -1530,8 +1530,12 @@ async function skillInstallCmd(root, source) {
|
|
|
1530
1530
|
const description = parsed.meta.description || '';
|
|
1531
1531
|
if (!name) { fail('SKILL.md frontmatter에 `name` 필수'); return process.exit(1); }
|
|
1532
1532
|
// .harness/skills/<id>/SKILL.md 저장
|
|
1533
|
-
|
|
1534
|
-
const
|
|
1533
|
+
// 1.9.350 (UR-0062 외부리뷰): skill id 정규화 — 선행/후행 ./- 제거 + .. 거부 + jail (path traversal 차단)
|
|
1534
|
+
const skillId = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '');
|
|
1535
|
+
if (!skillId || skillId.includes('..')) { fail(`유효하지 않은 skill id (path traversal 차단): ${name}`); return process.exit(1); }
|
|
1536
|
+
const _skillsRoot = path.join(root, '.harness', 'skills');
|
|
1537
|
+
const dir = path.join(_skillsRoot, skillId);
|
|
1538
|
+
if (!path.resolve(dir).startsWith(path.resolve(_skillsRoot) + path.sep)) { fail(`skill id jail 위반: ${name}`); return process.exit(1); }
|
|
1535
1539
|
mkdirp(dir);
|
|
1536
1540
|
writeUtf8(path.join(dir, 'SKILL.md'), body);
|
|
1537
1541
|
// skill.json도 함께 (자체 catalog 호환)
|
|
@@ -3041,12 +3045,14 @@ function _selfTestCases() {
|
|
|
3041
3045
|
{ name: 'UR-0058: lessons canonical JSON 레이어(_loadLessons/_saveLessons/lessonsJsonPath) + pure 파서/렌더 round-trip', run: () => { const m = require('../lib/pure-utils'); const objs = [{ date: '2026-06-05', text: 'A', tag: 't' }, { date: '2026-06-04', text: 'B', tag: null }]; const rt = m._parseLessonEntries(m._renderLessonsMd(objs)); const rtOk = JSON.stringify(rt) === JSON.stringify(objs); const tplOk = m._parseLessonEntries(m._renderLessonsMd([])).length === 0; const layerOk = typeof _loadLessons === 'function' && typeof _saveLessons === 'function' && typeof lessonsJsonPath === 'function' && _renderLessonsMd === m._renderLessonsMd; return rtOk && tplOk && layerOk; } },
|
|
3042
3046
|
{ name: 'UR-0025 심층: BUILTIN_CATALOG→lib/catalogs + _withBuiltinSource→pure-utils 분리 (1.9.341)', run: () => { const c = require('../lib/catalogs'); const m = require('../lib/pure-utils'); const catOk = c.BUILTIN_CATALOG && Object.keys(c.BUILTIN_CATALOG).length === 9 && c.BUILTIN_CATALOG.office && c.BUILTIN_CATALOG.office.version === '1.0.0'; const out = m._withBuiltinSource(c.BUILTIN_CATALOG); const work = Object.keys(out).length === 9 && Object.values(out).every(v => v._source === 'builtin') && out.office.version === '1.0.0' && Array.isArray(out.office.capabilities) && Object.keys(m._withBuiltinSource(null)).length === 0; const src = read(__filename); const moved = BUILTIN_CATALOG === c.BUILTIN_CATALOG && _withBuiltinSource === m._withBuiltinSource && !/const BUILTIN_CATALOG = \{/.test(src); return catOk && work && moved; } },
|
|
3043
3047
|
{ name: 'UR-0025 심층: ROADMAP_STATUS_LABEL/COLOR→lib/catalogs 분리 (1.9.342)', run: () => { const c = require('../lib/catalogs'); const lblOk = c.ROADMAP_STATUS_LABEL && Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_LABEL.blocked === '오류'; const colOk = c.ROADMAP_STATUS_COLOR && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11 && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6'; const src = read(__filename); const moved = ROADMAP_STATUS_LABEL === c.ROADMAP_STATUS_LABEL && ROADMAP_STATUS_COLOR === c.ROADMAP_STATUS_COLOR && !/const ROADMAP_STATUS_LABEL = \{/.test(src); return lblOk && colOk && moved; } },
|
|
3044
|
-
{ name: 'UR-0025 심층: SECRET_PATTERNS→lib/catalogs 보안 응집 분리 (1.9.343)', run: () => { const c = require('../lib/catalogs'); const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length ===
|
|
3048
|
+
{ name: 'UR-0025 심층: SECRET_PATTERNS→lib/catalogs 보안 응집 분리 (1.9.343)', run: () => { const c = require('../lib/catalogs'); const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 19 && c.SECRET_PATTERNS.every(p => p.name && p.re instanceof RegExp); const A = 'A'.repeat(40); const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const det = hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";'); const moved = SECRET_PATTERNS === c.SECRET_PATTERNS; return catOk && det && moved; } },
|
|
3045
3049
|
{ name: 'UR-0025 심층: SKILL_CATALOG_PRESETS→lib/catalogs 분리 (1.9.344)', run: () => { const c = require('../lib/catalogs'); const catOk = c.SKILL_CATALOG_PRESETS && Object.keys(c.SKILL_CATALOG_PRESETS).length === 2 && c.SKILL_CATALOG_PRESETS.vercel && c.SKILL_CATALOG_PRESETS.vercel.owner === 'vercel-labs' && c.SKILL_CATALOG_PRESETS.anthropic && c.SKILL_CATALOG_PRESETS.anthropic.repo === 'skills'; const moved = SKILL_CATALOG_PRESETS === c.SKILL_CATALOG_PRESETS; return catOk && moved; } },
|
|
3046
3050
|
{ name: 'UR-0025 심층: _esc(HTML escape)→pure-utils 분리 + XSS 가드 (1.9.345)', run: () => { const m = require('../lib/pure-utils'); const work = m._esc('&<>"\'') === '&<>"'' && m._esc('<script>x</script>') === '<script>x</script>' && m._esc(null) === '' && m._esc(undefined) === '' && m._esc(42) === '42'; const moved = _esc === m._esc; return work && moved; } },
|
|
3047
3051
|
{ name: 'UR-0025 심층: _roadmapTokenStyles→pure-utils 분리 (1.9.346)', run: () => { const m = require('../lib/pure-utils'); const out = m._roadmapTokenStyles({ 'color.primary': '#2563eb' }, { 'color-surface': '#fff', 'custom': '#abc' }); const work = out.startsWith(':root {') && out.includes('--lr-primary: #2563eb') && out.includes('--lr-surface: #fff') && out.includes('--lr-custom: #abc') && out.includes('--lr-card-bg') && m._roadmapTokenStyles(null, null).startsWith(':root {'); const moved = _roadmapTokenStyles === m._roadmapTokenStyles; return work && moved; } },
|
|
3048
3052
|
{ name: 'UR-0025 심층: _parseSkillMd(SKILL.md frontmatter, BOM-aware)→pure-utils 분리 (1.9.347)', run: () => { const m = require('../lib/pure-utils'); const r = m._parseSkillMd('---\nname: s1\ndescription: "d1"\n---\nbody'); const work = r.meta.name === 's1' && r.meta.description === 'd1' && r.body === 'body' && m._parseSkillMd('---\nname: b\n---\nx').meta.name === 'b' && Object.keys(m._parseSkillMd('plain text').meta).length === 0 && m._parseSkillMd('plain text').body === 'plain text' && m._parseSkillMd(null).body === ''; const moved = _parseSkillMd === m._parseSkillMd; return work && moved; } },
|
|
3049
3053
|
{ name: 'UR-0059(외부리뷰 P0): --path 라우팅 일관화 — bare args[N]||cwd 제거 + arg(--path) wrap', run: () => { const src = read(__filename); const bare1 = '(args[1] ' + '|| process.cwd())'; const bare2 = '(args[2] ' + '|| process.cwd())'; const noBare = !src.includes(bare1) && !src.includes(bare2); const wrapped = src.includes('arg(' + "'--path', args[1] || process.cwd())") && src.includes('arg(' + "'--path', args[2] || process.cwd())"); const argDefault = arg('--definitely-not-real-xyz', 'SENT') === 'SENT'; return noBare && wrapped && argDefault; } },
|
|
3054
|
+
{ name: 'UR-0061(외부리뷰 P1): roadmap CSS 값 살균 — :root/</style> breakout 차단', run: () => { const m = require('../lib/pure-utils'); const css = m._roadmapTokenStyles({ 'color.primary': 'red;}' + '</style><script>alert(1)</script>' }, {}); const blocked = !css.includes('<') && !css.includes('>'); const primaryLine = (css.split('\n').find(l => l.includes('--lr-primary')) || ''); const noBreakout = !primaryLine.replace(/;$/, '').includes('}'); const preserved = m._roadmapTokenStyles({ 'color.primary': '#2563eb' }, {}).includes('--lr-primary: #2563eb'); return blocked && noBreakout && preserved; } },
|
|
3055
|
+
{ name: 'UR-0060(외부리뷰 P1): SECRET_PATTERNS 19종 — GitLab/JWT/DB-URI/SendGrid/AWS-secret/Bearer 보강 + 오탐 가드', run: () => { const c = require('../lib/catalogs'); const hit = s => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const det = hit('glpat-' + 'x'.repeat(20)) && hit('eyJ' + 'x'.repeat(15) + '.eyJ' + 'y'.repeat(15) + '.' + 'z'.repeat(15)) && hit('postgres://u:p@host:5432/db') && hit('SG.' + 'x'.repeat(22) + '.' + 'y'.repeat(43)) && hit('aws_secret_access_key = "' + 'x'.repeat(40) + '"') && hit('Bearer ' + 'x'.repeat(25)); const clean = !hit('const u = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return c.SECRET_PATTERNS.length === 19 && det && clean; } },
|
|
3050
3056
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3051
3057
|
];
|
|
3052
3058
|
}
|
|
@@ -7099,11 +7105,12 @@ function scanSecrets(root) {
|
|
|
7099
7105
|
try { text = read(file); } catch { continue; }
|
|
7100
7106
|
if (text.length > 1024 * 1024) continue;
|
|
7101
7107
|
const fileRel = rel(root, file);
|
|
7108
|
+
// 1.9.350 (UR-0060 외부리뷰): leerness 자기 harness.js(regex 소스) + 생성 secret-policy 템플릿만 제외 — 정확 경로(사용자 파일명 substring false-negative 제거)
|
|
7109
|
+
if (path.resolve(file) === path.resolve(__filename) || /(^|[\\/])\.(?:harness|leerness)[\\/]secret-policy\.md$/.test(fileRel)) continue;
|
|
7102
7110
|
for (const { name, re } of SECRET_PATTERNS) {
|
|
7103
7111
|
re.lastIndex = 0;
|
|
7104
7112
|
let m;
|
|
7105
7113
|
while ((m = re.exec(text))) {
|
|
7106
|
-
if (fileRel.includes('harness.js') || fileRel.includes('secret-policy.md')) break;
|
|
7107
7114
|
const line = text.slice(0, m.index).split('\n').length;
|
|
7108
7115
|
findings.push({ file: fileRel, line, name, snippet: m[0].slice(0, 32) });
|
|
7109
7116
|
break;
|
|
@@ -20927,6 +20934,15 @@ function help() {
|
|
|
20927
20934
|
leerness lazy detect [path] [--auto-track] # --auto-track으로 새 TODO를 progress에 자동 등록 (1.9.7)
|
|
20928
20935
|
leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish # 사용자 룰 등록 (1.9.8)
|
|
20929
20936
|
leerness rule list|verify|pause <id>|resume <id>|remove <id>|stop|resume-all
|
|
20937
|
+
leerness context [path] [--json] # 에이전트 온보딩 컨텍스트 (1.9.292, UR-0065)
|
|
20938
|
+
leerness health [path] [--json] · leerness doctor [path] # 설치/상태 진단
|
|
20939
|
+
leerness pulse [path] · leerness milestones [path] · leerness round-history [path] # 종합 지표
|
|
20940
|
+
leerness decision add "<title>" [--reason ...] | decision list|drop <id> # 결정 메모리 (.harness, canonical JSON)
|
|
20941
|
+
leerness lesson save "<text>" [--tag t] | lesson list|drop <date|text> # 교훈 메모리
|
|
20942
|
+
leerness intent classify|expand|domains "<text>" # 의도 추론 + scope 확장 게이트 (1.9.213)
|
|
20943
|
+
leerness constraints list|check|add "<text>" # 플랫폼/API 제약 사전 점검 (1.9.208)
|
|
20944
|
+
leerness requests audit|add|list|complete|drop # 사용자 요청 추적 UR-XXXX (1.9.207)
|
|
20945
|
+
leerness skill install <SKILL.md|dir|url> · leerness skill discover --preset vercel|anthropic # 스킬 설치/탐색
|
|
20930
20946
|
leerness release bump [--patch|--minor|--major] # package.json 자동 bump (1.9.8)
|
|
20931
20947
|
leerness release note "<내용>" # CHANGELOG.md 자동 추가 (1.9.8)
|
|
20932
20948
|
leerness release publish [--dry-run] [--pack] [--git-push] [--gh-release] [--gh-pages] [--gh-pages-src file] [--npm-publish] [--auto] # 통합 배포 (1.9.8 + 1.9.10)\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n`);
|
|
@@ -21251,7 +21267,8 @@ async function main() {
|
|
|
21251
21267
|
if (sub === 'save') {
|
|
21252
21268
|
const textParts = [];
|
|
21253
21269
|
for (let i = 2; i < args.length; i++) {
|
|
21254
|
-
|
|
21270
|
+
// 1.9.351 (UR-0064 외부리뷰): --flag value 가 nonFlagArgs 로 제거되어 남는 경로형 positional 이 text 에 흡수되는 것 차단
|
|
21271
|
+
if (args[i].startsWith('--') || /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(args[i])) break;
|
|
21255
21272
|
textParts.push(args[i]);
|
|
21256
21273
|
}
|
|
21257
21274
|
return lessonSave(root, textParts.join(' '));
|
|
@@ -21275,10 +21292,11 @@ async function main() {
|
|
|
21275
21292
|
if (cmd === 'decision') {
|
|
21276
21293
|
const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || '';
|
|
21277
21294
|
if (sub === 'add') {
|
|
21278
|
-
// args[2..] 가 title (단, --flag
|
|
21295
|
+
// args[2..] 가 title (단, --flag 또는 경로형 positional 이 시작되기 전까지)
|
|
21279
21296
|
const titleParts = [];
|
|
21280
21297
|
for (let i = 2; i < args.length; i++) {
|
|
21281
|
-
|
|
21298
|
+
// 1.9.351 (UR-0064 외부리뷰): --flag value 제거 후 남는 경로형 positional 이 title 에 흡수되는 것 차단
|
|
21299
|
+
if (args[i].startsWith('--') || /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(args[i])) break;
|
|
21282
21300
|
titleParts.push(args[i]);
|
|
21283
21301
|
}
|
|
21284
21302
|
return decisionAdd(root, titleParts.join(' '));
|
package/lib/catalogs.js
CHANGED
|
@@ -377,6 +377,13 @@ const SECRET_PATTERNS = [
|
|
|
377
377
|
{ name: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
378
378
|
{ name: 'Generic private key', re: /-----BEGIN (?:RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/g },
|
|
379
379
|
{ name: 'Hardcoded password assignment', re: /\b(?:password|passwd|pwd|secret|api_key|apikey)\s*[:=]\s*["'][^"'\s]{6,}["']/gi },
|
|
380
|
+
// 1.9.350 (UR-0060 외부리뷰 3모델): 누락 패턴 보강
|
|
381
|
+
{ name: 'GitLab PAT', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
|
|
382
|
+
{ name: 'JWT', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
383
|
+
{ name: 'DB connection string (embedded password)', re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:[^@\s/]+@/gi },
|
|
384
|
+
{ name: 'SendGrid API key', re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g },
|
|
385
|
+
{ name: 'AWS Secret Access Key (context)', re: /\baws[^\n]{0,40}?(?:secret_access_key|secret_key|secret)[^\n]{0,12}?["']?[A-Za-z0-9/+]{40}["']?/gi },
|
|
386
|
+
{ name: 'Hardcoded Bearer token', re: /\bBearer\s+[A-Za-z0-9_\-.=]{20,}/g },
|
|
380
387
|
];
|
|
381
388
|
|
|
382
389
|
// 1.9.344 (UR-0025 심층): skill discover GitHub preset catalog (vercel/anthropic) — harness 에서 분리.
|
package/lib/pure-utils.js
CHANGED
|
@@ -412,7 +412,9 @@ function _roadmapTokenStyles(designTokens, cssVariables) {
|
|
|
412
412
|
if (!vars['lr-card-bg']) vars['lr-card-bg'] = vars['lr-surface'] || '#ffffff';
|
|
413
413
|
if (!vars['lr-edge']) vars['lr-edge'] = vars['lr-muted'] || '#cbd5e1';
|
|
414
414
|
if (!vars['lr-page-bg']) vars['lr-page-bg'] = '#f8fafc';
|
|
415
|
-
|
|
415
|
+
// 1.9.350 (UR-0060/0061 외부리뷰): CSS 값 살균 — whitelist 로 } < > ; { @ : / 등 제거(:root 규칙 breakout + </style> HTML 탈출 차단). 색상/길이 형식은 보존.
|
|
416
|
+
const _safeCss = v => String(v == null ? '' : v).replace(/[^#a-zA-Z0-9(),.%\s_-]/g, '').slice(0, 80);
|
|
417
|
+
return ':root {\n' + Object.entries(vars).map(([k, v]) => ` --${k}: ${_safeCss(v)};`).join('\n') + '\n }';
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
// 1.9.347 (UR-0025 심층): SKILL.md frontmatter 파서 — { meta, body }, BOM-aware (Windows Notepad 호환). 순수.
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -4434,7 +4434,7 @@ total++;
|
|
|
4434
4434
|
const c = require(path.resolve(__dirname, '..', 'lib', 'catalogs.js'));
|
|
4435
4435
|
const A = 'A'.repeat(40);
|
|
4436
4436
|
const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); });
|
|
4437
|
-
const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length ===
|
|
4437
|
+
const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 19
|
|
4438
4438
|
&& hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";');
|
|
4439
4439
|
const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
|
|
4440
4440
|
const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
|
|
@@ -4613,5 +4613,60 @@ total++;
|
|
|
4613
4613
|
if (!ok) failed++;
|
|
4614
4614
|
}
|
|
4615
4615
|
|
|
4616
|
+
// 1.9.350 회귀 (외부리뷰 P1 보안 하드닝): UR-0061 CSS breakout 차단 + UR-0062 skill traversal 차단 + UR-0060 scan false-neg/패턴 보강
|
|
4617
|
+
total++;
|
|
4618
|
+
{
|
|
4619
|
+
let ok = false;
|
|
4620
|
+
try {
|
|
4621
|
+
const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
|
|
4622
|
+
// UR-0061: roadmap CSS 값 살균 (순수)
|
|
4623
|
+
const css = m._roadmapTokenStyles({ 'color.primary': 'red;}' + '</style><script>x</script>' }, {});
|
|
4624
|
+
const cssOk = !css.includes('<') && !css.includes('>') && css.includes('--lr-primary:');
|
|
4625
|
+
// UR-0062: skill install name:.. traversal 차단 (end-to-end)
|
|
4626
|
+
const sd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-trav-'));
|
|
4627
|
+
cp.spawnSync(process.execPath, [CLI, 'init', sd, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
4628
|
+
const skf = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skf-'));
|
|
4629
|
+
fs.writeFileSync(path.join(skf, 'SKILL.md'), '---\nname: ..\ndescription: t\n---\n# b');
|
|
4630
|
+
const ir = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skf, '--path', sd], { encoding: 'utf8', timeout: 20000 });
|
|
4631
|
+
const travBlocked = /traversal|유효하지 않은 skill id|jail/.test((ir.stdout || '') + (ir.stderr || '')) && !fs.existsSync(path.join(sd, '.harness', 'SKILL.md'));
|
|
4632
|
+
fs.rmSync(skf, { recursive: true, force: true });
|
|
4633
|
+
// UR-0060: 사용자 harness.js 파일도 스캔(false-neg 제거) + 신규 GitLab 패턴 탐지
|
|
4634
|
+
const ud = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-scan-'));
|
|
4635
|
+
fs.writeFileSync(path.join(ud, 'harness.js'), 'const k = "glpat-' + 'x'.repeat(20) + '";\n');
|
|
4636
|
+
const scr = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', ud], { encoding: 'utf8', timeout: 20000 });
|
|
4637
|
+
const scanOk = /GitLab PAT/.test((scr.stdout || '') + (scr.stderr || ''));
|
|
4638
|
+
fs.rmSync(ud, { recursive: true, force: true });
|
|
4639
|
+
fs.rmSync(sd, { recursive: true, force: true });
|
|
4640
|
+
ok = cssOk && travBlocked && scanOk;
|
|
4641
|
+
} catch {}
|
|
4642
|
+
console.log(ok ? '✓ B(1.9.350) 외부리뷰 P1보안: CSS breakout/skill traversal 차단 + secret scan false-neg/패턴 보강 (UR-0060/0061/0062)' : '✗ P1 보안 하드닝 실패');
|
|
4643
|
+
if (!ok) failed++;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
// 1.9.351 회귀 (외부리뷰 UR-0064/0065): decision/lesson 제목 오염 차단 + 문서 정합(AGENTS.md .harness / --help 누락 명령)
|
|
4647
|
+
total++;
|
|
4648
|
+
{
|
|
4649
|
+
let ok = false;
|
|
4650
|
+
try {
|
|
4651
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-doc-'));
|
|
4652
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
4653
|
+
// UR-0064: decision add 제목에 경로형 positional 미흡수
|
|
4654
|
+
cp.spawnSync(process.execPath, [CLI, 'decision', 'add', 'PG 채택', '--reason', '관계형', '/abs/leak/path', '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
4655
|
+
const dj = JSON.parse(fs.readFileSync(path.join(d, '.harness', 'decisions.json'), 'utf8'));
|
|
4656
|
+
const titleOk = dj.length === 1 && dj[0].title === 'PG 채택' && !dj[0].title.includes('/abs/leak');
|
|
4657
|
+
// UR-0065 AGENTS.md: .harness 메인 워크스페이스 명시 + state substrate 별개
|
|
4658
|
+
const agents = fs.readFileSync(path.join(d, 'AGENTS.md'), 'utf8');
|
|
4659
|
+
const agentsOk = agents.includes('기본 워크스페이스 `.harness/`') && agents.includes('메인 워크스페이스(.harness)와 별개');
|
|
4660
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
4661
|
+
// UR-0065 --help: 이전 누락 명령 노출
|
|
4662
|
+
const hr = cp.spawnSync(process.execPath, [CLI, '--help'], { encoding: 'utf8', timeout: 20000 });
|
|
4663
|
+
const hout = (hr.stdout || '') + (hr.stderr || '');
|
|
4664
|
+
const helpOk = ['leerness context', 'leerness health', 'leerness intent', 'leerness constraints', 'leerness requests', 'leerness skill install'].every(c => hout.includes(c));
|
|
4665
|
+
ok = titleOk && agentsOk && helpOk;
|
|
4666
|
+
} catch {}
|
|
4667
|
+
console.log(ok ? '✓ B(1.9.351) 외부리뷰 UR-0064/0065: 제목 오염 차단 + AGENTS.md/--help 정합 (UR-0064/0065)' : '✗ UR-0064/0065 실패');
|
|
4668
|
+
if (!ok) failed++;
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4616
4671
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
4617
4672
|
if (failed > 0) process.exit(1);
|