Skip to content

Commit 58d669b

Browse files
serithemageclaude
andcommitted
Delegate CLAUDE.md to claude /init, add anti-reinvention rules
CLAUDE.md generation: - Delegates to `claude /init` (headless) instead of custom generation - Appends ROBOCO context via `<roboco>` tags - Fallback to template if /init fails or doesn't produce file - Verifies file existence after /init before declaring success Anti-reinvention: - CLAUDE.md: added Anti-Reinvention Rule section - PRD: added "How" column to all setup domain tables - Removed unused prompt utils (select, input) — Agent SDK handles dialogue - Fixed all lint warnings (unused imports/params) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 17335de commit 58d669b

8 files changed

Lines changed: 153 additions & 110 deletions

File tree

‎CLAUDE.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,19 @@ src/
6767
- `init` = first-time setup with interview, `install` = team member applies existing config
6868
- `doctor` checks ROBOCO CLI itself, `status` checks target repo setup
6969
- No `lint` command — static validation of vibe coding "correctness" is impractical
70+
- CLAUDE.md generation delegates to `claude /init`, not custom implementation
71+
72+
## Anti-Reinvention Rule
73+
74+
Before implementing any feature, check if it already exists in:
75+
1. **Claude Code built-in** — `/init`, `/compact`, `/review`, built-in tools (Read, Write, Glob, Grep, Bash)
76+
2. **Claude Code SDK / Agent SDK** — `query()`, hooks, permissions, MCP
77+
3. **OMC** — agents, skills, workflows (ralph, autopilot, ultrawork)
78+
4. **npm ecosystem** — established packages for common tasks
79+
80+
If existing functionality covers 80%+ of the need, **wrap/delegate** instead of reimplementing.
81+
Concrete examples:
82+
- CLAUDE.md generation → `claude /init` (headless) + `<roboco>` tag append
83+
- AI conversation → `@anthropic-ai/claude-agent-sdk` `query()`, not raw API
84+
- MCP server install → `claude mcp add`, not custom installer
85+
- Git hooks → `husky`, not custom scripts

‎docs/PRD.md‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,33 @@ npx roboco init [path]
3737

3838
### 4.1 Claude Code 환경 (필수)
3939

40-
| 산출물 | 설명 |
41-
|--------|------|
42-
| `CLAUDE.md` | 리포 특성에 맞는 AI 맞춤 컨텍스트 문서 (커뮤니티 표준 구조 적용) |
43-
| `.claude/` | settings, commands, skills 디렉토리 구성 |
44-
| Claude Code Hooks | `.claude/settings.json`에 스택별 hooks 템플릿 자동 생성 (lint/format/typecheck 자동화) |
45-
| MCP 서버 설정 | 필요한 MCP 서버 자동 구성 |
46-
| oh-my-claudecode (OMC) | 통째 설치 (dependency) |
40+
| 산출물 | 설명 | How (구현 방법론) |
41+
|--------|------|-----------------|
42+
| `CLAUDE.md` | 리포 특성에 맞는 AI 맞춤 컨텍스트 문서 | `claude /init` (headless)로 생성 → `<roboco>` 태그로 ROBOCO 컨텍스트 append. 직접 생성 금지 |
43+
| `.claude/settings.json` | 권한, hooks 설정 | JSON 직접 생성 (ROBOCO 고유 스키마) |
44+
| Claude Code Hooks | 스택별 lint/format 자동화 | 스택 감지 결과 기반으로 hooks 템플릿 렌더링 (TS→prettier, Py→black, Go→gofmt) |
45+
| MCP 서버 설정 | 필요한 MCP 서버 자동 구성 | `claude mcp add` CLI 위임. 직접 설정 파일 쓰기 금지 |
46+
| oh-my-claudecode (OMC) | 멀티에이전트 오케스트레이션 | `claude plugin install` CLI 위임 |
4747

4848
### 4.2 프로세스 문서 (선택)
4949

