@angar2/taskery 0.1.3 → 0.2.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 CHANGED
@@ -57,10 +57,13 @@ draft → planned → developing → developed → testing → tested → closed
57
57
  # 새 프로젝트
58
58
  npx -p @angar2/taskery create-taskery <project-name>
59
59
 
60
- # 기존 프로젝트에 도입
60
+ # 기존 프로젝트에 도입 (init 시 에이전트 플랫폼 선택)
61
61
  cd <project-name>
62
62
  npx @angar2/taskery init
63
63
 
64
+ # 다른 플랫폼 자산 추가 (예: 기존 설치에 Codex 추가)
65
+ npx @angar2/taskery add codex
66
+
64
67
  # 최신 버전 머지 갱신
65
68
  npx @angar2/taskery update
66
69
 
@@ -79,10 +82,13 @@ npm install -g @angar2/taskery
79
82
  # 새 프로젝트
80
83
  create-taskery <project-name>
81
84
 
82
- # 기존 프로젝트에 도입
85
+ # 기존 프로젝트에 도입 (init 시 에이전트 플랫폼 선택)
83
86
  cd <project-name>
84
87
  taskery init
85
88
 
89
+ # 다른 플랫폼 자산 추가 (예: 기존 설치에 Codex 추가)
90
+ taskery add codex
91
+
86
92
  # 최신 버전 머지 갱신
87
93
  taskery update
88
94
 
@@ -95,7 +101,7 @@ taskery prune
95
101
 
96
102
  ## 패키지 디렉토리 구조
97
103
 
98
- `taskery init` 직후 골격:
104
+ `taskery init` 직후 골격 (아래는 Claude Code 선택 시 예시):
99
105
 
100
106
  ```
101
107
  my-app/
@@ -128,6 +134,15 @@ my-app/
128
134
  └─ FRICTION_LOG.md # taskery 불편사항 누적 로그
129
135
  ```
130
136
 
137
+ `.project/`는 플랫폼 무관 공통 자산이다. 진입 문서와 에이전트 자산 위치는 선택한 플랫폼에 따라 갈린다.
138
+
139
+ | 플랫폼 | 진입 문서 | 스킬 위치 | hook 위치 | hook 등록 |
140
+ |--------|----------|----------|----------|----------|
141
+ | Claude Code | `CLAUDE.md` | `.claude/skills/` | `.claude/hooks/` | `.claude/settings.json` |
142
+ | Codex | `AGENTS.md` | `.agents/skills/` | `.codex/hooks/` | `.codex/config.toml` |
143
+
144
+ > 스킬 9종과 `git-guard.sh`는 양 플랫폼 동일 자산이다. 진입 문서, hook 등록 방식, 완료 보호 hook(`closed-immutable.sh`)의 구현만 플랫폼별로 다르다. `init`에서 둘 다 선택하면 폴더가 갈려 충돌 없이 공존한다.
145
+
131
146
  ---
132
147
 
133
148
  ## 멀티세션 (병렬 작업)
@@ -196,7 +211,7 @@ my-app/
196
211
  | `git-guard.sh` | git 명령 실행 직전 | 주력 브랜치 직접 커밋 / `--force` / `--no-verify` / 강제 브랜치 삭제 / `reset --hard` / `clean -fd` |
197
212
  | `closed-immutable.sh` | 파일 수정 직전 | 완료(`closed`)된 task.md 본 파일 재수정 (관련 spec-diff·스크린샷은 자유) |
198
213
 
199
- hook은 catastrophic 사고만 차단한다. 정상 흐름에는 간섭하지 않는다.
214
+ hook은 catastrophic 사고만 차단한다. 정상 흐름에는 간섭하지 않는다. 등록 방식은 플랫폼별로 다르다 — Claude Code는 `.claude/settings.json`, Codex는 `.codex/config.toml`(최초 1회 `/hooks` 신뢰 승인 필요).
200
215
 
201
216
  > 각 hook의 영역 분리 정신, 등록 매칭, 차단 정책, 예외 처리 절차는 [HOOKS.md](https://github.com/angar2/taskery/blob/main/plan/HOOKS.md)에서 확인 가능
202
217
 
@@ -210,7 +225,7 @@ cd <project-name>
210
225
  npx @angar2/taskery init
211
226
  ```
212
227
 
213
- 이후 메인 세션에 진입(`CLAUDE.md` 자동 정독)하여 다음 시퀀스로 호출한다. task 5 스킬은 호출과 동시에 상태를 전이시킨다.
228
+ 이후 메인 세션에 진입(진입 문서 `CLAUDE.md` 또는 `AGENTS.md` 자동 정독)하여 다음 시퀀스로 호출한다. task 5 스킬은 호출과 동시에 상태를 전이시킨다.
214
229
 
215
230
  ```
216
231
  /project-init # 진입 메타 문서와 디렉토리 골격 생성
package/bin/add.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // bin/add.js
3
+ // `npx @angar2/taskery add <platform>` — 기존 설치 리포에 다른 플랫폼 자산 추가.
4
+ //
5
+ // 동작:
6
+ // 1. <platform> 인자 검증 (claude | codex)
7
+ // 2. .taskery-manifest.json 필수 — 없으면 'init' 안내
8
+ // 3. 이미 manifest.platforms에 있으면 변경 없이 종료
9
+ // 4. 해당 플랫폼 전용 자산만 카피 (공통 .project/는 init이 이미 설치)
10
+ // 5. manifest.platforms push + files 갱신
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const readline = require('readline');
15
+
16
+ const {
17
+ MANIFEST_NAME,
18
+ findTemplateDir,
19
+ walkTemplate,
20
+ copyFile,
21
+ writeManifest,
22
+ readManifest,
23
+ getPackageVersion,
24
+ resolveInstallPlan,
25
+ PLATFORMS,
26
+ } = require('./lib');
27
+
28
+ async function confirm(msg) {
29
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30
+ return new Promise((resolve) => {
31
+ rl.question(`${msg} (y/N) `, (ans) => {
32
+ rl.close();
33
+ resolve(ans.trim().toLowerCase() === 'y');
34
+ });
35
+ });
36
+ }
37
+
38
+ async function main() {
39
+ const cwd = process.cwd();
40
+ const platform = (process.argv[2] || '').trim();
41
+
42
+ // 1. 플랫폼 인자 검증
43
+ if (!PLATFORMS.includes(platform)) {
44
+ console.error(
45
+ `taskery add: 플랫폼 인자 필요 (${PLATFORMS.join(' | ')}). 예: npx @angar2/taskery add codex`,
46
+ );
47
+ process.exit(1);
48
+ }
49
+
50
+ // 2. manifest 필수
51
+ const manifestPath = path.join(cwd, MANIFEST_NAME);
52
+ const manifest = readManifest(manifestPath);
53
+ if (!manifest) {
54
+ console.error(
55
+ `taskery add: ${MANIFEST_NAME} 없음 — 'npx @angar2/taskery init' 먼저 실행 필요.`,
56
+ );
57
+ process.exit(1);
58
+ }
59
+
60
+ // 3. 이미 설치된 플랫폼?
61
+ const platforms = Array.isArray(manifest.platforms) ? manifest.platforms : ['claude'];
62
+ if (platforms.includes(platform)) {
63
+ console.log(`taskery add: '${platform}' 이미 설치됨 — 변경 없음.`);
64
+ process.exit(0);
65
+ }
66
+
67
+ // 4. 해당 플랫폼 전용 + 공통(shared/) 자산만 카피 (agnostic은 init이 이미 설치)
68
+ const templateDir = findTemplateDir();
69
+ const templateFiles = walkTemplate(templateDir);
70
+ const plan = resolveInstallPlan(templateFiles, [platform], { includeAgnostic: false });
71
+
72
+ console.log(`\ntaskery v${getPackageVersion()} add ${platform}`);
73
+ console.log(`카피 대상: ${plan.length}개 파일\n`);
74
+
75
+ const files = { ...(manifest.files || {}) };
76
+ let copied = 0;
77
+ let skipped = 0;
78
+
79
+ for (const { templateRel, installRel, hash } of plan) {
80
+ const src = path.join(templateDir, templateRel);
81
+ const dst = path.join(cwd, installRel);
82
+ if (fs.existsSync(dst)) {
83
+ const ok = await confirm(` '${installRel}' 이미 존재 — 덮어쓸까?`);
84
+ if (!ok) {
85
+ console.log(` skip: ${installRel}`);
86
+ skipped++;
87
+ continue;
88
+ }
89
+ }
90
+ copyFile(src, dst);
91
+ files[installRel] = { hash, core: true, managed: true };
92
+ copied++;
93
+ console.log(` copy: ${installRel}`);
94
+ }
95
+
96
+ // 5. manifest 갱신
97
+ manifest.platforms = [...platforms, platform];
98
+ manifest.files = files;
99
+ manifest.updated_at = new Date().toISOString();
100
+ writeManifest(manifest, manifestPath);
101
+
102
+ console.log(`\n✅ taskery add ${platform} 완료 (카피 ${copied} / 스킵 ${skipped})`);
103
+ console.log(` platforms: ${manifest.platforms.join(', ')}`);
104
+ if (platform === 'codex') {
105
+ console.log(` 다음: Codex 최초 1회 '/hooks'로 hook trust 승인 (.codex/config.toml)`);
106
+ console.log(` AGENTS.md 정독 (코덱스 진입점)`);
107
+ }
108
+ }
109
+
110
+ main().catch((e) => {
111
+ console.error(`taskery add 실패: ${e.message}`);
112
+ process.exit(1);
113
+ });
package/bin/init.js CHANGED
@@ -27,6 +27,8 @@ const {
27
27
  generateProjectId,
28
28
  DEFAULT_STALE_DAYS,
29
29
  DEFAULT_LOCK_TIMEOUT_MS,
30
+ resolveInstallPlan,
31
+ gitignorePatternsFor,
30
32
  } = require('./lib');
31
33
 
32
34
  async function confirm(msg) {
@@ -39,6 +41,23 @@ async function confirm(msg) {
39
41
  });
40
42
  }
