@angar2/taskery 0.7.0 → 0.8.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/bin/close.js +13 -1
- package/bin/legacy-quotes.js +60 -0
- package/bin/lib.js +159 -7
- package/bin/migrate.js +261 -86
- package/bin/update.js +13 -7
- package/package.json +1 -1
- package/template/.codex/agents/task-tester.toml +10 -4
- package/template/.codex/agents/test-plan-author.toml +34 -0
- package/template/.project/rules/CHANGELOG_RULE.md +26 -20
- package/template/.project/rules/GIT_RULE.md +2 -0
- package/template/.project/rules/TASKERY_RULE.md +4 -4
- package/template/.project/rules/TASK_DOC_RULE.md +11 -1
- package/template/shared/skills/project-init/SKILL.md +1 -0
- package/template/shared/skills/task-close/SKILL.md +11 -8
- package/template/shared/skills/task-plan/SKILL.md +121 -6
- package/template/shared/skills/task-test/SKILL.md +22 -3
package/bin/close.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* 코드가 수행하고 멈춘다. 이후 비가역(사전 rebase+충돌해결 / 머지 락 / 부모 --no-ff 머지 /
|
|
9
9
|
* worktree remove / branch -d)은 스킬(task-close)이 git-guard hook + 복구출력 보호 하에 수행.
|
|
10
10
|
*
|
|
11
|
-
* stdout: { prepped, branch, parent, wtPath, registered, commits, aheadOfParent }. parent = 머지 대상. exit 0.
|
|
11
|
+
* stdout: { prepped, branch, parent, wtPath, registered, commits, aheadOfParent, changelog }. 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↔파일 매핑 모호 (수동 커밋 필요)
|
|
15
|
+
* - exit 4 + { blocked:'docs', missing } — PLAN 체크·수정이력 누락 (문서 채운 후 그대로 재호출)
|
|
15
16
|
* 그 외 실패 시 stderr + exit 1.
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -60,10 +61,21 @@ async function main() {
|
|
|
60
61
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
61
62
|
process.exit(3);
|
|
62
63
|
}
|
|
64
|
+
if (result.blocked === 'docs') {
|
|
65
|
+
console.error(`문서 게이트 차단 — 아래 문서에 TASK 표기 누락. 채운 후 그대로 재호출(변이 없음).`);
|
|
66
|
+
for (const miss of result.missing) {
|
|
67
|
+
console.error(` - [${miss.kind === 'plan' ? 'PLAN 체크리스트' : '수정 이력'}] ${miss.file}`);
|
|
68
|
+
}
|
|
69
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
70
|
+
process.exit(4);
|
|
71
|
+
}
|
|
63
72
|
if (result.blocked === 'mapping') {
|
|
64
73
|
console.error(
|
|
65
74
|
`Phase↔파일 매핑 모호 (${result.reason}) — Dev Plan '파일' 필드 확인 후 수동 Phase 커밋 필요.`,
|
|
66
75
|
);
|
|
76
|
+
console.error(
|
|
77
|
+
`미매핑 파일이 Dev Plan에 적혀 있다면 '- 파일:' 표기 형식(쉼표/가운뎃점/들여쓴 목록)을 확인 — 줄 전체가 경로 하나로 읽혔을 수 있습니다.`,
|
|
78
|
+
);
|
|
67
79
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
68
80
|
process.exit(2);
|
|
69
81
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 과거 taskery 진입 문서 템플릿이 쓰던 인용문 목록 — 재조립 시 리포 소유 문장과 구별하는 기준
|
|
2
|
+
/**
|
|
3
|
+
* bin/legacy-quotes.js
|
|
4
|
+
* v0.1.0~v0.8.0의 `template/CLAUDE.md`·`template/AGENTS.md`에 실렸던 `>` 인용문 전량(유니크 42줄).
|
|
5
|
+
*
|
|
6
|
+
* 쓰임: AGENTS.md 재조립(migrate.composeAgentsDoc)에서 리포 문서의 인용문을 판정한다.
|
|
7
|
+
* - 이 목록 또는 *현재 템플릿*에 있는 문장 → taskery가 쓴 것 → 버리고 새 문안으로 교체
|
|
8
|
+
* - 어느 쪽에도 없는 문장 → 리포가 쓴 것 → 그대로 보존
|
|
9
|
+
* 판정 실패는 **보존 쪽으로 실패한다** — 목록에 없으면 남긴다(중복은 눈에 띄지만 유실은 복구 불가).
|
|
10
|
+
*
|
|
11
|
+
* 생성 방법(재현): 각 릴리스 태그에서 두 템플릿의 `^> ` 줄을 뽑아 정렬·유니크.
|
|
12
|
+
* 새 버전의 문안은 목록에 추가할 필요가 없다 — 재조립이 현재 템플릿에서 실시간으로 읽는다.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const LEGACY_TEMPLATE_QUOTES = [
|
|
16
|
+
'> **검증 방법 → `.project/TEST-GUIDE.md`** — 각 테스트 방식(데이터 조회 / API 호출 / E2E / 시각 실행)을 *이 프로젝트에서 실제로 어떻게 돌리나*는 거기 단일 소스. `/task-plan`이 채우고 `/task-test` 격리 세션이 읽는다.',
|
|
17
|
+
'> **본 문서에는 규칙·사용법을 추가하지 않는다** — 규칙은 `.project/rules/`, 이 리포의 커스텀은 `*.local.md`가 담당한다. 본 문서에서 갱신이 허용되는 것은 아래 리포 값 칸(프로젝트 메타 · 각 명령)뿐이다.',
|
|
18
|
+
'> *코드 상태 검증* (빌드 / 린트 / 타입체크). 테스트 실행은 본 섹션 X — `## 테스트 명령` 참조.',
|
|
19
|
+
'> *테스트 실행* (단위 / 통합 / E2E 등). 코드 상태 검증은 `## 검증 명령` 참조.',
|
|
20
|
+
'> - Hook: `.codex/config.toml`의 `[[hooks.PreToolUse]]`로 등록 — 최초 1회 `/hooks` trust 승인 필요',
|
|
21
|
+
'> - 격리 검증(/task-test): `.codex/agents/task-tester.toml` 서브에이전트로 spawn',
|
|
22
|
+
'> - 스킬 본문: `.agents/skills/<스킬>/SKILL.md` (Claude의 `.claude/skills/`가 아님)',
|
|
23
|
+
'> CLI/라이브러리 등 검수 서버가 없는 프로젝트는 본 섹션 전체 삭제.',
|
|
24
|
+
'> Codex 환경 차이 (Claude와 다른 점만):',
|
|
25
|
+
'> `.codex/config.toml`의 `[[hooks.PreToolUse]]`로 등록. 최초 1회 `/hooks` trust 승인 후 자동 작동.',
|
|
26
|
+
'> `.project/tasks/<NNN_slug>/BACKLOG.md` = *plan(기능 그룹)별 task 후보 누적*. 사용자 발화로 1건씩 얕은 분석(개요 / 대상 영역) 곁들여 추가.',
|
|
27
|
+
'> `.project/tasks/<vX.X>/BACKLOG.md` = *버전별 task 후보 누적*. 사용자 발화로 1건씩 얕은 분석(개요 / 대상 영역) 곁들여 추가.',
|
|
28
|
+
'> `/project-init` 호출 시 위 4 항목 채워짐. 변경 필요하면 직접 Edit.',
|
|
29
|
+
'> `/project-init` 호출 시 위 4 항목 채워짐. 변경 필요하면 직접 편집.',
|
|
30
|
+
'> `/run-team`은 Claude 전용 — agent teams(실험 기능)가 Codex에 없어 Codex 설치에는 미포함.',
|
|
31
|
+
'> `/task-dev` self-check / `/task-close` 최종 게이트가 *이 섹션을 단일 진실 소스로 참조*.',
|
|
32
|
+
'> `/task-dev` self-check / `/task-test` 격리 게이트 / `/task-close` 최종 게이트 / `pre-commit-verify.sh` hook이 *모두 이 섹션을 단일 진실 소스로 참조*.',
|
|
33
|
+
'> `/task-dev` 구현 후 테스트 + `/task-test` 격리 세션이 *이 섹션을 단일 진실 소스로 참조*.',
|
|
34
|
+
'> `/task-dev`·`/task-test`가 검수 시점(세션 중단점)에 이 명령으로 서버를 *백그라운드* 기동하고 접속 URL을 보고한다.',
|
|
35
|
+
'> closed-immutable.sh는 코덱스 파일편집 도구(apply_patch)의 patch envelope에서 `*** Update File:` 경로를 파싱해 검사한다(Claude판은 `file_path` 키를 읽음 — 메커니즘만 다르고 효과 동일).',
|
|
36
|
+
'> taskery는 *plan/ ↔ template/ 자동 빌드 X*. 사용자가 직접 정합 유지.',
|
|
37
|
+
'> 각 명령은 그대로 실행 가능한 형태로 적는다. 해당 없는 항목은 행을 삭제한다.',
|
|
38
|
+
'> 다건 태스크를 *리더 메인 세션 1개가 agent teams로 팀원(독립 세션)에 분배*해 자동 병렬 처리하는 고기능. 위 멀티세션을 사용자가 일일이 띄우는 대신 리더가 띄우고 관리한다. 워크트리 격리·머지 직렬화는 기존 `/task-init`·`/task-close`가 그대로 담당.',
|
|
39
|
+
'> 다건 태스크를 리더가 팀원(독립 세션)에 분배해 자동 병렬 처리하는 고기능(`/run-team`)은 **Claude 전용**이다 — 기반인 agent teams 기능이 Codex에 없다.',
|
|
40
|
+
'> 멀티세션 = 같은 프로젝트에서 *여러 메인 세션이 독립 태스크를 병렬*로 진행. git worktree로 작업 폴더 격리, dev 머지 시 직렬화.',
|
|
41
|
+
'> 멀티세션 = 같은 프로젝트에서 *여러 메인 세션이 독립 태스크를 병렬*로 진행. git worktree로 작업 폴더 격리, 부모 브랜치 머지 시 직렬화.',
|
|
42
|
+
'> 멀티세션 포트 격리: 메인 세션(부모 브랜치) = 기준 포트, task 워크트리 = 기준 포트 + TASK번호. 상세는 GIT_RULE "멀티세션 검수 환경" 섹션.',
|
|
43
|
+
'> 멀티세션 포트 격리: 메인/dev 세션 = 기준 포트, task 워크트리 = 기준 포트 + TASK번호. 상세는 GIT_RULE "멀티세션 검수 환경" 섹션.',
|
|
44
|
+
'> 백틱(`...`) 안 명령 그대로 실행됨. 언어/프레임워크 따라 변경.',
|
|
45
|
+
'> 본 룰은 *모든 스킬 호출 / 사용자 발화 처리 / 작업 진행*에 적용. 위반 시 사용자 마찰 직결.',
|
|
46
|
+
'> 본 문서는 에이전트 최상위 지침이다. 세션 시작 시 자동 로드된다.',
|
|
47
|
+
'> 사람용 도메인 설명은 `.project/PROJECT.md`, 진입 가이드는 `.project/AGENT-GUIDE.md` 참조.',
|
|
48
|
+
'> 앱을 띄워 사용자가 직접 확인해야 하는 프로젝트에만 해당한다. 아니면 이 절 전체를 삭제한다.',
|
|
49
|
+
'> 위 4 항목 중 *프로젝트에 해당 없는 것*은 행 자체 삭제. 빈 백틱(\\`\\`)은 사용 금지.',
|
|
50
|
+
'> 위 항목 중 *프로젝트에 해당 없는 것*은 행 자체 삭제. 빈 백틱(\\`\\`)은 사용 금지.',
|
|
51
|
+
'> 위 항목 중 *프로젝트에 해당 없는 것*은 행 자체 삭제. 빈 백틱(``)은 사용 금지.',
|
|
52
|
+
'> 이 파일은 Codex 세션이 *매 세션 시작 시 자동 로드*하는 단일 진입점이다.',
|
|
53
|
+
'> 이 파일은 메인 세션이 *매 세션 시작 시 자동 로드*하는 단일 진입점이다.',
|
|
54
|
+
'> 이전 `pre-commit-verify.sh` hook은 폐기 — task-close Step 2 게이트 + git-guard 안전망으로 충분 (redundant 검증 사이클 제거). 폐기 사유는 [plan/HOOKS.md](../plan/HOOKS.md) §5 참조.',
|
|
55
|
+
'> 프로젝트 메타 + 검증 명령 + 룰 참조 + 스킬 8종 + 동기화 룰만 포함한다.',
|
|
56
|
+
'> 프로젝트 메타 + 검증 명령 + 룰 참조 + 스킬 9종 + 동기화 룰만 포함한다.',
|
|
57
|
+
'> 프로젝트 메타 + 검증/테스트/검수 명령 + 룰 참조 + 스킬 9종 + 동기화 룰만 포함한다.',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
module.exports = { LEGACY_TEMPLATE_QUOTES };
|
package/bin/lib.js
CHANGED
|
@@ -139,6 +139,12 @@ function sha256(filePath) {
|
|
|
139
139
|
return 'sha256:' + crypto.createHash('sha256').update(buf).digest('hex');
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// 파일이 아니라 *써 넣을 내용*의 해시. 조립해서 설치하는 자산(AGENTS.md)은 템플릿 원본과
|
|
143
|
+
// 내용이 다르므로, manifest에는 반드시 조립 결과물의 해시를 기록해야 한다.
|
|
144
|
+
function sha256Text(text) {
|
|
145
|
+
return 'sha256:' + crypto.createHash('sha256').update(Buffer.from(text, 'utf8')).digest('hex');
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
function walkTemplate(templateDir) {
|
|
143
149
|
// template/ 안 모든 파일 상대경로 + 해시 맵 리턴
|
|
144
150
|
// 결과: { 'CLAUDE.md': 'sha256:...', '.claude/skills/task-init/SKILL.md': 'sha256:...', ... }
|
|
@@ -1007,10 +1013,22 @@ const TAG_OF = {
|
|
|
1007
1013
|
|
|
1008
1014
|
function _parseDevPlanPhases(content) {
|
|
1009
1015
|
// ## Dev Plan 섹션의 `### Phase N — 이름` + `- 파일:` 필드 추출.
|
|
1016
|
+
// 다중 파일 표기 지원 — 한 줄(쉼표·가운뎃점 `·` 구분) + `- 파일:` 다음의 들여쓴 하위 목록.
|
|
1017
|
+
// `/`는 경로 문자라 분할자로 쓰지 않는다. `(신규)`류 꼬리 괄호 주석은 제거한다.
|
|
1010
1018
|
const lines = content.split('\n');
|
|
1011
1019
|
let inDevPlan = false;
|
|
1012
1020
|
const phases = [];
|
|
1013
1021
|
let cur = null;
|
|
1022
|
+
let fileIndent = null; // `- 파일:` 줄의 들여쓰기 — 이보다 깊은 목록 항목을 파일로 수집
|
|
1023
|
+
const pushFiles = (raw) => {
|
|
1024
|
+
for (const piece of raw.split(/[,·]/)) {
|
|
1025
|
+
const f = piece
|
|
1026
|
+
.replace(/`/g, '')
|
|
1027
|
+
.trim()
|
|
1028
|
+
.replace(/\s*[((][^))]*[))]\s*$/, ''); // 꼬리 괄호 주석 제거
|
|
1029
|
+
if (f && !/^[(<]/.test(f)) cur.files.push(f); // placeholder(<...>, (...)) 제외
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1014
1032
|
for (const l of lines) {
|
|
1015
1033
|
if (/^##\s+Dev Plan/.test(l)) {
|
|
1016
1034
|
inDevPlan = true;
|
|
@@ -1022,15 +1040,23 @@ function _parseDevPlanPhases(content) {
|
|
|
1022
1040
|
if (ph) {
|
|
1023
1041
|
cur = { num: parseInt(ph[1], 10), name: ph[2].trim(), files: [] };
|
|
1024
1042
|
phases.push(cur);
|
|
1043
|
+
fileIndent = null;
|
|
1025
1044
|
continue;
|
|
1026
1045
|
}
|
|
1027
1046
|
if (!cur) continue;
|
|
1028
|
-
const fm = l.match(
|
|
1047
|
+
const fm = l.match(/^(\s*)-\s*파일\s*:\s*(.*)$/);
|
|
1029
1048
|
if (fm) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1049
|
+
fileIndent = fm[1].length;
|
|
1050
|
+
if (fm[2].trim()) pushFiles(fm[2]);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (fileIndent != null) {
|
|
1054
|
+
const sub = l.match(/^(\s+)-\s+(.+)$/);
|
|
1055
|
+
if (sub && sub[1].length > fileIndent) {
|
|
1056
|
+
pushFiles(sub[2]);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
fileIndent = null; // 다른 필드·비목록 줄에서 하위 목록 수집 종료
|
|
1034
1060
|
}
|
|
1035
1061
|
}
|
|
1036
1062
|
return phases;
|
|
@@ -1149,10 +1175,116 @@ function splitUncommittedByPhase(wtPath, taskMdPath, meta) {
|
|
|
1149
1175
|
return result;
|
|
1150
1176
|
}
|
|
1151
1177
|
|
|
1178
|
+
// ─── close 문서 자동화 (recordion FRICTION 2026-08-09 — 문서 갱신 기계화) ─────────
|
|
1179
|
+
// CHANGELOG는 close가 직접 append(세션 준수 불요), PLAN 체크·수정이력은 grep 게이트로 차단.
|
|
1180
|
+
|
|
1181
|
+
function _parseTaskTitle(taskMdPath) {
|
|
1182
|
+
// task 문서 H1 → 제목 (`# TASK-NNN — 제목`의 제목부. 접두 없으면 전체).
|
|
1183
|
+
const m = fs.readFileSync(taskMdPath, 'utf8').match(/^#\s+(.+)$/m);
|
|
1184
|
+
if (!m) return null;
|
|
1185
|
+
return m[1].replace(/^TASK-\d+\s*[—-]\s*/, '').trim() || null;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function appendChangelogEntry(base, { taskNum, title, type, phases }) {
|
|
1189
|
+
// CHANGELOG 항목을 close가 직접 append — CHANGELOG_RULE §1 두 형식(단일 CHANGELOG.md / 월별) 지원,
|
|
1190
|
+
// 없으면 월별 파일 생성. 최신 맨 위 삽입(§3). 재호출 가드: [TASK-NNN] 실재 시 skip
|
|
1191
|
+
// (mapping·docs 콜백 후 재호출 중복 차단). base = task 문서와 동일 분기(등록=메인WT / 미등록=워크트리).
|
|
1192
|
+
const nnn = String(taskNum).padStart(3, '0');
|
|
1193
|
+
const dir = path.join(base, '.project', 'changelog');
|
|
1194
|
+
const single = path.join(dir, 'CHANGELOG.md');
|
|
1195
|
+
const file = fs.existsSync(single) ? single : path.join(dir, `${_todayLocal().slice(0, 7)}.md`);
|
|
1196
|
+
const exists = fs.existsSync(file);
|
|
1197
|
+
const cur = exists ? fs.readFileSync(file, 'utf8') : '';
|
|
1198
|
+
if (cur.includes(`[TASK-${nnn}]`)) return { file, skipped: true };
|
|
1199
|
+
const named = (phases || []).filter((p) => p.num > 0 && p.name);
|
|
1200
|
+
const summary = named.length
|
|
1201
|
+
? named.map((p) => ` - Phase ${p.num} — ${p.name}`).join('\n')
|
|
1202
|
+
: ` - ${title || `TASK-${nnn}`}`;
|
|
1203
|
+
const entry = [
|
|
1204
|
+
`## [TASK-${nnn}] ${title || ''}`.trimEnd(),
|
|
1205
|
+
'',
|
|
1206
|
+
`- **날짜**: ${_todayLocal()}`,
|
|
1207
|
+
`- **타입**: ${type}`,
|
|
1208
|
+
`- **변경 요약**:`,
|
|
1209
|
+
summary,
|
|
1210
|
+
].join('\n');
|
|
1211
|
+
let next;
|
|
1212
|
+
if (!exists) {
|
|
1213
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1214
|
+
next = entry + '\n';
|
|
1215
|
+
} else {
|
|
1216
|
+
// 첫 `## ` 항목 바로 위 삽입 (코드펜스 내부 제외). 항목이 없으면 헤더 뒤(끝) append.
|
|
1217
|
+
const lines = cur.split('\n');
|
|
1218
|
+
let fence = false;
|
|
1219
|
+
let idx = -1;
|
|
1220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1221
|
+
if (/^\s*(```|~~~)/.test(lines[i])) fence = !fence;
|
|
1222
|
+
else if (!fence && /^##\s+/.test(lines[i])) {
|
|
1223
|
+
idx = i;
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (idx === -1) {
|
|
1228
|
+
next = cur + (cur.endsWith('\n') ? '' : '\n') + '\n' + entry + '\n';
|
|
1229
|
+
} else {
|
|
1230
|
+
lines.splice(idx, 0, entry, '');
|
|
1231
|
+
next = lines.join('\n');
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
fs.writeFileSync(file, next);
|
|
1235
|
+
return { file, added: true };
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function checkDocsGate(base, { taskNum, src, plan }) {
|
|
1239
|
+
// 문서 게이트 — close의 모든 변이(mutation) 앞에서 실행. blocked여도 아무것도 안 바뀌어
|
|
1240
|
+
// 세션이 문서를 채운 후 그대로 재호출하면 된다. 반환 { ok:true } | { ok:false, missing:[...] }.
|
|
1241
|
+
// PLAN.md: 로드맵 출처(ST-/RM-)만 — 체크리스트에 TASK-NNN 표기 검사. BL·DR 출처는 항목
|
|
1242
|
+
// 자체가 없으므로 통과. 파일 부재(구형 plan)도 통과.
|
|
1243
|
+
// 수정이력: spec-diffs(항상 plan 폴더 공통 — 폴더 승격 무관, TASK_DOC_RULE §1.5)의 본 태스크
|
|
1244
|
+
// 파일이 가리키는 대상 문서(DEL 제외)에 TASK-NNN 표기 검사. 대상 부재 = 통과
|
|
1245
|
+
// (게이트 실패 방향은 '막는 쪽'이 아니라 '놓치는 쪽' — 정당한 close를 오차단하지 않는다).
|
|
1246
|
+
const nnn = String(taskNum).padStart(3, '0');
|
|
1247
|
+
const tag = `TASK-${nnn}`;
|
|
1248
|
+
const missing = [];
|
|
1249
|
+
if (/^(ST|RM)-/i.test(src || '')) {
|
|
1250
|
+
const planPath = path.join(base, '.project', 'plans', plan, 'PLAN.md');
|
|
1251
|
+
if (fs.existsSync(planPath) && !fs.readFileSync(planPath, 'utf8').includes(tag)) {
|
|
1252
|
+
missing.push({ kind: 'plan', file: planPath });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
const specDir = path.join(base, '.project', 'tasks', plan, 'spec-diffs');
|
|
1256
|
+
if (fs.existsSync(specDir)) {
|
|
1257
|
+
const seen = new Set();
|
|
1258
|
+
const mine = fs
|
|
1259
|
+
.readdirSync(specDir)
|
|
1260
|
+
.filter((f) => f.startsWith(`${nnn}_`) && f.endsWith('.md'));
|
|
1261
|
+
for (const f of mine) {
|
|
1262
|
+
const content = fs.readFileSync(path.join(specDir, f), 'utf8');
|
|
1263
|
+
// 헤딩 `## <.project/문서.md> [NEW|MOD|DEL]` — 꺾쇠 유무 관용 파싱
|
|
1264
|
+
for (const m of content.matchAll(/^##\s+<?([^\n>[\]]+?)>?\s*\[(NEW|MOD|DEL)\]/gm)) {
|
|
1265
|
+
if (m[2] === 'DEL') continue;
|
|
1266
|
+
const rel = m[1].trim();
|
|
1267
|
+
const target = fs.existsSync(path.join(base, rel))
|
|
1268
|
+
? path.join(base, rel)
|
|
1269
|
+
: path.join(base, '.project', rel);
|
|
1270
|
+
if (seen.has(target)) continue;
|
|
1271
|
+
seen.add(target);
|
|
1272
|
+
if (!fs.existsSync(target)) continue; // 대상 부재 → 통과
|
|
1273
|
+
if (!fs.readFileSync(target, 'utf8').includes(tag)) {
|
|
1274
|
+
missing.push({ kind: 'history', file: target });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return missing.length ? { ok: false, missing } : { ok: true };
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1152
1282
|
async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
1153
1283
|
// 결정적 준비만 — 비가역(머지·정리)·충돌 해결은 스킬이 후속 수행.
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1284
|
+
// 순서: status 게이트 → docs 게이트 → Phase 커밋 → setStatus(closed) → CHANGELOG append
|
|
1285
|
+
// → (미등록) .project/ 커밋 → 추적 마커. 게이트 2종은 모든 변이 앞.
|
|
1286
|
+
// 반환: { blocked } (status/docs 게이트 미충족·매핑 모호) 또는
|
|
1287
|
+
// { prepped, branch, parent, wtPath, registered, commits, aheadOfParent, changelog,
|
|
1156
1288
|
// multiPhase?, autoAssigned? (매핑 폴백 적용 시 내역) }. parent = 스킬 머지 대상.
|
|
1157
1289
|
const active = getActiveTasks(mainWtPath).find((t) => t.taskNum === taskNum);
|
|
1158
1290
|
if (!active) {
|
|
@@ -1172,6 +1304,13 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1172
1304
|
return { blocked: 'status', current: header.status, docPath: resolved.file };
|
|
1173
1305
|
}
|
|
1174
1306
|
|
|
1307
|
+
// 문서 게이트 — 모든 변이 앞. blocked 시 아무 것도 안 바뀌어 채우고 그대로 재호출.
|
|
1308
|
+
const docBase = resolved.registered ? mainWtPath : wtPath;
|
|
1309
|
+
const gate = checkDocsGate(docBase, { taskNum, src: active.src, plan: resolved.plan });
|
|
1310
|
+
if (!gate.ok) {
|
|
1311
|
+
return { blocked: 'docs', missing: gate.missing, docPath: resolved.file };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1175
1314
|
// Phase 커밋 (D2) — 코드 변경분만
|
|
1176
1315
|
const split = splitUncommittedByPhase(wtPath, resolved.file, { taskNum, type: active.type });
|
|
1177
1316
|
if (split.ambiguous) {
|
|
@@ -1188,6 +1327,15 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1188
1327
|
// status=closed (문서 위치 무관 — setStatus가 .gitignore 케이스 따라 처리)
|
|
1189
1328
|
await setStatus(resolved.file, 'closed');
|
|
1190
1329
|
|
|
1330
|
+
// CHANGELOG 자동 append — 등록=메인WT 직접 쓰기(.project는 gitignore라 커밋 불요) /
|
|
1331
|
+
// 미등록=워크트리 쓰기(아래 .project/ 문서 커밋이 함께 수거).
|
|
1332
|
+
const changelog = appendChangelogEntry(docBase, {
|
|
1333
|
+
taskNum,
|
|
1334
|
+
title: _parseTaskTitle(resolved.file),
|
|
1335
|
+
type: header.type,
|
|
1336
|
+
phases: _parseDevPlanPhases(fs.readFileSync(resolved.file, 'utf8')),
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1191
1339
|
const commits = [...split.committed];
|
|
1192
1340
|
// 미등록 케이스: .project/ 변경분(flows / 문서 / changelog)을 커밋
|
|
1193
1341
|
if (!resolved.registered) {
|
|
@@ -1254,6 +1402,7 @@ async function closeTask(mainWtPath, { taskNum }, opts = {}) {
|
|
|
1254
1402
|
docPath: resolved.file,
|
|
1255
1403
|
commits,
|
|
1256
1404
|
aheadOfParent: aheadFinal,
|
|
1405
|
+
changelog,
|
|
1257
1406
|
};
|
|
1258
1407
|
// 매핑 폴백 적용 내역 노출 — 코드가 자동 편입한 걸 스킬·사용자가 볼 수 있게
|
|
1259
1408
|
if (split.multiPhase) prepResult.multiPhase = split.multiPhase;
|
|
@@ -1275,6 +1424,7 @@ module.exports = {
|
|
|
1275
1424
|
gitignorePatternsFor,
|
|
1276
1425
|
findTemplateDir,
|
|
1277
1426
|
sha256,
|
|
1427
|
+
sha256Text,
|
|
1278
1428
|
walkTemplate,
|
|
1279
1429
|
mkdirp,
|
|
1280
1430
|
copyFile,
|
|
@@ -1329,5 +1479,7 @@ module.exports = {
|
|
|
1329
1479
|
// task close 결정적 준비 (D1·D2)
|
|
1330
1480
|
TAG_OF,
|
|
1331
1481
|
splitUncommittedByPhase,
|
|
1482
|
+
appendChangelogEntry,
|
|
1483
|
+
checkDocsGate,
|
|
1332
1484
|
closeTask,
|
|
1333
1485
|
};
|