leerness 1.3.1 → 1.4.0

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.
Files changed (3) hide show
  1. package/README.md +132 -92
  2. package/bin/harness.js +357 -18
  3. package/package.json +16 -4
package/README.md CHANGED
@@ -1,25 +1,22 @@
1
1
  # Leerness
2
2
 
3
- **Leerness는 AI 에이전트가 대규모 프로젝트에서도 필요한 문서를 먼저 읽고, 작업 적재적소에 프로젝트 메모리를 갱신하도록 만드는 AX 최적화 개발 하네스입니다.**
3
+ **Leerness는 AI 에이전트가 프로젝트의 맥락, 작업 규율, 언어 정책, 스킬 라이브러리, 진행 작업 상태를 일관되게 읽고 기록하도록 돕는 AX 최적화 개발 하네스입니다.**
4
4
 
5
- Leerness 목적은 단순히 `.md` 파일을 많이 만드는 것이 아닙니다. AI가 어떤 상황에 어떤 파일을 봐야 하는지, 어떤 작업 후 어떤 파일을 갱신해야 하는지까지 명확히 지시합니다.
5
+ Leerness 프로젝트 안에 `.harness/` 지식 저장소를 만들고, AI가 작업 유형별로 어떤 파일을 읽어야 하는지와 작업 후 어떤 파일을 갱신해야 하는지 명확히 지시합니다. 세션이 끝날 때는 완료/진행중/미완료/예정/보류/대기 작업과 검증 결과를 남기도록 강제합니다.
6
6
 
7
- ## 핵심 개념
7
+ ## 주요 기능
8
8
 
9
- ```text
10
- Agent = Model + Leerness Harness
11
- ```
12
-
13
- 모델은 추론하고, Leerness는 다음을 제공합니다.
14
-
15
- | 영역 | 역할 |
9
+ | 기능 | 설명 |
16
10
  |---|---|
17
- | Context Routing | 작업 유형별로 읽을 파일과 갱신할 파일을 지정 |
18
- | Writeback Policy | 어떤 정보를 어느 `.harness/*.md` 파일에 기록할지 정의 |
19
- | Task Type Map | 사용자 요청을 feature/ui/release/migration 등으로 분류 |
20
- | Project Memory | 목적, 현재 상태, 아키텍처, 결정 로그, 릴리즈 조건 유지 |
21
- | Skill Libraries | 검증된 성공 패턴을 재사용 가능한 스킬로 설치/배포 |
22
- | AX Guides | AI가 구버전 마이그레이션 또는 신규 설치를 안전하게 수행하도록 안내 |
11
+ | 언어 정책 | 설치 언어를 자동 감지하거나 `ko/en`으로 선택해 하네스와 스킬 문서의 작성 언어를 통일합니다. |
12
+ | Context Routing | feature, ui, release, migration, debug 작업 유형별 참조 파일과 갱신 파일을 안내합니다. |
13
+ | Writeback Policy | 목적, 현재 상태, 아키텍처, 결정 로그, 릴리즈 체크리스트 등 정보를 어디에 기록할지 정의합니다. |
14
+ | 진행 작업 추적 | 사용자 요청별 작업을 requested/planned/in-progress/waiting/on-hold/blocked/incomplete/done/dropped 상태로 추적합니다. |
15
+ | 작업 드랍 | 사용자가 이상 원하지 않는 작업은 삭제하지 않고 `dropped`로 표시해 이력을 보존합니다. |
16
+ | 세션 종료 인수인계 | 세션 종료 완료/진행중/미완료/예정/보류/대기/드랍 작업과 추천 방향을 표기합니다. |
17
+ | 스킬 라이브러리 | 검증된 작업 패턴을 스킬팩으로 설치하고, 한글명/가능 작업/최종 업데이트일/AI 검증 상태를 표시합니다. |
18
+ | AI 검증 게이트 | 검증된 스킬만 npm/git 업로드가 가능하도록 검증 메타데이터와 토큰 게이트를 사용합니다. |
19
+ | 디버그 | `leerness debug`로 AGENTS 방향지시, 언어 정책, 라우팅, 세션 종료 정책, 진행 작업 추적이 정상인지 점검합니다. |
23
20
 
24
21
  ## 설치
25
22
 
@@ -27,134 +24,177 @@ Agent = Model + Leerness Harness
27
24
  npx leerness init
28
25
  ```
29
26
 
27
+ 언어를 명시해서 설치:
28
+
29
+ ```bash
30
+ npx leerness init --language ko
31
+ npx leerness init --language en
32
+ ```
33
+
30
34
  추천 스킬 포함:
31
35
 
32
36
  ```bash
33
- npx leerness init --skills recommended
37
+ npx leerness init --language ko --skills recommended
34
38
  ```
35
39
 
36
40
  기존 프로젝트 또는 구버전 하네스 마이그레이션:
37
41
 
38
42
  ```bash
39
43
  npx leerness migrate --dry-run
40
- npx leerness migrate
44
+ npx leerness migrate --language ko
41
45
  ```
42
46
 
43
47
  ## 주요 명령어
44
48
 
45
49
  ```bash
46
- leerness init [path] [--yes] [--skills recommended|all|office,commerce-api]
47
- leerness migrate [path] [--dry-run]
50
+ leerness init [path] [--language auto|ko|en] [--skills recommended|all|office,commerce-api]
51
+ leerness migrate [path] [--dry-run] [--language auto|ko|en]
48
52
  leerness status [path]
49
53
  leerness verify [path]
54
+ leerness debug [path]
50
55
  leerness route <task-type>
56
+ ```
51
57
 
52
- leerness skill list
53
- leerness skill info <name>
54
- leerness skill add <name>
55
- leerness skill learn <name> --from <validated-skill-path>
58
+ 작업 추적:
56
59
 
57
- leerness library guide [path]
58
- leerness library validate <path> [--strict-ai]
59
- leerness library verify <path> --ai --reviewer leerness-ai
60
- leerness library build <path>
61
- leerness library publish <built-library> --target npm|git [--execute]
60
+ ```bash
61
+ leerness task list
62
+ leerness task list --status planned,waiting,on-hold
63
+ leerness task add "쿠팡 API 연동 검증" --status planned
64
+ leerness task update T-0002 --status in-progress --next "인증 응답 확인"
65
+ leerness task drop T-0002 --reason "사용자가 범위에서 제외"
62
66
  ```
63
67
 
64
- 작업 유형 라우팅 확인:
68
+ 세션 종료:
65
69
 
66
70
  ```bash
67
- leerness route release
68
- leerness route feature
69
- leerness route migration
70
- leerness route new-install
71
+ leerness route session-close
72
+ leerness session close
71
73
  ```
72
74
 
73
- ## 생성되는 핵심 파일
75
+ 스킬:
74
76
 
75
- ```text
76
- .harness/
77
- project-brief.md # 프로젝트 목적/사용자/성공 기준
78
- current-state.md # 현재 상태/다음 작업/블로커
79
- architecture.md # 구조/모듈/데이터 흐름
80
- context-map.md # 기능별 참조 파일 지도
81
- decisions.md # 결정 로그
82
- guardrails.md # 금지사항/보안/민감영역 규칙
83
- design-system.md # UI/UX/컴포넌트 일관성
84
- feature-contracts.md # 기능 입력/출력/상태/오류 계약
85
- testing-strategy.md # 검증 전략
86
- release-checklist.md # 배포/npm/git/환경변수/롤백 조건
87
- session-handoff.md # 다음 세션 인수인계
88
- context-routing.md # 작업 유형별 읽기/갱신 라우팅
89
- writeback-policy.md # 어떤 정보를 어디에 기록할지
90
- task-type-map.md # 사용자 요청 → 작업 유형 매핑
91
- AX_MIGRATION_GUIDE.md # AI용 구버전 마이그레이션 가이드
92
- AX_NEW_PROJECT_GUIDE.md # AI용 신규 설치/프로젝트 반영 가이드
93
- AX_SKILL_LIBRARY_GUIDE.md # AI용 스킬 라이브러리 가이드
77
+ ```bash
78
+ leerness skill list
79
+ leerness skill info ai-verified-skill-publisher
80
+ leerness skill add ai-verified-skill-publisher
94
81
  ```
95
82
 
96
- 핵심 파일에는 `readWhen`, `updateWhen`, `doNotStore` 메타데이터가 들어갑니다. AI는 이 정보를 기준으로 파일을 참조하고 갱신해야 합니다.
83
+ 스킬 라이브러리:
97
84
 
98
- ## AGENTS.md의 역할
85
+ ```bash
86
+ leerness library validate <path> --strict-ai
87
+ leerness library verify <path> --ai --reviewer leerness-ai
88
+ leerness library build <path>
89
+ leerness library publish <built-library> --target npm|git --execute
90
+ ```
99
91
 
100
- `AGENTS.md`는 단순 지침 파일이 아니라 AI 작업 라우터입니다.
92
+ ## 생성되는 핵심 파일
101
93
 
102
- 모든 작업은 다음 순서를 따릅니다.
94
+ ```text
95
+ .harness/
96
+ LANGUAGE # ko 또는 en
97
+ language-policy.md # 문서/스킬 작성 언어 규칙
98
+ project-brief.md # 프로젝트 목적/사용자/성공 기준
99
+ current-state.md # 현재 상태/다음 작업/블로커
100
+ architecture.md # 구조/모듈/데이터 흐름
101
+ context-map.md # 기능별 참조 파일 지도
102
+ decisions.md # 결정 로그
103
+ guardrails.md # 금지사항/보안/민감영역 규칙
104
+ design-system.md # UI/UX/컴포넌트 일관성
105
+ feature-contracts.md # 기능 입력/출력/상태/오류 계약
106
+ testing-strategy.md # 검증 전략
107
+ release-checklist.md # 배포/npm/git/환경변수/롤백 조건
108
+ progress-tracker.md # 사용자 요청별 진행/미완료/보류/대기 추적
109
+ session-handoff.md # 다음 세션 인수인계
110
+ session-close-policy.md # 세션 종료 보고 규칙
111
+ anti-lazy-work-policy.md # 게으른 작업 방지 규칙
112
+ debug-guide.md # 하네스 작동 점검 기준
113
+ context-routing.md # 작업 유형별 읽기/갱신 라우팅
114
+ writeback-policy.md # 어떤 정보를 어디에 기록할지
115
+ task-type-map.md # 사용자 요청 → 작업 유형 매핑
116
+ AX_MIGRATION_GUIDE.md # AI용 구버전 마이그레이션 가이드
117
+ AX_NEW_PROJECT_GUIDE.md # AI용 신규 설치/프로젝트 반영 가이드
118
+ AX_SKILL_LIBRARY_GUIDE.md # AI용 스킬 라이브러리 가이드
119
+ ```
103
120
 
