@angar2/taskery 0.1.1 → 0.1.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 +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 +39 -5
- 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 +280 -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 +141 -16
- package/template/.claude/skills/task-test/SKILL.md +60 -18
- package/template/.project/rules/CHANGELOG_RULE.md +91 -0
- package/template/.project/rules/GIT_RULE.md +126 -9
- package/template/.project/rules/MOCKUP_RULE.md +86 -0
- package/template/.project/rules/TASK_DOC_RULE.md +101 -75
- package/template/CLAUDE.md +73 -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.3",
|
|
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
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# PreToolUse(Bash) 훅 — catastrophic git 명령 차단
|
|
4
4
|
#
|
|
5
5
|
# 잡는 것:
|
|
6
|
-
# 1. main / dev 브랜치 직접 커밋
|
|
6
|
+
# 1. main / dev 브랜치 직접 커밋 (5종 변형 인식: -C / --git-dir / --work-tree)
|
|
7
7
|
# 2. git push --force / -f
|
|
8
8
|
# 3. git commit --no-verify
|
|
9
9
|
# 4. git branch -D (강제 삭제)
|
|
@@ -31,19 +31,53 @@ if ! echo "$CMD" | grep -qE '(^|;|&&|\|\|)\s*git\s'; then
|
|
|
31
31
|
exit 0
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# 헬퍼: git 명령에서 대상 워크트리 경로 추출 (5종 변형)
|
|
35
|
+
# 1. git -C <경로>
|
|
36
|
+
# 2. git --git-dir=<경로> (= 형태)
|
|
37
|
+
# 3. git --git-dir <경로> (공백 분리)
|
|
38
|
+
# 4. git --work-tree=<경로> (= 형태)
|
|
39
|
+
# 5. git --work-tree <경로> (공백 분리)
|
|
40
|
+
# 추출 실패 시 빈 문자열 (cwd 사용).
|
|
41
|
+
# --git-dir 변형은 `.git` 디렉토리를 가리키므로 dirname 처리해 워크트리 경로로 변환.
|
|
42
|
+
extract_target_path() {
|
|
43
|
+
local cmd="$1"
|
|
44
|
+
local path=""
|
|
45
|
+
|
|
46
|
+
if [[ "$cmd" =~ git[[:space:]]+-C[[:space:]]+([^[:space:]]+) ]]; then
|
|
47
|
+
path="${BASH_REMATCH[1]}"
|
|
48
|
+
elif [[ "$cmd" =~ --git-dir=([^[:space:]]+) ]]; then
|
|
49
|
+
path=$(dirname "${BASH_REMATCH[1]}")
|
|
50
|
+
elif [[ "$cmd" =~ --git-dir[[:space:]]+([^[:space:]]+) ]]; then
|
|
51
|
+
path=$(dirname "${BASH_REMATCH[1]}")
|
|
52
|
+
elif [[ "$cmd" =~ --work-tree=([^[:space:]]+) ]]; then
|
|
53
|
+
path="${BASH_REMATCH[1]}"
|
|
54
|
+
elif [[ "$cmd" =~ --work-tree[[:space:]]+([^[:space:]]+) ]]; then
|
|
55
|
+
path="${BASH_REMATCH[1]}"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "$path"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# 1. main / dev 직접 커밋 차단 (5종 변형 인식)
|
|
35
62
|
# 현재 브랜치 확인 + git commit 명령 감지 시 차단
|
|
36
|
-
if echo "$CMD" | grep -qE '(^|;|&&|\|\|)\s*git\s+commit\b'; then
|
|
37
|
-
|
|
63
|
+
if echo "$CMD" | grep -qE '(^|;|&&|\|\|)\s*git(\s+(-C\s+\S+|--git-dir[= ]\S+|--work-tree[= ]\S+))*\s+commit\b'; then
|
|
64
|
+
TARGET_PATH=$(extract_target_path "$CMD")
|
|
65
|
+
if [ -n "$TARGET_PATH" ]; then
|
|
66
|
+
CURRENT_BRANCH=$(git -C "$TARGET_PATH" branch --show-current 2>/dev/null)
|
|
67
|
+
else
|
|
68
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
|
|
69
|
+
fi
|
|
38
70
|
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "dev" ]; then
|
|
39
71
|
echo "git-guard: '$CURRENT_BRANCH' 브랜치 직접 커밋 차단. 작업 브랜치({타입}/{개발자}_TASK-NNN_slug)에서 커밋. (.project/rules/GIT_RULE.md §기본 원칙)" >&2
|
|
72
|
+
echo " 변형 인식: 'git -C <경로> ...' / 'git --git-dir=<경로> ...' / 'git --work-tree=<경로> ...' — 메인 cwd에서 워크트리 대상 커밋 가능." >&2
|
|
73
|
+
echo " 셸 prefix('cd <경로> && git ...' / '(cd ... && git ...)')는 hook이 정확히 인식 X — 'git -C <경로> ...' 형태로만 발행." >&2
|
|
40
74
|
exit 2
|
|
41
75
|
fi
|
|
42
76
|
fi
|
|
43
77
|
|
|
44
78
|
# 2. git commit --no-verify 차단
|
|
45
79
|
if echo "$CMD" | grep -qE 'git\s+commit\b.*--no-verify'; then
|
|
46
|
-
echo "git-guard: 'git commit --no-verify' 차단.
|
|
80
|
+
echo "git-guard: 'git commit --no-verify' 차단. commit hook 우회 금지. (.project/rules/GIT_RULE.md §금지 사항)" >&2
|
|
47
81
|
exit 2
|
|
48
82
|
fi
|
|
49
83
|
|