geobuke-code 0.4.0 → 0.4.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 CHANGED
@@ -25,6 +25,7 @@
25
25
  - **크로스-repo 가시성** — 등록한 다른 repo의 미해결 defer 요약 + 게이트 hook 건강성 롤업("회사 repo에서 게이트가 조용히 안 먹는다"를 한 명령으로 진단) ([크로스-repo](#크로스-repo-가시성))
26
26
  - **판정 드리프트 회귀락** — 실제 판정을 캡처해두고 모델/프롬프트/SDK 변화 후 재판정해 pass↔block 뒤집힘을 잡는 로컬 pre-flight ([`gbc gate snapshot`](#판정-드리프트-회귀락-gbc-gate-snapshot))
27
27
  - **관측 계측(M1~M3)** — 게이트 적중·재호출·통과 후 churn. 여러 repo는 `gbc metrics --all`로 병합 ([계측](#계측-m1m3))
28
+ - **운영 현황 관측(`/gbc-monitor` 스킬)** — 위 게이트 상태·계측·repo 건강성·드리프트락을 묶어 조회·해석하는 읽기전용 표면(상태 변경은 `/gate`) ([`/gbc-monitor`](#운영-현황-관측-gbc-monitor))
28
29
 
29
30
  ## 설치
30
31
 
@@ -34,7 +35,7 @@ npm install -g geobuke-code
34
35
 
35
36
  # 2) 대상 프로젝트에 게이트 설치
36
37
  cd <your-project>
37
- gbc init # .claude/settings.json에 hook(PreToolUse+Stop+SessionStart) + /gate skill 머지 (동의·백업)
38
+ gbc init # .claude/settings.json에 hook(PreToolUse+Stop+SessionStart) + /gate · /gbc-mute · /gbc-monitor skill 머지 (동의·백업)
38
39
  ```
39
40
 
40
41
  <details>
@@ -111,7 +112,7 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
111
112
 
112
113
  > 세션 진입 알림만 끄려면 `GBC_NO_SESSION_HINT=1`. 매 대화 종료(Stop) defer 리마인드만 끄려면 `gbc defer mute`(영속, 해제 `unmute` · 스킬 `/gbc-mute`) — 진입 알림은 남는다. 업데이트 안내만 끄려면 `GBC_NO_UPDATE_NOTICE=1`.
113
114
  > 프로젝트 hook이 구식이거나(SessionStart 누락·옛 명령) 새 버전이 나오면 gbc가 감지해 **`gbc update`**(전역 최신 + 현재 프로젝트 재init 한방) 또는 수동 `npm i -g geobuke-code@latest → gbc init --yes`를 안내한다. 단 안내는 **이미 hook이 등록된 프로젝트**(=한 번이라도 `gbc init`을 한 코호트)에만 도달한다 — 전혀 init하지 않은 프로젝트엔 실행할 hook이 없어 구조적으로 알릴 수 없다(gbc는 전역 hook을 깔지 않는다).
114
- > **업데이트 안내(①)는 네트워크를 게이트 핫패스에 들이지 않는다**: `~/.gbc/version-check.json` 캐시만 비교하고, 갱신 fetch는 안전한 비-핫패스에서만 짧은 타임아웃(1.5s)으로. ⓐSessionStart는 캐시가 stale이면 **표시 전에 갱신**해 신버전이 그 세션에 바로 뜬다(1세션 지연 없음). ⓑ**PreToolUse는 judge를 도는 편집(cache-miss)에서 캐시가 stale이면 refresh를 judge와 *병렬*로 건다**(0.3.0) — judge가 ≥1.5s라 지연 0이고, 사용자가 `gbc status`를 직접 치지 않아도 캐시가 최신이 된다. **cached-skip 핫패스에는 네트워크를 절대 넣지 않는다.** 조회 실패는 조용히 무시(fail-silent)되어 게이트 결정에 영향이 없다. 캐시 TTL 24h.
115
+ > **업데이트 안내(①)는 네트워크를 게이트 핫패스에 들이지 않는다**: `~/.gbc/version-check.json` 캐시만 비교하고, 갱신 fetch는 안전한 비-핫패스에서만 짧은 타임아웃(1.5s)으로. ⓐSessionStart는 캐시가 stale이면 **표시 전에 갱신**해 신버전이 그 세션에 바로 뜬다(1세션 지연 없음). ⓑ**PreToolUse는 judge를 도는 편집(cache-miss)에서 캐시가 stale이면 refresh를 judge와 *병렬*로 건다**(0.3.0) — judge가 ≥1.5s라 지연 0이고, 사용자가 `gbc status`를 직접 치지 않아도 캐시가 최신이 된다. **cached-skip 핫패스에는 네트워크를 절대 넣지 않는다.** 조회 실패는 조용히 무시(fail-silent)되어 게이트 결정에 영향이 없다. 캐시 TTL 12h.
115
116
 
116
117
  ### 시나리오 도출 루프 (수기 입력 불필요)
117
118
 
@@ -154,7 +155,7 @@ gbc gate review --spec 1 3 --defer 2 # 1,3은 승인→spec / 2는 미
154
155
 
155
156
  | 명령 | 설명 |
156
157
  |---|---|
157
- | `gbc init` | hook + `/gate` · `/gbc-mute` 스킬 설치 + 크로스-repo 레지스트리 자동등록(opt-out: `--no-register`) |
158
+ | `gbc init` | hook + `/gate` · `/gbc-mute` · `/gbc-monitor` 스킬 설치 + 크로스-repo 레지스트리 자동등록(opt-out: `--no-register`) |
158
159
  | `gbc update` | 전역 최신 설치(`npm i -g …@latest`) + 현재 프로젝트 재init 한방. `--dry-run`으로 실행 명령만 미리보기 |
159
160
  | `gbc status` | 게이트 상태 + 로드된 명세 + Stop 리마인드 음소거 여부 |
160
161
  | `gbc defer add "<케이스>"` | 케이스를 명시적으로 미루기 (→ open) |
@@ -175,6 +176,7 @@ gbc gate review --spec 1 3 --defer 2 # 1,3은 승인→spec / 2는 미
175
176
  | `gbc repos add [경로]` | 크로스-repo 레지스트리에 추가(생략 시 현재 폴더) |
176
177
  | `gbc repos list` | 등록된 repo + 각 repo의 미해결 defer 수 + **게이트 건강성**(hook 부재/구식 코호트) |
177
178
  | `gbc repos remove [경로]` | 레지스트리에서 제거 |
179
+ | `/gbc-monitor` 스킬 | 위 관측 명령(status·metrics --all·repos list·snapshot status)을 묶어 조회·해석하는 **읽기전용** 표면. 상태 변경은 `/gate` |
178
180
 
179
181
  우회: `GBC_NO_GATE=1` (계측됨 — 우회 자체가 게이트 가치 측정 데이터).
180
182
 
@@ -259,6 +261,26 @@ gbc gate snapshot replay --samples 5 # 케이스당 5회 모달 판정(잔여
259
261
  - **캡처 시점**: judge가 *실제로 평가한* cache-miss 편집만 기록된다(cached-skip·fail-open 제외). 특정 편집을 캡처하려면 `gbc gate reset` 후 그 편집을 수행한다.
260
262
  - **⚠️ 로컬 전용(privacy)**: `golden.json`은 정규화된 **편집 본문**을 담는다 — `events.jsonl`이 불변식으로 절대 저장하지 않는 내용이다. `.gbc/`는 gitignore이므로 이 골든셋은 **로컬 드리프트 점검**이지 커밋되는 CI 스위트가 아니다. 공유 CI로 쓰려면 편집 본문을 커밋하는 privacy 트레이드오프를 명시적으로 감수해야 한다.
261
263
 
264
+ ## 운영 현황 관측 (`/gbc-monitor`)
265
+
266
+ 위 운영층 명령들(`gbc status`·`gbc metrics --all`·`gbc repos list`·`gbc gate snapshot status`)은 강력하지만, 세션 안에서 **발견가능성**이 없었다 — 명령·플래그를 외워 직접 치거나 매번 요청해야 했다. `/gbc-monitor` 스킬은 이들을 **묶어 조회하고 해석**하는 읽기전용 표면이다.
267
+
268
+ ```
269
+ /gbc-monitor # 게이트 현황 종합(4개 묶음 + 해석)
270
+ 게이트 현황 보여줘 / 계측 어때 / repo 건강성 # 자연어로도 트리거(특정 항목만도 가능)
271
+ ```
272
+
273
+ 핵심은 **단순 별칭이 아니라 해석**이다 — 숫자가 정상/주의/행동필요 중 무엇인지 판정해준다:
274
+
275
+ - `repos ✗부재` → 게이트가 조용히 죽은 상태 → 그 repo `gbc init` 재실행 안내(단 `/tmp/*` 잔재는 `gbc repos remove` 청소 후보)
276
+ - `M1 churn` → **약신호 proxy** — "통과 후 결함 수"로 과대해석 금지(진짜 M1은 A-mode 사후대조 과제)
277
+ - `명세 비어있음 → 차단` → 버그가 아니라 게이트가 의도대로 켜진 정상 동작
278
+ - `M2 도중발견 비율 낮음` → 누락이 늦게 새기보다 게이트가 사전 차단을 잘 한다는 긍정 신호
279
+
280
+ ### 경계 — 관측하고, 액션은 가리킨다
281
+
282
+ `/gbc-monitor`와 `/gate`를 가르는 단 하나의 선: **"그 명령이 상태를 바꾸거나(mutate) API를 쓰는가?"** 그렇다면 `/gate`의 영역이다(미루기·리셋·`snapshot on/off/clear`·`replay`). `/gbc-monitor`는 **읽기전용 조회만** 하고, 변경이 필요하면 직접 실행하지 않고 `/gate`로 *가리킨다*. 이 분리 덕에 모니터링은 부작용 걱정 없이 언제든 안전하게 부를 수 있다. `gbc init`이 이 스킬을 `/gate`·`/gbc-mute`와 함께 설치한다.
283
+
262
284
  ## 정직한 한계
263
285
 
264
286
  - 사후 대조가 아닌 **구현 전 게이트**다 — "도중 탈선"은 못 잡는다(설계상 후속 C 영역).
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { homedir } from "node:os";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync, lstatSync, } from "node:fs";
8
8
  import { runPreToolUse, runStop, runSessionStart } from "./hook.js";
9
- import { loadPlanSpec, computeSpecHash, addSpecCase, readSpecCases, clearSpec } from "./spec.js";
9
+ import { loadPlanSpec, computeSpecHash, addSpecCase, readSpecCases, clearSpec, archiveSpec, } from "./spec.js";
10
10
  import { loadState, resetGate } from "./state.js";
11
11
  import { addDefer, loadDefers, resolveDefer, startDefer, reopenDefer } from "./defer.js";
12
12
  import { loadRepos, addRepo, removeRepo } from "./repos.js";
@@ -78,7 +78,7 @@ async function cmdInit(args) {
78
78
  const claudeDir = join(cwd, ".claude");
79
79
  const settingsPath = join(claudeDir, "settings.json");
80
80
  // 설치 대상 스킬들(제품소스 skills/<name>/SKILL.md → .claude/skills/<name>/SKILL.md).
81
- const skillNames = ["gate", "gbc-mute"];
81
+ const skillNames = ["gate", "gbc-mute", "gbc-monitor"];
82
82
  if (!yes) {
83
83
  console.log(`🐢 gbc init — 다음을 수행합니다 (프로젝트 로컬만, 전역 ~/.claude 미변경):
84
84
 
@@ -231,9 +231,13 @@ function cmdDefer(args) {
231
231
  console.error('사용: gbc defer add "<케이스 설명>"');
232
232
  process.exit(1);
233
233
  }
234
- addDefer(cwd, item);
235
- logCli(cwd, "defer-add", curHash(cwd));
236
- console.log(`🐢 미룸 등록: ${item}`);
234
+ if (addDefer(cwd, item).added) {
235
+ logCli(cwd, "defer-add", curHash(cwd));
236
+ console.log(`🐢 미룸 등록: ${item}`);
237
+ }
238
+ else {
239
+ console.log(`🐢 이미 미해결로 미룬 항목 — 중복 등록 skip: ${item}`);
240
+ }
237
241
  }
238
242
  else if (sub === "mute" || sub === "unmute") {
239
243
  const muted = sub === "mute";
@@ -295,9 +299,13 @@ function cmdSpec(args) {
295
299
  process.exit(1);
296
300
  }
297
301
  const beforeHash = curHash(cwd); // 변이 전 해시 = 수정 대상 작업단위와 상관
298
- addSpecCase(cwd, item);
299
- logCli(cwd, "spec-add", beforeHash);
300
- console.log(`🐢 명세 등록: ${item}`);
302
+ if (addSpecCase(cwd, item)) {
303
+ logCli(cwd, "spec-add", beforeHash);
304
+ console.log(`🐢 명세 등록: ${item}`);
305
+ }
306
+ else {
307
+ console.log(`🐢 이미 등록된 케이스 — 중복 등록 skip: ${item}`);
308
+ }
301
309
  }
302
310
  else if (sub === "show") {
303
311
  const cases = readSpecCases(cwd);
@@ -318,6 +326,27 @@ function cmdSpec(args) {
318
326
  process.exit(1);
319
327
  }
320
328
  }
329
+ // ---------- gbc done ----------
330
+ /**
331
+ * 작업단위 명시 종료(ST3). spec.md 본문을 아카이브→비우고 게이트를 리셋한다.
332
+ * drift 근본수정: "완료" 이벤트 부재로 옛 케이스가 누적·부활하던 것을, 명시적 완료 신호로 닫는다.
333
+ * gate reset 로직(resetGate)은 변경하지 않고 그대로 호출만 한다(재게이트 의미 보존).
334
+ * defer는 건드리지 않는다 — 미해결 defer는 작업단위를 넘어 이월되는 별도 수명주기다.
335
+ */
336
+ function cmdDone() {
337
+ const cwd = process.cwd();
338
+ const beforeHash = curHash(cwd);
339
+ const archived = archiveSpec(cwd);
340
+ logCli(cwd, "done", beforeHash);
341
+ resetGate(cwd);
342
+ if (archived) {
343
+ console.log(`🐢 작업단위 종료 — 명세 아카이브: ${archived}`);
344
+ }
345
+ else {
346
+ console.log("🐢 작업단위 종료 — 비울 명세가 없습니다(이미 비어 있음).");
347
+ }
348
+ console.log(" 게이트 리셋 완료. 다음 작업단위는 새 명세로 시작하세요('gbc spec add').");
349
+ }
321
350
  // ---------- gbc gate ----------
322
351
  async function cmdGate(args) {
323
352
  const cwd = process.cwd();
@@ -405,7 +434,7 @@ async function cmdGateSnapshotReplay(cwd, args) {
405
434
  const votes = { pass: 0, block: 0 };
406
435
  let lastMissing = [];
407
436
  for (let i = 0; i < samples; i++) {
408
- const v = await judge(c.spec, c.edit, c.defers, { temperature: 0 });
437
+ const v = await judge(c.spec, c.edit, c.defers, c.resolved ?? [], { temperature: 0 });
409
438
  votes[v.verdict]++;
410
439
  lastMissing = v.missing;
411
440
  }
@@ -473,19 +502,36 @@ function cmdGateReview(cwd, args) {
473
502
  process.exit(1);
474
503
  }
475
504
  const beforeHash = curHash(cwd); // 변이 전 해시 = 게이트된 작업단위와 상관(M1 churn)
505
+ const specAdded = [];
506
+ const specDup = [];
476
507
  for (const c of toSpec) {
477
- addSpecCase(cwd, c);
478
- logCli(cwd, "spec-add", beforeHash);
508
+ if (addSpecCase(cwd, c)) {
509
+ logCli(cwd, "spec-add", beforeHash);
510
+ specAdded.push(c);
511
+ }
512
+ else {
513
+ specDup.push(c);
514
+ }
479
515
  }
516
+ const deferAdded = [];
517
+ const deferDup = [];
480
518
  for (const c of toDefer) {
481
- addDefer(cwd, c);
482
- logCli(cwd, "defer-add", beforeHash);
519
+ if (addDefer(cwd, c).added) {
520
+ logCli(cwd, "defer-add", beforeHash);
521
+ deferAdded.push(c);
522
+ }
523
+ else {
524
+ deferDup.push(c);
525
+ }
483
526
  }
484
527
  clearPendingReview(cwd);
485
- if (toSpec.length > 0)
486
- console.log(`🐢 명세 등록 ${toSpec.length}건: ${toSpec.join(", ")}`);
487
- if (toDefer.length > 0)
488
- console.log(`🐢 미룸 등록 ${toDefer.length}건: ${toDefer.join(", ")}`);
528
+ if (specAdded.length > 0)
529
+ console.log(`🐢 명세 등록 ${specAdded.length}건: ${specAdded.join(", ")}`);
530
+ if (deferAdded.length > 0)
531
+ console.log(`🐢 미룸 등록 ${deferAdded.length}건: ${deferAdded.join(", ")}`);
532
+ if (specDup.length + deferDup.length > 0) {
533
+ console.log(`🐢 중복 skip ${specDup.length + deferDup.length}건: ${[...specDup, ...deferDup].join(", ")}`);
534
+ }
489
535
  console.log("→ 검토 완료(펜딩 비움). 같은 편집을 재시도하면 등록된 케이스 기준으로 재판정됩니다.");
490
536
  }
491
537
  // ---------- gbc metrics ----------
@@ -669,7 +715,7 @@ function usage() {
669
715
  console.log(`🐢 gbc — 거북이코드 구현-전 게이트
670
716
 
671
717
  사용:
672
- gbc init [--yes] [--no-register] 프로젝트에 hook + /gate · /gbc-mute 스킬 설치
718
+ gbc init [--yes] [--no-register] 프로젝트에 hook + /gate · /gbc-mute · /gbc-monitor 스킬 설치
673
719
  (--no-register: 크로스-repo 레지스트리 자동등록 생략)
674
720
  gbc update [--dry-run] 전역 최신 설치 + 현재 프로젝트 재init (한방 갱신)
675
721
  gbc status 게이트 상태 + 로드된 명세 확인
@@ -682,8 +728,9 @@ function usage() {
682
728
  gbc defer unmute Stop defer 알림 다시 켜기
683
729
  gbc spec add "<케이스>" 승인된 시나리오를 .gbc/spec.md에 등록
684
730
  gbc spec show 등록된 케이스 목록
685
- gbc spec clear 명세 비우기(작업단위 종료)
686
- gbc gate reset 작업단위 게이트 리셋
731
+ gbc spec clear 명세 비우기(아카이브 없이)
732
+ gbc done 작업단위 명시 종료(명세 아카이브→비움 + 게이트 리셋)
733
+ gbc gate reset 작업단위 게이트만 리셋(명세 보존·같은 단위 재게이트)
687
734
  gbc gate review block이 도출한 누락 케이스 체크리스트 보기
688
735
  gbc gate review --spec <ref> --defer <ref>
689
736
  누락 케이스 일괄 분류(승인→spec / 미룸→defer)
@@ -725,6 +772,8 @@ async function main() {
725
772
  return cmdSpec(rest);
726
773
  case "gate":
727
774
  return cmdGate(rest);
775
+ case "done":
776
+ return cmdDone();
728
777
  case "metrics":
729
778
  return cmdMetrics(rest);
730
779
  case "repos":
package/dist/defer.js CHANGED
@@ -29,13 +29,23 @@ export function loadDefers(cwd) {
29
29
  function save(cwd, defers) {
30
30
  writeJson(deferPath(cwd), defers);
31
31
  }
32
- /** 명시적으로 항목을 미룬다 (침묵 누락 차단의 유일한 정당 경로) */
32
+ /**
33
+ * 명시적으로 항목을 미룬다 (침묵 누락 차단의 유일한 정당 경로).
34
+ * 중복 감지(ST2): 정규화 텍스트가 **미해결(open+in_progress)** 항목과 동일하면 새로 추가하지 않고
35
+ * 기존 엔트리를 added:false로 반환한다 — 같은 '무관' defer가 시점만 달리 누적되던 증상(2026-06-26 진단) 차단.
36
+ * resolved된 동일 텍스트는 막지 않는다(완료 후 같은 케이스가 정당히 재발할 수 있음 → 재-defer 허용).
37
+ * @returns { entry, added } — added=false면 entry는 기존(미해결) 항목.
38
+ */
33
39
  export function addDefer(cwd, item) {
34
40
  const defers = loadDefers(cwd);
35
- const entry = { item: normalizeCase(item), at: nowIso(), status: "open" };
41
+ const normalized = normalizeCase(item);
42
+ const dup = defers.find((d) => d.status !== "resolved" && d.item === normalized);
43
+ if (dup)
44
+ return { entry: dup, added: false };
45
+ const entry = { item: normalized, at: nowIso(), status: "open" };
36
46
  defers.push(entry);
37
47
  save(cwd, defers);
38
- return entry;
48
+ return { entry, added: true };
39
49
  }
40
50
  /**
41
51
  * 미해결(=resolved 아님) defer 항목 텍스트만 (게이트 판정 입력용).
@@ -50,6 +60,16 @@ export function activeDeferItems(cwd) {
50
60
  export function unresolvedDefers(cwd) {
51
61
  return loadDefers(cwd).filter((d) => d.status !== "resolved");
52
62
  }
63
+ /**
64
+ * 완료(resolved) defer 항목 텍스트만 (게이트 judge에 [이미 완료된 항목]으로 전달).
65
+ * activeDeferItems가 resolved를 제외해 judge에 안 보이던 갭을 메운다 — judge가 완료 케이스를
66
+ * "계획됨+미defer 형제"로 오인해 며칠 지난 케이스를 침묵누락 차단하던 드리프트(2026-06-26 진단) 완화.
67
+ */
68
+ export function resolvedDeferItems(cwd) {
69
+ return loadDefers(cwd)
70
+ .filter((d) => d.status === "resolved")
71
+ .map((d) => d.item);
72
+ }
53
73
  /**
54
74
  * ref 문자열로 전환 대상 엔트리를 고른다. 세 형태 지원:
55
75
  * - "all": eligibleFrom 상태에 해당하는 전부
package/dist/hook.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { isGatedTool, normalizeEdit } from "./normalize.js";
5
5
  import { loadPlanSpec, computeSpecHash } from "./spec.js";
6
6
  import { isGated, markGated } from "./state.js";
7
- import { activeDeferItems, loadDefers } from "./defer.js";
7
+ import { activeDeferItems, resolvedDeferItems, loadDefers } from "./defer.js";
8
8
  import { isStopHintMuted, isGoldenCapture } from "./config.js";
9
9
  import { loadRepos } from "./repos.js";
10
10
  import { writePendingReview } from "./review.js";
@@ -159,12 +159,15 @@ export async function runPreToolUse(ctx) {
159
159
  const { judge } = await import("./judge.js");
160
160
  const editText = normalizeEdit(toolName, input.tool_input ?? {});
161
161
  const defers = activeDeferItems(cwd);
162
+ // 완료된 케이스를 judge에 [이미 완료된 항목]으로 함께 전달 → 과거 작업단위의 resolved 케이스를
163
+ // "미처리 형제"로 오인해 재차단하던 드리프트 완화(2026-06-26). active와 상호배타(filter 분리).
164
+ const resolved = resolvedDeferItems(cwd);
162
165
  // ①신버전 캐시 자동 refresh(0.3.0) — 사용자가 'gbc status'를 안 쳐도 캐시가 최신이 되게.
163
166
  // judge(네트워크·≥1.5s)와 *병렬*로만 건다 → 핫패스 지연 0. cache-miss(여기 = judge 도는
164
167
  // 비-핫패스)에서만 stale일 때. cached-skip 핫패스엔 절대 네트워크 안 넣는다(0.2.7 원칙 보존).
165
168
  // refreshVersionCache는 내부 fail-silent(reject 불가)라 judge 경로를 깨지 않는다.
166
169
  const refreshP = shouldRefreshCache(Boolean(ctx?.cliPath)) ? refreshVersionCache() : null;
167
- const verdict = await judge(specText, editText, defers);
170
+ const verdict = await judge(specText, editText, defers, resolved);
168
171
  if (refreshP)
169
172
  await refreshP; // judge 동안 이미 완료 — 이 편집의 notice가 갱신된 캐시를 읽도록
170
173
  // 골든셋 캡처(A2, opt-in) — judge가 실제 평가한 cache-miss edit만, fail-open 제외(실판정 아님).
@@ -179,6 +182,7 @@ export async function runPreToolUse(ctx) {
179
182
  edit: editText,
180
183
  spec: specText,
181
184
  defers,
185
+ resolved,
182
186
  expected: { verdict: verdict.verdict, missing: verdict.missing, reason: verdict.reason },
183
187
  });
184
188
  }
@@ -307,7 +311,7 @@ export async function runStop() {
307
311
  * dead doc이 된다. 매 세션 컨텍스트에 신뢰성 있게 규약을 주입하는 유일한 결정론 채널이 이 문자열이다.
308
312
  * (hook엔 추론을 넣지 않는다 — 텍스트만. 자연어/대상 감지·전환 실행은 에이전트 측 책임.)
309
313
  */
310
- const DEFER_PROTOCOL = "규약 — 항목 착수 시 'gbc defer start <ref>'로 진행중 표시, 사용자가 완료를 명시하면 'gbc defer resolve <ref>'로 종결(신호가 모호하면 resolve하지 말고 확인). 되돌리기는 'gbc defer reopen <ref>'. ref=번호|텍스트|all. 모든 자동 전환은 사용자에게 표면화.";
314
+ const DEFER_PROTOCOL = "규약 — 항목 착수 시 'gbc defer start <ref>'로 진행중 표시, 사용자가 완료를 명시하면 'gbc defer resolve <ref>'로 종결(신호가 모호하면 resolve하지 말고 확인). 되돌리기는 'gbc defer reopen <ref>'. ref=번호|텍스트|all. 모든 자동 전환은 사용자에게 표면화. 작업단위(현재 명세) 전체가 끝나면 'gbc done'으로 명시 종료 — 명세를 아카이브·비우고 게이트를 리셋한다(이걸 안 하면 옛 케이스가 다음 작업단위에 형제로 부활).";
311
315
  /**
312
316
  * 전체 defer 리스트에서 미해결(open+in_progress)만 골라 상태 마커와 함께 한 줄씩 포맷한다.
313
317
  * ★ 번호는 전체-리스트 위치(인덱스+1)로 매긴다 — `gbc defer list`·`gbc defer <N>` 인덱스 ref와 동일.
package/dist/judge.js CHANGED
@@ -50,10 +50,11 @@ const GATE_SYSTEM = `너는 코드 구현 직전에 동작하는 "게이트"다.
50
50
 
51
51
  [2단계 — 동작 편집일 때만 검사]
52
52
  (a) [계획 명세]가 없거나 빈약해서 이 동작의 의도·시나리오가 미지정인 채 구현되고 있는가? → **block** (시나리오 미지정).
53
- (b) 계획 명세가 있다면: 이 편집이 작성/수정하는 *바로 그 기능*에 대해 계획에 적힌 형제 케이스 중, 이 편집에서도 안 다뤄지고 [명시적으로 미룬 항목]에도 없는 것이 있는가? → **block** (침묵 누락).
53
+ (b) 계획 명세가 있다면: 이 편집이 작성/수정하는 *바로 그 기능*에 대해 계획에 적힌 형제 케이스 중, 이 편집에서도 안 다뤄지고 [명시적으로 미룬 항목]에도 [이미 완료된 항목]에도 없는 것이 있는가? → **block** (침묵 누락).
54
54
  - 예: 로그인 검증 함수를 쓰면서 계획의 로그인 검증 케이스(중복 이메일·비밀번호 길이 등)를 언급·등록 없이 빠뜨림.
55
55
  - 코드 주석으로 "나중에"라고만 적고 미룬 항목에 등록 안 한 것도 침묵 누락이다.
56
56
  - 계획이 요구한 동작 형태(예: 인라인 에러 메시지)를 충족 못 하고 다른 형태(예: bool만 반환)로 빠뜨린 것도 누락이다.
57
+ - ★ [이미 완료된 항목]에 있는 케이스는 **이전 작업단위에서 이미 처리된 것**이다. 형제 후보에서 제외하고 절대 누락으로 다시 차단(re-flag)하지 마라. 이 편집과 무관한 과거 완료 케이스를 침묵누락이라 막는 것은 오탐이다.
57
58
  (c) 위에 해당 없으면 → **pass**.
58
59
 
59
60
  핵심 균형:
@@ -62,13 +63,16 @@ const GATE_SYSTEM = `너는 코드 구현 직전에 동작하는 "게이트"다.
62
63
 
63
64
  오직 아래 JSON만 출력(설명·마크다운 펜스 금지):
64
65
  {"verdict":"block"|"pass","missing":["누락된 케이스"],"reason":"한 줄 사유"}`;
65
- function buildUserMessage(planSpec, editText, defers) {
66
- const deferText = defers.length > 0 ? defers.map((d) => `- ${d}`).join("\n") : "(없음)";
66
+ function buildUserMessage(planSpec, editText, defers, resolved = []) {
67
+ const fmt = (xs) => (xs.length > 0 ? xs.map((d) => `- ${d}`).join("\n") : "(없음)");
67
68
  return `[계획 명세]
68
69
  ${planSpec.trim() || "(계획 명세 없음 — 개발자가 곧바로 구현을 시작함)"}
69
70
 
70
71
  [명시적으로 미룬 항목]
71
- ${deferText}
72
+ ${fmt(defers)}
73
+
74
+ [이미 완료된 항목]
75
+ ${fmt(resolved)}
72
76
 
73
77
  [현재 편집]
74
78
  ${editText}`;
@@ -174,8 +178,8 @@ function judgeViaCliWin(system, user) {
174
178
  * 게이트 판정. ANTHROPIC_API_KEY 있으면 직접 API(빠름), 없으면 claude -p 폴백.
175
179
  * 실패 시 안전하게 pass(fail-open) — 게이트가 개발을 막아버리는 사고 방지.
176
180
  */
177
- export async function judge(planSpec, editText, defers = [], opts = {}) {
178
- const user = buildUserMessage(planSpec, editText, defers);
181
+ export async function judge(planSpec, editText, defers = [], resolved = [], opts = {}) {
182
+ const user = buildUserMessage(planSpec, editText, defers, resolved);
179
183
  const transport = selectedTransport();
180
184
  try {
181
185
  // claude -p 폴백은 temperature 플래그가 없어 핀 불가 → CLI-transport replay는 best-effort.
package/dist/spec.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, appendFileSync, existsSync } from "node:fs";
1
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { join, resolve, sep } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import { gbcDir } from "./store.js";
@@ -56,16 +56,24 @@ function specPath(cwd) {
56
56
  * 케이스 한 줄을 .gbc/spec.md에 append. 파일 없으면 헤더와 함께 생성.
57
57
  * 입력은 한 줄로 정규화한다: 줄바꿈→공백(readSpecCases 단일라인 매칭과 정합),
58
58
  * 길이 상한 절단(에이전트가 멀티라인/장문 출력을 그대로 add해도 안전).
59
+ *
60
+ * 중복 감지(ST2): 정규화 텍스트가 기존 케이스와 동일하면 등록하지 않고 false 반환
61
+ * (drift 진단 2026-06-26의 2차 증상 — 같은 케이스가 시점만 달리 더미에 누적되던 것 차단).
62
+ * @returns 새로 등록했으면 true, 정규화 동일 케이스가 이미 있어 skip했으면 false.
59
63
  */
60
64
  export function addSpecCase(cwd, item) {
65
+ const normalized = normalizeCase(item);
66
+ if (readSpecCases(cwd).some((c) => c === normalized))
67
+ return false;
61
68
  const path = specPath(cwd);
62
- const line = `- [ ] ${normalizeCase(item)}\n`;
69
+ const line = `- [ ] ${normalized}\n`;
63
70
  if (existsSync(path)) {
64
71
  appendFileSync(path, line, "utf8");
65
72
  }
66
73
  else {
67
74
  writeFileSync(path, `# 작업 명세\n\n${line}`, "utf8");
68
75
  }
76
+ return true;
69
77
  }
70
78
  /** 현재 spec.md의 케이스(체크리스트 라인) 텍스트만 추출. */
71
79
  export function readSpecCases(cwd) {
@@ -84,3 +92,35 @@ export function readSpecCases(cwd) {
84
92
  export function clearSpec(cwd) {
85
93
  writeFileSync(specPath(cwd), "", "utf8");
86
94
  }
95
+ /** 파일명 안전 타임스탬프(ISO의 ':' '.'을 '-'로). Date 금지 환경 방어(빈 문자열 폴백). */
96
+ function nowStamp() {
97
+ try {
98
+ return new Date().toISOString().replace(/[:.]/g, "-");
99
+ }
100
+ catch {
101
+ return "nodate";
102
+ }
103
+ }
104
+ /**
105
+ * 작업단위 종료(ST3 — gbc done): spec.md 본문을 .gbc/spec.archive/<specHash>-<stamp>.md로
106
+ * 보존한 뒤 비운다. 본문이 비어 있으면 아카이브할 것이 없어 null 반환(clearSpec도 생략).
107
+ *
108
+ * drift 근본수정(2026-06-26): 시스템에 작업단위 "완료" 이벤트가 없어 spec.md가 append 전용으로
109
+ * 영구 누적되고, 과거 완료 케이스가 새 작업단위 등록 시 형제로 부활하던 결함을 닫는다. 이 함수가
110
+ * 그 명시적 완료 이벤트의 데이터 동작이다(게이트 리셋은 호출부 cmdDone이 별도로 — resetGate 불변).
111
+ * @returns 아카이브 파일 경로(보존했으면), 비울 본문이 없으면 null.
112
+ */
113
+ export function archiveSpec(cwd) {
114
+ const path = specPath(cwd);
115
+ if (!existsSync(path))
116
+ return null;
117
+ const raw = readFileSync(path, "utf8");
118
+ if (raw.trim() === "")
119
+ return null;
120
+ const dir = join(gbcDir(cwd), "spec.archive");
121
+ mkdirSync(dir, { recursive: true });
122
+ const archivePath = join(dir, `${computeSpecHash(raw)}-${nowStamp()}.md`);
123
+ writeFileSync(archivePath, raw, "utf8");
124
+ clearSpec(cwd);
125
+ return archivePath;
126
+ }
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  const PKG = "geobuke-code";
8
- const TTL_MS = 24 * 60 * 60 * 1000; // 24h
8
+ const TTL_MS = 12 * 60 * 60 * 1000; // 12h
9
9
  const FETCH_TIMEOUT_MS = 1500;
10
10
  export function cachePath(home = homedir()) {
11
11
  return join(home, ".gbc", "version-check.json");
@@ -51,7 +51,7 @@ export function writeVersionCache(cache, home) {
51
51
  /* 캐시 기록 실패는 무시(fail-silent) */
52
52
  }
53
53
  }
54
- /** 캐시가 없거나 TTL(24h) 초과면 stale. now는 주입 가능(테스트). */
54
+ /** 캐시가 없거나 TTL(12h) 초과면 stale. now는 주입 가능(테스트). */
55
55
  export function isCacheStale(cache, now = Date.now()) {
56
56
  if (!cache)
57
57
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geobuke-code",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "거북이코드 — 구현 직전 강제 게이트. Claude Code PreToolUse hook으로 코드 변경 전 계획 케이스 누락·시나리오 미지정을 차단한다.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gate
3
- description: 거북이코드 구현-전 게이트를 관리한다. 로드된 계획 명세 확인, 미룬(defer) 항목 등록·조회·해결, 작업단위 게이트 리셋. PreToolUse hook이 코드 변경을 차단했을 때 이 스킬로 케이스를 명시적으로 미루거나 게이트 상태를 점검한다. '/gate', '게이트 상태', '케이스 미루기', 'defer 등록', '게이트 리셋', '게이트가 막아' 등 언급 시 호출.
3
+ description: 거북이코드 구현-전 게이트를 관리한다. 로드된 계획 명세 확인, 미룬(defer) 항목 등록·조회·해결, 작업단위 게이트 리셋, 작업단위 명시 종료(gbc done). PreToolUse hook이 코드 변경을 차단했을 때 이 스킬로 케이스를 명시적으로 미루거나 게이트 상태를 점검한다. '/gate', '게이트 상태', '케이스 미루기', 'defer 등록', '게이트 리셋', '작업단위 종료', 'gbc done', '게이트가 막아' 등 언급 시 호출.
4
4
  ---
5
5
 
6
6
  # /gate — 구현-전 게이트 관리
@@ -44,8 +44,9 @@ defer 항목은 **open(미착수) → in_progress(진행중) → resolved(해결
44
44
  | 백로그로 되돌리기 (→ open) | `gbc defer reopen <번호\|텍스트\|all>` |
45
45
  | 승인된 시나리오를 명세에 등록 | `gbc spec add "<케이스>"` |
46
46
  | 등록된 케이스 목록 | `gbc spec show` |
47
- | 명세 비우기(작업단위 종료) | `gbc spec clear` |
48
- | 작업단위 게이트 리셋(다음 편집에서 재발동) | `gbc gate reset` |
47
+ | 명세 비우기(아카이브 없이) | `gbc spec clear` |
48
+ | **작업단위 명시 종료**(명세 아카이브→비움 + 게이트 리셋) | `gbc done` |
49
+ | 작업단위 게이트만 리셋(명세 보존·같은 단위 재게이트) | `gbc gate reset` |
49
50
  | block이 도출한 누락 케이스 체크리스트 보기 | `gbc gate review` |
50
51
  | 누락 케이스 일괄 분류(승인→spec / 미룸→defer) | `gbc gate review --spec <번호\|텍스트\|all> --defer <번호\|텍스트\|all>` |
51
52
  | 판정 골든셋 캡처 토글·조회 | `gbc gate snapshot <on\|off\|status\|list\|clear>` |
@@ -66,6 +67,7 @@ defer 항목은 **open(미착수) → in_progress(진행중) → resolved(해결
66
67
  4. 재시도하면 통과한다.
67
68
  > 시나리오 도출은 코딩 에이전트 본체(Opus)가 대화 맥락으로, 게이트 판정은 haiku가 — 두 작업/두 모델 분리(gbc는 모델 계층을 소유하지 않는다).
68
69
  3. **세션 종료 시**: Stop hook이 미해결 defer를 "진행중 N · 미착수 M"으로 구분 리마인드한다. `gbc defer list`로 확인하고, 사용자 완료 선언이 있었으면 resolve, 아니면 다음 세션으로 의식적으로 이월한다(진행중 항목은 in_progress 그대로 남아 다음 SessionStart에 표면화).
70
+ 4. **작업단위(현재 명세) 전체가 끝났을 때 — `gbc done`으로 명시 종료**: 명세(`.gbc/spec.md`)를 `.gbc/spec.archive/`로 보존한 뒤 비우고 게이트를 리셋한다. **이걸 하지 않으면** 며칠 뒤 무관한 새 작업을 시작할 때 옛 명세의 미체크 케이스가 "현재 작업의 형제 케이스"로 부활해 침묵 누락 오탐을 낸다(2026-06-26 진단·근본수정). `gbc spec clear`는 아카이브 없이 비우기만, `gbc gate reset`은 명세를 **보존**한 채 같은 단위를 재게이트하는 별개 동작이다 — 작업단위를 끝낼 땐 `gbc done`을 쓴다.
69
71
 
70
72
  ## 명세 소스
71
73
 
@@ -80,6 +82,8 @@ defer 항목은 **open(미착수) → in_progress(진행중) → resolved(해결
80
82
 
81
83
  - **주석 defer는 defer가 아니다.** `// 비밀번호 검증은 다음에` 같은 코드 주석은 게이트가 침묵 누락으로 본다. 반드시 `gbc defer add`로 레지스트리에 등록해야 한다.
82
84
  - **게이트는 작업단위당 1회만 발동한다.** 명세가 바뀌거나 명세 밖 파일을 편집할 때 재발동한다. 강제로 다시 점검하려면 `gbc gate reset`.
85
+ - **작업단위를 끝내면 `gbc done`을 호출한다 — 안 하면 옛 케이스가 부활한다.** 명세는 append로만 누적되고 작업단위 "완료" 이벤트가 없어, 끝난 케이스가 `.gbc/spec.md`에 미체크로 남으면 다음 작업단위 등록 시 형제 케이스로 재차단된다(드리프트 오탐). `gbc done`이 명세를 아카이브·비워 이 누적을 끊는다. (완화책으로 0.4.2부터 resolved된 defer는 judge에 `[이미 완료된 항목]`으로 전달돼 재플래그를 줄이지만, 근본 정리는 `gbc done`이다.)
86
+ - **같은 케이스 중복 등록은 자동 skip된다(0.4.2).** `gbc spec add`/`gbc defer add`/`gbc gate review`가 정규화 동일 케이스(미해결)를 다시 등록하려 하면 "중복 등록 skip"으로 알리고 더미를 키우지 않는다. resolved된 항목의 동일 텍스트 재등록은 정당한 재-defer로 허용된다.
83
87
  - **게이트가 한 repo에서 아예 안 먹는다면** hook이 미등록·구식일 수 있다. `gbc repos list`가 등록된 각 repo의 게이트 건강성(`⚠️게이트hook부재`/`⚠️SessionStart누락`)을 표시한다 — 떴으면 그 repo에서 `gbc init --yes` 재실행. (크로스-repo는 hook 등록 여부만 검사하고 명령 freshness는 각 repo `gbc status`로 확인.)
84
88
  - **`--no-gate` / `GBC_NO_GATE=1` 우회는 계측된다.** 우회 자체가 게이트 가치 측정 데이터가 된다.
85
89
  - **판정 드리프트가 의심되면**(모델/gbc 업그레이드 후 게이트가 전과 다르게 군다) `gbc gate snapshot on`으로 한동안 캡처하고, 나중에 `gbc gate snapshot replay`로 재판정해 pass↔block 뒤집힘을 점검한다. 골든셋은 **편집 본문을 로컬 `.gbc/golden.json`에만** 저장한다(gitignore·로컬 pre-flight 전용 — 커밋하면 privacy 불변식 위반).
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: gbc-monitor
3
+ description: 거북이코드(gbc) 게이트의 운영 현황을 한 번에 조회·해석하는 읽기전용 관측 표면. 게이트 상태(status)·교차repo 계측(metrics)·repo 건강성(repos)·판정 드리프트락(snapshot) 현황을 묶어 보여주고, 숫자가 무엇을 뜻하는지 해석한다. '/gbc-monitor', '게이트 현황', '게이트 상태 보여줘', '계측 어때', 'gbc 계측', 'repo 건강성', '드리프트 상태', 'gbc 모니터링', '지금 게이트 어디까지 됐어' 등 언급 시 호출한다. 상태를 바꾸는 작업(미루기·리셋·캡처 토글·replay)은 이 스킬이 아니라 `/gate`다 — 여긴 보기만 한다.
4
+ ---
5
+
6
+ # /gbc-monitor — 게이트 운영 현황 관측
7
+
8
+ 거북이코드(`gbc`)는 게이트(차단)·defer(미루기)·계측·크로스repo 레지스트리를 운영하지만, 그 **현황을 한눈에 보는 표면**이 세션 안에 없었다. 명령(`gbc metrics --all` 등)을 외워 직접 치거나 매번 요청해야 했다. 이 스킬은 그 관측 정보를 **묶어서 조회하고 해석**하는 읽기전용 표면이다.
9
+
10
+ ## 핵심 원칙 — 관측하고, 액션은 가리킨다
11
+
12
+ 이 스킬과 `/gate`를 가르는 단 하나의 선은 **"그 명령이 상태를 바꾸거나(mutate) API를 쓰는가?"** 이다.
13
+
14
+ - **읽기전용 조회만 한다.** 상태를 바꾸지 않고, judge(API)를 호출하지 않는다.
15
+ - **액션이 필요하면 *가리킨다*, 직접 하지 않는다.** 예: 드리프트락이 꺼져 있으면 "켜라"가 아니라 "활성 검사는 `gbc gate snapshot replay`(judge 재실행·API 과금) — 필요 시 `/gate`"로 안내한다.
16
+
17
+ 이 경계 덕분에 `/gbc-monitor`는 부작용 없이 언제든 안전하게 부를 수 있고, 미루기·리셋·캡처 같은 변경은 전부 `/gate`로 일원화된다. 새 조회/변경 명령이 생겨도 이 규칙으로 어디 속할지 자동 결정된다.
18
+
19
+ ## 무엇을 보여주나 (전부 읽기전용)
20
+
21
+ 사용자 의도에 따라 **전체** 또는 **필요한 항목만** 조회한다. "현황/모니터링/상태"처럼 포괄적이면 4개를 다 모아 종합하고, "계측만"·"repo 건강성만"처럼 특정되면 해당 명령만 실행한다.
22
+
23
+ | 보고 싶은 것 | 명령 |
24
+ |---|---|
25
+ | 게이트 상태·로드된 명세·defer 요약·음소거 여부 | `gbc status` |
26
+ | 계측(M1~M3) — 등록된 모든 repo 병합 | `gbc metrics --all` |
27
+ | 크로스repo 목록 + 게이트 건강성 + 미해결 defer 수 | `gbc repos list` |
28
+ | 판정 드리프트락(골든셋) 캡처 상태·케이스 목록 | `gbc gate snapshot status` (필요 시 `list`) |
29
+
30
+ > 단일 repo 계측만 원하면 `gbc metrics`(--all 없이). JSON이 필요하면 `gbc metrics --all --json`.
31
+
32
+ ## 실행 흐름 (에이전트)
33
+
34
+ 1. **사용자 의도에서 범위를 판정**한다 — 포괄적이면 4개 전부, 특정되면 해당 명령만.
35
+ 2. 프로젝트 루트에서 명령을 실행하고 **출력을 그대로 수집**한다.
36
+ 3. 원문을 보여준 뒤, 아래 **해석 가이드**로 숫자가 뜻하는 바를 덧붙인다. 단순 나열이 아니라 "이 신호가 정상인지/주의인지/행동이 필요한지"를 한 줄로 판정해준다.
37
+ 4. 행동이 필요한 신호면 **해당 액션을 가리킨다**(`/gate`·`gbc repos remove`·`gbc init` 등) — 단, 이 스킬이 직접 실행하지 않는다.
38
+
39
+ ## 해석 가이드 (이 스킬의 핵심)
40
+
41
+ 조회는 명령이 해주지만, **숫자→의미 번역이 이 스킬의 가치**다. 자주 나오는 신호의 뜻:
42
+
43
+ - **`repos [✗부재]`** → 그 repo는 `.gbc`가 있는데 게이트 hook이 빠져 있다 = **게이트가 조용히 죽은 상태**(코드 변경이 차단 없이 통과). 행동: 그 repo에서 `gbc init` 재실행을 가리킨다. (단 `/tmp/gbc-test-*` 같은 임시 경로는 테스트 잔재이므로 청소 후보 → `gbc repos remove`.)
44
+ - **`snapshot ... 캡처 OFF`** → 판정 드리프트락 미가동 = 같은 코드의 게이트 판정이 바뀌어도 추적 안 됨. 정상일 수 있다(로컬 회귀 점검을 안 쓰는 중). 활성 검사가 필요하면 `gbc gate snapshot replay`(API 과금)를 가리킨다.
45
+ - **`M1` 통과후 churn** → **약신호 proxy일 뿐**이다. "churn N건"을 곧바로 "게이트 통과 후 결함 N건"으로 **과대해석하지 않는다**. 진짜 M1(post-gate 시나리오 위반율)은 A-mode 사후대조가 있어야 측정된다 — 지금 값은 "편집이 게이트 통과 후에도 계속됐다"는 거친 대리지표.
46
+ - **`명세 비어있음 → 모든 코드변경 차단`** → **버그가 아니라 정상 동작**이다. 빈 spec은 "아직 계획 명세가 없으니 코드 변경 전 도출·검증을 강제" = 게이트가 의도대로 켜진 상태. 명세를 채우면(`gbc spec add` 또는 도출 루프) 풀린다.
47
+ - **`M2` 도중발견 비율 낮음** → 누락이 defer로 늦게 잡히기보다 게이트가 **사전에 막고 있다**는 긍정 신호로 읽는다(높으면 반대 — 게이트를 빠져나간 뒤 수습이 잦다는 뜻).
48
+ - **`defer 미해결 > 0` (특히 진행중)** → 착수했지만 미종결인 항목 = 레이더에서 사라지기 쉬운 잔여. 종결/이월 판단이 필요하면 `/gate`(resolve·reopen)를 가리킨다.
49
+
50
+ ## 읽기전용 경계 (중요)
51
+
52
+ 이 스킬은 **절대** 다음을 하지 않는다 — 전부 `/gate`의 영역이다:
53
+ - `gbc defer add/start/resolve/reopen`, `gbc spec add/clear`, `gbc gate reset` (상태 변경)
54
+ - `gbc gate snapshot on/off/clear` (골든셋 mutate), `gbc gate snapshot replay` (judge 재실행·API 과금)
55
+
56
+ 이들이 필요해 보이면 **실행하지 말고** 사용자에게 `/gate`로 안내한다. "보는 곳"과 "바꾸는 곳"을 분리해야, 모니터링을 부작용 걱정 없이 언제든 부를 수 있다.
57
+
58
+ ## Known Pitfalls
59
+
60
+ - **얇은 별칭이 되지 않게 — 해석을 붙인다.** `gbc metrics`를 그냥 대신 실행만 하는 거라면 사용자가 직접 치는 것과 다를 바 없다. 가치는 4개 묶음 + "이게 정상/주의/행동필요 중 뭔지"의 판정이다.
61
+ - **`--all`을 잊지 않는다.** 계측은 기본이 현재 repo만이다. "전체 현황" 맥락에서는 `gbc metrics --all`로 교차repo를 병합해야 그림이 맞는다.
62
+ - **`✗부재`를 stale과 혼동하지 않는다.** `repos list`는 hook *존재 여부*만 본다(명령 freshness=설치경로 의존이라 타 repo에선 판정 불가). "명령이 구버전인지"는 그 repo에서 `gbc status`로 확인하라고 안내한다.