docs: add complete Korean (ko-KR) documentation — 12 translated files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,699 @@
|
||||
# 초기화 시 new-project config 완전 구체화
|
||||
|
||||
> **에이전트 작업자를 위한 안내:** 필수 하위 기술: superpowers:subagent-driven-development(권장) 또는 superpowers:executing-plans를 사용하여 이 계획을 작업 단위로 구현하십시오. 단계는 체크박스(`- [ ]`) 형식으로 진행 상황을 추적합니다.
|
||||
|
||||
**목표:** `/gsd:new-project`가 `.planning/config.json`을 생성할 때, 파일에 사용자가 선택한 6개 키만이 아닌 모든 유효한 기본값이 포함되도록 하여 개발자가 소스 코드를 읽지 않고도 모든 설정을 확인할 수 있게 합니다.
|
||||
|
||||
**아키텍처:** 새 프로젝트의 전체 config에 대한 단일 진실 공급원으로서 `config.cjs`에 단일 JS 함수 `buildNewProjectConfig(cwd, userChoices)`를 추가합니다. CLI 명령어 `config-new-project`로 노출합니다. 부분적인 JSON을 인라인으로 작성하는 대신 이 명령어를 호출하도록 `new-project.md` 워크플로우를 업데이트합니다.
|
||||
|
||||
**기술 스택:** Node.js/CommonJS, 기존 gsd-tools CLI, 테스트에는 `node:test`.
|
||||
|
||||
---
|
||||
|
||||
## 배경: 현재 상태
|
||||
|
||||
`new-project.md` Step 5는 이 부분적인 config를 작성합니다(AI가 템플릿을 채움):
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "...", "granularity": "...", "parallelization": "...",
|
||||
"commit_docs": "...", "model_profile": "...",
|
||||
"workflow": { "research", "plan_check", "verifier", "nyquist_validation" }
|
||||
}
|
||||
```
|
||||
|
||||
런타임에 `loadConfig()`가 자동으로 해석하는 누락된 키들:
|
||||
|
||||
- `search_gitignored: false`
|
||||
- `brave_search: false` (또는 환경 감지 시 `true`)
|
||||
- `git.branching_strategy: "none"`
|
||||
- `git.phase_branch_template: "gsd/phase-{phase}-{slug}"`
|
||||
- `git.milestone_branch_template: "gsd/{milestone}-{slug}"`
|
||||
|
||||
처음부터 존재해야 하는 전체 config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "yolo|interactive",
|
||||
"granularity": "coarse|standard|fine",
|
||||
"model_profile": "balanced",
|
||||
"commit_docs": true,
|
||||
"parallelization": true,
|
||||
"search_gitignored": false,
|
||||
"brave_search": false,
|
||||
"git": {
|
||||
"branching_strategy": "none",
|
||||
"phase_branch_template": "gsd/phase-{phase}-{slug}",
|
||||
"milestone_branch_template": "gsd/{milestone}-{slug}"
|
||||
},
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 파일 맵
|
||||
|
||||
| 파일 | 액션 | 목적 |
|
||||
|------|--------|---------|
|
||||
| `get-shit-done/bin/lib/config.cjs` | 수정 | `buildNewProjectConfig()` + `cmdConfigNewProject()` 추가 |
|
||||
| `get-shit-done/bin/gsd-tools.cjs` | 수정 | `config-new-project` case 등록 + usage 문자열 업데이트 |
|
||||
| `get-shit-done/workflows/new-project.md` | 수정 | Steps 2a + 5: 인라인 JSON 작성을 CLI 호출로 교체 |
|
||||
| `tests/config.test.cjs` | 수정 | `config-new-project` 테스트 스위트 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 작업 1: config.cjs에 `buildNewProjectConfig`와 `cmdConfigNewProject` 추가
|
||||
|
||||
**파일.**
|
||||
|
||||
- 수정: `get-shit-done/bin/lib/config.cjs`
|
||||
|
||||
- [ ] **Step 1.1: 실패하는 테스트 먼저 작성**
|
||||
|
||||
`tests/config.test.cjs`에 추가(`config-get` 스위트 뒤, `module.exports` 앞):
|
||||
|
||||
```js
|
||||
// ─── config-new-project ──────────────────────────────────────────────────────
|
||||
|
||||
describe('config-new-project command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('creates full config with all expected top-level and nested keys', () => {
|
||||
const choices = JSON.stringify({
|
||||
mode: 'interactive',
|
||||
granularity: 'standard',
|
||||
parallelization: true,
|
||||
commit_docs: true,
|
||||
model_profile: 'balanced',
|
||||
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true },
|
||||
});
|
||||
const result = runGsdTools(['config-new-project', choices], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const config = readConfig(tmpDir);
|
||||
|
||||
// 사용자 선택값 확인
|
||||
assert.strictEqual(config.mode, 'interactive');
|
||||
assert.strictEqual(config.granularity, 'standard');
|
||||
assert.strictEqual(config.parallelization, true);
|
||||
assert.strictEqual(config.commit_docs, true);
|
||||
assert.strictEqual(config.model_profile, 'balanced');
|
||||
|
||||
// 기본값이 구체화되었는지 확인
|
||||
assert.strictEqual(typeof config.search_gitignored, 'boolean');
|
||||
assert.strictEqual(typeof config.brave_search, 'boolean');
|
||||
|
||||
// git 섹션에 세 가지 키가 모두 존재하는지 확인
|
||||
assert.ok(config.git && typeof config.git === 'object', 'git section should exist');
|
||||
assert.strictEqual(config.git.branching_strategy, 'none');
|
||||
assert.strictEqual(config.git.phase_branch_template, 'gsd/phase-{phase}-{slug}');
|
||||
assert.strictEqual(config.git.milestone_branch_template, 'gsd/{milestone}-{slug}');
|
||||
|
||||
// workflow 섹션에 네 가지 키가 모두 존재하는지 확인
|
||||
assert.ok(config.workflow && typeof config.workflow === 'object', 'workflow section should exist');
|
||||
assert.strictEqual(config.workflow.research, true);
|
||||
assert.strictEqual(config.workflow.plan_check, true);
|
||||
assert.strictEqual(config.workflow.verifier, true);
|
||||
assert.strictEqual(config.workflow.nyquist_validation, true);
|
||||
});
|
||||
|
||||
test('user choices override defaults', () => {
|
||||
const choices = JSON.stringify({
|
||||
mode: 'yolo',
|
||||
granularity: 'coarse',
|
||||
parallelization: false,
|
||||
commit_docs: false,
|
||||
model_profile: 'quality',
|
||||
workflow: { research: false, plan_check: false, verifier: true, nyquist_validation: false },
|
||||
});
|
||||
const result = runGsdTools(['config-new-project', choices], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.mode, 'yolo');
|
||||
assert.strictEqual(config.granularity, 'coarse');
|
||||
assert.strictEqual(config.parallelization, false);
|
||||
assert.strictEqual(config.commit_docs, false);
|
||||
assert.strictEqual(config.model_profile, 'quality');
|
||||
assert.strictEqual(config.workflow.research, false);
|
||||
assert.strictEqual(config.workflow.plan_check, false);
|
||||
assert.strictEqual(config.workflow.verifier, true);
|
||||
assert.strictEqual(config.workflow.nyquist_validation, false);
|
||||
// 선택하지 않은 키에 대해서도 기본값이 존재해야 함
|
||||
assert.strictEqual(config.git.branching_strategy, 'none');
|
||||
assert.strictEqual(typeof config.search_gitignored, 'boolean');
|
||||
});
|
||||
|
||||
test('works with empty choices — all defaults materialized', () => {
|
||||
const result = runGsdTools(['config-new-project', '{}'], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.model_profile, 'balanced');
|
||||
assert.strictEqual(config.commit_docs, true);
|
||||
assert.strictEqual(config.parallelization, true);
|
||||
assert.strictEqual(config.search_gitignored, false);
|
||||
assert.ok(config.git && typeof config.git === 'object');
|
||||
assert.strictEqual(config.git.branching_strategy, 'none');
|
||||
assert.ok(config.workflow && typeof config.workflow === 'object');
|
||||
assert.strictEqual(config.workflow.nyquist_validation, true);
|
||||
});
|
||||
|
||||
test('is idempotent — returns already_exists if config exists', () => {
|
||||
// 첫 번째 호출: 생성
|
||||
const choices = JSON.stringify({ mode: 'yolo', granularity: 'fine' });
|
||||
const first = runGsdTools(['config-new-project', choices], tmpDir);
|
||||
assert.ok(first.success, `First call failed: ${first.error}`);
|
||||
const firstOut = JSON.parse(first.output);
|
||||
assert.strictEqual(firstOut.created, true);
|
||||
|
||||
// 두 번째 호출: 멱등성
|
||||
const second = runGsdTools(['config-new-project', choices], tmpDir);
|
||||
assert.ok(second.success, `Second call failed: ${second.error}`);
|
||||
const secondOut = JSON.parse(second.output);
|
||||
assert.strictEqual(secondOut.created, false);
|
||||
assert.strictEqual(secondOut.reason, 'already_exists');
|
||||
|
||||
// config 변경되지 않음
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.mode, 'yolo');
|
||||
assert.strictEqual(config.granularity, 'fine');
|
||||
});
|
||||
|
||||
test('auto_advance in workflow choices is preserved', () => {
|
||||
const choices = JSON.stringify({
|
||||
mode: 'yolo',
|
||||
granularity: 'standard',
|
||||
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true, auto_advance: true },
|
||||
});
|
||||
const result = runGsdTools(['config-new-project', choices], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const config = readConfig(tmpDir);
|
||||
assert.strictEqual(config.workflow.auto_advance, true);
|
||||
});
|
||||
|
||||
test('rejects invalid JSON choices', () => {
|
||||
const result = runGsdTools(['config-new-project', '{not-json}'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.ok(result.error.includes('Invalid JSON'), `Expected "Invalid JSON" in: ${result.error}`);
|
||||
});
|
||||
|
||||
test('output JSON has created:true on success', () => {
|
||||
const choices = JSON.stringify({ mode: 'interactive', granularity: 'standard' });
|
||||
const result = runGsdTools(['config-new-project', choices], tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.created, true);
|
||||
assert.strictEqual(out.path, '.planning/config.json');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: 실패하는 테스트 실행하여 실패 확인**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
node --test tests/config.test.cjs 2>&1 | grep -E "config-new-project|FAIL|Error"
|
||||
```
|
||||
|
||||
예상 결과: 모든 `config-new-project` 테스트가 "config-new-project is not a valid command" 또는 유사한 오류로 실패합니다.
|
||||
|
||||
- [ ] **Step 1.3: config.cjs에 `buildNewProjectConfig`와 `cmdConfigNewProject` 구현**
|
||||
|
||||
`get-shit-done/bin/lib/config.cjs`에서 `validateKnownConfigKeyPath` 함수 뒤(약 35번째 줄)와 `ensureConfigFile` 앞에 다음을 추가하십시오:
|
||||
|
||||
```js
|
||||
/**
|
||||
* 새 프로젝트의 완전히 구체화된 config를 빌드합니다.
|
||||
*
|
||||
* 다음 우선순위 순서로 병합합니다:
|
||||
* 1. 하드코딩된 기본값
|
||||
* 2. ~/.gsd/defaults.json의 사용자 수준 기본값(있는 경우)
|
||||
* 3. userChoices (new-project 중 사용자가 명시적으로 선택한 설정)
|
||||
*
|
||||
* 일반 객체를 반환합니다 — 파일을 직접 작성하지 않습니다.
|
||||
*/
|
||||
function buildNewProjectConfig(cwd, userChoices) {
|
||||
const choices = userChoices || {};
|
||||
const homedir = require('os').homedir();
|
||||
|
||||
// Brave Search API 키 가용성 감지
|
||||
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
||||
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
||||
|
||||
// 사용 가능한 경우 ~/.gsd/defaults.json에서 사용자 수준 기본값 로드
|
||||
const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json');
|
||||
let userDefaults = {};
|
||||
try {
|
||||
if (fs.existsSync(globalDefaultsPath)) {
|
||||
userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
|
||||
// 더 이상 사용되지 않는 "depth" 키를 "granularity"로 마이그레이션
|
||||
if ('depth' in userDefaults && !('granularity' in userDefaults)) {
|
||||
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
||||
userDefaults.granularity = depthToGranularity[userDefaults.depth] || userDefaults.depth;
|
||||
delete userDefaults.depth;
|
||||
try {
|
||||
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 잘못된 전역 기본값 무시
|
||||
}
|
||||
|
||||
const hardcoded = {
|
||||
model_profile: 'balanced',
|
||||
commit_docs: true,
|
||||
parallelization: true,
|
||||
search_gitignored: false,
|
||||
brave_search: hasBraveSearch,
|
||||
git: {
|
||||
branching_strategy: 'none',
|
||||
phase_branch_template: 'gsd/phase-{phase}-{slug}',
|
||||
milestone_branch_template: 'gsd/{milestone}-{slug}',
|
||||
},
|
||||
workflow: {
|
||||
research: true,
|
||||
plan_check: true,
|
||||
verifier: true,
|
||||
nyquist_validation: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 세 단계 병합: hardcoded <- userDefaults <- choices
|
||||
return {
|
||||
...hardcoded,
|
||||
...userDefaults,
|
||||
...choices,
|
||||
git: {
|
||||
...hardcoded.git,
|
||||
...(userDefaults.git || {}),
|
||||
...(choices.git || {}),
|
||||
},
|
||||
workflow: {
|
||||
...hardcoded.workflow,
|
||||
...(userDefaults.workflow || {}),
|
||||
...(choices.workflow || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 명령어: 새 프로젝트를 위한 완전히 구체화된 .planning/config.json을 생성합니다.
|
||||
*
|
||||
* 사용자가 선택한 설정을 JSON 문자열로 받습니다(/gsd:new-project 중 명시적으로
|
||||
* 구성한 키들). 나머지 키들은 하드코딩된 기본값과 선택적 ~/.gsd/defaults.json에서 채워집니다.
|
||||
*
|
||||
* 멱등성: config.json이 이미 존재하면 { created: false }를 반환합니다.
|
||||
*/
|
||||
function cmdConfigNewProject(cwd, choicesJson, raw) {
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
|
||||
// 멱등성: 기존 config를 덮어쓰지 않음
|
||||
if (fs.existsSync(configPath)) {
|
||||
output({ created: false, reason: 'already_exists' }, raw, 'exists');
|
||||
return;
|
||||
}
|
||||
|
||||
// 사용자 선택값 파싱
|
||||
let userChoices = {};
|
||||
if (choicesJson && choicesJson.trim() !== '') {
|
||||
try {
|
||||
userChoices = JSON.parse(choicesJson);
|
||||
} catch (err) {
|
||||
error('Invalid JSON for config-new-project: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// .planning 디렉토리가 존재하는지 확인
|
||||
try {
|
||||
if (!fs.existsSync(planningDir)) {
|
||||
fs.mkdirSync(planningDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
error('Failed to create .planning directory: ' + err.message);
|
||||
}
|
||||
|
||||
const config = buildNewProjectConfig(cwd, userChoices);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
output({ created: true, path: '.planning/config.json' }, raw, 'created');
|
||||
} catch (err) {
|
||||
error('Failed to write config.json: ' + err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`config.cjs` 하단의 `module.exports`에 `cmdConfigNewProject`도 추가하십시오.
|
||||
|
||||
- [ ] **Step 1.4: 테스트 실행하여 통과 확인**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
node --test tests/config.test.cjs 2>&1 | tail -20
|
||||
```
|
||||
|
||||
예상 결과: 모든 `config-new-project` 테스트가 통과합니다. 기존 테스트도 계속 통과합니다.
|
||||
|
||||
- [ ] **Step 1.5: 커밋**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
git add get-shit-done/bin/lib/config.cjs tests/config.test.cjs
|
||||
git commit -m "feat: add config-new-project command for full config materialization"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업 2: gsd-tools.cjs에 `config-new-project` 등록
|
||||
|
||||
**파일.**
|
||||
|
||||
- 수정: `get-shit-done/bin/gsd-tools.cjs`
|
||||
|
||||
- [ ] **Step 2.1: gsd-tools.cjs의 switch에 case 추가**
|
||||
|
||||
`config-get` case 뒤(약 401번째 줄)에 다음을 추가하십시오:
|
||||
|
||||
```js
|
||||
case 'config-new-project': {
|
||||
config.cmdConfigNewProject(cwd, args[1], raw);
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
178번째 줄의 usage 문자열도 `config-new-project`를 포함하도록 업데이트하십시오:
|
||||
|
||||
현재: `...config-ensure-section, init`
|
||||
변경: `...config-ensure-section, config-new-project, init`
|
||||
|
||||
- [ ] **Step 2.2: CLI 등록 스모크 테스트**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
node get-shit-done/bin/gsd-tools.cjs config-new-project '{"mode":"interactive","granularity":"standard"}' --cwd /tmp/gsd-smoke-$(date +%s)
|
||||
```
|
||||
|
||||
예상 결과: `{"created":true,"path":".planning/config.json"}` (또는 유사한 형태)가 출력됩니다.
|
||||
|
||||
정리: `rm -rf /tmp/gsd-smoke-*`
|
||||
|
||||
- [ ] **Step 2.3: 전체 테스트 스위트 실행**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
node --test tests/config.test.cjs 2>&1 | tail -10
|
||||
```
|
||||
|
||||
예상 결과: 모두 통과합니다.
|
||||
|
||||
- [ ] **Step 2.4: 커밋**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
git add get-shit-done/bin/gsd-tools.cjs
|
||||
git commit -m "feat: register config-new-project in gsd-tools CLI router"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업 3: config-new-project를 사용하도록 new-project.md 워크플로우 업데이트
|
||||
|
||||
**파일.**
|
||||
|
||||
- 수정: `get-shit-done/workflows/new-project.md`
|
||||
|
||||
이것이 핵심 변경사항입니다. 두 곳을 업데이트해야 합니다:
|
||||
|
||||
- **Step 2a** (auto 모드 config 생성, 약 168–195번째 줄)
|
||||
- **Step 5** (대화형 모드 config 생성, 약 470–498번째 줄)
|
||||
|
||||
- [ ] **Step 3.1: Step 2a(auto 모드) 업데이트**
|
||||
|
||||
Step 2a에서 config.json을 생성하는 블록을 찾으십시오:
|
||||
|
||||
```markdown
|
||||
Create `.planning/config.json` with mode set to "yolo":
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "yolo",
|
||||
"granularity": "[selected]",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
인라인 JSON 작성 지침을 다음으로 교체하십시오:
|
||||
|
||||
```markdown
|
||||
Create `.planning/config.json` using the CLI (fills in all defaults automatically):
|
||||
|
||||
```bash
|
||||
mkdir -p .planning
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project "$(cat <<'CHOICES'
|
||||
{
|
||||
"mode": "yolo",
|
||||
"granularity": "[selected: coarse|standard|fine]",
|
||||
"parallelization": [true|false],
|
||||
"commit_docs": [true|false],
|
||||
"model_profile": "[selected: quality|balanced|budget|inherit]",
|
||||
"workflow": {
|
||||
"research": [true|false],
|
||||
"plan_check": [true|false],
|
||||
"verifier": [true|false],
|
||||
"nyquist_validation": [true|false],
|
||||
"auto_advance": true
|
||||
}
|
||||
}
|
||||
CHOICES
|
||||
)"
|
||||
```
|
||||
|
||||
The command merges your selections with all runtime defaults (`search_gitignored`, `brave_search`, `git` section), producing a fully-materialized config.
|
||||
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Step 5(대화형 모드) 업데이트**
|
||||
|
||||
Step 5에서 config.json을 생성하는 블록을 찾으십시오:
|
||||
|
||||
```markdown
|
||||
Create `.planning/config.json` with all settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "yolo|interactive",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
다음으로 교체하십시오:
|
||||
|
||||
```markdown
|
||||
Create `.planning/config.json` using the CLI (fills in all defaults automatically):
|
||||
|
||||
```bash
|
||||
mkdir -p .planning
|
||||
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-new-project "$(cat <<'CHOICES'
|
||||
{
|
||||
"mode": "[selected: yolo|interactive]",
|
||||
"granularity": "[selected: coarse|standard|fine]",
|
||||
"parallelization": [true|false],
|
||||
"commit_docs": [true|false],
|
||||
"model_profile": "[selected: quality|balanced|budget|inherit]",
|
||||
"workflow": {
|
||||
"research": [true|false],
|
||||
"plan_check": [true|false],
|
||||
"verifier": [true|false],
|
||||
"nyquist_validation": [true|false]
|
||||
}
|
||||
}
|
||||
CHOICES
|
||||
)"
|
||||
```
|
||||
|
||||
The command merges your selections with all runtime defaults (`search_gitignored`, `brave_search`, `git` section), producing a fully-materialized config.
|
||||
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3: 워크플로우 파일이 올바르게 읽히는지 확인**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
grep -n "config-new-project\|config\.json\|CHOICES" get-shit-done/workflows/new-project.md
|
||||
```
|
||||
|
||||
예상 결과: `config-new-project` 2회 등장(단계당 하나씩), config 생성을 위한 인라인 JSON 템플릿 없음.
|
||||
|
||||
- [ ] **Step 3.4: 커밋**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
git add get-shit-done/workflows/new-project.md
|
||||
git commit -m "feat: use config-new-project in new-project workflow for full config materialization"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업 4: 검증
|
||||
|
||||
- [ ] **Step 4.1: 전체 테스트 스위트 실행**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
node --test tests/ 2>&1 | tail -30
|
||||
```
|
||||
|
||||
예상 결과: 모든 테스트가 통과합니다(회귀 없음).
|
||||
|
||||
- [ ] **Step 4.2: 수동 엔드투엔드 검증**
|
||||
|
||||
`new-project.md`가 새 프로젝트에서 수행하는 작업을 시뮬레이션합니다:
|
||||
|
||||
```bash
|
||||
# 새로운 프로젝트 디렉토리 생성
|
||||
TMP=$(mktemp -d)
|
||||
cd "$TMP"
|
||||
|
||||
# Step 1 시뮬레이션: init new-project가 반환하는 내용
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs init new-project --cwd "$TMP"
|
||||
|
||||
# Step 5 시뮬레이션: 전체 config 생성
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs config-new-project '{
|
||||
"mode": "interactive",
|
||||
"granularity": "standard",
|
||||
"parallelization": true,
|
||||
"commit_docs": true,
|
||||
"model_profile": "balanced",
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": true
|
||||
}
|
||||
}' --cwd "$TMP"
|
||||
|
||||
# 파일에 예상되는 12개 키가 모두 있는지 확인
|
||||
echo "=== Generated config.json ==="
|
||||
cat "$TMP/.planning/config.json"
|
||||
|
||||
# 정리
|
||||
rm -rf "$TMP"
|
||||
```
|
||||
|
||||
예상 출력: `mode`, `granularity`, `model_profile`, `commit_docs`, `parallelization`, `search_gitignored`, `brave_search`, `git`(3개 하위 키), `workflow`(4개 하위 키)가 있는 config.json — 총 12개 최상위 키(또는 `git`과 `workflow`를 단일 키로 계산하면 10개).
|
||||
|
||||
- [ ] **Step 4.3: 멱등성 확인**
|
||||
|
||||
```bash
|
||||
TMP=$(mktemp -d)
|
||||
CHOICES='{"mode":"yolo","granularity":"coarse"}'
|
||||
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs config-new-project "$CHOICES" --cwd "$TMP"
|
||||
FIRST=$(cat "$TMP/.planning/config.json")
|
||||
|
||||
# 두 번째 호출은 no-op이어야 함
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs config-new-project "$CHOICES" --cwd "$TMP"
|
||||
SECOND=$(cat "$TMP/.planning/config.json")
|
||||
|
||||
[ "$FIRST" = "$SECOND" ] && echo "IDEMPOTENT: OK" || echo "IDEMPOTENT: FAIL"
|
||||
rm -rf "$TMP"
|
||||
```
|
||||
|
||||
예상 결과: `IDEMPOTENT: OK`
|
||||
|
||||
- [ ] **Step 4.4: loadConfig가 새 형식을 올바르게 읽는지 확인**
|
||||
|
||||
```bash
|
||||
TMP=$(mktemp -d)
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs config-new-project '{
|
||||
"mode":"yolo","granularity":"standard","parallelization":true,"commit_docs":true,
|
||||
"model_profile":"balanced",
|
||||
"workflow":{"research":true,"plan_check":false,"verifier":true,"nyquist_validation":true}
|
||||
}' --cwd "$TMP"
|
||||
|
||||
# loadConfig는 plan_check(중첩된 workflow.plan_check로)를 올바르게 읽어야 함
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs config-get workflow.plan_check --cwd "$TMP"
|
||||
# 예상: false
|
||||
|
||||
node /Users/diego/Dev/get-shit-done/get-shit-done/bin/gsd-tools.cjs config-get git.branching_strategy --cwd "$TMP"
|
||||
# 예상: "none"
|
||||
|
||||
rm -rf "$TMP"
|
||||
```
|
||||
|
||||
- [ ] **Step 4.5: 최종 전체 테스트 스위트 + 커밋**
|
||||
|
||||
```bash
|
||||
cd /Users/diego/Dev/get-shit-done
|
||||
node --test tests/ 2>&1 | grep -E "pass|fail|error" | tail -5
|
||||
```
|
||||
|
||||
예상 결과: 모두 통과, 0개 실패.
|
||||
|
||||
---
|
||||
|
||||
## 부록: 업스트림용 PR 설명
|
||||
|
||||
```
|
||||
feat: materialize all config defaults at new-project initialization
|
||||
|
||||
**문제:**
|
||||
`/gsd:new-project`는 온보딩 중 사용자가 명시적으로 선택한 6개 키만으로
|
||||
`.planning/config.json`을 생성합니다. 5개의 추가 키
|
||||
(`search_gitignored`, `brave_search`, `git.branching_strategy`,
|
||||
`git.phase_branch_template`, `git.milestone_branch_template`)는
|
||||
런타임에 `loadConfig()`가 자동으로 해석하지만 디스크에는 기록되지 않습니다.
|
||||
|
||||
이로 인해 두 가지 문제가 발생합니다:
|
||||
1. **발견성**: 소스 코드를 읽지 않고는 `git.branching_strategy`를
|
||||
확인하거나 이해할 수 없습니다 — config에 표시되지 않습니다.
|
||||
2. **암묵적 확장**: `/gsd:settings` 또는 `config-set`이 처음으로 config에
|
||||
기록할 때도 해당 키들이 추가되지 않습니다. config는 유효한 구성의
|
||||
일부만 반영합니다.
|
||||
|
||||
**해결책:**
|
||||
`gsd-tools.cjs`에 `config-new-project` CLI 명령어를 추가합니다. 이 명령어는:
|
||||
- 사용자가 선택한 값을 JSON으로 받습니다.
|
||||
- 모든 런타임 기본값(환경 감지 `brave_search` 포함)과 병합합니다.
|
||||
- 완전히 구체화된 config를 한 번에 작성합니다.
|
||||
|
||||
하드코딩된 부분 JSON 템플릿을 작성하는 대신 이 명령어를 호출하도록
|
||||
`new-project.md` 워크플로우(Steps 2a와 5)를 업데이트합니다. 기본값은 이제
|
||||
정확히 한 곳에 있습니다: `config.cjs`의 `buildNewProjectConfig()`.
|
||||
|
||||
**이 접근이 보수적인 이유:**
|
||||
- `loadConfig()`, `ensureConfigFile()`, 또는 읽기 경로에는 변경 없음
|
||||
- 새로운 config 키 도입 없음
|
||||
- 의미적 변경 없음 — 시스템이 이미 자동으로 해석하던 동일한 값들
|
||||
- 완전한 하위 호환성: `loadConfig()`는 이전 부분 형식(기존 프로젝트)과
|
||||
새로운 전체 형식을 모두 계속 처리합니다.
|
||||
- 멱등성: `config-new-project`를 두 번 호출해도 안전합니다.
|
||||
- 새로운 사용자 대면 플래그 없음
|
||||
|
||||
**이것이 발견성을 개선하는 이유:**
|
||||
처음으로 `.planning/config.json`을 여는 개발자는 이제
|
||||
`git.branching_strategy: "none"`을 확인하고 GSD 소스를 읽지 않고도
|
||||
브랜칭이 가능하고 구성 가능하다는 것을 즉시 이해할 수 있습니다.
|
||||
```
|
||||
@@ -0,0 +1,185 @@
|
||||
# 멀티 프로젝트 워크스페이스 (`/gsd:new-workspace`)
|
||||
|
||||
**Issue:** #1241
|
||||
**Date:** 2026-03-20
|
||||
**Status:** Approved
|
||||
|
||||
## 문제
|
||||
|
||||
GSD는 작업 디렉토리당 하나의 `.planning/` 디렉토리에 종속되어 있습니다. 20개 이상의 하위 저장소가 있는 모노저장소 스타일 설정에서 여러 독립 프로젝트를 사용하는 사용자나 동일한 저장소에서 피처 브랜치 격리가 필요한 사용자는 수동 클론 및 상태 관리 없이 병렬 GSD 세션을 실행할 수 없습니다.
|
||||
|
||||
## 해결책
|
||||
|
||||
**물리적 워크스페이스 디렉토리**를 생성, 나열, 제거하는 세 가지 새로운 명령어입니다 — 각각은 저장소 복사본(git worktree 또는 클론)과 독립적인 `.planning/` 디렉토리를 포함합니다.
|
||||
|
||||
이것은 두 가지 사용 사례를 다룹니다:
|
||||
- **멀티 저장소 오케스트레이션(A):** 상위 디렉토리에서 여러 저장소에 걸친 워크스페이스
|
||||
- **피처 브랜치 격리(B):** 현재 저장소의 worktree를 포함하는 워크스페이스(`--repos .`인 경우 A의 특수 케이스)
|
||||
|
||||
## 명령어
|
||||
|
||||
### `/gsd:new-workspace`
|
||||
|
||||
저장소 복사본과 자체 `.planning/`이 있는 워크스페이스 디렉토리를 생성합니다.
|
||||
|
||||
```
|
||||
/gsd:new-workspace --name feature-b --repos hr-ui,ZeymoAPI --path ~/workspaces/feature-b
|
||||
/gsd:new-workspace --name feature-b --repos . --strategy worktree # 동일 저장소 격리
|
||||
```
|
||||
|
||||
**인수.**
|
||||
|
||||
| 플래그 | 필수 여부 | 기본값 | 설명 |
|
||||
|------|----------|---------|-------------|
|
||||
| `--name` | 예 | — | 워크스페이스 이름 |
|
||||
| `--repos` | 아니오 | 대화형 선택 | 쉼표로 구분된 저장소 경로 또는 이름 |
|
||||
| `--path` | 아니오 | `~/gsd-workspaces/<name>` | 대상 디렉토리 |
|
||||
| `--strategy` | 아니오 | `worktree` | `worktree`(가볍고 .git 공유) 또는 `clone`(완전히 독립) |
|
||||
| `--branch` | 아니오 | `workspace/<name>` | 체크아웃할 브랜치 |
|
||||
| `--auto` | 아니오 | false | 대화형 질문 건너뛰고 기본값 사용 |
|
||||
|
||||
### `/gsd:list-workspaces`
|
||||
|
||||
워크스페이스 매니페스트를 위해 `~/gsd-workspaces/*/WORKSPACE.md`를 스캔합니다. 이름, 경로, 저장소 수, GSD 상태(PROJECT.md 존재 여부, 현재 페이즈)가 있는 표를 표시합니다.
|
||||
|
||||
### `/gsd:remove-workspace`
|
||||
|
||||
확인 후 워크스페이스 디렉토리를 제거합니다. worktree 전략의 경우 먼저 각 멤버 저장소에 대해 `git worktree remove`를 실행합니다. 저장소에 커밋되지 않은 변경사항이 있으면 거부합니다.
|
||||
|
||||
## 디렉토리 구조
|
||||
|
||||
```
|
||||
~/gsd-workspaces/feature-b/ # 워크스페이스 루트
|
||||
├── WORKSPACE.md # 매니페스트
|
||||
├── .planning/ # 독립적인 GSD 계획 디렉토리
|
||||
│ ├── PROJECT.md # (사용자가 /gsd:new-project를 실행한 경우)
|
||||
│ ├── STATE.md
|
||||
│ └── config.json
|
||||
├── hr-ui/ # 소스 저장소의 git worktree
|
||||
│ └── (workspace/feature-b 브랜치의 저장소 내용)
|
||||
└── ZeymoAPI/ # 소스 저장소의 git worktree
|
||||
└── (workspace/feature-b 브랜치의 저장소 내용)
|
||||
```
|
||||
|
||||
주요 속성.
|
||||
- `.planning/`은 개별 저장소 내부가 아닌 워크스페이스 루트에 있습니다.
|
||||
- 각 저장소는 워크스페이스 루트 아래의 피어 디렉토리입니다.
|
||||
- `WORKSPACE.md`는 루트에 있는 유일한 GSD 전용 파일입니다(`.planning/` 외).
|
||||
- `--strategy clone`의 경우 동일한 구조이지만 저장소는 전체 클론입니다.
|
||||
|
||||
## WORKSPACE.md 형식
|
||||
|
||||
```markdown
|
||||
# Workspace: feature-b
|
||||
|
||||
Created: 2026-03-20
|
||||
Strategy: worktree
|
||||
|
||||
## Member Repos
|
||||
|
||||
| Repo | Source | Branch | Strategy |
|
||||
|------|--------|--------|----------|
|
||||
| hr-ui | /root/source/repos/hr-ui | workspace/feature-b | worktree |
|
||||
| ZeymoAPI | /root/source/repos/ZeymoAPI | workspace/feature-b | worktree |
|
||||
|
||||
## Notes
|
||||
|
||||
[사용자가 이 워크스페이스의 목적에 대한 컨텍스트를 추가할 수 있습니다]
|
||||
```
|
||||
|
||||
## 워크플로우
|
||||
|
||||
### `/gsd:new-workspace` 워크플로우 단계
|
||||
|
||||
1. **설정** — `init new-workspace` 호출, JSON 컨텍스트 파싱
|
||||
2. **입력 수집** — `--name`/`--repos`/`--path`가 제공되지 않으면 대화형으로 질문합니다. 저장소의 경우 cwd의 하위 `.git` 디렉토리를 옵션으로 표시합니다.
|
||||
3. **유효성 검사** — 대상 경로가 존재하지 않거나 비어 있음. 소스 저장소가 존재하고 git 저장소임.
|
||||
4. **워크스페이스 디렉토리 생성** — `mkdir -p <path>`
|
||||
5. **저장소 복사** — 각 저장소에 대해.
|
||||
- Worktree: `git worktree add <workspace>/<repo-name> -b workspace/<name>`
|
||||
- Clone: `git clone <source> <workspace>/<repo-name>`
|
||||
6. **WORKSPACE.md 작성** — 소스 경로, 전략, 브랜치가 있는 매니페스트
|
||||
7. **.planning/ 초기화** — `mkdir -p <workspace>/.planning`
|
||||
8. **/gsd:new-project 제안** — 새 워크스페이스에서 프로젝트 초기화를 실행할지 사용자에게 질문
|
||||
9. **커밋** — commit_docs가 활성화된 경우 WORKSPACE.md의 원자적 커밋
|
||||
10. **완료** — 워크스페이스 경로와 다음 단계 출력
|
||||
|
||||
### Init 함수(`cmdInitNewWorkspace`)
|
||||
|
||||
다음을 감지합니다.
|
||||
- cwd의 하위 git 저장소(대화형 저장소 선택을 위해)
|
||||
- 대상 경로가 이미 존재하는지 여부
|
||||
- 소스 저장소에 커밋되지 않은 변경사항이 있는지 여부
|
||||
- `git worktree`를 사용할 수 있는지 여부
|
||||
- 기본 워크스페이스 기본 디렉토리(`~/gsd-workspaces/`)
|
||||
|
||||
워크플로우 게이팅을 위한 플래그가 포함된 JSON을 반환합니다.
|
||||
|
||||
## 오류 처리
|
||||
|
||||
### 유효성 검사 오류(생성 차단)
|
||||
|
||||
- **대상 경로가 존재하고 비어 있지 않음** — 다른 이름/경로 선택 제안과 함께 오류
|
||||
- **소스 저장소 경로가 존재하지 않거나 git 저장소가 아님** — 실패한 저장소를 나열하는 오류
|
||||
- **`git worktree add` 실패**(예: 브랜치가 이미 존재) — `workspace/<name>-<timestamp>` 브랜치로 폴백, 그것도 실패하면 오류
|
||||
|
||||
### 정상적인 처리
|
||||
|
||||
- **소스 저장소에 커밋되지 않은 변경사항** — 경고하되 허용(worktree는 브랜치를 새로 체크아웃하며 작업 디렉토리 상태를 복사하지 않음)
|
||||
- **멀티 저장소 워크스페이스에서 부분 실패** — 성공한 저장소로 워크스페이스를 생성하고 실패를 보고하며 부분적인 WORKSPACE.md 작성
|
||||
- **`--repos .`(현재 저장소, 케이스 B)** — 디렉토리 이름 또는 git remote에서 저장소 이름 감지, 하위 디렉토리 이름으로 사용
|
||||
|
||||
### Remove-Workspace 안전성
|
||||
|
||||
- **워크스페이스 저장소에 커밋되지 않은 변경사항** — 제거를 거부하고 변경사항이 있는 저장소 출력
|
||||
- **Worktree 제거 실패**(예: 소스 저장소가 삭제됨) — 경고하고 디렉토리 정리를 계속 진행
|
||||
- **확인** — 워크스페이스 이름을 직접 입력하는 명시적 확인 필요
|
||||
|
||||
### List-Workspaces 엣지 케이스
|
||||
|
||||
- **`~/gsd-workspaces/`가 존재하지 않음** — "No workspaces found"
|
||||
- **WORKSPACE.md는 있지만 내부 저장소가 사라짐** — 워크스페이스를 표시하되 저장소를 누락으로 표시
|
||||
|
||||
## 테스팅
|
||||
|
||||
### 단위 테스트(`tests/workspace.test.cjs`)
|
||||
|
||||
1. `cmdInitNewWorkspace`가 올바른 JSON 반환 — 하위 git 저장소 감지, 대상 경로 유효성 검사, git worktree 가용성 감지
|
||||
2. WORKSPACE.md 생성 — 저장소 표, 전략, 날짜가 있는 올바른 형식
|
||||
3. 저장소 발견 — cwd 하위 항목에서 `.git` 디렉토리 식별, git 디렉토리가 아닌 디렉토리와 파일 건너뜀
|
||||
4. 유효성 검사 — 비어 있지 않은 기존 대상 경로 거부, git이 아닌 소스 경로 거부
|
||||
|
||||
### 통합 테스트(동일 파일)
|
||||
|
||||
5. Worktree 생성 — 워크스페이스 생성, 저장소 디렉토리가 유효한 git worktree인지 확인
|
||||
6. Clone 생성 — 워크스페이스 생성, 저장소가 독립적인 클론인지 확인
|
||||
7. 워크스페이스 나열 — 두 개의 워크스페이스 생성, 나열 출력에 둘 다 포함되는지 확인
|
||||
8. 워크스페이스 제거 — worktree로 워크스페이스 생성, 제거, 정리 확인
|
||||
9. 부분 실패 — 유효한 저장소 하나 + 유효하지 않은 경로 하나, 유효한 저장소만으로 워크스페이스 생성
|
||||
|
||||
모든 테스트는 임시 디렉토리를 사용하고 완료 후 정리합니다. 기존 `node:test` + `node:assert` 패턴을 따릅니다.
|
||||
|
||||
## 구현 파일
|
||||
|
||||
| 컴포넌트 | 경로 |
|
||||
|-----------|------|
|
||||
| 명령어: new-workspace | `commands/gsd/new-workspace.md` |
|
||||
| 명령어: list-workspaces | `commands/gsd/list-workspaces.md` |
|
||||
| 명령어: remove-workspace | `commands/gsd/remove-workspace.md` |
|
||||
| 워크플로우: new-workspace | `get-shit-done/workflows/new-workspace.md` |
|
||||
| 워크플로우: list-workspaces | `get-shit-done/workflows/list-workspaces.md` |
|
||||
| 워크플로우: remove-workspace | `get-shit-done/workflows/remove-workspace.md` |
|
||||
| Init 함수 | `get-shit-done/bin/lib/init.cjs`(`cmdInitNewWorkspace`, `cmdInitListWorkspaces`, `cmdInitRemoveWorkspace` 추가) |
|
||||
| 라우팅 | `get-shit-done/bin/gsd-tools.cjs`(init switch에 case 추가) |
|
||||
| 테스트 | `tests/workspace.test.cjs` |
|
||||
|
||||
## 설계 결정
|
||||
|
||||
| 결정 | 근거 |
|
||||
|----------|-----------|
|
||||
| 논리적 레지스트리 대신 물리적 디렉토리 | 파일 시스템이 진실 공급원 — GSD의 기존 cwd 기반 감지 패턴과 일치 |
|
||||
| 기본 전략으로 Worktree | 가볍고(.git 오브젝트 공유) 생성이 빠르며 정리가 쉬움 |
|
||||
| 워크스페이스 루트에 `.planning/` | 개별 저장소 계획으로부터 완전한 격리를 제공. 각 워크스페이스는 독립적인 GSD 프로젝트 |
|
||||
| 중앙 레지스트리 없음 | 상태 드리프트 방지. `list-workspaces`가 파일 시스템을 직접 스캔 |
|
||||
| A의 특수 케이스로서 케이스 B | `--repos .`는 동일한 메커니즘을 재사용하므로 특별한 피처 브랜치 코드 불필요 |
|
||||
| 기본 경로 `~/gsd-workspaces/<name>` | `list-workspaces`가 스캔할 예측 가능한 위치, 소스 저장소 외부에 워크스페이스 유지 |
|
||||
Reference in New Issue
Block a user