104
- 1. 작업 유형을 분류한다.
105
- 2. `.harness/context-routing.md`와 `.harness/task-type-map.md`를 읽는다.
106
- 3. 작업 유형별 필수 파일을 읽는다.
107
- 4. 작업을 수행한다.
108
- 5. `.harness/writeback-policy.md`에 따라 필요한 파일을 갱신한다.
109
- 6. `current-state.md`, `task-log.md`, `session-handoff.md`를 최신화한다.
121
+ ## 언어 정책
110
122
 
111
- ## AX 마이그레이션 가이드
123
+ 설치 `--language auto`가 기본입니다. Leerness는 기존 README, AGENTS, 하네스 문서를 보고 한국어/영어를 추정합니다. 대화형 터미널에서는 사용자가 직접 선택할 수 있습니다.
112
124
 
113
- 구버전 Leerness, project-harness, 자체 AGENTS/CLAUDE 문서가 있는 프로젝트는 다음 파일을 기준으로 AI가 마이그레이션합니다.
125
+ 언어 정책은 아래 파일에 저장됩니다.
114
126
 
115
127
  ```text
116
- .harness/AX_MIGRATION_GUIDE.md
128
+ .harness/LANGUAGE
129
+ .harness/language-policy.md
117
130
  ```
118
131
 
119
- 가이드는 다음을 지시합니다.
132
+ AI는 하네스 문서, 스킬 문서, 세션 인수인계, 진행 작업 목록을 언어로 작성해야 합니다. 단, 코드 식별자, 파일명, 명령어, 환경변수명, API 필드명은 원문을 유지합니다.
120
133
 
121
- - 기존 하네스/지침 파일 백업
122
- - legacy 내용의 새 목적지 매핑
123
- - 충돌되는 규칙 처리
124
- - 민감정보 제거
125
- - 새 Context Routing/Writeback Policy 생성
126
- - 마이그레이션 후 검증 및 session-handoff 갱신
134
+ ## 진행 작업과 드랍 처리
127
135
 
128
- ## AX 신규 설치 가이드
136
+ `progress-tracker.md`는 사용자 요청을 세션 간 추적합니다.
129
137
 
130
- 이미 진행 중인 프로젝트에 처음 Leerness를 설치했다면 다음 파일을 기준으로 프로젝트 내용을 반영합니다.
138
+ 상태값:
131
139
 
132
140
  ```text
133
- .harness/AX_NEW_PROJECT_GUIDE.md
141
+ requested
142
+ planned
143
+ in-progress
144
+ waiting
145
+ on-hold
146
+ blocked
147
+ incomplete
148
+ done
149
+ dropped
134
150
  ```
135
151
 
136
- 가이드는 AI에게 다음을 요구합니다.
137
-
138
- - 실제 파일 구조와 프레임워크 파악
139
- - package/config/route/API/DB/deploy/test 파일 확인
140
- - project-brief, architecture, context-map, design-system, feature-contracts 채우기
141
- - release-checklist와 testing-strategy를 실제 프로젝트 기준으로 작성
142
- - session-handoff에 다음 정확한 작업 기록
152
+ 사용자가 어떤 작업을 중단하거나 범위에서 제외하면 삭제하지 않고 `dropped`로 표시합니다.
143
153
 
144
- ## 스킬 라이브러리
154
+ ```bash
155
+ leerness task drop T-0004 --reason "사용자가 이번 범위에서 제외"
156
+ ```
145
157
 
146
- 스킬은 한글명, 가능한 작업, 최종 업데이트일, AI 검증 상태를 표시합니다.
158
+ 세션 종료 unresolved 상태인 작업은 자동으로 표기 대상입니다.
147
159
 
