commitgate 0.7.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/CHANGELOG.md +39 -0
- package/README.en.md +75 -3
- package/README.md +70 -3
- package/bin/init.ts +199 -28
- package/bin/uninstall.ts +5 -0
- package/package.json +73 -72
- package/req.config.json.sample +1 -0
- package/scripts/req/lib/config.ts +30 -0
- package/scripts/req/req-commit.ts +5 -4
- package/scripts/req/req-new.ts +35 -6
- package/scripts/req/req-next.ts +61 -0
- package/scripts/req/review-codex.ts +658 -31
- 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/workflow/machine.schema.json +5 -0
- package/workflow/req.config.schema.json +88 -13
- package/workflow/review-persona.md +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
이 프로젝트는 [Semantic Versioning](https://semver.org/lang/ko/)을 따릅니다.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.8.0
|
|
8
|
+
|
|
9
|
+
리뷰 루프 수렴 안정화와 design delta review가 핵심입니다. 모두 `0.7.0` 설치 모델 위의 **추가 기능**이라 기존
|
|
10
|
+
사용자는 별도 조치가 필요 없습니다 — 패키지를 업그레이드한 뒤 `commitgate init`으로 갱신된 관리 자산을 받습니다.
|
|
11
|
+
|
|
12
|
+
### Companion Skills
|
|
13
|
+
|
|
14
|
+
- **Companion Skills 추가 및 lifecycle 문서화** — `commitgate init`이 `.claude/skills/commitgate-*/SKILL.md` 4종
|
|
15
|
+
(`discovery`·`tdd`·`diagnosing-bugs`·`research`)을 함께 설치합니다. 설치·보존·경고·제거 계획·지원 범위는
|
|
16
|
+
[README](README.md#companion-skills) / [README (English)](README.en.md#companion-skills)를 참조하세요.
|
|
17
|
+
- **`init` 쓰기 경로 symlink confinement** — 설치 대상 전 경로에서 상위 디렉터리·leaf를 `lstat`으로 검사해
|
|
18
|
+
대상 루트 밖을 가리키는 symlink를 따라가지 않습니다(우발적 symlink로 인한 외부 파일 생성·덮어쓰기 차단).
|
|
19
|
+
|
|
20
|
+
### 리뷰 루프 수렴 안정화
|
|
21
|
+
|
|
22
|
+
- **리뷰 시도 계수·예산 게이트** — `(review_kind, phase_id)`별 review series로 시도를 계수하고, 자동 예산
|
|
23
|
+
(`reviewBudget.autoBudget`, 기본 5)을 넘으면 사람 예외 손기록이 있어야 진행, 하드캡(`reviewBudget.hardCap`,
|
|
24
|
+
기본 8)에서 완전 차단합니다. `req.config.json`의 `reviewBudget`로 조정합니다 — 무한 재리뷰 루프를 막습니다.
|
|
25
|
+
- **리뷰 배칭** — 한 라운드에서 여러 P1을 함께 반환하도록 유도해 라운드 수를 줄입니다.
|
|
26
|
+
- **대체 REQ lineage** — 미수렴 REQ를 사람 결정(`human-resolution`)으로 종료하고 `req:new --successor-of <REQ>`로
|
|
27
|
+
부모 이력을 보존한 대체 REQ를 만듭니다.
|
|
28
|
+
|
|
29
|
+
### Design delta review
|
|
30
|
+
|
|
31
|
+
- **design 재리뷰가 delta로 동작** — 승인된 설계 baseline 이후 **변경된 문서만** 심사하도록 리뷰 프롬프트를
|
|
32
|
+
구성합니다. 변경 문서는 `[변경됨]`, 미변경 문서는 `[승인 baseline]`으로 표시하고, "변경분·직접 영향만 심사,
|
|
33
|
+
승인 영역 재심사 금지" 계약을 리뷰어에게 겁니다. 미변경 문서 본문은 생략해 토큰을 절감합니다 — 승인 후
|
|
34
|
+
작은 편집이 전체 재리뷰를 유발해 승인이 되돌려지던 문제를 줄입니다.
|
|
35
|
+
- **full review escalation** — 변경이 너무 근본적이라 delta로 판단할 수 없으면 리뷰어가 `full_review_requested`로
|
|
36
|
+
전체 재리뷰를 요청할 수 있습니다(다음 라운드가 full 모드로 전환). `reviewPersonaPath: null`이어도 delta
|
|
37
|
+
design 리뷰에는 내장 delta 계약이 주입됩니다.
|
|
38
|
+
|
|
39
|
+
### 기타
|
|
40
|
+
|
|
41
|
+
- **ISO 타임스탬프 달력 검증** — 손기록·evidence의 ISO 타임스탬프를 형식뿐 아니라 달력 유효성까지 검사합니다
|
|
42
|
+
(`2026-99-99T…` 같은 달력상 불가능한 값을 거부).
|
|
43
|
+
|
|
5
44
|
## 0.7.0
|
|
6
45
|
|
|
7
46
|
**설치 모델이 바뀝니다 — 기존 사용자는 조치가 필요합니다.** 실행 코드와 런타임 의존성을 대상 프로젝트에 복사·주입하지 않고, `commitgate` 패키지에서 실행합니다. 프로젝트에는 거버넌스·감사 데이터만 남습니다.
|
package/README.en.md
CHANGED
|
@@ -57,7 +57,7 @@ git commit -m "chore: install commitgate"
|
|
|
57
57
|
| File | Read by |
|
|
58
58
|
|---|---|
|
|
59
59
|
| `AGENTS.md` | Codex CLI, Cursor — **the contract** |
|
|
60
|
-
| `.claude/skills/commitgate/SKILL.md` | Claude Code (auto-
|
|
60
|
+
| `.claude/skills/commitgate/SKILL.md` | Claude Code (auto-discovered — the model decides whether to use it) |
|
|
61
61
|
| `.claude/commands/req.md` | Claude Code (`/req` explicit call) |
|
|
62
62
|
| `.cursor/rules/commitgate.mdc` | Cursor (`alwaysApply`) |
|
|
63
63
|
| `CLAUDE.md` | Claude Code (always loaded) — created only if absent |
|
|
@@ -117,7 +117,7 @@ Repeat this loop without stopping and it drives design → Codex review → impl
|
|
|
117
117
|
|
|
118
118
|
`req:review-codex` puts `workflow/review-persona.md` in as the **first block** of the prompt. It is identical whether a human, Cursor, or Claude runs the command — it does not live where an agent can forget it. If the file is missing or empty, the review stops fail-closed.
|
|
119
119
|
|
|
120
|
-
Edit it for your project, or point `reviewPersonaPath` in `req.config.json` at a different file. Set it to `null` to disable.
|
|
120
|
+
Edit it for your project, or point `reviewPersonaPath` in `req.config.json` at a different file. Set it to `null` to disable — but **delta design reviews still inject the built-in delta contract** (the contract that tells the reviewer to re-check only what changed since the approved baseline, so it is attached regardless of the configured persona).
|
|
121
121
|
|
|
122
122
|
Both integration paths are valid: **through a PR (optional)** and **direct push**. A PR is not mandatory. But a direct push to a protected branch **bypasses the required status checks**, so it needs a separate "branch protection bypass를 사용한 direct push 승인" — holding bypass permission is not approval. In that case CI runs **after** the push, so its green is post-hoc verification, and the agent must not omit that from its report. tag, npm publish, and GitHub release are control points of their own, requested after CI is green and never bundled with the integration approval. See [AGENTS.template.md](AGENTS.template.md) and [docs/RELEASING.md](docs/RELEASING.md) for the full contract.
|
|
123
123
|
|
|
@@ -162,8 +162,57 @@ So that you do not miscalculate where your real defenses are:
|
|
|
162
162
|
| `.claude/skills/commitgate/SKILL.md` | Claude Code skill (pointer) |
|
|
163
163
|
| `.claude/commands/req.md` | `/req` slash command (pointer) |
|
|
164
164
|
| `.cursor/rules/commitgate.mdc` | Cursor rule (pointer) |
|
|
165
|
+
| `.claude/skills/commitgate-*/SKILL.md` | **Companion Skills** — four of them, see below (existing files preserved) |
|
|
165
166
|
| `package.json` scripts | `req:new`·`req:next`·`req:review-codex`·`req:doctor`·`req:commit` = `commitgate <verb>` (missing keys only) |
|
|
166
167
|
|
|
168
|
+
### Companion Skills
|
|
169
|
+
|
|
170
|
+
CommitGate is a **governance layer** — `req:next` computes the next action, and review/approval/evidence gate the commit.
|
|
171
|
+
What was missing was **method**: how to sharpen a vague request, how to write the test first, how to corner a bug.
|
|
172
|
+
Four skills, adapted from Matt Pocock's public skills (MIT) to fit CommitGate's authority boundaries, ship with it.
|
|
173
|
+
|
|
174
|
+
| Skill | When |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `commitgate-discovery` | **Before** `req:new` — turn a vague request into a REQ Brief. **User-invoked** |
|
|
177
|
+
| `commitgate-tdd` | When `req:next` returns `AGENT` — Red → Green → Refactor → stage |
|
|
178
|
+
| `commitgate-diagnosing-bugs` | Bugs, regressions, perf — feedback loop → reproduce/minimise → hypothesise → instrument → fix |
|
|
179
|
+
| `commitgate-research` | External technology choices — primary sources, findings with citations and limits |
|
|
180
|
+
|
|
181
|
+
**Auto-discovered, model-invoked.** The harness **discovers** skills automatically, but **the model decides**
|
|
182
|
+
whether to use one — that is probabilistic, so don't expect a skill to always fire. In Claude Code you can also
|
|
183
|
+
invoke one directly with `/commitgate-<name>`. On other harnesses, use whatever invocation that harness offers,
|
|
184
|
+
or follow the entry flow in `AGENTS.md`.
|
|
185
|
+
|
|
186
|
+
**Suggested flow**: `commitgate-discovery` to sharpen the request → `/req` (Claude Code) or the `AGENTS.md` entry
|
|
187
|
+
flow → `req:new` → repeat `req:next`.
|
|
188
|
+
|
|
189
|
+
#### Boundaries — read this
|
|
190
|
+
|
|
191
|
+
- 🔴 **`AGENTS.md` is the contract.** Skills carry **method**, not contract.
|
|
192
|
+
Without the skills installed, the **core workflow behaves identically**.
|
|
193
|
+
- 🔴 **Skill output is not approval evidence.** Neither a companion skill's output nor the result of running
|
|
194
|
+
Matt's external skills separately is **approval evidence** for CommitGate or Codex. Running the review, judging
|
|
195
|
+
approval, transitioning state, and committing are **CommitGate's alone**, and `req:next` is the authority on
|
|
196
|
+
what comes next.
|
|
197
|
+
- Skills are **cooperative text** — a skill doesn't block a commit; CommitGate's gate does.
|
|
198
|
+
|
|
199
|
+
#### Install, preservation, options
|
|
200
|
+
|
|
201
|
+
- **`--no-agent-entrypoints`**: skips the whole `.claude/` layer (including the four companion skills).
|
|
202
|
+
- **Existing files preserved (seed-once)**: skills are **meant to be edited**. A skill you modified is
|
|
203
|
+
**not overwritten, even with `--force`.** `AGENTS.md`, `CLAUDE.md`, and `workflow/.gitignore` follow the same policy.
|
|
204
|
+
- **gitignore warning**: if `.claude/` is gitignored, the skills never reach a teammate's fresh clone.
|
|
205
|
+
Install still proceeds, but CommitGate **warns** and tells you how to track them. **`--strict` stops before installing.**
|
|
206
|
+
- **Coexists with third-party skills**: third-party `tdd`, `grill-me`, etc. live at `.claude/skills/<name>/`, companions at
|
|
207
|
+
`.claude/skills/commitgate-<name>/` — **different paths, so neither touches the other.**
|
|
208
|
+
|
|
209
|
+
#### Attribution
|
|
210
|
+
|
|
211
|
+
Adapted from Matt Pocock's MIT-licensed public skills at baseline SHA `d574778f94cf620fcc8ce741584093bc650a61d3`
|
|
212
|
+
and **included as package payload**. CommitGate **does not run or depend on any external skill installer** at
|
|
213
|
+
runtime — these are pinned copies inside the package. Each SKILL.md carries the full MIT notice; see
|
|
214
|
+
`skills/ATTRIBUTION.md` in the package for details.
|
|
215
|
+
|
|
167
216
|
### What it does **not** install
|
|
168
217
|
|
|
169
218
|
| Item | Where it lives instead |
|
|
@@ -236,6 +285,29 @@ npx commitgate migrate --apply # rewrites only the req:* scripts in package.json
|
|
|
236
285
|
|
|
237
286
|
**Reproducibility**: the review model/effort pins in `req.config.json`, plus the schemas and persona, stay in your project, so past review inputs are reproducible from git history. Runtime versions are pinned by your lockfile — **commit `package-lock.json`** (or the pnpm/yarn equivalent).
|
|
238
287
|
|
|
288
|
+
### Companion Skills discovery scope
|
|
289
|
+
|
|
290
|
+
**Installation is identical everywhere.** What follows is about whether a harness **discovers** those files.
|
|
291
|
+
|
|
292
|
+
| harness | Discovery |
|
|
293
|
+
|---|---|
|
|
294
|
+
| **Claude Code** | Reads `.claude/skills/<name>/SKILL.md` natively |
|
|
295
|
+
| **Cursor (editor)** | Reads `.claude/skills` as a compatibility path |
|
|
296
|
+
| **Cursor (CLI)** | ⚠️ **May differ by version and run mode — not guaranteed** |
|
|
297
|
+
| **Codex** | **Out of product scope** — no companion entrypoint is installed. In CommitGate, Codex is the **Reviewer**; these four are **Builder aids** |
|
|
298
|
+
|
|
299
|
+
⚠️ **This is based on vendor primary documentation — the CommitGate team did not verify it empirically.**
|
|
300
|
+
Checked **2026-07-17** on win32 x64 / Node v20.19.5. If a vendor changes behaviour, this table goes stale.
|
|
301
|
+
|
|
302
|
+
⚠️ **We do not claim Cursor CLI is either supported or unsupported.** Cursor announced Agent Skills for both
|
|
303
|
+
editor and CLI, but discovery via the `.claude/skills` compatibility path is reported to differ by version and
|
|
304
|
+
run mode, and we could not verify it. If discovery doesn't happen, **the core workflow is unaffected** — skills
|
|
305
|
+
are a quality aid, and `AGENTS.md` is the contract.
|
|
306
|
+
|
|
307
|
+
We do **not** double-install into `.cursor/skills` to work around this: that path's CLI behaviour is also
|
|
308
|
+
uncertain, and the same content in two places invites drift. If the vendor fixes it, it works **with no change
|
|
309
|
+
on our side** — same path.
|
|
310
|
+
|
|
239
311
|
---
|
|
240
312
|
|
|
241
313
|
## Removing CommitGate
|
|
@@ -422,7 +494,7 @@ Defaults are enough for most projects. If needed, edit `req.config.json` in the
|
|
|
422
494
|
| `ticketRoot` | `"workflow"` | REQ ticket directory |
|
|
423
495
|
| `packageManager` | auto-detected | `npm`, `pnpm`, or `yarn` |
|
|
424
496
|
| `designDocs` | `00/01/02` docs | Design document filenames |
|
|
425
|
-
| `reviewPersonaPath` | `"workflow/review-persona.md"` | First block of the review prompt. `null` disables it |
|
|
497
|
+
| `reviewPersonaPath` | `"workflow/review-persona.md"` | First block of the review prompt. `null` disables it — but delta design reviews still inject the built-in delta contract |
|
|
426
498
|
| `reviewModel` | `"gpt-5.6-terra"` | codex review model (pinned via `-c model=`). `null` inherits your global codex config |
|
|
427
499
|
| `reviewReasoningEffort` | `"high"` | codex review reasoning effort. One of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. `null` inherits the global setting |
|
|
428
500
|
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ git commit -m "chore: install commitgate"
|
|
|
57
57
|
| 파일 | 읽는 도구 |
|
|
58
58
|
|---|---|
|
|
59
59
|
| `AGENTS.md` | Codex CLI, Cursor — **계약 정본** |
|
|
60
|
-
| `.claude/skills/commitgate/SKILL.md` | Claude Code (
|
|
60
|
+
| `.claude/skills/commitgate/SKILL.md` | Claude Code (자동 발견 — 호출은 모델 판단) |
|
|
61
61
|
| `.claude/commands/req.md` | Claude Code (`/req` 명시 호출) |
|
|
62
62
|
| `.cursor/rules/commitgate.mdc` | Cursor (`alwaysApply`) |
|
|
63
63
|
| `CLAUDE.md` | Claude Code (항상 로드) — 부재 시에만 생성 |
|
|
@@ -117,7 +117,7 @@ npm run req:next -- 2026-002
|
|
|
117
117
|
|
|
118
118
|
`req:review-codex`는 `workflow/review-persona.md`를 프롬프트 **첫 블록**으로 넣습니다. 사람이 직접 실행하든, Cursor가 실행하든, Claude가 실행하든 동일합니다 — 에이전트가 잊을 수 있는 자리에 두지 않습니다. 파일이 없거나 비어 있으면 리뷰가 fail-closed로 멈춥니다.
|
|
119
119
|
|
|
120
|
-
내용을 프로젝트에 맞게 고치거나, `req.config.json`의 `reviewPersonaPath`로 다른 파일을 지정할 수 있습니다. `null`로 두면
|
|
120
|
+
내용을 프로젝트에 맞게 고치거나, `req.config.json`의 `reviewPersonaPath`로 다른 파일을 지정할 수 있습니다. `null`로 두면 비활성화됩니다 — 다만 **delta design 리뷰에는 내장 delta 계약이 주입된다**(승인 baseline 이후 변경분만 재검토하도록 리뷰어에게 거는 계약이라, 설정 persona와 무관하게 붙습니다).
|
|
121
121
|
|
|
122
122
|
main에 반영하는 경로는 **PR 경유(선택)**와 **direct push** 둘 다 유효합니다. PR은 의무가 아닙니다. 다만 protected branch로 직접 push하면 required checks를 **우회**하므로 "branch protection bypass를 사용한 direct push 승인"을 따로 받아야 합니다 — bypass 권한이 있다는 사실은 승인이 아닙니다. 그리고 이때 CI는 push **이후에** 도는 **사후 검증**이라, 그 사실을 보고에서 생략하지 않습니다. tag, npm publish, GitHub release는 반영과 묶이지 않는 별도 통제점이고 CI green 이후에 요청합니다. 자세한 계약은 [AGENTS.template.md](AGENTS.template.md)와 [docs/RELEASING.md](docs/RELEASING.md)를 참고하세요.
|
|
123
123
|
|
|
@@ -162,8 +162,54 @@ CommitGate가 막는 것은 단순한 명령 실수가 아니라 **리뷰받지
|
|
|
162
162
|
| `.claude/skills/commitgate/SKILL.md` | Claude Code 스킬 (포인터) |
|
|
163
163
|
| `.claude/commands/req.md` | `/req` 슬래시 커맨드 (포인터) |
|
|
164
164
|
| `.cursor/rules/commitgate.mdc` | Cursor 규칙 (포인터) |
|
|
165
|
+
| `.claude/skills/commitgate-*/SKILL.md` | **Companion Skills** 4종 — 아래 참조 (기존 파일 보존) |
|
|
165
166
|
| `package.json` 스크립트 | `req:new`·`req:next`·`req:review-codex`·`req:doctor`·`req:commit` = `commitgate <verb>` (없는 키만) |
|
|
166
167
|
|
|
168
|
+
### Companion Skills
|
|
169
|
+
|
|
170
|
+
CommitGate는 **거버넌스 레이어**입니다 — `req:next`가 다음 행동을 계산하고, 리뷰·승인·증거가 커밋을 게이트합니다.
|
|
171
|
+
그런데 "무엇을 만들지 정리하는 법", "테스트를 어떻게 먼저 쓰는지" 같은 **방법론**은 비어 있었습니다.
|
|
172
|
+
Matt Pocock의 공개 skills(MIT)를 CommitGate의 권한 경계에 맞게 적응해 4종을 함께 설치합니다.
|
|
173
|
+
|
|
174
|
+
| 스킬 | 언제 |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `commitgate-discovery` | `req:new` **전** — 모호한 요구를 REQ Brief로 정리. **사용자 호출형** |
|
|
177
|
+
| `commitgate-tdd` | `req:next`가 `AGENT`일 때 — Red → Green → Refactor → stage |
|
|
178
|
+
| `commitgate-diagnosing-bugs` | 버그·회귀·성능 — 피드백 루프 → 재현·최소화 → 가설 → 계측 → 수정 |
|
|
179
|
+
| `commitgate-research` | 외부 기술 선택 — 1차 출처 조사, 결론·출처·한계 |
|
|
180
|
+
|
|
181
|
+
**자동 발견 · 모델 판단 호출.** harness가 스킬을 자동으로 **발견**하지만, 쓸지 **판단하는 건 모델**입니다 —
|
|
182
|
+
확률적이며 항상 뜬다고 기대하면 안 됩니다. Claude Code에서는 `/commitgate-<이름>`으로 **직접 호출**할 수도 있습니다.
|
|
183
|
+
다른 harness에서는 그 harness가 제공하는 호출 방식을 쓰거나, `AGENTS.md`의 진입 흐름을 따르세요.
|
|
184
|
+
|
|
185
|
+
**권장 흐름**: `commitgate-discovery`로 요구 정리 → `/req`(Claude Code) 또는 `AGENTS.md` 진입 → `req:new` → `req:next` 반복.
|
|
186
|
+
|
|
187
|
+
#### 경계 — 반드시 알아 두세요
|
|
188
|
+
|
|
189
|
+
- 🔴 **`AGENTS.md`가 계약 정본입니다.** 스킬은 **방법론**이지 계약이 아닙니다.
|
|
190
|
+
스킬을 설치하지 않아도 **핵심 워크플로는 완전히 동일하게** 동작합니다.
|
|
191
|
+
- 🔴 **스킬 결과는 승인 증거가 아닙니다.** companion skills의 산출물도, 외부 Matt skills를 따로 돌린 결과도
|
|
192
|
+
CommitGate·Codex의 **승인 근거가 되지 않습니다**. 리뷰 실행·승인 판정·상태 전이·커밋은 **CommitGate만** 담당하며,
|
|
193
|
+
다음 행동은 `req:next`가 정본입니다.
|
|
194
|
+
- 스킬은 **협조적 텍스트**입니다 — 스킬이 커밋을 막는 게 아니라, 막는 건 CommitGate의 게이트입니다.
|
|
195
|
+
|
|
196
|
+
#### 설치·보존·옵션
|
|
197
|
+
|
|
198
|
+
- **`--no-agent-entrypoints`**: `.claude/` 계층 전체를 건너뜁니다(companion 4종 포함).
|
|
199
|
+
- **기존 파일 보존(seed-once)**: 스킬은 **고치라고 만든 자산**입니다. 수정한 스킬은 **`--force`로도 덮어쓰지 않습니다.**
|
|
200
|
+
`AGENTS.md`·`CLAUDE.md`·`workflow/.gitignore`도 같은 정책입니다.
|
|
201
|
+
- **gitignore 경고**: `.claude/`를 gitignore하면 팀원의 fresh clone에 스킬이 전달되지 않습니다.
|
|
202
|
+
설치는 진행하되 **경고**하고 추적 방법을 안내합니다. **`--strict`면 설치 전에 중단**합니다.
|
|
203
|
+
- **타사 skill과 공존**: 타사 `tdd`·`grill-me` 등은 `.claude/skills/<이름>/`, companion은 `.claude/skills/commitgate-<이름>/` —
|
|
204
|
+
**경로가 달라 서로 건드리지 않습니다.**
|
|
205
|
+
|
|
206
|
+
#### 출처
|
|
207
|
+
|
|
208
|
+
Matt Pocock의 MIT 공개 skills를 기준 SHA `d574778f94cf620fcc8ce741584093bc650a61d3`에서 적응해
|
|
209
|
+
**패키지 payload로 포함**합니다. **외부 skill installer를 실행하거나 런타임 의존하지 않습니다** —
|
|
210
|
+
패키지 안에 고정된 사본입니다. 각 SKILL.md에 MIT 고지 전문이 동행하며, 자세한 출처는 패키지의
|
|
211
|
+
`skills/ATTRIBUTION.md`에 있습니다.
|
|
212
|
+
|
|
167
213
|
### 설치하지 **않는** 것
|
|
168
214
|
|
|
169
215
|
| 항목 | 어디에 있나 |
|
|
@@ -241,6 +287,27 @@ npx commitgate migrate --apply # package.json 의 req:* 만 전환
|
|
|
241
287
|
|
|
242
288
|
**재현성**: `req.config.json`의 리뷰 모델·추론강도 핀과 스키마·persona가 프로젝트에 남아 과거 리뷰 입력이 git 이력으로 재현됩니다. 런타임 버전은 **lockfile이 고정**하므로 `package-lock.json`(pnpm/yarn은 각 lockfile)을 **커밋하세요**.
|
|
243
289
|
|
|
290
|
+
### Companion Skills 발견 범위
|
|
291
|
+
|
|
292
|
+
**설치는 모든 환경에서 동일합니다.** 아래는 harness가 그 파일을 **발견하는지**에 대한 것입니다.
|
|
293
|
+
|
|
294
|
+
| harness | 발견 |
|
|
295
|
+
|---|---|
|
|
296
|
+
| **Claude Code** | `.claude/skills/<이름>/SKILL.md`를 native로 읽습니다 |
|
|
297
|
+
| **Cursor (editor)** | `.claude/skills`를 호환 경로로 읽습니다 |
|
|
298
|
+
| **Cursor (CLI)** | ⚠️ **버전·실행 모드별 동작 차이 가능 — 보장하지 않습니다** |
|
|
299
|
+
| **Codex** | **제품 범위 밖** — companion entrypoint를 설치하지 않습니다. CommitGate에서 Codex는 **Reviewer**이고 이 4종은 **Builder 보조**입니다 |
|
|
300
|
+
|
|
301
|
+
⚠️ **근거는 벤더 1차 문서입니다 — CommitGate 팀이 실측한 것이 아닙니다.** 확인 시점 **2026-07-17**, 확인 환경 win32 x64 / Node v20.19.5.
|
|
302
|
+
벤더가 동작을 바꾸면 이 표는 낡을 수 있습니다.
|
|
303
|
+
|
|
304
|
+
⚠️ **Cursor CLI를 "지원"·"미지원" 어느 쪽으로도 단정하지 않습니다.** Cursor는 editor·CLI 양쪽의 Agent Skills 지원을
|
|
305
|
+
발표했지만, `.claude/skills` 호환 경로의 CLI 발견은 버전·모드에 따라 차이가 보고돼 있고 우리는 검증하지 못했습니다.
|
|
306
|
+
발견되지 않아도 **핵심 워크플로에는 영향이 없습니다** — 스킬은 품질 보조 레이어이고 계약 정본은 `AGENTS.md`입니다.
|
|
307
|
+
|
|
308
|
+
우회를 위해 `.cursor/skills`에 **이중 설치하지 않습니다** — 그 경로도 CLI에서 동작이 불확실하고, 같은 내용이 두 곳에
|
|
309
|
+
깔리면 drift 위험이 생깁니다. 벤더가 고치면 **우리 변경 없이** 동작합니다(같은 경로를 읽으므로).
|
|
310
|
+
|
|
244
311
|
---
|
|
245
312
|
|
|
246
313
|
## 제거하려면
|
|
@@ -427,7 +494,7 @@ yarn req:next 2026-002 # yarn
|
|
|
427
494
|
| `ticketRoot` | `"workflow"` | REQ 티켓 폴더 |
|
|
428
495
|
| `packageManager` | 자동 감지 | `npm`, `pnpm`, `yarn` |
|
|
429
496
|
| `designDocs` | `00/01/02` 문서 | 설계 문서 파일명 |
|
|
430
|
-
| `reviewPersonaPath` | `"workflow/review-persona.md"` | 리뷰 프롬프트 첫 블록. `null`이면 비활성 |
|
|
497
|
+
| `reviewPersonaPath` | `"workflow/review-persona.md"` | 리뷰 프롬프트 첫 블록. `null`이면 비활성 — 단 delta design 리뷰에는 내장 delta 계약이 주입된다 |
|
|
431
498
|
| `reviewModel` | `"gpt-5.6-terra"` | codex 리뷰 모델(`-c model=`로 고정). `null`이면 codex 전역 설정을 상속 |
|
|
432
499
|
| `reviewReasoningEffort` | `"high"` | codex 리뷰 추론강도. `none`·`minimal`·`low`·`medium`·`high`·`xhigh` 중 하나. `null`이면 전역 상속 |
|
|
433
500
|
|
package/bin/init.ts
CHANGED
|
@@ -95,6 +95,23 @@ export const KIT_CLAUDE_DEST_REL = 'CLAUDE.md'
|
|
|
95
95
|
*/
|
|
96
96
|
export const KIT_GITIGNORE = { src: 'templates/workflow.gitignore', dest: 'workflow/.gitignore' } as const
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* companion skills (REQ-2026-020). Builder용 방법론 instruction asset — 실행 코드가 아니다.
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ `CONTRACT_POINTER_RELPATHS`에 **섞지 않는다**(설계 D6). 그 상수는 "계약 포인터"라는 의미를 갖고,
|
|
102
|
+
* companion skills는 **없어도 핵심 워크플로가 동일하게 동작**한다(R10). 의미가 다르므로 별도 목록이다.
|
|
103
|
+
* gitignore WARN/`--strict` 동작은 같은 축에서 받되, 기존 계약 포인터 경고를 약화시키지 않는다.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ `add()`를 타지 않는다(설계 D3). `add()`의 skip은 `existsSync && !force`라 **`--force`가 사용자 수정을 덮는다**.
|
|
106
|
+
* 스킬은 사용자가 고치라고 만든 자산이므로 `workflow/.gitignore`(D12)와 같은 seed-once 축으로 간다.
|
|
107
|
+
*/
|
|
108
|
+
export const KIT_COMPANION_SKILLS = [
|
|
109
|
+
{ src: 'skills/commitgate-discovery/SKILL.md', dest: '.claude/skills/commitgate-discovery/SKILL.md' },
|
|
110
|
+
{ src: 'skills/commitgate-tdd/SKILL.md', dest: '.claude/skills/commitgate-tdd/SKILL.md' },
|
|
111
|
+
{ src: 'skills/commitgate-diagnosing-bugs/SKILL.md', dest: '.claude/skills/commitgate-diagnosing-bugs/SKILL.md' },
|
|
112
|
+
{ src: 'skills/commitgate-research/SKILL.md', dest: '.claude/skills/commitgate-research/SKILL.md' },
|
|
113
|
+
] as const
|
|
114
|
+
|
|
98
115
|
/** `AGENTS.md`가 CommitGate 계약인지 판별하는 마커. 진입점 포인터들이 이 마커로 SSOT를 확인한다. */
|
|
99
116
|
export const AGENTS_CONTRACT_MARKER = '<!-- commitgate:contract -->'
|
|
100
117
|
|
|
@@ -398,6 +415,86 @@ function assertConfinedDest(targetRoot: string, destRel: string): void {
|
|
|
398
415
|
}
|
|
399
416
|
}
|
|
400
417
|
|
|
418
|
+
/**
|
|
419
|
+
* 쓰기 dest의 **confinement + leaf 상태를 한 번에** 판정한다 (REQ-2026-024 D1·D2).
|
|
420
|
+
*
|
|
421
|
+
* @returns 일반 파일이면 그 `Stats`, **실제 부재(ENOENT)면 `null`**. 그 밖은 전부 throw(fail-closed).
|
|
422
|
+
*
|
|
423
|
+
* 🔴 **`existsSync`를 부재 판정에 쓰면 안 된다.** 실측(REQ-2026-024 §2)으로 확인된 4가지 탈출:
|
|
424
|
+
* - **A. dangling leaf**: `existsSync`가 **false** → "부재" 오판 → 쓰기가 링크를 따라 대상 **밖에 생성**.
|
|
425
|
+
* - **B. ancestor dir symlink**: `existsSync`·`statSync`가 링크를 **따라가** 통과 → `mkdirSync`+쓰기가 밖에.
|
|
426
|
+
* - **C. live leaf + `--force`**: skip 조건이 `existsSync && !force`라 force면 `copyFileSync`가 **외부를 덮어씀**.
|
|
427
|
+
* - **D. live leaf + `writeFileSync`**: 기존 파일로 읽고 병합 → 쓰기가 링크 따라 **외부를 수정**.
|
|
428
|
+
* A만 막으면 C·D가 남는다 — 그쪽은 생성이 아니라 **기존 외부 파일 파괴**라 더 무겁다.
|
|
429
|
+
*
|
|
430
|
+
* 🔴 **반환값이 곧 부재 판정이다**(D2). 호출부가 `existsSync`를 쓸 이유를 없앤다 —
|
|
431
|
+
* 검사를 빼먹으려면 판정도 포기해야 하므로 **드리프트가 구조적으로 불가능**하다.
|
|
432
|
+
* "쓰기 전에 검사를 호출한다"는 규율은 이미 실패했다: `workflow/.gitignore`·companion에는 붙고
|
|
433
|
+
* 나머지 7종에는 안 붙은 것이 이번 결함이다.
|
|
434
|
+
*
|
|
435
|
+
* ⚠️ 루트 직속 dest(`AGENTS.md`·`package.json` 등)에서 `assertConfinedDest`는 **무동작**이다
|
|
436
|
+
* (`segs.length - 1 === 0`). 정상이다 — 그 상위는 `targetRoot` 자신이고 `runInit`이 이미 검사한다.
|
|
437
|
+
* 그 경우 leaf `lstat`이 방어를 맡는다.
|
|
438
|
+
*
|
|
439
|
+
* ⚠️ **TOCTOU는 막지 않는다.** 이 판정과 실제 쓰기 사이에 경로가 바뀌는 경쟁은 남는다
|
|
440
|
+
* (Node에 `O_NOFOLLOW` 원자 API가 없다). **협조적 사용자의 우발적 symlink**를 막는 것이다.
|
|
441
|
+
*/
|
|
442
|
+
function statWritableDest(targetRoot: string, destRel: string): ReturnType<typeof lstatSync> | null {
|
|
443
|
+
assertConfinedDest(targetRoot, destRel) // 상위 컴포넌트 전부 lstat (leaf는 아래에서)
|
|
444
|
+
const abs = join(targetRoot, destRel)
|
|
445
|
+
let st: ReturnType<typeof lstatSync>
|
|
446
|
+
try {
|
|
447
|
+
st = lstatSync(abs)
|
|
448
|
+
} catch (e) {
|
|
449
|
+
// ⚠️ **ENOENT만** 부재로 인정한다 — EACCES·ELOOP를 부재로 삼키면 apply에서 늦게 실패해 부분 설치가 된다(롤백 0줄).
|
|
450
|
+
if ((e as NodeJS.ErrnoException).code !== 'ENOENT')
|
|
451
|
+
throw new Error(`${destRel} 상태 확인 실패(${(e as Error).message}) — fail-closed.`)
|
|
452
|
+
return null
|
|
453
|
+
}
|
|
454
|
+
if (!st.isFile())
|
|
455
|
+
throw new Error(`${destRel} 가 일반 파일이 아닙니다(symlink·디렉터리·특수파일) — 그 경로를 옮기고 재시도하십시오.`)
|
|
456
|
+
return st
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** companion skills 계획 결과(순수 판정 — 쓰기 없음). */
|
|
460
|
+
export interface CompanionSkillsPlan {
|
|
461
|
+
/** 부재(lstat ENOENT)라 새로 만들 것. */
|
|
462
|
+
create: { srcAbs: string; destRel: string }[]
|
|
463
|
+
/** 이미 있고 **바이트 동일** = 직전 실행이 깐 것 → 설치 커밋 stage 목록에 편입. */
|
|
464
|
+
ownedSkips: string[]
|
|
465
|
+
/** 이미 있고 다름 = 사용자가 고친 것 → 보존(`--force`로도 안 덮음). */
|
|
466
|
+
userDiffers: string[]
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* companion skills seed-once 판정 + confinement preflight (설계 D3·D4).
|
|
471
|
+
* **순수 판정이라 아무것도 쓰지 않는다** — `--dry-run`도 이 검사를 그대로 받는다(쓰기 0건, 그러나 symlink면 실패).
|
|
472
|
+
*
|
|
473
|
+
* `workflow/.gitignore`(D12)와 같은 축이고, 다른 점은 **dest가 4개**라는 것뿐이다:
|
|
474
|
+
*
|
|
475
|
+
* 🔴 **각 최종 `SKILL.md` dest를 개별로 넘긴다.** skills 루트(`.claude/skills`)만 넘기면
|
|
476
|
+
* `assertConfinedDest`의 `i < segs.length - 1` 루프가 **마지막 컴포넌트를 검사하지 않아**
|
|
477
|
+
* `commitgate-<name>`이 외부 symlink여도 통과하고, `copyFileSync`가 대상 **밖에** 쓴다.
|
|
478
|
+
*
|
|
479
|
+
* confinement + leaf 판정은 `statWritableDest`가 한다 — 이 함수의 본문이 그 헬퍼의 출처다
|
|
480
|
+
* (REQ-2026-024 D1: 같은 규칙이 두 벌 있었고, 나머지 7종에 안 붙은 것이 결함이었다).
|
|
481
|
+
*/
|
|
482
|
+
export function planCompanionSkills(targetRoot: string): CompanionSkillsPlan {
|
|
483
|
+
const create: { srcAbs: string; destRel: string }[] = []
|
|
484
|
+
const ownedSkips: string[] = []
|
|
485
|
+
const userDiffers: string[] = []
|
|
486
|
+
for (const { src, dest } of KIT_COMPANION_SKILLS) {
|
|
487
|
+
// 상위 전부(.claude → .claude/skills → .claude/skills/commitgate-<name>) + leaf를 lstat으로 검사.
|
|
488
|
+
const st = statWritableDest(targetRoot, dest)
|
|
489
|
+
const destAbs = join(targetRoot, dest)
|
|
490
|
+
const srcAbs = join(PACKAGE_ROOT, src)
|
|
491
|
+
if (st === null) create.push({ srcAbs, destRel: dest })
|
|
492
|
+
else if (sha256File(destAbs) === sha256File(srcAbs)) ownedSkips.push(dest)
|
|
493
|
+
else userDiffers.push(dest) // 사용자가 고친 것 — `--force`도 보지 않는다(D3).
|
|
494
|
+
}
|
|
495
|
+
return { create, ownedSkips, userDiffers }
|
|
496
|
+
}
|
|
497
|
+
|
|
401
498
|
/**
|
|
402
499
|
* `<pm> install`이 만드는 `node_modules/`가 **워킹트리를 dirty하게 만드는가**.
|
|
403
500
|
* ignore되지도 tracked되지도 않으면 `?? node_modules/`로 나타나 `req:new --run`의 clean-tree 게이트를 막는다.
|
|
@@ -572,6 +669,11 @@ export interface PlanFacts {
|
|
|
572
669
|
workflowGitignoreWillCreate: boolean
|
|
573
670
|
/** 기존 `workflow/.gitignore`가 템플릿과 바이트 동일(= 직전 실행이 깐 것). ownedSkips에 직접 편입(D4 축). */
|
|
574
671
|
workflowGitignoreOwnedSkip: boolean
|
|
672
|
+
/**
|
|
673
|
+
* companion skills 계획(REQ-2026-020). `add()`를 타지 않으므로(D3 seed-once) 여기로 받아
|
|
674
|
+
* copies/ownedSkips에 **직접** 편입한다 — 그러지 않으면 stageList에서 빠져 unrelated로 오분류된다.
|
|
675
|
+
*/
|
|
676
|
+
companionSkills: CompanionSkillsPlan
|
|
575
677
|
}
|
|
576
678
|
|
|
577
679
|
/**
|
|
@@ -614,7 +716,11 @@ export function planInstall(targetRoot: string, force: boolean, pm: PackageManag
|
|
|
614
716
|
}
|
|
615
717
|
const add = (srcAbs: string, destRel: string): void => {
|
|
616
718
|
const destAbs = join(targetRoot, destRel)
|
|
617
|
-
|
|
719
|
+
// 🔴 confinement + leaf 판정(REQ-2026-024 D2). **반환값이 곧 부재 판정이다** — `existsSync`를 쓰면
|
|
720
|
+
// dangling leaf(A)를 부재로 오판하고, ancestor dir symlink(B)를 통과시키며, `--force`가 링크를 따라
|
|
721
|
+
// 대상 **밖** 사용자 파일을 덮어쓴다(C, 실측 E8). 검사와 판정이 같은 호출이라 빼먹을 수 없다.
|
|
722
|
+
const st = statWritableDest(targetRoot, destRel)
|
|
723
|
+
if (st !== null && !force) {
|
|
618
724
|
skips.push(destRel)
|
|
619
725
|
// 바이트가 같으면 CommitGate 소유(직전 실행이 깐 것). 다르면 사용자 파일 — 설치 커밋에 담지 않는다.
|
|
620
726
|
const a = sha(destAbs)
|
|
@@ -634,6 +740,11 @@ export function planInstall(targetRoot: string, force: boolean, pm: PackageManag
|
|
|
634
740
|
// workflow/.gitignore는 add()를 타지 않는다(D12: --force가 사용자 파일을 덮으면 안 됨). 바이트 동일 재실행분은
|
|
635
741
|
// 여기서 ownedSkips에 **직접** 편입 — 그러지 않으면 stageList에서 빠져 unrelated로 오분류된다(phase-2 리뷰 R1).
|
|
636
742
|
if (facts.workflowGitignoreOwnedSkip) ownedSkips.push(KIT_GITIGNORE.dest)
|
|
743
|
+
// companion skills도 add()를 타지 않는다(D3). 생성분은 copies로, 바이트 동일분은 ownedSkips로 직접 편입 —
|
|
744
|
+
// 그래야 artifacts·stageList·ignore 검사가 이들을 함께 본다. userDiffers는 사용자 파일이라 설치 커밋에 담지 않는다.
|
|
745
|
+
for (const c of facts.companionSkills.create) copies.push(c)
|
|
746
|
+
for (const d of facts.companionSkills.ownedSkips) ownedSkips.push(d)
|
|
747
|
+
for (const d of facts.companionSkills.userDiffers) skips.push(d)
|
|
637
748
|
|
|
638
749
|
return {
|
|
639
750
|
copies,
|
|
@@ -653,10 +764,20 @@ export function planInstall(targetRoot: string, force: boolean, pm: PackageManag
|
|
|
653
764
|
}
|
|
654
765
|
|
|
655
766
|
/**
|
|
656
|
-
* 진입점 dest
|
|
767
|
+
* 진입점 dest의 **ENOTDIR 조기 진단**(D8) — 더 나은 에러 메시지가 목적이다.
|
|
657
768
|
*
|
|
658
769
|
* `mkdirSync(recursive)`는 경로 중간 컴포넌트가 **파일**이면 ENOTDIR로 죽는다. apply 단계에서 그러면
|
|
659
|
-
* 앞의 파일들은 이미 복사된 뒤라 **부분 설치**가 된다. 쓰기 전에 걸러서 preflight→apply 계약을
|
|
770
|
+
* 앞의 파일들은 이미 복사된 뒤라 **부분 설치**가 된다. 쓰기 전에 걸러서 preflight→apply 계약을 지키고,
|
|
771
|
+
* `--no-agent-entrypoints`라는 출구를 안내한다.
|
|
772
|
+
*
|
|
773
|
+
* 🔴 **이 함수는 confinement 방어가 아니다**(REQ-2026-024 D4). `existsSync`·`statSync`는 **링크를 따라간다** —
|
|
774
|
+
* 상위가 외부를 가리키는 symlink여도 `isDirectory()`가 true라 그대로 통과한다(실측: v0.7.0에서 `.cursor`
|
|
775
|
+
* junction이 여기를 통과해 대상 밖에 파일을 만들었다).
|
|
776
|
+
* **방어는 `statWritableDest`가 한다** — 이 함수 뒤의 `add()`·개별 dest 판정이 그것을 호출한다.
|
|
777
|
+
* 여기를 lstat 기반으로 바꾸지 **않는다**: 같은 규칙이 두 벌이 되고, 그 이중화가 이번 결함의 발생 방식이다.
|
|
778
|
+
*
|
|
779
|
+
* ⚠️ 순서 의존: symlink dest에 대해 이 함수가 먼저 **다른 메시지**로 throw할 수 있다(예: 상위가 파일 symlink면
|
|
780
|
+
* "디렉터리가 아니라 파일입니다"). 보안상 무해하다 — 둘 다 쓰기 전 throw이고 메시지도 틀리지 않았다.
|
|
660
781
|
*/
|
|
661
782
|
function assertEntrypointPathsUsable(targetRoot: string): void {
|
|
662
783
|
const dests = [...KIT_AGENT_ENTRYPOINTS.map((e) => e.dest), KIT_CLAUDE_DEST_REL, KIT_AGENTS_CONTRACT_COPY_REL]
|
|
@@ -677,8 +798,23 @@ function assertEntrypointPathsUsable(targetRoot: string): void {
|
|
|
677
798
|
}
|
|
678
799
|
}
|
|
679
800
|
|
|
680
|
-
/**
|
|
681
|
-
|
|
801
|
+
/**
|
|
802
|
+
* 계획대로 복사(중첩 디렉터리 생성). 호출부가 `--dry-run`이면 이 함수를 아예 부르지 않는다.
|
|
803
|
+
*
|
|
804
|
+
* 🔴 **쓰기 전 전량 검증**(REQ-2026-024 D3). `plan.copies`는 `add()`만 채우지 않는다 —
|
|
805
|
+
* `planCompanionSkills`의 결과가 **직접 편입**된다(REQ-2026-020 D3). 즉 `add()`의 preflight를
|
|
806
|
+
* **우회하는 경로가 이미 존재한다.** 여기서 불변식을 강제해 미래의 우회도 잡는다.
|
|
807
|
+
*
|
|
808
|
+
* 🔴 **두 루프여야 한다.** 검사·쓰기를 한 루프에 섞으면 중간 throw 시 앞의 파일이 이미 복사돼
|
|
809
|
+
* **부분 설치**가 된다(롤백 0줄) — preflight→apply 계약 위반이다.
|
|
810
|
+
*
|
|
811
|
+
* ⚠️ 이 검사는 **불변식 강제**지 주 방어선이 아니다. 주 방어선은 preflight(`add()`·`planCompanionSkills`)다.
|
|
812
|
+
* preflight가 완전하면 여기는 **절대 발화하지 않는다** — 그래서 `runInit` 경유로는 검증할 수 없고
|
|
813
|
+
* (preflight가 먼저 터진다) 이 함수를 **직접 호출**하는 테스트만이 공허하지 않다. export 이유가 그것이다.
|
|
814
|
+
* TOCTOU도 막지 못한다 — 검사와 `copyFileSync` 사이에도 창이 있다.
|
|
815
|
+
*/
|
|
816
|
+
export function applyCopies(targetRoot: string, plan: InstallPlan): void {
|
|
817
|
+
for (const { destRel } of plan.copies) statWritableDest(targetRoot, destRel)
|
|
682
818
|
for (const { srcAbs, destRel } of plan.copies) {
|
|
683
819
|
const destAbs = join(targetRoot, destRel)
|
|
684
820
|
mkdirSync(dirname(destAbs), { recursive: true })
|
|
@@ -815,7 +951,10 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
815
951
|
assertGitWorkTree(targetRoot) // 실제 git probe(fake .git 마커 거부)
|
|
816
952
|
|
|
817
953
|
const pkgPath = join(targetRoot, 'package.json')
|
|
818
|
-
|
|
954
|
+
// 🔴 confinement를 **읽기보다 앞**에 둔다(REQ-2026-024 D5). symlink면 `writeFileSync`가 링크를 따라
|
|
955
|
+
// 대상 **밖** package.json을 수정한다(실측 E6). 검사를 읽기 뒤에 두면 외부 파일을 읽고 나서 막는 셈이다.
|
|
956
|
+
// 부재(null)는 기존 메시지 그대로 — 동작 변경은 symlink/특수파일 케이스에 한정된다.
|
|
957
|
+
if (statWritableDest(targetRoot, 'package.json') === null)
|
|
819
958
|
throw new Error(`package.json 없음: ${targetRoot} — 'npm init' 등으로 먼저 생성(req:* 스크립트 주입 대상).`)
|
|
820
959
|
const pkg = parseJsonObject(pkgPath, 'package.json') as {
|
|
821
960
|
scripts?: Record<string, string>
|
|
@@ -860,7 +999,9 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
860
999
|
}
|
|
861
1000
|
|
|
862
1001
|
const cfgPath = join(targetRoot, 'req.config.json')
|
|
863
|
-
|
|
1002
|
+
// 🔴 confinement를 **읽기보다 앞**에(D5). dangling이면 `writeFileSync`가 대상 밖에 **생성**하고(E4),
|
|
1003
|
+
// live symlink면 외부 파일을 읽어 병합한 뒤 링크를 따라 **수정**한다(E5). 아래 loadConfig도 이 파일을 읽는다.
|
|
1004
|
+
const existingCfg = statWritableDest(targetRoot, 'req.config.json') !== null ? parseJsonObject(cfgPath, 'req.config.json') : null
|
|
864
1005
|
// 기존 config는 워크플로 CONFIG_SCHEMA(additionalProperties·enum·type) + 경로 confinement까지 preflight 검증(phase R2 P2).
|
|
865
1006
|
// kit의 loadConfig를 재사용 — schema-invalid(unknown key·bad enum·escaping ticketRoot 등)면 복사 전 throw(첫 req:* 지연 실패 방지).
|
|
866
1007
|
// 병합은 유효 키만 추가(handoffPath:null·packageManager)라 "기존 유효 ⇒ 병합 유효".
|
|
@@ -909,7 +1050,9 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
909
1050
|
// `REQ_DEV_DEPS` 상수는 남는다 — `bin/uninstall.ts`가 기존 Stage A 설치본의 devDeps를 분류하는 데 쓴다.
|
|
910
1051
|
|
|
911
1052
|
const agentsPath = join(targetRoot, 'AGENTS.md')
|
|
912
|
-
|
|
1053
|
+
// 🔴 반환값이 곧 부재 판정이다(D2). `!existsSync`면 dangling을 부재로 오판해 `copyFileSync`가
|
|
1054
|
+
// 링크를 따라 대상 밖에 AGENTS.md를 만든다(실측 E1). 아래 마커 읽기(readFileSync)도 이 검사 뒤에 온다.
|
|
1055
|
+
const agentsCreated = statWritableDest(targetRoot, 'AGENTS.md') === null
|
|
913
1056
|
|
|
914
1057
|
const agentEntrypointsSkipped = opts.noAgentEntrypoints === true
|
|
915
1058
|
if (!agentEntrypointsSkipped) assertEntrypointPathsUsable(targetRoot)
|
|
@@ -920,33 +1063,25 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
920
1063
|
!agentEntrypointsSkipped && !agentsCreated && !readFileSync(agentsPath, 'utf8').includes(AGENTS_CONTRACT_MARKER)
|
|
921
1064
|
|
|
922
1065
|
const claudeMdPath = join(targetRoot, KIT_CLAUDE_DEST_REL)
|
|
923
|
-
|
|
1066
|
+
// `--no-agent-entrypoints`면 이 dest를 쓰지 않으므로 검사도 돌리지 않는다(D5/D7 의미 유지 — 단락 평가).
|
|
1067
|
+
const claudeMdCreated = !agentEntrypointsSkipped && statWritableDest(targetRoot, KIT_CLAUDE_DEST_REL) === null
|
|
924
1068
|
|
|
925
1069
|
// 마커가 없으면 포인터가 참조할 계약 템플릿을 **대상 repo에** 놓는다 — 그러지 않으면 복구 지시가 막다른 길이다.
|
|
926
1070
|
const contractCopyPath = join(targetRoot, KIT_AGENTS_CONTRACT_COPY_REL)
|
|
927
|
-
|
|
1071
|
+
// `agentsMarkerMissing`가 거짓이면 이 dest를 쓰지 않으므로 검사도 돌리지 않는다(단락 평가).
|
|
1072
|
+
// ⚠️ `|| opts.force`가 남아 있다 — force면 **존재해도 덮어쓴다**. 그래서 symlink 거부가 특히 중요하다:
|
|
1073
|
+
// 검사가 없으면 force가 링크를 따라 대상 밖 파일을 덮어쓴다(모드 C, `add()`의 E8과 같은 축).
|
|
1074
|
+
const agentsContractCopyCreated =
|
|
1075
|
+
agentsMarkerMissing && (statWritableDest(targetRoot, KIT_AGENTS_CONTRACT_COPY_REL) === null || opts.force)
|
|
928
1076
|
|
|
929
1077
|
// workflow/.gitignore(kit 파일, REQ-2026-012). AGENTS.md 정책: **부재 시에만** 생성, --force로도 안 덮음(D12).
|
|
930
1078
|
// --no-agent-entrypoints와 무관(D13).
|
|
931
1079
|
const workflowGitignorePath = join(targetRoot, KIT_GITIGNORE.dest)
|
|
932
1080
|
const workflowGitignoreSrcAbs = join(PACKAGE_ROOT, KIT_GITIGNORE.src)
|
|
933
|
-
// confinement: workflow/(또는 그 상위)가 symlink면 copyFileSync가 밖에 쓰거나 git이 경로를 stage하지
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
// fail-closed: 존재하면 반드시 **일반 파일**이어야 한다(P1). lstat로 symlink를 안 따라간다 —
|
|
938
|
-
// symlink면 copyFileSync가 링크 대상을 따라 쓰고 git도 링크된 .gitignore를 규칙으로 안 읽는다. 디렉터리·특수파일도 복사 불가.
|
|
939
|
-
// ⚠️ **ENOENT만** 부재로 인정한다 — EACCES·ELOOP 등을 부재로 삼키면 apply로 넘어가 늦게 실패한다.
|
|
940
|
-
let wgLstat: ReturnType<typeof lstatSync> | null = null
|
|
941
|
-
try {
|
|
942
|
-
wgLstat = lstatSync(workflowGitignorePath)
|
|
943
|
-
} catch (e) {
|
|
944
|
-
if ((e as NodeJS.ErrnoException).code !== 'ENOENT')
|
|
945
|
-
throw new Error(`${KIT_GITIGNORE.dest} 상태 확인 실패(${(e as Error).message}) — fail-closed.`)
|
|
946
|
-
wgLstat = null
|
|
947
|
-
}
|
|
948
|
-
if (wgLstat !== null && !wgLstat.isFile())
|
|
949
|
-
throw new Error(`${KIT_GITIGNORE.dest} 가 일반 파일이 아닙니다(symlink·디렉터리·특수파일) — 그 경로를 옮기고 재시도하십시오.`)
|
|
1081
|
+
// confinement + leaf: workflow/(또는 그 상위)가 symlink면 copyFileSync가 밖에 쓰거나 git이 경로를 stage하지 못하고,
|
|
1082
|
+
// leaf가 symlink면 링크 대상을 따라 쓰며 git도 링크된 .gitignore를 규칙으로 안 읽는다. 디렉터리·특수파일도 복사 불가.
|
|
1083
|
+
// preflight라 스키마 복사(같은 workflow/ 경유)보다 먼저 돌아 함께 보호된다.
|
|
1084
|
+
const wgLstat = statWritableDest(targetRoot, KIT_GITIGNORE.dest)
|
|
950
1085
|
const workflowGitignoreExists = wgLstat !== null // isFile 보장
|
|
951
1086
|
const workflowGitignoreCreated = !workflowGitignoreExists
|
|
952
1087
|
// 존재 & 바이트 동일 = 직전 실행이 깐 것(소유). ⚠️ add()를 타지 않으므로(D12) ownedSkips를 **직접** 채운다 —
|
|
@@ -963,6 +1098,13 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
963
1098
|
gitIsIgnored(targetRoot, KIT_GITIGNORE.dest) &&
|
|
964
1099
|
!gitIsTracked(targetRoot, KIT_GITIGNORE.dest)
|
|
965
1100
|
|
|
1101
|
+
// companion skills(REQ-2026-020 D3·D4). **preflight**라 `--dry-run`도 이 검사를 받는다 —
|
|
1102
|
+
// dry-run이 조용히 통과하면 실설치 직전에야 터진다. 판정은 순수(쓰기 0건)다.
|
|
1103
|
+
// `.claude/` 계층이므로 `--no-agent-entrypoints`면 통째로 건너뛴다(D5/D7 의미 유지).
|
|
1104
|
+
const companionSkills: CompanionSkillsPlan = agentEntrypointsSkipped
|
|
1105
|
+
? { create: [], ownedSkips: [], userDiffers: [] }
|
|
1106
|
+
: planCompanionSkills(targetRoot)
|
|
1107
|
+
|
|
966
1108
|
// 설치 **전** 워킹트리(쓰기 전 스냅샷). `.gitignore`의 dirty 여부 판정에도 쓰이므로 계획보다 먼저 찍는다.
|
|
967
1109
|
const porcelain = gitStatusEntries(targetRoot)
|
|
968
1110
|
const nmWillDirty = nodeModulesWillDirty(targetRoot)
|
|
@@ -978,6 +1120,7 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
978
1120
|
gitignoreWillJoin: gitignoreJoinsInstall(nmWillDirty, porcelain),
|
|
979
1121
|
workflowGitignoreWillCreate: workflowGitignoreCreated,
|
|
980
1122
|
workflowGitignoreOwnedSkip,
|
|
1123
|
+
companionSkills,
|
|
981
1124
|
})
|
|
982
1125
|
const artifacts = planArtifactPaths(plan)
|
|
983
1126
|
|
|
@@ -1018,6 +1161,32 @@ export function runInit(opts: InitOptions): InitResult {
|
|
|
1018
1161
|
if (opts.strict) throw new Error(`[--strict] ${msg}`)
|
|
1019
1162
|
console.warn(`⚠️ ${msg}\n (설치는 계속 — 안전한 커밋 안내는 생략됩니다)`)
|
|
1020
1163
|
}
|
|
1164
|
+
// companion skills가 ignored∧untracked면 설치 커밋에 못 담겨 팀원 fresh clone에 전달되지 않는다(REQ-2026-021 D1).
|
|
1165
|
+
//
|
|
1166
|
+
// 🔴 **`artifacts`로 판정하면 안 된다.** `userDiffers`는 `skips`로 가서 `planArtifactPaths`
|
|
1167
|
+
// (= `copies + ownedSkips + extras`)에 **없다** → `findIgnoredArtifacts`가 그 파일을 보지 못한다.
|
|
1168
|
+
// 그러면 나머지가 전부 추적된 상태에서 사용자가 skill 하나만 고쳤을 때 **경고 없이 `--strict`가 통과**한다.
|
|
1169
|
+
// 계획 3분류(create·ownedSkips·userDiffers)를 **전부** 본다 — 소유든 사용자 파일이든, 팀에 전달되지
|
|
1170
|
+
// 못하면 안전한 설치 안내를 낼 수 없다. `workflowGitignorePolicyAtRisk`와 같은 축이다(D1).
|
|
1171
|
+
//
|
|
1172
|
+
// ⚠️ `CONTRACT_POINTER_RELPATHS`에 섞지 않는다(REQ-2026-020 D6) — companion은 계약 포인터가 아니다.
|
|
1173
|
+
// 같은 WARN/strict **동작**만 별도 판정으로 준다. 기존 포인터 경고는 손대지 않는다(추가만).
|
|
1174
|
+
const companionAtRisk = [
|
|
1175
|
+
...companionSkills.create.map((c) => c.destRel),
|
|
1176
|
+
...companionSkills.ownedSkips,
|
|
1177
|
+
...companionSkills.userDiffers,
|
|
1178
|
+
].filter((p) => gitIsIgnored(targetRoot, p) && !gitIsTracked(targetRoot, p))
|
|
1179
|
+
|
|
1180
|
+
if (companionAtRisk.length > 0) {
|
|
1181
|
+
const msg =
|
|
1182
|
+
`companion skills가 무시되고 있어 설치 커밋에 담을 수 없습니다:\n` +
|
|
1183
|
+
companionAtRisk.map((p) => ` ${p}`).join('\n') +
|
|
1184
|
+
`\n 팀원의 fresh clone·CI에는 이 스킬들이 없습니다(그쪽 Builder는 방법론 보조를 못 받습니다).\n` +
|
|
1185
|
+
` 추적하려면: git add -f <위 경로> 또는 .gitignore 에서 해당 규칙을 걷어내십시오.`
|
|
1186
|
+
if (opts.strict) throw new Error(`[--strict] ${msg}`)
|
|
1187
|
+
console.warn(`⚠️ ${msg}\n (설치는 계속 — 강제 중단하려면 --strict)`)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1021
1190
|
// staged·overlapping이 있으면 **안전한 커밋 안내를 만들 수 없다**(커밋이 인덱스 전체를 담고, 겹침은 사후 분리 불가).
|
|
1022
1191
|
// 기본 모드는 설치를 막지 않는다(비파괴·비-breaking) — 안내를 내지 않을 뿐. `--strict`는 쓰기 전에 중단한다.
|
|
1023
1192
|
if (opts.strict && (preexistingDirty.staged.length > 0 || preexistingDirty.overlapping.length > 0)) {
|
|
@@ -1267,7 +1436,9 @@ function printHelp(): void {
|
|
|
1267
1436
|
|
|
1268
1437
|
옵션:
|
|
1269
1438
|
--dir <path> 대상 repo 루트(기본: 현재 디렉터리)
|
|
1270
|
-
--force
|
|
1439
|
+
--force 덮어쓰기 가능한 kit 항목만 갱신(기본: 스킵).
|
|
1440
|
+
AGENTS.md · CLAUDE.md · workflow/.gitignore · companion skills(.claude/skills/commitgate-*)는
|
|
1441
|
+
기존 파일을 **보존**합니다 — --force 로도 덮어쓰지 않습니다.
|
|
1271
1442
|
--dry-run 변경 없이 수행 예정 목록만 출력
|
|
1272
1443
|
--strict 정합성 경고를 설치 실패로 취급(fail-closed)
|
|
1273
1444
|
--no-agent-entrypoints
|
package/bin/uninstall.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
KIT_COPY_RELPATHS,
|
|
31
31
|
KIT_AGENT_ENTRYPOINTS,
|
|
32
32
|
KIT_GITIGNORE,
|
|
33
|
+
KIT_COMPANION_SKILLS,
|
|
33
34
|
KIT_CLAUDE_TEMPLATE_REL,
|
|
34
35
|
KIT_CLAUDE_DEST_REL,
|
|
35
36
|
KIT_AGENTS_CONTRACT_COPY_REL,
|
|
@@ -230,6 +231,10 @@ export function collectFacts(opts: UninstallOptions, run?: GitRunner): Uninstall
|
|
|
230
231
|
{ destRel: KIT_AGENTS_CONTRACT_COPY_REL, srcRel: 'AGENTS.template.md' },
|
|
231
232
|
// workflow/.gitignore(REQ-2026-012): src≠dest. identical이면 제거 대상, differs면 review(사용자 편집 보존). 부재 정상.
|
|
232
233
|
{ destRel: KIT_GITIGNORE.dest, srcRel: KIT_GITIGNORE.src },
|
|
234
|
+
// companion skills(REQ-2026-021 D5): src≠dest. `KIT_COMPANION_SKILLS`가 이미 `{src, dest}` shape라 직접 매핑한다.
|
|
235
|
+
// `--no-agent-entrypoints`·미설치면 부재가 **정상**이므로 present=false여도 문제 없다(entrypoint와 같은 성질).
|
|
236
|
+
// ⚠️ 분류는 기존 규칙을 그대로 탄다 — 새 예외를 만들지 않는다. `differs`는 "편집됐거나 **다른 버전**이 설치함"이다.
|
|
237
|
+
...KIT_COMPANION_SKILLS.map((e) => ({ destRel: e.dest, srcRel: e.src })),
|
|
233
238
|
]
|
|
234
239
|
const tool: ToolArtifact[] = toolEntries.map(({ destRel, srcRel }) => {
|
|
235
240
|
const dest = join(targetRoot, destRel)
|