leerness 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/harness.js CHANGED
@@ -5,48 +5,60 @@ 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');
8
9
 
9
- const VERSION = '1.0.0';
10
+ const VERSION = '1.3.0';
10
11
  const MARK = '<!-- leerness:managed -->';
11
12
  const MIGRATED = '<!-- leerness:migrated-legacy -->';
12
13
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
13
14
  const PACKS_DIR = path.join(PACKAGE_ROOT, 'skill-packs');
15
+ const DEFAULT_GIT_REPOSITORY = 'https://github.com/gugu9999gu/leerness';
14
16
  const c = { reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m', green:'\x1b[32m', yellow:'\x1b[33m', cyan:'\x1b[36m', red:'\x1b[31m', magenta:'\x1b[35m' };
15
17
 
16
18
  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'];
17
19
 
18
20
  const coreFiles = {
19
- 'AGENTS.md': [MARK,'# {{PROJECT}} AI Agent Harness','','Agent = Model + Harness.','','## Read Order','1. .harness/project-brief.md','2. .harness/current-state.md','3. .harness/architecture.md','4. .harness/context-map.md','5. .harness/guardrails.md','6. .harness/skill-index.md','7. .harness/skills-lock.json','','## Operating Rules','- Read project memory before editing.','- 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.','- After work, update current-state, task-log, and session-handoff.','- Record important decisions in decisions.md.','','## Skill Library Lifecycle','- Verified successful patterns can be learned with leerness skill learn.','- Skill libraries must pass secret scanning before build/publish/merge.','- npm/git upload is dry-run by default; --execute is required for real upload.','','## Response Contract','- Summary','- Files changed','- Verification','- Risks or assumptions','- Next step','{{LEGACY_AGENT}}',''].join('\n'),
20
- 'CLAUDE.md': [MARK,'# Claude Code Instructions','','Use AGENTS.md as the source of truth. Before editing, read .harness/current-state.md, architecture.md, context-map.md, guardrails.md, skills-lock.json, and the matching skill file.',''].join('\n'),
21
- '.cursor/rules/leerness.mdc': [MARK,'---','alwaysApply: true','---','Read AGENTS.md first. Follow .harness project memory, installed skills, design-system, feature-contracts, and guardrails.',''].join('\n'),
22
- '.github/copilot-instructions.md': [MARK,'# GitHub Copilot Instructions','','Use AGENTS.md and .harness/ as the project memory. Preserve architecture, feature contracts, security rules, and UI consistency.',''].join('\n'),
23
- '.gitignore': ['# Leerness local secrets','.env','.env.local','*.secret.json','.harness/skill-config.local.json',''].join('\n'),
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'),
24
26
  '.env.example': ['# Leerness environment variable examples','# Copy to .env.local and fill values locally. Never commit real secrets.','',''].join('\n'),
25
27
  '.harness/HARNESS_VERSION': '{{VERSION}}\n',
26
28
  '.harness/manifest.json': '{{MANIFEST}}\n',
27
29
  '.harness/skills-lock.json': '{{SKILLS_LOCK}}\n',
28
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'),
29
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'),
30
- '.harness/project-brief.md': [MARK,'# Project Brief: {{PROJECT}}','','## Purpose','','## Success Criteria','','## Users','','## Product Direction','{{LEGACY_BRIEF}}',''].join('\n'),
31
- '.harness/current-state.md': [MARK,'# Current State','','Updated: {{DATE}}','','## 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'),
32
- '.harness/architecture.md': [MARK,'# Architecture','','## Overview','','## Main Modules','','## Data Flow','','## External Services','','## Boundaries','{{LEGACY_ARCH}}',''].join('\n'),
33
- '.harness/context-map.md': [MARK,'# Context Map','','| 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'),
34
- '.harness/decisions.md': [MARK,'# Decision Log','','## Template','','### YYYY-MM-DD — Title','- Decision:','- Reason:','- Alternatives:','- Impact:','{{LEGACY_DECISIONS}}',''].join('\n'),
35
- '.harness/task-log.md': [MARK,'# Task Log','','## {{DATE}}','- Installed Leerness v{{VERSION}}.',''].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'),
36
38
  '.harness/constraints.md': [MARK,'# Constraints','','- Runtime/framework constraints','- Deployment constraints','- Security/privacy constraints','- Business rules',''].join('\n'),
37
- '.harness/guardrails.md': [MARK,'# Guardrails','','## 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'),
38
- '.harness/design-system.md': [MARK,'# Design System Memory','','## 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'),
39
- '.harness/feature-contracts.md': [MARK,'# Feature Contracts','','## Template','- Feature:','- Entry point:','- Input:','- Output:','- Error states:','- UI states:','- Related files:','- Tests:',''].join('\n'),
40
- '.harness/testing-strategy.md': [MARK,'# Testing Strategy','','- 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'),
41
- '.harness/review-checklist.md': [MARK,'# Review Checklist','','- [ ] 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'),
42
- '.harness/release-checklist.md': [MARK,'# Release Checklist','','- [ ] Build/test passed','- [ ] Env variables confirmed','- [ ] Migration impact checked','- [ ] Rollback path known','- [ ] Release notes prepared',''].join('\n'),
43
- '.harness/session-handoff.md': [MARK,'# Session Handoff','','## Done','-','','## Changed Files','-','','## Decisions','-','','## Risks','-','','## Next Exact Step','-',''].join('\n'),
44
- '.harness/skill-index.md': [MARK,'# Skill Index','','Installed skill libraries are tracked in `.harness/skills-lock.json`.','','## Commands','`leerness skill list`','`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 library publish .harness/library/my-skill/dist/my-skill --target npm --execute`','','## Metadata','Every skill should expose version, lastUpdated, lastUpdatedAt, and verification status.',''].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'),
45
52
  '.harness/AX_SKILL_LIBRARY_GUIDE.md': [MARK,
46
53
  '# Leerness AX Skill Library Guide',
47
54
  '',
48
55
  'AX는 AI eXperience입니다. AI 에이전트가 검증된 스킬 데이터를 안전하게 학습, 검증, 빌드, 업로드, 업데이트, 병합, 마이그레이션하도록 안내합니다.',
49
56
  '',
57
+ '## 스킬 라이브러리 표시 규격',
58
+ '- 모든 스킬은 name, displayNameKo, title, capabilities, lastUpdated, verification을 가진다.',
59
+ '- leerness skill list는 한글명과 가능한 작업을 먼저 보여준다.',
60
+ '- AI가 스킬을 업로드할 때는 ai-verified-skill-publisher 스킬의 절차를 따른다.',
61
+ '',
50
62
  '## 원칙',
51
63
  '- 실제 토큰, 쿠키, 비밀번호, 고객 데이터는 저장하지 않는다.',
52
64
  '- 환경변수 이름과 연결 규칙만 기록한다.',
@@ -114,7 +126,7 @@ function rel(root,p){ return path.relative(root,p).replace(/\\/g,'/') || '.'; }
114
126
  function isTextFile(p){ return /\.(md|mdc|txt|json|js|ts|tsx|jsx|yml|yaml|env|gitignore)$/i.test(p) || !path.extname(p); }
115
127
  function parseJsonSafe(s,fallback){ try { return JSON.parse(s); } catch { return fallback; } }
116
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(''); }
117
- function installGuide(){ log(c.bold+'설치 안내'+c.reset); log(' - 기존 AI 하네스/지침 파일을 감지하면 먼저 .harness/archive/ 에 백업합니다.'); log(' - .harness/ 아래에 프로젝트 메모리, 스킬, 디자인/기능 계약 문서를 생성합니다.'); log(' - 스킬 라이브러리는 실제 민감정보를 저장하지 않고 환경변수 이름만 기록합니다.'); log(' - library publish는 기본 dry-run이며, 실제 업로드는 --execute가 필요합니다.'); 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(''); }
118
130
  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); }
119
131
 
120
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; }); }
@@ -132,15 +144,15 @@ function makeContext(root,legacyText,selectedSkills){ const date=new Date().toIS
132
144
 
133
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)); }
134
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; }
135
- 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,requiresEnv:meta.requiresEnv||[]}; write(lp,JSON.stringify(lock,null,2)+'\n'); }
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'); }
136
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'); }
137
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; }
138
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); }
139
151
 
