leerness 1.3.2 → 1.5.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.
package/README.md CHANGED
@@ -1,27 +1,23 @@
1
1
  # Leerness
2
2
 
3
- **Leerness는 AI 에이전트가 대규모 프로젝트에서도 필요한 문서를 먼저 읽고, 작업 적재적소에 프로젝트 메모리를 갱신하며, 세션 종료마다 진행/미완료/추천 방향을 명확히 남기도록 만드는 AX 최적화 개발 하네스입니다.**
3
+ **Leerness는 AI 에이전트가 프로젝트의 목표, 계획, 진행률, 작업 규율, 스킬 라이브러리, 세션 인수인계를 일관되게 관리하도록 돕는 AX 최적화 개발 하네스입니다.**
4
4
 
5
- Leerness 목적은 단순히 `.md` 파일을 많이 만드는 것이 아닙니다. AI가 어떤 상황에 어떤 파일을 봐야 하는지, 어떤 작업 후 어떤 파일을 갱신해야 하는지까지 명확히 지시합니다.
5
+ Leerness `.harness/` 지식 저장소를 설치하고, AI가 작업 전에 어떤 파일을 읽어야 하는지와 작업 후 어떤 파일을 갱신해야 하는지 명확히 지시합니다. 프로젝트 계획은 `plan.md`, 실제 작업 상태는 `progress-tracker.md`, 실행 기준은 `guideline.md`로 분리해서 장기 맥락과 진행률을 안정적으로 유지합니다.
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가 구버전 마이그레이션 또는 신규 설치를 안전하게 수행하도록 안내 |
23
- | Session Close Policy | 세션 종료 완료/진행중/미완료/검증/추천 방향을 강제 기록 |
24
- | Anti-Lazy Work Policy | 검증 없는 완료 선언, 모호한 요약, 미완료 작업 은폐를 방지 |
11
+ | 계획 수립/수정 | `plan.md`로 프로젝트 목표, 범위, milestone, 제외/드랍 항목, 변경 이력을 관리합니다. |
12
+ | 진행률 추적 | `progress-tracker.md`로 사용자 요청별 작업 상태와 다음 액션을 추적합니다. |
13
+ | Guideline 정책 | `guideline.md`가 `plan.md`와 `progress-tracker.md`를 참조해 계획 이행 기준을 제공합니다. |
14
+ | 신규 프로젝트 부트스트랩 | 계획이 없는 프로젝트에서는 먼저 계획 초안을 만들고 이후 구현을 진행하도록 유도합니다. |
15
+ | Context Routing | planning, feature, ui, release, migration, debug 작업 유형별 참조/갱신 파일을 안내합니다. |
16
+ | Writeback Policy | 목적, 현재 상태, 아키텍처, 결정 로그, 릴리즈 체크리스트 정보를 어디에 기록할지 정의합니다. |
17
+ | 언어 정책 | 설치언어를 자동 감지하거나 `ko/en`으로 선택해 하네스와 스킬 문서의 작성 언어를 통일합니다. |
18
+ | 세션 종료 인수인계 | 세션 종료 완료/진행중/미완료/예정/보류/대기/드랍 작업과 추천 방향을 표기합니다. |
19
+ | 스킬 라이브러리 | 검증된 작업 패턴을 스킬팩으로 설치하고, 한글명/가능 작업/최종 업데이트일/AI 검증 상태를 표시합니다. |
20
+ | 디버그 | `leerness debug`로 AGENTS 방향지시, 계획, 진행률, 라우팅, 세션 종료 정책이 정상인지 점검합니다. |
25
21
 
26
22
  ## 설치
27
23
 
@@ -29,171 +25,133 @@ Agent = Model + Leerness Harness
29
25
  npx leerness init
30
26
  ```
31
27
 
32
- 추천 스킬 포함:
28
+ 언어와 추천 스킬을 지정해서 설치:
33
29
 
34
30
  ```bash
35
- npx leerness init --skills recommended
31
+ npx leerness init --language ko --skills recommended
36
32
  ```
37
33
 
38
34
  기존 프로젝트 또는 구버전 하네스 마이그레이션:
39
35
 
40
36
  ```bash
41
37
  npx leerness migrate --dry-run
42
- npx leerness migrate
38
+ npx leerness migrate --language ko
39
+ ```
40
+
41
+ ## 계획 관리
42
+
43
+ ```bash
44
+ leerness plan show
45
+ leerness plan init --goal "AI 상세페이지 생성 서비스"
46
+ leerness plan add "커머스 API 연동" --status planned
47
+ leerness plan update M-0002 --status in-progress --progress 40
48
+ leerness plan drop "관리자 대시보드" --reason "이번 버전 범위에서 제외"
49
+ leerness plan progress
50
+ leerness plan sync
43
51
  ```
44
52
 
53
+ `plan.md`는 전체 계획과 범위를 관리합니다. `progress-tracker.md`는 구체적인 작업 상태를 관리합니다. `guideline.md`는 계획을 어떤 기준으로 수행할지 정의합니다.
54
+
45
55
  ## 주요 명령어
46
56
 
47
57
  ```bash
48
- leerness init [path] [--yes] [--skills recommended|all|office,commerce-api]
49
- leerness migrate [path] [--dry-run]
58
+ leerness init [path] [--language auto|ko|en] [--skills recommended|all|office,commerce-api]
59
+ leerness migrate [path] [--dry-run] [--language auto|ko|en]
50
60
  leerness status [path]
51
61
  leerness verify [path]
52
- leerness route <task-type>
53
- leerness session close [path]
54
-
55
- leerness skill list
56
- leerness skill info <name>
57
- leerness skill add <name>
58
- leerness skill learn <name> --from <validated-skill-path>
59
-
60
- leerness library guide [path]
61
- leerness library validate <path> [--strict-ai]
62
- leerness library verify <path> --ai --reviewer leerness-ai
63
- leerness library build <path>
64
- leerness library publish <built-library> --target npm|git [--execute]
62
+ leerness debug [path]
63
+ leerness route <planning|feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation|debug|session-close>
65
64
  ```
66
65
 
67
- 작업 유형 라우팅 확인:
66
+ 작업 추적:
68
67
 
69
68
  ```bash
70
- leerness route release
71
- leerness route feature
72
- leerness route migration
73
- leerness route new-install
69
+ leerness task list
70
+ leerness task add "쿠팡 API 연동 검증" --status planned
71
+ leerness task update T-0002 --status in-progress --next "인증 응답 확인"
72
+ leerness task drop T-0002 --reason "사용자가 범위에서 제외"
74
73
  ```
75
74
 
76
- ## 생성되는 핵심 파일
75
+ 세션 종료:
77
76
 
78
- ```text
79
- .harness/
80
- project-brief.md # 프로젝트 목적/사용자/성공 기준
81
- current-state.md # 현재 상태/다음 작업/블로커
82
- architecture.md # 구조/모듈/데이터 흐름
83
- context-map.md # 기능별 참조 파일 지도
84
- decisions.md # 결정 로그
85
- guardrails.md # 금지사항/보안/민감영역 규칙
86
- design-system.md # UI/UX/컴포넌트 일관성
87
- feature-contracts.md # 기능 입력/출력/상태/오류 계약
88
- testing-strategy.md # 검증 전략
89
- release-checklist.md # 배포/npm/git/환경변수/롤백 조건
90
- session-handoff.md # 다음 세션 인수인계
91
- session-close-policy.md # 세션 종료 보고 규칙
92
- progress-tracker.md # 사용자 요청별 진행/미완료 추적
93
- anti-lazy-work-policy.md # 게으른 작업 방지 규칙
94
- context-routing.md # 작업 유형별 읽기/갱신 라우팅
95
- writeback-policy.md # 어떤 정보를 어디에 기록할지
96
- task-type-map.md # 사용자 요청 → 작업 유형 매핑
97
- AX_MIGRATION_GUIDE.md # AI용 구버전 마이그레이션 가이드
98
- AX_NEW_PROJECT_GUIDE.md # AI용 신규 설치/프로젝트 반영 가이드
99
- AX_SKILL_LIBRARY_GUIDE.md # AI용 스킬 라이브러리 가이드
77
+ ```bash
78
+ leerness route session-close
79
+ leerness session close
100
80
  ```
