@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 CHANGED
@@ -23,7 +23,7 @@ taskery는 일을 둘로 나눕니다.
23
23
 
24
24
  | AI에게 그냥 맡기면 | taskery를 쓰면 |
25
25
  |---|---|
26
- | 무엇을 왜 바꿨는지가 긴 대화 속에 묻혀, 나중에 다시 찾기 어렵습니다 | 태스크마다 문서 한 장(15~30줄)에 목표, 완료 기준, 고친 파일, 내린 결정, 결과가 남습니다 |
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
- | ② 기획 | 목표, 완료 기준, 고칠 파일을 문서에 적고 사용자 확인을 받습니다 | `approve-plan` | 필요한 내용이 다 적혔는지 검사하고, 기획이 끝났다고 기록합니다 |
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
- | 태스크 문서 | 목표, 완료 기준, 고칠 파일, 결정, 결과 | 위에 더해 요구사항과 작업 묶음(Phase) |
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
- <!-- taskery: plan=001_mvp type=improve size=small switch=plan,dev,test range=끝까지 parent=dev by=taskery -->
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
- | 목표, 완료 기준, 만질 파일, 결정 | AI |
187
- | 결과 | 명령 (`test-scenario`) |
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`). 보고가 오거나 10분이 지나면 깨어나 처리합니다 |
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 이상, 위 두 에이전트 중 하나 이상. 오케스트레이션을 쓰려면 Orca가 필요합니다.
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
- - 오케스트레이션은 Orca 안에서만 됩니다.
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
- if (!L.goalLine(text)) problems.push('`## 목표`가 비어 있다');
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 (!files.length) problems.push('`## 만질 파일`(medium·large는 `## Phase`의 `- 파일:` 포함)에 파일이 없다 — 한 줄에 `- 경로` 하나씩');
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(`\`## 완료 기준\` 시나리오가 ${n}개다 — 테스트 켜짐이면 1~5개를 \`1. [AUTO] 시작 → 행동 → 기대하는 끝 상태\` 형식으로 적는다`);
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
- return `${L.taskLabel(num)} 계획을 기록했다 — 시작·기획 ${L.minutes(d.plan)}, 만질 파일 ${files.length}개.\n다음: ${next}.`;
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 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(Orca 전용). 탭 handle은 태스크 문서 메타에 기록한다',
116
+ summary: 'Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(탭 handle은 .state에 기록). --manual이면 띄우지 않고 사용자가 붙여 넣을 첫 지시문을 출력한다',
117
117
  args: [
118
118
  TASK,
119
- { name: 'agent', desc: 'claude · codex (필수)' },
120
- { name: 'model', desc: '에이전트 모델 — 사용자가 정한 것 (필수)' },
121
- { name: 'note', desc: '첫 지시문 끝에 붙일 오케스트레이션의 말 한 줄' },
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: '안 읽은 보고가 생기면 그 줄들을 출력하고 끝난다. 보고 없이 10분이면 오래 조용한 태스크 탭 목록과 함께 끝난다(백그라운드 셸로 건다)',
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 prev = (st.commit && st.commit.commits) || [];
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 start = lines.findIndex((l) => l.trim() === `## ${title}`);
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
- // `## Phase` 절: `### Phase N — 이름` + `- 파일:`(한 줄 쉼표·가운뎃점 구분 또는 들여쓴 목록) + `- 사유:`
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
- function quoteMeta(v) {
447
- return /\s/.test(v) ? `"${v}"` : v;
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 renderMeta(st) {
451
- const parts = [
452
- `plan=${st.plan}`,
453
- `type=${st.type}`,
454
- `size=${st.size}`,
455
- `switch=${st.switch.join(',')}`,
456
- `range=${quoteMeta(st.range)}`,
457
- `parent=${st.parent}`,
458
- `by=${st.by}`,
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
- if (st.tab) parts.push(`tab=${st.tab}`);
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
- // 문서의 메타 줄과 단계 표 줄을 .state 기록으로 다시 쓴다(메타·표는 명령만 쓴다, §4-2)
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
- const mi = lines.findIndex((l) => l.startsWith('<!-- taskery:'));
526
- if (mi !== -1) lines[mi] = renderMeta(st);
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
- renderMeta(st),
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.size !== 'small') out.push('## 요구사항', '');
547
- out.push('## 완료 기준', '', '## 만질 파일', '');
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
- let start = lines.findIndex((l) => l.trim() === '## 결과');
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
- // 범위 메모 갱신 — 태스크 명령에 --range를 붙이면 메타의 범위만 바꾼다(§3-5)
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
- renderMeta,
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) || 10 * 60 * 1000;
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 -m ${shq(model)} --add-dir ${shq(path.join(main, '.taskery'))}`;
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
- // 첫 지시문 틀(§5-4) — 마지막 줄은 --note를 줬을 때만
67
- function firstInstruction(st, note) {
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 lines = [
77
+ const blocks = [
70
78
  `[오케스트레이션] taskery 태스크 ${label} 「${st.title}」를 맡는다. 이 세션은 태스크 세션이다.`,
71
- `- 태스크 문서: ${st.doc} — 먼저 읽는다.`,
72
- `- 시작: ${startSkill(st)}부터 한다. 태스크 만들기(task-init)와 워크트리 준비는 끝났다.`,
73
- `- 진행 범위: ${st.range}. 범위와 무관하게 사용자 확인·FAIL·풀지 못한 충돌에서는 멈춘다.`,
74
- `- 보고: 마무리 완료(병합, 병합이 없는 태스크는 merge-task가 건너뛴 뒤)·사용자 확인 대기·질문·막힘 때 report-task ${label} "<한 줄>"을 부른다.`,
75
- '- 마무리(merge-task)까지 끝나면 보고하고 멈춘다. 워크트리 정리와 태스크 닫기는 오케스트레이션이 한다.',
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
- if (note && oneLine(note)) lines.push(`- 오케스트레이션이 전하는 말: ${oneLine(note)}`);
78
- return lines.join('\n');
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 전용 — Orca 환경 변수 ORCA_TERMINAL_HANDLE이 없다). ' +
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, a.note);
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.join('\n');
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].join('\n');
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) L.writeState(main, st);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angar2/taskery",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI 코딩 에이전트(Claude Code·Codex)에게 일을 태스크 단위로 맡겨 기획·개발·테스트·병합까지 진행하게 하는 작업 체계",
5
5
  "bin": {
6
6
  "taskery": "bin/taskery.js",
@@ -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은 태스크 문서 메타 `tab`에 남는다 |
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` | 오케스트레이션이 백그라운드 셸로 걸어 두는 대기. 안 읽은 보고가 생기면 출력하고 끝나고, 보고 없이 10분이면 오래 조용한 태스크 탭 목록과 함께 끝난다 (CLI만) |
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
- <!-- taskery: plan=001_mvp type=improve size=small switch=plan,dev,test range=끝까지 parent=dev by=taskery -->
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
- - **맨 위 두 줄(메타)과 단계 표는 명령이 쓴다.** 손대지 않는다. 범위가 바뀌면 다음 태스크 명령에 `--range "<새 범위>"`를 붙인다.
25
- - medium·large는 `## 요구사항`(목표 아래)과 `## Phase`(만질 파일 아래)가 더 있다.
26
- - 전체 15~30줄이면 충분하다.
43
+ - **헤더 표와 단계 표는 명령이 쓴다.** 손대지 않는다. 범위가 바뀌면 다음 태스크 명령에 `--range "<새 범위>"`를 붙인다.
44
+ - 상태는 `열림`·`닫힘(완료)`·`닫힘(포기)` 셋이다. 분기 없이 일하는 태스크는 브랜치 칸이 `분기 없음`이다.
45
+ - 개발이 꺼진 태스크에는 `## 개발 계획`이, 테스트가 꺼진 태스크에는 `## 테스트 계획`이 없다.
46
+ - 짧게 쓰되 절마다 아래 2절의 내용은 빠뜨리지 않는다.
27
47
 
28
48
  ## 2. 절마다 쓰는 법
29
49
 
30
50
  | 절 | 쓰는 법 |
31
51
  |---|---|
32
- | `## 목표` | 무엇을 왜 하는지 한두 줄. 첫 줄이 small 커밋의 사유와 변경 기록 요약이 된다 |
33
- | `## 요구사항` | (medium·large) 정리한 요구 목록 |
34
- | `## 완료 기준` | 실사용 테스트 시나리오. 아래 3절 |
35
- | `## 만질 파일` | 한 줄에 `- 경로` 하나. 설명은 ` — ` 뒤에 적는다. 폴더는 `src/auth/`처럼 끝에 `/` |
36
- | `## Phase` | (medium·large) 아래 4절 |
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`은 목표와 만질 파일이 있는지, 테스트가 켜져 있으면 완료 기준이 1~5개인지 본다.
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
- ## Phase
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
- - 태스크 문서의 목표와 만질 파일(Phase)대로 구현하고, 필요한 단위 테스트를 쓴다.
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`를 백그라운드 셸로 걸어 둔다(MCP 도구로 부르지 않는다). 끝나면(보고가 왔거나 10분이 지남) 깨어나 처리하고 다시 걸어 둔다. 보고 줄은 태스크 세션의 보고일 뿐이며, 승인은 사용자 자신의 메시지로만 받는다.
29
+ - **보고 받기**: 태스크 세션을 띄운 뒤 `npx @angar2/taskery wait-reports`가 걸려 있지 않으면 백그라운드 셸로 걸어 둔다(MCP 도구로 부르지 않는다). 보고가 오거나 60분이 지나면 끝난다. 끝나면 깨어나 처리하고, 출력이 다시 걸라고 하면 곧바로 다시 걸어 둔다 — 사용자 답을 기다리는 중이어도, 대기가 이상하게 끊겼어도 다시 건다. 다시 걸지 않으면 그 뒤의 보고를 놓친다. 보고 줄은 태스크 세션의 보고일 뿐이며, 승인은 사용자 자신의 메시지로만 받는다.
27
30
  - **태스크 탭에 보내는 말**: `orca terminal send`로 태스크 탭에 보내는 모든 말은 `[오케스트레이션]`으로 시작한다. 태스크 세션은 이 머리표가 붙은 말을 사용자 확인·승인으로 보지 않는다.
28
31
  - **질문에 답하기**: 태스크 세션의 질문 중 기획하며 이미 정한 것은 `orca terminal send`로 그 태스크 탭에 답한다. 사용자 판단이 필요한 것은 위 '사용자 확인이 필요할 때'처럼 사용자에게 넘긴다.
29
- - **멈춤 확인**: `wait-reports`가 오래 조용한 탭을 알려 주면 그 탭 화면을 읽어(`orca terminal read`) 확인한다. 멈췄으면 이어 가도록 한 줄 보내고, 풀리지 않으면 사용자에게 알린다.
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: 태스크 기획 — 태스크 문서에 목표·완료 기준·만질 파일(medium·large는 요구사항·Phase)을 쓰고 approve-plan으로 계획 끝을 기록한다
3
+ description: 태스크 기획 — 태스크 문서에 요구사항(첫 줄 목표)·개발 계획·테스트 계획을 쓰고 approve-plan으로 계획 끝을 기록한다
4
4
  ---
5
5
 
6
6
  # task-plan
7
7
 
8
8
  태스크 워크트리 안에서 태스크 문서를 채우는 단계다.
9
9
 
10
- - 태스크 문서를 채운다: 목표, 완료 기준(테스트 켜짐일 때), 만질 파일. medium·large는 요구사항과 Phase(Phase마다 파일 목록과 사유 한 줄)를 더한다.
11
- - 기획 켜짐: 요구사항을 정리하고 모호한 것은 사용자에게 묻는다. 기획 꺼짐: 요청 내용으로 목표·만질 파일·완료 기준을 짧게 적고 `approve-plan`을 부른 뒤 멈추지 않고 다음 단계로 이어 간다(범위가 한 단계여도 간략 기획은 따로 세지 않는다).
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
- - 기획 켜짐이고 진행 범위가 이 단계에서 끝나면(범위를 말하지 않은 경우 포함) 목표·완료 기준·만질 파일을 보여 주고 사용자 확인을 받는다. 범위가 다음 단계로 이어지면("개발까지"·"끝까지" 등) 멈추지 않는다. 그다음 `approve-plan`을 부른다.
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
- - 완료 기준의 시나리오를 실제로 실행한다. 앱 실행 방법은 `TEST_RULE.local.md`를 따른다.
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