148
- ```bash
149
- leerness skill list
150
- leerness skill info ai-verified-skill-publisher
160
+ ```text
161
+ requested, planned, in-progress, waiting, on-hold, blocked, incomplete
151
162
  ```
152
163
 
153
- 검증된 스킬 업로드는 AI 검증 메타데이터가 있어야 하며, 실제 npm/git 업로드에는 토큰 게이트가 적용됩니다.
164
+ ## 디버그
165
+
166
+ 하네스가 제대로 작동하는지 확인합니다.
154
167
 
155
- ## 민감정보 원칙
168
+ ```bash
169
+ leerness debug
170
+ ```
156
171
 
157
- Leerness 파일에는 실제 토큰, 비밀번호, 쿠키, private key, 고객 개인정보를 저장하지 않습니다. 환경변수 이름과 secret manager key 이름만 기록합니다.
172
+ 점검 항목:
173
+
174
+ - AGENTS.md가 language-policy, context-routing, writeback-policy, progress-tracker, anti-lazy policy를 참조하는지
175
+ - `.harness/language-policy.md`가 존재하고 언어가 manifest에 기록됐는지
176
+ - context-routing/writeback/task-type-map이 있는지
177
+ - progress-tracker가 작업 상태 표를 갖고 있는지
178
+ - session-close-policy가 미완료/예정/보류/대기 작업 목록화를 요구하는지
179
+ - 하네스 파일에 토큰/비밀번호/private key 의심 패턴이 없는지
180
+
181
+ ## 세션 종료 보고 기준
182
+
183
+ AI는 의미 있는 세션이 끝날 때 아래를 정리해야 합니다.
184
+
185
+ - 이번 세션에서 완료한 작업
186
+ - 사용자가 요청한 작업 중 진행 중인 작업
187
+ - 미완료/미시작 작업
188
+ - 예정/planned 작업
189
+ - 보류/on-hold 작업
190
+ - 대기/waiting 작업
191
+ - 차단/blocked 작업
192
+ - 사용자가 드랍한 작업과 이유
193
+ - 실행한 검증과 결과
194
+ - 변경한 파일과 갱신한 하네스 메모리
195
+ - 리스크, 가정, 블로커
196
+ - 추가로 진행하면 좋은 추천 방향
197
+ - 다음 세션에서 바로 수행할 단 하나의 정확한 작업
158
198
 
159
199
  ## 라이선스
160
200
 
package/bin/harness.js CHANGED
@@ -6,7 +6,7 @@ const path = require('path');
6
6
  const readline = require('readline');
7
7
  const childProcess = require('child_process');
8
8
 
9
- const VERSION = '1.3.1';
9
+ const VERSION = '1.4.0';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const MIGRATED = '<!-- leerness:migrated-legacy -->';
12
12
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
@@ -27,7 +27,7 @@ function write(p,s){ fs.mkdirSync(path.dirname(p),{recursive:true}); fs.writeFil
27
27
  function rel(root,p){ return path.relative(root,p).replace(/\\/g,'/') || '.'; }
28
28
  function parseJsonSafe(s,fallback){ try { return JSON.parse(s); } catch { return fallback; } }
29
29
  function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
30
- function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'비파괴 마이그레이션 · context routing · skill library'+c.reset); log(''); }
30
+ function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'language policy · context routing · task tracking · debug'+c.reset); log(''); }
31
31
  function installGuide(){
32
32
  log(c.bold+'설치/마이그레이션 안내'+c.reset);
33
33
  log(' - 기존 파일은 먼저 .harness/archive/ 에 백업합니다.');
@@ -35,6 +35,7 @@ function installGuide(){
35
35
  log(' - .env.example과 .gitignore는 덮어쓰지 않고 필요한 항목만 병합합니다.');
36
36
  log(' - AGENTS/CLAUDE/Cursor/Copilot 지침과 AX 라우팅 가이드는 최신 기준으로 갱신합니다.');
37
37
  log(' - 기존 메모리 파일까지 강제로 템플릿 재생성하려면 --force를 명시하세요.');
38
+ log(' - 세션 종료 시 진행/미완료/추천 방향을 반드시 session-handoff에 남깁니다.');
38
39
  log('');
39
40
  }
40
41
  function projectName(root){ try{ const pkg=JSON.parse(read(path.join(root,'package.json'))); if(pkg.name) return String(pkg.name).replace(/^@[^/]+\//,''); }catch{} return path.basename(root); }
@@ -43,6 +44,50 @@ function today(){ return now().slice(0,10); }
43
44
  function fill(t,ctx){ return t.replace(/{{([A-Z_]+)}}/g,(_,k)=>ctx[k]||''); }
44
45
  function slug(s){ return String(s||'skill').toLowerCase().replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'') || 'skill'; }
45
46
 
47
+ function normalizeLanguage(v){
48
+ const raw=String(v||'auto').toLowerCase();
49
+ if(['ko','kr','korean','한국어','hangul'].includes(raw)) return 'ko';
50
+ if(['en','english'].includes(raw)) return 'en';
51
+ if(raw==='auto') return 'auto';
52
+ return raw;
53
+ }
54
+ function languageName(code){ return code==='ko'?'Korean':code==='en'?'English':code; }
55
+ function hasKorean(text){ return /[가-힣]/.test(String(text||'')); }
56
+ function detectLanguage(root){
57
+ const candidates=['README.md','AGENTS.md','CLAUDE.md','docs/guideline.md','.harness/project-brief.md','.harness/current-state.md'];
58
+ let ko=0,total=0;
59
+ for(const f of candidates){ const p=path.join(root,f); if(!exists(p)) continue; const body=read(p).slice(0,8000); total+=body.length; const m=body.match(/[가-힣]/g); if(m) ko+=m.length; }
60
+ if(ko>=20 || (total && ko/Math.max(total,1)>0.02)) return 'ko';
61
+ return 'en';
62
+ }
63
+ function readConfiguredLanguage(root){
64
+ const mf=path.join(root,'.harness/manifest.json');
65
+ if(exists(mf)){ const j=parseJsonSafe(read(mf),{}); if(j.language) return normalizeLanguage(j.language); }
66
+ const lp=path.join(root,'.harness/LANGUAGE');
67
+ if(exists(lp)) return normalizeLanguage(read(lp).trim());
68
+ return null;
69
+ }
70
+ async function chooseLanguage(root,flags){
71
+ const requested=normalizeLanguage(flags.language||flags.lang||'auto');
72
+ const configured=readConfiguredLanguage(root);
73
+ if(requested!=='auto') return requested;
74
+ if(configured) return configured;
75
+ const detected=detectLanguage(root);
76
+ if(flags.yes||flags.y||!process.stdin.isTTY) return detected;
77
+ log(c.bold+'문서 작성 언어 선택'+c.reset);
78
+ log(' 1) 자동 감지: '+languageName(detected));
79
+ log(' 2) 한국어');
80
+ log(' 3) English');
81
+ const ans=await ask('\n선택 (Enter=자동 감지): ');
82
+ if(ans==='2') return 'ko';
83
+ if(ans==='3') return 'en';
84
+ return detected;
85
+ }
86
+ function languagePolicyBody(code){
87
+ if(code==='ko') return `${MARK}\n---\nleernessRole: language-policy\nreadWhen: [every-task, documentation, skill-writing, session-close]\nupdateWhen: [user-language-preference-change, project-language-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Language Policy\n\n## Primary Language\n\nKorean\n\n## Rule\n\n- 하네스 문서, 스킬 문서, 세션 인수인계, 진행 작업 목록은 한국어로 작성한다.\n- 코드 식별자, 파일명, 명령어, API 필드명, 환경변수명은 원문을 유지한다.\n- 외부 오류 메시지는 원문을 보존하고, 필요한 경우 한국어 설명을 덧붙인다.\n- 스킬 라이브러리에는 한글명(displayNameKo)과 가능한 작업(capabilities)을 반드시 유지한다.\n`;
88
+ return `${MARK}\n---\nleernessRole: language-policy\nreadWhen: [every-task, documentation, skill-writing, session-close]\nupdateWhen: [user-language-preference-change, project-language-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Language Policy\n\n## Primary Language\n\nEnglish\n\n## Rule\n\n- Write harness documents, skill documents, session handoffs, and progress lists in English.\n- Preserve code identifiers, filenames, commands, API fields, and environment variable names exactly.\n- Preserve external error messages verbatim and add explanations when useful.\n- Skill libraries must keep displayNameKo when available and list clear capabilities.\n`;
89
+ }
90
+
46
91
  function copyRecursive(src,dst,ignoreAbs=[]){
47
92
  const abs=path.resolve(src); if(ignoreAbs.some(i=>abs===i||abs.startsWith(i+path.sep))) return;
48
93
  const st=fs.statSync(src);
@@ -94,12 +139,104 @@ function noteLegacyPreserved(root,found,dryRun){
94
139
  }
95
140
 
96
141
  const coreFiles = {
97
- 'AGENTS.md': `${MARK}\n# {{PROJECT}} AI Agent Harness\n\nAgent = Model + Leerness Harness.\n\n## Core Rule\nBefore editing, route the task. Read .harness/context-routing.md and use \`leerness route <task-type>\` when the task type is unclear.\n\n## Universal Read Order\n1. .harness/project-brief.md\n2. .harness/current-state.md\n3. .harness/context-routing.md\n4. .harness/writeback-policy.md\n5. .harness/task-type-map.md\n6. .harness/context-map.md\n7. .harness/guardrails.md\n8. .harness/skills-lock.json\n\n## Task Routing\n- Feature/API work: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.\n- UI/design work: design-system.md, feature-contracts.md, skills/ui-consistency.md.\n- Debugging: task-log.md, current-state.md, skills/debugging.md, related feature contract.\n- Refactoring: architecture.md, decisions.md, guardrails.md, skills/refactoring.md.\n- Release/deploy: release-checklist.md, testing-strategy.md, current-state.md, decisions.md.\n- Migration: AX_MIGRATION_GUIDE.md, context-routing.md, writeback-policy.md.\n- New install: AX_NEW_PROJECT_GUIDE.md and actual project config/source files.\n- Skill/library work: AX_SKILL_LIBRARY_GUIDE.md and ai-verified-skill-publisher when installed.\n\n## Writeback Rules\n- Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.\n- Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.\n- Update feature-contracts.md when input/output/state/error behavior changes.\n- Update design-system.md when UI rules, components, layout, spacing, or states change.\n- Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.\n- Update context-map.md when important files, modules, routes, commands, or ownership areas change.\n- Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.\n\n## Non-Destructive Migration Policy\n- Never overwrite existing project memory files unless the user explicitly requests --force.\n- Preserve .env.example and .gitignore; append missing Leerness entries only.\n- Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.\n\n## Response Contract\n- Task type and files consulted\n- Summary\n- Files changed\n- Verification\n- Memory files updated\n- Risks or assumptions\n- Next step\n{{LEGACY_AGENT}}\n`,
142
+ 'AGENTS.md': `${MARK}
143
+ # {{PROJECT}} AI Agent Harness
144
+
145
+ Agent = Model + Leerness Harness.
146
+
147
+ ## Core Rule
148
+ Before editing, route the task. Read .harness/language-policy.md, .harness/context-routing.md, and use \`leerness route <task-type>\` when the task type is unclear.
149
+
150
+ ## Universal Read Order
151
+ 1. .harness/project-brief.md
152
+ 2. .harness/current-state.md
153
+ 3. .harness/language-policy.md
154
+ 4. .harness/context-routing.md
155
+ 5. .harness/writeback-policy.md
156
+ 6. .harness/task-type-map.md
157
+ 7. .harness/context-map.md
158
+ 8. .harness/guardrails.md
159
+ 9. .harness/skills-lock.json
160
+
161
+ ## Language Rule
162
+ - Before writing or updating any harness/skill/session document, read .harness/language-policy.md.
163
+ - Use the configured project language consistently.
164
+ - Preserve code names, commands, file paths, environment variables, and API field names exactly.
165
+
166
+ ## Task Routing
167
+ - Feature/API work: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.
168
+ - UI/design work: design-system.md, feature-contracts.md, skills/ui-consistency.md.
169
+ - Debugging: task-log.md, current-state.md, skills/debugging.md, related feature contract.
170
+ - Refactoring: architecture.md, decisions.md, guardrails.md, skills/refactoring.md.
171
+ - Release/deploy: release-checklist.md, testing-strategy.md, current-state.md, decisions.md.
172
+ - Migration: AX_MIGRATION_GUIDE.md, context-routing.md, writeback-policy.md.
173
+ - New install: AX_NEW_PROJECT_GUIDE.md and actual project config/source files.
174
+ - Skill/library work: AX_SKILL_LIBRARY_GUIDE.md and ai-verified-skill-publisher when installed.
175
+ - Harness debug: debug-guide.md, language-policy.md, context-routing.md, writeback-policy.md, progress-tracker.md.
176
+
177
+ ## Writeback Rules
178
+ - Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.
179
+ - Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.
180
+ - Update feature-contracts.md when input/output/state/error behavior changes.
181
+ - Update design-system.md when UI rules, components, layout, spacing, or states change.
182
+ - Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.
183
+ - Update context-map.md when important files, modules, routes, commands, or ownership areas change.
184
+ - Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.
185
+
186
+ ## Non-Destructive Migration Policy
187
+ - Never overwrite existing project memory files unless the user explicitly requests --force.
188
+ - Preserve .env.example and .gitignore; append missing Leerness entries only.
189
+ - Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.
190
+
191
+ ## Progress Tracker Rule
192
+ - Track user-requested work in .harness/progress-tracker.md.
193
+ - Use statuses: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped.
194
+ - If the user drops a task, mark it as dropped with reason instead of deleting the history.
195
+ - Every session close must list active unresolved work: requested, planned, waiting, on-hold, blocked, in-progress, incomplete.
196
+
197
+ ## End-of-Session Contract
198
+ Every meaningful session must close with a handoff. Do not stop at "done". Before the final answer, check .harness/session-close-policy.md, .harness/progress-tracker.md, and .harness/anti-lazy-work-policy.md.
199
+
200
+ At the end of each session, list:
201
+ 1. Completed work in this session.
202
+ 2. User-requested work still in progress.
203
+ 3. User-requested work not started or incomplete.
204
+ 4. Planned, on-hold, waiting, blocked, requested, and incomplete work from progress-tracker.md.
205
+ 5. User-dropped work, if any.
206
+ 6. Verification performed and results.
207
+ 7. Memory files updated.
208
+ 8. Risks, assumptions, or blockers.
209
+ 9. Recommended next directions.
210
+ 10. The single next exact action.
211
+
212
+ ## Anti-Lazy Work Rule
213
+ - Do not hide unfinished work behind vague summaries.
214
+ - Do not claim completion without verification or explicit limits.
215
+ - If partial, say exactly what is partial and what remains.
216
+ - Prefer concrete file names, commands, and checks over generic phrases.
217
+ - If tests or verification were skipped, state why and what should be run next.
218
+
219
+ ## Response Contract
220
+ - Task type and files consulted
221
+ - Summary
222
+ - Completed work
223
+ - In-progress work
224
+ - Incomplete requested work
225
+ - Files changed
226
+ - Verification
227
+ - Memory files updated
228
+ - Risks or assumptions
229
+ - Recommended next directions
230
+ - Next exact step
231
+ {{LEGACY_AGENT}}
232
+ `,
98
233
  'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nUse AGENTS.md as the source of truth. Route every task through .harness/context-routing.md and .harness/task-type-map.md. Do not overwrite existing project memory during migration unless --force is explicit.\n`,
99
234
  '.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nRead AGENTS.md first. Follow .harness/context-routing.md, writeback-policy.md, installed skills, design-system, feature-contracts, and guardrails.\n`,
100
235
  '.github/copilot-instructions.md': `${MARK}\n# GitHub Copilot Instructions\n\nUse AGENTS.md and .harness/ as the project memory. Preserve existing project memory files unless --force is explicit.\n`,
101
236
  '.gitignore': `# Leerness local-only files\n.env\n.env.local\n*.secret.json\n.harness/skill-config.local.json\n.harness/skill-publish.local.json\n`,
102
237
  '.env.example': `# Leerness examples only. Copy to .env.local and fill locally. Never commit real secrets.\n`,
238
+ '.harness/LANGUAGE': '{{LANGUAGE}}\n',
239
+ '.harness/language-policy.md': '{{LANGUAGE_POLICY}}',
103
240
  '.harness/HARNESS_VERSION': '{{VERSION}}\n',
104
241
  '.harness/manifest.json': '{{MANIFEST}}\n',
105
242
  '.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
@@ -117,10 +254,92 @@ const coreFiles = {
117
254
  '.harness/testing-strategy.md': `${MARK}\n---\nleernessRole: testing-strategy\nreadWhen: [feature, debugging, refactor, release]\nupdateWhen: [test-command-change, new-critical-flow, regression-added]\ndoNotStore: [secrets]\n---\n\n# Testing Strategy\n\n## Commands\n\n## Critical Flows\n\n## Regression Notes\n`,
118
255
  '.harness/review-checklist.md': `${MARK}\n# Review Checklist\n\n- [ ] Architecture preserved\n- [ ] Feature contract preserved or updated\n- [ ] Design system followed\n- [ ] No secrets stored\n- [ ] Writeback files updated\n`,
119
256
  '.harness/release-checklist.md': `${MARK}\n---\nleernessRole: release-checklist\nreadWhen: [release, deploy, ci, npm-publish, git-push, env-change]\nupdateWhen: [deploy-failure, new-env-var, ci-change, rollback-change, release-rule-change]\ndoNotStore: [secrets, tokens, passwords, cookies]\n---\n\n# Release Checklist\n\n## Commands\n\n## Required Environment Variables\n\n## Verification\n\n## Rollback\n`,
120
- '.harness/session-handoff.md': `${MARK}\n---\nleernessRole: session-handoff\nreadWhen: [resume-work, every-new-session]\nupdateWhen: [end-of-session, handoff, blocked-work]\ndoNotStore: [secrets, tokens]\n---\n\n# Session Handoff\n\n## Done\n\n## Changed Files\n\n## Verification\n\n## Risks\n\n## Next Exact Step\n`,
257
+ '.harness/session-handoff.md': `${MARK}
258
+ ---
259
+ leernessRole: session-handoff
260
+ readWhen: [resume-work, every-new-session, end-of-session]
261
+ updateWhen: [end-of-session, handoff, blocked-work, partial-completion]
262
+ doNotStore: [secrets, tokens, raw-private-data]
263
+ ---
264
+
265
+ # Session Handoff
266
+
267
+ ## Session Summary
268
+ - Date:
269
+ - Task type:
270
+ - User request:
271
+
272
+ ## Completed This Session
273
+ -
274
+
275
+ ## In Progress From User Requests
276
+ -
277
+
278
+ ## Incomplete / Not Started From User Requests
279
+ -
280
+
281
+ ## Files Changed
282
+ -
283
+
284
+ ## Verification Performed
285
+ - Command/check:
286
+ - Result:
287
+
288
+ ## Memory Files Updated
289
+ -
290
+
291
+ ## Risks / Assumptions / Blockers
292
+ -
293
+
294
+ ## Recommended Next Directions
295
+ -
296
+
297
+ ## Next Exact Step
298
+ -
299
+ `,
300
+ '.harness/session-close-policy.md': `${MARK}\n---\nleernessRole: session-close-policy\nreadWhen: [end-of-session, every-final-response, partial-completion, handoff]\nupdateWhen: [session-close-format-change, repeated-handoff-failure, reporting-standard-change]\ndoNotStore: [secrets, tokens, credentials, raw-private-data]\n---\n\n# Session Close Policy\n\nEvery meaningful AI work session must end with a concrete handoff. This prevents hidden unfinished work and makes the next session restartable.\n\n## Required Final Checklist\n\nBefore the final answer, the AI must inspect whether the session had meaningful work. If yes, it must provide or update:\n\n1. Completed work in this session.\n2. User-requested work still in progress.\n3. User-requested work incomplete or not started.\n4. Verification performed and exact results.\n5. Files or documents changed.\n6. Harness memory files updated.\n7. Risks, assumptions, blockers, or skipped checks.\n8. Recommended next directions.\n9. The single next exact action.\n\n## Required Memory Writeback\n\nUpdate these files when meaningful work occurred:\n\n- current-state.md\n- task-log.md\n- session-handoff.md\n\nUpdate these when relevant:\n\n- decisions.md\n- feature-contracts.md\n- design-system.md\n- release-checklist.md\n- context-map.md\n- progress-tracker.md\n\n## Completion Labels\n\nUse one of these labels for each requested item:\n\n- done\n- in-progress\n- blocked\n- incomplete\n- skipped-with-reason\n\nNever mark work done if verification was not performed or if key requested scope remains unfinished.\n`,
301
+ '.harness/progress-tracker.md': `${MARK}
302
+ ---
303
+ leernessRole: progress-tracker
304
+ readWhen: [planning, resume-work, end-of-session, multi-step-work]
305
+ updateWhen: [task-started, task-completed, task-blocked, task-dropped, scope-change, end-of-session]
306
+ doNotStore: [secrets, tokens, credentials, raw-private-data]
307
+ ---
308
+
309
+ # Progress Tracker
310
+
311
+ Use this file to track user-requested work across sessions. Keep entries concrete and checkable. At session close, unresolved statuses must be listed.
312
+
313
+ | ID | User Request | Status | Owner | Last Update | Evidence / Notes | Next Action |
314
+ |---|---|---|---|---|---|---|
315
+ | T-0001 | Initialize Leerness project memory | done | AI | {{DATE}} | Leerness v{{VERSION}} installed or migrated. | Fill project-specific details. |
316
+
317
+ ## Status Values
318
+
319
+ - requested
320
+ - planned
321
+ - in-progress
322
+ - waiting
323
+ - on-hold
324
+ - blocked
325
+ - incomplete
326
+ - done
327
+ - dropped
328
+
329
+ ## Drop Policy
330
+
331
+ Dropped tasks are not deleted. Mark Status as dropped and write the reason in Evidence / Notes.
332
+
333
+ ## Session Close Rule
334
+
335
+ Every session-close report must list all tasks whose Status is planned, waiting, on-hold, blocked, in-progress, incomplete, or requested.
336
+ `,
337
+ '.harness/anti-lazy-work-policy.md': `${MARK}\n---\nleernessRole: anti-lazy-work-policy\nreadWhen: [every-task, end-of-session, verification, planning]\nupdateWhen: [quality-failure, repeated-shortcut, missed-verification, reporting-rule-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Anti-Lazy Work Policy\n\nThe AI must not appear productive while leaving important work vague or incomplete.\n\n## Required Behavior\n\n- State exactly what was done and what was not done.\n- Prefer concrete file paths, commands, checks, and outputs.\n- Do not skip obvious verification when tools are available.\n- If a check cannot be run, say so and provide the exact command to run.\n- Do not collapse multiple unfinished user requests into a generic sentence.\n- Do not overwrite project memory to avoid doing the harder merge.\n- Do not call a task complete only because files were generated. Confirm behavior or clearly label it unverified.\n\n## Laziness Warning Signs\n\n- done without changed files or verification.\n- should work without a check.\n- No mention of incomplete user-requested items.\n- No next exact action.\n- Memory files not updated after meaningful work.\n\n## Minimum Final Answer Standard\n\nA final answer after meaningful work must include:\n\n- Completed\n- In progress\n- Incomplete\n- Verification\n- Updated memory\n- Risks\n- Recommended next directions\n`,
338
+ '.harness/templates/end-of-session-report.md': `${MARK}\n# End-of-Session Report\n\n## Completed This Session\n-\n\n## In Progress From User Requests\n-\n\n## Incomplete / Not Started From User Requests\n-\n\n## Planned Tasks\n-\n\n## Waiting Tasks\n-\n\n## On-Hold Tasks\n-\n\n## Blocked Tasks\n-\n\n## Dropped By User\n-\n\n## Verification\n-\n\n## Files Changed\n-\n\n## Memory Files Updated\n-\n\n## Risks / Assumptions / Blockers\n-\n\n## Recommended Next Directions\n-\n\n## Next Exact Step\n-\n`,
339
+ '.harness/debug-guide.md': `${MARK}\n# Leerness Debug Guide\n\nUse this when checking whether the harness is actually guiding the AI.\n\n## Debug Checklist\n\n- AGENTS.md references language-policy, context-routing, writeback-policy, progress-tracker, and anti-lazy policy.\n- language-policy.md exists and defines one primary language.\n- context-routing.md maps task types to read/update files.\n- writeback-policy.md explains where each kind of information goes.\n- progress-tracker.md contains a task table and unresolved status values.\n- session-close-policy.md forces active unresolved work to be listed.\n- anti-lazy-work-policy.md prevents unverified completion claims.\n- skills-lock.json records installed skills.\n\nRun: leerness debug [path]\n`,
121
340
  '.harness/skill-index.md': `${MARK}\n# Skill Index\n\n| Task | Skill |\n|---|---|\n| Codebase analysis | skills/codebase-analysis.md |\n| Feature implementation | skills/feature-implementation.md |\n| Debugging | skills/debugging.md |\n| UI consistency | skills/ui-consistency.md |\n| Release | skills/release-check.md |\n`,
122
- '.harness/context-routing.md': `${MARK}\n# Context Routing\n\nUse this file to decide what to read before work and what to update afterward.\n\n## feature\nRead: project-brief, current-state, architecture, context-map, feature-contracts, skills/feature-implementation.\nUpdate: current-state, task-log, session-handoff, feature-contracts, context-map when paths change.\n\n## ui\nRead: design-system, feature-contracts, context-map, skills/ui-consistency.\nUpdate: design-system, feature-contracts, current-state, task-log, session-handoff.\n\n## debugging\nRead: current-state, task-log, feature-contracts, testing-strategy, skills/debugging.\nUpdate: task-log, current-state, session-handoff, testing-strategy when regression coverage changes.\n\n## release\nRead: release-checklist, testing-strategy, current-state, decisions, secret-policy.\nUpdate: release-checklist, task-log, current-state, session-handoff.\n\n## migration\nRead: AX_MIGRATION_GUIDE, writeback-policy, task-type-map.\nUpdate: only missing files by default; preserve project memory unless --force.\n`,
123
- '.harness/writeback-policy.md': `${MARK}\n# Writeback Policy\n\n## current-state.md\nCurrent progress, blockers, next work.\n\n## task-log.md\nWhat changed, when, and verification result.\n\n## session-handoff.md\nEnough context for the next AI session to continue.\n\n## decisions.md\nImportant choices and tradeoffs.\n\n## release-checklist.md\nDeploy commands, env requirements, rollback, failures.\n\n## design-system.md\nUI rules and reusable patterns.\n\n## feature-contracts.md\nInput/output/state/error contracts.\n\n## project-brief.md\nProduct purpose and success criteria only.\n`,
341
+ '.harness/context-routing.md': `${MARK}\n# Context Routing\n\nUse this file to decide what to read before work and what to update afterward.\n\n## feature\nRead: project-brief, current-state, architecture, context-map, feature-contracts, skills/feature-implementation.\nUpdate: current-state, task-log, session-handoff, feature-contracts, context-map when paths change.\n\n## ui\nRead: design-system, feature-contracts, context-map, skills/ui-consistency.\nUpdate: design-system, feature-contracts, current-state, task-log, session-handoff.\n\n## debugging\nRead: current-state, task-log, feature-contracts, testing-strategy, skills/debugging.\nUpdate: task-log, current-state, session-handoff, testing-strategy when regression coverage changes.\n\n## release\nRead: release-checklist, testing-strategy, current-state, decisions, secret-policy.\nUpdate: release-checklist, task-log, current-state, session-handoff.\n\n## migration\nRead: AX_MIGRATION_GUIDE, writeback-policy, task-type-map.\nUpdate: only missing files by default; preserve project memory unless --force.\n\n## session-close\nRead: session-close-policy, progress-tracker, current-state, task-log, session-handoff, anti-lazy-work-policy.\nUpdate: session-handoff, progress-tracker, current-state, task-log, and any relevant memory files changed by the session.\n`,
342
+ '.harness/writeback-policy.md': `${MARK}\n# Writeback Policy\n\n## current-state.md\nCurrent progress, blockers, next work.\n\n## task-log.md\nWhat changed, when, and verification result.\n\n## session-handoff.md\nEnough context for the next AI session to continue.\n\n## decisions.md\nImportant choices and tradeoffs.\n\n## release-checklist.md\nDeploy commands, env requirements, rollback, failures.\n\n## design-system.md\nUI rules and reusable patterns.\n\n## feature-contracts.md\nInput/output/state/error contracts.\n\n## project-brief.md\nProduct purpose and success criteria only.\n\n## progress-tracker.md\nUser-requested work items, status, evidence, and next actions across sessions.\n\n## session-close-policy.md\nFinal response and handoff rules. Update only when the reporting standard changes.\n\n## anti-lazy-work-policy.md\nQuality guardrails that prevent vague or incomplete closure. Update when repeated failure patterns appear.\n`,
124
343
  '.harness/task-type-map.md': `${MARK}\n# Task Type Map\n\n| User request | Task type | First files |\n|---|---|---|\n| 새 기능 | feature | feature-contracts, architecture |\n| 디자인/UI | ui | design-system |\n| 오류 수정 | debugging | task-log, debugging skill |\n| 구조 개선 | refactor | architecture, decisions |\n| 배포 | release | release-checklist |\n| 하네스 전환 | migration | AX_MIGRATION_GUIDE |\n| 신규 적용 | new-install | AX_NEW_PROJECT_GUIDE |\n| 스킬 저장/배포 | skill-library | AX_SKILL_LIBRARY_GUIDE |\n`,
125
344
  '.harness/AX_MIGRATION_GUIDE.md': `${MARK}\n# AX Migration Guide\n\n## Goal\nMigrate old harness files without losing project memory.\n\n## Procedure\n1. Run: leerness migrate --dry-run\n2. Confirm archive target.\n3. Run: leerness migrate\n4. Check .env.example and .gitignore were merged, not replaced.\n5. Check project memory files were preserved.\n6. Fill only missing context using archived legacy files.\n7. Run: leerness status && leerness verify.\n\n## Critical Rule\nDo not overwrite existing project-brief, current-state, architecture, decisions, release-checklist, feature-contracts, or design-system unless the user explicitly asks for --force.\n`,
126
345
  '.harness/AX_NEW_PROJECT_GUIDE.md': `${MARK}\n# AX New Project Guide\n\n## Goal\nAfter initial installation, populate Leerness memory from the actual project.\n\n## Read actual project files\n- package/config files\n- app/routes/pages\n- API/server/functions\n- DB/schema/rules\n- deploy/CI files\n- tests\n\n## Fill memory files\n- project-brief.md: purpose and success criteria\n- architecture.md: modules and data flow\n- context-map.md: important files and routes\n- design-system.md: existing UI patterns\n- feature-contracts.md: major features and states\n- release-checklist.md: real deploy commands and env requirements\n`,
@@ -137,8 +356,8 @@ const coreFiles = {
137
356
  '.harness/templates/decision.md': `${MARK}\n# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n`
138
357
  };
139
358
 
140
- const memoryFiles = new Set(['.harness/project-brief.md','.harness/current-state.md','.harness/architecture.md','.harness/context-map.md','.harness/decisions.md','.harness/task-log.md','.harness/constraints.md','.harness/guardrails.md','.harness/design-system.md','.harness/feature-contracts.md','.harness/testing-strategy.md','.harness/review-checklist.md','.harness/release-checklist.md','.harness/session-handoff.md','.harness/skill-index.md','.harness/secret-policy.md']);
141
- const refreshableFiles = new Set(['AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md','.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md','.harness/AX_SKILL_LIBRARY_GUIDE.md','.harness/AX_MIGRATION_GUIDE.md','.harness/AX_NEW_PROJECT_GUIDE.md','.harness/manifest.json','.harness/HARNESS_VERSION']);
359
+ const memoryFiles = new Set(['.harness/project-brief.md','.harness/current-state.md','.harness/architecture.md','.harness/context-map.md','.harness/decisions.md','.harness/task-log.md','.harness/constraints.md','.harness/guardrails.md','.harness/design-system.md','.harness/feature-contracts.md','.harness/testing-strategy.md','.harness/review-checklist.md','.harness/release-checklist.md','.harness/session-handoff.md','.harness/progress-tracker.md','.harness/language-policy.md','.harness/debug-guide.md','.harness/skill-index.md','.harness/secret-policy.md']);
360
+ const refreshableFiles = new Set(['AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md','.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md','.harness/AX_SKILL_LIBRARY_GUIDE.md','.harness/AX_MIGRATION_GUIDE.md','.harness/AX_NEW_PROJECT_GUIDE.md','.harness/session-close-policy.md','.harness/anti-lazy-work-policy.md','.harness/templates/end-of-session-report.md','.harness/debug-guide.md','.harness/language-policy.md','.harness/LANGUAGE','.harness/manifest.json','.harness/HARNESS_VERSION']);
142
361
  function uniqueLinesAppend(current, addition){
143
362
  const lines=current.split(/\r?\n/); const seen=new Set(lines.map(x=>x.trim()).filter(Boolean));
144
363
  const add=addition.split(/\r?\n/).filter(line=>{ const t=line.trim(); if(!t||seen.has(t)) return false; seen.add(t); return true; });
@@ -165,9 +384,9 @@ function writeCoreSafely(root,file,body,opts={}){
165
384
  }
166
385
  if(dryRun) info('[dry-run] 보존: '+file+' (덮어쓰려면 --force)'); else ok('보존: '+file+' (덮어쓰려면 --force)'); return 'preserved';
167
386
  }
168
- function manifest(root,selectedSkills){ return JSON.stringify({name:projectName(root),harnessVersion:VERSION,installedAt:now(),managedFiles:Object.keys(coreFiles),selectedSkills,nonDestructiveMigration:true},null,2); }
387
+ function manifest(root,selectedSkills,language){ return JSON.stringify({name:projectName(root),harnessVersion:VERSION,language,languageName:languageName(language),installedAt:now(),managedFiles:Object.keys(coreFiles),selectedSkills,nonDestructiveMigration:true,taskStatuses:['requested','planned','in-progress','waiting','on-hold','blocked','incomplete','done','dropped']},null,2); }
169
388
  function skillsLock(root,selectedSkills){ const lock={harnessVersion:VERSION,installedAt:now(),installedSkills:{}}; for(const name of selectedSkills){ const meta=getSkillMeta(name); if(meta) lock.installedSkills[name]={version:meta.version,source:'bundled',title:meta.title,displayNameKo:meta.displayNameKo||meta.title,lastUpdated:meta.lastUpdated,verificationStatus:(meta.verification||{}).status||'unknown'}; } return JSON.stringify(lock,null,2); }
170
- function makeContext(root,legacyText,selectedSkills){ return { PROJECT:projectName(root), DATE:today(), VERSION, LEGACY_AGENT:legacyBlock('agent instructions',pick(legacyText,['AGENTS.md','AGENT.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md'])), LEGACY_BRIEF:legacyBlock('project context',pick(legacyText,['PROJECT_CONTEXT.md','CONTEXT.md','docs/guideline.md','AI_HARNESS.md','HARNESS.md'])), LEGACY_STATE:legacyBlock('state',pick(legacyText,['CURRENT_STATE.md','TASK_LOG.md','docs/history.md'])), LEGACY_ARCH:legacyBlock('architecture',pick(legacyText,['ARCHITECTURE.md'])), LEGACY_DECISIONS:legacyBlock('decisions',pick(legacyText,['DECISIONS.md'])), MANIFEST:manifest(root,selectedSkills), SKILLS_LOCK:skillsLock(root,selectedSkills) }; }
389
+ function makeContext(root,legacyText,selectedSkills,language){ const lang=normalizeLanguage(language||readConfiguredLanguage(root)||detectLanguage(root)); return { PROJECT:projectName(root), DATE:today(), VERSION, LANGUAGE:lang, LANGUAGE_NAME:languageName(lang), LANGUAGE_POLICY:languagePolicyBody(lang), LEGACY_AGENT:legacyBlock('agent instructions',pick(legacyText,['AGENTS.md','AGENT.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md'])), LEGACY_BRIEF:legacyBlock('project context',pick(legacyText,['PROJECT_CONTEXT.md','CONTEXT.md','docs/guideline.md','AI_HARNESS.md','HARNESS.md'])), LEGACY_STATE:legacyBlock('state',pick(legacyText,['CURRENT_STATE.md','TASK_LOG.md','docs/history.md'])), LEGACY_ARCH:legacyBlock('architecture',pick(legacyText,['ARCHITECTURE.md'])), LEGACY_DECISIONS:legacyBlock('decisions',pick(legacyText,['DECISIONS.md'])), MANIFEST:manifest(root,selectedSkills,lang), SKILLS_LOCK:skillsLock(root,selectedSkills) }; }
171
390
 
172
391
  function listSkillPacks(){ if(!exists(PACKS_DIR)) return []; return fs.readdirSync(PACKS_DIR).map(n=>getSkillMeta(n)).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name)); }
173
392
  function getSkillMeta(name){ const metaPath=path.join(PACKS_DIR,name,'skill.json'); if(!exists(metaPath)) return null; const meta=parseJsonSafe(read(metaPath),null); if(!meta||!meta.name) return null; return meta; }
@@ -176,19 +395,20 @@ function appendEnvExample(root,meta){ const ep=path.join(root,'.env.example'); c
176
395
  function installSkill(root,name,dryRun=false){ const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return false; } const packRoot=path.join(PACKS_DIR,name); const destRoot=path.join(root,'.harness/skills',name); if(dryRun){ info('[dry-run] install skill: '+name); return true; } fs.mkdirSync(destRoot,{recursive:true}); for(const file of meta.files||[]){ const src=path.join(packRoot,file); const dest=path.join(destRoot,path.basename(file)); if(exists(src)){ write(dest,read(src)); ok('스킬 설치: '+rel(root,dest)); } } write(path.join(destRoot,'skill.json'),JSON.stringify(meta,null,2)+'\n'); updateSkillLock(root,meta,false); appendEnvExample(root,meta); return true; }
177
396
  function removeSkill(root,name){ const meta=getSkillMeta(name)||{name,title:name}; const dest=path.join(root,'.harness/skills',name); if(exists(dest)) fs.rmSync(dest,{recursive:true,force:true}); updateSkillLock(root,meta,true); ok('스킬 제거: '+name); }
178
397
 
179
- function parseArgs(argv){ const out={flags:{},positionals:[]}; const valueFlags=new Set(['skills','path','from','out','target','package','repo','version','title','description','category','source','name','registry','branch','message','reviewer','by','token-env']); for(let i=0;i<argv.length;i++){ const a=argv[i]; if(a.startsWith('--')){ const eq=a.indexOf('='); const key=eq>=0?a.slice(2,eq):a.slice(2); if(eq>=0) out.flags[key]=a.slice(eq+1); else if(valueFlags.has(key)&&argv[i+1]&&!argv[i+1].startsWith('-')) out.flags[key]=argv[++i]; else out.flags[key]=true; } else if(a.startsWith('-')) out.flags[a.slice(1)]=true; else out.positionals.push(a); } return out; }
398
+ function parseArgs(argv){ const out={flags:{},positionals:[]}; const valueFlags=new Set(['skills','path','from','out','target','package','repo','version','title','description','category','source','name','registry','branch','message','reviewer','by','token-env','language','lang','status','reason','owner','evidence','next','action']); for(let i=0;i<argv.length;i++){ const a=argv[i]; if(a.startsWith('--')){ const eq=a.indexOf('='); const key=eq>=0?a.slice(2,eq):a.slice(2); if(eq>=0) out.flags[key]=a.slice(eq+1); else if(valueFlags.has(key)&&argv[i+1]&&!argv[i+1].startsWith('-')) out.flags[key]=argv[++i]; else out.flags[key]=true; } else if(a.startsWith('-')) out.flags[a.slice(1)]=true; else out.positionals.push(a); } return out; }
180
399
  function splitSkills(value){ if(!value||value===true) return []; if(value==='recommended') return ['office','commerce-api','crawling','ai-verified-skill-publisher']; if(value==='all') return listSkillPacks().map(x=>x.name); return String(value).split(',').map(x=>x.trim()).filter(Boolean); }
181
400
  function ask(q){ const rl=readline.createInterface({input:process.stdin,output:process.stdout}); return new Promise(resolve=>rl.question(q,a=>{rl.close();resolve(a.trim());})); }
182
401
  async function chooseSkills(autoYes,provided){ if(provided!==undefined) return splitSkills(provided); if(autoYes||!process.stdin.isTTY) return []; const packs=listSkillPacks(); if(!packs.length) return []; log(c.bold+'설치할 스킬 라이브러리 선택'+c.reset); log(' 0) 기본 하네스만 설치'); packs.forEach((p,i)=>{ log(' '+(i+1)+') '+(p.displayNameKo||p.title)+' ('+p.name+')'); if((p.capabilities||[]).length) log(' 가능 작업: '+p.capabilities.slice(0,4).join(', ')); }); log(' all) 전체 설치'); const ans=await ask('\n선택 (예: 1,3 또는 all, Enter=기본): '); if(!ans||ans==='0') return []; if(ans.toLowerCase()==='all') return packs.map(p=>p.name); return ans.split(',').map(s=>parseInt(s.trim(),10)).filter(n=>n>=1&&n<=packs.length).map(n=>packs[n-1].name); }
183
402
 
184
403
  async function init(root,flags){
185
404
  root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root);
405
+ const selectedLanguage=await chooseLanguage(root,flags); info('문서 언어: '+languageName(selectedLanguage));
186
406
  const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills);