101
81
 
102
- 각 핵심 파일에는 `readWhen`, `updateWhen`, `doNotStore` 메타데이터가 들어갑니다. AI는 이 정보를 기준으로 파일을 참조하고 갱신해야 합니다.
103
-
104
- ## AGENTS.md의 역할
105
-
106
- `AGENTS.md`는 단순 지침 파일이 아니라 AI 작업 라우터입니다.
107
-
108
- 모든 작업은 다음 순서를 따릅니다.
109
-
110
- 1. 작업 유형을 분류한다.
111
- 2. `.harness/context-routing.md`와 `.harness/task-type-map.md`를 읽는다.
112
- 3. 작업 유형별 필수 파일을 읽는다.
113
- 4. 작업을 수행한다.
114
- 5. `.harness/writeback-policy.md`에 따라 필요한 파일을 갱신한다.
115
- 6. `current-state.md`, `task-log.md`, `session-handoff.md`를 최신화한다.
82
+ 스킬:
116
83
 
117
- ## AX 마이그레이션 가이드
118
-
119
- 구버전 Leerness, project-harness, 자체 AGENTS/CLAUDE 문서가 있는 프로젝트는 다음 파일을 기준으로 AI가 마이그레이션합니다.
120
-
121
- ```text
122
- .harness/AX_MIGRATION_GUIDE.md
84
+ ```bash
85
+ leerness skill list
86
+ leerness skill info ai-verified-skill-publisher
87
+ leerness skill add ai-verified-skill-publisher
123
88
  ```
124
89
 
125
- 가이드는 다음을 지시합니다.
126
-
127
- - 기존 하네스/지침 파일 백업
128
- - legacy 내용의 새 목적지 매핑
129
- - 충돌되는 규칙 처리
130
- - 민감정보 제거
131
- - 새 Context Routing/Writeback Policy 생성
132
- - 마이그레이션 후 검증 및 session-handoff 갱신
133
-
134
- ## AX 신규 설치 가이드
135
-
136
- 이미 진행 중인 프로젝트에 처음 Leerness를 설치했다면 다음 파일을 기준으로 프로젝트 내용을 반영합니다.
90
+ ## 생성되는 핵심 파일
137
91
 
138
92
  ```text
