create-harness-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/cli.js +204 -0
  4. package/dist/detect.js +128 -0
  5. package/dist/eslintPatch.js +58 -0
  6. package/dist/manifest.js +63 -0
  7. package/dist/ponytail.js +56 -0
  8. package/dist/prompts.js +85 -0
  9. package/dist/registry.js +334 -0
  10. package/dist/render.js +43 -0
  11. package/dist/suggest.js +114 -0
  12. package/dist/types.js +1 -0
  13. package/package.json +48 -0
  14. package/templates/core/AGENTS.md +55 -0
  15. package/templates/core/CLAUDE.md +11 -0
  16. package/templates/core/conventions/00-core.md +36 -0
  17. package/templates/core/conventions/10-architecture.md +54 -0
  18. package/templates/core/conventions/20-data-fetching.md +51 -0
  19. package/templates/core/conventions/30-design-system.md +46 -0
  20. package/templates/core/conventions/40-testing.md +46 -0
  21. package/templates/core/conventions/50-auth-http.md +45 -0
  22. package/templates/core/docs/architecture.md +21 -0
  23. package/templates/core/docs/decisions.md +16 -0
  24. package/templates/core/docs/product-spec.md +22 -0
  25. package/templates/core/docs/specs/_template.md +33 -0
  26. package/templates/core/docs/task-log.md +4 -0
  27. package/templates/core/gates/claude-settings.json +16 -0
  28. package/templates/core/gates/cursor-hooks.json +10 -0
  29. package/templates/core/gates/gate.mjs +115 -0
  30. package/templates/core/gates/pre-commit-gate.sh +7 -0
  31. package/templates/core/gates/run-checks.mjs +39 -0
  32. package/templates/core/workflows/ds-add.md +28 -0
  33. package/templates/core/workflows/ds-init.md +59 -0
  34. package/templates/core/workflows/impl.md +26 -0
  35. package/templates/core/workflows/ship.md +31 -0
  36. package/templates/core/workflows/spec.md +26 -0
  37. package/templates/core/workflows/verify.md +27 -0
  38. package/templates/presets/react-fe/configs/commitlint.config.js +36 -0
  39. package/templates/presets/react-fe/configs/eslint.harness.config.js +104 -0
  40. package/templates/presets/react-fe/configs/prettier.config.js +9 -0
  41. package/templates/presets/react-fe/design-system/_story-template.tsx +56 -0
  42. package/templates/presets/react-fe/design-system/stylelint.config.js +78 -0
  43. package/templates/presets/react-fe/design-system/tokens.css +57 -0
  44. package/templates/presets/react-fe/design-system/tokens.ts +40 -0
  45. package/templates/presets/react-fe/reference/auth-http/ProtectedRoute.tsx +62 -0
  46. package/templates/presets/react-fe/reference/auth-http/axiosInstance.ts +103 -0
  47. package/templates/presets/react-fe/reference/data-fetching/alertDialogStore.ts +30 -0
  48. package/templates/presets/react-fe/reference/data-fetching/api.ts +11 -0
  49. package/templates/presets/react-fe/reference/data-fetching/exampleApi.ts +46 -0
  50. package/templates/presets/react-fe/reference/data-fetching/exampleQueryKeys.ts +14 -0
  51. package/templates/presets/react-fe/reference/data-fetching/index.ts +25 -0
