geobuke-code 0.2.3 → 0.2.5

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
@@ -61,6 +61,8 @@ gbc init
61
61
 
62
62
  > ⚠️ **native Windows에선 API 키를 권장한다.** 키 없는 `claude -p` 폴백도 지원하지만(win32에선 `claude.cmd` 실행을 위해 셸 경유, 프롬프트는 인젝션 회피로 stdin 전달, 무응답 방지 kill-timeout), 폴백은 느리므로(~13–20s) 위 키 파일로 API 경로(~1–3s)를 쓰는 편이 빠르고 확실하다.
63
63
 
64
+ > 🔒 **회사·보안통제(EDR/그룹정책) 환경에선 API 키가 사실상 필수다.** native Windows에서 `claude.exe`가 EDR·정책에 막히면(증상: `claude exited 1: 액세스가 거부되었습니다`), 키 없는 `claude -p` 폴백 호출이 매번 실패해 게이트가 **조용히 fail-open**된다 — 게이트가 느려지는 게 아니라 **꺼진다**(`.gbc/failopen.log`에 누적). 이때 `ANTHROPIC_API_KEY`(또는 키 파일)를 설정하면 CLI spawn을 건너뛰고 직접 API로 판정하므로 **막힌 환경에서도 게이트가 살아 있다**. 즉 이 경우 키는 속도가 아니라 **게이트 작동 자체**의 문제다. (깨끗한 native Windows에선 keyless 폴백도 정상 동작함이 검증됨 — 실패는 환경의 CLI 차단에서 비롯된다.)
65
+
64
66
  키 해석 순서: `ANTHROPIC_API_KEY` 환경변수 > `~/.gbc/api-key` 파일.
65
67
 