139
- .harness/AX_NEW_PROJECT_GUIDE.md
93
+ .harness/
94
+ plan.md # 프로젝트 목표, 범위, milestone, 제외/드랍 항목
95
+ progress-tracker.md # 사용자 요청별 작업 상태와 진행률
96
+ guideline.md # plan/progress 기반 실행 기준
97
+ LANGUAGE # ko 또는 en
98
+ language-policy.md # 문서/스킬 작성 언어 규칙
99
+ project-brief.md # 프로젝트 목적/사용자/성공 기준
100
+ current-state.md # 현재 상태/다음 작업/블로커
101
+ architecture.md # 구조/모듈/데이터 흐름
102
+ context-map.md # 기능별 참조 파일 지도
103
+ decisions.md # 결정 로그
104
+ guardrails.md # 금지사항/보안/민감영역 규칙
105
+ design-system.md # UI/UX/컴포넌트 일관성
106
+ feature-contracts.md # 기능 입력/출력/상태/오류 계약
107
+ testing-strategy.md # 검증 전략
108
+ release-checklist.md # 배포/npm/git/환경변수/롤백 조건
109
+ session-handoff.md # 다음 세션 인수인계
110
+ session-close-policy.md # 세션 종료 보고 규칙
111
+ anti-lazy-work-policy.md # 게으른 작업 방지 규칙
112
+ context-routing.md # 작업 유형별 읽기/갱신 라우팅
113
+ writeback-policy.md # 어떤 정보를 어디에 기록할지
114
+ task-type-map.md # 사용자 요청 → 작업 유형 매핑
115
+ AX_PLAN_GUIDE.md # AI용 계획 수립/수정/동기화 가이드
116
+ AX_MIGRATION_GUIDE.md # AI용 구버전 마이그레이션 가이드
117
+ AX_NEW_PROJECT_GUIDE.md # AI용 신규 설치/프로젝트 반영 가이드
118
+ AX_SKILL_LIBRARY_GUIDE.md # AI용 스킬 라이브러리 가이드
140
119
  ```
141
120
 
142
- 가이드는 AI에게 다음을 요구합니다.
143
-
144
- - 실제 파일 구조와 프레임워크 파악
145
- - package/config/route/API/DB/deploy/test 파일 확인
146
- - project-brief, architecture, context-map, design-system, feature-contracts 채우기
147
- - release-checklist와 testing-strategy를 실제 프로젝트 기준으로 작성
148
- - session-handoff에 다음 정확한 작업 기록
121
+ ## 디버그
149
122
 
123
+ ```bash
124
+ leerness debug
125
+ ```
150
126
 
151
- ## 세션 종료 인수인계와 게으른 작업 방지
127
+ 점검 항목:
152
128
 
153
- Leerness는 의미 있는 작업 세션이 끝날 AI가 다음 항목을 반드시 정리하도록 지시합니다.
129
+ - `AGENTS.md`가 plan, guideline, language-policy, context-routing, writeback-policy, progress-tracker를 참조하는지
130
+ - `plan.md`에 milestone과 out-of-scope 영역이 있는지
131
+ - `guideline.md`가 `plan.md`와 `progress-tracker.md`를 참조하는지
132
+ - `progress-tracker.md`가 작업 상태 표를 갖고 있는지
133
+ - 세션 종료 정책이 미완료/예정/보류/대기 작업 목록화를 요구하는지
134
+ - 하네스 파일에 토큰/비밀번호/private key 의심 패턴이 없는지
154
135
 
155
- ```bash
156
- leerness route session-close
157
- leerness session close
158
- ```
136
+ ## 세션 종료 보고 기준
159
137
 
160
- 세션 종료 보고에는 다음이 포함되어야 합니다.
138
+ AI는 의미 있는 세션이 끝날 때 아래를 정리해야 합니다.
161
139
 
140
+ - 계획 진행 요약
162
141
  - 이번 세션에서 완료한 작업
163
- - 사용자가 요청한 작업 중 아직 진행 중인 작업
164
- - 사용자가 요청했지만 아직 미완료 또는 미시작인 작업
142
+ - 사용자가 요청한 작업 중 진행 중인 작업
143
+ - 미완료/미시작 작업
144
+ - 예정/planned 작업
145
+ - 보류/on-hold 작업
146
+ - 대기/waiting 작업
147
+ - 차단/blocked 작업
148
+ - 사용자가 드랍한 작업과 이유
165
149
  - 실행한 검증과 결과
166
150
  - 변경한 파일과 갱신한 하네스 메모리
167
151
  - 리스크, 가정, 블로커
168
152
  - 추가로 진행하면 좋은 추천 방향
169
153
  - 다음 세션에서 바로 수행할 단 하나의 정확한 작업
170
154
 
171
- 관련 파일:
172
-
173
- ```text
174
- .harness/session-close-policy.md
175
- .harness/progress-tracker.md
176
- .harness/anti-lazy-work-policy.md
177
- .harness/templates/end-of-session-report.md
178
- ```
179
-
180
- 이 정책은 AI가 “대충 완료”라고 말하거나, 검증하지 않은 작업을 완료로 표시하거나, 미완료 요청을 숨기는 것을 막기 위한 장치입니다.
181
-
182
- ## 스킬 라이브러리
183
-
184
- 스킬은 한글명, 가능한 작업, 최종 업데이트일, AI 검증 상태를 표시합니다.
185
-
186
- ```bash
187
- leerness skill list
188
- leerness skill info ai-verified-skill-publisher
189
- ```
190
-
191
- 검증된 스킬 업로드는 AI 검증 메타데이터가 있어야 하며, 실제 npm/git 업로드에는 토큰 게이트가 적용됩니다.
192
-
193
- ## 민감정보 원칙
194
-
195
- Leerness 파일에는 실제 토큰, 비밀번호, 쿠키, private key, 고객 개인정보를 저장하지 않습니다. 환경변수 이름과 secret manager key 이름만 기록합니다.
196
-
197
155
  ## 라이선스
198
156
 
199
157
  MIT
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.2';
9
+ const VERSION = '1.5.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 · session handoff'+c.reset); log(''); }
30
+ function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'plan orchestration · progress tracking · context routing · debug'+c.reset); log(''); }
31
31
  function installGuide(){
32
32
  log(c.bold+'설치/마이그레이션 안내'+c.reset);
33
33
  log(' - 기존 파일은 먼저 .harness/archive/ 에 백업합니다.');
@@ -44,6 +44,55 @@ function today(){ return now().slice(0,10); }
44
44
  function fill(t,ctx){ return t.replace(/{{([A-Z_]+)}}/g,(_,k)=>ctx[k]||''); }
45
45
  function slug(s){ return String(s||'skill').toLowerCase().replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'') || 'skill'; }
46
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
+
65
+ const pf=path.join(root,'.harness/plan.md');
66
+ if(exists(pf)){ const b=read(pf); if(b.includes('## Milestones')) ok('plan.md milestones 확인'); else { warnings++; warn('plan.md에 Milestones 섹션이 없습니다.'); } }
67
+ const gf=path.join(root,'.harness/guideline.md');
68
+ if(exists(gf)){ const b=read(gf); if(b.includes('plan.md')&&b.includes('progress-tracker.md')) ok('guideline.md가 plan/progress를 참조합니다.'); else { failures++; fail('guideline.md가 plan.md/progress-tracker.md를 참조하지 않습니다.'); } }
69
+ const mf=path.join(root,'.harness/manifest.json');
70
+ if(exists(mf)){ const j=parseJsonSafe(read(mf),{}); if(j.language) return normalizeLanguage(j.language); }
71
+ const lp=path.join(root,'.harness/LANGUAGE');
72
+ if(exists(lp)) return normalizeLanguage(read(lp).trim());
73
+ return null;
74
+ }
75
+ async function chooseLanguage(root,flags){
76
+ const requested=normalizeLanguage(flags.language||flags.lang||'auto');
77
+ const configured=readConfiguredLanguage(root);
78
+ if(requested!=='auto') return requested;
79
+ if(configured) return configured;
80
+ const detected=detectLanguage(root);
81
+ if(flags.yes||flags.y||!process.stdin.isTTY) return detected;
82
+ log(c.bold+'문서 작성 언어 선택'+c.reset);
83
+ log(' 1) 자동 감지: '+languageName(detected));
84
+ log(' 2) 한국어');
85
+ log(' 3) English');
86
+ const ans=await ask('\n선택 (Enter=자동 감지): ');
87
+ if(ans==='2') return 'ko';
88
+ if(ans==='3') return 'en';
89
+ return detected;
90
+ }
91
+ function languagePolicyBody(code){
92
+ 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`;
93
+ 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`;
94
+ }
95
+
47
96
  function copyRecursive(src,dst,ignoreAbs=[]){
48
97
  const abs=path.resolve(src); if(ignoreAbs.some(i=>abs===i||abs.startsWith(i+path.sep))) return;
49
98
  const st=fs.statSync(src);
@@ -95,18 +144,93 @@ function noteLegacyPreserved(root,found,dryRun){
95
144
  }
96
145
 
97
146
  const coreFiles = {
98
- '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## End-of-Session Contract
99
- 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.
147
+ 'AGENTS.md': `${MARK}
148
+ # {{PROJECT}} AI Agent Harness
149
+
150
+ Agent = Model + Leerness Harness.
151
+
152
+ ## Core Rule
153
+ 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.
154
+
155
+ ## Universal Read Order
156
+ 1. .harness/plan.md
157
+ 2. .harness/progress-tracker.md
158
+ 3. .harness/project-brief.md
159
+ 4. .harness/current-state.md
160
+ 5. .harness/language-policy.md
161
+ 6. .harness/context-routing.md
162
+ 7. .harness/writeback-policy.md
163
+ 8. .harness/task-type-map.md
164
+ 9. .harness/context-map.md
165
+ 10. .harness/guideline.md
166
+ 11. .harness/guardrails.md
167
+ 12. .harness/skills-lock.json
168
+
169
+ ## Language Rule
170
+ - Before writing or updating any harness/skill/session document, read .harness/language-policy.md.
171
+ - Use the configured project language consistently.
172
+ - Preserve code names, commands, file paths, environment variables, and API field names exactly.
173
+
174
+ ## Task Routing
175
+ - Feature/API work: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.
176
+ - UI/design work: design-system.md, feature-contracts.md, skills/ui-consistency.md.
177
+ - Debugging: task-log.md, current-state.md, skills/debugging.md, related feature contract.
178
+ - Refactoring: architecture.md, decisions.md, guardrails.md, skills/refactoring.md.
179
+ - Release/deploy: release-checklist.md, testing-strategy.md, current-state.md, decisions.md.
180
+ - Migration: AX_MIGRATION_GUIDE.md, context-routing.md, writeback-policy.md.
181
+ - New install: AX_NEW_PROJECT_GUIDE.md and actual project config/source files.
182
+ - Skill/library work: AX_SKILL_LIBRARY_GUIDE.md and ai-verified-skill-publisher when installed.
183
+ - Harness debug: debug-guide.md, language-policy.md, context-routing.md, writeback-policy.md, progress-tracker.md.
184
+
185
+ ## Writeback Rules
186
+ - Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.
187
+ - Update plan.md and progress-tracker.md when user scope, milestones, task status, exclusions, or planned work changes.
188
+ - Update guideline.md when execution standards, quality gates, or plan-following rules change.
189
+ - Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.
190
+ - Update feature-contracts.md when input/output/state/error behavior changes.
191
+ - Update design-system.md when UI rules, components, layout, spacing, or states change.
192
+ - Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.
193
+ - Update context-map.md when important files, modules, routes, commands, or ownership areas change.
194
+ - Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.
195
+
196
+ ## Non-Destructive Migration Policy
197
+ - Never overwrite existing project memory files unless the user explicitly requests --force.
198
+ - Preserve .env.example and .gitignore; append missing Leerness entries only.
199
+ - Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.
200
+
201
+
202
+ ## Plan Rule
203
+ - Use .harness/plan.md as the source of truth for the user's goal, project scope, milestones, included work, excluded work, and plan changes.
204
+ - Before starting feature/release/refactor/migration work, check whether the request maps to an existing plan item.
205
+ - If the user adds scope, update plan.md and progress-tracker.md.
206
+ - If the user excludes scope, mark it under Dropped / Out of Scope instead of deleting history.
207
+ - If the project looks new or lacks a usable plan, do not start broad implementation blindly. First create or ask for enough information to create .harness/plan.md.
208
+
209
+ ## Guideline Rule
210
+ - Use .harness/guideline.md for execution standards.
211
+ - guideline.md must reference plan.md and progress-tracker.md, but should not become the primary progress database.
212
+ - Progress is tracked in plan.md milestones and progress-tracker.md task rows.
213
+
214
+ ## Progress Tracker Rule
215
+ - Track user-requested work in .harness/progress-tracker.md.
216
+ - Use statuses: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped.
217
+ - If the user drops a task, mark it as dropped with reason instead of deleting the history.
218
+ - Every session close must list active unresolved work: requested, planned, waiting, on-hold, blocked, in-progress, incomplete.
219
+
220
+ ## End-of-Session Contract
221
+ 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.
100
222
 
101
223
  At the end of each session, list:
102
224
  1. Completed work in this session.
