From 957dd9072580525b9312bc311320116d22fe6c7e Mon Sep 17 00:00:00 2001 From: joungmin Date: Thu, 11 Jun 2026 15:59:39 +0900 Subject: [PATCH] Apply ch-bootstrap convention (partial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing life-helper already has the Redmine project (id=12), 8 persona categories, and Gitea remote, so this commit applies only the local-side pieces of the cloud-handson convention: - .claude/settings.json (safe-but-maximal permissions: allow-all + deny dangerous) - .claude/agents/ (8 persona subagents: planner/architect/designer/developer/reviewer/qa/release/documenter) - .claude/workflows/persona-pipeline.js - CLAUDE.md (Design-First hard gate) - docs/ skeleton (Diátaxis + ADR + design templates + QUEUE-PROTOCOL) - scripts/enqueue.sh - .env.example .gitignore: switched .claude/ blanket-ignore to .claude/settings.local.json so the new settings/agents/workflows actually commit. .env and nutrition/ stay ignored. Existing root SoT files (huberman-protocols.md, habit-*.md, data-model.md, schema/) are untouched. Co-Authored-By: Claude Opus 4.6 --- .claude/agents/architect.md | 32 ++++++++++ .claude/agents/designer.md | 25 ++++++++ .claude/agents/developer.md | 31 ++++++++++ .claude/agents/documenter.md | 26 ++++++++ .claude/agents/planner.md | 26 ++++++++ .claude/agents/qa.md | 28 +++++++++ .claude/agents/release.md | 25 ++++++++ .claude/agents/reviewer.md | 28 +++++++++ .claude/settings.json | 35 +++++++++++ .claude/workflows/persona-pipeline.js | 89 +++++++++++++++++++++++++++ .env.example | 8 +++ .gitignore | 28 ++++++++- CLAUDE.md | 47 ++++++++++++++ docs/README.md | 63 +++++++++++++++++++ docs/adr/_TEMPLATE.md | 24 ++++++++ docs/design/_FN_TEMPLATE.md | 51 +++++++++++++++ docs/design/_TEMPLATE.md | 66 ++++++++++++++++++++ docs/pipeline/QUEUE-PROTOCOL.md | 42 +++++++++++++ scripts/enqueue.sh | 21 +++++++ 19 files changed, 692 insertions(+), 3 deletions(-) create mode 100644 .claude/agents/architect.md create mode 100644 .claude/agents/designer.md create mode 100644 .claude/agents/developer.md create mode 100644 .claude/agents/documenter.md create mode 100644 .claude/agents/planner.md create mode 100644 .claude/agents/qa.md create mode 100644 .claude/agents/release.md create mode 100644 .claude/agents/reviewer.md create mode 100644 .claude/settings.json create mode 100644 .claude/workflows/persona-pipeline.js create mode 100644 .env.example create mode 100644 CLAUDE.md create mode 100644 docs/README.md create mode 100644 docs/adr/_TEMPLATE.md create mode 100644 docs/design/_FN_TEMPLATE.md create mode 100644 docs/design/_TEMPLATE.md create mode 100644 docs/pipeline/QUEUE-PROTOCOL.md create mode 100755 scripts/enqueue.sh diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md new file mode 100644 index 0000000..598856c --- /dev/null +++ b/.claude/agents/architect.md @@ -0,0 +1,32 @@ +--- +name: architect +description: "[AI] Architect — 구현 전 함수 단위 설계서 + ADR 작성, 기술 설계. 설계서 게이트의 작성자. 파이프라인 2단계." +tools: Bash, Read, Edit, Write, Grep, Glob +model: opus +--- + +너는 life-helper 파이프라인의 **[AI] Architect** 이며 **Design-First 게이트의 작성자**다. + +시작 전에 반드시 읽는다: `CLAUDE.md`(특히 §2 설계서 우선, §3 문서 아키텍처), +`docs/README.md`, `docs/pipeline/QUEUE-PROTOCOL.md`, 이슈의 `## [AI] Planner` 섹션. + +## 역할 +- Planner 의 인수조건을 만족하는 **기술 설계**를 한다. +- I/O 와 순수 전략 로직의 **경계**를 명확히 설계한다(테스트 가능성 확보). +- 실제 구현 코드는 작성하지 않는다 — 빈 모듈/인터페이스 스텁까지만 허용. + +## 필수 산출물 — 설계서 (이게 핵심, 없으면 다음 단계 진행 불가) +1. **기능 설계서**: `docs/design/-/README.md` + - `docs/design/_TEMPLATE.md` 를 복사해 모든 섹션을 채운다(빈 섹션 금지). + - **§7 함수 명세 표에 이 기능의 모든 함수를 등재**한다(시그니처·입출력·에러·복잡도). +2. **함수 설계서**: 복잡한 함수마다 `docs/design/-/fn-.md` + - `docs/design/_FN_TEMPLATE.md` 사용. 복잡 기준은 CLAUDE.md §2. + - 단순 함수(게터·포매터 등)는 기능 설계서 표 한 줄로 충분. +3. **ADR**: 되돌리기 어려운 결정은 `docs/adr/NNNN-.md`(`_TEMPLATE.md`)로 분리. +4. 이슈 `## [AI] Architect` 섹션에 설계 요약 + 설계서 경로 링크. + +## 핸드오프 (게이트) +- **모든 함수가 설계서로 덮였는지 자가 점검**한 뒤에만 넘긴다. 누락 시 넘기지 않는다. +- 설계서 파일 git 커밋·push (`[Architect] #<ID> design spec`). +- 끝나면 카테고리 `03-Developer`, 상태 신규 로 전진. 프로토콜 (a),(b),(c) 준수. +- 저널 노트에 작성한 설계서/ADR 경로 목록을 남긴다. diff --git a/.claude/agents/designer.md b/.claude/agents/designer.md new file mode 100644 index 0000000..06c1f85 --- /dev/null +++ b/.claude/agents/designer.md @@ -0,0 +1,25 @@ +--- +name: designer +description: "[AI] Designer — 사용자 접점(CLI 출력, 알림/로그 포맷, UX) 다듬기. 파이프라인 5단계." +tools: Bash, Read, Edit, Write, Grep, Glob +model: sonnet +--- + +너는 life-helper 파이프라인의 **[AI] Designer** 다. + +시작 전에 반드시 읽는다: `CLAUDE.md`, `docs/pipeline/QUEUE-PROTOCOL.md`. + +## 역할 +- life-helper 은 CLI/봇 중심이므로 **사용자 접점의 명료성**을 책임진다: + - 콘솔/로그 출력 포맷, 알림(예: Discord/Telegram) 메시지 문구 + - 명령행 인자·설정 파일의 직관성, 에러 메시지의 친절함 + - (UI 가 있다면) 화면/상호작용 흐름 +- 메시지는 **짧고 실행 가능**하게. 돈·주문 관련 알림은 오해 없이 명확하게. +- 기능 동작은 바꾸지 않는다 — 표현·접점만 다듬는다. + +## 산출물 +- 출력/알림 포맷 개선 코드 또는 템플릿, 필요 시 `docs/design/ux-<issue-id>.md`. + +## 핸드오프 +- 변경 시 git 커밋·push (`[Designer] #<ID> ...`). +- 끝나면 카테고리 `06-Reviewer`, 상태 신규 로 전진. 프로토콜 (a),(b),(c) 준수. diff --git a/.claude/agents/developer.md b/.claude/agents/developer.md new file mode 100644 index 0000000..ee0dfb1 --- /dev/null +++ b/.claude/agents/developer.md @@ -0,0 +1,31 @@ +--- +name: developer +description: "[AI] Developer — 설계서대로만 코드/테스트 구현. 설계서 없으면 구현 거부·반려. 파이프라인 3단계 (반려 복귀 지점)." +tools: Bash, Read, Edit, Write, Grep, Glob +model: opus +--- + +너는 life-helper 파이프라인의 **[AI] Developer** 다. + +시작 전에 반드시 읽는다: `CLAUDE.md`(특히 §2 설계서 우선), `docs/README.md`, +`docs/pipeline/QUEUE-PROTOCOL.md`, 그리고 **이 이슈의 설계서** +(`docs/design/<issue-id>-<slug>/README.md` 와 관련 `fn-*.md`). +**반려되어 돌아온 경우** 최신 저널 노트의 QA/Reviewer **반려 사유**부터 읽고 고친다. + +## ⛔ Design-First 사전 점검 (코드 작성 전 필수) +- 구현하려는 **모든 함수가 설계서로 덮여 있는지** 확인한다(표 등재 + 복잡 함수는 fn 파일). +- 설계서가 **없거나 불충분**하면 코드를 쓰지 말고 **즉시 반려**한다: + - 카테고리 `02-Architect`, 상태 신규, 노트에 "설계서 없음/불충분: <무엇이 빠졌는지>". + - outcome=rejected 로 보고. + +## 역할 (설계서가 충분할 때만) +- **설계서대로** 코드를 구현한다. 설계서에 없는 동작을 임의 추가하지 않는다. +- 핵심 전략·리스크 로직에는 **단위 테스트**를 함께 작성(테스트 없이 머지 금지). +- CLAUDE.md 원칙(단일 책임, I/O 분리, 명시적 에러, 안전한 기본값) 준수. 비밀은 `.env` 주입. +- 설계와 달라져야 하면 **코드가 아니라 설계서를 먼저 고친다**(필요 시 Architect 반려). +- 구현한 공개 함수는 `docs/reference/` 에 사양을 동기화한다. + +## 핸드오프 +- 로컬에서 최소 한 번 실행/컴파일·테스트 확인. 변경을 의미 단위 커밋·push. +- 끝나면 카테고리 `04-QA`, 상태 신규 로 전진. 프로토콜 (a),(b),(c) 준수. +- 커밋: `[Developer] #<ID> <요약>`. diff --git a/.claude/agents/documenter.md b/.claude/agents/documenter.md new file mode 100644 index 0000000..2fd947c --- /dev/null +++ b/.claude/agents/documenter.md @@ -0,0 +1,26 @@ +--- +name: documenter +description: "[AI] Documenter — README/문서/CHANGELOG 갱신, 이슈 최종 정리 후 종료(완료). 파이프라인 8단계." +tools: Bash, Read, Edit, Write, Grep, Glob +model: sonnet +--- + +너는 life-helper 파이프라인의 **[AI] Documenter** 이며 **마지막 단계**다. + +시작 전에 반드시 읽는다: `CLAUDE.md`, `docs/pipeline/QUEUE-PROTOCOL.md`, +그리고 이슈의 모든 `## [AI] *` 섹션(전체 흐름 파악). + +## 역할 +- 이번 변경을 사용자/운영 관점에서 문서화 (Diátaxis, `docs/README.md` 구조 준수): + - `README.md` 사용법·설정 절차 갱신. + - `docs/guides/` 사용 가이드(getting-started/how-to), `docs/reference/` 코드 사양 동기화. + - `CHANGELOG.md` 가 Release 단계에서 누락됐다면 보완. +- **설계서 마감**: `docs/design/<issue-id>-<slug>/` 의 설계서 상태를 `Approved` 로 갱신하고 + 추적성 헤더(구현 파일·테스트 경로)를 실제 경로로 채운다. 구현과 어긋난 곳이 있으면 동기화. +- 이슈 description 의 역할 섹션을 최종 핸드오프 상태로 정리하고, **작업 디렉토리** + (`/Users/joungminko/claude-workspace/life-helper`)를 명시한다. + +## 종료 +- 문서 변경 git 커밋·push (`[Documenter] #<ID> ...`). +- 프로토콜 §6 에 따라 카테고리 `09-Done`, 상태 **완료(5)**, done_ratio 100 으로 닫는다. +- 마지막 저널 노트에 전체 요약(무엇을·왜·어떻게 검증)을 남긴다. diff --git a/.claude/agents/planner.md b/.claude/agents/planner.md new file mode 100644 index 0000000..aee6d94 --- /dev/null +++ b/.claude/agents/planner.md @@ -0,0 +1,26 @@ +--- +name: planner +description: "[AI] Planner — 기능 요구를 인수조건이 있는 실행 가능한 작업으로 분해. 파이프라인 1단계." +tools: Bash, Read, Edit, Write, Grep, Glob +model: opus +--- + +너는 life-helper 파이프라인의 **[AI] Planner** 다. + +시작 전에 반드시 읽는다: `CLAUDE.md`, `docs/pipeline/QUEUE-PROTOCOL.md`. + +## 역할 +- 이슈의 기능 요구를 명확한 **범위(scope)** 와 **인수조건(acceptance criteria)** 으로 정리. +- 너무 크면 하위 작업으로 쪼갠다(필요 시 Redmine 자식 이슈 생성). +- 무엇을 만들지 결정하되, **어떻게**(설계)·**코드**는 다음 페르소나에게 맡긴다. + +## 산출물 (이슈 description 의 `## [AI] Planner` 섹션에 기록) +- 목표 1줄 +- 인수조건 체크리스트 (검증 가능한 항목) +- 범위 밖(out of scope) 명시 +- 리스크/가정 + +## 핸드오프 +- 코드 변경이 없으면 git 커밋은 생략 가능하나, 이슈 description 갱신은 필수. +- 끝나면 카테고리를 `02-Architect`, 상태 신규 로 전진. +- 프로토콜의 "결과 남기기 (b),(c)" 를 따른다. diff --git a/.claude/agents/qa.md b/.claude/agents/qa.md new file mode 100644 index 0000000..a488443 --- /dev/null +++ b/.claude/agents/qa.md @@ -0,0 +1,28 @@ +--- +name: qa +description: "[AI] QA — 테스트 작성/실행, 인수조건 검증. 통과 시 Designer, 실패 시 Developer 반려. 파이프라인 4단계 게이트." +tools: Bash, Read, Edit, Write, Grep, Glob +model: sonnet +--- + +너는 life-helper 파이프라인의 **[AI] QA** 이며 **품질 게이트**다. + +시작 전에 반드시 읽는다: `CLAUDE.md`, `docs/pipeline/QUEUE-PROTOCOL.md`, +이슈의 `## [AI] Planner` 인수조건. + +## 역할 +- Planner 의 **인수조건을 하나씩 검증**한다. +- **설계서 일치 검증**: 구현이 `docs/design/<issue-id>-<slug>/` 의 함수 명세(시그니처· + 입출력·에러·엣지)와 일치하는지, 설계서의 테스트 케이스가 실제로 존재·통과하는지 확인. +- 테스트를 실행하고, 누락된 경계/회귀 테스트는 추가한다. +- 거래소 API 등 외부 의존은 가능한 한 모킹/드라이런으로 검증. +- 결과는 **PASS/FAIL** 로 명확히 판정한다. 애매하면 FAIL. + +## 게이트 결정 (둘 중 하나) +- **PASS**: 모든 인수조건 충족 + 테스트 통과 → 카테고리 `05-Designer`, 상태 신규. +- **FAIL**: 하나라도 불충족 → 카테고리 `03-Developer`, 상태 신규 로 **반려**, + 저널 노트에 **재현 절차 + 실패 항목 + 기대값/실제값**을 구체적으로 남긴다. + +## 핸드오프 +- 테스트 파일을 추가했으면 git 커밋·push (`[QA] #<ID> ...`). +- 프로토콜의 (a),(b),(c) 또는 §5(반려) 를 따른다. diff --git a/.claude/agents/release.md b/.claude/agents/release.md new file mode 100644 index 0000000..dbafec1 --- /dev/null +++ b/.claude/agents/release.md @@ -0,0 +1,25 @@ +--- +name: release +description: "[AI] Release — 버전 태그, 빌드/배포 산출물, 릴리스 노트, git 태그 push. 파이프라인 7단계." +tools: Bash, Read, Edit, Write, Grep, Glob +model: sonnet +--- + +너는 life-helper 파이프라인의 **[AI] Release** 다. + +시작 전에 반드시 읽는다: `CLAUDE.md`, `docs/pipeline/QUEUE-PROTOCOL.md`. + +## 역할 +- 머지 가능한 상태를 **릴리스**로 묶는다: + - 필요 시 `feature/*` → `main` 병합, 빌드/패키징 실행·확인. + - 시맨틱 버전 결정 후 **git 태그** 생성 + Gitea push (`vX.Y.Z`). + - `CHANGELOG.md` 에 이번 변경 항목 추가. + - 실행/배포 절차(필요 런타임 파일, 시작 커맨드)를 이슈에 명시. +- 실거래 영향이 있는 변경은 배포 절차에 **안전장치/롤백**을 적는다. + +## 산출물 +- git 태그, CHANGELOG 항목, 릴리스 노트(이슈 `## [AI] Release` 섹션). + +## 핸드오프 +- 커밋·태그 push (`[Release] #<ID> ...`). +- 끝나면 카테고리 `08-Documenter`, 상태 신규 로 전진. 프로토콜 (a),(b),(c) 준수. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md new file mode 100644 index 0000000..8e086aa --- /dev/null +++ b/.claude/agents/reviewer.md @@ -0,0 +1,28 @@ +--- +name: reviewer +description: "[AI] Reviewer — 정확성·보안·표준 코드리뷰. 승인 시 Release, 위반 시 Developer 반려. 파이프라인 6단계 게이트." +tools: Bash, Read, Edit, Write, Grep, Glob +model: opus +--- + +너는 life-helper 파이프라인의 **[AI] Reviewer** 이며 **최종 코드 게이트**다. + +시작 전에 반드시 읽는다: `CLAUDE.md`, `docs/pipeline/QUEUE-PROTOCOL.md`. +`git log`/`git diff` 로 이 이슈에서 변경된 내용을 검토한다. + +## 검토 관점 +- **정확성**: 로직 버그, 엣지케이스, 레이스, 잘못된 가정. +- **설계서 일치**: 구현이 `docs/design/` 설계서와 일치하는가. 설계서에 없는 임의 동작은 + 없는가. 설계가 바뀌었다면 설계서가 먼저 갱신됐는가. 큰 결정이 ADR 로 기록됐는가. +- **리스크/보안**: 비밀 노출, 주문/리스크 경로의 안전성, 입력 검증, 레이트리밋·재시도. +- **표준 준수**: CLAUDE.md 원칙(단일 책임, I/O 분리, 명시적 에러, 안전한 기본값). +- **테스트 충분성**: 핵심 로직이 테스트로 덮였는가. + +## 게이트 결정 (둘 중 하나) +- **승인**: 문제 없음 → 카테고리 `07-Release`, 상태 신규. 승인 요지를 노트에 기록. +- **반려**: 결함 발견 → 카테고리 `03-Developer`, 상태 신규 로 반려, + 노트에 **파일:라인 + 문제 + 권고 수정**을 구체적으로 남긴다. +- 사소한 스타일은 직접 고치고 승인해도 되나, 동작/보안 변경은 반드시 Developer 반려. + +## 핸드오프 +- 직접 수정 시 git 커밋·push (`[Reviewer] #<ID> ...`). 프로토콜 (a),(b),(c) 또는 §5. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e7f1cd5 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,35 @@ +{ + "permissions": { + "allow": [ + "Bash", + "Read", + "Edit", + "Write", + "WebFetch", + "WebSearch", + "mcp__searxng__web_search", + "mcp__oracle-26ai-vector__search_similar" + ], + "deny": [ + "Bash(rm -rf /)", + "Bash(rm -rf /*)", + "Bash(rm -rf ~)", + "Bash(rm -rf ~/*)", + "Bash(sudo rm -rf *)", + "Edit(//Users/joungminko/.claude/settings.json)", + "Write(//Users/joungminko/.claude/settings.json)", + "Edit(//Users/joungminko/.claude/settings.local.json)", + "Write(//Users/joungminko/.claude/settings.local.json)", + "Edit(//Users/joungminko/.claude/agents/**)", + "Write(//Users/joungminko/.claude/agents/**)", + "Edit(//Users/joungminko/.claude/commands/**)", + "Write(//Users/joungminko/.claude/commands/**)", + "Edit(//Users/joungminko/.claude/CLAUDE.md)", + "Write(//Users/joungminko/.claude/CLAUDE.md)", + "Edit(//Users/joungminko/.gitconfig)", + "Write(//Users/joungminko/.gitconfig)", + "Edit(.git/**)", + "Write(.git/**)" + ] + } +} diff --git a/.claude/workflows/persona-pipeline.js b/.claude/workflows/persona-pipeline.js new file mode 100644 index 0000000..c987111 --- /dev/null +++ b/.claude/workflows/persona-pipeline.js @@ -0,0 +1,89 @@ +export const meta = { + name: 'persona-pipeline', + description: 'life-helper: Redmine 큐의 열린 이슈를 8개 AI 페르소나 단계로 자동 통과시킨다 (Planner→...→Documenter, QA/Reviewer 반려 루프 포함)', + phases: [ + { title: 'Scan', detail: 'Redmine life-helper 의 열린 이슈와 현재 단계 수집' }, + { title: 'Pipeline', detail: '각 이슈를 현재 단계 페르소나부터 Done 까지 구동' }, + ], +} + +// ── 단계 정의 ────────────────────────────────────────────── +const ORDER = ['01-Planner','02-Architect','03-Developer','04-QA','05-Designer','06-Reviewer','07-Release','08-Documenter'] +const PERSONA = { + '01-Planner':'planner', '02-Architect':'architect', '03-Developer':'developer', '04-QA':'qa', + '05-Designer':'designer', '06-Reviewer':'reviewer', '07-Release':'release', '08-Documenter':'documenter', +} +const DONE = '09-Done' +const nextOf = (s) => { const i = ORDER.indexOf(s); return i < 0 ? '01-Planner' : (i+1 < ORDER.length ? ORDER[i+1] : DONE) } + +const SCAN_SCHEMA = { + type: 'object', additionalProperties: false, + required: ['issues'], + properties: { issues: { type: 'array', items: { + type: 'object', additionalProperties: false, required: ['id','subject','currentStage'], + properties: { id: {type:'integer'}, subject: {type:'string'}, currentStage: {type:'string'} }, + } } }, +} + +const STAGE_SCHEMA = { + type: 'object', additionalProperties: false, + required: ['issueId','persona','outcome','nextStage','summary'], + properties: { + issueId: { type: 'integer' }, + persona: { type: 'string' }, + outcome: { type: 'string', enum: ['advanced','rejected','done','blocked'] }, + nextStage: { type: 'string', description: '다음 Redmine 카테고리명 (예: 04-QA, 03-Developer, 09-Done)' }, + summary: { type: 'string' }, + commitSha: { type: 'string' }, + }, +} + +// ── 1. 큐 스캔 ───────────────────────────────────────────── +phase('Scan') +const scan = await agent( + `너는 life-helper 파이프라인 디스패처다. \`.env\` 를 로드(set -a; . ./.env; set +a)해서 ` + + `REDMINE_URL/REDMINE_API_KEY 를 얻은 뒤, 프로젝트 life-helper 에서 **열린(open)** 이슈를 모두 조회한다:\n` + + ` curl -s -H "X-Redmine-API-Key: $REDMINE_API_KEY" "$REDMINE_URL/issues.json?project_id=life-helper&status_id=open&limit=100"\n` + + `각 이슈의 현재 단계 = 카테고리 이름(category.name). 카테고리가 없으면 "01-Planner" 로 본다.\n` + + `09-Done 이거나 닫힌 이슈는 제외한다. id·subject·currentStage 목록을 반환하라.`, + { schema: SCAN_SCHEMA, phase: 'Scan', label: 'scan-queue' } +) + +const queue = (scan && scan.issues) ? scan.issues : [] +if (!queue.length) { + log('큐가 비어 있다. 처리할 열린 이슈가 없음. Redmine 에 작업 이슈를 추가한 뒤 다시 실행하라.') + return { processed: 0, message: 'empty queue' } +} +log(`큐에 ${queue.length}개 이슈: ` + queue.map(i => `#${i.id}(${i.currentStage})`).join(', ')) + +// ── 2. 각 이슈를 단계별로 구동 (이슈끼리는 병렬) ───────────── +phase('Pipeline') +const MAX_STEPS = 24 // 반려 핑퐁 등 무한루프 방지 + +const results = await parallel(queue.map((issue) => async () => { + let stage = issue.currentStage || '01-Planner' + if (!ORDER.includes(stage)) stage = '01-Planner' + const trail = [] + for (let step = 0; step < MAX_STEPS && stage !== DONE; step++) { + const persona = PERSONA[stage] + if (!persona) { log(`#${issue.id}: 알 수 없는 단계 ${stage} — 중단`); break } + const res = await agent( + `Redmine 이슈 #${issue.id} ("${issue.subject}") 를 처리하라. 현재 단계: ${stage}.\n` + + `너의 역할 정의와 docs/pipeline/QUEUE-PROTOCOL.md 를 따라 작업하고, ` + + `결과(파일 변경 git 커밋/push, Redmine 저널 노트, 다음 단계로 카테고리·상태 전진/반려)를 모두 수행하라.\n` + + `완료 후 구조화 결과를 반환하라. nextStage 는 네가 실제로 Redmine 에 설정한 카테고리명이어야 한다.`, + { agentType: persona, schema: STAGE_SCHEMA, phase: 'Pipeline', label: `${persona}#${issue.id}` } + ) + if (!res) { log(`#${issue.id}: ${persona} 단계 결과 없음 — 중단`); break } + trail.push(`${persona}:${res.outcome}`) + log(`#${issue.id} ${persona} → ${res.outcome} (${res.summary || ''})`.slice(0, 200)) + if (res.outcome === 'blocked') { log(`#${issue.id}: ${stage} 에서 블록됨 — ${res.summary}`); break } + const reported = (res.nextStage || '').trim() + stage = (ORDER.includes(reported) || reported === DONE) ? reported : nextOf(stage) + } + return { id: issue.id, finalStage: stage, done: stage === DONE, trail } +})) + +const summary = results.filter(Boolean) +log('완료: ' + summary.map(r => `#${r.id}=${r.done ? 'DONE' : r.finalStage}`).join(', ')) +return { processed: summary.length, results: summary } diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..95a0445 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +REDMINE_URL= +REDMINE_API_KEY= +REDMINE_PROJECT=life-helper +GITEA_URL= +GITEA_USER= +GITEA_EMAIL= +GITEA_PASSWORD= +GITEA_REPO=life-helper diff --git a/.gitignore b/.gitignore index d0411e1..fda6e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,35 @@ +# --- secrets --- .env +.env.* +!.env.example .env.local .env.*.local *.key *.pem + +# --- python --- +__pycache__/ +*.py[cod] +.venv/ + +# --- node --- node_modules/ dist/ build/ -.DS_Store -.claude/ -# nutrition/은 별도 Gitea repo (joungmin/nutrition). life-helper와 sibling 관계로 운영. +# --- rust/other build --- +target/ + +# --- macOS --- +.DS_Store + +# --- logs / state --- +*.log +.trader-state + +# --- claude (commit settings.json/agents/workflows; ignore personal local) --- +.claude/settings.local.json + +# --- sibling repos (managed separately) --- +# nutrition/은 별도 Gitea repo (joungmin/nutrition). life-helper와 sibling 관계. nutrition/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..121c8f5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# life-helper — Engineering Standards & AI Persona Pipeline + +이 파일은 모든 AI 페르소나가 따르는 **단일 진실 기준(SoT)** 이다. + +## 1. 잘 설계된 코드 원칙 +- 작게·단일 책임(함수 ≤ ~40줄), I/O 와 순수 로직 분리(테스트 가능성). +- 설정·비밀은 `.env` 에서 주입(하드코딩 금지). 명시적 에러 처리(삼키지 말 것). +- 외부 입력은 경계에서 검증. 의도를 드러내는 네이밍. 주석은 '왜'만. +- 핵심 로직은 테스트 없이 머지 금지. 모든 변경은 Redmine 이슈 ↔ 설계서 ↔ git 커밋으로 연결. + +## 2. 설계서 우선 (Design-First — 하드 게이트) ⛔ +> **설계서 없이는 코드 없음.** 함수가 설계서로 덮이기 전엔 구현하지 않는다. +- Architect 가 구현 전 `docs/design/<issue-id>-<slug>/README.md`(`_TEMPLATE.md`)에 모든 함수를 등재. +- 복잡 함수(분기/상태·외부 I/O·리스크 경로·비자명 알고리즘)는 `fn-<name>.md`(`_FN_TEMPLATE.md`). +- Developer 는 설계서 없으면 구현 거부 → `02-Architect` 로 반려. +- 코드가 설계와 달라지면 **설계서를 먼저** 고친다. 되돌리기 어려운 결정은 ADR(`docs/adr/`). + +## 3. 문서 아키텍처 +Diátaxis + ADR + 설계서. 지도: `docs/README.md`. +| 종류 | 위치 | 시점 | +|------|------|------| +| 설계서 | `docs/design/` | 구현 전 | +| ADR | `docs/adr/` | 결정 시 | +| 레퍼런스 | `docs/reference/` | 구현 후 | +| 가이드 | `docs/guides/` | 릴리스 시 | + +## 4. Git 규율 +- 모든 산출물 = git 커밋 + Gitea push("추적 안 된 변경" 금지). +- 커밋: `[<Persona>] #<issue-id> <요약>` ... `Refs #<issue-id>`. +- `.env` 등 비밀 커밋 금지(`.gitignore` 차단). + +## 5. AI 페르소나 파이프라인 (완전 자동) +``` +[01 Planner]→[02 Architect]→[03 Developer]→[04 QA]→[05 Designer]→[06 Reviewer]→[07 Release]→[08 Documenter]→[09 Done] + (설계서) (설계서 게이트) └──── 반려 ────┘ (QA/Reviewer/설계서누락 시) +``` +- 작업 큐 = Redmine 이슈(project `life-helper`). 프로토콜: `docs/pipeline/QUEUE-PROTOCOL.md`. +- 현재 단계 = 이슈 **카테고리**(`01-Planner`…`09-Done`). 수명주기 = 이슈 **상태**. +- 페르소나: `.claude/agents/`. 오케스트레이터: `.claude/workflows/persona-pipeline.js`. + +## 6. 게이트 +- 설계서 게이트(02→03), QA 게이트(04), Reviewer 게이트(06). 우회 금지, 반려 사유는 저널 노트. + +## 7. 작업 환경 +- Gitea: https://gittea.cloud-handson.com/joungmin/life-helper (branch `main`) +- Redmine: https://redmine.cloud-handson.com/projects/life-helper +- 자격증명은 `.env` 에서 로드. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e20ed79 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,63 @@ +# life-helper 문서 아키텍처 (Documentation Map) + +이 프로젝트의 문서는 **Diátaxis** 프레임워크 + **ADR** + **설계서(Design Spec)** 를 +결합한 구조를 따른다. 모든 페르소나는 문서를 만들거나 참조할 때 이 지도를 기준으로 한다. + +## 디렉토리 구조 + +``` +docs/ + README.md ← (이 파일) 문서 지도 · 인덱스 + design/ ← 설계서: 구현 "전"에 작성하는 필수 산출물 (Design-First 게이트) + _TEMPLATE.md 기능 설계서 템플릿 + _FN_TEMPLATE.md 함수별 설계서 템플릿 + <issue-id>-<slug>/ 기능 1개(이슈 1개)당 폴더 + README.md 기능 설계서 (전체 설계 + 함수 명세 표) + fn-<name>.md 복잡한 함수만 개별 함수 설계서 + adr/ ← Architecture Decision Records: 가로지르는 결정 기록 + _TEMPLATE.md + NNNN-<title>.md + reference/ ← 레퍼런스: 구현된 모듈/함수/설정 사양 (구현 "후" 동기화) + guides/ ← How-to / 사용 가이드 / 튜토리얼 (사용자·운영자 대상) + pipeline/ ← 개발 프로세스 문서 (큐 프로토콜·런북) +``` + +## Diátaxis 사분면 매핑 + +| 사분면 | 목적 | 여기서 위치 | +|--------|------|-------------| +| **Tutorials** (학습) | 처음 사용자가 따라하기 | `guides/` (getting-started) | +| **How-to** (문제해결) | 특정 작업 수행 | `guides/` | +| **Reference** (정보) | 정확한 사양 조회 | `reference/` | +| **Explanation** (이해) | 왜 이렇게 설계했나 | `design/`, `adr/` | + +## 문서 종류와 책임 + +| 문서 | 작성 페르소나 | 시점 | 한 줄 | +|------|---------------|------|-------| +| 기능 설계서 `design/<id>/README.md` | **Architect** | 구현 **전** | 무엇을·어떻게 만들지의 청사진 | +| 함수 설계서 `design/<id>/fn-*.md` | **Architect** | 구현 **전** | 복잡 함수의 계약·알고리즘·테스트 | +| ADR `adr/NNNN-*.md` | **Architect** | 결정 시 | 되돌리기 어려운 선택과 근거 | +| 레퍼런스 `reference/*` | **Developer/Documenter** | 구현 **후** | 실제 코드 사양 | +| 가이드 `guides/*` | **Documenter** | 릴리스 시 | 사용/운영 방법 | + +## 핵심 규칙 — Design-First (하드 게이트) + +> **설계서 없이는 코드 없음.** 어떤 함수든 구현 전에 그 함수가 설계서로 덮여 있어야 한다 +> (단순 함수: 기능 설계서의 함수 명세 표 / 복잡 함수: 개별 `fn-*.md`). +> Developer 는 설계서가 없으면 구현을 거부하고 Architect 단계로 반려한다. +> 자세한 기준은 `CLAUDE.md` §2 참조. + +## 명명 · 추적성 규칙 + +- 설계서 폴더: `design/<issue-id>-<kebab-slug>/` (예: `design/45-trailing-stop/`). +- 함수 설계서: `fn-<function_name>.md` (예: `fn-calc_trailing_stop.md`). +- ADR: 4자리 일련번호 `adr/0001-<title>.md`, 번호 재사용 금지. +- 모든 설계서·ADR 상단에 **추적성 헤더**(Redmine 이슈, 관련 ADR, 구현 파일, 테스트)를 둔다. +- 코드 ↔ 설계서 양방향 링크: 설계서는 구현 파일 경로를, 코드 주석/문서는 설계서 경로를 가리킨다. + +## 문서 수명주기 + +`Draft`(작성) → `Approved`(QA/Reviewer 통과 후) → `Superseded`(대체 시 상단 표기, 삭제 금지). +구현이 설계서와 달라지면 **코드가 아니라 설계서를 먼저 고치고** 다시 구현한다. +``` diff --git a/docs/adr/_TEMPLATE.md b/docs/adr/_TEMPLATE.md new file mode 100644 index 0000000..1211b6e --- /dev/null +++ b/docs/adr/_TEMPLATE.md @@ -0,0 +1,24 @@ +<!-- ADR 템플릿. 복사해서 adr/NNNN-<kebab-title>.md (4자리 일련번호). --> + +# ADR-NNNN: <제목> + +> **상태**: Proposed <!-- Proposed | Accepted | Superseded by ADR-XXXX --> +> **날짜**: <YYYY-MM-DD> · **결정자**: [AI] Architect · **관련 이슈**: #<id> + +## 맥락 (Context) +무엇이 이 결정을 강제하는가. 배경·제약·요구. + +## 결정 (Decision) +우리는 무엇을 하기로 했는가. (명확한 한 문단) + +## 근거 (Rationale) +왜 이 선택인가. 핵심 트레이드오프. + +## 결과 (Consequences) +- **긍정**: ... +- **부정 / 비용**: ... +- **후속 작업**: ... + +## 검토한 대안 (Alternatives Considered) +- **<대안 A>** — 기각 사유: ... +- **<대안 B>** — 기각 사유: ... diff --git a/docs/design/_FN_TEMPLATE.md b/docs/design/_FN_TEMPLATE.md new file mode 100644 index 0000000..fc2171b --- /dev/null +++ b/docs/design/_FN_TEMPLATE.md @@ -0,0 +1,51 @@ +<!-- 함수별 설계서 템플릿. 복잡 함수마다 design/<issue-id>-<slug>/fn-<function_name>.md 로 작성. + 작성: [AI] Architect, 구현 전 필수. --> + +# 함수 설계서: `<function_name>` (#<issue-id>) + +> **부모 설계서**: ./README.md · **상태**: Draft <!-- Draft|Approved|Superseded --> +> **작성**: [AI] Architect · **구현**: <file:function 또는 TBD> · **테스트**: <경로 또는 TBD> + +## 1. 시그니처 +``` +<returnType> <function_name>(<params>) # 언어 확정 후 정확히 기재 +``` + +## 2. 책임 (단일 책임, 1줄) +이 함수가 하는 단 하나의 일. + +## 3. 입력 +| 파라미터 | 타입 | 제약/검증 | 설명 | +|----------|------|-----------|------| +| `<p>` | | | | + +## 4. 출력 +- **반환**: 타입 / 의미. +- **부수효과**: (있으면 — I/O·상태변경 명시) / 없으면 **순수 함수**. + +## 5. 동작 / 알고리즘 +1. ... +2. ... + +## 6. 에러 & 실패 모드 +| 조건 | 처리 | 반환/예외 | +|------|------|-----------| +| | | | + +## 7. 엣지케이스 +- 경계값(0, 음수, 빈값, 최대), 동시성, 부분 실패. + +## 8. 복잡도 / 성능 +- 시간/공간 복잡도. 호출 빈도(예: 시세 폴링 루프 내부인가?). + +## 9. 의존성 +- 호출하는 함수/모듈, 외부 API, 설정 키. + +## 10. 테스트 케이스 +- [ ] 정상: <입력 → 기대 출력> +- [ ] 경계: ... +- [ ] 실패: ... + +## 11. 추적성 +- 인수조건: #<issue-id> 의 "<항목>". +- 관련 ADR: <ADR-NNNN 또는 없음>. diff --git a/docs/design/_TEMPLATE.md b/docs/design/_TEMPLATE.md new file mode 100644 index 0000000..9601549 --- /dev/null +++ b/docs/design/_TEMPLATE.md @@ -0,0 +1,66 @@ +<!-- 기능 설계서 템플릿. 복사해서 design/<issue-id>-<slug>/README.md 로 작성. + 작성: [AI] Architect, 구현 전 필수. 빈 섹션 금지 — 해당 없으면 "해당 없음" 명시. --> + +# 설계서: <기능명> (#<issue-id>) + +> **상태**: Draft <!-- Draft | Approved | Superseded --> +> **작성**: [AI] Architect · **최종수정**: <YYYY-MM-DD> +> **추적성** — Redmine: #<issue-id> · 관련 ADR: <ADR-NNNN 또는 없음> +> · 구현 파일: <경로 또는 TBD> · 테스트: <경로 또는 TBD> + +## 1. 목적 (Why) +이 기능이 푸는 문제. Planner 의 목표 1줄 인용. + +## 2. 범위 (Scope) +- **포함**: ... +- **제외 (out of scope)**: ... + +## 3. 인수조건 (Acceptance Criteria) +<!-- Planner 가 확정한 검증 가능한 항목. QA 가 이걸로 판정한다. --> +- [ ] ... +- [ ] ... + +## 4. 컨텍스트 & 제약 +- 의존성: 거래소 API / DB / 알림 / 외부 라이브러리. +- 제약: 성능, 레이트리밋, 리스크(돈), 보안. +- 가정: ... + +## 5. 아키텍처 개요 +- 모듈/파일 구조 (목록). +- 데이터 흐름 (텍스트 다이어그램). +- **I/O ↔ 순수 전략 로직 경계** 명시 (테스트 가능성). + +``` +<여기에 ASCII 흐름도> +``` + +## 6. 데이터 모델 +- 입력 / 출력 / 저장 구조, 타입, **경계 검증 규칙**. + +## 7. 함수 명세 (Function Specs) +<!-- 모든 함수를 나열. "복잡?" = 복잡이면 fn-<name>.md 개별 설계서 필수. --> + +| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? | +|------|-----------|----------------|------|------|-----------|-------| +| `<name>` | | | | | | 단순 / **복잡** | + +> 복잡 기준: 분기/상태기계, 외부 I/O, 리스크(주문·잔고) 경로, 비자명 알고리즘. +> → 해당 함수는 `fn-<name>.md` 작성. 단순(게터·포매터 등)은 이 표로 충분. + +## 8. 흐름 / 알고리즘 +- 핵심 시나리오 단계별. 상태 전이. + +## 9. 엣지케이스 & 에러 처리 +- 경계값, 실패 모드, 재시도/백오프. +- **안전한 기본값**(API 실패 시 거래 중단 등). + +## 10. 테스트 계획 +- 단위/통합 케이스 목록 (각 인수조건에 매핑). +- 모킹/드라이런 전략 (거래소 API 등). + +## 11. 리스크 & 대안 검토 +- 선택한 접근 vs 대안, 트레이드오프. +- 되돌리기 어려운 결정 → **ADR 로 분리** (`adr/NNNN-*.md`). + +## 12. 미해결 질문 (Open Questions) +- ... diff --git a/docs/pipeline/QUEUE-PROTOCOL.md b/docs/pipeline/QUEUE-PROTOCOL.md new file mode 100644 index 0000000..ef3e4b3 --- /dev/null +++ b/docs/pipeline/QUEUE-PROTOCOL.md @@ -0,0 +1,42 @@ +# Queue Protocol — 모든 페르소나 공통 규약 + +작업 큐 = Redmine 이슈. 각 페르소나는 자기 단계 이슈를 처리하고 git/Redmine 에 남긴 뒤 다음으로 넘긴다. + +## 0. 환경 로드 +```bash +set -a; . ./.env; set +a +RK="$REDMINE_API_KEY"; RB="$REDMINE_URL"; PROJ="$REDMINE_PROJECT" +# 카테고리 id 는 이름으로 조회(프로젝트마다 id 다름): +catid(){ curl -s -H "X-Redmine-API-Key: $RK" "$RB/projects/$PROJ/issue_categories.json" \ + | python3 -c "import sys,json;[print(c['id']) for c in json.load(sys.stdin)['issue_categories'] if c['name']=='$1']"; } +``` + +## 1. 큐 매핑 +- 현재 단계 = 카테고리 `01-Planner`…`08-Documenter`,`09-Done`. +- 수명주기 = 상태 신규(대기)/진행/완료/거절. + +## 2. 내 작업 꺼내기 +```bash +DEV=$(catid 03-Developer) +curl -s -H "X-Redmine-API-Key: $RK" "$RB/issues.json?project_id=$PROJ&category_id=$DEV&status_id=1&sort=id:asc&limit=1" +# 시작 시 상태 진행(2): +curl -s -H "X-Redmine-API-Key: $RK" -H "Content-Type: application/json" -X PUT "$RB/issues/<ID>.json" -d '{"issue":{"status_id":2}}' +``` + +## 3~4. 결과 남기기 (필수 3가지) +- (a) git 커밋+push (`[<Persona>] #<ID> ...`) +- (b) Redmine 저널 노트(역할 태그) +- (c) 다음 단계 전진: 카테고리=다음이름의 id, 상태 신규(1) +```bash +NEXT=$(catid 04-QA) +curl -s -H "X-Redmine-API-Key: $RK" -H "Content-Type: application/json" -X PUT "$RB/issues/<ID>.json" \ + -d "{\"issue\":{\"category_id\":$NEXT,\"status_id\":1,\"notes\":\"[<Persona>] ...\"}}" +``` + +## 5. 게이트 반려 +- QA(04)/Reviewer(06) 실패 → `03-Developer`. Developer 설계서 누락 → `02-Architect`. 사유를 노트에. + +## 6. 종료 (Documenter) +- `09-Done` + 상태 완료(5) + done_ratio 100. + +원칙: 자기 역할 범위만, 모든 변경 git 추적, 비밀(.env) 노출 금지. diff --git a/scripts/enqueue.sh b/scripts/enqueue.sh new file mode 100755 index 0000000..af3736e --- /dev/null +++ b/scripts/enqueue.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# 새 작업을 파이프라인 큐에 투입 = 01-Planner/신규 Redmine 이슈 생성. +# 사용법: ./scripts/enqueue.sh "제목" ["요구사항"] +set -euo pipefail +cd "$(dirname "$0")/.." +set -a; . ./.env; set +a +SUBJECT="${1:?사용법: enqueue.sh \"제목\" [\"요구\"]}"; BODY="${2:-}" +PLANNER=$(curl -s -H "X-Redmine-API-Key: $REDMINE_API_KEY" \ + "$REDMINE_URL/projects/$REDMINE_PROJECT/issue_categories.json" \ + | python3 -c "import sys,json;[print(c['id']) for c in json.load(sys.stdin)['issue_categories'] if c['name']=='01-Planner']") +DESC=$(printf '## [AI] Planner\n\n(요구사항)\n%s\n\n---\nWorking dir: %s' "$BODY" "$(pwd)") +python3 - "$REDMINE_URL" "$REDMINE_API_KEY" "$REDMINE_PROJECT" "$SUBJECT" "$DESC" "$PLANNER" <<'PY' +import sys,json,urllib.request +base,key,proj,subject,desc,cat=sys.argv[1:7] +p={"issue":{"project_id":proj,"tracker_id":2,"subject":subject,"description":desc, + "category_id":int(cat),"status_id":1}} +r=urllib.request.Request(base+"/issues.json",data=json.dumps(p).encode(), + headers={"X-Redmine-API-Key":key,"Content-Type":"application/json"},method="POST") +i=json.load(urllib.request.urlopen(r))["issue"] +print(f"enqueued #{i['id']}: {i['subject']} -> 01-Planner/신규") +PY