@@ -0,0 +1,11 @@
1
+ @AGENTS.md
2
+
3
+ <!-- 정본은 AGENTS.md 하나다. 이 파일에는 Claude Code 전용 보충만 적는다. -->
4
+
5
+ ## Claude Code 보충
6
+
7
+ - 상세 컨벤션은 `{{RULES_DIR}}/` 아래 파일을 해당 영역 작업 전에 읽는다:
8
+ - `00-core` 항상 / `10-architecture` src 전반 / `20-data-fetching` queries·hooks·api
9
+ - `30-design-system` tsx·css / `40-testing` 테스트·스토리 / `50-auth-http` 인증·HTTP
10
+ - 워크플로는 `.claude/skills/` 의 spec / impl / verify / ship / design-system 스킬로 제공된다.
11
+ - 커밋 게이트는 `.claude/settings.json` 의 PreToolUse 훅으로 걸려 있다. 우회하지 않는다.
@@ -0,0 +1,36 @@
1
+ ---
2
+ description: 항상 적용되는 핵심 규칙 — 검증 게이트, 금지 사항, TODO 정책
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # 핵심 규칙
7
+
8
+ ## 작업 순서
9
+
10
+ 1. 새 기능은 `/spec` 으로 명세부터 쓴다. 수용 기준 없는 구현 착수 금지.
11
+ 2. 구현은 실패하는 테스트부터 (Red → Green → Refactor).
12
+ 3. 커밋 전 `node .harness/gates/run-checks.mjs` 통과 필수. 게이트가 강제한다.
13
+
14
+ ## 절대 금지
15
+
16
+ - `any` 타입 — `unknown` + 타입 좁히기를 쓴다
17
+ - `git commit --no-verify`, `git push --force`
18
+ - `.env*` 파일 커밋
19
+ - 테스트를 통과시키기 위한 단정문 약화 — 실패하면 코드를 고친다
20
+ - `console.log` 를 커밋에 포함 (디버깅 후 제거)
21
+
22
+ ## TODO 정책 (남발 금지)
23
+
24
+ 새 TODO를 `docs/product-spec.md` 에 추가하는 것은 다음을 **모두** 만족할 때만:
25
+
26
+ 1. 사용자가 명시적으로 요청했거나, 현재 작업의 직접적인 후속 조치다
27
+ 2. 지금 하지 않으면 버그·보안 문제로 이어진다
28
+ 3. 기존 TODO와 중복되지 않는다
29
+
30
+ "나중에 개선하면 좋을 것 같은" 아이디어는 TODO가 아니다. 필요하면 `docs/decisions.md` 의
31
+ 논의 섹션에 적고 사용자 판단에 맡긴다.
32
+
33
+ ## 커밋
34
+
35
+ - Conventional Commits (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`)
36
+ - 하나의 커밋은 하나의 논리적 변경만
@@ -0,0 +1,54 @@
1
+ ---
2
+ description: 레이어 구조, 단방향 의존, 공개 API 경계, 파일·명명 규칙
3
+ globs: src/**/*.{ts,tsx}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # 아키텍처
8
+
9
+ ## 레이어 (위에서 아래로만 의존)
10
+
11
+ ```
12
+ routes / components ← 페이지·UI. 조립만 한다
13
+
14
+ hooks ← 화면 상태·데이터 오케스트레이션 (커스텀 훅)
15
+
16
+ queries | stores ← 서버 상태(TanStack Query) | 클라이언트 상태(Zustand)
17
+
18
+ api / utils ← 원시 HTTP 호출, axiosInstance
19
+
20
+ mappers → types ← 응답 → 도메인 타입 변환, 전역 타입
21
+ ```
22
+
23
+ - **역방향 import 금지**: api 레이어가 hooks를 import하면 안 된다.
24
+ - **라우트 컴포넌트에 비즈니스 로직 금지**: fetch 호출·데이터 가공은 hooks/queries로 내린다.
25
+ 라우트는 훅을 호출하고 컴포넌트를 조립만 한다.
26
+
27
+ ## 공개 API 경계
28
+
29
+ - 기능 폴더(`src/queries/{Domain}/`, `src/components/{Domain}/`)는 `index.ts` 로만 import한다.
30
+ - 폴더 내부 파일로의 deep import 금지 — ESLint `no-restricted-paths` 가 error 처리한다.
31
+
32
+ ```ts
33
+ // 좋음
34
+ import { useExampleListQuery } from '../../queries/Example'
35
+ // 나쁨 — 내부 구현에 결합됨
36
+ import { fetchExampleList } from '../../queries/Example/exampleApi'
37
+ ```
38
+
39
+ ## 명명 규칙 (ESLint가 강제)
40
+
41
+ | 대상 | 규칙 | 예 |
42
+ |------|------|----|
43
+ | 인터페이스 | `I` 접두 + PascalCase | `IApiResponse`, `IUserInfo` |
44
+ | 타입 파라미터 | `T` 접두 | `<TData>`, `<TItem>` |
45
+ | boolean 변수·prop | `is/has/should/can/must/was/will` 접두 | `isLoading`, `hasError` |
46
+ | 컴포넌트 파일 | PascalCase | `ProtectedRoute.tsx` |
47
+ | 훅 | `use` 접두 camelCase | `useExampleListQuery` |
48
+ | 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
49
+
50
+ ## 폴더 구성
51
+
52
+ - 도메인별 하위 폴더: `components/Chat/`, `hooks/chat/`, `queries/Chat/`
53
+ - 공용은 `shared/`: `components/shared/`, `hooks/shared/`, `stores/shared/`
54
+ - 스타일은 컴포넌트 옆 `*.module.css` (CSS Modules)
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: 상태 4분류, TanStack Query 3계층 패턴, queryKey 규칙
3
+ globs: src/{queries,hooks,api,stores}/**/*.{ts,tsx}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # 데이터와 상태
8
+
9
+ ## 상태 4분류 — 저장 위치를 먼저 정한다
10
+
11
+ | 종류 | 저장소 | 예 |
12
+ |------|--------|----|
13
+ | 서버 캐시 | TanStack Query | 목록·상세 응답, 유저 정보 |
14
+ | 애플리케이션 상태 | Zustand (`stores/`) | 전역 모달 열림, 토스트 |
15
+ | UI 상태 | `useState` (컴포넌트 로컬) | 입력값, 아코디언 펼침 |
16
+ | URL 상태 | 라우터 (searchParams) | 필터, 페이지네이션, 탭 |
17
+
18
+ - **서버 캐시를 전역 스토어에 복사 금지.** Query 캐시가 정본이다. 복사하는 순간 동기화 버그가 시작된다.
19
+ - **필터·페이지네이션은 URL에.** 새로고침·공유·뒤로가기가 공짜로 동작한다.
20
+
21
+ ## 3계층 데이터 패턴
22
+
23
+ ```
24
+ queries/{Domain}/
25
+ ├── exampleApi.ts # 1) 원시 호출: axiosInstance 사용, IApiResponse<T> 반환
26
+ ├── exampleQueryKeys.ts # 2) queryKey 팩토리: 계층적 키
27
+ └── index.ts # 3) 공개 API: useQuery 훅만 export
28
+ ```
29
+
30
+ - 컴포넌트는 `queries/{Domain}` 의 훅만 쓴다. `axiosInstance` 직접 호출 금지.
31
+ - 화면 조합 로직(여러 쿼리 결합, 파생 상태)은 `hooks/{domain}/use*.ts` 에 둔다.
32
+
33
+ ## queryKey 규칙
34
+
35
+ ```ts
36
+ export const exampleQueryKeys = {
37
+ all: ['example'] as const,
38
+ lists: () => [...exampleQueryKeys.all, 'list'] as const,
39
+ list: (filters: IExampleFilters) =>
40
+ [...exampleQueryKeys.lists(), filters] as const,
41
+ detail: (id: string) => [...exampleQueryKeys.all, 'detail', id] as const,
42
+ }
43
+ ```
44
+
45
+ - 무효화는 상위 키로: `invalidateQueries({ queryKey: exampleQueryKeys.lists() })`
46
+ - 키에 들어가는 객체는 직렬화 가능해야 한다 (함수·클래스 인스턴스 금지)
47
+
48
+ ## 응답 래퍼
49
+
50
+ 모든 API 응답은 `IApiResponse<T>` (`src/types/api.ts`)를 통과한다.
51
+ 컴포넌트에 백엔드 원시 응답 형태가 새어나가지 않게 mappers에서 도메인 타입으로 변환한다.
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: 디자인 토큰 강제, 컴포넌트 선행 추가 규칙, Storybook 온디맨드 설치
3
+ globs: src/**/*.{tsx,css}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # 디자인시스템
8
+
9
+ ## 토큰은 닫힌 집합이다
10
+
11
+ - 색상은 **반드시** `src/design-system/tokens.css` 의 CSS 변수만 쓴다.
12
+ 원시값(`#hex`, `rgb()`, 색상 키워드)은 stylelint가 error 처리한다.
13
+ {{#if STYLELINT_BASELINE}}- 하네스 도입 전부터 원시값을 쓰던 CSS {{CSS_RAW_COLOR_FILES}}개는
14
+ `.harness/stylelint-baseline.json` 에 유예 목록으로 올라가 warning 으로만 뜬다.
15
+ **새로 만드는 파일은 유예 대상이 아니며 error다.** 유예 파일을 손볼 일이 생기면
16
+ 그 김에 원시값을 토큰으로 바꾸고 목록에서 경로를 지운다.
17
+ {{/if}}
18
+ - **존재하지 않는 토큰 이름을 발명하지 않는다.** 필요한 토큰이 없으면
19
+ 먼저 `tokens.css` 와 `tokens.ts` 양쪽에 추가하고 나서 쓴다.
20
+ - TS/JSX에서 토큰 값이 필요하면 `tokens.ts` 의 타입드 상수를 import한다 (문자열 하드코딩 금지).
21
+
22
+ ```css
23
+ /* 좋음 */
24
+ .card { background: var(--color-surface); border-radius: var(--radius-md); }
25
+ /* 나쁨 — stylelint error */
26
+ .card { background: #ffffff; border-radius: 8px; }
27
+ ```
28
+
29
+ - stylelint는 **색상만** 검사한다. 간격·타이포 토큰이 채워지면
30
+ `stylelint.config.js` 의 주석 처리된 속성을 켠다.
31
+
32
+ ## UI 작업 절차 — 컴포넌트가 레이아웃보다 먼저
33
+
34
+ 페이지·레이아웃 작업 지시를 받으면:
35
+
36
+ 1. `src/design-system/components/` 에 필요한 컴포넌트가 이미 있는지 확인
37
+ 2. 없으면 **레이아웃 착수 전에** 컴포넌트 + `*.stories.tsx`(play 함수 포함)를 먼저 추가 — `/ds-add` 워크플로
38
+ 3. Storybook이 아직 설치되지 않았다면 `/ds-init` 부터 실행
39
+
40
+ 이 순서를 건너뛰고 페이지 안에 일회성 스타일을 쌓으면 UI가 화면마다 어긋난다(드리프트).
41
+
42
+ ## 기존 컴포넌트 우선
43
+
44
+ - 새 버튼·입력·모달을 만들기 전에 `src/design-system/components/` 와
45
+ `src/components/shared/` 를 먼저 조회한다. 비슷한 것이 있으면 variant를 추가하지 새로 만들지 않는다.
46
+ - 스토리 작성 형식은 `src/design-system/_story-template.tsx` 를 따른다.
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: 테스트 우선 개발, 단정문 약화 금지, 스토리 play 함수 규칙
3
+ globs: **/*.{test,spec,stories}.{ts,tsx}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # 테스트
8
+
9
+ ## 순서가 규칙이다
10
+
11
+ - 구현 전에 **실패하는 테스트**를 먼저 쓴다 (Red → Green → Refactor).
12
+ - 명세(`docs/specs/*.md`)의 수용 기준 하나 = 테스트 하나. 매핑이 안 되는 수용 기준은 명세가 모호한 것이다.
13
+
14
+ ## 단정문 약화 금지
15
+
16
+ 테스트가 실패할 때 허용되는 행동은 두 가지뿐이다:
17
+
18
+ 1. 코드를 고친다 (대부분 이쪽)
19
+ 2. 요구사항이 바뀌었음을 확인하고 테스트를 **의도적으로** 수정한다 — 커밋 메시지에 이유를 남긴다
20
+
21
+ `expect(x).toBe(3)` 이 실패한다고 `expect(x).toBeGreaterThan(0)` 으로 바꾸는 것,
22
+ `it.skip`, 빈 catch로 감싸기는 모두 금지다. `/ship` 단계에서 단정문 약화 여부를 검토한다.
23
+
24
+ ## 무엇을 테스트하나
25
+
26
+ | 대상 | 도구 | 기준 |
27
+ |------|------|------|
28
+ | 유틸·mapper·순수 로직 | Vitest 단위 테스트 | 경계값·에러 케이스 포함 |
29
+ | 훅 | Vitest + Testing Library `renderHook` | 로딩·성공·실패 상태 |
30
+ | 디자인시스템 컴포넌트 | 스토리 + `play` 함수 | 상호작용·포커스 관리 |
31
+ | 컴포넌트 동작 | Testing Library | 사용자 관점 쿼리 (`getByRole` 우선) |
32
+
33
+ - 구현 세부(내부 state, 호출 횟수)가 아니라 **동작**을 테스트한다.
34
+ - 스냅샷 테스트는 의도적으로 검토 가능한 작은 단위에만 쓴다.
35
+
36
+ ## 스토리 play 함수
37
+
38
+ 스크린샷·시각 검증으로 못 잡는 것(포커스 이동, 키보드 조작, aria 상태)을 play 함수로 잡는다.
39
+
40
+ ```tsx
41
+ play: async ({ canvas, userEvent }) => {
42
+ const button = canvas.getByRole('button', { name: '저장' })
43
+ await userEvent.click(button)
44
+ await expect(canvas.getByRole('status')).toHaveTextContent('저장됨')
45
+ }
46
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ description: axios 단일 인스턴스, 토큰 처리, 401 처리, ProtectedRoute 가드
3
+ globs: src/utils/**/*.ts,src/**/auth/**/*.{ts,tsx},src/components/shared/ProtectedRoute.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # 인증과 HTTP
8
+
9
+ ## 단일 axios 인스턴스
10
+
11
+ - 모든 HTTP 호출은 `src/utils/axiosInstance.ts` 의 `axiosInstance` 를 쓴다.
12
+ `axios.get(...)` 이나 새 인스턴스 생성 금지 — 인터셉터를 우회하게 된다.
13
+ - (예외: SSE 스트리밍은 `fetch` 기반 별도 레이어를 쓴다)
14
+
15
+ ## 토큰 규칙
16
+
17
+ - access token은 **메모리에만** 둔다 (`setAccessToken`/`getAccessToken`).
18
+ `localStorage`·`sessionStorage` 저장 금지 — XSS에 그대로 노출된다.
19
+ - refresh token은 HttpOnly 쿠키. 클라이언트 코드가 읽을 수 없고, 읽으려 하지 않는다.
20
+ - 요청 인터셉터가 `Authorization: Bearer` 를 자동 첨부한다. 개별 호출에서 헤더 수동 설정 금지.
21
+
22
+ ## 401 처리
23
+
24
+ - refresh 요청은 `refreshPromise` 로 중복 제거한다 — 동시에 만료된 요청 10개가
25
+ refresh를 10번 부르면 안 된다.
26
+ - refresh 까지 실패하면 `window.dispatchEvent(new CustomEvent('auth:logout'))` 로
27
+ 이벤트만 발행한다. 인터셉터가 라우터를 직접 조작하지 않는다 (레이어 역전 금지).
28
+ 로그아웃 처리와 리다이렉트는 이 이벤트를 구독하는 인증 훅의 책임이다.
29
+
30
+ ## 라우트 보호
31
+
32
+ - 로그인 필요 페이지는 `ProtectedRoute` 로 감싼다. 페이지 컴포넌트 안에서
33
+ `if (!user) navigate('/login')` 하지 않는다.
34
+ - 역할 제한은 `requiredRoles` prop으로:
35
+
36
+ ```tsx
37
+ <ProtectedRoute requiredRoles={['ROLE_ADMIN']}>
38
+ <Admin />
39
+ </ProtectedRoute>
40
+ ```
41
+
42
+ ## 환경 변수
43
+
44
+ - API 주소 등은 `import.meta.env.VITE_*` 로만 접근하고 `.env.example` 에 항목을 유지한다.
45
+ - 시크릿(키·비밀번호)은 프론트엔드 번들에 절대 넣지 않는다. `VITE_` 접두가 붙는 순간 공개다.
@@ -0,0 +1,21 @@
1
+ # 아키텍처 — {{PROJECT_NAME}}
2
+
3
+ > 구조가 바뀔 때마다 갱신한다. 에이전트는 큰 작업 전에 이 문서를 읽는다.
4
+
5
+ ## 시스템 개요
6
+
7
+ <!-- TODO: 프론트엔드가 어떤 백엔드/외부 시스템과 어떻게 통신하는지 다이어그램 또는 목록 -->
8
+
9
+ ## 레이어
10
+
11
+ `{{RULES_DIR}}/10-architecture` 의 레이어 규칙을 따른다. 이 프로젝트 고유의 예외나 보충이 있으면 여기 적는다.
12
+
13
+ ## 주요 디렉터리
14
+
15
+ <!-- TODO: src/ 2-depth 트리와 각 폴더 한 줄 설명 -->
16
+
17
+ ## 외부 의존성
18
+
19
+ | 의존성 | 용도 | 도입 이유 |
20
+ |--------|------|-----------|
21
+ | | | |
@@ -0,0 +1,16 @@
1
+ # 결정 기록 — {{PROJECT_NAME}}
2
+
3
+ > 기술적 결정을 내릴 때마다 **근거와 함께** 기록한다. 결론만 적으면 몇 주 뒤 같은 논쟁을 반복한다.
4
+ > 형식: 최신이 위. 번복된 결정은 지우지 말고 취소선 + 번복 사유.
5
+
6
+ ## 결정
7
+
8
+ ### YYYY-MM-DD 예시 — (첫 결정을 여기에)
9
+
10
+ - **결정**:
11
+ - **대안**:
12
+ - **근거**:
13
+
14
+ ## 논의 중 (아직 결정 아님)
15
+
16
+ <!-- 확정 안 된 아이디어는 여기. TODO로 바로 승격하지 않는다 -->
@@ -0,0 +1,22 @@
1
+ # 프로덕트 명세 — {{PROJECT_NAME}}
2
+
3
+ > 이 서비스가 무엇인지, 남은 일이 무엇인지의 정본. 에이전트가 기능을 제안·구현할 때 기준이 된다.
4
+
5
+ ## 서비스 개요
6
+
7
+ <!-- TODO: 누가, 무엇을 위해 쓰는 서비스인지 -->
8
+
9
+ ## 완료된 기능
10
+
11
+ <!-- 기능이 /ship 되면 여기로 옮긴다 -->
12
+
13
+ ## TODO
14
+
15
+ > 새 항목 추가 기준(`{{RULES_DIR}}/00-core` TODO 정책): 명시적 요청이거나 직접 후속 조치,
16
+ > 방치 시 버그·보안 문제, 기존 항목과 중복 없음 — 셋 다 만족할 때만.
17
+
18
+ - [ ]
19
+
20
+ ## 하지 않기로 한 것
21
+
22
+ <!-- 범위 밖으로 결정한 것과 이유. "왜 없어요?"에 답하는 섹션 -->
@@ -0,0 +1,33 @@
1
+ # 명세: <기능 이름>
2
+
3
+ - 작성일: YYYY-MM-DD
4
+ - 상태: 초안 | 확정 | 구현됨
5
+
6
+ ## 목표
7
+
8
+ <!-- 이 기능이 해결하는 문제 한 문단. "무엇을 만든다"가 아니라 "무엇이 문제다" -->
9
+
10
+ ## 범위 밖
11
+
12
+ <!-- 이번에 하지 않는 것. 스코프 크리프 방지에 가장 중요한 섹션 -->
13
+
14
+ -
15
+
16
+ ## 수용 기준
17
+
18
+ <!-- 각 항목은 테스트 하나로 번역 가능한 문장이어야 한다.
19
+ 좋음: "빈 목록이면 '데이터 없음' 문구가 보인다"
20
+ 나쁨: "UX가 좋다", "잘 동작한다" -->
21
+
22
+ - [ ] AC1:
23
+ - [ ] AC2:
24
+
25
+ ## 영향 범위
26
+
27
+ - 만질 파일:
28
+ - 새 의존성:
29
+ - 기존 기능 영향:
30
+
31
+ ## 열린 질문
32
+
33
+ <!-- 구현 전에 사용자에게 확인받아야 하는 것 -->
@@ -0,0 +1,4 @@
1
+ # 작업 로그 — {{PROJECT_NAME}}
2
+
3
+ > `/ship` 시 맨 위에 한 줄씩 추가된다. 형식: `- YYYY-MM-DD <해시 7자> <요약>`
4
+
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.harness/gates/pre-commit-gate.sh claude",
10
+ "timeout": 600
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "beforeShellExecution": [
5
+ {
6
+ "command": "./.harness/gates/pre-commit-gate.sh cursor"
7
+ }
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable */
3
+ // ↑ Node 인프라 스크립트 — 브라우저 전용 eslint 설정(no-undef: process 등)에 걸리지 않게 한다.
4
+ /**
5
+ * 커밋 게이트 본체. Cursor(beforeShellExecution)와 Claude Code(PreToolUse) 훅이
6
+ * pre-commit-gate.sh 를 통해 같은 이 스크립트를 부른다.
7
+ *
8
+ * 정책:
9
+ * - `git commit` 감지 시 → --no-verify 거부, .env 스테이징 거부, checks 실패 시 거부
10
+ * - `git push --force`/-f 거부 (--force-with-lease 는 허용)
11
+ *
12
+ * 사용: gate.mjs <cursor|claude> (훅 입력 JSON은 stdin)
13
+ */
14
+ import { execSync, spawnSync } from 'node:child_process'
15
+ import { readFileSync } from 'node:fs'
16
+ import path from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const tool = process.argv[2] === 'claude' ? 'claude' : 'cursor'
20
+ const gatesDir = path.dirname(fileURLToPath(import.meta.url))
21
+ const projectRoot = path.resolve(gatesDir, '..', '..')
22
+
23
+ const respond = (decision, reason) => {
24
+ if (tool === 'claude') {
25
+ console.log(
26
+ JSON.stringify({
27
+ hookSpecificOutput: {
28
+ hookEventName: 'PreToolUse',
29
+ permissionDecision: decision,
30
+ permissionDecisionReason: reason ?? '',
31
+ },
32
+ }),
33
+ )
34
+ } else {
35
+ console.log(
36
+ JSON.stringify(
37
+ decision === 'deny'
38
+ ? { permission: 'deny', userMessage: reason, agentMessage: reason }
39
+ : { permission: 'allow' },
40
+ ),
41
+ )
42
+ }
43
+ process.exit(0)
44
+ }
45
+
46
+ let command = ''
47
+ try {
48
+ const raw = readFileSync(0, 'utf-8')
49
+ const input = raw.trim() ? JSON.parse(raw) : {}
50
+ command =
51
+ tool === 'claude'
52
+ ? (input.tool_input && input.tool_input.command) || ''
53
+ : input.command || ''
54
+ } catch {
55
+ // 입력을 못 읽으면 판단 불가 — 무관한 명령을 막지 않도록 허용
56
+ respond('allow')
57
+ }
58
+
59
+ const isGitCommit = /\bgit\b[^&|;]*\bcommit\b/.test(command)
60
+ const isGitPush = /\bgit\b[^&|;]*\bpush\b/.test(command)
61
+
62
+ if (isGitPush) {
63
+ const stripped = command.replace(/--force-with-lease(=\S+)?/g, '')
64
+ if (/(\s--force\b|\s-f\b)/.test(stripped)) {
65
+ respond(
66
+ 'deny',
67
+ 'force push는 금지되어 있습니다. 필요하면 --force-with-lease 를 사전 협의 후 사용하세요.',
68
+ )
69
+ }
70
+ }
71
+
72
+ if (isGitCommit) {
73
+ if (/--no-verify\b|\s-n\b/.test(command)) {
74
+ respond('deny', 'git commit --no-verify 는 게이트 우회이므로 금지입니다.')
75
+ }
76
+
77
+ let staged = ''
78
+ try {
79
+ staged = execSync('git diff --cached --name-only', {
80
+ cwd: projectRoot,
81
+ encoding: 'utf-8',
82
+ })
83
+ } catch {
84
+ // git 저장소가 아니면 이후 검사만 진행
85
+ }
86
+ const stagedEnv = staged
87
+ .split('\n')
88
+ .filter((file) => /(^|\/)\.env(\.\w+)?$/.test(file))
89
+ .filter((file) => !file.endsWith('.env.example'))
90
+ if (stagedEnv.length > 0) {
91
+ respond(
92
+ 'deny',
93
+ `.env 파일이 스테이징되어 있습니다: ${stagedEnv.join(', ')} — 시크릿 커밋 금지.`,
94
+ )
95
+ }
96
+
97
+ const result = spawnSync(
98
+ process.execPath,
99
+ [path.join(gatesDir, 'run-checks.mjs')],
100
+ { cwd: projectRoot, encoding: 'utf-8' },
101
+ )
102
+ if (result.status !== 0) {
103
+ const tail = `${result.stdout ?? ''}${result.stderr ?? ''}`
104
+ .split('\n')
105
+ .filter(Boolean)
106
+ .slice(-15)
107
+ .join('\n')
108
+ respond(
109
+ 'deny',
110
+ `커밋 전 검증(checks)이 실패했습니다. 고친 뒤 다시 커밋하세요.\n${tail}`,
111
+ )
112
+ }
113
+ }
114
+
115
+ respond('allow')
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # 두 도구(Cursor·Claude Code)가 공유하는 커밋 게이트 진입점.
3
+ # 훅 입력 JSON은 stdin으로 들어오고, $1 로 어느 도구인지 받는다.
4
+ set -euo pipefail
5
+
6
+ GATE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ exec node "$GATE_DIR/gate.mjs" "${1:-cursor}"
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable */
3
+ // ↑ Node 인프라 스크립트 — 브라우저 전용 eslint 설정(no-undef: process 등)에 걸리지 않게 한다.
4
+ /**
5
+ * .harness/config.json 의 checks 를 순서대로 실행한다.
6
+ * /verify 워크플로와 커밋 게이트가 같은 이 스크립트를 부르므로
7
+ * 로컬·에이전트·CI의 판정 기준이 하나로 유지된다.
8
+ */
9
+ import { spawnSync } from 'node:child_process'
10
+ import { readFileSync } from 'node:fs'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ const gatesDir = path.dirname(fileURLToPath(import.meta.url))
15
+ const projectRoot = path.resolve(gatesDir, '..', '..')
16
+ const configPath = path.resolve(gatesDir, '..', 'config.json')
17
+
18
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'))
19
+ const checks = Array.isArray(config.checks) ? config.checks : []
20
+
21
+ if (checks.length === 0) {
22
+ console.log('checks 가 비어 있습니다 (.harness/config.json)')
23
+ process.exit(0)
24
+ }
25
+
26
+ for (const check of checks) {
27
+ console.log(`\n▶ ${check.id}: ${check.command}`)
28
+ const result = spawnSync(check.command, {
29
+ cwd: projectRoot,
30
+ shell: true,
31
+ stdio: 'inherit',
32
+ })
33
+ if (result.status !== 0) {
34
+ console.error(`\n✗ ${check.id} 실패 (exit ${result.status})`)
35
+ process.exit(result.status ?? 1)
36
+ }
37
+ }
38
+
39
+ console.log(`\n✓ 모든 체크 통과 (${checks.map((check) => check.id).join(' → ')})`)
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: Add a design-system component with story and play function BEFORE layout work.
3
+ ---
4
+
5
+ # /ds-add — 레이아웃 전에 컴포넌트부터
6
+
7
+ UI 작업 지시를 받았을 때, 페이지 레이아웃에 착수하기 **전에** 실행하는 절차다.
8
+
9
+ ## 절차
10
+
11
+ 1. **재고 조사**: `src/design-system/components/` 와 `src/components/shared/` 를 조회한다.
12
+ - 필요한 컴포넌트가 이미 있으면 그대로 쓴다. 끝.
13
+ - 비슷한 것이 있으면 variant/prop 추가를 우선 검토한다. 복제 금지.
14
+ 2. **Storybook 확인**: `.storybook/` 이 없으면 먼저 `/ds-init` 을 실행한다.
15
+ 3. **컴포넌트 작성**: `src/design-system/components/<Name>/` 에:
16
+ - `<Name>.tsx` — 토큰만 사용 (`tokens.css` 변수·`tokens.ts` 상수), 원시 색상값 금지
17
+ - `<Name>.module.css`
18
+ - `<Name>.stories.tsx` — `src/design-system/_story-template.tsx` 형식을 따르고 **play 함수 필수**:
19
+ 주요 상호작용(클릭·입력)과 포커스·aria 상태를 단정한다
20
+ - `index.ts` — 공개 API
21
+ 4. **검증**: 스토리 테스트와 stylelint 통과 확인.
22
+ 5. 이제 페이지 레이아웃 작업에 착수한다. 페이지에서는 방금 만든 컴포넌트를 조립만 한다.
23
+
24
+ ## 금지
25
+
26
+ - 페이지 파일 안에 일회성 버튼·인풋 스타일 작성 (드리프트의 시작)
27
+ - 스토리 없는 디자인시스템 컴포넌트
28
+ - 토큰에 없는 색·간격을 쓰기 위해 인라인 style로 우회