103
225
  2. User-requested work still in progress.
104
226
  3. User-requested work not started or incomplete.
105
- 4. Verification performed and results.
106
- 5. Memory files updated.
107
- 6. Risks, assumptions, or blockers.
108
- 7. Recommended next directions.
109
- 8. The single next exact action.
227
+ 4. Planned, on-hold, waiting, blocked, requested, and incomplete work from progress-tracker.md.
228
+ 5. User-dropped work, if any.
229
+ 6. Verification performed and results.
230
+ 7. Memory files updated.
231
+ 8. Risks, assumptions, or blockers.
232
+ 9. Recommended next directions.
233
+ 10. The single next exact action.
110
234
 
111
235
  ## Anti-Lazy Work Rule
112
236
  - Do not hide unfinished work behind vague summaries.
@@ -134,12 +258,85 @@ At the end of each session, list:
134
258
  '.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`,
135
259
  '.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`,
136
260
  '.env.example': `# Leerness examples only. Copy to .env.local and fill locally. Never commit real secrets.\n`,
261
+ '.harness/LANGUAGE': '{{LANGUAGE}}\n',
262
+ '.harness/language-policy.md': '{{LANGUAGE_POLICY}}',
137
263
  '.harness/HARNESS_VERSION': '{{VERSION}}\n',
138
264
  '.harness/manifest.json': '{{MANIFEST}}\n',
139
265
  '.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
140
266
  '.harness/skill-config.schema.json': `${MARK}\n{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "title": "Leerness Skill Config",\n "type": "object",\n "additionalProperties": true\n}\n`,
267
+ '.harness/plan.md': `${MARK}
268
+ ---
269
+ leernessRole: plan
270
+ readWhen: [planning, every-feature, scope-change, new-project, resume-work, session-close]
271
+ updateWhen: [new-user-request, scope-added, scope-dropped, milestone-change, progress-change, plan-reprioritized]
272
+ doNotStore: [secrets, tokens, credentials, raw-private-data]
273
+ ---
274
+
275
+ # Project Plan
276
+
277
+ Updated: {{DATE}}
278
+
279
+ ## Project Goal
280
+ - Define the user's requested purpose here.
281
+
282
+ ## Scope
283
+ - Included work should be listed here.
284
+
285
+ ## Out of Scope / Dropped
286
+ | Item | Reason | Dropped At |
287
+ |---|---|---|
288
+
289
+ ## Milestones
290
+ | ID | Milestone | Status | Progress | Related Tasks | Notes |
291
+ |---|---|---|---:|---|---|
292
+ | M-0001 | Establish Leerness plan | planned | 0% | T-0001 | Replace with project-specific milestones. |
293
+
294
+ ## Plan Change Log
295
+ | Date | Change | Reason | Requested By |
296
+ |---|---|---|---|
297
+ | {{DATE}} | Plan file created | Leerness v{{VERSION}} installation or migration | Leerness |
298
+
299
+ ## Rules
300
+ - Add new user-requested scope before implementation when it changes the overall plan.
301
+ - Do not silently implement dropped or out-of-scope work.
302
+ - Link plan milestones to progress-tracker task IDs when possible.
303
+ - Keep this file high-level; detailed task status belongs in progress-tracker.md.
304
+ {{LEGACY_PLAN}}
305
+ `,
306
+ '.harness/guideline.md': `${MARK}
307
+ ---
308
+ leernessRole: guideline
309
+ readWhen: [every-task, planning, implementation, review, release]
310
+ updateWhen: [standard-change, quality-gate-change, plan-following-rule-change, repeated-process-failure]
311
+ doNotStore: [secrets, tokens, credentials, raw-private-data]
312
+ ---
313
+
314
+ # Project Guideline
315
+
316
+ ## Source of Direction
317
+ - Primary plan: .harness/plan.md
318
+ - Task status and progress: .harness/progress-tracker.md
319
+ - Current handoff state: .harness/current-state.md and .harness/session-handoff.md
320
+
321
+ ## How to Use the Plan
322
+ 1. Check whether the user request exists in plan.md.
323
+ 2. If it is new scope, add it to plan.md and progress-tracker.md before broad implementation.
324
+ 3. If it is excluded or dropped, do not implement it unless the user reopens it.
325
+ 4. Update progress after implementation or verification changes.
326
+
327
+ ## Progress Policy
328
+ - guideline.md describes how progress should be handled.
329
+ - plan.md and progress-tracker.md store actual progress values.
330
+ - Do not duplicate detailed task tables here.
331
+
332
+ ## Quality Gates
333
+ - Follow context-routing.md before editing.
334
+ - Follow writeback-policy.md after editing.
335
+ - Verify with project-specific commands before marking work done.
336
+ - Never mark work done if requested scope remains incomplete.
337
+ `,
141
338
  '.harness/project-brief.md': `${MARK}\n---\nleernessRole: project-brief\nreadWhen: [every-task, planning, product-direction, onboarding]\nupdateWhen: [purpose-change, user-change, success-criteria-change, product-direction-change]\ndoNotStore: [secrets, tokens, credentials, raw-customer-data]\n---\n\n# Project Brief: {{PROJECT}}\n\n## Purpose\n\n## Success Criteria\n\n## Users\n\n## Product Direction\n{{LEGACY_BRIEF}}\n`,
142
- '.harness/current-state.md': `${MARK}\n---\nleernessRole: current-state\nreadWhen: [every-task, resume-work, planning, debugging, release]\nupdateWhen: [after-meaningful-work, blocker-change, next-step-change, status-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Current State\n\nUpdated: {{DATE}}\n\n## Now\n- Leerness v{{VERSION}} installed or migrated.\n\n## Next\n- Fill project-brief, context-map, design-system, and feature-contracts.\n\n## Blockers\n- None recorded.\n{{LEGACY_STATE}}\n`,
339
+ '.harness/current-state.md': `${MARK}\n---\nleernessRole: current-state\nreadWhen: [every-task, resume-work, planning, debugging, release]\nupdateWhen: [after-meaningful-work, blocker-change, next-step-change, status-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Current State\n\nUpdated: {{DATE}}\n\n## Now\n- Leerness v{{VERSION}} installed or migrated.\n\n## Next\n- Fill plan.md, project-brief, context-map, design-system, and feature-contracts.\n\n## Blockers\n- None recorded.\n{{LEGACY_STATE}}\n`,
143
340
  '.harness/architecture.md': `${MARK}\n---\nleernessRole: architecture\nreadWhen: [feature, refactor, integration, api, database, deployment]\nupdateWhen: [module-change, data-flow-change, integration-change, boundary-change]\ndoNotStore: [secrets, credentials]\n---\n\n# Architecture\n\n## Overview\n\n## Main Modules\n\n## Data Flow\n\n## External Services\n\n## Boundaries\n{{LEGACY_ARCH}}\n`,
144
341
  '.harness/context-map.md': `${MARK}\n---\nleernessRole: context-map\nreadWhen: [every-task, file-discovery, impact-analysis]\nupdateWhen: [new-important-file, moved-module, new-route, new-service, ownership-change]\ndoNotStore: [secrets, tokens]\n---\n\n# Context Map\n\n| Area | Files | Notes |\n|---|---|---|\n| UI | src/components/**, app/** | Check design-system.md first. |\n| API | src/api/**, server/**, functions/** | Preserve response contracts. |\n| Data | db/**, firestore/**, prisma/** | Confirm migrations. |\n| Tests | test/**, tests/**, __tests__/** | Add or update checks. |\n`,
145
342
  '.harness/decisions.md': `${MARK}\n---\nleernessRole: decisions\nreadWhen: [architecture, refactor, release, dependency-change, irreversible-change]\nupdateWhen: [important-decision, tradeoff, architecture-change, dependency-change, rollback-relevant-change]\ndoNotStore: [secrets, credentials]\n---\n\n# Decision Log\n\n## Template\n\n### YYYY-MM-DD — Title\n- Decision:\n- Reason:\n- Alternatives:\n- Impact:\n{{LEGACY_DECISIONS}}\n`,
