@angar2/taskery 1.0.0 → 1.0.1
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.
- package/README.md +29 -15
- package/bin/check.js +21 -5
- package/bin/commands.js +4 -4
- package/bin/finish.js +14 -2
- package/bin/lib.js +103 -36
- package/bin/orche.js +1 -1
- package/bin/prepare.js +4 -1
- package/package.json +1 -1
- package/template/.taskery/rules/CHANGELOG_RULE.md +1 -1
- package/template/.taskery/rules/TASKERY_RULE.md +2 -2
- package/template/.taskery/rules/TASK_DOC_RULE.md +38 -18
- package/template/shared/skills/task-close/SKILL.md +1 -1
- package/template/shared/skills/task-dev/SKILL.md +2 -2
- package/template/shared/skills/task-plan/SKILL.md +5 -5
- package/template/shared/skills/task-test/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ taskery는 일을 둘로 나눕니다.
|
|
|
23
23
|
|
|
24
24
|
| AI에게 그냥 맡기면 | taskery를 쓰면 |
|
|
25
25
|
|---|---|
|
|
26
|
-
| 무엇을 왜 바꿨는지가 긴 대화 속에 묻혀, 나중에 다시 찾기 어렵습니다 | 태스크마다 문서 한
|
|
26
|
+
| 무엇을 왜 바꿨는지가 긴 대화 속에 묻혀, 나중에 다시 찾기 어렵습니다 | 태스크마다 문서 한 장에 요구사항, 개발 계획, 완료 기준, 내린 결정, 실제 결과(테스트·커밋·병합)가 남습니다 |
|
|
27
27
|
| AI가 "완료했습니다"라고 하지만, 기능이 처음부터 끝까지 실제로 되는지는 아무도 해 보지 않았습니다 | 사용자가 화면에서 보게 될 결과로 완료 기준을 적고, 실제로 해 봅니다. 증거가 없으면 통과로 인정하지 않습니다 |
|
|
28
28
|
| 확인한다며 앱 화면을 띄우는 느린 테스트까지 매번 돌려 한참 기다립니다 | 개발 중에는 화면을 켜지 않는 코드 테스트만 돌리고, 3분을 넘으면 알려 줍니다 |
|
|
29
29
|
| AI가 어디까지 하고 멈출지 예측할 수 없습니다 | 사용자가 말한 데까지만 합니다. 말하지 않으면 한 단계만 하고 멈춥니다 |
|
|
@@ -78,7 +78,7 @@ taskery는 이 목록을 읽어 **지금 시작할 수 있는 태스크**를 알
|
|
|
78
78
|
| 단계 | AI가 하는 일 | 끝에 부르는 명령 | 명령이 하는 일 |
|
|
79
79
|
|---|---|---|---|
|
|
80
80
|
| ① 시작 | 요청이나 백로그 항목을 읽고, 태스크 하나로 묶을 범위·종류·크기·이름을 정합니다 | `prepare-task` | 겹치지 않는 번호를 발급하고, 전용 브랜치와 작업 폴더를 만들고, 태스크 문서를 만듭니다 |
|
|
81
|
-
| ② 기획 |
|
|
81
|
+
| ② 기획 | 요구사항, 개발 계획, 완료 기준을 문서에 적고 사용자 확인을 받습니다 | `approve-plan` | 필요한 내용이 다 적혔는지 검사하고, 기획이 끝났다고 기록합니다 |
|
|
82
82
|
| ③ 개발 | 코드를 고치고 필요한 단위 테스트를 씁니다 | `test-code` | 등록된 코드 테스트를 돌리고, 통과한 순간의 코드 상태를 기록합니다 |
|
|
83
83
|
| ④ 테스트 | 앱을 한 번 켜서 완료 기준을 실제로 해 보고 증거를 남깁니다 | `test-scenario` | 완료 기준마다 결과와 증거를 기록합니다. 증거 없는 통과는 받지 않습니다 |
|
|
84
84
|
| ⑤ 마무리 | 태스크 중 내린 결정 가운데 앞으로도 지킬 것을 프로젝트 문서에 반영합니다 | `verify-close` → `commit-task` → `merge-task` → `close-task` | 검사하고, 커밋하고, 병합하고, 작업 폴더를 치운 뒤 걸린 시간을 보고합니다 |
|
|
@@ -133,7 +133,7 @@ AI가 어디서 멈출지는 사용자의 말로 정해집니다.
|
|
|
133
133
|
|
|
134
134
|
| | small | medium · large |
|
|
135
135
|
|---|---|---|
|
|
136
|
-
| 태스크 문서 |
|
|
136
|
+
| 태스크 문서 | 요구사항, 개발 계획(고칠 파일), 완료 기준, 결정, 결과 | small과 같고, 개발 계획을 작업 묶음(Phase)으로 나눕니다 |
|
|
137
137
|
| 커밋 | 1개 | 작업 묶음마다 1개 |
|
|
138
138
|
|
|
139
139
|
크기는 AI가 제안합니다.
|
|
@@ -157,36 +157,50 @@ AI가 어디서 멈출지는 사용자의 말로 정해집니다.
|
|
|
157
157
|
|
|
158
158
|
```markdown
|
|
159
159
|
# TASK-012 설정 화면 저장 버튼 색 변경
|
|
160
|
-
|
|
160
|
+
|
|
161
|
+
| 생성일 | 플랜 | 유형 | 크기 | 스위치 | 범위 | 부모 브랜치 | 브랜치 | 상태 |
|
|
162
|
+
|---|---|---|---|---|---|---|---|---|
|
|
163
|
+
| 2026-10-04 | 001_mvp | improve | small | 기획·개발·테스트 | 끝까지 | dev | improve/claude_TASK-012_save-button-color | 닫힘(완료) |
|
|
161
164
|
|
|
162
165
|
| 시작·기획 | 개발 | 테스트 | 닫기 |
|
|
163
166
|
|---|---|---|---|
|
|
164
|
-
| ✅ 2분 | ✅ 3분 |
|
|
167
|
+
| ✅ 2분 | ✅ 3분 | ✅ 4분 | ✅ 1분 (합계 10분) |
|
|
165
168
|
|
|
166
|
-
##
|
|
169
|
+
## 요구사항
|
|
167
170
|
저장 버튼이 다른 버튼과 구분되지 않아 눈에 띄는 파란색으로 바꾼다.
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
- 사용자 요구: 저장 버튼이 바로 눈에 띄어야 한다.
|
|
173
|
+
- 하지 않는 것: 다른 버튼 색, 버튼 배치.
|
|
174
|
+
|
|
175
|
+
## 개발 계획
|
|
176
|
+
- `src/ui/SettingsView.tsx` — 저장 버튼 색을 디자인 토큰 primary로
|
|
177
|
+
|
|
178
|
+
## 테스트 계획
|
|
170
179
|
1. [AUTO] 설정 화면 → 저장 버튼을 본다 → 버튼이 파란색으로 보인다
|
|
171
180
|
2. [USER] 설정 화면 → 이름을 바꾸고 저장 → 목록에 바뀐 이름이 보인다
|
|
172
181
|
|
|
173
|
-
## 만질 파일
|
|
174
|
-
- src/ui/SettingsView.tsx
|
|
175
|
-
|
|
176
182
|
## 결정
|
|
177
183
|
- 파란색은 디자인 토큰 primary를 쓴다 — 다크 모드에서도 대비가 유지된다
|
|
178
184
|
|
|
179
185
|
## 결과
|
|
186
|
+
- 코드 테스트 · 통과 · 명령 2개 · 1분 · 2026-10-04 14:10
|
|
187
|
+
- 시나리오 1 · PASS · 캡처 screenshots/save-button.png · 2026-10-04 14:12
|
|
188
|
+
- 시나리오 2 · PASS · 사용자 확인 · 2026-10-04 14:14
|
|
189
|
+
- 커밋 · improve: [TASK-012] 설정 화면 저장 버튼 색 변경
|
|
190
|
+
- 병합 · dev ← e71b0a4 · 2026-10-04 14:15
|
|
180
191
|
```
|
|
181
192
|
|
|
182
193
|
| 부분 | 누가 쓰나 |
|
|
183
194
|
|---|---|
|
|
184
|
-
|
|
|
195
|
+
| 헤더 표(생성일, 플랜, 종류, 크기, 스위치, 범위, 부모 브랜치, 브랜치, 상태) | 명령 |
|
|
185
196
|
| 단계 표 | 명령 — 명령이 성공했을 때만 칸이 채워집니다 |
|
|
186
|
-
|
|
|
187
|
-
| 결과
|
|
197
|
+
| 요구사항(첫 줄은 목표), 개발 계획, 테스트 계획, 결정 | AI |
|
|
198
|
+
| 결과(코드 테스트, 완료 기준 결과, 커밋, 병합) | 명령 |
|
|
199
|
+
|
|
200
|
+
- medium·large는 개발 계획을 작업 묶음(Phase)으로 나누고, 묶음마다 고칠 파일과 이유를 적습니다.
|
|
201
|
+
- 개발을 끈 태스크에는 개발 계획이, 테스트를 끈 태스크에는 테스트 계획이 없습니다.
|
|
188
202
|
|
|
189
|
-
단계 표는 AI의 "했습니다"가 아니라 **명령이 실제로 통과한 기록**입니다. 그래서 문서만 보면 태스크가 정말 어디까지 왔는지 알 수 있습니다. 태스크 상태는 열림과 닫힘
|
|
203
|
+
단계 표는 AI의 "했습니다"가 아니라 **명령이 실제로 통과한 기록**입니다. 그래서 문서만 보면 태스크가 정말 어디까지 왔는지 알 수 있습니다. 태스크 상태는 열림과 닫힘(완료·포기)뿐이고, 손으로 바꾸는 상태는 없습니다.
|
|
190
204
|
|
|
191
205
|
---
|
|
192
206
|
|
|
@@ -421,7 +435,7 @@ taskery 파일은 그 리포에만 적용되는 git 무시 목록(`.git/info/exc
|
|
|
421
435
|
| `project-init` | 처음 한 번 | 프로젝트 정보, 용어집, 제품 문서, 코드 테스트 등록, 프로젝트 전용 규칙, 첫 플랜을 만듭니다 |
|
|
422
436
|
| `plan-init` | 새 기능 묶음을 시작할 때 | 플랜 폴더와 `PLAN.md`(목표, 선행 관계가 있는 태스크 목록)를 만듭니다 |
|
|
423
437
|
| `task-init` | 태스크를 열 때 | 요청이나 백로그 항목을 태스크 하나로 정리해 엽니다 |
|
|
424
|
-
| `task-plan` | 기획 단계 |
|
|
438
|
+
| `task-plan` | 기획 단계 | 요구사항, 개발 계획, 완료 기준을 적습니다 |
|
|
425
439
|
| `task-dev` | 개발 단계 | 구현하고 코드 테스트를 통과시킵니다 |
|
|
426
440
|
| `task-test` | 테스트 단계 | 완료 기준을 실제로 해 보고 증거를 남깁니다 |
|
|
427
441
|
| `task-close` | 마무리 단계 | 검사, 커밋, 병합을 하고, 태스크를 연 쪽이 작업 폴더를 정리합니다 |
|
package/bin/check.js
CHANGED
|
@@ -8,13 +8,21 @@ async function approvePlan(ctx, a) {
|
|
|
8
8
|
if (st.closed) L.fail(`${L.taskLabel(num)}은 이미 닫혔다.`);
|
|
9
9
|
const text = L.readDoc(main, st);
|
|
10
10
|
const problems = [];
|
|
11
|
-
|
|
11
|
+
const newDoc = L.isNewDoc(text);
|
|
12
|
+
const goal = L.goalProblem(text);
|
|
13
|
+
if (goal) problems.push(goal);
|
|
12
14
|
const files = L.plannedFiles(text);
|
|
13
|
-
if (
|
|
15
|
+
if (L.sw(st, 'dev') && !files.length) {
|
|
16
|
+
problems.push(
|
|
17
|
+
newDoc
|
|
18
|
+
? '`## 개발 계획`에 파일이 없다 — small은 한 줄에 `- 경로` 하나, medium·large는 Phase마다 `- 파일:`'
|
|
19
|
+
: '`## 만질 파일`(medium·large는 `## Phase`의 `- 파일:` 포함)에 파일이 없다 — 한 줄에 `- 경로` 하나씩',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
14
22
|
if (L.sw(st, 'test')) {
|
|
15
23
|
const n = L.parseCriteria(text).length;
|
|
16
24
|
if (n < 1 || n > 5) {
|
|
17
|
-
problems.push(`\`## 완료
|
|
25
|
+
problems.push(`\`## ${newDoc ? '테스트 계획' : '완료 기준'}\` 완료 기준이 ${n}개다 — 테스트 켜짐이면 1~5개를 \`1. [AUTO] 시작 → 행동 → 기대하는 끝 상태\` 형식으로 적는다`);
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
if (problems.length) L.fail(`approve-plan: ${L.taskLabel(num)} 태스크 문서를 고친 뒤 다시 부른다.\n${problems.map((p) => `- ${p}`).join('\n')}\n문서: ${L.docShown(main, st)}`);
|
|
@@ -23,7 +31,8 @@ async function approvePlan(ctx, a) {
|
|
|
23
31
|
L.syncDoc(main, st);
|
|
24
32
|
const d = L.stageDurations(st);
|
|
25
33
|
const next = L.sw(st, 'dev') ? '개발(task-dev)' : '테스트(task-test)';
|
|
26
|
-
|
|
34
|
+
const fileNote = L.sw(st, 'dev') ? `, 계획 파일 ${files.length}개` : '';
|
|
35
|
+
return `${L.taskLabel(num)} 계획을 기록했다 — 시작·기획 ${L.minutes(d.plan)}${fileNote}.\n다음: ${next}.`;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
// 등록된 코드 테스트를 dir에서 차례로 실행한다. 실패하면 멈춤 오류를 던진다
|
|
@@ -92,6 +101,13 @@ async function testCode(ctx, a) {
|
|
|
92
101
|
st.testCode = { at: L.nowIso(), fingerprint: L.fingerprint(dir) };
|
|
93
102
|
L.writeState(main, st);
|
|
94
103
|
L.syncDoc(main, st);
|
|
104
|
+
L.appendResult(
|
|
105
|
+
main,
|
|
106
|
+
st,
|
|
107
|
+
result.none
|
|
108
|
+
? `- 코드 테스트 · 등록된 명령 없음 · ${L.clock(st.testCode.at)}`
|
|
109
|
+
: `- 코드 테스트 · 통과 · 명령 ${result.outputs.length}개 · ${L.minutes(result.ms)} · ${L.clock(st.testCode.at)}`,
|
|
110
|
+
);
|
|
95
111
|
lines.push(`${L.taskLabel(st.num)} 개발 칸을 기록했다 — 개발 ${L.minutes(L.stageDurations(st).dev)}.`);
|
|
96
112
|
}
|
|
97
113
|
if (!result.none) lines.push(overNotice(result.ms, cmds).trim());
|
|
@@ -112,7 +128,7 @@ async function testScenario(ctx, a) {
|
|
|
112
128
|
if (st.closed) L.fail(`${L.taskLabel(num)}은 이미 닫혔다.`);
|
|
113
129
|
const criteria = L.parseCriteria(L.readDoc(main, st));
|
|
114
130
|
const n = parseInt(a.number, 10);
|
|
115
|
-
if (!criteria.length) L.fail('test-scenario: 태스크 문서 `## 완료
|
|
131
|
+
if (!criteria.length) L.fail('test-scenario: 태스크 문서 `## 테스트 계획`에 완료 기준이 없다.');
|
|
116
132
|
if (!(n >= 1 && n <= criteria.length)) L.fail(`test-scenario: 시나리오 번호는 1~${criteria.length} 중 하나다 (받음: '${a.number}').`);
|
|
117
133
|
const result = String(a.result || '').toLowerCase();
|
|
118
134
|
if (!RESULTS.includes(result)) L.fail("test-scenario: 결과는 pass · fail · accept 중 하나다.");
|
package/bin/commands.js
CHANGED
|
@@ -11,7 +11,7 @@ const { orcaDispatchTask, reportTask, waitReports } = require('./orche');
|
|
|
11
11
|
|
|
12
12
|
// 인자 종류: positional(순서대로) · string(--이름 값) · bool(--이름) · list(--이름 값 값 …)
|
|
13
13
|
const TASK = { name: 'task', positional: true, desc: '태스크 번호 (TASK-012 또는 12)' };
|
|
14
|
-
const RANGE = { name: 'range', desc: '사용자가 새로 말한 진행 범위 — 태스크
|
|
14
|
+
const RANGE = { name: 'range', desc: '사용자가 새로 말한 진행 범위 — 태스크 문서 헤더 표의 범위 칸만 바꾼다' };
|
|
15
15
|
|
|
16
16
|
const COMMANDS = [
|
|
17
17
|
{
|
|
@@ -50,7 +50,7 @@ const COMMANDS = [
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
name: 'approve-plan',
|
|
53
|
-
summary: '태스크 문서(
|
|
53
|
+
summary: '태스크 문서(요구사항·개발 계획·완료 기준)를 검사하고 계획 끝을 기록한다',
|
|
54
54
|
args: [TASK, RANGE],
|
|
55
55
|
run: approvePlan,
|
|
56
56
|
},
|
|
@@ -113,7 +113,7 @@ const COMMANDS = [
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
name: 'orca-dispatch-task',
|
|
116
|
-
summary: 'Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(Orca 전용). 탭 handle은
|
|
116
|
+
summary: 'Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(Orca 전용). 탭 handle은 .state에 기록한다',
|
|
117
117
|
args: [
|
|
118
118
|
TASK,
|
|
119
119
|
{ name: 'agent', desc: 'claude · codex (필수)' },
|
|
@@ -137,7 +137,7 @@ const COMMANDS = [
|
|
|
137
137
|
},
|
|
138
138
|
];
|
|
139
139
|
|
|
140
|
-
// 명령 실행 공통 — 본진을 찾고, --range가 있으면
|
|
140
|
+
// 명령 실행 공통 — 본진을 찾고, --range가 있으면 범위를 먼저 갱신한다
|
|
141
141
|
// ask는 묻는 명령(prune)에 CLI가 넘기는 질문 함수다. MCP에는 없다
|
|
142
142
|
async function execute(cmd, args, { cwd = process.cwd(), main = null, ask = null } = {}) {
|
|
143
143
|
const ctx = { cwd, main: main || L.findMain(cwd), ask };
|
package/bin/finish.js
CHANGED
|
@@ -40,7 +40,7 @@ async function verifyClose(ctx, a) {
|
|
|
40
40
|
}
|
|
41
41
|
if (L.sw(st, 'test')) {
|
|
42
42
|
const criteria = L.parseCriteria(L.readDoc(main, st));
|
|
43
|
-
if (!criteria.length) problems.push('`## 완료
|
|
43
|
+
if (!criteria.length) problems.push('`## 테스트 계획`에 완료 기준이 없다');
|
|
44
44
|
for (let i = 1; i <= criteria.length; i++) {
|
|
45
45
|
const last = lastEntry(st, i);
|
|
46
46
|
if (!last) problems.push(`시나리오 ${i}의 결과·증거가 없다 — test-scenario로 기록한다`);
|
|
@@ -106,11 +106,14 @@ async function commitTask(ctx, a) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
const
|
|
109
|
+
const before = st.commit;
|
|
110
|
+
const prev = (before && before.commits) || [];
|
|
110
111
|
const commits = [...prev, ...made];
|
|
111
112
|
const noCodeChange = !L.sw(st, 'dev') || (st.noBranch ? commits.length === 0 : !hasCodeChange(main, st));
|
|
112
113
|
st.commit = { at: L.nowIso(), noCodeChange, commits };
|
|
113
114
|
L.writeState(main, st);
|
|
115
|
+
for (const m of made) L.appendResult(main, st, `- 커밋 · ${m.subject}`);
|
|
116
|
+
if (L.sw(st, 'dev') && noCodeChange && !before) L.appendResult(main, st, '- 커밋 · 코드 변경 없음');
|
|
114
117
|
|
|
115
118
|
// 병합 확인 화면 — 커밋 목록·바뀐 파일·단계별 시간·계획 확인 뒤 추가된 파일
|
|
116
119
|
let changed;
|
|
@@ -199,6 +202,7 @@ async function mergeTask(ctx, a) {
|
|
|
199
202
|
if (st.merge && st.merge.commit) return `${L.taskLabel(st.num)}은 이미 병합했다 (${st.merge.commit.slice(0, 7)}).`;
|
|
200
203
|
|
|
201
204
|
if (L.needsNoMerge(st)) {
|
|
205
|
+
if (st.merge && st.merge.skipped) return `${L.taskLabel(st.num)} 병합은 이미 건너뛰었다(${st.merge.skipped}).\n다음: 태스크를 연 주인이 본진에서 close-task를 부른다.`;
|
|
202
206
|
let note = '';
|
|
203
207
|
if (st.noWorktree && L.currentBranch(main) !== st.parent) {
|
|
204
208
|
L.git(main, ['checkout', st.parent]);
|
|
@@ -207,6 +211,7 @@ async function mergeTask(ctx, a) {
|
|
|
207
211
|
const why = !L.sw(st, 'dev') ? '개발 꺼짐' : st.noBranch ? '브랜치 생략' : '코드 변경 없음';
|
|
208
212
|
st.merge = { at: L.nowIso(), skipped: why };
|
|
209
213
|
L.writeState(main, st);
|
|
214
|
+
L.appendResult(main, st, `- 병합 · 건너뜀(${why}) · ${L.clock(st.merge.at)}`);
|
|
210
215
|
return `${L.taskLabel(st.num)} 병합을 건너뛴다(${why}).${note}\n다음: 태스크를 연 주인이 본진에서 close-task를 부른다.`;
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -233,6 +238,12 @@ async function mergeTask(ctx, a) {
|
|
|
233
238
|
st.testCode.fingerprint = fp;
|
|
234
239
|
L.writeState(main, st);
|
|
235
240
|
notes.push(r.none ? '부모의 새 커밋을 받았다 — 코드 테스트 없음으로 등록된 리포.' : `부모의 새 커밋을 받아 코드 테스트를 다시 돌렸다 — 통과(${L.minutes(r.ms)}).`);
|
|
241
|
+
const at = L.clock(L.nowIso());
|
|
242
|
+
L.appendResult(
|
|
243
|
+
main,
|
|
244
|
+
st,
|
|
245
|
+
r.none ? `- 코드 테스트 · 부모 새 커밋 받음 · 등록된 명령 없음 · ${at}` : `- 코드 테스트 · 부모 새 커밋 받아 다시 통과 · ${L.minutes(r.ms)} · ${at}`,
|
|
246
|
+
);
|
|
236
247
|
}
|
|
237
248
|
if (st.noWorktree) L.git(main, ['checkout', st.parent]);
|
|
238
249
|
const args = rule.merge === 'ff-only' ? ['merge', '--ff-only', st.branch] : ['merge', '--no-ff', '--no-edit', st.branch];
|
|
@@ -240,6 +251,7 @@ async function mergeTask(ctx, a) {
|
|
|
240
251
|
const head = L.git(main, ['rev-parse', 'HEAD']);
|
|
241
252
|
st.merge = { at: L.nowIso(), commit: head };
|
|
242
253
|
L.writeState(main, st);
|
|
254
|
+
L.appendResult(main, st, `- 병합 · ${st.parent} ← ${head.slice(0, 7)} · ${L.clock(st.merge.at)}`);
|
|
243
255
|
return [
|
|
244
256
|
`${L.taskLabel(st.num)}을 ${st.parent}에 병합했다(${rule.merge}, ${head.slice(0, 7)}).`,
|
|
245
257
|
...notes,
|
package/bin/lib.js
CHANGED
|
@@ -343,19 +343,41 @@ function readDoc(main, st) {
|
|
|
343
343
|
return fs.readFileSync(file, 'utf8');
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
//
|
|
346
|
+
// 줄마다 코드 블록 안인지 — ``` 또는 ~~~로 시작하는 줄이 블록을 열고 닫는다. 닫히지 않은 블록은 문서 끝에서 닫힌 것으로 본다
|
|
347
|
+
function fenceMask(lines) {
|
|
348
|
+
const mask = [];
|
|
349
|
+
let open = false;
|
|
350
|
+
for (const l of lines) {
|
|
351
|
+
if (/^\s*(```|~~~)/.test(l)) {
|
|
352
|
+
mask.push(true);
|
|
353
|
+
open = !open;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
mask.push(open);
|
|
357
|
+
}
|
|
358
|
+
return mask;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// `## 제목` 절 본문(다음 `## `까지). 코드 블록 안의 `## ` 줄은 절 제목으로 보지 않는다
|
|
347
362
|
function section(text, title) {
|
|
348
363
|
const lines = text.split('\n');
|
|
349
|
-
const
|
|
364
|
+
const fenced = fenceMask(lines);
|
|
365
|
+
const start = lines.findIndex((l, i) => !fenced[i] && l.trim() === `## ${title}`);
|
|
350
366
|
if (start === -1) return null;
|
|
351
367
|
const out = [];
|
|
352
368
|
for (let i = start + 1; i < lines.length; i++) {
|
|
353
|
-
if (/^## /.test(lines[i])) break;
|
|
369
|
+
if (!fenced[i] && /^## /.test(lines[i])) break;
|
|
354
370
|
out.push(lines[i]);
|
|
355
371
|
}
|
|
356
372
|
return out.join('\n');
|
|
357
373
|
}
|
|
358
374
|
|
|
375
|
+
// 1.0.1 문서 = `## 개발 계획` 또는 `## 테스트 계획` 절이 있다(renderDoc이 둘 중 하나는 반드시 만든다).
|
|
376
|
+
// 없으면 1.0.0 문서(목표·완료 기준·만질 파일·Phase 절)로 읽는다
|
|
377
|
+
function isNewDoc(text) {
|
|
378
|
+
return section(text, '개발 계획') !== null || section(text, '테스트 계획') !== null;
|
|
379
|
+
}
|
|
380
|
+
|
|
359
381
|
function meaningfulLines(body) {
|
|
360
382
|
return (body || '')
|
|
361
383
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
@@ -380,9 +402,10 @@ function parseFileList(body) {
|
|
|
380
402
|
.filter(Boolean);
|
|
381
403
|
}
|
|
382
404
|
|
|
383
|
-
//
|
|
405
|
+
// Phase: `### Phase N — 이름` + `- 파일:`(한 줄 쉼표·가운뎃점 구분 또는 들여쓴 목록) + `- 사유:`
|
|
406
|
+
// 1.0.1 문서는 `## 개발 계획` 안에, 1.0.0 문서는 `## Phase` 절에 있다
|
|
384
407
|
function parsePhases(text) {
|
|
385
|
-
const body = section(text, 'Phase');
|
|
408
|
+
const body = section(text, isNewDoc(text) ? '개발 계획' : 'Phase');
|
|
386
409
|
if (!body) return [];
|
|
387
410
|
const phases = [];
|
|
388
411
|
let cur = null;
|
|
@@ -422,16 +445,35 @@ function parsePhases(text) {
|
|
|
422
445
|
|
|
423
446
|
// 완료 기준 — `[AUTO]`·`[USER]`가 붙은 줄을 차례로 1번부터 센다
|
|
424
447
|
function parseCriteria(text) {
|
|
425
|
-
return meaningfulLines(section(text, '완료 기준')).filter((l) =>
|
|
448
|
+
return meaningfulLines(section(text, isNewDoc(text) ? '테스트 계획' : '완료 기준')).filter((l) =>
|
|
426
449
|
/^(?:\d+[.)]|[-*])\s*\[(AUTO|USER)\]/.test(l),
|
|
427
450
|
);
|
|
428
451
|
}
|
|
429
452
|
|
|
453
|
+
// 목표 = 1.0.1은 `## 요구사항` 첫 줄, 1.0.0은 `## 목표` 첫 줄(1.0.0 medium은 두 절이 다 있다)
|
|
430
454
|
function goalLine(text) {
|
|
431
|
-
return meaningfulLines(section(text, '목표'))[0] || '';
|
|
455
|
+
return meaningfulLines(section(text, isNewDoc(text) ? '요구사항' : '목표'))[0] || '';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// approve-plan의 목표 검사 — 문제가 없으면 null
|
|
459
|
+
function goalProblem(text) {
|
|
460
|
+
const goal = goalLine(text);
|
|
461
|
+
if (!isNewDoc(text)) return goal ? null : '`## 목표`가 비어 있다';
|
|
462
|
+
if (!goal) return '`## 요구사항` 첫 줄(목표)이 비어 있다 — 무엇을 왜 하는지 한 문장';
|
|
463
|
+
if (/^(?:[-*]|\d+[.)])\s/.test(goal)) return '`## 요구사항` 첫 줄은 목록 기호 없는 목표 한 문장이어야 한다(무엇을 왜) — 요구 목록은 그 아래에';
|
|
464
|
+
return null;
|
|
432
465
|
}
|
|
433
466
|
|
|
467
|
+
// 계획 파일 — 1.0.1은 `## 개발 계획`의 Phase `- 파일:`(Phase가 있으면 그것만), 없으면 목록.
|
|
468
|
+
// 1.0.0은 `## 만질 파일` + `## Phase`
|
|
434
469
|
function plannedFiles(text) {
|
|
470
|
+
if (isNewDoc(text)) {
|
|
471
|
+
const phases = parsePhases(text);
|
|
472
|
+
if (!phases.length) return parseFileList(section(text, '개발 계획'));
|
|
473
|
+
const files = [];
|
|
474
|
+
for (const p of phases) for (const f of p.files) if (!files.includes(f)) files.push(f);
|
|
475
|
+
return files;
|
|
476
|
+
}
|
|
435
477
|
const files = parseFileList(section(text, '만질 파일'));
|
|
436
478
|
for (const p of parsePhases(text)) for (const f of p.files) if (!files.includes(f)) files.push(f);
|
|
437
479
|
return files;
|
|
@@ -443,22 +485,29 @@ function fileMatches(file, planned) {
|
|
|
443
485
|
return file === base || file.startsWith(base + '/');
|
|
444
486
|
}
|
|
445
487
|
|
|
446
|
-
|
|
447
|
-
|
|
488
|
+
// ─── 헤더 표 ────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
const HEADER_HEAD = '| 생성일 | 플랜 | 유형 | 크기 | 스위치 | 범위 | 부모 브랜치 | 브랜치 | 상태 |';
|
|
491
|
+
const HEADER_SEP = '|---|---|---|---|---|---|---|---|---|';
|
|
492
|
+
const SWITCH_LABEL = { plan: '기획', dev: '개발', test: '테스트' };
|
|
493
|
+
|
|
494
|
+
function cell(v) {
|
|
495
|
+
return String(v == null ? '' : v).replace(/\r?\n/g, ' ').replace(/\|/g, '\\|');
|
|
448
496
|
}
|
|
449
497
|
|
|
450
|
-
function
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
498
|
+
function renderHeaderRow(st) {
|
|
499
|
+
const cells = [
|
|
500
|
+
clock(st.times.prepare).slice(0, 10),
|
|
501
|
+
st.item ? `${st.plan} (항목 ${st.item})` : st.plan,
|
|
502
|
+
st.type,
|
|
503
|
+
st.size,
|
|
504
|
+
['plan', 'dev', 'test'].filter((n) => sw(st, n)).map((n) => SWITCH_LABEL[n]).join('·'),
|
|
505
|
+
st.range,
|
|
506
|
+
st.parent,
|
|
507
|
+
st.noBranch ? '분기 없음' : st.branch,
|
|
508
|
+
!st.closed ? '열림' : st.closed.finished ? '닫힘(완료)' : '닫힘(포기)',
|
|
459
509
|
];
|
|
460
|
-
|
|
461
|
-
return `<!-- taskery: ${parts.join(' ')} -->`;
|
|
510
|
+
return `| ${cells.map(cell).join(' | ')} |`;
|
|
462
511
|
}
|
|
463
512
|
|
|
464
513
|
// ─── 시간 · 단계 표 ─────────────────────────────────
|
|
@@ -517,44 +566,60 @@ function renderTableRow(st) {
|
|
|
517
566
|
return `| ${stageCells(st).join(' | ')} |`;
|
|
518
567
|
}
|
|
519
568
|
|
|
520
|
-
//
|
|
569
|
+
// 문서 머리(첫 `## ` 앞)에서 표 머리 줄을 찾는다
|
|
570
|
+
function headIndex(lines, head) {
|
|
571
|
+
let end = lines.findIndex((l) => /^## /.test(l));
|
|
572
|
+
if (end === -1) end = lines.length;
|
|
573
|
+
for (let i = 0; i < end; i++) {
|
|
574
|
+
if (lines[i].trim() === head) return lines[i + 2] !== undefined && i + 2 < end && lines[i + 2].trim().startsWith('|') ? i : -1;
|
|
575
|
+
}
|
|
576
|
+
return -1;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 문서의 헤더 표와 단계 표 줄을 .state 기록으로 다시 쓴다(두 표는 명령만 쓴다).
|
|
580
|
+
// 1.0.0 문서는 제목 바로 아래 메타 줄을 헤더 표로 바꾼다
|
|
521
581
|
function syncDoc(main, st) {
|
|
522
582
|
const file = docAbs(main, st);
|
|
523
583
|
if (!fs.existsSync(file)) return;
|
|
524
584
|
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const hi = lines.findIndex((l) => l.trim() === TABLE_HEAD);
|
|
528
|
-
if (hi !== -1 && lines[hi + 2] !== undefined && lines[hi + 2].trim().startsWith('|')) {
|
|
529
|
-
lines[hi + 2] = renderTableRow(st);
|
|
585
|
+
if (headIndex(lines, HEADER_HEAD) === -1 && lines[1] !== undefined && lines[1].startsWith('<!-- taskery:')) {
|
|
586
|
+
lines.splice(1, 1, '', HEADER_HEAD, HEADER_SEP, renderHeaderRow(st));
|
|
530
587
|
}
|
|
588
|
+
const hi = headIndex(lines, HEADER_HEAD);
|
|
589
|
+
if (hi !== -1) lines[hi + 2] = renderHeaderRow(st);
|
|
590
|
+
const ti = headIndex(lines, TABLE_HEAD);
|
|
591
|
+
if (ti !== -1) lines[ti + 2] = renderTableRow(st);
|
|
531
592
|
fs.writeFileSync(file, lines.join('\n'));
|
|
532
593
|
}
|
|
533
594
|
|
|
534
595
|
function renderDoc(st) {
|
|
535
596
|
const out = [
|
|
536
597
|
`# ${taskLabel(st.num)} ${st.title}`,
|
|
537
|
-
|
|
598
|
+
'',
|
|
599
|
+
HEADER_HEAD,
|
|
600
|
+
HEADER_SEP,
|
|
601
|
+
renderHeaderRow(st),
|
|
538
602
|
'',
|
|
539
603
|
TABLE_HEAD,
|
|
540
604
|
'|---|---|---|---|',
|
|
541
605
|
renderTableRow(st),
|
|
542
606
|
'',
|
|
543
|
-
'##
|
|
607
|
+
'## 요구사항',
|
|
544
608
|
'',
|
|
545
609
|
];
|
|
546
|
-
if (st
|
|
547
|
-
|
|
548
|
-
if (st.size !== 'small') out.push('## Phase', '');
|
|
610
|
+
if (sw(st, 'dev')) out.push('## 개발 계획', '');
|
|
611
|
+
if (sw(st, 'test')) out.push('## 테스트 계획', '');
|
|
549
612
|
out.push('## 결정', '', '## 결과', '');
|
|
550
613
|
return out.join('\n');
|
|
551
614
|
}
|
|
552
615
|
|
|
553
|
-
// `## 결과` 절 끝에 한 줄을 붙인다
|
|
616
|
+
// `## 결과` 절 끝에 한 줄을 붙인다(명령만 쓴다). 문서가 없으면 아무것도 하지 않는다
|
|
554
617
|
function appendResult(main, st, line) {
|
|
555
618
|
const file = docAbs(main, st);
|
|
619
|
+
if (!fs.existsSync(file)) return;
|
|
556
620
|
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
557
|
-
|
|
621
|
+
const fenced = fenceMask(lines);
|
|
622
|
+
let start = lines.findIndex((l, i) => !fenced[i] && l.trim() === '## 결과');
|
|
558
623
|
if (start === -1) {
|
|
559
624
|
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
560
625
|
lines.push('', '## 결과', '');
|
|
@@ -562,7 +627,7 @@ function appendResult(main, st, line) {
|
|
|
562
627
|
}
|
|
563
628
|
let end = lines.length;
|
|
564
629
|
for (let i = start + 1; i < lines.length; i++) {
|
|
565
|
-
if (/^## /.test(lines[i])) {
|
|
630
|
+
if (!fenced[i] && /^## /.test(lines[i])) {
|
|
566
631
|
end = i;
|
|
567
632
|
break;
|
|
568
633
|
}
|
|
@@ -591,7 +656,7 @@ function todayLocal() {
|
|
|
591
656
|
return clock(nowIso()).slice(0, 10);
|
|
592
657
|
}
|
|
593
658
|
|
|
594
|
-
// 범위
|
|
659
|
+
// 범위 갱신 — 태스크 명령에 --range를 붙이면 헤더 표의 범위 칸만 바꾼다(§3-5)
|
|
595
660
|
function updateRange(main, st, range) {
|
|
596
661
|
if (!range || range === st.range) return st;
|
|
597
662
|
st.range = range;
|
|
@@ -652,14 +717,16 @@ module.exports = {
|
|
|
652
717
|
docShown,
|
|
653
718
|
readDoc,
|
|
654
719
|
section,
|
|
720
|
+
isNewDoc,
|
|
655
721
|
meaningfulLines,
|
|
656
722
|
parseFileList,
|
|
657
723
|
parsePhases,
|
|
658
724
|
parseCriteria,
|
|
659
725
|
goalLine,
|
|
726
|
+
goalProblem,
|
|
660
727
|
plannedFiles,
|
|
661
728
|
fileMatches,
|
|
662
|
-
|
|
729
|
+
renderHeaderRow,
|
|
663
730
|
stageDurations,
|
|
664
731
|
stageCells,
|
|
665
732
|
minutes,
|
package/bin/orche.js
CHANGED
|
@@ -125,7 +125,7 @@ async function orcaDispatchTask(ctx, a) {
|
|
|
125
125
|
const handle = created && created.terminal && created.terminal.handle;
|
|
126
126
|
if (!handle) L.fail(`orca-dispatch-task: ${label} 탭을 열었지만 Orca가 탭 handle을 돌려주지 않았다. 'orca terminal list'로 확인한다.`);
|
|
127
127
|
|
|
128
|
-
// 탭 handle을
|
|
128
|
+
// 탭 handle을 .state에 기록한다 — 첫 지시문을 보내기 전이라 태스크 세션의 기록과 겹치지 않는다
|
|
129
129
|
const fresh = L.readState(main, num);
|
|
130
130
|
fresh.tab = handle;
|
|
131
131
|
L.writeState(main, fresh);
|
package/bin/prepare.js
CHANGED
|
@@ -293,7 +293,10 @@ async function prepareTask(ctx, a) {
|
|
|
293
293
|
if (a.item != null) st.item = parseInt(a.item, 10);
|
|
294
294
|
const fromNotes = fromNums.length ? B.linkTasks(main, fromNums, L.taskLabel(num)) : [];
|
|
295
295
|
if (fromNums.length) st.from = fromNums.map((n) => `BL-${n}`);
|
|
296
|
-
if (a.item != null || fromNums.length)
|
|
296
|
+
if (a.item != null || fromNums.length) {
|
|
297
|
+
L.writeState(main, st);
|
|
298
|
+
L.syncDoc(main, st);
|
|
299
|
+
}
|
|
297
300
|
|
|
298
301
|
const out = [
|
|
299
302
|
`${L.taskLabel(num)} 「${st.title}」을 열었다.`,
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## 항목 틀
|
|
11
11
|
|
|
12
|
-
명령은 아래 코드 블록을 읽어 `{num}`(세 자리 번호)·`{title}`(태스크 제목)·`{date}`(닫은 날)·`{type}`(유형)·`{goal}`(`##
|
|
12
|
+
명령은 아래 코드 블록을 읽어 `{num}`(세 자리 번호)·`{title}`(태스크 제목)·`{date}`(닫은 날)·`{type}`(유형)·`{goal}`(`## 요구사항` 첫 줄)을 채운다. 틀을 고치면 다음 항목부터 그 모양으로 적힌다.
|
|
13
13
|
|
|
14
14
|
```markdown
|
|
15
15
|
## [TASK-{num}] {title}
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
| `backlog-add "<제목>" [--type <종류>]` | 백로그 번호를 발급해 `BACKLOG.md` 열린 항목 맨 위에 빈 양식 |
|
|
45
45
|
| `backlog-get [BL-번호]` | 번호를 주면 그 항목 전문, 없으면 열린 항목 목록 |
|
|
46
46
|
| `backlog-mark <BL-번호> <TASK>` | `--from` 없이 연 태스크를 백로그 항목에 연결 |
|
|
47
|
-
| `orca-dispatch-task <TASK> --agent claude\|codex --model <모델> [--note "<한 줄>"]` | 오케스트레이션이 Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(Orca 전용). 탭 handle은
|
|
47
|
+
| `orca-dispatch-task <TASK> --agent claude\|codex --model <모델> [--note "<한 줄>"]` | 오케스트레이션이 Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(Orca 전용). 탭 handle은 `.state`에 남는다 |
|
|
48
48
|
| `report-task <TASK> "<한 줄>"` | 태스크 세션이 오케스트레이션에 보고 한 줄을 남긴다(`.taskery/reports.log`) |
|
|
49
49
|
| `wait-reports` | 오케스트레이션이 백그라운드 셸로 걸어 두는 대기. 안 읽은 보고가 생기면 출력하고 끝나고, 보고 없이 10분이면 오래 조용한 태스크 탭 목록과 함께 끝난다 (CLI만) |
|
|
50
50
|
| `init` · `update` · `add <claude\|codex>` | 설치 · 갱신 · 플랫폼 추가 (CLI만) |
|
|
51
51
|
|
|
52
|
-
태스크 명령(`approve-plan`~`merge-task`)에 `--range "<새 범위>"`를 붙이면 태스크
|
|
52
|
+
태스크 명령(`approve-plan`~`merge-task`)에 `--range "<새 범위>"`를 붙이면 태스크 문서 헤더 표의 범위 칸만 바뀐다.
|
|
53
53
|
|
|
54
54
|
## 4. 코드 테스트와 실사용 테스트
|
|
55
55
|
|
|
@@ -8,41 +8,59 @@
|
|
|
8
8
|
|
|
9
9
|
```markdown
|
|
10
10
|
# TASK-012 설정 화면 저장 버튼 색 변경
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
| 생성일 | 플랜 | 유형 | 크기 | 스위치 | 범위 | 부모 브랜치 | 브랜치 | 상태 |
|
|
13
|
+
|---|---|---|---|---|---|---|---|---|
|
|
14
|
+
| 2026-10-04 | 001_mvp (항목 3) | improve | small | 기획·개발·테스트 | 끝까지 | dev | improve/claude_TASK-012_save-button-color | 열림 |
|
|
12
15
|
|
|
13
16
|
| 시작·기획 | 개발 | 테스트 | 닫기 |
|
|
14
17
|
|---|---|---|---|
|
|
15
18
|
| ✅ 2분 | ✅ 3분 | ⏳ | – |
|
|
16
19
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
## 요구사항
|
|
21
|
+
저장 버튼이 다른 버튼과 구분되지 않아 눈에 띄는 파란색으로 바꾼다.
|
|
22
|
+
|
|
23
|
+
- 사용자 요구: 저장 버튼이 바로 눈에 띄어야 한다.
|
|
24
|
+
- 다크 모드에서도 구분돼야 한다.
|
|
25
|
+
- 하지 않는 것: 다른 버튼 색, 버튼 배치.
|
|
26
|
+
|
|
27
|
+
## 개발 계획
|
|
28
|
+
- `src/ui/SettingsView.tsx` — 저장 버튼 색을 디자인 토큰 primary로
|
|
29
|
+
|
|
30
|
+
## 테스트 계획
|
|
31
|
+
1. [AUTO] 설정 화면 → 저장 버튼을 본다 → 버튼이 파란색으로 보인다
|
|
32
|
+
|
|
20
33
|
## 결정
|
|
34
|
+
- 파란색은 디자인 토큰 primary를 쓴다 — 다크 모드에서도 대비가 유지된다
|
|
35
|
+
|
|
21
36
|
## 결과
|
|
37
|
+
- 코드 테스트 · 통과 · 명령 2개 · 1분 · 2026-10-04 14:10
|
|
38
|
+
- 시나리오 1 · PASS · 캡처 screenshots/save-button.png · 2026-10-04 14:20
|
|
39
|
+
- 커밋 · improve: [TASK-012] 설정 화면 저장 버튼 색 변경
|
|
40
|
+
- 병합 · dev ← e71b0a4 · 2026-10-04 14:25
|
|
22
41
|
```
|
|
23
42
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
43
|
+
- **헤더 표와 단계 표는 명령이 쓴다.** 손대지 않는다. 범위가 바뀌면 다음 태스크 명령에 `--range "<새 범위>"`를 붙인다.
|
|
44
|
+
- 상태는 `열림`·`닫힘(완료)`·`닫힘(포기)` 셋이다. 분기 없이 일하는 태스크는 브랜치 칸이 `분기 없음`이다.
|
|
45
|
+
- 개발이 꺼진 태스크에는 `## 개발 계획`이, 테스트가 꺼진 태스크에는 `## 테스트 계획`이 없다.
|
|
46
|
+
- 짧게 쓰되 절마다 아래 2절의 내용은 빠뜨리지 않는다.
|
|
27
47
|
|
|
28
48
|
## 2. 절마다 쓰는 법
|
|
29
49
|
|
|
30
50
|
| 절 | 쓰는 법 |
|
|
31
51
|
|---|---|
|
|
32
|
-
| `##
|
|
33
|
-
| `##
|
|
34
|
-
| `##
|
|
35
|
-
| `##
|
|
36
|
-
| `##
|
|
37
|
-
| `## 결정` | 다른 선택지가 있었고 나중에 "왜 이렇게 했지"를 물을 만한 결정만 `무엇을 정했나 — 왜` 한 줄. 없으면 비운다 |
|
|
38
|
-
| `## 결과` | `test-scenario`가 시나리오 결과를 한 줄씩 붙인다. 직접 쓰지 않는다 |
|
|
52
|
+
| `## 요구사항` | **첫 줄은 목표** — 무엇을 왜 하는지 한 문장, 목록 기호 없이. 이 줄이 small 커밋의 사유와 변경 기록 요약이 된다. 그 아래 목록으로 사용자 요구, 정한 범위, 한계, 하지 않는 것을 적는다 |
|
|
53
|
+
| `## 개발 계획` | small은 한 줄에 `- 경로 — 무엇을` 하나. 폴더는 `src/auth/`처럼 끝에 `/`. medium·large는 아래 4절의 Phase |
|
|
54
|
+
| `## 테스트 계획` | 완료 기준(실사용 테스트 시나리오). 아래 3절 |
|
|
55
|
+
| `## 결정` | 다른 선택지가 있었고 나중에 "왜 이렇게 했지"를 물을 만한 결정만 `무엇을 정했나 — 왜` 한 줄로 적는다. 사용자가 고른 것도 적는다. 없으면 비운다 |
|
|
56
|
+
| `## 결과` | `test-code`·`test-scenario`·`commit-task`·`merge-task`가 한 줄씩 붙인다. 직접 쓰지 않는다 |
|
|
39
57
|
|
|
40
|
-
`approve-plan`은
|
|
58
|
+
`approve-plan`은 요구사항 첫 줄(목표)이 있는지, 개발이 켜져 있으면 개발 계획에 파일이 있는지, 테스트가 켜져 있으면 완료 기준이 1~5개인지 본다.
|
|
41
59
|
|
|
42
60
|
## 3. 완료 기준
|
|
43
61
|
|
|
44
62
|
```markdown
|
|
45
|
-
##
|
|
63
|
+
## 테스트 계획
|
|
46
64
|
1. [AUTO] 설정 화면 → 저장 버튼을 본다 → 버튼이 파란색으로 보인다
|
|
47
65
|
2. [USER] 설정 화면 → 이름을 바꾸고 저장 → 목록에 바뀐 이름이 보인다
|
|
48
66
|
```
|
|
@@ -54,10 +72,10 @@
|
|
|
54
72
|
- 무언가를 없애는 태스크는 대신 보여야 할 것을 함께 적는다.
|
|
55
73
|
- 이 태스크 브랜치에서 실제로 해 볼 수 있는 것만 적는다.
|
|
56
74
|
|
|
57
|
-
## 4. Phase (medium·large)
|
|
75
|
+
## 4. Phase (medium·large — `## 개발 계획` 안)
|
|
58
76
|
|
|
59
77
|
```markdown
|
|
60
|
-
##
|
|
78
|
+
## 개발 계획
|
|
61
79
|
### Phase 1 — 로그인 API
|
|
62
80
|
- 파일: `src/auth/login.ts`, `src/auth/session.ts`
|
|
63
81
|
- 사유: 토큰 인증으로 바꾼다
|
|
@@ -69,6 +87,7 @@
|
|
|
69
87
|
```
|
|
70
88
|
|
|
71
89
|
- `commit-task`가 Phase마다 커밋 하나를 만든다. 파일은 한 줄에 쉼표로, 또는 들여쓴 목록으로 적는다.
|
|
90
|
+
- Phase를 쓰면 계획 파일은 Phase의 `- 파일:`에서만 읽는다.
|
|
72
91
|
- 한 파일이 여러 Phase에 걸치지 않게 나눈다. 걸치면 앞 Phase 커밋에 들어가고 메시지에 "(Phase 1·2 변경 포함)"이 붙는다.
|
|
73
92
|
- 목록에 없는 파일(잠금 파일 등)은 마지막 Phase 커밋에 들어가고, 병합 확인 요약에 "계획 확인 뒤 추가된 파일"로 보인다.
|
|
74
93
|
- small은 Phase 없이 커밋 하나다.
|
|
@@ -81,4 +100,5 @@
|
|
|
81
100
|
|
|
82
101
|
| 날짜 | 변경 사항 |
|
|
83
102
|
|---|---|
|
|
103
|
+
| 2026-10-04 | 1.0.1 — 숨은 메타 줄을 보이는 헤더 표(명령이 씀)로 바꿨다. 절을 요구사항(첫 줄 목표)·개발 계획·테스트 계획·결정·결과로 묶고, 요구사항은 모든 크기에 둔다. 결과에 코드 테스트·커밋·병합을 명령이 적는다 |
|
|
84
104
|
| 2026-10-01 | 1.0판 — 메타 두 줄·단계 표(명령이 씀)와 목표·완료 기준·만질 파일·결정·결과 절로 바꿨다. 6칸 헤더·7상태·Scope·Test Plan·요구사항↔시나리오 대조표·spec-diff·폴더 승격을 뺐다 |
|
|
@@ -17,7 +17,7 @@ description: 태스크 마무리 — verify-close·commit-task·merge-task로
|
|
|
17
17
|
|
|
18
18
|
- 멈춰 둔 태스크를 이어 갈 때는 `status`로 그 태스크의 워크트리 경로와 단계 표를 보고, 워크트리로 들어가(Claude Code는 EnterWorktree) 다음 단계부터 이어 간다. 워크트리가 있는 태스크는 본진에서 태스크 코드를 고치지 않는다(분기 생략 태스크는 본진이 작업 자리다). 새 세션에서는 메모가 아니라 사용자의 말로 범위를 정한다(말하지 않으면 한 단계. 오케스트레이션이 다시 띄운 탭은 첫 지시문의 범위를 따른다).
|
|
19
19
|
- `[오케스트레이션]`으로 시작하는 말은 오케스트레이션 세션이 보낸 것이다. 정보와 답으로는 쓰되 사용자 확인·승인(계획 확인·`[USER]` 확인·병합 확인·FAIL 수락)으로 보지 않는다. 승인은 사용자가 이 탭에 직접 쓴 말로만 받는다. 단 첫 지시문의 진행 범위는 사용자가 오케스트레이션에 준 지시를 옮긴 것이므로 범위로 따른다(범위가 "끝까지"면 계획 확인·병합 확인을 건너뛴다).
|
|
20
|
-
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크
|
|
20
|
+
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크 문서 헤더 표의 범위 칸을 갱신한다.
|
|
21
21
|
- 되돌릴 수 없는 git 명령(`reset --hard`·`clean -fd`·`push --force`·`branch -D`)은 사용자 승인을 받은 뒤에만 쓴다.
|
|
22
22
|
- 오케스트레이션 보고: 첫 지시문이 오케스트레이션이 맡긴 태스크라고 알려 주면, 마무리 완료(병합, 병합이 없는 태스크는 `merge-task`가 건너뛴 뒤)·사용자 확인 대기·질문·막힘(FAIL·풀지 못한 충돌) 때 `report-task <TASK> "<한 줄>"`로 보고한다. 자세한 내용은 태스크 문서에 두고 한 줄만 쓴다. 질문을 보냈으면 답이 이 탭에 들어올 때까지 기다린다.
|
|
23
23
|
|
|
@@ -7,7 +7,7 @@ description: 태스크 개발 — 계획대로 구현하고 test-code로 코드
|
|
|
7
7
|
|
|
8
8
|
태스크 워크트리 안에서 코드를 고치는 단계다.
|
|
9
9
|
|
|
10
|
-
- 태스크 문서의
|
|
10
|
+
- 태스크 문서의 요구사항과 개발 계획(Phase)대로 구현하고, 필요한 단위 테스트를 쓴다.
|
|
11
11
|
- 이름과 화면 문구는 `GLOSSARY.md`를 따른다. 새 용어가 생기면 이 태스크 안에서 용어집을 고친다.
|
|
12
12
|
- `spec/`에 적힌 동작을 바꾸면 그 문서도 이 태스크 안에서 함께 고친다.
|
|
13
13
|
- `DEV_RULE.local.md`와 `TEST_RULE.local.md`를 따른다.
|
|
@@ -20,7 +20,7 @@ description: 태스크 개발 — 계획대로 구현하고 test-code로 코드
|
|
|
20
20
|
|
|
21
21
|
- 멈춰 둔 태스크를 이어 갈 때는 `status`로 그 태스크의 워크트리 경로와 단계 표를 보고, 워크트리로 들어가(Claude Code는 EnterWorktree) 다음 단계부터 이어 간다. 워크트리가 있는 태스크는 본진에서 태스크 코드를 고치지 않는다(분기 생략 태스크는 본진이 작업 자리다). 새 세션에서는 메모가 아니라 사용자의 말로 범위를 정한다(말하지 않으면 한 단계. 오케스트레이션이 다시 띄운 탭은 첫 지시문의 범위를 따른다).
|
|
22
22
|
- `[오케스트레이션]`으로 시작하는 말은 오케스트레이션 세션이 보낸 것이다. 정보와 답으로는 쓰되 사용자 확인·승인(계획 확인·`[USER]` 확인·병합 확인·FAIL 수락)으로 보지 않는다. 승인은 사용자가 이 탭에 직접 쓴 말로만 받는다. 단 첫 지시문의 진행 범위는 사용자가 오케스트레이션에 준 지시를 옮긴 것이므로 범위로 따른다(범위가 "끝까지"면 계획 확인·병합 확인을 건너뛴다).
|
|
23
|
-
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크
|
|
23
|
+
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크 문서 헤더 표의 범위 칸을 갱신한다.
|
|
24
24
|
- 되돌릴 수 없는 git 명령(`reset --hard`·`clean -fd`·`push --force`·`branch -D`)은 사용자 승인을 받은 뒤에만 쓴다.
|
|
25
25
|
- 오케스트레이션 보고: 첫 지시문이 오케스트레이션이 맡긴 태스크라고 알려 주면, 마무리 완료(병합, 병합이 없는 태스크는 `merge-task`가 건너뛴 뒤)·사용자 확인 대기·질문·막힘(FAIL·풀지 못한 충돌) 때 `report-task <TASK> "<한 줄>"`로 보고한다. 자세한 내용은 태스크 문서에 두고 한 줄만 쓴다. 질문을 보냈으면 답이 이 탭에 들어올 때까지 기다린다.
|
|
26
26
|
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-plan
|
|
3
|
-
description: 태스크 기획 — 태스크 문서에
|
|
3
|
+
description: 태스크 기획 — 태스크 문서에 요구사항(첫 줄 목표)·개발 계획·테스트 계획을 쓰고 approve-plan으로 계획 끝을 기록한다
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# task-plan
|
|
7
7
|
|
|
8
8
|
태스크 워크트리 안에서 태스크 문서를 채우는 단계다.
|
|
9
9
|
|
|
10
|
-
- 태스크 문서를 채운다:
|
|
11
|
-
- 기획 켜짐: 요구사항을 정리하고 모호한 것은 사용자에게 묻는다. 기획 꺼짐: 요청 내용으로
|
|
10
|
+
- 태스크 문서를 채운다: 요구사항(첫 줄은 목표 — 무엇을 왜 하는지 한 문장, 그 아래 사용자 요구·정한 범위·한계·하지 않는 것), 개발 계획(개발 켜짐일 때 — small은 파일 목록, medium·large는 Phase마다 파일 목록과 사유 한 줄), 테스트 계획(테스트 켜짐일 때 — 완료 기준).
|
|
11
|
+
- 기획 켜짐: 요구사항을 정리하고 모호한 것은 사용자에게 묻는다. 기획 꺼짐: 요청 내용으로 요구사항·개발 계획·테스트 계획을 짧게 적고 `approve-plan`을 부른 뒤 멈추지 않고 다음 단계로 이어 간다(범위가 한 단계여도 간략 기획은 따로 세지 않는다).
|
|
12
12
|
- 필요한 것만 읽는다: `PLAN.md`, 관련 `spec/` 문서, `GLOSSARY.md`, 태스크 문서 쓰는 법이 헷갈리면 `TASK_DOC_RULE.md`, 목업을 요청받았으면 `MOCKUP_RULE.md`.
|
|
13
13
|
- 완료 기준은 한 줄 `[AUTO|USER] 시작 → 행동 → 기대하는 끝 상태`로 최대 5개 적는다. 시나리오 하나에 의도 하나, 한 번의 실행 흐름이다. 기대값은 사용자가 화면에서 보는 결과로만 적고 실행 방법(쿼리·훅·명령)은 적지 않는다. 무언가를 없애는 태스크는 대신 보여야 할 것을 함께 적는다.
|
|
14
14
|
- 완료 기준은 이 태스크 브랜치에서 실제로 해 볼 수 있는 것만 적는다. 다른 태스크가 끝나야 되는 확인은 적지 않고 사용자에게 알린다.
|
|
15
15
|
- 한 파일이 여러 Phase에 걸치지 않게 Phase를 나눈다.
|
|
16
16
|
- 다른 선택지가 있었고 나중에 "왜 이렇게 했지"를 물을 만한 결정을 내리면 태스크 문서 `## 결정`에 `무엇을 정했나 — 왜` 한 줄로 적는다. 사용자가 고른 것도 적는다. 없으면 비워 둔다.
|
|
17
|
-
- 기획 켜짐이고 진행 범위가 이 단계에서 끝나면(범위를 말하지 않은 경우 포함)
|
|
17
|
+
- 기획 켜짐이고 진행 범위가 이 단계에서 끝나면(범위를 말하지 않은 경우 포함) 요구사항·개발 계획·테스트 계획을 보여 주고 사용자 확인을 받는다. 범위가 다음 단계로 이어지면("개발까지"·"끝까지" 등) 멈추지 않는다. 그다음 `approve-plan`을 부른다.
|
|
18
18
|
|
|
19
19
|
## 공통
|
|
20
20
|
|
|
21
21
|
- 멈춰 둔 태스크를 이어 갈 때는 `status`로 그 태스크의 워크트리 경로와 단계 표를 보고, 워크트리로 들어가(Claude Code는 EnterWorktree) 다음 단계부터 이어 간다. 워크트리가 있는 태스크는 본진에서 태스크 코드를 고치지 않는다(분기 생략 태스크는 본진이 작업 자리다). 새 세션에서는 메모가 아니라 사용자의 말로 범위를 정한다(말하지 않으면 한 단계. 오케스트레이션이 다시 띄운 탭은 첫 지시문의 범위를 따른다).
|
|
22
22
|
- `[오케스트레이션]`으로 시작하는 말은 오케스트레이션 세션이 보낸 것이다. 정보와 답으로는 쓰되 사용자 확인·승인(계획 확인·`[USER]` 확인·병합 확인·FAIL 수락)으로 보지 않는다. 승인은 사용자가 이 탭에 직접 쓴 말로만 받는다. 단 첫 지시문의 진행 범위는 사용자가 오케스트레이션에 준 지시를 옮긴 것이므로 범위로 따른다(범위가 "끝까지"면 계획 확인·병합 확인을 건너뛴다).
|
|
23
|
-
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크
|
|
23
|
+
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크 문서 헤더 표의 범위 칸을 갱신한다.
|
|
24
24
|
- 되돌릴 수 없는 git 명령(`reset --hard`·`clean -fd`·`push --force`·`branch -D`)은 사용자 승인을 받은 뒤에만 쓴다.
|
|
25
25
|
- 오케스트레이션 보고: 첫 지시문이 오케스트레이션이 맡긴 태스크라고 알려 주면, 마무리 완료(병합, 병합이 없는 태스크는 `merge-task`가 건너뛴 뒤)·사용자 확인 대기·질문·막힘(FAIL·풀지 못한 충돌) 때 `report-task <TASK> "<한 줄>"`로 보고한다. 자세한 내용은 태스크 문서에 두고 한 줄만 쓴다. 질문을 보냈으면 답이 이 탭에 들어올 때까지 기다린다.
|
|
26
26
|
|
|
@@ -7,7 +7,7 @@ description: 태스크 실사용 테스트 — 완료 기준 시나리오를 실
|
|
|
7
7
|
|
|
8
8
|
태스크 워크트리 안에서 태스크의 의도가 처음부터 끝까지 되는지 확인하는 단계다.
|
|
9
9
|
|
|
10
|
-
- 완료
|
|
10
|
+
- 테스트 계획의 완료 기준을 실제로 실행한다. 앱 실행 방법은 `TEST_RULE.local.md`를 따른다.
|
|
11
11
|
- 시나리오는 의도를 정면으로 해 본다. "빠져나갈 수 없다"가 의도면 실제로 빠져나가 보고, 버튼이 비활성인지 같은 우회 표시만 보고 PASS로 적지 않는다.
|
|
12
12
|
- 앱은 한 번 켜서 시나리오 전부를 확인한다. 켜기 전에 이미 떠 있는지 확인하고, 끌 때는 이 태스크가 띄운 것만 끈다. 이름으로 한꺼번에 죽이지 않는다 — 다른 세션의 프로세스까지 죽는다.
|
|
13
13
|
- 같은 관찰이 두 번 연속 기대와 다르면 더 재시도하지 않고 FAIL과 증거로 끝낸다. 원인 찾기는 개발 단계의 몫이다.
|
|
@@ -24,7 +24,7 @@ description: 태스크 실사용 테스트 — 완료 기준 시나리오를 실
|
|
|
24
24
|
|
|
25
25
|
- 멈춰 둔 태스크를 이어 갈 때는 `status`로 그 태스크의 워크트리 경로와 단계 표를 보고, 워크트리로 들어가(Claude Code는 EnterWorktree) 다음 단계부터 이어 간다. 워크트리가 있는 태스크는 본진에서 태스크 코드를 고치지 않는다(분기 생략 태스크는 본진이 작업 자리다). 새 세션에서는 메모가 아니라 사용자의 말로 범위를 정한다(말하지 않으면 한 단계. 오케스트레이션이 다시 띄운 탭은 첫 지시문의 범위를 따른다).
|
|
26
26
|
- `[오케스트레이션]`으로 시작하는 말은 오케스트레이션 세션이 보낸 것이다. 정보와 답으로는 쓰되 사용자 확인·승인(계획 확인·`[USER]` 확인·병합 확인·FAIL 수락)으로 보지 않는다. 승인은 사용자가 이 탭에 직접 쓴 말로만 받는다. 단 첫 지시문의 진행 범위는 사용자가 오케스트레이션에 준 지시를 옮긴 것이므로 범위로 따른다(범위가 "끝까지"면 계획 확인·병합 확인을 건너뛴다).
|
|
27
|
-
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크
|
|
27
|
+
- 진행 범위는 사용자의 가장 최근 지시를 따른다. 사용자가 범위를 새로 말하면 다음에 부르는 태스크 명령에 `--range "<새 범위>"`를 붙여 태스크 문서 헤더 표의 범위 칸을 갱신한다.
|
|
28
28
|
- 되돌릴 수 없는 git 명령(`reset --hard`·`clean -fd`·`push --force`·`branch -D`)은 사용자 승인을 받은 뒤에만 쓴다.
|
|
29
29
|
- 오케스트레이션 보고: 첫 지시문이 오케스트레이션이 맡긴 태스크라고 알려 주면, 마무리 완료(병합, 병합이 없는 태스크는 `merge-task`가 건너뛴 뒤)·사용자 확인 대기·질문·막힘(FAIL·풀지 못한 충돌) 때 `report-task <TASK> "<한 줄>"`로 보고한다. 자세한 내용은 태스크 문서에 두고 한 줄만 쓴다. 질문을 보냈으면 답이 이 탭에 들어올 때까지 기다린다.
|
|
30
30
|
|