187
407
  const found=detectLegacy(root), legacyText=collectLegacyText(found);
188
408
  if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
189
409
  const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive));
190
410
  noteLegacyPreserved(root,found,false);
191
- const ctx=makeContext(root,legacyText,selectedSkills);
411
+ const ctx=makeContext(root,legacyText,selectedSkills,selectedLanguage);
192
412
  for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{force:Boolean(flags.force)});
193
413
  if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); }
194
414
  ok('설치 완료'); info('신규 프로젝트라면 .harness/AX_NEW_PROJECT_GUIDE.md를 따라 실제 프로젝트 내용을 반영하세요.');
@@ -200,7 +420,8 @@ function migrate(root,flags){
200
420
  else { warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
201
421
  const archive=archiveLegacy(root,found,dryRun); if(archive) info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive));
202
422
  noteLegacyPreserved(root,found,dryRun);
203
- const ctx=makeContext(root,collectLegacyText(found),[]);
423
+ const selectedLanguage=normalizeLanguage(flags.language||flags.lang||readConfiguredLanguage(root)||detectLanguage(root)); info('문서 언어: '+languageName(selectedLanguage));
424
+ const ctx=makeContext(root,collectLegacyText(found),[],selectedLanguage);
204
425
  for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{dryRun,force});
