leerness 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +39 -1
  2. package/bin/harness.js +280 -431
  3. package/package.json +10 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Leerness
2
2
 
3
- **Leerness는 AI 에이전트가 대규모 프로젝트에서도 필요한 문서를 먼저 읽고, 작업 후 적재적소에 프로젝트 메모리를 갱신하도록 만드는 AX 최적화 개발 하네스입니다.**
3
+ **Leerness는 AI 에이전트가 대규모 프로젝트에서도 필요한 문서를 먼저 읽고, 작업 후 적재적소에 프로젝트 메모리를 갱신하며, 세션 종료마다 진행/미완료/추천 방향을 명확히 남기도록 만드는 AX 최적화 개발 하네스입니다.**
4
4
 
5
5
  Leerness의 목적은 단순히 `.md` 파일을 많이 만드는 것이 아닙니다. AI가 어떤 상황에 어떤 파일을 봐야 하는지, 어떤 작업 후 어떤 파일을 갱신해야 하는지까지 명확히 지시합니다.
6
6
 
@@ -20,6 +20,8 @@ Agent = Model + Leerness Harness
20
20
  | Project Memory | 목적, 현재 상태, 아키텍처, 결정 로그, 릴리즈 조건 유지 |
21
21
  | Skill Libraries | 검증된 성공 패턴을 재사용 가능한 스킬로 설치/배포 |
22
22
  | AX Guides | AI가 구버전 마이그레이션 또는 신규 설치를 안전하게 수행하도록 안내 |
23
+ | Session Close Policy | 세션 종료 시 완료/진행중/미완료/검증/추천 방향을 강제 기록 |
24
+ | Anti-Lazy Work Policy | 검증 없는 완료 선언, 모호한 요약, 미완료 작업 은폐를 방지 |
23
25
 
24
26
  ## 설치
25
27
 
@@ -48,6 +50,7 @@ leerness migrate [path] [--dry-run]
48
50
  leerness status [path]
49
51
  leerness verify [path]
50
52
  leerness route <task-type>
53
+ leerness session close [path]
51
54
 
52
55
  leerness skill list
53
56
  leerness skill info <name>
@@ -85,6 +88,9 @@ leerness route new-install
85
88
  testing-strategy.md # 검증 전략
86
89
  release-checklist.md # 배포/npm/git/환경변수/롤백 조건
87
90
  session-handoff.md # 다음 세션 인수인계
91
+ session-close-policy.md # 세션 종료 보고 규칙
92
+ progress-tracker.md # 사용자 요청별 진행/미완료 추적
93
+ anti-lazy-work-policy.md # 게으른 작업 방지 규칙
88
94
  context-routing.md # 작업 유형별 읽기/갱신 라우팅
89
95
  writeback-policy.md # 어떤 정보를 어디에 기록할지
90
96
  task-type-map.md # 사용자 요청 → 작업 유형 매핑
@@ -141,6 +147,38 @@ leerness route new-install
141
147
  - release-checklist와 testing-strategy를 실제 프로젝트 기준으로 작성
142
148
  - session-handoff에 다음 정확한 작업 기록
143
149
 
150
+
151
+ ## 세션 종료 인수인계와 게으른 작업 방지
152
+
153
+ Leerness는 의미 있는 작업 세션이 끝날 때 AI가 다음 항목을 반드시 정리하도록 지시합니다.
154
+
155
+ ```bash
156
+ leerness route session-close
157
+ leerness session close
158
+ ```
159
+
160
+ 세션 종료 보고에는 다음이 포함되어야 합니다.
161
+
162
+ - 이번 세션에서 완료한 작업
163
+ - 사용자가 요청한 작업 중 아직 진행 중인 작업
164
+ - 사용자가 요청했지만 아직 미완료 또는 미시작인 작업
165
+ - 실행한 검증과 결과
166
+ - 변경한 파일과 갱신한 하네스 메모리
167
+ - 리스크, 가정, 블로커
168
+ - 추가로 진행하면 좋은 추천 방향
169
+ - 다음 세션에서 바로 수행할 단 하나의 정확한 작업
170
+
171
+ 관련 파일:
172
+
173
+ ```text
174
+ .harness/session-close-policy.md
175
+ .harness/progress-tracker.md
176
+ .harness/anti-lazy-work-policy.md
177
+ .harness/templates/end-of-session-report.md
178
+ ```
179
+
180
+ 이 정책은 AI가 “대충 완료”라고 말하거나, 검증하지 않은 작업을 완료로 표시하거나, 미완료 요청을 숨기는 것을 막기 위한 장치입니다.
181
+
144
182
  ## 스킬 라이브러리
145
183
 
146
184
  스킬은 한글명, 가능한 작업, 최종 업데이트일, AI 검증 상태를 표시합니다.
package/bin/harness.js CHANGED
@@ -5,9 +5,8 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const readline = require('readline');
7
7
  const childProcess = require('child_process');
8
- const os = require('os');
9
8
 
10
- const VERSION = '1.3.0';
9
+ const VERSION = '1.3.2';
11
10
  const MARK = '<!-- leerness:managed -->';
12
11
  const MIGRATED = '<!-- leerness:migrated-legacy -->';
