@angar2/taskery 1.0.0 → 1.0.2
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 +34 -20
- package/bin/check.js +21 -5
- package/bin/commands.js +12 -8
- package/bin/finish.js +14 -2
- package/bin/lib.js +103 -36
- package/bin/orche.js +80 -30
- 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 +4 -3
- 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-orche/SKILL.md +5 -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
|
|
|
@@ -261,7 +275,7 @@ Node 프로젝트는 `package-lock.json`이 있으면 `npm ci`로 설치합니
|
|
|
261
275
|
|
|
262
276
|
## 오케스트레이션 — 한 줄 지시로 여러 태스크를 나눠 맡기기
|
|
263
277
|
|
|
264
|
-
**오케스트레이션** = 한 세션(관리 세션)이 백로그를 태스크로 나누고, 태스크마다 새 AI 탭을 띄워 맡기고, 보고를 받아 정리하는 방식입니다. Orca 안에서 씁니다.
|
|
278
|
+
**오케스트레이션** = 한 세션(관리 세션)이 백로그를 태스크로 나누고, 태스크마다 새 AI 탭을 띄워 맡기고, 보고를 받아 정리하는 방식입니다. Orca 안에서 씁니다. Orca를 쓸 수 없을 때는 사용자가 직접 세션을 열어 첫 지시를 붙여 넣는 방식으로도 진행할 수 있습니다.
|
|
265
279
|
|
|
266
280
|
```
|
|
267
281
|
👤 사용자
|
|
@@ -280,9 +294,9 @@ Node 프로젝트는 `package-lock.json`이 있으면 `npm ci`로 설치합니
|
|
|
280
294
|
|---|---|
|
|
281
295
|
| ① | 사용자가 관리 세션에 한 줄로 지시합니다. 예: "백로그 BL-1~3을 끝까지 진행해" |
|
|
282
296
|
| ② | 관리 세션이 백로그와 `PLAN.md`를 읽고 태스크로 나눕니다. 어떤 태스크를 어떤 순서와 AI로 진행할지 표로 보여 주고 확인받습니다. 지시에 이미 정해져 있으면 다시 묻지 않습니다 |
|
|
283
|
-
| ③ | 태스크마다 작업 폴더를 만들고(`prepare-task`), Orca 새 탭에 Claude나 Codex를 띄워 첫 지시를 보냅니다(`orca-dispatch-task`) |
|
|
297
|
+
| ③ | 태스크마다 작업 폴더를 만들고(`prepare-task`), Orca 새 탭에 Claude나 Codex를 띄워 첫 지시를 보냅니다(`orca-dispatch-task`). 첫 지시는 늘 같은 틀(기본 정보·작업 방식·이번 태스크 지시)로 나갑니다. Orca를 쓸 수 없거나 사용자가 원하면, 관리 세션이 첫 지시를 보여 주고 사용자가 직접 연 세션에 붙여 넣습니다 |
|
|
284
298
|
| ④ | 각 탭이 자기 작업 폴더에서 기획부터 마무리까지 진행합니다. 끝났을 때, 확인을 기다릴 때, 질문이 있을 때, 막혔을 때 보고함에 한 줄 남깁니다(`report-task`) |
|
|
285
|
-
| ⑤ | 관리 세션은 기다리는 동안 잠들어 있어 토큰을 쓰지 않습니다(`wait-reports`). 보고가 오거나
|
|
299
|
+
| ⑤ | 관리 세션은 기다리는 동안 잠들어 있어 토큰을 쓰지 않습니다(`wait-reports`). 보고가 오거나 60분이 지나면 깨어나 처리합니다 |
|
|
286
300
|
| ⑥ | 사용자 확인이 필요하면 관리 세션이 "TASK-108 탭에서 화면 확인해 주세요"라고 알립니다. 사용자는 그 탭에서 직접 답합니다 |
|
|
287
301
|
| ⑦ | 끝난 태스크는 병합 잠금 안에서 하나씩 dev에 병합됩니다(`merge-task`) |
|
|
288
302
|
| ⑧ | 관리 세션이 끝난 태스크의 작업 폴더와 탭을 치우고(`close-task`), 시작할 수 있는 다음 태스크로 빈자리를 채웁니다. 그리고 ③으로 돌아갑니다 |
|
|
@@ -331,7 +345,7 @@ Node 프로젝트는 `package-lock.json`이 있으면 `npm ci`로 설치합니
|
|
|
331
345
|
|
|
332
346
|
## 빠른 시작
|
|
333
347
|
|
|
334
|
-
> **준비물**: Node.js 18 이상, git 2.31 이상, 위 두 에이전트 중 하나 이상.
|
|
348
|
+
> **준비물**: Node.js 18 이상, git 2.31 이상, 위 두 에이전트 중 하나 이상. 오케스트레이션은 Orca를 기본으로 씁니다(Orca가 없으면 태스크 세션을 직접 엽니다).
|
|
335
349
|
|
|
336
350
|
### 1. 설치
|
|
337
351
|
|
|
@@ -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` | 마무리 단계 | 검사, 커밋, 병합을 하고, 태스크를 연 쪽이 작업 폴더를 정리합니다 |
|
|
@@ -476,7 +490,7 @@ taskery 파일은 그 리포에만 적용되는 git 무시 목록(`.git/info/exc
|
|
|
476
490
|
- PR로 병합하는 방식은 다루지 않습니다. 병합은 로컬에서만 합니다.
|
|
477
491
|
- 태스크 문서와 백로그는 git 이력에 남지 않습니다.
|
|
478
492
|
- 빌드 결과 복제는 macOS에서만 됩니다. 다른 환경에서는 평소처럼 빌드합니다.
|
|
479
|
-
-
|
|
493
|
+
- 오케스트레이션의 탭 자동 띄우기는 Orca 안에서만 됩니다. 그 밖에서는 사용자가 태스크 세션을 직접 열고 첫 지시를 붙여 넣습니다.
|
|
480
494
|
- Codex는 작업 폴더로 옮겨 갈 수 없습니다. Codex로 태스크를 하려면 오케스트레이션이 Codex 탭을 띄우거나, 사용자가 그 작업 폴더에서 Codex를 엽니다.
|
|
481
495
|
|
|
482
496
|
---
|
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,12 +113,16 @@ const COMMANDS = [
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
name: 'orca-dispatch-task',
|
|
116
|
-
summary: 'Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(
|
|
116
|
+
summary: 'Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(탭 handle은 .state에 기록). --manual이면 띄우지 않고 사용자가 붙여 넣을 첫 지시문을 출력한다',
|
|
117
117
|
args: [
|
|
118
118
|
TASK,
|
|
119
|
-
{ name: 'agent', desc: 'claude · codex (필수)' },
|
|
120
|
-
{ name: 'model', desc: '에이전트 모델 — 사용자가 정한 것 (필수)' },
|
|
121
|
-
{ name: '
|
|
119
|
+
{ name: 'agent', desc: 'claude · codex (--manual이 아니면 필수)' },
|
|
120
|
+
{ name: 'model', desc: '에이전트 모델 — 사용자가 정한 것 (--manual이 아니면 필수)' },
|
|
121
|
+
{ name: 'manual', type: 'bool', desc: '손 전달 — 탭을 띄우지 않고 작업 폴더·실행 안내·첫 지시문을 출력한다. Orca를 쓸 수 없거나 사용자가 태스크 세션을 직접 열겠다고 했을 때' },
|
|
122
|
+
{ name: 'scope', desc: "첫 지시문 '맡는 일' 칸 — 이번 태스크가 맡는 일" },
|
|
123
|
+
{ name: 'avoid', desc: "첫 지시문 '손대지 않는 것' 칸" },
|
|
124
|
+
{ name: 'read', desc: "첫 지시문 '먼저 읽을 것' 칸 — 태스크 문서·플랜 문서 말고 더 읽을 문서" },
|
|
125
|
+
{ name: 'extra', desc: "첫 지시문 '그 밖' 칸 — 위 칸에 들지 않는 이번 태스크만의 지시" },
|
|
122
126
|
],
|
|
123
127
|
run: orcaDispatchTask,
|
|
124
128
|
},
|
|
@@ -130,14 +134,14 @@ const COMMANDS = [
|
|
|
130
134
|
},
|
|
131
135
|
{
|
|
132
136
|
name: 'wait-reports',
|
|
133
|
-
summary: '안 읽은 보고가 생기면
|
|
137
|
+
summary: '안 읽은 보고가 생기면 출력하고 끝난다. 보고 없이 60분이면 오래 조용한 태스크 탭 목록과 함께 끝난다(백그라운드 셸로 건다)',
|
|
134
138
|
args: [],
|
|
135
139
|
cliOnly: true,
|
|
136
140
|
run: waitReports,
|
|
137
141
|
},
|
|
138
142
|
];
|
|
139
143
|
|
|
140
|
-
// 명령 실행 공통 — 본진을 찾고, --range가 있으면
|
|
144
|
+
// 명령 실행 공통 — 본진을 찾고, --range가 있으면 범위를 먼저 갱신한다
|
|
141
145
|
// ask는 묻는 명령(prune)에 CLI가 넘기는 질문 함수다. MCP에는 없다
|
|
142
146
|
async function execute(cmd, args, { cwd = process.cwd(), main = null, ask = null } = {}) {
|
|
143
147
|
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
|
@@ -6,7 +6,7 @@ const L = require('./lib');
|
|
|
6
6
|
|
|
7
7
|
const AGENTS = ['claude', 'codex'];
|
|
8
8
|
// 보고 없이 기다리는 시간이자 '오래 조용한 탭'의 기준. 환경 변수는 시험 전용 통로다
|
|
9
|
-
const WAIT_MS = Number(process.env.TASKERY_WAIT_REPORTS_MS) ||
|
|
9
|
+
const WAIT_MS = Number(process.env.TASKERY_WAIT_REPORTS_MS) || 60 * 60 * 1000;
|
|
10
10
|
const POLL_MS = Math.min(1000, WAIT_MS);
|
|
11
11
|
// 첫 지시문을 보낸 뒤 턴 시작을 지켜보는 시간(orca terminal send --wait-submit)
|
|
12
12
|
const SUBMIT_WAIT_SEC = 30;
|
|
@@ -45,10 +45,10 @@ function shq(v) {
|
|
|
45
45
|
return /^[A-Za-z0-9_.\/:=@%+-]+$/.test(v) ? v : `'${String(v).replace(/'/g, `'\\''`)}'`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// 에이전트 실행 명령 — 권한·승인 옵션은 붙이지 않는다(사용자 설정 그대로, I5)
|
|
48
|
+
// 에이전트 실행 명령 — 권한·승인 옵션은 붙이지 않는다(사용자 설정 그대로, I5). 모델이 없으면 모델 옵션을 뺀다
|
|
49
49
|
function agentCommand(main, agent, model) {
|
|
50
|
-
if (agent === 'claude') return `claude --model ${shq(model)}
|
|
51
|
-
return `codex
|
|
50
|
+
if (agent === 'claude') return model ? `claude --model ${shq(model)}` : 'claude';
|
|
51
|
+
return `codex ${model ? `-m ${shq(model)} ` : ''}--add-dir ${shq(path.join(main, '.taskery'))}`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// 단계 표에서 비어 있는 첫 단계의 스킬 — 새 태스크면 task-plan
|
|
@@ -63,19 +63,40 @@ function oneLine(text) {
|
|
|
63
63
|
return String(text).replace(/\s*\n\s*/g, ' ').trim();
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// 첫 지시문
|
|
67
|
-
|
|
66
|
+
// 첫 지시문 '■ 이번 태스크 지시' 칸 — 인자 이름과 칸 이름, 나오는 순서
|
|
67
|
+
const BRIEF = [
|
|
68
|
+
['scope', '맡는 일'],
|
|
69
|
+
['avoid', '손대지 않는 것'],
|
|
70
|
+
['read', '먼저 읽을 것'],
|
|
71
|
+
['extra', '그 밖'],
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// 첫 지시문 틀(§5-4) — '■ 이번 태스크 지시' 칸은 넘긴 칸이 있을 때만
|
|
75
|
+
function firstInstruction(st, brief = {}) {
|
|
68
76
|
const label = L.taskLabel(st.num);
|
|
69
|
-
const
|
|
77
|
+
const blocks = [
|
|
70
78
|
`[오케스트레이션] taskery 태스크 ${label} 「${st.title}」를 맡는다. 이 세션은 태스크 세션이다.`,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
[
|
|
80
|
+
'■ 기본 정보',
|
|
81
|
+
`- 태스크 문서: ${st.doc}`,
|
|
82
|
+
`- 플랜 문서: .taskery/plans/${st.plan}/PLAN.md`,
|
|
83
|
+
`- 시작 단계: ${startSkill(st)} (태스크 만들기(task-init)와 워크트리 준비는 끝났다)`,
|
|
84
|
+
`- 진행 범위: ${st.range}`,
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
'■ 작업 방식',
|
|
88
|
+
'- 태스크 문서와 플랜 문서를 먼저 읽는다.',
|
|
89
|
+
'- 맡은 일 밖은 손대지 않는다. 필요해 보이면 report-task로 묻는다.',
|
|
90
|
+
'- 진행 범위와 무관하게 사용자 확인·FAIL·풀지 못한 충돌에서는 멈춘다.',
|
|
91
|
+
'- 사용자 확인은 그대로 따라 할 수 있는 실행 절차(명령·누를 곳·기대 결과)와 함께 요청한다.',
|
|
92
|
+
'- 규칙 파일(.taskery/rules/·AGENTS.md)은 사용자 승인 없이 고치지 않는다.',
|
|
93
|
+
`- 보고: 마무리 완료(병합, 병합이 없는 태스크는 merge-task가 건너뛴 뒤)·사용자 확인 대기·질문·막힘 때 report-task ${label} "<한 줄>"을 부른다.`,
|
|
94
|
+
'- 멈춤: 마무리(merge-task)까지 끝나면 보고하고 멈춘다. 워크트리 정리와 태스크 닫기는 오케스트레이션이 한다.',
|
|
95
|
+
],
|
|
76
96
|
];
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
const given = BRIEF.map(([key, name]) => [name, brief[key] == null ? '' : oneLine(brief[key])]).filter(([, v]) => v);
|
|
98
|
+
if (given.length) blocks.push(['■ 이번 태스크 지시', ...given.map(([name, v]) => `- ${name}: ${v}`)]);
|
|
99
|
+
return blocks.map((b) => (Array.isArray(b) ? b.join('\n') : b)).join('\n\n');
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
// 지시문을 보내고 턴 시작이 관측됐는지 돌려준다. 받지 않았으면 refused. 관측할 수 없는 곳(영수증 없음·provider가 보고 못 함)은 보낸 것으로 본다
|
|
@@ -98,40 +119,63 @@ function onScreen(handle, marker) {
|
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
|
|
122
|
+
// 손 전달(--manual) — 탭을 띄우지 않고 사용자에게 전할 작업 폴더·실행 안내·첫 지시문을 돌려준다
|
|
123
|
+
function manualDispatch(main, st, a, brief) {
|
|
124
|
+
const prevTab = st.tab;
|
|
125
|
+
// 남은 탭 기록을 지운다 — 남겨 두면 wait-reports가 그 handle을 계속 확인한다
|
|
126
|
+
if (prevTab) {
|
|
127
|
+
delete st.tab;
|
|
128
|
+
L.writeState(main, st);
|
|
129
|
+
L.syncDoc(main, st);
|
|
130
|
+
}
|
|
131
|
+
const run = a.agent ? agentCommand(main, a.agent, a.model && String(a.model)) : `Claude Code는 claude, Codex는 ${agentCommand(main, 'codex')}`;
|
|
132
|
+
return [
|
|
133
|
+
'손 전달 — 탭을 띄우지 않았다. 사용자에게 아래를 전하고, 사용자가 작업 폴더에서 에이전트 세션을 열어 첫 지시문을 그대로 붙여 넣게 한다.',
|
|
134
|
+
`- 작업 폴더: ${L.workDir(main, st)}`,
|
|
135
|
+
`- 실행: ${run}`,
|
|
136
|
+
...(prevTab ? [`- 앞서 연 탭 ${prevTab}은 기록에서 지웠다. 그 탭에서 세션이 이미 일을 시작하지 않았는지 확인하고 닫는다.`] : []),
|
|
137
|
+
'--- 첫 지시문 ---',
|
|
138
|
+
firstInstruction(st, brief),
|
|
139
|
+
].join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
101
142
|
async function orcaDispatchTask(ctx, a) {
|
|
102
143
|
const main = ctx.main;
|
|
144
|
+
L.requireInstalled(main);
|
|
145
|
+
if (!a.task) L.fail('orca-dispatch-task: 빠진 입력이 있다.\n- <task> — 태스크 번호 (첫 번째 인자)');
|
|
146
|
+
const num = L.parseTaskNum(a.task);
|
|
147
|
+
const st = L.readState(main, num);
|
|
148
|
+
const label = L.taskLabel(num);
|
|
149
|
+
if (st.closed) L.fail(`${label}은 이미 닫혔다.`);
|
|
150
|
+
const dir = L.workDir(main, st);
|
|
151
|
+
if (!fs.existsSync(dir)) L.fail(`orca-dispatch-task: ${label}의 워크트리가 없다(${dir}). 'status'로 확인한다.`);
|
|
152
|
+
if (a.agent && !AGENTS.includes(a.agent)) L.fail(`orca-dispatch-task: --agent '${a.agent}'을 모른다. ${AGENTS.join(' · ')} 중 하나를 넣는다.`);
|
|
153
|
+
const brief = { scope: a.scope, avoid: a.avoid, read: a.read, extra: a.extra };
|
|
154
|
+
if (a.manual) return manualDispatch(main, st, a, brief);
|
|
103
155
|
if (!L.inOrca()) {
|
|
104
156
|
L.fail(
|
|
105
|
-
'orca-dispatch-task: Orca 탭 안이 아니라 태스크 세션을 띄울 수 없다(Orca
|
|
106
|
-
'Orca
|
|
157
|
+
'orca-dispatch-task: Orca 탭 안이 아니라 태스크 세션을 띄울 수 없다(Orca 환경 변수 ORCA_TERMINAL_HANDLE이 없다). ' +
|
|
158
|
+
'Orca 탭 안에서 불렀다면 MCP 도구가 아니라 CLI(npx @angar2/taskery orca-dispatch-task …)로 다시 부른다. ' +
|
|
159
|
+
'Orca를 쓸 수 없으면 --manual로 다시 불러 사용자에게 손 전달한다.',
|
|
107
160
|
);
|
|
108
161
|
}
|
|
109
|
-
L.requireInstalled(main);
|
|
110
162
|
const missing = [];
|
|
111
|
-
if (!a.task) missing.push('<task> — 태스크 번호 (첫 번째 인자)');
|
|
112
163
|
if (!a.agent) missing.push(`--agent — ${AGENTS.join(' · ')}`);
|
|
113
164
|
if (!a.model) missing.push('--model — 에이전트 모델 (사용자가 정한 것)');
|
|
114
165
|
if (missing.length) L.fail(`orca-dispatch-task: 빠진 입력이 있다.\n${missing.map((m) => `- ${m}`).join('\n')}`);
|
|
115
|
-
if (!AGENTS.includes(a.agent)) L.fail(`orca-dispatch-task: --agent '${a.agent}'을 모른다. ${AGENTS.join(' · ')} 중 하나를 넣는다.`);
|
|
116
|
-
const num = L.parseTaskNum(a.task);
|
|
117
|
-
const st = L.readState(main, num);
|
|
118
|
-
const label = L.taskLabel(num);
|
|
119
|
-
if (st.closed) L.fail(`${label}은 이미 닫혔다.`);
|
|
120
|
-
const dir = L.workDir(main, st);
|
|
121
|
-
if (!fs.existsSync(dir)) L.fail(`orca-dispatch-task: ${label}의 워크트리가 없다(${dir}). 'status'로 확인한다.`);
|
|
122
166
|
|
|
123
167
|
const command = agentCommand(main, a.agent, String(a.model));
|
|
124
168
|
const created = orca(['terminal', 'create', '--worktree', `path:${dir}`, '--title', label, '--command', command]);
|
|
125
169
|
const handle = created && created.terminal && created.terminal.handle;
|
|
126
170
|
if (!handle) L.fail(`orca-dispatch-task: ${label} 탭을 열었지만 Orca가 탭 handle을 돌려주지 않았다. 'orca terminal list'로 확인한다.`);
|
|
127
171
|
|
|
128
|
-
// 탭 handle을
|
|
172
|
+
// 탭 handle을 .state에 기록한다 — 첫 지시문을 보내기 전이라 태스크 세션의 기록과 겹치지 않는다
|
|
129
173
|
const fresh = L.readState(main, num);
|
|
130
174
|
fresh.tab = handle;
|
|
131
175
|
L.writeState(main, fresh);
|
|
132
176
|
L.syncDoc(main, fresh);
|
|
133
177
|
|
|
134
|
-
const text = firstInstruction(fresh,
|
|
178
|
+
const text = firstInstruction(fresh, brief);
|
|
135
179
|
const manual = `탭 화면을 'orca terminal read --terminal ${handle}'로 확인하고, 준비되면 아래 첫 지시문을 'orca terminal send --terminal ${handle} --text "<첫 지시문>" --enter'로 보낸다.\n--- 첫 지시문 ---\n${text}`;
|
|
136
180
|
let waited;
|
|
137
181
|
try {
|
|
@@ -170,7 +214,7 @@ async function orcaDispatchTask(ctx, a) {
|
|
|
170
214
|
`- 폴더: ${dir}`,
|
|
171
215
|
`- 첫 지시문을 보냈다(시작: ${startSkill(fresh)}, 범위: ${fresh.range}).`,
|
|
172
216
|
...(sendNote ? [`- 알림: ${sendNote}`] : []),
|
|
173
|
-
'다음: wait-reports
|
|
217
|
+
'다음: wait-reports가 걸려 있지 않으면 백그라운드 셸로 걸어 둔다.',
|
|
174
218
|
].join('\n');
|
|
175
219
|
}
|
|
176
220
|
|
|
@@ -223,18 +267,24 @@ function quietTabs(main) {
|
|
|
223
267
|
return out;
|
|
224
268
|
}
|
|
225
269
|
|
|
270
|
+
// 끝날 때 열린 태스크가 있으면 다시 걸라는 줄을 붙인다 — 다시 걸지 않으면 그 뒤의 보고를 놓친다
|
|
271
|
+
function withRearm(main, lines) {
|
|
272
|
+
if (L.listStates(main).some((st) => !st.closed)) lines.push('맡긴 태스크가 남아 있으면 곧바로 wait-reports를 다시 걸어 둔다(사용자 답을 기다리는 중이어도).');
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
226
276
|
async function waitReports(ctx) {
|
|
227
277
|
const main = ctx.main;
|
|
228
278
|
L.requireInstalled(main);
|
|
229
279
|
const until = Date.now() + WAIT_MS;
|
|
230
280
|
for (;;) {
|
|
231
281
|
const lines = takeUnread(main);
|
|
232
|
-
if (lines.length) return lines
|
|
282
|
+
if (lines.length) return withRearm(main, lines);
|
|
233
283
|
if (Date.now() >= until) break;
|
|
234
284
|
await new Promise((r) => setTimeout(r, Math.min(POLL_MS, Math.max(0, until - Date.now()))));
|
|
235
285
|
}
|
|
236
286
|
const quiet = quietTabs(main);
|
|
237
|
-
return [`보고 없이 ${L.minutes(WAIT_MS)}이 지났다.`, `오래 조용한 탭: ${quiet.length ? '' : '없음'}`.trimEnd(), ...quiet]
|
|
287
|
+
return withRearm(main, [`보고 없이 ${L.minutes(WAIT_MS)}이 지났다.`, `오래 조용한 탭: ${quiet.length ? '' : '없음'}`.trimEnd(), ...quiet]);
|
|
238
288
|
}
|
|
239
289
|
|
|
240
290
|
module.exports = { orcaDispatchTask, reportTask, waitReports, firstInstruction };
|
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 <모델> [--
|
|
47
|
+
| `orca-dispatch-task <TASK> [--agent claude\|codex --model <모델>] [--manual] [--scope "<맡는 일>"] [--avoid "<손대지 않는 것>"] [--read "<먼저 읽을 것>"] [--extra "<그 밖>"]` | Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(`--manual`이 아니면 `--agent`·`--model` 필수, 탭 handle은 `.state`에 남는다). `--manual`이면 띄우지 않고 작업 폴더·실행 안내와 함께 사용자가 붙여 넣을 첫 지시문을 출력한다(손 전달) |
|
|
48
48
|
| `report-task <TASK> "<한 줄>"` | 태스크 세션이 오케스트레이션에 보고 한 줄을 남긴다(`.taskery/reports.log`) |
|
|
49
|
-
| `wait-reports` | 오케스트레이션이 백그라운드 셸로 걸어 두는 대기. 안 읽은 보고가 생기면 출력하고 끝나고, 보고 없이
|
|
49
|
+
| `wait-reports` | 오케스트레이션이 백그라운드 셸로 걸어 두는 대기. 안 읽은 보고가 생기면 출력하고 끝나고, 보고 없이 60분이면 오래 조용한 태스크 탭 목록과 함께 끝난다 (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
|
|
|
@@ -110,3 +110,4 @@
|
|
|
110
110
|
| 2026-10-01 | S4 — 로컬 규칙 틀(`TEST_RULE.local.md`·`DEV_RULE.local.md`)을 `init`이 만든다는 줄을 더했다 |
|
|
111
111
|
| 2026-10-01 | S3 — 오케스트레이션 명령 `orca-dispatch-task`·`report-task`·`wait-reports`와 `reports.log`를 더했다 |
|
|
112
112
|
| 2026-10-01 | S2 — `prune`·`backlog-*`·`prepare-task --from`·`status`의 시작할 수 있는 태스크, 새 워크트리 준비(빌드 결과 폴더 APFS 복제·exclude 등록·닫을 때 본진 씨앗 갱신·`npm ci`)를 더했다 |
|
|
113
|
+
| 2026-10-09 | 첫 지시문 정형화 — orca-dispatch-task의 --note를 칸별 인자로 바꾸고 손 전달(--manual)을 더했다. wait-reports 기다리는 시간을 60분으로 늘렸다 |
|
|
@@ -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
|
|
|
@@ -9,6 +9,9 @@ description: 오케스트레이션 — 백로그를 태스크로 기획하고,
|
|
|
9
9
|
|
|
10
10
|
- 태스크는 서브에이전트에 맡기지 않는다. 태스크 세션은 Orca 새 탭의 독립 세션이며, 사용자가 그 탭에서 확인하고 개입할 수 있어야 한다.
|
|
11
11
|
- 태스크마다 `task-init`(여는 일)을 이 세션이 하고, `orca-dispatch-task`로 태스크 세션을 띄운다. 에이전트와 모델은 사용자가 정하고, 정하지 않았으면 태스크 구성 확인 때 묻는다.
|
|
12
|
+
- 첫 지시문의 역할·작업 방식은 명령이 같은 문장으로 채운다. 이 세션은 이번 태스크만의 지시를 칸별로 넘긴다 — `--scope`(맡는 일)·`--avoid`(손대지 않는 것)·`--read`(태스크 문서·플랜 문서 말고 먼저 읽을 것)·`--extra`(그 밖). 리포 규칙(`*.local.md`)에 이미 있는 것과 첫 지시문에 이미 있는 것(보고·멈춤 등)은 다시 쓰지 않는다. 필요 없는 칸은 비운다.
|
|
13
|
+
- **손 전달**: Orca를 쓸 수 없거나(Orca 밖·탭 띄우기 실패) 사용자가 태스크 세션을 직접 열겠다고 하면 `orca-dispatch-task --manual`로 부른다. 명령이 내는 작업 폴더·실행 안내·첫 지시문을 사용자에게 전해 붙여 넣어 달라고 하고, `wait-reports`가 걸려 있지 않으면 곧바로 건다(사용자가 언제 붙여 넣든 보고는 `reports.log`에 남는다).
|
|
14
|
+
- 손으로 연 세션에 보낼 말도 `[오케스트레이션]`을 붙여 사용자에게 전달을 부탁한다.
|
|
12
15
|
- 마무리 완료 보고(병합, 또는 병합이 없는 태스크의 마무리)가 오면 `close-task`로 정리하고, 연결된 백로그와 남은 태스크를 확인한다.
|
|
13
16
|
- 보고에 `.local` 규칙 제안이 담겨 오면 사용자에게 보여 주고, 사용자가 이 세션에서 승인하면 이 세션이 `.local`에 넣는다.
|
|
14
17
|
|
|
@@ -23,10 +26,10 @@ description: 오케스트레이션 — 백로그를 태스크로 기획하고,
|
|
|
23
26
|
- **태스크가 많을 때**: 동시에 진행하는 태스크 세션 수에 상한을 둔다. 사용자가 정하지 않았으면 시작 전에 한 번 묻는다. 태스크 하나가 끝나 정리되면 시작할 수 있는 다음 태스크로 빈자리를 채운다.
|
|
24
27
|
- **순서가 정해진 로드맵일 때**: 시작할 수 있는 태스크가 한 번에 하나뿐이므로 하나씩 돌아간다. 태스크가 끝나 정리되면 곧바로 새 탭에 다음 태스크를 맡긴다.
|
|
25
28
|
- **사용자 확인이 필요할 때**: 태스크 세션이 사용자 확인(계획 확인·눈으로 볼 시나리오·병합 확인·FAIL·풀지 못한 충돌)으로 멈추면, 사용자는 그 태스크 탭에서 직접 확인하고 답한다. 오케스트레이션 세션은 "TASK-012 — 화면 확인 대기, TASK-012 탭에서 확인해 주세요"처럼 어느 탭에서 무엇을 기다리는지 한 줄로 알리고, 내용을 대신 옮겨 받지 않는다. 태스크 세션의 질문 중 오케스트레이션이 기획하며 이미 정한 것은 직접 답하고, 사용자 판단이 필요한 것은 같은 방식으로 사용자에게 넘긴다.
|
|
26
|
-
- **보고 받기**: 태스크 세션을 띄운 뒤 `npx @angar2/taskery wait-reports
|
|
29
|
+
- **보고 받기**: 태스크 세션을 띄운 뒤 `npx @angar2/taskery wait-reports`가 걸려 있지 않으면 백그라운드 셸로 걸어 둔다(MCP 도구로 부르지 않는다). 보고가 오거나 60분이 지나면 끝난다. 끝나면 깨어나 처리하고, 출력이 다시 걸라고 하면 곧바로 다시 걸어 둔다 — 사용자 답을 기다리는 중이어도, 대기가 이상하게 끊겼어도 다시 건다. 다시 걸지 않으면 그 뒤의 보고를 놓친다. 보고 줄은 태스크 세션의 보고일 뿐이며, 승인은 사용자 자신의 메시지로만 받는다.
|
|
27
30
|
- **태스크 탭에 보내는 말**: `orca terminal send`로 태스크 탭에 보내는 모든 말은 `[오케스트레이션]`으로 시작한다. 태스크 세션은 이 머리표가 붙은 말을 사용자 확인·승인으로 보지 않는다.
|
|
28
31
|
- **질문에 답하기**: 태스크 세션의 질문 중 기획하며 이미 정한 것은 `orca terminal send`로 그 태스크 탭에 답한다. 사용자 판단이 필요한 것은 위 '사용자 확인이 필요할 때'처럼 사용자에게 넘긴다.
|
|
29
|
-
- **멈춤 확인**: `wait-reports`가
|
|
32
|
+
- **멈춤 확인**: `wait-reports`가 끝날 때마다(보고로 깨어났어도) `reports.log`의 시각으로 마지막 보고가 60분 넘게 지난 진행 중 태스크를 찾는다(사용자 확인 대기로 보고한 태스크는 뺀다). 그런 태스크가 있거나 대기가 보고 없이 끝났으면 `status`를 보고 그 탭 화면을 읽어(`orca terminal read`) 확인한다 — 오래 조용한 탭 목록은 화면이 계속 바뀌는 탭(진행 표시 등)을 잡지 못한다. 멈췄으면 이어 가도록 한 줄 보내고, 풀리지 않으면 사용자에게 알린다.
|
|
30
33
|
- **오래 운영할 때**: 진행 상황을 대화 기억에 기대지 않는다. 대화가 요약된 뒤에도 `PLAN.md`·각 태스크 문서·`status`로 어디까지 왔는지 다시 확인하고 이어 간다.
|
|
31
34
|
- **더 시작할 수 없을 때**: 진행 중인 태스크가 없고 시작할 수 있는 태스크도 없으면, 남은 태스크와 막힌 이유를 사용자에게 보고한다.
|
|
32
35
|
|
|
@@ -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
|
|