140
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; }
141
- function splitSkills(value){ if(!value||value===true) return []; if(value==='recommended') return ['office','commerce-api','crawling']; if(value==='all') return listSkillPacks().map(x=>x.name); return String(value).split(',').map(x=>x.trim()).filter(Boolean); }
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); }
142
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());})); }
143
- 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.title+' ('+p.name+')')); 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); }
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); }
144
156
 
145
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를 프로젝트에 맞게 채우세요.'); }
146
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('마이그레이션 완료'); }
@@ -161,9 +173,9 @@ function buildSkillLibrary(dir,flags){ dir=path.resolve(dir||process.cwd()); con
161
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)); }
162
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); }
163
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; }
164
- 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]','','기본 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; }
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; }
165
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); }
166
- 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]','','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 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]',' 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')); }
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')); }
167
179
 
168
180
 
169
181
  function nowIso(){ return new Date().toISOString(); }
@@ -309,11 +321,67 @@ function migrateSkillLibrary(dir,flags){
309
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); }
310
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'); }
311
323
  }
312
- function publishSkillLibrary(dir,flags){
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;
332
+ }
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){
313
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; }
314
- if(!isAiVerified(check.meta)){ fail('AI 검증된 스킬만 업로드할 수 있습니다. `leerness library verify <path> --ai`를 먼저 실행하세요.'); process.exitCode=1; return; }
315
- 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] AI 검증 통과. 실행 예정: (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; }
316
- if(target==='git'){ const repo=flags.repo; const branch=flags.branch||'main'; const message=flags.message||('Publish verified skill library '+check.meta.name+'@'+(check.meta.version||'0.1.0')); if(!execute){ info('[dry-run] AI 검증 통과. 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; }
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;
378
+ }
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;
384
+ }
317
385
  fail('지원하지 않는 publish target: '+target); process.exitCode=1;
318
386
  }