13
12
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
@@ -17,103 +16,6 @@ const c = { reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m', green:'\x1b[32m', ye
17
16
 
18
17
  const legacyItems = ['AI_HARNESS.md','HARNESS.md','PROJECT_CONTEXT.md','CONTEXT.md','ARCHITECTURE.md','DECISIONS.md','CURRENT_STATE.md','TASK_LOG.md','AGENT.md','AGENTS.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md','docs/guideline.md','docs/history.md','.ai','harness','.harness'];
19
18
 
20
- const coreFiles = {
21
- 'AGENTS.md': [MARK,'# {{PROJECT}} AI Agent Harness','','Agent = Model + Leerness Harness.','','## Core Rule','Before editing, route the task. Read `.harness/context-routing.md` and use `leerness route <task-type>` when the task type is unclear.','','## Universal Read Order','1. .harness/project-brief.md','2. .harness/current-state.md','3. .harness/context-routing.md','4. .harness/writeback-policy.md','5. .harness/task-type-map.md','6. .harness/context-map.md','7. .harness/guardrails.md','8. .harness/skills-lock.json','','## Task Routing','- Feature/API work: read architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.','- UI/design work: read design-system.md, feature-contracts.md, skills/ui-consistency.md.','- Debugging: read task-log.md, current-state.md, skills/debugging.md, related feature contract.','- Refactoring: read architecture.md, decisions.md, guardrails.md, skills/refactoring.md.','- Release/deploy: read release-checklist.md, testing-strategy.md, current-state.md, decisions.md.','- Documentation/context work: read writeback-policy.md and update the correct memory files.','- Skill/library work: read AX_SKILL_LIBRARY_GUIDE.md and skills/ai-verified-skill-publisher when installed.','','## Writeback Rules','- Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.','- Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.','- Update feature-contracts.md when input/output/state/error behavior changes.','- Update design-system.md when UI rules, components, layout, spacing, or states change.','- Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.','- Update context-map.md when important files, modules, routes, commands, or ownership areas change.','- Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.','','## Operating Rules','- Preserve existing architecture, feature contracts, and design system unless the user explicitly asks to change them.','- Use the matching skill from .harness/skills when a task fits a known pattern.','- Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.','- Record variable names only; store real values in .env.local, CI secrets, or cloud secret manager.','','## Response Contract','- Task type and files consulted','- Summary','- Files changed','- Verification','- Memory files updated','- Risks or assumptions','- Next step','{{LEGACY_AGENT}}',''].join('\n'),
22
- 'CLAUDE.md': [MARK,'# Claude Code Instructions','','Use AGENTS.md as the source of truth. Route every task through .harness/context-routing.md and .harness/task-type-map.md. Before editing, read current-state.md, writeback-policy.md, context-map.md, guardrails.md, skills-lock.json, and the matching skill file.',''].join('\n'),
23
- '.cursor/rules/leerness.mdc': [MARK,'---','alwaysApply: true','---','Read AGENTS.md first. Follow .harness/context-routing.md, writeback-policy.md, installed skills, design-system, feature-contracts, and guardrails.',''].join('\n'),
24
- '.github/copilot-instructions.md': [MARK,'# GitHub Copilot Instructions','','Use AGENTS.md and .harness/ as the project memory. Route the task with context-routing.md, then preserve architecture, feature contracts, security rules, and UI consistency.',''].join('\n'),
25
- '.gitignore': ['# Leerness local secrets','.env','.env.local','*.secret.json','.harness/skill-config.local.json','.harness/skill-publish.local.json',''].join('\n'),
26
- '.env.example': ['# Leerness environment variable examples','# Copy to .env.local and fill values locally. Never commit real secrets.','',''].join('\n'),
27
- '.harness/HARNESS_VERSION': '{{VERSION}}\n',
28
- '.harness/manifest.json': '{{MANIFEST}}\n',
29
- '.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
30
- '.harness/skill-config.schema.json': [MARK,'{',' "$schema": "https://json-schema.org/draft/2020-12/schema",',' "title": "Leerness Skill Config",',' "type": "object",',' "properties": {',' "envSource": { "type": "string", "default": ".env.local" },',' "installedSkills": { "type": "object" }',' },',' "additionalProperties": true','}',''].join('\n'),
31
- '.harness/secret-policy.md': [MARK,'# Secret Policy','','## Never store in harness files','- API keys','- Access tokens','- Refresh tokens','- Passwords','- Cookies','- Private customer data','- Payment credentials','','## Allowed in harness files','- Environment variable names','- Secret manager key names','- Redacted examples','- Fake fixtures','','## Default locations','- Local: .env.local','- CI/CD: GitHub Actions Secrets or provider secrets','- Cloud: Secret Manager or runtime environment variables',''].join('\n'),
32
- '.harness/project-brief.md': [MARK,'---','leernessRole: project-brief','readWhen: [every-task, planning, product-direction, onboarding]','updateWhen: [purpose-change, user-change, success-criteria-change, product-direction-change]','doNotStore: [secrets, tokens, credentials, raw-customer-data]','---','','# Project Brief: {{PROJECT}}','','## When to read','Read this before meaningful work to understand why the project exists and what success means.','','## When to update','Update only when product purpose, target users, success criteria, scope, or strategic direction changes.','','## Purpose','','## Success Criteria','','## Users','','## Product Direction','{{LEGACY_BRIEF}}',''].join('\n'),
33
- '.harness/current-state.md': [MARK,'---','leernessRole: current-state','readWhen: [every-task, resume-work, planning, debugging, release]','updateWhen: [after-meaningful-work, blocker-change, next-step-change, status-change]','doNotStore: [secrets, tokens, credentials]','---','','# Current State','','Updated: {{DATE}}','','## When to read','Read at the start of every session to know where the project currently stands.','','## When to update','Update after meaningful work, changed blockers, or changed next steps.','','## Now','- Leerness v{{VERSION}} installed or migrated.','','## Next','- Fill project-brief, context-map, design-system, and feature-contracts.','','## Blockers','- None recorded.','{{LEGACY_STATE}}',''].join('\n'),
34
- '.harness/architecture.md': [MARK,'---','leernessRole: architecture','readWhen: [feature, refactor, integration, api, database, deployment]','updateWhen: [module-change, data-flow-change, integration-change, boundary-change]','doNotStore: [secrets, credentials]','---','','# Architecture','','## When to read','Read before structural changes, feature work touching multiple modules, integrations, API, database, or deployment changes.','','## When to update','Update when module boundaries, data flow, external services, or ownership changes.','','## Overview','','## Main Modules','','## Data Flow','','## External Services','','## Boundaries','{{LEGACY_ARCH}}',''].join('\n'),
35
- '.harness/context-map.md': [MARK,'---','leernessRole: context-map','readWhen: [every-task, file-discovery, impact-analysis]','updateWhen: [new-important-file, moved-module, new-route, new-service, ownership-change]','doNotStore: [secrets, tokens]','---','','# Context Map','','## When to read','Read when deciding which files or modules are relevant to a task.','','## When to update','Update when important files, routes, modules, services, or ownership areas change.','','| Area | Files | Notes |','|---|---|---|','| UI | src/components/**, app/** | Check design-system.md first. |','| API | src/api/**, server/**, functions/** | Preserve response contracts. |','| Data | db/**, firestore/**, prisma/** | Confirm migrations. |','| Tests | test/**, tests/**, __tests__/** | Add or update checks. |',''].join('\n'),
36
- '.harness/decisions.md': [MARK,'---','leernessRole: decisions','readWhen: [architecture, refactor, release, dependency-change, irreversible-change]','updateWhen: [important-decision, tradeoff, architecture-change, dependency-change, rollback-relevant-change]','doNotStore: [secrets, credentials]','---','','# Decision Log','','## When to read','Read before changing architecture, dependencies, deployment, data model, API contracts, or irreversible behavior.','','## When to update','Update when a decision affects future work or explains why an approach was chosen.','','## Template','','### YYYY-MM-DD — Title','- Decision:','- Reason:','- Alternatives:','- Impact:','{{LEGACY_DECISIONS}}',''].join('\n'),
37
- '.harness/task-log.md': [MARK,'---','leernessRole: task-log','readWhen: [debugging, regression, audit, resume-work]','updateWhen: [after-meaningful-work, bug-fix, release, migration, skill-learning]','doNotStore: [secrets, tokens, private-customer-data]','---','','# Task Log','','## When to read','Read to understand previous work, regressions, attempted fixes, and release/migration history.','','## When to update','Update after meaningful work with what changed and how it was verified.','','## {{DATE}}','- Installed Leerness v{{VERSION}}.',''].join('\n'),
38
- '.harness/constraints.md': [MARK,'# Constraints','','- Runtime/framework constraints','- Deployment constraints','- Security/privacy constraints','- Business rules',''].join('\n'),
39
- '.harness/guardrails.md': [MARK,'---','leernessRole: guardrails','readWhen: [every-task, security, auth, payment, db, refactor, release]','updateWhen: [new-risk, repeated-ai-mistake, policy-change, sensitive-area-change]','doNotStore: [secrets, tokens, credentials]','---','','# Guardrails','','## When to read','Read before every task, especially auth, payment, database, security, refactoring, release, or external integration work.','','## When to update','Update when a new risk, repeated mistake, project policy, or sensitive area rule is discovered.','','## Never','- Do not perform unrequested large rewrites.','- Do not change public API, database schema, auth, payment, or environment variable names without identifying impact.','- Do not hardcode secrets.','- Do not create a new design pattern when an existing one fits.','','## Always','- Inspect current structure first.','- Make the smallest safe change.','- Verify behavior.','- Update project memory after meaningful changes.',''].join('\n'),
40
- '.harness/design-system.md': [MARK,'---','leernessRole: design-system','readWhen: [ui, ux, component, styling, layout, accessibility]','updateWhen: [new-ui-pattern, changed-component-rule, visual-standard-change, state-pattern-change]','doNotStore: [secrets]','---','','# Design System Memory','','## When to read','Read before UI, UX, component, layout, responsive, accessibility, or styling changes.','','## When to update','Update when reusable UI rules, component variants, spacing, states, or accessibility patterns change.','','## Layout','- Reuse existing spacing, component variants, typography, and breakpoints.','','## Components','| Component | Purpose | Rules |','|---|---|---|','| Button | Primary actions | Reuse existing variants. |','| Card | Grouped content | Keep spacing and radius consistent. |','| Form | Input flows | Include loading, error, and empty states. |',''].join('\n'),
41
- '.harness/feature-contracts.md': [MARK,'---','leernessRole: feature-contracts','readWhen: [feature, api, ui-state, integration, debugging, regression]','updateWhen: [input-change, output-change, state-change, error-change, api-contract-change]','doNotStore: [secrets, raw-private-data]','---','','# Feature Contracts','','## When to read','Read before implementing or changing a feature, API, UI state, integration, or bug fix that touches behavior.','','## When to update','Update when inputs, outputs, states, errors, API contracts, or validation rules change.','','## Template','- Feature:','- Entry point:','- Input:','- Output:','- Error states:','- UI states:','- Related files:','- Tests:',''].join('\n'),
42
- '.harness/testing-strategy.md': [MARK,'---','leernessRole: testing-strategy','readWhen: [feature, debugging, release, refactor, migration]','updateWhen: [new-test-pattern, changed-critical-flow, recurring-bug, release-risk]','doNotStore: [secrets, private-test-data]','---','','# Testing Strategy','','## When to read','Read before feature, bug fix, refactor, migration, or release work to choose verification scope.','','## When to update','Update when a new test pattern, critical flow, regression risk, or verification standard appears.','','- Unit: pure logic and adapters','- Integration: API, DB, third-party providers','- E2E/manual: key user flows','- Regression: previously fixed bugs and successful skills',''].join('\n'),
43
- '.harness/review-checklist.md': [MARK,'---','leernessRole: review-checklist','readWhen: [before-final-answer, pull-request, code-review, handoff]','updateWhen: [new-review-risk, repeated-defect, quality-standard-change]','doNotStore: [secrets]','---','','# Review Checklist','','## When to read','Read before finalizing meaningful changes, PRs, reviews, or handoffs.','','## When to update','Update when recurring review failures or new quality gates are discovered.','','- [ ] Existing architecture preserved','- [ ] Feature contracts respected','- [ ] Design system followed','- [ ] Secrets not exposed','- [ ] Tests or manual verification completed','- [ ] current-state/task-log/session-handoff updated',''].join('\n'),
44
- '.harness/release-checklist.md': [MARK,'---','leernessRole: release-checklist','readWhen: [release, deploy, npm-publish, git-push, ci-cd, env-change]','updateWhen: [deployment-failure, new-env-var, rollback-change, ci-change, publish-rule-change]','doNotStore: [access-tokens, passwords, cookies, private-keys]','---','','# Release Checklist','','## When to read','Read before deployment, npm publish, git push release, CI/CD change, environment variable change, or migration.','','## When to update','Update when a release fails, a new release condition appears, an env var changes, CI/CD changes, or rollback steps change.','','- [ ] Build/test passed','- [ ] Env variables confirmed','- [ ] Migration impact checked','- [ ] Rollback path known','- [ ] Release notes prepared',''].join('\n'),
45
- '.harness/session-handoff.md': [MARK,'---','leernessRole: session-handoff','readWhen: [resume-work, new-session, handoff]','updateWhen: [end-of-session, interrupted-work, unresolved-risk, next-step-change]','doNotStore: [secrets, tokens]','---','','# Session Handoff','','## When to read','Read at the start of a new AI session or when continuing interrupted work.','','## When to update','Update at the end of meaningful work with the exact next step.','','## Done','-','','## Changed Files','-','','## Decisions','-','','## Risks','-','','## Next Exact Step','-',''].join('\n'),
46
- '.harness/skill-index.md': [MARK,'---','leernessRole: skill-index','readWhen: [skill, automation, integration, repeated-task, library]','updateWhen: [skill-installed, skill-removed, skill-updated, new-skill-learned]','doNotStore: [secrets, tokens]','---','','# Skill Index','','Installed skill libraries are tracked in `.harness/skills-lock.json`.','','## When to read','Read when a task resembles a known repeated pattern or domain workflow.','','## When to update','Update when skills are installed, removed, updated, learned, or migrated.','','## Commands','`leerness skill list` — 한글명/가능 작업/업데이트/검증 상태 표시','`leerness skill info <name>`','`leerness skill add commerce-api`','`leerness skill learn my-skill --from .harness/skills/...`','`leerness library verify .harness/library/my-skill --ai`','`leerness library build .harness/library/my-skill`','`leerness skill add ai-verified-skill-publisher`','`leerness library publish .harness/library/my-skill/dist/my-skill --target npm --execute`','','## Metadata','Every skill should expose version, displayNameKo, capabilities, lastUpdated, lastUpdatedAt, and verification status.',''].join('\n'),
47
- '.harness/context-routing.md': [MARK,'---','leernessRole: context-routing','readWhen: [every-task, task-classification, before-editing]','updateWhen: [new-task-type, changed-routing, new-required-file, repeated-missed-context]','doNotStore: [secrets, tokens]','---','','# Context Routing','','Use this file to decide which memory files to read before work and which files to update after work.','','## Universal baseline','','Read before every meaningful task:','- project-brief.md','- current-state.md','- context-routing.md','- writeback-policy.md','- task-type-map.md','- context-map.md','- guardrails.md','','## Task routes','','### feature','Read: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md, testing-strategy.md','Update: current-state.md, task-log.md, session-handoff.md, feature-contracts.md, context-map.md when file map changes','','### ui','Read: design-system.md, feature-contracts.md, context-map.md, skills/ui-consistency.md','Update: design-system.md, feature-contracts.md, current-state.md, task-log.md, session-handoff.md','','### debugging','Read: current-state.md, task-log.md, feature-contracts.md, testing-strategy.md, skills/debugging.md','Update: task-log.md, current-state.md, session-handoff.md, testing-strategy.md when regression coverage changes','','### refactor','Read: architecture.md, decisions.md, guardrails.md, testing-strategy.md, skills/refactoring.md','Update: architecture.md, decisions.md when reasoning matters, task-log.md, session-handoff.md','','### release','Read: release-checklist.md, testing-strategy.md, current-state.md, decisions.md, secret-policy.md','Update: release-checklist.md, task-log.md, current-state.md, session-handoff.md','','### migration','Read: AX_MIGRATION_GUIDE.md, architecture.md, decisions.md, release-checklist.md, testing-strategy.md','Update: current-state.md, task-log.md, session-handoff.md, context-map.md, release-checklist.md, decisions.md when needed','','### new-install','Read: AX_NEW_PROJECT_GUIDE.md, project-brief.md, context-map.md, guardrails.md','Update: project-brief.md, architecture.md, context-map.md, design-system.md, feature-contracts.md, current-state.md','','### skill-library','Read: AX_SKILL_LIBRARY_GUIDE.md, skill-index.md, skills-lock.json, secret-policy.md','Update: skill-index.md, skills-lock.json, task-log.md, session-handoff.md','',''].join('\n'),
48
- '.harness/writeback-policy.md': [MARK,'---','leernessRole: writeback-policy','readWhen: [every-task, documentation, memory-update, handoff]','updateWhen: [new-memory-rule, repeated-missing-update, changed-document-role]','doNotStore: [secrets, tokens]','---','','# Writeback Policy','','This file defines where project knowledge must be recorded.','','| Information | Write to | Notes |','|---|---|---|','| Current progress, blockers, next step | current-state.md | Update after meaningful work. |','| Work performed and verification | task-log.md | Date-based factual log. |','| Next-session handoff | session-handoff.md | Keep exact next action. |','| Structural decisions and tradeoffs | decisions.md | Include reason and impact. |','| File/module map | context-map.md | Update when files/routes/services change. |','| Feature behavior | feature-contracts.md | Inputs, outputs, states, errors, tests. |','| UI/UX rules | design-system.md | Reusable component and state rules. |','| Release/env/rollback gates | release-checklist.md | Update after publish/deploy findings. |','| Testing patterns | testing-strategy.md | Regression and verification strategy. |','| Repeated successful workflows | skill-index.md and .harness/skills/ | Never include secret values. |','','## Do not record','- Actual access tokens, passwords, cookies, private keys, customer private data','- Raw production exports unless redacted and explicitly allowed','- Temporary guesses that were disproved; move those to task-log as failed attempts if useful','',''].join('\n'),
49
- '.harness/task-type-map.md': [MARK,'---','leernessRole: task-type-map','readWhen: [every-task, task-classification, ambiguous-request]','updateWhen: [new-task-wording, new-domain-workflow, repeated-misclassification]','doNotStore: [secrets, tokens]','---','','# Task Type Map','','Map user requests to task types, then follow context-routing.md.','','| User request pattern | Task type | First files to consult |','|---|---|---|','| 새 기능, 구현, API 추가 | feature | feature-contracts.md, architecture.md |','| 디자인 맞춰줘, UI 수정, 반응형 | ui | design-system.md, feature-contracts.md |','| 오류, 에러, 안 됨, 원인 | debugging | task-log.md, current-state.md, debugging skill |','| 구조 개선, 리팩토링 | refactor | architecture.md, decisions.md, guardrails.md |','| 배포, npm publish, git push, 릴리즈 | release | release-checklist.md, testing-strategy.md |','| 구버전 하네스 반영, 이전 파일 정리 | migration | AX_MIGRATION_GUIDE.md, context-routing.md |','| 처음 설치, 기존 프로젝트 반영 | new-install | AX_NEW_PROJECT_GUIDE.md, project-brief.md |','| 스킬로 저장, 스킬 업로드, 라이브러리화 | skill-library | AX_SKILL_LIBRARY_GUIDE.md, skill-index.md |','| 문서 갱신, 맥락 정리 | documentation | writeback-policy.md, context-routing.md |','',''].join('\n'),
50
- '.harness/AX_MIGRATION_GUIDE.md': [MARK,'---','leernessRole: ax-migration-guide','readWhen: [migration, legacy-harness, old-version, upgrade]','updateWhen: [migration-rule-change, legacy-pattern-change, failed-migration]','doNotStore: [secrets, tokens, credentials]','---','','# AX Migration Guide for AI Agents','','Use this guide when upgrading an existing project that already has AGENTS.md, CLAUDE.md, .harness/, docs/guideline.md, old project-harness files, or older Leerness output.','','## Goal','Migrate without losing project memory. Preserve useful legacy content, remove ambiguity about source-of-truth files, and create routing/writeback rules so future work references the right files.','','## Required steps','1. Run `leerness migrate --dry-run` and inspect detected legacy files.','2. Back up existing harness files under `.harness/archive/legacy-migration-*`.','3. Generate or update context-routing.md, writeback-policy.md, task-type-map.md, AGENTS.md, and AX guides.','4. Move useful old content into the correct files: project purpose to project-brief.md, architecture to architecture.md, task history to task-log.md/current-state.md, release notes to release-checklist.md.','5. Replace stale instruction files with pointers to AGENTS.md and .harness/context-routing.md when safe.','6. Run `leerness status` and `leerness verify`.','7. Update session-handoff.md with what was migrated and what remains.','','## Mapping legacy content','| Legacy content | New destination |','|---|---|','| Project goal / product context | project-brief.md |','| Architecture / module map | architecture.md, context-map.md |','| Old task history | task-log.md, current-state.md |','| Release/deploy notes | release-checklist.md |','| AI rules / prompts | AGENTS.md, guardrails.md, context-routing.md |','| Successful repeated workflow | .harness/skills/ or skill library candidate |','','## Safety rules','- Do not delete legacy files before archive exists.','- Do not copy secret values into harness files. Replace with environment variable names.','- Do not merge contradictory rules silently; record conflict in decisions.md or session-handoff.md.','- Keep the current active source of truth obvious: AGENTS.md + .harness/context-routing.md.','',''].join('\n'),
51
- '.harness/AX_NEW_PROJECT_GUIDE.md': [MARK,'---','leernessRole: ax-new-project-guide','readWhen: [new-install, onboarding, first-run, existing-project-analysis]','updateWhen: [onboarding-rule-change, project-discovery-change, new-baseline-file]','doNotStore: [secrets, tokens, credentials]','---','','# AX New Project Installation Guide for AI Agents','','Use this guide after installing Leerness into a new or ongoing project. The goal is to reflect the real project, not leave generic templates.','','## Required project discovery','1. Identify package/framework/runtime from package.json, lockfiles, config files, and folder structure.','2. Identify entry points, routes, API handlers, DB/schema files, deployment config, test commands, and UI system.','3. Fill project-brief.md with purpose, users, success criteria, and product direction.','4. Fill architecture.md with modules, boundaries, data flow, and external services.','5. Fill context-map.md with exact file paths for UI, API, data, auth, deploy, tests, and docs.','6. Fill design-system.md from existing UI components and patterns.','7. Fill feature-contracts.md for important flows and APIs.','8. Fill release-checklist.md from actual deployment/npm/git/CI requirements.','9. Fill testing-strategy.md from actual scripts and manual flows.','10. Update current-state.md and session-handoff.md with the next exact action.','','## AI behavior','- Do not leave placeholders when the project contains enough information.','- Do not invent architecture; mark unknowns explicitly.','- Prefer exact file paths over vague folder names.','- Store environment variable names only, never values.','- When a repeated successful workflow is found, propose a skill candidate.','','## Completion checklist','- [ ] `leerness route feature` returns useful guidance.','- [ ] project-brief.md is specific to this project.','- [ ] context-map.md contains real paths.','- [ ] release-checklist.md matches actual deploy/publish flow.','- [ ] AGENTS.md directs future AI work to the right files.','- [ ] session-handoff.md has the next exact step.','',''].join('\n'),
52
- '.harness/AX_SKILL_LIBRARY_GUIDE.md': [MARK,
53
- '# Leerness AX Skill Library Guide',
54
- '',
55
- 'AX는 AI eXperience입니다. AI 에이전트가 검증된 스킬 데이터를 안전하게 학습, 검증, 빌드, 업로드, 업데이트, 병합, 마이그레이션하도록 안내합니다.',
56
- '',
57
- '## 스킬 라이브러리 표시 규격',
58
- '- 모든 스킬은 name, displayNameKo, title, capabilities, lastUpdated, verification을 가진다.',
59
- '- leerness skill list는 한글명과 가능한 작업을 먼저 보여준다.',
60
- '- AI가 스킬을 업로드할 때는 ai-verified-skill-publisher 스킬의 절차를 따른다.',
61
- '',
62
- '## 원칙',
63
- '- 실제 토큰, 쿠키, 비밀번호, 고객 데이터는 저장하지 않는다.',
64
- '- 환경변수 이름과 연결 규칙만 기록한다.',
65
- '- 스킬 업로드는 AI 검증 메타데이터가 있을 때만 허용한다.',
66
- '- 업데이트 또는 마이그레이션 후에는 verification.status를 needs-review로 되돌린다.',
67
- '- 각 스킬에는 lastUpdated, lastUpdatedAt, verification 정보를 둔다.',
68
- '',
69
- '## 표준 흐름',
70
- '1. 성공한 구현 결과를 스킬 후보로 정리한다.',
71
- '2. leerness skill learn <name> --from <path> 로 재사용 가능한 절차를 추출한다.',
72
- '3. leerness library validate <path> --strict-ai 로 구조와 민감정보를 검사한다.',
73
- '4. leerness library verify <path> --ai --reviewer leerness-ai 로 AI 검증 메타데이터를 기록한다.',
74
- '5. leerness library build <path> 로 배포 가능한 라이브러리를 만든다.',
75
- '6. leerness library publish <built-path> --target npm|git --execute 로 검증된 라이브러리만 업로드한다.',
76
- '',
77
- '## 필수 메타데이터',
78
- '- name',
79
- '- version',
80
- '- title',
81
- '- category',
82
- '- lastUpdated',
83
- '- lastUpdatedAt',
84
- '- sensitiveDataPolicy',
85
- '- requiresEnv',
86
- '- verification.status',
87
- '- verification.verifiedAt',
88
- '- verification.method',
89
- '',
90
- '## 업로드 차단 조건',
91
- '- verification.status가 passed가 아니다.',
92
- '- verification.method에 ai가 없다.',
93
- '- 민감정보 의심 패턴이 발견됐다.',
94
- '- lastUpdated 또는 lastUpdatedAt이 없다.',
95
- '- --execute 없이 실제 업로드를 시도했다.',
96
- '',
97
- '## AI 에이전트 체크리스트',
98
- '- 스킬 목적과 사용 조건이 명확한가',
99
- '- 구현 절차가 재현 가능한가',
100
- '- 민감정보가 아니라 환경변수 이름만 있는가',
101
- '- 검증 방법과 실패 대응법이 있는가',
102
- '- 병합 후 skills-lock.json에 출처와 검증 상태가 기록되는가',
103
- '',
104
- ].join('\n'),
105
- '.harness/skills/core/codebase-analysis.md': [MARK,'# Skill: Codebase Analysis','','1. Read current-state and context-map.','2. Locate related files.','3. Identify data flow and ownership.','4. Summarize risks before editing.',''].join('\n'),
106
- '.harness/skills/core/feature-implementation.md': [MARK,'# Skill: Feature Implementation','','1. Convert the requirement into a feature contract.','2. Find existing patterns.','3. Implement the smallest safe change.','4. Verify behavior.','5. Update harness memory.',''].join('\n'),
107
- '.harness/skills/core/refactoring.md': [MARK,'# Skill: Refactoring','','Keep behavior unchanged. Move in small steps. Preserve public contracts. Verify after each meaningful change.',''].join('\n'),
108
- '.harness/skills/core/debugging.md': [MARK,'# Skill: Debugging','','Separate symptom, reproduction, root cause, fix, and prevention. Record repeated failure patterns in guardrails.',''].join('\n'),
109
- '.harness/skills/core/ui-consistency.md': [MARK,'# Skill: UI Consistency','','Read design-system.md and existing similar screens before changing UI. Include loading, empty, error, desktop, and mobile states.',''].join('\n'),
110
- '.harness/skills/core/security-review.md': [MARK,'# Skill: Security Review','','Check secrets, auth boundaries, input validation, logging, dependency risk, and permission scope.',''].join('\n'),
111
- '.harness/skills/core/release-check.md': [MARK,'# Skill: Release Check','','Verify build, tests, env vars, migration impact, and rollback path.',''].join('\n'),
112
- '.harness/skills/core/documentation-update.md': [MARK,'# Skill: Documentation Update','','Update current-state, task-log, decisions, context-map, feature-contracts, design-system, and session-handoff when relevant.',''].join('\n'),
113
- '.harness/templates/session-summary.md': [MARK,'# Session Summary','','## Done','','## Files Changed','','## Verification','','## Next',''].join('\n'),
114
- '.harness/templates/decision.md': [MARK,'# Decision','','## Decision','','## Reason','','## Alternatives','','## Impact',''].join('\n')
115
- };
116
-
117
19
  function log(m=''){ console.log(m); }
118
20
  function ok(m){ log(c.green+'✓'+c.reset+' '+m); }
119
21
  function info(m){ log(c.cyan+'ℹ'+c.reset+' '+m); }
@@ -123,364 +25,311 @@ function exists(p){ return fs.existsSync(p); }
123
25
  function read(p){ return fs.readFileSync(p,'utf8'); }
124
26
  function write(p,s){ fs.mkdirSync(path.dirname(p),{recursive:true}); fs.writeFileSync(p,s,'utf8'); }
125
27
  function rel(root,p){ return path.relative(root,p).replace(/\\/g,'/') || '.'; }
126
- function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
127
28
  function parseJsonSafe(s,fallback){ try { return JSON.parse(s); } catch { return fallback; } }
128
- function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'맞춤성장형 AI 개발 하네스 · context, skills, design, consistency'+c.reset); log(''); }
129
- function installGuide(){ log(c.bold+'설치 안내'+c.reset); log(' - 기존 AI 하네스/지침 파일을 감지하면 먼저 .harness/archive/ 에 백업합니다.'); log(' - .harness/ 아래에 프로젝트 메모리, 스킬, 디자인/기능 계약 문서를 생성합니다.'); log(' - 스킬 라이브러리는 실제 민감정보를 저장하지 않고 환경변수 이름만 기록합니다.'); log(' - library publish는 기본 dry-run이며, 실제 업로드는 --execute가 필요합니다.'); log(' - 검증된 스킬팩 실제 업로드 시 npm/git 토큰을 환경변수·로컬 설정에서 찾고, 없으면 입력을 요구합니다.'); log(''); }
29
+ function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
30
+ function banner(){ log(''); log(c.bold+c.magenta+'Leerness v'+VERSION+c.reset); log(c.dim+'비파괴 마이그레이션 · context routing · session handoff'+c.reset); log(''); }
31
+ function installGuide(){
32
+ log(c.bold+'설치/마이그레이션 안내'+c.reset);
33
+ log(' - 기존 파일은 먼저 .harness/archive/ 에 백업합니다.');
34
+ log(' - project-brief/current-state/release-checklist 등 기존 프로젝트 메모리는 기본 보존합니다.');
35
+ log(' - .env.example과 .gitignore는 덮어쓰지 않고 필요한 항목만 병합합니다.');
36
+ log(' - AGENTS/CLAUDE/Cursor/Copilot 지침과 AX 라우팅 가이드는 최신 기준으로 갱신합니다.');
37
+ log(' - 기존 메모리 파일까지 강제로 템플릿 재생성하려면 --force를 명시하세요.');
38
+ log(' - 세션 종료 시 진행/미완료/추천 방향을 반드시 session-handoff에 남깁니다.');
39
+ log('');
40
+ }
130
41
  function projectName(root){ try{ const pkg=JSON.parse(read(path.join(root,'package.json'))); if(pkg.name) return String(pkg.name).replace(/^@[^/]+\//,''); }catch{} return path.basename(root); }
42
+ function now(){ return new Date().toISOString(); }
43
+ function today(){ return now().slice(0,10); }
44
+ function fill(t,ctx){ return t.replace(/{{([A-Z_]+)}}/g,(_,k)=>ctx[k]||''); }
45
+ function slug(s){ return String(s||'skill').toLowerCase().replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'') || 'skill'; }
131
46
 
132
- function detectLegacy(root){ return legacyItems.map(item=>({item,full:path.join(root,item)})).filter(e=>{ if(!exists(e.full)) return false; if(e.item==='.harness'){ const vf=path.join(root,'.harness/HARNESS_VERSION'); return !exists(vf) || read(vf).trim()!==VERSION; } try{ if(fs.statSync(e.full).isFile() && isTextFile(e.item)){ const b=read(e.full); if(b.includes(MARK)||b.includes(MIGRATED)) return false; } }catch{} return true; }); }
133
- function copyRecursive(src,dst,ignoreAbs=[]){ const abs=path.resolve(src); if(ignoreAbs.some(i=>abs===i||abs.startsWith(i+path.sep))) return; const st=fs.statSync(src); 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); } else { fs.mkdirSync(path.dirname(dst),{recursive:true}); fs.copyFileSync(src,dst); } }
47
+ function copyRecursive(src,dst,ignoreAbs=[]){
48
+ const abs=path.resolve(src); if(ignoreAbs.some(i=>abs===i||abs.startsWith(i+path.sep))) return;
49
+ const st=fs.statSync(src);
50
+ 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); }
51
+ else { fs.mkdirSync(path.dirname(dst),{recursive:true}); fs.copyFileSync(src,dst); }
52
+ }
53
+ function detectLegacy(root){
54
+ return legacyItems.map(item=>({item,full:path.join(root,item)})).filter(e=>{
55
+ if(!exists(e.full)) return false;
56
+ if(e.item==='.harness'){
57
+ const vf=path.join(root,'.harness/HARNESS_VERSION');
58
+ return !exists(vf) || read(vf).trim()!==VERSION;
59
+ }
60
+ try{
61
+ if(fs.statSync(e.full).isFile() && isTextFile(e.item)){
62
+ const b=read(e.full);
63
+ if(b.includes(MARK)||b.includes(MIGRATED)) return false;
64
+ }
65
+ }catch{}
66
+ return true;
67
+ });
68
+ }
134
69
  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; }