@@ -195,15 +392,86 @@ doNotStore: [secrets, tokens, raw-private-data]
195
392
  -
196
393
  `,
197
394
  '.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`,
198
- '.harness/progress-tracker.md': `${MARK}\n---\nleernessRole: progress-tracker\nreadWhen: [planning, resume-work, end-of-session, multi-step-work]\nupdateWhen: [task-started, task-completed, task-blocked, scope-change, end-of-session]\ndoNotStore: [secrets, tokens, credentials, raw-private-data]\n---\n\n# Progress Tracker\n\nUse this file to track user-requested work across sessions. Keep entries concrete and checkable.\n\n| ID | User Request | Status | Owner | Last Update | Evidence / Notes | Next Action |\n|---|---|---|---|---|---|---|\n| T-0001 | Initialize Leerness project memory | done | AI | {{DATE}} | Leerness v{{VERSION}} installed or migrated. | Fill project-specific details. |\n\n## Status Values\n\n- requested\n- in-progress\n- done\n- blocked\n- incomplete\n- skipped-with-reason\n`,
395
+ '.harness/progress-tracker.md': `${MARK}
396
+ ---
397
+ leernessRole: progress-tracker
398
+ readWhen: [planning, resume-work, end-of-session, multi-step-work]
399
+ updateWhen: [task-started, task-completed, task-blocked, task-dropped, scope-change, end-of-session]
400
+ doNotStore: [secrets, tokens, credentials, raw-private-data]
401
+ ---
402
+
403
+ # Progress Tracker
404
+
405
+ Use this file to track user-requested work across sessions. Keep entries concrete and checkable. Link tasks to plan.md milestones when possible. At session close, unresolved statuses must be listed.
406
+
407
+ | ID | User Request | Status | Owner | Last Update | Evidence / Notes | Next Action |
408
+ |---|---|---|---|---|---|---|
409
+ | T-0001 | Initialize Leerness project memory | done | AI | {{DATE}} | Leerness v{{VERSION}} installed or migrated. | Fill project-specific details. |
410
+
411
+ ## Status Values
412
+
413
+ - requested
414
+ - planned
415
+ - in-progress
416
+ - waiting
417
+ - on-hold
418
+ - blocked
419
+ - incomplete
420
+ - done
421
+ - dropped
422
+
423
+ ## Drop Policy
424
+
425
+ Dropped tasks are not deleted. Mark Status as dropped and write the reason in Evidence / Notes.
426
+
427
+ ## Session Close Rule
428
+
429
+ Every session-close report must list all tasks whose Status is planned, waiting, on-hold, blocked, in-progress, incomplete, or requested.
430
+ `,
199
431
  '.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`,
200
- '.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## 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`,
432
+ '.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`,
433
+ '.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 plan.md, guideline.md, 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- plan.md exists and contains milestones/out-of-scope areas.
434
+ - guideline.md references plan.md and progress-tracker.md.
435
+ - 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`,
201
436
  '.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`,
202
437
  '.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`,
203
438
  '.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`,
204
- '.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`,
439
+ '.harness/task-type-map.md': `${MARK}\n# Task Type Map\n\n| User request | Task type | First files |\n|---|---|---|\n| 계획 수립/수정 | planning | plan, progress-tracker, guideline |
440
+ | 새 기능 | 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`,
205
441
  '.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`,
206
- '.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`,
442
+ '.harness/AX_PLAN_GUIDE.md': `${MARK}
443
+ # AX Plan Guide
444
+
445
+ Use this guide when creating, updating, dropping, or syncing the project plan.
446
+
447
+ ## Purpose
448
+ plan.md keeps the user's intended outcome, scope, milestones, exclusions, and plan changes visible across sessions. progress-tracker.md keeps concrete task states. guideline.md defines how the plan should be followed.
449
+
450
+ ## When a user asks for new work
451
+ 1. Check .harness/plan.md.
452
+ 2. Decide whether the request is an existing milestone, a subtask, new scope, or out of scope.
453
+ 3. If new scope, add a milestone or task reference.
454
+ 4. Add or update the matching row in progress-tracker.md.
455
+ 5. Route implementation using context-routing.md.
456
+
457
+ ## When a user drops work
458
+ 1. Do not delete history.
459
+ 2. Mark the task dropped in progress-tracker.md.
460
+ 3. Add it to plan.md Out of Scope / Dropped with the reason.
461
+ 4. Mention the drop in session-handoff.md.
462
+
463
+ ## New project detection
464
+ If the project lacks a meaningful plan, project brief, source structure, or clear success criteria, first create a plan draft from the user request. If the user request is not enough, ask for the missing goal/scope before broad implementation.
465
+
466
+ ## Sync rule
467
+ Run or follow: leerness plan sync
468
+ - plan.md: milestones and scope
469
+ - progress-tracker.md: concrete task statuses
470
+ - guideline.md: execution standards and progress policy
471
+ - current-state.md: immediate next work
472
+ `,
473
+ '.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- plan.md: user goal, scope, milestones, dropped/out-of-scope work
474
+ - 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`,
207
475
  '.harness/AX_SKILL_LIBRARY_GUIDE.md': `${MARK}\n# AX Skill Library Guide\n\n## AI-verified skill lifecycle\n1. Learn from a validated implementation.\n2. Remove secrets and keep env variable names only.\n3. Add displayNameKo, capabilities, lastUpdated, verification metadata.\n4. Run validate.\n5. Run verify --ai.\n6. Build.\n7. Publish dry-run.\n8. Publish with --execute only after token gate passes.\n`,
208
476
  '.harness/skills/codebase-analysis.md': `${MARK}\n# Skill: Codebase Analysis\n\nRead context-map, architecture, current-state, and related source files before proposing changes.\n`,
209
477
  '.harness/skills/feature-implementation.md': `${MARK}\n# Skill: Feature Implementation\n\nDefine contract, inspect existing patterns, implement minimal change, verify, update memory.\n`,
@@ -217,8 +485,8 @@ doNotStore: [secrets, tokens, raw-private-data]
217
485
  '.harness/templates/decision.md': `${MARK}\n# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n`
218
486
  };
219
487
 
220
- 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/skill-index.md','.harness/secret-policy.md']);
221
- 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/manifest.json','.harness/HARNESS_VERSION']);
488
+ const memoryFiles = new Set(['.harness/plan.md','.harness/guideline.md','.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']);
489
+ 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_PLAN_GUIDE.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']);
222
490
  function uniqueLinesAppend(current, addition){
223
491
  const lines=current.split(/\r?\n/); const seen=new Set(lines.map(x=>x.trim()).filter(Boolean));
224
492
  const add=addition.split(/\r?\n/).filter(line=>{ const t=line.trim(); if(!t||seen.has(t)) return false; seen.add(t); return true; });
@@ -245,9 +513,9 @@ function writeCoreSafely(root,file,body,opts={}){
245
513
  }
246
514
  if(dryRun) info('[dry-run] 보존: '+file+' (덮어쓰려면 --force)'); else ok('보존: '+file+' (덮어쓰려면 --force)'); return 'preserved';
247
515
  }
248
- function manifest(root,selectedSkills){ return JSON.stringify({name:projectName(root),harnessVersion:VERSION,installedAt:now(),managedFiles:Object.keys(coreFiles),selectedSkills,nonDestructiveMigration:true},null,2); }
516
+ 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'],planEnabled:true},null,2); }
249
517
  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); }
250
- 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) }; }
518
+ 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_PLAN:legacyBlock('plan context',pick(legacyText,['PLAN.md','plan.md','.harness/plan.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) }; }
251
519
 
252
520
  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)); }
253
521
  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; }
@@ -256,19 +524,20 @@ function appendEnvExample(root,meta){ const ep=path.join(root,'.env.example'); c
256
524
  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; }
257
525
  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); }
258
526
 
259
- 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; }
527
+ 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; }
260
528
  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); }