66
68
  ```bash
@@ -94,11 +96,11 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
94
96
 
95
97
  | 시점 | hook (matcher) | 동작 |
96
98
  |---|---|---|
97
- | **세션 시작·재개** | SessionStart (`startup\|resume`) | `.gbc/defers.json`의 미해결 항목을 표면화(이전 작업 잔여 환기). 잔여 없으면 무출력. `compact`엔 발화 안 함(노이즈 방지) |
99
+ | **세션 시작·재개** | SessionStart (`startup\|resume`) | `.gbc/defers.json`의 미해결 항목을 "진행중 N · 미착수 M"로 구분 표면화(이전 작업 잔여 환기). 잔여 없으면 무출력. `compact`엔 발화 안 함(노이즈 방지) |
98
100
  | **코드 변경 직전** | PreToolUse (`Edit\|Write\|MultiEdit`) | 명세 ↔ 변경 ↔ defer 대조 → 통과(침묵)/차단(시나리오 도출 지시)/fail-open |
99
101
  | **작업단위당 1회** | (PreToolUse 캐시) | 같은 명세 해시 내에선 첫 편집만 판정, 이후 통과 → 매 편집 지연 회피 |
100
102
  | **응답 종료** | Stop | 계측 flush(`events.jsonl`) |
101
- | **업데이트 필요 시** | (PreToolUse·SessionStart·`gbc status`) | hook 구버전(②) 또는 신버전 출시(①)면 갱신 안내. PreToolUse는 세션당 1회(`systemMessage` 비차단), SessionStart는 진입 시 표시. 게이트 통과/차단 동작은 불변 |
103
+ | **업데이트 필요 시** | (PreToolUse·SessionStart) | hook 구버전(②) 또는 신버전 출시(①)면 갱신 안내. PreToolUse는 세션당 1회(`systemMessage` 비차단), SessionStart는 진입 시 표시. `gbc status`는 캐시만 갱신하고 안내는 **표시하지 않는다**(명시 진단 명령). 게이트 통과/차단 동작은 불변 |
102
104
 
103
105
  > 세션 진입 알림만 끄려면 `GBC_NO_SESSION_HINT=1`. 업데이트 안내만 끄려면 `GBC_NO_UPDATE_NOTICE=1`.
104
106
  > 프로젝트 hook이 구식이거나(SessionStart 누락·옛 명령) 새 버전이 나오면 gbc가 감지해 `gbc init --yes` 재실행 또는 `npm i -g geobuke-code@latest`를 안내한다. PreToolUse 경로로도 알리므로 "설치만 하고 init 안 한" 경우에도 도달한다.
@@ -134,9 +136,11 @@ phase-protocol/계획 → /plan(SubTask) → 【게이트: 구현 직전 케이
134
136
  |---|---|
135
137
  | `gbc init` | hook + /gate skill 설치 |
136
138
  | `gbc status` | 게이트 상태 + 로드된 명세 확인 |
137
- | `gbc defer add "<케이스>"` | 케이스를 명시적으로 미루기 |
138
- | `gbc defer list` | 미룬 항목 목록 |
139
- | `gbc defer resolve <번호\|텍스트>` | 미룬 항목 해결 |
139
+ | `gbc defer add "<케이스>"` | 케이스를 명시적으로 미루기 (→ open) |
140
+ | `gbc defer list` | 미룬 항목 목록 (상태: 미해결/진행중/해결) |
141
+ | `gbc defer start <번호\|텍스트\|all>` | 착수 표시 (open → 진행중) |
142
+ | `gbc defer resolve <번호\|텍스트\|all>` | 종결 표시 (→ 해결) |
143
+ | `gbc defer reopen <번호\|텍스트\|all>` | 백로그로 되돌리기 (→ open) |
140
144
  | `gbc spec add "<케이스>"` | 승인된 시나리오를 `.gbc/spec.md`에 등록 |
141
145
  | `gbc spec show` | 등록된 케이스 목록 |
142
146
  | `gbc spec clear` | 명세 비우기(작업단위 종료) |
package/dist/cli.js CHANGED
@@ -7,10 +7,10 @@ import { mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync, } fro
7
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
- import { addDefer, loadDefers, resolveDefer } from "./defer.js";
10
+ import { addDefer, loadDefers, resolveDefer, startDefer, reopenDefer } from "./defer.js";
11
11
  import { selectedTransport } from "./judge.js";
12
12
  import { buildPreCommand, normalizeHooks, ensureSessionStartHook } from "./install.js";
13
- import { isCacheStale, readVersionCache, refreshVersionCache, buildVersionNotice, } from "./version.js";
13
+ import { isCacheStale, readVersionCache, refreshVersionCache, } from "./version.js";
14
14
  import { logEvent, parseEvents, computeMetrics } from "./metrics.js";
15
15
  const CLI_PATH = fileURLToPath(import.meta.url);
16
16
  const PKG_ROOT = join(dirname(CLI_PATH), ".."); // dist/cli.js → 패키지 루트
@@ -170,20 +170,23 @@ async function cmdStatus() {
170
170
  const hash = computeSpecHash(text);
171
171
  const state = loadState(cwd);
172
172
  const defers = loadDefers(cwd);
173
- const unresolved = defers.filter((d) => !d.resolved);
173
+ const unresolved = defers.filter((d) => d.status !== "resolved");
174
+ const inProgress = defers.filter((d) => d.status === "in_progress").length;
174
175
  console.log(`🐢 거북이 게이트 상태 — ${cwd}
175
176
  버전: ${PKG_VERSION || "(불명)"}
176
177
  트랜스포트: ${selectedTransport()}
177
178
  명세 소스: ${source} ${text ? `(${text.length}자)` : "(비어있음 → 모든 코드변경 차단)"}
178
179
  명세 해시: ${hash}
179
180
  작업단위 게이트: ${state && state.specHash === hash && state.gated ? "통과됨(이 단위 재게이트 안 함)" : "미통과(다음 편집에서 발동)"}
180
- defer: 전체 ${defers.length} / 미해결 ${unresolved.length}`);
181
+ defer: 전체 ${defers.length} / 미해결 ${unresolved.length} (진행중 ${inProgress} · 미착수 ${unresolved.length - inProgress})`);
181
182
  if (unresolved.length > 0) {
182
- console.log(unresolved.map((d, i) => ` ${i + 1}. ${d.item}`).join("\n"));
183
+ console.log(unresolved
184
+ .map((d, i) => ` ${i + 1}. ${d.status === "in_progress" ? "▶[진행중] " : ""}${d.item}`)
185
+ .join("\n"));
183
186
  }