135
70
  function pick(obj,keys){ const out={}; for(const k of keys) if(obj[k]) out[k]=obj[k]; return out; }
136
71
  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'); }
137
- function archiveLegacy(root,found,dryRun){ if(!found.length) return null; const stamp=new Date().toISOString().replace(/[:.]/g,'-'); const archive=path.join(root,'.harness/archive/legacy-migration-'+stamp); if(dryRun) return archive; fs.mkdirSync(archive,{recursive:true}); const archiveRoot=path.resolve(path.join(root,'.harness/archive')); for(const f of found){ try{ const name=f.item==='.harness'?'.harness-before-v'+VERSION:f.item; copyRecursive(f.full,path.join(archive,name),[archiveRoot]); }catch(e){ warn('백업 실패: '+f.item+' ('+e.message+')'); } } write(path.join(archive,'migration-manifest.json'),JSON.stringify({version:VERSION,archivedAt:new Date().toISOString(),items:found.map(x=>x.item)},null,2)+'\n'); return archive; }
72
+ function archiveLegacy(root,found,dryRun){
73
+ if(!found.length) return null;
74
+ const stamp=now().replace(/[:.]/g,'-');
75
+ const archive=path.join(root,'.harness/archive/legacy-migration-'+stamp);
76
+ if(dryRun) return archive;
77
+ fs.mkdirSync(archive,{recursive:true});
78
+ const archiveRoot=path.resolve(path.join(root,'.harness/archive'));
79
+ for(const f of found){
80
+ try{ copyRecursive(f.full,path.join(archive,f.item==='.harness'?'.harness-before-v'+VERSION:f.item),[archiveRoot]); }
81
+ catch(e){ warn('백업 실패: '+f.item+' ('+e.message+')'); }
82
+ }
83
+ write(path.join(archive,'migration-manifest.json'),JSON.stringify({version:VERSION,archivedAt:now(),items:found.map(x=>x.item)},null,2)+'\n');
84
+ return archive;
85
+ }
138
86
  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'; }
139
- function neutralizeLegacy(root,found,dryRun){ for(const f of found){ if(f.item==='.harness'||coreFiles[f.item]) continue; try{ if(!fs.statSync(f.full).isFile()) continue; }catch{ continue; } const target=targetForLegacy(f.item); const body=[MIGRATED,'# Migrated legacy harness file','','Active source of truth: '+target,'','Original content was backed up under .harness/archive/.',''].join('\n'); if(dryRun) info('[dry-run] legacy pointer: '+f.item+' -> '+target); else write(f.full,body); } }
140
- function fill(t,ctx){ return t.replace(/{{([A-Z_]+)}}/g,(_,k)=>ctx[k]||''); }
141
- function manifest(root,selectedSkills){ return JSON.stringify({name:projectName(root),harnessVersion:VERSION,installedAt:new Date().toISOString(),managedFiles:Object.keys(coreFiles),selectedSkills},null,2); }
142
- function skillsLock(root,selectedSkills){ const lock={harnessVersion:VERSION,installedAt:new Date().toISOString(),installedSkills:{}}; for(const name of selectedSkills){ const meta=getSkillMeta(name); if(meta) lock.installedSkills[name]={version:meta.version,source:'bundled',title:meta.title}; } return JSON.stringify(lock,null,2); }
143
- function makeContext(root,legacyText,selectedSkills){ const date=new Date().toISOString().slice(0,10); return { PROJECT:projectName(root), DATE:date, VERSION, LEGACY_AGENT:legacyBlock('agent instructions',pick(legacyText,['AGENTS.md','AGENT.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md'])), LEGACY_BRIEF:legacyBlock('project context',pick(legacyText,['PROJECT_CONTEXT.md','CONTEXT.md','docs/guideline.md','AI_HARNESS.md','HARNESS.md'])), LEGACY_STATE:legacyBlock('state',pick(legacyText,['CURRENT_STATE.md','TASK_LOG.md','docs/history.md'])), LEGACY_ARCH:legacyBlock('architecture',pick(legacyText,['ARCHITECTURE.md'])), LEGACY_DECISIONS:legacyBlock('decisions',pick(legacyText,['DECISIONS.md'])), MANIFEST:manifest(root,selectedSkills), SKILLS_LOCK:skillsLock(root,selectedSkills) }; }
87
+ function noteLegacyPreserved(root,found,dryRun){
88
+ for(const f of found){
89
+ if(f.item==='.harness'||coreFiles[f.item]) continue;
90
+ try{ if(!fs.statSync(f.full).isFile()) continue; }catch{ continue; }
91
+ const target=targetForLegacy(f.item);
92
+ if(dryRun) info('[dry-run] legacy file preserved: '+f.item+' (suggested source: '+target+')');
93
+ else info('보존: '+f.item+' (참조 권장: '+target+')');
94
+ }
95
+ }
144
96
 
