@angar2/taskery 1.0.1 → 1.0.3

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
@@ -275,7 +275,7 @@ Node 프로젝트는 `package-lock.json`이 있으면 `npm ci`로 설치합니
275
275
 
276
276
  ## 오케스트레이션 — 한 줄 지시로 여러 태스크를 나눠 맡기기
277
277
 
278
- **오케스트레이션** = 한 세션(관리 세션)이 백로그를 태스크로 나누고, 태스크마다 새 AI 탭을 띄워 맡기고, 보고를 받아 정리하는 방식입니다. Orca 안에서 씁니다.
278
+ **오케스트레이션** = 한 세션(관리 세션)이 백로그를 태스크로 나누고, 태스크마다 새 AI 탭을 띄워 맡기고, 보고를 받아 정리하는 방식입니다. Orca 안에서 씁니다. Orca를 쓸 수 없을 때는 사용자가 직접 세션을 열어 첫 지시를 붙여 넣는 방식으로도 진행할 수 있습니다.
279
279
 
280
280
  ```
281
281
  👤 사용자
@@ -294,9 +294,9 @@ Node 프로젝트는 `package-lock.json`이 있으면 `npm ci`로 설치합니
294
294
  |---|---|
295
295
  | ① | 사용자가 관리 세션에 한 줄로 지시합니다. 예: "백로그 BL-1~3을 끝까지 진행해" |
296
296
  | ② | 관리 세션이 백로그와 `PLAN.md`를 읽고 태스크로 나눕니다. 어떤 태스크를 어떤 순서와 AI로 진행할지 표로 보여 주고 확인받습니다. 지시에 이미 정해져 있으면 다시 묻지 않습니다 |
297
- | ③ | 태스크마다 작업 폴더를 만들고(`prepare-task`), Orca 새 탭에 Claude나 Codex를 띄워 첫 지시를 보냅니다(`orca-dispatch-task`) |
297
+ | ③ | 태스크마다 작업 폴더를 만들고(`prepare-task`), Orca 새 탭에 Claude나 Codex를 띄워 첫 지시를 보냅니다(`orca-dispatch-task`). 첫 지시는 늘 같은 틀(기본 정보·작업 방식·이번 태스크 지시)로 나갑니다. Orca를 쓸 수 없거나 사용자가 원하면, 관리 세션이 첫 지시를 보여 주고 사용자가 직접 연 세션에 붙여 넣습니다 |
298
298
  | ④ | 각 탭이 자기 작업 폴더에서 기획부터 마무리까지 진행합니다. 끝났을 때, 확인을 기다릴 때, 질문이 있을 때, 막혔을 때 보고함에 한 줄 남깁니다(`report-task`) |
299
- | ⑤ | 관리 세션은 기다리는 동안 잠들어 있어 토큰을 쓰지 않습니다(`wait-reports`). 보고가 오거나 10분이 지나면 깨어나 처리합니다 |
299
+ | ⑤ | 관리 세션은 기다리는 동안 잠들어 있어 토큰을 쓰지 않습니다(`wait-reports`). 보고가 오거나 60분이 지나면 깨어나 처리합니다 |
300
300
  | ⑥ | 사용자 확인이 필요하면 관리 세션이 "TASK-108 탭에서 화면 확인해 주세요"라고 알립니다. 사용자는 그 탭에서 직접 답합니다 |
301
301
  | ⑦ | 끝난 태스크는 병합 잠금 안에서 하나씩 dev에 병합됩니다(`merge-task`) |
302
302
  | ⑧ | 관리 세션이 끝난 태스크의 작업 폴더와 탭을 치우고(`close-task`), 시작할 수 있는 다음 태스크로 빈자리를 채웁니다. 그리고 ③으로 돌아갑니다 |
@@ -345,7 +345,7 @@ Node 프로젝트는 `package-lock.json`이 있으면 `npm ci`로 설치합니
345
345
 
346
346
  ## 빠른 시작
347
347
 
348
- > **준비물**: Node.js 18 이상, git 2.31 이상, 위 두 에이전트 중 하나 이상. 오케스트레이션을 쓰려면 Orca가 필요합니다.
348
+ > **준비물**: Node.js 18 이상, git 2.31 이상, 위 두 에이전트 중 하나 이상. 오케스트레이션은 Orca를 기본으로 씁니다(Orca가 없으면 태스크 세션을 직접 엽니다).
349
349
 
350
350
  ### 1. 설치
351
351
 
@@ -490,7 +490,7 @@ taskery 파일은 그 리포에만 적용되는 git 무시 목록(`.git/info/exc
490
490
  - PR로 병합하는 방식은 다루지 않습니다. 병합은 로컬에서만 합니다.