50-
| 산출물 | 설명 |
51-
|--------|------|
52-
| 바이브 코딩 5단계 템플릿 | 의도전달 → 요건정의 → 조사 → 계획 → 구현 |
53-
| OpenSpec | 프로세스 문서화 프레임워크 |
50+
| 산출물 | 설명 | How |
51+
|--------|------|-----|
52+
| 바이브 코딩 5단계 템플릿 | 의도전달 → 요건정의 → 조사 → 계획 → 구현 | 정적 마크다운 템플릿 복사 (ROBOCO 고유 콘텐츠) |
53+
| OpenSpec | 프로세스 문서화 프레임워크 | openspec CLI/config 위임 |
5454

5555
각 단계별로 문서를 남기고 언제든지 원하는 단계부터 다시 시작할 수 있어야 한다.
5656

5757
### 4.3 CI/CD 파이프라인 (선택)
5858

59-
| 산출물 | 설명 |
60-
|--------|------|
61-
| pre-commit hooks | 바이브 코딩 규칙 강제 |
62-
| GitHub Actions 워크플로우 | CI에서 규칙 검증 자동화 |
59+
| 산출물 | 설명 | How |
60+
|--------|------|-----|
61+
| pre-commit hooks | 바이브 코딩 규칙 강제 | `husky` 위임. `.husky/pre-commit` 생성, 직접 .git/hooks 조작 금지 |
62+
| GitHub Actions 워크플로우 | CI에서 규칙 검증 자동화 | 정적 YAML 템플릿 복사 (ROBOCO 고유 워크플로우) |
6363

6464
### 4.4 스택 분석 및 최적화 (자동)
6565

66-
- 리포의 기술 스택 자동 감지
66+
- 리포의 기술 스택 자동 감지 (ROBOCO 고유 기능 — package.json, pyproject.toml, go.mod 등 파싱)
6767
- 언어 비의존 공통 설정 기반으로 스택별 최적화된 설정 제공
6868

6969
## 5. 명령어

‎src/commands/add.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { resolve } from 'node:path';
22
import ora from 'ora';
33
import { fileExists, readJson, writeJson } from '../utils/fs.js';
4-
import { installTools, printInstallReport } from '../core/installer.js';
4+
import { installTools } from '../core/installer.js';
55
import { logger } from '../utils/logger.js';
66
import type { RobocoConfig, ToolSelection } from '../types/index.js';
77