184
- const verNotice = buildVersionNotice(PKG_VERSION, readVersionCache());
185
- if (verNotice)
186
- console.log(` ${verNotice}`);
187
+ // 신버전 업데이트 안내(buildVersionNotice)는 여기서 출력하지 않는다 — 안내 자리는
188
+ // SessionStart·PreToolUse 자동 채널 전용이고, status는 명시 진단 명령이라 나그 부적절.
189
+ // (캐시 stale-refresh는 위에서 유지: SessionStart seed 신선도 목적, 표시와 무관.)
187
190
  }
188
191
  // ---------- gbc defer ----------
189
192
  function cmdDefer(args) {
@@ -205,17 +208,32 @@ function cmdDefer(args) {
205
208
  console.log("(미룬 항목 없음)");
206
209
  return;
207
210
  }
208
- defers.forEach((d, i) => console.log(`${i + 1}. [${d.resolved ? "해결" : "미해결"}] ${d.item}`));
209
- }
210
- else if (sub === "resolve") {
211
+ const label = {
212
+ open: "미해결",
213
+ in_progress: "진행중",
214
+ resolved: "해결",
215
+ };
216
+ defers.forEach((d, i) => console.log(`${i + 1}. [${label[d.status]}] ${d.item}`));
217
+ }
218
+ else if (sub === "start" || sub === "resolve" || sub === "reopen") {
211
219
  const ref = args.slice(1).join(" ").trim();
212
- const r = resolveDefer(cwd, ref);
213
- if (r)
214
- logCli(cwd, "defer-resolve", curHash(cwd));
215
- console.log(r ? `🐢 해결 표시: ${r.item}` : `매칭되는 미룬 항목 없음: ${ref}`);
220
+ if (!ref) {
221
+ console.error(`사용: gbc defer ${sub} <번호|텍스트|all>`);
222
+ process.exit(1);
223
+ }
224
+ const fn = sub === "start" ? startDefer : sub === "resolve" ? resolveDefer : reopenDefer;
225
+ const verb = sub === "start" ? "착수" : sub === "resolve" ? "해결" : "되돌림(open)";
226
+ const changed = fn(cwd, ref);
227
+ if (changed.length > 0) {
228
+ logCli(cwd, `defer-${sub}`, curHash(cwd));
229
+ console.log(`🐢 ${verb} ${changed.length}건: ${changed.map((d) => d.item).join(", ")}`);
230
+ }
231
+ else {
232
+ console.log(`매칭되는 항목 없음(0건): ${ref}`);
233
+ }
216
234
  }
217
235
  else {
218
- console.error("사용: gbc defer <add|list|resolve> ...");
236
+ console.error("사용: gbc defer <add|list|start|resolve|reopen> ...");
219
237
  process.exit(1);
220
238
  }
221
239
  }
@@ -296,9 +314,11 @@ function usage() {
296
314
  사용:
297
315
  gbc init [--yes] 프로젝트에 hook + /gate 스킬 설치
298
316
  gbc status 게이트 상태 + 로드된 명세 확인
299
- gbc defer add "<케이스>" 케이스를 명시적으로 미루기
300
- gbc defer list 미룬 항목 목록
301
- gbc defer resolve <번호|텍스트> 미룬 항목 해결
317
+ gbc defer add "<케이스>" 케이스를 명시적으로 미루기 (→ open)
318
+ gbc defer list 미룬 항목 목록 (상태: 미해결/진행중/해결)
319
+ gbc defer start <번호|텍스트|all> 착수 표시 (open → 진행중)
320
+ gbc defer resolve <번호|텍스트|all> 종결 표시 (→ 해결; 항상 사용자 점검 후)
321
+ gbc defer reopen <번호|텍스트|all> 백로그로 되돌리기 (→ open)
302
322
  gbc spec add "<케이스>" 승인된 시나리오를 .gbc/spec.md에 등록
303
323
  gbc spec show 등록된 케이스 목록
304
324
  gbc spec clear 명세 비우기(작업단위 종료)
package/dist/defer.js CHANGED
@@ -12,44 +12,93 @@ function nowIso() {
12
12
  return "";
13
13
  }
14
14
  }