145
- 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)); }
146
- 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; }
147
- 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=new Date().toISOString(); 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'); }
148
- 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+'\n# '+(meta.title||meta.name)+' ('+meta.name+')\n'+missing.map(n=>n+'=').join('\n')+'\n'); }
149
- 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; }
150
- 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); }
97
+ const coreFiles = {
98
+ 'AGENTS.md': `${MARK}\n# {{PROJECT}} AI Agent Harness\n\nAgent = Model + Leerness Harness.\n\n## Core Rule\nBefore editing, route the task. Read .harness/context-routing.md and use \`leerness route <task-type>\` when the task type is unclear.\n\n## Universal Read Order\n1. .harness/project-brief.md\n2. .harness/current-state.md\n3. .harness/context-routing.md\n4. .harness/writeback-policy.md\n5. .harness/task-type-map.md\n6. .harness/context-map.md\n7. .harness/guardrails.md\n8. .harness/skills-lock.json\n\n## Task Routing\n- Feature/API work: architecture.md, feature-contracts.md, context-map.md, skills/feature-implementation.md.\n- UI/design work: design-system.md, feature-contracts.md, skills/ui-consistency.md.\n- Debugging: task-log.md, current-state.md, skills/debugging.md, related feature contract.\n- Refactoring: architecture.md, decisions.md, guardrails.md, skills/refactoring.md.\n- Release/deploy: release-checklist.md, testing-strategy.md, current-state.md, decisions.md.\n- Migration: AX_MIGRATION_GUIDE.md, context-routing.md, writeback-policy.md.\n- New install: AX_NEW_PROJECT_GUIDE.md and actual project config/source files.\n- Skill/library work: AX_SKILL_LIBRARY_GUIDE.md and ai-verified-skill-publisher when installed.\n\n## Writeback Rules\n- Always update current-state.md, task-log.md, and session-handoff.md after meaningful work.\n- Update decisions.md when a structural, technology, API, schema, deployment, or irreversible decision is made.\n- Update feature-contracts.md when input/output/state/error behavior changes.\n- Update design-system.md when UI rules, components, layout, spacing, or states change.\n- Update release-checklist.md when deployment, environment variables, rollback, CI, npm, or git release requirements change.\n- Update context-map.md when important files, modules, routes, commands, or ownership areas change.\n- Update project-brief.md only when product purpose, target users, success criteria, or project direction changes.\n\n## Non-Destructive Migration Policy\n- Never overwrite existing project memory files unless the user explicitly requests --force.\n- Preserve .env.example and .gitignore; append missing Leerness entries only.\n- Keep secrets, tokens, cookies, credentials, and customer private data out of harness files.\n\n## End-of-Session Contract
99
+ Every meaningful session must close with a handoff. Do not stop at \"done\". Before the final answer, check .harness/session-close-policy.md, .harness/progress-tracker.md, and .harness/anti-lazy-work-policy.md.
151
100
 
152
- 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']); 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; }
153
- 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); }
154
- 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());})); }
155
- 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); }
101
+ At the end of each session, list:
102
+ 1. Completed work in this session.
103
+ 2. User-requested work still in progress.
104
+ 3. User-requested work not started or incomplete.
105
+ 4. Verification performed and results.
106
+ 5. Memory files updated.
107
+ 6. Risks, assumptions, or blockers.
108
+ 7. Recommended next directions.
109
+ 8. The single next exact action.
156
110
 
157
- async function init(root,flags){ root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root); const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills); const found=detectLegacy(root); const legacyText=collectLegacyText(found); if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); } const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive)); neutralizeLegacy(root,found,false); const ctx=makeContext(root,legacyText,selectedSkills); for(const [file,template] of Object.entries(coreFiles)){ const target=path.join(root,file); const body=fill(template,ctx); if(exists(target)&&read(target)===body){ ok('유지: '+file); continue; } const existed=exists(target); if(file==='.gitignore'&&existed){ const current=read(target); const additions=body.split('\n').filter(line=>line&&!current.includes(line)).join('\n'); if(additions) write(target,current.replace(/\s*$/,'\n')+additions+'\n'); ok('보강: .gitignore'); continue; } write(target,body); ok((existed?'업데이트: ':'생성: ')+file); } if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); } log(''); ok('설치 완료'); log('다음 단계: .harness/project-brief.md, context-map.md, design-system.md를 프로젝트에 맞게 채우세요.'); }
158
- function migrate(root,flags){ root=path.resolve(root||process.cwd()); banner(); installGuide(); const dryRun=Boolean(flags['dry-run']); const found=detectLegacy(root); if(!found.length){ ok('마이그레이션할 legacy 항목이 없습니다.'); return; } warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); const archive=archiveLegacy(root,found,dryRun); info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive)); if(!dryRun) neutralizeLegacy(root,found,false); const ctx=makeContext(root,collectLegacyText(found),[]); for(const [file,template] of Object.entries(coreFiles)){ const target=path.join(root,file); if(dryRun) info('[dry-run] create/update: '+file); else write(target,fill(template,ctx)); } if(!dryRun) ok('마이그레이션 완료'); }
159
- 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(', '):'없음')); }
160
- 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('검증 완료'); }
111
+ ## Anti-Lazy Work Rule
112
+ - Do not hide unfinished work behind vague summaries.
113
+ - Do not claim completion without verification or explicit limits.
114
+ - If partial, say exactly what is partial and what remains.
115
+ - Prefer concrete file names, commands, and checks over generic phrases.
116
+ - If tests or verification were skipped, state why and what should be run next.
161
117
 
162
- function slugifyName(value){ return String(value||'').trim().toLowerCase().replace(/^@[^/]+\//,'').replace(/[^a-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'custom-skill'; }
163
- function packageSafeName(value){ const raw=String(value||'').trim(); if(raw.startsWith('@')) return raw; return 'leerness-skill-'+slugifyName(raw).replace(/^leerness-skill-/,'').replace(/^harness-skill-/,''); }
164
- function isInside(parent,child){ const r=path.relative(path.resolve(parent),path.resolve(child)); return r&&!r.startsWith('..')&&!path.isAbsolute(r); }
165
- function scanSensitiveText(body){ const patterns=[{name:'OpenAI style API key',re:/sk-[a-zA-Z0-9_-]{20,}/g},{name:'GitHub token',re:/gh[pousr]_[a-zA-Z0-9_]{20,}/g},{name:'npm token',re:/npm_[a-zA-Z0-9]{20,}/g},{name:'AWS access key',re:/AKIA[0-9A-Z]{16}/g},{name:'private key block',re:/-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g},{name:'password assignment',re:/(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/ig},{name:'secret assignment',re:/(?:secret|token|api[_-]?key)\s*[:=]\s*['"][^'"]{12,}['"]/ig}]; const findings=[]; for(const p of patterns){ let m; while((m=p.re.exec(body))) findings.push({type:p.name,index:m.index,sample:m[0].slice(0,16)+'...'}); } return findings; }
166
- function scanSensitivePath(target){ const findings=[]; function scan(p){ if(!exists(p)) return; const st=fs.statSync(p); if(st.isDirectory()){ for(const n of fs.readdirSync(p)){ if(['node_modules','.git','dist','coverage'].includes(n)) continue; scan(path.join(p,n)); } } else if(st.isFile()&&isTextFile(p)){ for(const f of scanSensitiveText(read(p))) findings.push({file:p,...f}); } } scan(target); return findings; }
167
- function skillLibraryFiles(dir){ const files=[]; function walk(p){ if(!exists(p)) return; const st=fs.statSync(p); if(st.isDirectory()){ for(const n of fs.readdirSync(p)){ if(['node_modules','.git','dist','coverage'].includes(n)) continue; walk(path.join(p,n)); } } else if(st.isFile()) files.push(p); } walk(dir); return files; }
168
- 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, packageName:data.name, description:data.description||'', requiresEnv:data.harnessSkill?.requiresEnv||[], files:data.harnessSkill?.files||[] }; return data; } return null; }
169
- function validateSkillLibrary(dir,options={}){ dir=path.resolve(dir); let failures=0; const meta=readSkillLibraryMeta(dir); if(!meta||!meta.name){ fail('skill-library.json 또는 skill.json에 name이 필요합니다.'); failures++; } const sd=path.join(dir,'skills'); if(!exists(sd)){ fail('skills/ 폴더가 필요합니다.'); failures++; } else if(!skillLibraryFiles(sd).some(f=>f.endsWith('.md'))){ fail('skills/*.md 파일이 최소 1개 필요합니다.'); failures++; } const findings=scanSensitivePath(dir); if(findings.length){ fail('민감정보 의심 패턴 감지. 배포/빌드가 차단됩니다.'); findings.slice(0,20).forEach(f=>warn(rel(dir,f.file)+' · '+f.type+' · '+f.sample)); if(findings.length>20) warn('추가 '+(findings.length-20)+'건 생략'); failures+=findings.length; } for(const env of new Set(meta?.requiresEnv||[])){ if(!/^[A-Z][A-Z0-9_]*$/.test(env)) warn('환경변수 이름 형식 확인 필요: '+env); } if(!options.silent){ if(failures) fail('스킬 라이브러리 검증 실패: '+failures); else ok('스킬 라이브러리 검증 완료: '+(meta?.name||path.basename(dir))); } return {ok:failures===0,failures,meta,findings}; }
170
- function inferEnvNamesFromText(body){ const names=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)) names.add(v); } return Array.from(names).sort(); }
171
- function learnSkillLibrary(root,flags){ root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slugifyName(flags.name||flags.category||path.basename(from)); const version=String(flags.version||'0.1.0'); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name)); if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; } const sourceFiles=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); if(!sourceFiles.length){ fail('학습 가능한 텍스트 스킬 파일이 없습니다.'); process.exitCode=1; return; } fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const requiresEnv=new Set(); const copied=[]; for(const f of sourceFiles){ const body=read(f); for(const e of inferEnvNamesFromText(body)) requiresEnv.add(e); const base=path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); const destName=base.endsWith('.md')?base:base+'.md'; const dest=path.join(outRoot,'skills',destName); const header=[MARK,'# Learned Skill: '+destName.replace(/\.md$/,''),'','Source: '+rel(root,f),'Learned: '+new Date().toISOString(),''].join('\n'); write(dest,body.includes(MARK)?body:header+'\n'+body); copied.push('skills/'+destName); } const meta={name,version,title:flags.title||name,description:flags.description||'Verified project skill library extracted from a successful implementation.',category:flags.category||'custom',compatibleHarness:'>=3.2.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(requiresEnv).sort(),files:copied,learnedFrom:rel(root,from),learnedAt:new Date().toISOString()}; 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\n## Policy\n\nThis library stores environment variable names only. Do not commit real secrets.\n\n## Required env\n\n'+(meta.requiresEnv.map(e=>'- '+e).join('\n')||'- None')+'\n'); write(path.join(outRoot,'env.example'),meta.requiresEnv.map(e=>e+'=').join('\n')+(meta.requiresEnv.length?'\n':'')); const result=validateSkillLibrary(outRoot,{silent:true}); if(!result.ok){ fail('학습 결과에 민감정보 또는 구조 문제가 있어 확인이 필요합니다: '+outRoot); process.exitCode=1; return; } ok('검증된 스킬 라이브러리 학습 완료: '+outRoot); info('다음: leerness library build '+outRoot); }
172
- function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok){ process.exitCode=1; return; } const meta=check.meta; const out=path.resolve(flags.out||path.join(dir,'dist')); const packageName=flags.package||meta.packageName||packageSafeName(meta.name); const libRoot=path.join(out,slugifyName(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','env.example','skills','examples','migrations']){ const src=path.join(dir,item); if(exists(src)) copyRecursive(src,path.join(libRoot,item)); } if(!exists(path.join(libRoot,'skill-library.json'))&&exists(path.join(libRoot,'skill.json'))) fs.copyFileSync(path.join(libRoot,'skill.json'),path.join(libRoot,'skill-library.json')); const pkg={name:packageName,version:meta.version||'0.1.0',description:meta.description||meta.title||meta.name,type:'commonjs',files:['skill-library.json','README.md','env.example','skills/','examples/','migrations/'],keywords:['leerness','harness-skill','ai-skill-library',meta.category||'custom'].filter(Boolean),license:'MIT',publishConfig:{access:'public'},harnessSkill:{name:meta.name,version:meta.version,title:meta.title,requiresEnv:meta.requiresEnv||[],sensitiveDataPolicy:meta.sensitiveDataPolicy||'env-reference-only',compatibleHarness:meta.compatibleHarness||'>=3.2.0'}}; write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n'); ok('스킬 라이브러리 빌드 완료: '+libRoot); info('npm 배포: leerness library publish '+libRoot+' --target npm --execute'); info('git 배포: leerness library publish '+libRoot+' --target git --repo <git-url> --execute'); }
173
- function mergeSkillLibrary(root,source,flags){ root=path.resolve(root||process.cwd()); source=path.resolve(source||flags.source||''); if(!source||!exists(source)){ fail('병합할 스킬 라이브러리 경로가 필요합니다.'); process.exitCode=1; return; } const check=validateSkillLibrary(source,{silent:false}); if(!check.ok){ process.exitCode=1; return; } const meta=check.meta; const name=slugifyName(meta.name); const dest=path.join(root,'.harness/skills',name); fs.mkdirSync(dest,{recursive:true}); const srcSkills=path.join(source,'skills'); if(exists(srcSkills)) copyRecursive(srcSkills,dest); write(path.join(dest,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); updateSkillLock(root,{name,version:meta.version||'0.1.0',title:meta.title||name,requiresEnv:meta.requiresEnv||[],source:'library'},false); appendEnvExample(root,{name,title:meta.title||name,requiresEnv:meta.requiresEnv||[]}); ok('스킬 라이브러리 병합 완료: '+rel(root,dest)); }
174
- function migrateSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); if(!exists(dir)){ fail('마이그레이션 대상 경로가 없습니다: '+dir); process.exitCode=1; return; } const meta=readSkillLibraryMeta(dir)||{}; const migrated={name:slugifyName(flags.name||meta.name||path.basename(dir)),version:String(flags.version||meta.version||'0.1.0'),title:flags.title||meta.title||meta.description||path.basename(dir),description:flags.description||meta.description||'Migrated Leerness skill library.',category:flags.category||meta.category||'custom',compatibleHarness:'>=3.2.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(new Set(meta.requiresEnv||meta.harnessSkill?.requiresEnv||[])),migratedAt:new Date().toISOString()}; const skillsDir=path.join(dir,'skills'); if(!exists(skillsDir)) fs.mkdirSync(skillsDir,{recursive:true}); const mdFiles=skillLibraryFiles(dir).filter(f=>f.endsWith('.md')&&!isInside(skillsDir,f)&&!f.includes(path.sep+'node_modules'+path.sep)); for(const f of mdFiles){ if(path.basename(f).toLowerCase()==='readme.md') continue; const dest=path.join(skillsDir,path.basename(f)); if(!exists(dest)) fs.copyFileSync(f,dest); } migrated.files=skillLibraryFiles(skillsDir).filter(f=>f.endsWith('.md')).map(f=>rel(dir,f)); write(path.join(dir,'skill-library.json'),JSON.stringify(migrated,null,2)+'\n'); if(!exists(path.join(dir,'README.md'))) write(path.join(dir,'README.md'),'# '+migrated.title+'\n\n'+migrated.description+'\n'); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok) process.exitCode=1; else ok('스킬 라이브러리 마이그레이션 완료: '+dir); }
175
- function publishSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok){ process.exitCode=1; return; } if(target==='npm'){ if(!exists(path.join(dir,'package.json'))){ warn('package.json이 없습니다. 먼저 build를 실행하세요.'); info('leerness library build '+dir); process.exitCode=1; return; } const args=['publish','--access','public'].concat(flags.registry?['--registry',flags.registry]:[]); if(!execute){ info('[dry-run] 실행 예정: (cd '+dir+') npm '+args.join(' ')); info('실제 배포는 --execute를 붙이세요.'); return; } const r=childProcess.spawnSync('npm',args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); process.exitCode=r.status||0; return; } if(target==='git'){ const repo=flags.repo; const branch=flags.branch||'main'; const message=flags.message||('Publish skill library '+check.meta.name+'@'+(check.meta.version||'0.1.0')); if(!execute){ info('[dry-run] git target repo: '+(repo||'(current repo)')); info('[dry-run] branch: '+branch); info('[dry-run] commit message: '+message); info('실제 push는 --execute를 붙이세요.'); return; } 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(repo&&!exists(path.join(dir,'.git'))){ run('git',['init']); run('git',['remote','add','origin',repo]); } run('git',['add','.']); run('git',['commit','-m',message]); run('git',['branch','-M',branch]); run('git',['push','-u','origin',branch]); return; } fail('지원하지 않는 publish target: '+target); process.exitCode=1; }
176
- function libraryCommand(args,flags){ const sub=args[1]||'help'; if(sub==='help'){ log(['Leerness Skill Library Commands','',' leerness skill learn <name> --from .harness/skills/<name> [--out ./library/<name>]',' leerness library validate <path>',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]','','기본 publish는 dry-run입니다. 실제 npm/git 업로드는 --execute가 필요합니다.',''].join('\n')); return; } if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false}); if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags); if(sub==='merge') return mergeSkillLibrary(flags.path||process.cwd(),args[2]||flags.source,flags); if(sub==='migrate') return migrateSkillLibrary(args[2]||process.cwd(),flags); if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags); fail('알 없는 library 명령: '+sub); process.exitCode=1; }
177
- function skillCommand(args,flags){ const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd()); if(sub==='learn'){ flags.name=args[2]||flags.name; return learnSkillLibrary(root,flags); } if(sub==='library') return libraryCommand(['library'].concat(args.slice(2)),flags); if(sub==='list'){ banner(); log('사용 가능한 스킬 라이브러리'); for(const p of listSkillPacks()){ log('- '+p.name+'@'+p.version+': '+p.title); log(' '+p.description); if((p.requiresEnv||[]).length) log(' env: '+(p.requiresEnv||[]).join(', ')); } return; } const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill add commerce-api'); return; } if(sub==='add'||sub==='install') return installSkill(root,name,Boolean(flags['dry-run'])); if(sub==='remove'||sub==='rm') return removeSkill(root,name); if(sub==='update') return installSkill(root,name,false); fail('알 수 없는 skill 명령: '+sub); }
178
- function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills office,commerce-api|recommended|all]',' leerness migrate [path] [--dry-run]',' leerness status [path]',' leerness verify [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation>','','Skills:',' leerness skill list',' leerness skill info <name>',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill update <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path> [--out <library-path>]','','Skill library lifecycle:',' leerness library validate <path>',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]',' leerness --version','','Examples:',' npx leerness init --skills recommended',' npx leerness skill learn coupang-order-sync --from .harness/skills/commerce-api/order-sync.md',' npx leerness library build .harness/library/coupang-order-sync',' npx leerness library publish .harness/library/coupang-order-sync/dist/coupang-order-sync --target npm --execute',''].join('\n')); }
118
+ ## Response Contract
119
+ - Task type and files consulted
120
+ - Summary
121
+ - Completed work
122
+ - In-progress work
123
+ - Incomplete requested work
124
+ - Files changed
125
+ - Verification
126
+ - Memory files updated
127
+ - Risks or assumptions
128
+ - Recommended next directions
129
+ - Next exact step
130
+ {{LEGACY_AGENT}}
131
+ `,
132
+ '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`,
133
+ '.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`,
134
+ '.github/copilot-instructions.md': `${MARK}\n# GitHub Copilot Instructions\n\nUse AGENTS.md and .harness/ as the project memory. Preserve existing project memory files unless --force is explicit.\n`,
135
+ '.gitignore': `# Leerness local-only files\n.env\n.env.local\n*.secret.json\n.harness/skill-config.local.json\n.harness/skill-publish.local.json\n`,
136
+ '.env.example': `# Leerness examples only. Copy to .env.local and fill locally. Never commit real secrets.\n`,
137
+ '.harness/HARNESS_VERSION': '{{VERSION}}\n',
138
+ '.harness/manifest.json': '{{MANIFEST}}\n',
139
+ '.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
140
+ '.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`,
141
+ '.harness/project-brief.md': `${MARK}\n---\nleernessRole: project-brief\nreadWhen: [every-task, planning, product-direction, onboarding]\nupdateWhen: [purpose-change, user-change, success-criteria-change, product-direction-change]\ndoNotStore: [secrets, tokens, credentials, raw-customer-data]\n---\n\n# Project Brief: {{PROJECT}}\n\n## Purpose\n\n## Success Criteria\n\n## Users\n\n## Product Direction\n{{LEGACY_BRIEF}}\n`,
142
+ '.harness/current-state.md': `${MARK}\n---\nleernessRole: current-state\nreadWhen: [every-task, resume-work, planning, debugging, release]\nupdateWhen: [after-meaningful-work, blocker-change, next-step-change, status-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Current State\n\nUpdated: {{DATE}}\n\n## Now\n- Leerness v{{VERSION}} installed or migrated.\n\n## Next\n- Fill project-brief, context-map, design-system, and feature-contracts.\n\n## Blockers\n- None recorded.\n{{LEGACY_STATE}}\n`,
143
+ '.harness/architecture.md': `${MARK}\n---\nleernessRole: architecture\nreadWhen: [feature, refactor, integration, api, database, deployment]\nupdateWhen: [module-change, data-flow-change, integration-change, boundary-change]\ndoNotStore: [secrets, credentials]\n---\n\n# Architecture\n\n## Overview\n\n## Main Modules\n\n## Data Flow\n\n## External Services\n\n## Boundaries\n{{LEGACY_ARCH}}\n`,
144
+ '.harness/context-map.md': `${MARK}\n---\nleernessRole: context-map\nreadWhen: [every-task, file-discovery, impact-analysis]\nupdateWhen: [new-important-file, moved-module, new-route, new-service, ownership-change]\ndoNotStore: [secrets, tokens]\n---\n\n# Context Map\n\n| Area | Files | Notes |\n|---|---|---|\n| UI | src/components/**, app/** | Check design-system.md first. |\n| API | src/api/**, server/**, functions/** | Preserve response contracts. |\n| Data | db/**, firestore/**, prisma/** | Confirm migrations. |\n| Tests | test/**, tests/**, __tests__/** | Add or update checks. |\n`,
145
+ '.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`,
146
+ '.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`,
147
+ '.harness/constraints.md': `${MARK}\n# Constraints\n\n- Runtime/framework/deployment constraints\n- Security/privacy/business constraints\n`,
148
+ '.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`,
149
+ '.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`,
150
+ '.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`,
151
+ '.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`,
152
+ '.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`,
153
+ '.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`,
154
+ '.harness/session-handoff.md': `${MARK}
155
+ ---
156
+ leernessRole: session-handoff
157
+ readWhen: [resume-work, every-new-session, end-of-session]
158
+ updateWhen: [end-of-session, handoff, blocked-work, partial-completion]
159
+ doNotStore: [secrets, tokens, raw-private-data]
160
+ ---
179
161
 
162
+ # Session Handoff
180
163
 
181
- function nowIso(){ return new Date().toISOString(); }
182
- function dateOnly(iso){ return String(iso||nowIso()).slice(0,10); }
183
- function normalizeSkillMeta(meta, fallbackName){
184
- meta = meta || {};
185
- const updated = meta.lastUpdatedAt || meta.updatedAt || meta.learnedAt || meta.migratedAt || nowIso();
186
- meta.name = slugifyName(meta.name || fallbackName || 'custom-skill');
187
- meta.version = String(meta.version || '0.1.0');
188
- meta.title = meta.title || meta.description || meta.name;
189
- meta.category = meta.category || 'custom';
190
- meta.compatibleHarness = meta.compatibleHarness || '>=1.0.0';
191
- meta.sensitiveDataPolicy = meta.sensitiveDataPolicy || 'env-reference-only';
192
- meta.requiresEnv = Array.from(new Set(meta.requiresEnv || meta.harnessSkill?.requiresEnv || [])).sort();
193
- meta.lastUpdatedAt = updated;
194
- meta.lastUpdated = meta.lastUpdated || dateOnly(updated);
195
- meta.verification = meta.verification || { status:'needs-review', method:'none', verifiedBy:null, verifiedAt:null, checks:[] };
196
- return meta;
197
- }
198
- function isAiVerified(meta){
199
- const v = (meta||{}).verification || {};
200
- return v.status === 'passed' && /ai/i.test(String(v.method||'')) && Boolean(v.verifiedAt);
201
- }
202
- function verificationLabel(meta){
203
- const v=(meta||{}).verification||{};
204
- if(isAiVerified(meta)) return 'AI verified '+String(v.verifiedAt).slice(0,10);
205
- return v.status || 'needs-review';
164
+ ## Session Summary
165
+ - Date:
166
+ - Task type:
167
+ - User request:
168
+
169
+ ## Completed This Session
170
+ -
171
+
172
+ ## In Progress From User Requests
173
+ -
174
+
175
+ ## Incomplete / Not Started From User Requests
176
+ -
177
+
178
+ ## Files Changed
179
+ -
180
+
181
+ ## Verification Performed
182
+ - Command/check:
183
+ - Result:
184
+
185
+ ## Memory Files Updated
186
+ -
187
+
188
+ ## Risks / Assumptions / Blockers
189
+ -
190
+
191
+ ## Recommended Next Directions
192
+ -
193
+
194
+ ## Next Exact Step
195
+ -
196
+ `,
197
+ '.harness/session-close-policy.md': `${MARK}\n---\nleernessRole: session-close-policy\nreadWhen: [end-of-session, every-final-response, partial-completion, handoff]\nupdateWhen: [session-close-format-change, repeated-handoff-failure, reporting-standard-change]\ndoNotStore: [secrets, tokens, credentials, raw-private-data]\n---\n\n# Session Close Policy\n\nEvery meaningful AI work session must end with a concrete handoff. This prevents hidden unfinished work and makes the next session restartable.\n\n## Required Final Checklist\n\nBefore the final answer, the AI must inspect whether the session had meaningful work. If yes, it must provide or update:\n\n1. Completed work in this session.\n2. User-requested work still in progress.\n3. User-requested work incomplete or not started.\n4. Verification performed and exact results.\n5. Files or documents changed.\n6. Harness memory files updated.\n7. Risks, assumptions, blockers, or skipped checks.\n8. Recommended next directions.\n9. The single next exact action.\n\n## Required Memory Writeback\n\nUpdate these files when meaningful work occurred:\n\n- current-state.md\n- task-log.md\n- session-handoff.md\n\nUpdate these when relevant:\n\n- decisions.md\n- feature-contracts.md\n- design-system.md\n- release-checklist.md\n- context-map.md\n- progress-tracker.md\n\n## Completion Labels\n\nUse one of these labels for each requested item:\n\n- done\n- in-progress\n- blocked\n- incomplete\n- skipped-with-reason\n\nNever mark work done if verification was not performed or if key requested scope remains unfinished.\n`,
198
+ '.harness/progress-tracker.md': `${MARK}\n---\nleernessRole: progress-tracker\nreadWhen: [planning, resume-work, end-of-session, multi-step-work]\nupdateWhen: [task-started, task-completed, task-blocked, scope-change, end-of-session]\ndoNotStore: [secrets, tokens, credentials, raw-private-data]\n---\n\n# Progress Tracker\n\nUse this file to track user-requested work across sessions. Keep entries concrete and checkable.\n\n| ID | User Request | Status | Owner | Last Update | Evidence / Notes | Next Action |\n|---|---|---|---|---|---|---|\n| T-0001 | Initialize Leerness project memory | done | AI | {{DATE}} | Leerness v{{VERSION}} installed or migrated. | Fill project-specific details. |\n\n## Status Values\n\n- requested\n- in-progress\n- done\n- blocked\n- incomplete\n- skipped-with-reason\n`,
199
+ '.harness/anti-lazy-work-policy.md': `${MARK}\n---\nleernessRole: anti-lazy-work-policy\nreadWhen: [every-task, end-of-session, verification, planning]\nupdateWhen: [quality-failure, repeated-shortcut, missed-verification, reporting-rule-change]\ndoNotStore: [secrets, tokens, credentials]\n---\n\n# Anti-Lazy Work Policy\n\nThe AI must not appear productive while leaving important work vague or incomplete.\n\n## Required Behavior\n\n- State exactly what was done and what was not done.\n- Prefer concrete file paths, commands, checks, and outputs.\n- Do not skip obvious verification when tools are available.\n- If a check cannot be run, say so and provide the exact command to run.\n- Do not collapse multiple unfinished user requests into a generic sentence.\n- Do not overwrite project memory to avoid doing the harder merge.\n- Do not call a task complete only because files were generated. Confirm behavior or clearly label it unverified.\n\n## Laziness Warning Signs\n\n- done without changed files or verification.\n- should work without a check.\n- No mention of incomplete user-requested items.\n- No next exact action.\n- Memory files not updated after meaningful work.\n\n## Minimum Final Answer Standard\n\nA final answer after meaningful work must include:\n\n- Completed\n- In progress\n- Incomplete\n- Verification\n- Updated memory\n- Risks\n- Recommended next directions\n`,
200
+ '.harness/templates/end-of-session-report.md': `${MARK}\n# End-of-Session Report\n\n## Completed This Session\n-\n\n## In Progress From User Requests\n-\n\n## Incomplete / Not Started From User Requests\n-\n\n## Verification\n-\n\n## Files Changed\n-\n\n## Memory Files Updated\n-\n\n## Risks / Assumptions / Blockers\n-\n\n## Recommended Next Directions\n-\n\n## Next Exact Step\n-\n`,
201
+ '.harness/skill-index.md': `${MARK}\n# Skill Index\n\n| Task | Skill |\n|---|---|\n| Codebase analysis | skills/codebase-analysis.md |\n| Feature implementation | skills/feature-implementation.md |\n| Debugging | skills/debugging.md |\n| UI consistency | skills/ui-consistency.md |\n| Release | skills/release-check.md |\n`,
202
+ '.harness/context-routing.md': `${MARK}\n# Context Routing\n\nUse this file to decide what to read before work and what to update afterward.\n\n## feature\nRead: project-brief, current-state, architecture, context-map, feature-contracts, skills/feature-implementation.\nUpdate: current-state, task-log, session-handoff, feature-contracts, context-map when paths change.\n\n## ui\nRead: design-system, feature-contracts, context-map, skills/ui-consistency.\nUpdate: design-system, feature-contracts, current-state, task-log, session-handoff.\n\n## debugging\nRead: current-state, task-log, feature-contracts, testing-strategy, skills/debugging.\nUpdate: task-log, current-state, session-handoff, testing-strategy when regression coverage changes.\n\n## release\nRead: release-checklist, testing-strategy, current-state, decisions, secret-policy.\nUpdate: release-checklist, task-log, current-state, session-handoff.\n\n## migration\nRead: AX_MIGRATION_GUIDE, writeback-policy, task-type-map.\nUpdate: only missing files by default; preserve project memory unless --force.\n\n## session-close\nRead: session-close-policy, progress-tracker, current-state, task-log, session-handoff, anti-lazy-work-policy.\nUpdate: session-handoff, progress-tracker, current-state, task-log, and any relevant memory files changed by the session.\n`,
203
+ '.harness/writeback-policy.md': `${MARK}\n# Writeback Policy\n\n## current-state.md\nCurrent progress, blockers, next work.\n\n## task-log.md\nWhat changed, when, and verification result.\n\n## session-handoff.md\nEnough context for the next AI session to continue.\n\n## decisions.md\nImportant choices and tradeoffs.\n\n## release-checklist.md\nDeploy commands, env requirements, rollback, failures.\n\n## design-system.md\nUI rules and reusable patterns.\n\n## feature-contracts.md\nInput/output/state/error contracts.\n\n## project-brief.md\nProduct purpose and success criteria only.\n\n## progress-tracker.md\nUser-requested work items, status, evidence, and next actions across sessions.\n\n## session-close-policy.md\nFinal response and handoff rules. Update only when the reporting standard changes.\n\n## anti-lazy-work-policy.md\nQuality guardrails that prevent vague or incomplete closure. Update when repeated failure patterns appear.\n`,
204
+ '.harness/task-type-map.md': `${MARK}\n# Task Type Map\n\n| User request | Task type | First files |\n|---|---|---|\n| 새 기능 | feature | feature-contracts, architecture |\n| 디자인/UI | ui | design-system |\n| 오류 수정 | debugging | task-log, debugging skill |\n| 구조 개선 | refactor | architecture, decisions |\n| 배포 | release | release-checklist |\n| 하네스 전환 | migration | AX_MIGRATION_GUIDE |\n| 신규 적용 | new-install | AX_NEW_PROJECT_GUIDE |\n| 스킬 저장/배포 | skill-library | AX_SKILL_LIBRARY_GUIDE |\n`,
205
+ '.harness/AX_MIGRATION_GUIDE.md': `${MARK}\n# AX Migration Guide\n\n## Goal\nMigrate old harness files without losing project memory.\n\n## Procedure\n1. Run: leerness migrate --dry-run\n2. Confirm archive target.\n3. Run: leerness migrate\n4. Check .env.example and .gitignore were merged, not replaced.\n5. Check project memory files were preserved.\n6. Fill only missing context using archived legacy files.\n7. Run: leerness status && leerness verify.\n\n## Critical Rule\nDo not overwrite existing project-brief, current-state, architecture, decisions, release-checklist, feature-contracts, or design-system unless the user explicitly asks for --force.\n`,
206
+ '.harness/AX_NEW_PROJECT_GUIDE.md': `${MARK}\n# AX New Project Guide\n\n## Goal\nAfter initial installation, populate Leerness memory from the actual project.\n\n## Read actual project files\n- package/config files\n- app/routes/pages\n- API/server/functions\n- DB/schema/rules\n- deploy/CI files\n- tests\n\n## Fill memory files\n- project-brief.md: purpose and success criteria\n- architecture.md: modules and data flow\n- context-map.md: important files and routes\n- design-system.md: existing UI patterns\n- feature-contracts.md: major features and states\n- release-checklist.md: real deploy commands and env requirements\n`,
207
+ '.harness/AX_SKILL_LIBRARY_GUIDE.md': `${MARK}\n# AX Skill Library Guide\n\n## AI-verified skill lifecycle\n1. Learn from a validated implementation.\n2. Remove secrets and keep env variable names only.\n3. Add displayNameKo, capabilities, lastUpdated, verification metadata.\n4. Run validate.\n5. Run verify --ai.\n6. Build.\n7. Publish dry-run.\n8. Publish with --execute only after token gate passes.\n`,
208
+ '.harness/skills/codebase-analysis.md': `${MARK}\n# Skill: Codebase Analysis\n\nRead context-map, architecture, current-state, and related source files before proposing changes.\n`,
209
+ '.harness/skills/feature-implementation.md': `${MARK}\n# Skill: Feature Implementation\n\nDefine contract, inspect existing patterns, implement minimal change, verify, update memory.\n`,
210
+ '.harness/skills/refactoring.md': `${MARK}\n# Skill: Refactoring\n\nPreserve behavior and contracts. Record important decisions.\n`,
211
+ '.harness/skills/debugging.md': `${MARK}\n# Skill: Debugging\n\nReproduce, isolate cause, patch minimally, verify, add regression note.\n`,
212
+ '.harness/skills/ui-consistency.md': `${MARK}\n# Skill: UI Consistency\n\nRead design-system and existing adjacent screens before styling.\n`,
213
+ '.harness/skills/security-review.md': `${MARK}\n# Skill: Security Review\n\nCheck secrets, auth, permissions, logging, and sensitive data exposure.\n`,
214
+ '.harness/skills/release-check.md': `${MARK}\n# Skill: Release Check\n\nCheck tests, build, env vars, migration, rollback, publish token gate.\n`,
215
+ '.harness/skills/documentation-update.md': `${MARK}\n# Skill: Documentation Update\n\nFollow writeback-policy and update the specific memory file.\n`,
216
+ '.harness/templates/session-summary.md': `${MARK}\n# Session Summary\n\n## Done\n\n## Files Changed\n\n## Verification\n\n## Next\n`,
217
+ '.harness/templates/decision.md': `${MARK}\n# Decision\n\n## Decision\n\n## Reason\n\n## Alternatives\n\n## Impact\n`
218
+ };
219
+
220
+ const memoryFiles = new Set(['.harness/project-brief.md','.harness/current-state.md','.harness/architecture.md','.harness/context-map.md','.harness/decisions.md','.harness/task-log.md','.harness/constraints.md','.harness/guardrails.md','.harness/design-system.md','.harness/feature-contracts.md','.harness/testing-strategy.md','.harness/review-checklist.md','.harness/release-checklist.md','.harness/session-handoff.md','.harness/progress-tracker.md','.harness/skill-index.md','.harness/secret-policy.md']);
221
+ const refreshableFiles = new Set(['AGENTS.md','CLAUDE.md','.cursor/rules/leerness.mdc','.github/copilot-instructions.md','.harness/context-routing.md','.harness/writeback-policy.md','.harness/task-type-map.md','.harness/AX_SKILL_LIBRARY_GUIDE.md','.harness/AX_MIGRATION_GUIDE.md','.harness/AX_NEW_PROJECT_GUIDE.md','.harness/session-close-policy.md','.harness/anti-lazy-work-policy.md','.harness/templates/end-of-session-report.md','.harness/manifest.json','.harness/HARNESS_VERSION']);
222
+ function uniqueLinesAppend(current, addition){
223
+ const lines=current.split(/\r?\n/); const seen=new Set(lines.map(x=>x.trim()).filter(Boolean));
224
+ const add=addition.split(/\r?\n/).filter(line=>{ const t=line.trim(); if(!t||seen.has(t)) return false; seen.add(t); return true; });
225
+ if(!add.length) return current;
226
+ return current.replace(/\s*$/,'\n')+add.join('\n')+'\n';
206
227
  }