205
426
  if(!dryRun){ ok('마이그레이션 완료'); info('기존 프로젝트 메모리 파일은 보존되었습니다. 템플릿 재생성이 필요하면 --force를 명시하세요.'); }
206
427
  }
@@ -220,13 +441,131 @@ function validateSkillLibrary(dir,opts={}){ dir=path.resolve(dir); let failures=
220
441
  function learnSkillLibrary(root,flags){ root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slug(flags.name||path.basename(from)); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name)); if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; } const files=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const envs=new Set(), copied=[]; for(const f of files){ const body=read(f); inferEnvNames(body).forEach(e=>envs.add(e)); const dest='skills/'+path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); write(path.join(outRoot,dest),body); copied.push(dest); } const meta={name,version:String(flags.version||'0.1.0'),title:flags.title||name,description:flags.description||'Learned Leerness skill library.',category:flags.category||'custom',requiresEnv:Array.from(envs).sort(),files:copied,lastUpdated:today(),lastUpdatedAt:now(),verification:{status:'needs-review',method:'none',verifiedBy:null,verifiedAt:null,checks:[]}}; write(path.join(outRoot,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(outRoot,'README.md'),'# '+meta.title+'\n\n'+meta.description+'\n'); ok('스킬 라이브러리 학습 완료: '+outRoot); info('다음: leerness library verify '+outRoot+' --ai --reviewer leerness-ai'); }
