From d048ad2e3ca12f196e266ca570bbb2f7ba06feb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EC=A2=85=EC=9C=A4?= Date: Wed, 29 Apr 2026 00:03:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20v0.0.5=20=E2=80=94=20Asset=20Store=20id?= =?UTF-8?q?entity=20+=20production=20pipeline=20closure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single consolidated release. Originally drafted as 0.0.5 (rename) + 0.0.6 (pipeline closure); both ship together as 0.0.5 because each is the other's prerequisite. ────────────────────────────────────────────────────────────────────── Asset Store identity correction ────────────────────────────────────────────────────────────────────── Audit: 8 scripts, all Korean UI strings, and the schema framing used "asset/자산", but the file/field names were "RAG". Only query-assets.sh --format inject is strict RAG; the rest is asset CRUD + lifecycle. Renamed: - .harnish/harnish-rag.jsonl → .harnish/harnish-assets.jsonl (init-assets.sh auto-migrates idempotently — no data loss) - harnish-rag-archive.jsonl → harnish-assets-archive.jsonl - schema.json: rag_record → asset_record, rag_file → asset_file - 14 scripts: RAG_FILE / RAG → ASSET_FILE / ASSETS Added: - 2 migration regression tests (legacy file rename + idempotency) - README "Memory Model" section (two-tier: Asset Store + Skills) - Honest skillify framing (draft generator, not autonomous) - common.sh: resolve_asset_file() + resolve_legacy_asset_file(); resolve_rag_file() kept as deprecated alias ────────────────────────────────────────────────────────────────────── Production pipeline closure (Trigger → Record → Skillify) ────────────────────────────────────────────────────────────────────── Three production gaps found via ralphi live audit. All closed end-to-end. CRITICAL — pending → assets persistence: - Pre-fix: Stop hook deleted pending file without persisting failures ("failures become guardrails" was broken at the first hop). - Post-fix: scripts/promote-pending.sh dedup's by (tool, first_error_line) and auto-records as failure assets with tags [auto, tool:, session:] + occurrences metadata. - detect-asset.sh Stop event now invokes promote-pending automatically. HIGH — skillify production-grade scaffold: - description includes Triggers: line auto-extracted from asset titles (5 candidates, frequency-ranked). - Body sectioned by asset type (Failures / Patterns / Guardrails / Decisions / Snippets) with metadata (level / confidence / stability / resolved). - references/source-assets.jsonl preserves original assets. MEDIUM — query --format inject RCA enrichment: - guardrail/level, decision/confidence, pattern/sN inline header. - context: line per asset; failure shows resolved: status. ────────────────────────────────────────────────────────────────────── README cleanup — "ralph loop" honesty ────────────────────────────────────────────────────────────────────── Quickstart/Usage used to break the loop into [READ]/[ACT]/[LOG]/[PROGRESS], which read like a "RALP" acronym. It isn't — ralphi and the ralph loop are both named after Ralph Wiggum (keep trying, don't give up). The acronym-shape has been removed; explicit Naming-section reference added. ────────────────────────────────────────────────────────────────────── Tests: 80/80 bats + 60/60 scripts/test-all.sh. - tests/e2e_pipeline.bats +4 (trigger→record, dedup, skillify quality, inject enrichment) - tests/scripts_advanced.bats +2 (rename migration + idempotency) CI: matrix reduced to macos-latest only. Compatibility: - Drop-in upgrade. Auto-migration runs on first init-assets.sh. - CLI unchanged. resolve_rag_file() kept as deprecated alias. --- .claude-plugin/plugin.json | 2 +- .github/workflows/tests.yml | 7 +- CHANGELOG.md | 39 ++++- README.ko.md | 78 +++++---- README.md | 79 +++++---- VERSION | 2 +- scripts/abstract-asset.sh | 14 +- scripts/check-thresholds.sh | 6 +- scripts/common.sh | 14 +- scripts/compress-assets.sh | 12 +- scripts/detect-asset.sh | 20 ++- scripts/init-assets.sh | 13 +- scripts/localize-asset.sh | 14 +- scripts/migrate.sh | 18 +-- scripts/promote-pending.sh | 150 ++++++++++++++++++ scripts/purge-assets.sh | 16 +- scripts/quality-gate.sh | 6 +- scripts/query-assets.sh | 23 ++- scripts/record-asset.sh | 12 +- scripts/skillify.sh | 123 ++++++++++++-- scripts/test-all.sh | 50 +++--- skills/drafti-architect/SKILL.ko.md | 2 +- skills/drafti-architect/SKILL.md | 2 +- skills/drafti-feature/SKILL.ko.md | 2 +- skills/drafti-feature/SKILL.md | 2 +- skills/forki/SKILL.ko.md | 2 +- skills/forki/SKILL.md | 2 +- skills/impl/SKILL.ko.md | 4 +- skills/impl/SKILL.md | 4 +- skills/impl/references/retention-policy.ko.md | 2 +- skills/impl/references/retention-policy.md | 2 +- skills/impl/references/schema.json | 10 +- skills/impl/references/thresholds.ko.md | 2 +- skills/impl/references/thresholds.md | 2 +- skills/ralphi/SKILL.ko.md | 2 +- skills/ralphi/SKILL.md | 2 +- tests/e2e_assets.bats | 32 ++-- tests/e2e_pipeline.bats | 150 ++++++++++++++++++ tests/e2e_workflow.bats | 2 +- tests/scripts.bats | 12 +- tests/scripts_advanced.bats | 55 +++++-- 41 files changed, 765 insertions(+), 226 deletions(-) create mode 100755 scripts/promote-pending.sh create mode 100644 tests/e2e_pipeline.bats diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a772d2f..46b125e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "harnish", - "version": "0.0.4", + "version": "0.0.5", "description": "자율 구현 엔진. PRD 생성(drafti) → 자율 구현(harnish) → 점검(ralphi).", "author": { "name": "jazz1x", diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 158e675..a3d94b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -20,12 +20,7 @@ jobs: with: python-version: "3.12" - - name: Install bats + jq (ubuntu) - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get update && sudo apt-get install -y bats jq - - name: Install bats + jq (macos) - if: matrix.os == 'macos-latest' run: brew install bats-core jq - name: Run test suite diff --git a/CHANGELOG.md b/CHANGELOG.md index ed17c79..406b4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.5] - 2026-04-28 + +This release combines the asset-store identity correction with the production pipeline closure originally drafted as 0.0.6. Both ship together as 0.0.5. + +### Fixed (CRITICAL — closed loop restored) +- **Trigger→Record pipeline closure**: hook이 `/tmp/harnish-pending-*.jsonl`에 적재한 실패 컨텍스트가 세션 종료 시 삭제만 되고 자산으로 영속화되지 않던 버그를 수정. `Stop` 이벤트가 새 `promote-pending.sh`를 호출하여 자동 dedup + `record-asset.sh` 호출 + 영속화. README의 "failures become guardrails" 약속이 처음으로 실제 동작. + +### Changed +- **BREAKING (file rename, auto-migrated)**: Asset store renamed `.harnish/harnish-rag.jsonl` → `.harnish/harnish-assets.jsonl`. `init-assets.sh` performs an idempotent atomic `mv` on first run if the legacy file is present and the new file is absent. No data loss. +- Archive file renamed: `harnish-rag-archive.jsonl` → `harnish-assets-archive.jsonl` (`purge-assets.sh --execute` output) +- L0 Contract (`schema.json`) field rename: `rag_record` → `asset_record`, `storage.rag_file` → `storage.asset_file` +- 14 scripts: `RAG_FILE` / `RAG` shell variables → `ASSET_FILE` / `ASSETS`; `harnish-rag.jsonl` literals replaced +- `common.sh`: `resolve_rag_file()` kept as deprecated alias of `resolve_asset_file()`; new `resolve_legacy_asset_file()` for migration paths +- `scripts/skillify.sh` production-grade 업그레이드: + - description에 `Triggers: ...` 자동 생성 (자산 title에서 빈도 기반 5개 후보 추출) + - body 구조화: §1 가이드라인 (LLM finalize) / §2 타입별 자산 섹션 (Failures/Patterns/Guardrails/Decisions/Snippets) / §3 메타데이터 + - `references/source-assets.jsonl` 트레이서빌리티 보존 + - 자산 메타 필드 노출: level / confidence / stability / resolved +- `scripts/query-assets.sh --format inject` 출력에 RCA 컨텍스트 포함: + - guardrail은 `[guardrail/soft]`, decision은 `[decision/medium]`, pattern은 `[pattern/s1]`로 type 머리에 메타 노출 + - 각 자산에 `context:` 라인 추가, failure는 `resolved:` 표시 + +### Added +- `scripts/promote-pending.sh` — pending JSONL을 `(tool, first_error_line)` 키로 deduplicate 후 자동으로 `failure` 자산 등록. 태그: `auto`, `tool:`, `session:`. context에 occurrences 카운트. +- `tests/e2e_pipeline.bats` — 4개 production E2E (trigger→record, dedup, skillify quality, inject 풍부화). +- `tests/scripts_advanced.bats` +2: legacy `harnish-rag.jsonl` → `harnish-assets.jsonl` migration regression + idempotency test +- README "Memory Model" section: documents the two-tier model (Tier 1 Asset Store + Tier 2 Skills) and clarifies that only `query-assets.sh --format inject` is strict RAG + +### Documentation +- README "ralph loop"의 잘못된 약자 풀이 (`Read → Act → Log → Progress`) 제거. 이름은 심슨의 Ralph Wiggum에서 유래한 것이며 약자가 아님을 명시. +- Honest framing of `skillify.sh`: it is a draft generator (asset bundling + TODO scaffold), **not** autonomous skill graduation. Future feature flagged on roadmap. +- README/SKILL.md/thresholds.md path references updated to `harnish-assets.jsonl` + ## [0.0.4] - 2026-04-27 ### Fixed @@ -72,7 +105,7 @@ First public release. 5 skills + shared script suite + asset infrastructure + au #### impl `0.0.1` (the "harnish" engine) - 자율 구현 엔진 (시딩 + ralph 루프 + 앵커링 + 경험 축적) - 모드 A: PRD → 원자적 태스크 분해 → `harnish-current-work.json` 생성 -- 모드 B: ralph 루프 (Read → Act → Log → Progress → repeat) +- 모드 B: ralph 루프 (한 태스크씩 자동 실행 → 결과 기록 → 진행률 갱신 반복) - 모드 C: 세션 복원 (앵커링) + 자산 감지/기록/압축/스킬화 - §B.9 acceptance_criteria 실행: bash/조건/혼합/없음 4가지 분기 - 다언어 타입 체커 (Python/TS/Go/Java/Rust) @@ -119,7 +152,9 @@ First public release. 5 skills + shared script suite + asset infrastructure + au - README 구조 정리 (galmuri 동일 톤): badges, install steps, quickstart, usage, hooks, assets, worktrees, fork & customize, naming, triad - VERSIONING.md, references/* 가이드 -[Unreleased]: https://github.com/jazz1x/harnish/compare/v0.0.3...HEAD +[Unreleased]: https://github.com/jazz1x/harnish/compare/v0.0.5...HEAD +[0.0.5]: https://github.com/jazz1x/harnish/compare/v0.0.4...v0.0.5 +[0.0.4]: https://github.com/jazz1x/harnish/compare/v0.0.3...v0.0.4 [0.0.3]: https://github.com/jazz1x/harnish/compare/v0.0.2...v0.0.3 [0.0.2]: https://github.com/jazz1x/harnish/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/jazz1x/harnish/releases/tag/v0.0.1 diff --git a/README.ko.md b/README.ko.md index 80e86fd..2ec3728 100644 --- a/README.ko.md +++ b/README.ko.md @@ -2,10 +2,10 @@ > Claude Code 플러그인 — 자율 구현 엔진 -![version](https://img.shields.io/badge/version-0.0.4-blue) +![version](https://img.shields.io/badge/version-0.0.5-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![claude-code](https://img.shields.io/badge/claude--code-plugin-purple) -![tests](https://img.shields.io/badge/tests-74%20passing-brightgreen) +![tests](https://img.shields.io/badge/tests-80%20passing-brightgreen) **harnish** (harness + ish) = "대충 하네스 비스무리한 것" — 작업할수록 똑똑해지는 구현 환경. 실패가 가드레일이 되고, 패턴이 축적되며, 세션과 워크트리가 바뀌어도 맥락이 유실되지 않는다. @@ -16,8 +16,8 @@ | Skill | Command | Role | |-------|---------|------| | **forki** | `/harnish:forki` | 의사결정 강제 (2지선택 + D/E/V/R + trade-off, HITL 전용) | -| **drafti-architect** | `/harnish:drafti-architect` | 기술 주도 설계 PRD 생성 | | **drafti-feature** | `/harnish:drafti-feature` | 기획 기반 구현 명세 PRD 생성 | +| **drafti-architect** | `/harnish:drafti-architect` | 기술 주도 설계 PRD 생성 | | **impl** | `/harnish:impl` | 자율 구현 엔진 — "harnish" 엔진 (시딩 + ralph 루프 + 앵커링 + 경험축적) | | **ralphi** | `/harnish:ralphi` | 점검 (HITL 보고 또는 자율 수정) | @@ -59,7 +59,7 @@ Claude Code 세션 안에서 실행: 예상 출력: ``` -✓ Installed harnish@0.0.4 — 5 skills registered (forki, drafti-architect, drafti-feature, impl, ralphi) +✓ Installed harnish@0.0.5 — 5 skills registered (forki, drafti-feature, drafti-architect, impl, ralphi) ``` ### 3. 확인 @@ -72,8 +72,8 @@ Claude Code 세션 안에서 실행: ``` /harnish:forki -/harnish:drafti-architect /harnish:drafti-feature +/harnish:drafti-architect /harnish:impl /harnish:ralphi ``` @@ -102,21 +102,14 @@ harnish 의 훅은 `hooks/hooks.json` 에 정의되어 있고, 플러그인 로 /harnish:impl ``` -예시 흐름 (단순화): +예시 흐름: ``` -user > /harnish:impl docs/prd-redis-cache.md - -step 1 > PRD 읽기 → 3 phase, 12개 원자적 태스크 식별 -step 2 > 시딩 → .harnish/harnish-current-work.json 생성 -step 3 > "Phase 1 Task 1.1 준비됨. 'loop' 입력 시 ralph 루프 시작" -user > loop - -step 4 > [READ] task 1.1 → [ACT] 코드 작성 → [LOG] 결과 → [PROGRESS] 1/12 -step 5 > Pass → 자산 기록 (pattern: connection-pool-init) -step 6 > 자동 진행 → task 1.2 … - ⋮ -step N > Phase 1 완료 → 마일스톤 보고 → Phase 2 계속? (y/n) +user > /harnish:impl docs/prd-redis-cache.md + → 3 phase, 12개 원자적 태스크가 .harnish/harnish-current-work.json 에 시딩됨 +user > loop + → task 1.1 → 코드 → 기록 → 자산 등록 → 1.2 자동 진행 → … → Phase 1 완료 + → 마일스톤 보고 → Phase 2 계속? (y/n) ``` PRD 경로나 작업 설명 없이 호출하면 harnish 가 먼저 묻는다: @@ -145,13 +138,13 @@ PRD 경로나 작업 설명 없이 호출하면 harnish 가 먼저 묻는다: ### 1. PRD 생성 (설계) ``` -사용자: "Redis 캐시 레이어 설계해줘" -→ drafti-architect가 설계 대안 2~3개 탐색, 트레이드오프 분석 -→ docs/prd-redis-cache.md 생성 - 사용자: "이 기획서로 PRD 만들어" (기획 문서 첨부) → drafti-feature가 구현 명세 PRD 생성 (피쳐플래그는 필요 시만) → docs/prd-user-profile-edit.md 생성 + +사용자: "Redis 캐시 레이어 설계해줘" +→ drafti-architect가 설계 대안 2~3개 탐색, 트레이드오프 분석 +→ docs/prd-redis-cache.md 생성 ``` ### 2. 자율 구현 (harnish) @@ -165,7 +158,8 @@ PRD 경로나 작업 설명 없이 호출하면 harnish 가 먼저 묻는다: → "Phase 3개, Task 12개 시딩 완료 — 확인 후 '루프 돌려'" 사용자: "루프 돌려" -→ ralph 루프 자동 실행 (Read → Act → Log → Progress → repeat) +→ ralph 루프가 한 태스크씩 자동 실행 (Phase 끝까지 반복) + ("ralph"는 심슨의 랄프 위검에서 따온 이름 — 약자 풀이 아님) → 매 3액션마다 harnish-current-work.json 갱신, Phase 완료 시 마일스톤 보고 사용자: (새 세션에서) "이어서 진행" @@ -193,7 +187,9 @@ PRD 경로나 작업 설명 없이 호출하면 harnish 가 먼저 묻는다: → 축적된 failure/pattern/guardrail/snippet/decision 현황 조회 사용자: "스킬로 만들어" -→ 압축된 자산에서 재사용 가능한 SKILL.md 초안 생성 +→ 압축된 자산을 SKILL.md scaffold로 묶음 (원본 자산 본문 + TODO 마커). + 본문은 LLM이 마무리해야 함 — draft generator일 뿐 자율 graduating은 + 아니다. 진짜 자율 승격은 향후 기능. ``` ## Hooks @@ -208,9 +204,35 @@ harnish 의 훅은 `hooks/hooks.json` 으로 설치 시 자동 등록된다. 별 실패는 신호/노이즈로 분류된다 — 단순 에러(`No such file`, `permission denied`, `command not found` 등)는 필터되고 의미 있는 실패만 자산이 된다. +## Memory Model + +harnish는 **2단 기억 구조** (two-tier memory)로 동작한다. 각 tier의 역할이 다르며, 둘을 잇는 다리는 현재 반자동 상태. + +| Tier | 저장 위치 | 수명 | 역할 | 로딩 방식 | +|------|----------|------|------|----------| +| **Tier 1 — Asset Store** (episodic) | `.harnish/harnish-assets.jsonl` | 프로젝트별, 세션 간 누적, TTL purge | 일어난 일을 기록 (failure, pattern, guardrail, snippet, decision) | `query-assets.sh --format inject` 로 컨텍스트 주입 (실제 RAG 경로) | +| **Tier 2 — Skills** (procedural) | `skills/*/SKILL.md` | 영구 (소스 트리 버전 관리) | 안정화된 행동 양식 | Claude Code가 자동 로드, 트리거 시 발동 | + +`skillify.sh`가 그 다리 — 압축된 Tier-1 자산을 Tier-2 SKILL.md scaffold로 묶는다. v0.0.5부터 scaffold는 **production-grade**: + +- frontmatter `Triggers:` 가 자산 title에서 자동 추출됨 +- body는 자산 타입별 섹션 + 메타 (level / confidence / stability / resolved) +- `references/source-assets.jsonl` 로 원본 트레이서빌리티 보존 +- §1은 여전히 LLM finalize 필요 (1-3개 가이드라인 도출) — draft generator일 뿐, 자율 graduating 아님 + +**Trigger → Record → Skillify 파이프라인 (v0.0.5에서 닫힘):** + +``` +PostToolUseFailure → detect-asset.sh (노이즈 필터) → /tmp/harnish-pending-*.jsonl +Stop → promote-pending.sh (dedup) → harnish-assets.jsonl +"스킬로 만들어" → skillify.sh → SKILL.md draft + references/ +``` + +> **왜 "RAG"가 아니라 "assets"인가?** 엄밀한 의미의 RAG는 `query-assets.sh --format inject` 한 경로뿐. 나머지는 캡처 / 요약 / 노화 / 스킬 초안 공급 — 즉 자산 CRUD + 라이프사이클. + ## Assets -모든 학습 결과는 `.harnish/harnish-rag.jsonl` 에 한 줄당 한 JSON 객체로 기록된다. 6가지 자산 타입: +모든 학습 결과는 `.harnish/harnish-assets.jsonl` 에 한 줄당 한 JSON 객체로 기록된다. 6가지 자산 타입: | Type | 기록 시점 | |------|---------| @@ -232,7 +254,7 @@ bash scripts/purge-assets.sh # dry-run purge (- bash scripts/migrate.sh # 스키마 최신 버전으로 백필 ``` -`.harnish/` 는 프로젝트 CWD 안에 위치하며 세션 간 유지된다. `impl`, `drafti-architect`, `drafti-feature` 가 각 스킬의 Step 2에서 태그 기반으로 관련 자산을 자동 참조한다. +`.harnish/` 는 프로젝트 CWD 안에 위치하며 세션 간 유지된다. `impl`, `drafti-feature`, `drafti-architect` 가 각 스킬의 Step 2에서 태그 기반으로 관련 자산을 자동 참조한다. ## 워크트리 @@ -255,7 +277,7 @@ mkdir -p .claude/skills cp -r /path/to/harnish/skills/forki .claude/skills/ ``` -해당 스킬이 `forki` 로 호출 가능 (플러그인 네임스페이스 없음). `forki` 대신 `impl`, `ralphi`, `drafti-architect`, `drafti-feature` 중 어느 것도 가능. +해당 스킬이 `forki` 로 호출 가능 (플러그인 네임스페이스 없음). `forki` 대신 `impl`, `ralphi`, `drafti-feature`, `drafti-architect` 중 어느 것도 가능. ### B. 자체 플러그인 마켓으로 포크 @@ -283,7 +305,7 @@ git -C /path/to/harnish pull # 업데이트 - **harnish** = harness + ish (자율 구현 엔진) - **ralphi** = ralph + i (점검) - 유래: 심슨 가족의 캐릭터 '랄프 위검'처럼 포기하지 않고 끈질기게 시도한다는 의미 -- **drafti** = draft + i (PRD 생성 — drafti-architect + drafti-feature) +- **drafti** = draft + i (PRD 생성 — drafti-feature + drafti-architect) - **forki** = fork + i (의사결정 강제 — 2지선택 + D/E/V/R + trade-off, HITL 전용) ## Triad diff --git a/README.md b/README.md index 9a4b2df..e72149b 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ > Claude Code plugin — autonomous implementation engine -![version](https://img.shields.io/badge/version-0.0.4-blue) +![version](https://img.shields.io/badge/version-0.0.5-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![claude-code](https://img.shields.io/badge/claude--code-plugin-purple) -![tests](https://img.shields.io/badge/tests-74%20passing-brightgreen) +![tests](https://img.shields.io/badge/tests-80%20passing-brightgreen) **harnish** (harness + ish) — an implementation environment that gets smarter as you work. Failures become guardrails, patterns accumulate, and context persists across sessions and worktrees. @@ -16,8 +16,8 @@ | Skill | Command | Role | |-------|---------|------| | **forki** | `/harnish:forki` | Decision forcing (binary fork + D/E/V/R + trade-off, HITL only) | -| **drafti-architect** | `/harnish:drafti-architect` | Tech-driven design PRD generation | | **drafti-feature** | `/harnish:drafti-feature` | Planning-based implementation spec PRD | +| **drafti-architect** | `/harnish:drafti-architect` | Tech-driven design PRD generation | | **impl** | `/harnish:impl` | Autonomous implementation engine — the "harnish" engine (seeding + ralph loop + anchoring + experience) | | **ralphi** | `/harnish:ralphi` | Inspection (HITL reporting or autonomous fix) | @@ -59,7 +59,7 @@ Expected output: Expected output: ``` -✓ Installed harnish@0.0.4 — 5 skills registered (forki, drafti-architect, drafti-feature, impl, ralphi) +✓ Installed harnish@0.0.5 — 5 skills registered (forki, drafti-feature, drafti-architect, impl, ralphi) ``` ### 3. Verify @@ -72,8 +72,8 @@ You should see `harnish` in the list. The five slash commands below should be in ``` /harnish:forki -/harnish:drafti-architect /harnish:drafti-feature +/harnish:drafti-architect /harnish:impl /harnish:ralphi ``` @@ -102,21 +102,14 @@ Once installed, the fastest path end-to-end: /harnish:impl ``` -Sample flow (simplified): +Sample flow: ``` -user > /harnish:impl docs/prd-redis-cache.md - -step 1 > Reading PRD → 3 phases, 12 atomic tasks identified -step 2 > Seeding → .harnish/harnish-current-work.json created -step 3 > "Phase 1 Task 1.1 ready. Run 'loop' to start ralph loop?" -user > loop - -step 4 > [READ] task 1.1 → [ACT] write code → [LOG] result → [PROGRESS] 1/12 -step 5 > Pass → asset recorded (pattern: connection-pool-init) -step 6 > Auto-advance to task 1.2 … - ⋮ -step N > Phase 1 complete → milestone report → continue Phase 2? (y/n) +user > /harnish:impl docs/prd-redis-cache.md + → 3 phases, 12 atomic tasks seeded into .harnish/harnish-current-work.json +user > loop + → task 1.1 → code → log → asset recorded → auto-advance 1.2 → … → Phase 1 done + → milestone report → continue Phase 2? (y/n) ``` If invoked with no PRD path or task description, harnish will ask: @@ -145,13 +138,13 @@ User: "Should we use Postgres or MongoDB for this?" ### 1. PRD Generation (Design) ``` -User: "Design a Redis cache layer" -→ drafti-architect explores 2-3 design alternatives with trade-off analysis -→ generates docs/prd-redis-cache.md - User: "Create a PRD from this planning doc" (with planning document attached) → drafti-feature generates implementation spec PRD (feature flags only when needed) → generates docs/prd-user-profile-edit.md + +User: "Design a Redis cache layer" +→ drafti-architect explores 2-3 design alternatives with trade-off analysis +→ generates docs/prd-redis-cache.md ``` ### 2. Autonomous Implementation (harnish) @@ -165,7 +158,8 @@ User: "Start implementation" or "Decompose tasks" → "3 Phases, 12 Tasks seeded — review then 'run the loop'" User: "Run the loop" -→ ralph loop auto-executes (Read → Act → Log → Progress → repeat) +→ The ralph loop runs one task at a time until the phase is done + (named after Ralph Wiggum — keep trying, don't give up; not an acronym) → Updates harnish-current-work.json every 3 actions, milestone report on phase completion User: (in a new session) "Continue where I left off" @@ -193,7 +187,10 @@ User: "Asset status" → Shows accumulated failure/pattern/guardrail/snippet/decision assets User: "Make this a skill" -→ Generates reusable SKILL.md draft from compressed assets +→ Bundles compressed assets into a SKILL.md scaffold (with raw asset + bodies + a TODO marker). The LLM must finalize the body — this is a + draft generator, not autonomous skill graduation. Truly autonomous + promotion is a planned future feature. ``` ## Hooks @@ -208,9 +205,35 @@ harnish registers the following hooks automatically on install via `hooks/hooks. Failures are classified by signal-to-noise: simple errors (`No such file`, `permission denied`, `command not found`, etc.) are filtered out so only meaningful failures become assets. +## Memory Model + +harnish runs a **two-tier memory** system. Each tier serves a different role; the bridge between them is currently semi-manual. + +| Tier | Storage | Lifetime | Role | Loaded as | +|------|---------|----------|------|-----------| +| **Tier 1 — Asset Store** (episodic) | `.harnish/harnish-assets.jsonl` | Per-project, accumulates across sessions, TTL-purged | Records what happened (failures, patterns, guardrails, snippets, decisions) | Injected into context on demand via `query-assets.sh --format inject` (this is the actual RAG path) | +| **Tier 2 — Skills** (procedural) | `skills/*/SKILL.md` | Permanent (versioned in source tree) | Codifies stable behavior | Auto-loaded by Claude Code as triggerable skills | + +`skillify.sh` is the bridge — it bundles compressed Tier-1 assets into a Tier-2 SKILL.md scaffold. As of v0.0.5 the scaffold is **production-grade**: + +- Frontmatter `Triggers:` auto-extracted from asset titles +- Body sectioned by asset type, with metadata (level / confidence / stability / resolved) +- `references/source-assets.jsonl` preserves originals for traceability +- §1 still needs LLM finalization of 1-3 actionable guidelines — draft generator, not autonomous graduation + +**Trigger → Record → Skillify pipeline (closed in v0.0.5):** + +``` +PostToolUseFailure → detect-asset.sh (noise filter) → /tmp/harnish-pending-*.jsonl +Stop → promote-pending.sh (dedup) → harnish-assets.jsonl +"make it a skill" → skillify.sh → SKILL.md draft + references/ +``` + +> **Why "assets" not "RAG"?** Only `query-assets.sh --format inject` is RAG in the strict sense. The rest is capture / summarize / age-out / feed-into-skill — i.e. asset CRUD + lifecycle. + ## Assets -Every accumulated learning is recorded in `.harnish/harnish-rag.jsonl` (one JSON object per line). Six asset types: +Every accumulated learning is recorded in `.harnish/harnish-assets.jsonl` (one JSON object per line). Six asset types: | Type | Captured when | |------|---------------| @@ -232,7 +255,7 @@ bash scripts/purge-assets.sh # dry-run purge (- bash scripts/migrate.sh # backfill schema to latest version ``` -`.harnish/` lives inside your project CWD and persists across sessions. `impl`, `drafti-architect`, and `drafti-feature` reference relevant assets automatically (tag-based query in Step 2 of each skill). +`.harnish/` lives inside your project CWD and persists across sessions. `impl`, `drafti-feature`, and `drafti-architect` reference relevant assets automatically (tag-based query in Step 2 of each skill). ## Worktrees @@ -255,7 +278,7 @@ mkdir -p .claude/skills cp -r /path/to/harnish/skills/forki .claude/skills/ ``` -The skill is available as `forki` (no plugin namespace). Replace `forki` with any of: `impl`, `ralphi`, `drafti-architect`, `drafti-feature`. +The skill is available as `forki` (no plugin namespace). Replace `forki` with any of: `impl`, `ralphi`, `drafti-feature`, `drafti-architect`. ### B. Fork as your own plugin marketplace @@ -283,7 +306,7 @@ git -C /path/to/harnish pull # update later - **harnish** = harness + ish (autonomous implementation engine) - **ralphi** = ralph + i (inspection) - Origin: named after Ralph Wiggum from The Simpsons — keep trying, don't give up -- **drafti** = draft + i (PRD generation — drafti-architect + drafti-feature) +- **drafti** = draft + i (PRD generation — drafti-feature + drafti-architect) - **forki** = fork + i (decision forcing — binary fork + D/E/V/R + trade-off, HITL only) ## Triad diff --git a/VERSION b/VERSION index 81340c7..bbdeab6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.4 +0.0.5 diff --git a/scripts/abstract-asset.sh b/scripts/abstract-asset.sh index 49df6aa..2aa92a0 100755 --- a/scripts/abstract-asset.sh +++ b/scripts/abstract-asset.sh @@ -25,15 +25,15 @@ if [[ -z "$SLUG" ]]; then exit 1 fi -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" -if [[ ! -f "$RAG_FILE" ]]; then - echo "오류: $RAG_FILE 없음" >&2 +if [[ ! -f "$ASSET_FILE" ]]; then + echo "오류: $ASSET_FILE 없음" >&2 exit 1 fi # 원본 찾기 -ORIGINAL=$(jq -c --arg s "$SLUG" 'select(.slug == $s)' "$RAG_FILE" 2>/dev/null | head -1) +ORIGINAL=$(jq -c --arg s "$SLUG" 'select(.slug == $s)' "$ASSET_FILE" 2>/dev/null | head -1) if [[ -z "$ORIGINAL" ]]; then echo "오류: slug '$SLUG' 없음" >&2 @@ -42,10 +42,10 @@ fi # scope를 generic으로 변경한 사본 추가 (atomic write) ABSTRACTED=$(echo "$ORIGINAL" | jq -c '.scope = "generic" | .slug = .slug + "-generic" | .context = .context + " (추상화)"') -TMPRAG=$(mktemp "${RAG_FILE}.XXXXXX") +TMPRAG=$(mktemp "${ASSET_FILE}.XXXXXX") trap 'rm -f "$TMPRAG"' EXIT -cp "$RAG_FILE" "$TMPRAG" +cp "$ASSET_FILE" "$TMPRAG" echo "$ABSTRACTED" >> "$TMPRAG" -mv "$TMPRAG" "$RAG_FILE" +mv "$TMPRAG" "$ASSET_FILE" echo "{\"status\":\"abstracted\",\"slug\":\"${SLUG}-generic\"}" diff --git a/scripts/check-thresholds.sh b/scripts/check-thresholds.sh index b358738..be9895e 100755 --- a/scripts/check-thresholds.sh +++ b/scripts/check-thresholds.sh @@ -21,14 +21,14 @@ while [[ $# -gt 0 ]]; do esac done -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" -if [[ ! -f "$RAG_FILE" ]] || [[ ! -s "$RAG_FILE" ]]; then +if [[ ! -f "$ASSET_FILE" ]] || [[ ! -s "$ASSET_FILE" ]]; then echo "자산 없음" exit 0 fi -jq -c 'select(.compressed != true) | .tags[]' "$RAG_FILE" 2>/dev/null \ +jq -c 'select(.compressed != true) | .tags[]' "$ASSET_FILE" 2>/dev/null \ | sort | uniq -c | sort -rn \ | awk -v t="$THRESHOLD" '{ count=$1; tag=$2; diff --git a/scripts/common.sh b/scripts/common.sh index 462cdfb..73c478e 100755 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -45,11 +45,21 @@ resolve_progress_file() { echo "$(resolve_base_dir)/harnish-current-work.json" } -# RAG 자산 파일 경로 -resolve_rag_file() { +# Asset Store 파일 경로 (Tier 1 episodic memory) +resolve_asset_file() { + echo "$(resolve_base_dir)/harnish-assets.jsonl" +} + +# 레거시 RAG 파일 경로 (마이그레이션 감지용; v0.0.4까지는 이 이름이었음) +resolve_legacy_asset_file() { echo "$(resolve_base_dir)/harnish-rag.jsonl" } +# Deprecated alias — 외부 호출자 호환을 위해 유지. 향후 메이저 릴리스에서 제거 예정. +resolve_rag_file() { + resolve_asset_file +} + # 스킬 디렉토리 (references/ 접근용) resolve_skill_dir() { echo "$(cd "${SCRIPT_DIR:-$(pwd)}/../skills/impl" && pwd)" diff --git a/scripts/compress-assets.sh b/scripts/compress-assets.sh index bc51adb..075480e 100755 --- a/scripts/compress-assets.sh +++ b/scripts/compress-assets.sh @@ -24,16 +24,16 @@ while [[ $# -gt 0 ]]; do esac done -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" -if [[ ! -f "$RAG_FILE" ]] || [[ ! -s "$RAG_FILE" ]]; then +if [[ ! -f "$ASSET_FILE" ]] || [[ ! -s "$ASSET_FILE" ]]; then echo '{"status":"empty","compressed":0}' exit 0 fi # 대상 태그 결정 if $ALL; then - TAGS_OVER=$(jq -c 'select(.compressed != true) | .tags[]' "$RAG_FILE" 2>/dev/null \ + TAGS_OVER=$(jq -c 'select(.compressed != true) | .tags[]' "$ASSET_FILE" 2>/dev/null \ | sort | uniq -c | sort -rn \ | awk -v t="$THRESHOLD" '$1 >= t {print $2}' | tr -d '"') elif [[ -n "$TAG" ]]; then @@ -52,7 +52,7 @@ fi if $DRY_RUN; then # candidates JSON 출력만, 파일 변경 없음 CANDIDATES=$(echo "$TAGS_OVER" | awk 'NF' | while read -r t; do - cnt=$(jq -c --arg t "$t" 'select(.compressed != true) | select(.tags[] == $t)' "$RAG_FILE" | wc -l | xargs) + cnt=$(jq -c --arg t "$t" 'select(.compressed != true) | select(.tags[] == $t)' "$ASSET_FILE" | wc -l | xargs) jq -n -c --arg tag "$t" --argjson count "$cnt" --argjson threshold "$THRESHOLD" \ '{tag:$tag, count:$count, would_compress:($count >= $threshold)}' done | jq -s .) @@ -63,7 +63,7 @@ fi COMPRESSED=0 TMPFILE=$(mktemp) trap 'rm -f "$TMPFILE" "${TMPFILE}.new"' EXIT -cp "$RAG_FILE" "$TMPFILE" +cp "$ASSET_FILE" "$TMPFILE" while IFS= read -r target_tag; do [[ -z "$target_tag" ]] && continue @@ -100,5 +100,5 @@ while IFS= read -r target_tag; do ((COMPRESSED++)) || true done <<< "$TAGS_OVER" -mv "$TMPFILE" "$RAG_FILE" +mv "$TMPFILE" "$ASSET_FILE" echo "{\"status\":\"compressed\",\"compressed\":${COMPRESSED}}" diff --git a/scripts/detect-asset.sh b/scripts/detect-asset.sh index 77341fb..840ae2d 100755 --- a/scripts/detect-asset.sh +++ b/scripts/detect-asset.sh @@ -2,7 +2,7 @@ # detect-asset.sh — Claude Code hook에서 호출. 자산 감지 + pending 관리. # # 노이즈 줄이기: 단순 오류, 테스트 실행, 읽기 전용 작업은 무시. -# pending은 /tmp에 저장 (세션 내 임시 데이터, RAG 오염 방지). +# pending은 /tmp에 저장 (세션 내 임시 데이터, Asset Store 오염 방지). set -euo pipefail @@ -10,7 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" BASE="$(resolve_base_dir)" -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" # hook은 조용히 실패해야 함 trap 'exit 0' ERR @@ -50,17 +50,27 @@ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""') # ── Stop 이벤트: 임계치 + 품질 게이트 ── if [[ "$EVENT" == "Stop" ]]; then # 임계치 확인 - if [[ -f "$RAG_FILE" ]] && [[ -s "$RAG_FILE" ]]; then + if [[ -f "$ASSET_FILE" ]] && [[ -s "$ASSET_FILE" ]]; then THRESHOLD_OUT=$(bash "$SCRIPT_DIR/check-thresholds.sh" --base-dir "$BASE" 2>/dev/null || true) if [[ -n "$THRESHOLD_OUT" ]]; then echo "$THRESHOLD_OUT" fi fi - # pending 보고 + 삭제 + # pending → assets 자동 승격 + 삭제 if [[ -f "$PENDING_FILE" ]] && [[ -s "$PENDING_FILE" ]]; then PENDING_COUNT=$(wc -l < "$PENDING_FILE" | xargs) - echo "harnish: 세션 종료 — ${PENDING_COUNT}건 pending 자산 미처리" + # promote-pending 호출 (dedup 후 record-asset) + PROMOTE_OUT=$(bash "$SCRIPT_DIR/promote-pending.sh" --session "$SESSION_HASH" --base-dir "$BASE" 2>/dev/null || echo '{"promoted":0,"deduplicated":0}') + PROMOTED=$(echo "$PROMOTE_OUT" | jq -r '.promoted // 0' 2>/dev/null || echo "0") + DEDUP=$(echo "$PROMOTE_OUT" | jq -r '.deduplicated // 0' 2>/dev/null || echo "0") + + if [[ "$PROMOTED" -gt 0 ]]; then + echo "harnish: 세션 종료 — pending ${PENDING_COUNT}건 → ${PROMOTED}건 자산 승격 (중복 ${DEDUP}건 통합)" + else + echo "harnish: 세션 종료 — ${PENDING_COUNT}건 pending 처리 실패" + fi + # promote 성공 여부와 무관하게 pending 삭제 (오염 방지; 데이터는 assets에 영속화됨) rm -f "$PENDING_FILE" fi diff --git a/scripts/init-assets.sh b/scripts/init-assets.sh index 4e8834e..3dbe434 100755 --- a/scripts/init-assets.sh +++ b/scripts/init-assets.sh @@ -29,10 +29,17 @@ log() { $QUIET || echo "$*"; } mkdir -p "$BASE" -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" +LEGACY_RAG_FILE="$BASE/harnish-rag.jsonl" WORK_FILE="$BASE/harnish-current-work.json" -[[ -f "$RAG_FILE" ]] || touch "$RAG_FILE" -[[ -f "$WORK_FILE" ]] || echo '{}' > "$WORK_FILE" +# 레거시 → 신규 자동 이전 (idempotent: 신규가 이미 있으면 무시) +if [[ -f "$LEGACY_RAG_FILE" ]] && [[ ! -f "$ASSET_FILE" ]]; then + mv "$LEGACY_RAG_FILE" "$ASSET_FILE" + log "ℹ legacy harnish-rag.jsonl → harnish-assets.jsonl 자동 이전" +fi + +[[ -f "$ASSET_FILE" ]] || touch "$ASSET_FILE" +[[ -f "$WORK_FILE" ]] || echo '{}' > "$WORK_FILE" log "✓ .harnish/ 초기화 완료 ($BASE)" diff --git a/scripts/localize-asset.sh b/scripts/localize-asset.sh index 852fa43..590e668 100755 --- a/scripts/localize-asset.sh +++ b/scripts/localize-asset.sh @@ -25,14 +25,14 @@ if [[ -z "$SLUG" ]]; then exit 1 fi -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" -if [[ ! -f "$RAG_FILE" ]]; then - echo "오류: $RAG_FILE 없음" >&2 +if [[ ! -f "$ASSET_FILE" ]]; then + echo "오류: $ASSET_FILE 없음" >&2 exit 1 fi -ORIGINAL=$(jq -c --arg s "$SLUG" 'select(.slug == $s)' "$RAG_FILE" 2>/dev/null | head -1) +ORIGINAL=$(jq -c --arg s "$SLUG" 'select(.slug == $s)' "$ASSET_FILE" 2>/dev/null | head -1) if [[ -z "$ORIGINAL" ]]; then echo "오류: slug '$SLUG' 없음" >&2 @@ -41,10 +41,10 @@ fi # scope를 project로 변경한 사본 추가 (atomic write) LOCALIZED=$(echo "$ORIGINAL" | jq -c '.scope = "project" | .slug = .slug + "-local" | .context = .context + " (로컬화)"') -TMPRAG=$(mktemp "${RAG_FILE}.XXXXXX") +TMPRAG=$(mktemp "${ASSET_FILE}.XXXXXX") trap 'rm -f "$TMPRAG"' EXIT -cp "$RAG_FILE" "$TMPRAG" +cp "$ASSET_FILE" "$TMPRAG" echo "$LOCALIZED" >> "$TMPRAG" -mv "$TMPRAG" "$RAG_FILE" +mv "$TMPRAG" "$ASSET_FILE" echo "{\"status\":\"localized\",\"slug\":\"$(echo "$ORIGINAL" | jq -r '.slug')-local\"}" diff --git a/scripts/migrate.sh b/scripts/migrate.sh index 90acad7..4fc884f 100755 --- a/scripts/migrate.sh +++ b/scripts/migrate.sh @@ -19,20 +19,20 @@ while [[ $# -gt 0 ]]; do esac done -RAG="$BASE/harnish-rag.jsonl" +ASSETS="$BASE/harnish-assets.jsonl" LOG="$BASE/harnish-migration-log.jsonl" -[[ -f "$RAG" ]] || { echo '{"status":"no-op","reason":"rag file absent"}'; exit 0; } -[[ -s "$RAG" ]] || { echo '{"status":"no-op","reason":"rag file empty"}'; exit 0; } +[[ -f "$ASSETS" ]] || { echo '{"status":"no-op","reason":"asset file absent"}'; exit 0; } +[[ -s "$ASSETS" ]] || { echo '{"status":"no-op","reason":"asset file empty"}'; exit 0; } # Backup NOW_EPOCH=$(date +%s) -BAK="${RAG}.bak.${NOW_EPOCH}" -cp "$RAG" "$BAK" +BAK="${ASSETS}.bak.${NOW_EPOCH}" +cp "$ASSETS" "$BAK" # Migrate: backfill 3 fields if schema_version missing or older NOW_UTC=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -TMP=$(mktemp "${RAG}.XXXXXX") +TMP=$(mktemp "${ASSETS}.XXXXXX") trap 'rm -f "$TMP"' EXIT MIGRATED=0 @@ -52,9 +52,9 @@ while IFS= read -r line; do echo "$line" >> "$TMP" SKIPPED=$((SKIPPED+1)) fi -done < "$RAG" +done < "$ASSETS" -mv "$TMP" "$RAG" +mv "$TMP" "$ASSETS" # Log jq -n -c \ @@ -68,7 +68,7 @@ jq -n -c \ >> "$LOG" # Backup 보존: 최신 3개만 유지, 나머지 삭제 -BAK_FILES=$(ls -t "${RAG}".bak.* 2>/dev/null || true) +BAK_FILES=$(ls -t "${ASSETS}".bak.* 2>/dev/null || true) if [[ -n "$BAK_FILES" ]]; then echo "$BAK_FILES" | tail -n +4 | xargs rm -f 2>/dev/null || true fi diff --git a/scripts/promote-pending.sh b/scripts/promote-pending.sh new file mode 100755 index 0000000..09fc291 --- /dev/null +++ b/scripts/promote-pending.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# promote-pending.sh — /tmp/harnish-pending-*.jsonl을 deduplicate 후 자산으로 자동 등록 +# +# Layer: L4 (Interface) — hook과 record-asset.sh 사이의 promotion 레이어 +# 의존: common.sh, record-asset.sh +# +# 사용법: +# promote-pending.sh --session SESSION_ID [--base-dir .harnish] [--dry-run] +# promote-pending.sh # CLAUDE_SESSION_ID 또는 PID 해시 +# +# 출력 (JSON): +# {"status":"promoted","promoted":N,"deduplicated":M,"skipped":K} +# {"status":"empty"} +# {"status":"no_pending"} + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +BASE="$(resolve_base_dir)" +SESSION="" +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case $1 in + --session) SESSION="$2"; shift 2;; + --base-dir) BASE="$2"; shift 2;; + --dry-run) DRY_RUN=true; shift;; + *) shift;; + esac +done + +# 세션 해시 결정 +if [[ -z "$SESSION" ]]; then + if [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then + SESSION="$CLAUDE_SESSION_ID" + else + SESSION=$(echo "$$" | md5 2>/dev/null | cut -c1-8 || echo "$$" | md5sum 2>/dev/null | cut -c1-8 || echo "unknown") + fi +fi + +PENDING_FILE="/tmp/harnish-pending-${SESSION}.jsonl" + +if [[ ! -f "$PENDING_FILE" ]]; then + echo '{"status":"no_pending","promoted":0,"deduplicated":0,"skipped":0}' + exit 0 +fi + +if [[ ! -s "$PENDING_FILE" ]]; then + echo '{"status":"empty","promoted":0,"deduplicated":0,"skipped":0}' + exit 0 +fi + +# 첫 의미있는 라인을 추출하는 jq filter (헬퍼) +# pending JSONL의 각 라인은 {event, tool, output, session, date} +# dedup key = tool + first non-empty line of output (50자 truncate) + +# Step 1: dedup +# 각 라인을 key로 그룹화하고 첫 번째 항목 + count 보존 +DEDUP_OUT=$(jq -sc ' + map( + . + { + __key: ((.tool // "") + "|" + ( + (.output // "") | split("\n") | map(select(length > 0)) | first // "" | .[0:50] + )) + } + ) + | group_by(.__key) + | map({ + tool: (.[0].tool // ""), + output: (.[0].output // ""), + session: (.[0].session // ""), + date: (.[0].date // ""), + occurrences: length + }) +' "$PENDING_FILE") + +UNIQUE_COUNT=$(echo "$DEDUP_OUT" | jq 'length') +TOTAL_COUNT=$(wc -l < "$PENDING_FILE" | xargs) +DEDUP_COUNT=$((TOTAL_COUNT - UNIQUE_COUNT)) + +if [[ "$UNIQUE_COUNT" -eq 0 ]]; then + echo '{"status":"empty","promoted":0,"deduplicated":0,"skipped":0}' + exit 0 +fi + +# Step 2: dry-run 처리 +if $DRY_RUN; then + jq -n -c \ + --arg status "dry_run" \ + --argjson promoted "$UNIQUE_COUNT" \ + --argjson dedup "$DEDUP_COUNT" \ + --argjson candidates "$DEDUP_OUT" \ + '{status:$status, promoted:$promoted, deduplicated:$dedup, candidates:$candidates}' + exit 0 +fi + +# Step 3: 각 unique entry를 record-asset.sh로 등록 +PROMOTED=0 +SKIPPED=0 +SHORT_SESSION="${SESSION:0:8}" + +# echo "$DEDUP_OUT" | jq -c '.[]'를 줄별 처리 +while IFS= read -r entry; do + [[ -z "$entry" ]] && continue + + TOOL=$(echo "$entry" | jq -r '.tool // ""') + OUTPUT=$(echo "$entry" | jq -r '.output // ""') + OCCURRENCES=$(echo "$entry" | jq -r '.occurrences // 1') + + # 빈 output skip + [[ -z "$OUTPUT" ]] && SKIPPED=$((SKIPPED + 1)) && continue + + # 첫 라인을 title로 + FIRST_LINE=$(echo "$OUTPUT" | grep -m1 '[^[:space:]]' || echo "") + [[ -z "$FIRST_LINE" ]] && SKIPPED=$((SKIPPED + 1)) && continue + + # title은 60자 truncate + TITLE="${FIRST_LINE:0:60}" + + # tags + TAGS="auto,tool:${TOOL},session:${SHORT_SESSION}" + + # context + CONTEXT="auto-promoted from pending (occurrences: ${OCCURRENCES})" + + # record-asset.sh 호출 + if bash "$SCRIPT_DIR/record-asset.sh" \ + --type failure \ + --tags "$TAGS" \ + --title "$TITLE" \ + --body "$OUTPUT" \ + --context "$CONTEXT" \ + --scope project \ + --session-id "$SESSION" \ + --base-dir "$BASE" >/dev/null 2>&1; then + PROMOTED=$((PROMOTED + 1)) + else + SKIPPED=$((SKIPPED + 1)) + fi +done < <(echo "$DEDUP_OUT" | jq -c '.[]') + +# Step 4: 출력 +jq -n -c \ + --arg status "promoted" \ + --argjson promoted "$PROMOTED" \ + --argjson dedup "$DEDUP_COUNT" \ + --argjson skipped "$SKIPPED" \ + '{status:$status, promoted:$promoted, deduplicated:$dedup, skipped:$skipped}' diff --git a/scripts/purge-assets.sh b/scripts/purge-assets.sh index 01fdb1e..4d29561 100755 --- a/scripts/purge-assets.sh +++ b/scripts/purge-assets.sh @@ -19,10 +19,10 @@ while [[ $# -gt 0 ]]; do esac done -RAG="$BASE/harnish-rag.jsonl" -ARCHIVE="$BASE/harnish-rag-archive.jsonl" +ASSETS="$BASE/harnish-assets.jsonl" +ARCHIVE="$BASE/harnish-assets-archive.jsonl" -[[ -f "$RAG" ]] || { echo '{"status":"no-op","reason":"rag file absent"}'; exit 0; } +[[ -f "$ASSETS" ]] || { echo '{"status":"no-op","reason":"asset file absent"}'; exit 0; } # Hardcoded defaults (retention-policy.md 참조; 향후 파싱 확장 가능) # ttl_days: decision=365, failure=90, pattern=never, guardrail=never, snippet=180 @@ -54,7 +54,7 @@ PURGE_FILTER=' ' CANDIDATES=$(jq -c --argjson now_epoch "$NOW_EPOCH" --argjson safety_sec "$SAFETY_SEC" \ - "select($PURGE_FILTER)" "$RAG" 2>/dev/null || echo "") + "select($PURGE_FILTER)" "$ASSETS" 2>/dev/null || echo "") CANDIDATE_COUNT=$(echo "$CANDIDATES" | awk 'NF' | wc -l | xargs) if ! $EXECUTE; then @@ -71,11 +71,11 @@ fi # Append candidates to archive echo "$CANDIDATES" >> "$ARCHIVE" -# Rewrite rag with non-candidates -TMP=$(mktemp "${RAG}.XXXXXX") +# Rewrite asset file with non-candidates +TMP=$(mktemp "${ASSETS}.XXXXXX") trap 'rm -f "$TMP"' EXIT jq -c --argjson now_epoch "$NOW_EPOCH" --argjson safety_sec "$SAFETY_SEC" \ - "select($PURGE_FILTER | not)" "$RAG" > "$TMP" -mv "$TMP" "$RAG" + "select($PURGE_FILTER | not)" "$ASSETS" > "$TMP" +mv "$TMP" "$ASSETS" echo "{\"status\":\"purged\",\"purged\":$CANDIDATE_COUNT,\"archive\":\"$ARCHIVE\"}" diff --git a/scripts/quality-gate.sh b/scripts/quality-gate.sh index 27b81bf..ca99966 100755 --- a/scripts/quality-gate.sh +++ b/scripts/quality-gate.sh @@ -20,9 +20,9 @@ while [[ $# -gt 0 ]]; do esac done -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" -if [[ ! -f "$RAG_FILE" ]] || [[ ! -s "$RAG_FILE" ]]; then +if [[ ! -f "$ASSET_FILE" ]] || [[ ! -s "$ASSET_FILE" ]]; then [[ "$FORMAT" == "json" ]] && echo '{"status":"empty","issues":[]}' || echo "자산 없음" exit 0 fi @@ -38,7 +38,7 @@ ISSUES=$(jq -c 'select(.compressed != true) | (if (.body | length) == 0 then "body 비어있음" else empty end), (if (.context | length) == 0 then "context 비어있음" else empty end) ] | if length > 0 then {slug: $r.slug, title: $r.title, quality: (if length > 2 then "poor" elif length > 0 then "fair" else "good" end), issues: .} else empty end -' "$RAG_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo "[]") +' "$ASSET_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo "[]") ISSUE_COUNT=$(echo "$ISSUES" | jq 'length') diff --git a/scripts/query-assets.sh b/scripts/query-assets.sh index eb75497..9296907 100755 --- a/scripts/query-assets.sh +++ b/scripts/query-assets.sh @@ -32,7 +32,7 @@ fi [[ -z "$BASE_DIR" ]] && BASE_DIR="$(resolve_base_dir)" -RAG_FILE="${BASE_DIR}/harnish-rag.jsonl" +ASSET_FILE="${BASE_DIR}/harnish-assets.jsonl" # --- 빈 결과 처리 --- empty_result() { @@ -46,7 +46,7 @@ empty_result() { exit 0 } -if [[ ! -f "$RAG_FILE" ]] || [[ ! -s "$RAG_FILE" ]]; then +if [[ ! -f "$ASSET_FILE" ]] || [[ ! -s "$ASSET_FILE" ]]; then empty_result fi @@ -75,7 +75,7 @@ fi JQ_FILTER="${JQ_FILTER} | select(.tags as \$t | ${TAG_JSON} | any(. as \$q | \$t | any(. == \$q)))" # --- 검색 실행 --- -RESULTS=$(jq -c "${JQ_FILTER}" "$RAG_FILE" 2>/dev/null | head -n "$LIMIT" | jq -s '.' 2>/dev/null || echo "[]") +RESULTS=$(jq -c "${JQ_FILTER}" "$ASSET_FILE" 2>/dev/null | head -n "$LIMIT" | jq -s '.' 2>/dev/null || echo "[]") RESULT_COUNT=$(echo "$RESULTS" | jq 'length') if [[ "$RESULT_COUNT" -eq 0 ]]; then @@ -86,13 +86,13 @@ fi # 출력 전에 실행 (empty_result 분기 시 skip됨 = OK, 매칭 0이면 갱신 불필요) NOW_UTC=$(date -u +"%Y-%m-%dT%H:%M:%SZ") MATCHED_SLUGS=$(echo "$RESULTS" | jq -c '[.[].slug]') -TMP_RAG=$(mktemp "${RAG_FILE}.XXXXXX") +TMP_RAG=$(mktemp "${ASSET_FILE}.XXXXXX") trap 'rm -f "$TMP_RAG"' EXIT jq -c --arg now "$NOW_UTC" --argjson slugs "$MATCHED_SLUGS" \ 'if (.slug as $s | $slugs | any(. == $s)) then . + {last_accessed_at: $now, access_count: ((.access_count // 0) + 1)} - else . end' "$RAG_FILE" > "$TMP_RAG" -mv "$TMP_RAG" "$RAG_FILE" + else . end' "$ASSET_FILE" > "$TMP_RAG" +mv "$TMP_RAG" "$ASSET_FILE" # --- 출력 --- case "$FORMAT" in @@ -114,7 +114,16 @@ case "$FORMAT" in echo "### 관련 자산 (asset-recorder)" echo "" echo "$RESULTS" | jq -r '.[] | - "- **[\(.type)] \(.title)**: \(.body[0:100])"' + # type 헤더 — guardrail은 level, decision은 confidence, pattern은 stability를 머리에 노출 + ( "[\(.type)" + + (if .level then "/\(.level)" else "" end) + + (if .confidence then "/\(.confidence)" else "" end) + + (if .stability then "/s\(.stability)" else "" end) + + "]" ) as $hdr | + "- **\($hdr) \(.title)**: \(.body[0:120])" + + "\n - context: \(.context // "(none)")" + + (if .resolved != null then " | resolved: \(.resolved)" else "" end) + ' ;; *) echo "오류: 알 수 없는 포맷 '$FORMAT'" >&2; exit 1;; diff --git a/scripts/record-asset.sh b/scripts/record-asset.sh index b3a7b89..b5680ce 100755 --- a/scripts/record-asset.sh +++ b/scripts/record-asset.sh @@ -66,7 +66,7 @@ if [[ ! -d "$BASE" ]]; then bash "$SCRIPT_DIR/init-assets.sh" --base-dir "$BASE" --quiet fi -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" # --- 본문 --- BODY_CONTENT="$BODY" @@ -76,10 +76,10 @@ fi # --- 슬러그 (중복 방지: 동일 slug 존재 시 -2, -3 ... suffix) --- SLUG=$(slugify "$TITLE") -if [[ -f "$RAG_FILE" ]] && [[ -s "$RAG_FILE" ]]; then +if [[ -f "$ASSET_FILE" ]] && [[ -s "$ASSET_FILE" ]]; then BASE_SLUG="$SLUG" COUNTER=2 - while jq -e --arg s "$SLUG" 'select(.slug == $s)' "$RAG_FILE" 2>/dev/null | grep -q .; do + while jq -e --arg s "$SLUG" 'select(.slug == $s)' "$ASSET_FILE" 2>/dev/null | grep -q .; do SLUG="${BASE_SLUG}-${COUNTER}" COUNTER=$((COUNTER + 1)) done @@ -114,11 +114,11 @@ case "$TYPE" in esac # --- append (atomic: copy + append + mv) --- -TMPRAG=$(mktemp "${RAG_FILE}.XXXXXX") +TMPRAG=$(mktemp "${ASSET_FILE}.XXXXXX") trap 'rm -f "$TMPRAG"' EXIT -cp "$RAG_FILE" "$TMPRAG" +cp "$ASSET_FILE" "$TMPRAG" echo "$RECORD" >> "$TMPRAG" -mv "$TMPRAG" "$RAG_FILE" +mv "$TMPRAG" "$ASSET_FILE" # --- RCA 검증 --- RCA_WARNINGS=() diff --git a/scripts/skillify.sh b/scripts/skillify.sh index 4e35ada..12d9682 100755 --- a/scripts/skillify.sh +++ b/scripts/skillify.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash -# skillify.sh — JSONL 자산에서 스킬 초안 생성 +# skillify.sh — JSONL 자산에서 production-grade SKILL.md scaffold 생성 +# +# v0.0.5: description에 Triggers 자동 생성, body 구조화 (type별 섹션), +# references/source-assets.jsonl로 트레이서빌리티 보존. +# 여전히 LLM이 §1 가이드라인 finalize 필요 (autonomous 아님). # # 사용법: -# skillify.sh --tag docker --skill-name docker-patterns [--base-dir .harnish] +# skillify.sh --tag docker --skill-name docker-patterns [--output-dir skills] [--base-dir .harnish] set -euo pipefail @@ -27,15 +31,17 @@ if [[ -z "$TAG" || -z "$SKILL_NAME" ]]; then exit 1 fi -RAG_FILE="$BASE/harnish-rag.jsonl" +ASSET_FILE="$BASE/harnish-assets.jsonl" -if [[ ! -f "$RAG_FILE" ]]; then - echo "오류: $RAG_FILE 없음" >&2 +if [[ ! -f "$ASSET_FILE" ]]; then + echo "오류: $ASSET_FILE 없음" >&2 exit 1 fi # 해당 태그의 자산 수집 -ASSETS=$(jq -c --arg t "$TAG" 'select(.tags[] == $t) | select(.compressed != true)' "$RAG_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo "[]") +ASSETS=$(jq -c --arg t "$TAG" \ + 'select(.tags[] == $t) | select(.compressed != true)' \ + "$ASSET_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo "[]") COUNT=$(echo "$ASSETS" | jq 'length') if [[ "$COUNT" -eq 0 ]]; then @@ -43,25 +49,116 @@ if [[ "$COUNT" -eq 0 ]]; then exit 1 fi -# 스킬 초안 생성 +# 타입별 카운트 +N_FAILURE=$(echo "$ASSETS" | jq '[.[] | select(.type == "failure")] | length') +N_PATTERN=$(echo "$ASSETS" | jq '[.[] | select(.type == "pattern")] | length') +N_GUARDRAIL=$(echo "$ASSETS" | jq '[.[] | select(.type == "guardrail")] | length') +N_DECISION=$(echo "$ASSETS" | jq '[.[] | select(.type == "decision")] | length') +N_SNIPPET=$(echo "$ASSETS" | jq '[.[] | select(.type == "snippet")] | length') + +# Triggers 후보 자동 추출 (자산 title의 토큰 빈도 기반) +TRIGGER_CANDIDATES=$(echo "$ASSETS" \ + | jq -r '.[].title' \ + | tr '[:upper:]' '[:lower:]' \ + | tr -c '[:alnum:]\n' ' ' \ + | tr ' ' '\n' \ + | grep -E '^[a-z][a-z0-9]{2,}$' \ + | sort | uniq -c | sort -rn \ + | awk '{print $2}' \ + | head -5 \ + | paste -s -d ',' - || echo "") + +# 디렉토리 생성 SKILL_DIR="${OUTPUT_DIR}/${SKILL_NAME}" -mkdir -p "$SKILL_DIR" +REFS_DIR="${SKILL_DIR}/references" +mkdir -p "$REFS_DIR" + +# references/source-assets.jsonl — 트레이서빌리티 +echo "$ASSETS" | jq -c '.[]' > "${REFS_DIR}/source-assets.jsonl" + +# Triggers 문자열 (description용) +BASE_TRIGGERS="\"${TAG}\", \"${TAG} 패턴\", \"${TAG} 가이드\", \"apply ${TAG}\", \"use ${TAG}\"" +if [[ -n "$TRIGGER_CANDIDATES" ]]; then + EXTRA=$(echo "$TRIGGER_CANDIDATES" | tr ',' '\n' | sed 's/^/, "/' | sed 's/$/"/' | tr -d '\n') + TRIGGER_STR="${BASE_TRIGGERS}${EXTRA}" +else + TRIGGER_STR="${BASE_TRIGGERS}" +fi +NOW_DATE=$(date +%Y-%m-%d) + +# SKILL.md frontmatter + 헤더 cat > "${SKILL_DIR}/SKILL.md" << EOF --- name: ${SKILL_NAME} version: 0.0.1 description: > - ${TAG} 관련 축적 경험 기반 스킬 (${COUNT}건 자산에서 생성) + ${TAG} 관련 축적 경험 기반 스킬. ${COUNT}건 자산 (failure:${N_FAILURE}, pattern:${N_PATTERN}, guardrail:${N_GUARDRAIL}, decision:${N_DECISION}, snippet:${N_SNIPPET})에서 자동 생성. + Triggers: ${TRIGGER_STR}. --- # ${SKILL_NAME} -> TODO: Claude가 아래 자산들을 분석하여 스킬 내용을 작성해야 합니다. +> 자동 생성된 스킬 초안 — §1 가이드라인을 LLM이 finalize 필요. +> 원본 자산은 \`references/source-assets.jsonl\`에 보존됨. + +## 1. 가이드라인 (LLM finalize) -## 원본 자산 (${COUNT}건) +> **TODO**: \`references/source-assets.jsonl\`의 자산을 분석하여 1-3개 가이드라인으로 요약하세요. +> 각 가이드라인은 1-3줄로, "언제 적용 / 무엇을 할 것 / 무엇을 피할 것" 형태로. +> 마치면 이 섹션 헤더의 "(LLM finalize)" 마커를 제거. + +## 2. 원본 자산 (${COUNT}건) + +EOF -$(echo "$ASSETS" | jq -r '.[] | "- [\(.type)] \(.title): \(.body[0:80])"') +# Type별 섹션 (자산 0건 type은 생략) +emit_section() { + local section_title="$1" + local type_key="$2" + local count="$3" + [[ "$count" -eq 0 ]] && return + { + echo "### ${section_title} (${count})" + echo "" + echo "$ASSETS" | jq -r --arg t "$type_key" ' + .[] | select(.type == $t) | + "- **\(.title)** — \(.body[0:200])\n - context: \(.context // "(none)")" + + (if .level then "\n - level: \(.level)" else "" end) + + (if .confidence then "\n - confidence: \(.confidence)" else "" end) + + (if .stability then "\n - stability: \(.stability)" else "" end) + + (if .resolved != null then "\n - resolved: \(.resolved)" else "" end) + ' + echo "" + } >> "${SKILL_DIR}/SKILL.md" +} + +emit_section "Failures" "failure" "$N_FAILURE" +emit_section "Patterns" "pattern" "$N_PATTERN" +emit_section "Guardrails" "guardrail" "$N_GUARDRAIL" +emit_section "Decisions" "decision" "$N_DECISION" +emit_section "Snippets" "snippet" "$N_SNIPPET" + +# 메타데이터 +cat >> "${SKILL_DIR}/SKILL.md" << EOF +## 3. 메타데이터 + +- 생성일: ${NOW_DATE} +- 원본 태그: \`${TAG}\` +- 자산 수: ${COUNT} (failure:${N_FAILURE} | pattern:${N_PATTERN} | guardrail:${N_GUARDRAIL} | decision:${N_DECISION} | snippet:${N_SNIPPET}) +- 원본 보존: \`references/source-assets.jsonl\` +- skillify_version: 0.0.5 EOF -echo "{\"status\":\"generated\",\"skill_dir\":\"${SKILL_DIR}\",\"asset_count\":${COUNT}}" +# 결과 출력 +jq -n -c \ + --arg status "generated" \ + --arg dir "$SKILL_DIR" \ + --argjson count "$COUNT" \ + --argjson n_failure "$N_FAILURE" \ + --argjson n_pattern "$N_PATTERN" \ + --argjson n_guardrail "$N_GUARDRAIL" \ + --argjson n_decision "$N_DECISION" \ + --argjson n_snippet "$N_SNIPPET" \ + '{status:$status, skill_dir:$dir, asset_count:$count, + breakdown:{failure:$n_failure, pattern:$n_pattern, guardrail:$n_guardrail, decision:$n_decision, snippet:$n_snippet}}' diff --git a/scripts/test-all.sh b/scripts/test-all.sh index 5e1acfa..a73ca81 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -80,8 +80,8 @@ fi echo "${BOLD}[자산 초기화]${NC}" bash "$HARNISH_ROOT/scripts/init-assets.sh" --base-dir "$ASSET_DIR" >/dev/null 2>&1 -if [[ -f "$ASSET_DIR/harnish-rag.jsonl" ]] && [[ -f "$ASSET_DIR/harnish-current-work.json" ]]; then - pass "init-assets.sh: harnish-rag.jsonl + harnish-current-work.json 생성" +if [[ -f "$ASSET_DIR/harnish-assets.jsonl" ]] && [[ -f "$ASSET_DIR/harnish-current-work.json" ]]; then + pass "init-assets.sh: harnish-assets.jsonl + harnish-current-work.json 생성" else fail "init-assets.sh" "JSONL 또는 work 파일 미생성" fi @@ -93,7 +93,7 @@ echo "${BOLD}[자산 기록]${NC}" for asset_type in failure pattern guardrail snippet decision; do before_lines=0 - [[ -f "$ASSET_DIR/harnish-rag.jsonl" ]] && before_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) + [[ -f "$ASSET_DIR/harnish-assets.jsonl" ]] && before_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) output=$(bash "$HARNISH_ROOT/scripts/record-asset.sh" \ --type "$asset_type" \ @@ -104,11 +104,11 @@ for asset_type in failure pattern guardrail snippet decision; do --base-dir "$ASSET_DIR" 2>&1) rc=$? - after_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) + after_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) if [[ $rc -eq 0 ]] && [[ "$after_lines" -gt "$before_lines" ]]; then # 추가된 줄이 유효한 JSON인지 확인 - last_line=$(tail -1 "$ASSET_DIR/harnish-rag.jsonl") + last_line=$(tail -1 "$ASSET_DIR/harnish-assets.jsonl") if echo "$last_line" | jq empty 2>/dev/null; then pass "record-asset.sh --type $asset_type" else @@ -131,7 +131,7 @@ while IFS= read -r line; do if ! echo "$line" | jq empty 2>/dev/null; then invalid_lines=$((invalid_lines + 1)) fi -done < "$ASSET_DIR/harnish-rag.jsonl" +done < "$ASSET_DIR/harnish-assets.jsonl" if [[ "$invalid_lines" -eq 0 ]] && [[ "$line_num" -gt 0 ]]; then pass "JSONL 무결성: ${line_num}줄 모두 유효한 JSON" @@ -142,10 +142,10 @@ fi # ════════════════════════════════════════ # 5. record-asset.sh --stdin 모드 # ════════════════════════════════════════ -before_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) +before_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) echo '{"type":"failure","tags":["stdin-test"],"title":"stdin 테스트","body":"stdin으로 기록"}' \ | bash "$HARNISH_ROOT/scripts/record-asset.sh" --stdin --base-dir "$ASSET_DIR" >/dev/null 2>&1 -after_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) +after_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) if [[ "$after_lines" -gt "$before_lines" ]]; then pass "record-asset.sh --stdin 모드" else @@ -225,7 +225,7 @@ rc=$? if [[ $rc -eq 0 ]]; then # JSONL에서 compressed:true 레코드 확인 - compressed_count=$(jq -c 'select(.compressed == true)' "$ASSET_DIR/harnish-rag.jsonl" 2>/dev/null | wc -l | xargs) + compressed_count=$(jq -c 'select(.compressed == true)' "$ASSET_DIR/harnish-assets.jsonl" 2>/dev/null | wc -l | xargs) if [[ "$compressed_count" -gt 0 ]]; then pass "compress-assets.sh: compressed:true 마킹 (${compressed_count}건)" else @@ -259,14 +259,14 @@ bash "$HARNISH_ROOT/scripts/record-asset.sh" \ --base-dir "$ASSET_DIR" >/dev/null 2>&1 # slug 추출 -src_slug=$(jq -r 'select(.scope == "project") | .slug' "$ASSET_DIR/harnish-rag.jsonl" 2>/dev/null | head -1) +src_slug=$(jq -r 'select(.scope == "project") | .slug' "$ASSET_DIR/harnish-assets.jsonl" 2>/dev/null | head -1) abstract_slug="" if [[ -n "$src_slug" ]]; then - before_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) + before_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) output=$(bash "$HARNISH_ROOT/scripts/abstract-asset.sh" --slug "$src_slug" --base-dir "$ASSET_DIR" 2>&1) rc=$? - after_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) + after_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) if [[ $rc -eq 0 ]] && [[ "$after_lines" -gt "$before_lines" ]]; then abstract_slug=$(echo "$output" | jq -r '.slug // ""' 2>/dev/null) @@ -282,9 +282,9 @@ fi # 12. localize-asset.sh (JSONL --slug) # ════════════════════════════════════════ if [[ -n "$abstract_slug" ]]; then - before_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) + before_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) output=$(bash "$HARNISH_ROOT/scripts/localize-asset.sh" --slug "$abstract_slug" --base-dir "$ASSET_DIR" 2>&1) - after_lines=$(wc -l < "$ASSET_DIR/harnish-rag.jsonl" | xargs) + after_lines=$(wc -l < "$ASSET_DIR/harnish-assets.jsonl" | xargs) if [[ $? -eq 0 ]] && [[ "$after_lines" -gt "$before_lines" ]]; then pass "localize-asset.sh --slug" else @@ -982,11 +982,11 @@ mkdir -p "$DRY_FIXTURE" for i in 1 2 3 4 5 6; do jq -n -c --arg t "dry-test-tag" --argjson i "$i" \ '{schema_version:"0.0.2",type:"pattern",slug:"p\($i)",title:"t\($i)",tags:[$t],date:"2026-01-01",scope:"generic",body:"b",context:"c",session:"s",last_accessed_at:"2026-01-01T00:00:00Z",access_count:0}' \ - >> "$DRY_FIXTURE/harnish-rag.jsonl" + >> "$DRY_FIXTURE/harnish-assets.jsonl" done -HASH_BEFORE=$(shasum "$DRY_FIXTURE/harnish-rag.jsonl" | awk '{print $1}') +HASH_BEFORE=$(shasum "$DRY_FIXTURE/harnish-assets.jsonl" | awk '{print $1}') DRY_OUT=$(bash "$HARNISH_ROOT/scripts/compress-assets.sh" --all --dry-run --base-dir "$DRY_FIXTURE" 2>&1) -HASH_AFTER=$(shasum "$DRY_FIXTURE/harnish-rag.jsonl" | awk '{print $1}') +HASH_AFTER=$(shasum "$DRY_FIXTURE/harnish-assets.jsonl" | awk '{print $1}') if [[ "$HASH_BEFORE" == "$HASH_AFTER" ]] && echo "$DRY_OUT" | grep -q '"status":"dry_run"'; then pass "compress-assets --dry-run 비파괴 + dry_run status" else @@ -1001,12 +1001,12 @@ echo "${BOLD}[v0.0.2: migrate.sh 백필]${NC}" MIG_FIX="$TMPDIR_BASE/mig-fix/.harnish" mkdir -p "$MIG_FIX" jq -n -c '{type:"pattern",slug:"legacy",title:"legacy","tags":["l"],"date":"2026-01-15",scope:"generic",body:"b",context:"c",session:"s"}' \ - > "$MIG_FIX/harnish-rag.jsonl" + > "$MIG_FIX/harnish-assets.jsonl" bash "$HARNISH_ROOT/scripts/migrate.sh" --base-dir "$MIG_FIX" >/dev/null 2>&1 || true -MIG_VER=$(jq -r '.schema_version' "$MIG_FIX/harnish-rag.jsonl") -MIG_LA=$(jq -r '.last_accessed_at' "$MIG_FIX/harnish-rag.jsonl") -MIG_AC=$(jq -r '.access_count' "$MIG_FIX/harnish-rag.jsonl") -BAK_EXISTS=$(ls "$MIG_FIX"/harnish-rag.jsonl.bak* 2>/dev/null | wc -l | xargs) +MIG_VER=$(jq -r '.schema_version' "$MIG_FIX/harnish-assets.jsonl") +MIG_LA=$(jq -r '.last_accessed_at' "$MIG_FIX/harnish-assets.jsonl") +MIG_AC=$(jq -r '.access_count' "$MIG_FIX/harnish-assets.jsonl") +BAK_EXISTS=$(ls "$MIG_FIX"/harnish-assets.jsonl.bak* 2>/dev/null | wc -l | xargs) if [[ "$MIG_VER" == "0.0.2" ]] && [[ "$MIG_LA" == "2026-01-15" ]] && [[ "$MIG_AC" == "0" ]] && [[ "$BAK_EXISTS" -ge 1 ]]; then pass "migrate.sh 백필: schema_version=0.0.2, last_accessed_at=date, access_count=0, .bak 생성" else @@ -1023,10 +1023,10 @@ mkdir -p "$PURGE_FIX" OLD_DATE=$(date -u -v-400d +"%Y-%m-%d" 2>/dev/null || date -u -d "-400 days" +"%Y-%m-%d" 2>/dev/null || echo "2024-01-01") jq -n -c --arg d "$OLD_DATE" \ '{schema_version:"0.0.2",type:"decision",slug:"old-dec",title:"old",tags:["x"],date:$d,scope:"generic",body:"b",context:"c",session:"s",last_accessed_at:$d,access_count:0}' \ - > "$PURGE_FIX/harnish-rag.jsonl" -HASH_BEFORE=$(shasum "$PURGE_FIX/harnish-rag.jsonl" | awk '{print $1}') + > "$PURGE_FIX/harnish-assets.jsonl" +HASH_BEFORE=$(shasum "$PURGE_FIX/harnish-assets.jsonl" | awk '{print $1}') PURGE_OUT=$(bash "$HARNISH_ROOT/scripts/purge-assets.sh" --base-dir "$PURGE_FIX" 2>&1) -HASH_AFTER=$(shasum "$PURGE_FIX/harnish-rag.jsonl" | awk '{print $1}') +HASH_AFTER=$(shasum "$PURGE_FIX/harnish-assets.jsonl" | awk '{print $1}') if [[ "$HASH_BEFORE" == "$HASH_AFTER" ]] && echo "$PURGE_OUT" | grep -q '"status":"dry_run"'; then pass "purge-assets 기본 dry-run 비파괴 + status" else diff --git a/skills/drafti-architect/SKILL.ko.md b/skills/drafti-architect/SKILL.ko.md index 68168d1..c73939f 100644 --- a/skills/drafti-architect/SKILL.ko.md +++ b/skills/drafti-architect/SKILL.ko.md @@ -1,6 +1,6 @@ --- name: drafti-architect -version: 0.0.4 +version: 0.0.5 description: > 기술 설계 PRD 생성기. 기획 문서 없이 기술 문제 정의만으로 구현 가능한 PRD를 생성한다. 트리거: "drafti-architect", "drafti", "drafti 설계", "설계해", "아키텍처 PRD", diff --git a/skills/drafti-architect/SKILL.md b/skills/drafti-architect/SKILL.md index 88b01d1..56af232 100644 --- a/skills/drafti-architect/SKILL.md +++ b/skills/drafti-architect/SKILL.md @@ -1,6 +1,6 @@ --- name: drafti-architect -version: 0.0.4 +version: 0.0.5 description: > Technical design PRD generator. Creates an implementation-ready PRD from a technical problem definition alone, without a planning document. Triggers: "drafti-architect", "drafti", "drafti 설계", "설계해", "design this", "아키텍처 PRD", "architecture PRD", diff --git a/skills/drafti-feature/SKILL.ko.md b/skills/drafti-feature/SKILL.ko.md index e45296a..8c5bd6b 100644 --- a/skills/drafti-feature/SKILL.ko.md +++ b/skills/drafti-feature/SKILL.ko.md @@ -1,6 +1,6 @@ --- name: drafti-feature -version: 0.0.4 +version: 0.0.5 description: > 기획 기반 구현 명세 PRD 생성기. 기획 요구사항을 구현 가능한 명세로 변환한다. 트리거: "drafti-feature", "drafti", "drafti 피쳐", "이 기획서로 PRD 만들어", "피쳐 PRD", diff --git a/skills/drafti-feature/SKILL.md b/skills/drafti-feature/SKILL.md index c5fe153..1977aa5 100644 --- a/skills/drafti-feature/SKILL.md +++ b/skills/drafti-feature/SKILL.md @@ -1,6 +1,6 @@ --- name: drafti-feature -version: 0.0.4 +version: 0.0.5 description: > Planning-based implementation spec PRD generator. Converts planning requirements into an implementation-ready spec. Triggers: "drafti-feature", "drafti", "drafti 피쳐", "이 기획서로 PRD 만들어", "create PRD from this planning doc", diff --git a/skills/forki/SKILL.ko.md b/skills/forki/SKILL.ko.md index 8511bd1..13e8b8e 100644 --- a/skills/forki/SKILL.ko.md +++ b/skills/forki/SKILL.ko.md @@ -1,6 +1,6 @@ --- name: forki -version: 0.0.4 +version: 0.0.5 description: > 의사결정 강제 스킬. 문제를 역할 분해(Decision/Execution/Validation/Recovery)로 2지선택으로 환원, trade-off를 드러내고 단일 선택을 강제. diff --git a/skills/forki/SKILL.md b/skills/forki/SKILL.md index 1c197a5..a7ad523 100644 --- a/skills/forki/SKILL.md +++ b/skills/forki/SKILL.md @@ -1,6 +1,6 @@ --- name: forki -version: 0.0.4 +version: 0.0.5 description: > Decision-forcing skill. Reduces a problem to a binary fork via role decomposition (Decision / Execution / Validation / Recovery), surfaces trade-offs, forces a single choice. diff --git a/skills/impl/SKILL.ko.md b/skills/impl/SKILL.ko.md index fb7efe5..3e42292 100644 --- a/skills/impl/SKILL.ko.md +++ b/skills/impl/SKILL.ko.md @@ -1,6 +1,6 @@ --- name: impl -version: 0.0.4 +version: 0.0.5 description: > 자율 구현 엔진 ("harnish" 엔진). PRD→태스크 분해, ralph 루프 자율 실행, 세션 간 맥락 유지, 경험 축적. 트리거: "impl", "harnish", "harnish 시작", "harnish 돌려", "harnish 이어서", @@ -291,7 +291,7 @@ bash "$HARNISH_ROOT/scripts/compress-assets.sh" --dry-run --all --base-dir "$(pw ``` ✅ 세션 요약 변경된 파일: {개수} ({짧은 목록}) -핵심 결정: {이 세션에서 기록된 .harnish/harnish-rag.jsonl의 1-2줄 항목} +핵심 결정: {이 세션에서 기록된 .harnish/harnish-assets.jsonl의 1-2줄 항목} 다음 제안: {예: ralphi 수정 | 배포 | 인계 | 다른 플러그인 호출} ``` diff --git a/skills/impl/SKILL.md b/skills/impl/SKILL.md index 1613119..19ceacd 100644 --- a/skills/impl/SKILL.md +++ b/skills/impl/SKILL.md @@ -1,6 +1,6 @@ --- name: impl -version: 0.0.4 +version: 0.0.5 description: > Autonomous implementation engine (the "harnish" engine). PRD to task decomposition, ralph loop autonomous execution, cross-session context preservation, experience accumulation. Triggers: "impl", "harnish", "harnish 시작", "harnish 돌려", "harnish 이어서", @@ -281,7 +281,7 @@ bash "$HARNISH_ROOT/scripts/compress-assets.sh" --dry-run --all --base-dir "$(pw ``` ✅ Session summary Files changed: {count} ({short list}) -Key decisions: {1-2 one-liners from .harnish/harnish-rag.jsonl recorded this session} +Key decisions: {1-2 one-liners from .harnish/harnish-assets.jsonl recorded this session} Suggested next: {e.g., ralphi fix | deploy | handoff | sibling plugin invocation} ``` diff --git a/skills/impl/references/retention-policy.ko.md b/skills/impl/references/retention-policy.ko.md index f799be2..31a14dd 100644 --- a/skills/impl/references/retention-policy.ko.md +++ b/skills/impl/references/retention-policy.ko.md @@ -27,5 +27,5 @@ - `(now - created_at) <= ttl_days * 86400` → 제외 (TTL 미도래) - `min_access_count` 있고 `access_count < min_access_count` → 후보 - 그 외 → 후보 -3. 후보들을 `harnish-rag-archive.jsonl`에 append +3. 후보들을 `harnish-assets-archive.jsonl`에 append 4. 원본에서 후보 제거 diff --git a/skills/impl/references/retention-policy.md b/skills/impl/references/retention-policy.md index 7891b44..a0440e6 100644 --- a/skills/impl/references/retention-policy.md +++ b/skills/impl/references/retention-policy.md @@ -27,5 +27,5 @@ - `(now - created_at) <= ttl_days * 86400` → 제외 (TTL 미도래) - `min_access_count` 있고 `access_count < min_access_count` → candidate - 그 외 → candidate -3. candidates를 `harnish-rag-archive.jsonl`에 append +3. candidates를 `harnish-assets-archive.jsonl`에 append 4. 원본에서 candidates 제거 diff --git a/skills/impl/references/schema.json b/skills/impl/references/schema.json index fbbe2be..d8da6e3 100644 --- a/skills/impl/references/schema.json +++ b/skills/impl/references/schema.json @@ -72,8 +72,8 @@ } }, - "rag_record": { - "_comment": "harnish-rag.jsonl 각 레코드의 필드 정의. v0.0.2부터 3개 필드 필수.", + "asset_record": { + "_comment": "harnish-assets.jsonl 각 레코드의 필드 정의. v0.0.2부터 3개 필드 필수.", "required": { "schema_version": { "format": "X.Y.Z", @@ -95,9 +95,9 @@ "storage": { "_comment": "JSONL 기반 단일 파일 저장. .meta/index.json 불필요.", - "rag_file": { - "path": ".harnish/harnish-rag.jsonl", - "description": "모든 자산을 1줄=1레코드로 저장하는 JSONL 파일" + "asset_file": { + "path": ".harnish/harnish-assets.jsonl", + "description": "모든 자산을 1줄=1레코드로 저장하는 JSONL 파일 (Tier 1 episodic memory). v0.0.4까지는 harnish-rag.jsonl이었음 — init-assets.sh가 자동 마이그레이션." }, "work_file": { "path": ".harnish/harnish-current-work.json", diff --git a/skills/impl/references/thresholds.ko.md b/skills/impl/references/thresholds.ko.md index 1405ee9..be21acf 100644 --- a/skills/impl/references/thresholds.ko.md +++ b/skills/impl/references/thresholds.ko.md @@ -28,7 +28,7 @@ 2. 중복 자산 식별 및 병합 3. 핵심 내용만 추출하여 하나의 요약 문서로 4. 원본 레코드에 `compressed: true` 추가 -5. 요약본 1건 harnish-rag.jsonl에 append +5. 요약본 1건 harnish-assets.jsonl에 append ## 스킬화 프로세스 diff --git a/skills/impl/references/thresholds.md b/skills/impl/references/thresholds.md index f5fbc34..e77a3e2 100644 --- a/skills/impl/references/thresholds.md +++ b/skills/impl/references/thresholds.md @@ -28,7 +28,7 @@ 2. Identify and merge duplicate assets 3. Extract only essential content into a single summary document 4. Add `compressed: true` to original records -5. Append 1 summary entry to harnish-rag.jsonl +5. Append 1 summary entry to harnish-assets.jsonl ## Skillification Process diff --git a/skills/ralphi/SKILL.ko.md b/skills/ralphi/SKILL.ko.md index 512811f..34b1408 100644 --- a/skills/ralphi/SKILL.ko.md +++ b/skills/ralphi/SKILL.ko.md @@ -1,6 +1,6 @@ --- name: ralphi -version: 0.0.4 +version: 0.0.5 description: > 점검 스킬. 트리거: "점검해", "확인해", "검증해", "ralphi", "셀프점검", "커버리지 확인", "테스트 갭", diff --git a/skills/ralphi/SKILL.md b/skills/ralphi/SKILL.md index 9d1cc06..6129060 100644 --- a/skills/ralphi/SKILL.md +++ b/skills/ralphi/SKILL.md @@ -1,6 +1,6 @@ --- name: ralphi -version: 0.0.4 +version: 0.0.5 description: > Inspection skill. Triggers: "점검해", "확인해", "검증해", "ralphi", "셀프점검", "커버리지 확인", "테스트 갭", diff --git a/tests/e2e_assets.bats b/tests/e2e_assets.bats index a46aba0..cd1894f 100644 --- a/tests/e2e_assets.bats +++ b/tests/e2e_assets.bats @@ -42,12 +42,12 @@ teardown() { --base-dir "$ASSET_BASE_DIR" # 5건 기록 확인 - COUNT=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + COUNT=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) [ "$COUNT" -eq 5 ] # 모든 라인이 유효한 JSON while IFS= read -r line; do echo "$line" | python3 -m json.tool >/dev/null - done < "$ASSET_BASE_DIR/harnish-rag.jsonl" + done < "$ASSET_BASE_DIR/harnish-assets.jsonl" } # ─── Step 2: query-assets — 태그 필터 + access_count 증분 ─────────────────── @@ -72,7 +72,7 @@ teardown() { # access_count 증분 확인 ACCESS=$(jq -r 'select(.slug == "backoff") | .access_count' \ - "$ASSET_BASE_DIR/harnish-rag.jsonl") + "$ASSET_BASE_DIR/harnish-assets.jsonl") [ "$ACCESS" -ge 1 ] } @@ -124,12 +124,12 @@ teardown() { [ "$status" -eq 0 ] # 압축 요약 레코드에 TODO 없음 (C1 버그 수정 회귀) - run grep "TODO" "$ASSET_BASE_DIR/harnish-rag.jsonl" + run grep "TODO" "$ASSET_BASE_DIR/harnish-assets.jsonl" [ "$status" -ne 0 ] # 압축 요약 레코드가 존재하고 타이틀이 맞음 COMPRESSED_TITLE=$(jq -r 'select(.compressed_summary == true) | .title' \ - "$ASSET_BASE_DIR/harnish-rag.jsonl") + "$ASSET_BASE_DIR/harnish-assets.jsonl") [[ "$COMPRESSED_TITLE" =~ "redis" ]] } @@ -139,10 +139,10 @@ teardown() { --type snippet --tags "dry" --title "s-$i" \ --body "b" --context "c" --base-dir "$ASSET_BASE_DIR" done - BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) bash "$REPO_ROOT/scripts/compress-assets.sh" \ --tag dry --dry-run --base-dir "$ASSET_BASE_DIR" - AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) [ "$BEFORE" -eq "$AFTER" ] } @@ -152,7 +152,7 @@ teardown() { # 의도적으로 body 빈 레코드 삽입 (스크립트 통해 불가 — 직접 append) echo '{"type":"pattern","slug":"bad","title":"bad","tags":["x"],"body":"", "context":"","schema_version":"0.0.2","last_accessed_at":"2026-01-01T00:00:00Z","access_count":0}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" run bash "$REPO_ROOT/scripts/quality-gate.sh" \ --base-dir "$ASSET_BASE_DIR" @@ -163,7 +163,7 @@ teardown() { @test "E2E assets step 5b: quality-gate json output has expected schema" { echo '{"type":"pattern","slug":"ok","title":"ok","tags":["x"],"body":"detail", "context":"ctx","schema_version":"0.0.2","last_accessed_at":"2026-01-01T00:00:00Z","access_count":0}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" run bash "$REPO_ROOT/scripts/quality-gate.sh" \ --base-dir "$ASSET_BASE_DIR" --format json @@ -178,9 +178,9 @@ teardown() { @test "E2E assets step 6: migrate backfills schema_version on old records" { # v0.0.1 레코드 삽입 echo '{"type":"failure","slug":"old-1","title":"old","tags":["a"],"body":"b","date":"2025-01-01","schema_version":"0.0.1"}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" echo '{"type":"pattern","slug":"old-2","title":"old2","tags":["b"],"body":"c","date":"2025-01-01","schema_version":"0.0.1"}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" run bash "$REPO_ROOT/scripts/migrate.sh" --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] @@ -188,12 +188,12 @@ teardown() { # 모든 레코드가 0.0.2로 업그레이드 됐는지 확인 OLD_COUNT=$(jq 'select(.schema_version == "0.0.1")' \ - "$ASSET_BASE_DIR/harnish-rag.jsonl" | wc -l | xargs) + "$ASSET_BASE_DIR/harnish-assets.jsonl" | wc -l | xargs) [ "$OLD_COUNT" -eq 0 ] # access_count 필드 백필 확인 NO_ACCESS=$(jq 'select(.access_count == null)' \ - "$ASSET_BASE_DIR/harnish-rag.jsonl" | wc -l | xargs) + "$ASSET_BASE_DIR/harnish-assets.jsonl" | wc -l | xargs) [ "$NO_ACCESS" -eq 0 ] } @@ -204,14 +204,14 @@ teardown() { --type decision --tags "arch" --title "choice" \ --body "chose A over B" --context "ctx" \ --base-dir "$ASSET_BASE_DIR" - BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) # dry-run (기본 모드) run bash "$REPO_ROOT/scripts/purge-assets.sh" --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] echo "$output" | python3 -m json.tool > /dev/null - AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) [ "$BEFORE" -eq "$AFTER" ] } @@ -240,7 +240,7 @@ teardown() { # 4. Compress bash "$REPO_ROOT/scripts/compress-assets.sh" \ --tag api --base-dir "$ASSET_BASE_DIR" - run grep "TODO" "$ASSET_BASE_DIR/harnish-rag.jsonl" + run grep "TODO" "$ASSET_BASE_DIR/harnish-assets.jsonl" [ "$status" -ne 0 ] # 5. Quality gate diff --git a/tests/e2e_pipeline.bats b/tests/e2e_pipeline.bats new file mode 100644 index 0000000..390a80c --- /dev/null +++ b/tests/e2e_pipeline.bats @@ -0,0 +1,150 @@ +#!/usr/bin/env bats +# tests/e2e_pipeline.bats — v0.0.5 production pipeline E2E +# +# 시나리오: +# 1. hook trigger → pending → Stop → assets 자동 승격 (CRITICAL gap 회귀) +# 2. 동일 에러 N회 → dedup 1건 + occurrences:N 메타 +# 3. skillify → Triggers + 타입별 섹션 + references/source-assets.jsonl +# 4. query --format inject → context + level + confidence + resolved 포함 + +load "$BATS_TEST_DIRNAME/setup.bash" + +setup() { + harnish_sandbox_setup + cd "$CLAUDE_PROJECT_DIR" +} + +teardown() { + # pending 파일이 leak되지 않도록 정리 + find /tmp -maxdepth 1 -name 'harnish-pending-pipeline-*.jsonl' -delete 2>/dev/null || true + harnish_sandbox_teardown +} + +# ─── 시나리오 1 — hook → pending → Stop → assets 자동 승격 ───────────────── + +@test "E2E pipeline 1: hook trigger promotes pending to assets on Stop" { + bash "$REPO_ROOT/scripts/init-assets.sh" --quiet + SESSION="pipeline-promote-$$" + PF="/tmp/harnish-pending-${SESSION}.jsonl" + rm -f "$PF" + + # 의미있는 실패 1건 — pending 적재 + echo '{"hook_event_name":"PostToolUseFailure","tool_name":"Bash","tool_output":"ImportError: requests module","session_id":"'"$SESSION"'"}' \ + | ASSET_BASE_DIR="$ASSET_BASE_DIR" CLAUDE_SESSION_ID="$SESSION" \ + bash "$REPO_ROOT/scripts/detect-asset.sh" + [ -s "$PF" ] + + # 자산 파일은 아직 비어있음 + [ ! -s "$ASSET_BASE_DIR/harnish-assets.jsonl" ] + + # Stop event → 자동 승격 + run bash -c " + echo '{\"hook_event_name\":\"Stop\",\"session_id\":\"$SESSION\"}' \ + | ASSET_BASE_DIR='$ASSET_BASE_DIR' CLAUDE_SESSION_ID='$SESSION' \ + bash '$REPO_ROOT/scripts/detect-asset.sh' + " + [ "$status" -eq 0 ] + [[ "$output" =~ "자산 승격" ]] + + # pending은 삭제 + [ ! -f "$PF" ] + # 자산 파일에 기록 존재 + [ -s "$ASSET_BASE_DIR/harnish-assets.jsonl" ] + # 자동 태그 검증 + jq -e '.tags[] | select(.=="auto")' "$ASSET_BASE_DIR/harnish-assets.jsonl" >/dev/null + jq -e '.tags[] | select(.=="tool:Bash")' "$ASSET_BASE_DIR/harnish-assets.jsonl" >/dev/null +} + +# ─── 시나리오 2 — 동일 에러 N회 dedup ────────────────────────────────────── + +@test "E2E pipeline 2: duplicate errors are dedup'd with occurrences metadata" { + bash "$REPO_ROOT/scripts/init-assets.sh" --quiet + SESSION="pipeline-dedup-$$" + PF="/tmp/harnish-pending-${SESSION}.jsonl" + rm -f "$PF" + + # 동일 에러 5회 + for i in 1 2 3 4 5; do + echo '{"hook_event_name":"PostToolUseFailure","tool_name":"Bash","tool_output":"E_DUP","session_id":"'"$SESSION"'"}' \ + | ASSET_BASE_DIR="$ASSET_BASE_DIR" CLAUDE_SESSION_ID="$SESSION" \ + bash "$REPO_ROOT/scripts/detect-asset.sh" >/dev/null + done + + # promote-pending 직접 호출 (Stop 우회) + run bash "$REPO_ROOT/scripts/promote-pending.sh" \ + --session "$SESSION" --base-dir "$ASSET_BASE_DIR" + [ "$status" -eq 0 ] + PROMOTED=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['promoted'])") + DEDUP=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['deduplicated'])") + [ "$PROMOTED" = "1" ] + [ "$DEDUP" = "4" ] + + # context에 occurrences: 5 + CTX=$(jq -r '.context' "$ASSET_BASE_DIR/harnish-assets.jsonl") + [[ "$CTX" =~ "occurrences: 5" ]] +} + +# ─── 시나리오 3 — skillify production 산출물 ─────────────────────────────── + +@test "E2E pipeline 3: skillify generates Triggers + sectioned body + references trail" { + bash "$REPO_ROOT/scripts/init-assets.sh" --quiet + # 다양한 타입의 redis 자산 + bash "$REPO_ROOT/scripts/record-asset.sh" --type pattern --tags redis \ + --title "redis-pat-1" --body "set/get pattern" --context "ctx" \ + --base-dir "$ASSET_BASE_DIR" >/dev/null + bash "$REPO_ROOT/scripts/record-asset.sh" --type guardrail --tags redis \ + --title "no-flushall" --body "never FLUSHALL in prod" --context "safety" \ + --base-dir "$ASSET_BASE_DIR" >/dev/null + bash "$REPO_ROOT/scripts/record-asset.sh" --type failure --tags redis \ + --title "redis-down" --body "connection refused" --context "outage" \ + --base-dir "$ASSET_BASE_DIR" >/dev/null + + OUT_DIR="$CLAUDE_PROJECT_DIR/skills-out" + run bash "$REPO_ROOT/scripts/skillify.sh" \ + --tag redis --skill-name redis-skill \ + --output-dir "$OUT_DIR" \ + --base-dir "$ASSET_BASE_DIR" + [ "$status" -eq 0 ] + + SKILL_FILE="$OUT_DIR/redis-skill/SKILL.md" + REFS_FILE="$OUT_DIR/redis-skill/references/source-assets.jsonl" + + [ -f "$SKILL_FILE" ] + [ -f "$REFS_FILE" ] + + # Triggers 단어 + 타입별 섹션 + 메타 + grep -q "Triggers:" "$SKILL_FILE" + grep -q "## .*Patterns" "$SKILL_FILE" + grep -q "## .*Guardrails" "$SKILL_FILE" + grep -q "## .*Failures" "$SKILL_FILE" + grep -q "skillify_version: 0.0.5" "$SKILL_FILE" + + # references에 3건 자산 보존 + REFS_COUNT=$(wc -l < "$REFS_FILE" | xargs) + [ "$REFS_COUNT" -eq 3 ] +} + +# ─── 시나리오 4 — inject 컨텍스트 풍부화 ────────────────────────────────── + +@test "E2E pipeline 4: query --format inject includes context + level + confidence" { + bash "$REPO_ROOT/scripts/init-assets.sh" --quiet + bash "$REPO_ROOT/scripts/record-asset.sh" --type guardrail --tags arch \ + --title "no-direct-prod" --body "never deploy directly" --context "safety" \ + --base-dir "$ASSET_BASE_DIR" >/dev/null + bash "$REPO_ROOT/scripts/record-asset.sh" --type decision --tags arch \ + --title "use-event-bus" --body "event-driven over RPC" --context "scaling design" \ + --base-dir "$ASSET_BASE_DIR" >/dev/null + bash "$REPO_ROOT/scripts/record-asset.sh" --type failure --tags arch \ + --title "deploy-rollback" --body "manual rollback fail" --context "post-mortem" \ + --base-dir "$ASSET_BASE_DIR" >/dev/null + + run bash "$REPO_ROOT/scripts/query-assets.sh" \ + --tags arch --format inject --base-dir "$ASSET_BASE_DIR" + [ "$status" -eq 0 ] + + # guardrail에 level, decision에 confidence, failure에 resolved 표시 + [[ "$output" =~ "soft" ]] + [[ "$output" =~ "medium" ]] + [[ "$output" =~ "context:" ]] + [[ "$output" =~ "resolved:" ]] +} diff --git a/tests/e2e_workflow.bats b/tests/e2e_workflow.bats index f0f84b1..191d084 100644 --- a/tests/e2e_workflow.bats +++ b/tests/e2e_workflow.bats @@ -62,7 +62,7 @@ teardown() { run bash "$REPO_ROOT/scripts/init-assets.sh" --quiet [ "$status" -eq 0 ] [ -d "$ASSET_BASE_DIR" ] - [ -f "$ASSET_BASE_DIR/harnish-rag.jsonl" ] + [ -f "$ASSET_BASE_DIR/harnish-assets.jsonl" ] [ -f "$ASSET_BASE_DIR/harnish-current-work.json" ] } diff --git a/tests/scripts.bats b/tests/scripts.bats index 03b6d16..5622338 100644 --- a/tests/scripts.bats +++ b/tests/scripts.bats @@ -22,7 +22,7 @@ teardown() { run bash "$REPO_ROOT/scripts/init-assets.sh" --quiet [ "$status" -eq 0 ] [ -d "$ASSET_BASE_DIR" ] - [ -f "$ASSET_BASE_DIR/harnish-rag.jsonl" ] + [ -f "$ASSET_BASE_DIR/harnish-assets.jsonl" ] [ -f "$ASSET_BASE_DIR/harnish-current-work.json" ] # work file must be at minimum a valid JSON object. run python3 -m json.tool "$ASSET_BASE_DIR/harnish-current-work.json" @@ -32,10 +32,10 @@ teardown() { @test "init-assets.sh is idempotent (second run leaves files intact)" { bash "$REPO_ROOT/scripts/init-assets.sh" --quiet echo '{"type":"pattern","tags":["x"],"title":"t","body":"b"}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" bash "$REPO_ROOT/scripts/init-assets.sh" --quiet # Existing line must still be there (not truncated). - run grep -c '"title":"t"' "$ASSET_BASE_DIR/harnish-rag.jsonl" + run grep -c '"title":"t"' "$ASSET_BASE_DIR/harnish-assets.jsonl" [ "$status" -eq 0 ] [ "$output" = "1" ] } @@ -49,11 +49,11 @@ teardown() { --title "exp-backoff" --body "wait 2^n seconds" \ --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] - [ -s "$ASSET_BASE_DIR/harnish-rag.jsonl" ] + [ -s "$ASSET_BASE_DIR/harnish-assets.jsonl" ] # Each line must be valid JSON. while IFS= read -r line; do echo "$line" | python3 -m json.tool >/dev/null - done < "$ASSET_BASE_DIR/harnish-rag.jsonl" + done < "$ASSET_BASE_DIR/harnish-assets.jsonl" } @test "record-asset.sh accepts JSON via --stdin" { @@ -63,7 +63,7 @@ teardown() { | bash '$REPO_ROOT/scripts/record-asset.sh' --stdin --base-dir '$ASSET_BASE_DIR' " [ "$status" -eq 0 ] - grep -q '"title":"cache-miss"' "$ASSET_BASE_DIR/harnish-rag.jsonl" + grep -q '"title":"cache-miss"' "$ASSET_BASE_DIR/harnish-assets.jsonl" } # ---------- query-assets.sh ---------- diff --git a/tests/scripts_advanced.bats b/tests/scripts_advanced.bats index f5aa134..d9003b6 100644 --- a/tests/scripts_advanced.bats +++ b/tests/scripts_advanced.bats @@ -128,7 +128,7 @@ print(json.dumps(d)) @test "quality-gate.sh flags records with empty body" { echo '{"type":"pattern","slug":"x","title":"t","tags":["a"],"body":"","context":"","schema_version":"0.0.2","last_accessed_at":"2026-01-01T00:00:00Z","access_count":0}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" run bash "$REPO_ROOT/scripts/quality-gate.sh" --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] [[ "$output" =~ "보완" ]] @@ -150,11 +150,11 @@ print(json.dumps(d)) bash "$REPO_ROOT/scripts/record-asset.sh" \ --type pattern --tags "api" --title "p2" --body "b2" \ --base-dir "$ASSET_BASE_DIR" - BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) run bash "$REPO_ROOT/scripts/compress-assets.sh" \ --tag api --dry-run --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] - AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) [ "$BEFORE" -eq "$AFTER" ] } @@ -168,7 +168,7 @@ print(json.dumps(d)) --tag db --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] # compressed summary record must not contain the word TODO - run grep -c "TODO" "$ASSET_BASE_DIR/harnish-rag.jsonl" + run grep -c "TODO" "$ASSET_BASE_DIR/harnish-assets.jsonl" [ "$output" = "0" ] } @@ -181,7 +181,7 @@ print(json.dumps(d)) run bash "$REPO_ROOT/scripts/abstract-asset.sh" \ --slug "my-pattern" --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] - grep -q '"slug":"my-pattern-generic"' "$ASSET_BASE_DIR/harnish-rag.jsonl" + grep -q '"slug":"my-pattern-generic"' "$ASSET_BASE_DIR/harnish-assets.jsonl" } @test "abstract-asset.sh exits 1 on missing slug" { @@ -199,7 +199,7 @@ print(json.dumps(d)) run bash "$REPO_ROOT/scripts/localize-asset.sh" \ --slug "gen-rule" --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] - grep -q '"slug":"gen-rule-local"' "$ASSET_BASE_DIR/harnish-rag.jsonl" + grep -q '"slug":"gen-rule-local"' "$ASSET_BASE_DIR/harnish-assets.jsonl" } @test "localize-asset.sh exits 1 on missing slug" { @@ -218,11 +218,11 @@ print(json.dumps(d)) @test "migrate.sh backfills schema_version on v0.0.1 records" { echo '{"type":"pattern","slug":"old","title":"t","tags":["x"],"body":"b","schema_version":"0.0.1"}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" run bash "$REPO_ROOT/scripts/migrate.sh" --base-dir "$ASSET_BASE_DIR" [ "$status" -eq 0 ] [[ "$output" =~ "migrated" ]] - VERSION=$(jq -r '.schema_version' "$ASSET_BASE_DIR/harnish-rag.jsonl") + VERSION=$(jq -r '.schema_version' "$ASSET_BASE_DIR/harnish-assets.jsonl") [ "$VERSION" = "0.0.2" ] } @@ -248,14 +248,45 @@ print(json.dumps(d)) [ "$status" -eq 1 ] } +# ─── init-assets.sh — legacy migration ─────────────────────────────────────── + +@test "init-assets.sh migrates legacy harnish-rag.jsonl to harnish-assets.jsonl" { + # Wipe sandbox dir and start fresh, simulating an old-format install. + rm -rf "$ASSET_BASE_DIR" + mkdir -p "$ASSET_BASE_DIR" + echo '{"type":"pattern","slug":"legacy","title":"legacy-record","tags":["x"],"body":"b","date":"2025-01-01","schema_version":"0.0.2"}' \ + > "$ASSET_BASE_DIR/harnish-rag.jsonl" + + run bash "$REPO_ROOT/scripts/init-assets.sh" --base-dir "$ASSET_BASE_DIR" + [ "$status" -eq 0 ] + # 새 파일이 생성되어야 함 + [ -f "$ASSET_BASE_DIR/harnish-assets.jsonl" ] + # 레거시 파일은 사라져야 함 + [ ! -f "$ASSET_BASE_DIR/harnish-rag.jsonl" ] + # 콘텐츠가 보존되어야 함 + grep -q '"slug":"legacy"' "$ASSET_BASE_DIR/harnish-assets.jsonl" +} + +@test "init-assets.sh idempotent migration — leaves new file alone if both exist" { + rm -rf "$ASSET_BASE_DIR" + mkdir -p "$ASSET_BASE_DIR" + # 둘 다 있으면 신규를 보존하고 레거시를 건드리지 않아야 함 + echo '{"slug":"new"}' > "$ASSET_BASE_DIR/harnish-assets.jsonl" + echo '{"slug":"legacy"}' > "$ASSET_BASE_DIR/harnish-rag.jsonl" + bash "$REPO_ROOT/scripts/init-assets.sh" --base-dir "$ASSET_BASE_DIR" --quiet + grep -q '"slug":"new"' "$ASSET_BASE_DIR/harnish-assets.jsonl" + # 레거시 파일은 그대로 존재 (사용자가 수동 삭제할 수 있도록) + [ -f "$ASSET_BASE_DIR/harnish-rag.jsonl" ] +} + # ─── purge-assets.sh --execute ─────────────────────────────────────────────── @test "purge-assets.sh --execute purges old records and creates archive" { bash "$REPO_ROOT/scripts/init-assets.sh" --quiet # Insert a very old decision record (> 365 days, access_count 0) echo '{"type":"decision","slug":"stale-dec","title":"old decision","tags":["arch"],"body":"body","context":"ctx","date":"2020-01-01","scope":"generic","session":"manual","schema_version":"0.0.2","last_accessed_at":"2020-01-01T00:00:00Z","access_count":0,"confidence":"medium"}' \ - >> "$ASSET_BASE_DIR/harnish-rag.jsonl" - BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + >> "$ASSET_BASE_DIR/harnish-assets.jsonl" + BEFORE=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) run bash "$REPO_ROOT/scripts/purge-assets.sh" --execute \ --base-dir "$ASSET_BASE_DIR" @@ -264,9 +295,9 @@ print(json.dumps(d)) STATUS=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['status'])") [ "$STATUS" = "purged" ] - AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-rag.jsonl" | xargs) + AFTER=$(wc -l < "$ASSET_BASE_DIR/harnish-assets.jsonl" | xargs) [ "$AFTER" -lt "$BEFORE" ] - [ -f "$ASSET_BASE_DIR/harnish-rag-archive.jsonl" ] + [ -f "$ASSET_BASE_DIR/harnish-assets-archive.jsonl" ] } @test "purge-assets.sh --execute no-op when no candidates" {