319
387
  function libraryGuide(root,flags={}){
@@ -325,7 +393,7 @@ function libraryGuide(root,flags={}){
325
393
  }
326
394
  function libraryCommand(args,flags){
327
395
  const sub=args[1]||'help';
328
- 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.1.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]','','업로드는 AI 검증 메타데이터가 있는 스킬만 가능하며 기본 publish는 dry-run입니다.',''].join('\n')); return; }
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; }
329
397
  if(sub==='guide') return libraryGuide(args[2]||flags.path||process.cwd(),flags);
330
398
  if(sub==='status') return libraryStatus(args[2]||process.cwd());
331
399
  if(sub==='validate') return validateSkillLibrary(args[2]||process.cwd(),{silent:false,strictAi:Boolean(flags['strict-ai']||flags.strictAi)});
@@ -337,18 +405,40 @@ function libraryCommand(args,flags){
337
405
  if(sub==='publish'||sub==='upload') return publishSkillLibrary(args[2]||process.cwd(),flags);
338
406
  fail('알 수 없는 library 명령: '+sub); process.exitCode=1;
339
407
  }
408
+
409
+ 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
+ }
421
+
340
422
  function skillCommand(args,flags){
341
423
  const sub=args[1]||'list'; const root=path.resolve(flags.path||process.cwd());
342
424
  if(sub==='learn'){ flags.name=args[2]||flags.name; return learnSkillLibrary(root,flags); }
343
425
  if(sub==='library') return libraryCommand(['library'].concat(args.slice(2)),flags);
344
426
  if(sub==='list'){
345
427
  banner(); log('사용 가능한 스킬 라이브러리');
346
- for(const p of listSkillPacks()){
347
- log('- '+p.name+'@'+p.version+': '+p.title);
348
- log(' '+p.description);
349
- log(' updated: '+(p.lastUpdated||'unknown')+' · verification: '+verificationLabel(p));
350
- if((p.requiresEnv||[]).length) log(' env: '+(p.requiresEnv||[]).join(', '));
351
- }
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)); }
352
442
  return;
353
443
  }
354
444
  const name=args[2]; if(!name){ fail('스킬 이름이 필요합니다. 예: leerness skill add commerce-api'); return; }
@@ -363,7 +453,34 @@ function status(root){
363
453
  const names=Object.keys(lock.installedSkills||{}); log('설치 스킬: '+(names.length?names.join(', '):'없음'));
364
454
  for(const n of names){ const m=lock.installedSkills[n]; log(' - '+n+'@'+(m.version||'?')+' · updated '+(m.lastUpdated||'unknown')+' · '+(m.verificationStatus||'unknown')); }
365
455
  }
