commitgate 0.3.1 → 0.4.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/AGENTS.template.md +9 -0
- package/README.en.md +85 -35
- package/README.md +87 -37
- package/bin/init.ts +155 -6
- package/bin/uninstall.ts +54 -12
- package/package.json +5 -2
- package/req.config.json.sample +14 -13
- package/scripts/req/lib/config.ts +32 -1
- package/scripts/req/req-next.ts +638 -0
- package/scripts/req/review-codex.ts +152 -6
- package/templates/CLAUDE.template.md +15 -0
- package/templates/claude-command.md +36 -0
- package/templates/claude-skill.md +59 -0
- package/templates/cursor-rule.mdc +55 -0
- package/workflow/req.config.schema.json +1 -0
- package/workflow/review-persona.md +45 -0
package/bin/uninstall.ts
CHANGED
|
@@ -21,12 +21,17 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
|
21
21
|
import { createHash } from 'node:crypto'
|
|
22
22
|
import { resolve, join, relative } from 'node:path'
|
|
23
23
|
import { pathToFileURL } from 'node:url'
|
|
24
|
-
import { loadConfig, stripBom, DEFAULTS } from '../scripts/req/lib/config'
|
|
24
|
+
import { loadConfig, stripBom, DEFAULTS, DEFAULT_REVIEW_PERSONA_RELPATH } from '../scripts/req/lib/config'
|
|
25
25
|
import { createGitAdapter, type GitAdapter, type GitRunner } from '../scripts/req/lib/adapters'
|
|
26
26
|
import {
|
|
27
27
|
PACKAGE_ROOT,
|
|
28
28
|
KIT_SOURCE_DIR_REL,
|
|
29
29
|
KIT_SCHEMA_RELPATHS,
|
|
30
|
+
KIT_COPY_RELPATHS,
|
|
31
|
+
KIT_AGENT_ENTRYPOINTS,
|
|
32
|
+
KIT_CLAUDE_TEMPLATE_REL,
|
|
33
|
+
KIT_CLAUDE_DEST_REL,
|
|
34
|
+
KIT_AGENTS_CONTRACT_COPY_REL,
|
|
30
35
|
REQ_SCRIPTS,
|
|
31
36
|
REQ_DEV_DEPS,
|
|
32
37
|
assertGitWorkTree,
|
|
@@ -36,7 +41,11 @@ export interface UninstallOptions {
|
|
|
36
41
|
dir: string
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* CommitGate가 복사한 파일. `path`는 **대상 repo의 경로(dest)**다.
|
|
46
|
+
* 원본(src)은 패키지 안에서 경로가 다를 수 있다(`templates/claude-skill.md` → `.claude/skills/commitgate/SKILL.md`).
|
|
47
|
+
* 바이트 비교로 무결성만 표기.
|
|
48
|
+
*/
|
|
40
49
|
export interface ToolArtifact {
|
|
41
50
|
path: string // repo-상대
|
|
42
51
|
present: boolean
|
|
@@ -173,11 +182,13 @@ export function collectFacts(opts: UninstallOptions, run?: GitRunner): Uninstall
|
|
|
173
182
|
// (깨진 config 때문에 제거 안내를 못 받는 건 부당하다 — planner는 쓰기가 없어 강등이 안전).
|
|
174
183
|
let ticketRoot = DEFAULTS.ticketRoot
|
|
175
184
|
let schemaPath = DEFAULTS.schemaPath
|
|
185
|
+
let reviewPersonaPath = DEFAULTS.reviewPersonaPath
|
|
176
186
|
let configError: string | null = null
|
|
177
187
|
try {
|
|
178
188
|
const cfg = loadConfig({ root: targetRoot })
|
|
179
189
|
ticketRoot = cfg.ticketRoot
|
|
180
190
|
schemaPath = cfg.schemaPath
|
|
191
|
+
reviewPersonaPath = cfg.reviewPersonaPath
|
|
181
192
|
} catch (e) {
|
|
182
193
|
configError = (e as Error).message
|
|
183
194
|
}
|
|
@@ -190,25 +201,41 @@ export function collectFacts(opts: UninstallOptions, run?: GitRunner): Uninstall
|
|
|
190
201
|
info.push(
|
|
191
202
|
`schemaPath=${schemaPath} — 런타임이 읽는 경로이지만 init이 복사한 파일이 아닙니다(제거 후보 아님).`,
|
|
192
203
|
)
|
|
204
|
+
// reviewPersonaPath도 같은 축 분기(REQ-2026-010). null=의도적 비활성이므로 알릴 것이 없다.
|
|
205
|
+
if (reviewPersonaPath !== null && reviewPersonaPath !== DEFAULT_REVIEW_PERSONA_RELPATH)
|
|
206
|
+
info.push(
|
|
207
|
+
`reviewPersonaPath=${reviewPersonaPath} — 런타임이 읽는 경로이지만 init이 복사한 파일이 아닙니다(제거 후보 아님).`,
|
|
208
|
+
)
|
|
193
209
|
|
|
194
|
-
// ── tool: kit 소스 + init이 **실제로 복사한**
|
|
210
|
+
// ── tool: kit 소스 + init이 **실제로 복사한** 파일들.
|
|
211
|
+
//
|
|
212
|
+
// ⚠️ `src`와 `dest`가 다른 항목이 있다(REQ-2026-010 D8). `KIT_AGENT_ENTRYPOINTS`는 `templates/*` →
|
|
213
|
+
// `.claude/…`·`.cursor/…`로 가고, `AGENTS.commitgate.md`는 `AGENTS.template.md`의 사본이다.
|
|
214
|
+
// "원본을 `join(PACKAGE_ROOT, rel)`로 찾는다"는 가정을 버리고 매핑을 명시한다.
|
|
215
|
+
// 복사 축(KIT_COPY_RELPATHS)을 쓴다. 위 info의 스키마 축(KIT_SCHEMA_RELPATHS)과 의도적으로 다른 상수다.
|
|
195
216
|
const kitSourceRels = walkFiles(join(PACKAGE_ROOT, KIT_SOURCE_DIR_REL)).map(toRel)
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
const toolEntries: { destRel: string; srcRel: string }[] = [
|
|
218
|
+
...kitSourceRels.map((r) => ({ destRel: r, srcRel: r })),
|
|
219
|
+
...KIT_COPY_RELPATHS.map((r) => ({ destRel: r, srcRel: r })),
|
|
220
|
+
...KIT_AGENT_ENTRYPOINTS.map((e) => ({ destRel: e.dest, srcRel: e.src })),
|
|
221
|
+
// 마커 없는 AGENTS.md 경로에서만 설치되는 계약 템플릿 사본. 부재가 정상이므로 present=false여도 문제 없다.
|
|
222
|
+
{ destRel: KIT_AGENTS_CONTRACT_COPY_REL, srcRel: 'AGENTS.template.md' },
|
|
223
|
+
]
|
|
224
|
+
const tool: ToolArtifact[] = toolEntries.map(({ destRel, srcRel }) => {
|
|
225
|
+
const dest = join(targetRoot, destRel)
|
|
226
|
+
const src = join(PACKAGE_ROOT, srcRel)
|
|
200
227
|
if (!existsSync(dest))
|
|
201
|
-
return { path:
|
|
228
|
+
return { path: destRel, present: false, match: 'absent', tracked: false, introducedBy: null }
|
|
202
229
|
const match: ToolArtifact['match'] =
|
|
203
230
|
existsSync(src) && sha256(dest) === sha256(src) ? 'identical' : 'differs'
|
|
204
|
-
const tracked = isTracked(git,
|
|
231
|
+
const tracked = isTracked(git, destRel)
|
|
205
232
|
return {
|
|
206
|
-
path:
|
|
233
|
+
path: destRel,
|
|
207
234
|
present: true,
|
|
208
235
|
match,
|
|
209
236
|
tracked,
|
|
210
237
|
// untracked면 이력의 add 커밋은 "지금 이 설치본"의 도입 커밋이 아니다 → 조회조차 하지 않는다(phase R2 P2).
|
|
211
|
-
introducedBy: tracked ? (introducingCommit(git,
|
|
238
|
+
introducedBy: tracked ? (introducingCommit(git, destRel)?.sha ?? null) : null,
|
|
212
239
|
}
|
|
213
240
|
})
|
|
214
241
|
|
|
@@ -228,6 +255,21 @@ export function collectFacts(opts: UninstallOptions, run?: GitRunner): Uninstall
|
|
|
228
255
|
})
|
|
229
256
|
}
|
|
230
257
|
|
|
258
|
+
// CLAUDE.md는 AGENTS.md와 같은 계층이다 — init이 **부재 시에만** 만들고 `--force`로도 덮어쓰지 않는다.
|
|
259
|
+
// 사용자 파일일 수 있으므로 자동 제거 대상이 아니다(REQ-2026-010 D7).
|
|
260
|
+
const claudeAbs = join(targetRoot, KIT_CLAUDE_DEST_REL)
|
|
261
|
+
if (existsSync(claudeAbs)) {
|
|
262
|
+
const tpl = join(PACKAGE_ROOT, KIT_CLAUDE_TEMPLATE_REL)
|
|
263
|
+
const same = existsSync(tpl) && sha256(claudeAbs) === sha256(tpl)
|
|
264
|
+
ambiguous.push({
|
|
265
|
+
path: KIT_CLAUDE_DEST_REL,
|
|
266
|
+
present: true,
|
|
267
|
+
note: same
|
|
268
|
+
? 'init 템플릿과 동일 — 그래도 자동 제거 대상이 아닙니다(에이전트 지침 파일)'
|
|
269
|
+
: '템플릿과 다름 — 사용자/팀이 작성했거나 편집했습니다',
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
231
273
|
const cfgAbs = join(targetRoot, 'req.config.json')
|
|
232
274
|
if (existsSync(cfgAbs)) {
|
|
233
275
|
const parsed = readJsonObject(cfgAbs)
|
|
@@ -421,7 +463,7 @@ export function renderPlan(plan: UninstallPlan): string {
|
|
|
421
463
|
|
|
422
464
|
L.push('## 5. 잔여물 경고')
|
|
423
465
|
L.push(' - git은 빈 디렉터리를 추적하지 않습니다. 위 파일을 지운 뒤 `git status`가 clean이어도')
|
|
424
|
-
L.push(` 빈 디렉터리(scripts/ · ${facts.ticketRoot}/)가 파일시스템에 남을 수 있습니다.`)
|
|
466
|
+
L.push(` 빈 디렉터리(scripts/ · ${facts.ticketRoot}/ · .claude/ · .cursor/)가 파일시스템에 남을 수 있습니다.`)
|
|
425
467
|
L.push(' - node_modules의 ajv · cross-spawn · tsx 는 다른 패키지도 쓸 수 있어 제거를 권하지 않습니다.')
|
|
426
468
|
L.push('')
|
|
427
469
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commitgate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CommitGate — Builder↔Reviewer(Claude↔Codex) fail-closed 커밋 게이트: 리뷰·승인·증거 없인 커밋을 통과시키지 않는 AI REQ 워크플로 kit (req:new/review-codex/doctor/commit)",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "CommitGate — Builder↔Reviewer(Claude↔Codex) fail-closed 커밋 게이트: 리뷰·승인·증거 없인 커밋을 통과시키지 않는 AI REQ 워크플로 kit (req:new/next/review-codex/doctor/commit)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "sol5288",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"req:new": "tsx scripts/req/req-new.ts",
|
|
33
33
|
"req:review-codex": "tsx scripts/req/review-codex.ts",
|
|
34
34
|
"req:doctor": "tsx scripts/req/req-doctor.ts",
|
|
35
|
+
"req:next": "tsx scripts/req/req-next.ts",
|
|
35
36
|
"req:commit": "tsx scripts/req/req-commit.ts",
|
|
36
37
|
"test": "vitest run",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
@@ -41,7 +42,9 @@
|
|
|
41
42
|
"scripts/req",
|
|
42
43
|
"workflow/machine.schema.json",
|
|
43
44
|
"workflow/req.config.schema.json",
|
|
45
|
+
"workflow/review-persona.md",
|
|
44
46
|
"bin",
|
|
47
|
+
"templates",
|
|
45
48
|
"AGENTS.template.md",
|
|
46
49
|
"req.config.json.sample",
|
|
47
50
|
"README.md",
|
package/req.config.json.sample
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"ticketRoot": "workflow",
|
|
3
|
-
"schemaPath": "workflow/machine.schema.json",
|
|
4
|
-
"handoffPath": null,
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"ticketRoot": "workflow",
|
|
3
|
+
"schemaPath": "workflow/machine.schema.json",
|
|
4
|
+
"handoffPath": null,
|
|
5
|
+
"reviewPersonaPath": "workflow/review-persona.md",
|
|
6
|
+
"branchPrefix": "feat/req-",
|
|
7
|
+
"packageManager": "npm",
|
|
8
|
+
"granularityMaxFiles": 8,
|
|
9
|
+
"designDocs": {
|
|
10
|
+
"requirement": "00-requirement.md",
|
|
11
|
+
"design": "01-design.md",
|
|
12
|
+
"plan": "02-plan.md"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -26,6 +26,8 @@ export interface RawConfig {
|
|
|
26
26
|
ticketRoot?: string
|
|
27
27
|
schemaPath?: string
|
|
28
28
|
handoffPath?: string | null
|
|
29
|
+
/** null = 의도적 비활성(persona 블록 생략). 미지정 = DEFAULTS(활성). */
|
|
30
|
+
reviewPersonaPath?: string | null
|
|
29
31
|
branchPrefix?: string
|
|
30
32
|
packageManager?: PackageManager
|
|
31
33
|
granularityMaxFiles?: number
|
|
@@ -38,6 +40,7 @@ export interface ResolvedConfig {
|
|
|
38
40
|
ticketRoot: string
|
|
39
41
|
schemaPath: string
|
|
40
42
|
handoffPath: string | null
|
|
43
|
+
reviewPersonaPath: string | null
|
|
41
44
|
branchPrefix: string
|
|
42
45
|
packageManager: PackageManager
|
|
43
46
|
granularityMaxFiles: number
|
|
@@ -46,8 +49,22 @@ export interface ResolvedConfig {
|
|
|
46
49
|
workflowDirAbs: string
|
|
47
50
|
schemaPathAbs: string
|
|
48
51
|
handoffPathAbs: string | null
|
|
52
|
+
reviewPersonaPathAbs: string | null
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Codex 리뷰 프롬프트에 주입되는 **리뷰어 페르소나** 문서의 repo-상대 경로(코어 기본값).
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ 이 상수는 두 축의 SSOT다(REQ-2026-010 D3-1).
|
|
59
|
+
* - **설치 축**: `bin/init.ts`의 `KIT_COPY_RELPATHS`가 이 경로를 대상 repo에 복사한다.
|
|
60
|
+
* - **설정 축**: `DEFAULTS.reviewPersonaPath`가 이 값으로 해소된다(phase-1b에서 도입).
|
|
61
|
+
*
|
|
62
|
+
* 둘이 갈라지면 신규 설치본은 프롬프트 조립 시 이 파일을 찾지 못하고 **모든 리뷰가 fail-closed로 멈춘다.**
|
|
63
|
+
* `tests/unit/init.test.ts`의 "설치 축 SSOT"가 그 드리프트를 회귀로 잡는다.
|
|
64
|
+
* `package.json`의 `files[]`는 또 **다른 축**(npm tarball 적재분)이므로 함께 갱신해야 한다.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_REVIEW_PERSONA_RELPATH = 'workflow/review-persona.md'
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
69
|
* 코어 기본값. `req.config.json` 부재 시 이 값으로 해소된다.
|
|
53
70
|
*
|
|
@@ -63,6 +80,9 @@ export const DEFAULTS = {
|
|
|
63
80
|
ticketRoot: 'workflow',
|
|
64
81
|
schemaPath: 'workflow/machine.schema.json',
|
|
65
82
|
handoffPath: null as string | null,
|
|
83
|
+
// ⚠️ handoffPath와 달리 코어 기본이 **활성**이다. init이 이 경로에 파일을 깔기 때문(KIT_COPY_RELPATHS).
|
|
84
|
+
// 비활성이 필요하면 config에 `null`을 명시한다. `as string | null`은 handoffPath와 같은 이유(직접 import 소비자 계약).
|
|
85
|
+
reviewPersonaPath: DEFAULT_REVIEW_PERSONA_RELPATH as string | null,
|
|
66
86
|
branchPrefix: 'feat/req-',
|
|
67
87
|
packageManager: 'pnpm' as PackageManager,
|
|
68
88
|
granularityMaxFiles: 8,
|
|
@@ -79,6 +99,8 @@ export const CONFIG_SCHEMA = {
|
|
|
79
99
|
ticketRoot: { type: 'string', minLength: 1 },
|
|
80
100
|
schemaPath: { type: 'string', minLength: 1 },
|
|
81
101
|
handoffPath: { type: ['string', 'null'] },
|
|
102
|
+
// null = 의도적 비활성. 문자열이면 minLength 1(빈 문자열은 "비활성"의 애매한 표현 → 거부, null을 쓰게 한다).
|
|
103
|
+
reviewPersonaPath: { type: ['string', 'null'], minLength: 1 },
|
|
82
104
|
branchPrefix: { type: 'string', minLength: 1 }, // 빈 prefix는 D11 무력화 → 금지
|
|
83
105
|
packageManager: { type: 'string', enum: ['pnpm', 'npm', 'yarn'] },
|
|
84
106
|
granularityMaxFiles: { type: 'integer', minimum: 1 },
|
|
@@ -156,17 +178,25 @@ export function loadConfig(opts: { root?: string | null; cwd?: string } = {}): R
|
|
|
156
178
|
ticketRoot: raw.ticketRoot ?? DEFAULTS.ticketRoot,
|
|
157
179
|
schemaPath: raw.schemaPath ?? DEFAULTS.schemaPath,
|
|
158
180
|
handoffPath: raw.handoffPath !== undefined ? raw.handoffPath : DEFAULTS.handoffPath, // null = 명시적 비활성
|
|
181
|
+
reviewPersonaPath:
|
|
182
|
+
raw.reviewPersonaPath !== undefined ? raw.reviewPersonaPath : DEFAULTS.reviewPersonaPath, // null = 명시적 비활성
|
|
159
183
|
branchPrefix: raw.branchPrefix ?? DEFAULTS.branchPrefix,
|
|
160
184
|
packageManager: raw.packageManager ?? DEFAULTS.packageManager,
|
|
161
185
|
granularityMaxFiles: raw.granularityMaxFiles ?? DEFAULTS.granularityMaxFiles,
|
|
162
186
|
designDocs: { ...DEFAULTS.designDocs, ...(raw.designDocs ?? {}) },
|
|
163
187
|
}
|
|
164
188
|
|
|
165
|
-
// repo-내부 자원(ticketRoot·schemaPath)은 **상대경로 + root 하위**만(절대경로·탈출 금지 → portable).
|
|
189
|
+
// repo-내부 자원(ticketRoot·schemaPath·reviewPersonaPath)은 **상대경로 + root 하위**만(절대경로·탈출 금지 → portable).
|
|
190
|
+
// handoffPath만 면제 — 형제 repo의 SSOT 문서를 읽는 **외부 참조**이기 때문.
|
|
191
|
+
// reviewPersonaPath는 패키지가 배포하고 init이 repo 안에 까는 자원이라 schemaPath와 같은 축이다(REQ-2026-010 D2).
|
|
166
192
|
assertRelative(merged.ticketRoot, 'ticketRoot')
|
|
167
193
|
assertRelative(merged.schemaPath, 'schemaPath')
|
|
168
194
|
assertUnderRoot(rootAbs, merged.ticketRoot, 'ticketRoot')
|
|
169
195
|
assertUnderRoot(rootAbs, merged.schemaPath, 'schemaPath')
|
|
196
|
+
if (merged.reviewPersonaPath !== null) {
|
|
197
|
+
assertRelative(merged.reviewPersonaPath, 'reviewPersonaPath')
|
|
198
|
+
assertUnderRoot(rootAbs, merged.reviewPersonaPath, 'reviewPersonaPath')
|
|
199
|
+
}
|
|
170
200
|
|
|
171
201
|
return {
|
|
172
202
|
root: rootAbs,
|
|
@@ -174,6 +204,7 @@ export function loadConfig(opts: { root?: string | null; cwd?: string } = {}): R
|
|
|
174
204
|
workflowDirAbs: resolve(rootAbs, merged.ticketRoot),
|
|
175
205
|
schemaPathAbs: resolve(rootAbs, merged.schemaPath),
|
|
176
206
|
handoffPathAbs: merged.handoffPath ? resolve(rootAbs, merged.handoffPath) : null,
|
|
207
|
+
reviewPersonaPathAbs: merged.reviewPersonaPath ? resolve(rootAbs, merged.reviewPersonaPath) : null,
|
|
177
208
|
}
|
|
178
209
|
}
|
|
179
210
|
|