207
- function readSkillLibraryMeta(dir){
208
- for(const cnd of [path.join(dir,'skill-library.json'),path.join(dir,'skill.json'),path.join(dir,'package.json')]){
209
- if(!exists(cnd)) continue;
210
- const data=parseJsonSafe(read(cnd),null); if(!data) continue;
211
- if(path.basename(cnd)==='package.json') return normalizeSkillMeta({ name:data.harnessSkill?.name||data.name, version:data.version||'0.1.0', title:data.harnessSkill?.title||data.description||data.name, packageName:data.name, description:data.description||'', requiresEnv:data.harnessSkill?.requiresEnv||[], files:data.harnessSkill?.files||[], lastUpdated:data.harnessSkill?.lastUpdated, lastUpdatedAt:data.harnessSkill?.lastUpdatedAt, verification:data.harnessSkill?.verification }, path.basename(dir));
212
- return normalizeSkillMeta(data, path.basename(dir));
228
+ function mergeSkillLockJson(current,incoming){ const a=parseJsonSafe(current,{installedSkills:{}}), b=parseJsonSafe(incoming,{installedSkills:{}}); const merged={...b,...a}; merged.harnessVersion=VERSION; merged.updatedAt=now(); merged.installedSkills={...(b.installedSkills||{}),...(a.installedSkills||{})}; return JSON.stringify(merged,null,2)+'\n'; }
229
+ function writeCoreSafely(root,file,body,opts={}){
230
+ const target=path.join(root,file), dryRun=Boolean(opts.dryRun), force=Boolean(opts.force), existed=exists(target);
231
+ if(!existed){ if(dryRun) info('[dry-run] 생성: '+file); else { write(target,body); ok('생성: '+file); } return 'created'; }
232
+ const current=read(target); if(current===body){ if(dryRun) info('[dry-run] 유지: '+file); else ok('유지: '+file); return 'same'; }
233
+ if(file==='.gitignore'||file==='.env.example'){
234
+ const merged=uniqueLinesAppend(current,body); if(merged===current){ if(dryRun) info('[dry-run] 보존: '+file); else ok('보존: '+file); return 'preserved'; }
235
+ if(dryRun) info('[dry-run] 병합: '+file); else { write(target,merged); ok('병합: '+file); } return 'merged';
213
236
  }
214
- return null;
215
- }
216
- function writeSkillLibraryMeta(dir,meta){ write(path.join(dir,'skill-library.json'),JSON.stringify(normalizeSkillMeta(meta,path.basename(dir)),null,2)+'\n'); }
217
- function validateSkillLibrary(dir,options={}){
218
- dir=path.resolve(dir); let failures=0; const meta=readSkillLibraryMeta(dir);
219
- if(!meta||!meta.name){ fail('skill-library.json 또는 skill.json에 name이 필요합니다.'); failures++; }
220
- const sd=path.join(dir,'skills');
221
- if(!exists(sd)){ fail('skills/ 폴더가 필요합니다.'); failures++; }
222
- else if(!skillLibraryFiles(sd).some(f=>f.endsWith('.md'))){ fail('skills/*.md 파일이 최소 1개 필요합니다.'); failures++; }
223
- const findings=scanSensitivePath(dir);
224
- if(findings.length){ fail('민감정보 의심 패턴 감지. 업로드/빌드/병합이 차단됩니다.'); findings.slice(0,20).forEach(f=>warn(rel(dir,f.file)+' · '+f.type+' · '+f.sample)); if(findings.length>20) warn('추가 '+(findings.length-20)+'건 생략'); failures+=findings.length; }
225
- for(const env of new Set(meta?.requiresEnv||[])){ if(!/^[A-Z][A-Z0-9_]*$/.test(env)) warn('환경변수 이름 형식 확인 필요: '+env); }
226
- if(meta && !meta.lastUpdated) warn('lastUpdated 메타데이터가 없습니다. v1.0.0에서는 표시를 권장합니다.');
227
- if((options.strictAi||options['strict-ai']) && !isAiVerified(meta)){ fail('AI 검증 메타데이터가 없습니다. `leerness library verify <path> --ai`가 필요합니다.'); failures++; }
228
- if(!options.silent){ if(failures) fail('스킬 라이브러리 검증 실패: '+failures); else ok('스킬 라이브러리 검증 완료: '+(meta?.name||path.basename(dir))+' · '+verificationLabel(meta)); }
229
- return {ok:failures===0,failures,meta,findings};
230
- }
231
- function verifySkillLibrary(dir,flags={}){
232
- dir=path.resolve(dir||process.cwd());
233
- const check=validateSkillLibrary(dir,{silent:false});
234
- if(!check.ok){ process.exitCode=1; return; }
235
- if(!flags.ai){ fail('업로드 가능한 검증 기록은 AI 검증으로만 생성됩니다. `--ai`를 붙여 AI 검증 게이트를 명시하세요.'); process.exitCode=1; return; }
236
- const meta=normalizeSkillMeta(check.meta,path.basename(dir));
237
- const reviewedAt=nowIso();
238
- meta.lastUpdatedAt = meta.lastUpdatedAt || reviewedAt;
239
- meta.lastUpdated = meta.lastUpdated || dateOnly(meta.lastUpdatedAt);
240
- meta.verification = { status:'passed', method:'ai-assisted-review', verifiedBy:String(flags.reviewer||flags.by||'leerness-ai'), verifiedAt:reviewedAt, checks:['structure','secret-scan','env-reference-only','reusability','migration-readiness','metadata-completeness'] };
241
- writeSkillLibraryMeta(dir,meta);
242
- write(path.join(dir,'ai-verification.json'),JSON.stringify({skill:meta.name,version:meta.version,status:'passed',method:'ai-assisted-review',verifiedBy:meta.verification.verifiedBy,verifiedAt:reviewedAt,checks:meta.verification.checks},null,2)+'\n');
243
- ok('AI 검증 완료: '+meta.name+'@'+meta.version+' · '+dateOnly(reviewedAt));
244
- }
245
- function libraryStatus(dir){
246
- dir=path.resolve(dir||process.cwd()); const meta=readSkillLibraryMeta(dir);
247
- if(!meta){ fail('스킬 라이브러리 메타데이터를 찾지 못했습니다.'); process.exitCode=1; return; }
248
- banner();
249
- log('스킬: '+meta.name+'@'+meta.version);
250
- log('제목: '+(meta.title||''));
251
- log('카테고리: '+(meta.category||''));
252
- log('최종 업데이트: '+(meta.lastUpdated||'unknown')+' ('+(meta.lastUpdatedAt||'unknown')+')');
253
- log('검증: '+verificationLabel(meta));
254
- if((meta.requiresEnv||[]).length) log('환경변수: '+meta.requiresEnv.join(', '));
255
- }
256
- function updateSkillLock(root,meta,remove=false){
257
- const lp=path.join(root,'.harness/skills-lock.json');
258
- const lock=exists(lp)?parseJsonSafe(read(lp),{harnessVersion:VERSION,installedSkills:{}}):{harnessVersion:VERSION,installedSkills:{}};
259
- lock.harnessVersion=VERSION; lock.updatedAt=nowIso(); lock.installedSkills=lock.installedSkills||{};
260
- if(remove) delete lock.installedSkills[meta.name];
261
- else lock.installedSkills[meta.name]={version:meta.version,source:meta.source||'bundled',title:meta.title,requiresEnv:meta.requiresEnv||[],lastUpdated:meta.lastUpdated||dateOnly(meta.lastUpdatedAt),lastUpdatedAt:meta.lastUpdatedAt||nowIso(),verificationStatus:(meta.verification||{}).status||'unknown',verifiedAt:(meta.verification||{}).verifiedAt||null};
262
- write(lp,JSON.stringify(lock,null,2)+'\n');
263
- }
264
- function getSkillMeta(name){
265
- const metaPath=path.join(PACKS_DIR,name,'skill.json'); if(!exists(metaPath)) return null;
266
- const meta=parseJsonSafe(read(metaPath),null); if(!meta||!meta.name) return null;
267
- return normalizeSkillMeta(meta,name);
268
- }
269
- function installSkill(root,name,dryRun=false){
270
- const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return false; }
271
- const packRoot=path.join(PACKS_DIR,name); const destRoot=path.join(root,'.harness/skills',name);
272
- if(dryRun){ info('[dry-run] install skill: '+name+' · updated '+(meta.lastUpdated||'unknown')+' · '+verificationLabel(meta)); return true; }
273
- fs.mkdirSync(destRoot,{recursive:true});
274
- 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)); } }
275
- write(path.join(destRoot,'skill.json'),JSON.stringify(meta,null,2)+'\n');
276
- updateSkillLock(root,meta,false); appendEnvExample(root,meta); return true;
277
- }
278
- function learnSkillLibrary(root,flags){
279
- root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slugifyName(flags.name||flags.category||path.basename(from)); const version=String(flags.version||'0.1.0'); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name));
280
- if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; }
281
- const sourceFiles=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep));
282
- if(!sourceFiles.length){ fail('학습 가능한 텍스트 스킬 파일이 없습니다.'); process.exitCode=1; return; }
283
- fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const requiresEnv=new Set(); const copied=[];
284
- for(const f of sourceFiles){ const body=read(f); for(const e of inferEnvNamesFromText(body)) requiresEnv.add(e); const base=path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); const destName=base.endsWith('.md')?base:base+'.md'; const dest=path.join(outRoot,'skills',destName); const header=[MARK,'# Learned Skill: '+destName.replace(/\.md$/,''),'','Source: '+rel(root,f),'Learned: '+nowIso(),'Verification: needs-review',''].join('\n'); write(dest,body.includes(MARK)?body:header+'\n'+body); copied.push('skills/'+destName); }
285
- const t=nowIso(); const meta=normalizeSkillMeta({name,version,title:flags.title||name,description:flags.description||'Verified project skill library extracted from a successful implementation.',category:flags.category||'custom',compatibleHarness:'>=1.0.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(requiresEnv).sort(),files:copied,learnedFrom:rel(root,from),learnedAt:t,lastUpdated:dateOnly(t),lastUpdatedAt:t,verification:{status:'needs-review',method:'none',verifiedBy:null,verifiedAt:null,checks:[]}}, name);
286
- writeSkillLibraryMeta(outRoot,meta);
287
- write(path.join(outRoot,'README.md'),'# '+meta.title+'\n\n'+meta.description+'\n\n## Metadata\n\n- Version: '+meta.version+'\n- Last updated: '+meta.lastUpdated+'\n- Verification: '+verificationLabel(meta)+'\n\n## Policy\n\nThis library stores environment variable names only. Do not commit real secrets.\n\n## Required env\n\n'+(meta.requiresEnv.map(e=>'- '+e).join('\n')||'- None')+'\n');
288
- write(path.join(outRoot,'env.example'),meta.requiresEnv.map(e=>e+'=').join('\n')+(meta.requiresEnv.length?'\n':''));
289
- const result=validateSkillLibrary(outRoot,{silent:true}); if(!result.ok){ fail('학습 결과에 민감정보 또는 구조 문제가 있어 확인이 필요합니다: '+outRoot); process.exitCode=1; return; }
290
- ok('스킬 라이브러리 학습 완료: '+outRoot); warn('아직 업로드 검증 전입니다. 다음 명령으로 AI 검증을 완료하세요.'); info('leerness library verify '+outRoot+' --ai --reviewer leerness-ai');
291
- }
292
- function buildSkillLibrary(dir,flags){
293
- dir=path.resolve(dir||process.cwd()); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok){ process.exitCode=1; return; }
294
- const meta=normalizeSkillMeta(check.meta,path.basename(dir)); const out=path.resolve(flags.out||path.join(dir,'dist')); const packageName=flags.package||meta.packageName||packageSafeName(meta.name); const libRoot=path.join(out,slugifyName(meta.name));
295
- if(exists(libRoot)) fs.rmSync(libRoot,{recursive:true,force:true}); fs.mkdirSync(libRoot,{recursive:true});
296
- 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)); }
297
- writeSkillLibraryMeta(libRoot,meta);
298
- const pkg={name:packageName,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',meta.category||'custom'].filter(Boolean),license:'MIT',publishConfig:{access:'public'},harnessSkill:{name:meta.name,version:meta.version,title:meta.title,requiresEnv:meta.requiresEnv||[],sensitiveDataPolicy:meta.sensitiveDataPolicy||'env-reference-only',compatibleHarness:meta.compatibleHarness||'>=1.0.0',lastUpdated:meta.lastUpdated,lastUpdatedAt:meta.lastUpdatedAt,verification:meta.verification}};
299
- write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n');
300
- ok('스킬 라이브러리 빌드 완료: '+libRoot); info('상태 확인: leerness library status '+libRoot); info('npm 배포: leerness library publish '+libRoot+' --target npm --execute');
301
- }
302
- function updateSkillLibrary(dir,flags){
303
- dir=path.resolve(dir||process.cwd()); const from=path.resolve(flags.from||flags.source||''); if(!from||!exists(from)){ fail('업데이트 원본 경로가 필요합니다: --from <path>'); process.exitCode=1; return; }
304
- const old=readSkillLibraryMeta(dir)||normalizeSkillMeta({name:path.basename(dir)},path.basename(dir)); const sourceFiles=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); if(!sourceFiles.length){ fail('업데이트할 텍스트 스킬 파일이 없습니다.'); process.exitCode=1; return; }
305
- fs.mkdirSync(path.join(dir,'skills'),{recursive:true}); const copied=[]; const envs=new Set(old.requiresEnv||[]);
306
- for(const f of sourceFiles){ const body=read(f); for(const e of inferEnvNamesFromText(body)) envs.add(e); const base=path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); const destName=base.endsWith('.md')?base:base+'.md'; write(path.join(dir,'skills',destName),body); copied.push('skills/'+destName); }
307
- const t=nowIso(); const meta=normalizeSkillMeta({...old,version:String(flags.version||old.version||'0.1.0'),requiresEnv:Array.from(envs).sort(),files:Array.from(new Set([...(old.files||[]),...copied])),lastUpdated:dateOnly(t),lastUpdatedAt:t,updatedFrom:from,verification:{status:'needs-review',method:'updated-after-verification',verifiedBy:null,verifiedAt:null,checks:[]}}, old.name);
308
- writeSkillLibraryMeta(dir,meta); warn('스킬 라이브러리 업데이트 완료. 검증 상태가 needs-review로 초기화되었습니다.'); info('다음: leerness library verify '+dir+' --ai --reviewer leerness-ai');
309
- }
310
- function mergeSkillLibrary(root,source,flags){
311
- root=path.resolve(root||process.cwd()); source=path.resolve(source||flags.source||''); if(!source||!exists(source)){ fail('병합할 스킬 라이브러리 경로가 필요합니다.'); process.exitCode=1; return; }
312
- const check=validateSkillLibrary(source,{silent:false,strictAi:true}); if(!check.ok){ process.exitCode=1; return; }
313
- const meta=normalizeSkillMeta(check.meta,path.basename(source)); const name=slugifyName(meta.name); const dest=path.join(root,'.harness/skills',name); fs.mkdirSync(dest,{recursive:true});
314
- const srcSkills=path.join(source,'skills'); if(exists(srcSkills)) copyRecursive(srcSkills,dest); write(path.join(dest,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); if(exists(path.join(source,'ai-verification.json'))) fs.copyFileSync(path.join(source,'ai-verification.json'),path.join(dest,'ai-verification.json'));
315
- updateSkillLock(root,{...meta,source:'library'},false); appendEnvExample(root,{name,title:meta.title||name,requiresEnv:meta.requiresEnv||[]}); ok('검증된 스킬 라이브러리 병합 완료: '+rel(root,dest));
316
- }
317
- function migrateSkillLibrary(dir,flags){
318
- dir=path.resolve(dir||process.cwd()); if(!exists(dir)){ fail('마이그레이션 대상 경로가 없습니다: '+dir); process.exitCode=1; return; }
319
- const old=readSkillLibraryMeta(dir)||{}; const t=nowIso(); const migrated=normalizeSkillMeta({name:slugifyName(flags.name||old.name||path.basename(dir)),version:String(flags.version||old.version||'0.1.0'),title:flags.title||old.title||old.description||path.basename(dir),description:flags.description||old.description||'Migrated Leerness skill library.',category:flags.category||old.category||'custom',compatibleHarness:'>=1.0.0',sensitiveDataPolicy:'env-reference-only',requiresEnv:Array.from(new Set(old.requiresEnv||old.harnessSkill?.requiresEnv||[])),migratedAt:t,lastUpdated:dateOnly(t),lastUpdatedAt:t,verification:{status:'needs-review',method:'migrated-after-verification',verifiedBy:null,verifiedAt:null,checks:[]}}, path.basename(dir));
320
- const skillsDir=path.join(dir,'skills'); if(!exists(skillsDir)) fs.mkdirSync(skillsDir,{recursive:true}); const mdFiles=skillLibraryFiles(dir).filter(f=>f.endsWith('.md')&&!isInside(skillsDir,f)&&!f.includes(path.sep+'node_modules'+path.sep));
321
- for(const f of mdFiles){ if(path.basename(f).toLowerCase()==='readme.md') continue; const dest=path.join(skillsDir,path.basename(f)); if(!exists(dest)) fs.copyFileSync(f,dest); }
322
- migrated.files=skillLibraryFiles(skillsDir).filter(f=>f.endsWith('.md')).map(f=>rel(dir,f)); writeSkillLibraryMeta(dir,migrated); if(!exists(path.join(dir,'README.md'))) write(path.join(dir,'README.md'),'# '+migrated.title+'\n\n'+migrated.description+'\n'); const check=validateSkillLibrary(dir,{silent:false}); if(!check.ok) process.exitCode=1; else { warn('마이그레이션 완료. 검증 상태가 needs-review입니다.'); info('다음: leerness library verify '+dir+' --ai --reviewer leerness-ai'); }
323
- }
324
- function findProjectRootForPublish(start){
325
- let cur=path.resolve(start||process.cwd());
326
- if(exists(cur)&&fs.statSync(cur).isFile()) cur=path.dirname(cur);
327
- for(;;){
328
- if(exists(path.join(cur,'.harness'))) return cur;
329
- const parent=path.dirname(cur);
330
- if(parent===cur) return null;
331
- cur=parent;
237
+ if(file==='.harness/skills-lock.json'){
238
+ const merged=mergeSkillLockJson(current,body); if(dryRun) info('[dry-run] 병합: '+file); else { write(target,merged); ok('병합: '+file); } return 'merged';
332
239
  }
333
- }
334
- function readPublishLocalConfig(dir){
335
- const projectRoot=findProjectRootForPublish(dir)||process.cwd();
336
- const candidates=[path.join(projectRoot,'.harness','skill-publish.local.json'),path.join(projectRoot,'.harness','skill-config.local.json'),path.join(projectRoot,'.leerness.publish.local.json'),path.join(dir,'skill-publish.local.json')];
337
- for(const p of candidates){ if(exists(p)){ const cfg=parseJsonSafe(read(p),null); if(cfg) return {path:p,config:cfg}; warn('로컬 publish 설정 JSON을 읽지 못했습니다: '+p); } }
338
- return {path:null,config:{}};
339
- }
340
- function deepGet(obj,keys){ for(const k of keys){ if(!obj) return undefined; obj=obj[k]; } return obj; }
341
- function tokenFromConfig(target,dir){
342
- const loaded=readPublishLocalConfig(dir); const cfg=loaded.config||{}; const auth=cfg.publishAuth||cfg.auth||{};
343
- const envKeys=target==='npm' ? [auth.npmTokenEnv,cfg.npmTokenEnv,'LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN'] : [auth.gitTokenEnv,auth.githubTokenEnv,cfg.gitTokenEnv,cfg.githubTokenEnv,'LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN','GIT_TOKEN'];
344
- for(const k of envKeys.filter(Boolean)){ if(process.env[k]) return {token:process.env[k],source:'env:'+k,configPath:loaded.path}; }
345
- const direct=target==='npm' ? (auth.npmToken||cfg.npmToken||deepGet(cfg,['npm','token'])) : (auth.gitToken||auth.githubToken||cfg.gitToken||cfg.githubToken||deepGet(cfg,['git','token'])||deepGet(cfg,['github','token']));
346
- if(direct){ warn('로컬 설정 파일의 직접 토큰을 사용합니다. 가능하면 토큰값 대신 tokenEnv 방식이 더 안전합니다: '+loaded.path); return {token:direct,source:'local-config:'+loaded.path,configPath:loaded.path}; }
347
- return {token:null,source:null,configPath:loaded.path};
348
- }
349
- function repoFromConfig(dir,flags){ const loaded=readPublishLocalConfig(dir); const cfg=loaded.config||{}; const auth=cfg.publishAuth||cfg.auth||{}; return flags.repo||auth.gitRemoteUrl||cfg.gitRemoteUrl||cfg.repository||process.env.LEERNESS_GIT_REPO||process.env.GIT_REMOTE_URL||DEFAULT_GIT_REPOSITORY; }
350
- function promptSecret(question){
351
- return new Promise(resolve=>{
352
- if(!process.stdin.isTTY||!process.stdout.isTTY){ resolve(''); return; }
353
- const stdin=process.stdin; let value=''; process.stdout.write(question);
354
- const cleanup=()=>{ stdin.removeListener('data',onData); try{ stdin.setRawMode(false); }catch{} process.stdout.write('\n'); resolve(value.trim()); };
355
- const onData=(ch)=>{ ch=String(ch); if(ch==='\u0003'){ process.stdout.write('\n'); process.exit(130); } if(ch==='\r'||ch==='\n') return cleanup(); if(ch==='\u007f'||ch==='\b'){ if(value.length){ value=value.slice(0,-1); process.stdout.write('\b \b'); } return; } value+=ch; process.stdout.write('*'); };
356
- try{ stdin.setRawMode(true); }catch{} stdin.resume(); stdin.setEncoding('utf8'); stdin.on('data',onData);
357
- });
358
- }
359
- async function resolvePublishToken(target,dir,flags){
360
- if(flags.token){ warn('--token 인자는 shell history에 남을 수 있습니다. 가능하면 환경변수나 입력 프롬프트를 사용하세요.'); return {token:String(flags.token),source:'--token'}; }
361
- if(flags['token-env']&&process.env[String(flags['token-env'])]) return {token:process.env[String(flags['token-env'])],source:'env:'+flags['token-env']};
362
- const cfgToken=tokenFromConfig(target,dir); if(cfgToken.token) return cfgToken; if(flags['no-prompt']) return {token:null,source:null};
363
- const label=target==='npm'?'npm access token':'git/GitHub access token'; const token=await promptSecret(label+' 입력: '); return {token,source:token?'interactive-prompt':null};
364
- }
365
- function writeTempNpmrc(token,registry){ const host=(registry||'https://registry.npmjs.org/').replace(/^https?:/,'').replace(/\/$/,''); const file=path.join(os.tmpdir(),'leerness-npmrc-'+Date.now()+'-'+Math.random().toString(16).slice(2)); fs.writeFileSync(file,'registry='+(registry||'https://registry.npmjs.org/')+'\n'+host+'/:_authToken='+token+'\n',{encoding:'utf8',mode:0o600}); return file; }
366
- function gitRun(dir,args,token){ const finalArgs=token?['-c','http.extraHeader=Authorization: Bearer '+token].concat(args):args; const r=childProcess.spawnSync('git',finalArgs,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(r.status) process.exit(r.status); }
367
- function ensureGitRemote(dir,repo){ const get=childProcess.spawnSync('git',['remote','get-url','origin'],{cwd:dir,encoding:'utf8',shell:process.platform==='win32'}); if(get.status===0) gitRun(dir,['remote','set-url','origin',repo]); else gitRun(dir,['remote','add','origin',repo]); }
368
- async function publishSkillLibrary(dir,flags){
369
- dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const check=validateSkillLibrary(dir,{silent:false,strictAi:true}); if(!check.ok){ process.exitCode=1; return; }
370
- if(!isAiVerified(check.meta)){ fail('AI 검증된 스킬만 업로드할 수 있습니다. leerness library verify <path> --ai 를 먼저 실행하세요.'); process.exitCode=1; return; }
371
- if(target==='npm'){
372
- if(!exists(path.join(dir,'package.json'))){ warn('package.json이 없습니다. 먼저 build를 실행하세요.'); info('leerness library build '+dir); process.exitCode=1; return; }
373
- const args=['publish','--access','public'].concat(flags.registry?['--registry',flags.registry]:[]);
374
- if(!execute){ info('[dry-run] AI 검증 통과. 실행 예정: (cd '+dir+') npm '+args.join(' ')); info('실제 배포는 --execute를 붙이세요. --execute 시 npm 토큰을 환경변수/로컬 설정에서 찾고 없으면 입력을 요구합니다.'); return; }
375
- const cred=await resolvePublishToken('npm',dir,flags);
376
- if(!cred.token){ fail('npm access token이 필요합니다. LEERNESS_NPM_TOKEN, NPM_TOKEN, NODE_AUTH_TOKEN 또는 .harness/skill-publish.local.json을 설정하거나 프롬프트에 입력하세요.'); process.exitCode=1; return; }
377
- let userconfig=null; try{ userconfig=writeTempNpmrc(cred.token,flags.registry); info('npm 인증 소스: '+cred.source); const r=childProcess.spawnSync('npm',args.concat(['--userconfig',userconfig]),{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); process.exitCode=r.status||0; } finally { if(userconfig&&exists(userconfig)) try{ fs.rmSync(userconfig,{force:true}); }catch{} } return;
240
+ if(force || refreshableFiles.has(file)){
241
+ if(dryRun) info('[dry-run] '+(force?'강제 ':'')+'갱신: '+file); else { write(target,body); ok((force?'강제 ':'')+'갱신: '+file); } return 'updated';
378
242
  }
379
- if(target==='git'){
380
- const repo=repoFromConfig(dir,flags); const branch=flags.branch||'main'; const message=flags.message||('Publish verified skill library '+check.meta.name+'@'+(check.meta.version||'0.1.0'));
381
- if(!execute){ info('[dry-run] AI 검증 통과. git target repo: '+repo); info('[dry-run] branch: '+branch); info('[dry-run] commit message: '+message); info('실제 push는 --execute를 붙이세요. --execute 시 git/GitHub 토큰을 환경변수/로컬 설정에서 찾고 없으면 입력을 요구합니다.'); return; }
382
- const cred=await resolvePublishToken('git',dir,flags); if(!cred.token){ fail('git/GitHub access token이 필요합니다. LEERNESS_GIT_TOKEN, LEERNESS_GITHUB_TOKEN, GITHUB_TOKEN, GH_TOKEN 또는 .harness/skill-publish.local.json을 설정하거나 프롬프트에 입력하세요.'); process.exitCode=1; return; }
383
- info('git 인증 소스: '+cred.source); if(!exists(path.join(dir,'.git'))) gitRun(dir,['init']); ensureGitRemote(dir,repo); gitRun(dir,['add','.']); const commit=childProcess.spawnSync('git',['commit','-m',message],{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(commit.status&&commit.status!==1) process.exit(commit.status); gitRun(dir,['branch','-M',branch]); gitRun(dir,['push','-u','origin',branch],cred.token); return;
243
+ if(memoryFiles.has(file)){
244
+ if(dryRun) info('[dry-run] 보존: '+file+' (기존 프로젝트 메모리 유지)'); else ok('보존: '+file+' (기존 프로젝트 메모리 유지)'); return 'preserved';
384
245
  }
385
- fail('지원하지 않는 publish target: '+target); process.exitCode=1;
246
+ if(dryRun) info('[dry-run] 보존: '+file+' (덮어쓰려면 --force)'); else ok('보존: '+file+' (덮어쓰려면 --force)'); return 'preserved';
386
247
  }
387
- function libraryGuide(root,flags={}){
388
- root=path.resolve(root||flags.path||process.cwd()); const target=path.join(root,'.harness/AX_SKILL_LIBRARY_GUIDE.md');
389
- if(exists(target)){ ok('AX 가이드 위치: '+target); log(read(target)); return; }
390
- const bundled=path.join(PACKAGE_ROOT,'docs','AX_SKILL_LIBRARY_GUIDE.md');
391
- if(exists(bundled)){ log(read(bundled)); return; }
392
- warn('AX 가이드 파일을 찾지 못했습니다. init을 먼저 실행하세요.');
248
+ function manifest(root,selectedSkills){ return JSON.stringify({name:projectName(root),harnessVersion:VERSION,installedAt:now(),managedFiles:Object.keys(coreFiles),selectedSkills,nonDestructiveMigration:true},null,2); }
249
+ function skillsLock(root,selectedSkills){ const lock={harnessVersion:VERSION,installedAt:now(),installedSkills:{}}; for(const name of selectedSkills){ const meta=getSkillMeta(name); if(meta) lock.installedSkills[name]={version:meta.version,source:'bundled',title:meta.title,displayNameKo:meta.displayNameKo||meta.title,lastUpdated:meta.lastUpdated,verificationStatus:(meta.verification||{}).status||'unknown'}; } return JSON.stringify(lock,null,2); }
250
+ function makeContext(root,legacyText,selectedSkills){ return { PROJECT:projectName(root), DATE:today(), VERSION, LEGACY_AGENT:legacyBlock('agent instructions',pick(legacyText,['AGENTS.md','AGENT.md','CLAUDE.md','.cursorrules','.cursor/rules/project-rules.mdc','.cursor/rules/leerness.mdc','.github/copilot-instructions.md'])), LEGACY_BRIEF:legacyBlock('project context',pick(legacyText,['PROJECT_CONTEXT.md','CONTEXT.md','docs/guideline.md','AI_HARNESS.md','HARNESS.md'])), LEGACY_STATE:legacyBlock('state',pick(legacyText,['CURRENT_STATE.md','TASK_LOG.md','docs/history.md'])), LEGACY_ARCH:legacyBlock('architecture',pick(legacyText,['ARCHITECTURE.md'])), LEGACY_DECISIONS:legacyBlock('decisions',pick(legacyText,['DECISIONS.md'])), MANIFEST:manifest(root,selectedSkills), SKILLS_LOCK:skillsLock(root,selectedSkills) }; }
251
+
252
+ function listSkillPacks(){ if(!exists(PACKS_DIR)) return []; return fs.readdirSync(PACKS_DIR).map(n=>getSkillMeta(n)).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name)); }
253
+ 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; }
254
+ 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'); }
255
+ 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'); }
256
+ function installSkill(root,name,dryRun=false){ const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return false; } const packRoot=path.join(PACKS_DIR,name); const destRoot=path.join(root,'.harness/skills',name); if(dryRun){ info('[dry-run] install skill: '+name); return true; } fs.mkdirSync(destRoot,{recursive:true}); for(const file of meta.files||[]){ const src=path.join(packRoot,file); const dest=path.join(destRoot,path.basename(file)); if(exists(src)){ write(dest,read(src)); ok('스킬 설치: '+rel(root,dest)); } } write(path.join(destRoot,'skill.json'),JSON.stringify(meta,null,2)+'\n'); updateSkillLock(root,meta,false); appendEnvExample(root,meta); return true; }
257
+ function removeSkill(root,name){ const meta=getSkillMeta(name)||{name,title:name}; const dest=path.join(root,'.harness/skills',name); if(exists(dest)) fs.rmSync(dest,{recursive:true,force:true}); updateSkillLock(root,meta,true); ok('스킬 제거: '+name); }
258
+
259
+ function parseArgs(argv){ const out={flags:{},positionals:[]}; const valueFlags=new Set(['skills','path','from','out','target','package','repo','version','title','description','category','source','name','registry','branch','message','reviewer','by','token-env']); for(let i=0;i<argv.length;i++){ const a=argv[i]; if(a.startsWith('--')){ const eq=a.indexOf('='); const key=eq>=0?a.slice(2,eq):a.slice(2); if(eq>=0) out.flags[key]=a.slice(eq+1); else if(valueFlags.has(key)&&argv[i+1]&&!argv[i+1].startsWith('-')) out.flags[key]=argv[++i]; else out.flags[key]=true; } else if(a.startsWith('-')) out.flags[a.slice(1)]=true; else out.positionals.push(a); } return out; }
260
+ function splitSkills(value){ if(!value||value===true) return []; if(value==='recommended') return ['office','commerce-api','crawling','ai-verified-skill-publisher']; if(value==='all') return listSkillPacks().map(x=>x.name); return String(value).split(',').map(x=>x.trim()).filter(Boolean); }
261
+ function ask(q){ const rl=readline.createInterface({input:process.stdin,output:process.stdout}); return new Promise(resolve=>rl.question(q,a=>{rl.close();resolve(a.trim());})); }
262
+ async function chooseSkills(autoYes,provided){ if(provided!==undefined) return splitSkills(provided); if(autoYes||!process.stdin.isTTY) return []; const packs=listSkillPacks(); if(!packs.length) return []; log(c.bold+'설치할 스킬 라이브러리 선택'+c.reset); log(' 0) 기본 하네스만 설치'); packs.forEach((p,i)=>{ log(' '+(i+1)+') '+(p.displayNameKo||p.title)+' ('+p.name+')'); if((p.capabilities||[]).length) log(' 가능 작업: '+p.capabilities.slice(0,4).join(', ')); }); log(' all) 전체 설치'); const ans=await ask('\n선택 (예: 1,3 또는 all, Enter=기본): '); if(!ans||ans==='0') return []; if(ans.toLowerCase()==='all') return packs.map(p=>p.name); return ans.split(',').map(s=>parseInt(s.trim(),10)).filter(n=>n>=1&&n<=packs.length).map(n=>packs[n-1].name); }
263
+
264
+ async function init(root,flags){
265
+ root=path.resolve(root||process.cwd()); fs.mkdirSync(root,{recursive:true}); banner(); installGuide(); info('대상: '+root);
266
+ const selectedSkills=await chooseSkills(Boolean(flags.yes||flags.y),flags.skills);
267
+ const found=detectLegacy(root), legacyText=collectLegacyText(found);
268
+ if(found.length){ warn('기존 하네스/지침 파일 감지: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
269
+ const archive=archiveLegacy(root,found,false); if(archive) info('백업 완료: '+rel(root,archive));
270
+ noteLegacyPreserved(root,found,false);
271
+ const ctx=makeContext(root,legacyText,selectedSkills);
272
+ for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{force:Boolean(flags.force)});
273
+ if(selectedSkills.length){ log(''); info('선택 스킬 설치 중: '+selectedSkills.join(', ')); for(const name of selectedSkills) installSkill(root,name,false); }
274
+ ok('설치 완료'); info('신규 프로젝트라면 .harness/AX_NEW_PROJECT_GUIDE.md를 따라 실제 프로젝트 내용을 반영하세요.');
393
275
  }
394
- function libraryCommand(args,flags){
395
- const sub=args[1]||'help';
396
- if(sub==='help'){ log(['Leerness Skill Library Commands','',' leerness library guide [project-path]',' leerness library status <path>',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library update <path> --from <validated-new-skill-path> [--version 1.3.0]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]','','업로드는 AI 검증 메타데이터가 있는 스킬만 가능하며 기본 publish는 dry-run입니다. --execute 시 npm/git 토큰이 필요합니다.',''].join('\n')); return; }
397
- if(sub==='guide') return libraryGuide(args[2]||flags.path||process.cwd(),flags);
398
- if(sub==='status') return libraryStatus(args[2]||process.cwd());
399
- if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false,strictAi:Boolean(flags['strict-ai']||flags.strictAi)});
400
- if(sub==='verify') return verifySkillLibrary(args[2]||process.cwd(),flags);
401
- if(sub==='build') return buildSkillLibrary(args[2]||process.cwd(),flags);
402
- if(sub==='update') return updateSkillLibrary(args[2]||process.cwd(),flags);
403
- if(sub==='merge') return mergeSkillLibrary(flags.path||process.cwd(),args[2]||flags.source,flags);
404
- if(sub==='migrate') return migrateSkillLibrary(args[2]||process.cwd(),flags);
405
- if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags);
406
- fail('알 수 없는 library 명령: '+sub); process.exitCode=1;
276
+ function migrate(root,flags){
277
+ root=path.resolve(root||process.cwd()); banner(); installGuide(); const dryRun=Boolean(flags['dry-run']); const force=Boolean(flags.force);
278
+ const found=detectLegacy(root);
279
+ if(!found.length) ok('마이그레이션할 legacy 항목이 없습니다. 누락/라우팅 파일만 점검합니다.');
280
+ else { warn('마이그레이션 대상: '+found.length+'개'); found.forEach(f=>log(' - '+f.item)); }
281
+ const archive=archiveLegacy(root,found,dryRun); if(archive) info((dryRun?'[dry-run] 백업 예정: ':'백업 완료: ')+rel(root,archive));
282
+ noteLegacyPreserved(root,found,dryRun);
283
+ const ctx=makeContext(root,collectLegacyText(found),[]);
284
+ for(const [file,template] of Object.entries(coreFiles)) writeCoreSafely(root,file,fill(template,ctx),{dryRun,force});
285
+ if(!dryRun){ ok('마이그레이션 완료'); info('기존 프로젝트 메모리 파일은 보존되었습니다. 템플릿 재생성이 필요하면 --force를 명시하세요.'); }
407
286
  }
287
+ 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(', '):'없음')); }
288
+ 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; }
289
+ 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('검증 완료'); }
408
290
 
409
291
  function skillDisplayName(meta){ return meta.displayNameKo || meta.titleKo || meta.title || meta.name; }
410
- function skillCapabilities(meta){ return Array.isArray(meta.capabilities) ? meta.capabilities : []; }
411
- function renderSkillMeta(meta){
412
- log('- '+meta.name+'@'+meta.version+' · '+skillDisplayName(meta));
413
- if(meta.title && meta.title!==skillDisplayName(meta)) log(' English: '+meta.title);
414
- if(meta.categoryKo || meta.category) log(' 분류: '+(meta.categoryKo||meta.category)+' / '+(meta.category||''));
415
- if(meta.description) log(' 설명: '+meta.description);
416
- const caps=skillCapabilities(meta);
417
- if(caps.length){ log(' 가능한 작업:'); caps.forEach(x=>log(' - '+x)); }
418
- log(' 업데이트: '+(meta.lastUpdated||'unknown')+' · 검증: '+verificationLabel(meta));
419
- if((meta.requiresEnv||[]).length) log(' 필요한 환경변수: '+meta.requiresEnv.join(', '));
420
- }
292
+ function verificationLabel(meta){ const v=(meta||{}).verification||{}; return v.status ? (v.status+(v.verifiedAt?' '+String(v.verifiedAt).slice(0,10):'')) : 'unknown'; }
293
+ 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(', ')); }
294
+ 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); }
421
295
 