261
529
  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());})); }
262
530
  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); }
263
531
 
264
532
  async function init(root,flags){
265
533
  root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root);
534
+ const selectedLanguage=await chooseLanguage(root,flags); info('문서 언어: '+languageName(selectedLanguage));
266
535
  const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills);
267
536
  const found=detectLegacy(root), legacyText=collectLegacyText(found);
268
537
  if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
269
538
  const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive));
270
539
  noteLegacyPreserved(root,found,false);
271
- const ctx=makeContext(root,legacyText,selectedSkills);
540
+ const ctx=makeContext(root,legacyText,selectedSkills,selectedLanguage);
272
541
  for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{force:Boolean(flags.force)});
273
542
  if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); }
274
543
  ok('설치 완료'); info('신규 프로젝트라면 .harness/AX_NEW_PROJECT_GUIDE.md를 따라 실제 프로젝트 내용을 반영하세요.');
@@ -280,7 +549,8 @@ function migrate(root,flags){
280
549
  else { warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
281
550
  const archive=archiveLegacy(root,found,dryRun); if(archive) info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive));
282
551
  noteLegacyPreserved(root,found,dryRun);
283
- const ctx=makeContext(root,collectLegacyText(found),[]);
552
+ const selectedLanguage=normalizeLanguage(flags.language||flags.lang||readConfiguredLanguage(root)||detectLanguage(root)); info('문서 언어: '+languageName(selectedLanguage));
553
+ const ctx=makeContext(root,collectLegacyText(found),[],selectedLanguage);
284
554
  for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{dryRun,force});
285
555
  if(!dryRun){ ok('마이그레이션 완료'); info('기존 프로젝트 메모리 파일은 보존되었습니다. 템플릿 재생성이 필요하면 --force를 명시하세요.'); }
286
556
  }
@@ -300,12 +570,189 @@ function validateSkillLibrary(dir,opts={}){ dir=path.resolve(dir); let failures=
300
570
  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'); }
301
571
  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); }
302
572
  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); }
303
- 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; }
573
+ 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; }
304
574
  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); }
305
575
  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']); }
306
576
  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); }
307
577
 
308
578
 