‎src/commands/audit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export async function auditCommand(options: AuditOptions): Promise<void> {
5252

5353
async function auditClaudeEnv(
5454
targetPath: string,
55-
analysis: ReturnType<typeof analyze> extends Promise<infer T> ? T : never,
55+
_analysis: ReturnType<typeof analyze> extends Promise<infer T> ? T : never,
5656
): Promise<AuditCategory> {
5757
const cat: AuditCategory = {
5858
name: 'Claude Code Environment',

‎src/core/generator.ts‎

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'node:path';
2+
import { execa } from 'execa';
23
import type { AnalysisResult, InterviewResult, RobocoConfig } from '../types/index.js';
3-
import { ensureDir, fileExists, writeText } from '../utils/fs.js';
4+
import { ensureDir, fileExists, writeText, readText } from '../utils/fs.js';
45
import { logger } from '../utils/logger.js';
56

67
interface FileOperation {
@@ -17,7 +18,12 @@ export async function generate(
1718
const files: FileOperation[] = [];
1819

1920
// Claude Code Environment (required)
20-
files.push(...generateClaudeEnv(targetPath, analysis, interviewResult));
21+
// Step 1: Run claude /init to generate base CLAUDE.md
22+
await runClaudeInit(targetPath);
23+
// Step 2: Append ROBOCO context + generate settings/hooks
24+
await appendRobocoContext(targetPath, analysis, interviewResult);
25+
// Step 3: Generate .claude/settings.json and hooks
26+
files.push(...generateClaudeSettings(targetPath, analysis));
2127

2228
// Process Documents (optional)
2329
if (interviewResult.setupDomains.processDocs) {
@@ -51,50 +57,84 @@ function relative(base: string, full: string): string {
5157
return full.startsWith(base) ? full.slice(base.length + 1) : full;
5258
}
5359

54-
function generateClaudeEnv(
55-
targetPath: string,
56-
analysis: AnalysisResult,
57-
_interviewResult: InterviewResult,
58-
): FileOperation[] {
59-
const files: FileOperation[] = [];
60-
const projectName = analysis.git.repoName ?? 'My Project';
61-
const stackList =
62-
[...analysis.stack.languages, ...analysis.stack.frameworks].join(', ') || 'Not detected';
63-
64-
files.push({
65-
path: join(targetPath, 'CLAUDE.md'),
66-
content: `# ${projectName}
60+
async function runClaudeInit(targetPath: string): Promise<void> {
61+
const claudeMdPath = join(targetPath, 'CLAUDE.md');
62+
if (await fileExists(claudeMdPath)) {
63+
logger.info('CLAUDE.md already exists — skipping claude /init');
64+
return;
65+
}
6766

68-
## Project Overview
69-
<!-- Describe your project here -->
67+
try {
68+
await execa('claude', ['/init'], {
69+
cwd: targetPath,
70+
timeout: 30000,
71+
env: { ...process.env, CLAUDE_CODE_HEADLESS: '1' },
72+
stdin: 'ignore',
73+
});
74+
// Verify claude /init actually created the file
75+
if (await fileExists(claudeMdPath)) {
76+
logger.success('CLAUDE.md generated via claude /init');
77+
return;
78+
}
79+
} catch {
80+
// Fall through to template generation
81+
}
7082

71-
## Tech Stack
72-
${stackList}
83+
logger.warn('claude /init did not produce CLAUDE.md — generating template');
84+
const { basename } = await import('node:path');
85+
await writeText(
86+
claudeMdPath,
87+
`# ${basename(targetPath)}\n\n## Project Overview\n\nDescribe your project here.\n`,
88+
);
89+
}
7390

74-
## Development Commands
75-
\`\`\`bash
76-
# Build
77-
# Test
78-
# Lint
79-
\`\`\`
91+
async function appendRobocoContext(
92+
targetPath: string,
93+
analysis: AnalysisResult,
94+
interviewResult: InterviewResult,
95+
): Promise<void> {
96+
const claudeMdPath = join(targetPath, 'CLAUDE.md');
97+
if (!(await fileExists(claudeMdPath))) return;
98+
99+
const existing = await readText(claudeMdPath);
100+
if (existing.includes('<roboco>')) {
101+
logger.info('ROBOCO context already present in CLAUDE.md — skipping');
102+
return;
103+
}
80104

81-
## Coding Conventions
82-
<!-- Add your coding conventions here -->
105+
const tools = Object.entries(interviewResult.tools)
106+
.filter(([, v]) => v)
107+
.map(([k]) => k)
108+
.join(', ');
83109

110+
const robocoBlock = `
111+
<roboco>
84112
## Vibe Coding Process
85-
This project follows the 5-stage vibe coding process:
113+
114+
This project uses ROBOCO for AI-native development with the 5-stage vibe coding process:
86115
1. **Intent** — Communicate what you want to build
87116
2. **Requirements** — Define requirements through deep interview
88117
3. **Research** — Investigate approaches and tools
89118
4. **Plan** — Create an implementation plan
90119
5. **Implement** — Build with AI assistance
91120
92-
## Non-Goals
93-
<!-- List things explicitly out of scope -->
94-
`,
95-
description: 'CLAUDE.md',
96-
});
121+
Each stage produces documents in \`docs/vibe-coding/\`. You can restart from any stage.
122+
123+
## ROBOCO Configuration
124+
125+
- **Stack**: ${[...analysis.stack.languages, ...analysis.stack.frameworks].join(', ') || 'Not detected'}
126+
- **Tools**: ${tools}
127+
- **Config**: \`.roboco/config.json\`
128+
129+
Run \`roboco status\` to check setup, \`roboco audit\` for maturity scoring.
130+
</roboco>
131+
`;
132+
133+
await writeText(claudeMdPath, existing.trimEnd() + '\n' + robocoBlock);
134+
logger.success('ROBOCO context appended to CLAUDE.md');
135+
}
97136

137+
function generateClaudeSettings(targetPath: string, analysis: AnalysisResult): FileOperation[] {
98138
const hooks = generateHooksForStack(analysis.stack.languages);
99139
const settings = {
100140
permissions: {
@@ -114,13 +154,13 @@ This project follows the 5-stage vibe coding process:
114154
...(Object.keys(hooks).length > 0 ? { hooks } : {}),
115155
};
116156

117-
files.push({
118-
path: join(targetPath, '.claude', 'settings.json'),
119-
content: JSON.stringify(settings, null, 2) + '\n',
120-
description: '.claude/settings.json',
121-
});
122-
123-
return files;
157+
return [
158+
{
159+
path: join(targetPath, '.claude', 'settings.json'),
160+
content: JSON.stringify(settings, null, 2) + '\n',
161+
description: '.claude/settings.json',
162+
},
163+
];
124164
}
125165

126166
function generateHooksForStack(languages: string[]): Record<string, unknown> {

‎src/utils/prompt.ts‎

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,3 @@ export async function confirm(message: string, defaultYes = true): Promise<boole
1616
});
1717
});
1818
}
19-
20-
export async function select(message: string, choices: string[]): Promise<number> {
21-
const rl = createRl();
22-
const list = choices.map((c, i) => ` ${i + 1}) ${c}`).join('\n');
23-
return new Promise((resolve) => {
24-
rl.question(`${message}\n${list}\n> `, (answer: string) => {
25-
rl.close();
26-
const idx = parseInt(answer.trim(), 10) - 1;
27-
resolve(idx >= 0 && idx < choices.length ? idx : 0);
28-
});
29-
});
30-
}
31-
32-
export async function input(message: string): Promise<string> {
33-
const rl = createRl();
34-
return new Promise((resolve) => {
35-
rl.question(`${message} `, (answer: string) => {
36-
rl.close();
37-
resolve(answer.trim());
38-
});
39-
});
40-
}

‎tests/integration/init-pipeline.test.ts‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { interview } from '../../src/core/interviewer.js';
88
import { generate } from '../../src/core/generator.js';
99
import { fileExists } from '../../src/utils/fs.js';
1010

11+
// Pre-create CLAUDE.md to skip `claude /init` in tests
12+
async function seedClaudeMd(dir: string, name: string): Promise<void> {
13+
await writeFile(join(dir, 'CLAUDE.md'), `# ${name}\n\n## Overview\nTest project.\n`);
14+
}
15+
1116
describe('init pipeline (analyze → interview → generate)', () => {
1217
let tempDir: string;
1318

@@ -24,43 +29,39 @@ describe('init pipeline (analyze → interview → generate)', () => {
2429
);
2530
await writeFile(join(tempDir, 'tsconfig.json'), '{}');
2631
await mkdir(join(tempDir, 'src'));
32+
await seedClaudeMd(tempDir, 'test-project');
2733
});
2834

2935
afterEach(async () => {
3036
await rm(tempDir, { recursive: true, force: true });
3137
});
3238

3339
it('full pipeline produces valid ROBOCO setup', async () => {
34-
// Step 1: Analyze
3540
const analysis = await analyze(tempDir);
3641
expect(analysis.stack.languages).toContain('TypeScript');
3742
expect(analysis.stack.frameworks).toContain('React');
3843
expect(analysis.git.isRepo).toBe(true);
3944

40-
// Step 2: Interview (auto)
4145
const result = await interview(analysis, { auto: true });
4246
expect(result.tools.omc).toBe(true);
4347
expect(result.setupDomains.claudeEnv).toBe(true);
4448

45-
// Step 3: Generate
4649
const created = await generate(tempDir, analysis, result);
4750
expect(created.length).toBeGreaterThan(0);
4851

49-
// Verify artifacts
5052
expect(await fileExists(join(tempDir, 'CLAUDE.md'))).toBe(true);
5153
expect(await fileExists(join(tempDir, '.claude', 'settings.json'))).toBe(true);
5254
expect(await fileExists(join(tempDir, '.roboco', 'config.json'))).toBe(true);
5355

54-
// Verify CLAUDE.md contains project info
5556
const claudeMd = await readFile(join(tempDir, 'CLAUDE.md'), 'utf-8');
57+
expect(claudeMd).toContain('test-project');
58+
expect(claudeMd).toContain('<roboco>');
5659
expect(claudeMd).toContain('TypeScript');
5760

58-
// Verify settings.json has TypeScript hooks
5961
const settings = JSON.parse(await readFile(join(tempDir, '.claude', 'settings.json'), 'utf-8'));
6062
expect(settings.permissions).toBeDefined();
6163
expect(settings.hooks).toBeDefined();
6264

63-
// Verify config is valid
6465
const config = JSON.parse(await readFile(join(tempDir, '.roboco', 'config.json'), 'utf-8'));
6566
expect(config.version).toBe('0.1.0');
6667
expect(config.analysis.stack.languages).toContain('TypeScript');
@@ -69,24 +70,20 @@ describe('init pipeline (analyze → interview → generate)', () => {
6970
it('pipeline with full options generates all artifacts', async () => {
7071
const analysis = await analyze(tempDir);
7172
const result = await interview(analysis, { auto: true });
72-
// Auto mode enables processDocs + cicd for git repos
7373
expect(result.setupDomains.processDocs).toBe(true);
7474
expect(result.setupDomains.cicd).toBe(true);
7575

7676
await generate(tempDir, analysis, result);
7777

78-
// Process docs
7978
expect(await fileExists(join(tempDir, 'docs', 'vibe-coding', '01-intent.md'))).toBe(true);
8079
expect(await fileExists(join(tempDir, 'docs', 'vibe-coding', '05-implement.md'))).toBe(true);
81-
82-
// CI/CD
8380
expect(await fileExists(join(tempDir, '.github', 'workflows', 'vibe-coding-check.yml'))).toBe(
8481
true,
8582
);
83+
expect(await fileExists(join(tempDir, '.husky', 'pre-commit'))).toBe(true);
8684
});
8785

8886
it('pipeline handles Python project correctly', async () => {
89-
// Replace with Python project
9087
await rm(join(tempDir, 'package.json'));
9188
await rm(join(tempDir, 'tsconfig.json'));
9289
await writeFile(join(tempDir, 'requirements.txt'), 'flask\ndjango\n');
@@ -102,14 +99,13 @@ describe('init pipeline (analyze → interview → generate)', () => {
10299
expect(JSON.stringify(settings.hooks)).toContain('black');
103100
});
104101

105-
it('pipeline preserves existing CLAUDE.md', async () => {
106-
await writeFile(join(tempDir, 'CLAUDE.md'), '# Existing content\nDo not overwrite');
107-
102+
it('pipeline preserves existing CLAUDE.md and appends roboco context', async () => {
108103
const analysis = await analyze(tempDir);
109104
const result = await interview(analysis, { auto: true });
110105
await generate(tempDir, analysis, result);
111106

112107
const content = await readFile(join(tempDir, 'CLAUDE.md'), 'utf-8');
113-
expect(content).toContain('Existing content');
108+
expect(content).toContain('test-project');
109+
expect(content).toContain('<roboco>');
114110
});
115111
});

0 commit comments

Comments
 (0)