41
43
 
44
+ // 에이전트 플랫폼 선택 — 고른 것만 독립 설치 (공통 .project/는 항상)
45
+ async function selectPlatforms() {
46
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
47
+ return new Promise((resolve) => {
48
+ rl.question(
49
+ `\n에이전트 플랫폼 선택:\n 1) Claude Code\n 2) Codex\n 3) 둘 다\n선택 (1/2/3, 기본 1): `,
50
+ (ans) => {
51
+ rl.close();
52
+ const c = ans.trim() || '1';
53
+ if (c === '2') resolve(['codex']);
54
+ else if (c === '3') resolve(['claude', 'codex']);
55
+ else resolve(['claude']);
56
+ },
57
+ );
58
+ });
59
+ }
60
+
42
61
  async function main() {
43
62
  const cwd = process.cwd();
44
63
  const templateDir = findTemplateDir();
@@ -55,26 +74,28 @@ async function main() {
55
74
  }
56
75
  }
57
76
 
58
- // 2. template/ 정독 + 해시 맵
77
+ // 2. 플랫폼 선택 + template/ 정독 → 설치 계획 전개 (공통 shared/는 플랫폼 경로로 매핑)
78
+ const platforms = await selectPlatforms();
59
79
  const templateFiles = walkTemplate(templateDir);
60
- const fileEntries = Object.entries(templateFiles);
80
+ const plan = resolveInstallPlan(templateFiles, platforms);
61
81
 
62
82
  console.log(`\ntaskery v${getPackageVersion()} init`);
63
83
  console.log(`대상: ${cwd}`);
64
- console.log(`카피 대상: ${fileEntries.length}개 파일\n`);
84
+ console.log(`플랫폼: ${platforms.join(', ')}`);
85
+ console.log(`카피 대상: ${plan.length}개 파일\n`);
65
86
 
66
- // 3. 카피 + manifest 빌드
87
+ // 3. 카피 + manifest 빌드 (manifest 키 = 설치 경로 installRel)
67
88
  const manifestFiles = {};
68
89
  let copied = 0;
69
90
  let skipped = 0;
70
91
 
