@angar2/taskery 0.3.1 → 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/README.md +4 -1
- package/bin/backlog.js +131 -0
- package/bin/close.js +74 -0
- package/bin/fork.js +109 -0
- package/bin/init.js +31 -3
- package/bin/lib.js +613 -1
- package/bin/mcp.js +279 -0
- package/bin/plan.js +85 -0
- package/bin/set-status.js +83 -0
- package/bin/taskery.js +41 -2
- package/package.json +4 -2
- package/template/.claude/skills/run-team/SKILL.md +145 -0
- package/template/.project/rules/GIT_RULE.md +2 -1
- package/template/AGENTS.md +9 -0
- package/template/CLAUDE.md +17 -1
- package/template/shared/skills/add-backlog/SKILL.md +16 -87
- package/template/shared/skills/plan-init/SKILL.md +41 -116
- package/template/shared/skills/task-close/SKILL.md +52 -141
- package/template/shared/skills/task-dev/SKILL.md +4 -4
- package/template/shared/skills/task-init/SKILL.md +37 -244
- package/template/shared/skills/task-plan/SKILL.md +3 -2
- package/template/shared/skills/task-test/SKILL.md +5 -4
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,6 +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+) + 골격 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 등록)
|
|
12
18
|
* help — 사용법 출력
|
|
13
19
|
*/
|
|
14
20
|
|
|
@@ -17,9 +23,10 @@ const path = require('path');
|
|
|
17
23
|
|
|
18
24
|
const sub = process.argv[2];
|
|
19
25
|
|
|
20
|
-
function runScript(scriptName) {
|
|
26
|
+
function runScript(scriptName, injectArgs = []) {
|
|
27
|
+
// injectArgs: dispatcher가 스크립트에 주입하는 선행 인자(예: backlog 서브op). 그 뒤로 사용자 인자.
|
|
21
28
|
const scriptPath = path.resolve(__dirname, scriptName);
|
|
22
|
-
const r = spawnSync(process.execPath, [scriptPath, ...process.argv.slice(3)], {
|
|
29
|
+
const r = spawnSync(process.execPath, [scriptPath, ...injectArgs, ...process.argv.slice(3)], {
|
|
23
30
|
stdio: 'inherit',
|
|
24
31
|
});
|
|
25
32
|
process.exit(r.status ?? 1);
|
|
@@ -34,6 +41,14 @@ function help() {
|
|
|
34
41
|
npx @angar2/taskery update 최신 버전 fetch + 머지 갱신
|
|
35
42
|
npx @angar2/taskery status 멀티세션 현황 (진행중 태스크 / 워크트리 / 머지 락)
|
|
36
43
|
npx @angar2/taskery prune stale 워크트리 / 브랜치 대화형 정리
|
|
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 등록)
|
|
37
52
|
npx @angar2/taskery help 도움말
|
|
38
53
|
|
|
39
54
|
새 프로젝트 시작:
|
|
@@ -74,6 +89,30 @@ switch (sub) {
|
|
|
74
89
|
}
|
|
75
90
|
runScript('prune.js');
|
|
76
91
|
break;
|
|
92
|
+
case 'fork':
|
|
93
|
+
runScript('fork.js');
|
|
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;
|
|
77
116
|
case 'help':
|
|
78
117
|
case '--help':
|
|
79
118
|
case '-h':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angar2/taskery",
|
|
3
|
-
"version": "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
|
-
"
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
|
+
"proper-lockfile": "^4.1.2",
|
|
48
|
+
"zod": "^4.4.3"
|
|
47
49
|
}
|
|
48
50
|
}
|