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
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* pnpm req:review-codex --ticket <dir> # 임의 티켓 디렉터리
|
|
18
18
|
* 옵션: --handoff <path> (미지정 시 req.config.json의 handoffPath. 둘 다 없으면 handoff 블록 생략 — 코어 기본은 비활성)
|
|
19
19
|
*/
|
|
20
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
21
|
-
import { resolve, join, relative } from 'node:path'
|
|
20
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
21
|
+
import { resolve, join, relative, sep } from 'node:path'
|
|
22
22
|
import { pathToFileURL } from 'node:url'
|
|
23
23
|
import { createHash } from 'node:crypto'
|
|
24
24
|
import Ajv from 'ajv'
|
|
@@ -67,6 +67,8 @@ export interface ReviewContext {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export interface ReviewPromptInput {
|
|
70
|
+
/** 리뷰어 역할 정의(REQ-2026-010 D1). 첫 블록. null/공백이면 생략. 본문 문자열이지 경로가 아니다. */
|
|
71
|
+
persona?: string | null
|
|
70
72
|
handoff?: string | null
|
|
71
73
|
reviewContext?: ReviewContext | null
|
|
72
74
|
reviewBaseSha: string
|
|
@@ -78,16 +80,20 @@ export interface ReviewPromptInput {
|
|
|
78
80
|
|
|
79
81
|
/**
|
|
80
82
|
* 순수 함수: 리뷰 프롬프트 조립 (§9.5).
|
|
81
|
-
* 순서 = [handoff?] → [Review Context?] → REVIEW_BASE_SHA → REVIEW_KIND → codex-request 본문 → 권위 아티팩트.
|
|
83
|
+
* 순서 = [persona?] → [handoff?] → [Review Context?] → REVIEW_BASE_SHA → REVIEW_KIND → codex-request 본문 → 권위 아티팩트.
|
|
82
84
|
* 권위 아티팩트: kind=phase → staged diff(현행), kind=design → 설계 문서 00/01/02 본문(DEC-WF-027 결정#3).
|
|
83
|
-
* handoff·reviewContext는 선택. 빈 request는 fail-closed로 거부. kind 기본값 phase(하위호환).
|
|
85
|
+
* persona·handoff·reviewContext는 선택. 빈 request는 fail-closed로 거부. kind 기본값 phase(하위호환).
|
|
86
|
+
*
|
|
87
|
+
* persona가 맨 앞인 이유(REQ-2026-010 D1): 리뷰어의 **역할 정의**는 컨텍스트·판정 대상보다 먼저 와야 한다.
|
|
88
|
+
* ⚠️ 이 함수는 파일을 읽지 않는다 — persona는 이미 읽힌 **본문**이다. 읽기·부재 판정은 `loadReviewPersona`가 한다.
|
|
84
89
|
*/
|
|
85
90
|
export function assembleReviewPrompt(input: ReviewPromptInput): string {
|
|
86
|
-
const { handoff, reviewContext, reviewBaseSha, requestBody, stagedDiff, designDocs } = input
|
|
91
|
+
const { persona, handoff, reviewContext, reviewBaseSha, requestBody, stagedDiff, designDocs } = input
|
|
87
92
|
const kind: ReviewKind = input.reviewKind ?? 'phase'
|
|
88
93
|
if (!reviewBaseSha) throw new Error('reviewBaseSha 필요')
|
|
89
94
|
if (!requestBody || !requestBody.trim()) throw new Error('codex-request.md 본문이 비어 있음')
|
|
90
95
|
const blocks: string[] = []
|
|
96
|
+
if (persona && persona.trim()) blocks.push(persona.trim())
|
|
91
97
|
if (handoff && handoff.trim()) blocks.push(handoff.trim())
|
|
92
98
|
if (reviewContext) {
|
|
93
99
|
blocks.push(
|
|
@@ -123,6 +129,48 @@ export function assembleReviewPrompt(input: ReviewPromptInput): string {
|
|
|
123
129
|
return blocks.join('\n')
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
/**
|
|
133
|
+
* persona 문서 로드 — **fail-closed** (REQ-2026-010 D3).
|
|
134
|
+
*
|
|
135
|
+
* `handoff`의 `existsSync` silent-skip 패턴을 의도적으로 **따르지 않는다**:
|
|
136
|
+
* - handoff는 있으면 좋은 **읽기 전용 참조**라, 없으면 조용히 생략해도 리뷰가 성립한다.
|
|
137
|
+
* - persona는 **리뷰 품질 계약**이다. 조용히 빠진 채 exit 0으로 승인이 나오면,
|
|
138
|
+
* "약한 리뷰가 통과했다"는 신호가 어디에도 남지 않는다 — 정확히 이 티켓이 없애려는 실패 양식.
|
|
139
|
+
*
|
|
140
|
+
* 비활성 경로는 **하나뿐**이다: `req.config.json`에 `reviewPersonaPath: null`을 명시한다(암묵 < 명시).
|
|
141
|
+
*
|
|
142
|
+
* 거부하는 것 — 셋 다 "persona 없이 리뷰가 exit 0으로 통과"하거나 계약을 우회하는 경로다.
|
|
143
|
+
*
|
|
144
|
+
* 1. **부재**.
|
|
145
|
+
* 2. **빈 내용**(0바이트·공백 only) — phase-1b R1 P2. `assembleReviewPrompt`가 `persona.trim()`으로 블록을
|
|
146
|
+
* 생략하므로, 내용을 안 보면 fail-closed 계약이 **파일 하나 비우는 것으로 무너진다.**
|
|
147
|
+
* 3. **realpath가 root 밖이거나 일반 파일이 아닌 경우** — phase-1b R2 P2. `loadConfig`의 confinement는
|
|
148
|
+
* config의 **문자열 경로**만 검사하는데 `readFileSync`는 **symlink를 따라간다.** `workflow/review-persona.md`를
|
|
149
|
+
* repo 밖 파일로 향하는 링크로 바꾸면 그 내용이 프롬프트 첫 블록으로 Codex에 전송된다(D2 계약 우회 + 유출).
|
|
150
|
+
* 그래서 읽기 직전에 **realpath 기준으로** root 하위 regular file인지 다시 확인한다.
|
|
151
|
+
*
|
|
152
|
+
* `rootAbs`도 realpath로 정규화한다 — 임시 디렉터리(예: macOS `/tmp` → `/private/tmp`)처럼 root 자체가
|
|
153
|
+
* symlink 경유일 때 문자열 비교가 거짓 음성을 내기 때문이다.
|
|
154
|
+
*/
|
|
155
|
+
export function loadReviewPersona(pathAbs: string | null, rootAbs: string): string | null {
|
|
156
|
+
if (pathAbs === null) return null
|
|
157
|
+
const recovery = ` → \`npx commitgate --force\`로 복원하거나, 의도한 비활성이면 req.config.json에 "reviewPersonaPath": null 을 명시하세요.`
|
|
158
|
+
if (!existsSync(pathAbs)) throw new Error(`리뷰어 페르소나 문서 없음: ${pathAbs}\n${recovery}`)
|
|
159
|
+
|
|
160
|
+
const rootReal = resolve(realpathSync(rootAbs))
|
|
161
|
+
const targetReal = resolve(realpathSync(pathAbs)) // symlink 해소
|
|
162
|
+
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep))
|
|
163
|
+
throw new Error(
|
|
164
|
+
`리뷰어 페르소나 문서가 repo 밖을 가리킵니다(symlink?): ${pathAbs} → ${targetReal}\n${recovery}`,
|
|
165
|
+
)
|
|
166
|
+
if (!statSync(targetReal).isFile())
|
|
167
|
+
throw new Error(`리뷰어 페르소나 문서가 일반 파일이 아닙니다: ${pathAbs}\n${recovery}`)
|
|
168
|
+
|
|
169
|
+
const body = readFileSync(targetReal, 'utf8')
|
|
170
|
+
if (!body.trim()) throw new Error(`리뷰어 페르소나 문서가 비어 있음: ${pathAbs}\n${recovery}`)
|
|
171
|
+
return body
|
|
172
|
+
}
|
|
173
|
+
|
|
126
174
|
/**
|
|
127
175
|
* git 바인딩 캡처 (§8.4): diff '텍스트'가 아니라 staged **tree OID**(git write-tree)를 바인딩.
|
|
128
176
|
* gitFn 주입 가능(테스트용).
|
|
@@ -133,6 +181,24 @@ export function captureGitBinding(gitFn: GitFn = git): { reviewBaseSha: string;
|
|
|
133
181
|
return { reviewBaseSha, reviewTree }
|
|
134
182
|
}
|
|
135
183
|
|
|
184
|
+
/**
|
|
185
|
+
* 인덱스 전체의 **읽기 전용** 신원 해시 (REQ-2026-010 D6-2).
|
|
186
|
+
*
|
|
187
|
+
* `captureGitBinding`의 tree OID와 값은 다르지만 **동치 관계**다: 인덱스 내용(mode·blob sha·stage·path)이
|
|
188
|
+
* 같으면 같고 다르면 다르다. 존재 이유는 `req:next`가 `git write-tree`를 **부를 수 없기 때문**이다 —
|
|
189
|
+
* 그 명령은 object DB에 tree object를 쓴다(D6-1의 무쓰기 계약 위반).
|
|
190
|
+
*
|
|
191
|
+
* ⚠️ 승인 바인딩이 아니다. `approved_diff_hash`는 여전히 tree OID다. 이 해시는 `last_review.compare_hash`
|
|
192
|
+
* 전용이고, 어떤 게이트(D6/D9/doctor)도 읽지 않는다. 이 경계가 흐려지면 D9가 다른 해시에 바인딩된다.
|
|
193
|
+
*/
|
|
194
|
+
export function captureIndexHash(gitFn: GitFn = git): string {
|
|
195
|
+
const lines = gitFn(['ls-files', '-s'])
|
|
196
|
+
.split('\n')
|
|
197
|
+
.map((l) => l.trim())
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
return createHash('sha256').update([...lines].sort().join('\n')).digest('hex')
|
|
200
|
+
}
|
|
201
|
+
|
|
136
202
|
/** 티켓 설계 문서 3종의 repo-relative 경로. shorthand 금지 — 각 경로를 티켓 디렉터리로 정규화. 파일명은 config(designDocs) 주입. */
|
|
137
203
|
export function designDocPaths(ticketRelDir: string, designDocs: DesignDocs): [string, string, string] {
|
|
138
204
|
const dir = ticketRelDir.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
@@ -327,6 +393,65 @@ export interface BlockedReviewMarker extends BlockedReviewTarget {
|
|
|
327
393
|
blocked_at: string
|
|
328
394
|
}
|
|
329
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 직전 리뷰의 **자문(advisory) 마커** — REQ-2026-010 D6-2. `req:next`의 G2(바인딩 신선도) 전용.
|
|
398
|
+
*
|
|
399
|
+
* ⚠️ **어떤 게이트도 이 필드를 읽지 않는다.** 승인 바인딩은 `approved_diff_hash`(tree OID) /
|
|
400
|
+
* `design_approved_hash`이고, `req:doctor`의 D-체크도 여기를 보지 않는다. `req:next`가
|
|
401
|
+
* "이 바인딩은 직전 리뷰가 이미 보고 승인하지 않았다"를 알아 무한 재리뷰 루프를 끊는 데만 쓴다.
|
|
402
|
+
*
|
|
403
|
+
* `compare_hash`는 **읽기 전용 명령으로 재계산 가능한** 값이어야 한다(`req:next`는 `write-tree` 금지):
|
|
404
|
+
* - design → `captureDesignBinding`의 designHash (`git ls-files -s -- <00,01,02>`)
|
|
405
|
+
* - phase → `captureIndexHash` (`git ls-files -s` 전체)
|
|
406
|
+
*
|
|
407
|
+
* `errors`는 `outcome === 'invalid'`일 때만 채운다 — `req:next`는 검증기를 다시 돌리지 않으므로
|
|
408
|
+
* 진단 본문을 리뷰 시점에 함께 저장해야 한다. 상한(20개 × 500자)이 state 비대를 막는다.
|
|
409
|
+
*/
|
|
410
|
+
export interface LastReviewMarker {
|
|
411
|
+
review_kind: ReviewKind
|
|
412
|
+
phase_id: string | null
|
|
413
|
+
outcome: ReviewOutcome
|
|
414
|
+
compare_hash: string | null
|
|
415
|
+
/** 같은 (review_kind, phase_id, compare_hash) 반복 횟수. `blocked_review.count`와 동일 의미론. */
|
|
416
|
+
count: number
|
|
417
|
+
errors: string[]
|
|
418
|
+
at: string
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** `last_review.errors` 상한 — state 비대 방지. */
|
|
422
|
+
export const LAST_REVIEW_MAX_ERRORS = 20
|
|
423
|
+
export const LAST_REVIEW_MAX_ERROR_LEN = 500
|
|
424
|
+
|
|
425
|
+
function sameLastReviewTarget(a: LastReviewMarker | undefined, kind: ReviewKind, phaseId: string | null, compareHash: string | null): boolean {
|
|
426
|
+
return !!a && a.review_kind === kind && a.phase_id === phaseId && a.compare_hash === compareHash
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* `last_review` 마커 기록(순수). 같은 target이면 `count` 증가, target이 바뀌면 1로 리셋.
|
|
431
|
+
* `errors`는 invalid에서만 저장하고 상한을 적용한다(그 외 outcome은 빈 배열 — findings는 `responses/` 아카이브에 남는다).
|
|
432
|
+
*/
|
|
433
|
+
export function recordLastReview(
|
|
434
|
+
state: WorkflowState,
|
|
435
|
+
args: { kind: ReviewKind; phaseId: string | null; outcome: ReviewOutcome; compareHash: string | null; errors: string[]; at: string },
|
|
436
|
+
): WorkflowState {
|
|
437
|
+
const prev = state.last_review as LastReviewMarker | undefined
|
|
438
|
+
const count = sameLastReviewTarget(prev, args.kind, args.phaseId, args.compareHash) ? prev!.count + 1 : 1
|
|
439
|
+
const errors =
|
|
440
|
+
args.outcome === 'invalid'
|
|
441
|
+
? args.errors.slice(0, LAST_REVIEW_MAX_ERRORS).map((e) => e.slice(0, LAST_REVIEW_MAX_ERROR_LEN))
|
|
442
|
+
: []
|
|
443
|
+
const marker: LastReviewMarker = {
|
|
444
|
+
review_kind: args.kind,
|
|
445
|
+
phase_id: args.phaseId,
|
|
446
|
+
outcome: args.outcome,
|
|
447
|
+
compare_hash: args.compareHash,
|
|
448
|
+
count,
|
|
449
|
+
errors,
|
|
450
|
+
at: args.at,
|
|
451
|
+
}
|
|
452
|
+
return { ...state, last_review: marker }
|
|
453
|
+
}
|
|
454
|
+
|
|
330
455
|
export interface WorkflowState {
|
|
331
456
|
id: string
|
|
332
457
|
phase: string
|
|
@@ -638,12 +763,26 @@ export function resolveReviewOutcome(args: {
|
|
|
638
763
|
blockedTarget: BlockedReviewTarget
|
|
639
764
|
responseSha256: string | null
|
|
640
765
|
blockedAt: string
|
|
766
|
+
/** REQ-2026-010 D6-2: `req:next`가 읽기 전용으로 재계산할 수 있는 바인딩 해시. 미제공이면 `last_review` 미기록(하위호환). */
|
|
767
|
+
compareHash?: string | null
|
|
641
768
|
}): { outcome: ReviewOutcome; exitCode: number; finalState: WorkflowState } {
|
|
642
769
|
const outcome = classifyReview(args.result, args.kind)
|
|
643
|
-
const
|
|
770
|
+
const afterBlocked =
|
|
644
771
|
outcome === 'blocked'
|
|
645
772
|
? recordBlockedReview(args.result.nextState, args.blockedTarget, args.responseSha256, args.blockedAt)
|
|
646
773
|
: clearBlockedReview(args.result.nextState)
|
|
774
|
+
// last_review는 **모든 outcome**에서 기록한다(approved 포함) — G2가 "직전 리뷰가 이 바인딩을 봤는가"를 알아야 한다.
|
|
775
|
+
const finalState =
|
|
776
|
+
args.compareHash === undefined
|
|
777
|
+
? afterBlocked
|
|
778
|
+
: recordLastReview(afterBlocked, {
|
|
779
|
+
kind: args.kind,
|
|
780
|
+
phaseId: args.blockedTarget.phase_id,
|
|
781
|
+
outcome,
|
|
782
|
+
compareHash: args.compareHash,
|
|
783
|
+
errors: args.result.errors,
|
|
784
|
+
at: args.blockedAt,
|
|
785
|
+
})
|
|
647
786
|
return { outcome, exitCode: reviewOutcomeExitCode(outcome), finalState }
|
|
648
787
|
}
|
|
649
788
|
|
|
@@ -964,6 +1103,9 @@ function main(): void {
|
|
|
964
1103
|
if (!existsSync(requestPath)) throw new Error(`codex-request.md 없음: ${requestPath}`)
|
|
965
1104
|
const requestBody = readFileSync(requestPath, 'utf8')
|
|
966
1105
|
|
|
1106
|
+
// persona: cfg.reviewPersonaPathAbs(null=명시적 비활성). 부재·빈 내용·root 밖 symlink는 **throw**(D3, fail-closed).
|
|
1107
|
+
const persona = loadReviewPersona(cfg.reviewPersonaPathAbs, cfg.root)
|
|
1108
|
+
|
|
967
1109
|
// handoff: --handoff 우선, 없으면 cfg.handoffPathAbs(null=비활성 — 부재 시 생략, 현재 동작 보존).
|
|
968
1110
|
const handoffPath = opts.handoff ? resolve(opts.handoff) : cfg.handoffPathAbs
|
|
969
1111
|
const handoff = handoffPath && existsSync(handoffPath) ? readFileSync(handoffPath, 'utf8') : null
|
|
@@ -1016,6 +1158,7 @@ function main(): void {
|
|
|
1016
1158
|
previousResult: readPreviousResult(ticketDir),
|
|
1017
1159
|
}
|
|
1018
1160
|
const prompt = assembleReviewPrompt({
|
|
1161
|
+
persona,
|
|
1019
1162
|
handoff,
|
|
1020
1163
|
reviewContext,
|
|
1021
1164
|
reviewBaseSha,
|
|
@@ -1127,12 +1270,15 @@ function main(): void {
|
|
|
1127
1270
|
const responseSha256 = existsSync(respPath)
|
|
1128
1271
|
? createHash('sha256').update(readFileSync(respPath)).digest('hex')
|
|
1129
1272
|
: null
|
|
1273
|
+
// D6-2: req:next가 write-tree 없이 재계산할 수 있는 바인딩 해시. design=designHash, phase=인덱스 전체 해시.
|
|
1274
|
+
const compareHash = opts.kind === 'design' ? designHash ?? null : captureIndexHash()
|
|
1130
1275
|
const { outcome, exitCode, finalState } = resolveReviewOutcome({
|
|
1131
1276
|
result,
|
|
1132
1277
|
kind: opts.kind,
|
|
1133
1278
|
blockedTarget,
|
|
1134
1279
|
responseSha256,
|
|
1135
1280
|
blockedAt: approvedAt,
|
|
1281
|
+
compareHash,
|
|
1136
1282
|
})
|
|
1137
1283
|
writeState(ticketDir, finalState)
|
|
1138
1284
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 프로젝트 지침
|
|
2
|
+
|
|
3
|
+
<!-- 이 파일은 `npx commitgate`가 CLAUDE.md가 없을 때만 생성한 템플릿입니다. 자유롭게 수정하세요. -->
|
|
4
|
+
|
|
5
|
+
## CommitGate
|
|
6
|
+
|
|
7
|
+
이 저장소는 **CommitGate**를 쓴다. 코드 변경은 REQ 티켓 단위로 묶이고, Codex가 승인한 staged tree만 커밋된다.
|
|
8
|
+
|
|
9
|
+
코드를 커밋하게 되는 요청은 일반 구현으로 처리하지 말고 이 워크플로를 따른다.
|
|
10
|
+
|
|
11
|
+
- **계약 정본**: 저장소 루트의 [`AGENTS.md`](./AGENTS.md). 절대 규칙·통제점·승인 문장이 거기 있다.
|
|
12
|
+
(`<!-- commitgate:contract -->` 마커가 없으면 CommitGate 계약이 아니다 — init이 함께 설치한 루트의 `AGENTS.commitgate.md`를 계약으로 읽고, 사용자에게 `AGENTS.md`로의 병합을 요청하라.)
|
|
13
|
+
- **다음 행동은 추측하지 않는다**: `npm run req:next -- <REQ-id>`가 알려 준다.
|
|
14
|
+
`RUN`은 그대로 실행, `AGENT`는 그 작업 수행 후 `git add`, `AWAIT_HUMAN`은 **멈추고 승인 문장을 그대로** 받는다.
|
|
15
|
+
- 자세한 진입 절차는 `/req` 슬래시 커맨드 또는 `.claude/skills/commitgate/SKILL.md`에 있다.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: CommitGate REQ 워크플로로 요구사항을 처리한다 (티켓 발행 → 설계 → Codex 리뷰 → 구현 → 커밋)
|
|
3
|
+
argument-hint: <요구사항>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
이 요청을 일반 구현으로 처리하지 말고, 이 저장소에 설치된 **CommitGate**로 처리하라.
|
|
7
|
+
|
|
8
|
+
## 요구사항
|
|
9
|
+
|
|
10
|
+
$ARGUMENTS
|
|
11
|
+
|
|
12
|
+
위 내용이 아래 네 칸으로 정리되지 않으면 **먼저 사용자에게 물어라.** 추측해서 채우지 마라.
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
- 무엇을:
|
|
16
|
+
- 왜:
|
|
17
|
+
- 제약:
|
|
18
|
+
- 완료 기준:
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 계약
|
|
22
|
+
|
|
23
|
+
저장소 루트의 `AGENTS.md`를 읽어라. 절대 규칙·통제점·승인 문장의 정본이다.
|
|
24
|
+
(`<!-- commitgate:contract -->` 마커가 없으면 init이 함께 설치한 루트의 `AGENTS.commitgate.md`를 계약으로 읽고, 사용자에게 `AGENTS.md`로의 병합을 요청하라.)
|
|
25
|
+
|
|
26
|
+
## 절차
|
|
27
|
+
|
|
28
|
+
1. `npm run req:new -- <slug> --run` — 티켓과 브랜치를 만든다.
|
|
29
|
+
2. 그다음부터는 **`npm run req:next -- <REQ-id>`가 시키는 대로** 한다.
|
|
30
|
+
- `RUN` → 출력된 명령을 그대로 실행 → 다시 `req:next`
|
|
31
|
+
- `AGENT` → 그 작업을 하고 `git add` → 다시 `req:next`
|
|
32
|
+
- `AWAIT_HUMAN` → **멈추고** 출력된 승인 문장을 그대로 받는다
|
|
33
|
+
- `DONE` / `BLOCKED` → 사용자에게 보고
|
|
34
|
+
3. 이 루프를 끊지 말고 반복한다. 다음 행동을 스스로 추측하지 마라.
|
|
35
|
+
|
|
36
|
+
첫 응답은 발행한 REQ 번호, 브랜치, phase 분해, 통제점을 요약해서 보여 준다.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commitgate
|
|
3
|
+
description: 이 저장소의 REQ 워크플로(CommitGate)로 요구사항을 처리한다. 기능 추가·버그 수정·리팩터링·문서 변경 등 코드를 커밋하게 되는 모든 작업에 사용한다. Codex 리뷰 승인 없이는 커밋이 통과하지 않는다.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CommitGate
|
|
7
|
+
|
|
8
|
+
이 저장소는 **CommitGate**를 쓴다. 코드 변경은 REQ 티켓 단위로 묶이고, Codex가 승인한 staged tree만 커밋된다.
|
|
9
|
+
|
|
10
|
+
**일반 구현으로 처리하지 마라.** 아래 계약을 먼저 읽어라.
|
|
11
|
+
|
|
12
|
+
## 계약 (정본)
|
|
13
|
+
|
|
14
|
+
저장소 루트의 **`AGENTS.md`**가 정본이다. 절대 규칙, 통제점 표, 승인 문장이 거기 있다.
|
|
15
|
+
|
|
16
|
+
> `AGENTS.md`에 `<!-- commitgate:contract -->` 마커가 없으면 그 파일은 CommitGate 계약이 아니다.
|
|
17
|
+
> 그 경우 `npx commitgate`가 저장소 루트에 **`AGENTS.commitgate.md`**(계약 템플릿 사본)를 함께 설치해 둔다.
|
|
18
|
+
> 그 파일을 계약으로 읽고, 사용자에게 `AGENTS.md`로의 병합을 요청하라.
|
|
19
|
+
|
|
20
|
+
## 요구사항 받기
|
|
21
|
+
|
|
22
|
+
사용자 요청이 아래 네 칸으로 정리되지 않으면 먼저 물어라. 추측하지 마라.
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
- 무엇을:
|
|
26
|
+
- 왜:
|
|
27
|
+
- 제약:
|
|
28
|
+
- 완료 기준:
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
정리되면 `req:new`로 티켓을 만든다.
|
|
32
|
+
|
|
33
|
+
## 진행 방법 — `req:next`가 시키는 대로
|
|
34
|
+
|
|
35
|
+
**다음 행동을 스스로 추측하지 마라.** 도구가 상태에서 계산해 준다.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm run req:next -- <REQ-id>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
출력의 `kind`가 정본이다(`--json`으로 기계 판독 가능).
|
|
42
|
+
|
|
43
|
+
| kind | 할 일 |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `RUN` | 출력된 명령을 **그대로** 실행하고, 다시 `req:next` |
|
|
46
|
+
| `AGENT` | 도구가 대신 못 하는 작업(구현·문서 작성·`git add`). 하고 나서 다시 `req:next` |
|
|
47
|
+
| `AWAIT_HUMAN` | **멈춘다.** 출력된 승인 문장을 사용자에게서 **그 문장 그대로** 받기 전에는 진행하지 않는다 |
|
|
48
|
+
| `DONE` | 이 티켓에서 도구가 할 일이 없다. 통합은 별도 통제점 |
|
|
49
|
+
| `BLOCKED` | 사람에게 보고한다. 같은 리뷰를 재시도하지 마라 |
|
|
50
|
+
|
|
51
|
+
이 루프를 **끊지 말고** 반복한다. `AWAIT_HUMAN`·`BLOCKED`·오류에서만 멈춘다.
|
|
52
|
+
|
|
53
|
+
## 반드시 지킬 것
|
|
54
|
+
|
|
55
|
+
- 리뷰 대상은 `git add` 한 파일뿐이다.
|
|
56
|
+
- `state.json`과 `responses/`는 직접 `git add` 하지 않는다 — 도구가 관리한다.
|
|
57
|
+
- `req:review-codex`가 exit 3(NEEDS_FIX)이면 findings를 수정하고 재리뷰한다.
|
|
58
|
+
- exit 2(BLOCKED)면 **같은 리뷰를 재시도하지 말고** 사람에게 보고한다.
|
|
59
|
+
- 승인은 받은 문장 그대로만 유효하며, 다음 통제점으로 **이월되지 않는다**.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: CommitGate — 코드 변경은 REQ 티켓 + Codex 리뷰 승인을 거쳐야 커밋된다
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CommitGate
|
|
7
|
+
|
|
8
|
+
이 저장소는 **CommitGate**를 쓴다. 코드 변경은 REQ 티켓 단위로 묶이고, Codex가 승인한 staged tree만 커밋된다.
|
|
9
|
+
|
|
10
|
+
코드를 커밋하게 되는 요청은 **일반 구현으로 처리하지 마라.**
|
|
11
|
+
|
|
12
|
+
## 계약 (정본)
|
|
13
|
+
|
|
14
|
+
저장소 루트의 **`AGENTS.md`**가 정본이다. 절대 규칙, 통제점 표, 승인 문장이 거기 있다.
|
|
15
|
+
|
|
16
|
+
> `AGENTS.md`에 `<!-- commitgate:contract -->` 마커가 없으면 그 파일은 CommitGate 계약이 아니다.
|
|
17
|
+
> 그 경우 저장소 루트의 **`AGENTS.commitgate.md`**(init이 함께 설치한 계약 템플릿 사본)를 계약으로 읽고,
|
|
18
|
+
> 사용자에게 `AGENTS.md`로의 병합을 요청하라.
|
|
19
|
+
|
|
20
|
+
## 요구사항 받기
|
|
21
|
+
|
|
22
|
+
아래 네 칸이 채워지지 않으면 먼저 물어라. 추측하지 마라.
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
- 무엇을:
|
|
26
|
+
- 왜:
|
|
27
|
+
- 제약:
|
|
28
|
+
- 완료 기준:
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 진행 방법 — `req:next`가 시키는 대로
|
|
32
|
+
|
|
33
|
+
다음 행동을 스스로 추측하지 마라. 도구가 상태에서 계산한다.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm run req:new -- <slug> --run # 티켓·브랜치 생성
|
|
37
|
+
npm run req:next -- <REQ-id> # 그다음은 항상 이것
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| kind | 할 일 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `RUN` | 출력된 명령을 그대로 실행하고 다시 `req:next` |
|
|
43
|
+
| `AGENT` | 구현·문서 작성·`git add` 후 다시 `req:next` |
|
|
44
|
+
| `AWAIT_HUMAN` | **멈춘다.** 출력된 승인 문장을 그대로 받기 전엔 진행 금지 |
|
|
45
|
+
| `DONE` | 도구가 할 일 없음. 통합은 별도 통제점 |
|
|
46
|
+
| `BLOCKED` | 사람에게 보고. 같은 리뷰 재시도 금지 |
|
|
47
|
+
|
|
48
|
+
이 루프를 끊지 말고 반복한다.
|
|
49
|
+
|
|
50
|
+
## 반드시 지킬 것
|
|
51
|
+
|
|
52
|
+
- 리뷰 대상은 `git add` 한 파일뿐이다.
|
|
53
|
+
- `state.json`·`responses/`는 직접 `git add` 하지 않는다.
|
|
54
|
+
- `req:review-codex` exit 3(NEEDS_FIX) → 수정 후 재리뷰. exit 2(BLOCKED) → 재시도 금지, 사람에게 보고.
|
|
55
|
+
- 승인은 받은 문장 그대로만 유효하고 다음 통제점으로 이월되지 않는다.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"ticketRoot": { "type": "string", "minLength": 1 },
|
|
6
6
|
"schemaPath": { "type": "string", "minLength": 1 },
|
|
7
7
|
"handoffPath": { "type": ["string", "null"] },
|
|
8
|
+
"reviewPersonaPath": { "type": ["string", "null"], "minLength": 1 },
|
|
8
9
|
"branchPrefix": { "type": "string", "minLength": 1 },
|
|
9
10
|
"packageManager": { "type": "string", "enum": ["pnpm", "npm", "yarn"] },
|
|
10
11
|
"granularityMaxFiles": { "type": "integer", "minimum": 1 },
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Reviewer 역할 (PM)
|
|
2
|
+
|
|
3
|
+
너는 이 프로젝트의 **PM**이다. 이 변경의 **적절성과 완성도**를 확보할 책임이 있다.
|
|
4
|
+
|
|
5
|
+
- Builder가 작성한 리뷰 요청서(`codex-request.md`)의 "리뷰 포인트"는 심사 범위의 **하한**이지 상한이 아니다. 요청서가 묻지 않은 결함도 스스로 분석해 지적하라.
|
|
6
|
+
- Builder가 짜 놓은 리뷰 프레임에 갇히지 마라. 무엇을 봐야 하는지는 네가 판단한다.
|
|
7
|
+
- 개발 부채가 남지 않도록 하라. 지금 넘어가면 나중에 갚아야 하는 것을 식별하라.
|
|
8
|
+
|
|
9
|
+
## 판정은 구조화 응답 필드로만 낸다
|
|
10
|
+
|
|
11
|
+
| 필드 | 담는 것 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `findings[]` | **이 변경을 지금 커밋하면 안 되는 이유**만 |
|
|
14
|
+
| `observations[]` | 비차단 의견 — 스타일 취향, 범위 밖 개선, 후속 티켓 후보 |
|
|
15
|
+
| `next_action` | Builder가 다음에 할 일 |
|
|
16
|
+
| `status` / `commit_approved` | 게이트 판정 |
|
|
17
|
+
|
|
18
|
+
## 승인 규칙 (어기면 워크플로가 응답을 거부한다)
|
|
19
|
+
|
|
20
|
+
**승인(`commit_approved=yes`)은 `findings`가 0건일 때만 가능하다.** 지적이 하나라도 있으면 승인할 수 없다 — 워크플로가 그 모순을 검출해 응답 전체를 무효 처리한다.
|
|
21
|
+
|
|
22
|
+
그래서 `findings`에 무엇을 넣을지가 곧 승인 여부다.
|
|
23
|
+
|
|
24
|
+
- **`findings`에 넣을 것**: 정확성 결함, 안전·보안 구멍, fail-closed 우회, 계약 위반, 범위 이탈, 검증 누락 — 지금 커밋되면 안 되는 것.
|
|
25
|
+
- **`observations`에 넣을 것**: 이름·주석·구조 취향, 후속 리팩터 제안, 이 phase 범위를 넘는 개선, "나중에 보면 좋을 것".
|
|
26
|
+
|
|
27
|
+
**"개발 부채가 남지 않도록 하라"를 "부채 후보를 전부 `findings`로 올리라"로 읽지 마라.** 지금 넘어가도 되는 부채는 `observations`에 기록해 다음 티켓의 입력으로 만든다. 차단과 비차단의 경계를 흐리면 승인이 영영 나지 않고, 그것은 리뷰의 실패다.
|
|
28
|
+
|
|
29
|
+
`observations`에는 `severity`를 붙이지 않는다. severity가 붙는 순간 차단 신호가 되어 경계가 무너진다.
|
|
30
|
+
|
|
31
|
+
## 지적의 형태
|
|
32
|
+
|
|
33
|
+
- 결함마다 **재현 경로 또는 실패 시나리오**를 적어라. "이 입력/상태에서 이 결과가 나온다."
|
|
34
|
+
- 파일이 특정되면 `file`에 적어라. 전역 지적이면 비워도 된다.
|
|
35
|
+
- 추측이면 추측이라고 말하고 `observations`로 내려라. 확신하는 것만 `findings`에 올린다.
|
|
36
|
+
- 결함이 없으면 `findings` 없이 승인하라. 하고 싶은 말은 `observations`에 남긴다.
|
|
37
|
+
|
|
38
|
+
## 리뷰 대상
|
|
39
|
+
|
|
40
|
+
리뷰 종류는 프롬프트의 **REVIEW_KIND**를 따른다.
|
|
41
|
+
|
|
42
|
+
- `design` — 권위 아티팩트는 설계 문서 3종이다. 구현 diff가 없는 것이 정상이다. 설계의 결함·누락·모순을 본다.
|
|
43
|
+
- `phase` — 권위 아티팩트는 staged diff다. 그 diff만 심사한다.
|
|
44
|
+
|
|
45
|
+
리뷰 대상이 아닌 것을 근거로 지적하지 마라. 설계 리뷰에서 "구현이 없다"는 지적은 성립하지 않는다.
|