71
- for (const [rel, hash] of fileEntries) {
72
- const src = path.join(templateDir, rel);
73
- const dst = path.join(cwd, rel);
92
+ for (const { templateRel, installRel, hash } of plan) {
93
+ const src = path.join(templateDir, templateRel);
94
+ const dst = path.join(cwd, installRel);
74
95
 
75
96
  // .local.md suffix는 사용자 영역 — taskery template에는 없어야 정상이지만 방어적으로 스킵
76
- if (isLocalOverride(rel)) {
77
- console.log(` skip (.local override 영역): ${rel}`);
97
+ if (isLocalOverride(installRel)) {
98
+ console.log(` skip (.local override 영역): ${installRel}`);
78
99
  skipped++;
79
100
  continue;
80
101
  }
@@ -82,18 +103,18 @@ async function main() {
82
103
  // 사용자 .local.md 충돌 검사 — 사용자가 직접 만든 *.local.md 보호
83
104
  // 본 init은 신규 설치 default, 충돌 거의 없지만 update 흐름과 일관성 유지
84
105
  if (fs.existsSync(dst)) {
85
- const ok = await confirm(` '${rel}' 이미 존재 — 덮어쓸까?`);
106
+ const ok = await confirm(` '${installRel}' 이미 존재 — 덮어쓸까?`);
86
107
  if (!ok) {
87
- console.log(` skip: ${rel}`);
108
+ console.log(` skip: ${installRel}`);
88
109
  skipped++;
89
110
  continue;
90
111
  }
91
112
  }
92
113
 
93
114
  copyFile(src, dst);
94
- manifestFiles[rel] = { hash, core: true, managed: true };
115
+ manifestFiles[installRel] = { hash, core: true, managed: true };
95
116
  copied++;
96
- console.log(` copy: ${rel}`);
117
+ console.log(` copy: ${installRel}`);
97
118
  }
98
119
 
99
120
  // 4. manifest 작성 — 멀티세션(0.1.2+): projectId / stale_days / lock_timeout_ms 포함
@@ -101,6 +122,7 @@ async function main() {
101
122
  version: getPackageVersion(),
102
123
  installed_at: new Date().toISOString(),
103
124
  projectId: generateProjectId(),
125
+ platforms,
104
126
  stale_days: DEFAULT_STALE_DAYS,
105
127
  lock_timeout_ms: DEFAULT_LOCK_TIMEOUT_MS,
106
128
  files: manifestFiles,
@@ -111,14 +133,9 @@ async function main() {
111
133
  // 공개 repo면 내부 워크플로 파일 노출 회피 — taskery 내부 영역 .gitignore 등록 제안.
112
134
  // 사용자 NO면 패스 (그대로 둠). 이미 등록되어 있으면 스킵.
113
135
  const gitignorePath = path.join(cwd, '.gitignore');
114
- const taskeryPatterns = [
115
- '.project/',
116
- '.claude/',
117
- 'CLAUDE.md',
118
- '.taskery-manifest.json',
119
- ];
136
+ const taskeryPatterns = gitignorePatternsFor(platforms);
120
137
  const addGitignore = await confirm(
121
- `\ntaskery 내부 파일(.project/, .claude/, CLAUDE.md, .taskery-manifest.json)을 .gitignore에 등록할까?\n (공개 repo면 권장 — 내부 워크플로 파일 노출 회피)`,
138
+ `\ntaskery 내부 파일(${taskeryPatterns.join(', ')})을 .gitignore에 등록할까?\n (공개 repo면 권장 — 내부 워크플로 파일 노출 회피)`,
122
139
  );
123
140
  if (addGitignore) {
124
141
  const existing = fs.existsSync(gitignorePath)
@@ -147,12 +164,15 @@ async function main() {
147
164
  // 5. 결과 보고
148
165
  console.log(`\n✅ taskery init 완료`);
149
166
  console.log(` 카피: ${copied}개 / 스킵: ${skipped}개`);
167
+ console.log(` 플랫폼: ${platforms.join(', ')}`);
150
168
  console.log(` manifest: ${MANIFEST_NAME}\n`);
169
+ const entryFile = platforms.includes('claude') ? 'CLAUDE.md' : 'AGENTS.md';
151
170
  console.log(`다음 단계:`);
152
- console.log(` 1. CLAUDE.md 정독 + 프로젝트 메타 + 검증 명령 채우기`);
153
- console.log(` 2. Claude Code 진입 → '/project-init' 호출 → '.project/' 진입 문서 골격 생성`);
154
- console.log(` 3. '/plan-init <vX.X>' 호출 → 9 기획 문서 작성`);
155
- console.log(` 4. '/task-init' 호출 → 첫 task 시작\n`);
171
+ console.log(` 1. ${entryFile} 정독 + 프로젝트 메타 + 검증 명령 채우기`);
172
+ if (platforms.includes('codex')) {
173
+ console.log(` 2. Codex 최초 1회 '/hooks'로 hook trust 승인 (.codex/config.toml — git-guard / closed-immutable)`);
174
+ }
175
+ console.log(` 3. 에이전트 진입 → '/project-init' → '/plan-init <vX.X>' → '/task-init'\n`);
156
176
  }
157
177
 
158
178
  main().catch((e) => {
package/bin/lib.js CHANGED
@@ -43,6 +43,81 @@ const { execFileSync } = require('child_process');
43
43
  const LOCAL_SUFFIX = '.local.md';
44
44
  const MANIFEST_NAME = '.taskery-manifest.json';
45
45
 
46
+ // === 멀티 플랫폼 (0.2.0+) ===
47
+ // 공통 소스 + 플랫폼별 조립 설치: 공통 자산(shared/)은 단일 소스로 보관하고
48
+ // 설치 시 고른 플랫폼의 실제 경로로 매핑 복사한다. 플랫폼 고유 자산은 그대로 복사.
49
+ const PLATFORMS = ['claude', 'codex'];
50
+ const DEFAULT_PLATFORMS = ['claude']; // 기존(0.1.x) 설치 마이그레이션 기본값
51
+
52
+ // shared/ 공통 자산의 플랫폼별 설치 경로 매핑.
53
+ // shared 하위 첫 세그먼트(skills|hooks) → 각 플랫폼의 실제 설치 루트.
54
+ // 스킬·git-guard 본문은 양 플랫폼 동일(검증 완료) — 단일 소스로 보관, 설치 때 갈라 깐다.
55
+ const SHARED_DEST = {
56
+ claude: { skills: '.claude/skills', hooks: '.claude/hooks' },
57
+ codex: { skills: '.agents/skills', hooks: '.codex/hooks' },
58
+ };
59
+
60
+ // template 상대경로 → 소속 ('agnostic' | 'shared' | 'claude' | 'codex')
61
+ function platformOf(relPath) {
62
+ if (relPath.startsWith('shared/')) return 'shared'; // 공통 — 설치 시 플랫폼별 매핑
63
+ if (relPath === 'CLAUDE.md' || relPath.startsWith('.claude/')) return 'claude';
64
+ if (relPath === 'AGENTS.md' || relPath.startsWith('.codex/')) return 'codex';
65
+ return 'agnostic'; // .project/, .gitignore 등 — 항상 설치
66
+ }
67
+
68
+ // shared/ 상대경로를 특정 플랫폼의 설치 경로로 변환.
69
+ // 예: 'shared/skills/task-init/SKILL.md' + 'codex' → '.agents/skills/task-init/SKILL.md'
70
+ function mapSharedDest(relPath, platform) {
71
+ const rest = relPath.slice('shared/'.length); // 'skills/task-init/SKILL.md'
72
+ const sep = rest.indexOf('/');
73
+ const top = sep === -1 ? rest : rest.slice(0, sep); // 'skills'
74
+ const tail = sep === -1 ? '' : rest.slice(sep); // '/task-init/SKILL.md'
75
+ const root = SHARED_DEST[platform] && SHARED_DEST[platform][top];
76
+ if (!root) {
77
+ throw new Error(`shared 자산 매핑 미정의: ${relPath} (platform=${platform})`);
78
+ }
79
+ return root + tail;
80
+ }
81
+
82
+ // template 파일맵을 설치 계획으로 전개 — [{ templateRel, installRel, hash }].
83
+ // - agnostic: 그대로 (includeAgnostic=false면 제외 — add 용)
84
+ // - shared: 선택 플랫폼마다 매핑 경로로 1건씩 전개 (1:N)
85
+ // - 플랫폼 고유: 선택 플랫폼이면 그대로
86
+ function resolveInstallPlan(templateFiles, platforms, { includeAgnostic = true } = {}) {
87
+ const plan = [];
88
+ for (const [rel, hash] of Object.entries(templateFiles)) {
89
+ const p = platformOf(rel);
90
+ if (p === 'agnostic') {
91
+ if (includeAgnostic) plan.push({ templateRel: rel, installRel: rel, hash });
92
+ } else if (p === 'shared') {
93
+ for (const platform of platforms) {
94
+ plan.push({ templateRel: rel, installRel: mapSharedDest(rel, platform), hash });
95
+ }
96
+ } else if (platforms.includes(p)) {
97
+ plan.push({ templateRel: rel, installRel: rel, hash });
98
+ }
99
+ }
100
+ return plan;
101
+ }
102
+
103
+ // .gitignore 등록 패턴 (플랫폼별)
104
+ const GITIGNORE_PATTERNS = {
105
+ agnostic: ['.project/', '.taskery-manifest.json'],
106
+ claude: ['.claude/', 'CLAUDE.md'],
107
+ codex: ['.codex/', '.agents/', 'AGENTS.md'],
108
+ };
109
+
110
+ // 선택 플랫폼 + agnostic의 gitignore 패턴 합성 (중복 제거)
111
+ function gitignorePatternsFor(platforms) {
112
+ const out = [...GITIGNORE_PATTERNS.agnostic];
113
+ for (const p of platforms) {
114
+ for (const pat of GITIGNORE_PATTERNS[p] || []) {
115
+ if (!out.includes(pat)) out.push(pat);
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+
46
121
  function findTemplateDir() {
47
122
  // 패키지 루트는 이 lib.js 파일의 부모(bin/)의 부모
48
123
  const pkgRoot = path.resolve(__dirname, '..');
@@ -461,6 +536,15 @@ async function markBacklogChecked(mainWtPath, blId, taskNum) {
461
536
  module.exports = {
462
537
  LOCAL_SUFFIX,
463
538
  MANIFEST_NAME,
539
+ // 멀티 플랫폼 (0.2.0+)
540
+ PLATFORMS,
541
+ DEFAULT_PLATFORMS,
542
+ SHARED_DEST,
543
+ platformOf,
544
+ mapSharedDest,
545
+ resolveInstallPlan,
546
+ GITIGNORE_PATTERNS,
547
+ gitignorePatternsFor,
464
548
  findTemplateDir,
465
549
  sha256,
466
550
  walkTemplate,
package/bin/taskery.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * `npx @angar2/taskery <subcommand>` — 진입점 dispatcher.
5
5
  *
6
6
  * 서브커맨드:
7
- * init — 현재 디렉토리에 taskery 자산 카피 + manifest 생성
7
+ * init — 현재 디렉토리에 taskery 자산 카피 + manifest 생성 (플랫폼 선택)
8
+ * add — 기존 설치 리포에 플랫폼 자산 추가 (claude | codex)
8
9
  * update — 최신 버전 fetch + manifest 비교 + 머지 갱신
9
10
  * status — 멀티세션 현황 요약 (진행중 태스크 / 워크트리 / 머지 락, 0.1.2+)
10
11
  * prune — stale 워크트리 / 브랜치 대화형 정리 (0.1.2+)
@@ -28,7 +29,8 @@ function help() {
28
29
  console.log(`taskery v${require('./lib').getPackageVersion()}
29
30
 
30
31
  사용법:
31
- npx @angar2/taskery init 현재 디렉토리에 taskery 자산 설치
32
+ npx @angar2/taskery init 현재 디렉토리에 taskery 자산 설치 (플랫폼 선택)
33
+ npx @angar2/taskery add <p> 기존 설치에 플랫폼 추가 (claude | codex)
32
34
  npx @angar2/taskery update 최신 버전 fetch + 머지 갱신
33
35
  npx @angar2/taskery status 멀티세션 현황 (진행중 태스크 / 워크트리 / 머지 락)
34
36
  npx @angar2/taskery prune stale 워크트리 / 브랜치 대화형 정리
@@ -45,6 +47,9 @@ switch (sub) {
45
47
  case 'init':
46
48
  runScript('init.js');
47
49
  break;
50
+ case 'add':
51
+ runScript('add.js');
52
+ break;
48
53
  case 'update':
49
54
  runScript('update.js');
50
55
  break;
package/bin/update.js CHANGED
@@ -34,6 +34,8 @@ const {
34
34
  generateProjectId,
35
35
  DEFAULT_STALE_DAYS,
36
36
  DEFAULT_LOCK_TIMEOUT_MS,
37
+ DEFAULT_PLATFORMS,
38
+ resolveInstallPlan,
37
39
  } = require('./lib');
38
40
 
39
41
  async function confirm(msg) {
@@ -60,8 +62,17 @@ async function main() {
60
62
  process.exit(1);
61
63
  }
62
64
 
63
- // 2. template/ 새 해시 맵
64
- const newTemplateFiles = walkTemplate(templateDir);
65
+ // 2. 설치된 플랫폼 자산만 갱신 대상 (platforms 누락 시 마이그레이션 기본값)
66
+ // 설치 계획(installRel 기준)으로 전개 — shared/는 플랫폼 경로로 매핑된 상태.
67
+ // newTemplateFiles: installRel → { hash(소스), templateRel(복사 원본) }
68
+ const platforms = Array.isArray(oldManifest.platforms)
69
+ ? oldManifest.platforms
70
+ : DEFAULT_PLATFORMS;
71
+ const plan = resolveInstallPlan(walkTemplate(templateDir), platforms);
72
+ const newTemplateFiles = {};
73
+ for (const it of plan) {
74
+ newTemplateFiles[it.installRel] = { hash: it.hash, templateRel: it.templateRel };
75
+ }
65
76
  const oldFiles = oldManifest.files || {};
66
77
 
67
78
  console.log(`\ntaskery update`);
@@ -71,8 +82,8 @@ async function main() {
71
82
  const newFiles = {};
72
83
  const summary = { unchanged: 0, new: 0, autoUpdated: 0, customizedReplaced: 0, skipped: 0, removed: 0 };
73
84
 
74
- // 3. 새 template 파일 분기
75
- for (const [rel, newHash] of Object.entries(newTemplateFiles)) {
85
+ // 3. 새 template 파일 분기 (rel = 설치 경로 installRel)
86
+ for (const [rel, { hash: newHash, templateRel }] of Object.entries(newTemplateFiles)) {
76
87
  if (isLocalOverride(rel)) {
77
88
  // template에 .local.md 들어가면 안 되지만 방어적 스킵
78
89
  console.log(` skip (.local override 영역): ${rel}`);
@@ -80,7 +91,7 @@ async function main() {
80
91
  continue;
81
92
  }
82
93
 
83
- const src = path.join(templateDir, rel);
94
+ const src = path.join(templateDir, templateRel);
84
95
  const dst = path.join(cwd, rel);
85
96
  const oldEntry = oldFiles[rel];
86
97
 
@@ -149,8 +160,8 @@ async function main() {
149
160
  }
150
161
  }
151
162
 
152
- // 5. manifest 갱신 — 멀티세션(0.1.2+) 필드 누락 시 자동 마이그레이션
153
- const migration = { projectId: false, stale_days: false, lock_timeout_ms: false };
163
+ // 5. manifest 갱신 — 멀티세션(0.1.2+) / 플랫폼(0.2.0+) 필드 누락 시 자동 마이그레이션
164
+ const migration = { projectId: false, stale_days: false, lock_timeout_ms: false, platforms: false };
154
165
  const projectId = oldManifest.projectId || (migration.projectId = true, generateProjectId());
155
166
  const stale_days =
156
167
  typeof oldManifest.stale_days === 'number'
@@ -160,12 +171,14 @@ async function main() {
160
171
  typeof oldManifest.lock_timeout_ms === 'number'
161
172
  ? oldManifest.lock_timeout_ms
162
173
  : (migration.lock_timeout_ms = true, DEFAULT_LOCK_TIMEOUT_MS);
174
+ if (!Array.isArray(oldManifest.platforms)) migration.platforms = true;
163
175
 
164
176
  const newManifest = {
165
177
  version: getPackageVersion(),
166
178
  installed_at: oldManifest.installed_at,
167
179
  updated_at: new Date().toISOString(),
168
180
  projectId,
181
+ platforms,
169
182
  stale_days,
170
183
  lock_timeout_ms,
171
184
  files: newFiles,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@angar2/taskery",
3
- "version": "0.1.3",
4
- "description": "Lightweight task system for Claude Code — 1 메인 세션(또는 멀티세션) + 스킬 9종 + catastrophic hook 안전망 2종",
3
+ "version": "0.2.1",
4
+ "description": "Lightweight task system for Claude Code & Codex — 1 메인 세션(또는 멀티세션) + 스킬 9종 + catastrophic hook 안전망 2종",
5
5
  "bin": {
6
6
  "taskery": "bin/taskery.js",
7
7
  "create-taskery": "bin/create.js"
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "keywords": [
20
20
  "claude-code",
21
+ "codex",
21
22
  "task-management",
22
23
  "ai-coding-workflow",
23
24
  "skills",
@@ -0,0 +1,20 @@
1
+ # taskery 격리 검증 서브에이전트 — /task-test가 spawn하는 독립 tester
2
+ #
3
+ # 클로드의 "Task tool 격리 세션"에 대응하는 코덱스 서브에이전트 정의.
4
+ # 메인 세션의 plan/dev 컨텍스트(확증편향) 없이 task.md만 보고 독립 검증한다.
5
+ #
6
+ # ⚠ model 명시 필수 — 생략하면 자식 모델 resolve가 service tier validation에서 실패한다.
7
+ # 환경의 사용 가능 모델에 맞게 조정한다(기본 gpt-5.5).
8
+
9
+ name = "task-tester"
10
+ description = "격리 검증 에이전트. 메인의 확증편향 없이 task.md만 보고 Test Plan을 독립 수행한다. /task-test가 spawn한다."
11
+ model = "gpt-5.5"
12
+ sandbox_mode = "read-only"
13
+ developer_instructions = """
14
+ 너는 격리된 tester다. 메인 세션의 가정을 일체 신뢰하지 말고, 넘겨받은 task.md 하나만 보고 자기완결적으로 검증하라.
15
+ - task.md의 ## Test Plan 시나리오를 순서대로 그대로 수행한다(임의 추가/스킵/변형 금지).
16
+ - [AUTO] 시나리오는 실제 명령을 실행하고 로그를 확보한다. 실행 없이 pass 처리 금지.
17
+ - [USER] 시나리오는 UNCERTAIN으로 분류하고 사용자 직접 검수 항목으로 리턴한다.
18
+ - 코드와 동작만 신뢰한다. "잘 될 것"이라는 가정 금지. 코드 파일 수정 금지.
19
+ - 검증을 마치면 task-test SKILL.md의 결과 형식(종합 판정 / 시나리오별 결과 / 검증·테스트 명령 결과)을 그대로 리턴한다.
20
+ """
@@ -0,0 +1,17 @@
1
+ # taskery Codex 프로젝트 설정 — PreToolUse hook 2종 등록 (git 안전망 + closed 보호)
2
+ #
3
+ # hook 스크립트 경로는 git 리포 루트 기준으로 해석한다($(git rev-parse --show-toplevel)).
4
+ # 설치 위치와 무관하게 동작하도록 절대경로 하드코딩을 피한다.
5
+ # 최초 1회 코덱스 `/hooks`로 trust 승인이 필요하다(승인 전 hook은 자동 skip).
6
+
7
+ [[hooks.PreToolUse]]
8
+ matcher = "^Bash$"
9
+ [[hooks.PreToolUse.hooks]]
10
+ type = "command"
11
+ command = 'bash "$(git rev-parse --show-toplevel)/.codex/hooks/git-guard.sh"'
12
+
13
+ [[hooks.PreToolUse]]
14
+ matcher = "apply_patch"
15
+ [[hooks.PreToolUse.hooks]]
16
+ type = "command"
17
+ command = 'bash "$(git rev-parse --show-toplevel)/.codex/hooks/closed-immutable.sh"'
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # .codex/hooks/closed-immutable.sh
3
+ # Codex PreToolUse(apply_patch) 훅 — closed 상태 task.md 재수정 차단 (코덱스 전용판)
4
+ #
5
+ # 클로드판(.claude/hooks/closed-immutable.sh)과의 차이:
6
+ # - 클로드: tool_input.file_path 1개를 읽음 (Write|Edit 도구)
7
+ # - 코덱스: 파일 편집이 apply_patch 도구 1개로 처리되며 tool_input.command 안에
8
+ # patch envelope 전체(`*** Begin Patch` ... `*** Update File: <경로>` ...)가 들어옴.
9
+ # file_path 키가 없으므로 command에서 `*** Update File:` / `*** Add File:` 헤더의
10
+ # 경로를 정규식 추출해 각각 검사한다. matcher = "apply_patch".
11
+ #
12
+ # 잡는 것:
13
+ # 1. .project/tasks/<vX.X>/<NNN>_<slug>.md (단일 파일)
14
+ # 2. .project/tasks/<vX.X>/TASK-<NNN>_<slug>/task.md (폴더 승격)
15
+ # → 헤더 status가 'closed'면 차단 (exit 2)
16
+ #
17
+ # 차단 안 함 (의도적): spec-diffs / screenshots / mockup — 역사적 자료 자유 수정.
18
+ # Add File은 대상 파일이 존재하지 않으므로 자연 통과(새 파일은 closed일 수 없음).
19
+ #
20
+ # 우회: 헤더 status를 closed → developing 등으로 직접 변경 후 편집.
21
+ # Catastrophic 영역 (완료 보호). 잘 지키면 작동 0회 (무해).
22
+
23
+ INPUT=$(cat)
24
+
25
+ # tool_input.command (apply_patch envelope 전체) 추출
26
+ CMD=$(echo "$INPUT" | python3 -c "
27
+ import sys, json
28
+ try:
29
+ print(json.load(sys.stdin).get('tool_input', {}).get('command', ''))
30
+ except Exception:
31
+ print('')
32
+ " 2>/dev/null)
33
+
34
+ # Update/Add File 헤더에서 대상 경로 추출 (한 patch에 여러 파일 가능)
35
+ PATHS=$(echo "$CMD" | grep -oE '^\*\*\* (Update|Add) File: .+$' | sed -E 's/^\*\*\* (Update|Add) File: //')
36
+
37
+ [ -z "$PATHS" ] && exit 0
38
+
39
+ while IFS= read -r FILE_PATH; do
40
+ # task.md 본 파일 패턴만 검사 (spec-diffs/screenshots/mockup 직속은 보호 대상 X)
41
+ echo "$FILE_PATH" | grep -qE '\.project/tasks/v[^/]+/([0-9]+_[^/]+\.md|TASK-[^/]+/task\.md)$' || continue
42
+ # 존재하는 파일만 (Add File 신규 생성은 통과)
43
+ [ -f "$FILE_PATH" ] || continue
44
+
45
+ # 헤더 status 추출 (5컬럼 표 — 생성일 / 플랜 / 유형 / 규모 / 상태)
46
+ STATUS=$(TM="$FILE_PATH" python3 -c "
47
+ import os, re
48
+ try:
49
+ content = open(os.environ['TM']).read()
50
+ except Exception:
51
+ print('')
52
+ raise SystemExit(0)
53
+ WL = r'(draft|planned|developing|developed|testing|tested|closed)'
54
+ m = re.search(rf'^\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|\s*{WL}\s*\|\s*\$', content, re.MULTILINE)
55
+ print(m.group(1) if m else '')
56
+ " 2>/dev/null)
57
+
58
+ if [ "$STATUS" = "closed" ]; then
59
+ echo "closed-immutable: '$FILE_PATH' 차단. task 상태가 'closed' — 재수정 금지." >&2
60
+ echo " 의도적 재진입 시: sed로 헤더 status를 'developing' 등으로 변경 후 편집 가능." >&2
61
+ echo " closed task 코드 / spec-diffs / screenshots / mockup은 보호 영역 외 — 자유 수정. 코드 변경은 새 task로 처리가 정상 흐름." >&2
62
+ exit 2
63
+ fi
64
+ done <<< "$PATHS"
65
+
66
+ exit 0
@@ -133,6 +133,7 @@ docs: [TASK-{NNN}] CHANGELOG 업데이트
133
133
  - 머지 커밋 메시지는 git 기본 메시지 사용
134
134
  - `-m` 옵션으로 커밋 메시지 지정 금지
135
135
  - **`--no-ff` 강제 (일반 타입)**: feature/bug/improvement/refactor 등 일반 작업 브랜치 → dev 병합 시 `git merge --no-ff {브랜치명}` 사용. fast-forward 병합 절대 금지. 머지 커밋이 없으면 작업 브랜치 삭제 후 분기 정보 영구 손실(90일 reflog GC 후 추적 불가).
136
+ - **추적 변경 0 → 마커 빈커밋 (`--allow-empty` 금지의 유일 예외)**: 코드 변경 0 + `.project` gitignore로 작업 브랜치가 dev보다 앞선 커밋이 0개이면 `--no-ff`도 *Already up to date*가 되어 머지 커밋이 생성되지 않는다. 이 경우 `/task-close`가 추적 마커 빈커밋 1개(`--allow-empty`)를 생성해 분기·채번 정보를 보존한다 (상세: task-close Step 6-8). 그 외 빈커밋은 금지.
136
137
  - **plan/roadmap 임시 docs 브랜치 예외**: plan-init 단계의 단일 커밋(plan 신규/카피)에 한정 `--ff-only` 가능.
137
138
  - **task 진행 중 ROADMAP/플랜 문서 갱신은 별도 `docs/*` 브랜치 분리 절대 금지**: 해당 task의 작업 브랜치 (`feature/*` / `improve/*` 등) 안에서 수정 + 다른 구현 커밋과 함께 `--no-ff` 머지로 dev 병합 (분기 정보 손실 방지).
138
139
 
@@ -213,6 +214,14 @@ docs: [TASK-{NNN}] CHANGELOG 업데이트
213
214
  | 제거 | `git -C "$MAIN_WT" worktree remove "$WT_PATH"` | `/task-close` (자동 흐름 + 안전 검증) |
214
215
  | stale 정리 | `git -C "$MAIN_WT" worktree prune` | `npx @angar2/taskery prune` (사용자 확인 후) |
215
216
 
217
+ ### 워크트리 실행 환경 (의존성 — 앱 실행·검수 시)
218
+
219
+ 새 워크트리에는 의존성·빌드 산출물이 없다(생태계마다 형태 상이). 이를 메인 워크트리에서 **심링크로 끌어오지 않는다.**
220
+
221
+ - 심링크는 실제 경로가 메인 워크트리를 가리켜, *파일 접근을 워크트리 내부로 제한하는 도구*(개발 서버 등)에서 로딩 실패(예: 빈 화면)를 일으킨다.
222
+ - 빌드·테스트는 이 제한을 거치지 않아 심링크로도 통과하므로 "코드는 정상인데 실행만 깨짐"으로 오진하기 쉽다.
223
+ - 해당 task에서 앱을 **실행·검수**해야 한다면 심링크 대신 워크트리 안에 의존성을 실제로 마련한다(프로젝트 고유의 설치 명령 사용). 실행·검수가 필요 없는 task는 무관하다.
224
+
216
225
  ### 메인 워크트리 절대 경로 검출
217
226
 
218
227
  워크트리 안에서 메인 워크트리 메타(`.project/`, `CLAUDE.md`)에 접근할 때 사용:
@@ -240,6 +249,16 @@ git -C "$MAIN_WT" branch --no-merged dev --list \
240
249
  - retries: 5회, 1초 간격
241
250
  - 락 외 *사전 rebase* (UX용 조기 충돌 감지) → 락 안 *재 rebase*로 락 외에서 발생한 다른 세션 머지 흡수
242
251
 
252
+ ### 멀티세션 검수 환경 (dev 서버 · 터널 — 검수 서버를 띄우는 프로젝트 한정)
253
+
254
+ 여러 세션이 병렬로 검수용 dev 서버·터널을 띄울 때 포트·프로세스가 충돌하지 않도록 task별로 자원을 독립 소유한다. CLI·라이브러리 등 검수 서버가 없는 프로젝트는 무관.
255
+
256
+ - **포트 격리 (결정적)**: 메인/dev 세션 = 기준 포트, task 워크트리 = 기준 포트 + TASK번호. 기준 포트는 프로젝트가 `CLAUDE.md`(코덱스는 `AGENTS.md`) `## 검수 실행 명령`에 선언한다 (멀티 프로젝트 동시 운영 시 프로젝트마다 다른 기준 포트). TASK번호는 1부터라 offset 0(기준 포트)이 메인 몫으로 항상 비어 충돌하지 않는다.
257
+ - **터널 (필수 아님)**: 모바일 검수 등 필요한 task만, 자기 포트를 가리키는 별도 프로세스로 판다 → task별 독립 URL.
258
+ - **자기 것만 정리**: `/task-close` 시 본 task가 띄운 서버·터널(자기 포트 것)만 종료한다. **`pkill -f` 같은 광역 종료 금지** — 다른 세션의 서버까지 죽이는 원인.
259
+ - **검수 기동 (자동)**: 사용자가 결과를 *눈으로 검수*하는 시점에 위 포트로 서버를 백그라운드 기동하고 접속 URL을 보고한다 (사용자가 매번 실행을 지시하지 않게). 정확한 발동 지점은 각 스킬 참조 — `/task-dev`는 세션이 거기서 끝날 때(Step 9), `/task-test`는 UNCERTAIN 검수 직전(Step 5)·PASS 후 종료 시(Step 7). FAIL→수정 흐름에서는 기동하지 않는다. 단계가 *기능상* 서버를 요구하면(E2E 등) 검수 시점과 무관하게 띄운다.
260
+ - **효과 (정직)**: 포트 충돌은 결정적으로 제거된다. 다른 세션의 종료·턴 경계 프로세스 소실은 위 규율·백그라운드 실행으로 완화되나 문서 규율이라 강제는 아니다(best-effort).
261
+
243
262
  ---
244
263
 
245
264
  ## Hook 안전망 — `git-guard.sh`
@@ -284,3 +303,6 @@ git -C "$MAIN_WT" branch --no-merged dev --list \
284
303
  | 2026-05-31 | 멀티세션 0.1.2 오버라이드 — 브랜치명에 출처(BL/RM/DR) 추가 / 케이스 2(TASK 없는 브랜치) 시스템 외 명시 / `/task-close` 자동 삭제 + 워크트리 제거 면제 조항 / 멀티세션 워크트리 정책 섹션 신규 (메인=dev 전용 / SSoT 조회 / 머지 락 직렬화) |
285
304
  | 2026-05-31 | 0.1.2 백로그 스킬 추가 정합 — 출처 표 `BL-NNN` 채번 주체 `/backlog-add` → `/add-backlog`로 갱신 + 버전별 경로(`.project/tasks/<vX.X>/BACKLOG.md`) 명시 |
286
305
  | 2026-06-02 | 0.1.3 F2·F3 정합 — §멀티세션 워크트리 정책 *메인 HEAD 떼기 금지* + *모호 발화 자의 해석 금지* / §git-guard.sh 5종 변형 인식 (-C / --git-dir / --work-tree) + 셸 prefix 금지 명시. stash FRICTION_LOG 2026-06-01 반영 |
306
+ | 2026-06-25 | 추적 변경 0(코드 0 · `.project` gitignore) close 시 마커 빈커밋으로 분기·채번 정보 보존 — `--no-ff` 머지 커밋 미생성 케이스의 `--allow-empty` 유일 예외 명시 (상세: task-close Step 6-8). FRICTION 검토 F1 반영 |
307
+ | 2026-06-25 | §멀티세션 워크트리 정책에 "워크트리 실행 환경" 추가 — 워크트리에 의존성 심링크 금지(개발 서버 fs 접근 제한으로 로딩 실패), 실행·검수 task는 워크트리 안에 의존성 실제 마련. FRICTION 검토 F6 반영 |
308
+ | 2026-06-25 | §멀티세션 워크트리 정책에 "멀티세션 검수 환경" 추가 — task별 dev 서버 포트 격리(메인=기준 포트, task=기준+TASK번호) + 터널 독립 + close 시 자기 것만 종료(광역 종료 금지) + 사용자 검수 시점 서버 자동 기동(task-test UNCERTAIN/PASS·task-dev 종료, FAIL 제외). FRICTION 검토 F7 반영 |
@@ -0,0 +1,194 @@
1
+ # AGENTS.md
2
+
3
+ > 이 파일은 Codex 세션이 *매 세션 시작 시 자동 로드*하는 단일 진입점이다.
4
+ > 프로젝트 메타 + 검증/테스트/검수 명령 + 룰 참조 + 스킬 9종 + 동기화 룰만 포함한다.
5
+ > 사람용 도메인 설명은 `.project/PROJECT.md`, 진입 가이드는 `.project/AGENT-GUIDE.md` 참조.
6
+ >
7
+ > Codex 환경 차이 (Claude와 다른 점만):
8
+ > - 스킬 본문: `.agents/skills/<스킬>/SKILL.md` (Claude의 `.claude/skills/`가 아님)
9
+ > - Hook: `.codex/config.toml`의 `[[hooks.PreToolUse]]`로 등록 — 최초 1회 `/hooks` trust 승인 필요
10
+ > - 격리 검증(/task-test): `.codex/agents/task-tester.toml` 서브에이전트로 spawn
11
+
12
+ ---
13
+
14
+ ## 프로젝트 메타
15
+
16
+ - **이름**: <프로젝트명>
17
+ - **타입**: <frontend / backend / fullstack / cli / library / other>
18
+ - **소개 (한 줄)**: <도메인 / 비전>
19
+ - **상세**: `.project/PROJECT.md` 참조
20
+
21
+ > `/project-init` 호출 시 위 4 항목 채워짐. 변경 필요하면 직접 편집.
22
+
23
+ ---
24
+
25
+ ## 메인 세션 최상위 룰
26
+
27
+ > 본 룰은 *모든 스킬 호출 / 사용자 발화 처리 / 작업 진행*에 적용. 위반 시 사용자 마찰 직결.
28
+
29
+ 1. **범위 준수** — 사용자가 명시한 범위/행동만 수행. 명시 외 자체 진입 영구 금지. 예: 사용자가 *"X부터 Y까지"* 지시 → Y 끝에서 정지 + 상태 보고 + 다음 단계 명시 호출 대기.
30
+ 2. **Skill 정식 발동** — task 단계는 반드시 해당 스킬(`.agents/skills/<스킬>/SKILL.md`)을 정식 호출. 가이드 본문을 머릿속 절차로 대체 금지 (컴팩트 세션 / 시스템 리마인더 인지 상태에서도 동일).
31
+ 3. **워크트리 자가 진단** — 세션 시작 시 현재 cwd가 *메인 워크트리*인지 *태스크 워크트리*인지 자가 판단 (`git rev-parse --show-toplevel` + `~/.taskery/worktrees/` 경로 비교). 태스크 워크트리면 *해당 진행중 태스크 컨텍스트*로 진입, 메인 워크트리면 *새 태스크 / 진행중 목록 인터뷰* 흐름.
32
+ 4. **모호 발화 자의 해석 금지** — *"워크트리 없이"* / *"메인에서"* / *"이 자리에서 그냥"* 류로 읽히는 발화 = 코어 규칙(메인 워크트리 = dev 전용) 충돌 신호. 즉시 정지 + 규칙 한 줄 명시 + 1줄 confirm 요청. 자의 해석 후 진행 영구 금지. 사용자 의도가 *오타/모호*일 가능성을 *워크트리 사용*으로 읽는 게 자연스러움.
33
+
34
+ ---
35
+
36
+ ## 멀티세션 워크트리 (0.1.2+)
37
+
38
+ > 멀티세션 = 같은 프로젝트에서 *여러 메인 세션이 독립 태스크를 병렬*로 진행. git worktree로 작업 폴더 격리, dev 머지 시 직렬화.
39
+
40
+ - **메인 워크트리 = dev 전용**: 메인 워크트리는 항상 `dev` 체크아웃 상태. 모든 태스크 작업은 *예외 없이* 별도 워크트리에서 수행. 메인 워크트리 HEAD를 dev에서 떼는 어떤 작업(`git checkout <task-branch>` / `git switch <task-branch>` / `git reset` HEAD 이동 / `git rebase` HEAD 이동 등)도 영구 금지. *"잠깐만 메인에서"* / *"테스트 한 번만 메인에서"* 같은 예외 발화도 거부 — 별도 워크트리에서 처리.
41
+ - **워크트리 위치**: `~/.taskery/worktrees/<projectId>/TASK-NNN_<출처>_<슬러그>/`
42
+ - `<projectId>`: `.taskery-manifest.json` `projectId` (8자 hex)
43
+ - `<출처>`: `BL-NNN` / `RM-NNN` / `DR`
44
+ - **`/task-init`이 워크트리 생성** + **`/task-close`가 dev 머지 + 워크트리 자동 제거** (자동 흐름. 보존 키워드 `keep` / `브랜치 남겨` 등 발화 시 둘 다 보존).
45
+ - **메인 메타 접근**: 워크트리에서 `.project/`, `AGENTS.md` 등 접근 시 *메인 워크트리 절대 경로* 사용:
46
+ ```sh
47
+ MAIN_WT=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")
48
+ ```
49
+ - **수동 git 작업 시 정합성 보장 X** — taskery 명령(스킬 / `npx @angar2/taskery <서브>`)으로만 운영.
50
+ - **CLI 보조 명령**:
51
+ - `npx @angar2/taskery status` — 진행중 태스크 + 워크트리 + dev 머지 상태 요약
52
+ - `npx @angar2/taskery prune` — stale 워크트리 대화형 정리
53
+
54
+ 상세: `.project/rules/GIT_RULE.md` "멀티세션 워크트리 정책" 섹션.
55
+
56
+ 요건: git ≥ 2.31 (`--path-format=absolute` 옵션 필요).
57
+
58
+ ---
59
+
60
+ ## 백로그 (0.1.2+)
61
+
62
+ > `.project/tasks/<vX.X>/BACKLOG.md` = *버전별 task 후보 누적*. 사용자 발화로 1건씩 얕은 분석(개요 / 대상 영역) 곁들여 추가.
63
+
64
+ - **`/add-backlog`**: 사용자 *"~ 백로그에 추가"* 발화 → 얕은 분석(코드 탐색 X, 추정 수준) → BL-NNN 채번 → BACKLOG.md append (`[ ]` 대기).
65
+ - **`/task-init` 연동**: 사용자 *"백로그의 BL-NNN 진행"* 발화 → 해당 항목 *확인 마킹*. `[ ]` → `[x]` + `- TASK: TASK-NNN` 추가 (다회 진행 시 콤마).
66
+ - **체크박스 의미**: `[ ]` = 미확인 (task로 옮기지 않은 메모) / `[x]` = 확인 완료 (task로 옮김). `[x]`는 *task로 옮겼다*는 메모일 뿐, dev 머지·완료 여부와는 무관하다.
67
+ - **`[x]` 항목이 실제로 완료된 작업인지 굳이 확인하려 들지 않는다.** 진행·완료 상태는 백로그가 아니라 `taskery status`(살아있는 브랜치) + dev 머지커밋이 판단한다.
68
+ - **머지 여부를 직접 알아야 할 경우에만** 아래 순서로 작업 완료 여부를 확인한다:
69
+ 1. `taskery status` 진행중 목록에 그 TASK가 **있으면** → 아직 진행 중(dev 미머지)이므로 `git log dev --grep`이 비는 것이 정상이다. 재조회하지 않는다.
70
+ 2. 목록에 **없으면** → `git log dev --grep 'BL-NNN'` + 브랜치명으로 작업 완료 여부를 확인한다.
71
+
72
+ 상세: `.agents/skills/add-backlog/SKILL.md`.
73
+
74
+ ---
75
+
76
+ ## 검증 명령
77
+
78
+ > *코드 상태 검증* (빌드 / 린트 / 타입체크). 테스트 실행은 본 섹션 X — `## 테스트 명령` 참조.
79
+ > `/task-dev` self-check / `/task-close` 최종 게이트가 *이 섹션을 단일 진실 소스로 참조*.
80
+ > 백틱(`...`) 안 명령 그대로 실행됨. 언어/프레임워크 따라 변경.
81
+
82
+ - 린트: `<예: npm run lint>`
83
+ - 타입체크: `<예: npm run typecheck>` (TypeScript / Flow / mypy 등 — 해당 시)
84
+ - 빌드: `<예: npm run build>`
85
+
86
+ > 위 항목 중 *프로젝트에 해당 없는 것*은 행 자체 삭제. 빈 백틱(``)은 사용 금지.
87
+
88
+ ---
89
+
90
+ ## 테스트 명령
91
+
92
+ > *테스트 실행* (단위 / 통합 / E2E 등). 코드 상태 검증은 `## 검증 명령` 참조.
93
+ > `/task-dev` 구현 후 테스트 + `/task-test` 격리 세션이 *이 섹션을 단일 진실 소스로 참조*.
94
+ > 백틱(`...`) 안 명령 그대로 실행됨. 언어/프레임워크 따라 변경.
95
+
96
+ - 단위 테스트: `<예: npm test>`
97
+ - 통합 테스트: `<예: npm run test:integration>` (있을 시)
98
+ - E2E 테스트: `<예: npx playwright test>` (있을 시)
99
+
100
+ > 위 항목 중 *프로젝트에 해당 없는 것*은 행 자체 삭제. 빈 백틱(``)은 사용 금지.
101
+
102
+ ---
103
+
104
+ ## 검수 실행 명령 (해당 시 — 앱 실행·검수가 필요한 프로젝트)
105
+
106
+ > `/task-dev`·`/task-test`가 검수 시점(세션 중단점)에 이 명령으로 서버를 *백그라운드* 기동하고 접속 URL을 보고한다.
107
+ > 멀티세션 포트 격리: 메인/dev 세션 = 기준 포트, task 워크트리 = 기준 포트 + TASK번호. 상세는 GIT_RULE "멀티세션 검수 환경" 섹션.
108
+ > CLI/라이브러리 등 검수 서버가 없는 프로젝트는 본 섹션 전체 삭제.
109
+
110
+ - 기준 포트: `<예: 5100>`
111
+ - 실행 명령: `<예: npm run dev -- --port <PORT>>` (`<PORT>` = 기준 포트 + TASK번호, 메인/dev는 기준 포트)
112
+ - 터널 (모바일 검수 등 필요 시 — 선택): `<예: cloudflared tunnel --url http://localhost:<PORT>>`
113
+
114
+ ---
115
+
116
+ ## 룰 문서 참조
117
+
118
+ | 룰 | 위치 | 역할 |
119
+ |----|------|------|
120
+ | TASK_DOC_RULE | `.project/rules/TASK_DOC_RULE.md` | task 문서 양식 (헤더 5컬럼 / 6 섹션 / 7 상태) |
121
+ | GIT_RULE | `.project/rules/GIT_RULE.md` (있으면) → 글로벌 `~/.claude/rules/GIT_RULE.md` (fallback) | git 정책 (브랜치 / 커밋 / 머지) |
122
+ | CHANGELOG_RULE | `.project/rules/CHANGELOG_RULE.md` | CHANGELOG 작성 정책 (위치 / 형식 / 필수 필드) |
123
+ | MOCKUP_RULE | `.project/rules/MOCKUP_RULE.md` | UX/UI task의 HTML 목업 위치 / 형식 / 네이밍 |
124
+ | `*.local.md` | `.project/rules/` | (옵션) 사용자 오버라이드 — `*.local.md` suffix는 npx 미터치 |
125
+
126
+ ---
127
+
128
+ ## 스킬 9종 — project > plan > task 위계 + 회고
129
+
130
+ | 스킬 | 레벨 | 역할 |
131
+ |------|------|------|
132
+ | `/project-init` | project | PROJECT/AGENT-GUIDE/LINKED-REPOS/.env 골격 생성 (1회성) |
133
+ | `/plan-init` | plan | `.project/plans/<vX.X>/` 안 기획 문서 작성 |
134
+ | `/task-init` | task | task.md 빈 골격 + status=draft |
135
+ | `/task-plan` | task | Requirements/Scope/Dev Plan/Test Plan 작성 (draft → planned) |
136
+ | `/task-dev` | task | Phase 순서 구현 + self-check (planned → developed) |
137
+ | `/task-test` | task | task-tester 서브에이전트 격리 검증 (developed → tested) |
138
+ | `/task-close` | task | 검증 명령 게이트 + 커밋 + dev 병합 (tested → closed) |
139
+ | `/add-backlog` | meta | 사용자 발화로 버전별 BACKLOG.md에 항목 1건 추가 (얕은 분석 + BL-NNN 채번 — 0.1.2+) |
140
+ | `/log-friction` | meta | FRICTION_LOG.md에 사용자 불편 한 행 기록 |
141
+
142
+ 본문은 `.agents/skills/<스킬>/SKILL.md` 참조.
143
+
144
+ ---
145
+
146
+ ## Hook 안전망 — Catastrophic 2종
147
+
148
+ > `.codex/config.toml`의 `[[hooks.PreToolUse]]`로 등록. 최초 1회 `/hooks` trust 승인 후 자동 작동.
149
+
150
+ | Hook | 영역 | 잡는 것 |
151
+ |------|------|--------|
152
+ | `git-guard.sh` | PreToolUse(Bash) | main/dev 직접 커밋 / `--force` / `--no-verify` / `branch -D` / `reset --hard` / `clean -fd` |
153
+ | `closed-immutable.sh` | PreToolUse(apply_patch) | `closed` 상태 task.md 본 파일 재수정 차단 (정상 흐름은 새 task로). spec-diffs / screenshots / mockup은 자유 수정 (역사적 자료) |
154
+
155
+ **잘 지키면 hook 작동 0회 (무해). catastrophic만 차단.**
156
+
157
+ > closed-immutable.sh는 코덱스 파일편집 도구(apply_patch)의 patch envelope에서 `*** Update File:` 경로를 파싱해 검사한다(Claude판은 `file_path` 키를 읽음 — 메커니즘만 다르고 효과 동일).
158
+
159
+ ---
160
+
161
+ ## 불편 데이터
162
+
163
+ - **누적**: `.project/FRICTION_LOG.md` — 메인이 한 줄씩 추가 (사용자 불만 발화 캐치 또는 사용자 명시)
164
+ - **등록**: `/log-friction` 스킬 — 사용자 명시 호출 / 불만 발화 캐치 / task-close 직후 마찰 신호 자체 감지 시 한 행 기록
165
+
166
+ ---
167
+
168
+ ## 동기화 룰 (사용자 의무)
169
+
170
+ > taskery는 *plan/ ↔ template/ 자동 빌드 X*. 사용자가 직접 정합 유지.
171
+
172
+ - **`.project/plans/<vX.X>/` 변경 시** → 관련 task의 `spec-diffs/` 갱신 (`/task-plan` Phase 0 흐름 — 진행 중 task 있으면)
173
+ - **`AGENTS.md` 검증 명령 변경 시** → 모든 스킬 + hook이 그대로 따름. 별도 동기화 불필요
174
+ - **룰(`*.md` in `.project/rules/`) 변경 시** → 스킬 instruction이 다음 호출부터 변경 반영
175
+ - **`*.local.md` 사용자 오버라이드** → `npx @angar2/taskery update`가 미터치. 코어 룰 갱신 시 `*.bak` 백업 후 사용자 confirm
176
+
177
+ ---
178
+
179
+ ## 멀티리포 통신 (해당 시)
180
+
181
+ - 송신: `.project/shared/sent/<filename>.md` 작성
182
+ - 수신: 연결 리포에서 `received/`로 카피 후 정독
183
+ - 처리 완료: `sent/completed/` 또는 `received/completed/`로 이동
184
+ - 연결 리포 경로: `.project/.env` (gitignore)
185
+ - 리포 목록: `.project/LINKED-REPOS.md`
186
+
187
+ ---
188
+
189
+ ## 메인 세션 진입 시
190
+
191
+ 1. 본 파일(`AGENTS.md`) 정독 — 검증 명령 + 룰 위치 + 스킬 목록 파악
192
+ 2. `.project/AGENT-GUIDE.md` 정독 — 활성 plan 버전 + 폴더 구조 + 작업 흐름
193
+ 3. `.project/plans/<활성버전>/PLAN.md` 정독 — 기획 문서 인덱스 + 진행 상태
194
+ 4. 사용자 발화 받기 → 적절한 스킬 호출 또는 직접 작업
@@ -1,7 +1,7 @@
1
1
  # CLAUDE.md
2
2
 
3
3
  > 이 파일은 메인 세션이 *매 세션 시작 시 자동 로드*하는 단일 진입점이다.
4
- > 프로젝트 메타 + 검증 명령 + 룰 참조 + 스킬 9종 + 동기화 룰만 포함한다.
4
+ > 프로젝트 메타 + 검증/테스트/검수 명령 + 룰 참조 + 스킬 9종 + 동기화 룰만 포함한다.
5
5
  > 사람용 도메인 설명은 `.project/PROJECT.md`, 진입 가이드는 `.project/AGENT-GUIDE.md` 참조.
6
6
 
7
7
  ---
@@ -58,7 +58,11 @@
58
58
 
59
59
  - **`/add-backlog`**: 사용자 *"~ 백로그에 추가"* 발화 → 얕은 분석(코드 탐색 X, 추정 수준) → BL-NNN 채번 → BACKLOG.md append (`[ ]` 대기).
60
60
  - **`/task-init` 연동**: 사용자 *"백로그의 BL-NNN 진행"* 발화 → 해당 항목 *확인 마킹*. `[ ]` → `[x]` + `- TASK: TASK-NNN` 추가 (다회 진행 시 콤마).
61
- - **체크박스 의미**: `[ ]` = 미확인 (task로 옮기지 않은 메모) / `[x]` = 확인 완료 (task로 옮김). **dev 머지 완료 의미 X** — 완료 여부는 `git log dev --grep 'BL-NNN'` + 브랜치명 추적 + `taskery status`.
61
+ - **체크박스 의미**: `[ ]` = 미확인 (task로 옮기지 않은 메모) / `[x]` = 확인 완료 (task로 옮김). `[x]`는 *task로 옮겼다*는 메모일 뿐, dev 머지·완료 여부와는 무관하다.
62
+ - **`[x]` 항목이 실제로 완료된 작업인지 굳이 확인하려 들지 않는다.** 진행·완료 상태는 백로그가 아니라 `taskery status`(살아있는 브랜치) + dev 머지커밋이 판단한다.
63
+ - **머지 여부를 직접 알아야 할 경우에만** 아래 순서로 작업 완료 여부를 확인한다:
64
+ 1. `taskery status` 진행중 목록에 그 TASK가 **있으면** → 아직 진행 중(dev 미머지)이므로 `git log dev --grep`이 비는 것이 정상이다. 재조회하지 않는다.
65
+ 2. 목록에 **없으면** → `git log dev --grep 'BL-NNN'` + 브랜치명으로 작업 완료 여부를 확인한다.
62
66
  - **글로벌 `.project/BACKLOG.md`** (plan 기획 후보 카탈로그) 는 본 흐름 무관 — `/plan-init` 영역.
63
67
 
64
68
  상세: `.claude/skills/add-backlog/SKILL.md`.
@@ -93,6 +97,18 @@
93
97
 
94
98
  ---
95
99
 
100
+ ## 검수 실행 명령 (해당 시 — 앱 실행·검수가 필요한 프로젝트)
101
+
102
+ > `/task-dev`·`/task-test`가 검수 시점(세션 중단점)에 이 명령으로 서버를 *백그라운드* 기동하고 접속 URL을 보고한다.
103
+ > 멀티세션 포트 격리: 메인/dev 세션 = 기준 포트, task 워크트리 = 기준 포트 + TASK번호. 상세는 GIT_RULE "멀티세션 검수 환경" 섹션.
104
+ > CLI/라이브러리 등 검수 서버가 없는 프로젝트는 본 섹션 전체 삭제.
105
+
106
+ - 기준 포트: `<예: 5100>`
107
+ - 실행 명령: `<예: npm run dev -- --port <PORT>>` (`<PORT>` = 기준 포트 + TASK번호, 메인/dev는 기준 포트)
108
+ - 터널 (모바일 검수 등 필요 시 — 선택): `<예: cloudflared tunnel --url http://localhost:<PORT>>`
109
+
110
+ ---
111
+
96
112
  ## 룰 문서 참조
97
113
 
98
114
  | 룰 | 위치 | 역할 |
@@ -66,7 +66,7 @@ LOCK_FILE="$HOME/.taskery/${PROJECT_ID}.merge.lock"
66
66
  git -C "$WT_PATH" status --porcelain
67
67
  ```
68
68
  - 결과 *있음* = 정상 흐름 (taskery 정책: `/task-dev` = *git 작업 X*). 본 변경분은 Step 6-3에서 *task-close가 자동 Phase 커밋 생성*으로 처리.
69
- - 결과 *없음* = 변경분 0건 → Step 2 검증만 수행 후 Step 6-3 자동 커밋 단계 자연 스킵 (빈 커밋 X).
69
+ - 결과 *없음* = 변경분 0건 → Step 2 검증만 수행 후 Step 6-3 자동 커밋 단계 스킵. **단 6-7까지 거쳐도 워크트리 브랜치가 dev보다 앞선 커밋이 0개면 Step 6-8에서 추적 마커 빈커밋 1개 생성** (`.project` gitignore + docs/분석 전용 task → 채번 보존).
70
70
  - **차단 X** — 본 단계는 *상태 인지*용. close 중단 사유 X.
71
71
  4. **task 파일 + GIT_RULE 확인**:
72
72
  - task 문서 위치 분기 (`.gitignore` 케이스):
@@ -245,6 +245,22 @@ docs: [TASK-<NNN>] 태스크 문서 완료
245
245
  - 등록 — `$MAIN_WT/.project/changelog/<YYYY-MM>.md` `withMetaLock` 갱신 (커밋 X)
246
246
  - 본 task에서 변경분 없으면 본 단계 *완전히 스킵* (빈 commit 금지).
247
247
 
248
+ #### 6-8. 추적 마커 빈커밋 (폴백 — 머지 커밋 미생성 케이스 한정)
249
+
250
+ > **조건**: 6-3 ~ 6-7을 모두 거쳤는데도 워크트리 브랜치가 dev보다 앞선 커밋이 **0개**인 경우에만 발동.
251
+ > **발생 케이스**: `.project`가 gitignore된 프로젝트에서 *코드 변경 0 + 산출물이 메인 워크트리 직접 수정 task 문서뿐*인 docs/분석 전용 task. 워크트리에 추적 가능한 변경이 없어 6-3 ~ 6-7이 전부 스킵된 상태.
252
+ > **이유**: 이대로 Step 8 `--no-ff` 머지를 하면 브랜치가 dev와 동일 커밋이라 *Already up to date* → 머지 커밋이 생성되지 않는다. 그러면 dev 히스토리에 `TASK-NNN` 흔적이 남지 않고, 이후 작업 브랜치가 자동 삭제(Step 13)되면 `getNextTaskNumber`가 진행중 브랜치·dev 머지 양쪽에서 번호를 찾지 못해 **다음 task가 같은 번호를 재사용/충돌**한다.
253
+
254
+ ```sh
255
+ if [ "$(git -C "$WT_PATH" rev-list --count dev..HEAD)" -eq 0 ]; then
256
+ git -C "$WT_PATH" commit --allow-empty \
257
+ -m "docs: [TASK-<NNN>] 추적 마커 — 코드 0·.project gitignore (채번 보존)"
258
+ fi
259
+ ```
260
+
261
+ - 마커 커밋으로 브랜치가 dev보다 1커밋 앞서게 되어, Step 8 `--no-ff` 머지가 정상 머지 커밋을 생성한다 (머지 커밋 메시지의 브랜치명에 `TASK-NNN`이 포함되어 grep 추적 가능).
262
+ - 추적 변경이 하나라도 있는 일반 task는 조건(`-eq 0`)에 걸리지 않아 발동하지 않는다 — 기존 동작 그대로.
263
+
248
264
  ### Step 7 — 메인 워크트리 uncommitted 검증 (머지 직전)
249
265
 
250
266
  ```sh
@@ -342,8 +358,9 @@ git -C "$MAIN_WT" worktree remove "$WT_PATH"
342
358
  - **자료 한계 보고** — 충돌 분석 시 *2/3순위 fallback 사용했음*을 사용자에게 명시. 묵묵히 진행 X.
343
359
  - **destructive 명령 사용자 승인 필수** — `git reset --hard` / `git push --force` / `git branch -D` / `git clean -fd` 사용자 명시 승인 없이 절대 실행 X. (단 `/task-close` 자동 흐름 `git branch -d` + `worktree remove`는 §"작업 브랜치 삭제 정책 — 자동 삭제 면제 조항" 면제)
344
360
  - **민감 정보 staging X** — `.env` / `credentials.json` / API key 등 staging 절대 X.
345
- - **빈 commit 금지** — CHANGELOG 변경 없으면 6-7 단계 스킵. `--allow-empty` 사용 금지.
361
+ - **빈 commit 금지 (예외 1건)** — CHANGELOG 변경 없으면 6-7 단계 스킵, `--allow-empty` 사용 금지. **단 Step 6-8 추적 마커 빈커밋만 예외** — 워크트리 브랜치가 dev보다 앞선 커밋이 0개일 때 채번 보존용으로 1개 허용.
346
362
  - **`-m` 옵션 머지 커밋에 사용 금지** — git 기본 메시지 사용.
363
+ - **검수 서버/터널 정리는 자기 것만** — 본 task가 검수용으로 띄운 서버·터널(자기 포트 것)만 종료. `pkill -f` 류 광역 종료 금지 (다른 세션 프로세스까지 죽임). 상세: GIT_RULE "멀티세션 검수 환경".
347
364
 
348
365
  ## 상태 전이
349
366
 
@@ -359,4 +376,4 @@ git -C "$MAIN_WT" worktree remove "$WT_PATH"
359
376
  - **task 문서 단일 진실 소스** — `.gitignore` 케이스에 따라 메인 워크트리 또는 워크트리. 다른 세션의 충돌 해결 자료 정독 시 *SSoT 조회 → 워크트리 경로 산출* (미등록 케이스) 또는 *메인 워크트리 절대 경로* (등록 케이스).
360
377
  - **워크트리 + 브랜치 자동 제거** — GIT_RULE.md "작업 브랜치 삭제 정책" 면제 조항 (taskery 한정). 보존 키워드 시 양쪽 보존.
361
378
  - **safety net** — Step 14 복구 명령 출력. 사용자가 잘못 삭제 인지 시 즉시 복구 가능.
362
- - **BACKLOG.md 무관** — `.project/tasks/<vX.X>/BACKLOG.md` 체크 마킹은 `/task-init` Step 7.5가 처리(`[ ]` → `[x]` + `- TASK: TASK-NNN`). `[x]` = *task로 옮김* 의미라 close 시점에 추가 마킹 X. 완료 추적은 `git log dev --grep 'BL-NNN'` + `npx @angar2/taskery status`.
379
+ - **BACKLOG.md 무관** — `.project/tasks/<vX.X>/BACKLOG.md` 체크 마킹은 `/task-init` Step 7.5가 처리(`[ ]` → `[x]` + `- TASK: TASK-NNN`). `[x]` = *task로 옮김* 의미라 close 시점에 추가 마킹 X. 완료 추적은 `npx @angar2/taskery status`(진행 중이면 dev 미머지가 정상) → 목록에 없을 때만 `git log dev --grep 'BL-NNN'` 순.
@@ -167,6 +167,14 @@ stash FRICTION_LOG #14+19 / #25 반영 — 유닛 테스트는 본 단계에서
167
167
  - 다음: /task-test TASK-<NNN> 으로 격리 세션 검증
168
168
  ```
169
169
 
170
+ ### Step 9 — 검수 서버 기동 (중단점·해당 시)
171
+
172
+ **조건**: ① 이번 세션이 여기서 멈추고 사용자에게 제어를 넘김(= `/task-test`로 계속 진행하지 않음) + ② `CLAUDE.md`(코덱스 `AGENTS.md`)에 `## 검수 실행 명령`이 선언됨.
173
+
174
+ - 두 조건 충족 시 — 그 명령으로 서버를 *백그라운드* 기동(포트 = 기준 포트 + TASK번호) + 접속 URL 보고. 모바일 검수가 필요하다고 판단되면 터널도 기동.
175
+ - `/task-test`로 계속 진행하면 본 단계 생략 (마지막 단계에서만 기동 — 상세: GIT_RULE "멀티세션 검수 환경").
176
+ - `## 검수 실행 명령` 미선언(CLI/라이브러리 등) 또는 사용자 *"안 띄워도 돼"* 발화 시 생략.
177
+
170
178
  ## 도구 가이드
171
179
 
172
180
  - **Read**: task 파일 / `CLAUDE.md` 검증 명령 / 관련 코드 정독
@@ -185,6 +193,7 @@ stash FRICTION_LOG #14+19 / #25 반영 — 유닛 테스트는 본 단계에서
185
193
  - **로그 심기 잊지 X** — Dev Plan 작성 시 `[로그]` 항목 명시되어 있으면 *반드시* 실제 로그 코드 삽입. 격리 세션 / 운영 디버깅 모두 의존.
186
194
  - **Result 섹션 진행만** — 테스트 결과는 `/task-test`가 기록. 메인이 *"잘 됐을 거야"* 가정 작성 금지.
187
195
  - **flows/ 업데이트 누락 X** — 신규 기능 / 로직 변경이면 `.project/flows/<module>.md` 갱신. 미갱신 시 다음 task가 옛 정보 보고 헤맴.
196
+ - **워크트리에서 앱 실행·검수 시 의존성 심링크 금지** — 워크트리엔 의존성·빌드 산출물이 없는데, 메인 워크트리에서 심링크로 끌어오면 개발 서버 등이 로딩 실패(빈 화면)한다. 실행·검수하는 task면 워크트리 안에 실제로 의존성을 마련한다. 상세: GIT_RULE "워크트리 실행 환경" 섹션.
188
197
 
189
198
  ## 상태 전이
190
199
 
@@ -183,6 +183,8 @@ Task tool로 sub-agent spawn. prompt는 *자기완결적* — task.md 경로 +
183
183
 
184
184
  격리 세션이 `[USER]` 시나리오 또는 자동화 불가능한 시나리오(UI 인터랙션 / 시각 디자인 / 사용자 입력 등) 발견 시. **자동 PASS 전이 X — 사용자 직접 검수 PASS 받은 후에만 `tested` 전이**.
185
185
 
186
+ > **검수 서버 먼저 기동** (`## 검수 실행 명령` 선언 + 시각/UI 검수 항목 포함 시): 체크리스트를 제시하기 *전에* 서버를 백그라운드 기동(포트 = 기준 포트 + TASK번호)하고 접속 URL을 함께 제공한다 — 사용자가 실제 화면을 보며 ✓/✗ 검수하도록. 격리 세션이 기능상 이미 띄웠으면 재사용. 사용자 *"안 띄워도 돼"* 시 생략. 상세: GIT_RULE "멀티세션 검수 환경".
187
+
186
188
  1. 메인이 사용자에게 *체크리스트 형식*으로 항목 보고 (stash FRICTION_LOG #14+19 반영):
187
189
 
188
190
  ```
@@ -202,6 +204,15 @@ USER 검수 — N 항목 (목업 기준: <mockup path — 있을 시>)
202
204
 
203
205
  ### Step 6 — 결과 보고 (위 분기별 메시지)
204
206
 
207
+ ### Step 7 — 검수 서버 기동 (PASS 후 세션 종료·해당 시)
208
+
209
+ **조건**: ① 종합 판정 PASS + 이번 세션이 여기서 멈추고 사용자에게 제어를 넘김(= `/task-close`로 바로 진행하지 않음) + ② `CLAUDE.md`(코덱스 `AGENTS.md`)에 `## 검수 실행 명령`이 선언됨.
210
+
211
+ - 두 조건 충족 시 — 그 명령으로 서버를 *백그라운드* 기동(포트 = 기준 포트 + TASK번호) + 접속 URL 보고. 모바일 검수가 필요하다고 판단되면 터널도 기동.
212
+ - 격리 세션 검증이 *기능상* 이미 서버를 띄워 둔 경우 — 죽였다 다시 띄우지 말고 그대로 두고 URL만 보고.
213
+ - **FAIL → 수정 흐름에서는 기동하지 않는다** (수정하러 가는 단계라 검수 무의미). **UNCERTAIN 검수는 Step 5에서 이미 기동**했으므로 여기서 중복 기동 X.
214
+ - `## 검수 실행 명령` 미선언 또는 사용자 *"안 띄워도 돼"* 발화 시 생략. 상세: GIT_RULE "멀티세션 검수 환경".
215
+
205
216
  ## 도구 가이드
206
217
 
207
218
  - **Read**: task 파일 / `## Test Plan` + `## Dev Plan` 정독
File without changes