221
442
  function verifySkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const res=validateSkillLibrary(dir,{silent:false}); if(!res.ok){ process.exitCode=1; return; } const meta=res.meta; meta.verification={status:'passed',method:'ai-assisted-review',verifiedBy:String(flags.reviewer||flags.by||'leerness-ai'),verifiedAt:now(),checks:['structure','secret-scan','env-reference-only','metadata']}; meta.lastUpdated=today(); meta.lastUpdatedAt=now(); write(path.join(dir,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(dir,'ai-verification.json'),JSON.stringify(meta.verification,null,2)+'\n'); ok('AI 검증 완료: '+meta.name); }
222
443
  function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const res=validateSkillLibrary(dir,{silent:false,strictAi:Boolean(flags['strict-ai'])}); if(!res.ok){ process.exitCode=1; return; } const meta=res.meta; const out=path.resolve(flags.out||path.join(dir,'dist')); const libRoot=path.join(out,slug(meta.name)); if(exists(libRoot)) fs.rmSync(libRoot,{recursive:true,force:true}); fs.mkdirSync(libRoot,{recursive:true}); for(const item of ['README.md','skill-library.json','skill.json','ai-verification.json','env.example','skills','examples','migrations']){ const src=path.join(dir,item); if(exists(src)) copyRecursive(src,path.join(libRoot,item)); } const pkg={name:flags.package||('leerness-skill-'+slug(meta.name)),version:meta.version||'0.1.0',description:meta.description||meta.title||meta.name,type:'commonjs',files:['skill-library.json','ai-verification.json','README.md','env.example','skills/','examples/','migrations/'],keywords:['leerness','harness-skill','ai-skill-library'],license:'MIT',publishConfig:{access:'public'},harnessSkill:meta}; write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n'); ok('스킬 라이브러리 빌드 완료: '+libRoot); }
223
- function resolvePublishToken(target,flags){ if(flags['token-env']&&process.env[flags['token-env']]) return process.env[flags['token-env']]; const names=target==='npm'?['LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN']:['LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN']; for(const n of names) if(process.env[n]) return process.env[n]; return null; }
444
+ function resolvePublishToken(target,flags){ if(flags['token-env','language','lang','status','reason','owner','evidence','next','action']&&process.env[flags['token-env','language','lang','status','reason','owner','evidence','next','action']]) return process.env[flags['token-env','language','lang','status','reason','owner','evidence','next','action']]; const names=target==='npm'?['LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN']:['LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN']; for(const n of names) if(process.env[n]) return process.env[n]; return null; }
224
445
  function publishSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const res=validateSkillLibrary(dir,{silent:false,strictAi:true}); if(!res.ok){ process.exitCode=1; return; } if(!execute){ info('[dry-run] '+target+' publish target: '+dir); info('실제 업로드는 --execute가 필요합니다.'); return; } const token=resolvePublishToken(target,flags); if(!token && flags['no-prompt']){ fail('업로드 토큰이 없습니다. 환경변수 또는 --token-env를 설정하세요.'); process.exitCode=1; return; } if(target==='npm'){ const env={...process.env}; if(token) env.NODE_AUTH_TOKEN=token; const r=childProcess.spawnSync('npm',['publish','--access','public'],{cwd:dir,stdio:'inherit',env,shell:process.platform==='win32'}); process.exitCode=r.status||0; return; } if(target==='git'){ const repo=flags.repo||DEFAULT_GIT_REPOSITORY; info('Git target: '+repo); const run=(cmd,args)=>{ const r=childProcess.spawnSync(cmd,args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(r.status) process.exit(r.status); }; if(!exists(path.join(dir,'.git'))) run('git',['init']); try{ run('git',['remote','add','origin',repo]); }catch{} run('git',['add','.']); run('git',['commit','-m',flags.message||('Publish skill library '+res.meta.name)]); run('git',['branch','-M',flags.branch||'main']); run('git',['push','-u','origin',flags.branch||'main']); return; } fail('지원하지 않는 target: '+target); }
225
446
  function libraryGuide(root){ root=path.resolve(root||process.cwd()); const p=path.join(root,'.harness/AX_SKILL_LIBRARY_GUIDE.md'); if(exists(p)) log(read(p)); else log(coreFiles['.harness/AX_SKILL_LIBRARY_GUIDE.md']); }
