@angar2/taskery 0.8.1 → 1.0.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 +423 -188
- package/bin/add.js +27 -99
- package/bin/backlog.js +240 -113
- package/bin/check.js +166 -0
- package/bin/commands.js +151 -0
- package/bin/finish.js +353 -0
- package/bin/init.js +99 -190
- package/bin/install.js +143 -0
- package/bin/lib.js +513 -1325
- package/bin/mcp.js +31 -263
- package/bin/orche.js +240 -0
- package/bin/plan.js +58 -120
- package/bin/prepare.js +326 -0
- package/bin/prune.js +46 -176
- package/bin/status.js +57 -151
- package/bin/taskery.js +76 -120
- package/bin/update.js +66 -269
- package/package.json +5 -5
- package/template/.taskery/BACKLOG.md +38 -0
- package/template/.taskery/rules/CHANGELOG_RULE.md +31 -0
- package/template/.taskery/rules/DEV_RULE.local.md +8 -0
- package/template/.taskery/rules/GIT_RULE.md +82 -0
- package/template/.taskery/rules/MOCKUP_RULE.md +19 -0
- package/template/.taskery/rules/TASKERY_RULE.md +112 -0
- package/template/.taskery/rules/TASK_DOC_RULE.md +84 -0
- package/template/.taskery/rules/TEST_RULE.local.md +16 -0
- package/template/AGENTS.md +15 -53
- package/template/shared/skills/add-backlog/SKILL.md +8 -38
- package/template/shared/skills/log-friction/SKILL.md +10 -88
- package/template/shared/skills/plan-init/SKILL.md +10 -98
- package/template/shared/skills/project-init/SKILL.md +13 -342
- package/template/shared/skills/task-close/SKILL.md +16 -319
- package/template/shared/skills/task-dev/SKILL.md +19 -233
- package/template/shared/skills/task-init/SKILL.md +11 -68
- package/template/shared/skills/task-orche/SKILL.md +35 -0
- package/template/shared/skills/task-plan/SKILL.md +19 -498
- package/template/shared/skills/task-test/SKILL.md +23 -342
- package/bin/close.js +0 -86
- package/bin/fork.js +0 -112
- package/bin/legacy-quotes.js +0 -60
- package/bin/migrate.js +0 -481
- package/bin/set-status.js +0 -98
- package/template/.claude/hooks/closed-immutable.sh +0 -77
- package/template/.claude/settings.json +0 -18
- package/template/.claude/skills/run-team/SKILL.md +0 -145
- package/template/.codex/agents/task-tester.toml +0 -41
- package/template/.codex/agents/test-plan-author.toml +0 -34
- package/template/.codex/config.toml +0 -17
- package/template/.codex/hooks/closed-immutable.sh +0 -66
- package/template/.project/changelog/.gitkeep +0 -0
- package/template/.project/flows/.gitkeep +0 -0
- package/template/.project/plans/.gitkeep +0 -0
- package/template/.project/rules/CHANGELOG_RULE.md +0 -97
- package/template/.project/rules/GIT_RULE.md +0 -325
- package/template/.project/rules/MOCKUP_RULE.md +0 -86
- package/template/.project/rules/TASKERY_RULE.md +0 -237
- package/template/.project/rules/TASK_DOC_RULE.md +0 -668
- package/template/.project/shared/received/completed/.gitkeep +0 -0
- package/template/.project/shared/sent/completed/.gitkeep +0 -0
- package/template/.project/tasks/.gitkeep +0 -0
- package/template/shared/hooks/git-guard.sh +0 -108
package/bin/legacy-quotes.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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/migrate.js
DELETED
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
// 0.7.0 문서 체계 재편 마이그레이션 — update가 기존 설치본을 새 구조로 이행시킨다
|
|
2
|
-
/**
|
|
3
|
-
* bin/migrate.js
|
|
4
|
-
* `npx @angar2/taskery update`가 호출하는 0.7.0 이행 로직 모음.
|
|
5
|
-
*
|
|
6
|
-
* 이행 4종:
|
|
7
|
-
* 1. ingestActivePlan — 구형 AGENT-GUIDE.md '## 활성 plan 버전' → manifest.activePlan (1회)
|
|
8
|
-
* 2. recomposeEntryDocs — 진입 문서 **재조립**. 현 진입 문서(AGENTS.md, 없으면 구 CLAUDE.md)에서
|
|
9
|
-
* 리포 내용을 건져 새 AGENTS.md 템플릿에 끼워넣는다. 최초 이행 때는
|
|
10
|
-
* CLAUDE.md를 `@AGENTS.md` 한 줄로 축약한다.
|
|
11
|
-
* **매 update 항상 실행한다(1회성 아님).** AGENTS.md는 리포 값을 품어
|
|
12
|
-
* 템플릿과 바이트 일치가 원리적으로 불가능하므로, 본 루프의 복사 대상이
|
|
13
|
-
* 되어선 안 된다(0.7.0 결함: 이행 후 본 루프가 값을 통째로 덮거나,
|
|
14
|
-
* 거절하면 구판 문안에 영구 고착됐다).
|
|
15
|
-
* **update 본 루프보다 먼저 실행하고, 처리한 경로를 본 루프에서 제외한다.**
|
|
16
|
-
* 3. migrateGitignore — AGENTS.md가 agnostic으로 바뀌어 모든 리포에 새로 깔리는데
|
|
17
|
-
* .gitignore 기록은 init에만 있어 누락된다. 누락 패턴 append 제안.
|
|
18
|
-
* 4. migrateTestGuide — 폐지되는 TEST-GUIDE.md의 축적 내용을 TEST_RULE.local.md로 1회 이관.
|
|
19
|
-
*
|
|
20
|
-
* 원칙: 사용자 값은 절대 유실시키지 않는다. 추출 실패 시 어느 파일도 덮지 않고 `.new`로 보류한다.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
const path = require('path');
|
|
25
|
-
|
|
26
|
-
const { sha256, sha256Text, copyFile, gitignorePatternsFor } = require('./lib');
|
|
27
|
-
const { LEGACY_TEMPLATE_QUOTES } = require('./legacy-quotes');
|
|
28
|
-
|
|
29
|
-
// 진입 문서에서 이식할 *리포 값* 섹션. 헤딩 텍스트 접두로 매칭한다 —
|
|
30
|
-
// 실제 문서의 헤딩은 '## 검수 실행 명령 (해당 시 — 앱 실행·검수가 필요한 프로젝트)'처럼
|
|
31
|
-
// 뒤에 부가 텍스트가 붙는다. 완전 일치를 요구하면 못 찾는다.
|
|
32
|
-
const REPO_VALUE_SECTIONS = ['프로젝트 메타', '검증 명령', '테스트 명령', '검수 실행 명령'];
|
|
33
|
-
// 이 중 없어도 이행을 계속하는 항목 — 앱 검수가 없는 프로젝트는 이 절을 지웠을 수 있다.
|
|
34
|
-
const OPTIONAL_SECTIONS = new Set(['검수 실행 명령']);
|
|
35
|
-
// 진입 문서 이행 완료 마커 — manifest에 기록해 재실행을 막는다.
|
|
36
|
-
const ENTRY_DOCS_MARKER = '0.7.0';
|
|
37
|
-
|
|
38
|
-
function isBefore070(version) {
|
|
39
|
-
if (typeof version !== 'string') return true;
|
|
40
|
-
const [maj = 0, min = 0] = version.split('.').map((n) => parseInt(n, 10) || 0);
|
|
41
|
-
return maj < 1 && min < 7;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 마크다운을 '## ' 헤딩 단위로 쪼개 { key, heading, body } 목록으로 만든다.
|
|
45
|
-
// 코드펜스 내부는 헤딩으로 보지 않는다 — 셸 주석(`## ...`)이 든 예시 블록을 헤딩으로 오인하면
|
|
46
|
-
// 그 뒤 본문이 조용히 잘려 사용자 값이 유실된다(경고도 안 뜬다).
|
|
47
|
-
function splitSections(content) {
|
|
48
|
-
const lines = content.split('\n');
|
|
49
|
-
const out = [];
|
|
50
|
-
let cur = null;
|
|
51
|
-
let inFence = false;
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
|
|
54
|
-
const m = inFence ? null : line.match(/^##\s+(.*)$/);
|
|
55
|
-
if (m) {
|
|
56
|
-
if (cur) out.push(cur);
|
|
57
|
-
cur = { heading: line, title: m[1].trim(), body: [] };
|
|
58
|
-
} else if (cur) {
|
|
59
|
-
cur.body.push(line);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (cur) out.push(cur);
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 문서를 { 머리말, 섹션[] }로 쪼갠다. splitSections와 달리 첫 '## ' 앞의 머리말을 버리지 않는다 —
|
|
67
|
-
// 재조립은 문서 전체를 다시 써내므로 머리말(제목·최상위 인용문)도 결과물에 들어가야 한다.
|
|
68
|
-
function parseDoc(content) {
|
|
69
|
-
const preamble = [];
|
|
70
|
-
const sections = [];
|
|
71
|
-
let cur = null;
|
|
72
|
-
let inFence = false;
|
|
73
|
-
for (const line of content.split('\n')) {
|
|
74
|
-
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
|
|
75
|
-
const m = inFence ? null : line.match(/^##\s+(.*)$/);
|
|
76
|
-
if (m) {
|
|
77
|
-
cur = { heading: line, title: m[1].trim(), body: [] };
|
|
78
|
-
sections.push(cur);
|
|
79
|
-
} else if (cur) {
|
|
80
|
-
cur.body.push(line);
|
|
81
|
-
} else {
|
|
82
|
-
preamble.push(line);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return { preamble, sections };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// taskery가 쓴 인용문 집합 = 과거 템플릿 전량(고정 목록) ∪ *현재* 템플릿(실시간 추출).
|
|
89
|
-
// 현재 템플릿을 함께 넣는 것이 핵심이다 — 그래야 다음 버전의 새 문안도 그 다음 update에서
|
|
90
|
-
// 자기 자신을 중복 없이 걷어낸다(목록을 손으로 관리하지 않아도 영구히 자기유지된다).
|
|
91
|
-
function boilerplateQuotes(templateContent) {
|
|
92
|
-
const set = new Set(LEGACY_TEMPLATE_QUOTES.map((s) => s.trim()));
|
|
93
|
-
for (const l of templateContent.split('\n')) {
|
|
94
|
-
if (/^\s*>/.test(l)) set.add(l.trim());
|
|
95
|
-
}
|
|
96
|
-
return set;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 우리가 수행한 파일 이름 변경 — 리포가 직접 쓴 문장 안의 죽은 경로를 대응 경로로 바꾼다.
|
|
100
|
-
// 1:1로 확정된 것만 넣는다(추측 치환 금지).
|
|
101
|
-
const DEAD_PATH_REWRITES = [['.project/TEST-GUIDE.md', '.project/rules/TEST_RULE.local.md']];
|
|
102
|
-
|
|
103
|
-
// 인용문 블록(헤딩 직후 연속된 '>' 줄)과 꼬리(끝의 빈 줄·구분선)를 템플릿 본문에서 뽑는다.
|
|
104
|
-
function splitTemplateBody(body) {
|
|
105
|
-
let i = 0;
|
|
106
|
-
while (i < body.length && body[i].trim() === '') i++;
|
|
107
|
-
const quotes = [];
|
|
108
|
-
while (i < body.length && /^\s*>/.test(body[i])) quotes.push(body[i++]);
|
|
109
|
-
const tail = [];
|
|
110
|
-
let j = body.length - 1;
|
|
111
|
-
while (j >= i && (body[j].trim() === '' || body[j].trim() === '---')) tail.unshift(body[j--]);
|
|
112
|
-
return { quotes, tail };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* AGENTS.md 재조립 — 새 템플릿의 지침 + 현 문서의 리포 내용.
|
|
117
|
-
*
|
|
118
|
-
* 절마다: 새 템플릿 헤딩 + 새 템플릿 인용문 + 현 문서 본문(단, taskery 인용문은 제거).
|
|
119
|
-
* 값 줄·표·산문은 판정 대상이 아니라 전부 보존된다. 리포가 직접 쓴 인용문도 보존된다.
|
|
120
|
-
*
|
|
121
|
-
* @returns {{ content, kept, dropped, rewritten, omitted, missing }}
|
|
122
|
-
* kept — 보존한 리포 인용문 / dropped — 걷어낸 taskery 구 문안
|
|
123
|
-
* rewritten — 죽은 경로를 치환한 줄 / omitted — 현 문서에 없어 결과물에서 뺀 절
|
|
124
|
-
* missing — omitted 중 필수 절(호출자가 보류 판정에 쓴다)
|
|
125
|
-
*/
|
|
126
|
-
function composeAgentsDoc(currentContent, templateContent) {
|
|
127
|
-
const tpl = parseDoc(templateContent);
|
|
128
|
-
const cur = parseDoc(currentContent);
|
|
129
|
-
const boiler = boilerplateQuotes(templateContent);
|
|
130
|
-
const kept = [];
|
|
131
|
-
const dropped = [];
|
|
132
|
-
const rewritten = [];
|
|
133
|
-
const omitted = [];
|
|
134
|
-
const out = [...tpl.preamble];
|
|
135
|
-
|
|
136
|
-
for (const sec of tpl.sections) {
|
|
137
|
-
const key = REPO_VALUE_SECTIONS.find((k) => sec.title.startsWith(k));
|
|
138
|
-
if (!key) {
|
|
139
|
-
out.push(sec.heading, ...sec.body);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const hit = cur.sections.find((s) => s.title.startsWith(key));
|
|
143
|
-
const { quotes, tail } = splitTemplateBody(sec.body);
|
|
144
|
-
if (!hit) {
|
|
145
|
-
// 리포가 지운 절은 되살리지 않는다(삭제 의사 존중). 절 사이 구분선만 유지한다.
|
|
146
|
-
omitted.push(key);
|
|
147
|
-
out.push(...tail);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const repoBody = [];
|
|
151
|
-
for (const raw of hit.body) {
|
|
152
|
-
const isQuote = /^\s*>/.test(raw);
|
|
153
|
-
if (isQuote && boiler.has(raw.trim())) {
|
|
154
|
-
dropped.push(raw.trim());
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
let line = raw;
|
|
158
|
-
for (const [from, to] of DEAD_PATH_REWRITES) {
|
|
159
|
-
if (line.includes(from)) line = line.split(from).join(to);
|
|
160
|
-
}
|
|
161
|
-
if (line !== raw) rewritten.push({ before: raw.trim(), after: line.trim() });
|
|
162
|
-
if (isQuote) kept.push(line.trim());
|
|
163
|
-
repoBody.push(line);
|
|
164
|
-
}
|
|
165
|
-
while (repoBody.length && repoBody[0].trim() === '') repoBody.shift();
|
|
166
|
-
while (
|
|
167
|
-
repoBody.length &&
|
|
168
|
-
(repoBody[repoBody.length - 1].trim() === '' || repoBody[repoBody.length - 1].trim() === '---')
|
|
169
|
-
) {
|
|
170
|
-
repoBody.pop();
|
|
171
|
-
}
|
|
172
|
-
out.push(sec.heading);
|
|
173
|
-
if (repoBody.length === 0) {
|
|
174
|
-
// 절은 있는데 내용이 비었다 → 새 템플릿 본문(플레이스홀더)을 그대로 쓴다.
|
|
175
|
-
out.push(...sec.body);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (quotes.length) out.push('', ...quotes);
|
|
179
|
-
out.push('', ...repoBody, ...tail);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 결과물에 실리지 않은 현 문서의 내용 — 리포 값 4개 절 *밖*에 사람이 적어 둔 줄이 여기 걸린다.
|
|
183
|
-
// 그 자리는 taskery 소유라 템플릿 판으로 덮이는 것이 설계지만, 재조립은 매 update 도는 상시
|
|
184
|
-
// 동작이므로 조용히 사라지면 안 된다. 버리되 반드시 알린다(.bak이 원본 참조처).
|
|
185
|
-
const inResult = new Set(out.map((l) => l.trim()));
|
|
186
|
-
const rewrittenBefore = new Set(rewritten.map((r) => r.before));
|
|
187
|
-
const unmapped = [];
|
|
188
|
-
for (const line of currentContent.split('\n')) {
|
|
189
|
-
const l = line.trim();
|
|
190
|
-
if (!l || l === '---') continue;
|
|
191
|
-
if (inResult.has(l) || boiler.has(l) || rewrittenBefore.has(l)) continue;
|
|
192
|
-
unmapped.push(l);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
content: out.join('\n'),
|
|
197
|
-
kept,
|
|
198
|
-
dropped,
|
|
199
|
-
rewritten,
|
|
200
|
-
omitted,
|
|
201
|
-
unmapped,
|
|
202
|
-
missing: omitted.filter((k) => !OPTIONAL_SECTIONS.has(k)),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 1. 활성 plan 이식 — manifest.activePlan이 이미 있으면 그것을 그대로 보존한다.
|
|
208
|
-
* (update가 manifest를 새로 조립하므로 보존을 명시하지 않으면 매번 증발한다.)
|
|
209
|
-
*/
|
|
210
|
-
function ingestActivePlan(cwd, oldManifest) {
|
|
211
|
-
if (oldManifest.activePlan) return { activePlan: oldManifest.activePlan, ingested: false };
|
|
212
|
-
const guidePath = path.join(cwd, '.project', 'AGENT-GUIDE.md');
|
|
213
|
-
if (!fs.existsSync(guidePath)) return { activePlan: null, ingested: false };
|
|
214
|
-
const sec = fs.readFileSync(guidePath, 'utf8').match(/^##\s*활성 plan 버전[^\n]*\n+([^\n]+)/m);
|
|
215
|
-
if (!sec) return { activePlan: null, ingested: false };
|
|
216
|
-
const firstLine = sec[1].trim();
|
|
217
|
-
if (!firstLine || firstLine.startsWith('<')) return { activePlan: null, ingested: false };
|
|
218
|
-
return { activePlan: firstLine.split(/\s+/)[0], ingested: true };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 2. 진입 문서 재편.
|
|
223
|
-
* @returns {{ handled: string[], entries: object, notes: string[], held: boolean }}
|
|
224
|
-
* handled — update 본 루프에서 제외할 설치 경로
|
|
225
|
-
* entries — manifest.files에 기록할 항목 (설치 경로 → { hash, core, managed })
|
|
226
|
-
*/
|
|
227
|
-
function recomposeEntryDocs(cwd, templateDir, platforms, oldManifest) {
|
|
228
|
-
const notes = [];
|
|
229
|
-
const entries = {};
|
|
230
|
-
const skip = { handled: [], entries, notes, held: false, done: false };
|
|
231
|
-
|
|
232
|
-
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
233
|
-
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
234
|
-
const hasAgents = fs.existsSync(agentsPath);
|
|
235
|
-
const hasClaude = fs.existsSync(claudePath);
|
|
236
|
-
// 최초 이행 여부는 manifest 마커 하나로만 판정한다. 파일 구조로 추정하면 플랫폼별로 오판하고
|
|
237
|
-
// (codex 단독은 CLAUDE.md가 없다), 버전으로 거르면 보류된 리포가 영영 재시도되지 않는다.
|
|
238
|
-
const migrated = oldManifest.entryDocs === ENTRY_DOCS_MARKER;
|
|
239
|
-
|
|
240
|
-
// 조립 소스: 현 AGENTS.md가 최우선(이행 후의 정상 경로). 없으면 미이행 리포의 구 CLAUDE.md.
|
|
241
|
-
const srcPath = hasAgents ? agentsPath : hasClaude && !migrated ? claudePath : null;
|
|
242
|
-
if (!srcPath) return skip; // 진입 문서 자체가 없음 → 본 루프의 '신규' 분기가 템플릿을 깐다
|
|
243
|
-
const srcName = path.basename(srcPath);
|
|
244
|
-
|
|
245
|
-
const newAgentsSrc = path.join(templateDir, 'AGENTS.md');
|
|
246
|
-
const newAgents = fs.readFileSync(newAgentsSrc, 'utf8');
|
|
247
|
-
const current = fs.readFileSync(srcPath, 'utf8');
|
|
248
|
-
const composed = composeAgentsDoc(current, newAgents);
|
|
249
|
-
|
|
250
|
-
// 최초 이행에서 필수 절을 못 찾으면 소스 판정이 틀렸다는 뜻이다 → 어느 파일도 덮지 않고 보류.
|
|
251
|
-
// (이행 후의 필수 절 부재는 사용자가 지운 것이므로 보류하지 않고 그 삭제를 존중한다.)
|
|
252
|
-
if (!migrated && composed.missing.length > 0) {
|
|
253
|
-
fs.writeFileSync(path.join(cwd, 'AGENTS.md.new'), newAgents);
|
|
254
|
-
notes.push(
|
|
255
|
-
`⚠ 진입 문서 이행 보류 — ${srcName}에서 필수 섹션을 찾지 못함 (${composed.missing.join(', ')}).`,
|
|
256
|
-
` 새 판을 AGENTS.md.new 로 두었습니다. 리포 값을 직접 옮긴 후 AGENTS.md 로 바꿔주세요.`,
|
|
257
|
-
` (기존 파일은 그대로 보존되었고 manifest도 갱신하지 않아 다음 update가 다시 시도합니다.)`,
|
|
258
|
-
);
|
|
259
|
-
return { handled: ['AGENTS.md', 'CLAUDE.md'], entries, notes, held: true, done: false };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 결과물이 현 파일과 다를 때만 쓴다. .bak은 *최초 1회만* — 이미 있으면 그게 사용자의 원본이다.
|
|
263
|
-
const changed = !hasAgents || composed.content !== current;
|
|
264
|
-
if (changed) {
|
|
265
|
-
if (hasAgents && !fs.existsSync(agentsPath + '.bak')) {
|
|
266
|
-
fs.copyFileSync(agentsPath, agentsPath + '.bak');
|
|
267
|
-
notes.push(` 직전 AGENTS.md는 AGENTS.md.bak 으로 백업.`);
|
|
268
|
-
}
|
|
269
|
-
fs.writeFileSync(agentsPath, composed.content);
|
|
270
|
-
}
|
|
271
|
-
// manifest에는 **조립 결과물**의 해시를 기록한다. 템플릿 원본 해시를 적으면 실제 파일과
|
|
272
|
-
// 영구 불일치해 매 update마다 '사용자 customize' 오검출이 뜬다(0.7.0 결함).
|
|
273
|
-
entries['AGENTS.md'] = { hash: sha256Text(composed.content), core: true, managed: true };
|
|
274
|
-
|
|
275
|
-
if (!migrated) {
|
|
276
|
-
notes.push(
|
|
277
|
-
`진입 문서 재편 — ${srcName}의 리포 내용을 새 AGENTS.md로 재조립.`,
|
|
278
|
-
` 구 문서의 taskery 지침 본문은 새 체계(.project/rules/)로 대체됩니다 — 원본은 ${srcName}.bak.`,
|
|
279
|
-
);
|
|
280
|
-
} else if (changed) {
|
|
281
|
-
notes.push(`AGENTS.md 재조립 — 리포 내용 보존, taskery 지침만 새 판으로 교체.`);
|
|
282
|
-
}
|
|
283
|
-
if (changed) {
|
|
284
|
-
if (composed.dropped.length) {
|
|
285
|
-
notes.push(` 구 taskery 문안 ${composed.dropped.length}줄 제거(새 문안이 대체).`);
|
|
286
|
-
}
|
|
287
|
-
if (composed.kept.length) {
|
|
288
|
-
notes.push(` 리포가 직접 쓴 인용문 ${composed.kept.length}줄 보존.`);
|
|
289
|
-
}
|
|
290
|
-
for (const r of composed.rewritten) {
|
|
291
|
-
notes.push(` 경로 갱신: ${r.before}`);
|
|
292
|
-
notes.push(` → ${r.after}`);
|
|
293
|
-
}
|
|
294
|
-
if (composed.omitted.length) {
|
|
295
|
-
notes.push(` 리포에 없어 결과물에서 뺀 절: ${composed.omitted.join(', ')} (되살리지 않음)`);
|
|
296
|
-
}
|
|
297
|
-
if (composed.unmapped.length && migrated) {
|
|
298
|
-
// 리포 값 칸 밖의 내용은 AGENTS.md가 소유하지 않는다(TASKERY_RULE — 규칙은 rules/, 커스텀은 *.local.md).
|
|
299
|
-
// 그래도 사람이 적어 둔 것이므로 전량을 보여주고 원본 위치를 알린다.
|
|
300
|
-
// 최초 이행에서는 알리지 않는다 — 그때의 미반영분은 전량이 *구 taskery 문서 본문*이라
|
|
301
|
-
// (0.6.1 기준 88줄) 경고가 아니라 노이즈다. 그 경우는 아래 한 줄과 .bak으로 갈음한다.
|
|
302
|
-
notes.push(
|
|
303
|
-
` ⚠ 리포 값 칸(프로젝트 메타 · 각 명령) 밖의 아래 ${composed.unmapped.length}줄은 결과물에 싣지 않았습니다.`,
|
|
304
|
-
` 그 자리는 taskery 지침 영역이라 새 판으로 교체됩니다. 남길 내용이면 값 칸이나 *.local.md로 옮기세요.`,
|
|
305
|
-
` (직전 원본: AGENTS.md.bak)`,
|
|
306
|
-
);
|
|
307
|
-
for (const l of composed.unmapped) notes.push(` · ${l.length > 96 ? l.slice(0, 96) + '…' : l}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const handled = ['AGENTS.md'];
|
|
312
|
-
|
|
313
|
-
// CLAUDE.md는 `@AGENTS.md` 한 줄로 축약 — 최초 이행 때 1회(claude 플랫폼일 때만).
|
|
314
|
-
// 이행 후에는 순수 템플릿 파일이라 본 루프가 정상 관리한다.
|
|
315
|
-
if (!migrated && platforms.includes('claude')) {
|
|
316
|
-
const claudeSrc = path.join(templateDir, 'CLAUDE.md');
|
|
317
|
-
if (hasClaude && !fs.existsSync(claudePath + '.bak')) {
|
|
318
|
-
fs.copyFileSync(claudePath, claudePath + '.bak');
|
|
319
|
-
notes.push(` 기존 CLAUDE.md는 CLAUDE.md.bak 으로 백업.`);
|
|
320
|
-
}
|
|
321
|
-
copyFile(claudeSrc, claudePath);
|
|
322
|
-
entries['CLAUDE.md'] = { hash: sha256(claudeSrc), core: true, managed: true };
|
|
323
|
-
handled.push('CLAUDE.md');
|
|
324
|
-
notes.push(` CLAUDE.md는 '@AGENTS.md' 임포트 한 줄로 축약.`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return { handled, entries, notes, held: false, done: true };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* 2-b. 구형 AGENT-GUIDE.md 정리 — 활성 plan이 manifest로 이관된 뒤에도 남아 있으면
|
|
332
|
-
* 같은 정보를 두 곳에 보유하게 되고, manifest 값이 비는 순간 lib의 구형 폴백이
|
|
333
|
-
* 낡은 값을 되살린다. 삭제는 하지 않고(사용자 파일 불가침) 이름을 바꿔 폴백 경로에서 뺀다.
|
|
334
|
-
* @returns {string|null} 처리 안내 (해당 없으면 null)
|
|
335
|
-
*/
|
|
336
|
-
function retireLegacyAgentGuide(cwd, activePlan) {
|
|
337
|
-
if (!activePlan) return null; // manifest가 아직 활성 plan을 못 들었으면 폴백을 남겨둔다
|
|
338
|
-
const guidePath = path.join(cwd, '.project', 'AGENT-GUIDE.md');
|
|
339
|
-
if (!fs.existsSync(guidePath)) return null;
|
|
340
|
-
const retired = guidePath + '.obsolete';
|
|
341
|
-
if (fs.existsSync(retired)) return null;
|
|
342
|
-
fs.renameSync(guidePath, retired);
|
|
343
|
-
return `구형 .project/AGENT-GUIDE.md 는 활성 plan이 manifest로 이관돼 역할이 끝났습니다 — AGENT-GUIDE.md.obsolete 로 이름 변경(내용 보존). 확인 후 삭제해도 됩니다.`;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* 3. .gitignore 누락 패턴 — 이미 taskery 패턴을 쓰는 리포에만 제안한다.
|
|
348
|
-
* (init에서 등록을 거부한 리포는 패턴이 하나도 없으므로 건드리지 않는다.)
|
|
349
|
-
* @returns {{ missing: string[], apply: function }} — missing이 비면 제안 불필요
|
|
350
|
-
*/
|
|
351
|
-
function planGitignore(cwd, platforms) {
|
|
352
|
-
const gitignorePath = path.join(cwd, '.gitignore');
|
|
353
|
-
if (!fs.existsSync(gitignorePath)) return { missing: [] };
|
|
354
|
-
const wanted = gitignorePatternsFor(platforms);
|
|
355
|
-
const existing = new Set(
|
|
356
|
-
fs.readFileSync(gitignorePath, 'utf8').split('\n').map((l) => l.trim()),
|
|
357
|
-
);
|
|
358
|
-
const already = wanted.filter((p) => existing.has(p));
|
|
359
|
-
if (already.length === 0) return { missing: [] }; // taskery 패턴 미사용 리포 — 선택 존중
|
|
360
|
-
const missing = wanted.filter((p) => !existing.has(p));
|
|
361
|
-
return {
|
|
362
|
-
missing,
|
|
363
|
-
apply() {
|
|
364
|
-
fs.appendFileSync(gitignorePath, `\n${missing.join('\n')}\n`);
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// TEST_RULE.local.md 필수 절 골격(TASKERY_RULE §8) — 이관 본문에 없으면 append한다.
|
|
370
|
-
// 문안은 project-init 골격과 동일 유지. '방식별 실행 경로'는 이관 본문 자체가 그 내용이라 제외.
|
|
371
|
-
const TEST_RULE_REQUIRED_SECTIONS = [
|
|
372
|
-
{
|
|
373
|
-
key: '테스트 실행 환경',
|
|
374
|
-
block: [
|
|
375
|
-
'## 테스트 실행 환경 (격리 실행 경로)',
|
|
376
|
-
'',
|
|
377
|
-
'> 실제 앱을 띄워 UI를 자동 조작하는 스위트를 **사용자의 화면·마우스·키보드를 뺏지 않고** 돌리는 경로.',
|
|
378
|
-
'> 여기가 비어 있으면 자동 게이트·격리 세션은 그 스위트를 실행하지 않고 사용자 승인을 받는다.',
|
|
379
|
-
'> 적을 수 있는 것: 창 숨김·헤드리스 플래그 / 가상 디스플레이·컨테이너 / VM / CI 러너 위임 / 점유 스위트를 제외하는 한정 실행 옵션.',
|
|
380
|
-
'> 결정 자체도 기록 대상 — 격리 경로를 두지 않기로 했다면 *"없음 — 매번 승인(사용자 결정 YYYY-MM-DD)"*로 적어 미결정 상태와 구분한다.',
|
|
381
|
-
'',
|
|
382
|
-
'<아직 미정 — 여기에 이 리포의 격리 실행 경로를 채운다>',
|
|
383
|
-
].join('\n'),
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
key: '범위·방식 정책',
|
|
387
|
-
block: [
|
|
388
|
-
'## 범위·방식 정책',
|
|
389
|
-
'',
|
|
390
|
-
'> 무엇을 얼마나 검증하나 — 수정 루프에서의 실행 범위, 테스트 신설 상한 등.',
|
|
391
|
-
'',
|
|
392
|
-
'<아직 미정 — 여기에 이 리포의 범위 정책을 채운다>',
|
|
393
|
-
].join('\n'),
|
|
394
|
-
},
|
|
395
|
-
];
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* 4. TEST-GUIDE.md → TEST_RULE.local.md 이관.
|
|
399
|
-
* 폐지되는 문서라 그대로 두면 새 참조처들이 읽지 않아 축적 내용이 고아가 된다.
|
|
400
|
-
* 원본은 지우지 않는다(사용자 파일 불가침).
|
|
401
|
-
* @returns {{ available: boolean, apply: function }} — apply()는 append한 필수 절 key 목록을 반환
|
|
402
|
-
*/
|
|
403
|
-
function planTestGuide(cwd) {
|
|
404
|
-
const guidePath = path.join(cwd, '.project', 'TEST-GUIDE.md');
|
|
405
|
-
const localPath = path.join(cwd, '.project', 'rules', 'TEST_RULE.local.md');
|
|
406
|
-
if (!fs.existsSync(guidePath)) return { available: false };
|
|
407
|
-
if (fs.existsSync(localPath)) {
|
|
408
|
-
// 둘 다 있으면 자동 병합하지 않는다(사용자 파일 훼손 위험). 다만 침묵하면
|
|
409
|
-
// TEST-GUIDE에 쌓인 내용이 아무도 안 읽는 채로 남으므로 반드시 알린다.
|
|
410
|
-
return { available: false, conflict: true };
|
|
411
|
-
}
|
|
412
|
-
return {
|
|
413
|
-
available: true,
|
|
414
|
-
apply() {
|
|
415
|
-
const body = fs.readFileSync(guidePath, 'utf8');
|
|
416
|
-
// 머리말만 새 역할에 맞게 갈아끼우고 본문(축적된 실행 경로)은 그대로 옮긴다.
|
|
417
|
-
const stripped = body.replace(/^#\s*TEST-GUIDE[^\n]*\n(>[^\n]*\n)*/, '');
|
|
418
|
-
const header = [
|
|
419
|
-
'# TEST_RULE.local.md — 이 리포의 검증 규칙',
|
|
420
|
-
'',
|
|
421
|
-
'> 이 프로젝트에서 각 테스트 방식을 *실제로 어떻게 실행하나* + *무엇을 얼마나 검증하나*의 단일 소스.',
|
|
422
|
-
'> `.project/TEST-GUIDE.md`(0.7.0에서 폐지)의 내용을 이관한 문서다. 구성과 의무는 `TASKERY_RULE` §8 참조.',
|
|
423
|
-
'> `*.local.md`는 taskery update가 건드리지 않는다 — 이 리포가 소유한다.',
|
|
424
|
-
'',
|
|
425
|
-
].join('\n');
|
|
426
|
-
// 필수 절 골격 보강 — TEST-GUIDE에는 없던 절(TASKERY_RULE §8)이라 이관만으로는 빠진다.
|
|
427
|
-
// 헤딩은 접두 일치로 검사한다(완전 일치 요구 금지 — 뒤에 부가 텍스트가 붙는다).
|
|
428
|
-
const titles = splitSections(stripped).map((s) => s.title);
|
|
429
|
-
const appended = TEST_RULE_REQUIRED_SECTIONS.filter(
|
|
430
|
-
(sec) => !titles.some((t) => t.startsWith(sec.key)),
|
|
431
|
-
);
|
|
432
|
-
const tail = appended.length
|
|
433
|
-
? `${stripped.endsWith('\n') ? '' : '\n'}\n${appended.map((s) => s.block).join('\n\n')}\n`
|
|
434
|
-
: '';
|
|
435
|
-
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
436
|
-
fs.writeFileSync(localPath, header + stripped + tail);
|
|
437
|
-
return appended.map((s) => s.key);
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* 5. `.gitignore` 병합 — **덮어쓰기 금지.**
|
|
444
|
-
* template/.gitignore는 manifest 관리 자산이라 update의 '신규' 분기가 그대로 복사해버리는데,
|
|
445
|
-
* 실사용 리포는 대부분 .gitignore를 manifest에 안 들고 있다(init 이전부터 있던 파일).
|
|
446
|
-
* 그대로 두면 사용자의 node_modules·빌드 산출물·프로젝트 고유 무시 규칙이 통째로 사라진다.
|
|
447
|
-
* → 없으면 복사, 있으면 *빠진 줄만 덧붙인다*. 기존 줄은 절대 건드리지 않는다.
|
|
448
|
-
* @returns {{ merged: boolean, added: string[] }}
|
|
449
|
-
*/
|
|
450
|
-
function mergeGitignore(srcPath, dstPath) {
|
|
451
|
-
if (!fs.existsSync(dstPath)) {
|
|
452
|
-
copyFile(srcPath, dstPath);
|
|
453
|
-
return { merged: false, added: [] };
|
|
454
|
-
}
|
|
455
|
-
const cur = fs.readFileSync(dstPath, 'utf8');
|
|
456
|
-
const have = new Set(cur.split('\n').map((l) => l.trim()));
|
|
457
|
-
const added = fs
|
|
458
|
-
.readFileSync(srcPath, 'utf8')
|
|
459
|
-
.split('\n')
|
|
460
|
-
.map((l) => l.trim())
|
|
461
|
-
.filter((l) => l && !l.startsWith('#') && !have.has(l));
|
|
462
|
-
if (added.length === 0) return { merged: true, added: [] };
|
|
463
|
-
const lead = cur.endsWith('\n') ? '' : '\n';
|
|
464
|
-
fs.appendFileSync(dstPath, `${lead}\n# taskery 템플릿 추가분\n${added.join('\n')}\n`);
|
|
465
|
-
return { merged: true, added };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
module.exports = {
|
|
469
|
-
ENTRY_DOCS_MARKER,
|
|
470
|
-
mergeGitignore,
|
|
471
|
-
isBefore070,
|
|
472
|
-
splitSections,
|
|
473
|
-
parseDoc,
|
|
474
|
-
boilerplateQuotes,
|
|
475
|
-
composeAgentsDoc,
|
|
476
|
-
ingestActivePlan,
|
|
477
|
-
retireLegacyAgentGuide,
|
|
478
|
-
recomposeEntryDocs,
|
|
479
|
-
planGitignore,
|
|
480
|
-
planTestGuide,
|
|
481
|
-
};
|
package/bin/set-status.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// task 상태 전이 CLI — 7×7 유효전이 검증 후 헤더 상태 컬럼 치환 (잘못된 전이 거부)
|
|
3
|
-
/**
|
|
4
|
-
* bin/set-status.js
|
|
5
|
-
* `npx @angar2/taskery set-status <TASK-NNN> <state>` — task 헤더 상태 전이.
|
|
6
|
-
*
|
|
7
|
-
* 현재 상태를 코드로 읽고 TASK_DOC_RULE §1.2 유효전이표로 검증 → 통과 시 헤더 치환.
|
|
8
|
-
* 잘못된 전이(예: draft→closed)는 거부. 같은 상태 재지정은 idempotent no-op.
|
|
9
|
-
* task 문서 위치는 resolveTaskDocPath가 .gitignore 케이스로 단일 판정.
|
|
10
|
-
*
|
|
11
|
-
* closed 전이는 본 CLI가 거부 — `/task-close`(close 파이프라인)가 내부 setStatus로 처리.
|
|
12
|
-
* stdout: { taskNum, from, to, changed, path }. 실패 시 stderr + exit 1.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const path = require('path');
|
|
16
|
-
|
|
17
|
-
const {
|
|
18
|
-
MANIFEST_NAME,
|
|
19
|
-
readManifest,
|
|
20
|
-
getMainWorktreePath,
|
|
21
|
-
gitCapture,
|
|
22
|
-
resolveTaskDocPath,
|
|
23
|
-
setStatus,
|
|
24
|
-
TRANSITIONS,
|
|
25
|
-
getActiveTasks,
|
|
26
|
-
getWorktreePath,
|
|
27
|
-
getProjectId,
|
|
28
|
-
} = require('./lib');
|
|
29
|
-
|
|
30
|
-
async function main() {
|
|
31
|
-
const [taskArg, state] = process.argv.slice(2);
|
|
32
|
-
|
|
33
|
-
if (!taskArg || !state) {
|
|
34
|
-
console.error('사용법: npx @angar2/taskery set-status <TASK-NNN> <state>');
|
|
35
|
-
console.error(`state: ${Object.keys(TRANSITIONS).join(' | ')}`);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
const m = String(taskArg).match(/(\d+)/);
|
|
39
|
-
if (!m) {
|
|
40
|
-
console.error(`TASK 번호 파싱 실패 (받음: ${taskArg}). 예: TASK-007`);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
const taskNum = parseInt(m[1], 10);
|
|
44
|
-
|
|
45
|
-
if (!Object.prototype.hasOwnProperty.call(TRANSITIONS, state)) {
|
|
46
|
-
console.error(`알 수 없는 상태: ${state} (유효: ${Object.keys(TRANSITIONS).join(', ')})`);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
if (state === 'closed') {
|
|
50
|
-
console.error(`closed 전이는 set-status가 아니라 '/task-close'(close 파이프라인)가 담당.`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let mainWt;
|
|
55
|
-
try {
|
|
56
|
-
mainWt = getMainWorktreePath();
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.error(`taskery set-status 실패: ${e.message}`);
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
if (!readManifest(path.join(mainWt, MANIFEST_NAME))) {
|
|
62
|
-
console.error(`${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 현재 작업트리 toplevel = 미등록 케이스의 문서 base.
|
|
67
|
-
const wtPath = gitCapture(process.cwd(), ['rev-parse', '--show-toplevel'], { allowFail: true }) || mainWt;
|
|
68
|
-
|
|
69
|
-
let resolved = resolveTaskDocPath(mainWt, wtPath, { taskNum });
|
|
70
|
-
|
|
71
|
-
// 미등록 케이스에서 메인 워크트리(또는 다른 task의 워크트리)에서 호출하면 문서가 그 트리에 없다.
|
|
72
|
-
// 호출 위치를 강요하는 대신 SSoT에서 해당 task의 워크트리를 역조회해 한 번 더 찾는다
|
|
73
|
-
// — 스킬이 "호출 위치 자유"를 전제하므로 CLI가 그 약속을 지켜야 한다.
|
|
74
|
-
if (!resolved) {
|
|
75
|
-
const hit = getActiveTasks(mainWt).find((t) => t.taskNum === taskNum);
|
|
76
|
-
if (hit) {
|
|
77
|
-
const owner = getWorktreePath(getProjectId(mainWt), hit);
|
|
78
|
-
if (owner !== wtPath) resolved = resolveTaskDocPath(mainWt, owner, { taskNum });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!resolved) {
|
|
83
|
-
console.error(
|
|
84
|
-
`TASK-${String(taskNum).padStart(3, '0')} 문서를 찾지 못함 (.project/tasks/*/ 아래 단일 파일/폴더 승격 모두 탐색 + 진행중 task 워크트리 역조회).`,
|
|
85
|
-
);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const result = await setStatus(resolved.file, state);
|
|
91
|
-
process.stdout.write(JSON.stringify({ taskNum, ...result }) + '\n');
|
|
92
|
-
} catch (e) {
|
|
93
|
-
console.error(`taskery set-status 실패: ${e.message}`);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
main();
|