15
+ /**
16
+ * 원시 엔트리를 status 단일 소스로 정규화한다(마이그레이션).
17
+ * 옛 0.2.4 이하 포맷 {resolved:boolean}을 읽을 때 status로 승격:
18
+ * resolved:true→"resolved", false/부재→"open". 이미 status가 있으면 그대로.
19
+ */
20
+ function promote(raw) {
21
+ const status = raw.status ?? (raw.resolved === true ? "resolved" : "open");
22
+ return { item: raw.item, at: raw.at, status };
23
+ }
24
+ /** 디스크에서 defer 엔트리를 읽어 status 포맷으로 정규화 반환(읽기 시 자동 승격) */
15
25
  export function loadDefers(cwd) {
16
- return readJson(deferPath(cwd), []);
26
+ return readJson(deferPath(cwd), []).map(promote);
17
27
  }
28
+ /** 저장은 항상 status 포맷으로 통일 — promote가 옛 resolved 필드를 떨궈 단일 소스 보장(drift 방지) */
18
29
  function save(cwd, defers) {
19
30
  writeJson(deferPath(cwd), defers);
20
31
  }
21
32
  /** 명시적으로 항목을 미룬다 (침묵 누락 차단의 유일한 정당 경로) */
22
33
  export function addDefer(cwd, item) {
23
34
  const defers = loadDefers(cwd);
24
- const entry = { item: normalizeCase(item), at: nowIso(), resolved: false };
35
+ const entry = { item: normalizeCase(item), at: nowIso(), status: "open" };
25
36
  defers.push(entry);
26
37
  save(cwd, defers);
27
38
  return entry;
28
39
  }
29
- /** 미해결 defer 항목 텍스트만 (게이트 판정 입력용) */
40
+ /**
41
+ * 미해결(=resolved 아님) defer 항목 텍스트만 (게이트 판정 입력용).
42
+ * gate-neutral: open + in_progress 모두 '아직 안 끝난 의도적 미룸'으로 judge에 전달 → 차단 로직 무변경.
43
+ */
30
44
  export function activeDeferItems(cwd) {
31
45
  return loadDefers(cwd)
32
- .filter((d) => !d.resolved)
46
+ .filter((d) => d.status !== "resolved")
33
47
  .map((d) => d.item);
34
48
  }
35
- /** 미해결 defer 엔트리 (Stop hook 리마인드용) */
49
+ /** 미해결(open+in_progress) defer 엔트리 (Stop hook·SessionStart 리마인드용) */
36
50
  export function unresolvedDefers(cwd) {
37
- return loadDefers(cwd).filter((d) => !d.resolved);
51
+ return loadDefers(cwd).filter((d) => d.status !== "resolved");
38
52
  }