226
447
  function libraryCommand(args,flags){ const sub=args[1]||'help'; if(sub==='guide') return libraryGuide(args[2]||flags.path||process.cwd()); if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false,strictAi:Boolean(flags['strict-ai'])}); if(sub==='verify') return verifySkillLibrary(args[2]||process.cwd(),flags); if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags); if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags); if(sub==='status'){ const meta=readSkillLibraryMeta(args[2]||process.cwd()); if(!meta) return fail('메타데이터 없음'); renderSkillMeta(meta); return; } fail('알 수 없는 library 명령: '+sub); }
227
448
 
228
- const routeData={feature:{read:['project-brief.md','current-state.md','architecture.md','context-map.md','feature-contracts.md'],update:['current-state.md','task-log.md','session-handoff.md','feature-contracts.md']},ui:{read:['design-system.md','feature-contracts.md','context-map.md'],update:['design-system.md','feature-contracts.md','current-state.md','task-log.md']},debugging:{read:['current-state.md','task-log.md','feature-contracts.md','testing-strategy.md'],update:['task-log.md','current-state.md','session-handoff.md']},refactor:{read:['architecture.md','decisions.md','guardrails.md'],update:['architecture.md','decisions.md','task-log.md']},release:{read:['release-checklist.md','testing-strategy.md','current-state.md','decisions.md','secret-policy.md'],update:['release-checklist.md','task-log.md','current-state.md','session-handoff.md']},migration:{read:['AX_MIGRATION_GUIDE.md','context-routing.md','writeback-policy.md'],update:['Only missing files by default','Use --force only when requested']},'new-install':{read:['AX_NEW_PROJECT_GUIDE.md','actual project files'],update:['project-brief.md','architecture.md','context-map.md','design-system.md','feature-contracts.md','release-checklist.md']},'skill-library':{read:['AX_SKILL_LIBRARY_GUIDE.md','skill-index.md','secret-policy.md'],update:['skill-index.md','skills-lock.json','task-log.md']},documentation:{read:['writeback-policy.md','context-routing.md'],update:['specific memory file','task-log.md','session-handoff.md']}};
229
- function routeCommand(task){ banner(); if(!task||task==='list'){ log('사용 가능한 task type: '+Object.keys(routeData).join(', ')); return; } const r=routeData[task]; if(!r){ fail('알 수 없는 task type: '+task); process.exitCode=1; return; } log('Task route: '+task); log('\nRead before work:'); r.read.forEach(x=>log(' - .harness/'+x)); log('\nUpdate after work:'); r.update.forEach(x=>log(' - .harness/'+x)); }
230
- function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills recommended|all|office,commerce-api] [--force]',' leerness migrate [path] [--dry-run] [--force]',' leerness status [path]',' leerness verify [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation>','','Skills:',' leerness skill list',' leerness skill info <name>',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path>','','Skill library:',' leerness library guide [path]',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--package leerness-skill-name]',' leerness library publish <built-library> --target npm|git [--execute]',''].join('\n')); }
231
- async function main(){ const parsed=parseArgs(process.argv.slice(2)); const args=parsed.positionals, flags=parsed.flags; if(flags.version||flags.v){ log(VERSION); return; } if(flags.help||flags.h){ help(); return; } const cmd=args[0]||'init'; if(cmd==='init') return init(args[1]||process.cwd(),flags); if(cmd==='migrate') return migrate(args[1]||process.cwd(),flags); if(cmd==='status') return status(args[1]||process.cwd()); if(cmd==='verify') return verify(args[1]||process.cwd()); if(cmd==='route') return routeCommand(args[1]||'list'); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
449
+
450
+
451
+ const ACTIVE_TASK_STATUSES = new Set(['requested','planned','in-progress','waiting','on-hold','blocked','incomplete']);
452
+ const ALL_TASK_STATUSES = new Set(['requested','planned','in-progress','waiting','on-hold','blocked','incomplete','done','dropped']);
453
+ function progressFile(root){ return path.join(root,'.harness/progress-tracker.md'); }
454
+ function splitTableLine(line){ return line.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(x=>x.trim()); }
455
+ function readProgressTasks(root){
456
+ const file=progressFile(root); if(!exists(file)) return [];
457
+ const tasks=[];
458
+ for(const line of read(file).split(/\r?\n/)){
459
+ if(!/^\|\s*T-[0-9A-Za-z_-]+\s*\|/.test(line)) continue;
460
+ const cols=splitTableLine(line); if(cols.length<7) continue;
461
+ tasks.push({id:cols[0],request:cols[1],status:cols[2],owner:cols[3],lastUpdate:cols[4],evidence:cols[5],nextAction:cols[6]});
462
+ }
463
+ return tasks;
464
+ }
465
+ function taskTable(tasks){
466
+ return '| ID | User Request | Status | Owner | Last Update | Evidence / Notes | Next Action |\n|---|---|---|---|---|---|---|\n'+tasks.map(t=>`| ${t.id} | ${String(t.request||'').replace(/\|/g,'/')} | ${t.status||'requested'} | ${t.owner||'AI'} | ${t.lastUpdate||today()} | ${String(t.evidence||'').replace(/\|/g,'/')} | ${String(t.nextAction||'').replace(/\|/g,'/')} |`).join('\n')+'\n';
467
+ }
468
+ function writeProgressTasks(root,tasks){
469
+ const file=progressFile(root);
470
+ const lang=readConfiguredLanguage(root)||detectLanguage(root);
471
+ const intro=lang==='ko'
472
+ ? '# Progress Tracker\n\n사용자가 요청한 작업을 세션 간 추적합니다. 세션 종료 시 미완료/예정/보류/대기/진행중 작업은 반드시 표시합니다.\n\n'
473
+ : '# Progress Tracker\n\nTrack user-requested work across sessions. At session close, unresolved/planned/on-hold/waiting/in-progress tasks must be listed.\n\n';
474
+ const status='\n## Status Values\n\n- requested\n- planned\n- in-progress\n- waiting\n- on-hold\n- blocked\n- incomplete\n- done\n- dropped\n\n## Drop Policy\n\nDropped tasks are not deleted. Mark Status as dropped and write the reason in Evidence / Notes.\n';
475
+ write(file,`${MARK}\n---\nleernessRole: progress-tracker\nreadWhen: [planning, resume-work, end-of-session, multi-step-work]\nupdateWhen: [task-started, task-completed, task-blocked, task-dropped, scope-change, end-of-session]\ndoNotStore: [secrets, tokens, credentials, raw-private-data]\n---\n\n`+intro+taskTable(tasks)+status);
476
+ }
477
+ function nextTaskId(tasks){
478
+ let max=0; for(const t of tasks){ const m=String(t.id||'').match(/T-(\d+)/); if(m) max=Math.max(max,parseInt(m[1],10)); }
479
+ return 'T-'+String(max+1).padStart(4,'0');
480
+ }
481
+ function renderTasks(tasks,filterStatuses){
482
+ const filtered=filterStatuses?tasks.filter(t=>filterStatuses.has(t.status)):tasks;
483
+ if(!filtered.length){ log(' - 없음'); return; }
484
+ for(const t of filtered) log(` - ${t.id} [${t.status}] ${t.request} :: next=${t.nextAction||'-'} :: note=${t.evidence||'-'}`);
485
+ }
486
+ function taskCommand(args,flags){
487
+ const sub=args[1]||'list'; const root=path.resolve(flags.path||args[3]||process.cwd());
488
+ const tasks=readProgressTasks(root);
489
+ if(sub==='list'){
490
+ banner();
491
+ const status=flags.status?new Set(String(flags.status).split(',').map(x=>x.trim()).filter(Boolean)):null;
492
+ log('Progress tasks: '+root); renderTasks(tasks,status); return;
493
+ }
494
+ if(sub==='add'){
495
+ const request=args[2]||flags.title||flags.name; if(!request){ fail('추가할 작업 내용을 입력하세요. 예: leerness task add "배포 검증" --status planned'); process.exitCode=1; return; }
496
+ const status=String(flags.status||'planned'); if(!ALL_TASK_STATUSES.has(status)){ fail('지원하지 않는 status: '+status); process.exitCode=1; return; }
497
+ const t={id:nextTaskId(tasks),request,status,owner:String(flags.owner||'AI'),lastUpdate:today(),evidence:String(flags.evidence||'added by leerness task add'),nextAction:String(flags.next||flags.action||'Define next exact step')};
498
+ tasks.push(t); writeProgressTasks(root,tasks); ok('작업 추가: '+t.id); return;
499
+ }
500
+ if(sub==='update'){
501
+ const id=args[2]; const t=tasks.find(x=>x.id===id); if(!t){ fail('작업 ID를 찾을 수 없습니다: '+id); process.exitCode=1; return; }
502
+ if(flags.status){ const st=String(flags.status); if(!ALL_TASK_STATUSES.has(st)){ fail('지원하지 않는 status: '+st); process.exitCode=1; return; } t.status=st; }
503
+ if(flags.evidence) t.evidence=String(flags.evidence);
504
+ if(flags.next||flags.action) t.nextAction=String(flags.next||flags.action);
505
+ if(flags.owner) t.owner=String(flags.owner);
506
+ t.lastUpdate=today(); writeProgressTasks(root,tasks); ok('작업 업데이트: '+id); return;
507
+ }
508
+ if(sub==='drop'||sub==='remove'){
509
+ const id=args[2]; const t=tasks.find(x=>x.id===id); if(!t){ fail('작업 ID를 찾을 수 없습니다: '+id); process.exitCode=1; return; }
510
+ t.status='dropped'; t.lastUpdate=today(); t.evidence=String(flags.reason||flags.evidence||'Dropped by user'); t.nextAction='None'; writeProgressTasks(root,tasks); ok('작업 드랍 처리: '+id); return;
511
+ }
512
+ fail('알 수 없는 task 명령: '+sub);
513
+ }
514
+ function debugHarness(root){
515
+ root=path.resolve(root||process.cwd()); banner(); let failures=0,warnings=0;
516
+ const required=['AGENTS.md','.harness/language-policy.md','.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md','.harness/session-close-policy.md','.harness/progress-tracker.md','.harness/anti-lazy-work-policy.md','.harness/debug-guide.md'];
517
+ for(const f of required){ if(exists(path.join(root,f))) ok('존재: '+f); else { failures++; fail('누락: '+f); } }
518
+ const ag=path.join(root,'AGENTS.md');
519
+ if(exists(ag)){
520
+ const body=read(ag);
521
+ for(const term of ['language-policy.md','context-routing.md','writeback-policy.md','progress-tracker.md','anti-lazy-work-policy.md','End-of-Session Contract']){
522
+ if(body.includes(term)) ok('AGENTS 방향지시 포함: '+term); else { failures++; fail('AGENTS 방향지시 누락: '+term); }
523
+ }
524
+ }
525
+ const mf=path.join(root,'.harness/manifest.json');
526
+ if(exists(mf)){ const m=parseJsonSafe(read(mf),{}); if(m.language) ok('언어 설정: '+m.language+' ('+(m.languageName||languageName(m.language))+')'); else { warnings++; warn('manifest language 누락'); } }
527
+ const tasks=readProgressTasks(root);
528
+ const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
529
+ log('\nActive unresolved tasks:'); renderTasks(active);
530
+ const badSensitive=scanSensitivePath(path.join(root,'.harness')).filter(x=>!x.file.includes(path.sep+'archive'+path.sep));
531
+ if(badSensitive.length){ failures+=badSensitive.length; badSensitive.forEach(x=>fail('민감정보 의심: '+rel(root,x.file)+' '+x.type)); } else ok('하네스 민감정보 스캔 통과');
532
+ log('\nDebug summary: '+(failures?'FAIL':'PASS')+' / warnings='+warnings+' / failures='+failures);
533
+ if(failures) process.exitCode=1;
534
+ }
535
+
536
+ function closeSession(root){
537
+ root=path.resolve(root||process.cwd());
538
+ banner();
539
+ const template=path.join(root,'.harness/templates/end-of-session-report.md');
540
+ const policy=path.join(root,'.harness/session-close-policy.md');
541
+ const progress=path.join(root,'.harness/progress-tracker.md');
542
+ log('Session close checklist');
543
+ log('');
544
+ log('Read before closing:');
545
+ ['.harness/session-close-policy.md','.harness/progress-tracker.md','.harness/current-state.md','.harness/task-log.md','.harness/session-handoff.md','.harness/anti-lazy-work-policy.md'].forEach(x=>log(' - '+x));
546
+ log('');
547
+ log('Required final report sections:');
548
+ ['Completed This Session','In Progress From User Requests','Incomplete / Not Started From User Requests','Planned Tasks','Waiting Tasks','On-Hold Tasks','Blocked Tasks','Dropped By User','Verification','Files Changed','Memory Files Updated','Risks / Assumptions / Blockers','Recommended Next Directions','Next Exact Step'].forEach(x=>log(' - '+x));
549
+ log('');
550
+ if(exists(template)) log(read(template));
551
+ else log('# End-of-Session Report\n\n## Completed This Session\n-\n\n## In Progress From User Requests\n-\n\n## Incomplete / Not Started From User Requests\n-\n\n## Planned Tasks\n-\n\n## Waiting Tasks\n-\n\n## On-Hold Tasks\n-\n\n## Blocked Tasks\n-\n\n## Dropped By User\n-\n\n## Verification\n-\n\n## Files Changed\n-\n\n## Memory Files Updated\n-\n\n## Risks / Assumptions / Blockers\n-\n\n## Recommended Next Directions\n-\n\n## Next Exact Step\n-\n');
552
+ const tasks=readProgressTasks(root);
553
+ const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
554
+ const dropped=tasks.filter(t=>t.status==='dropped');
555
+ log('');
556
+ log('Tracked unresolved / planned / waiting / on-hold / in-progress work:');
557
+ renderTasks(active);
558
+ log('');
559
+ log('Dropped by user:');
560
+ renderTasks(dropped);
561
+ if(!exists(policy)) warn('session-close-policy.md가 없습니다. leerness migrate를 실행하세요.');
562
+ if(!exists(progress)) warn('progress-tracker.md가 없습니다. leerness migrate를 실행하세요.');
563
+ }
564
+ function sessionCommand(args){ const sub=args[1]||'close'; if(sub==='close'||sub==='handoff'||sub==='end') return closeSession(args[2]||process.cwd()); fail('알 수 없는 session 명령: '+sub); }
565
+
566
+ const routeData={feature:{read:['project-brief.md','current-state.md','architecture.md','context-map.md','feature-contracts.md'],update:['current-state.md','task-log.md','session-handoff.md','feature-contracts.md']},ui:{read:['design-system.md','feature-contracts.md','context-map.md'],update:['design-system.md','feature-contracts.md','current-state.md','task-log.md']},debugging:{read:['current-state.md','task-log.md','feature-contracts.md','testing-strategy.md'],update:['task-log.md','current-state.md','session-handoff.md']},refactor:{read:['architecture.md','decisions.md','guardrails.md'],update:['architecture.md','decisions.md','task-log.md']},release:{read:['release-checklist.md','testing-strategy.md','current-state.md','decisions.md','secret-policy.md'],update:['release-checklist.md','task-log.md','current-state.md','session-handoff.md']},migration:{read:['AX_MIGRATION_GUIDE.md','context-routing.md','writeback-policy.md'],update:['Only missing files by default','Use --force only when requested']},'new-install':{read:['AX_NEW_PROJECT_GUIDE.md','actual project files'],update:['project-brief.md','architecture.md','context-map.md','design-system.md','feature-contracts.md','release-checklist.md']},'skill-library':{read:['AX_SKILL_LIBRARY_GUIDE.md','skill-index.md','secret-policy.md'],update:['skill-index.md','skills-lock.json','task-log.md']},documentation:{read:['writeback-policy.md','context-routing.md'],update:['specific memory file','task-log.md','session-handoff.md']},debug:{read:['debug-guide.md','AGENTS.md','language-policy.md','context-routing.md','writeback-policy.md','progress-tracker.md','session-close-policy.md'],update:['debug findings in task-log.md','progress-tracker.md when user-requested debug work changes']},'session-close':{read:['session-close-policy.md','progress-tracker.md','current-state.md','task-log.md','session-handoff.md','anti-lazy-work-policy.md'],update:['session-handoff.md','progress-tracker.md','current-state.md','task-log.md','relevant changed memory files']}};
567
+ function routePath(x){ if(String(x).includes('/')||String(x).endsWith('.md')&&['AGENTS.md','CLAUDE.md','README.md'].includes(String(x))) return String(x); if(String(x).includes(' ')) return String(x); return '.harness/'+x; }
568
+ function routeCommand(task){ banner(); if(!task||task==='list'){ log('사용 가능한 task type: '+Object.keys(routeData).join(', ')); return; } const r=routeData[task]; if(!r){ fail('알 수 없는 task type: '+task); process.exitCode=1; return; } log('Task route: '+task); log('\nRead before work:'); r.read.forEach(x=>log(' - '+routePath(x))); log('\nUpdate after work:'); r.update.forEach(x=>log(' - '+routePath(x))); }
569
+ function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--language auto|ko|en] [--skills recommended|all|office,commerce-api] [--force]',' leerness migrate [path] [--dry-run] [--language auto|ko|en] [--force]',' leerness status [path]',' leerness verify [path]',' leerness debug [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation|debug|session-close>','','Tasks:',' leerness task list [--status planned,waiting,on-hold]',' leerness task add "request" [--status planned]',' leerness task update T-0002 --status in-progress',' leerness task drop T-0002 --reason "user dropped"','','Session:',' leerness session close [path]','','Skills:',' leerness skill list',' leerness skill info <name>',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path>','','Skill library:',' leerness library guide [path]',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--package leerness-skill-name]',' leerness library publish <built-library> --target npm|git [--execute]',''].join('\n')); }
570
+ async function main(){ const parsed=parseArgs(process.argv.slice(2)); const args=parsed.positionals, flags=parsed.flags; if(flags.version||flags.v){ log(VERSION); return; } if(flags.help||flags.h){ help(); return; } const cmd=args[0]||'init'; if(cmd==='init') return init(args[1]||process.cwd(),flags); if(cmd==='migrate') return migrate(args[1]||process.cwd(),flags); if(cmd==='status') return status(args[1]||process.cwd()); if(cmd==='verify') return verify(args[1]||process.cwd()); if(cmd==='route') return routeCommand(args[1]||'list'); if(cmd==='debug') return debugHarness(args[1]||process.cwd()); if(cmd==='task') return taskCommand(args,flags); if(cmd==='session') return sessionCommand(args); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
232
571
  main().catch(err=>{ fail(err.stack||err.message); process.exit(1); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.3.1",
4
- "description": "Leerness: Context Routing, Writeback Policy, AX 마이그레이션/신규설치 가이드로 AI가 적재적소의 프로젝트 메모리를 읽고 갱신하게 하는 AX 최적화 개발 하네스.",
3
+ "version": "1.4.0",
4
+ "description": "Leerness: 언어 정책, 진행 작업 드랍, 세션 종료 미완료 목록, 라우팅 디버그를 지원하는 AX 최적화 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
7
7
  "ai",
@@ -23,7 +23,19 @@
23
23
  "task-type-map",
24
24
  "ai-migration-guide",
25
25
  "new-project-guide",
26
- "project-memory-router"
26
+ "project-memory-router",
27
+ "session-close",
28
+ "session-handoff",
29
+ "unfinished-task-tracking",
30
+ "anti-lazy-work",
31
+ "progress-tracker",
32
+ "end-of-session-report",
33
+ "direction-routing-debug",
34
+ "pending-work-tracking",
35
+ "language-policy",
36
+ "auto-language-detection",
37
+ "task-drop",
38
+ "harness-debug"
27
39
  ],
28
40
  "bin": {
29
41
  "leerness": "./bin/harness.js"
@@ -37,7 +49,7 @@
37
49
  "LICENSE"
38
50
  ],
39
51
  "scripts": {
40
- "test": "node ./bin/harness.js --help && node ./bin/harness.js route release && node ./bin/harness.js init --yes --skills office,commerce-api,ai-verified-skill-publisher ./tmp-harness-test && node ./bin/harness.js status ./tmp-harness-test && node ./bin/harness.js skill list && node ./bin/harness.js skill info ai-verified-skill-publisher && node ./bin/harness.js library guide ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
52
+ "test": "node ./bin/harness.js --help && node ./bin/harness.js init --yes --language ko --skills office,commerce-api,ai-verified-skill-publisher ./tmp-harness-test && node ./bin/harness.js task add \"테스트 예정 작업\" --status planned --path ./tmp-harness-test && node ./bin/harness.js task drop T-0002 --reason \"테스트 드랍\" --path ./tmp-harness-test && node ./bin/harness.js route debug && node ./bin/harness.js session close ./tmp-harness-test && node ./bin/harness.js debug ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
41
53
  "prepack": "node ./bin/harness.js --version"
42
54
  },
43
55
  "repository": {