579
+
580
+ function planFile(root){ return path.join(root,'.harness/plan.md'); }
581
+ function ensurePlan(root){
582
+ const p=planFile(root);
583
+ if(!exists(p)){
584
+ const ctx=makeContext(root,{},[],readConfiguredLanguage(root)||detectLanguage(root));
585
+ writeCoreSafely(root,'.harness/plan.md',fill(coreFiles['.harness/plan.md'],ctx),{force:false,dryRun:false});
586
+ }
587
+ return p;
588
+ }
589
+ function readPlanText(root){ const p=ensurePlan(root); return read(p); }
590
+ function writePlanText(root,body){ write(planFile(root),body); }
591
+ function nextMilestoneId(body){ let max=0; for(const m of body.matchAll(/\bM-(\d+)\b/g)) max=Math.max(max,parseInt(m[1],10)); return 'M-'+String(max+1).padStart(4,'0'); }
592
+ function appendPlanChange(body,change,reason,by){
593
+ const row=`| ${today()} | ${String(change).replace(/\|/g,'/')} | ${String(reason||'-').replace(/\|/g,'/')} | ${String(by||'user').replace(/\|/g,'/')} |`;
594
+ if(body.includes('## Plan Change Log')) return body.replace(/(## Plan Change Log[\s\S]*?\n\|---\|---\|---\|---\|\n)/, '$1'+row+'\n');
595
+ return body.replace(/\s*$/,'\n\n## Plan Change Log\n| Date | Change | Reason | Requested By |\n|---|---|---|---|\n'+row+'\n');
596
+ }
597
+ function planAdd(root,text,flags={}){
598
+ root=path.resolve(root||process.cwd()); const file=ensurePlan(root); let body=read(file); const id=nextMilestoneId(body);
599
+ const status=String(flags.status||'planned'); const progress=String(flags.progress||'0').replace(/%$/,'')+'%';
600
+ const row=`| ${id} | ${String(text).replace(/\|/g,'/')} | ${status} | ${progress} | - | ${String(flags.note||'Added from user request').replace(/\|/g,'/')} |`;
601
+ if(body.includes('## Milestones')){
602
+ body=body.replace(/(## Milestones[\s\S]*?\n\|---\|---\|---\|---:?\|---\|---\|\n)/, '$1'+row+'\n');
603
+ } else {
604
+ body=body.replace(/\s*$/,'\n\n## Milestones\n| ID | Milestone | Status | Progress | Related Tasks | Notes |\n|---|---|---|---:|---|---|\n'+row+'\n');
605
+ }
606
+ body=appendPlanChange(body,'Added '+id+': '+text,flags.reason||'new user request',flags.by||'user');
607
+ writePlanText(root,body); ok('계획 추가: '+id);
608
+ const tasks=readProgressTasks(root); tasks.push({id:nextTaskId(tasks),request:text,status:status==='done'?'done':'planned',owner:'AI',lastUpdate:today(),evidence:'Linked to '+id,nextAction:String(flags.next||'Start or refine this plan item')}); writeProgressTasks(root,tasks);
609
+ }
610
+ function planDrop(root,item,flags={}){
611
+ root=path.resolve(root||process.cwd()); const file=ensurePlan(root); let body=read(file); const reason=String(flags.reason||'Dropped by user');
612
+ const row=`| ${String(item).replace(/\|/g,'/')} | ${reason.replace(/\|/g,'/')} | ${today()} |`;
613
+ if(body.includes('## Out of Scope / Dropped')) body=body.replace(/(## Out of Scope \/ Dropped[\s\S]*?\n\|---\|---\|---\|\n)/, '$1'+row+'\n');
614
+ else body=body.replace(/\s*$/,'\n\n## Out of Scope / Dropped\n| Item | Reason | Dropped At |\n|---|---|---|\n'+row+'\n');
615
+ body=appendPlanChange(body,'Dropped: '+item,reason,flags.by||'user'); writePlanText(root,body); ok('계획 드랍 기록: '+item);
616
+ const tasks=readProgressTasks(root); let matched=false; for(const t of tasks){ if(String(t.request).includes(item)||String(item).includes(t.request)){ t.status='dropped'; t.evidence=reason; t.nextAction='None'; t.lastUpdate=today(); matched=true; } }
617
+ if(!matched) tasks.push({id:nextTaskId(tasks),request:item,status:'dropped',owner:'AI',lastUpdate:today(),evidence:reason,nextAction:'None'}); writeProgressTasks(root,tasks);
618
+ }
619
+ function planUpdate(root,id,flags={}){
620
+ root=path.resolve(root||process.cwd()); const file=ensurePlan(root); let body=read(file); let changed=false;
621
+ body=body.split(/\r?\n/).map(line=>{
622
+ if(!line.startsWith('| '+id+' |')) return line;
623
+ const cols=splitTableLine(line); if(cols.length<6) return line;
624
+ if(flags.title||flags.name) cols[1]=String(flags.title||flags.name).replace(/\|/g,'/');
625
+ if(flags.status) cols[2]=String(flags.status);
626
+ if(flags.progress!==undefined) cols[3]=String(flags.progress).replace(/%$/,'')+'%';
627
+ if(flags.tasks) cols[4]=String(flags.tasks).replace(/\|/g,'/');
628
+ if(flags.note) cols[5]=String(flags.note).replace(/\|/g,'/');
629
+ changed=true; return '| '+cols.join(' | ')+' |';
630
+ }).join('\n');
631
+ if(!changed){ fail('계획 ID를 찾을 수 없습니다: '+id); process.exitCode=1; return; }
632
+ body=appendPlanChange(body,'Updated '+id,flags.reason||'plan update',flags.by||'user'); writePlanText(root,body); ok('계획 업데이트: '+id);
633
+ }
634
+ function planProgress(root){
635
+ root=path.resolve(root||process.cwd()); const body=readPlanText(root); const rows=[];
636
+ for(const line of body.split(/\r?\n/)){ if(/^\|\s*M-/.test(line)){ const c=splitTableLine(line); if(c.length>=6) rows.push({id:c[0],milestone:c[1],status:c[2],progress:c[3],tasks:c[4],notes:c[5]}); } }
637
+ banner(); log('Plan progress: '+root); if(!rows.length){ warn('계획 milestone이 없습니다. leerness plan add 를 사용하세요.'); return; }
638
+ rows.forEach(r=>log(` - ${r.id} [${r.status}] ${r.progress} ${r.milestone} :: tasks=${r.tasks}`));
639
+ }
640
+ function planSync(root){
641
+ root=path.resolve(root||process.cwd()); ensurePlan(root); const tasks=readProgressTasks(root);
642
+ const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
643
+ const done=tasks.filter(t=>t.status==='done').length; const total=tasks.filter(t=>t.status!=='dropped').length; const pct=total?Math.round(done/total*100):0;
644
+ const cs=path.join(root,'.harness/current-state.md');
645
+ if(exists(cs)){
646
+ let body=read(cs); const block=`\n## Plan Sync\n- Updated: ${today()}\n- Non-dropped task progress: ${done}/${total} (${pct}%)\n- Active unresolved tasks: ${active.length}\n- Source: plan.md + progress-tracker.md\n`;
647
+ if(body.includes('## Plan Sync')) body=body.replace(/\n## Plan Sync[\s\S]*?(?=\n## |$)/, block);
648
+ else body=body.replace(/\s*$/, block+'\n');
649
+ write(cs,body); ok('current-state.md plan sync 갱신');
650
+ }
651
+ ok('plan/progress sync 완료: '+done+'/'+total+' = '+pct+'%');
652
+ }
653
+ function planCommand(args,flags){
654
+ const sub=args[1]||'show'; const root=path.resolve(flags.path||((sub==='show'||sub==='progress'||sub==='sync'||sub==='init')?args[2]:process.cwd())||process.cwd());
655
+ if(sub==='show'){ log(readPlanText(root)); return; }
656
+ if(sub==='init'){ ensurePlan(root); if(flags.goal){ planAdd(root,String(flags.goal),{status:'planned',reason:'initial project goal',by:'user'}); } ok('plan.md 준비 완료'); return; }
657
+ if(sub==='add'){ const text=args[2]||flags.title||flags.name; if(!text){ fail('추가할 계획 내용을 입력하세요.'); process.exitCode=1; return; } return planAdd(root,text,flags); }
658
+ if(sub==='drop'||sub==='remove'){ const item=args[2]||flags.title||flags.name; if(!item){ fail('드랍할 계획 항목을 입력하세요.'); process.exitCode=1; return; } return planDrop(root,item,flags); }
659
+ if(sub==='update'){ const id=args[2]; if(!id){ fail('업데이트할 계획 ID를 입력하세요.'); process.exitCode=1; return; } return planUpdate(root,id,flags); }
660
+ if(sub==='progress') return planProgress(root);
661
+ if(sub==='sync') return planSync(root);
662
+ fail('알 수 없는 plan 명령: '+sub);
663
+ }
664
+
665
+
666
+ const ACTIVE_TASK_STATUSES = new Set(['requested','planned','in-progress','waiting','on-hold','blocked','incomplete']);
667
+ const ALL_TASK_STATUSES = new Set(['requested','planned','in-progress','waiting','on-hold','blocked','incomplete','done','dropped']);
668
+ function progressFile(root){ return path.join(root,'.harness/progress-tracker.md'); }
669
+ function splitTableLine(line){ return line.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(x=>x.trim()); }
670
+ function readProgressTasks(root){
671
+ const file=progressFile(root); if(!exists(file)) return [];
672
+ const tasks=[];
673
+ for(const line of read(file).split(/\r?\n/)){
674
+ if(!/^\|\s*T-[0-9A-Za-z_-]+\s*\|/.test(line)) continue;
675
+ const cols=splitTableLine(line); if(cols.length<7) continue;
676
+ tasks.push({id:cols[0],request:cols[1],status:cols[2],owner:cols[3],lastUpdate:cols[4],evidence:cols[5],nextAction:cols[6]});
677
+ }
678
+ return tasks;
679
+ }
680
+ function taskTable(tasks){
681
+ 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';
682
+ }
683
+ function writeProgressTasks(root,tasks){
684
+ const file=progressFile(root);
685
+ const lang=readConfiguredLanguage(root)||detectLanguage(root);
686
+ const intro=lang==='ko'
687
+ ? '# Progress Tracker\n\n사용자가 요청한 작업을 세션 간 추적합니다. 세션 종료 시 미완료/예정/보류/대기/진행중 작업은 반드시 표시합니다.\n\n'
688
+ : '# 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';
689
+ 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';
690
+ 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);
691
+ }
692
+ function nextTaskId(tasks){
693
+ 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)); }
694
+ return 'T-'+String(max+1).padStart(4,'0');
695
+ }
696
+ function renderTasks(tasks,filterStatuses){
697
+ const filtered=filterStatuses?tasks.filter(t=>filterStatuses.has(t.status)):tasks;
698
+ if(!filtered.length){ log(' - 없음'); return; }
699
+ for(const t of filtered) log(` - ${t.id} [${t.status}] ${t.request} :: next=${t.nextAction||'-'} :: note=${t.evidence||'-'}`);
700
+ }
701
+ function taskCommand(args,flags){
702
+ const sub=args[1]||'list'; const root=path.resolve(flags.path||args[3]||process.cwd());
703
+ const tasks=readProgressTasks(root);
704
+ if(sub==='list'){
705
+ banner();
706
+ const status=flags.status?new Set(String(flags.status).split(',').map(x=>x.trim()).filter(Boolean)):null;
707
+ log('Progress tasks: '+root); renderTasks(tasks,status); return;
708
+ }
709
+ if(sub==='add'){
710
+ const request=args[2]||flags.title||flags.name; if(!request){ fail('추가할 작업 내용을 입력하세요. 예: leerness task add "배포 검증" --status planned'); process.exitCode=1; return; }
711
+ const status=String(flags.status||'planned'); if(!ALL_TASK_STATUSES.has(status)){ fail('지원하지 않는 status: '+status); process.exitCode=1; return; }
712
+ 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')};
713
+ tasks.push(t); writeProgressTasks(root,tasks); ok('작업 추가: '+t.id); return;
714
+ }
715
+ if(sub==='update'){
716
+ const id=args[2]; const t=tasks.find(x=>x.id===id); if(!t){ fail('작업 ID를 찾을 수 없습니다: '+id); process.exitCode=1; return; }
717
+ if(flags.status){ const st=String(flags.status); if(!ALL_TASK_STATUSES.has(st)){ fail('지원하지 않는 status: '+st); process.exitCode=1; return; } t.status=st; }
718
+ if(flags.evidence) t.evidence=String(flags.evidence);
719
+ if(flags.next||flags.action) t.nextAction=String(flags.next||flags.action);
720
+ if(flags.owner) t.owner=String(flags.owner);
721
+ t.lastUpdate=today(); writeProgressTasks(root,tasks); ok('작업 업데이트: '+id); return;
722
+ }
723
+ if(sub==='drop'||sub==='remove'){
724
+ const id=args[2]; const t=tasks.find(x=>x.id===id); if(!t){ fail('작업 ID를 찾을 수 없습니다: '+id); process.exitCode=1; return; }
725
+ 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;
726
+ }
727
+ fail('알 수 없는 task 명령: '+sub);
728
+ }
729
+ function debugHarness(root){
730
+ root=path.resolve(root||process.cwd()); banner(); let failures=0,warnings=0;
731
+ const required=['AGENTS.md','.harness/plan.md','.harness/guideline.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'];
732
+ for(const f of required){ if(exists(path.join(root,f))) ok('존재: '+f); else { failures++; fail('누락: '+f); } }
733
+ const ag=path.join(root,'AGENTS.md');
734
+ if(exists(ag)){
735
+ const body=read(ag);
736
+ for(const term of ['plan.md','guideline.md','language-policy.md','context-routing.md','writeback-policy.md','progress-tracker.md','anti-lazy-work-policy.md','End-of-Session Contract']){
737
+ if(body.includes(term)) ok('AGENTS 방향지시 포함: '+term); else { failures++; fail('AGENTS 방향지시 누락: '+term); }
738
+ }
739
+ }
740
+
741
+ const pf=path.join(root,'.harness/plan.md');
742
+ if(exists(pf)){ const b=read(pf); if(b.includes('## Milestones')) ok('plan.md milestones 확인'); else { warnings++; warn('plan.md에 Milestones 섹션이 없습니다.'); } }
743
+ const gf=path.join(root,'.harness/guideline.md');
744
+ if(exists(gf)){ const b=read(gf); if(b.includes('plan.md')&&b.includes('progress-tracker.md')) ok('guideline.md가 plan/progress를 참조합니다.'); else { failures++; fail('guideline.md가 plan.md/progress-tracker.md를 참조하지 않습니다.'); } }
745
+ const mf=path.join(root,'.harness/manifest.json');
746
+ if(exists(mf)){ const m=parseJsonSafe(read(mf),{}); if(m.language) ok('언어 설정: '+m.language+' ('+(m.languageName||languageName(m.language))+')'); else { warnings++; warn('manifest language 누락'); } }
747
+ const tasks=readProgressTasks(root);
748
+ const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
749
+ log('\nActive unresolved tasks:'); renderTasks(active);
750
+ const badSensitive=scanSensitivePath(path.join(root,'.harness')).filter(x=>!x.file.includes(path.sep+'archive'+path.sep));
751
+ if(badSensitive.length){ failures+=badSensitive.length; badSensitive.forEach(x=>fail('민감정보 의심: '+rel(root,x.file)+' '+x.type)); } else ok('하네스 민감정보 스캔 통과');
752
+ log('\nDebug summary: '+(failures?'FAIL':'PASS')+' / warnings='+warnings+' / failures='+failures);
753
+ if(failures) process.exitCode=1;
754
+ }
755
+
309
756
  function closeSession(root){
310
757
  root=path.resolve(root||process.cwd());
311
758
  banner();
@@ -315,21 +762,30 @@ function closeSession(root){
315
762
  log('Session close checklist');
316
763
  log('');
317
764
  log('Read before closing:');
318
- ['.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));
765
+ ['.harness/session-close-policy.md','.harness/plan.md','.harness/guideline.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));
319
766
  log('');
320
767
  log('Required final report sections:');
321
- ['Completed This Session','In Progress From User Requests','Incomplete / Not Started From User Requests','Verification','Files Changed','Memory Files Updated','Risks / Assumptions / Blockers','Recommended Next Directions','Next Exact Step'].forEach(x=>log(' - '+x));
768
+ ['Plan Progress Summary','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));
322
769
  log('');
323
770
  if(exists(template)) log(read(template));
324
- 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## 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');
771
+ 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');
772
+ const tasks=readProgressTasks(root);
773
+ const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
774
+ const dropped=tasks.filter(t=>t.status==='dropped');
775
+ log('');
776
+ log('Tracked unresolved / planned / waiting / on-hold / in-progress work:');
777
+ renderTasks(active);
778
+ log('');
779
+ log('Dropped by user:');
780
+ renderTasks(dropped);
325
781
  if(!exists(policy)) warn('session-close-policy.md가 없습니다. leerness migrate를 실행하세요.');
326
782
  if(!exists(progress)) warn('progress-tracker.md가 없습니다. leerness migrate를 실행하세요.');
327
783
  }
328
784
  function sessionCommand(args){ const sub=args[1]||'close'; if(sub==='close'||sub==='handoff'||sub==='end') return closeSession(args[2]||process.cwd()); fail('알 수 없는 session 명령: '+sub); }
329
785
 
330
- 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']},'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']}};
331
- 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)); }
332
- 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]',
333
- ' leerness session close [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation|session-close>','','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')); }
334
- 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==='session') return sessionCommand(args); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
786
+ const routeData={planning:{read:['plan.md','progress-tracker.md','project-brief.md','current-state.md','guideline.md'],update:['plan.md','progress-tracker.md','current-state.md','session-handoff.md']},feature:{read:['plan.md','progress-tracker.md','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:['plan.md','progress-tracker.md','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','AX_PLAN_GUIDE.md','actual project files'],update:['plan.md','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','plan.md','guideline.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','plan.md','guideline.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']}};
787
+ 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; }
788
+ 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))); }
789
+ 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 <planning|feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation|debug|session-close>','','Plan:',' leerness plan show [path]',' leerness plan init [path] --goal "project goal"',' leerness plan add "milestone or scope" [--status planned]',' leerness plan drop "item" --reason "user excluded"',' leerness plan update M-0002 --status in-progress --progress 40',' leerness plan progress [path]',' leerness plan sync [path]','','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')); }
790
+ 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==='plan') return planCommand(args,flags); 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; }
335
791
  main().catch(err=>{ fail(err.stack||err.message); process.exit(1); });
