leerness 1.36.58 → 1.36.60
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 +20 -0
- package/README.md +4 -4
- package/bin/leerness.js +58 -9
- package/lib/pure-utils.js +19 -4
- package/package.json +1 -1
- package/scripts/e2e.js +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.60 — 2026-07-21 — 언어 전환 현지화 병합 (F-05 2회차, 전 검수 High 해소) — R-0001 검수 5회전
|
|
4
|
+
|
|
5
|
+
KO→EN(역방향 포함) 전환 시 지시 레이어가 새 언어로 재생성되고 사용자 커스텀만 정확히 1회 이월된다.
|
|
6
|
+
|
|
7
|
+
- **차감 병합**: _managedMerge 에 altTemplate — 구 언어 canonical 라인은 커스텀이 아니므로 미이월 (종전: KO 템플릿 통째 Preserved 이월). session-workflow.md managed 편입(전환 시에도 낡은 언어로 방치되던 것). 보존 래퍼 문구 현지화(en 프로젝트에 한글 래퍼가 새 한글원이 되던 것).
|
|
8
|
+
- **검수 2차 High 반영 — 전환-감지 게이트 + 출현-횟수 차감**: 무조건 차감은 동일-언어 재init 에서 "반대-언어 템플릿과 우연히 일치하는 정당한 커스텀"을 유실시켰다(실측) — 이전 저장 언어를 포착해 실제 전환일 때만 차감 + Set 대신 출현 1회분만 소비(복제 커스텀 생존).
|
|
9
|
+
- (검수 #2) adapter 경로에 래퍼 언어 전달(전환-감지 없는 경로라 altTemplate 은 의도적 미적용). (검수 #3) 구버전 workflow 의 자동생성 이력 로그(MCP 도구-카운트/마일스톤 불릿)는 좁은 시그니처로 필터 — 낡은 모순 잔재 방지, 사용자 산문 보존.
|
|
10
|
+
- 실측: 동일-언어 커스텀 생존 / 전환 차감 유지(잔존 한글=커스텀 1건) / 이력 필터+커스텀 보존. selftest 332(+1), 검수 통과 프로브(migrate/update 동일 경로·EN 래퍼 혼재 무중복·5종 바이트 멱등).
|
|
11
|
+
|
|
12
|
+
## 1.36.59 — 2026-07-21 — en 지시-레이어 완전 번역 (외부감사 F-05 시리즈 1회차) — R-0001 검수 4회전
|
|
13
|
+
|
|
14
|
+
실측 베이스라인: en init 산출물 61개 중 한글 잔존 53개. 1회차 = en 에이전트가 "지시"로 읽는 최고 영향 5종.
|
|
15
|
+
|
|
16
|
+
- **en 완역 5종**: AGENTS.md · CLAUDE.md · session-workflow.md · .cursor/leerness.mdc · copilot-instructions.md — 한글 0 실측. frontmatter(doNotStore)까지 영어(fm lang 인자). ko 무회귀.
|
|
17
|
+
- (검수 #2) **계약 동등성 보강**: 검수가 KO 대비 누락 6종 적발 — lens 자기질문 원문/brainstorm 트리거/5세션 회고/rule list 안내/push 클래스/release bump·note·publish 자동화 지시 전부 EN 반영 + workflow 에 복구 절차·자동회복/보안·빠른 체크리스트·안티패턴 섹션 추가 (이력성 도구-카운트 로그는 드리프트 소음이라 의도적 비이식).
|
|
18
|
+
- (검수 #3) **integrity 펜스-H1 오인 수정**: 코드펜스 안 `# 주석`을 H1 로 인정해 H1 상실을 놓치던 것 — 펜스 밖 라인만 검사.
|
|
19
|
+
- (검수 #4) e2e 강화: spawn status 단언 + en 재init 커스텀 1회 생존 + 계약 존재 검사 확대 + finally 정리.
|
|
20
|
+
- **이연(정직, 시리즈 2회차)**: (검수 #1 High) KO→EN 전환/마이그레이션 시 지시 레이어 현지화(구 템플릿 차감 병합 + session-workflow managed 편입 + 보존 래퍼 문구 현지화) — 기존 동작 대비 비회귀(HEAD 동일)이므로 신규 en init 한정으로 이번 출하. 잔여 한글 표면 ~50개(정책문서/AX 가이드/commands)도 후속.
|
|
21
|
+
- 검증: en 5종 한글 0 + ko 보존 + 커스텀 보존 실측, selftest 331, e2e +1 강화판.
|
|
22
|
+
|
|
3
23
|
## 1.36.58 — 2026-07-21 — MCP core 프로필 (외부감사 F-09 종결) — R-0001 검수 3회전
|
|
4
24
|
|
|
5
25
|
86종 전체 tools/list(약 45KB)가 세션 시작 토큰/도구 선택 혼선을 키우는 클라이언트용 경량 프로필.
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.60 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.60는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.58는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.60 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,6 +235,6 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.60: 2026-07-22
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
|
240
240
|
|
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.
|
|
37
|
+
const VERSION = '1.36.60';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -285,8 +285,10 @@ function _uiLang(root) {
|
|
|
285
285
|
}
|
|
286
286
|
// ko/en 쌍에서 해석된 UI 언어로 선택 (Phase 1: 첫 화면 한정 사용).
|
|
287
287
|
function _tx(lang, ko, en) { return lang === 'en' ? en : ko; }
|
|
288
|
-
function fm(role, readWhen, updateWhen, body) {
|
|
289
|
-
|
|
288
|
+
function fm(role, readWhen, updateWhen, body, lang = 'ko') {
|
|
289
|
+
// 1.36.59 (F-05): en 프로젝트는 frontmatter 의 doNotStore 항목도 영어
|
|
290
|
+
const dns = lang === 'en' ? ['real tokens', 'passwords', 'production cookies', 'raw sensitive personal data'] : ['실제 토큰', '비밀번호', '운영 쿠키', '민감한 개인정보 원문'];
|
|
291
|
+
return `---\nleernessRole: ${role}\nreadWhen:\n${readWhen.map(x => ' - ' + x).join('\n')}\nupdateWhen:\n${updateWhen.map(x => ' - ' + x).join('\n')}\ndoNotStore:\n${dns.map(x => ' - ' + x).join('\n')}\n---\n${MARK}\n${body}`;
|
|
290
292
|
}
|
|
291
293
|
function ask(question) {
|
|
292
294
|
return new Promise(resolve => {
|
|
@@ -660,6 +662,15 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
|
|
|
660
662
|
'.claude/commands/update.md': `# /update\n\nleerness 자동 업데이트를 실행합니다 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n`,
|
|
661
663
|
'.claude/skills/leerness.md': `---\nname: leerness\ndescription: Leerness harness commands - handoff, audit, scan secrets, encoding check, lazy detect, session close, update. Use when the user asks to load project context, verify work quality, scan secrets, check encoding, or end a session.\n---\n\n# leerness skill\n\n## When to use\n- 사용자가 프로젝트 컨텍스트를 로드해달라고 할 때\n- 완료 선언 전 자기 검증을 요청할 때\n- 세션을 종료하거나 인수인계를 요청할 때\n- 시크릿/한글 인코딩 점검을 요청할 때\n- 새 leerness 버전 적용을 요청할 때\n\n## Commands\n\n\`\`\`bash\nleerness handoff . # 컨텍스트 로드\nleerness check . # pre-action 체크\nleerness audit . # 일관성/계획 정렬 감사\nleerness scan secrets . # 시크릿 패턴 스캔\nleerness encoding check . # UTF-8/BOM/NUL\nleerness lazy detect . # 게으름 평가\nleerness memory search "key" # 결정/이력 검색\nleerness session close . # 종료 보고 + handoff 자동 생성\nleerness update --yes # 자동 업데이트\n\`\`\`\n`,
|
|
662
664
|
};
|
|
665
|
+
// 1.36.59 (외부감사 F-05 시리즈 1회차): en 프로젝트에서 에이전트가 "지시"로 읽는 최상위 5종은 완전 영어 —
|
|
666
|
+
// 실측: en init 산출물 61개 중 53개 한글 잔존, 그중 지시 레이어가 최고 영향. 나머지 표면은 후속 회차.
|
|
667
|
+
if (lang === 'en') {
|
|
668
|
+
_files['AGENTS.md'] = `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ First action every session\n**Read \`.harness/session-workflow.md\` first and follow its 6-step workflow**: analyze request → plan → distribute → sub-agent work → integrated verification → close. Keep the same flow regardless of round length/complexity — that is what prevents drift.\n\n## Static vs dynamic — the leerness boundary\n**AGENTS.md = static project instructions** (coding rules, test commands, prohibitions, deploy steps — rarely change).\n**leerness = dynamic work state, memory, verification, handoff** (current goal, changed files, failed attempts, verification results, next-agent handoff — change every task).\n- Put rules/commands/prohibitions here in AGENTS.md.\n- Dynamic state (decisions/lessons/plan/progress/verification/handoff) is recorded by leerness in the **default workspace \`.harness/\`** (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md) via \`leerness handoff\`, \`decision add\`, \`lesson save\`, etc.\n- (Optional) the JSON state substrate of \`leerness state show|start|record|verify|handoff\` (or MCP \`leerness_state_*\`) lives in \`.leerness/\` — the cross-agent handoff standard, separate from the main workspace.\n- leerness **complements** AGENTS.md, it does not replace it. Static instructions here, dynamic state in leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (6-step workflow — highest priority)\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** (user-defined standing rules — follow every session)\n\n## Required behavior\n- Run \`leerness handoff .\` at session start to load context (handoff prints active rules automatically).\n- **Ask-on-ambiguity duty**: if the user's request has parts open to interpretation (vague qualifiers, pronouns, multiple alternatives, unclear scope), **ask the user instead of guessing**. Ask the questions produced by \`leerness clarify "<request>"\` verbatim. Even without detected signals, when your own judgment is split, asking wins.\n- **Preview-approval duty (new features)**: when the user asks for a new feature, **do not write code first**. ① Register a preview with \`leerness preview add "<feature>" --design "<design/UX>" --features "<list>"\`, ② present it and ask for approval or revisions, ③ on approval run \`leerness preview approve <P-ID>\`; on revision \`leerness preview revise <P-ID> --note "..."\` and re-present. **Never write the feature's code before approve.**\n- Classify work with \`leerness route <task-type>\` (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- Never delete protected files/managed sections — merge, archive, or mark deprecated instead.\n- After meaningful changes update progress-tracker, current-state, task-log, session-handoff.\n- Before claiming completion, self-verify with \`leerness check .\` or \`leerness lazy detect .\` and answer the \`leerness lens\` self-questions per domain (code: "would a senior developer find this needlessly complex?" / design: "would a senior designer and an ordinary user find this pretty and intuitive?").\n- Before changes run the guards: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- Before duplicating a capability check design-system.md, consistency-policy.md, reuse-map.md.\n- Close every session with \`leerness session close .\` — 9 categories (done/in-progress/incomplete/planned/waiting/on-hold/blocked/dropped/verification) + active-rule verification.\n- Updates: \`leerness update --check\` (detect) → \`leerness update --yes\` (auto-migrate).\n\n## Natural-language retro/insights/brainstorm\n| User phrase | Run immediately |\n|---|---|\n| "retrospective / look back / wrap up" | \`leerness retro\` |\n| "retro for last N days" | \`leerness retro --days N\` |\n| "stats / cumulative metrics / insights" | \`leerness insights\` |\n| "brainstorm about X / materials on X / review before starting X" | \`leerness brainstorm "X"\` |\n\nsession close prints a one-line summary automatically every time and runs a deep retrospective every 5 sessions; call the commands immediately when the user asks explicitly.\n\n## Natural-language standing rules\nWhen the user states a standing rule ("do Y every X"), register it immediately:\n\n| User phrase | Run immediately |\n|---|---|\n| "bump the version every update" | \`leerness rule add "bump version (patch)" --trigger every-update\` |\n| "add patch notes every commit" | \`leerness rule add "add patch notes" --trigger every-commit\` |\n| "deploy at session close" | \`leerness rule add "deploy (release publish)" --trigger session-close\` |\n| "pause/stop rule X" | \`leerness rule pause <ID>\` (find the ID with \`rule list\`) |\n| "remove rule X" | \`leerness rule remove <ID>\` |\n| "stop all rules" | \`leerness rule stop\` |\n| "resume rules" | \`leerness rule resume-all\` or \`leerness rule resume <ID>\` |\n\nAfter registering, report the result (ID + trigger + description) and apply it every session until the user explicitly says stop/remove.\n\n## Automatic rule verification\n- **every-update / version rules**: checks package.json version change.\n- **CHANGELOG / patch-note rules**: checks CHANGELOG.md mtime.\n- **test / verify rules**: checks today's verify traces in review-evidence.md.\n- **deploy / publish / push rules**: not auto-verifiable → guides \`leerness release publish\`.\n\nAutomate verifiable rules with \`leerness release bump\`, \`leerness release note "..."\`, and \`leerness release publish\`.\n`;
|
|
669
|
+
_files['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**⭐ First action every session**: follow the 6-step workflow in \`.harness/session-workflow.md\` (analyze → plan → distribute → sub-agent → verify → close). On drift critical, recover with \`leerness drift check --auto-fix\`.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## Natural-language standing rules\nWhen the user states "do Y every X", immediately run \`leerness rule add "Y" --trigger every-X\`. Registered rules are printed by every \`handoff\` and verified by \`session close\`. Only \`rule pause/remove\` when the user explicitly says stop/remove.\n\nSee the "Natural-language standing rules" table in AGENTS.md for the full mapping.\n`;
|
|
670
|
+
_files['.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`;
|
|
671
|
+
_files['.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`;
|
|
672
|
+
_files['.harness/session-workflow.md'] = fm('session-workflow', ['session start', 'new user request arrives', 'before distributing complex work'], ['workflow step changes'], `# Session Workflow — 6 steps of AI harness engineering\n\n> **At the start of every session the main agent reads this document first and follows the 6 steps as written.**\n> Same flow regardless of round length/complexity — that is what prevents drift.\n\n## Step 1. Analyze the request + check the environment\n\`\`\`bash\nleerness handoff . # load context + automatic drift warning\nleerness drift check . # signals + 4-level verdict\n\`\`\`\n- Decompose the request (5W1H). If ambiguous, ask clarifying questions (except autonomous mode).\n- On drift critical, run \`leerness session close .\` or \`drift check --auto-fix\` first.\n\n## Step 2. Plan\n- 3+ steps → use TodoWrite or \`leerness plan add\`.\n- New capability → search existing assets first via \`leerness reuse-map\` / \`reuse find <query>\`.\n- Multiple modules → define an integration spec up front (e.g. TICK_SPEC.md).\n\n## Step 3. Distribute — sub-agent mapping\n\`\`\`bash\nleerness agents list # ready CLIs\nleerness agents quota # limits\nleerness agents dispatch "<task>" --to <id>\n\`\`\`\n- Best sub-agent by task type: text/translation/analysis → claude · deep code reasoning → codex · direct file edits → agy --yolo · security review → \`leerness review --persona security\`.\n- **Conflict-prevention rules (mandatory)**: give each sub-agent *the exact files only it may modify*; require mtime-verification reports (concurrent writes risk last-writer-wins); verify against the spec afterwards with \`leerness contract verify\`.\n\n## Step 4. Sub-agent work + individual self-verification\n- Each sub-agent reports only after passing its own module tests.\n- Report format: line counts, tests N/N PASS, issues found, mtime verification.\n\n## Step 5. Integrated verification\n\`\`\`bash\nleerness contract verify SPEC.md src/<mod>.js\nleerness verify-claim T-XXX --run-tests --strict-claims\nleerness review <file> --persona security,performance,ux\n\`\`\`\n- The main agent writes and runs its own integration scenario (independent verification).\n- Cross-check that sub-agent verification and main verification *agree*.\n\n## Step 6. Close + handoff + next-round suggestion\n\`\`\`bash\nleerness session close . # --suggest on by default\nleerness session close . --no-suggest\n\n# or separately:\nleerness skill suggest .\nleerness drift check .\nleerness audit . --fix\n\`\`\`\n\n## 🧠 Memory CRUD Quick Reference\n\n| Surface | CREATE | READ | DELETE | RESTORE |\n|---|---|---|---|---|\n| **tasks** | task add | task list --json | task drop | task update |\n| **decisions** | decision add | decision list --json | decision drop | memory restore decisions |\n| **lessons** | lesson save | lesson list [--tag] | lesson drop | memory restore lessons |\n| **plan** | plan add | plan list --json | plan remove | memory restore plan |\n| **rules** | rule add | rule list --json | rule remove | (rule pause/resume) |\n\n\`\`\`bash\nleerness memory status [--json]\nleerness memory archive list [--surface s]\nleerness memory restore <surface> <target>\n\`\`\`\n\n**Recovering a wrongly saved item**:\n1. \`memory archive list\` — list restore candidates\n2. \`memory restore decisions "PostgreSQL"\` — archive → active\n3. handoff automatically notes archive activity within 24h every session\n\n## Auto-recovery & security\n- A skipped session close triggers drift critical at the next session start — recover with \`drift check --auto-fix\` (runs session close automatically on critical).\n- handoff auto-recalls past lessons (by current task keywords) and auto-suggests installed skills.\n- handoff prints a 1–2 line security summary (.env ↔ .env.example sync + .gitignore secret gaps); a missing .env in .gitignore is 🚨 CRITICAL — \`LEERNESS_AUTO_SECURITY_FIX=1\` runs \`audit --fix\` automatically.\n- \`leerness health\` gives a one-line combined check (drift + security + skills + usage + tasks).\n\n---\n\n## Quick checklist (before the session ends)\n- [ ] This round's tasks are registered in plan/progress-tracker (or task sync)\n- [ ] Every done item has evidence attached (verify-claim PASS)\n- [ ] contract verify PASS when sub-agents were used\n- [ ] drift score ≤ 30 — \`leerness drift check\`\n- [ ] session close was run\n- [ ] \`leerness health\` overall check\n- [ ] If .env is used: .gitignore secret patterns OK + .env.example synced\n- [ ] On security critical: \`LEERNESS_AUTO_SECURITY_FIX=1\` or \`audit --fix\`\n\n## Anti-patterns (drift signals)\n- ⚠ "work is done, just report and stop" → missed session close → drift critical next session\n- ⚠ "only TodoWrite updated, leerness unused" → \`task sync --from\` or \`task add\` is mandatory\n- ⚠ distributing to sub-agents without explicit file paths → concurrent-write conflicts\n- ⚠ self-reported "tests ran, PASS" only → verify-claim --run-tests not executed\n- ⚠ skipping contract verify → spec-mismatch bugs reach the user\n`, 'en');
|
|
673
|
+
}
|
|
663
674
|
// 1.9.276: minimal 모드 — 코어가 요구하지 않는 파일 제외 (verify 필수 파일은 유지).
|
|
664
675
|
if (opts.minimal) { for (const k of MINIMAL_SKIP_KEYS) delete _files[k]; }
|
|
665
676
|
return _files;
|
|
@@ -727,9 +738,9 @@ function createBackup(root, reason, files, dry = false) {
|
|
|
727
738
|
}
|
|
728
739
|
|
|
729
740
|
// 1.9.1 P2 / 1.9.368 (UR-0025): MERGE_OVERWRITE_FILES → lib/catalogs, managedMerge 순수 코어 → lib/pure-utils. 얇은 래퍼.
|
|
730
|
-
function managedMerge(file, next, previous, archiveDir) {
|
|
741
|
+
function managedMerge(file, next, previous, archiveDir, mergeOpts = {}) {
|
|
731
742
|
const archiveRel = archiveDir ? path.relative(process.cwd(), archiveDir).replace(/\\/g, '/') : '.harness/archive';
|
|
732
|
-
return _managedMerge(file, next, previous, archiveRel, MERGE_OVERWRITE_FILES);
|
|
743
|
+
return _managedMerge(file, next, previous, archiveRel, MERGE_OVERWRITE_FILES, mergeOpts); // 1.36.60: altTemplate/lang 전달
|
|
733
744
|
}
|
|
734
745
|
|
|
735
746
|
function writeIfSafe(root, file, content, opts = {}) {
|
|
@@ -740,7 +751,7 @@ function writeIfSafe(root, file, content, opts = {}) {
|
|
|
740
751
|
// 병합을 우회해 CLAUDE.md 커스텀 지시가 archive 에만 남았다(1.36.28 보존 재설계의 force 구멍).
|
|
741
752
|
if (already && opts.mergeManaged) {
|
|
742
753
|
const prev = read(p);
|
|
743
|
-
writeUtf8(p, managedMerge(file, content, prev, opts.archiveDir));
|
|
754
|
+
writeUtf8(p, managedMerge(file, content, prev, opts.archiveDir, { altTemplate: opts.altTemplate, lang: opts.lang }));
|
|
744
755
|
return { action: 'merged', file };
|
|
745
756
|
}
|
|
746
757
|
if (already && !opts.force) return { action: 'preserved', file };
|
|
@@ -977,6 +988,17 @@ async function install(root, opts = {}) {
|
|
|
977
988
|
// 1.9.10: 스킬 카탈로그 출처 안내
|
|
978
989
|
// 1.9.184 (사용자 명시): leerness-skillpack 미사용 정책 — 안내 메시지 제거. builtin catalog 만 사용.
|
|
979
990
|
const files = coreFiles(root, lang, skills, { minimal: !!opts.minimal });
|
|
991
|
+
// 1.36.60 (F-05 2회차, 검수 #1 High): 반대-언어 canonical 템플릿 — 언어 전환 재init/마이그레이션 시
|
|
992
|
+
// 구 언어 템플릿 라인을 "사용자 커스텀"으로 오인해 통째로 Preserved 에 이월하던 것을 차감 병합으로 해소.
|
|
993
|
+
// (검수 2차 High): 차감은 "실제 언어 전환일 때만" — 동일-언어 재init 에 무조건 적용하면 반대-언어 템플릿과
|
|
994
|
+
// 우연히 일치하는 정당한 커스텀이 유실된다. 이전 저장 언어를 쓰기 전에 포착해 전환 여부로 게이트.
|
|
995
|
+
let _prevLang = null;
|
|
996
|
+
try { _prevLang = read(path.join(root, '.harness', 'LANGUAGE')).trim().toLowerCase() || null; } catch {}
|
|
997
|
+
if (!_prevLang) { try { _prevLang = String(JSON.parse(read(path.join(root, '.harness', 'manifest.json'))).language || '').toLowerCase() || null; } catch {} }
|
|
998
|
+
const _langSwitched = !!(_prevLang && (_prevLang === 'ko' || _prevLang === 'en') && _prevLang !== lang);
|
|
999
|
+
const _altLang = lang === 'en' ? 'ko' : 'en';
|
|
1000
|
+
let _altFiles = {};
|
|
1001
|
+
if (_langSwitched) { try { _altFiles = coreFiles(root, _altLang, skills, { minimal: !!opts.minimal }); } catch {} }
|
|
980
1002
|
const backup = createBackup(root, opts.force ? 'force' : (opts.migration ? 'migration' : 'init'), files, opts.dry);
|
|
981
1003
|
if (opts.dry) {
|
|
982
1004
|
log(`Backup target: ${backup.archiveDir}`);
|
|
@@ -987,6 +1009,7 @@ async function install(root, opts = {}) {
|
|
|
987
1009
|
}
|
|
988
1010
|
const managedOverwrite = new Set([
|
|
989
1011
|
'AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md',
|
|
1012
|
+
'.harness/session-workflow.md', // 1.36.60 (검수 #1): managed 편입 — 종전엔 재init 시 구 언어/구 버전 그대로 방치
|
|
990
1013
|
'.harness/HARNESS_VERSION','.harness/manifest.json','.harness/LANGUAGE','.harness/skills-lock.json',
|
|
991
1014
|
'.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md',
|
|
992
1015
|
'.harness/leerness-maintenance.md','.harness/protected-files.md','.harness/AX_MIGRATION_GUIDE.md',
|
|
@@ -1021,7 +1044,7 @@ async function install(root, opts = {}) {
|
|
|
1021
1044
|
actions.push({ file:f, action });
|
|
1022
1045
|
continue;
|
|
1023
1046
|
}
|
|
1024
|
-
const r = writeIfSafe(root, f, c, { force: effForce, mergeManaged, archiveDir: backup.archiveDir });
|
|
1047
|
+
const r = writeIfSafe(root, f, c, { force: effForce, mergeManaged, archiveDir: backup.archiveDir, altTemplate: _altFiles[f], lang });
|
|
1025
1048
|
actions.push(r);
|
|
1026
1049
|
_done++;
|
|
1027
1050
|
drawProgress(_done, f);
|
|
@@ -5192,6 +5215,25 @@ function _selfTestCases() {
|
|
|
5192
5215
|
const s = read(__filename);
|
|
5193
5216
|
const wired = s.includes("_profile === 'core' ? _ALL_TOOLS.filter") && s.includes("Unknown tool: ${name} (core profile");
|
|
5194
5217
|
return allExist && loop && cleanSer && wired;
|
|
5218
|
+
} },
|
|
5219
|
+
{ name: '언어 전환 병합 (1.36.60, F-05 2회차 검수 High): altTemplate 차감 — 구 언어 템플릿을 커스텀 오인 이월 차단 + 커스텀 보존 + en 래퍼 — 순수 행위검사', run: () => {
|
|
5220
|
+
const p = require('../lib/pure-utils');
|
|
5221
|
+
const koT = '# 지시\n한국어 규칙 1\n한국어 규칙 2\n';
|
|
5222
|
+
const enT = '# Instructions\nEnglish rule 1\nEnglish rule 2\n';
|
|
5223
|
+
const prev = koT + 'MY CUSTOM LINE\n';
|
|
5224
|
+
// 전환: KO(prev) → EN(next), altTemplate=koT — 한국어 템플릿 라인은 이월 안 되고 커스텀만
|
|
5225
|
+
const m = p._managedMerge('AGENTS.md', enT, prev, '.harness/archive', null, { altTemplate: koT, lang: 'en' });
|
|
5226
|
+
const noKoTemplate = !m.includes('한국어 규칙 1') && !m.includes('한국어 규칙 2');
|
|
5227
|
+
const customKept = (m.match(/MY CUSTOM LINE/g) || []).length === 1;
|
|
5228
|
+
const enWrapper = m.includes('carried over from the previous version') && !/[가-힣]/.test(m.replace('MY CUSTOM LINE', ''));
|
|
5229
|
+
// altTemplate 없이(구 동작): 커스텀 오인 이월 — 회귀 방지용 대조군 (구 언어 라인이 이월됨을 확인)
|
|
5230
|
+
const m2 = p._managedMerge('AGENTS.md', enT, prev, '.harness/archive', null, {});
|
|
5231
|
+
const control = m2.includes('한국어 규칙 1');
|
|
5232
|
+
// 멱등: 전환 결과에 같은 병합 재적용 시 커스텀 중복 없음
|
|
5233
|
+
const m3 = p._managedMerge('AGENTS.md', enT, m, '.harness/archive', null, { altTemplate: koT, lang: 'en' });
|
|
5234
|
+
const idem = (m3.match(/MY CUSTOM LINE/g) || []).length === 1;
|
|
5235
|
+
const wired = read(__filename).includes('altTemplate: _altFiles[f]') && read(__filename).includes("'.harness/session-workflow.md', // 1.36.60");
|
|
5236
|
+
return noKoTemplate && customKept && enWrapper && control && idem && wired;
|
|
5195
5237
|
} }
|
|
5196
5238
|
];
|
|
5197
5239
|
}
|
|
@@ -5596,7 +5638,14 @@ function integrityCmd(root) {
|
|
|
5596
5638
|
let cur = ''; try { cur = read(fp); } catch { kind = 'unreadable'; }
|
|
5597
5639
|
if (!kind) {
|
|
5598
5640
|
// (검수 #3) H1 상실 = "H1 이 하나도 없음"(정확 일치 아님 — 사용자가 제목을 번역/개명해도 무탐)
|
|
5599
|
-
|
|
5641
|
+
// 1.36.59 (검수): 코드펜스 안의 `# 주석`을 H1 으로 오인하던 것 제외 — 펜스 밖 라인만 검사
|
|
5642
|
+
let _fence = false;
|
|
5643
|
+
const _hasH1 = cur.split('\n').some(l => {
|
|
5644
|
+
const t = l.trim();
|
|
5645
|
+
if (t.startsWith('```')) { _fence = !_fence; return false; }
|
|
5646
|
+
return !_fence && /^# \S/.test(t);
|
|
5647
|
+
});
|
|
5648
|
+
if (!_hasH1) kind = 'h1_lost';
|
|
5600
5649
|
else if (cur.length < tmpl.length * 0.4) kind = 'truncated';
|
|
5601
5650
|
}
|
|
5602
5651
|
}
|
|
@@ -20349,7 +20398,7 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
20349
20398
|
const results = [];
|
|
20350
20399
|
for (const k of a.keys) {
|
|
20351
20400
|
if (files[k] == null) continue;
|
|
20352
|
-
results.push(writeIfSafe(root, k, files[k], { mergeManaged: managedOverwrite.has(k) }));
|
|
20401
|
+
results.push(writeIfSafe(root, k, files[k], { mergeManaged: managedOverwrite.has(k), lang: lang === 'en' ? 'en' : 'ko' })); // 1.36.60 (검수 2차 #2): 래퍼 언어 전달 (altTemplate 은 전환-감지 없는 이 경로에선 미적용 — High 회귀 방지)
|
|
20353
20402
|
}
|
|
20354
20403
|
if (a.mcp) results.push(_mergeMcpJson(root));
|
|
20355
20404
|
if (json) { log(JSON.stringify({ adapter: tool, files: results }, null, 2)); return; }
|
package/lib/pure-utils.js
CHANGED
|
@@ -765,10 +765,16 @@ function _mergeReadmeSection(existing, block, START, END) {
|
|
|
765
765
|
}
|
|
766
766
|
// 1.9.368 (UR-0025): 관리 파일 마이그레이션 머지 순수 코어 — 이전 내용을 migration-preserved 블록으로 보존(데이터/인덱스 파일은 overwrite).
|
|
767
767
|
// archiveRel(사전 계산된 표시 경로) + overwriteSet 을 인자로 주입 → path/process/상수 비결합(순수).
|
|
768
|
-
function _managedMerge(file, next, previous, archiveRel, overwriteSet) {
|
|
768
|
+
function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}) {
|
|
769
769
|
if (!previous || previous.trim() === next.trim()) return next;
|
|
770
770
|
const tag = '<!-- leerness:migration-preserved -->';
|
|
771
771
|
if (overwriteSet && overwriteSet.has(String(file).replace(/\\/g, '/'))) return next;
|
|
772
|
+
// 1.36.60 (F-05 2회차, 검수 High): 언어 전환 시 구 언어 canonical 템플릿(altTemplate)의 라인도 차감 —
|
|
773
|
+
// 종전엔 KO→EN 재init 에서 KO 템플릿 전체가 "커스텀"으로 오인돼 Preserved 에 통째 이월됐다.
|
|
774
|
+
// (검수 2차 High): 차감은 "출현 횟수" 기반 — canonical 1회분만 소비해, 사용자가 의도적으로 템플릿 라인을
|
|
775
|
+
// 복제한 커스텀(2번째 출현)은 생존한다. Set 차감은 그 복제본까지 지웠다.
|
|
776
|
+
const altCounts = new Map();
|
|
777
|
+
for (const l of String(opts.altTemplate || '').split('\n').map(x => x.trim()).filter(Boolean)) altCounts.set(l, (altCounts.get(l) || 0) + 1);
|
|
772
778
|
// 1.36.28 (사용자 보고 — 반복 마이그레이션 데이터 유실): 종전 <details> 중첩 설계는 previous 에 preserved 태그가
|
|
773
779
|
// 있으면 통째로 next 만 반환 → 2번째 마이그레이션부터 사용자가 CLAUDE/AGENTS 에 직접 쓴 커스텀 지시가 조용히
|
|
774
780
|
// 사라졌다(매 버전 AI 마이그레이션 흐름에서 상시 발화). 재설계: 라인-집합 diff 로 "새 템플릿에 없는 라인"만
|
|
@@ -780,17 +786,26 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet) {
|
|
|
780
786
|
for (const raw of previous.split('\n')) {
|
|
781
787
|
const l = raw.trim();
|
|
782
788
|
if (!l || nextLines.has(l)) continue;
|
|
783
|
-
|
|
789
|
+
const _ac = altCounts.get(l);
|
|
790
|
+
if (_ac > 0) { altCounts.set(l, _ac - 1); continue; } // canonical 출현분만 소비 (복제 커스텀 생존)
|
|
791
|
+
// 1.36.60 (검수 2차 #3): 구버전 workflow 의 자동생성 이력 로그(도구-카운트/마일스톤 불릿)는 커스텀이 아니라
|
|
792
|
+
// 릴리스마다 낡는 생성물 — 좁은 시그니처만 필터 (사용자 산문이 이 패턴과 겹칠 확률은 무시 가능)
|
|
793
|
+
if (String(file).includes('session-workflow') && (/^- .*MCP \*\*\d+\s*도구/.test(l) || /^- MCP \*\*\d+/.test(l) || /^- 🏆 마일스톤/.test(l) || /^- 🎉 \*\*\d+ 라운드/.test(l))) continue;
|
|
794
|
+
// preserved 래퍼 자체의 보일러플레이트는 이월 대상 아님 (구 <details> 형식 + 신 평문 형식 + en 래퍼 모두)
|
|
784
795
|
if (l === tag || l === '---' || l === '<details>' || l === '</details>') continue;
|
|
785
796
|
if (/^```/.test(l) || /^<summary>/.test(l)) continue;
|
|
786
|
-
if (/^## Preserved previous content|^Previous content was backed up|^`[^`]*archive[^`]*`$|^> 이전 버전에서
|
|
797
|
+
if (/^## Preserved previous content|^Previous content was backed up|^`[^`]*archive[^`]*`$|^> 이전 버전에서 이어진|^> Custom user\/project content carried over/.test(l)) continue;
|
|
787
798
|
if (seen.has(l)) continue;
|
|
788
799
|
seen.add(l);
|
|
789
800
|
custom.push(raw.replace(/\\`\\`\\`/g, '```')); // 구 형식에서 이스케이프된 펜스 복원
|
|
790
801
|
}
|
|
791
802
|
if (!custom.length) return next;
|
|
792
803
|
const ar = archiveRel || '.harness/archive';
|
|
793
|
-
|
|
804
|
+
// 1.36.60 (검수): 보존 래퍼 문구도 프로젝트 언어를 따른다 — en 프로젝트에 한글 래퍼가 새 한글원을 만들던 것 해소
|
|
805
|
+
const note = opts.lang === 'en'
|
|
806
|
+
? `> Custom user/project content carried over from the previous version — re-carried automatically on every migration. Full original backup: \`${ar}\``
|
|
807
|
+
: `> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: \`${ar}\``;
|
|
808
|
+
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\n${note}\n\n` + custom.join('\n') + '\n';
|
|
794
809
|
}
|
|
795
810
|
|
|
796
811
|
// 1.9.369 (UR-0025): --skills 값 파싱 순수 코어 — catalog 주입(harness skillCatalog 비결합). all/recommended/csv 처리 + catalog 필터.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.60",
|
|
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
|
@@ -7074,5 +7074,36 @@ total++;
|
|
|
7074
7074
|
if (!ok) failed++;
|
|
7075
7075
|
}
|
|
7076
7076
|
|
|
7077
|
+
// 1.36.59 (외부감사 F-05 1회차): en init 의 지시-레이어 5종은 한글 0 + ko 완전 보존
|
|
7078
|
+
total++;
|
|
7079
|
+
{
|
|
7080
|
+
let ok = false;
|
|
7081
|
+
const _dirs = [];
|
|
7082
|
+
try {
|
|
7083
|
+
const H = /[가-힣]/;
|
|
7084
|
+
const de = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-en5-')); _dirs.push(de);
|
|
7085
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'init', de, '--yes', '--language', 'en', '--no-env'], { encoding: 'utf8', timeout: 30000 });
|
|
7086
|
+
const five = ['AGENTS.md', 'CLAUDE.md', '.harness/session-workflow.md', '.cursor/rules/leerness.mdc', '.github/copilot-instructions.md'];
|
|
7087
|
+
const enOk = r1.status === 0 && five.every(p => { const t = fs.readFileSync(path.join(de, p), 'utf8'); return t.length > 100 && !H.test(t); });
|
|
7088
|
+
// en 지시 본문의 핵심 계약 존재(번역이 빈 껍데기가 아님) — 검수 #2 반영: lens 질문/체크리스트/안티패턴/자동화 지시까지
|
|
7089
|
+
const ag = fs.readFileSync(path.join(de, 'AGENTS.md'), 'utf8');
|
|
7090
|
+
const wf = fs.readFileSync(path.join(de, '.harness/session-workflow.md'), 'utf8');
|
|
7091
|
+
const substOk = ag.includes('Ask-on-ambiguity duty') && ag.includes('Preview-approval duty') && ag.includes('senior developer') && ag.includes('leerness release bump')
|
|
7092
|
+
&& wf.includes('## Step 6') && wf.includes('Quick checklist') && wf.includes('Anti-patterns') && wf.includes('LEERNESS_AUTO_SECURITY_FIX');
|
|
7093
|
+
// (검수 #4) en 재init: 커스텀 라인 정확히 1회 생존 + exit 0
|
|
7094
|
+
fs.appendFileSync(path.join(de, 'AGENTS.md'), '\nMY EN CUSTOM LINE\n');
|
|
7095
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'init', de, '--yes', '--language', 'en', '--no-env'], { encoding: 'utf8', timeout: 30000 });
|
|
7096
|
+
const ag2 = fs.readFileSync(path.join(de, 'AGENTS.md'), 'utf8');
|
|
7097
|
+
const custOk = r2.status === 0 && (ag2.match(/MY EN CUSTOM LINE/g) || []).length === 1 && ag2.includes('Ask-on-ambiguity duty');
|
|
7098
|
+
const dk = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ko5-')); _dirs.push(dk);
|
|
7099
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'init', dk, '--yes', '--language', 'ko', '--no-env'], { encoding: 'utf8', timeout: 30000 });
|
|
7100
|
+
const koOk = r3.status === 0 && H.test(fs.readFileSync(path.join(dk, 'AGENTS.md'), 'utf8')) && fs.readFileSync(path.join(dk, '.harness/session-workflow.md'), 'utf8').includes('6단계');
|
|
7101
|
+
ok = enOk && substOk && custOk && koOk;
|
|
7102
|
+
if (!ok) console.log(` [en5 디버그] en=${enOk} subst=${substOk} cust=${custOk} ko=${koOk}`);
|
|
7103
|
+
} catch (e) {} finally { _dirs.forEach(d => { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} }); }
|
|
7104
|
+
console.log(ok ? '✓ B(1.36.59) F-05 1회차: en init 지시 5종(AGENTS/CLAUDE/workflow/cursor/copilot) 한글 0 + 핵심 계약 보존 + ko 무회귀' : '✗ en 지시 5종 실패');
|
|
7105
|
+
if (!ok) failed++;
|
|
7106
|
+
}
|
|
7107
|
+
|
|
7077
7108
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7078
7109
|
if (failed > 0) process.exit(1);
|