@angar2/taskery 0.5.0 → 0.6.1
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 +11 -5
- package/bin/close.js +2 -2
- package/bin/fork.js +5 -2
- package/bin/lib.js +128 -57
- package/bin/mcp.js +4 -3
- package/bin/status.js +6 -3
- package/package.json +1 -1
- package/template/.claude/hooks/closed-immutable.sh +5 -3
- package/template/.codex/agents/task-tester.toml +6 -3
- package/template/.codex/hooks/closed-immutable.sh +2 -2
- package/template/.project/TEST-GUIDE.md +11 -0
- package/template/.project/rules/GIT_RULE.md +32 -19
- package/template/.project/rules/TASK_DOC_RULE.md +12 -10
- package/template/AGENTS.md +16 -14
- package/template/CLAUDE.md +16 -14
- package/template/shared/skills/add-backlog/SKILL.md +1 -1
- package/template/shared/skills/plan-init/SKILL.md +2 -2
- package/template/shared/skills/project-init/SKILL.md +2 -2
- package/template/shared/skills/task-close/SKILL.md +53 -24
- package/template/shared/skills/task-dev/SKILL.md +33 -2
- package/template/shared/skills/task-init/SKILL.md +16 -16
- package/template/shared/skills/task-plan/SKILL.md +17 -10
- package/template/shared/skills/task-test/SKILL.md +64 -11
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
|
|
|
@@ -39,7 +42,7 @@ draft → planned → developing → developed → testing → tested → closed
|
|
|
39
42
|
| `developed` | 구현 완료 (자가 검증 통과) |
|
|
40
43
|
| `testing` | 독립 검증 진행 중 |
|
|
41
44
|
| `tested` | 독립 검증 완료 |
|
|
42
|
-
| `closed` | git 커밋 +
|
|
45
|
+
| `closed` | git 커밋 + 부모 브랜치 병합 완료 |
|
|
43
46
|
|
|
44
47
|
> 상태별 전이 조건과 task 문서 양식은 [TASK-DOC.md](https://github.com/angar2/taskery/blob/main/plan/TASK-DOC.md)에서 확인 가능
|
|
45
48
|
|
|
@@ -125,6 +128,8 @@ my-app/
|
|
|
125
128
|
│ ├─ GIT_RULE.md # 로컬 깃 운영 규칙
|
|
126
129
|
│ ├─ CHANGELOG_RULE.md # CHANGELOG 작성 규칙
|
|
127
130
|
│ ├─ MOCKUP_RULE.md # UX/UI HTML 목업 규칙
|
|
131
|
+
│ ├─ DEV_RULE.local.md # (선택) 이 프로젝트의 구현·테스트 실행 방식
|
|
132
|
+
│ ├─ TEST_RULE.local.md # (선택) 이 프로젝트의 검증 범위·방식
|
|
128
133
|
│ └─ *.local.md # 사용자 오버라이드 규칙 (패키지 업데이트 대상 제외)
|
|
129
134
|
├─ plans/ # plan(기능 그룹)별 PLAN.md / ROADMAP.md (<NNN_slug>/)
|
|
130
135
|
├─ tasks/ # task 문서 (<NNN_slug>/BACKLOG.md / spec-diffs / screenshots / mockup 포함)
|
|
@@ -153,8 +158,8 @@ my-app/
|
|
|
153
158
|
같은 프로젝트에서 여러 메인 세션을 동시에 운영해 독립 task를 병렬로 진행한다.
|
|
154
159
|
|
|
155
160
|
- 새 task를 시작하면(`/task-init`) 작업 폴더(`~/.taskery/worktrees/<projectId>/TASK-NNN_<출처>_<슬러그>/`)와 작업 브랜치를 함께 분기한다.
|
|
156
|
-
- 작업 폴더에서 새 메인 세션을 열어 진행하거나, 메인 세션 1개로 모든 작업을 지휘하거나, 메인이 서브 세션을 호출해 병렬로 진행한다 — 운영 방식은 자유. 메인 워크트리(원본 폴더)는 `dev`
|
|
157
|
-
- task 완료(`/task-close`) 시 머지 락으로 직렬화한 후
|
|
161
|
+
- 작업 폴더에서 새 메인 세션을 열어 진행하거나, 메인 세션 1개로 모든 작업을 지휘하거나, 메인이 서브 세션을 호출해 병렬로 진행한다 — 운영 방식은 자유. 메인 워크트리(원본 폴더)는 task를 시작한 브랜치(*부모 브랜치*, 기본은 `dev`)를 그대로 유지한다.
|
|
162
|
+
- task 완료(`/task-close`) 시 머지 락으로 직렬화한 후 **그 부모 브랜치**에 `--no-ff` 병합한다(개인은 `dev`, 회사/로드맵은 서 있던 브랜치). 다른 세션이 먼저 머지해 충돌이 발생하면 본 세션이 단순/의미적/판단 불가 3단계로 해결을 시도한다.
|
|
158
163
|
- 머지 직후 작업 폴더와 작업 브랜치는 자동 정리된다(보존 키워드 사용 시 양쪽 유지). 안전망으로 복구 명령이 함께 출력된다.
|
|
159
164
|
|
|
160
165
|
요건: git ≥ 2.31. 보조 명령 `npx @angar2/taskery status` / `prune`로 진행 현황과 stale 정리를 확인한다.
|
|
@@ -189,7 +194,7 @@ my-app/
|
|
|
189
194
|
| `/task-plan` | task | task의 요구사항·범위·개발 계획·테스트 계획 작성 |
|
|
190
195
|
| `/task-dev` | task | 계획에 따른 단계별 구현 + 자가 검증 |
|
|
191
196
|
| `/task-test` | task | 별도 격리 세션에서 독립 검증 (메인 가정 차단) |
|
|
192
|
-
| `/task-close` | task | 최종 검증 후 git 커밋 +
|
|
197
|
+
| `/task-close` | task | 최종 검증 후 git 커밋 + 부모 브랜치 `--no-ff` 병합 |
|
|
193
198
|
| `/add-backlog` | meta | 사용자 발화로 plan(기능 그룹)별 `tasks/<NNN_slug>/BACKLOG.md`에 task 후보 1건씩 추가 (0.1.2+) |
|
|
194
199
|
| `/log-friction` | meta | 사용자 불편을 `.project/FRICTION_LOG.md`에 한 행 기록 |
|
|
195
200
|
| `/run-team` | meta | 여러 작업을 묶을 수 있는 단위로 task로 나눠 자동 병렬 처리 — 리더 세션이 팀원(독립 세션)에게 task를 1건씩 분배하고 흐름을 관리 (Claude 전용 · 실험 기능 전제 · 0.3.x+) |
|
|
@@ -240,7 +245,7 @@ npx @angar2/taskery init
|
|
|
240
245
|
/task-plan TASK-001 # draft → planned : 요구사항·범위·개발 계획·테스트 계획 작성
|
|
241
246
|
/task-dev TASK-001 # planned → developed : 단계별 구현과 자체 검증
|
|
242
247
|
/task-test TASK-001 # developed → tested : 별도 격리 세션으로 독립 검증
|
|
243
|
-
/task-close TASK-001 # tested → closed : 최종 검증 후 git 커밋과
|
|
248
|
+
/task-close TASK-001 # tested → closed : 최종 검증 후 git 커밋과 부모 브랜치 병합
|
|
244
249
|
|
|
245
250
|
# 불편 발생 시 등록
|
|
246
251
|
/log-friction # 사용자 불편 한 행 기록
|
|
@@ -258,6 +263,7 @@ npx @angar2/taskery init
|
|
|
258
263
|
- 사용자가 재구현을 요청하면 → `testing` → `developing`으로 회귀해 `/task-dev`가 자동 발동된다.
|
|
259
264
|
- 사용자가 결함을 인지한 채 종결을 지시하면 → `testing` → `tested`로 진행하고 결함을 명시한 후 `/task-close`로 이행된다.
|
|
260
265
|
- UNCERTAIN(자동 검증이 불가능한 시나리오)은 사용자가 직접 검수한 결과를 답하면 메인 세션이 PASS/FAIL로 해석해 위 두 흐름 중 하나로 합류시킨다.
|
|
266
|
+
- 검증에 필요한 테스트가 화면·입력을 점유해 실행을 보류한 경우에는, 지금 실행할지 / 자리를 비울 때로 미룰지 / 창을 띄우지 않는 실행 경로를 마련할지를 사용자가 고른다. 승인 없이 임의로 실행하지 않는다.
|
|
261
267
|
|
|
262
268
|
---
|
|
263
269
|
|
package/bin/close.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* `npx @angar2/taskery close <TASK-NNN>` — close 결정적 준비.
|
|
6
6
|
*
|
|
7
7
|
* 게이트(status=tested) → Phase 커밋(D2 자동 매핑) → status=closed → flows/문서 커밋 → 추적마커까지
|
|
8
|
-
* 코드가 수행하고 멈춘다. 이후 비가역(사전 rebase+충돌해결 / 머지 락 /
|
|
8
|
+
* 코드가 수행하고 멈춘다. 이후 비가역(사전 rebase+충돌해결 / 머지 락 / 부모 --no-ff 머지 /
|
|
9
9
|
* worktree remove / branch -d)은 스킬(task-close)이 git-guard hook + 복구출력 보호 하에 수행.
|
|
10
10
|
*
|
|
11
|
-
* stdout: { prepped, branch, wtPath, registered, commits,
|
|
11
|
+
* stdout: { prepped, branch, parent, wtPath, registered, commits, aheadOfParent }. parent = 머지 대상. exit 0.
|
|
12
12
|
* 콜백(스킬이 LLM 판단 후 처리):
|
|
13
13
|
* - exit 3 + { blocked:'status', current } — status가 tested 아님 (게이트 차단)
|
|
14
14
|
* - exit 2 + { blocked:'mapping', files, phases, reason } — Phase↔파일 매핑 모호 (수동 커밋 필요)
|
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)) {
|
|
@@ -96,6 +98,7 @@ async function main() {
|
|
|
96
98
|
title: opts.title,
|
|
97
99
|
type,
|
|
98
100
|
size: opts.size,
|
|
101
|
+
parent: result.parent,
|
|
99
102
|
promoted: !!opts.promote,
|
|
100
103
|
});
|
|
101
104
|
out = { ...result, docPath: doc.file, registered: doc.registered, promoted: doc.promoted };
|
package/bin/lib.js
CHANGED
|
@@ -22,11 +22,11 @@
|
|
|
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
|
-
* - getActiveTasks(mainWtPath): SSoT 조회 (git branch --
|
|
26
|
-
* - getNextTaskNumber(mainWtPath): (진행중 ∪
|
|
27
|
-
* - forkTask(mainWtPath, {type,dev,src,slug}): init 락 안에서
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
25
|
+
* - getActiveTasks(mainWtPath): SSoT 조회 (git branch --list 'feature/*_TASK-*' ...)
|
|
26
|
+
* - getNextTaskNumber(mainWtPath): (진행중 ∪ 전 ref 머지 히스토리) 최대 + 1 (부모 무관 전역 채번)
|
|
27
|
+
* - forkTask(mainWtPath, {type,dev,src,slug}): init 락 안에서 부모(현재 브랜치) 캡처+채번+워크트리·브랜치 생성 원자 실행
|
|
28
|
+
* - currentBranch(mainWtPath): 메인 워크트리 현재 브랜치 = 부모 브랜치(detached면 'HEAD')
|
|
29
|
+
* - assertMainWorktreeOn(mainWtPath, branch): 메인 워크트리가 지정 브랜치 체크아웃 상태인지 검증
|
|
30
30
|
*
|
|
31
31
|
* 백로그 (0.1.2+):
|
|
32
32
|
* - getActiveVersion(mainWtPath): .project/AGENT-GUIDE.md에서 활성 plan 식별자(버전 또는 기능 그룹명) 추출
|
|
@@ -247,7 +247,7 @@ function getWorktreesRoot(projectId) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function getWorktreePath(projectId, { taskNum, src, slug }) {
|
|
250
|
-
// src = 'BL-NNN' | '
|
|
250
|
+
// src = 'BL-NNN' | 'ST-N'(로드맵 Stage) | 'DR' (구형 'RM-NNN' 호환)
|
|
251
251
|
const nnn = String(taskNum).padStart(3, '0');
|
|
252
252
|
return path.join(getWorktreesRoot(projectId), `TASK-${nnn}_${src}_${slug}`);
|
|
253
253
|
}
|
|
@@ -308,17 +308,15 @@ async function withMetaLock(filePath, fn, opts = {}) {
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
throw new Error('dev 브랜치 부재. 사용자 결정 필요 (생성?).');
|
|
315
|
-
}
|
|
311
|
+
function currentBranch(mainWtPath) {
|
|
312
|
+
// 메인 워크트리의 현재 브랜치 = 태스크의 부모 브랜치(0.6.0). detached HEAD면 'HEAD' 반환.
|
|
313
|
+
return gitCapture(mainWtPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
316
314
|
}
|
|
317
315
|
|
|
318
|
-
function
|
|
319
|
-
const current =
|
|
320
|
-
if (current !==
|
|
321
|
-
throw new Error(`메인 워크트리가
|
|
316
|
+
function assertMainWorktreeOn(mainWtPath, branch) {
|
|
317
|
+
const current = currentBranch(mainWtPath);
|
|
318
|
+
if (current !== branch) {
|
|
319
|
+
throw new Error(`메인 워크트리가 ${branch} 아님 (현재: ${current}). 부모 브랜치로 체크아웃 필요.`);
|
|
322
320
|
}
|
|
323
321
|
}
|
|
324
322
|
|
|
@@ -341,8 +339,8 @@ function parseBranchName(branch) {
|
|
|
341
339
|
}
|
|
342
340
|
|
|
343
341
|
function getActiveTasks(mainWtPath) {
|
|
344
|
-
// SSoT — TASK-* 패턴 작업 브랜치 (분기 직후 빈 브랜치 포함). `--no-merged
|
|
345
|
-
// 워크트리 분기 직후 브랜치는
|
|
342
|
+
// SSoT — TASK-* 패턴 작업 브랜치 (분기 직후 빈 브랜치 포함). `--no-merged <부모>` 미사용:
|
|
343
|
+
// 워크트리 분기 직후 브랜치는 부모와 동일 commit이라 `--no-merged`가 *완전 머지 상태*로
|
|
346
344
|
// 처리 → 빈 결과 → getNextTaskNumber 충돌. 브랜치 존재 자체가 SSoT (정상 흐름은 task-close
|
|
347
345
|
// 자동 삭제로 자연 정리, 보존 키워드 잔존분도 *진행중*으로 잡힘이 정합).
|
|
348
346
|
const out = gitCapture(mainWtPath, ['branch', '--list', ...BRANCH_PATTERNS]);
|
|
@@ -358,12 +356,14 @@ function getActiveTasks(mainWtPath) {
|
|
|
358
356
|
function getNextTaskNumber(mainWtPath) {
|
|
359
357
|
// 1. 진행중 (SSoT)
|
|
360
358
|
const activeNums = getActiveTasks(mainWtPath).map((t) => t.taskNum);
|
|
361
|
-
// 2.
|
|
362
|
-
//
|
|
363
|
-
//
|
|
359
|
+
// 2. 머지 히스토리 (GIT_RULE 풍부 메시지) — 모든 ref(`--all`) 스캔.
|
|
360
|
+
// 부모 브랜치가 dev로 고정되지 않으므로(0.6.0) 닫힌 태스크는 각자의 부모에 병합돼 있다.
|
|
361
|
+
// 특정 부모(과거 'dev') 하나만 grep하면 다른 부모로 닫힌 번호를 놓쳐 채번 충돌 → `--all`로
|
|
362
|
+
// 전 ref의 TASK-NNN 최대를 취해 번호를 리포 전역으로 유지.
|
|
363
|
+
// --extended-regexp(ERE)에서 `+`는 수량자 — `\+`는 *리터럴 +* 라 "TASK-001"을 못 잡는다.
|
|
364
364
|
const log = gitCapture(mainWtPath, [
|
|
365
365
|
'log',
|
|
366
|
-
'
|
|
366
|
+
'--all',
|
|
367
367
|
'--grep',
|
|
368
368
|
'TASK-[0-9]+',
|
|
369
369
|
'--extended-regexp',
|
|
@@ -383,10 +383,14 @@ async function forkTask(mainWtPath, { type, dev, src, slug }) {
|
|
|
383
383
|
// getNextTaskNumber 직후 worktree add까지 끝내므로 다음 호출은 늘어난 번호를 본다.
|
|
384
384
|
const projectId = getProjectId(mainWtPath);
|
|
385
385
|
return withMetaLock(getInitLockPath(projectId), () => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
386
|
+
// 부모 = 메인 워크트리의 현재 브랜치(0.6.0). dev 하드코딩 제거 — 서 있는 브랜치가 곧 부모.
|
|
387
|
+
const parent = currentBranch(mainWtPath);
|
|
388
|
+
if (parent === 'HEAD') {
|
|
389
|
+
throw new Error('메인 워크트리가 detached HEAD — 부모로 삼을 브랜치에 체크아웃 필요.');
|
|
390
|
+
}
|
|
391
|
+
// SSoT 안전망 — 같은 항목 출처(BL-NNN/구형 RM-NNN)가 이미 진행중이면 거부.
|
|
392
|
+
// DR(별도 ID 없음)·ST-N(Stage 하나에서 복수 task가 정상)은 검사 제외.
|
|
393
|
+
if (/^(BL|RM)-/.test(src)) {
|
|
390
394
|
const dup = getActiveTasks(mainWtPath).find((t) => t.src === src);
|
|
391
395
|
if (dup) {
|
|
392
396
|
throw new Error(`${src} 이미 진행중 (${dup.branch}) — 다른 세션이 같은 항목 진행`);
|
|
@@ -397,11 +401,12 @@ async function forkTask(mainWtPath, { type, dev, src, slug }) {
|
|
|
397
401
|
const branch = `${type}/${dev}_TASK-${nnn}_${src}_${slug}`;
|
|
398
402
|
const wtPath = getWorktreePath(projectId, { taskNum, src, slug });
|
|
399
403
|
mkdirp(getWorktreesRoot(projectId));
|
|
400
|
-
// 동일 브랜치명은 git이 거부(같은 항목 동시 분기
|
|
401
|
-
|
|
404
|
+
// 부모 브랜치를 base로 워크트리·브랜치 생성. 동일 브랜치명은 git이 거부(같은 항목 동시 분기
|
|
405
|
+
// 차단). 실패 시 throw → fork.js가 사용자 보고.
|
|
406
|
+
execFileSync('git', ['-C', mainWtPath, 'worktree', 'add', wtPath, '-b', branch, parent], {
|
|
402
407
|
stdio: 'pipe',
|
|
403
408
|
});
|
|
404
|
-
return { taskNum, nnn, branch, wtPath, projectId };
|
|
409
|
+
return { taskNum, nnn, branch, wtPath, projectId, parent };
|
|
405
410
|
});
|
|
406
411
|
}
|
|
407
412
|
|
|
@@ -434,12 +439,15 @@ const BACKLOG_PLACEHOLDER =
|
|
|
434
439
|
function getActiveVersion(mainWtPath) {
|
|
435
440
|
// .project/AGENT-GUIDE.md '## 활성 plan 버전' 섹션 다음 비어 있지 않은 첫 줄에서 활성 plan 식별자 추출.
|
|
436
441
|
// plan = 기능 그룹 단위 → 식별자는 vX.X 버전명 또는 기능 그룹 slug(compare-products 등) 모두 가능.
|
|
442
|
+
// 헤딩 줄 끝 부가 텍스트(HTML 주석 등) 허용 — project-init 골격이 헤딩에 주석을 달므로
|
|
443
|
+
// 접두 일치로 판정(_setActivePlan의 findIndex 기준과 동일). 완전 일치 요구 시
|
|
444
|
+
// 자기 골격을 자기가 못 읽는 자기모순 (recordion FRICTION 2026-08-06, scaffoldError 6회).
|
|
437
445
|
const guidePath = path.join(mainWtPath, '.project', 'AGENT-GUIDE.md');
|
|
438
446
|
if (!fs.existsSync(guidePath)) {
|
|
439
447
|
throw new Error(`.project/AGENT-GUIDE.md 부재. /project-init 먼저 호출.`);
|
|
440
448
|
}
|
|
441
449
|
const content = fs.readFileSync(guidePath, 'utf8');
|
|
442
|
-
const sec = content.match(
|
|
450
|
+
const sec = content.match(/^##\s*활성 plan 버전[^\n]*\n+([^\n]+)/m);
|
|
443
451
|
if (!sec) {
|
|
444
452
|
throw new Error(`.project/AGENT-GUIDE.md '## 활성 plan 버전' 섹션 부재.`);
|
|
445
453
|
}
|
|
@@ -593,7 +601,7 @@ async function markBacklogChecked(mainWtPath, blId, taskNum) {
|
|
|
593
601
|
}
|
|
594
602
|
|
|
595
603
|
// ─── task 헤더 / 상태 전이 (코드화) ─────────────────────────────────
|
|
596
|
-
// 헤더 5컬럼
|
|
604
|
+
// 헤더 파싱(6컬럼 · 구 5컬럼 폴백) · task 문서 위치 판정 · 7상태 유효전이 검증을 코드로 단일화.
|
|
597
605
|
// 계약: TASK_DOC_RULE §1.1(헤더) · §1.2(7상태) · §1.5(문서 위치).
|
|
598
606
|
|
|
599
607
|
// 7×7 유효전이표 (TASK_DOC_RULE §1.2 + FAIL/UNCERTAIN 분기). from → 허용 to 목록.
|
|
@@ -639,7 +647,10 @@ function _locateHeaderRow(content) {
|
|
|
639
647
|
}
|
|
640
648
|
|
|
641
649
|
function parseTaskHeader(taskMdPath) {
|
|
642
|
-
//
|
|
650
|
+
// 헤더를 구조화 → { created, plan, type, size, parent, status }. 표 부재/컬럼 부족 시 throw.
|
|
651
|
+
// 6컬럼(0.6.0+): 생성일·플랜·유형·규모·부모 브랜치·상태. 부모 브랜치는 상태 *앞* — 상태를
|
|
652
|
+
// 항상 마지막 셀로 유지해 setStatus의 `length-2` 로직이 구·신 양쪽에서 성립하게 함.
|
|
653
|
+
// 5컬럼(구): 부모 칸 부재 → parent='dev' 폴백(0.6.0 이전 문서 하위호환).
|
|
643
654
|
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
644
655
|
const loc = _locateHeaderRow(content);
|
|
645
656
|
if (!loc) throw new Error(`헤더 표 파싱 실패 (TASK_DOC_RULE §1.1 형식 아님): ${taskMdPath}`);
|
|
@@ -647,13 +658,17 @@ function parseTaskHeader(taskMdPath) {
|
|
|
647
658
|
if (inner.length < 5) {
|
|
648
659
|
throw new Error(`헤더 표 컬럼 부족(${inner.length}/5): ${taskMdPath}`);
|
|
649
660
|
}
|
|
661
|
+
if (inner.length >= 6) {
|
|
662
|
+
const [created, plan, type, size, parent, status] = inner;
|
|
663
|
+
return { created, plan, type, size, parent, status };
|
|
664
|
+
}
|
|
650
665
|
const [created, plan, type, size, status] = inner;
|
|
651
|
-
return { created, plan, type, size, status };
|
|
666
|
+
return { created, plan, type, size, parent: 'dev', status };
|
|
652
667
|
}
|
|
653
668
|
|
|
654
669
|
function isProjectRegistered(mainWtPath) {
|
|
655
670
|
// .gitignore에 .project/ 등록 여부 — check-ignore exit 0 = 등록(퍼블릭 default, 문서는 메인WT 공유),
|
|
656
|
-
// exit 1 = 미등록(문서는 워크트리 안, 머지로
|
|
671
|
+
// exit 1 = 미등록(문서는 워크트리 안, 머지로 부모 반영). task-init Step7 분기와 동일 판정.
|
|
657
672
|
try {
|
|
658
673
|
execFileSync(
|
|
659
674
|
'git',
|
|
@@ -719,7 +734,8 @@ async function setStatus(taskMdPath, next) {
|
|
|
719
734
|
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
720
735
|
const loc = _locateHeaderRow(content);
|
|
721
736
|
if (!loc) throw new Error(`헤더 표 파싱 실패: ${taskMdPath}`);
|
|
722
|
-
const
|
|
737
|
+
const trimmed = loc.cells.slice(1, -1).map((c) => c.trim());
|
|
738
|
+
const cur = trimmed[trimmed.length - 1]; // 상태 = 항상 마지막 셀 (6컬럼: 부모 브랜치가 그 앞)
|
|
723
739
|
if (cur === next) return { changed: false, from: cur, to: next, path: taskMdPath };
|
|
724
740
|
const allowed = TRANSITIONS[cur] || [];
|
|
725
741
|
if (!allowed.includes(next)) {
|
|
@@ -767,14 +783,14 @@ function _buildTaskDocPath(base, plan, { taskNum, slug, promoted }) {
|
|
|
767
783
|
return { dir: planDir, file: path.join(planDir, `${nnn}_${slug}.md`) };
|
|
768
784
|
}
|
|
769
785
|
|
|
770
|
-
function _renderTaskSkeleton({ taskNum, title, created, plan, docType, size }) {
|
|
786
|
+
function _renderTaskSkeleton({ taskNum, title, created, plan, docType, size, parent }) {
|
|
771
787
|
const nnn = String(taskNum).padStart(3, '0');
|
|
772
788
|
return [
|
|
773
789
|
`# TASK-${nnn} — ${title}`,
|
|
774
790
|
'',
|
|
775
|
-
'| 생성일 | 플랜 | 유형 | 규모 | 상태 |',
|
|
776
|
-
'
|
|
777
|
-
`| ${created} | ${plan} | ${docType} | ${size} | draft |`,
|
|
791
|
+
'| 생성일 | 플랜 | 유형 | 규모 | 부모 브랜치 | 상태 |',
|
|
792
|
+
'|--------|------|------|------|-------------|------|',
|
|
793
|
+
`| ${created} | ${plan} | ${docType} | ${size} | ${parent} | draft |`,
|
|
778
794
|
'',
|
|
779
795
|
'## Requirements',
|
|
780
796
|
'',
|
|
@@ -800,14 +816,16 @@ function _renderTaskSkeleton({ taskNum, title, created, plan, docType, size }) {
|
|
|
800
816
|
}
|
|
801
817
|
|
|
802
818
|
function scaffoldTaskDoc(mainWtPath, wtPath, meta) {
|
|
803
|
-
// meta: { taskNum, slug, title, type(브랜치 타입), size, plan?, promoted?, created? }
|
|
819
|
+
// meta: { taskNum, slug, title, type(브랜치 타입), size, parent, plan?, promoted?, created? }
|
|
804
820
|
// 위치: 등록(.gitignore) 케이스 → 메인WT, 미등록 → 워크트리. plan 미지정 시 AGENT-GUIDE에서 검출.
|
|
821
|
+
// parent 미지정 시 현재 브랜치 캡처(단독 호출 안전망 — 통상 fork가 넘김).
|
|
805
822
|
// taskNum이 고유(fork init 락 채번)라 동일 문서 동시 쓰기 경합 없음 → 락 불요.
|
|
806
823
|
const registered = isProjectRegistered(mainWtPath);
|
|
807
824
|
const base = registered ? mainWtPath : wtPath;
|
|
808
825
|
const plan = meta.plan || getActiveVersion(mainWtPath);
|
|
809
826
|
const docType = DOC_TYPE_OF[meta.type] || meta.type;
|
|
810
827
|
const created = meta.created || _todayLocal();
|
|
828
|
+
const parent = meta.parent || currentBranch(mainWtPath);
|
|
811
829
|
const { dir, file } = _buildTaskDocPath(base, plan, {
|
|
812
830
|
taskNum: meta.taskNum,
|
|
813
831
|
slug: meta.slug,
|
|
@@ -826,6 +844,7 @@ function scaffoldTaskDoc(mainWtPath, wtPath, meta) {
|
|
|
826
844
|
plan,
|
|
827
845
|
docType,
|
|
828
846
|
size: meta.size,
|
|
847
|
+
parent,
|
|
829
848
|
}),
|
|
830
849
|
);
|
|
831
850
|
return { registered, promoted: !!meta.promoted, plan, file };
|
|
@@ -934,7 +953,7 @@ function initPlan(mainWtPath, slug, opts = {}) {
|
|
|
934
953
|
|
|
935
954
|
// ─── task close 결정적 준비 (코드화, D1·D2) ─────────────────────────
|
|
936
955
|
// closeTask는 *결정적 준비만* 수행: 게이트(status=tested) → Phase 커밋(D2) → status=closed →
|
|
937
|
-
// flows/문서 커밋 → 추적마커. 비가역 3종(
|
|
956
|
+
// flows/문서 커밋 → 추적마커. 비가역 3종(부모 브랜치 --no-ff 머지 · worktree remove · branch -d)과
|
|
938
957
|
// 충돌 해결·머지 락은 스킬(LLM)이 수행 — 위험 최소화 + 충돌은 LLM 판단 영역.
|
|
939
958
|
|
|
940
959
|
// 브랜치 타입 → 커밋 태그 (GIT_RULE 6-3).
|
|
@@ -991,10 +1010,26 @@ function _porcelainFiles(wtPath) {
|
|
|
991
1010
|
.filter(Boolean);
|
|
992
1011
|
}
|
|
993
1012
|
|
|
1013
|
+
// 빌드 산출물 판정 — 소스 변경에 도구가 따라 바꾸는 파생 파일 (Phase 소속을 묻는 것 자체가 무의미).
|
|
1014
|
+
// Dev Plan에 명시 안 돼도 미매핑 blocked 대신 마지막 Phase에 자동 편입 (stash·recordion FRICTION: pbxproj 미매핑만 6회).
|
|
1015
|
+
const BUILD_ARTIFACT_RE = [
|
|
1016
|
+
/\.pbxproj$/,
|
|
1017
|
+
/(^|\/)package-lock\.json$/,
|
|
1018
|
+
/(^|\/)yarn\.lock$/,
|
|
1019
|
+
/(^|\/)pnpm-lock\.yaml$/,
|
|
1020
|
+
/(^|\/)Cargo\.lock$/,
|
|
1021
|
+
/(^|\/)Gemfile\.lock$/,
|
|
1022
|
+
/(^|\/)Podfile\.lock$/,
|
|
1023
|
+
];
|
|
1024
|
+
|
|
994
1025
|
function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
995
1026
|
// 코드 변경분(.project/ 외)을 Dev Plan Phase에 매핑해 Phase별 자동 커밋. meta: { taskNum, type }.
|
|
996
|
-
//
|
|
997
|
-
//
|
|
1027
|
+
// 매핑 폴백 3규칙 (stash 6회 + recordion 5회 blocked 마찰 반영 — 11회 전부 기계적 편입이었음):
|
|
1028
|
+
// 1. 다중 Phase 매핑 → 가장 이른 Phase에 편입 + 커밋 메시지에 '(Phase a·b 변경 포함)' 표기.
|
|
1029
|
+
// 뒤 Phase가 앞 Phase 타입·함수를 참조하므로 이른 쪽이 의존 방향과 일치. 중간 커밋 독립 빌드는 비보장(GIT_RULE 명문).
|
|
1030
|
+
// 2. 미매핑 + 빌드 산출물(BUILD_ARTIFACT_RE) → 매핑된 그룹 중 마지막 Phase(없으면 Dev Plan 마지막)에
|
|
1031
|
+
// 자동 편입 + '(빌드 산출물 자동 편입)' 표기.
|
|
1032
|
+
// 3. 미매핑 + 일반 파일 → { ambiguous } 반환(커밋 X) — 계획 누락 가능성이라 사람 판단으로 유지.
|
|
998
1033
|
// .project/ 메타(flows/문서/changelog)는 제외 — closeTask가 별도 커밋.
|
|
999
1034
|
const codeFiles = _porcelainFiles(wtPath).filter((f) => !f.startsWith('.project/'));
|
|
1000
1035
|
if (codeFiles.length === 0) return { committed: [], empty: true };
|
|
@@ -1013,21 +1048,44 @@ function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
|
1013
1048
|
)
|
|
1014
1049
|
.map(({ i }) => i);
|
|
1015
1050
|
const fileToPhase = {};
|
|
1051
|
+
const multiPhase = []; // 규칙 1 적용분 — { file, phases:[num...], assignedTo }
|
|
1052
|
+
const artifactFiles = []; // 규칙 2 후보 — 매핑 확정 후 편입 Phase 결정
|
|
1016
1053
|
for (const f of codeFiles) {
|
|
1017
1054
|
const m = matchOf(f);
|
|
1018
|
-
if (m.length
|
|
1055
|
+
if (m.length === 1) {
|
|
1056
|
+
fileToPhase[f] = m[0];
|
|
1057
|
+
} else if (m.length > 1) {
|
|
1058
|
+
const earliest = Math.min(...m);
|
|
1059
|
+
fileToPhase[f] = earliest;
|
|
1060
|
+
multiPhase.push({ file: f, phases: m.map((i) => phases[i].num), assignedTo: phases[earliest].num });
|
|
1061
|
+
} else if (BUILD_ARTIFACT_RE.some((re) => re.test(f))) {
|
|
1062
|
+
artifactFiles.push(f);
|
|
1063
|
+
} else {
|
|
1019
1064
|
return {
|
|
1020
1065
|
ambiguous: true,
|
|
1021
|
-
reason:
|
|
1066
|
+
reason: `Phase 미매핑: ${f}`,
|
|
1022
1067
|
files: codeFiles,
|
|
1023
1068
|
phases: phases.map((p) => ({ num: p.num, name: p.name, files: p.files })),
|
|
1024
1069
|
};
|
|
1025
1070
|
}
|
|
1026
|
-
fileToPhase[f] = m[0];
|
|
1027
1071
|
}
|
|
1072
|
+
// 규칙 2 — 산출물 편입: 매핑된 그룹 중 마지막 Phase, 하나도 없으면 Dev Plan 마지막 Phase
|
|
1073
|
+
const autoAssigned = [];
|
|
1074
|
+
if (artifactFiles.length > 0) {
|
|
1075
|
+
const mappedIdx = Object.values(fileToPhase);
|
|
1076
|
+
const lastIdx = mappedIdx.length > 0 ? Math.max(...mappedIdx) : phases.length - 1;
|
|
1077
|
+
for (const f of artifactFiles) {
|
|
1078
|
+
fileToPhase[f] = lastIdx;
|
|
1079
|
+
autoAssigned.push({ file: f, assignedTo: phases[lastIdx].num });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
// 파일별 커밋 메시지 표기 (자동 편입의 근거를 커밋에 남김 — 코드가 몰래 정한 걸 드러낸다)
|
|
1083
|
+
const noteOf = {};
|
|
1084
|
+
for (const x of multiPhase) noteOf[x.file] = ` (Phase ${x.phases.join('·')} 변경 포함)`;
|
|
1085
|
+
for (const x of autoAssigned) noteOf[x.file] = ' (빌드 산출물 자동 편입)';
|
|
1028
1086
|
// Phase별 그룹 → 순서대로 커밋
|
|
1029
1087
|
const groups = new Map();
|
|
1030
|
-
for (const f of
|
|
1088
|
+
for (const f of Object.keys(fileToPhase)) {
|
|
1031
1089
|
const i = fileToPhase[f];
|
|
1032
1090
|
if (!groups.has(i)) groups.set(i, []);
|
|
1033
1091
|
groups.get(i).push(f);
|
|
@@ -1041,28 +1099,36 @@ function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
|
1041
1099
|
execFileSync('git', ['-C', wtPath, 'add', '--', ...grp], { stdio: 'pipe' });
|
|
1042
1100
|
const msg =
|
|
1043
1101
|
`${tag}: [TASK-${nnn}] Phase ${p.num} - ${p.name}\n\n` +
|
|
1044
|
-
grp.map((f) => `- ${f}`).join('\n') +
|
|
1102
|
+
grp.map((f) => `- ${f}${noteOf[f] || ''}`).join('\n') +
|
|
1045
1103
|
`\n- 사유: ${p.name}`;
|
|
1046
1104
|
execFileSync('git', ['-C', wtPath, 'commit', '-m', msg], { stdio: 'pipe' });
|
|
1047
1105
|
committed.push({ phase: p.num, name: p.name, files: grp });
|
|
1048
1106
|
}
|
|
1049
|
-
|
|
1107
|
+
const result = { committed };
|
|
1108
|
+
if (multiPhase.length > 0) result.multiPhase = multiPhase;
|
|
1109
|
+
if (autoAssigned.length > 0) result.autoAssigned = autoAssigned;
|
|
1110
|
+
return result;
|
|
1050
1111
|
}
|
|
1051
1112
|
|
|
1052
1113
|
async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
1053
1114
|
// 결정적 준비만 — 비가역(머지·정리)·충돌 해결은 스킬이 후속 수행.
|
|
1054
|
-
// 반환: { blocked } (게이트 미충족/매핑 모호) 또는
|
|
1115
|
+
// 반환: { blocked } (게이트 미충족/매핑 모호) 또는
|
|
1116
|
+
// { prepped, branch, parent, wtPath, registered, commits, aheadOfParent,
|
|
1117
|
+
// multiPhase?, autoAssigned? (매핑 폴백 적용 시 내역) }. parent = 스킬 머지 대상.
|
|
1055
1118
|
const active = getActiveTasks(mainWtPath).find((t) => t.taskNum === taskNum);
|
|
1056
1119
|
if (!active) {
|
|
1057
1120
|
throw new Error(`TASK-${String(taskNum).padStart(3, '0')} 진행중 브랜치 없음 (SSoT 조회).`);
|
|
1058
1121
|
}
|
|
1059
|
-
assertMainWorktreeOnDev(mainWtPath);
|
|
1060
1122
|
const projectId = getProjectId(mainWtPath);
|
|
1061
1123
|
const wtPath = getWorktreePath(projectId, { taskNum, src: active.src, slug: active.slug });
|
|
1062
1124
|
const resolved = resolveTaskDocPath(mainWtPath, wtPath, { taskNum });
|
|
1063
1125
|
if (!resolved) throw new Error(`TASK-${taskNum} 문서 못 찾음.`);
|
|
1064
1126
|
|
|
1065
1127
|
const header = parseTaskHeader(resolved.file);
|
|
1128
|
+
// 부모 = 헤더 기록값(0.6.0, 구 문서는 'dev' 폴백). 메인 워크트리가 그 부모에 서 있어야
|
|
1129
|
+
// 스킬 후속 머지가 올바른 브랜치로 간다.
|
|
1130
|
+
const parent = header.parent;
|
|
1131
|
+
assertMainWorktreeOn(mainWtPath, parent);
|
|
1066
1132
|
if (header.status !== 'tested') {
|
|
1067
1133
|
return { blocked: 'status', current: header.status, docPath: resolved.file };
|
|
1068
1134
|
}
|
|
@@ -1114,9 +1180,9 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1114
1180
|
}
|
|
1115
1181
|
}
|
|
1116
1182
|
|
|
1117
|
-
// 추적 마커 빈커밋 (
|
|
1183
|
+
// 추적 마커 빈커밋 (부모보다 앞선 커밋 0개 — docs/분석 task + .project gitignore 케이스)
|
|
1118
1184
|
const ahead = parseInt(
|
|
1119
|
-
gitCapture(wtPath, ['rev-list', '--count',
|
|
1185
|
+
gitCapture(wtPath, ['rev-list', '--count', `${parent}..HEAD`], { allowFail: true }) || '0',
|
|
1120
1186
|
10,
|
|
1121
1187
|
);
|
|
1122
1188
|
if (ahead === 0) {
|
|
@@ -1136,19 +1202,24 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1136
1202
|
}
|
|
1137
1203
|
|
|
1138
1204
|
const aheadFinal = parseInt(
|
|
1139
|
-
gitCapture(wtPath, ['rev-list', '--count',
|
|
1205
|
+
gitCapture(wtPath, ['rev-list', '--count', `${parent}..HEAD`], { allowFail: true }) || '0',
|
|
1140
1206
|
10,
|
|
1141
1207
|
);
|
|
1142
|
-
|
|
1208
|
+
const prepResult = {
|
|
1143
1209
|
prepped: true,
|
|
1144
1210
|
taskNum,
|
|
1145
1211
|
branch: active.branch,
|
|
1212
|
+
parent,
|
|
1146
1213
|
wtPath,
|
|
1147
1214
|
registered: resolved.registered,
|
|
1148
1215
|
docPath: resolved.file,
|
|
1149
1216
|
commits,
|
|
1150
|
-
|
|
1217
|
+
aheadOfParent: aheadFinal,
|
|
1151
1218
|
};
|
|
1219
|
+
// 매핑 폴백 적용 내역 노출 — 코드가 자동 편입한 걸 스킬·사용자가 볼 수 있게
|
|
1220
|
+
if (split.multiPhase) prepResult.multiPhase = split.multiPhase;
|
|
1221
|
+
if (split.autoAssigned) prepResult.autoAssigned = split.autoAssigned;
|
|
1222
|
+
return prepResult;
|
|
1152
1223
|
}
|
|
1153
1224
|
|
|
1154
1225
|
module.exports = {
|
|
@@ -1187,8 +1258,8 @@ module.exports = {
|
|
|
1187
1258
|
getInitLockPath,
|
|
1188
1259
|
withMergeLock,
|
|
1189
1260
|
withMetaLock,
|
|
1190
|
-
|
|
1191
|
-
|
|
1261
|
+
currentBranch,
|
|
1262
|
+
assertMainWorktreeOn,
|
|
1192
1263
|
parseBranchName,
|
|
1193
1264
|
getActiveTasks,
|
|
1194
1265
|
getNextTaskNumber,
|
package/bin/mcp.js
CHANGED
|
@@ -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와 함께 주면 골격 생성)'),
|
|
@@ -213,6 +213,7 @@ server.registerTool(
|
|
|
213
213
|
title: a.title,
|
|
214
214
|
type: a.type,
|
|
215
215
|
size: a.size,
|
|
216
|
+
parent: result.parent,
|
|
216
217
|
promoted: !!a.promote,
|
|
217
218
|
});
|
|
218
219
|
out = { ...result, docPath: doc.file, registered: doc.registered, promoted: doc.promoted };
|
|
@@ -228,7 +229,7 @@ server.registerTool(
|
|
|
228
229
|
'task_close',
|
|
229
230
|
{
|
|
230
231
|
description:
|
|
231
|
-
'close 결정적 준비만 — Phase 커밋·status=closed·문서 커밋·추적마커. 게이트(status=tested)·매핑 모호 시 blocked 반환. 비가역(
|
|
232
|
+
'close 결정적 준비만 — Phase 커밋·status=closed·문서 커밋·추적마커. 게이트(status=tested)·매핑 모호 시 blocked 반환. 반환 parent = 스킬 머지 대상 부모 브랜치. 비가역(부모 --no-ff 머지·worktree/branch 정리·충돌 해결)은 스킬(task-close)이 후속 수행.',
|
|
232
233
|
inputSchema: { task: z.union([z.string(), z.number()]).describe('TASK-NNN 또는 번호') },
|
|
233
234
|
},
|
|
234
235
|
handler(async (a) => {
|
|
@@ -245,7 +246,7 @@ server.registerTool(
|
|
|
245
246
|
`Phase↔파일 매핑 모호 (${result.reason}) — Dev Plan '파일' 필드 확인 후 수동 Phase 커밋 → task_close 재호출.`,
|
|
246
247
|
);
|
|
247
248
|
}
|
|
248
|
-
return ok(result, `prep
|
|
249
|
+
return ok(result, `prep 완료 (부모=${result.parent}). 비가역(사전 rebase·충돌해결·머지 락·부모 --no-ff 머지·정리)은 task-close 스킬이 후속 수행.`);
|
|
249
250
|
}),
|
|
250
251
|
);
|
|
251
252
|
|
package/bin/status.js
CHANGED
|
@@ -56,11 +56,14 @@ function main() {
|
|
|
56
56
|
const lockTimeoutMs = manifest.lock_timeout_ms ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
57
57
|
|
|
58
58
|
const currentBranch = gitCapture(mainWt, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
59
|
-
const
|
|
59
|
+
const detached = currentBranch === 'HEAD';
|
|
60
60
|
|
|
61
61
|
console.log(`\ntaskery status\n`);
|
|
62
62
|
console.log(`메인 워크트리: ${mainWt}`);
|
|
63
|
-
|
|
63
|
+
// 현재 브랜치 = 새 태스크의 부모 브랜치(0.6.0). detached HEAD면 fork 불가.
|
|
64
|
+
console.log(
|
|
65
|
+
`현재 브랜치(= 새 태스크 부모): ${currentBranch}${detached ? ' ⚠ (detached HEAD — 브랜치 체크아웃 필요)' : ''}`,
|
|
66
|
+
);
|
|
64
67
|
console.log(`projectId: ${projectId}`);
|
|
65
68
|
console.log(`stale_days: ${staleDays} / lock_timeout_ms: ${lockTimeoutMs}\n`);
|
|
66
69
|
|
|
@@ -126,7 +129,7 @@ function main() {
|
|
|
126
129
|
);
|
|
127
130
|
const orphans = folders.filter((f) => !activeFolders.has(f));
|
|
128
131
|
if (orphans.length > 0) {
|
|
129
|
-
console.log(`⚠ SSoT에 없는 워크트리 폴더 ${orphans.length}건 (이미
|
|
132
|
+
console.log(`⚠ SSoT에 없는 워크트리 폴더 ${orphans.length}건 (이미 부모 머지된 잔존?):`);
|
|
130
133
|
for (const o of orphans) {
|
|
131
134
|
console.log(` ${path.join(worktreesRoot, o)}`);
|
|
132
135
|
}
|
package/package.json
CHANGED
|
@@ -48,7 +48,9 @@ if [ ! -f "$TASK_MD" ]; then
|
|
|
48
48
|
exit 0
|
|
49
49
|
fi
|
|
50
50
|
|
|
51
|
-
# 헤더 status 추출
|
|
51
|
+
# 헤더 status 추출 — status는 헤더 표의 *마지막* 셀.
|
|
52
|
+
# 5컬럼(구): 생성일 / 플랜 / 유형 / 규모 / 상태
|
|
53
|
+
# 6컬럼(0.6.0+): 생성일 / 플랜 / 유형 / 규모 / 부모 브랜치 / 상태
|
|
52
54
|
# python3 정규식 사용 (mac BSD grep -P 미지원)
|
|
53
55
|
STATUS=$(TM="$TASK_MD" python3 -c "
|
|
54
56
|
import os, re
|
|
@@ -59,8 +61,8 @@ except Exception:
|
|
|
59
61
|
raise SystemExit(0)
|
|
60
62
|
# 7-state whitelist
|
|
61
63
|
WL = r'(draft|planned|developing|developed|testing|tested|closed)'
|
|
62
|
-
#
|
|
63
|
-
m = re.search(rf'^\|[^|]*\|
|
|
64
|
+
# status = 마지막 셀. 선행 셀 4개(구 5컬럼) 또는 5개(신 6컬럼) 뒤에 온다.
|
|
65
|
+
m = re.search(rf'^\|(?:[^|]*\|){{4,5}}\s*{WL}\s*\|\s*\$', content, re.MULTILINE)
|
|
64
66
|
print(m.group(1) if m else '')
|
|
65
67
|
" 2>/dev/null)
|
|
66
68
|
|
|
@@ -24,9 +24,12 @@ developer_instructions = """
|
|
|
24
24
|
- task.md의 ## Test Plan 시나리오를 순서대로 그대로 수행한다(임의 추가/스킵/변형 금지).
|
|
25
25
|
- 문 앞 검사(먼저) — 각 [AUTO] 시나리오가 [실행 명령/입력 + 구체적·관측 가능한 기대값(출력값/상태코드/어서션/조회 결과/화면 상태)] 한 쌍을 갖췄는지 확인한다. "정상 동작/잘 됨/확인한다" 같은 소원은 기대값이 아니다. 미달인 [AUTO]는 검사하지 말고 "시험문제 결함(검증 불가)"으로 분류 + 무엇이 빠졌는지 지목한다(PASS 반올림 금지).
|
|
26
26
|
- 증거 일치 시만 PASS — 자격 갖춘 [AUTO]는 실제로 실행해 *관측 결과가 기대값과 일치할 때만* PASS. 코드 정독 "그럴듯함"·추정으로 PASS 금지. 증거(명령 출력/응답 로그/캡처/조회 결과)를 첨부한다.
|
|
27
|
-
- [AUTO]/[USER] 분류 — [USER]는 정답지가 *주관*인 것만(시각 미세 취향 / 색조·2px / UX 느낌) → UNCERTAIN(사람 검수) + 증거 캡처로 리턴. 시각 *객관* 깨짐(요소 빠짐/빈 화면/배치 딴판/색 완전히 틀림)은 [USER]가 아니라 [AUTO] — 실제 화면 캡처 → 승인 목업과 대조해 네가 직접 판정한다(어긋나면 *구체적 어긋남 목록* + FAIL). 시각이라고 무조건 사람에게 넘기지 마라.
|
|
28
|
-
- UNCERTAIN
|
|
27
|
+
- [AUTO]/[USER] 분류 — [USER]는 정답지가 *주관*인 것만(시각 미세 취향 / 색조·2px / UX 느낌) → UNCERTAIN(사람 검수) + 증거 캡처로 리턴. 시각 *객관* 깨짐(요소 빠짐/빈 화면/배치 딴판/색 완전히 틀림)은 [USER]가 아니라 [AUTO] — 실제 화면 캡처 → 승인 목업과 대조해 네가 직접 판정한다(어긋나면 *구체적 어긋남 목록* + FAIL). 시각이라고 무조건 사람에게 넘기지 마라. 단 시각 객관의 [AUTO] 승격은 TEST-GUIDE의 *시각 실행* 절에 캡처 실행 경로가 있을 때 한정이다 — 경로가 없어 task.md가 [USER]로 분류했으면 그 분류를 존중한다.
|
|
28
|
+
- UNCERTAIN 세 종류 — (사람 검수)=정답지가 주관인 영역 / (검증 불가)=[AUTO]인데 대조할 기대값을 구성 못 함=시험문제 결함 / (실행 보류)=화면·입력 점유 제약으로 실행하지 않음=환경 제약(사용자 승인 또는 격리 실행 경로 필요. 무엇을 왜 실행하지 않았는지 기록해 리턴). 검증 불가는 PASS로 반올림하거나 사람 검수로 떠넘기지 말고, *무엇을 시도했고 왜 기대값을 구성할 수 없는지* 근거를 반드시 기록한다(task-plan으로 Test Plan을 보수해야 하는 신호).
|
|
29
29
|
- 신규 테스트 추가 시 카운트 +N / 모든 PASS만으로 단정 X — 신규 *테스트 식별자*(suite/class/function 등)가 runner 로그에 실제 등장했는지 grep으로 확인한다(1 hit 이상, 없으면 Suite 미실행 → FAIL). grep/Read 존재 확인은 보조용 — 실동작 PASS를 대체할 수 없다.
|
|
30
|
+
- 픽스처 현실성 — 픽스처·시드가 실제 산출물과 형식·정밀도가 같은지 의심하고, 의심되면 실제 코드(writer·생성기)로 재생성해 재실행한 결과로 판정한다. 재생성 경로가 없어 대조를 구성할 수 없으면 시험문제 결함(검증 불가)으로 분류한다.
|
|
31
|
+
- 어서션 정면성 — 기대값이 요구사항을 정면으로 겨누는지 확인한다("벗어날 수 없다"면 실제로 벗어나 볼 것. 버튼 disabled 확인은 우회 지표다). 우회 지표로 검사하는 [AUTO] 시나리오는 시험문제 결함으로 분류하고 무엇이 정면 관측인지 지목한다.
|
|
32
|
+
- 화면·입력 점유 금지 — 실제 앱을 띄워 UI를 자동 조작하는 스위트는 사용자의 화면·입력을 점유한다. TEST-GUIDE의 *테스트 실행 환경* 절에 격리 실행 경로(창 숨김·헤드리스 / 컨테이너 / VM / CI)가 있으면 그 경로로 실행하고, 없으면 비점유 부분만 실행하거나 실행 보류(사용자 승인 필요)로 표시해 리턴한다. 헤드리스 모드가 있으면 항상 그쪽을 쓴다.
|
|
30
33
|
- 코드와 동작만 신뢰한다. 코드 파일 수정 금지. 임시 파일(테스트 스크립트 등)은 종료 후 삭제한다.
|
|
31
|
-
- 검증을 마치면 넘겨받은 검사 지시의 결과 형식(종합 판정 PASS/FAIL/UNCERTAIN / 시나리오별 결과 / 검증·테스트 명령 결과 / UNCERTAIN
|
|
34
|
+
- 검증을 마치면 넘겨받은 검사 지시의 결과 형식(종합 판정 PASS/FAIL/UNCERTAIN / 시나리오별 결과 / 검증·테스트 명령 결과 / UNCERTAIN 3종 / 시험문제 결함 / 실행 보류)을 그대로 리턴한다.
|
|
32
35
|
"""
|