491
491
  - 태스크 문서와 백로그는 git 이력에 남지 않습니다.
492
492
  - 빌드 결과 복제는 macOS에서만 됩니다. 다른 환경에서는 평소처럼 빌드합니다.
493
- - 오케스트레이션은 Orca 안에서만 됩니다.
493
+ - 오케스트레이션의 탭 자동 띄우기는 Orca 안에서만 됩니다. 그 밖에서는 사용자가 태스크 세션을 직접 열고 첫 지시를 붙여 넣습니다.
494
494
  - Codex는 작업 폴더로 옮겨 갈 수 없습니다. Codex로 태스크를 하려면 오케스트레이션이 Codex 탭을 띄우거나, 사용자가 그 작업 폴더에서 Codex를 엽니다.
495
495
 
496
496
  ---
package/bin/commands.js CHANGED
@@ -113,12 +113,16 @@ const COMMANDS = [
113
113
  },
114
114
  {
115
115
  name: 'orca-dispatch-task',
116
- summary: 'Orca 새 탭에 태스크 세션을 띄우고 첫 지시문을 보낸다(Orca 전용). 탭 handle은 .state에 기록한다',
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,7 +134,7 @@ 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,
package/bin/finish.js CHANGED
@@ -286,6 +286,15 @@ function writeChangelog(main, st, goal) {
286
286
  return file;
287
287
  }
288
288
 
289
+ // orca worktree rm 뒤 폴더가 사라지기를 기다리는 최대 시간
290
+ const ORCA_RM_WAIT_MS = 30 * 1000;
291
+
292
+ // 경로가 사라질 때까지 기다린다(최대 ms)
293
+ function waitGone(p, ms) {
294
+ const until = Date.now() + ms;
295
+ while (fs.existsSync(p) && Date.now() < until) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
296
+ }
297
+
289
298
  // 워크트리·브랜치 정리 — 커밋 안 된 변경이 있으면 남기고, 브랜치는 병합된 것만 지운다.
290
299
  // force는 prune에서 사용자가 확인했을 때만 — 병합 안 된 브랜치도 지운다(복구 명령 출력)
291
300
  function cleanup(main, st, { force = false } = {}) {
@@ -307,6 +316,8 @@ function cleanup(main, st, { force = false } = {}) {
307
316
  try {
308
317
  execFileSync('orca', ['worktree', 'rm', '--worktree', `path:${st.worktree}`, '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
309
318
  notes.push(`Orca 워크트리를 지웠다: ${st.worktree}`);
319
+ // Orca는 답한 뒤에도 뒤에서 폴더를 지우고 병합된 브랜치도 함께 지운다 — 폴더가 사라질 때까지 기다린 뒤 브랜치를 본다
320
+ waitGone(st.worktree, ORCA_RM_WAIT_MS);
310
321
  } catch (e) {
311
322
  notes.push(`Orca 워크트리를 지우지 못해 남겼다: ${((e.stderr || '') + '').trim() || e.message}`);
312
323
  branchFree = false;
@@ -325,7 +336,10 @@ function cleanup(main, st, { force = false } = {}) {
325
336
  }
326
337
  if (!merged && !force) notes.push(`브랜치 ${st.branch}는 부모에 병합되지 않아 남겼다.`);
327
338
  else if (!branchFree || L.currentBranch(main) === st.branch) notes.push(`브랜치 ${st.branch}를 지우지 못해 남겼다(사용 중).`);
328
- else if (L.git(main, ['branch', merged ? '-d' : '-D', st.branch], { allowFail: true }) === null) notes.push(`브랜치 ${st.branch}를 지우지 못해 남겼다.`);
339
+ else if (L.git(main, ['branch', merged ? '-d' : '-D', st.branch], { allowFail: true }) === null) {
340
+ // 그 사이 Orca가 지웠으면 실패가 아니다
341
+ notes.push(L.branchExists(main, st.branch) ? `브랜치 ${st.branch}를 지우지 못해 남겼다.` : `브랜치 ${st.branch}가 지워졌다. 복구: git branch ${st.branch} ${tip}`);
342
+ }
329
343
  else notes.push(`브랜치 ${st.branch}를 지웠다. 복구: git branch ${st.branch} ${tip}`);
330
344
  return notes;
331
345
  }
package/bin/lib.js CHANGED
@@ -716,6 +716,7 @@ module.exports = {
716
716
  docAbs,
717
717
  docShown,
718
718
  readDoc,
719
+ fenceMask,
719
720
  section,
720
721
  isNewDoc,
721
722
  meaningfulLines,
package/bin/orche.js CHANGED
@@ -6,10 +6,12 @@ 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;
13
+ // 턴 시작 미관측 뒤 Enter를 보내고 화면을 읽기 전 기다리는 시간
14
+ const SCREEN_SETTLE_MS = 2000;
13
15
 
14
16
  function reportsFile(main) {
15
17
  return path.join(main, '.taskery', 'reports.log');
@@ -45,10 +47,10 @@ function shq(v) {
45
47
  return /^[A-Za-z0-9_.\/:=@%+-]+$/.test(v) ? v : `'${String(v).replace(/'/g, `'\\''`)}'`;
46
48
  }
47
49
 
48
- // 에이전트 실행 명령 — 권한·승인 옵션은 붙이지 않는다(사용자 설정 그대로, I5)
50
+ // 에이전트 실행 명령 — 권한·승인 옵션은 붙이지 않는다(사용자 설정 그대로, I5). 모델이 없으면 모델 옵션을 뺀다
49
51
  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'))}`;
52
+ if (agent === 'claude') return model ? `claude --model ${shq(model)}` : 'claude';
53
+ return `codex ${model ? `-m ${shq(model)} ` : ''}--add-dir ${shq(path.join(main, '.taskery'))}`;
52
54
  }
53
55
 
54
56
  // 단계 표에서 비어 있는 첫 단계의 스킬 — 새 태스크면 task-plan
@@ -63,19 +65,40 @@ function oneLine(text) {
63
65
  return String(text).replace(/\s*\n\s*/g, ' ').trim();
64
66
  }
65
67
 
66
- // 첫 지시문 틀(§5-4) — 마지막 줄은 --note를 줬을 때만
67
- function firstInstruction(st, note) {
68
+ // 첫 지시문 '■ 이번 태스크 지시' 칸 — 인자 이름과 칸 이름, 나오는 순서
69
+ const BRIEF = [
70
+ ['scope', '맡는 일'],
71
+ ['avoid', '손대지 않는 것'],
72
+ ['read', '먼저 읽을 것'],
73
+ ['extra', '그 밖'],
74
+ ];
75
+
76
+ // 첫 지시문 틀(§5-4) — '■ 이번 태스크 지시' 칸은 넘긴 칸이 있을 때만
77
+ function firstInstruction(st, brief = {}) {
68
78
  const label = L.taskLabel(st.num);
69
- const lines = [
79
+ const blocks = [
70
80
  `[오케스트레이션] 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)까지 끝나면 보고하고 멈춘다. 워크트리 정리와 태스크 닫기는 오케스트레이션이 한다.',
81
+ [
82
+ '■ 기본 정보',
83
+ `- 태스크 문서: ${st.doc}`,
84
+ `- 플랜 문서: .taskery/plans/${st.plan}/PLAN.md`,
85
+ `- 시작 단계: ${startSkill(st)} (태스크 만들기(task-init)와 워크트리 준비는 끝났다)`,
86
+ `- 진행 범위: ${st.range}`,
87
+ ],
88
+ [
89
+ '■ 작업 방식',
90
+ '- 태스크 문서와 플랜 문서를 먼저 읽는다.',
91
+ '- 맡은 일 밖은 손대지 않는다. 필요해 보이면 report-task로 묻는다.',
92
+ '- 진행 범위와 무관하게 사용자 확인·FAIL·풀지 못한 충돌에서는 멈춘다.',
93
+ '- 사용자 확인은 그대로 따라 할 수 있는 실행 절차(명령·누를 곳·기대 결과)와 함께 요청한다.',
94
+ '- 규칙 파일(.taskery/rules/·AGENTS.md)은 사용자 승인 없이 고치지 않는다.',
95
+ `- 보고: 마무리 완료(병합, 병합이 없는 태스크는 merge-task가 건너뛴 뒤)·사용자 확인 대기·질문·막힘 때 report-task ${label} "<한 줄>"을 부른다.`,
96
+ '- 멈춤: 마무리(merge-task)까지 끝나면 보고하고 멈춘다. 워크트리 정리와 태스크 닫기는 오케스트레이션이 한다.',
97
+ ],
76
98
  ];
77
- if (note && oneLine(note)) lines.push(`- 오케스트레이션이 전하는 말: ${oneLine(note)}`);
78
- return lines.join('\n');
99
+ const given = BRIEF.map(([key, name]) => [name, brief[key] == null ? '' : oneLine(brief[key])]).filter(([, v]) => v);
100
+ if (given.length) blocks.push(['■ 이번 태스크 지시', ...given.map(([name, v]) => `- ${name}: ${v}`)]);
101
+ return blocks.map((b) => (Array.isArray(b) ? b.join('\n') : b)).join('\n\n');
79
102
  }
80
103
 
81
104
  // 지시문을 보내고 턴 시작이 관측됐는지 돌려준다. 받지 않았으면 refused. 관측할 수 없는 곳(영수증 없음·provider가 보고 못 함)은 보낸 것으로 본다
@@ -98,27 +121,50 @@ function onScreen(handle, marker) {
98
121
  }
99
122
  }
100
123
 
124
+ // 손 전달(--manual) — 탭을 띄우지 않고 사용자에게 전할 작업 폴더·실행 안내·첫 지시문을 돌려준다
125
+ function manualDispatch(main, st, a, brief) {
126
+ const prevTab = st.tab;
127
+ // 남은 탭 기록을 지운다 — 남겨 두면 wait-reports가 그 handle을 계속 확인한다
128
+ if (prevTab) {
129
+ delete st.tab;
130
+ L.writeState(main, st);
131
+ L.syncDoc(main, st);
132
+ }
133
+ const run = a.agent ? agentCommand(main, a.agent, a.model && String(a.model)) : `Claude Code는 claude, Codex는 ${agentCommand(main, 'codex')}`;
134
+ return [
135
+ '손 전달 — 탭을 띄우지 않았다. 사용자에게 아래를 전하고, 사용자가 작업 폴더에서 에이전트 세션을 열어 첫 지시문을 그대로 붙여 넣게 한다.',
136
+ `- 작업 폴더: ${L.workDir(main, st)}`,
137
+ `- 실행: ${run}`,
138
+ ...(prevTab ? [`- 앞서 연 탭 ${prevTab}은 기록에서 지웠다. 그 탭에서 세션이 이미 일을 시작하지 않았는지 확인하고 닫는다.`] : []),
139
+ '--- 첫 지시문 ---',
140
+ firstInstruction(st, brief),
141
+ ].join('\n');
142
+ }
143
+
101
144
  async function orcaDispatchTask(ctx, a) {
102
145
  const main = ctx.main;
146
+ L.requireInstalled(main);
147
+ if (!a.task) L.fail('orca-dispatch-task: 빠진 입력이 있다.\n- <task> — 태스크 번호 (첫 번째 인자)');
148
+ const num = L.parseTaskNum(a.task);
149
+ const st = L.readState(main, num);
150
+ const label = L.taskLabel(num);
151
+ if (st.closed) L.fail(`${label}은 이미 닫혔다.`);
152
+ const dir = L.workDir(main, st);
153
+ if (!fs.existsSync(dir)) L.fail(`orca-dispatch-task: ${label}의 워크트리가 없다(${dir}). 'status'로 확인한다.`);
154
+ if (a.agent && !AGENTS.includes(a.agent)) L.fail(`orca-dispatch-task: --agent '${a.agent}'을 모른다. ${AGENTS.join(' · ')} 중 하나를 넣는다.`);
155
+ const brief = { scope: a.scope, avoid: a.avoid, read: a.read, extra: a.extra };
156
+ if (a.manual) return manualDispatch(main, st, a, brief);
103
157
  if (!L.inOrca()) {
104
158
  L.fail(
105
- 'orca-dispatch-task: Orca 탭 안이 아니라 태스크 세션을 띄울 수 없다(Orca 전용 — Orca 환경 변수 ORCA_TERMINAL_HANDLE이 없다). ' +
106
- 'Orca 탭에서 연 오케스트레이션 세션에서 부르거나, 사용자가 그 태스크의 워크트리에서 직접 세션을 연다.',
159
+ 'orca-dispatch-task: Orca 탭 안이 아니라 태스크 세션을 띄울 수 없다(Orca 환경 변수 ORCA_TERMINAL_HANDLE이 없다). ' +
160
+ 'Orca 탭 안에서 불렀다면 MCP 도구가 아니라 CLI(npx @angar2/taskery orca-dispatch-task …)로 다시 부른다. ' +
161
+ 'Orca를 쓸 수 없으면 --manual로 다시 불러 사용자에게 손 전달한다.',
107
162
  );
108
163
  }
109
- L.requireInstalled(main);
110
164
  const missing = [];
111
- if (!a.task) missing.push('<task> — 태스크 번호 (첫 번째 인자)');
112
165
  if (!a.agent) missing.push(`--agent — ${AGENTS.join(' · ')}`);
113
166
  if (!a.model) missing.push('--model — 에이전트 모델 (사용자가 정한 것)');
114
167
  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
168
 
123
169
  const command = agentCommand(main, a.agent, String(a.model));
124
170
  const created = orca(['terminal', 'create', '--worktree', `path:${dir}`, '--title', label, '--command', command]);
@@ -131,7 +177,7 @@ async function orcaDispatchTask(ctx, a) {
131
177
  L.writeState(main, fresh);
132
178
  L.syncDoc(main, fresh);
133
179
 
134
- const text = firstInstruction(fresh, a.note);
180
+ const text = firstInstruction(fresh, brief);
135
181
  const manual = `탭 화면을 'orca terminal read --terminal ${handle}'로 확인하고, 준비되면 아래 첫 지시문을 'orca terminal send --terminal ${handle} --text "<첫 지시문>" --enter'로 보낸다.\n--- 첫 지시문 ---\n${text}`;
136
182
  let waited;
137
183
  try {
@@ -153,13 +199,23 @@ async function orcaDispatchTask(ctx, a) {
153
199
  if (r.refused) L.fail(`orca-dispatch-task: ${label} 탭(${handle})이 첫 지시문을 받지 않았다 — ${r.refused}\n${manual}`);
154
200
  return r.observed;
155
201
  };
156
- // 턴 시작이 관측되지 않으면 화면을 보고, 지시문이 보이지 않을 때만 한 번 더 보낸다(준비 전 입력이 사라진 경우)
202
+ // Orca가 턴 시작을 관측했다고 답해도 지시문이 입력창에 제출되지 않은 채 남은 적이 있다(stash TASK-105).
203
+ // 그래서 관측 여부와 무관하게 Enter만 한 번 더 보낸다 — 입력창이 비었거나 이미 진행 중이면 영향이 없다
204
+ const observed = send();
157
205
  let sendNote = null;
158
- if (!send()) {
206
+ try {
207
+ orca(['terminal', 'send', '--terminal', handle, '--enter']);
208
+ } catch (e) {
209
+ if (observed) sendNote = `확인용 Enter를 보내지 못했다(${e.message}) — 탭에서 진행을 확인한다.`;
210
+ else L.fail(`orca-dispatch-task: ${label} 탭(${handle})에서 첫 지시문의 턴 시작이 관측되지 않았고, Enter도 보내지 못했다 — ${e.message}\n'orca terminal read --terminal ${handle}'로 화면을 읽어, 지시문이 입력창에 남아 있으면 Enter를, 없으면 아래 첫 지시문을 보낸다.\n--- 첫 지시문 ---\n${text}`);
211
+ }
212
+ // 관측되지 않았으면 화면을 본다 — 지시문이 보이면 Enter로 들어간 것이고, 없으면 준비 전에 사라진 것이라 한 번 더 보낸다
213
+ if (!observed) {
214
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, SCREEN_SETTLE_MS);
159
215
  if (onScreen(handle, text.split('\n')[0].split(' ').slice(0, 4).join(' '))) {
160
- sendNote = '턴 시작은 관측되지 않았지만 탭 화면에 첫 지시문이 보인다 — 탭에서 진행을 확인한다.';
216
+ sendNote = '턴 시작이 관측되지 않아 Enter를 한 번 더 보냈고, 탭 화면에 첫 지시문이 보인다 — 탭에서 진행을 확인한다.';
161
217
  } else if (send()) {
162
- sendNote = '첫 전송은 턴 시작이 관측되지 않고 화면에도 없어 한 번 더 보냈다.';
218
+ sendNote = '첫 전송은 턴 시작이 관측되지 않고 Enter 뒤 화면에도 없어 한 번 더 보냈다.';
163
219
  } else {
164
220
  L.fail(`orca-dispatch-task: ${label} 탭(${handle})에 첫 지시문을 두 번 보냈지만 턴 시작이 관측되지 않았다.\n${manual}`);
165
221
  }
@@ -170,7 +226,7 @@ async function orcaDispatchTask(ctx, a) {
170
226
  `- 폴더: ${dir}`,
171
227
  `- 첫 지시문을 보냈다(시작: ${startSkill(fresh)}, 범위: ${fresh.range}).`,
172
228
  ...(sendNote ? [`- 알림: ${sendNote}`] : []),
173
- '다음: wait-reports를 백그라운드 셸로 걸어 둔다.',
229
+ '다음: wait-reports가 걸려 있지 않으면 백그라운드 셸로 걸어 둔다.',
174
230
  ].join('\n');
175
231
  }
176
232
 
@@ -223,18 +279,24 @@ function quietTabs(main) {
223
279
  return out;
224
280
  }
225
281
 
282
+ // 끝날 때 열린 태스크가 있으면 다시 걸라는 줄을 붙인다 — 다시 걸지 않으면 그 뒤의 보고를 놓친다
283
+ function withRearm(main, lines) {
284
+ if (L.listStates(main).some((st) => !st.closed)) lines.push('맡긴 태스크가 남아 있으면 곧바로 wait-reports를 다시 걸어 둔다(사용자 답을 기다리는 중이어도).');
285
+ return lines.join('\n');
286
+ }
287
+
226
288
  async function waitReports(ctx) {
227
289
  const main = ctx.main;
228
290
  L.requireInstalled(main);
229
291
  const until = Date.now() + WAIT_MS;
230
292
  for (;;) {
231
293
  const lines = takeUnread(main);
232
- if (lines.length) return lines.join('\n');
294
+ if (lines.length) return withRearm(main, lines);
233
295
  if (Date.now() >= until) break;
234
296
  await new Promise((r) => setTimeout(r, Math.min(POLL_MS, Math.max(0, until - Date.now()))));
235
297
  }
236
298
  const quiet = quietTabs(main);
237
- return [`보고 없이 ${L.minutes(WAIT_MS)}이 지났다.`, `오래 조용한 탭: ${quiet.length ? '' : '없음'}`.trimEnd(), ...quiet].join('\n');
299
+ return withRearm(main, [`보고 없이 ${L.minutes(WAIT_MS)}이 지났다.`, `오래 조용한 탭: ${quiet.length ? '' : '없음'}`.trimEnd(), ...quiet]);
238
300
  }
239
301
 
240
302
  module.exports = { orcaDispatchTask, reportTask, waitReports, firstInstruction };
package/bin/prepare.js CHANGED
@@ -218,12 +218,26 @@ async function installPackages(dir) {
218
218
  return [`npm ci 실패(${took}) — 워크트리에서 직접 설치한다(${dir}):\n${L.tail(r.out, 15)}`];
219
219
  }
220
220
 
221
- // PLAN.md 목록 항목 줄 끝에 태스크 번호를 붙인다(`--item`)
222
- function linkItem(main, plan, item, label) {
221
+ // PLAN.md `## 태스크 목록` 절에서 항목 줄을 찾는다. 다른 절의 번호 목록(목표 등)은 보지 않는다. 없으면 멈춘다
222
+ function itemLine(main, plan, item) {
223
223
  const file = path.join(main, '.taskery', 'plans', plan, 'PLAN.md');
224
224
  const lines = fs.readFileSync(file, 'utf8').split('\n');
225
- const idx = lines.findIndex((l) => new RegExp(`^\\s*${item}\\.\\s`).test(l));
226
- if (idx === -1) L.fail(`prepare-task: ${plan}/PLAN.md에 항목 ${item}이 없다. 목록 형식은 '${item}. <한 줄 설명> — 선행: <항목 번호들 또는 없음>'이다.`);
225
+ const fenced = L.fenceMask(lines);
226
+ const start = lines.findIndex((l, i) => !fenced[i] && l.trim() === '## 태스크 목록');
227
+ let idx = -1;
228
+ for (let i = start + 1; start !== -1 && i < lines.length && !(!fenced[i] && /^## /.test(lines[i])); i++) {
229
+ if (!fenced[i] && new RegExp(`^\\s*${item}\\.\\s`).test(lines[i])) {
230
+ idx = i;
231
+ break;
232
+ }
233
+ }
234
+ if (idx === -1) L.fail(`prepare-task: ${plan}/PLAN.md \`## 태스크 목록\`에 항목 ${item}이 없다. 목록 형식은 '${item}. <한 줄 설명> — 선행: <항목 번호들 또는 없음>'이다.`);
235
+ return { file, lines, idx };
236
+ }
237
+
238
+ // 항목 줄 끝에 태스크 번호를 붙인다(`--item`)
239
+ function linkItem(main, plan, item, label) {
240
+ const { file, lines, idx } = itemLine(main, plan, item);
227
241
  const linked = lines[idx].match(/TASK-\d+/g) || [];
228
242
  lines[idx] = `${lines[idx].replace(/\s+$/, '')} (${label})`;
229
243
  fs.writeFileSync(file, lines.join('\n'));
@@ -236,6 +250,7 @@ async function prepareTask(ctx, a) {
236
250
  const rule = L.readGitRule(main);
237
251
  const switches = validate(a, rule);
238
252
  const plan = resolvePlan(main, a.plan);
253
+ if (a.item != null) itemLine(main, plan, a.item); // 브랜치·워크트리를 만들기 전에 확인한다
239
254
  const noBranch = !!a['no-branch'];
240
255
  const noWorktree = noBranch || !!a['no-worktree'];
241
256
  const parent = checkMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angar2/taskery",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "AI 코딩 에이전트(Claude Code·Codex)에게 일을 태스크 단위로 맡겨 기획·개발·테스트·병합까지 진행하게 하는 작업 체계",
5
5
  "bin": {
6
6
  "taskery": "bin/taskery.js",
@@ -44,9 +44,9 @@
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은 `.state`에 남는다 |
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
52
  태스크 명령(`approve-plan`~`merge-task`)에 `--range "<새 범위>"`를 붙이면 태스크 문서 헤더 표의 범위 칸만 바뀐다.
@@ -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분으로 늘렸다 |
@@ -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