39
- /** 인덱스(1-base) 또는 부분 텍스트 매칭으로 defer 해결 표시 */
40
- export function resolveDefer(cwd, ref) {
41
- const defers = loadDefers(cwd);
42
- let target;
43
- const idx = Number.parseInt(ref, 10);
44
- if (!Number.isNaN(idx) && idx >= 1 && idx <= defers.length) {
45
- target = defers[idx - 1];
53
+ /**
54
+ * ref 문자열로 전환 대상 엔트리를 고른다. 세 형태 지원:
55
+ * - "all": eligibleFrom 상태에 해당하는 전부
56
+ * - 공백구분 토큰이 전부 정수: 복수 인덱스(1-base). 인덱스는 명시 지정이라 적격 무시(사용자가 번호를 안다)
57
+ * - 그 외: 통째로 부분 텍스트 1건 매칭(적격 항목 중) — 공백 포함 문구 하위호환
58
+ */
59
+ function selectTargets(defers, ref, eligibleFrom) {
60
+ const trimmed = ref.trim();
61
+ // 빈 ref 가드: includes("")는 항상 첫 항목을 매칭하므로 빈 문자열이 엉뚱한 항목을 고른다.
62
+ // CLI(cli.ts)는 이미 빈 ref를 사전 차단하지만, selectTargets가 라이브러리로 직접 호출될 때를 위한 방어.
63
+ if (trimmed === "")
64
+ return [];
65
+ const eligible = (d) => eligibleFrom.includes(d.status);
66
+ if (trimmed === "all")
67
+ return defers.filter(eligible);
68
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
69
+ const allInts = tokens.length > 0 && tokens.every((t) => /^\d+$/.test(t));
70
+ if (allInts) {
71
+ const out = [];
72
+ for (const t of tokens) {
73
+ const idx = Number.parseInt(t, 10);
74
+ if (idx >= 1 && idx <= defers.length && !out.includes(defers[idx - 1])) {
75
+ out.push(defers[idx - 1]);
76
+ }
77
+ }
78
+ return out;
46
79
  }
47
- else {
48
- target = defers.find((d) => !d.resolved && d.item.includes(ref));
49
- }
50
- if (!target)
51
- return null;
52
- target.resolved = true;
53
- save(cwd, defers);
54
- return target;
80
+ const t = defers.find((d) => eligible(d) && d.item.includes(trimmed));
81
+ return t ? [t] : [];
82
+ }
83
+ /** 선택된 대상을 toStatus로 전환하고 저장. 전환된 엔트리 배열 반환(매칭 0건이면 빈 배열). */
84
+ function transition(cwd, ref, toStatus, eligibleFrom) {
85
+ const defers = loadDefers(cwd);
86
+ const targets = selectTargets(defers, ref, eligibleFrom);
87
+ for (const t of targets)
88
+ t.status = toStatus;
89
+ if (targets.length > 0)
90
+ save(cwd, defers);
91
+ return targets;
92
+ }
93
+ /** open → in_progress (착수). 텍스트/all 적격 = open. 인덱스는 명시 지정. */
94
+ export function startDefer(cwd, ref) {
95
+ return transition(cwd, ref, "in_progress", ["open"]);
96
+ }
97
+ /** → resolved (종결, 항상 사람 선언). 텍스트/all 적격 = open + in_progress. */
98
+ export function resolveDefer(cwd, ref) {
99
+ return transition(cwd, ref, "resolved", ["open", "in_progress"]);
100
+ }
101
+ /** → open (백로그로 되돌리기: 보류/이월 또는 잘못된 resolve 취소). 텍스트/all 적격 = in_progress + resolved. */
102
+ export function reopenDefer(cwd, ref) {
103
+ return transition(cwd, ref, "open", ["in_progress", "resolved"]);
55
104
  }
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, unresolvedDefers, loadDefers } from "./defer.js";
7
+ import { activeDeferItems, loadDefers } from "./defer.js";
8
8
  import { readProjectSettings, buildUpdateNotice, wasNotified, markNotified } from "./notice.js";
9
9
  import { isCacheStale, readVersionCache, refreshVersionCache } from "./version.js";
10
10
  import { appendFileSync } from "node:fs";
@@ -236,28 +236,62 @@ export async function runStop() {
236
236
  // defers.json 없으면(파일 부재) 조용히 통과
237
237
  if (loadDefers(cwd).length === 0)
238
238
  process.exit(0);
239
- const un = unresolvedDefers(cwd);
240
- if (un.length === 0)
239
+ const all = loadDefers(cwd);
240
+ if (all.filter((d) => d.status !== "resolved").length === 0)
241
241
  process.exit(0);
242
- const items = un.map((d, i) => `${i + 1}. ${d.item}`).join("\n");
243
- emit({
244
- decision: "block",
245
- reason: `🐢 미해결 defer ${un.length}건이 남아 있습니다:\n${items}\n` +
246
- `해결했으면 'gbc defer resolve <번호>', 다음 세션으로 이월할 거면 의식적으로 확인하세요. ` +
247
- `(이 리마인드는 1회만 표시됩니다.)`,
248
- });
242
+ emit({ decision: "block", reason: buildStopReminder(all) });
249
243
  process.exit(0);
250
244
  }
245
+ /**
246
+ * defer 전환 행동 규약 — SessionStart/Stop 알림 문자열에 임베드한다.
247
+ * 규약 발화 자리가 hint 문자열인 이유: SKILL.md는 skill 실행 시점에만 읽혀 자유 편집·대화 중엔
248
+ * dead doc이 된다. 매 세션 컨텍스트에 신뢰성 있게 규약을 주입하는 유일한 결정론 채널이 이 문자열이다.
249
+ * (hook엔 추론을 넣지 않는다 — 텍스트만. 자연어/대상 감지·전환 실행은 에이전트 측 책임.)
250
+ */
251
+ const DEFER_PROTOCOL = "규약 — 항목 착수 시 'gbc defer start <ref>'로 진행중 표시, 사용자가 완료를 명시하면 'gbc defer resolve <ref>'로 종결(신호가 모호하면 resolve하지 말고 확인). 되돌리기는 'gbc defer reopen <ref>'. ref=번호|텍스트|all. 모든 자동 전환은 사용자에게 표면화.";
252
+ /**
253
+ * 전체 defer 리스트에서 미해결(open+in_progress)만 골라 상태 마커와 함께 한 줄씩 포맷한다.
254
+ * ★ 번호는 전체-리스트 위치(인덱스+1)로 매긴다 — `gbc defer list`·`gbc defer <N>` 인덱스 ref와 동일.
255
+ * 부분집합 번호를 쓰면 resolved가 앞에 있을 때 표시 번호 ≠ 실제 인덱스가 되어 엉뚱한 항목을 친다.
256
+ */
257
+ function formatDeferList(all) {
258
+ return all
259
+ .map((d, i) => ({ d, n: i + 1 }))
260
+ .filter((x) => x.d.status !== "resolved")
261
+ .map((x) => `${x.n}. ${x.d.status === "in_progress" ? "▶[진행중]" : "[미착수]"} ${x.d.item}`)
262
+ .join("\n");
263
+ }
264
+ /** 미해결 건수를 진행중/미착수로 분해한 머리말 조각 */
265
+ function statusBreakdown(unresolved) {
266
+ const inProgress = unresolved.filter((d) => d.status === "in_progress").length;
267
+ return `진행중 ${inProgress} · 미착수 ${unresolved.length - inProgress}`;
268
+ }
251
269
  /**
252
270
  * 세션 진입(startup|resume) 시 미해결 defer 잔여를 표면화하는 알림 문자열. 없으면 "".
271
+ * 입력은 전체 defer 리스트(loadDefers) — 표시 번호를 전체-인덱스로 맞추기 위함(인덱스 ref 정합).
253
272
  * gbc 자기 소유 데이터(.gbc/defers.json)만 사용 — scratch/메모리 미접근(다른 하네스와 혼재·환각 방지).
273
+ * in_progress를 open과 구분 표면화("진행중 N · 미착수 M") — 착수했지만 미종결 항목이 잊히지 않게.
274
+ */
275
+ export function buildSessionStartHint(all) {
276
+ const unresolved = all.filter((d) => d.status !== "resolved");
277
+ if (unresolved.length === 0)
278
+ return "";
279
+ return (`🐢 거북이 게이트 — 미해결 defer ${unresolved.length}건 (${statusBreakdown(unresolved)}, 이전 작업 잔여):\n` +
280
+ `${formatDeferList(all)}\n` +
281
+ `필요하면 사용자에게 이어서 처리할지 확인하세요. ${DEFER_PROTOCOL}`);
282
+ }
283
+ /**
284
+ * Stop hook 리마인드 문자열. 없으면 "". 입력은 전체 defer 리스트(번호=전체-인덱스, 인덱스 ref 정합).
285
+ * SessionStart와 동일하게 in_progress를 차등 표면화한다 — "착수했지만 미종결" 항목이 레이더에서
286
+ * 사라지지 않게(resolve가 리마인드에서 항목을 떨구는 harm 완화).
254
287
  */
255
- export function buildSessionStartHint(unresolved) {
288
+ export function buildStopReminder(all) {
289
+ const unresolved = all.filter((d) => d.status !== "resolved");
256
290
  if (unresolved.length === 0)
257
291
  return "";
258
- const items = unresolved.map((d, i) => `${i + 1}. ${d.item}`).join("\n");
259
- return (`🐢 거북이 게이트 — 미해결 defer ${unresolved.length}건 (이전 작업 잔여):\n${items}\n` +
260
- `필요하면 사용자에게 이어서 처리할지 확인하세요. 해결은 'gbc defer resolve <번호>'.`);
292
+ return (`🐢 미해결 defer ${unresolved.length}건이 남아 있습니다 (${statusBreakdown(unresolved)}):\n` +
293
+ `${formatDeferList(all)}\n` +
294
+ `${DEFER_PROTOCOL} 다음 세션으로 이월할 거면 의식적으로 확인하세요. (이 리마인드는 1회만 표시됩니다.)`);
261
295
  }
262
296
  /**
263
297
  * SessionStart: 세션 진입 시 미해결 defer를 stdout(plain text)으로 표면화 → Claude 컨텍스트 주입.
@@ -276,7 +310,7 @@ export async function runSessionStart(ctx) {
276
310
  const parts = [];
277
311
  // 미해결 defer 알림(GBC_NO_SESSION_HINT로 opt-out — 기존 동작 보존).
278
312
  if (process.env.GBC_NO_SESSION_HINT !== "1") {
279
- const hint = buildSessionStartHint(unresolvedDefers(cwd));
313
+ const hint = buildSessionStartHint(loadDefers(cwd));
280
314
  if (hint)
281
315
  parts.push(hint);
282
316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geobuke-code",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "거북이코드 — 구현 직전 강제 게이트. Claude Code PreToolUse hook으로 코드 변경 전 계획 케이스 누락·시나리오 미지정을 차단한다.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,24 @@ description: 거북이코드 구현-전 게이트를 관리한다. 로드된 계
12
12
  - **미루기는 명시 등록만 허용한다.** "추후작업"이라고 머릿속/주석으로만 미루면 게이트가 침묵 누락으로 차단한다. 정당한 미루기는 반드시 `gbc defer add`로 등록해야 통과된다. (= 통증 "추후작업 미루다 누락" 직격)
13
13
  - **게이트는 완전구현을 요구하지 않는다.** 케이스가 다뤄지기 시작했거나 명시 defer되면 통과. 침묵 누락과 시나리오 미지정만 막는다.
14
14
 
15
+ ## defer 수명주기 — 자연어로 전환한다 (사용자가 명령을 직접 칠 필요 없음)
16
+
17
+ defer 항목은 **open(미착수) → in_progress(진행중) → resolved(해결)** 3상태를 갖는다. 대부분의 경우 사용자는 `gbc defer …`를 직접 입력하지 않는다 — **에이전트가 대화(자연어)와 편집 대상을 감지해 백그라운드에서 전환을 실행**하고, 사용자에게 표면화한다. (명령은 수동 보정용으로 항상 사용 가능.)
18
+
19
+ | 전환 | 트리거(감지) | 에이전트 행동 |
20
+ |---|---|---|
21
+ | **start** (open→진행중) | 그 defer 항목을 **실제로 착수**할 때(NL "이거 할게" 또는 해당 코드 편집 시작) | `gbc defer start <ref>` 자동 실행 + 표면화. 보수적으로 — 실제 착수할 때만(투기적 표시 금지). |
22
+ | **resolve** (→해결) | **사용자의 명시적 완료 선언**("X 끝났어", "점검 OK") | 명확하면 `gbc defer resolve <ref>` 실행 + **반드시 표면화**. |
23
+ | **reopen** (→open) | 사용자가 보류/이월/잘못된 resolve 취소를 요청 | `gbc defer reopen <ref>` 실행 + 표면화. |
24
+
25
+ **resolve 모호성 규칙 (load-bearing — 미완성 항목이 조용히 잊히는 harm 차단):**
26
+ - **명확한 완료 선언**("로그인 검증 끝냈어") → 자동 resolve + 표면화.
27
+ - **모호한 신호**("다음으로 넘어가자", "대충 됐어") → **resolve하지 말고 사용자에게 확인**한다. resolve된 항목은 리마인드/SessionStart에서 사라지므로, 잘못 resolve하면 미완성인 채 잊힌다.
28
+ - resolve는 **절대 게이트(judge)가 편집을 보고 추론하지 않는다** — 항상 사람의 명시 선언이 트리거. (start만 편집 감지로 자동.)
29
+ - 모든 자동 전환은 **사용자에게 표면화**해 catch·reopen할 수 있게 한다.
30
+
31
+ > 이상적 흐름: defer 확인 → (특정/전체 항목) start → 구현 → **사용자 점검** → resolve. 세션 내 완전 해소 안 되는 항목은 in_progress로 이월되고, SessionStart가 "진행중 N · 미착수 M"으로 구분 표면화한다.
32
+
15
33
  ## 명령 (bash로 실행)
16
34
 
17
35
  게이트는 현재 프로젝트 루트의 `gbc`를 사용한다. 작업 디렉토리에서 실행:
@@ -19,9 +37,11 @@ description: 거북이코드 구현-전 게이트를 관리한다. 로드된 계
19
37
  | 의도 | 명령 |
20
38
  |---|---|
21
39
  | 게이트 상태·로드된 명세 확인 | `gbc status` |
22
- | 미룬 항목 목록 | `gbc defer list` |
23
- | 케이스를 명시적으로 미루기 | `gbc defer add "<케이스 설명>"` |
24
- | 미룬 항목 해결 표시 | `gbc defer resolve <번호 또는 텍스트>` |
40
+ | 미룬 항목 목록(상태: 미해결/진행중/해결) | `gbc defer list` |
41
+ | 케이스를 명시적으로 미루기 (→ open) | `gbc defer add "<케이스 설명>"` |
42
+ | 착수 표시 (open 진행중) | `gbc defer start <번호\|텍스트\|all>` |
43
+ | 종결 표시 (→ 해결) | `gbc defer resolve <번호\|텍스트\|all>` |
44
+ | 백로그로 되돌리기 (→ open) | `gbc defer reopen <번호\|텍스트\|all>` |
25
45
  | 승인된 시나리오를 명세에 등록 | `gbc spec add "<케이스>"` |
26
46
  | 등록된 케이스 목록 | `gbc spec show` |
27
47
  | 명세 비우기(작업단위 종료) | `gbc spec clear` |
@@ -38,7 +58,7 @@ description: 거북이코드 구현-전 게이트를 관리한다. 로드된 계
38
58
  3. 승인된 케이스를 `gbc spec add "<케이스>"`로 등록하거나 `.gbc/spec.md`에 직접 작성한다.
39
59
  4. 재시도하면 통과한다.
40
60
  > 시나리오 도출은 코딩 에이전트 본체(Opus)가 대화 맥락으로, 게이트 판정은 haiku가 — 두 작업/두 모델 분리(gbc는 모델 계층을 소유하지 않는다).
41
- 3. **세션 종료 시**: Stop hook이 미해결 defer를 리마인드한다. `gbc defer list`로 확인하고 해결하거나 다음 세션으로 의식적으로 이월한다.
61
+ 3. **세션 종료 시**: Stop hook이 미해결 defer를 "진행중 N · 미착수 M"으로 구분 리마인드한다. `gbc defer list`로 확인하고, 사용자 완료 선언이 있었으면 resolve, 아니면 다음 세션으로 의식적으로 이월한다(진행중 항목은 in_progress 그대로 남아 다음 SessionStart에 표면화).
42
62
 
43
63
  ## 명세 소스
44
64