leerness 1.6.0 → 1.8.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 +42 -141
- package/bin/harness.js +447 -897
- package/docs/AX_CONSISTENCY_GUIDE.md +9 -0
- package/docs/AX_MIGRATION_GUIDE.md +7 -27
- package/docs/AX_PLAN_GUIDE.md +6 -14
- package/package.json +8 -43
- package/skill-packs/ads-analytics/README.md +4 -22
- package/skill-packs/ai-verified-skill-publisher/README.md +4 -22
- package/skill-packs/appstore-review/README.md +4 -22
- package/skill-packs/commerce-api/README.md +4 -22
- package/skill-packs/crawling/README.md +4 -22
- package/skill-packs/firebase/README.md +4 -22
- package/skill-packs/office/README.md +4 -22
- package/docs/AX_NEW_PROJECT_GUIDE.md +0 -25
- package/docs/AX_SKILL_LIBRARY_GUIDE.md +0 -29
- package/skill-packs/ads-analytics/skill.json +0 -28
- package/skill-packs/ads-analytics/skills/conversion-attribution.md +0 -6
- package/skill-packs/ads-analytics/skills/ga4-debugging.md +0 -7
- package/skill-packs/ads-analytics/skills/roas-check.md +0 -7
- package/skill-packs/ai-verified-skill-publisher/skill.json +0 -31
- package/skill-packs/ai-verified-skill-publisher/skills/ai-verified-skill-upload.md +0 -16
- package/skill-packs/ai-verified-skill-publisher/skills/publish-auth-token-gate.md +0 -30
- package/skill-packs/ai-verified-skill-publisher/skills/skill-library-release-flow.md +0 -10
- package/skill-packs/ai-verified-skill-publisher/skills/skill-metadata-standard.md +0 -5
- package/skill-packs/appstore-review/skill.json +0 -28
- package/skill-packs/appstore-review/skills/privacy-labels.md +0 -6
- package/skill-packs/appstore-review/skills/review-response.md +0 -7
- package/skill-packs/appstore-review/skills/webview-app.md +0 -6
- package/skill-packs/commerce-api/skill.json +0 -32
- package/skill-packs/commerce-api/skills/common-auth.md +0 -22
- package/skill-packs/commerce-api/skills/coupang.md +0 -19
- package/skill-packs/commerce-api/skills/lotteon.md +0 -17
- package/skill-packs/commerce-api/skills/order-sync.md +0 -9
- package/skill-packs/commerce-api/skills/smartstore.md +0 -17
- package/skill-packs/crawling/skill.json +0 -28
- package/skill-packs/crawling/skills/browser-automation.md +0 -14
- package/skill-packs/crawling/skills/cloud-runtime.md +0 -7
- package/skill-packs/crawling/skills/download-automation.md +0 -9
- package/skill-packs/firebase/skill.json +0 -31
- package/skill-packs/firebase/skills/firestore-indexes.md +0 -6
- package/skill-packs/firebase/skills/functions-deploy.md +0 -13
- package/skill-packs/firebase/skills/secrets.md +0 -6
- package/skill-packs/office/skill.json +0 -28
- package/skill-packs/office/skills/excel-automation.md +0 -24
- package/skill-packs/office/skills/powerpoint-generation.md +0 -17
- package/skill-packs/office/skills/word-template.md +0 -15
package/bin/harness.js
CHANGED
|
@@ -3,920 +3,470 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const cp = require('child_process');
|
|
6
7
|
const readline = require('readline');
|
|
7
|
-
const childProcess = require('child_process');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.
|
|
9
|
+
const VERSION = '1.8.0';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
function readConfiguredLanguage(root){
|
|
64
|
-
const mf=path.join(root,'.harness/manifest.json');
|
|
65
|
-
if(exists(mf)){ const j=parseJsonSafe(read(mf),{}); if(j.language) return normalizeLanguage(j.language); }
|
|
66
|
-
const lp=path.join(root,'.harness/LANGUAGE');
|
|
67
|
-
if(exists(lp)) return normalizeLanguage(read(lp).trim());
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
async function chooseLanguage(root,flags){
|
|
71
|
-
const requested=normalizeLanguage(flags.language||flags.lang||'auto');
|
|
72
|
-
const configured=readConfiguredLanguage(root);
|
|
73
|
-
if(requested!=='auto') return requested;
|
|
74
|
-
if(configured) return configured;
|
|
75
|
-
const detected=detectLanguage(root);
|
|
76
|
-
if(flags.yes||flags.y||!process.stdin.isTTY) return detected;
|
|
77
|
-
log(c.bold+'문서 작성 언어 선택'+c.reset);
|
|
78
|
-
log(' 1) 자동 감지: '+languageName(detected));
|
|
79
|
-
log(' 2) 한국어');
|
|
80
|
-
log(' 3) English');
|
|
81
|
-
const ans=await ask('\n선택 (Enter=자동 감지): ');
|
|
82
|
-
if(ans==='2') return 'ko';
|
|
83
|
-
if(ans==='3') return 'en';
|
|
84
|
-
return detected;
|
|
85
|
-
}
|
|
86
|
-
function languagePolicyBody(code){
|
|
87
|
-
if(code==='ko') return `${MARK}\n---\nleernessRole: language-policy\nreadWhen: [every-task, documentation, skill-writing, session-close]\nupdateWhen: [user-language-preference-change, project-language-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Language Policy\n\n## Primary Language\n\nKorean\n\n## Rule\n\n- 하네스 문서, 스킬 문서, 세션 인수인계, 진행 작업 목록은 한국어로 작성한다.\n- 코드 식별자, 파일명, 명령어, API 필드명, 환경변수명은 원문을 유지한다.\n- 외부 오류 메시지는 원문을 보존하고, 필요한 경우 한국어 설명을 덧붙인다.\n- 스킬 라이브러리에는 한글명(displayNameKo)과 가능한 작업(capabilities)을 반드시 유지한다.\n`;
|
|
88
|
-
return `${MARK}\n---\nleernessRole: language-policy\nreadWhen: [every-task, documentation, skill-writing, session-close]\nupdateWhen: [user-language-preference-change, project-language-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Language Policy\n\n## Primary Language\n\nEnglish\n\n## Rule\n\n- Write harness documents, skill documents, session handoffs, and progress lists in English.\n- Preserve code identifiers, filenames, commands, API fields, and environment variable names exactly.\n- Preserve external error messages verbatim and add explanations when useful.\n- Skill libraries must keep displayNameKo when available and list clear capabilities.\n`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function copyRecursive(src,dst,ignoreAbs=[]){
|
|
92
|
-
const abs=path.resolve(src); if(ignoreAbs.some(i=>abs===i||abs.startsWith(i+path.sep))) return;
|
|
93
|
-
const st=fs.statSync(src);
|
|
94
|
-
if(st.isDirectory()){ fs.mkdirSync(dst,{recursive:true}); for(const n of fs.readdirSync(src)) copyRecursive(path.join(src,n),path.join(dst,n),ignoreAbs); }
|
|
95
|
-
else { fs.mkdirSync(path.dirname(dst),{recursive:true}); fs.copyFileSync(src,dst); }
|
|
96
|
-
}
|
|
97
|
-
function detectLegacy(root){
|
|
98
|
-
return legacyItems.map(item=>({item,full:path.join(root,item)})).filter(e=>{
|
|
99
|
-
if(!exists(e.full)) return false;
|
|
100
|
-
if(e.item==='.harness'){
|
|
101
|
-
const vf=path.join(root,'.harness/HARNESS_VERSION');
|
|
102
|
-
return !exists(vf) || read(vf).trim()!==VERSION;
|
|
103
|
-
}
|
|
104
|
-
try{
|
|
105
|
-
if(fs.statSync(e.full).isFile() && isTextFile(e.item)){
|
|
106
|
-
const b=read(e.full);
|
|
107
|
-
if(b.includes(MARK)||b.includes(MIGRATED)) return false;
|
|
108
|
-
}
|
|
109
|
-
}catch{}
|
|
110
|
-
return true;
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
function collectLegacyText(found){ const out={}; for(const f of found){ try{ if(fs.statSync(f.full).isFile() && isTextFile(f.item)) out[f.item]=read(f.full); }catch{} } return out; }
|
|
114
|
-
function pick(obj,keys){ const out={}; for(const k of keys) if(obj[k]) out[k]=obj[k]; return out; }
|
|
115
|
-
function legacyBlock(title,obj){ const entries=Object.entries(obj).filter(([,v])=>String(v).trim()); if(!entries.length) return ''; return '\n---\n## Migrated legacy notes: '+title+'\n\n'+entries.map(([k,v])=>'### '+k+'\n\n'+String(v).trim()+'\n').join('\n'); }
|
|
116
|
-
function archiveLegacy(root,found,dryRun){
|
|
117
|
-
if(!found.length) return null;
|
|
118
|
-
const stamp=now().replace(/[:.]/g,'-');
|
|
119
|
-
const archive=path.join(root,'.harness/archive/legacy-migration-'+stamp);
|
|
120
|
-
if(dryRun) return archive;
|
|
121
|
-
fs.mkdirSync(archive,{recursive:true});
|
|
122
|
-
const archiveRoot=path.resolve(path.join(root,'.harness/archive'));
|
|
123
|
-
for(const f of found){
|
|
124
|
-
try{ copyRecursive(f.full,path.join(archive,f.item==='.harness'?'.harness-before-v'+VERSION:f.item),[archiveRoot]); }
|
|
125
|
-
catch(e){ warn('백업 실패: '+f.item+' ('+e.message+')'); }
|
|
11
|
+
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
|
+
const README_END = '<!-- leerness:project-readme:end -->';
|
|
13
|
+
|
|
14
|
+
const skillCatalog = {
|
|
15
|
+
'office': {
|
|
16
|
+
displayNameKo: '마이크로소프트 오피스 자동화 스킬 라이브러리',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
lastUpdated: '2026-05-06',
|
|
19
|
+
verification: 'passed',
|
|
20
|
+
capabilities: ['Word/Excel/PowerPoint 문서 자동화', '템플릿 기반 문서 생성', '표/차트/요약 문서화', '민감정보 제외 규칙 적용']
|
|
21
|
+
},
|
|
22
|
+
'commerce-api': {
|
|
23
|
+
displayNameKo: '커머스 API 연동 스킬 라이브러리',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
lastUpdated: '2026-05-06',
|
|
26
|
+
verification: 'passed',
|
|
27
|
+
capabilities: ['쿠팡·롯데온·스마트스토어 API 연동 설계', '주문/상품/매출 동기화', '환경변수 기반 인증 분리', '레이트리밋/재시도/오류 처리']
|
|
28
|
+
},
|
|
29
|
+
'crawling': {
|
|
30
|
+
displayNameKo: '크롤링·브라우저 자동화 스킬 라이브러리',
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
lastUpdated: '2026-05-06',
|
|
33
|
+
verification: 'passed',
|
|
34
|
+
capabilities: ['Playwright 기반 자동화', '다운로드/로그인 세션 처리', '스크린샷 기반 실패 진단', '약관/권한/차단 위험 점검']
|
|
35
|
+
},
|
|
36
|
+
'firebase': {
|
|
37
|
+
displayNameKo: 'Firebase·Cloud Functions 스킬 라이브러리',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
lastUpdated: '2026-05-06',
|
|
40
|
+
verification: 'passed',
|
|
41
|
+
capabilities: ['Firebase Functions 배포 구조', '환경변수/시크릿 분리', '권한/IAM 점검', '로컬 에뮬레이터 검증']
|
|
42
|
+
},
|
|
43
|
+
'ads-analytics': {
|
|
44
|
+
displayNameKo: '광고·GA4 분석 스킬 라이브러리',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
lastUpdated: '2026-05-06',
|
|
47
|
+
verification: 'passed',
|
|
48
|
+
capabilities: ['GA4 이벤트/전환 점검', '광고 데이터 수집 구조화', '소스/매체 분석', '리포트 자동화']
|
|
49
|
+
},
|
|
50
|
+
'appstore-review': {
|
|
51
|
+
displayNameKo: '앱스토어 심사 대응 스킬 라이브러리',
|
|
52
|
+
version: '1.0.0',
|
|
53
|
+
lastUpdated: '2026-05-06',
|
|
54
|
+
verification: 'passed',
|
|
55
|
+
capabilities: ['심사 문구 분석', '개인정보 라벨 점검', '리젝 대응 초안', '웹뷰/앱 데이터 수집 구분']
|
|
56
|
+
},
|
|
57
|
+
'ai-verified-skill-publisher': {
|
|
58
|
+
displayNameKo: 'AI 검증 스킬 업로드·라이브러리화 스킬',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
lastUpdated: '2026-05-06',
|
|
61
|
+
verification: 'passed',
|
|
62
|
+
capabilities: ['검증된 스킬 정규화', '민감정보 스캔', 'AI 검증 메타데이터 작성', 'npm/git 업로드 dry-run 및 실행 게이트']
|
|
126
63
|
}
|
|
127
|
-
write(path.join(archive,'migration-manifest.json'),JSON.stringify({version:VERSION,archivedAt:now(),items:found.map(x=>x.item)},null,2)+'\n');
|
|
128
|
-
return archive;
|
|
129
|
-
}
|
|
130
|
-
function targetForLegacy(item){ if(/ARCHITECTURE/i.test(item)) return '.harness/architecture.md'; if(/DECISION/i.test(item)) return '.harness/decisions.md'; if(/CURRENT|TASK_LOG|history/i.test(item)) return '.harness/current-state.md'; if(/AGENT|CLAUDE|cursor|copilot|cursorrules/i.test(item)) return 'AGENTS.md'; return '.harness/project-brief.md'; }
|
|
131
|
-
function noteLegacyPreserved(root,found,dryRun){
|
|
132
|
-
for(const f of found){
|
|
133
|
-
if(f.item==='.harness'||coreFiles[f.item]) continue;
|
|
134
|
-
try{ if(!fs.statSync(f.full).isFile()) continue; }catch{ continue; }
|
|
135
|
-
const target=targetForLegacy(f.item);
|
|
136
|
-
if(dryRun) info('[dry-run] legacy file preserved: '+f.item+' (suggested source: '+target+')');
|
|
137
|
-
else info('보존: '+f.item+' (참조 권장: '+target+')');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const coreFiles = {
|
|
142
|
-
'AGENTS.md': `${MARK}
|
|
143
|
-
# {{PROJECT}} AI Agent Harness
|
|
144
|
-
|
|
145
|
-
Agent = Model + Leerness Harness.
|
|
146
|
-
|
|
147
|
-
## Core Rule
|
|
148
|
-
Before editing, route the task. Read .harness/language-policy.md, .harness/context-routing.md, and use \`leerness route <task-type>\` when the task type is unclear.
|
|
149
|
-
|
|
150
|
-
## Universal Read Order
|
|
151
|
-
1. .harness/plan.md
|
|
152
|
-
2. .harness/progress-tracker.md
|
|
153
|
-
3. .harness/project-brief.md
|
|
154
|
-
4. .harness/current-state.md
|
|
155
|
-
5. .harness/language-policy.md
|
|
156
|
-
6. .harness/leerness-maintenance.md
|
|
157
|
-
7. .harness/context-routing.md
|
|
158
|
-
7. .harness/writeback-policy.md
|
|
159
|
-
8. .harness/task-type-map.md
|
|
160
|
-
9. .harness/context-map.md
|
|
161
|
-
10. .harness/guideline.md
|
|
162
|
-
11. .harness/guardrails.md
|
|
163
|
-
12. .harness/skills-lock.json
|
|
164
|
-
|
|
165
|
-
## Language Rule
|
|
166
|
-
- Before writing or updating any harness/skill/session document, read .harness/language-policy.md.
|
|
167
|
-
- Use the configured project language consistently.
|
|
168
|
-
- Preserve code names, commands, file paths, environment variables, and API field names exactly.
|
|
169
|
-
|
|
170
|
-
## Leerness Version Maintenance
|
|
171
|
-
- Before long-running development, release, migration, or harness-related work, check .harness/leerness-maintenance.md.
|
|
172
|
-
- Prefer leerness self check . when Leerness is already available.
|
|
173
|
-
- If the installed package may be old, run npm view leerness version and compare it with leerness --version and .harness/HARNESS_VERSION.
|
|
174
|
-
- If npm reports a newer Leerness version, run npx --yes leerness@<latest> migrate . --dry-run first, inspect changes, then run the migration only when it is safe.
|
|
175
|
-
- Record version checks, fallback pinned versions, failed npm lookups, and migration decisions in task-log.md and session-handoff.md.
|
|
176
|
-
|
|
177
|
-
## Task Routing
|
|
178
|
-
- Feature/API work: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.
|
|
179
|
-
- UI/design work: design-system.md, feature-contracts.md, skills/ui-consistency.md.
|
|
180
|
-
- Debugging: task-log.md, current-state.md, skills/debugging.md, related feature contract.
|
|
181
|
-
- Refactoring: architecture.md, decisions.md, guardrails.md, skills/refactoring.md.
|
|
182
|
-
- Release/deploy: release-checklist.md, testing-strategy.md, current-state.md, decisions.md.
|
|
183
|
-
- Migration: AX_MIGRATION_GUIDE.md, context-routing.md, writeback-policy.md.
|
|
184
|
-
- New install: AX_NEW_PROJECT_GUIDE.md and actual project config/source files.
|
|
185
|
-
- Skill/library work: AX_SKILL_LIBRARY_GUIDE.md and ai-verified-skill-publisher when installed.
|
|
186
|
-
- Harness debug: debug-guide.md, language-policy.md, context-routing.md, writeback-policy.md, progress-tracker.md.
|
|
187
|
-
|
|
188
|
-
## Writeback Rules
|
|
189
|
-
- Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.
|
|
190
|
-
- Update plan.md and progress-tracker.md when user scope, milestones, task status, exclusions, or planned work changes.
|
|
191
|
-
- Update guideline.md when execution standards, quality gates, or plan-following rules change.
|
|
192
|
-
- Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.
|
|
193
|
-
- Update feature-contracts.md when input/output/state/error behavior changes.
|
|
194
|
-
- Update design-system.md when UI rules, components, layout, spacing, or states change.
|
|
195
|
-
- Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.
|
|
196
|
-
- Update context-map.md when important files, modules, routes, commands, or ownership areas change.
|
|
197
|
-
- Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.
|
|
198
|
-
|
|
199
|
-
## Non-Destructive Migration Policy
|
|
200
|
-
- Never overwrite existing project memory files unless the user explicitly requests --force.
|
|
201
|
-
- Preserve .env.example and .gitignore; append missing Leerness entries only.
|
|
202
|
-
- Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
## Plan Rule
|
|
206
|
-
- Use .harness/plan.md as the source of truth for the user's goal, project scope, milestones, included work, excluded work, and plan changes.
|
|
207
|
-
- Before starting feature/release/refactor/migration work, check whether the request maps to an existing plan item.
|
|
208
|
-
- If the user adds scope, update plan.md and progress-tracker.md.
|
|
209
|
-
- If the user excludes scope, mark it under Dropped / Out of Scope instead of deleting history.
|
|
210
|
-
- 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.
|
|
211
|
-
|
|
212
|
-
## Guideline Rule
|
|
213
|
-
- Use .harness/guideline.md for execution standards.
|
|
214
|
-
- guideline.md must reference plan.md and progress-tracker.md, but should not become the primary progress database.
|
|
215
|
-
- Progress is tracked in plan.md milestones and progress-tracker.md task rows.
|
|
216
|
-
|
|
217
|
-
## Progress Tracker Rule
|
|
218
|
-
- Track user-requested work in .harness/progress-tracker.md.
|
|
219
|
-
- Use statuses: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped.
|
|
220
|
-
- If the user drops a task, mark it as dropped with reason instead of deleting the history.
|
|
221
|
-
- Every session close must list active unresolved work: requested, planned, waiting, on-hold, blocked, in-progress, incomplete.
|
|
222
|
-
|
|
223
|
-
## End-of-Session Contract
|
|
224
|
-
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.
|
|
225
|
-
|
|
226
|
-
At the end of each session, list:
|
|
227
|
-
1. Completed work in this session.
|
|
228
|
-
2. User-requested work still in progress.
|
|
229
|
-
3. User-requested work not started or incomplete.
|
|
230
|
-
4. Planned, on-hold, waiting, blocked, requested, and incomplete work from progress-tracker.md.
|
|
231
|
-
5. User-dropped work, if any.
|
|
232
|
-
6. Verification performed and results.
|
|
233
|
-
7. Memory files updated.
|
|
234
|
-
8. Risks, assumptions, or blockers.
|
|
235
|
-
9. Recommended next directions.
|
|
236
|
-
10. The single next exact action.
|
|
237
|
-
|
|
238
|
-
## Anti-Lazy Work Rule
|
|
239
|
-
- Do not hide unfinished work behind vague summaries.
|
|
240
|
-
- Do not claim completion without verification or explicit limits.
|
|
241
|
-
- If partial, say exactly what is partial and what remains.
|
|
242
|
-
- Prefer concrete file names, commands, and checks over generic phrases.
|
|
243
|
-
- If tests or verification were skipped, state why and what should be run next.
|
|
244
|
-
|
|
245
|
-
## Response Contract
|
|
246
|
-
- Task type and files consulted
|
|
247
|
-
- Summary
|
|
248
|
-
- Completed work
|
|
249
|
-
- In-progress work
|
|
250
|
-
- Incomplete requested work
|
|
251
|
-
- Files changed
|
|
252
|
-
- Verification
|
|
253
|
-
- Memory files updated
|
|
254
|
-
- Risks or assumptions
|
|
255
|
-
- Recommended next directions
|
|
256
|
-
- Next exact step
|
|
257
|
-
{{LEGACY_AGENT}}
|
|
258
|
-
`,
|
|
259
|
-
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nUse AGENTS.md as the source of truth. Route every task through .harness/context-routing.md and .harness/task-type-map.md. Do not overwrite existing project memory during migration unless --force is explicit.\n`,
|
|
260
|
-
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nRead AGENTS.md first. Follow .harness/context-routing.md, writeback-policy.md, installed skills, design-system, feature-contracts, and guardrails.\n`,
|
|
261
|
-
'.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`,
|
|
262
|
-
'.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`,
|
|
263
|
-
'.env.example': `# Leerness examples only. Copy to .env.local and fill locally. Never commit real secrets.\n`,
|
|
264
|
-
'.harness/LANGUAGE': '{{LANGUAGE}}\n',
|
|
265
|
-
'.harness/language-policy.md': '{{LANGUAGE_POLICY}}',
|
|
266
|
-
'.harness/leerness-maintenance.md': `${MARK}
|
|
267
|
-
---
|
|
268
|
-
leernessRole: leerness-maintenance
|
|
269
|
-
readWhen: [new-session, long-running-work, migration, release, debug, harness-related-work]
|
|
270
|
-
updateWhen: [version-check, migration-decision, npm-lookup-failure, pinned-version-fallback, leerness-upgrade]
|
|
271
|
-
doNotStore: [npm-token, github-token, credentials, cookies, secrets]
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
# Leerness Maintenance Policy
|
|
275
|
-
|
|
276
|
-
## Goal
|
|
277
|
-
|
|
278
|
-
Keep the project on an appropriate Leerness version so AI agents use the latest routing, migration, planning, and session-close rules.
|
|
279
|
-
|
|
280
|
-
## Required Check
|
|
281
|
-
|
|
282
|
-
Before major work, migration, release, or harness debugging:
|
|
283
|
-
|
|
284
|
-
1. Check the project version from .harness/HARNESS_VERSION.
|
|
285
|
-
2. Check the currently executable CLI with leerness --version when available.
|
|
286
|
-
3. Check npm registry with npm view leerness version.
|
|
287
|
-
4. If npm is unavailable, use the latest pinned version recorded in this project and record the failed lookup.
|
|
288
|
-
5. If a newer version exists, run dry-run migration first.
|
|
289
|
-
|
|
290
|
-
## Safe Migration Flow
|
|
291
|
-
|
|
292
|
-
Commands:
|
|
293
|
-
|
|
294
|
-
npm view leerness version
|
|
295
|
-
leerness --version
|
|
296
|
-
npx --yes leerness@<latest> migrate . --dry-run
|
|
297
|
-
npx --yes leerness@<latest> migrate .
|
|
298
|
-
npx --yes leerness@<latest> verify .
|
|
299
|
-
npx --yes leerness@<latest> debug .
|
|
300
|
-
|
|
301
|
-
## Non-Destructive Rule
|
|
302
|
-
|
|
303
|
-
- Do not overwrite project memory files unless the user explicitly requests it.
|
|
304
|
-
- Do not replace .env.example or .gitignore; merge only missing Leerness entries.
|
|
305
|
-
- Record unresolved risks in session-handoff.md.
|
|
306
|
-
|
|
307
|
-
## Writeback
|
|
308
|
-
|
|
309
|
-
After checking or migrating Leerness, update:
|
|
310
|
-
|
|
311
|
-
- .harness/task-log.md
|
|
312
|
-
- .harness/current-state.md when it changes the next work state
|
|
313
|
-
- .harness/session-handoff.md
|
|
314
|
-
`,
|
|
315
|
-
'.harness/HARNESS_VERSION': '{{VERSION}}\n',
|
|
316
|
-
'.harness/manifest.json': '{{MANIFEST}}\n',
|
|
317
|
-
'.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
|
|
318
|
-
'.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`,
|
|
319
|
-
'.harness/plan.md': `${MARK}
|
|
320
|
-
---
|
|
321
|
-
leernessRole: plan
|
|
322
|
-
readWhen: [planning, every-feature, scope-change, new-project, resume-work, session-close]
|
|
323
|
-
updateWhen: [new-user-request, scope-added, scope-dropped, milestone-change, progress-change, plan-reprioritized]
|
|
324
|
-
doNotStore: [secrets, tokens, credentials, raw-private-data]
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
# Project Plan
|
|
328
|
-
|
|
329
|
-
Updated: {{DATE}}
|
|
330
|
-
|
|
331
|
-
## Project Goal
|
|
332
|
-
- Define the user's requested purpose here.
|
|
333
|
-
|
|
334
|
-
## Scope
|
|
335
|
-
- Included work should be listed here.
|
|
336
|
-
|
|
337
|
-
## Out of Scope / Dropped
|
|
338
|
-
| Item | Reason | Dropped At |
|
|
339
|
-
|---|---|---|
|
|
340
|
-
|
|
341
|
-
## Milestones
|
|
342
|
-
| ID | Milestone | Status | Progress | Related Tasks | Notes |
|
|
343
|
-
|---|---|---|---:|---|---|
|
|
344
|
-
| M-0001 | Establish Leerness plan | planned | 0% | T-0001 | Replace with project-specific milestones. |
|
|
345
|
-
|
|
346
|
-
## Plan Change Log
|
|
347
|
-
| Date | Change | Reason | Requested By |
|
|
348
|
-
|---|---|---|---|
|
|
349
|
-
| {{DATE}} | Plan file created | Leerness v{{VERSION}} installation or migration | Leerness |
|
|
350
|
-
|
|
351
|
-
## Rules
|
|
352
|
-
- Add new user-requested scope before implementation when it changes the overall plan.
|
|
353
|
-
- Do not silently implement dropped or out-of-scope work.
|
|
354
|
-
- Link plan milestones to progress-tracker task IDs when possible.
|
|
355
|
-
- Keep this file high-level; detailed task status belongs in progress-tracker.md.
|
|
356
|
-
{{LEGACY_PLAN}}
|
|
357
|
-
`,
|
|
358
|
-
'.harness/guideline.md': `${MARK}
|
|
359
|
-
---
|
|
360
|
-
leernessRole: guideline
|
|
361
|
-
readWhen: [every-task, planning, implementation, review, release]
|
|
362
|
-
updateWhen: [standard-change, quality-gate-change, plan-following-rule-change, repeated-process-failure]
|
|
363
|
-
doNotStore: [secrets, tokens, credentials, raw-private-data]
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
# Project Guideline
|
|
367
|
-
|
|
368
|
-
## Source of Direction
|
|
369
|
-
- Primary plan: .harness/plan.md
|
|
370
|
-
- Task status and progress: .harness/progress-tracker.md
|
|
371
|
-
- Current handoff state: .harness/current-state.md and .harness/session-handoff.md
|
|
372
|
-
|
|
373
|
-
## How to Use the Plan
|
|
374
|
-
1. Check whether the user request exists in plan.md.
|
|
375
|
-
2. If it is new scope, add it to plan.md and progress-tracker.md before broad implementation.
|
|
376
|
-
3. If it is excluded or dropped, do not implement it unless the user reopens it.
|
|
377
|
-
4. Update progress after implementation or verification changes.
|
|
378
|
-
|
|
379
|
-
## Progress Policy
|
|
380
|
-
- guideline.md describes how progress should be handled.
|
|
381
|
-
- plan.md and progress-tracker.md store actual progress values.
|
|
382
|
-
- Do not duplicate detailed task tables here.
|
|
383
|
-
|
|
384
|
-
## Quality Gates
|
|
385
|
-
- Follow context-routing.md before editing.
|
|
386
|
-
- Follow writeback-policy.md after editing.
|
|
387
|
-
- Verify with project-specific commands before marking work done.
|
|
388
|
-
- Never mark work done if requested scope remains incomplete.
|
|
389
|
-
`,
|
|
390
|
-
'.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`,
|
|
391
|
-
'.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`,
|
|
392
|
-
'.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`,
|
|
393
|
-
'.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`,
|
|
394
|
-
'.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`,
|
|
395
|
-
'.harness/task-log.md': `${MARK}\n---\nleernessRole: task-log\nreadWhen: [debugging, audit, handoff, regression]\nupdateWhen: [after-meaningful-work, failed-attempt, verification-result]\ndoNotStore: [secrets, tokens]\n---\n\n# Task Log\n\n## {{DATE}}\n- Leerness v{{VERSION}} installed or migrated.\n`,
|
|
396
|
-
'.harness/constraints.md': `${MARK}\n# Constraints\n\n- Runtime/framework/deployment constraints\n- Security/privacy/business constraints\n`,
|
|
397
|
-
'.harness/guardrails.md': `${MARK}\n---\nleernessRole: guardrails\nreadWhen: [every-task, security, integration, refactor]\nupdateWhen: [new-risk, repeated-error, policy-change]\ndoNotStore: [secrets, tokens, private-data]\n---\n\n# Guardrails\n\n## Never\n- Store real secrets in code or harness files.\n- Overwrite existing project memory during migration without --force.\n- Change API responses, DB schema, env names, or auth flow without impact review.\n- Perform broad refactoring unless requested.\n\n## Always\n- Preserve architecture and contracts.\n- Record decisions and update writeback files.\n`,
|
|
398
|
-
'.harness/design-system.md': `${MARK}\n---\nleernessRole: design-system\nreadWhen: [ui, layout, component, style, visual-consistency]\nupdateWhen: [new-component-pattern, style-rule-change, state-pattern-change]\ndoNotStore: [secrets, user-private-data]\n---\n\n# Design System Memory\n\n## Layout\n\n## Components\n\n## States\n- Loading\n- Empty\n- Error\n- Success\n`,
|
|
399
|
-
'.harness/feature-contracts.md': `${MARK}\n---\nleernessRole: feature-contracts\nreadWhen: [feature, api, ui-state, debugging, refactor]\nupdateWhen: [input-change, output-change, state-change, error-change, contract-change]\ndoNotStore: [secrets, raw-private-data]\n---\n\n# Feature Contracts\n\n## Template\n- Feature:\n- Entry point:\n- Input:\n- Output:\n- UI states:\n- Error states:\n- Related files:\n- Tests:\n`,
|
|
400
|
-
'.harness/testing-strategy.md': `${MARK}\n---\nleernessRole: testing-strategy\nreadWhen: [feature, debugging, refactor, release]\nupdateWhen: [test-command-change, new-critical-flow, regression-added]\ndoNotStore: [secrets]\n---\n\n# Testing Strategy\n\n## Commands\n\n## Critical Flows\n\n## Regression Notes\n`,
|
|
401
|
-
'.harness/review-checklist.md': `${MARK}\n# Review Checklist\n\n- [ ] Architecture preserved\n- [ ] Feature contract preserved or updated\n- [ ] Design system followed\n- [ ] No secrets stored\n- [ ] Writeback files updated\n`,
|
|
402
|
-
'.harness/release-checklist.md': `${MARK}\n---\nleernessRole: release-checklist\nreadWhen: [release, deploy, ci, npm-publish, git-push, env-change]\nupdateWhen: [deploy-failure, new-env-var, ci-change, rollback-change, release-rule-change]\ndoNotStore: [secrets, tokens, passwords, cookies]\n---\n\n# Release Checklist\n\n## Commands\n\n## Required Environment Variables\n\n## Verification\n\n## Rollback\n`,
|
|
403
|
-
'.harness/session-handoff.md': `${MARK}
|
|
404
|
-
---
|
|
405
|
-
leernessRole: session-handoff
|
|
406
|
-
readWhen: [resume-work, every-new-session, end-of-session]
|
|
407
|
-
updateWhen: [end-of-session, handoff, blocked-work, partial-completion]
|
|
408
|
-
doNotStore: [secrets, tokens, raw-private-data]
|
|
409
|
-
---
|
|
410
|
-
|
|
411
|
-
# Session Handoff
|
|
412
|
-
|
|
413
|
-
## Session Summary
|
|
414
|
-
- Date:
|
|
415
|
-
- Task type:
|
|
416
|
-
- User request:
|
|
417
|
-
|
|
418
|
-
## Completed This Session
|
|
419
|
-
-
|
|
420
|
-
|
|
421
|
-
## In Progress From User Requests
|
|
422
|
-
-
|
|
423
|
-
|
|
424
|
-
## Incomplete / Not Started From User Requests
|
|
425
|
-
-
|
|
426
|
-
|
|
427
|
-
## Files Changed
|
|
428
|
-
-
|
|
429
|
-
|
|
430
|
-
## Verification Performed
|
|
431
|
-
- Command/check:
|
|
432
|
-
- Result:
|
|
433
|
-
|
|
434
|
-
## Memory Files Updated
|
|
435
|
-
-
|
|
436
|
-
|
|
437
|
-
## Risks / Assumptions / Blockers
|
|
438
|
-
-
|
|
439
|
-
|
|
440
|
-
## Recommended Next Directions
|
|
441
|
-
-
|
|
442
|
-
|
|
443
|
-
## Next Exact Step
|
|
444
|
-
-
|
|
445
|
-
`,
|
|
446
|
-
'.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`,
|
|
447
|
-
'.harness/progress-tracker.md': `${MARK}
|
|
448
|
-
---
|
|
449
|
-
leernessRole: progress-tracker
|
|
450
|
-
readWhen: [planning, resume-work, end-of-session, multi-step-work]
|
|
451
|
-
updateWhen: [task-started, task-completed, task-blocked, task-dropped, scope-change, end-of-session]
|
|
452
|
-
doNotStore: [secrets, tokens, credentials, raw-private-data]
|
|
453
|
-
---
|
|
454
|
-
|
|
455
|
-
# Progress Tracker
|
|
456
|
-
|
|
457
|
-
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.
|
|
458
|
-
|
|
459
|
-
| ID | User Request | Status | Owner | Last Update | Evidence / Notes | Next Action |
|
|
460
|
-
|---|---|---|---|---|---|---|
|
|
461
|
-
| T-0001 | Initialize Leerness project memory | done | AI | {{DATE}} | Leerness v{{VERSION}} installed or migrated. | Fill project-specific details. |
|
|
462
|
-
|
|
463
|
-
## Status Values
|
|
464
|
-
|
|
465
|
-
- requested
|
|
466
|
-
- planned
|
|
467
|
-
- in-progress
|
|
468
|
-
- waiting
|
|
469
|
-
- on-hold
|
|
470
|
-
- blocked
|
|
471
|
-
- incomplete
|
|
472
|
-
- done
|
|
473
|
-
- dropped
|
|
474
|
-
|
|
475
|
-
## Drop Policy
|
|
476
|
-
|
|
477
|
-
Dropped tasks are not deleted. Mark Status as dropped and write the reason in Evidence / Notes.
|
|
478
|
-
|
|
479
|
-
## Session Close Rule
|
|
480
|
-
|
|
481
|
-
Every session-close report must list all tasks whose Status is planned, waiting, on-hold, blocked, in-progress, incomplete, or requested.
|
|
482
|
-
`,
|
|
483
|
-
'.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`,
|
|
484
|
-
'.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`,
|
|
485
|
-
'.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.
|
|
486
|
-
- guideline.md references plan.md and progress-tracker.md.
|
|
487
|
-
- 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`,
|
|
488
|
-
'.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`,
|
|
489
|
-
'.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`,
|
|
490
|
-
'.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`,
|
|
491
|
-
'.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 |
|
|
492
|
-
| 새 기능 | 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`,
|
|
493
|
-
'.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`,
|
|
494
|
-
'.harness/AX_PLAN_GUIDE.md': `${MARK}
|
|
495
|
-
# AX Plan Guide
|
|
496
|
-
|
|
497
|
-
Use this guide when creating, updating, dropping, or syncing the project plan.
|
|
498
|
-
|
|
499
|
-
## Purpose
|
|
500
|
-
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.
|
|
501
|
-
|
|
502
|
-
## When a user asks for new work
|
|
503
|
-
1. Check .harness/plan.md.
|
|
504
|
-
2. Decide whether the request is an existing milestone, a subtask, new scope, or out of scope.
|
|
505
|
-
3. If new scope, add a milestone or task reference.
|
|
506
|
-
4. Add or update the matching row in progress-tracker.md.
|
|
507
|
-
5. Route implementation using context-routing.md.
|
|
508
|
-
|
|
509
|
-
## When a user drops work
|
|
510
|
-
1. Do not delete history.
|
|
511
|
-
2. Mark the task dropped in progress-tracker.md.
|
|
512
|
-
3. Add it to plan.md Out of Scope / Dropped with the reason.
|
|
513
|
-
4. Mention the drop in session-handoff.md.
|
|
514
|
-
|
|
515
|
-
## New project detection
|
|
516
|
-
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.
|
|
517
|
-
|
|
518
|
-
## Sync rule
|
|
519
|
-
Run or follow: leerness plan sync
|
|
520
|
-
- plan.md: milestones and scope
|
|
521
|
-
- progress-tracker.md: concrete task statuses
|
|
522
|
-
- guideline.md: execution standards and progress policy
|
|
523
|
-
- current-state.md: immediate next work
|
|
524
|
-
`,
|
|
525
|
-
'.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
|
|
526
|
-
- 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`,
|
|
527
|
-
'.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`,
|
|
528
|
-
'.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`,
|
|
529
|
-
'.harness/skills/feature-implementation.md': `${MARK}\n# Skill: Feature Implementation\n\nDefine contract, inspect existing patterns, implement minimal change, verify, update memory.\n`,
|
|
530
|
-
'.harness/skills/refactoring.md': `${MARK}\n# Skill: Refactoring\n\nPreserve behavior and contracts. Record important decisions.\n`,
|
|
531
|
-
'.harness/skills/debugging.md': `${MARK}\n# Skill: Debugging\n\nReproduce, isolate cause, patch minimally, verify, add regression note.\n`,
|
|
532
|
-
'.harness/skills/ui-consistency.md': `${MARK}\n# Skill: UI Consistency\n\nRead design-system and existing adjacent screens before styling.\n`,
|
|
533
|
-
'.harness/skills/security-review.md': `${MARK}\n# Skill: Security Review\n\nCheck secrets, auth, permissions, logging, and sensitive data exposure.\n`,
|
|
534
|
-
'.harness/skills/release-check.md': `${MARK}\n# Skill: Release Check\n\nCheck tests, build, env vars, migration, rollback, publish token gate.\n`,
|
|
535
|
-
'.harness/skills/documentation-update.md': `${MARK}\n# Skill: Documentation Update\n\nFollow writeback-policy and update the specific memory file.\n`,
|
|
536
|
-
'.harness/templates/session-summary.md': `${MARK}\n# Session Summary\n\n## Done\n\n## Files Changed\n\n## Verification\n\n## Next\n`,
|
|
537
|
-
'.harness/templates/decision.md': `${MARK}\n# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n`
|
|
538
64
|
};
|
|
539
65
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
66
|
+
const routes = {
|
|
67
|
+
planning: {
|
|
68
|
+
read: ['.harness/plan.md', '.harness/progress-tracker.md', '.harness/project-brief.md', '.harness/current-state.md', '.harness/guideline.md'],
|
|
69
|
+
update: ['.harness/plan.md', '.harness/progress-tracker.md', '.harness/current-state.md', '.harness/session-handoff.md']
|
|
70
|
+
},
|
|
71
|
+
feature: {
|
|
72
|
+
read: ['.harness/plan.md', '.harness/current-state.md', '.harness/architecture.md', '.harness/context-map.md', '.harness/feature-contracts.md', '.harness/skills/feature-implementation.md', '.harness/reuse-map.md'],
|
|
73
|
+
update: ['.harness/progress-tracker.md', '.harness/feature-contracts.md', '.harness/current-state.md', '.harness/task-log.md', '.harness/session-handoff.md']
|
|
74
|
+
},
|
|
75
|
+
consistency: {
|
|
76
|
+
read: ['.harness/design-system.md', '.harness/consistency-policy.md', '.harness/reuse-map.md', '.harness/context-map.md'],
|
|
77
|
+
update: ['.harness/design-system.md', '.harness/reuse-map.md', '.harness/task-log.md', '.harness/session-handoff.md']
|
|
78
|
+
},
|
|
79
|
+
release: {
|
|
80
|
+
read: ['.harness/plan.md', '.harness/release-checklist.md', '.harness/testing-strategy.md', '.harness/current-state.md', '.harness/leerness-maintenance.md'],
|
|
81
|
+
update: ['.harness/release-checklist.md', '.harness/progress-tracker.md', '.harness/task-log.md', '.harness/session-handoff.md']
|
|
82
|
+
},
|
|
83
|
+
migration: {
|
|
84
|
+
read: ['.harness/AX_MIGRATION_GUIDE.md', '.harness/protected-files.md', '.harness/context-routing.md', '.harness/writeback-policy.md'],
|
|
85
|
+
update: ['.harness/current-state.md', '.harness/task-log.md', '.harness/session-handoff.md']
|
|
86
|
+
},
|
|
87
|
+
'session-close': {
|
|
88
|
+
read: ['.harness/session-close-policy.md', '.harness/progress-tracker.md', '.harness/anti-lazy-work-policy.md', '.harness/plan.md'],
|
|
89
|
+
update: ['.harness/session-handoff.md', '.harness/progress-tracker.md', '.harness/current-state.md', '.harness/task-log.md']
|
|
90
|
+
},
|
|
91
|
+
'harness-maintenance': {
|
|
92
|
+
read: ['.harness/leerness-maintenance.md', '.harness/HARNESS_VERSION', '.harness/protected-files.md'],
|
|
93
|
+
update: ['.harness/task-log.md', '.harness/session-handoff.md']
|
|
565
94
|
}
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
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, leernessMaintenanceEnabled:true},null,2); }
|
|
569
|
-
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); }
|
|
570
|
-
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) }; }
|
|
571
|
-
|
|
572
|
-
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)); }
|
|
573
|
-
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; }
|
|
574
|
-
function updateSkillLock(root,meta,remove=false){ const lp=path.join(root,'.harness/skills-lock.json'); const lock=exists(lp)?parseJsonSafe(read(lp),{harnessVersion:VERSION,installedSkills:{}}):{harnessVersion:VERSION,installedSkills:{}}; lock.harnessVersion=VERSION; lock.updatedAt=now(); lock.installedSkills=lock.installedSkills||{}; if(remove) delete lock.installedSkills[meta.name]; else lock.installedSkills[meta.name]={version:meta.version,source:meta.source||'bundled',title:meta.title,displayNameKo:meta.displayNameKo||meta.title,categoryKo:meta.categoryKo||meta.category,capabilities:meta.capabilities||[],requiresEnv:meta.requiresEnv||[],lastUpdated:meta.lastUpdated,lastUpdatedAt:meta.lastUpdatedAt,verificationStatus:(meta.verification||{}).status||'unknown'}; write(lp,JSON.stringify(lock,null,2)+'\n'); }
|
|
575
|
-
function appendEnvExample(root,meta){ const ep=path.join(root,'.env.example'); const existing=exists(ep)?read(ep):''; const missing=(meta.requiresEnv||[]).filter(n=>!existing.includes(n+'=')); if(!missing.length) return; write(ep,existing.replace(/\s*$/,'\n')+'\n# '+(meta.title||meta.name)+' ('+meta.name+')\n'+missing.map(n=>n+'=').join('\n')+'\n'); }
|
|
576
|
-
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; }
|
|
577
|
-
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); }
|
|
578
|
-
|
|
579
|
-
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; }
|
|
580
|
-
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); }
|
|
581
|
-
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());})); }
|
|
582
|
-
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); }
|
|
583
|
-
|
|
584
|
-
async function init(root,flags){
|
|
585
|
-
root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root);
|
|
586
|
-
const selectedLanguage=await chooseLanguage(root,flags); info('문서 언어: '+languageName(selectedLanguage));
|
|
587
|
-
const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills);
|
|
588
|
-
const found=detectLegacy(root), legacyText=collectLegacyText(found);
|
|
589
|
-
if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
|
|
590
|
-
const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive));
|
|
591
|
-
noteLegacyPreserved(root,found,false);
|
|
592
|
-
const ctx=makeContext(root,legacyText,selectedSkills,selectedLanguage);
|
|
593
|
-
for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{force:Boolean(flags.force)});
|
|
594
|
-
if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); }
|
|
595
|
-
ok('설치 완료'); info('신규 프로젝트라면 .harness/AX_NEW_PROJECT_GUIDE.md를 따라 실제 프로젝트 내용을 반영하세요.');
|
|
596
|
-
}
|
|
597
|
-
function migrate(root,flags){
|
|
598
|
-
root=path.resolve(root||process.cwd()); banner(); installGuide(); const dryRun=Boolean(flags['dry-run']); const force=Boolean(flags.force);
|
|
599
|
-
const found=detectLegacy(root);
|
|
600
|
-
if(!found.length) ok('마이그레이션할 legacy 항목이 없습니다. 누락/라우팅 파일만 점검합니다.');
|
|
601
|
-
else { warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
|
|
602
|
-
const archive=archiveLegacy(root,found,dryRun); if(archive) info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive));
|
|
603
|
-
noteLegacyPreserved(root,found,dryRun);
|
|
604
|
-
const selectedLanguage=normalizeLanguage(flags.language||flags.lang||readConfiguredLanguage(root)||detectLanguage(root)); info('문서 언어: '+languageName(selectedLanguage));
|
|
605
|
-
const ctx=makeContext(root,collectLegacyText(found),[],selectedLanguage);
|
|
606
|
-
for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{dryRun,force});
|
|
607
|
-
if(!dryRun){ ok('마이그레이션 완료'); info('기존 프로젝트 메모리 파일은 보존되었습니다. 템플릿 재생성이 필요하면 --force를 명시하세요.'); }
|
|
608
|
-
}
|
|
609
|
-
function status(root){ root=path.resolve(root||process.cwd()); const vf=path.join(root,'.harness/HARNESS_VERSION'); const version=exists(vf)?read(vf).trim():'not installed'; const missing=Object.keys(coreFiles).filter(f=>!exists(path.join(root,f))); const lp=path.join(root,'.harness/skills-lock.json'); const lock=exists(lp)?parseJsonSafe(read(lp),{installedSkills:{}}):{installedSkills:{}}; banner(); log('대상: '+root); log('버전: '+version); log('파일: '+(Object.keys(coreFiles).length-missing.length)+'/'+Object.keys(coreFiles).length); if(missing.length){ warn('누락 파일'); missing.forEach(x=>log(' - '+x)); } else ok('필수 파일 모두 존재'); const names=Object.keys(lock.installedSkills||{}); log('설치 스킬: '+(names.length?names.join(', '):'없음')); }
|
|
610
|
-
function scanSensitivePath(root){ const out=[]; if(!exists(root)) return out; const patterns=[{type:'npm token',re:/npm_[A-Za-z0-9]{20,}/},{type:'github token',re:/gh[pousr]_[A-Za-z0-9_]{20,}/},{type:'private key',re:/-----BEGIN [A-Z ]*PRIVATE KEY-----/},{type:'password assignment',re:/(password|secret|token|api[_-]?key)\s*[:=]\s*['\"][^'\"]{8,}/i}]; function walk(p){ const st=fs.statSync(p); if(st.isDirectory()){ const b=path.basename(p); if(['node_modules','.git','archive','dist'].includes(b)) return; for(const n of fs.readdirSync(p)) walk(path.join(p,n)); } else if(st.isFile()&&isTextFile(p)){ const body=read(p); for(const pat of patterns){ const m=body.match(pat.re); if(m) out.push({file:p,type:pat.type,sample:m[0].slice(0,60)}); } } } try{ walk(root); }catch{} return out; }
|
|
611
|
-
function verify(root){ root=path.resolve(root||process.cwd()); let failures=0; banner(); for(const file of Object.keys(coreFiles)){ const target=path.join(root,file); if(!exists(target)){ failures++; warn('누락: '+file); continue; } const body=read(target); if(/{{[A-Z_]+}}/.test(body)){ failures++; warn('플레이스홀더 남음: '+file); } } const suspicious=[]; for(const x of ['.harness','AGENTS.md','CLAUDE.md']) for(const f of scanSensitivePath(path.join(root,x))) suspicious.push(f); if(suspicious.length){ failures+=suspicious.length; suspicious.forEach(x=>warn('민감정보 의심: '+rel(root,x.file)+' · '+x.type)); } if(failures){ fail('검증 실패: '+failures); process.exitCode=1; } else ok('검증 완료'); }
|
|
612
|
-
|
|
613
|
-
function skillDisplayName(meta){ return meta.displayNameKo || meta.titleKo || meta.title || meta.name; }
|
|
614
|
-
function verificationLabel(meta){ const v=(meta||{}).verification||{}; return v.status ? (v.status+(v.verifiedAt?' '+String(v.verifiedAt).slice(0,10):'')) : 'unknown'; }
|
|
615
|
-
function renderSkillMeta(meta){ log('- '+meta.name+'@'+meta.version+' · '+skillDisplayName(meta)); if(meta.categoryKo||meta.category) log(' 분류: '+(meta.categoryKo||meta.category)); if(meta.description) log(' 설명: '+meta.description); if(Array.isArray(meta.capabilities)&&meta.capabilities.length){ log(' 가능한 작업:'); meta.capabilities.forEach(x=>log(' - '+x)); } log(' 업데이트: '+(meta.lastUpdated||'unknown')+' · 검증: '+verificationLabel(meta)); if((meta.requiresEnv||[]).length) log(' 필요한 환경변수: '+meta.requiresEnv.join(', ')); }
|
|
616
|
-
function skillCommand(args,flags){ const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd()); if(sub==='list'){ banner(); log('사용 가능한 스킬 라이브러리'); for(const p of listSkillPacks()) renderSkillMeta(p); return; } if(sub==='info'||sub==='show'){ const name=args[2]; if(!name) return fail('스킬 이름이 필요합니다.'); const meta=getSkillMeta(name); if(!meta) return fail('알 수 없는 스킬: '+name); banner(); renderSkillMeta(meta); const rp=path.join(PACKS_DIR,name,'README.md'); if(exists(rp)){ log('\n--- README ---\n'); log(read(rp)); } return; } const name=args[2]; if(!name) return fail('스킬 이름이 필요합니다.'); if(sub==='add'||sub==='install'||sub==='update') return installSkill(root,name,Boolean(flags['dry-run'])); if(sub==='remove'||sub==='rm') return removeSkill(root,name); if(sub==='learn') return learnSkillLibrary(root,{...flags,name}); fail('알 수 없는 skill 명령: '+sub); }
|
|
617
|
-
|
|
618
|
-
function skillLibraryFiles(dir){ const out=[]; if(!exists(dir)) return out; function walk(p){ const st=fs.statSync(p); if(st.isDirectory()){ const b=path.basename(p); if(['node_modules','.git','dist'].includes(b)) return; for(const n of fs.readdirSync(p)) walk(path.join(p,n)); } else if(st.isFile()) out.push(p); } walk(dir); return out; }
|
|
619
|
-
function inferEnvNames(body){ const set=new Set(); const re=/\b[A-Z][A-Z0-9_]{3,}\b/g; let m; while((m=re.exec(body))){ const v=m[0]; if(/(KEY|TOKEN|SECRET|PASSWORD|CLIENT|VENDOR|ID|URL|HOST|BUCKET|PROJECT)/.test(v)) set.add(v); } return Array.from(set).sort(); }
|
|
620
|
-
function readSkillLibraryMeta(dir){ for(const cnd of [path.join(dir,'skill-library.json'),path.join(dir,'skill.json'),path.join(dir,'package.json')]){ if(!exists(cnd)) continue; const data=parseJsonSafe(read(cnd),null); if(!data) continue; if(path.basename(cnd)==='package.json') return {name:data.harnessSkill?.name||data.name,version:data.version||'0.1.0',title:data.harnessSkill?.title||data.description||data.name,description:data.description||'',requiresEnv:data.harnessSkill?.requiresEnv||[],verification:data.harnessSkill?.verification,lastUpdated:data.harnessSkill?.lastUpdated,lastUpdatedAt:data.harnessSkill?.lastUpdatedAt}; return data; } return null; }
|
|
621
|
-
function validateSkillLibrary(dir,opts={}){ dir=path.resolve(dir); let failures=0; const meta=readSkillLibraryMeta(dir); if(!meta||!meta.name){ failures++; fail('skill-library.json 또는 skill.json에 name이 필요합니다.'); } const sd=path.join(dir,'skills'); if(!exists(sd)||!skillLibraryFiles(sd).some(f=>f.endsWith('.md'))){ failures++; fail('skills/*.md 파일이 필요합니다.'); } const findings=scanSensitivePath(dir); if(findings.length){ failures+=findings.length; fail('민감정보 의심 패턴 감지.'); findings.slice(0,10).forEach(f=>warn(rel(dir,f.file)+' · '+f.type)); } if(opts.strictAi){ const v=(meta||{}).verification||{}; if(!(v.status==='passed'&&/ai/i.test(String(v.method||''))&&v.verifiedAt)){ failures++; fail('AI 검증 메타데이터가 필요합니다.'); } } if(!opts.silent){ if(failures) fail('검증 실패: '+failures); else ok('스킬 라이브러리 검증 완료: '+meta.name); } return {ok:failures===0,meta,findings}; }
|
|
622
|
-
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'); }
|
|
623
|
-
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); }
|
|
624
|
-
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); }
|
|
625
|
-
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; }
|
|
626
|
-
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); }
|
|
627
|
-
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']); }
|
|
628
|
-
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); }
|
|
629
|
-
|
|
630
|
-
|
|
95
|
+
};
|
|
631
96
|
|
|
632
|
-
function
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
97
|
+
function log(s = '') { console.log(s); }
|
|
98
|
+
function ok(s) { log('✓ ' + s); }
|
|
99
|
+
function warn(s) { log('⚠ ' + s); }
|
|
100
|
+
function fail(s) { log('✗ ' + s); }
|
|
101
|
+
function absRoot(p) { return path.resolve(p || process.cwd()); }
|
|
102
|
+
function exists(p) { return fs.existsSync(p); }
|
|
103
|
+
function read(p) { return fs.readFileSync(p, 'utf8'); }
|
|
104
|
+
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
|
|
105
|
+
function write(p, s) { mkdirp(path.dirname(p)); fs.writeFileSync(p, s, 'utf8'); }
|
|
106
|
+
function append(p, s) { mkdirp(path.dirname(p)); fs.appendFileSync(p, s, 'utf8'); }
|
|
107
|
+
function rel(root, p) { return path.relative(root, p).replace(/\\/g, '/') || '.'; }
|
|
108
|
+
function today() { return new Date().toISOString().slice(0, 10); }
|
|
109
|
+
function now() { return new Date().toISOString(); }
|
|
110
|
+
function arg(name, def = null) { const i = process.argv.indexOf(name); return i >= 0 ? (process.argv[i + 1] || true) : def; }
|
|
111
|
+
function has(name) { return process.argv.includes(name); }
|
|
112
|
+
function nonFlagArgs() {
|
|
113
|
+
const out = [];
|
|
114
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--version','--from','--repo']);
|
|
115
|
+
const a = process.argv.slice(2);
|
|
116
|
+
for (let i = 0; i < a.length; i++) {
|
|
117
|
+
const x = a[i];
|
|
118
|
+
if (x.startsWith('-')) { if (withValue.has(x) && a[i+1] && !a[i+1].startsWith('-')) i++; continue; }
|
|
119
|
+
out.push(x);
|
|
638
120
|
}
|
|
639
|
-
return
|
|
640
|
-
}
|
|
641
|
-
function
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function detectProjectName(root) { try { const pkg = JSON.parse(read(path.join(root, 'package.json'))); if (pkg.name) return pkg.name; } catch {} return path.basename(root); }
|
|
124
|
+
function detectLanguage(root) {
|
|
125
|
+
const v = String(arg('--language', 'auto')).toLowerCase();
|
|
126
|
+
if (v === 'ko' || v === 'en') return v;
|
|
127
|
+
const candidates = ['README.md', 'docs/guideline.md', '.harness/project-brief.md'];
|
|
128
|
+
let text = '';
|
|
129
|
+
for (const c of candidates) { const p = path.join(root, c); if (exists(p)) text += read(p).slice(0, 2000); }
|
|
130
|
+
return /[가-힣]/.test(text) ? 'ko' : 'en';
|
|
131
|
+
}
|
|
132
|
+
function fm(role, readWhen, updateWhen, body) {
|
|
133
|
+
return `---\nleernessRole: ${role}\nreadWhen:\n${readWhen.map(x => ' - ' + x).join('\n')}\nupdateWhen:\n${updateWhen.map(x => ' - ' + x).join('\n')}\ndoNotStore:\n - 실제 토큰\n - 비밀번호\n - 운영 쿠키\n - 민감한 개인정보 원문\n---\n${MARK}\n${body}`;
|
|
134
|
+
}
|
|
135
|
+
function managedReadmeBlock(project) {
|
|
136
|
+
return [
|
|
137
|
+
README_START,
|
|
138
|
+
'## Leerness Project Harness',
|
|
139
|
+
'',
|
|
140
|
+
'이 프로젝트는 Leerness 하네스를 사용합니다. AI 에이전트는 작업 전 `.harness/context-routing.md`, `.harness/plan.md`, `.harness/progress-tracker.md`, `.harness/guideline.md`를 확인하고 작업 후 관련 메모리를 갱신해야 합니다.',
|
|
141
|
+
'',
|
|
142
|
+
'### Core Commands',
|
|
143
|
+
'',
|
|
144
|
+
'```bash',
|
|
145
|
+
'leerness status .',
|
|
146
|
+
'leerness verify .',
|
|
147
|
+
'leerness debug .',
|
|
148
|
+
'leerness route <task-type>',
|
|
149
|
+
'leerness session close .',
|
|
150
|
+
'```',
|
|
151
|
+
'',
|
|
152
|
+
'### Current Planning Files',
|
|
153
|
+
'',
|
|
154
|
+
'- `.harness/plan.md`: 전체 목표, milestone, 제외/드랍 범위',
|
|
155
|
+
'- `.harness/progress-tracker.md`: 요청 단위 상태와 증거',
|
|
156
|
+
'- `.harness/current-state.md`: 지금 이어서 할 작업',
|
|
157
|
+
'- `.harness/session-handoff.md`: 다음 세션 인수인계',
|
|
158
|
+
'',
|
|
159
|
+
`Last synced by Leerness v${VERSION}: ${today()}`,
|
|
160
|
+
README_END,
|
|
161
|
+
''
|
|
162
|
+
].join('\n');
|
|
163
|
+
}
|
|
164
|
+
function mergeSection(existing, block) {
|
|
165
|
+
if (!existing) return `# Project\n\n${block}`;
|
|
166
|
+
const s = existing.indexOf(README_START); const e = existing.indexOf(README_END);
|
|
167
|
+
if (s >= 0 && e >= s) return existing.slice(0, s).trimEnd() + '\n\n' + block + '\n' + existing.slice(e + README_END.length).trimStart();
|
|
168
|
+
return existing.trimEnd() + '\n\n' + block;
|
|
169
|
+
}
|
|
170
|
+
function skillLock(skills) {
|
|
171
|
+
const data = { leernessVersion: VERSION, updatedAt: now(), installedSkills: {} };
|
|
172
|
+
for (const s of skills) data.installedSkills[s] = skillCatalog[s] || { version: 'unknown' };
|
|
173
|
+
return JSON.stringify(data, null, 2) + '\n';
|
|
174
|
+
}
|
|
175
|
+
function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
176
|
+
const project = detectProjectName(root);
|
|
177
|
+
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
178
|
+
return {
|
|
179
|
+
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## Mandatory read order\n1. .harness/context-routing.md\n2. .harness/plan.md\n3. .harness/progress-tracker.md\n4. .harness/guideline.md\n5. .harness/current-state.md\n6. .harness/protected-files.md\n7. .harness/writeback-policy.md\n\n## Required behavior\n- Use the language defined in .harness/LANGUAGE and .harness/language-policy.md.\n- Classify the task type before editing. Run or consult: leerness route <task-type>.\n- Do not delete .harness/, AGENTS.md, CLAUDE.md, or Leerness managed sections. Archive or merge instead.\n- Update plan/progress/current-state/session-handoff after meaningful work.\n- Keep plan.md and progress-tracker.md separate: plan is roadmap, progress is request-level status.\n- Before creating duplicate UI or API patterns, check design-system.md, consistency-policy.md, and reuse-map.md.\n- End each session with completed/in-progress/incomplete/planned/waiting/on-hold/blocked/dropped lists.\n`,
|
|
180
|
+
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Read .harness/context-routing.md and .harness/protected-files.md before changes. Do not remove Leerness harness files.\n`,
|
|
181
|
+
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .harness/context-routing.md. Preserve Leerness protected files.\n`,
|
|
182
|
+
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory. Do not remove protected Leerness files.\n`,
|
|
183
|
+
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
184
|
+
'.harness/LANGUAGE': lang + '\n',
|
|
185
|
+
'.harness/manifest.json': JSON.stringify({ project, leernessVersion: VERSION, language: lang, installedAt: now() }, null, 2) + '\n',
|
|
186
|
+
'.harness/skills-lock.json': skillLock(selectedSkills),
|
|
187
|
+
'.harness/project-brief.md': fm('project-brief', ['프로젝트 목적 확인', '신규 기능 판단', '계획 수립'], ['프로젝트 목적 변경', '사용자/범위 변경'], `# Project Brief\n\n## Project\n${project}\n\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n\n## Users\n-\n\n## Success Criteria\n-\n`),
|
|
188
|
+
'.harness/plan.md': fm('plan', ['작업 시작 전', '새 요청 접수', '범위 변경', '신규 프로젝트 감지'], ['계획 추가/수정/드랍', 'milestone 변경', '목표 변경'], `# Plan\n\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n\n## Scope\n- 포함 범위를 기록합니다.\n\n## Out of Scope / Dropped\n| ID | Item | Reason | Date |\n|---|---|---|---|\n\n## Milestones\n\n### M-0001. 프로젝트 계획 정리\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] project-brief.md를 실제 프로젝트 목적에 맞게 작성\n- [ ] context-map.md를 실제 파일 구조에 맞게 작성\n`),
|
|
189
|
+
'.harness/progress-tracker.md': fm('progress-tracker', ['세션 시작', '세션 종료', '사용자 요청 상태 확인'], ['작업 상태 변경', '검증 결과 추가', '사용자 요청 드랍'], `# Progress Tracker\n\n| ID | Status | Request | Evidence | Next Action | Updated |\n|---|---|---|---|---|---|\n\nStatus values: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped\n`),
|
|
190
|
+
'.harness/guideline.md': fm('guideline', ['구현 전 품질 기준 확인', '계획 이행 기준 확인'], ['개발 기준 변경', '검증 루틴 변경'], `# Guideline\n\n## Operating Principle\n- plan.md의 목표와 범위를 기준으로 작업합니다.\n- progress-tracker.md의 요청 상태를 기준으로 완료/미완료를 판단합니다.\n- guideline.md에는 진행률 수치를 직접 기록하지 않습니다. 진행률은 plan.md와 progress-tracker.md에서 관리합니다.\n\n## Quality Gate\n- 변경 전 관련 route를 확인합니다.\n- 변경 후 verify/debug/session close를 수행합니다.\n`),
|
|
191
|
+
'.harness/plan-progress-boundary.md': fm('plan-progress-boundary', ['계획과 진행률이 중복될 때', '작업 추적 구조 변경'], ['역할 분리 기준 변경'], `# Plan / Progress Boundary\n\n## plan.md\n- 전체 목표, milestone, 포함/제외 범위, 계획 변경 이력을 기록합니다.\n\n## progress-tracker.md\n- 사용자 요청 단위의 상태, 증거, 다음 액션을 기록합니다.\n\n## guideline.md\n- plan/progress를 수행할 때 지켜야 할 실행 기준을 기록합니다.\n`),
|
|
192
|
+
'.harness/current-state.md': fm('current-state', ['세션 시작', '작업 이어받기'], ['현재 상태 변경', '다음 작업 변경'], `# Current State\n\nUpdated: ${today()}\n\n## Now\n-\n\n## Next\n-\n\n## Blockers\n-\n`),
|
|
193
|
+
'.harness/context-routing.md': fm('context-routing', ['모든 작업 전', '작업 유형 판단'], ['새 작업 유형 추가', '참조 파일 변경'], `# Context Routing\n\n${Object.entries(routes).map(([k, v]) => `## ${k}\nRead:\n${v.read.map(x => '- ' + x).join('\n')}\n\nUpdate:\n${v.update.map(x => '- ' + x).join('\n')}`).join('\n\n')}\n`),
|
|
194
|
+
'.harness/writeback-policy.md': fm('writeback-policy', ['작업 완료 전', '문서 갱신 판단'], ['기록 대상 변경'], `# Writeback Policy\n\n- plan.md: 사용자 목적, milestone, 범위 추가/제외\n- progress-tracker.md: 요청 단위 상태와 증거\n- current-state.md: 현재 상태와 다음 작업\n- task-log.md: 수행 이력\n- session-handoff.md: 다음 세션 인수인계\n- decisions.md: 되돌리기 어려운 결정\n- design-system.md: UI/UX/컴포넌트 기준\n- feature-contracts.md: 입력/출력/상태/오류 계약\n`),
|
|
195
|
+
'.harness/task-type-map.md': fm('task-type-map', ['사용자 요청 분류'], ['작업 유형 추가'], `# Task Type Map\n\n| User Request | Task Type | Route |\n|---|---|---|\n| 계획 세워줘 | planning | leerness route planning |\n| 기능 구현 | feature | leerness route feature |\n| 디자인 통일 | consistency | leerness route consistency |\n| 배포 | release | leerness route release |\n| 마이그레이션 | migration | leerness route migration |\n| 세션 종료 | session-close | leerness route session-close |\n`),
|
|
196
|
+
'.harness/protected-files.md': fm('protected-files', ['파일 삭제/정리/마이그레이션 전'], ['보호 대상 변경'], `# Protected Files\n\nAI agents must not delete or reset these files without explicit user approval.\n\n- .harness/\n- .harness/skills/\n- .harness/library/\n- AGENTS.md\n- CLAUDE.md\n- .cursor/rules/leerness.mdc\n- .github/copilot-instructions.md\n- README.md Leerness managed section\n\nUse merge, archive, or deprecated markers instead of deletion.\n`),
|
|
197
|
+
'.harness/architecture.md': fm('architecture', ['기능 구현', '리팩토링', '마이그레이션'], ['구조 변경'], `# Architecture\n\n## Overview\n- 실제 구조를 기록하세요.\n\n## Data Flow\n-\n`),
|
|
198
|
+
'.harness/context-map.md': fm('context-map', ['관련 파일 탐색', '기능 구현 전'], ['파일 구조 변경'], `# Context Map\n\n| Area | Files | Notes |\n|---|---|---|\n| App | src/** | 실제 경로로 업데이트 |\n| Tests | tests/** | 검증 경로 |\n`),
|
|
199
|
+
'.harness/decisions.md': fm('decisions', ['설계 결정 확인'], ['중요 결정 발생'], `# Decisions\n\n## Template\n### ${today()} — Decision\n- Decision:\n- Reason:\n- Alternatives:\n- Impact:\n`),
|
|
200
|
+
'.harness/task-log.md': fm('task-log', ['작업 이력 확인'], ['모든 의미 있는 작업 후'], `# Task Log\n\n## ${today()}\n- Leerness v${VERSION} initialized or migrated.\n`),
|
|
201
|
+
'.harness/guardrails.md': fm('guardrails', ['모든 작업 전', '보안/권한/리팩토링 전'], ['금지 규칙 변경'], `# Guardrails\n\n- 토큰/키/비밀번호를 저장하지 않습니다.\n- 요청 없는 대규모 리팩토링을 하지 않습니다.\n- API/DB/환경변수 변경은 영향 범위를 기록합니다.\n- Leerness 보호 파일을 삭제하지 않습니다.\n`),
|
|
202
|
+
'.harness/design-system.md': fm('design-system', ['UI 변경', '컴포넌트 추가', 'designguide 병합'], ['디자인 기준 변경', '재사용 패턴 발견'], `# Design System\n\n## Canonical File\n이 파일은 designguide.md, design-guide.md와 같은 디자인 가이드의 기준 파일입니다.\n\n## Reusable Patterns\n| Pattern | Where | Reuse Rule |\n|---|---|---|\n`),
|
|
203
|
+
'.harness/consistency-policy.md': fm('consistency-policy', ['UI/기능 중복 생성 전', '재사용 판단'], ['일관성 정책 변경'], `# Consistency Policy\n\n동일한 기능을 하는 요소는 새로 만들기 전에 기존 구현을 찾아 재사용, 확장, 연결합니다.\n\n## Recursive Reuse Rule\n1. 같은 기능의 기존 요소를 찾습니다.\n2. 자기 참조/기저 규칙/재귀 흐름이 필요한지 확인합니다.\n3. 기존 요소를 재사용하거나 확장합니다.\n4. 불가피하게 새로 만들면 reuse-map.md에 이유를 기록합니다.\n`),
|
|
204
|
+
'.harness/reuse-map.md': fm('reuse-map', ['새 컴포넌트/API/helper 생성 전', '중복 기능 감지'], ['재사용 가능한 요소 추가'], `# Reuse Map\n\n| Capability | Existing Element | Reuse Method | Notes |\n|---|---|---|---|\n`),
|
|
205
|
+
'.harness/feature-contracts.md': fm('feature-contracts', ['기능 구현/수정 전'], ['기능 입출력/상태/오류 변경'], `# Feature Contracts\n\n## Template\n- Feature:\n- Input:\n- Output:\n- States:\n- Errors:\n- Related files:\n`),
|
|
206
|
+
'.harness/testing-strategy.md': fm('testing-strategy', ['검증 전', '릴리즈 전'], ['테스트 전략 변경'], `# Testing Strategy\n\n- Typecheck\n- Lint\n- Unit/Integration/E2E\n- Manual smoke test\n`),
|
|
207
|
+
'.harness/review-checklist.md': fm('review-checklist', ['PR/리뷰 전'], ['리뷰 기준 변경'], `# Review Checklist\n\n- [ ] 계획과 맞는가\n- [ ] 진행 상태를 갱신했는가\n- [ ] 보호 파일을 삭제하지 않았는가\n- [ ] 디자인/기능 재사용을 확인했는가\n`),
|
|
208
|
+
'.harness/release-checklist.md': fm('release-checklist', ['배포 전'], ['배포 조건/환경변수/롤백 변경'], `# Release Checklist\n\n- [ ] npm view leerness version 확인\n- [ ] leerness verify .\n- [ ] leerness debug .\n- [ ] 프로젝트 typecheck/lint/test\n- [ ] 환경변수 확인\n- [ ] 롤백 방법 확인\n`),
|
|
209
|
+
'.harness/session-close-policy.md': fm('session-close-policy', ['세션 종료 전'], ['세션 종료 형식 변경'], `# Session Close Policy\n\nEvery session must list:\n- Completed\n- In progress\n- Incomplete\n- Planned\n- Waiting\n- On hold\n- Blocked\n- Dropped\n- Verification\n- Recommended next direction\n- Next exact step\n`),
|
|
210
|
+
'.harness/anti-lazy-work-policy.md': fm('anti-lazy-work-policy', ['완료 선언 전'], ['게으른 작업 방지 기준 변경'], `# Anti Lazy Work Policy\n\nDo not claim completion without evidence. Report unfinished work honestly. Run verification when possible. Leave exact next steps.\n`),
|
|
211
|
+
'.harness/session-handoff.md': fm('session-handoff', ['세션 시작', '다음 작업 이어받기'], ['세션 종료'], `# Session Handoff\n\n## Completed\n-\n\n## In Progress\n-\n\n## Incomplete / Waiting / On Hold / Blocked\n-\n\n## Dropped\n-\n\n## Verification\n-\n\n## Recommended Direction\n-\n\n## Next Exact Step\n-\n`),
|
|
212
|
+
'.harness/leerness-maintenance.md': fm('leerness-maintenance', ['작업 시작', '마이그레이션/릴리즈 전'], ['버전 정책 변경'], `# Leerness Maintenance\n\nAI agents should check:\n\n\`\`\`bash\nnpm view leerness version\nleerness --version\ncat .harness/HARNESS_VERSION\n\`\`\`\n\nIf a newer version exists, use dry-run before migration.\n`),
|
|
213
|
+
'.harness/language-policy.md': fm('language-policy', ['문서 작성 전'], ['언어 변경'], `# Language Policy\n\nSelected language: ${lang}\n\nAll Leerness notes, skill notes, session reports, and task lists should use this language unless the user asks otherwise.\n`),
|
|
214
|
+
'.harness/secret-policy.md': fm('secret-policy', ['스킬/배포/설정 변경 전'], ['민감정보 정책 변경'], `# Secret Policy\n\nStore environment variable names, not secret values. Use .env.local, CI secrets, or cloud secrets for real values.\n`),
|
|
215
|
+
'.harness/AX_PLAN_GUIDE.md': fm('ax-plan-guide', ['계획 수립/변경', '신규 프로젝트'], ['계획 가이드 변경'], `# AX Plan Guide\n\n1. Check whether the user request belongs to existing plan.md.\n2. If it is new scope, add it to plan.md and progress-tracker.md.\n3. If user drops scope, mark dropped instead of deleting.\n4. For a completely new project, create plan.md before implementation.\n`),
|
|
216
|
+
'.harness/AX_MIGRATION_GUIDE.md': fm('ax-migration-guide', ['마이그레이션 전'], ['마이그레이션 정책 변경'], `# AX Migration Guide\n\n- Back up before changes.\n- Preserve existing project memory.\n- Merge .env.example and .gitignore instead of overwriting.\n- Do not delete protected files.\n`),
|
|
217
|
+
'.harness/AX_NEW_PROJECT_GUIDE.md': fm('ax-new-project-guide', ['신규 프로젝트 감지'], ['신규 설치 정책 변경'], `# AX New Project Guide\n\nBefore coding, ask or infer the project goal, users, scope, out-of-scope, stack, deployment target, and milestones. Then fill plan.md and project-brief.md.\n`),
|
|
218
|
+
'.harness/AX_SKILL_LIBRARY_GUIDE.md': fm('ax-skill-library-guide', ['스킬 학습/검증/업로드'], ['스킬 정책 변경'], `# AX Skill Library Guide\n\nValidated skills require metadata, sensitive data scan, AI verification, dry-run publish, and explicit execute approval.\n`),
|
|
219
|
+
'.harness/skill-index.md': fm('skill-index', ['작업별 스킬 선택'], ['스킬 추가/삭제'], `# Skill Index\n\n| ID | Korean Name | Capabilities | Last Updated | Verification |\n|---|---|---|---|---|\n${skillRows}\n`),
|
|
220
|
+
'.harness/templates/end-of-session-report.md': `# End of Session Report\n\n## Completed\n\n## In Progress\n\n## Incomplete\n\n## Planned\n\n## Waiting\n\n## On Hold\n\n## Blocked\n\n## Dropped\n\n## Verification\n\n## Recommended Direction\n\n## Next Exact Step\n`,
|
|
221
|
+
'.harness/templates/decision.md': '# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function parseSkillsValue(v) {
|
|
225
|
+
if (!v || v === true) return [];
|
|
226
|
+
if (v === 'all') return Object.keys(skillCatalog);
|
|
227
|
+
if (v === 'recommended') return ['office', 'commerce-api', 'ai-verified-skill-publisher'];
|
|
228
|
+
return String(v).split(',').map(s => s.trim()).filter(Boolean).filter(s => skillCatalog[s]);
|
|
229
|
+
}
|
|
230
|
+
function parseSkills() { return parseSkillsValue(arg('--skills', '')); }
|
|
231
|
+
function detectLanguageValue(root, value = 'auto') {
|
|
232
|
+
const v = String(value || 'auto').toLowerCase();
|
|
233
|
+
if (v === 'ko' || v === 'en') return v;
|
|
234
|
+
const candidates = ['README.md', 'docs/guideline.md', '.harness/project-brief.md', '.harness/plan.md'];
|
|
235
|
+
let text = '';
|
|
236
|
+
for (const c of candidates) { const p = path.join(root, c); if (exists(p)) text += read(p).slice(0, 3000); }
|
|
237
|
+
return /[가-힣]/.test(text) ? 'ko' : 'en';
|
|
238
|
+
}
|
|
239
|
+
function ask(question) {
|
|
240
|
+
return new Promise(resolve => {
|
|
241
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
242
|
+
rl.question(question, answer => { rl.close(); resolve(String(answer || '').trim()); });
|
|
243
|
+
});
|
|
648
244
|
}
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
245
|
+
async function resolveInstallOptions(root, opts = {}) {
|
|
246
|
+
const explicitLang = arg('--language', null);
|
|
247
|
+
const explicitSkills = arg('--skills', null);
|
|
248
|
+
let lang = explicitLang ? detectLanguageValue(root, explicitLang) : detectLanguageValue(root, 'auto');
|
|
249
|
+
let skills = explicitSkills ? parseSkillsValue(explicitSkills) : [];
|
|
250
|
+
const shouldAsk = !has('--yes') && !opts.nonInteractive && process.stdin.isTTY && process.stdout.isTTY && !opts.migration;
|
|
251
|
+
if (shouldAsk && !explicitLang) {
|
|
252
|
+
log('\n설치 언어를 선택하세요.');
|
|
253
|
+
log('1) 자동 감지');
|
|
254
|
+
log('2) 한국어');
|
|
255
|
+
log('3) English');
|
|
256
|
+
const a = await ask('선택 [1]: ');
|
|
257
|
+
lang = a === '2' ? 'ko' : a === '3' ? 'en' : detectLanguageValue(root, 'auto');
|
|
657
258
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
function planUpdate(root,id,flags={}){
|
|
672
|
-
root=path.resolve(root||process.cwd()); const file=ensurePlan(root); let body=read(file); let changed=false;
|
|
673
|
-
body=body.split(/\r?\n/).map(line=>{
|
|
674
|
-
if(!line.startsWith('| '+id+' |')) return line;
|
|
675
|
-
const cols=splitTableLine(line); if(cols.length<6) return line;
|
|
676
|
-
if(flags.title||flags.name) cols[1]=String(flags.title||flags.name).replace(/\|/g,'/');
|
|
677
|
-
if(flags.status) cols[2]=String(flags.status);
|
|
678
|
-
if(flags.progress!==undefined) cols[3]=String(flags.progress).replace(/%$/,'')+'%';
|
|
679
|
-
if(flags.tasks) cols[4]=String(flags.tasks).replace(/\|/g,'/');
|
|
680
|
-
if(flags.note) cols[5]=String(flags.note).replace(/\|/g,'/');
|
|
681
|
-
changed=true; return '| '+cols.join(' | ')+' |';
|
|
682
|
-
}).join('\n');
|
|
683
|
-
if(!changed){ fail('계획 ID를 찾을 수 없습니다: '+id); process.exitCode=1; return; }
|
|
684
|
-
body=appendPlanChange(body,'Updated '+id,flags.reason||'plan update',flags.by||'user'); writePlanText(root,body); ok('계획 업데이트: '+id);
|
|
685
|
-
}
|
|
686
|
-
function planProgress(root){
|
|
687
|
-
root=path.resolve(root||process.cwd()); const body=readPlanText(root); const rows=[];
|
|
688
|
-
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]}); } }
|
|
689
|
-
banner(); log('Plan progress: '+root); if(!rows.length){ warn('계획 milestone이 없습니다. leerness plan add 를 사용하세요.'); return; }
|
|
690
|
-
rows.forEach(r=>log(` - ${r.id} [${r.status}] ${r.progress} ${r.milestone} :: tasks=${r.tasks}`));
|
|
691
|
-
}
|
|
692
|
-
function planSync(root){
|
|
693
|
-
root=path.resolve(root||process.cwd()); ensurePlan(root); const tasks=readProgressTasks(root);
|
|
694
|
-
const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
|
|
695
|
-
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;
|
|
696
|
-
const cs=path.join(root,'.harness/current-state.md');
|
|
697
|
-
if(exists(cs)){
|
|
698
|
-
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`;
|
|
699
|
-
if(body.includes('## Plan Sync')) body=body.replace(/\n## Plan Sync[\s\S]*?(?=\n## |$)/, block);
|
|
700
|
-
else body=body.replace(/\s*$/, block+'\n');
|
|
701
|
-
write(cs,body); ok('current-state.md plan sync 갱신');
|
|
259
|
+
if (shouldAsk && !explicitSkills) {
|
|
260
|
+
log('\n설치할 스킬 라이브러리를 선택하세요.');
|
|
261
|
+
log('0) 기본 하네스만 설치');
|
|
262
|
+
log('1) 추천 스킬 설치: office, commerce-api, ai-verified-skill-publisher');
|
|
263
|
+
log('2) 전체 스킬 설치');
|
|
264
|
+
log('3) 직접 입력');
|
|
265
|
+
skillList();
|
|
266
|
+
const a = await ask('선택 [1]: ');
|
|
267
|
+
if (!a || a === '1') skills = parseSkillsValue('recommended');
|
|
268
|
+
else if (a === '2') skills = parseSkillsValue('all');
|
|
269
|
+
else if (a === '3') skills = parseSkillsValue(await ask('스킬 ID를 쉼표로 입력: '));
|
|
270
|
+
else if (a === '0') skills = [];
|
|
702
271
|
}
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
function
|
|
706
|
-
|
|
707
|
-
if(
|
|
708
|
-
|
|
709
|
-
if(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const ACTIVE_TASK_STATUSES = new Set(['requested','planned','in-progress','waiting','on-hold','blocked','incomplete']);
|
|
719
|
-
const ALL_TASK_STATUSES = new Set(['requested','planned','in-progress','waiting','on-hold','blocked','incomplete','done','dropped']);
|
|
720
|
-
function progressFile(root){ return path.join(root,'.harness/progress-tracker.md'); }
|
|
721
|
-
function splitTableLine(line){ return line.trim().replace(/^\|/,'').replace(/\|$/,'').split('|').map(x=>x.trim()); }
|
|
722
|
-
function readProgressTasks(root){
|
|
723
|
-
const file=progressFile(root); if(!exists(file)) return [];
|
|
724
|
-
const tasks=[];
|
|
725
|
-
for(const line of read(file).split(/\r?\n/)){
|
|
726
|
-
if(!/^\|\s*T-[0-9A-Za-z_-]+\s*\|/.test(line)) continue;
|
|
727
|
-
const cols=splitTableLine(line); if(cols.length<7) continue;
|
|
728
|
-
tasks.push({id:cols[0],request:cols[1],status:cols[2],owner:cols[3],lastUpdate:cols[4],evidence:cols[5],nextAction:cols[6]});
|
|
272
|
+
return { lang, skills };
|
|
273
|
+
}
|
|
274
|
+
function copyRecursiveSafe(src, dst) {
|
|
275
|
+
if (!exists(src)) return;
|
|
276
|
+
if (src.includes(path.sep + '.harness' + path.sep + 'archive')) return;
|
|
277
|
+
const st = fs.statSync(src);
|
|
278
|
+
if (st.isDirectory()) {
|
|
279
|
+
mkdirp(dst);
|
|
280
|
+
for (const e of fs.readdirSync(src)) {
|
|
281
|
+
if (e === 'node_modules' || e === '.git') continue;
|
|
282
|
+
copyRecursiveSafe(path.join(src, e), path.join(dst, e));
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
mkdirp(path.dirname(dst));
|
|
286
|
+
fs.copyFileSync(src, dst);
|
|
729
287
|
}
|
|
730
|
-
return tasks;
|
|
731
|
-
}
|
|
732
|
-
function taskTable(tasks){
|
|
733
|
-
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';
|
|
734
288
|
}
|
|
735
|
-
function
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
if(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
289
|
+
function migrationCandidates(root, files) {
|
|
290
|
+
const fixed = [
|
|
291
|
+
'AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.cursor/rules/project-rules.mdc',
|
|
292
|
+
'.github/copilot-instructions.md','README.md','.env.example','.gitignore',
|
|
293
|
+
'docs/guideline.md','docs/history.md','guideline.md','history.md',
|
|
294
|
+
'AI_HARNESS.md','HARNESS.md','PROJECT_CONTEXT.md','CONTEXT.md','ARCHITECTURE.md','DECISIONS.md','CURRENT_STATE.md','TASK_LOG.md',
|
|
295
|
+
'.harness','.ai','harness'
|
|
296
|
+
];
|
|
297
|
+
const all = Array.from(new Set([...fixed, ...Object.keys(files)]));
|
|
298
|
+
return all.filter(f => exists(path.join(root, f)));
|
|
299
|
+
}
|
|
300
|
+
function createBackup(root, reason, files, dry = false) {
|
|
301
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
302
|
+
const ar = path.join(root, '.harness/archive', `leerness-${VERSION}-${stamp}`);
|
|
303
|
+
const candidates = migrationCandidates(root, files);
|
|
304
|
+
if (dry) return { archiveDir: ar, candidates };
|
|
305
|
+
mkdirp(ar);
|
|
306
|
+
const fileRoot = path.join(ar, 'files');
|
|
307
|
+
for (const f of candidates) copyRecursiveSafe(path.join(root, f), path.join(fileRoot, f === '.harness' ? '.harness-before-migration' : f));
|
|
308
|
+
write(path.join(ar, 'migration-manifest.json'), JSON.stringify({
|
|
309
|
+
version: VERSION,
|
|
310
|
+
reason,
|
|
311
|
+
createdAt: now(),
|
|
312
|
+
policy: 'backup-before-write; preserve-by-default; merge-managed-files; merge-env-and-gitignore',
|
|
313
|
+
candidates
|
|
314
|
+
}, null, 2) + '\n');
|
|
315
|
+
return { archiveDir: ar, candidates };
|
|
316
|
+
}
|
|
317
|
+
function managedMerge(file, next, previous, archiveDir) {
|
|
318
|
+
if (!previous || previous.trim() === next.trim()) return next;
|
|
319
|
+
const tag = '<!-- leerness:migration-preserved -->';
|
|
320
|
+
if (previous.includes(tag)) return next;
|
|
321
|
+
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\nPrevious content was backed up before migration. Archive reference:\n\n\`${archiveDir ? path.relative(process.cwd(), archiveDir).replace(/\\/g, '/') : '.harness/archive'}\`\n\nThe content below is retained so AI agents can migrate useful custom instructions instead of losing them.\n\n<details>\n<summary>Previous ${file}</summary>\n\n\`\`\`md\n${previous.replace(/```/g, '\\`\\`\\`')}\n\`\`\`\n\n</details>\n`;
|
|
322
|
+
}
|
|
323
|
+
function writeIfSafe(root, file, content, opts = {}) {
|
|
324
|
+
const p = path.join(root, file);
|
|
325
|
+
const already = exists(p);
|
|
326
|
+
if (already && !opts.force && !opts.mergeManaged) return { action: 'preserved', file };
|
|
327
|
+
if (already && opts.mergeManaged && !opts.force) {
|
|
328
|
+
const prev = read(p);
|
|
329
|
+
write(p, managedMerge(file, content, prev, opts.archiveDir));
|
|
330
|
+
return { action: 'merged', file };
|
|
766
331
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
332
|
+
write(p, content);
|
|
333
|
+
return { action: already ? 'updated' : 'created', file };
|
|
334
|
+
}
|
|
335
|
+
function mergeLinesFile(p, lines) {
|
|
336
|
+
const current = exists(p) ? read(p) : '';
|
|
337
|
+
let next = current;
|
|
338
|
+
for (const line of lines) if (!next.includes(line)) next += (next.endsWith('\n') || !next ? '' : '\n') + line + '\n';
|
|
339
|
+
write(p, next);
|
|
340
|
+
}
|
|
341
|
+
function writeMigrationReport(root, backup, actions) {
|
|
342
|
+
const p = path.join(root, '.harness/migration-report.md');
|
|
343
|
+
const rows = actions.map(a => `| ${a.file} | ${a.action} |`).join('\n');
|
|
344
|
+
write(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example and .gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
|
|
345
|
+
}
|
|
346
|
+
function syncReadme(root) {
|
|
347
|
+
const p = path.join(root, 'README.md');
|
|
348
|
+
const existing = exists(p) ? read(p) : '';
|
|
349
|
+
write(p, mergeSection(existing, managedReadmeBlock(detectProjectName(root))));
|
|
350
|
+
ok('README.md Leerness section synced');
|
|
351
|
+
}
|
|
352
|
+
async function install(root, opts = {}) {
|
|
353
|
+
root = absRoot(root); mkdirp(root);
|
|
354
|
+
const resolved = await resolveInstallOptions(root, opts);
|
|
355
|
+
const lang = resolved.lang;
|
|
356
|
+
const skills = resolved.skills;
|
|
357
|
+
log(`\nLeerness v${VERSION}`);
|
|
358
|
+
log(`Target: ${root}`);
|
|
359
|
+
log(`Language: ${lang}`);
|
|
360
|
+
log(`Skills: ${skills.length ? skills.join(', ') : 'none'}`);
|
|
361
|
+
const files = coreFiles(root, lang, skills);
|
|
362
|
+
const backup = createBackup(root, opts.force ? 'force' : (opts.migration ? 'migration' : 'init'), files, opts.dry);
|
|
363
|
+
if (opts.dry) {
|
|
364
|
+
log(`Backup target: ${backup.archiveDir}`);
|
|
365
|
+
log('Files that would be backed up:');
|
|
366
|
+
backup.candidates.forEach(f => log('- ' + f));
|
|
367
|
+
} else {
|
|
368
|
+
ok(`backup created: ${rel(root, backup.archiveDir)}`);
|
|
778
369
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
370
|
+
const managedOverwrite = new Set([
|
|
371
|
+
'AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md',
|
|
372
|
+
'.harness/HARNESS_VERSION','.harness/manifest.json','.harness/LANGUAGE','.harness/skills-lock.json',
|
|
373
|
+
'.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md',
|
|
374
|
+
'.harness/leerness-maintenance.md','.harness/protected-files.md','.harness/AX_MIGRATION_GUIDE.md',
|
|
375
|
+
'.harness/AX_NEW_PROJECT_GUIDE.md','.harness/AX_SKILL_LIBRARY_GUIDE.md'
|
|
376
|
+
]);
|
|
377
|
+
const actions = [];
|
|
378
|
+
for (const [f, c] of Object.entries(files)) {
|
|
379
|
+
const existsNow = exists(path.join(root, f));
|
|
380
|
+
const mergeManaged = managedOverwrite.has(f);
|
|
381
|
+
if (opts.dry) {
|
|
382
|
+
const action = existsNow ? (mergeManaged || opts.force ? 'merge/update from backup-aware template' : 'preserve') : 'create';
|
|
383
|
+
log(`[dry-run] ${action}: ${f}`);
|
|
384
|
+
actions.push({ file:f, action });
|
|
385
|
+
continue;
|
|
790
386
|
}
|
|
387
|
+
const r = writeIfSafe(root, f, c, { force: opts.force, mergeManaged, archiveDir: backup.archiveDir });
|
|
388
|
+
actions.push(r);
|
|
389
|
+
ok(`${r.action}: ${r.file}`);
|
|
791
390
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if(exists(mf)){ const m=parseJsonSafe(read(mf),{}); if(m.language) ok('언어 설정: '+m.language+' ('+(m.languageName||languageName(m.language))+')'); else { warnings++; warn('manifest language 누락'); } }
|
|
799
|
-
const tasks=readProgressTasks(root);
|
|
800
|
-
const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
|
|
801
|
-
log('\nActive unresolved tasks:'); renderTasks(active);
|
|
802
|
-
const badSensitive=scanSensitivePath(path.join(root,'.harness')).filter(x=>!x.file.includes(path.sep+'archive'+path.sep));
|
|
803
|
-
if(badSensitive.length){ failures+=badSensitive.length; badSensitive.forEach(x=>fail('민감정보 의심: '+rel(root,x.file)+' '+x.type)); } else ok('하네스 민감정보 스캔 통과');
|
|
804
|
-
log('\nDebug summary: '+(failures?'FAIL':'PASS')+' / warnings='+warnings+' / failures='+failures);
|
|
805
|
-
if(failures) process.exitCode=1;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function closeSession(root){
|
|
809
|
-
root=path.resolve(root||process.cwd());
|
|
810
|
-
banner();
|
|
811
|
-
const template=path.join(root,'.harness/templates/end-of-session-report.md');
|
|
812
|
-
const policy=path.join(root,'.harness/session-close-policy.md');
|
|
813
|
-
const progress=path.join(root,'.harness/progress-tracker.md');
|
|
814
|
-
log('Session close checklist');
|
|
815
|
-
log('');
|
|
816
|
-
log('Read before closing:');
|
|
817
|
-
['.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));
|
|
818
|
-
log('');
|
|
819
|
-
log('Required final report sections:');
|
|
820
|
-
['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));
|
|
821
|
-
log('');
|
|
822
|
-
if(exists(template)) log(read(template));
|
|
823
|
-
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');
|
|
824
|
-
const tasks=readProgressTasks(root);
|
|
825
|
-
const active=tasks.filter(t=>ACTIVE_TASK_STATUSES.has(t.status));
|
|
826
|
-
const dropped=tasks.filter(t=>t.status==='dropped');
|
|
827
|
-
log('');
|
|
828
|
-
log('Tracked unresolved / planned / waiting / on-hold / in-progress work:');
|
|
829
|
-
renderTasks(active);
|
|
830
|
-
log('');
|
|
831
|
-
log('Dropped by user:');
|
|
832
|
-
renderTasks(dropped);
|
|
833
|
-
if(!exists(policy)) warn('session-close-policy.md가 없습니다. leerness migrate를 실행하세요.');
|
|
834
|
-
if(!exists(progress)) warn('progress-tracker.md가 없습니다. leerness migrate를 실행하세요.');
|
|
835
|
-
}
|
|
836
|
-
function sessionCommand(args){ const sub=args[1]||'close'; if(sub==='close'||sub==='handoff'||sub==='end') return closeSession(args[2]||process.cwd()); fail('알 수 없는 session 명령: '+sub); }
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
function compareVersions(a,b){
|
|
840
|
-
const pa=String(a||'0.0.0').trim().replace(/^v/,'').split('.').map(x=>parseInt(x,10)||0);
|
|
841
|
-
const pb=String(b||'0.0.0').trim().replace(/^v/,'').split('.').map(x=>parseInt(x,10)||0);
|
|
842
|
-
for(let i=0;i<Math.max(pa.length,pb.length,3);i++){ const x=pa[i]||0,y=pb[i]||0; if(x>y) return 1; if(x<y) return -1; }
|
|
843
|
-
return 0;
|
|
844
|
-
}
|
|
845
|
-
function projectHarnessVersion(root){ const p=path.join(root,'.harness/HARNESS_VERSION'); return exists(p)?read(p).trim():null; }
|
|
846
|
-
function npmLatestLeerness(timeoutMs=15000){
|
|
847
|
-
try{
|
|
848
|
-
const out=childProcess.execFileSync('npm',['view','leerness','version','--silent'],{encoding:'utf8',timeout:timeoutMs,stdio:['ignore','pipe','pipe']}).trim();
|
|
849
|
-
return {ok:true,version:out,error:null};
|
|
850
|
-
}catch(e){ return {ok:false,version:null,error:(e.stderr&&String(e.stderr).trim())||e.message}; }
|
|
851
|
-
}
|
|
852
|
-
function appendTaskLog(root,lines){
|
|
853
|
-
const p=path.join(root,'.harness/task-log.md'); if(!exists(p)) return;
|
|
854
|
-
const body=read(p).replace(/\s*$/,'\n')+`\n## ${today()} — Leerness maintenance\n`+lines.map(x=>'- '+x).join('\n')+'\n';
|
|
855
|
-
write(p,body);
|
|
856
|
-
}
|
|
857
|
-
function selfCheck(root,flags={}){
|
|
858
|
-
root=path.resolve(root||process.cwd()); banner();
|
|
859
|
-
const projectVersion=projectHarnessVersion(root);
|
|
860
|
-
log('Leerness self check');
|
|
861
|
-
log(' Project path: '+root);
|
|
862
|
-
log(' CLI version: '+VERSION);
|
|
863
|
-
log(' Project HARNESS_VERSION: '+(projectVersion||'not installed'));
|
|
864
|
-
log(' npm check command: npm view leerness version');
|
|
865
|
-
const res=npmLatestLeerness(flags.timeout?Number(flags.timeout):15000);
|
|
866
|
-
if(!res.ok){
|
|
867
|
-
warn('npm 최신 버전 확인 실패: '+res.error);
|
|
868
|
-
warn('네트워크 또는 npm registry 문제일 수 있습니다. 고정 버전으로 검증하고, 실패 사실을 session-handoff에 기록하세요.');
|
|
869
|
-
appendTaskLog(root,[`npm view leerness version failed: ${res.error}`,`CLI version used: ${VERSION}`,`Project HARNESS_VERSION: ${projectVersion||'not installed'}`]);
|
|
870
|
-
process.exitCode=flags.strict?1:0;
|
|
871
|
-
return {latest:null,projectVersion,cliVersion:VERSION,needsMigration:false};
|
|
872
|
-
}
|
|
873
|
-
const latest=res.version;
|
|
874
|
-
log(' npm latest: '+latest);
|
|
875
|
-
const newerThanCli=compareVersions(latest,VERSION)>0;
|
|
876
|
-
const newerThanProject=projectVersion?compareVersions(latest,projectVersion)>0:true;
|
|
877
|
-
const needs=Boolean(newerThanCli||newerThanProject||projectVersion!==VERSION);
|
|
878
|
-
if(!needs){ ok('Leerness is current for this project.'); }
|
|
879
|
-
else{
|
|
880
|
-
warn('Leerness update/migration review recommended.');
|
|
881
|
-
log('');
|
|
882
|
-
log('Recommended safe flow:');
|
|
883
|
-
log(` npx --yes leerness@${latest} migrate "${root}" --dry-run`);
|
|
884
|
-
log(` npx --yes leerness@${latest} migrate "${root}"`);
|
|
885
|
-
log(` npx --yes leerness@${latest} verify "${root}"`);
|
|
886
|
-
log(` npx --yes leerness@${latest} debug "${root}"`);
|
|
391
|
+
if (!opts.dry) {
|
|
392
|
+
mergeLinesFile(path.join(root, '.gitignore'), ['.harness/skill-publish.local.json', '.harness/**/*.local.json', '.env.local']);
|
|
393
|
+
mergeLinesFile(path.join(root, '.env.example'), ['# Leerness uses environment variable names only. Do not store real secrets here.', 'LEERNESS_NPM_TOKEN=', 'LEERNESS_GITHUB_TOKEN=']);
|
|
394
|
+
syncReadme(root);
|
|
395
|
+
installSkills(root, skills);
|
|
396
|
+
writeMigrationReport(root, backup, actions);
|
|
887
397
|
}
|
|
888
|
-
appendTaskLog(root,[`npm latest checked: ${latest}`,`CLI version used: ${VERSION}`,`Project HARNESS_VERSION: ${projectVersion||'not installed'}`,`Migration recommended: ${needs?'yes':'no'}`]);
|
|
889
|
-
return {latest,projectVersion,cliVersion:VERSION,needsMigration:needs};
|
|
890
398
|
}
|
|
891
|
-
function
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
function
|
|
919
|
-
function
|
|
920
|
-
function
|
|
921
|
-
|
|
922
|
-
|
|
399
|
+
function installSkills(root, skills) {
|
|
400
|
+
for (const name of skills) addSkill(root, name, true);
|
|
401
|
+
}
|
|
402
|
+
function addSkill(root, name, silent = false) {
|
|
403
|
+
const meta = skillCatalog[name]; if (!meta) { fail(`Unknown skill: ${name}`); return; }
|
|
404
|
+
const dir = path.join(root, '.harness/skills', name); mkdirp(dir);
|
|
405
|
+
write(path.join(dir, 'skill.json'), JSON.stringify({ name, ...meta, verification: { status: meta.verification, method: 'leerness-curated' } }, null, 2) + '\n');
|
|
406
|
+
write(path.join(dir, 'README.md'), `# ${meta.displayNameKo}\n\n## Capabilities\n${meta.capabilities.map(x => '- ' + x).join('\n')}\n\n## Sensitive Data Policy\n실제 토큰이나 비밀번호를 기록하지 않고 환경변수 이름만 기록합니다.\n`);
|
|
407
|
+
if (!silent) ok(`skill installed: ${name}`);
|
|
408
|
+
}
|
|
409
|
+
function skillList() {
|
|
410
|
+
log('| ID | 한글명 | 가능한 작업 | 최종 업데이트 | 검증 |');
|
|
411
|
+
log('|---|---|---|---|---|');
|
|
412
|
+
for (const [k, v] of Object.entries(skillCatalog)) log(`| ${k} | ${v.displayNameKo} | ${v.capabilities.join('<br>')} | ${v.lastUpdated} | ${v.verification} |`);
|
|
413
|
+
}
|
|
414
|
+
function skillInfo(name) {
|
|
415
|
+
const v = skillCatalog[name]; if (!v) return fail(`Unknown skill: ${name}`);
|
|
416
|
+
log(`${name}`); log(`한글명: ${v.displayNameKo}`); log(`버전: ${v.version}`); log(`최종 업데이트: ${v.lastUpdated}`); log(`검증: ${v.verification}`); log('가능한 작업:'); v.capabilities.forEach(x => log('- ' + x));
|
|
417
|
+
}
|
|
418
|
+
function planPath(root) { return path.join(root, '.harness/plan.md'); }
|
|
419
|
+
function progressPath(root) { return path.join(root, '.harness/progress-tracker.md'); }
|
|
420
|
+
function nextId(root, prefix) {
|
|
421
|
+
const all = [planPath(root), progressPath(root)].map(p => exists(p) ? read(p) : '').join('\n');
|
|
422
|
+
const re = new RegExp(prefix + '-(\\d{4})', 'g'); let max = 0, m;
|
|
423
|
+
while ((m = re.exec(all))) max = Math.max(max, Number(m[1]));
|
|
424
|
+
return `${prefix}-${String(max + 1).padStart(4, '0')}`;
|
|
425
|
+
}
|
|
426
|
+
function planShow(root) { const p = planPath(root); log(exists(p) ? read(p) : 'plan.md not found'); }
|
|
427
|
+
function planInit(root) { const goal = arg('--goal', ''); if (!exists(planPath(root))) install(root); append(planPath(root), `\n## User Goal\n- ${goal || '사용자 목적을 작성하세요.'}\n`); ok('plan initialized'); }
|
|
428
|
+
function planAdd(root, text) { const id = nextId(root, 'M'); append(planPath(root), `\n### ${id}. ${text}\nStatus: ${arg('--status','planned')}\nProgress: ${arg('--progress','0')}%\n\nTasks:\n- [ ] ${text}\n`); append(progressPath(root), `| T-${id.slice(2)} | ${arg('--status','planned')} | ${text} | plan:${id} | 다음 액션 작성 | ${today()} |\n`); ok(`plan added: ${id}`); }
|
|
429
|
+
function planDrop(root, text) { const id = nextId(root, 'D'); const reason = arg('--reason', '사용자 요청으로 제외'); append(planPath(root), `\n| ${id} | ${text} | ${reason} | ${today()} |\n`); append(progressPath(root), `| T-${id.slice(2)} | dropped | ${text} | drop:${reason} | 없음 | ${today()} |\n`); ok(`plan dropped: ${id}`); }
|
|
430
|
+
function planProgress(root) { const p = exists(progressPath(root)) ? read(progressPath(root)) : ''; const counts = {}; for (const s of ['requested','planned','in-progress','waiting','on-hold','blocked','incomplete','done','dropped']) counts[s] = (p.match(new RegExp(`\\|\\s*${s}\\s*\\|`, 'g'))||[]).length; log(JSON.stringify(counts, null, 2)); }
|
|
431
|
+
function planSync(root) { append(path.join(root,'.harness/task-log.md'), `\n## ${today()}\n- Synced plan.md and progress-tracker.md.\n`); ok('plan/progress sync noted'); }
|
|
432
|
+
function taskList(root) { log(exists(progressPath(root)) ? read(progressPath(root)) : 'progress-tracker.md not found'); }
|
|
433
|
+
function taskAdd(root, text) { const id = nextId(root, 'T'); append(progressPath(root), `| ${id} | ${arg('--status','requested')} | ${text} | user-request | 다음 액션 작성 | ${today()} |\n`); ok(`task added: ${id}`); }
|
|
434
|
+
function taskUpdate(root, id) { append(progressPath(root), `| ${id} | ${arg('--status','in-progress')} | updated task | manual-update | ${arg('--next','다음 액션 작성')} | ${today()} |\n`); ok(`task updated: ${id}`); }
|
|
435
|
+
function taskDrop(root, id) { append(progressPath(root), `| ${id} | dropped | dropped task | ${arg('--reason','사용자 요청으로 제외')} | 없음 | ${today()} |\n`); ok(`task dropped: ${id}`); }
|
|
436
|
+
function route(name) { const r = routes[name]; if (!r) { fail('Unknown route'); log(Object.keys(routes).join(', ')); return; } log(`# Route: ${name}\n`); log('Read before work:'); r.read.forEach(x => log('- ' + x)); log('\nUpdate after work:'); r.update.forEach(x => log('- ' + x)); }
|
|
437
|
+
function status(root) { root = absRoot(root); const version = exists(path.join(root,'.harness/HARNESS_VERSION')) ? read(path.join(root,'.harness/HARNESS_VERSION')).trim() : 'not installed'; const files = Object.keys(coreFiles(root, exists(path.join(root,'.harness/LANGUAGE')) ? read(path.join(root,'.harness/LANGUAGE')).trim() : 'ko')); const missing = files.filter(f => !exists(path.join(root,f))); log(`Leerness: ${version}`); log(`Files: ${files.length - missing.length}/${files.length}`); if (missing.length) missing.forEach(x => warn('missing: ' + x)); else ok('required files present'); }
|
|
438
|
+
function verify(root) { root = absRoot(root); let bad = 0; for (const f of ['.harness/plan.md','.harness/progress-tracker.md','.harness/guideline.md','.harness/protected-files.md','.harness/design-system.md','AGENTS.md']) { if (!exists(path.join(root,f))) { bad++; fail(`missing: ${f}`); } } const g = exists(path.join(root,'.harness/guideline.md')) ? read(path.join(root,'.harness/guideline.md')) : ''; if (!g.includes('plan.md') || !g.includes('progress-tracker.md')) { bad++; fail('guideline.md must reference plan.md and progress-tracker.md'); } const a = exists(path.join(root,'AGENTS.md')) ? read(path.join(root,'AGENTS.md')) : ''; if (!a.includes('protected-files.md')) { bad++; fail('AGENTS.md must reference protected-files.md'); } if (bad) process.exitCode = 1; else ok('verify passed'); }
|
|
439
|
+
function debug(root) { root = absRoot(root); let warnings = 0, failures = 0; const checks = ['.harness/context-routing.md','.harness/writeback-policy.md','.harness/plan-progress-boundary.md','.harness/consistency-policy.md','.harness/reuse-map.md','.harness/leerness-maintenance.md']; for (const f of checks) { if (exists(path.join(root,f))) ok(f); else { warnings++; warn('missing: ' + f); } } const pg = exists(planPath(root)) && exists(progressPath(root)); if (pg) ok('plan/progress files exist'); else { failures++; fail('plan/progress missing'); } log(`Debug summary: warnings=${warnings} failures=${failures}`); if (failures) process.exitCode = 1; }
|
|
440
|
+
function sessionClose(root) { root = absRoot(root); log('# Session Close'); log('## Task Lists'); const p = exists(progressPath(root)) ? read(progressPath(root)) : ''; for (const s of ['done','in-progress','incomplete','planned','waiting','on-hold','blocked','dropped']) { log(`\n### ${s}`); const rows = p.split('\n').filter(line => line.includes(`| ${s} |`) || line.includes(`| ${s} `)); if (rows.length) rows.forEach(x => log(x)); else log('- 없음'); } log('\n## Required final response sections'); log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한 작업'); }
|
|
441
|
+
function readmeCmd(root) { syncReadme(absRoot(root)); }
|
|
442
|
+
function consistencyCheck(root) { root = absRoot(root); const candidates = ['designguide.md','design-guide.md','.harness/designguide.md','docs/designguide.md','docs/design-guide.md']; const found = candidates.filter(f => exists(path.join(root,f))); log('Canonical design file: .harness/design-system.md'); if (found.length) { warn('merge candidates found:'); found.forEach(x => log('- ' + x)); } else ok('no duplicate design guide candidates'); }
|
|
443
|
+
function mergeDesign(root) { root = absRoot(root); const canonical = path.join(root,'.harness/design-system.md'); const candidates = ['designguide.md','design-guide.md','.harness/designguide.md','docs/designguide.md','docs/design-guide.md']; let merged = ''; for (const f of candidates) { const p = path.join(root,f); if (exists(p)) merged += `\n\n## Merged from ${f}\n\n` + read(p); }
|
|
444
|
+
if (merged) append(canonical, merged); ok(merged ? 'design guides merged into .harness/design-system.md' : 'nothing to merge'); }
|
|
445
|
+
function selfCheck(root) { let latest = 'unknown'; try { latest = cp.execSync('npm view leerness version', { encoding:'utf8', stdio:['ignore','pipe','ignore'], timeout:10000 }).trim(); } catch { latest = 'npm registry unavailable'; } const local = exists(path.join(root,'.harness/HARNESS_VERSION')) ? read(path.join(root,'.harness/HARNESS_VERSION')).trim() : 'not installed'; log(`CLI: ${VERSION}`); log(`Project: ${local}`); log(`NPM latest: ${latest}`); }
|
|
446
|
+
function publishLibrary(targetPath, target) { const execute = has('--execute'); if (!execute) { log('dry-run only. Add --execute to publish.'); return; } const envs = target === 'npm' ? ['LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN'] : ['LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN']; const found = envs.find(e => process.env[e]); if (!found && has('--no-prompt')) { fail('publish token not found'); process.exitCode = 1; return; } if (!found) { fail(`publish token required. Set one of: ${envs.join(', ')}`); process.exitCode = 1; return; } ok(`token provided by ${found}; publish command would execute for ${targetPath}`); }
|
|
447
|
+
function help() { log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop [args]\n leerness skill list|info|add <name>\n leerness session close [path]\n`); }
|
|
448
|
+
async function main() {
|
|
449
|
+
const args = nonFlagArgs(); const cmd = args[0] || 'init';
|
|
450
|
+
if (has('--version') || has('-v')) return log(VERSION);
|
|
451
|
+
if (has('--help') || has('-h')) return help();
|
|
452
|
+
if (cmd === 'init') return await install(args[1] || process.cwd(), { force:false, dry:false, migration:false });
|
|
453
|
+
if (cmd === 'migrate') return await install(args[1] || process.cwd(), { force:has('--force'), dry:has('--dry-run'), migration:true });
|
|
454
|
+
if (cmd === 'status') return status(args[1] || process.cwd());
|
|
455
|
+
if (cmd === 'verify') return verify(args[1] || process.cwd());
|
|
456
|
+
if (cmd === 'debug') return debug(args[1] || process.cwd());
|
|
457
|
+
if (cmd === 'route') return route(args[1] || 'planning');
|
|
458
|
+
if (cmd === 'self' && args[1] === 'check') return selfCheck(absRoot(args[2] || process.cwd()));
|
|
459
|
+
if (cmd === 'self' && args[1] === 'migrate') return log('Run: npx --yes leerness@latest migrate . --dry-run, then migrate without --dry-run after review.');
|
|
460
|
+
if (cmd === 'readme' && args[1] === 'sync') return readmeCmd(args[2] || process.cwd());
|
|
461
|
+
if (cmd === 'consistency' && args[1] === 'check') return consistencyCheck(args[2] || process.cwd());
|
|
462
|
+
if (cmd === 'consistency' && args[1] === 'merge-design-guide') return mergeDesign(args[2] || process.cwd());
|
|
463
|
+
if (cmd === 'session' && args[1] === 'close') return sessionClose(args[2] || process.cwd());
|
|
464
|
+
if (cmd === 'skill' && args[1] === 'list') return skillList();
|
|
465
|
+
if (cmd === 'skill' && args[1] === 'info') return skillInfo(args[2]);
|
|
466
|
+
if (cmd === 'skill' && args[1] === 'add') return addSkill(absRoot(arg('--path', process.cwd())), args[2]);
|
|
467
|
+
if (cmd === 'plan') { const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'show'; if (sub==='show') return planShow(root); if (sub==='init') return planInit(root); if (sub==='add') return planAdd(root, args.slice(2).join(' ') || '새 계획'); if (sub==='drop') return planDrop(root, args.slice(2).join(' ') || '드랍 항목'); if (sub==='progress') return planProgress(root); if (sub==='sync') return planSync(root); }
|
|
468
|
+
if (cmd === 'task') { const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'list'; if (sub==='list') return taskList(root); if (sub==='add') return taskAdd(root, args.slice(2).join(' ') || '새 작업'); if (sub==='update') return taskUpdate(root, args[2] || 'T-0001'); if (sub==='drop') return taskDrop(root, args[2] || 'T-0001'); }
|
|
469
|
+
if (cmd === 'library' && args[1] === 'publish') return publishLibrary(args[2] || '.', arg('--target','npm'));
|
|
470
|
+
return help();
|
|
471
|
+
}
|
|
472
|
+
main().catch(err => { fail(err && err.message ? err.message : String(err)); process.exitCode = 1; });
|