422
- function skillCommand(args,flags){
423
- const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd());
424
- if(sub==='learn'){ flags.name=args[2]||flags.name; return learnSkillLibrary(root,flags); }
425
- if(sub==='library') return libraryCommand(['library'].concat(args.slice(2)),flags);
426
- if(sub==='list'){
427
- banner(); log('사용 가능한 스킬 라이브러리');
428
- log('한글명, 가능한 작업, 최종 업데이트일, AI 검증 상태를 함께 표시합니다.');
429
- for(const p of listSkillPacks()) renderSkillMeta(p);
430
- log('');
431
- info('상세 보기: leerness skill info <name>');
432
- info('설치 예시: leerness skill add ai-verified-skill-publisher');
433
- return;
434
- }
435
- if(sub==='info'||sub==='show'){
436
- const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill info commerce-api'); return; }
437
- const meta=getSkillMeta(name); if(!meta){ fail('알 수 없는 스킬 라이브러리: '+name); info('사용 가능 목록: '+listSkillPacks().map(x=>x.name).join(', ')); return; }
438
- banner(); renderSkillMeta(meta);
439
- const packRoot=path.join(PACKS_DIR,name);
440
- const guide=path.join(packRoot,'README.md');
441
- if(exists(guide)){ log('\n--- README ---\n'); log(read(guide)); }
442
- return;
443
- }
444
- const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill add commerce-api'); return; }
445
- if(sub==='add'||sub==='install') return installSkill(root,name,Boolean(flags['dry-run']));
446
- if(sub==='remove'||sub==='rm') return removeSkill(root,name);
447
- if(sub==='update') return installSkill(root,name,false);
448
- fail('알 수 없는 skill 명령: '+sub);
449
- }
450
- function status(root){
451
- 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:{}};
452
- 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('필수 파일 모두 존재');
453
- const names=Object.keys(lock.installedSkills||{}); log('설치 스킬: '+(names.length?names.join(', '):'없음'));
454
- for(const n of names){ const m=lock.installedSkills[n]; log(' - '+n+'@'+(m.version||'?')+' · updated '+(m.lastUpdated||'unknown')+' · '+(m.verificationStatus||'unknown')); }
455
- }
296
+ 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; }
297
+ 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(); }
298
+ 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; }
299
+ 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}; }
300
+ function learnSkillLibrary(root,flags){ root=path.resolve(root||process.cwd()); const from=path.resolve(flags.from||path.join(root,'.harness/skills')); const name=slug(flags.name||path.basename(from)); const outRoot=path.resolve(flags.out||path.join(root,'.harness/library',name)); if(!exists(from)){ fail('학습할 스킬 경로가 없습니다: '+from); process.exitCode=1; return; } const files=skillLibraryFiles(from).filter(f=>isTextFile(f)&&!f.includes(path.sep+'archive'+path.sep)); fs.mkdirSync(path.join(outRoot,'skills'),{recursive:true}); const envs=new Set(), copied=[]; for(const f of files){ const body=read(f); inferEnvNames(body).forEach(e=>envs.add(e)); const dest='skills/'+path.basename(f).replace(/[^a-zA-Z0-9._-]/g,'-'); write(path.join(outRoot,dest),body); copied.push(dest); } const meta={name,version:String(flags.version||'0.1.0'),title:flags.title||name,description:flags.description||'Learned Leerness skill library.',category:flags.category||'custom',requiresEnv:Array.from(envs).sort(),files:copied,lastUpdated:today(),lastUpdatedAt:now(),verification:{status:'needs-review',method:'none',verifiedBy:null,verifiedAt:null,checks:[]}}; write(path.join(outRoot,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(outRoot,'README.md'),'# '+meta.title+'\n\n'+meta.description+'\n'); ok('스킬 라이브러리 학습 완료: '+outRoot); info('다음: leerness library verify '+outRoot+' --ai --reviewer leerness-ai'); }
301
+ function verifySkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const res=validateSkillLibrary(dir,{silent:false}); if(!res.ok){ process.exitCode=1; return; } const meta=res.meta; meta.verification={status:'passed',method:'ai-assisted-review',verifiedBy:String(flags.reviewer||flags.by||'leerness-ai'),verifiedAt:now(),checks:['structure','secret-scan','env-reference-only','metadata']}; meta.lastUpdated=today(); meta.lastUpdatedAt=now(); write(path.join(dir,'skill-library.json'),JSON.stringify(meta,null,2)+'\n'); write(path.join(dir,'ai-verification.json'),JSON.stringify(meta.verification,null,2)+'\n'); ok('AI 검증 완료: '+meta.name); }
302
+ function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const res=validateSkillLibrary(dir,{silent:false,strictAi:Boolean(flags['strict-ai'])}); if(!res.ok){ process.exitCode=1; return; } const meta=res.meta; const out=path.resolve(flags.out||path.join(dir,'dist')); const libRoot=path.join(out,slug(meta.name)); if(exists(libRoot)) fs.rmSync(libRoot,{recursive:true,force:true}); fs.mkdirSync(libRoot,{recursive:true}); for(const item of ['README.md','skill-library.json','skill.json','ai-verification.json','env.example','skills','examples','migrations']){ const src=path.join(dir,item); if(exists(src)) copyRecursive(src,path.join(libRoot,item)); } const pkg={name:flags.package||('leerness-skill-'+slug(meta.name)),version:meta.version||'0.1.0',description:meta.description||meta.title||meta.name,type:'commonjs',files:['skill-library.json','ai-verification.json','README.md','env.example','skills/','examples/','migrations/'],keywords:['leerness','harness-skill','ai-skill-library'],license:'MIT',publishConfig:{access:'public'},harnessSkill:meta}; write(path.join(libRoot,'package.json'),JSON.stringify(pkg,null,2)+'\n'); ok('스킬 라이브러리 빌드 완료: '+libRoot); }
303
+ function resolvePublishToken(target,flags){ if(flags['token-env']&&process.env[flags['token-env']]) return process.env[flags['token-env']]; const names=target==='npm'?['LEERNESS_NPM_TOKEN','NPM_TOKEN','NODE_AUTH_TOKEN']:['LEERNESS_GIT_TOKEN','LEERNESS_GITHUB_TOKEN','GITHUB_TOKEN','GH_TOKEN']; for(const n of names) if(process.env[n]) return process.env[n]; return null; }
304
+ function publishSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); const target=String(flags.target||'npm'); const execute=Boolean(flags.execute); const res=validateSkillLibrary(dir,{silent:false,strictAi:true}); if(!res.ok){ process.exitCode=1; return; } if(!execute){ info('[dry-run] '+target+' publish target: '+dir); info('실제 업로드는 --execute가 필요합니다.'); return; } const token=resolvePublishToken(target,flags); if(!token && flags['no-prompt']){ fail('업로드 토큰이 없습니다. 환경변수 또는 --token-env를 설정하세요.'); process.exitCode=1; return; } if(target==='npm'){ const env={...process.env}; if(token) env.NODE_AUTH_TOKEN=token; const r=childProcess.spawnSync('npm',['publish','--access','public'],{cwd:dir,stdio:'inherit',env,shell:process.platform==='win32'}); process.exitCode=r.status||0; return; } if(target==='git'){ const repo=flags.repo||DEFAULT_GIT_REPOSITORY; info('Git target: '+repo); const run=(cmd,args)=>{ const r=childProcess.spawnSync(cmd,args,{cwd:dir,stdio:'inherit',shell:process.platform==='win32'}); if(r.status) process.exit(r.status); }; if(!exists(path.join(dir,'.git'))) run('git',['init']); try{ run('git',['remote','add','origin',repo]); }catch{} run('git',['add','.']); run('git',['commit','-m',flags.message||('Publish skill library '+res.meta.name)]); run('git',['branch','-M',flags.branch||'main']); run('git',['push','-u','origin',flags.branch||'main']); return; } fail('지원하지 않는 target: '+target); }
305
+ function libraryGuide(root){ root=path.resolve(root||process.cwd()); const p=path.join(root,'.harness/AX_SKILL_LIBRARY_GUIDE.md'); if(exists(p)) log(read(p)); else log(coreFiles['.harness/AX_SKILL_LIBRARY_GUIDE.md']); }
306
+ 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); }
456
307
 
