@angar2/taskery 0.6.0 → 0.7.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 +15 -7
- package/bin/fork.js +4 -2
- package/bin/init.js +7 -2
- package/bin/lib.js +147 -55
- package/bin/mcp.js +3 -3
- package/bin/migrate.js +306 -0
- package/bin/plan.js +61 -21
- package/bin/set-status.js +18 -3
- package/bin/status.js +12 -0
- package/bin/taskery.js +7 -2
- package/bin/update.js +72 -0
- package/package.json +1 -1
- package/template/.claude/skills/run-team/SKILL.md +4 -4
- package/template/.codex/agents/task-tester.toml +9 -6
- package/template/.project/rules/CHANGELOG_RULE.md +1 -1
- package/template/.project/rules/GIT_RULE.md +19 -5
- package/template/.project/rules/MOCKUP_RULE.md +1 -1
- package/template/.project/rules/TASKERY_RULE.md +237 -0
- package/template/.project/rules/TASK_DOC_RULE.md +3 -2
- package/template/AGENTS.md +26 -177
- package/template/CLAUDE.md +1 -206
- package/template/shared/skills/log-friction/SKILL.md +21 -1
- package/template/shared/skills/plan-init/SKILL.md +8 -8
- package/template/shared/skills/project-init/SKILL.md +79 -43
- package/template/shared/skills/task-close/SKILL.md +42 -8
- package/template/shared/skills/task-dev/SKILL.md +45 -13
- package/template/shared/skills/task-init/SKILL.md +13 -13
- package/template/shared/skills/task-plan/SKILL.md +24 -17
- package/template/shared/skills/task-test/SKILL.md +71 -20
- package/template/.project/FRICTION_LOG.md +0 -16
- package/template/.project/TEST-GUIDE.md +0 -51
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@ AI 코딩 에이전트의 자율 개발을 위한 Task 기반의 가드레일
|
|
|
16
16
|
| 에이전트가 catastrophic 사고를 일으킬 수 있음 | 필수 hook으로 차단 — 정상 흐름에는 무간섭 |
|
|
17
17
|
| 에이전트가 작성한 코드의 자가검증으로 인해 문제를 놓침 (confirmation bias) | 테스트 시 별도 격리 세션 호출(`/task-test`) — 메인 세션의 가정 없이 독립 검증 |
|
|
18
18
|
| 단일 메인 세션 운영으로 인해 task를 직렬로 진행해야 하는 병목 | 같은 프로젝트에서 여러 메인 세션이 독립 작업 폴더(worktree)로 병렬 진행 — 머지 시점에 직렬화로 정합성 유지 |
|
|
19
|
+
| 검수 중 한 줄만 고쳐도 전체 테스트를 다시 돌려 대기 시간이 길어짐 | 수정 구간에는 변경이 닿는 테스트만 실행 — 전체 실행은 task당 1회로 제한 |
|
|
20
|
+
| 테스트가 앱 화면을 띄우고 조작해 그동안 컴퓨터를 쓸 수 없음 | 화면·입력을 점유하는 테스트는 창을 띄우지 않는 실행 경로가 준비된 경우에만 자동 실행 — 없으면 사용자 승인 후 실행 |
|
|
21
|
+
| 프로젝트마다 개발·테스트 방식이 다른데 공통 규칙만 적용됨 | 프로젝트 전용 규칙 파일(`*.local.md`)을 각 스킬이 읽어 우선 적용 — 패키지를 갱신해도 덮어쓰지 않음 |
|
|
19
22
|
|
|
20
23
|
> catastrophic 사고 예시 — git 운영 정책 위반, 검증 우회, 완료된 task 문서 재수정 등
|
|
21
24
|
|
|
@@ -105,7 +108,8 @@ taskery prune
|
|
|
105
108
|
|
|
106
109
|
```
|
|
107
110
|
my-app/
|
|
108
|
-
├─
|
|
111
|
+
├─ AGENTS.md # AI 에이전트 진입 문서 (본문 단일 소스)
|
|
112
|
+
├─ CLAUDE.md # Claude Code용 — `@AGENTS.md` 임포트 한 줄
|
|
109
113
|
├─ .taskery-manifest.json # 패키지 업데이트 추적
|
|
110
114
|
├─ .gitignore
|
|
111
115
|
├─ .claude/
|
|
@@ -114,17 +118,19 @@ my-app/
|
|
|
114
118
|
│ └─ hooks/<hook-name>.sh # 2 hook
|
|
115
119
|
└─ .project/
|
|
116
120
|
├─ PROJECT.md # 프로젝트 개요
|
|
117
|
-
├─ AGENT-GUIDE.md # AI 에이전트 가이드
|
|
118
121
|
├─ LINKED-REPOS.md # 관계 리포지토리 정보
|
|
119
122
|
├─ GLOSSARY.md # 도메인 용어집 (영문/한글 표기 일관성)
|
|
120
123
|
├─ .env # 사용자 설정 환경변수 (관계 리포지토리 환경변수 등)
|
|
121
124
|
├─ SERVICE-POLICY.md / TECH-STACK.md / ARCHITECTURE.md # 제품 관통 문서 — 정적 (project-init 작성)
|
|
122
125
|
├─ DATA-MODEL.md / API-SPEC.md / FEATURES.md / UX-UI.md # 제품 관통 문서 — 성장 (골격 → plan·task가 채움)
|
|
123
126
|
├─ rules/
|
|
127
|
+
│ ├─ TASKERY_RULE.md # taskery 사용 설명서 (세션 필독)
|
|
124
128
|
│ ├─ TASK_DOC_RULE.md # task 문서 작성 규칙
|
|
125
129
|
│ ├─ GIT_RULE.md # 로컬 깃 운영 규칙
|
|
126
130
|
│ ├─ CHANGELOG_RULE.md # CHANGELOG 작성 규칙
|
|
127
131
|
│ ├─ MOCKUP_RULE.md # UX/UI HTML 목업 규칙
|
|
132
|
+
│ ├─ DEV_RULE.local.md # 이 프로젝트의 구현 정책 (모든 리포 필수)
|
|
133
|
+
│ ├─ TEST_RULE.local.md # 이 프로젝트의 검증 실행 경로·범위 (모든 리포 필수)
|
|
128
134
|
│ └─ *.local.md # 사용자 오버라이드 규칙 (패키지 업데이트 대상 제외)
|
|
129
135
|
├─ plans/ # plan(기능 그룹)별 PLAN.md / ROADMAP.md (<NNN_slug>/)
|
|
130
136
|
├─ tasks/ # task 문서 (<NNN_slug>/BACKLOG.md / spec-diffs / screenshots / mockup 포함)
|
|
@@ -133,17 +139,18 @@ my-app/
|
|
|
133
139
|
├─ shared/ # 관계 리포지토리 소통 메세지함
|
|
134
140
|
│ ├─ sent/completed/
|
|
135
141
|
│ └─ received/completed/
|
|
136
|
-
|
|
137
|
-
└─ TEST-GUIDE.md # 검증 방법 (방식별 이 프로젝트 실행 경로 — /task-plan이 채움)
|
|
142
|
+
└─ FRICTION_LOG.md # taskery 불편사항 누적 로그 (/log-friction 첫 기록 시 생성)
|
|
138
143
|
```
|
|
139
144
|
|
|
140
145
|
`.project/`는 플랫폼 무관 공통 자산이다. 진입 문서와 에이전트 자산 위치는 선택한 플랫폼에 따라 갈린다.
|
|
141
146
|
|
|
142
147
|
| 플랫폼 | 진입 문서 | 스킬 위치 | hook 위치 | hook 등록 |
|
|
143
148
|
|--------|----------|----------|----------|----------|
|
|
144
|
-
| Claude Code | `CLAUDE.md` | `.claude/skills/` | `.claude/hooks/` | `.claude/settings.json` |
|
|
149
|
+
| Claude Code | `CLAUDE.md` (→ `AGENTS.md` 임포트) | `.claude/skills/` | `.claude/hooks/` | `.claude/settings.json` |
|
|
145
150
|
| Codex | `AGENTS.md` | `.agents/skills/` | `.codex/hooks/` | `.codex/config.toml` |
|
|
146
151
|
|
|
152
|
+
진입 문서 본문은 `AGENTS.md` 한 벌이다. Claude Code는 `AGENTS.md`를 자동으로 읽지 않으므로 `CLAUDE.md`가 이를 임포트한다.
|
|
153
|
+
|
|
147
154
|
> 스킬 9종과 `git-guard.sh`는 양 플랫폼 동일 자산이다. 진입 문서, hook 등록 방식, 완료 보호 hook(`closed-immutable.sh`)의 구현만 플랫폼별로 다르다. `init`에서 둘 다 선택하면 폴더가 갈려 충돌 없이 공존한다.
|
|
148
155
|
|
|
149
156
|
---
|
|
@@ -202,7 +209,7 @@ my-app/
|
|
|
202
209
|
|
|
203
210
|
| 스킬 | 생성 위치 | 생성 문서 |
|
|
204
211
|
|------|---------|---------|
|
|
205
|
-
| `/project-init` | `.project/` | PROJECT.md /
|
|
212
|
+
| `/project-init` | `.project/` | PROJECT.md / LINKED-REPOS.md / GLOSSARY.md / .env + 제품 관통 문서(SERVICE-POLICY / TECH-STACK / ARCHITECTURE 작성, DATA-MODEL / API-SPEC / FEATURES / UX-UI 골격 — 타입 해당분) + 리포 로컬 룰 초안(TEST_RULE.local.md / DEV_RULE.local.md) |
|
|
206
213
|
| `/plan-init` | `.project/plans/<NNN_slug>/` | PLAN.md / ROADMAP.md + 제품 관통 문서 FEATURES/UX-UI에 기능 그룹 의도 추가 (제품 관통 문서 전체를 한꺼번에 만들지는 않음) |
|
|
207
214
|
| `/task-init` | `.project/tasks/<NNN_slug>/` | `<NNN>_<slug>.md` (단일 파일) 또는 `TASK-<NNN>_<slug>/task.md` (사용자 명시 시 폴더 승격) |
|
|
208
215
|
|
|
@@ -231,7 +238,7 @@ cd <project-name>
|
|
|
231
238
|
npx @angar2/taskery init
|
|
232
239
|
```
|
|
233
240
|
|
|
234
|
-
이후 메인 세션에 진입(진입 문서
|
|
241
|
+
이후 메인 세션에 진입(진입 문서 자동 정독 — 사용법 전체는 `.project/rules/TASKERY_RULE.md`)하여 다음 시퀀스로 호출한다. task 5 스킬은 호출과 동시에 상태를 전이시킨다.
|
|
235
242
|
|
|
236
243
|
```
|
|
237
244
|
/project-init # 진입 메타 문서와 디렉토리 골격 생성
|
|
@@ -258,6 +265,7 @@ npx @angar2/taskery init
|
|
|
258
265
|
- 사용자가 재구현을 요청하면 → `testing` → `developing`으로 회귀해 `/task-dev`가 자동 발동된다.
|
|
259
266
|
- 사용자가 결함을 인지한 채 종결을 지시하면 → `testing` → `tested`로 진행하고 결함을 명시한 후 `/task-close`로 이행된다.
|
|
260
267
|
- UNCERTAIN(자동 검증이 불가능한 시나리오)은 사용자가 직접 검수한 결과를 답하면 메인 세션이 PASS/FAIL로 해석해 위 두 흐름 중 하나로 합류시킨다.
|
|
268
|
+
- 검증에 필요한 테스트가 화면·입력을 점유해 실행을 보류한 경우에는, 지금 실행할지 / 자리를 비울 때로 미룰지 / 창을 띄우지 않는 실행 경로를 마련할지를 사용자가 고른다. 승인 없이 임의로 실행하지 않는다.
|
|
261
269
|
|
|
262
270
|
---
|
|
263
271
|
|
package/bin/fork.js
CHANGED
|
@@ -57,8 +57,10 @@ async function main() {
|
|
|
57
57
|
console.error(`type은 ${VALID_TYPES.join(' | ')} 중 하나 (받음: ${type})`);
|
|
58
58
|
process.exit(1);
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// ST-N = 로드맵 Stage 출처 (ROADMAP은 Stage 단위·RM 채번 없음 — recordion FRICTION 2026-08-03 반영).
|
|
61
|
+
// RM-NNN은 구형 호환으로 계속 수용.
|
|
62
|
+
if (!/^(BL-\d+|RM-\d+|ST-\d+|DR)$/.test(src)) {
|
|
63
|
+
console.error(`src는 BL-NNN | ST-N(로드맵 Stage) | DR 형식 (받음: ${src}. 구형 RM-NNN도 호환)`);
|
|
62
64
|
process.exit(1);
|
|
63
65
|
}
|
|
64
66
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
|
package/bin/init.js
CHANGED
|
@@ -31,6 +31,8 @@ const {
|
|
|
31
31
|
gitignorePatternsFor,
|
|
32
32
|
} = require('./lib');
|
|
33
33
|
|
|
34
|
+
const { ENTRY_DOCS_MARKER } = require('./migrate');
|
|
35
|
+
|
|
34
36
|
async function confirm(msg) {
|
|
35
37
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
36
38
|
return new Promise((resolve) => {
|
|
@@ -125,6 +127,8 @@ async function main() {
|
|
|
125
127
|
platforms,
|
|
126
128
|
stale_days: DEFAULT_STALE_DAYS,
|
|
127
129
|
lock_timeout_ms: DEFAULT_LOCK_TIMEOUT_MS,
|
|
130
|
+
activePlan: null, // /plan-init이 채운다 (0.7.0+ 활성 plan SSoT)
|
|
131
|
+
entryDocs: ENTRY_DOCS_MARKER, // 신규 설치는 이미 새 진입 문서 체계 — update가 이행을 시도하지 않도록
|
|
128
132
|
files: manifestFiles,
|
|
129
133
|
};
|
|
130
134
|
writeManifest(manifest, manifestPath);
|
|
@@ -189,10 +193,11 @@ async function main() {
|
|
|
189
193
|
console.log(` 카피: ${copied}개 / 스킵: ${skipped}개`);
|
|
190
194
|
console.log(` 플랫폼: ${platforms.join(', ')}`);
|
|
191
195
|
console.log(` manifest: ${MANIFEST_NAME}\n`);
|
|
192
|
-
const entryFile = platforms.includes('claude') ? 'CLAUDE.md' : 'AGENTS.md';
|
|
193
196
|
console.log(`다음 단계:`);
|
|
194
197
|
let step = 1;
|
|
195
|
-
|
|
198
|
+
// 진입 문서 본문은 AGENTS.md 한 벌 — CLAUDE.md는 `@AGENTS.md` 임포트 한 줄이라 채울 것이 없다.
|
|
199
|
+
console.log(` ${step++}. AGENTS.md 정독 + 프로젝트 메타 + 검증/테스트 명령 채우기`);
|
|
200
|
+
console.log(` (.project/rules/TASKERY_RULE.md = taskery 사용 설명서 — 세션이 필독)`);
|
|
196
201
|
if (platforms.includes('claude')) {
|
|
197
202
|
console.log(` ${step++}. Claude 첫 세션에서 taskery MCP 서버 승인 (.mcp.json — 구조화 도구 8종; 미승인 시 npx CLI 폴백)`);
|
|
198
203
|
}
|
package/bin/lib.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - getInitLockPath(projectId): ~/.taskery/<projectId>.init.lock (task 분기 직렬화용)
|
|
23
23
|
* - withMergeLock(projectId, fn, opts): proper-lockfile로 머지 락 + fn 실행 + 자동 release
|
|
24
24
|
* - withMetaLock(filePath, fn, opts): 메타 파일 쓰기 락 + fn 실행
|
|
25
|
+
* - withMetaLockSync(filePath, fn, opts): 위의 동기 변형 (sync 호출 체인용 — manifest 갱신)
|
|
25
26
|
* - getActiveTasks(mainWtPath): SSoT 조회 (git branch --list 'feature/*_TASK-*' ...)
|
|
26
27
|
* - getNextTaskNumber(mainWtPath): (진행중 ∪ 전 ref 머지 히스토리) 최대 + 1 (부모 무관 전역 채번)
|
|
27
28
|
* - forkTask(mainWtPath, {type,dev,src,slug}): init 락 안에서 부모(현재 브랜치) 캡처+채번+워크트리·브랜치 생성 원자 실행
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
* - assertMainWorktreeOn(mainWtPath, branch): 메인 워크트리가 지정 브랜치 체크아웃 상태인지 검증
|
|
30
31
|
*
|
|
31
32
|
* 백로그 (0.1.2+):
|
|
32
|
-
* - getActiveVersion(mainWtPath): .
|
|
33
|
+
* - getActiveVersion(mainWtPath): 활성 plan 식별자 — manifest.activePlan 우선, 구형 AGENT-GUIDE.md 폴백 (0.7.0+)
|
|
34
|
+
* - setActivePlan(mainWtPath, plan): manifest.activePlan 갱신 (plan 폴더 실재 검증 포함 — plan-switch)
|
|
33
35
|
* - getBacklogPath(mainWtPath, activeVersion?): .project/tasks/<활성 plan>/BACKLOG.md 절대 경로
|
|
34
36
|
* - parseBacklogItem(mainWtPath, blId): BL-NNN 항목 메타 파싱 ({ status, type, title, slug, summary, target, taskNums })
|
|
35
37
|
* - appendBacklogItem(mainWtPath, meta): withMetaLock + BL-NNN 채번 + 항목 append (placeholder 치환 우선)
|
|
@@ -63,8 +65,10 @@ const SHARED_DEST = {
|
|
|
63
65
|
function platformOf(relPath) {
|
|
64
66
|
if (relPath.startsWith('shared/')) return 'shared'; // 공통 — 설치 시 플랫폼별 매핑
|
|
65
67
|
if (relPath === 'CLAUDE.md' || relPath.startsWith('.claude/')) return 'claude';
|
|
66
|
-
if (relPath
|
|
67
|
-
|
|
68
|
+
if (relPath.startsWith('.codex/')) return 'codex';
|
|
69
|
+
// AGENTS.md는 0.7.0부터 agnostic — 진입 문서 본문의 단일 소스이고
|
|
70
|
+
// CLAUDE.md는 이를 `@AGENTS.md`로 임포트만 한다(Claude Code는 AGENTS.md를 자동 로드하지 않음).
|
|
71
|
+
return 'agnostic'; // AGENTS.md, .project/, .gitignore 등 — 항상 설치
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// shared/ 상대경로를 특정 플랫폼의 설치 경로로 변환.
|
|
@@ -104,9 +108,9 @@ function resolveInstallPlan(templateFiles, platforms, { includeAgnostic = true }
|
|
|
104
108
|
|
|
105
109
|
// .gitignore 등록 패턴 (플랫폼별)
|
|
106
110
|
const GITIGNORE_PATTERNS = {
|
|
107
|
-
agnostic: ['.project/', '.taskery-manifest.json'],
|
|
111
|
+
agnostic: ['.project/', '.taskery-manifest.json', 'AGENTS.md'],
|
|
108
112
|
claude: ['.claude/', 'CLAUDE.md'],
|
|
109
|
-
codex: ['.codex/', '.agents/'
|
|
113
|
+
codex: ['.codex/', '.agents/'],
|
|
110
114
|
};
|
|
111
115
|
|
|
112
116
|
// 선택 플랫폼 + agnostic의 gitignore 패턴 합성 (중복 제거)
|
|
@@ -247,7 +251,7 @@ function getWorktreesRoot(projectId) {
|
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
function getWorktreePath(projectId, { taskNum, src, slug }) {
|
|
250
|
-
// src = 'BL-NNN' | '
|
|
254
|
+
// src = 'BL-NNN' | 'ST-N'(로드맵 Stage) | 'DR' (구형 'RM-NNN' 호환)
|
|
251
255
|
const nnn = String(taskNum).padStart(3, '0');
|
|
252
256
|
return path.join(getWorktreesRoot(projectId), `TASK-${nnn}_${src}_${slug}`);
|
|
253
257
|
}
|
|
@@ -308,6 +312,27 @@ async function withMetaLock(filePath, fn, opts = {}) {
|
|
|
308
312
|
}
|
|
309
313
|
}
|
|
310
314
|
|
|
315
|
+
function withMetaLockSync(filePath, fn, opts = {}) {
|
|
316
|
+
// withMetaLock의 동기 변형. manifest 갱신 경로(initPlan → plan.js main)가 전부 sync라
|
|
317
|
+
// async 락을 쓰면 호출 사슬 전체를 async로 바꿔야 해 동기 락을 별도로 둔다.
|
|
318
|
+
const lockfile = require('proper-lockfile');
|
|
319
|
+
if (!fs.existsSync(filePath)) {
|
|
320
|
+
mkdirp(path.dirname(filePath));
|
|
321
|
+
fs.writeFileSync(filePath, '');
|
|
322
|
+
}
|
|
323
|
+
// sync API는 retries를 지원하지 않는다(proper-lockfile 제약). 경합 시 즉시 던지는데,
|
|
324
|
+
// manifest 쓰기는 사용자가 직접 부르는 명령들뿐이라 동시 실행이 실질적으로 없다.
|
|
325
|
+
const release = lockfile.lockSync(filePath, {
|
|
326
|
+
stale: opts.stale ?? DEFAULT_LOCK_TIMEOUT_MS,
|
|
327
|
+
realpath: false,
|
|
328
|
+
});
|
|
329
|
+
try {
|
|
330
|
+
return fn();
|
|
331
|
+
} finally {
|
|
332
|
+
release();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
311
336
|
function currentBranch(mainWtPath) {
|
|
312
337
|
// 메인 워크트리의 현재 브랜치 = 태스크의 부모 브랜치(0.6.0). detached HEAD면 'HEAD' 반환.
|
|
313
338
|
return gitCapture(mainWtPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
@@ -388,8 +413,9 @@ async function forkTask(mainWtPath, { type, dev, src, slug }) {
|
|
|
388
413
|
if (parent === 'HEAD') {
|
|
389
414
|
throw new Error('메인 워크트리가 detached HEAD — 부모로 삼을 브랜치에 체크아웃 필요.');
|
|
390
415
|
}
|
|
391
|
-
// SSoT 안전망 — 같은 출처(BL-NNN
|
|
392
|
-
|
|
416
|
+
// SSoT 안전망 — 같은 항목 출처(BL-NNN/구형 RM-NNN)가 이미 진행중이면 거부.
|
|
417
|
+
// DR(별도 ID 없음)·ST-N(Stage 하나에서 복수 task가 정상)은 검사 제외.
|
|
418
|
+
if (/^(BL|RM)-/.test(src)) {
|
|
393
419
|
const dup = getActiveTasks(mainWtPath).find((t) => t.src === src);
|
|
394
420
|
if (dup) {
|
|
395
421
|
throw new Error(`${src} 이미 진행중 (${dup.branch}) — 다른 세션이 같은 항목 진행`);
|
|
@@ -435,28 +461,51 @@ function computeNextPlanNumber(mainWtPath) {
|
|
|
435
461
|
const BACKLOG_PLACEHOLDER =
|
|
436
462
|
'(사용자 발화 또는 task-close 직후 메인 감지로 한 행씩 추가. 빈 상태 default.)';
|
|
437
463
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
464
|
+
// 구형(0.7.0 미만) 활성 plan 저장소 — .project/AGENT-GUIDE.md '## 활성 plan 버전' 섹션.
|
|
465
|
+
// 0.7.0에서 SSoT가 manifest.activePlan으로 이관됐으나, update 전 리포를 무중단으로 굴리기 위해
|
|
466
|
+
// 읽기 폴백으로만 남긴다. 쓰기는 하지 않는다(_setActivePlan은 manifest만 갱신).
|
|
467
|
+
// 헤딩 줄 끝 부가 텍스트(HTML 주석 등) 허용 — project-init 골격이 헤딩에 주석을 달므로 접두 일치로 판정.
|
|
468
|
+
// 완전 일치를 요구하면 자기 골격을 자기가 못 읽는 자기모순 (recordion FRICTION 2026-08-06, scaffoldError 6회).
|
|
469
|
+
function _readLegacyActivePlan(mainWtPath) {
|
|
441
470
|
const guidePath = path.join(mainWtPath, '.project', 'AGENT-GUIDE.md');
|
|
442
|
-
if (!fs.existsSync(guidePath))
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const content = fs.readFileSync(guidePath, 'utf8');
|
|
446
|
-
const sec = content.match(/##\s*활성 plan 버전\s*\n+([^\n]+)/);
|
|
447
|
-
if (!sec) {
|
|
448
|
-
throw new Error(`.project/AGENT-GUIDE.md '## 활성 plan 버전' 섹션 부재.`);
|
|
449
|
-
}
|
|
471
|
+
if (!fs.existsSync(guidePath)) return null;
|
|
472
|
+
const sec = fs.readFileSync(guidePath, 'utf8').match(/^##\s*활성 plan 버전[^\n]*\n+([^\n]+)/m);
|
|
473
|
+
if (!sec) return null;
|
|
450
474
|
// 첫 토큰 = plan 폴더 이름(' — 설명' 앞부분). '<예: ...>' 자리표시자는 미설정으로 간주.
|
|
451
475
|
const firstLine = sec[1].trim();
|
|
452
|
-
if (!firstLine || firstLine.startsWith('<'))
|
|
453
|
-
throw new Error(
|
|
454
|
-
`'## 활성 plan 버전' 값 미설정(${firstLine || '빈 줄'}) — /plan-init으로 활성 plan 갱신 필요.`,
|
|
455
|
-
);
|
|
456
|
-
}
|
|
476
|
+
if (!firstLine || firstLine.startsWith('<')) return null;
|
|
457
477
|
return firstLine.split(/\s+/)[0];
|
|
458
478
|
}
|
|
459
479
|
|
|
480
|
+
function getActiveVersion(mainWtPath) {
|
|
481
|
+
// 활성 plan 식별자 = manifest.activePlan (0.7.0+ SSoT). plan = 기능 그룹 단위라
|
|
482
|
+
// 식별자는 NNN_slug 폴더명(구형은 vX.X 버전명도 가능).
|
|
483
|
+
const manifest = readManifest(path.join(mainWtPath, MANIFEST_NAME));
|
|
484
|
+
if (manifest && manifest.activePlan) return manifest.activePlan;
|
|
485
|
+
|
|
486
|
+
// 구형 폴백 — update 전 리포. 읽기만 하고 승격은 update가 1회 수행한다.
|
|
487
|
+
const legacy = _readLegacyActivePlan(mainWtPath);
|
|
488
|
+
if (legacy) return legacy;
|
|
489
|
+
|
|
490
|
+
if (!manifest) {
|
|
491
|
+
throw new Error(`${MANIFEST_NAME} 부재. 'npx @angar2/taskery init' 먼저 실행.`);
|
|
492
|
+
}
|
|
493
|
+
throw new Error(
|
|
494
|
+
`활성 plan 미설정 (manifest.activePlan 없음) — /plan-init으로 plan을 생성하거나 ` +
|
|
495
|
+
`'npx @angar2/taskery plan-switch <NNN_slug>'로 지정 필요.`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function setActivePlan(mainWtPath, plan) {
|
|
500
|
+
// plan-switch — 대상 plan 폴더 실재 검증 후 manifest.activePlan 갱신.
|
|
501
|
+
const planDir = path.join(mainWtPath, '.project', 'plans', plan);
|
|
502
|
+
if (!fs.existsSync(planDir)) {
|
|
503
|
+
throw new Error(`plan 폴더 부재: ${planDir} — 존재하는 plan만 활성으로 지정 가능.`);
|
|
504
|
+
}
|
|
505
|
+
_setActivePlan(mainWtPath, plan);
|
|
506
|
+
return { plan, planDir };
|
|
507
|
+
}
|
|
508
|
+
|
|
460
509
|
function getBacklogPath(mainWtPath, activeVersion) {
|
|
461
510
|
const v = activeVersion ?? getActiveVersion(mainWtPath);
|
|
462
511
|
return path.join(mainWtPath, '.project', 'tasks', v, 'BACKLOG.md');
|
|
@@ -813,7 +862,7 @@ function _renderTaskSkeleton({ taskNum, title, created, plan, docType, size, par
|
|
|
813
862
|
|
|
814
863
|
function scaffoldTaskDoc(mainWtPath, wtPath, meta) {
|
|
815
864
|
// meta: { taskNum, slug, title, type(브랜치 타입), size, parent, plan?, promoted?, created? }
|
|
816
|
-
// 위치: 등록(.gitignore) 케이스 → 메인WT, 미등록 → 워크트리. plan 미지정 시
|
|
865
|
+
// 위치: 등록(.gitignore) 케이스 → 메인WT, 미등록 → 워크트리. plan 미지정 시 활성 plan(manifest.activePlan)에서 검출.
|
|
817
866
|
// parent 미지정 시 현재 브랜치 캡처(단독 호출 안전망 — 통상 fork가 넘김).
|
|
818
867
|
// taskNum이 고유(fork init 락 채번)라 동일 문서 동시 쓰기 경합 없음 → 락 불요.
|
|
819
868
|
const registered = isProjectRegistered(mainWtPath);
|
|
@@ -847,7 +896,7 @@ function scaffoldTaskDoc(mainWtPath, wtPath, meta) {
|
|
|
847
896
|
}
|
|
848
897
|
|
|
849
898
|
// ─── plan 생성 (코드화, C3) ─────────────────────────────────────────
|
|
850
|
-
// 채번 + legacy 게이트 + 폴더 mkdir + ROADMAP/PLAN/BACKLOG 골격 +
|
|
899
|
+
// 채번 + legacy 게이트 + 폴더 mkdir + ROADMAP/PLAN/BACKLOG 골격 + manifest.activePlan 갱신을 코드로.
|
|
851
900
|
// LLM 몫(FEATURES/UX-UI delta · ROADMAP Stage 내용 · PLAN 링크)은 골격의 placeholder로 남긴다.
|
|
852
901
|
|
|
853
902
|
function _renderRoadmap(plan) {
|
|
@@ -902,29 +951,23 @@ function _renderPlanBacklog(plan) {
|
|
|
902
951
|
}
|
|
903
952
|
|
|
904
953
|
function _setActivePlan(mainWtPath, plan) {
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const hIdx = lines.findIndex((l) => /^##\s*활성 plan 버전/.test(l));
|
|
912
|
-
if (hIdx === -1) {
|
|
913
|
-
throw new Error("AGENT-GUIDE.md '## 활성 plan 버전' 섹션 부재.");
|
|
954
|
+
// 활성 plan SSoT = manifest.activePlan (0.7.0+). 구형 AGENT-GUIDE.md는 갱신하지 않는다
|
|
955
|
+
// (읽기 폴백 전용 — 두 곳에 쓰면 다시 갈린다).
|
|
956
|
+
// manifest는 읽고-고쳐-쓰기라 다른 기록자(init/update/add)와 겹치면 필드가 유실될 수 있어 락으로 감싼다.
|
|
957
|
+
const manifestPath = path.join(mainWtPath, MANIFEST_NAME);
|
|
958
|
+
if (!fs.existsSync(manifestPath)) {
|
|
959
|
+
throw new Error(`${MANIFEST_NAME} 부재. 'npx @angar2/taskery init' 먼저 실행.`);
|
|
914
960
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
lines.splice(hIdx + 1, 0, '', valueLine); // 값 줄 부재 → 헤딩 뒤 삽입
|
|
922
|
-
}
|
|
923
|
-
fs.writeFileSync(guidePath, lines.join('\n'));
|
|
961
|
+
withMetaLockSync(manifestPath, () => {
|
|
962
|
+
const manifest = readManifest(manifestPath);
|
|
963
|
+
if (!manifest) throw new Error(`${MANIFEST_NAME} 읽기 실패.`);
|
|
964
|
+
manifest.activePlan = plan;
|
|
965
|
+
writeManifest(manifest, manifestPath);
|
|
966
|
+
});
|
|
924
967
|
}
|
|
925
968
|
|
|
926
969
|
function initPlan(mainWtPath, slug, opts = {}) {
|
|
927
|
-
// 채번 → legacy 게이트 → 폴더 + 골격 +
|
|
970
|
+
// 채번 → legacy 게이트 → 폴더 + 골격 + manifest.activePlan 갱신. slug 검증은 CLI 측.
|
|
928
971
|
// legacy 폴더(NNN_ 아닌 plan 폴더) 잔존 + !force → { gated:true } 반환(CLI가 사용자 confirm 후 force 재호출).
|
|
929
972
|
const { next, legacyDirs } = computeNextPlanNumber(mainWtPath);
|
|
930
973
|
if (legacyDirs.length && !opts.force) {
|
|
@@ -1006,10 +1049,26 @@ function _porcelainFiles(wtPath) {
|
|
|
1006
1049
|
.filter(Boolean);
|
|
1007
1050
|
}
|
|
1008
1051
|
|
|
1052
|
+
// 빌드 산출물 판정 — 소스 변경에 도구가 따라 바꾸는 파생 파일 (Phase 소속을 묻는 것 자체가 무의미).
|
|
1053
|
+
// Dev Plan에 명시 안 돼도 미매핑 blocked 대신 마지막 Phase에 자동 편입 (stash·recordion FRICTION: pbxproj 미매핑만 6회).
|
|
1054
|
+
const BUILD_ARTIFACT_RE = [
|
|
1055
|
+
/\.pbxproj$/,
|
|
1056
|
+
/(^|\/)package-lock\.json$/,
|
|
1057
|
+
/(^|\/)yarn\.lock$/,
|
|
1058
|
+
/(^|\/)pnpm-lock\.yaml$/,
|
|
1059
|
+
/(^|\/)Cargo\.lock$/,
|
|
1060
|
+
/(^|\/)Gemfile\.lock$/,
|
|
1061
|
+
/(^|\/)Podfile\.lock$/,
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1009
1064
|
function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
1010
1065
|
// 코드 변경분(.project/ 외)을 Dev Plan Phase에 매핑해 Phase별 자동 커밋. meta: { taskNum, type }.
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1066
|
+
// 매핑 폴백 3규칙 (stash 6회 + recordion 5회 blocked 마찰 반영 — 11회 전부 기계적 편입이었음):
|
|
1067
|
+
// 1. 다중 Phase 매핑 → 가장 이른 Phase에 편입 + 커밋 메시지에 '(Phase a·b 변경 포함)' 표기.
|
|
1068
|
+
// 뒤 Phase가 앞 Phase 타입·함수를 참조하므로 이른 쪽이 의존 방향과 일치. 중간 커밋 독립 빌드는 비보장(GIT_RULE 명문).
|
|
1069
|
+
// 2. 미매핑 + 빌드 산출물(BUILD_ARTIFACT_RE) → 매핑된 그룹 중 마지막 Phase(없으면 Dev Plan 마지막)에
|
|
1070
|
+
// 자동 편입 + '(빌드 산출물 자동 편입)' 표기.
|
|
1071
|
+
// 3. 미매핑 + 일반 파일 → { ambiguous } 반환(커밋 X) — 계획 누락 가능성이라 사람 판단으로 유지.
|
|
1013
1072
|
// .project/ 메타(flows/문서/changelog)는 제외 — closeTask가 별도 커밋.
|
|
1014
1073
|
const codeFiles = _porcelainFiles(wtPath).filter((f) => !f.startsWith('.project/'));
|
|
1015
1074
|
if (codeFiles.length === 0) return { committed: [], empty: true };
|
|
@@ -1028,21 +1087,44 @@ function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
|
1028
1087
|
)
|
|
1029
1088
|
.map(({ i }) => i);
|
|
1030
1089
|
const fileToPhase = {};
|
|
1090
|
+
const multiPhase = []; // 규칙 1 적용분 — { file, phases:[num...], assignedTo }
|
|
1091
|
+
const artifactFiles = []; // 규칙 2 후보 — 매핑 확정 후 편입 Phase 결정
|
|
1031
1092
|
for (const f of codeFiles) {
|
|
1032
1093
|
const m = matchOf(f);
|
|
1033
|
-
if (m.length
|
|
1094
|
+
if (m.length === 1) {
|
|
1095
|
+
fileToPhase[f] = m[0];
|
|
1096
|
+
} else if (m.length > 1) {
|
|
1097
|
+
const earliest = Math.min(...m);
|
|
1098
|
+
fileToPhase[f] = earliest;
|
|
1099
|
+
multiPhase.push({ file: f, phases: m.map((i) => phases[i].num), assignedTo: phases[earliest].num });
|
|
1100
|
+
} else if (BUILD_ARTIFACT_RE.some((re) => re.test(f))) {
|
|
1101
|
+
artifactFiles.push(f);
|
|
1102
|
+
} else {
|
|
1034
1103
|
return {
|
|
1035
1104
|
ambiguous: true,
|
|
1036
|
-
reason:
|
|
1105
|
+
reason: `Phase 미매핑: ${f}`,
|
|
1037
1106
|
files: codeFiles,
|
|
1038
1107
|
phases: phases.map((p) => ({ num: p.num, name: p.name, files: p.files })),
|
|
1039
1108
|
};
|
|
1040
1109
|
}
|
|
1041
|
-
fileToPhase[f] = m[0];
|
|
1042
1110
|
}
|
|
1111
|
+
// 규칙 2 — 산출물 편입: 매핑된 그룹 중 마지막 Phase, 하나도 없으면 Dev Plan 마지막 Phase
|
|
1112
|
+
const autoAssigned = [];
|
|
1113
|
+
if (artifactFiles.length > 0) {
|
|
1114
|
+
const mappedIdx = Object.values(fileToPhase);
|
|
1115
|
+
const lastIdx = mappedIdx.length > 0 ? Math.max(...mappedIdx) : phases.length - 1;
|
|
1116
|
+
for (const f of artifactFiles) {
|
|
1117
|
+
fileToPhase[f] = lastIdx;
|
|
1118
|
+
autoAssigned.push({ file: f, assignedTo: phases[lastIdx].num });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// 파일별 커밋 메시지 표기 (자동 편입의 근거를 커밋에 남김 — 코드가 몰래 정한 걸 드러낸다)
|
|
1122
|
+
const noteOf = {};
|
|
1123
|
+
for (const x of multiPhase) noteOf[x.file] = ` (Phase ${x.phases.join('·')} 변경 포함)`;
|
|
1124
|
+
for (const x of autoAssigned) noteOf[x.file] = ' (빌드 산출물 자동 편입)';
|
|
1043
1125
|
// Phase별 그룹 → 순서대로 커밋
|
|
1044
1126
|
const groups = new Map();
|
|
1045
|
-
for (const f of
|
|
1127
|
+
for (const f of Object.keys(fileToPhase)) {
|
|
1046
1128
|
const i = fileToPhase[f];
|
|
1047
1129
|
if (!groups.has(i)) groups.set(i, []);
|
|
1048
1130
|
groups.get(i).push(f);
|
|
@@ -1056,18 +1138,22 @@ function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
|
1056
1138
|
execFileSync('git', ['-C', wtPath, 'add', '--', ...grp], { stdio: 'pipe' });
|
|
1057
1139
|
const msg =
|
|
1058
1140
|
`${tag}: [TASK-${nnn}] Phase ${p.num} - ${p.name}\n\n` +
|
|
1059
|
-
grp.map((f) => `- ${f}`).join('\n') +
|
|
1141
|
+
grp.map((f) => `- ${f}${noteOf[f] || ''}`).join('\n') +
|
|
1060
1142
|
`\n- 사유: ${p.name}`;
|
|
1061
1143
|
execFileSync('git', ['-C', wtPath, 'commit', '-m', msg], { stdio: 'pipe' });
|
|
1062
1144
|
committed.push({ phase: p.num, name: p.name, files: grp });
|
|
1063
1145
|
}
|
|
1064
|
-
|
|
1146
|
+
const result = { committed };
|
|
1147
|
+
if (multiPhase.length > 0) result.multiPhase = multiPhase;
|
|
1148
|
+
if (autoAssigned.length > 0) result.autoAssigned = autoAssigned;
|
|
1149
|
+
return result;
|
|
1065
1150
|
}
|
|
1066
1151
|
|
|
1067
1152
|
async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
1068
1153
|
// 결정적 준비만 — 비가역(머지·정리)·충돌 해결은 스킬이 후속 수행.
|
|
1069
1154
|
// 반환: { blocked } (게이트 미충족/매핑 모호) 또는
|
|
1070
|
-
// { prepped, branch, parent, wtPath, registered, commits, aheadOfParent
|
|
1155
|
+
// { prepped, branch, parent, wtPath, registered, commits, aheadOfParent,
|
|
1156
|
+
// multiPhase?, autoAssigned? (매핑 폴백 적용 시 내역) }. parent = 스킬 머지 대상.
|
|
1071
1157
|
const active = getActiveTasks(mainWtPath).find((t) => t.taskNum === taskNum);
|
|
1072
1158
|
if (!active) {
|
|
1073
1159
|
throw new Error(`TASK-${String(taskNum).padStart(3, '0')} 진행중 브랜치 없음 (SSoT 조회).`);
|
|
@@ -1158,7 +1244,7 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1158
1244
|
gitCapture(wtPath, ['rev-list', '--count', `${parent}..HEAD`], { allowFail: true }) || '0',
|
|
1159
1245
|
10,
|
|
1160
1246
|
);
|
|
1161
|
-
|
|
1247
|
+
const prepResult = {
|
|
1162
1248
|
prepped: true,
|
|
1163
1249
|
taskNum,
|
|
1164
1250
|
branch: active.branch,
|
|
@@ -1169,6 +1255,10 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1169
1255
|
commits,
|
|
1170
1256
|
aheadOfParent: aheadFinal,
|
|
1171
1257
|
};
|
|
1258
|
+
// 매핑 폴백 적용 내역 노출 — 코드가 자동 편입한 걸 스킬·사용자가 볼 수 있게
|
|
1259
|
+
if (split.multiPhase) prepResult.multiPhase = split.multiPhase;
|
|
1260
|
+
if (split.autoAssigned) prepResult.autoAssigned = split.autoAssigned;
|
|
1261
|
+
return prepResult;
|
|
1172
1262
|
}
|
|
1173
1263
|
|
|
1174
1264
|
module.exports = {
|
|
@@ -1207,6 +1297,7 @@ module.exports = {
|
|
|
1207
1297
|
getInitLockPath,
|
|
1208
1298
|
withMergeLock,
|
|
1209
1299
|
withMetaLock,
|
|
1300
|
+
withMetaLockSync,
|
|
1210
1301
|
currentBranch,
|
|
1211
1302
|
assertMainWorktreeOn,
|
|
1212
1303
|
parseBranchName,
|
|
@@ -1217,6 +1308,7 @@ module.exports = {
|
|
|
1217
1308
|
// 백로그 (0.1.2+)
|
|
1218
1309
|
BACKLOG_PLACEHOLDER,
|
|
1219
1310
|
getActiveVersion,
|
|
1311
|
+
setActivePlan,
|
|
1220
1312
|
getBacklogPath,
|
|
1221
1313
|
computeNextBLNumber,
|
|
1222
1314
|
formatBacklogBlock,
|
package/bin/mcp.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 도구 8종 = CLI 명령과 동일한 lib.js 함수의 다른 입구(네이티브 구조화 인자/반환).
|
|
8
8
|
* backlog_add / backlog_get / backlog_mark — 활성 plan BACKLOG.md 조작
|
|
9
9
|
* set_status — task 헤더 상태 전이 (7×7 유효전이 검증, closed 거부)
|
|
10
|
-
* plan_init — plan 생성 (채번+골격+
|
|
10
|
+
* plan_init — plan 생성 (채번+골격+manifest.activePlan 갱신)
|
|
11
11
|
* task_init — task 분기 (fork 원자 채번 + §1.3 골격 자동 생성)
|
|
12
12
|
* task_close — close 결정적 준비만 (Phase커밋·status=closed·마커). 비가역 머지/정리는 스킬.
|
|
13
13
|
* status — 멀티세션 현황 + 활성 plan
|
|
@@ -159,7 +159,7 @@ server.registerTool(
|
|
|
159
159
|
'plan_init',
|
|
160
160
|
{
|
|
161
161
|
description:
|
|
162
|
-
'plan(기능 그룹) 생성 — 채번 + 폴더 + ROADMAP/PLAN/BACKLOG 골격 +
|
|
162
|
+
'plan(기능 그룹) 생성 — 채번 + 폴더 + ROADMAP/PLAN/BACKLOG 골격 + 활성 plan 갱신. 이후 골격 내용은 LLM이 채움. legacy 폴더 잔존 시 gated 반환(force로 재호출).',
|
|
163
163
|
inputSchema: {
|
|
164
164
|
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, '영어 kebab-case'),
|
|
165
165
|
force: z.boolean().optional().describe('legacy 게이트 무시'),
|
|
@@ -189,7 +189,7 @@ server.registerTool(
|
|
|
189
189
|
inputSchema: {
|
|
190
190
|
type: z.enum(VALID_TYPES),
|
|
191
191
|
dev: z.string().describe('개발자 식별자 (claude / angar2 등)'),
|
|
192
|
-
src: z.string().regex(/^(BL-\d+|RM-\d+|DR)$/, 'BL-NNN |
|
|
192
|
+
src: z.string().regex(/^(BL-\d+|RM-\d+|ST-\d+|DR)$/, 'BL-NNN | ST-N(로드맵 Stage) | DR (구형 RM-NNN 호환)'),
|
|
193
193
|
slug: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, '영어 kebab-case'),
|
|
194
194
|
size: z.enum(VALID_SIZES).optional(),
|
|
195
195
|
title: z.string().optional().describe('한국어 제목 (size와 함께 주면 골격 생성)'),
|