@@ -0,0 +1,17 @@
1
+ # AX Plan Guide
2
+
3
+ Leerness uses `.harness/plan.md` as the high-level project plan and `.harness/progress-tracker.md` as the concrete task status database.
4
+
5
+ ## When to read
6
+ - Before feature, release, refactor, migration, or broad implementation work
7
+ - When a user adds, excludes, reprioritizes, or changes scope
8
+ - At session close
9
+
10
+ ## When to update
11
+ - New user-requested scope appears
12
+ - A milestone changes status or progress
13
+ - A user drops or excludes work
14
+ - A task moves between planned, in-progress, waiting, on-hold, blocked, incomplete, done, or dropped
15
+
16
+ ## Rule
17
+ `guideline.md` defines how the plan is followed. It should reference `plan.md` and `progress-tracker.md`, but it should not be the primary place for detailed task progress.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.3.2",
4
- "description": "Leerness: 세션 종료 인수인계, 진행/미완료 작업 추적, 추천 방향 제시, 게으른 작업 방지 정책을 포함한 AX 최적화 개발 하네스.",
3
+ "version": "1.5.0",
4
+ "description": "Leerness: plan.md 기반 계획 수립/수정, progress-tracker 진행률 추적, guideline 참조 정책, 세션 인수인계와 AX 라우팅을 지원하는 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
7
7
  "ai",
@@ -29,7 +29,21 @@
29
29
  "unfinished-task-tracking",
30
30
  "anti-lazy-work",
31
31
  "progress-tracker",
32
- "end-of-session-report"
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",
39
+ "plan-md",
40
+ "project-plan",
41
+ "plan-orchestration",
42
+ "plan-progress",
43
+ "guideline-policy",
44
+ "planning-router",
45
+ "project-roadmap",
46
+ "scope-drop"
33
47
  ],
34
48
  "bin": {
35
49
  "leerness": "./bin/harness.js"
@@ -43,7 +57,7 @@
43
57
  "LICENSE"
44
58
  ],
45
59
  "scripts": {
46
- "test": "node ./bin/harness.js --help && node ./bin/harness.js route session-close && 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 session close ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
60
+ "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 plan add \"테스트 계획 항목\" --status planned --path ./tmp-harness-test && node ./bin/harness.js plan progress ./tmp-harness-test && node ./bin/harness.js plan drop \"테스트 계획 항목\" --reason \"테스트 제외\" --path ./tmp-harness-test && node ./bin/harness.js plan sync ./tmp-harness-test && node ./bin/harness.js task add \"테스트 예정 작업\" --status planned --path ./tmp-harness-test && node ./bin/harness.js route planning && 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",
47
61
  "prepack": "node ./bin/harness.js --version"
48
62
  },
49
63
  "repository": {