457
- const taskRouteData = {
458
- feature: {read:['project-brief.md','current-state.md','context-routing.md','architecture.md','context-map.md','feature-contracts.md','skills/feature-implementation.md','testing-strategy.md'], update:['current-state.md','task-log.md','session-handoff.md','feature-contracts.md','context-map.md when important paths change']},
459
- ui: {read:['project-brief.md','current-state.md','design-system.md','feature-contracts.md','context-map.md','skills/ui-consistency.md'], update:['design-system.md','feature-contracts.md','current-state.md','task-log.md','session-handoff.md']},
460
- debugging: {read:['current-state.md','task-log.md','feature-contracts.md','testing-strategy.md','skills/debugging.md','context-map.md'], update:['task-log.md','current-state.md','session-handoff.md','testing-strategy.md when regression coverage changes']},
461
- refactor: {read:['architecture.md','decisions.md','guardrails.md','testing-strategy.md','skills/refactoring.md'], update:['architecture.md when structure changes','decisions.md when reasoning matters','task-log.md','session-handoff.md']},
462
- release: {read:['release-checklist.md','testing-strategy.md','current-state.md','decisions.md','secret-policy.md'], update:['release-checklist.md','task-log.md','current-state.md','session-handoff.md']},
463
- migration: {read:['AX_MIGRATION_GUIDE.md','context-routing.md','writeback-policy.md','architecture.md','decisions.md','release-checklist.md'], update:['current-state.md','task-log.md','session-handoff.md','context-map.md','release-checklist.md','decisions.md when needed']},
464
- 'new-install': {read:['AX_NEW_PROJECT_GUIDE.md','project-brief.md','context-map.md','guardrails.md','package/config files'], update:['project-brief.md','architecture.md','context-map.md','design-system.md','feature-contracts.md','release-checklist.md','testing-strategy.md','current-state.md','session-handoff.md']},
465
- 'skill-library': {read:['AX_SKILL_LIBRARY_GUIDE.md','skill-index.md','skills-lock.json','secret-policy.md'], update:['skill-index.md','skills-lock.json','task-log.md','session-handoff.md']},
466
- documentation: {read:['writeback-policy.md','context-routing.md','task-type-map.md'], update:['the specific memory file defined by writeback-policy.md','task-log.md','session-handoff.md']}
467
- };
468
- function routeCommand(task){
308
+
309
+ function closeSession(root){
310
+ root=path.resolve(root||process.cwd());
469
311
  banner();
470
- if(!task||task==='list'){
471
- log('사용 가능한 task type: '+Object.keys(taskRouteData).join(', '));
472
- log('예: leerness route release');
473
- return;
474
- }
475
- const r=taskRouteData[task];
476
- if(!r){ fail('알 수 없는 task type: '+task); log('사용 가능: '+Object.keys(taskRouteData).join(', ')); process.exitCode=1; return; }
477
- log('Task route: '+task);
478
- log('\nRead before work:'); r.read.forEach(x=>log(' - .harness/'+x));
479
- log('\nUpdate after work:'); r.update.forEach(x=>log(' - .harness/'+x));
480
- log('\nPolicy: read AGENTS.md, context-routing.md, writeback-policy.md, then the files above. Do not store secrets in harness files.');
312
+ const template=path.join(root,'.harness/templates/end-of-session-report.md');
313
+ const policy=path.join(root,'.harness/session-close-policy.md');
314
+ const progress=path.join(root,'.harness/progress-tracker.md');
315
+ log('Session close checklist');
316
+ log('');
317
+ log('Read before closing:');
318
+ ['.harness/session-close-policy.md','.harness/progress-tracker.md','.harness/current-state.md','.harness/task-log.md','.harness/session-handoff.md','.harness/anti-lazy-work-policy.md'].forEach(x=>log(' - '+x));
319
+ log('');
320
+ log('Required final report sections:');
321
+ ['Completed This Session','In Progress From User Requests','Incomplete / Not Started From User Requests','Verification','Files Changed','Memory Files Updated','Risks / Assumptions / Blockers','Recommended Next Directions','Next Exact Step'].forEach(x=>log(' - '+x));
322
+ log('');
323
+ if(exists(template)) log(read(template));
324
+ else log('# End-of-Session Report\n\n## Completed This Session\n-\n\n## In Progress From User Requests\n-\n\n## Incomplete / Not Started From User Requests\n-\n\n## Verification\n-\n\n## Files Changed\n-\n\n## Memory Files Updated\n-\n\n## Risks / Assumptions / Blockers\n-\n\n## Recommended Next Directions\n-\n\n## Next Exact Step\n-\n');
325
+ if(!exists(policy)) warn('session-close-policy.md가 없습니다. leerness migrate를 실행하세요.');
326
+ if(!exists(progress)) warn('progress-tracker.md가 없습니다. leerness migrate를 실행하세요.');
481
327
  }
328
+ function sessionCommand(args){ const sub=args[1]||'close'; if(sub==='close'||sub==='handoff'||sub==='end') return closeSession(args[2]||process.cwd()); fail('알 수 없는 session 명령: '+sub); }
482
329
 
483
- function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills office,commerce-api|recommended|all]',' leerness migrate [path] [--dry-run]',' leerness status [path]',' leerness verify [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation>','','Skills:',' leerness skill list',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill update <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path> [--out <library-path>]','','Skill library lifecycle:',' leerness library guide [path]',' leerness library status <path>',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--out ./dist] [--package leerness-skill-name]',' leerness library update <path> --from <validated-new-skill-path> [--version 1.3.0]',' leerness library merge <source-library> [--path <project>]',' leerness library migrate <path> [--version 1.0.0]',' leerness library publish <built-library> --target npm|git [--execute] [--repo https://github.com/gugu9999gu/leerness]',' leerness --version','','Examples:',' npx leerness init --skills recommended',' npx leerness skill learn coupang-order-sync --from .harness/skills/commerce-api/order-sync.md',' npx leerness library verify .harness/library/coupang-order-sync --ai --reviewer leerness-ai',' npx leerness library build .harness/library/coupang-order-sync',' npx leerness library publish .harness/library/coupang-order-sync/dist/coupang-order-sync --target npm --execute',''].join('\n')); }
484
-
485
- async function main(){ const parsed=parseArgs(process.argv.slice(2)); const args=parsed.positionals; const flags=parsed.flags; if(flags.version||flags.v){ log(VERSION); return; } if(flags.help||flags.h){ help(); return; } const cmd=args[0]||'init'; if(cmd==='init') return init(args[1]||process.cwd(),flags); if(cmd==='migrate') return migrate(args[1]||process.cwd(),flags); if(cmd==='status') return status(args[1]||process.cwd()); if(cmd==='verify') return verify(args[1]||process.cwd()); if(cmd==='route') return routeCommand(args[1]||'list'); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
330
+ const routeData={feature:{read:['project-brief.md','current-state.md','architecture.md','context-map.md','feature-contracts.md'],update:['current-state.md','task-log.md','session-handoff.md','feature-contracts.md']},ui:{read:['design-system.md','feature-contracts.md','context-map.md'],update:['design-system.md','feature-contracts.md','current-state.md','task-log.md']},debugging:{read:['current-state.md','task-log.md','feature-contracts.md','testing-strategy.md'],update:['task-log.md','current-state.md','session-handoff.md']},refactor:{read:['architecture.md','decisions.md','guardrails.md'],update:['architecture.md','decisions.md','task-log.md']},release:{read:['release-checklist.md','testing-strategy.md','current-state.md','decisions.md','secret-policy.md'],update:['release-checklist.md','task-log.md','current-state.md','session-handoff.md']},migration:{read:['AX_MIGRATION_GUIDE.md','context-routing.md','writeback-policy.md'],update:['Only missing files by default','Use --force only when requested']},'new-install':{read:['AX_NEW_PROJECT_GUIDE.md','actual project files'],update:['project-brief.md','architecture.md','context-map.md','design-system.md','feature-contracts.md','release-checklist.md']},'skill-library':{read:['AX_SKILL_LIBRARY_GUIDE.md','skill-index.md','secret-policy.md'],update:['skill-index.md','skills-lock.json','task-log.md']},documentation:{read:['writeback-policy.md','context-routing.md'],update:['specific memory file','task-log.md','session-handoff.md']},'session-close':{read:['session-close-policy.md','progress-tracker.md','current-state.md','task-log.md','session-handoff.md','anti-lazy-work-policy.md'],update:['session-handoff.md','progress-tracker.md','current-state.md','task-log.md','relevant changed memory files']}};
331
+ function routeCommand(task){ banner(); if(!task||task==='list'){ log('사용 가능한 task type: '+Object.keys(routeData).join(', ')); return; } const r=routeData[task]; if(!r){ fail('알 수 없는 task type: '+task); process.exitCode=1; return; } log('Task route: '+task); log('\nRead before work:'); r.read.forEach(x=>log(' - .harness/'+x)); log('\nUpdate after work:'); r.update.forEach(x=>log(' - .harness/'+x)); }
332
+ function help(){ log(['Leerness v'+VERSION,'','Usage:',' leerness init [path] [--yes] [--skills recommended|all|office,commerce-api] [--force]',' leerness migrate [path] [--dry-run] [--force]',' leerness status [path]',' leerness verify [path]',
333
+ ' leerness session close [path]',' leerness route <feature|ui|debugging|refactor|release|migration|new-install|skill-library|documentation|session-close>','','Skills:',' leerness skill list',' leerness skill info <name>',' leerness skill add <name> [--path <project>]',' leerness skill remove <name> [--path <project>]',' leerness skill learn <name> --from <validated-skill-path>','','Skill library:',' leerness library guide [path]',' leerness library validate <path> [--strict-ai]',' leerness library verify <path> --ai --reviewer leerness-ai',' leerness library build <path> [--package leerness-skill-name]',' leerness library publish <built-library> --target npm|git [--execute]',''].join('\n')); }
334
+ async function main(){ const parsed=parseArgs(process.argv.slice(2)); const args=parsed.positionals, flags=parsed.flags; if(flags.version||flags.v){ log(VERSION); return; } if(flags.help||flags.h){ help(); return; } const cmd=args[0]||'init'; if(cmd==='init') return init(args[1]||process.cwd(),flags); if(cmd==='migrate') return migrate(args[1]||process.cwd(),flags); if(cmd==='status') return status(args[1]||process.cwd()); if(cmd==='verify') return verify(args[1]||process.cwd()); if(cmd==='route') return routeCommand(args[1]||'list'); if(cmd==='session') return sessionCommand(args); if(cmd==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
486
335
  main().catch(err=>{ fail(err.stack||err.message); process.exit(1); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.3.0",
4
- "description": "Leerness: Context Routing, Writeback Policy, AX 마이그레이션/신규설치 가이드로 AI가 적재적소의 프로젝트 메모리를 읽고 갱신하게 하는 AX 최적화 개발 하네스.",
3
+ "version": "1.3.2",
4
+ "description": "Leerness: 세션 종료 인수인계, 진행/미완료 작업 추적, 추천 방향 제시, 게으른 작업 방지 정책을 포함한 AX 최적화 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
7
7
  "ai",
@@ -23,7 +23,13 @@
23
23
  "task-type-map",
24
24
  "ai-migration-guide",
25
25
  "new-project-guide",
26
- "project-memory-router"
26
+ "project-memory-router",
27
+ "session-close",
28
+ "session-handoff",
29
+ "unfinished-task-tracking",
30
+ "anti-lazy-work",
31
+ "progress-tracker",
32
+ "end-of-session-report"
27
33
  ],
28
34
  "bin": {
29
35
  "leerness": "./bin/harness.js"
@@ -37,7 +43,7 @@
37
43
  "LICENSE"
38
44
  ],
39
45
  "scripts": {
40
- "test": "node ./bin/harness.js --help && node ./bin/harness.js route release && node ./bin/harness.js init --yes --skills office,commerce-api,ai-verified-skill-publisher ./tmp-harness-test && node ./bin/harness.js status ./tmp-harness-test && node ./bin/harness.js skill list && node ./bin/harness.js skill info ai-verified-skill-publisher && node ./bin/harness.js library guide ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
46
+ "test": "node ./bin/harness.js --help && node ./bin/harness.js route session-close && node ./bin/harness.js init --yes --skills office,commerce-api,ai-verified-skill-publisher ./tmp-harness-test && node ./bin/harness.js status ./tmp-harness-test && node ./bin/harness.js session close ./tmp-harness-test && node ./bin/harness.js verify ./tmp-harness-test",
41
47
  "prepack": "node ./bin/harness.js --version"
42
48
  },
43
49
  "repository": {