@angar2/taskery 0.1.1 → 0.1.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 +44 -7
- package/bin/init.js +45 -2
- package/bin/lib.js +396 -2
- package/bin/prune.js +189 -0
- package/bin/status.js +148 -0
- package/bin/taskery.js +25 -0
- package/bin/update.js +26 -2
- package/package.json +7 -3
- package/template/.claude/hooks/closed-immutable.sh +5 -4
- package/template/.claude/hooks/git-guard.sh +1 -1
- package/template/.claude/settings.json +1 -2
- package/template/.claude/skills/add-backlog/SKILL.md +117 -0
- package/template/.claude/skills/plan-init/SKILL.md +27 -4
- package/template/.claude/skills/project-init/SKILL.md +32 -5
- package/template/.claude/skills/task-close/SKILL.md +269 -112
- package/template/.claude/skills/task-dev/SKILL.md +53 -8
- package/template/.claude/skills/task-init/SKILL.md +190 -50
- package/template/.claude/skills/task-plan/SKILL.md +114 -18
- package/template/.claude/skills/task-test/SKILL.md +59 -18
- package/template/.project/rules/CHANGELOG_RULE.md +91 -0
- package/template/.project/rules/GIT_RULE.md +119 -8
- package/template/.project/rules/MOCKUP_RULE.md +85 -0
- package/template/.project/rules/TASK_DOC_RULE.md +78 -76
- package/template/CLAUDE.md +72 -7
- package/template/.claude/hooks/pre-commit-verify.sh +0 -97
package/bin/prune.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/prune.js
|
|
4
|
+
* `npx @angar2/taskery prune` — stale 워크트리 / 브랜치 대화형 정리.
|
|
5
|
+
*
|
|
6
|
+
* 흐름:
|
|
7
|
+
* 1. git worktree prune (자동, 케이스 A 폴더 없는 등록 정리)
|
|
8
|
+
* 2. 의심 항목 목록 표시 (케이스 B/C/D + orphan)
|
|
9
|
+
* 3. 사용자 항목별 보존 / 삭제 선택
|
|
10
|
+
* 4. 선택된 항목 삭제:
|
|
11
|
+
* - 브랜치: git branch -D <name> ← 사용자 명시 선택이므로 -D 허용 (rebase 잔여 등 -d 거부 가능)
|
|
12
|
+
* - 워크트리: git worktree remove --force <path>
|
|
13
|
+
* 5. 결과 보고
|
|
14
|
+
*
|
|
15
|
+
* 본 명령은 *변경 발생 가능* — 사용자 confirm 항목별로 받음.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const readline = require('readline');
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
MANIFEST_NAME,
|
|
24
|
+
readManifest,
|
|
25
|
+
gitCapture,
|
|
26
|
+
getMainWorktreePath,
|
|
27
|
+
getActiveTasks,
|
|
28
|
+
getWorktreesRoot,
|
|
29
|
+
DEFAULT_STALE_DAYS,
|
|
30
|
+
} = require('./lib');
|
|
31
|
+
const { execFileSync } = require('child_process');
|
|
32
|
+
|
|
33
|
+
function ask(rl, q) {
|
|
34
|
+
return new Promise((resolve) => rl.question(q, (a) => resolve(a.trim().toLowerCase())));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function daysAgo(date) {
|
|
38
|
+
return Math.floor((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const mainWt = getMainWorktreePath();
|
|
43
|
+
const manifest = readManifest(path.join(mainWt, MANIFEST_NAME));
|
|
44
|
+
if (!manifest) {
|
|
45
|
+
console.error(`${MANIFEST_NAME} 없음 — 'init' 먼저.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const projectId = manifest.projectId;
|
|
49
|
+
const staleDays = manifest.stale_days ?? DEFAULT_STALE_DAYS;
|
|
50
|
+
|
|
51
|
+
console.log(`\ntaskery prune\n`);
|
|
52
|
+
console.log(`메인 워크트리: ${mainWt}`);
|
|
53
|
+
console.log(`stale_days: ${staleDays}\n`);
|
|
54
|
+
|
|
55
|
+
// 1. git worktree prune (자동 — 케이스 A)
|
|
56
|
+
console.log(`Step 1 — git worktree prune (폴더 없는 워크트리 등록 정리)`);
|
|
57
|
+
try {
|
|
58
|
+
execFileSync('git', ['-C', mainWt, 'worktree', 'prune', '-v'], { stdio: 'inherit' });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(` git worktree prune 실패: ${e.message}`);
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
// 2. 의심 항목 수집
|
|
65
|
+
const active = getActiveTasks(mainWt);
|
|
66
|
+
const worktreesRoot = getWorktreesRoot(projectId);
|
|
67
|
+
|
|
68
|
+
// 케이스 B/C/D
|
|
69
|
+
const suspects = [];
|
|
70
|
+
for (const t of active) {
|
|
71
|
+
const wtPath = path.join(
|
|
72
|
+
worktreesRoot,
|
|
73
|
+
`TASK-${String(t.taskNum).padStart(3, '0')}_${t.src}_${t.slug}`,
|
|
74
|
+
);
|
|
75
|
+
const wtExists = fs.existsSync(wtPath);
|
|
76
|
+
const lastCommit = gitCapture(mainWt, ['log', '-1', '--format=%ci', t.branch], {
|
|
77
|
+
allowFail: true,
|
|
78
|
+
});
|
|
79
|
+
const lastDate = lastCommit ? new Date(lastCommit) : null;
|
|
80
|
+
const days = lastDate ? daysAgo(lastDate) : null;
|
|
81
|
+
|
|
82
|
+
let kase = null;
|
|
83
|
+
if (!wtExists) kase = 'A (폴더 없음 — git worktree prune 처리됐을 가능성)';
|
|
84
|
+
else if (days !== null && days >= staleDays) kase = `C (워크트리 폴더 존재 + ${days}일 비활성)`;
|
|
85
|
+
|
|
86
|
+
if (wtExists) {
|
|
87
|
+
const rebaseApply = path.join(wtPath, '.git', 'rebase-apply');
|
|
88
|
+
const rebaseMerge = path.join(wtPath, '.git', 'rebase-merge');
|
|
89
|
+
if (fs.existsSync(rebaseApply) || fs.existsSync(rebaseMerge)) {
|
|
90
|
+
kase = `D (rebase 잔여 상태)`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (kase) {
|
|
95
|
+
suspects.push({ branch: t.branch, wtPath, wtExists, days, kase });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// orphan 워크트리 (SSoT에 없는 폴더)
|
|
100
|
+
const orphans = [];
|
|
101
|
+
if (fs.existsSync(worktreesRoot)) {
|
|
102
|
+
const folders = fs.readdirSync(worktreesRoot).filter((n) => n.startsWith('TASK-'));
|
|
103
|
+
const activeFolders = new Set(
|
|
104
|
+
active.map(
|
|
105
|
+
(t) => `TASK-${String(t.taskNum).padStart(3, '0')}_${t.src}_${t.slug}`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
for (const f of folders) {
|
|
109
|
+
if (!activeFolders.has(f)) {
|
|
110
|
+
orphans.push(path.join(worktreesRoot, f));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (suspects.length === 0 && orphans.length === 0) {
|
|
116
|
+
console.log(`✅ 정리할 의심 항목 없음.`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Step 2 — 의심 항목 (${suspects.length + orphans.length}건)\n`);
|
|
121
|
+
|
|
122
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
+
const toDelete = { branches: [], worktrees: [] };
|
|
124
|
+
|
|
125
|
+
// suspects 처리
|
|
126
|
+
for (const s of suspects) {
|
|
127
|
+
console.log(`[${s.kase}] ${s.branch}`);
|
|
128
|
+
console.log(` 워크트리: ${s.wtPath}${s.wtExists ? '' : ' (폴더 X)'}`);
|
|
129
|
+
if (s.days !== null) console.log(` 마지막 커밋: ${s.days}일 전`);
|
|
130
|
+
const ans = await ask(rl, ` 삭제? (y=브랜치+워크트리 / w=워크트리만 / k=보존, 기본 k) `);
|
|
131
|
+
if (ans === 'y') {
|
|
132
|
+
toDelete.branches.push(s.branch);
|
|
133
|
+
if (s.wtExists) toDelete.worktrees.push(s.wtPath);
|
|
134
|
+
} else if (ans === 'w' && s.wtExists) {
|
|
135
|
+
toDelete.worktrees.push(s.wtPath);
|
|
136
|
+
} else {
|
|
137
|
+
console.log(` 보존`);
|
|
138
|
+
}
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// orphan 처리
|
|
143
|
+
for (const o of orphans) {
|
|
144
|
+
console.log(`[orphan] 워크트리 폴더 (SSoT에 없는 브랜치)`);
|
|
145
|
+
console.log(` ${o}`);
|
|
146
|
+
const ans = await ask(rl, ` 워크트리 폴더 삭제? (y/N) `);
|
|
147
|
+
if (ans === 'y') toDelete.worktrees.push(o);
|
|
148
|
+
else console.log(` 보존`);
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
rl.close();
|
|
153
|
+
|
|
154
|
+
// 3. 실제 삭제
|
|
155
|
+
if (toDelete.branches.length === 0 && toDelete.worktrees.length === 0) {
|
|
156
|
+
console.log(`삭제 대상 없음.`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`\nStep 3 — 삭제 실행\n`);
|
|
161
|
+
|
|
162
|
+
for (const wt of toDelete.worktrees) {
|
|
163
|
+
try {
|
|
164
|
+
execFileSync('git', ['-C', mainWt, 'worktree', 'remove', '--force', wt], {
|
|
165
|
+
stdio: 'inherit',
|
|
166
|
+
});
|
|
167
|
+
console.log(` worktree removed: ${wt}`);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.error(` worktree remove 실패 (${wt}): ${e.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const b of toDelete.branches) {
|
|
174
|
+
try {
|
|
175
|
+
// -D 강제 삭제 — 사용자 명시 선택 (prune 대화형) 이므로 허용
|
|
176
|
+
execFileSync('git', ['-C', mainWt, 'branch', '-D', b], { stdio: 'inherit' });
|
|
177
|
+
console.log(` branch deleted: ${b}`);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error(` branch -D 실패 (${b}): ${e.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`\n✅ taskery prune 완료 — 워크트리 ${toDelete.worktrees.length}건 / 브랜치 ${toDelete.branches.length}건 삭제.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((e) => {
|
|
187
|
+
console.error(`taskery prune 실패: ${e.message}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
package/bin/status.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/status.js
|
|
4
|
+
* `npx @angar2/taskery status` — 멀티세션 현황 요약.
|
|
5
|
+
*
|
|
6
|
+
* 출력:
|
|
7
|
+
* - 메인 워크트리 + 현재 브랜치
|
|
8
|
+
* - projectId / stale_days / lock_timeout_ms
|
|
9
|
+
* - 진행중 태스크 (SSoT) + 각 워크트리 폴더 존재 여부 + 마지막 커밋 시각
|
|
10
|
+
* - stale 의심 항목 (케이스 A/B/C/D)
|
|
11
|
+
* - 머지 락 상태 (잡혀 있으면 누가 잡고 있는지)
|
|
12
|
+
*
|
|
13
|
+
* 본 명령은 *변경 X — 읽기 전용*. 정리는 `taskery prune`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
MANIFEST_NAME,
|
|
22
|
+
readManifest,
|
|
23
|
+
gitCapture,
|
|
24
|
+
getMainWorktreePath,
|
|
25
|
+
getActiveTasks,
|
|
26
|
+
getWorktreesRoot,
|
|
27
|
+
getMergeLockPath,
|
|
28
|
+
DEFAULT_STALE_DAYS,
|
|
29
|
+
DEFAULT_LOCK_TIMEOUT_MS,
|
|
30
|
+
} = require('./lib');
|
|
31
|
+
|
|
32
|
+
function daysAgo(date) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const ms = now - date.getTime();
|
|
35
|
+
return Math.floor(ms / (1000 * 60 * 60 * 24));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function main() {
|
|
39
|
+
// 1. 메인 워크트리 + manifest
|
|
40
|
+
let mainWt;
|
|
41
|
+
try {
|
|
42
|
+
mainWt = getMainWorktreePath();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`taskery status 실패: ${e.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const manifest = readManifest(path.join(mainWt, MANIFEST_NAME));
|
|
49
|
+
if (!manifest) {
|
|
50
|
+
console.error(`${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const projectId = manifest.projectId;
|
|
55
|
+
const staleDays = manifest.stale_days ?? DEFAULT_STALE_DAYS;
|
|
56
|
+
const lockTimeoutMs = manifest.lock_timeout_ms ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
57
|
+
|
|
58
|
+
const currentBranch = gitCapture(mainWt, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
59
|
+
const isOnDev = currentBranch === 'dev';
|
|
60
|
+
|
|
61
|
+
console.log(`\ntaskery status\n`);
|
|
62
|
+
console.log(`메인 워크트리: ${mainWt}`);
|
|
63
|
+
console.log(`현재 브랜치: ${currentBranch}${isOnDev ? '' : ' ⚠ (메인 워크트리 = dev 정책 위배)'}`);
|
|
64
|
+
console.log(`projectId: ${projectId}`);
|
|
65
|
+
console.log(`stale_days: ${staleDays} / lock_timeout_ms: ${lockTimeoutMs}\n`);
|
|
66
|
+
|
|
67
|
+
// 2. 머지 락 상태
|
|
68
|
+
const lockPath = getMergeLockPath(projectId);
|
|
69
|
+
const lockDir = lockPath + '.lock'; // proper-lockfile은 <file>.lock 디렉토리 생성
|
|
70
|
+
if (fs.existsSync(lockDir)) {
|
|
71
|
+
let lockInfo = '';
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.statSync(lockDir);
|
|
74
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
75
|
+
lockInfo = ` (잡힘 ${Math.round(ageMs / 1000)}초 전)`;
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
console.log(`머지 락: 활성${lockInfo}`);
|
|
78
|
+
} else {
|
|
79
|
+
console.log(`머지 락: 비활성`);
|
|
80
|
+
}
|
|
81
|
+
console.log();
|
|
82
|
+
|
|
83
|
+
// 3. 진행중 태스크 (SSoT)
|
|
84
|
+
const active = getActiveTasks(mainWt);
|
|
85
|
+
console.log(`진행중 태스크 (SSoT, ${active.length}건):`);
|
|
86
|
+
|
|
87
|
+
if (active.length === 0) {
|
|
88
|
+
console.log(` (없음)`);
|
|
89
|
+
} else {
|
|
90
|
+
const worktreesRoot = getWorktreesRoot(projectId);
|
|
91
|
+
for (const t of active) {
|
|
92
|
+
const wtPath = path.join(worktreesRoot, `TASK-${String(t.taskNum).padStart(3, '0')}_${t.src}_${t.slug}`);
|
|
93
|
+
const wtExists = fs.existsSync(wtPath);
|
|
94
|
+
const lastCommit = gitCapture(mainWt, [
|
|
95
|
+
'log', '-1', '--format=%ci', t.branch,
|
|
96
|
+
], { allowFail: true });
|
|
97
|
+
const lastDate = lastCommit ? new Date(lastCommit) : null;
|
|
98
|
+
const daysOld = lastDate ? daysAgo(lastDate) : null;
|
|
99
|
+
|
|
100
|
+
console.log(` ${t.branch}`);
|
|
101
|
+
console.log(` 워크트리: ${wtPath}${wtExists ? '' : ' ⚠ 폴더 없음 (케이스 A — prune 필요)'}`);
|
|
102
|
+
if (lastDate) {
|
|
103
|
+
console.log(` 마지막 커밋: ${lastCommit} (${daysOld}일 전)${daysOld >= staleDays ? ' ⚠ stale 의심 (케이스 B/C)' : ''}`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(` 마지막 커밋: (조회 실패)`);
|
|
106
|
+
}
|
|
107
|
+
if (wtExists) {
|
|
108
|
+
const rebaseApply = path.join(wtPath, '.git', 'rebase-apply');
|
|
109
|
+
const rebaseMerge = path.join(wtPath, '.git', 'rebase-merge');
|
|
110
|
+
if (fs.existsSync(rebaseApply) || fs.existsSync(rebaseMerge)) {
|
|
111
|
+
console.log(` ⚠ rebase 잔여 상태 (케이스 D) — taskery prune 또는 워크트리에서 git rebase --abort`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
// 4. 워크트리 폴더 vs SSoT mismatch (orphan)
|
|
119
|
+
const worktreesRoot = getWorktreesRoot(projectId);
|
|
120
|
+
if (fs.existsSync(worktreesRoot)) {
|
|
121
|
+
const folders = fs.readdirSync(worktreesRoot).filter((n) => n.startsWith('TASK-'));
|
|
122
|
+
const activeFolders = new Set(
|
|
123
|
+
active.map(
|
|
124
|
+
(t) => `TASK-${String(t.taskNum).padStart(3, '0')}_${t.src}_${t.slug}`,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
const orphans = folders.filter((f) => !activeFolders.has(f));
|
|
128
|
+
if (orphans.length > 0) {
|
|
129
|
+
console.log(`⚠ SSoT에 없는 워크트리 폴더 ${orphans.length}건 (이미 dev 머지된 잔존?):`);
|
|
130
|
+
for (const o of orphans) {
|
|
131
|
+
console.log(` ${path.join(worktreesRoot, o)}`);
|
|
132
|
+
}
|
|
133
|
+
console.log(` → 'taskery prune' 으로 정리`);
|
|
134
|
+
console.log();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`기타 명령:`);
|
|
139
|
+
console.log(` npx @angar2/taskery prune stale 워크트리 / 브랜치 대화형 정리`);
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
main();
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error(`taskery status 실패: ${e.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
package/bin/taskery.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* 서브커맨드:
|
|
7
7
|
* init — 현재 디렉토리에 taskery 자산 카피 + manifest 생성
|
|
8
8
|
* update — 최신 버전 fetch + manifest 비교 + 머지 갱신
|
|
9
|
+
* status — 멀티세션 현황 요약 (진행중 태스크 / 워크트리 / 머지 락, 0.1.2+)
|
|
10
|
+
* prune — stale 워크트리 / 브랜치 대화형 정리 (0.1.2+)
|
|
9
11
|
* help — 사용법 출력
|
|
10
12
|
*/
|
|
11
13
|
|
|
@@ -28,6 +30,8 @@ function help() {
|
|
|
28
30
|
사용법:
|
|
29
31
|
npx @angar2/taskery init 현재 디렉토리에 taskery 자산 설치
|
|
30
32
|
npx @angar2/taskery update 최신 버전 fetch + 머지 갱신
|
|
33
|
+
npx @angar2/taskery status 멀티세션 현황 (진행중 태스크 / 워크트리 / 머지 락)
|
|
34
|
+
npx @angar2/taskery prune stale 워크트리 / 브랜치 대화형 정리
|
|
31
35
|
npx @angar2/taskery help 도움말
|
|
32
36
|
|
|
33
37
|
새 프로젝트 시작:
|
|
@@ -44,6 +48,27 @@ switch (sub) {
|
|
|
44
48
|
case 'update':
|
|
45
49
|
runScript('update.js');
|
|
46
50
|
break;
|
|
51
|
+
case 'status':
|
|
52
|
+
runScript('status.js');
|
|
53
|
+
break;
|
|
54
|
+
case 'prune':
|
|
55
|
+
if (process.argv[3] === '--help' || process.argv[3] === '-h') {
|
|
56
|
+
console.log(`taskery prune — stale 워크트리 / 브랜치 대화형 정리
|
|
57
|
+
|
|
58
|
+
사용법:
|
|
59
|
+
npx @angar2/taskery prune
|
|
60
|
+
|
|
61
|
+
설명:
|
|
62
|
+
.taskery-manifest.json의 stale_days (기본 30일) 기준으로 비활성 워크트리 + 브랜치를 대화형 정리합니다.
|
|
63
|
+
케이스 분기: A (워크트리 + 브랜치 모두 stale) / B (브랜치만 stale) / C (워크트리 폴더 잔존) / D (정합 영역).
|
|
64
|
+
각 케이스마다 y/n/k 선택 가능 (y = 삭제 / n = 건너뜀 / k = 보존 명시).
|
|
65
|
+
|
|
66
|
+
상세: https://github.com/angar2/taskery
|
|
67
|
+
`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
runScript('prune.js');
|
|
71
|
+
break;
|
|
47
72
|
case 'help':
|
|
48
73
|
case '--help':
|
|
49
74
|
case '-h':
|
package/bin/update.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* bin/update.js
|
|
4
4
|
* `npx @angar2/taskery update` — template/ 최신 자산과 사용자 측 비교 + 머지 갱신.
|
|
5
5
|
*
|
|
6
|
-
* 머지 로직 (§
|
|
6
|
+
* 머지 로직 (DISTRIBUTION.md §6):
|
|
7
7
|
* 1. .taskery-manifest.json (cwd) 읽기 — 없으면 에러 ('init' 먼저)
|
|
8
8
|
* 2. template/ 정독 → 새 해시 맵
|
|
9
9
|
* 3. 각 파일 분기:
|
|
@@ -31,6 +31,9 @@ const {
|
|
|
31
31
|
readManifest,
|
|
32
32
|
getPackageVersion,
|
|
33
33
|
isLocalOverride,
|
|
34
|
+
generateProjectId,
|
|
35
|
+
DEFAULT_STALE_DAYS,
|
|
36
|
+
DEFAULT_LOCK_TIMEOUT_MS,
|
|
34
37
|
} = require('./lib');
|
|
35
38
|
|
|
36
39
|
async function confirm(msg) {
|
|
@@ -146,15 +149,36 @@ async function main() {
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
// 5. manifest 갱신
|
|
152
|
+
// 5. manifest 갱신 — 멀티세션(0.1.2+) 필드 누락 시 자동 마이그레이션
|
|
153
|
+
const migration = { projectId: false, stale_days: false, lock_timeout_ms: false };
|
|
154
|
+
const projectId = oldManifest.projectId || (migration.projectId = true, generateProjectId());
|
|
155
|
+
const stale_days =
|
|
156
|
+
typeof oldManifest.stale_days === 'number'
|
|
157
|
+
? oldManifest.stale_days
|
|
158
|
+
: (migration.stale_days = true, DEFAULT_STALE_DAYS);
|
|
159
|
+
const lock_timeout_ms =
|
|
160
|
+
typeof oldManifest.lock_timeout_ms === 'number'
|
|
161
|
+
? oldManifest.lock_timeout_ms
|
|
162
|
+
: (migration.lock_timeout_ms = true, DEFAULT_LOCK_TIMEOUT_MS);
|
|
163
|
+
|
|
150
164
|
const newManifest = {
|
|
151
165
|
version: getPackageVersion(),
|
|
152
166
|
installed_at: oldManifest.installed_at,
|
|
153
167
|
updated_at: new Date().toISOString(),
|
|
168
|
+
projectId,
|
|
169
|
+
stale_days,
|
|
170
|
+
lock_timeout_ms,
|
|
154
171
|
files: newFiles,
|
|
155
172
|
};
|
|
156
173
|
writeManifest(newManifest, manifestPath);
|
|
157
174
|
|
|
175
|
+
const migratedFields = Object.entries(migration)
|
|
176
|
+
.filter(([, added]) => added)
|
|
177
|
+
.map(([k]) => k);
|
|
178
|
+
if (migratedFields.length > 0) {
|
|
179
|
+
console.log(` manifest 마이그레이션: ${migratedFields.join(', ')} 필드 추가`);
|
|
180
|
+
}
|
|
181
|
+
|
|
158
182
|
// 결과 보고
|
|
159
183
|
console.log(`\n✅ taskery update 완료`);
|
|
160
184
|
console.log(` 동일: ${summary.unchanged}, 신규: ${summary.new}, 자동 갱신: ${summary.autoUpdated}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angar2/taskery",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Lightweight task system for Claude Code — 1 메인 세션 + 스킬
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Lightweight task system for Claude Code — 1 메인 세션(또는 멀티세션) + 스킬 9종 + catastrophic hook 안전망 2종",
|
|
5
5
|
"bin": {
|
|
6
6
|
"taskery": "bin/taskery.js",
|
|
7
7
|
"create-taskery": "bin/create.js"
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
|
-
"node": ">=18.0.0"
|
|
41
|
+
"node": ">=18.0.0",
|
|
42
|
+
"git": ">=2.31.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"proper-lockfile": "^4.1.2"
|
|
42
46
|
}
|
|
43
47
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
# 차단 안 함 (의도적):
|
|
11
11
|
# - .project/tasks/<vX.X>/spec-diffs/*.md — 역사적 자료, 자유 수정
|
|
12
12
|
# - .project/tasks/<vX.X>/screenshots/* — 자유
|
|
13
|
-
# -
|
|
13
|
+
# - .project/tasks/<vX.X>/mockup/*.html — UX/UI 목업, 자유 수정 (vX.X 공통)
|
|
14
|
+
# - 폴더 승격 task 폴더 안 추가 자료 (서브 문서 등) — 자유
|
|
14
15
|
# - 코드 영역 — closed task 코드 재수정은 *새 task로 처리*가 정상 흐름
|
|
15
16
|
#
|
|
16
17
|
# 우회 (의도적으로 closed task를 풀어 다시 진행해야 하는 경우):
|
|
@@ -32,9 +33,9 @@ except Exception:
|
|
|
32
33
|
" 2>/dev/null)
|
|
33
34
|
|
|
34
35
|
# task.md 본 파일 매칭 (단일 + 폴더 승격)
|
|
35
|
-
# - 단일: .project/tasks/v*/<NNN>_<slug>.md (NNN_<slug> 패턴 — vX.X 직속, spec-diffs/screenshots 하위 X)
|
|
36
|
+
# - 단일: .project/tasks/v*/<NNN>_<slug>.md (NNN_<slug> 패턴 — vX.X 직속, spec-diffs/screenshots/mockup 하위 X)
|
|
36
37
|
# - 폴더: .project/tasks/v*/TASK-<NNN>_<slug>/task.md
|
|
37
|
-
# spec-diffs/ + screenshots/ 직속 파일은 매칭 안 됨 (보호 대상 X)
|
|
38
|
+
# spec-diffs/ + screenshots/ + mockup/ 직속 파일은 매칭 안 됨 (보호 대상 X)
|
|
38
39
|
if ! echo "$FILE_PATH" | grep -qE '\.project/tasks/v[^/]+/([0-9]+_[^/]+\.md|TASK-[^/]+/task\.md)$'; then
|
|
39
40
|
exit 0
|
|
40
41
|
fi
|
|
@@ -67,7 +68,7 @@ print(m.group(1) if m else '')
|
|
|
67
68
|
if [ "$STATUS" = "closed" ]; then
|
|
68
69
|
echo "closed-immutable: '$FILE_PATH' 차단. task '$TASK_MD' 상태가 'closed' — 재수정 금지." >&2
|
|
69
70
|
echo " 의도적 재진입 필요 시: sed로 헤더 status를 'developing' 또는 다른 상태로 직접 변경 후 Edit 가능." >&2
|
|
70
|
-
echo " *closed task 코드 / spec-diffs / screenshots
|
|
71
|
+
echo " *closed task 코드 / spec-diffs / screenshots / mockup*은 보호 영역 외 — 자유 수정. 단 코드 변경은 *새 task로 처리*가 정상 흐름." >&2
|
|
71
72
|
exit 2
|
|
72
73
|
fi
|
|
73
74
|
|
|
@@ -43,7 +43,7 @@ fi
|
|
|
43
43
|
|
|
44
44
|
# 2. git commit --no-verify 차단
|
|
45
45
|
if echo "$CMD" | grep -qE 'git\s+commit\b.*--no-verify'; then
|
|
46
|
-
echo "git-guard: 'git commit --no-verify' 차단.
|
|
46
|
+
echo "git-guard: 'git commit --no-verify' 차단. commit hook 우회 금지. (.project/rules/GIT_RULE.md §금지 사항)" >&2
|
|
47
47
|
exit 2
|
|
48
48
|
fi
|
|
49
49
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-backlog
|
|
3
|
+
description: 사용자 발화로 *버전별* tasks/<vX.X>/BACKLOG.md에 task 후보 1건 추가 — 얕은 분석(개요/대상 영역) + BL-NNN 채번 + withMetaLock 직렬화 (0.1.2+)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /add-backlog
|
|
7
|
+
|
|
8
|
+
## 개요
|
|
9
|
+
|
|
10
|
+
`/add-backlog`는 사용자 발화를 받아 *버전별 백로그* `.project/tasks/<vX.X>/BACKLOG.md`에 task 후보를 1건씩 누적한다. 멀티세션 환경에서 `withMetaLock`으로 동시 쓰기 직렬화. **얕은 분석**만 — 코드 직접 탐색 X, 추정 수준. 본격 분석은 다음 단계 `/task-plan`에서.
|
|
11
|
+
|
|
12
|
+
글로벌 `.project/BACKLOG.md` (plan 기획 후보 카탈로그) 는 `/plan-init` 영역으로 본 스킬 무관.
|
|
13
|
+
|
|
14
|
+
## 호출 시점
|
|
15
|
+
|
|
16
|
+
- 사용자 명시 호출: `/add-backlog 로그인 빈 화면 떠`
|
|
17
|
+
- 발화 캐치: *"~ 백로그에 추가해 줘"* / *"백로그에 ~"* 등 백로그 추가 의도 발화 description 매칭으로 메인이 자동 발동
|
|
18
|
+
|
|
19
|
+
## 입력 처리
|
|
20
|
+
|
|
21
|
+
- 무인자 호출: 사용자가 *"백로그 추가"*만 말한 경우 — 어떤 항목인지 한 줄 인터뷰
|
|
22
|
+
- 인자 호출: 인자에 백로그 후보 주제. 1건 기본. 다건 명시 시 순차 처리
|
|
23
|
+
|
|
24
|
+
## 단계 (Step 1~6)
|
|
25
|
+
|
|
26
|
+
### Step 1 — 사전 검증
|
|
27
|
+
|
|
28
|
+
- 메인 워크트리 검출:
|
|
29
|
+
```sh
|
|
30
|
+
MAIN_WT=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")
|
|
31
|
+
```
|
|
32
|
+
- 메인 워크트리 dev 체크아웃 검증 — 위배 시 사용자 호출 + 중단
|
|
33
|
+
- 활성 버전 검출 — `$MAIN_WT/.project/AGENT-GUIDE.md` `## 활성 plan 버전` 섹션 다음 비어 있지 않은 첫 줄에서 `vX.X` 패턴 추출
|
|
34
|
+
- BACKLOG.md 경로 = `$MAIN_WT/.project/tasks/<vX.X>/BACKLOG.md`. 부재 시 사용자 호출: *"`/plan-init` 먼저 호출해 활성 버전 디렉토리 + 빈 BACKLOG.md 생성 필요"* + 중단
|
|
35
|
+
|
|
36
|
+
### Step 2 — 백로그 항목 후보 추출
|
|
37
|
+
|
|
38
|
+
- 사용자 발화에서 1건 추출. 다건 명시 시 순차 처리 (각 1건씩 본 흐름 반복)
|
|
39
|
+
- 입력이 비어 있으면 사용자에게 *"어떤 항목? 한 줄로 알려줘"* 인터뷰
|
|
40
|
+
|
|
41
|
+
### Step 3 — 얕은 분석 (LLM, 코드 탐색 X)
|
|
42
|
+
|
|
43
|
+
추정 수준의 메타 산출:
|
|
44
|
+
- **유형** (`feature` / `bug` / `improve` / `refactor` / `docs` / `chore`) — 자동 추정, 모호 시 사용자 confirm
|
|
45
|
+
- **제목** — 한 줄, 간결, 한국어
|
|
46
|
+
- **개요** — 한 줄. bug → 원인 추정 / feature → 구상 / improve → 개선 방향
|
|
47
|
+
- **대상 영역** — 한 줄. 이 파일 / 모듈일 거다 — 추정 수준
|
|
48
|
+
|
|
49
|
+
> *코드 직접 탐색 금지*. 본격 분석은 다음 단계 `/task-plan`에서. 본 스킬은 *메모지 누적*만.
|
|
50
|
+
|
|
51
|
+
### Step 4 — 결정적 슬러그 산출
|
|
52
|
+
|
|
53
|
+
- 한국어 제목 → 영어 *의미 변환* → kebab-case (3 단어 이내, 음역 X)
|
|
54
|
+
- 예: *"로그인 페이지 빈 화면"* → `login-empty-page` / *"다크모드 토글"* → `dark-mode-toggle`
|
|
55
|
+
|
|
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 — 결과 보고
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
✅ BL-<NNN> [<유형>] <제목> — <vX.X> 백로그 추가됐어.
|
|
86
|
+
- 개요: <개요>
|
|
87
|
+
- 대상 영역: <대상 영역>
|
|
88
|
+
- 슬러그: <slug>
|
|
89
|
+
다음: 진행 의향이면 *"BL-<NNN> 진행"*이라고 말해. `/task-init`이 메타 그대로 받아 워크트리를 분기하고 항목을 `[x]` 확인 마킹할게.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 도구 가이드
|
|
93
|
+
|
|
94
|
+
- **Bash**: 메인 워크트리 검출 / dev 체크아웃 검증
|
|
95
|
+
- **Read**: `$MAIN_WT/.project/AGENT-GUIDE.md` (활성 버전 검출) / 기존 BACKLOG.md 정독 (중복 / 유사 항목 사전 확인)
|
|
96
|
+
- **bin/lib.js**: `getActiveVersion` / `getBacklogPath` / `appendBacklogItem` 호출
|
|
97
|
+
- **AskUserQuestion**: 유형 모호 시 confirm — 한 번에 한 질문
|
|
98
|
+
|
|
99
|
+
## 주의사항
|
|
100
|
+
|
|
101
|
+
- **얕은 분석만** — 본 스킬에서 도메인 코드 Read · Grep 금지. 본격 분석은 `/task-plan`
|
|
102
|
+
- **유형 자동 추정 + 모호 시 confirm** — *"메뉴 검색 개선"* 같이 feature/improve 모호 시 사용자 결정 받기
|
|
103
|
+
- **메인 워크트리 dev 검증 누락 X** — 위배 시 즉시 중단. 워크트리 안에서 호출돼도 메인 절대 경로의 BACKLOG.md에 쓰기
|
|
104
|
+
- **활성 버전 부재 시 즉시 중단** — `AGENT-GUIDE.md` 부재 / `## 활성 plan 버전` 섹션 부재 / vX.X 패턴 추출 실패 모두 명확한 에러 메시지 후 사용자 호출
|
|
105
|
+
- **결정적 슬러그 best-effort** — LLM 비결정성 수용. race 차단 본체는 SSoT 2층(`/task-init` §4.4)이 담당. 슬러그 변형 발생 시 사용자 confirm
|
|
106
|
+
- **글로벌 BACKLOG.md (plan 기획 후보)** 와 혼동 X — 본 스킬은 *버전별*만 다룸. 글로벌은 `/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`
|