366
- 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]','','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.1.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]',' 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')); }
367
456
 
368
- 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==='skill') return skillCommand(args,flags); if(cmd==='library') return libraryCommand(args,flags); help(); process.exitCode=1; }
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){
469
+ 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.');
481
+ }
482
+
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; }
369
486
  main().catch(err=>{ fail(err.stack||err.message); process.exit(1); });
@@ -0,0 +1,29 @@
1
+ # AX Migration Guide for AI Agents
2
+
3
+ 구버전 하네스, 자체 AGENTS/CLAUDE 지침, 오래된 `.harness/` 파일을 Leerness 최신 구조로 옮기기 위한 AI 전용 가이드입니다.
4
+
5
+ ## 목표
6
+
7
+ 기존 지식을 잃지 않고, 새 구조에서 AI가 어떤 파일을 읽고 어떤 파일을 갱신해야 하는지 명확하게 만듭니다.
8
+
9
+ ## 절차
10
+
11
+ 1. `leerness migrate --dry-run`으로 감지 목록을 확인합니다.
12
+ 2. 기존 파일을 `.harness/archive/legacy-migration-*`에 백업합니다.
13
+ 3. legacy 내용을 새 목적지로 분류합니다.
14
+ 4. `context-routing.md`, `writeback-policy.md`, `task-type-map.md`를 생성합니다.
15
+ 5. `AGENTS.md`가 새 라우팅 파일들을 보도록 갱신합니다.
16
+ 6. 민감정보 값은 제거하고 변수명만 남깁니다.
17
+ 7. `leerness verify`를 실행합니다.
18
+ 8. `session-handoff.md`에 마이그레이션 결과와 다음 작업을 기록합니다.
19
+
20
+ ## Legacy mapping
21
+
22
+ | Legacy content | New destination |
23
+ |---|---|
24
+ | 프로젝트 목적/방향 | project-brief.md |
25
+ | 현재 상태/작업 이력 | current-state.md, task-log.md |
26
+ | 구조/모듈/데이터 흐름 | architecture.md, context-map.md |
27
+ | 기존 AI 규칙 | AGENTS.md, guardrails.md, context-routing.md |
28
+ | 배포 이슈 | release-checklist.md |
29
+ | 성공한 반복 작업 | skill candidate 또는 .harness/skills/ |
@@ -0,0 +1,25 @@
1
+ # AX New Project Installation Guide for AI Agents
2
+
3
+ 진행 중인 프로젝트에 Leerness를 처음 적용할 때, AI가 프로젝트의 실제 내용을 하네스에 반영하기 위한 가이드입니다.
4
+
5
+ ## 목표
6
+
7
+ 템플릿을 그대로 두지 않고, 프로젝트 구조와 운영 방식이 반영된 하네스를 만듭니다.
8
+
9
+ ## 절차
10
+
11
+ 1. package.json, lockfile, framework config, routing, API, DB, deploy, test 파일을 확인합니다.
12
+ 2. project-brief.md에 목적, 사용자, 성공 기준을 작성합니다.
13
+ 3. architecture.md에 모듈, 경계, 데이터 흐름, 외부 서비스를 작성합니다.
14
+ 4. context-map.md에 실제 파일 경로를 기록합니다.
15
+ 5. design-system.md에 기존 UI 패턴을 기록합니다.
16
+ 6. feature-contracts.md에 주요 기능의 입력/출력/상태/오류를 기록합니다.
17
+ 7. release-checklist.md에 실제 배포/npm/git/CI/env 조건을 기록합니다.
18
+ 8. testing-strategy.md에 실제 검증 방법을 기록합니다.
19
+ 9. current-state.md와 session-handoff.md에 다음 정확한 작업을 기록합니다.
20
+
21
+ ## 금지
22
+
23
+ - 확인하지 않은 구조를 추측해서 쓰지 않습니다.
24
+ - 실제 토큰, 쿠키, 비밀번호를 기록하지 않습니다.
25
+ - 모르는 항목은 `Unknown` 또는 `Needs confirmation`으로 표시합니다.