commitgate 0.4.0 → 0.8.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 +8 -0
- package/CHANGELOG.md +154 -0
- package/README.en.md +187 -20
- package/README.md +187 -20
- package/bin/commitgate.mjs +15 -6
- package/bin/dispatch.d.mts +6 -0
- package/bin/dispatch.mjs +38 -0
- package/bin/init.ts +965 -84
- package/bin/migrate.ts +244 -0
- package/bin/uninstall.ts +48 -1
- package/package.json +73 -69
- package/req.config.json.sample +3 -0
- package/scripts/req/lib/adapters.ts +56 -8
- package/scripts/req/lib/config.ts +55 -0
- package/scripts/req/lib/porcelain.ts +104 -0
- package/scripts/req/lib/scratch.ts +104 -0
- package/scripts/req/req-commit.ts +21 -7
- package/scripts/req/req-doctor.ts +135 -31
- package/scripts/req/req-new.ts +94 -15
- package/scripts/req/req-next.ts +94 -17
- package/scripts/req/review-codex.ts +851 -94
- package/scripts/verify-review-overrides.mjs +96 -0
- package/skills/ATTRIBUTION.md +85 -0
- package/skills/commitgate-diagnosing-bugs/SKILL.md +149 -0
- package/skills/commitgate-discovery/SKILL.md +93 -0
- package/skills/commitgate-research/SKILL.md +85 -0
- package/skills/commitgate-tdd/SKILL.md +113 -0
- package/templates/CLAUDE.template.md +2 -1
- package/templates/claude-command.md +5 -2
- package/templates/claude-skill.md +4 -1
- package/templates/cursor-rule.mdc +5 -2
- package/templates/workflow.gitignore +8 -0
- package/workflow/machine.schema.json +10 -1
- package/workflow/req.config.schema.json +90 -10
- package/workflow/review-persona.md +56 -1
package/scripts/req/req-next.ts
CHANGED
|
@@ -15,22 +15,28 @@
|
|
|
15
15
|
* ⚠️ **강제(enforcement)가 아니라 자문(advisory)이다.** 승인 게이트는 여전히 `req:review-codex`/`req:commit`에 있다.
|
|
16
16
|
* `req:next`가 틀려도 게이트는 뚫리지 않는다. 그래서 애매하면 `RUN` 쪽으로 기운다(fail-forward).
|
|
17
17
|
*
|
|
18
|
-
* 사용:
|
|
18
|
+
* 사용: req:next <REQ-id> [--json] [--root <path>] [--ticket <dir>] (저장소 패키지매니저의 실행 형식으로)
|
|
19
19
|
*/
|
|
20
20
|
import { resolve, join, relative } from 'node:path'
|
|
21
21
|
import { pathToFileURL } from 'node:url'
|
|
22
22
|
import { loadConfig, buildScriptInvocation, type PackageManager } from './lib/config'
|
|
23
23
|
import { createGitAdapter, type GitAdapter } from './lib/adapters'
|
|
24
|
+
import { parseStatusZ, STATUS_Z_ARGS } from './lib/porcelain'
|
|
25
|
+
import { reviewScratchPaths } from './lib/scratch'
|
|
24
26
|
import {
|
|
25
27
|
loadState,
|
|
26
28
|
readPhases,
|
|
27
29
|
captureDesignBinding,
|
|
28
30
|
captureIndexHash,
|
|
29
31
|
findUnstagedOrUntracked,
|
|
32
|
+
isLegacyTicket,
|
|
33
|
+
openSeriesAttempts,
|
|
34
|
+
isSeriesKeyTerminal,
|
|
30
35
|
type WorkflowState,
|
|
31
36
|
type ReviewKind,
|
|
32
37
|
type LastReviewMarker,
|
|
33
38
|
} from './review-codex'
|
|
39
|
+
import type { ReviewBudget } from './lib/config'
|
|
34
40
|
|
|
35
41
|
// ─────────────────────────────────────────────── 읽기 전용 git 경계 (D6-1) ──
|
|
36
42
|
|
|
@@ -128,6 +134,8 @@ export interface NextInput {
|
|
|
128
134
|
worktreeReviewClean: boolean
|
|
129
135
|
/** 현재 인덱스 전체 해시(`captureIndexHash`). 계산 불가면 null. */
|
|
130
136
|
currentIndexHash: string | null
|
|
137
|
+
/** REQ-2026-028 A-2a: review 예산(G3 escalated 판정용). main이 cfg에서 채운다. */
|
|
138
|
+
reviewBudget: ReviewBudget
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
/** `consumed_approvals[]`에서 phase_id를 안전하게 읽는다. */
|
|
@@ -317,6 +325,14 @@ function commitCmd(pm: PackageManager, target: NextTarget): string {
|
|
|
317
325
|
return buildScriptInvocation(pm, 'req:commit', [...targetArgs(target), '--run']).join(' ')
|
|
318
326
|
}
|
|
319
327
|
|
|
328
|
+
/**
|
|
329
|
+
* 대상(REQ id 또는 `--ticket`) 미지정 에러 문구(DEC-011-1). **config 로드 이후**라 pm별로 파생한다.
|
|
330
|
+
* 리터럴을 박으면 다른 pm 프로젝트의 사용자가 그대로 따라 할 수 없는 명령을 안내받는다.
|
|
331
|
+
*/
|
|
332
|
+
export function missingTargetHint(pm: PackageManager): string {
|
|
333
|
+
return `REQ id 또는 --ticket <dir> 필요 (예: ${buildScriptInvocation(pm, 'req:next', ['2026-010']).join(' ')})`
|
|
334
|
+
}
|
|
335
|
+
|
|
320
336
|
interface RunCandidate {
|
|
321
337
|
command: string
|
|
322
338
|
kind: ReviewKind
|
|
@@ -345,6 +361,48 @@ function gateRunCandidate(input: NextInput, cand: RunCandidate): NextAction {
|
|
|
345
361
|
'워킹트리에 unstaged/untracked 변경이 있어 리뷰(D10)가 실패한다. 의도한 변경은 `git add`, 그 외는 정리한 뒤 다시 req:next.',
|
|
346
362
|
}
|
|
347
363
|
|
|
364
|
+
// terminal (REQ-2026-029 A-2b): human-resolution으로 종결된 키 → AWAIT_HUMAN. **G3보다 앞**(R4) — 종결된
|
|
365
|
+
// series는 예산 안내("고치고 예외 받으라")가 아니라 "이미 끝났다 — 대체 REQ" 안내가 맞다. G1보다는 뒤
|
|
366
|
+
// (dirty면 정리 먼저). 우선순위: G1 → terminal → G3 → G2.
|
|
367
|
+
if (isSeriesKeyTerminal(input.state, cand.kind, cand.phaseId))
|
|
368
|
+
return {
|
|
369
|
+
kind: 'AWAIT_HUMAN',
|
|
370
|
+
detail: '이 series는 human-resolution으로 종결됐다. 같은 키에서 자동으로 재개하지 않는다.',
|
|
371
|
+
controlPoint: 'human-resolution 종결됨',
|
|
372
|
+
approvalSentence: '대체가 필요하면 `req:new --successor-of <이 REQ>`로 만든다(종결 상태 유지 결정도 사람이 한다)',
|
|
373
|
+
diagnostics: ['종결 사유: human-resolution', '재개는 자동으로 일어나지 않는다 — 대체 REQ 또는 종결 유지.'],
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// G3 (REQ-2026-028 A-2a): 자동 예산 소진(escalated) → AWAIT_HUMAN. **G2보다 앞**(R13) — 5회차 NEEDS_FIX
|
|
377
|
+
// 직후엔 escalated와 같은 바인딩 needs-fix가 동시 성립하는데, G2가 먼저 "findings 고치고 다시 add"(AGENT)를
|
|
378
|
+
// 내면 그 조언이 거짓이다(고쳐도 사람 승인 없이 6회차가 안 열린다). escalated는 파생값(저장 안 함, R11).
|
|
379
|
+
const openAttempts = openSeriesAttempts(input.state, cand.kind, cand.phaseId)
|
|
380
|
+
const { autoBudget, hardCap } = input.reviewBudget
|
|
381
|
+
if (openAttempts >= autoBudget) {
|
|
382
|
+
const nextAttempt = openAttempts + 1
|
|
383
|
+
const lrOutcome = (input.state.last_review as LastReviewMarker | undefined)?.outcome ?? '(없음)'
|
|
384
|
+
const hardBlocked = openAttempts >= hardCap
|
|
385
|
+
// ⚠️ "위험 수용"은 어느 문구에도 넣지 않는다(배분표 ④ — 부정문으로도 금지). 긍정 선택지만 나열.
|
|
386
|
+
const options = hardBlocked
|
|
387
|
+
? '예외로도 진행 불가 — 종료하거나 정합한 대체 REQ를 작성한다.'
|
|
388
|
+
: '사람 승인 시 1회 예외 가능(review_exception_confirmed) · 종료 · 정합한 대체 REQ 작성.'
|
|
389
|
+
return {
|
|
390
|
+
kind: 'AWAIT_HUMAN',
|
|
391
|
+
detail: hardBlocked
|
|
392
|
+
? `이 series는 하드 상한(hardCap=${hardCap})에 도달했다. ${nextAttempt}회차는 어떤 경로로도 실행하지 않는다.`
|
|
393
|
+
: `이 series는 자동 예산(autoBudget=${autoBudget})을 소진했다. ${nextAttempt}회차는 사람 결정이 필요하다.`,
|
|
394
|
+
controlPoint: 'review 예산 소진(escalated)',
|
|
395
|
+
approvalSentence: hardBlocked
|
|
396
|
+
? 'review 하드 상한 도달 — 종료 또는 대체 REQ 작성(둘 중 하나를 사람이 결정)'
|
|
397
|
+
: `review ${nextAttempt}회차 예외 승인(또는 종료·대체 REQ 작성)`,
|
|
398
|
+
diagnostics: [
|
|
399
|
+
`series 시도 수(openAttempts)=${openAttempts} · 다음 회차=${nextAttempt}`,
|
|
400
|
+
`직전 리뷰 outcome=${lrOutcome}`,
|
|
401
|
+
`선택지: ${options}`,
|
|
402
|
+
],
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
348
406
|
// G2
|
|
349
407
|
const lr = input.state.last_review as LastReviewMarker | undefined
|
|
350
408
|
const sameTarget =
|
|
@@ -428,6 +486,18 @@ export function resolveNext(input: NextInput): NextAction {
|
|
|
428
486
|
approvalSentence: 'req:commit --run 승인',
|
|
429
487
|
}
|
|
430
488
|
|
|
489
|
+
// 1.5 legacy ticket(REQ-2026-027 D1): 모델 버전 부재 = legacy. 살아 있는 승인(1번)보다는 뒤 —
|
|
490
|
+
// 그건 소비만 하면 되고 새 외부 호출이 아니다. design/phase RUN 후보(2·3번)보다는 **앞** — 그 후보를
|
|
491
|
+
// 내면 사용자가 실행한 뒤에야 호출 지점에서 throw된다(R2는 AWAIT_HUMAN을 요구). 자동 초기화하지 않는다.
|
|
492
|
+
if (isLegacyTicket(state))
|
|
493
|
+
return {
|
|
494
|
+
kind: 'AWAIT_HUMAN',
|
|
495
|
+
detail:
|
|
496
|
+
'legacy ticket(review_series_model_version 부재)이다. 자동으로 새 모델로 초기화하지 않는다 — 사람이 이 티켓을 새 series 모델로 채택할지 결정해야 한다.',
|
|
497
|
+
controlPoint: 'legacy 티켓 채택',
|
|
498
|
+
approvalSentence: 'state.json에 review_series_model_version: 1 추가(이 티켓을 새 모델로 채택) 승인',
|
|
499
|
+
}
|
|
500
|
+
|
|
431
501
|
// 2. 설계 문서가 인덱스에 없으면 3번의 freshness 판정(captureDesignBinding)이 throw한다. 여기서 먼저 거른다.
|
|
432
502
|
if (!input.designDocsInIndex)
|
|
433
503
|
return {
|
|
@@ -553,7 +623,11 @@ export function parseArgs(argv: string[]): Opts {
|
|
|
553
623
|
for (let i = 0; i < argv.length; i++) {
|
|
554
624
|
const a = argv[i]
|
|
555
625
|
if (a === undefined) continue
|
|
556
|
-
|
|
626
|
+
// bare `--`는 옵션이 아니라 POSIX end-of-options 마커다(DEC-011-3). npm은 `npm run x -- a`에서
|
|
627
|
+
// 이를 제거하지만 pnpm/yarn은 그대로 넘긴다 → 흡수한다. 이후 인자도 계속 옵션으로 파싱한다
|
|
628
|
+
// (전부 위치인자로 삼키면 `req:commit <id> -- --run`이 조용히 dry-run이 된다).
|
|
629
|
+
if (a === '--') continue
|
|
630
|
+
else if (a === '--json') o.json = true
|
|
557
631
|
else if (a === '--ticket') o.ticket = argv[++i] ?? null
|
|
558
632
|
else if (a === '--root') {
|
|
559
633
|
const v = argv[++i]
|
|
@@ -578,15 +652,14 @@ export function renderAction(displayId: string, a: NextAction): string {
|
|
|
578
652
|
return lines.join('\n')
|
|
579
653
|
}
|
|
580
654
|
|
|
581
|
-
function main(): void {
|
|
582
|
-
const opts = parseArgs(
|
|
655
|
+
export function main(argv: string[] = process.argv.slice(2)): void {
|
|
656
|
+
const opts = parseArgs(argv)
|
|
583
657
|
const cfg = loadConfig({ root: opts.root })
|
|
584
658
|
const roGit = createReadOnlyGit(createGitAdapter(cfg.root))
|
|
585
659
|
|
|
586
660
|
// target은 **사용자가 티켓을 지목한 방식 그대로** 보존한다(R5). `--ticket`으로 읽었으면 후속 명령도 `--ticket`을 쓴다 —
|
|
587
661
|
// 그러지 않으면 `req:review-codex -- <reqId>`가 기본 위치의 **다른 티켓**을 리뷰한다.
|
|
588
|
-
if (!opts.ticket && !opts.reqId)
|
|
589
|
-
throw new Error('REQ id 또는 --ticket <dir> 필요 (예: npm run req:next -- 2026-010)')
|
|
662
|
+
if (!opts.ticket && !opts.reqId) throw new Error(missingTargetHint(cfg.packageManager))
|
|
590
663
|
const target: NextTarget = opts.ticket
|
|
591
664
|
? { kind: 'ticket', ticketDir: opts.ticket }
|
|
592
665
|
: { kind: 'req', reqId: (opts.reqId as string).replace(/^REQ-/, '') }
|
|
@@ -605,16 +678,9 @@ function main(): void {
|
|
|
605
678
|
currentDesignHash = null
|
|
606
679
|
}
|
|
607
680
|
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
// review-codex/doctor와 동일한 스크래치 집합 — 워크플로 도구가 쓰는 메타데이터는 D10 대상이 아니다.
|
|
612
|
-
const repoRel = (abs: string) => relative(cfg.root, abs).replace(/\\/g, '/')
|
|
613
|
-
const scratch = [
|
|
614
|
-
repoRel(join(ticketDir, 'codex-response.json')),
|
|
615
|
-
repoRel(join(ticketDir, '.review-preview.txt')),
|
|
616
|
-
repoRel(join(ticketDir, 'state.json')),
|
|
617
|
-
]
|
|
681
|
+
const statusEntries = parseStatusZ(roGit([...STATUS_Z_ARGS]))
|
|
682
|
+
// review-codex/doctor와 동일한 스크래치 집합(lib/scratch SSOT) — 워크플로 도구가 쓰는 메타데이터는 D10 대상이 아니다.
|
|
683
|
+
const scratch = reviewScratchPaths(ticketRel)
|
|
618
684
|
|
|
619
685
|
const action = resolveNext({
|
|
620
686
|
target,
|
|
@@ -623,8 +689,9 @@ function main(): void {
|
|
|
623
689
|
designDocsInIndex: currentDesignHash !== null,
|
|
624
690
|
currentDesignHash,
|
|
625
691
|
hasStagedChanges: roGit(['diff', '--cached', '--name-only']).trim().length > 0,
|
|
626
|
-
worktreeReviewClean: findUnstagedOrUntracked(
|
|
692
|
+
worktreeReviewClean: findUnstagedOrUntracked(statusEntries, scratch, ticketRel).length === 0,
|
|
627
693
|
currentIndexHash: captureIndexHash(roGit),
|
|
694
|
+
reviewBudget: cfg.reviewBudget,
|
|
628
695
|
})
|
|
629
696
|
|
|
630
697
|
if (opts.json) console.log(JSON.stringify({ req_id: state.id, ...action }, null, 2))
|
|
@@ -634,5 +701,15 @@ function main(): void {
|
|
|
634
701
|
if (code !== 0) process.exit(code)
|
|
635
702
|
}
|
|
636
703
|
|
|
704
|
+
/** bin dispatch 진입점(친절한 1줄 오류 + exit 1 경계). 직접 `tsx` 실행은 아래 `if (isMain) main()`이 그대로 담당(하위호환). */
|
|
705
|
+
export function runCli(argv: string[]): void {
|
|
706
|
+
try {
|
|
707
|
+
main(argv)
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.error(`commitgate: ${err instanceof Error ? err.message : String(err)}`)
|
|
710
|
+
process.exitCode = 1
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
637
714
|
const isMain = import.meta.url === pathToFileURL(process.argv[1] ?? '').href
|
|
638
715
|
if (isMain) main()
|