@angar2/taskery 0.4.0 → 0.5.0

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/bin/mcp.js ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ // taskery MCP 서버 — lib.js core 함수를 구조화 도구로 노출 (CLI와 동일 단일 홈, 로직 중복 0)
3
+ /**
4
+ * bin/mcp.js
5
+ * `npx @angar2/taskery mcp` — stdio MCP 서버. Claude(.mcp.json) / Codex(codex mcp add) 등록.
6
+ *
7
+ * 도구 8종 = CLI 명령과 동일한 lib.js 함수의 다른 입구(네이티브 구조화 인자/반환).
8
+ * backlog_add / backlog_get / backlog_mark — 활성 plan BACKLOG.md 조작
9
+ * set_status — task 헤더 상태 전이 (7×7 유효전이 검증, closed 거부)
10
+ * plan_init — plan 생성 (채번+골격+AGENT-GUIDE 갱신)
11
+ * task_init — task 분기 (fork 원자 채번 + §1.3 골격 자동 생성)
12
+ * task_close — close 결정적 준비만 (Phase커밋·status=closed·마커). 비가역 머지/정리는 스킬.
13
+ * status — 멀티세션 현황 + 활성 plan
14
+ *
15
+ * SDK(@modelcontextprotocol/sdk)는 dual CJS/ESM — require 조건으로 CJS 빌드 해결(검증 완료).
16
+ */
17
+
18
+ const path = require('path');
19
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
20
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
21
+ const { z } = require('zod');
22
+ const lib = require('./lib');
23
+
24
+ const VALID_TYPES = ['feature', 'bug', 'improve', 'refactor', 'docs', 'chore'];
25
+ const VALID_SIZES = ['micro', 'small', 'medium', 'large'];
26
+
27
+ // 메인 워크트리 검출 + manifest 확인. 실패 시 throw(핸들러 래퍼가 isError 응답으로 변환).
28
+ function resolveMainWt() {
29
+ const mainWt = lib.getMainWorktreePath();
30
+ if (!lib.readManifest(path.join(mainWt, lib.MANIFEST_NAME))) {
31
+ throw new Error(`${lib.MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`);
32
+ }
33
+ return mainWt;
34
+ }
35
+
36
+ // 'TASK-007' | 'TASK-7' | 7 | '7' → 7 (CLI 관용성 미러링)
37
+ function parseTaskNum(token) {
38
+ const m = String(token).match(/(\d+)/);
39
+ return m ? parseInt(m[1], 10) : NaN;
40
+ }
41
+
42
+ // 미등록 케이스 문서는 task 워크트리에 있음 — 서버 cwd 고정이라 SSoT로 워크트리 유도.
43
+ function resolveTaskDoc(mainWt, taskNum) {
44
+ const active = lib.getActiveTasks(mainWt).find((t) => t.taskNum === taskNum);
45
+ const wtPath = active
46
+ ? lib.getWorktreePath(lib.getProjectId(mainWt), {
47
+ taskNum,
48
+ src: active.src,
49
+ slug: active.slug,
50
+ })
51
+ : mainWt;
52
+ return lib.resolveTaskDocPath(mainWt, wtPath, { taskNum });
53
+ }
54
+
55
+ // 구조화 결과(JSON) + 선택 가이드 → MCP content.
56
+ function ok(data, guide) {
57
+ const text = guide ? `${JSON.stringify(data)}\n\n${guide}` : JSON.stringify(data);
58
+ return { content: [{ type: 'text', text }] };
59
+ }
60
+ function fail(message) {
61
+ return { content: [{ type: 'text', text: `오류: ${message}` }], isError: true };
62
+ }
63
+
64
+ // 핸들러 공통 래퍼 — 예외를 isError 응답으로 변환(서버 크래시 방지).
65
+ function handler(fn) {
66
+ return async (args) => {
67
+ try {
68
+ return await fn(args || {});
69
+ } catch (e) {
70
+ return fail(e.message);
71
+ }
72
+ };
73
+ }
74
+
75
+ const server = new McpServer({ name: 'taskery', version: lib.getPackageVersion() });
76
+
77
+ server.registerTool(
78
+ 'backlog_add',
79
+ {
80
+ description:
81
+ '활성 plan BACKLOG.md에 task 후보 1건 추가 (BL-NNN 채번 + withMetaLock 직렬화). 얕은 분석 메타를 인자로 받는다.',
82
+ inputSchema: {
83
+ type: z.enum(VALID_TYPES),
84
+ title: z.string().describe('한 줄 제목(한국어)'),
85
+ slug: z.string().describe('영어 kebab-case 슬러그'),
86
+ summary: z.string().describe('한 줄 개요'),
87
+ target: z.string().describe('한 줄 대상 영역'),
88
+ },
89
+ },
90
+ handler(async (a) => {
91
+ const mainWt = resolveMainWt();
92
+ const blNum = await lib.appendBacklogItem(mainWt, a);
93
+ const blId = `BL-${String(blNum).padStart(3, '0')}`;
94
+ return ok({ blNum, blId }, `${blId} 추가됨. 진행 의향이면 task_init이 이 메타로 워크트리를 분기한다.`);
95
+ }),
96
+ );
97
+
98
+ server.registerTool(
99
+ 'backlog_get',
100
+ {
101
+ description:
102
+ '활성 plan BACKLOG.md에서 BL-NNN 항목 메타 조회 → { blId, status, type, title, slug, summary, target, taskNums }.',
103
+ inputSchema: { blId: z.string().regex(/^BL-\d+$/, 'BL-NNN 형식') },
104
+ },
105
+ handler(async (a) => {
106
+ const mainWt = resolveMainWt();
107
+ const item = lib.parseBacklogItem(mainWt, a.blId);
108
+ if (!item) throw new Error(`${a.blId} 항목 없음.`);
109
+ return ok(item);
110
+ }),
111
+ );
112
+
113
+ server.registerTool(
114
+ 'backlog_mark',
115
+ {
116
+ description: 'BL-NNN을 [x] 확인 마킹 + TASK-NNN 연결 (withMetaLock). task_init 직후 호출.',
117
+ inputSchema: {
118
+ blId: z.string().regex(/^BL-\d+$/, 'BL-NNN 형식'),
119
+ task: z.union([z.string(), z.number()]).describe('TASK-NNN 또는 번호'),
120
+ },
121
+ },
122
+ handler(async (a) => {
123
+ const taskNum = parseTaskNum(a.task);
124
+ if (!Number.isInteger(taskNum)) throw new Error(`TASK 번호 파싱 실패 (받음: ${a.task}).`);
125
+ const mainWt = resolveMainWt();
126
+ await lib.markBacklogChecked(mainWt, a.blId, taskNum);
127
+ return ok({ blId: a.blId, taskNum });
128
+ }),
129
+ );
130
+
131
+ server.registerTool(
132
+ 'set_status',
133
+ {
134
+ description:
135
+ 'task 헤더 상태 전이 (TASK_DOC_RULE §1.2 7×7 유효전이 검증). 잘못된 전이 거부. closed 전이는 task_close가 담당하므로 여기선 거부.',
136
+ inputSchema: {
137
+ task: z.union([z.string(), z.number()]).describe('TASK-NNN 또는 번호'),
138
+ state: z.string().describe('전이 목표 상태'),
139
+ },
140
+ },
141
+ handler(async (a) => {
142
+ const taskNum = parseTaskNum(a.task);
143
+ if (!Number.isInteger(taskNum)) throw new Error(`TASK 번호 파싱 실패 (받음: ${a.task}).`);
144
+ if (!Object.prototype.hasOwnProperty.call(lib.TRANSITIONS, a.state)) {
145
+ throw new Error(`알 수 없는 상태: ${a.state} (유효: ${Object.keys(lib.TRANSITIONS).join(', ')})`);
146
+ }
147
+ if (a.state === 'closed') {
148
+ throw new Error(`closed 전이는 set_status가 아니라 task_close(close 파이프라인)가 담당.`);
149
+ }
150
+ const mainWt = resolveMainWt();
151
+ const resolved = resolveTaskDoc(mainWt, taskNum);
152
+ if (!resolved) throw new Error(`TASK-${String(taskNum).padStart(3, '0')} 문서를 찾지 못함.`);
153
+ const result = await lib.setStatus(resolved.file, a.state);
154
+ return ok({ taskNum, ...result });
155
+ }),
156
+ );
157
+
158
+ server.registerTool(
159
+ 'plan_init',
160
+ {
161
+ description:
162
+ 'plan(기능 그룹) 생성 — 채번 + 폴더 + ROADMAP/PLAN/BACKLOG 골격 + AGENT-GUIDE 활성plan 갱신. 이후 골격 내용은 LLM이 채움. legacy 폴더 잔존 시 gated 반환(force로 재호출).',
163
+ inputSchema: {
164
+ slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, '영어 kebab-case'),
165
+ force: z.boolean().optional().describe('legacy 게이트 무시'),
166
+ },
167
+ },
168
+ handler(async (a) => {
169
+ const mainWt = resolveMainWt();
170
+ const result = lib.initPlan(mainWt, a.slug, { force: !!a.force });
171
+ if (result.gated) {
172
+ return ok(
173
+ { gated: true, legacyDirs: result.legacyDirs },
174
+ `plans/에 NNN 채번 아닌 폴더(${result.legacyDirs.join(', ')}) 잔존 — 사용자 confirm 후 force:true로 재호출.`,
175
+ );
176
+ }
177
+ return ok(
178
+ { plan: result.plan, nnn: result.nnn, planDir: result.planDir, tasksDir: result.tasksDir },
179
+ `골격 생성됨. ROADMAP Stage 내용·PLAN 링크·FEATURES/UX-UI delta는 placeholder를 채운다.`,
180
+ );
181
+ }),
182
+ );
183
+
184
+ server.registerTool(
185
+ 'task_init',
186
+ {
187
+ description:
188
+ 'task 분기 — 채번+워크트리+브랜치 생성을 init 락으로 원자 실행. size·title 주면 §1.3 골격 task.md도 자동 생성(생성일·status=draft 자동). 통상 /task-init 경유.',
189
+ inputSchema: {
190
+ type: z.enum(VALID_TYPES),
191
+ dev: z.string().describe('개발자 식별자 (claude / angar2 등)'),
192
+ src: z.string().regex(/^(BL-\d+|RM-\d+|DR)$/, 'BL-NNN | RM-NNN | DR'),
193
+ slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, '영어 kebab-case'),
194
+ size: z.enum(VALID_SIZES).optional(),
195
+ title: z.string().optional().describe('한국어 제목 (size와 함께 주면 골격 생성)'),
196
+ promote: z.boolean().optional(),
197
+ },
198
+ },
199
+ handler(async (a) => {
200
+ const mainWt = resolveMainWt();
201
+ const result = await lib.forkTask(mainWt, {
202
+ type: a.type,
203
+ dev: a.dev,
204
+ src: a.src,
205
+ slug: a.slug,
206
+ });
207
+ let out = result;
208
+ if (a.size && a.title) {
209
+ try {
210
+ const doc = lib.scaffoldTaskDoc(mainWt, result.wtPath, {
211
+ taskNum: result.taskNum,
212
+ slug: a.slug,
213
+ title: a.title,
214
+ type: a.type,
215
+ size: a.size,
216
+ promoted: !!a.promote,
217
+ });
218
+ out = { ...result, docPath: doc.file, registered: doc.registered, promoted: doc.promoted };
219
+ } catch (e) {
220
+ out = { ...result, scaffoldError: e.message };
221
+ }
222
+ }
223
+ return ok(out, `워크트리 생성됨. 제목 확인 후 /task-plan으로 진행.`);
224
+ }),
225
+ );
226
+
227
+ server.registerTool(
228
+ 'task_close',
229
+ {
230
+ description:
231
+ 'close 결정적 준비만 — Phase 커밋·status=closed·문서 커밋·추적마커. 게이트(status=tested)·매핑 모호 시 blocked 반환. 비가역(dev --no-ff 머지·worktree/branch 정리·충돌 해결)은 스킬(task-close)이 후속 수행.',
232
+ inputSchema: { task: z.union([z.string(), z.number()]).describe('TASK-NNN 또는 번호') },
233
+ },
234
+ handler(async (a) => {
235
+ const taskNum = parseTaskNum(a.task);
236
+ if (!Number.isInteger(taskNum)) throw new Error(`TASK 번호 파싱 실패 (받음: ${a.task}).`);
237
+ const mainWt = resolveMainWt();
238
+ const result = await lib.closeTask(mainWt, { taskNum });
239
+ if (result.blocked === 'status') {
240
+ return ok(result, `게이트 차단 — status=${result.current} (tested 아님). /task-test 통과 후 재호출.`);
241
+ }
242
+ if (result.blocked === 'mapping') {
243
+ return ok(
244
+ result,
245
+ `Phase↔파일 매핑 모호 (${result.reason}) — Dev Plan '파일' 필드 확인 후 수동 Phase 커밋 → task_close 재호출.`,
246
+ );
247
+ }
248
+ return ok(result, `prep 완료. 비가역(사전 rebase·충돌해결·머지 락·dev 머지·정리)은 task-close 스킬이 후속 수행.`);
249
+ }),
250
+ );
251
+
252
+ server.registerTool(
253
+ 'status',
254
+ {
255
+ description: '멀티세션 현황 — 진행중 task + 워크트리 + 활성 plan 버전.',
256
+ inputSchema: {},
257
+ },
258
+ handler(async () => {
259
+ const mainWt = resolveMainWt();
260
+ const activeTasks = lib.getActiveTasks(mainWt);
261
+ let activePlan = null;
262
+ try {
263
+ activePlan = lib.getActiveVersion(mainWt);
264
+ } catch (e) {
265
+ activePlan = null;
266
+ }
267
+ return ok({ activePlan, activeTasks });
268
+ }),
269
+ );
270
+
271
+ async function main() {
272
+ const transport = new StdioServerTransport();
273
+ await server.connect(transport);
274
+ }
275
+
276
+ main().catch((e) => {
277
+ console.error(`taskery mcp 실패: ${e.message}`);
278
+ process.exit(1);
279
+ });
package/bin/plan.js ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ // plan(기능 그룹) 생성 CLI — 채번+폴더+ROADMAP/PLAN/BACKLOG 골격+AGENT-GUIDE 갱신 원자 실행
3
+ /**
4
+ * bin/plan.js
5
+ * `npx @angar2/taskery plan-init <slug> [--force]` — plan 생성.
6
+ *
7
+ * 채번(NNN) + legacy 게이트 + 폴더 mkdir(plans/<NNN_slug> + tasks/<NNN_slug>/{spec-diffs,screenshots,mockup})
8
+ * + ROADMAP/PLAN/BACKLOG 골격 Write + AGENT-GUIDE 활성 plan 갱신을 코드가 수행.
9
+ * 성공 시 JSON 한 줄 — { plan, nnn, planDir, tasksDir }. 이후 ROADMAP Stage 내용·PLAN 링크·
10
+ * FEATURES/UX-UI delta는 LLM(plan-init 스킬)이 골격 placeholder를 채운다.
11
+ *
12
+ * legacy 폴더(NNN_ 아닌 plan 폴더) 잔존 + --force 없음 → exit 2 + { gated:true, legacyDirs }
13
+ * (스킬이 사용자 confirm 후 --force로 재호출). 그 외 실패 시 stderr + exit 1.
14
+ */
15
+
16
+ const path = require('path');
17
+
18
+ const {
19
+ MANIFEST_NAME,
20
+ readManifest,
21
+ getMainWorktreePath,
22
+ initPlan,
23
+ } = require('./lib');
24
+
25
+ function main() {
26
+ const argv = process.argv.slice(2);
27
+ const positional = [];
28
+ let force = false;
29
+ for (const a of argv) {
30
+ if (a === '--force') force = true;
31
+ else if (!a.startsWith('--')) positional.push(a);
32
+ }
33
+ const slug = positional[0];
34
+
35
+ if (!slug) {
36
+ console.error('사용법: npx @angar2/taskery plan-init <slug> [--force]');
37
+ console.error('예: npx @angar2/taskery plan-init compare-products');
38
+ process.exit(1);
39
+ }
40
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
41
+ console.error(`slug은 영어 kebab-case (받음: ${slug})`);
42
+ process.exit(1);
43
+ }
44
+
45
+ let mainWt;
46
+ try {
47
+ mainWt = getMainWorktreePath();
48
+ } catch (e) {
49
+ console.error(`taskery plan-init 실패: ${e.message}`);
50
+ process.exit(1);
51
+ }
52
+ if (!readManifest(path.join(mainWt, MANIFEST_NAME))) {
53
+ console.error(`${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`);
54
+ process.exit(1);
55
+ }
56
+
57
+ let result;
58
+ try {
59
+ result = initPlan(mainWt, slug, { force });
60
+ } catch (e) {
61
+ console.error(`taskery plan-init 실패: ${e.message}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ if (result.gated) {
66
+ // legacy 폴더 잔존 — 사용자 confirm 전 생성 금지. 스킬이 경고 + confirm 후 --force 재호출.
67
+ console.error(
68
+ `plans/에 NNN 채번이 아닌 폴더(${result.legacyDirs.join(', ')})가 있어 활성 plan이 갈릴 위험. ` +
69
+ `수동 이전 후 진행하거나, 그대로 진행하려면 --force 추가.`,
70
+ );
71
+ process.stdout.write(JSON.stringify({ gated: true, legacyDirs: result.legacyDirs }) + '\n');
72
+ process.exit(2);
73
+ }
74
+
75
+ process.stdout.write(
76
+ JSON.stringify({
77
+ plan: result.plan,
78
+ nnn: result.nnn,
79
+ planDir: result.planDir,
80
+ tasksDir: result.tasksDir,
81
+ }) + '\n',
82
+ );
83
+ }
84
+
85
+ main();
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // task 상태 전이 CLI — 7×7 유효전이 검증 후 헤더 상태 컬럼 치환 (잘못된 전이 거부)
3
+ /**
4
+ * bin/set-status.js
5
+ * `npx @angar2/taskery set-status <TASK-NNN> <state>` — task 헤더 상태 전이.
6
+ *
7
+ * 현재 상태를 코드로 읽고 TASK_DOC_RULE §1.2 유효전이표로 검증 → 통과 시 헤더 치환.
8
+ * 잘못된 전이(예: draft→closed)는 거부. 같은 상태 재지정은 idempotent no-op.
9
+ * task 문서 위치는 resolveTaskDocPath가 .gitignore 케이스로 단일 판정.
10
+ *
11
+ * closed 전이는 본 CLI가 거부 — `/task-close`(close 파이프라인)가 내부 setStatus로 처리.
12
+ * stdout: { taskNum, from, to, changed, path }. 실패 시 stderr + exit 1.
13
+ */
14
+
15
+ const path = require('path');
16
+
17
+ const {
18
+ MANIFEST_NAME,
19
+ readManifest,
20
+ getMainWorktreePath,
21
+ gitCapture,
22
+ resolveTaskDocPath,
23
+ setStatus,
24
+ TRANSITIONS,
25
+ } = require('./lib');
26
+
27
+ async function main() {
28
+ const [taskArg, state] = process.argv.slice(2);
29
+
30
+ if (!taskArg || !state) {
31
+ console.error('사용법: npx @angar2/taskery set-status <TASK-NNN> <state>');
32
+ console.error(`state: ${Object.keys(TRANSITIONS).join(' | ')}`);
33
+ process.exit(1);
34
+ }
35
+ const m = String(taskArg).match(/(\d+)/);
36
+ if (!m) {
37
+ console.error(`TASK 번호 파싱 실패 (받음: ${taskArg}). 예: TASK-007`);
38
+ process.exit(1);
39
+ }
40
+ const taskNum = parseInt(m[1], 10);
41
+
42
+ if (!Object.prototype.hasOwnProperty.call(TRANSITIONS, state)) {
43
+ console.error(`알 수 없는 상태: ${state} (유효: ${Object.keys(TRANSITIONS).join(', ')})`);
44
+ process.exit(1);
45
+ }
46
+ if (state === 'closed') {
47
+ console.error(`closed 전이는 set-status가 아니라 '/task-close'(close 파이프라인)가 담당.`);
48
+ process.exit(1);
49
+ }
50
+
51
+ let mainWt;
52
+ try {
53
+ mainWt = getMainWorktreePath();
54
+ } catch (e) {
55
+ console.error(`taskery set-status 실패: ${e.message}`);
56
+ process.exit(1);
57
+ }
58
+ if (!readManifest(path.join(mainWt, MANIFEST_NAME))) {
59
+ console.error(`${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`);
60
+ process.exit(1);
61
+ }
62
+
63
+ // 현재 작업트리 toplevel = 미등록 케이스의 문서 base (호출 위치 = 해당 워크트리).
64
+ const wtPath = gitCapture(process.cwd(), ['rev-parse', '--show-toplevel'], { allowFail: true }) || mainWt;
65
+
66
+ const resolved = resolveTaskDocPath(mainWt, wtPath, { taskNum });
67
+ if (!resolved) {
68
+ console.error(
69
+ `TASK-${String(taskNum).padStart(3, '0')} 문서를 찾지 못함 (.project/tasks/*/ 아래 단일 파일/폴더 승격 모두 탐색).`,
70
+ );
71
+ process.exit(1);
72
+ }
73
+
74
+ try {
75
+ const result = await setStatus(resolved.file, state);
76
+ process.stdout.write(JSON.stringify({ taskNum, ...result }) + '\n');
77
+ } catch (e) {
78
+ console.error(`taskery set-status 실패: ${e.message}`);
79
+ process.exit(1);
80
+ }
81
+ }
82
+
83
+ main();
package/bin/taskery.js CHANGED
@@ -9,7 +9,12 @@
9
9
  * update — 최신 버전 fetch + manifest 비교 + 머지 갱신
10
10
  * status — 멀티세션 현황 요약 (진행중 태스크 / 워크트리 / 머지 락, 0.1.2+)
11
11
  * prune — stale 워크트리 / 브랜치 대화형 정리 (0.1.2+)
12
- * fork — task 분기 (채번+워크트리+브랜치 생성을 init 락으로 원자 실행, 0.3.2+)
12
+ * fork — task 분기 (채번+워크트리+브랜치 생성 init 락 원자 실행, 0.3.2+) + 골격 task.md 자동 생성(--size/--title)
13
+ * backlog-add/get/mark — 활성 plan BACKLOG.md 조작 (채번/조회/확인 마킹, 코드화)
14
+ * set-status — task 헤더 상태 전이 (7×7 유효전이 검증, 코드화)
15
+ * plan-init — plan 생성 (채번+폴더+ROADMAP/PLAN/BACKLOG 골격+AGENT-GUIDE 갱신, 코드화)
16
+ * close — close 결정적 준비 (Phase커밋+status=closed+문서커밋+추적마커; 비가역 머지/정리는 스킬, 코드화)
17
+ * mcp — stdio MCP 서버 기동 (lib core를 도구 8종으로 노출; Claude/Codex 등록)
13
18
  * help — 사용법 출력
14
19
  */
15
20
 
@@ -18,9 +23,10 @@ const path = require('path');
18
23
 
19
24
  const sub = process.argv[2];
20
25
 
21
- function runScript(scriptName) {
26
+ function runScript(scriptName, injectArgs = []) {
27
+ // injectArgs: dispatcher가 스크립트에 주입하는 선행 인자(예: backlog 서브op). 그 뒤로 사용자 인자.
22
28
  const scriptPath = path.resolve(__dirname, scriptName);
23
- const r = spawnSync(process.execPath, [scriptPath, ...process.argv.slice(3)], {
29
+ const r = spawnSync(process.execPath, [scriptPath, ...injectArgs, ...process.argv.slice(3)], {
24
30
  stdio: 'inherit',
25
31
  });
26
32
  process.exit(r.status ?? 1);
@@ -35,7 +41,14 @@ function help() {
35
41
  npx @angar2/taskery update 최신 버전 fetch + 머지 갱신
36
42
  npx @angar2/taskery status 멀티세션 현황 (진행중 태스크 / 워크트리 / 머지 락)
37
43
  npx @angar2/taskery prune stale 워크트리 / 브랜치 대화형 정리
38
- npx @angar2/taskery fork <type> <dev> <src> <slug> task 분기 (통상 /task-init 경유)
44
+ npx @angar2/taskery fork <type> <dev> <src> <slug> [--size <s> --title "<제목>" --promote] task 분기 + 골격 생성 (통상 /task-init 경유)
45
+ npx @angar2/taskery backlog-add --type <t> --title <제목> --slug <slug> --summary <개요> --target <대상영역> 백로그 추가
46
+ npx @angar2/taskery backlog-get <BL-NNN> 백로그 항목 조회 (JSON)
47
+ npx @angar2/taskery backlog-mark <BL-NNN> <TASK-NNN> 백로그 확인 마킹
48
+ npx @angar2/taskery set-status <TASK-NNN> <state> task 상태 전이 (유효전이 검증)
49
+ npx @angar2/taskery plan-init <slug> [--force] plan 생성 (채번+폴더+골격+활성plan 갱신)
50
+ npx @angar2/taskery close <TASK-NNN> close 결정적 준비 (Phase커밋+status=closed+추적마커)
51
+ npx @angar2/taskery mcp MCP stdio 서버 기동 (Claude .mcp.json / Codex codex mcp add 등록)
39
52
  npx @angar2/taskery help 도움말
40
53
 
41
54
  새 프로젝트 시작:
@@ -79,6 +92,27 @@ switch (sub) {
79
92
  case 'fork':
80
93
  runScript('fork.js');
81
94
  break;
95
+ case 'backlog-add':
96
+ runScript('backlog.js', ['add']);
97
+ break;
98
+ case 'backlog-get':
99
+ runScript('backlog.js', ['get']);
100
+ break;
101
+ case 'backlog-mark':
102
+ runScript('backlog.js', ['mark']);
103
+ break;
104
+ case 'set-status':
105
+ runScript('set-status.js');
106
+ break;
107
+ case 'plan-init':
108
+ runScript('plan.js');
109
+ break;
110
+ case 'close':
111
+ runScript('close.js');
112
+ break;
113
+ case 'mcp':
114
+ runScript('mcp.js');
115
+ break;
82
116
  case 'help':
83
117
  case '--help':
84
118
  case '-h':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angar2/taskery",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Lightweight task system for Claude Code & Codex — 1 메인 세션(또는 멀티세션) + 스킬 9종 + catastrophic hook 안전망 2종",
5
5
  "bin": {
6
6
  "taskery": "bin/taskery.js",
@@ -43,6 +43,8 @@
43
43
  "git": ">=2.31.0"
44
44
  },
45
45
  "dependencies": {
46
- "proper-lockfile": "^4.1.2"
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "proper-lockfile": "^4.1.2",
48
+ "zod": "^4.4.3"
47
49
  }
48
50
  }
@@ -5,81 +5,31 @@ description: 사용자 발화로 *plan(기능 그룹)별* tasks/<NNN_slug>/BACKL
5
5
 
6
6
  # /add-backlog
7
7
 
8
- ## 개요
9
-
10
- `/add-backlog`는 사용자 발화를 받아 *plan(기능 그룹)별 백로그* `.project/tasks/<NNN_slug>/BACKLOG.md`에 task 후보를 1건씩 누적한다. 멀티세션 환경에서 `withMetaLock`으로 동시 쓰기 직렬화. **얕은 분석**만 — 코드 직접 탐색 X, 추정 수준. 본격 분석은 다음 단계 `/task-plan`에서.
11
-
12
- 글로벌 `.project/BACKLOG.md` (plan 기획 후보 카탈로그) 는 `/plan-init` 영역으로 본 스킬 무관.
8
+ 사용자 발화를 받아 *활성 plan별 백로그* `.project/tasks/<NNN_slug>/BACKLOG.md`에 task 후보를 1건씩 누적한다. **얕은 분석**만 — 코드 직접 탐색 X, 추정 수준. 본격 분석은 다음 단계 `/task-plan`.
13
9
 
14
10
  ## 호출 시점
15
11
 
16
- - 사용자 명시 호출: `/add-backlog 로그인 빈 화면 떠`
17
- - 발화 캐치: *"~ 백로그에 추가해 줘"* / *"백로그에 ~"* 등 백로그 추가 의도 발화 description 매칭으로 메인이 자동 발동
18
-
19
- ## 입력 처리
20
-
21
- - 무인자 호출: 사용자가 *"백로그 추가"*만 말한 경우 — 어떤 항목인지 한 줄 인터뷰
22
- - 인자 호출: 인자에 백로그 후보 주제. 1건 기본. 다건 명시 시 순차 처리
23
-
24
- ## 단계 (Step 1~6)
12
+ - 명시 호출: `/add-backlog 로그인 빈 화면 떠`
13
+ - 발화 캐치: *"~ 백로그에 추가해 줘"* 등 백로그 추가 의도 발화
14
+ - 무인자 호출 시 *"어떤 항목? 한 줄로 알려줘"* 인터뷰. 다건 명시 시 1건씩 순차 처리.
25
15
 
26
- ### Step 1 — 사전 검증
16
+ ## 판단 (LLM, 코드 탐색 X)
27
17
 
28
- - 메인 워크트리 검출:
29
- ```sh
30
- MAIN_WT=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")
31
- ```
32
- - 메인 워크트리 dev 체크아웃 검증 — 위배 시 사용자 호출 + 중단
33
- - 활성 plan 검출 — `$MAIN_WT/.project/AGENT-GUIDE.md` `## 활성 plan 버전` 섹션 다음 비어 있지 않은 첫 줄에서 활성 plan 식별자(첫 토큰 — `NNN_slug` 폴더명) 추출
34
- - BACKLOG.md 경로 = `$MAIN_WT/.project/tasks/<NNN_slug>/BACKLOG.md`. 부재 시 사용자 호출: *"`/plan-init` 먼저 호출해 활성 plan 디렉토리 + 빈 BACKLOG.md 생성 필요"* + 중단
18
+ 발화에서 후보 1건을 뽑아 추정 수준의 메타를 산출한다:
35
19
 
36
- ### Step 2 — 백로그 항목 후보 추출
37
-
38
- - 사용자 발화에서 1건 추출. 다건 명시 시 순차 처리 (각 1건씩 본 흐름 반복)
39
- - 입력이 비어 있으면 사용자에게 *"어떤 항목? 한 줄로 알려줘"* 인터뷰
40
-
41
- ### Step 3 — 얕은 분석 (LLM, 코드 탐색 X)
42
-
43
- 추정 수준의 메타 산출:
44
- - **유형** (`feature` / `bug` / `improve` / `refactor` / `docs` / `chore`) — 자동 추정, 모호 시 사용자 confirm
20
+ - **유형** (`feature` / `bug` / `improve` / `refactor` / `docs` / `chore`) — 자동 추정, *모호 시 사용자 confirm* (예: *"메뉴 검색 개선"* = feature/improve 모호)
45
21
  - **제목** — 한 줄, 간결, 한국어
46
22
  - **개요** — 한 줄. bug → 원인 추정 / feature → 구상 / improve → 개선 방향
47
- - **대상 영역** — 한 줄. 이 파일 / 모듈일 거다 — 추정 수준
23
+ - **대상 영역** — 한 줄. 추정 수준 (이 파일 / 모듈일 거다)
24
+ - **슬러그** — 한국어 제목 → 영어 *의미 변환* → kebab-case (3 단어 이내, 음역 X). 예: *"로그인 빈 화면"* → `login-empty-page`
48
25
 
49
- > *코드 직접 탐색 금지*. 본격 분석은 다음 단계 `/task-plan`에서. 본 스킬은 *메모지 누적*만.
26
+ > *코드 직접 탐색 금지*. 본 스킬은 *메모지 누적*만. 자동 추정 진행 X — 메타는 사용자 OK 또는 명백한 발화 매칭 후에만 추가.
50
27
 
51
- ### Step 4 — 결정적 슬러그 산출
28
+ ## 추가 + 보고
52
29
 
53
- - 한국어 제목 → 영어 *의미 변환* → kebab-case (3 단어 이내, 음역 X)
54
- - 예: *"로그인 페이지 빈 화면"* → `login-empty-page` / *"다크모드 토글"* → `dark-mode-toggle`
30
+ `backlog_add` 도구로 추가한다 (MCP 도구 `backlog_add` 또는 동일 `npx @angar2/taskery backlog-add --type <t> --title <제목> --slug <slug> --summary <개요> --target <대상영역>`). 채번(BL-NNN)·서식·placeholder 치환·`withMetaLock` 직렬화·메인 워크트리 위치는 코드가 보장한다. 활성 plan BACKLOG.md 부재 시 도구가 에러를 반환한다 → *"`/plan-init` 먼저 호출해 활성 plan 디렉토리 + BACKLOG.md 생성 필요"* 안내 후 중단.
55
31
 
56
- ### Step 5 — BL-NNN 채번 + append (withMetaLock 직렬화)
57
-
58
- `bin/lib.js`의 `appendBacklogItem(mainWtPath, meta)` 호출.
59
-
60
- ```js
61
- const lib = require('@angar2/taskery/bin/lib'); // 또는 패키지 내부 경로
62
- const blNum = await lib.appendBacklogItem(MAIN_WT, {
63
- type: '<유형>',
64
- title: '<제목>',
65
- slug: '<슬러그>',
66
- summary: '<개요>',
67
- target: '<대상 영역>',
68
- });
69
- ```
70
-
71
- 내부 흐름:
72
- 1. `withMetaLock(backlogPath)` 안에서:
73
- - BACKLOG.md 전체에서 `BL-(\d+)` 패턴 grep → 최대값 + 1. 빈 파일 → BL-001 시작
74
- - 항목 블록 포맷팅:
75
- ```
76
- - [ ] **BL-NNN** [유형] 제목 `slug`
77
- - 개요: ...
78
- - 대상 영역: ...
79
- ```
80
- - plan-init 빈 골격의 placeholder 라인 (`(사용자 발화 또는 task-close 직후 메인 감지로 한 행씩 추가. 빈 상태 default.)`) 발견 시 → 그 자리에 치환. 미발견 시 → 파일 끝 빈 줄 + 블록 append
81
-
82
- ### Step 6 — 결과 보고
32
+ 성공 반환 `{ blNum, blId }`로 보고한다:
83
33
 
84
34
  ```
85
35
  ✅ BL-<NNN> [<유형>] <제목> — <NNN_slug> 백로그 추가됐어.
@@ -89,29 +39,8 @@ const blNum = await lib.appendBacklogItem(MAIN_WT, {
89
39
  다음: 진행 의향이면 *"BL-<NNN> 진행"*이라고 말해. `/task-init`이 메타 그대로 받아 워크트리를 분기하고 항목을 `[x]` 확인 마킹할게.
90
40
  ```
91
41
 
92
- ## 도구 가이드
93
-
94
- - **Bash**: 메인 워크트리 검출 / dev 체크아웃 검증
95
- - **Read**: `$MAIN_WT/.project/AGENT-GUIDE.md` (활성 plan 검출) / 기존 BACKLOG.md 정독 (중복 / 유사 항목 사전 확인)
96
- - **bin/lib.js**: `getActiveVersion` / `getBacklogPath` / `appendBacklogItem` 호출
97
- - **AskUserQuestion**: 유형 모호 시 confirm — 한 번에 한 질문
98
-
99
42
  ## 주의사항
100
43
 
101
- - **얕은 분석만** — 본 스킬에서 도메인 코드 Read · Grep 금지. 본격 분석은 `/task-plan`
102
- - **유형 자동 추정 + 모호 시 confirm** — *"메뉴 검색 개선"* 같이 feature/improve 모호 시 사용자 결정 받기
103
- - **메인 워크트리 dev 검증 누락 X** — 위배 시 즉시 중단. 워크트리 안에서 호출돼도 메인 절대 경로의 BACKLOG.md에 쓰기
104
- - **활성 plan 부재 시 즉시 중단** — `AGENT-GUIDE.md` 부재 / `## 활성 plan 버전` 섹션 부재 / plan 식별자 미설정(`<예: …>` 자리표시자) 모두 명확한 에러 메시지 후 사용자 호출
105
- - **결정적 슬러그 best-effort** — LLM 비결정성 수용. race 차단 본체는 SSoT 2층(`/task-init` §4.4)이 담당. 슬러그 변형 발생 시 사용자 confirm
106
- - **글로벌 BACKLOG.md (plan 기획 후보)** 와 혼동 X — 본 스킬은 *활성 plan(기능 그룹)별*만 다룸. 글로벌은 `/plan-init` 영역
107
- - **자동 추정 진행 X** — 유형 / 제목 / 개요 / 대상 영역 모두 사용자 OK 또는 명백한 발화 매칭 후에만 BACKLOG.md 쓰기
108
-
109
- ## 상태 전이
110
-
111
- 해당 없음 (백로그 메모지 누적 — task 상태 X).
112
-
113
- ## 멀티세션 정합
114
-
115
- - **위치 = 메인 워크트리 절대 경로** — 워크트리 안에서 호출돼도 메인 절대 경로의 BACKLOG.md를 단일 소스로 사용
116
- - **`withMetaLock` 직렬화** — 두 메인 세션이 동시 `/add-backlog` 호출 시 락으로 직렬화 (proper-lockfile, stale=30s, retries=5). 한 세션 `/add-backlog` + 다른 세션 `/task-init` (BL 마킹) 동시 시도 시 락 정합
117
- - **체크박스 의미** — `[ ]` = 미확인 (task로 옮기지 않은 메모) / `[x]` = 확인 완료 (task로 옮김). **dev 머지 완료 의미 X** — 완료 추적은 `git log dev --grep 'BL-NNN'` + 브랜치명 + `taskery status`
44
+ - **얕은 분석만** — 도메인 코드 Read · Grep 금지. 본격 분석은 `/task-plan`.
45
+ - **글로벌 `.project/BACKLOG.md`(plan 기획 후보)와 혼동 X** — 본 스킬은 *활성 plan별*만. 글로벌은 `/plan-init` 영역.
46
+ - **체크박스 의미** — `[ ]` = 미확인(메모) / `[x]` = 확인(task로 옮김). dev 머지·완료 여부와 무관 — 완료 추적은 `taskery status` + dev 머지커밋.