geobuke-code 0.2.0 → 0.2.2
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 +31 -16
- package/dist/cli.js +25 -14
- package/dist/hook.js +65 -24
- package/dist/install.js +43 -26
- package/dist/judge.js +28 -3
- package/dist/spec.js +5 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ npm install -g geobuke-code
|
|
|
27
27
|
|
|
28
28
|
# 2) 대상 프로젝트에 게이트 설치
|
|
29
29
|
cd <your-project>
|
|
30
|
-
gbc init # .claude/settings.json에 hook + /gate skill 머지 (동의·백업)
|
|
30
|
+
gbc init # .claude/settings.json에 hook(PreToolUse+Stop+SessionStart) + /gate skill 머지 (동의·백업)
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
<details>
|
|
@@ -53,27 +53,30 @@ gbc init
|
|
|
53
53
|
|
|
54
54
|
</details>
|
|
55
55
|
|
|
56
|
-
`gbc init`은 **프로젝트 로컬 `.claude/settings.json`만** 머지한다(append·멱등·백업). 전역 `~/.claude`는 건드리지 않는다.
|
|
56
|
+
`gbc init`은 **프로젝트 로컬 `.claude/settings.json`만** 머지한다(append·멱등·백업). 전역 `~/.claude`는 건드리지 않는다. hook 명령은 **셸 무관 순수 형태**(`node "<path>" hook pre-tool-use`)라 Windows(cmd.exe)/bash/zsh/Mac에서 동일하게 동작한다.
|
|
57
57
|
|
|
58
58
|
## 빠른 게이트 활성화 (API 키 — 선택)
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
키가 없으면 `claude -p` 폴백(~13–20s)으로 무설정 동작한다. **haiku 직접 API(~1–3s)**를 쓰려면 **키 파일만 만들면 된다** — gbc가 실행 시 직접 읽으므로 settings.json 수정이나 셸 주입은 불필요하다(0.2.1+).
|
|
61
61
|
|
|
62
|
-
> ⚠️
|
|
62
|
+
> ⚠️ **native Windows에선 API 키가 사실상 필수다.** `claude -p` 폴백은 `claude.cmd`(배치 shim)를 셸 없이 실행하지 못해(ENOENT) fail-open으로 빠질 수 있다. Windows에선 위 키 파일을 만들어 API 경로로 쓰는 것을 권장한다(WSL/Mac/Linux는 폴백 정상).
|
|
63
|
+
|
|
64
|
+
키 해석 순서: `ANTHROPIC_API_KEY` 환경변수 > `~/.gbc/api-key` 파일.
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
|
-
#
|
|
67
|
+
# bash / zsh / WSL / Mac
|
|
66
68
|
mkdir -p ~/.gbc && printf '%s' 'sk-ant-...' > ~/.gbc/api-key && chmod 600 ~/.gbc/api-key
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"command": "ANTHROPIC_API_KEY=\"$(cat ~/.gbc/api-key)\" node \"…/dist/cli.js\" hook pre-tool-use"
|
|
71
|
+
```powershell
|
|
72
|
+
# native Windows (PowerShell) — -NoNewline 필수(끝에 개행 붙으면 키 오염)
|
|
73
|
+
New-Item -ItemType Directory -Force -Path "$HOME\.gbc" | Out-Null
|
|
74
|
+
Set-Content -Path "$HOME\.gbc\api-key" -Value "sk-ant-..." -NoNewline
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
`
|
|
77
|
+
> ⚠️ **`export ANTHROPIC_API_KEY=…` 전역 설정(또는 settings.json top-level `env`) 금지.** Claude Code 본체가 그 키로 **과금 전환**된다(구독 대신 키 과금). 키 파일 방식은 gbc 판정 호출에만 키가 쓰이므로 이 함정을 구조적으로 피한다.
|
|
78
|
+
|
|
79
|
+
`gbc status`는 키 파일/환경변수를 반영해 `트랜스포트: api`로 표시한다(0.2.1+).
|
|
77
80
|
|
|
78
81
|
## 동작 원리
|
|
79
82
|
|
|
@@ -81,11 +84,23 @@ mkdir -p ~/.gbc && printf '%s' 'sk-ant-...' > ~/.gbc/api-key && chmod 600 ~/.gbc
|
|
|
81
84
|
phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이스확정】 → 구현(Claude Code) → 검증
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
게이트는 계획 명세를
|
|
85
|
-
`$GBC_SPEC_FILE` > `.gbc/spec.md` > `scratch.md`
|
|
87
|
+
게이트는 계획 명세를 `.gbc/spec.md`(단일 정본)에서 읽는다. 다른 파일을 명세로 쓰려면 `$GBC_SPEC_FILE` 환경변수로 그 경로를 명시 지정한다(우선순위 `$GBC_SPEC_FILE` > `.gbc/spec.md`). gbc가 소유하지 않은 파일을 자동 폴백하지 않으므로, 진행추적 파일 등이 명세로 오인되지 않는다.
|
|
86
88
|
|
|
87
89
|
코드 변경 직전 PreToolUse hook이 명세 ↔ 변경 ↔ 미룬 항목을 대조해 통과/차단을 판정한다.
|
|
88
90
|
|
|
91
|
+
### 동작 시점
|
|
92
|
+
|
|
93
|
+
`gbc init`이 프로젝트 `.claude/settings.json`에 아래 hook을 멱등 등록한다. gbc는 `.gbc/`만 읽으므로(다른 하네스의 메모리·진행추적 파일 미접근) 어떤 환경에서든 동일하게 동작한다.
|
|
94
|
+
|
|
95
|
+
| 시점 | hook (matcher) | 동작 |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| **세션 시작·재개** | SessionStart (`startup\|resume`) | `.gbc/defers.json`의 미해결 항목을 표면화(이전 작업 잔여 환기). 잔여 없으면 무출력. `compact`엔 발화 안 함(노이즈 방지) |
|
|
98
|
+
| **코드 변경 직전** | PreToolUse (`Edit\|Write\|MultiEdit`) | 명세 ↔ 변경 ↔ defer 대조 → 통과(침묵)/차단(시나리오 도출 지시)/fail-open |
|
|
99
|
+
| **작업단위당 1회** | (PreToolUse 캐시) | 같은 명세 해시 내에선 첫 편집만 판정, 이후 통과 → 매 편집 지연 회피 |
|
|
100
|
+
| **응답 종료** | Stop | 계측 flush(`events.jsonl`) |
|
|
101
|
+
|
|
102
|
+
> 세션 진입 알림만 끄려면 `GBC_NO_SESSION_HINT=1`. 기존 설치(0.2.1 이하 init)는 `gbc init --yes` 재실행으로 SessionStart hook이 추가된다.
|
|
103
|
+
|
|
89
104
|
### 시나리오 도출 루프 (수기 입력 불필요)
|
|
90
105
|
|
|
91
106
|
명세가 비어 **시나리오 미지정**으로 차단되면, 사용자가 파일을 직접 쓰지 않는다. 차단 메시지가 코딩 에이전트에게 다음을 지시한다:
|
|
@@ -103,12 +118,12 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
|
|
|
103
118
|
|
|
104
119
|
| 조건 | 트랜스포트 | 지연 |
|
|
105
120
|
|---|---|---|
|
|
106
|
-
| `ANTHROPIC_API_KEY`
|
|
107
|
-
|
|
|
121
|
+
| 키 있음 (`ANTHROPIC_API_KEY` env 또는 `~/.gbc/api-key` 파일) | Anthropic API 직접 (haiku, 최소 시스템프롬프트) | ~1–3s (목표) |
|
|
122
|
+
| 키 없음 | `claude -p` 폴백 (CC 인증 재사용, 무설정 / ⚠️native Windows 미지원) | ~13–20s |
|
|
108
123
|
|
|
109
124
|
**작업단위 1회**: 게이트는 작업단위(계획 명세 해시)당 한 번만 발동한다. 명세가 바뀌거나 명세 밖 파일을 편집할 때만 재발동 → 매 편집 지연을 피한다.
|
|
110
125
|
|
|
111
|
-
> 빠른 게이트를 원하면 `
|
|
126
|
+
> 빠른 게이트를 원하면 `~/.gbc/api-key` 키 파일을 만들어라(설정법·과금 주의: 위 [「빠른 게이트 활성화」](#빠른-게이트-활성화-api-키--선택)). 없으면 `claude -p` 폴백으로 무설정 동작하되 작업단위당 한 번 느리다.
|
|
112
127
|
|
|
113
128
|
## 명령
|
|
114
129
|
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,12 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync, } from "node:fs";
|
|
7
|
-
import { runPreToolUse, runStop } from "./hook.js";
|
|
7
|
+
import { runPreToolUse, runStop, runSessionStart } from "./hook.js";
|
|
8
8
|
import { loadPlanSpec, computeSpecHash, addSpecCase, readSpecCases, clearSpec } from "./spec.js";
|
|
9
9
|
import { loadState, resetGate } from "./state.js";
|
|
10
10
|
import { addDefer, loadDefers, resolveDefer } from "./defer.js";
|
|
11
11
|
import { selectedTransport } from "./judge.js";
|
|
12
|
-
import { buildPreCommand,
|
|
12
|
+
import { buildPreCommand, normalizeHooks, ensureSessionStartHook } from "./install.js";
|
|
13
13
|
import { logEvent, parseEvents, computeMetrics } from "./metrics.js";
|
|
14
14
|
const CLI_PATH = fileURLToPath(import.meta.url);
|
|
15
15
|
const PKG_ROOT = join(dirname(CLI_PATH), ".."); // dist/cli.js → 패키지 루트
|
|
@@ -60,11 +60,13 @@ function cmdInit(args) {
|
|
|
60
60
|
console.log(`🐢 gbc init — 다음을 수행합니다 (프로젝트 로컬만, 전역 ~/.claude 미변경):
|
|
61
61
|
|
|
62
62
|
대상 프로젝트: ${cwd}
|
|
63
|
-
1) ${settingsPath} 에 PreToolUse(Edit|Write) + Stop hook 추가 (머지·멱등)
|
|
63
|
+
1) ${settingsPath} 에 PreToolUse(Edit|Write) + Stop + SessionStart hook 추가 (머지·멱등)
|
|
64
64
|
- 기존 settings.json 있으면 백업: settings.json.bak-<시각>
|
|
65
65
|
2) ${join(skillDestDir, "SKILL.md")} 에 /gate 스킬 설치
|
|
66
|
-
3) hook 명령: ${buildPreCommand(CLI_PATH
|
|
67
|
-
|
|
66
|
+
3) hook 명령: ${buildPreCommand(CLI_PATH)}
|
|
67
|
+
${hasApiKey()
|
|
68
|
+
? " (~/.gbc/api-key 감지됨 → 빠른 haiku API 경로로 동작)"
|
|
69
|
+
: " (~/.gbc/api-key 없음 → claude -p 폴백. 빠른 경로 원하면 키 파일 생성)"}
|
|
68
70
|
실행하려면: gbc init --yes
|
|
69
71
|
`);
|
|
70
72
|
return;
|
|
@@ -86,21 +88,20 @@ function cmdInit(args) {
|
|
|
86
88
|
}
|
|
87
89
|
const hooks = (settings.hooks ??= {});
|
|
88
90
|
const serialized = JSON.stringify(settings);
|
|
89
|
-
|
|
90
|
-
// PreToolUse (멱등). 신규면 추가, 이미 있으면 keyless→키주입 업그레이드(skip만 하지 않음).
|
|
91
|
+
// PreToolUse (멱등). 신규면 추가, 이미 있으면 옛 명령(keyless·bash 키주입)을 pure로 정규화.
|
|
91
92
|
if (!serialized.includes("hook pre-tool-use")) {
|
|
92
93
|
(hooks.PreToolUse ??= []).push({
|
|
93
94
|
matcher: "Edit|Write|MultiEdit",
|
|
94
|
-
hooks: [{ type: "command", command: buildPreCommand(CLI_PATH
|
|
95
|
+
hooks: [{ type: "command", command: buildPreCommand(CLI_PATH) }],
|
|
95
96
|
});
|
|
96
|
-
console.log(` + PreToolUse hook
|
|
97
|
+
console.log(` + PreToolUse hook 추가`);
|
|
97
98
|
}
|
|
98
99
|
else {
|
|
99
|
-
const n =
|
|
100
|
+
const n = normalizeHooks(settings, CLI_PATH);
|
|
100
101
|
if (n > 0)
|
|
101
|
-
console.log(` ↑ PreToolUse hook
|
|
102
|
+
console.log(` ↑ PreToolUse hook 정규화 (${n}건, 셸 무관 명령으로)`);
|
|
102
103
|
else
|
|
103
|
-
console.log(` = PreToolUse hook 이미
|
|
104
|
+
console.log(` = PreToolUse hook 이미 표준 (skip)`);
|
|
104
105
|
}
|
|
105
106
|
// Stop (멱등)
|
|
106
107
|
if (!serialized.includes("hook stop")) {
|
|
@@ -112,6 +113,13 @@ function cmdInit(args) {
|
|
|
112
113
|
else {
|
|
113
114
|
console.log(` = Stop hook 이미 존재 (skip)`);
|
|
114
115
|
}
|
|
116
|
+
// SessionStart (멱등) — 세션 진입(startup|resume) 시 미해결 defer 알림
|
|
117
|
+
if (ensureSessionStartHook(settings, CLI_PATH)) {
|
|
118
|
+
console.log(` + SessionStart hook 추가`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log(` = SessionStart hook 이미 존재 (skip)`);
|
|
122
|
+
}
|
|
115
123
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
116
124
|
// /gate 스킬 설치
|
|
117
125
|
if (existsSync(skillSrc)) {
|
|
@@ -123,7 +131,7 @@ function cmdInit(args) {
|
|
|
123
131
|
✅ 설치 완료. 트랜스포트: ${transport}${transport === "cli"
|
|
124
132
|
? " (ANTHROPIC_API_KEY 설정 시 직접 API로 ~1–3s, 미설정 시 claude -p 폴백 ~13–20s)"
|
|
125
133
|
: ""}
|
|
126
|
-
계획 명세는
|
|
134
|
+
계획 명세는 .gbc/spec.md 에 작성하세요(없으면 시나리오 미지정으로 차단 → 도출·검증 루프 발동: 에이전트가 요청에서 시나리오를 도출해 사용자 검증 후 'gbc spec add'로 등록).`);
|
|
127
135
|
}
|
|
128
136
|
// ---------- gbc status ----------
|
|
129
137
|
function cmdStatus() {
|
|
@@ -264,6 +272,7 @@ function usage() {
|
|
|
264
272
|
gbc metrics [--json] 계측 리포트(M1~M3, B-모드 관측 프록시)
|
|
265
273
|
gbc hook pre-tool-use (내부) PreToolUse hook
|
|
266
274
|
gbc hook stop (내부) Stop hook
|
|
275
|
+
gbc hook session-start (내부) SessionStart hook (미해결 defer 알림)
|
|
267
276
|
`);
|
|
268
277
|
}
|
|
269
278
|
async function main() {
|
|
@@ -274,7 +283,9 @@ async function main() {
|
|
|
274
283
|
return runPreToolUse();
|
|
275
284
|
if (rest[0] === "stop")
|
|
276
285
|
return runStop();
|
|
277
|
-
|
|
286
|
+
if (rest[0] === "session-start")
|
|
287
|
+
return runSessionStart();
|
|
288
|
+
console.error("사용: gbc hook <pre-tool-use|stop|session-start>");
|
|
278
289
|
process.exit(1);
|
|
279
290
|
break;
|
|
280
291
|
case "init":
|
package/dist/hook.js
CHANGED
|
@@ -29,10 +29,12 @@ export function buildBlockReason(verdict, specEmpty, source) {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* pass verdict를 작업단위 캐시(markGated)에 넣어도 되는가.
|
|
32
|
-
* fail-open(판정 실패 안전통과)은 제외 — 일시 장애가 작업단위 내내 게이트를 무력화하는 것을 막는다.
|
|
32
|
+
* - fail-open(판정 실패 안전통과)은 제외 — 일시 장애가 작업단위 내내 게이트를 무력화하는 것을 막는다.
|
|
33
|
+
* - 빈 명세(specEmpty)도 제외 — 빈-spec hash는 상수라 한번 캐시되면 영원히 무효화 안 됨
|
|
34
|
+
* (= 게이트 교차세션 영구 우회, 2026-06-22 진단·수정). 빈 명세는 항상 재판정해야 한다.
|
|
33
35
|
*/
|
|
34
|
-
export function shouldCacheVerdict(verdict) {
|
|
35
|
-
return verdict.verdict === "pass" && !verdict.failOpen;
|
|
36
|
+
export function shouldCacheVerdict(verdict, specEmpty) {
|
|
37
|
+
return verdict.verdict === "pass" && !verdict.failOpen && !specEmpty;
|
|
36
38
|
}
|
|
37
39
|
function readStdin() {
|
|
38
40
|
return new Promise((resolve) => {
|
|
@@ -99,12 +101,16 @@ export async function runPreToolUse() {
|
|
|
99
101
|
}
|
|
100
102
|
const { text: specText, source } = loadPlanSpec(cwd);
|
|
101
103
|
const specHash = computeSpecHash(specText);
|
|
104
|
+
const specEmpty = specText.trim() === "";
|
|
102
105
|
// 계측용 해시: 빈 spec은 ""(센티넬)로 기록 → M1 churn 교차세션 합산 방지.
|
|
103
|
-
|
|
104
|
-
const logHash = specText.trim() === "" ? "" : specHash;
|
|
106
|
+
const logHash = specEmpty ? "" : specHash;
|
|
105
107
|
// 작업단위 1회: 이미 게이트 통과한 단위면 즉시 통과 (judge 미호출, 핫패스)
|
|
106
108
|
// 계측: cached-skip도 기록해야 M3(작업단위당 edit 반복)이 진짜 횟수를 잡는다.
|
|
107
|
-
|
|
109
|
+
// ⚠️ 빈 명세는 캐시를 절대 조회하지 않는다(read-side 가드) — 빈-spec hash는 상수라
|
|
110
|
+
// 한번 캐시된 pass가 영원히 무효화되지 않아 게이트가 교차세션으로 영구 무력화되던
|
|
111
|
+
// 결함(2026-06-22 진단)을 근본 차단. 빈 명세는 항상 재판정: judge [1단계] 사소한
|
|
112
|
+
// 편집 pass, [2단계]a 동작 편집 block. (기존에 오염된 state.json도 자동으로 무시됨)
|
|
113
|
+
if (!specEmpty && isGated(cwd, specHash)) {
|
|
108
114
|
logEvent(cwd, {
|
|
109
115
|
at: nowIso(),
|
|
110
116
|
session,
|
|
@@ -121,39 +127,42 @@ export async function runPreToolUse() {
|
|
|
121
127
|
const defers = activeDeferItems(cwd);
|
|
122
128
|
const verdict = await judge(specText, editText, defers);
|
|
123
129
|
if (verdict.verdict === "pass") {
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
// fail-open(판정 실패) 먼저 분기 — 빈-spec 정상 pass가 fail-open으로 오분류되지 않게.
|
|
131
|
+
if (verdict.failOpen) {
|
|
132
|
+
// 판정 실패로 안전 통과. 캐시하지 않아(작업단위 무력화 방지) 다음 편집에서 재판정.
|
|
133
|
+
// 계측 + systemMessage로 사용자에게 "게이트가 검사 못 했음"을 알린다(조용한 무력화 방지).
|
|
134
|
+
logFailOpen(cwd, toolName, verdict.reason);
|
|
126
135
|
logEvent(cwd, {
|
|
127
136
|
at: nowIso(),
|
|
128
137
|
session,
|
|
129
138
|
specHash: logHash,
|
|
130
139
|
kind: "gate",
|
|
131
140
|
tool: toolName,
|
|
132
|
-
decision: "
|
|
133
|
-
|
|
141
|
+
decision: "failopen",
|
|
142
|
+
});
|
|
143
|
+
emit({
|
|
144
|
+
systemMessage: `🐢 거북이 게이트 — 판정 실패로 안전 통과(fail-open). 이 편집은 게이트 검사를 받지 못했습니다: ${verdict.reason}`,
|
|
145
|
+
hookSpecificOutput: {
|
|
146
|
+
hookEventName: "PreToolUse",
|
|
147
|
+
permissionDecision: "allow",
|
|
148
|
+
permissionDecisionReason: verdict.reason,
|
|
149
|
+
},
|
|
134
150
|
});
|
|
135
|
-
process.exit(0);
|
|
151
|
+
process.exit(0);
|
|
136
152
|
}
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
// 정상 pass. 단 빈 명세 pass는 절대 캐시하지 않는다(상수 hash 영구 우회 방지).
|
|
154
|
+
if (shouldCacheVerdict(verdict, specEmpty))
|
|
155
|
+
markGated(cwd, specHash, verdict.reason);
|
|
140
156
|
logEvent(cwd, {
|
|
141
157
|
at: nowIso(),
|
|
142
158
|
session,
|
|
143
159
|
specHash: logHash,
|
|
144
160
|
kind: "gate",
|
|
145
161
|
tool: toolName,
|
|
146
|
-
decision: "
|
|
162
|
+
decision: "pass",
|
|
163
|
+
deferCount: defers.length,
|
|
147
164
|
});
|
|
148
|
-
|
|
149
|
-
systemMessage: `🐢 거북이 게이트 — 판정 실패로 안전 통과(fail-open). 이 편집은 게이트 검사를 받지 못했습니다: ${verdict.reason}`,
|
|
150
|
-
hookSpecificOutput: {
|
|
151
|
-
hookEventName: "PreToolUse",
|
|
152
|
-
permissionDecision: "allow",
|
|
153
|
-
permissionDecisionReason: verdict.reason,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
process.exit(0);
|
|
165
|
+
process.exit(0); // 정상 통과 (자동승인 X — 무출력)
|
|
157
166
|
}
|
|
158
167
|
// block: 사람 pause (ask 기본) — 사유가 사용자에게 표시됨
|
|
159
168
|
// 시나리오 미지정(명세 빈약)과 침묵 누락을 다르게 안내한다.
|
|
@@ -208,3 +217,35 @@ export async function runStop() {
|
|
|
208
217
|
});
|
|
209
218
|
process.exit(0);
|
|
210
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* 세션 진입(startup|resume) 시 미해결 defer 잔여를 표면화하는 알림 문자열. 없으면 "".
|
|
222
|
+
* gbc 자기 소유 데이터(.gbc/defers.json)만 사용 — scratch/메모리 미접근(다른 하네스와 혼재·환각 방지).
|
|
223
|
+
*/
|
|
224
|
+
export function buildSessionStartHint(unresolved) {
|
|
225
|
+
if (unresolved.length === 0)
|
|
226
|
+
return "";
|
|
227
|
+
const items = unresolved.map((d, i) => `${i + 1}. ${d.item}`).join("\n");
|
|
228
|
+
return (`🐢 거북이 게이트 — 미해결 defer ${unresolved.length}건 (이전 작업 잔여):\n${items}\n` +
|
|
229
|
+
`필요하면 사용자에게 이어서 처리할지 확인하세요. 해결은 'gbc defer resolve <번호>'.`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* SessionStart: 세션 진입 시 미해결 defer를 stdout(plain text)으로 표면화 → Claude 컨텍스트 주입.
|
|
233
|
+
* 잔여 없으면 무출력. GBC_NO_SESSION_HINT=1로 opt-out. 결정론적(LLM·코드비교 없음).
|
|
234
|
+
*/
|
|
235
|
+
export async function runSessionStart() {
|
|
236
|
+
if (process.env.GBC_NO_SESSION_HINT === "1")
|
|
237
|
+
process.exit(0);
|
|
238
|
+
let input = {};
|
|
239
|
+
try {
|
|
240
|
+
const raw = await readStdin();
|
|
241
|
+
input = raw ? JSON.parse(raw) : {};
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
const cwd = input.cwd || process.cwd();
|
|
247
|
+
const hint = buildSessionStartHint(unresolvedDefers(cwd));
|
|
248
|
+
if (hint)
|
|
249
|
+
process.stdout.write(hint);
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -1,39 +1,56 @@
|
|
|
1
1
|
// gbc init 설치 로직 (순수함수 — cli.ts main() 부작용 없이 단위테스트 가능).
|
|
2
|
-
// 키 주입은
|
|
2
|
+
// 키 주입은 셸이 아니라 gbc 코드(judge.ts resolveApiKey)가 처리한다 → hook 명령은
|
|
3
|
+
// 셸 무관 순수 형태라 native Windows(cmd.exe)/bash/zsh/Mac에서 동일하게 동작한다.
|
|
3
4
|
/**
|
|
4
|
-
* PreToolUse hook 명령
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* PreToolUse hook 명령 생성 — 셸 무관 순수 명령.
|
|
6
|
+
* `node "<cliPath>" hook pre-tool-use` 형태만 생성한다. 키 주입(셸 prefix)·셸 확장 없음.
|
|
7
|
+
* - cliPath는 큰따옴표로만 감싼다(공백 포함 경로 안전). 큰따옴표는 cmd.exe·POSIX sh 공통.
|
|
8
|
+
* - 백슬래시를 이스케이프하지 않는다: Windows 경로(C:\...)의 구분자이며, settings.json에
|
|
9
|
+
* 기록될 때 cli.ts의 JSON.stringify가 `\`→`\\` 처리를 담당한다(여기서 또 하면 이중).
|
|
10
|
+
* - cliPath는 import.meta.url 기반 설치 경로(사용자 입력 아님)라 셸 인젝션 위험이 실질적으로
|
|
11
|
+
* 없어 별도 메타문자 이스케이프를 두지 않는다(이전 shDquote 방어 제거 — 보안 재검토 반영).
|
|
7
12
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* `"` 백틱 `$` `\` 를 백슬래시 처리한다(settings.json 명령 인젝션 방지).
|
|
11
|
-
*/
|
|
12
|
-
function shDquote(s) {
|
|
13
|
-
return s.replace(/(["`$\\])/g, "\\$1");
|
|
14
|
-
}
|
|
15
|
-
export function buildPreCommand(cliPath, useKey) {
|
|
16
|
-
const base = `node "${shDquote(cliPath)}" hook pre-tool-use`;
|
|
17
|
-
// $HOME·$(...)는 의도된 셸 확장이므로 이스케이프하지 않는다.
|
|
18
|
-
return useKey ? `ANTHROPIC_API_KEY="$(cat "$HOME/.gbc/api-key")" ${base}` : base;
|
|
13
|
+
export function buildPreCommand(cliPath) {
|
|
14
|
+
return `node "${cliPath}" hook pre-tool-use`;
|
|
19
15
|
}
|
|
20
16
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* 기존 PreToolUse hook 명령을 현재 표준(셸 무관 pure 명령)으로 정규화한다.
|
|
18
|
+
* keyless 명령·옛 bash 키주입 prefix 명령을 모두 pure로 교체 → "모든 OS 동일 명령" 목표 달성.
|
|
19
|
+
* settings를 제자리 수정하고 변경 건수를 반환한다(멱등: 이미 표준이면 0건).
|
|
23
20
|
*/
|
|
24
|
-
export function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const target = buildPreCommand(cliPath, true);
|
|
28
|
-
let upgraded = 0;
|
|
21
|
+
export function normalizeHooks(settings, cliPath) {
|
|
22
|
+
const target = buildPreCommand(cliPath);
|
|
23
|
+
let changed = 0;
|
|
29
24
|
for (const entry of settings.hooks?.PreToolUse ?? []) {
|
|
30
25
|
for (const h of entry.hooks ?? []) {
|
|
31
|
-
|
|
32
|
-
if (h.command.includes("hook pre-tool-use") && !h.command.includes("ANTHROPIC_API_KEY")) {
|
|
26
|
+
if (h.command.includes("hook pre-tool-use") && h.command !== target) {
|
|
33
27
|
h.command = target;
|
|
34
|
-
|
|
28
|
+
changed++;
|
|
35
29
|
}
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
|
-
return
|
|
32
|
+
return changed;
|
|
33
|
+
}
|
|
34
|
+
/** SessionStart hook 명령 — 셸 무관 순수 명령(buildPreCommand와 동일 규약). */
|
|
35
|
+
export function buildSessionStartCommand(cliPath) {
|
|
36
|
+
return `node "${cliPath}" hook session-start`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* SessionStart hook을 멱등 등록한다. matcher "startup|resume"로 신규 진입·재개에만 발화
|
|
40
|
+
* (compact마다 반복 노이즈 방지). 이미 'hook session-start' 명령이 있으면 추가하지 않는다.
|
|
41
|
+
* settings를 제자리 수정하고, 새로 추가했으면 true(이미 있으면 false)를 반환한다.
|
|
42
|
+
*/
|
|
43
|
+
export function ensureSessionStartHook(settings, cliPath) {
|
|
44
|
+
for (const entry of settings.hooks?.SessionStart ?? []) {
|
|
45
|
+
for (const h of entry.hooks ?? []) {
|
|
46
|
+
if (h.command.includes("hook session-start"))
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const hooks = (settings.hooks ??= {});
|
|
51
|
+
(hooks.SessionStart ??= []).push({
|
|
52
|
+
matcher: "startup|resume",
|
|
53
|
+
hooks: [{ type: "command", command: buildSessionStartCommand(cliPath) }],
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
39
56
|
}
|
package/dist/judge.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
const execFileAsync = promisify(execFile);
|
|
4
7
|
const MODEL = process.env.GBC_MODEL ?? "claude-haiku-4-5";
|
|
8
|
+
/**
|
|
9
|
+
* API 키 해석 (크로스플랫폼, 셸 무관).
|
|
10
|
+
* 1) ANTHROPIC_API_KEY 환경변수 우선, 2) 없으면 ~/.gbc/api-key 파일.
|
|
11
|
+
* STUB
|
|
12
|
+
*/
|
|
13
|
+
export function resolveApiKey(opts = {}) {
|
|
14
|
+
const env = opts.env ?? process.env;
|
|
15
|
+
const fromEnv = env.ANTHROPIC_API_KEY;
|
|
16
|
+
if (fromEnv && fromEnv.trim())
|
|
17
|
+
return fromEnv;
|
|
18
|
+
const home = opts.homeDir ?? homedir();
|
|
19
|
+
const read = opts.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
20
|
+
try {
|
|
21
|
+
// bash `$(cat)`는 trailing newline을 벗기지만 readFileSync는 안 벗긴다 → 명시 trim 필수.
|
|
22
|
+
const key = read(join(home, ".gbc", "api-key")).trim();
|
|
23
|
+
return key || null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null; // 파일 부재/읽기 실패 → 키 없음(claude -p 폴백)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
5
29
|
/**
|
|
6
30
|
* 최소 게이트 시스템 프롬프트.
|
|
7
31
|
* 의미론(핵심): 한 편집이 모든 케이스를 *완전 구현*할 필요는 없다.
|
|
@@ -53,15 +77,16 @@ function parseVerdict(raw) {
|
|
|
53
77
|
reason: typeof j.reason === "string" ? j.reason : "",
|
|
54
78
|
};
|
|
55
79
|
}
|
|
56
|
-
/** 트랜스포트 선택 결과 (디버그/리포트용) */
|
|
80
|
+
/** 트랜스포트 선택 결과 (디버그/리포트용). env 또는 키파일에 키가 있으면 api. */
|
|
57
81
|
export function selectedTransport() {
|
|
58
|
-
return
|
|
82
|
+
return resolveApiKey() ? "api" : "cli";
|
|
59
83
|
}
|
|
60
84
|
/** 직접 Anthropic API (haiku). SDK는 여기서만 lazy import → hook 핫패스 보호. */
|
|
61
85
|
async function judgeViaApi(system, user) {
|
|
62
86
|
const mod = await import("@anthropic-ai/sdk");
|
|
63
87
|
const Anthropic = mod.default;
|
|
64
|
-
|
|
88
|
+
// 키를 코드에서 해석(env 또는 ~/.gbc/api-key)해 명시 전달 — 셸 주입 불필요(크로스플랫폼).
|
|
89
|
+
const client = new Anthropic({ apiKey: resolveApiKey() ?? undefined });
|
|
65
90
|
const resp = await client.messages.create({
|
|
66
91
|
model: MODEL,
|
|
67
92
|
max_tokens: 1024,
|
package/dist/spec.js
CHANGED
|
@@ -5,7 +5,11 @@ import { gbcDir } from "./store.js";
|
|
|
5
5
|
const MAX_SPEC = 12000; // 명세 텍스트 절단 (프롬프트 비대화 방지)
|
|
6
6
|
/**
|
|
7
7
|
* 계획 명세를 디스크에서 로드한다. (advisor④: durable 소스만 — 라이브 SubTask는 영속 X)
|
|
8
|
-
* 우선순위: GBC_SPEC_FILE > .gbc/spec.md >
|
|
8
|
+
* 우선순위: GBC_SPEC_FILE > .gbc/spec.md > "" (빈 명세 = 시나리오 미지정 → 통증#2 차단)
|
|
9
|
+
*
|
|
10
|
+
* .gbc/spec.md가 단일 정본(canonical). 다른 파일(예: 하네스의 scratch.md)을 명세로 쓰려면
|
|
11
|
+
* GBC_SPEC_FILE로 명시 지정한다 — gbc가 소유 안 한 파일을 자동 폴백하지 않는다(0.2.2:
|
|
12
|
+
* scratch.md 자동 폴백 제거. 진행추적 파일을 시나리오 명세로 오인하던 거짓음성 차단).
|
|
9
13
|
*
|
|
10
14
|
* 느슨 매칭은 게이트 LLM이 담당한다(체크리스트 라인/SubTask 항목). 로더는 텍스트만 제공.
|
|
11
15
|
*/
|
|
@@ -14,7 +18,6 @@ export function loadPlanSpec(cwd) {
|
|
|
14
18
|
if (process.env.GBC_SPEC_FILE)
|
|
15
19
|
candidates.push(process.env.GBC_SPEC_FILE);
|
|
16
20
|
candidates.push(join(cwd, ".gbc", "spec.md"));
|
|
17
|
-
candidates.push(join(cwd, "scratch.md"));
|
|
18
21
|
for (const path of candidates) {
|
|
19
22
|
if (existsSync(path)) {
|
|
20
23
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geobuke-code",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "거북이코드 — 구현 직전 강제 게이트. Claude Code PreToolUse hook으로 코드 변경 전 계획 케이스 누락·시나리오 미지정을 차단한다.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
|
+
"dev": "tsc --watch",
|
|
18
19
|
"test": "node --test 'test/**/*.test.mjs'",
|
|
19
20
|
"eval": "node dist/eval/regression.js",
|
|
20
21
|
"prepare": "tsc",
|