@tuzi-ince/hi-loop 0.3.1 → 0.3.3

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
@@ -33,9 +33,12 @@ npm install -g @tuzi-ince/hi-loop
33
33
 
34
34
  # 2) 프로젝트에 주입 (멱등 — 여러 번 돌려도 안전)
35
35
  cd /path/to/my-project
36
- hi-loop-setup # .mcp.json / .gitignore / docs / tests 자동 주입
36
+ hi-loop-setup # .mcp.json / .gitignore / docs·tests / CLAUDE.md / docs/design.md 자동 주입
37
37
  ```
38
38
 
39
+ `hi-loop-setup` 은 **표준 설계 문서 `docs/design.md`** 를 함께 만든다(있으면 보존). 이 문서를 채우면
40
+ 문서↔소스 정합 장치(`--reconcile-spec` / `--spec`)의 오라클이 된다 — 아래 [문서↔소스 정합](#문서를-상시-오라클로--표준-문서-고정---spec) 참조.
41
+
39
42
  소스에서 개발용으로 설치하려면:
40
43
 
41
44
  ```bash
@@ -129,12 +132,15 @@ goal만 바꿔 여러 번 돌려도 앞의 산출물이 살아남는다.
129
132
  ```bash
130
133
  hi-loop run --goal "결제 모듈의 중복 검증 로직을 하나로 합쳐라" \
131
134
  --test "npm test" \
135
+ --spec docs/spec.md \
132
136
  --verify-spec
133
137
  ```
134
138
 
135
139
  - 기존 `npm test` 가 회귀 방지선이다 — 개선하다 무언가 깨면 Tier 1에서 걸린다.
136
140
  - `--verify-spec` 은 테스트 통과 **후** 별도 검증자가 스펙 대비 구현을 심판한다.
137
141
  구조가 아니라 **의도**를 본다("리팩터링했다"는데 동작이 달라졌으면 기각).
142
+ - `--spec docs/spec.md` 는 **사람이 관리하는 표준 문서를 오라클로 고정**한다 — 단말적 개선
143
+ 요청이 문서와 어긋나는 것을 잡는다(→ [문서↔소스 정합](#문서를-상시-오라클로--표준-문서-고정---spec)).
138
144
  - 개선 중 락파일·설정·마이그레이션을 건드리면 보고서 Gaps에 ⚠️로 공개된다
139
145
  (→ [영향도 분석](#4-영향도-분석--변경-위험-파악)).
140
146
 
@@ -402,6 +408,67 @@ false green을 disclosed green으로 바꾼다.
402
408
 
403
409
  구조가 아니라 의도를 본다. 비용이 늘어 opt-in이다.
404
410
 
411
+ ### 문서를 상시 오라클로 — 표준 문서 고정 (`--spec`)
412
+
413
+ 구현하다 보면 단말적 요청들이 작성된 문서와 상이하게 들어와 **문서↔소스 갭**이 생기고,
414
+ 그 갭이 환각의 빌미가 된다. 기본 동작은 goal마다 스펙을 새로 만들어(goal 해시 분기) 이 갭을
415
+ **막지 못한다** — 스펙 파일만 여러 개로 늘 뿐이다. `--spec <path>` 가 이 구멍을 닫는다.
416
+
417
+ ```bash
418
+ hi-loop run --goal "..." --spec docs/spec.md --verify-spec
419
+ ```
420
+
421
+ - 스펙 오라클을 이 경로로 **고정**한다. DESIGN_REVIEW·CODE_REVIEW·`--verify-spec` 이 모두
422
+ `state.specPath` 하나에서 읽으므로, 셋 다 이 표준 문서를 기준으로 심판한다.
423
+ - goal이 달라도 **같은 문서를 오라클로 공유**한다 — 새 요청이 문서와 어긋나면 오라클이 잡는다.
424
+ - 고정한 문서에 **이미 내용이 있으면 PLAN이 덮어쓰지 않는다.** 사람이 관리하는 표준 문서를
425
+ 진실로 받아들이고, 그에 맞춰 테스트만 쓴다. 비어 있을 때만 스펙을 작성한다.
426
+
427
+ > 기본(자기서술 스펙)은 "에이전트가 자기가 쓴 테스트를 통과"일 뿐이다(Gaps가 이를 공개한다).
428
+ > `--spec`으로 **외부 오라클**을 주면 그 한계를 좁힌다.
429
+
430
+ ### 문서와 어긋나는 요청을 조용히 넘기지 않는다 (`--reconcile`)
431
+
432
+ `--spec`이 "이 문서를 오라클로 써라"라면, `--reconcile`은 **"이 요청이 기존 문서와 모순되는지
433
+ 먼저 확인하라"**다. 표준 문서를 고정하지 않아도, 요청이 문서를 배신하는 순간을 잡는다.
434
+
435
+ ```bash
436
+ hi-loop run --goal "결제에서 음수 잔액도 허용하라" --reconcile # --full 이면 자동 켜짐
437
+ ```
438
+
439
+ - 기존 표준 문서가 있고 이번 요청이 그것과 별개 스펙으로 **포크될 참이면**, 포크 직전에
440
+ 별도 판정자(plan 모드, 새 세션)가 goal과 표준 문서를 대조한다.
441
+ - **대조 대상 우선순위**: `--reconcile-spec <path>`(명시) → `docs/spec.md`가 아니라 **`docs/design.md`
442
+ (setup이 만드는 사람 관리 표준 문서)** → 없으면 `docs/spec.md` 폴백. 즉 `hi-loop-setup`을 했다면
443
+ `--full`/`--reconcile`만으로 별도 지정 없이 **design.md가 자동으로 오라클이 된다.** (spec.md는
444
+ PLAN이 쓴 에이전트 산출물이라 오라클로 약하다 — 사람 문서가 있으면 그쪽이 옳다.)
445
+ - **모순이면 멈추고 사람에게 묻는다**(상호배타 3지선다): **(a)** 표준 문서를 오라클로 고정하고
446
+ 진행 / **(b)** 새 스펙으로 분기(요청이 문서를 갱신) / **(c)** 중단하고 문서를 먼저 정리.
447
+ - 모순이 없으면(또는 표준 문서가 없으면) 판정자를 부르지도 않는다 — 비용 0. 판정 실패는
448
+ 통과로 흘린다(애매한 것까지 막으면 정상 작업이 멈춘다). opt-in.
449
+
450
+ > 이것이 "단말적 요청이 문서와 상이하게 들어와 갭이 벌어지는" 상황을 **엔진 차원에서** 막는
451
+ > 장치다. `--spec`(고정)과 `--reconcile`(감지)은 짝으로 쓰면 문서↔소스 정합이 가장 단단하다.
452
+
453
+ ### 에이전트를 못 부르면 스택이 아니라 지침을 준다 (preflight)
454
+
455
+ 에이전트(claude) 미설치·인증 만료·API 키 부재는 이 엔진의 버그가 아니라 환경 문제다.
456
+ 루프 한복판의 스택트레이스가 아니라 **행동지침 한 줄**로 바꾼다.
457
+
458
+ - 에이전트를 부르는 명령은 루프 진입 전 `claude --version`(비용 0)으로 실행 가능성을 확인한다.
459
+ 미설치(ENOENT)·권한(EACCES)이면 회차를 태우지 않고 멈추며 설치·`HILOOP_AGENT_CMD` 안내를 준다.
460
+ - 인증/구독/키 문제는 `--version`으론 알 수 없다(비용을 안 들인다). 첫 호출 stderr를 패턴으로
461
+ 읽어 힌트로 번역하고, 못 알아본 실패는 원문을 그대로 보여준다(감추지 않는다).
462
+ - 환경 오류는 CLI에서 **스택 없이 메시지만**, MCP에선 접두 없이 그대로 전달한다. 진짜 버그만
463
+ 스택을 노출한다.
464
+
465
+ ### MCP는 호스트가 정한 프로젝트를 앵커로 삼는다
466
+
467
+ MCP 도구의 `cwd` 기본값은 `CLAUDE_PROJECT_DIR || process.cwd()` 다 — 호스트(클로드코드/커서)가
468
+ "어느 프로젝트인가"로 주입하는 값을 따른다. 호출 시 `cwd`를 명시하면 그것이 최우선. (CLI는
469
+ 반대로 순수 `process.cwd()` — 터미널에서 cd한 곳이 사용자의 명시 의도이고, 통합 터미널에
470
+ 새어든 env가 그것을 덮으면 하위 프로젝트 겨냥이 깨지기 때문이다.)
471
+
405
472
  ### 💸 비용은 엔진이 센다 — 그리고 싸지 않다
406
473
 
407
474
  **`--max-loops` 는 비용 상한이 아니다.** 실패 루프가 10회를 다 쓰면 비용이 크게 뛸 수 있다.
@@ -425,6 +492,7 @@ hi-loop run --goal "..." --max-loops 5 --budget-usd 1 --no-stagnation
425
492
  | `HILOOP_VERIFY_CMD` / `HILOOP_VERIFY_MODEL` | `--verify-spec` 검증자의 실행 명령·모델 (미설정 시 구현자와 동일). 검증에 더 센 모델을 쓸 수 있다 |
426
493
  | `HILOOP_REVIEW_CMD` / `HILOOP_REVIEW_MODEL` | 설계·코드 리뷰어의 실행 명령·모델 (미설정 시 `HILOOP_VERIFY_*` → 구현자 순으로 폴백) |
427
494
  | `HILOOP_DISCOVER_CMD` / `HILOOP_DISCOVER_MODEL` | 발굴 단계의 실행 명령·모델 |
495
+ | `HILOOP_RECONCILE_CMD` / `HILOOP_RECONCILE_MODEL` | 문서 정합 판정자(`--reconcile`)의 실행 명령·모델 (미설정 시 `HILOOP_VERIFY_*` → 구현자 순 폴백) |
428
496
  | `HILOOP_AGENT_TIMEOUT_MS` / `HILOOP_TEST_TIMEOUT_MS` | 에이전트(30분)·테스트(10분) 타임아웃. 쓰레기값은 기본값으로 되돌림 |
429
497
  | `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | 시작/핸드오프/성공/실패 알림. 미설정 시 조용히 생략 |
430
498
 
@@ -454,6 +522,10 @@ src/report.js 완료 보고 — Gaps 공개 + 상태 요약
454
522
  src/cli-options.js CLI 인자 → runLoop 옵션 번역 (잘못된 값은 조용히 흘리지 않는다)
455
523
  src/state.js .agent-state.json (원자적 저장 + 다이어트 + 산출물 경로 + resume + Gaps)
456
524
  src/runners.js 에이전트/테스트 실행 어댑터 (주입 가능) + 비용 파싱 + 타임아웃
525
+ src/preflight.js 에이전트 실행 preflight + 실패 진단 (미설치·인증·키 → 행동지침)
526
+ src/reconcile.js 문서 정합 게이트 — 요청↔표준 문서 모순 판정 + ask 표면화 (FR-21)
527
+ src/cli-commands.js CLI 보조 명령 — answer / rollback / config (bin 은 라우팅만)
528
+ src/stage-context.js stage 핸들러 실행 맥락 조립
457
529
  src/prompts.js PDCA 단계별 프롬프트 + 검증자 프롬프트
458
530
  src/integrity.js 테스트 무결성 지문·비교 — 보상 해킹 방어
459
531
  src/checkpoint.js git stash 체크포인트·롤백 + 삭제 관측
package/bin/hi-loop.js CHANGED
@@ -27,11 +27,12 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
27
27
 
28
28
  사용법:
29
29
  hi-loop run --goal "<요구사항>" [--test "npm test"] [--max-loops 10] [--budget-usd 5]
30
- [--stagnation 3 | --no-stagnation] [--verify-spec] [--cwd .]
30
+ [--stagnation 3 | --no-stagnation] [--verify-spec] [--spec docs/spec.md] [--cwd .]
31
31
  [--ship "<배포 명령>"] [--on-ship-fail stop|heal]
32
32
  [--review | --no-review] [--max-review-rounds 2]
33
33
  [--design-review | --no-design-review] [--max-design-rounds 2]
34
34
  [--full] [--discover | --no-discover]
35
+ [--reconcile | --no-reconcile] [--reconcile-spec docs/spec.md]
35
36
  [--watch "<헬스체크 명령>"] [--watch-for 5m] [--watch-every 30s]
36
37
  [--watch-tolerate 1] [--on-watch-fail stop|rollback|heal]
37
38
  [--branch [name]] [--commit] [--commit-message "..."]
@@ -79,7 +80,7 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
79
80
  export async function main(argv = process.argv.slice(2)) {
80
81
  const args = parseArgs(argv, {
81
82
  alias: { g: 'goal', t: 'test', c: 'cwd', h: 'help', v: 'version', m: 'max-loops' },
82
- boolean: ['help', 'version', 'dry-run', 'no-stagnation', 'verify-spec', 'flaky-probe', 'yes', 'review', 'no-review', 'design-review', 'no-design-review', 'full', 'discover', 'no-discover', 'commit'],
83
+ boolean: ['help', 'version', 'dry-run', 'no-stagnation', 'verify-spec', 'flaky-probe', 'yes', 'review', 'no-review', 'design-review', 'no-design-review', 'full', 'discover', 'no-discover', 'reconcile', 'no-reconcile', 'commit'],
83
84
  // 개수가 정해지지 않은 입력. `--check A --when X --check B` 처럼 위치로 짝을 맺는다.
84
85
  repeat: ['check', 'when'],
85
86
  });
@@ -144,6 +145,18 @@ export async function main(argv = process.argv.slice(2)) {
144
145
  if (options.branch == null && config.branchPolicy === 'always') options.branch = true;
145
146
  if (!options.commit && config.commitPolicy === 'auto') options.commit = true;
146
147
 
148
+ // 에이전트를 실제로 부르는 명령이면, 루프 진입 전에 실행 가능한지 확인한다.
149
+ // "claude 가 없다/실행 안 된다"를 첫 회차의 스택트레이스가 아니라 여기서 한 줄로 잡는다.
150
+ // ship/watch 는 에이전트를 부르지 않으므로 건너뛴다(불필요한 spawn 방지).
151
+ if (['run', 'discover', 'plan', 'review'].includes(command)) {
152
+ const { preflightAgent } = await import('../src/preflight.js');
153
+ const pre = await preflightAgent();
154
+ if (!pre.ok) {
155
+ process.stderr.write(`[hi-loop] ${pre.message}\n`);
156
+ return 1;
157
+ }
158
+ }
159
+
147
160
  const { runLoop } = await import('../src/loop.js');
148
161
  const result = await runLoop({
149
162
  goal,
@@ -168,63 +181,13 @@ export async function main(argv = process.argv.slice(2)) {
168
181
  process.stdout.write(`${summarizeState(loadState(statePathFor(cwd)))}\n`);
169
182
  return 0;
170
183
  }
171
- case 'answer': {
172
- const { loadState, saveState, statePathFor } = await import('../src/state.js');
173
- const { applyAnswer } = await import('../src/ask.js');
174
- const statePath = statePathFor(cwd);
175
- const state = loadState(statePath);
176
- if (!state?.ask) {
177
- process.stderr.write('대기 중인 질문이 없습니다.\n');
178
- return 1;
179
- }
180
- const note = args.note && args.note !== true ? String(args.note) : '';
181
- const res = applyAnswer(state, { choice: args._[1], note });
182
- if (!res.ok) {
183
- process.stderr.write(`${res.reason}\n`);
184
- return 2;
185
- }
186
- saveState(statePath, res.state);
187
- const last = res.state.assumptions[res.state.assumptions.length - 1];
188
- process.stdout.write(`✅ 기록했습니다: ${last.text}\n 이어서 진행하려면 같은 goal 로 hi-loop run 을 다시 실행하세요.\n`);
189
- return 0;
190
- }
191
- case 'rollback': {
192
- const { loadState, statePathFor } = await import('../src/state.js');
193
- const { rollbackTo } = await import('../src/checkpoint.js');
194
- const state = loadState(statePathFor(cwd));
195
- const checkpoints = state?.checkpoints ?? [];
196
- if (checkpoints.length === 0) {
197
- process.stderr.write('되돌릴 체크포인트가 없습니다 (git 저장소가 아니거나 아직 스냅샷이 없음).\n');
198
- return 1;
199
- }
200
- // --to N 이면 N회차 직전 체크포인트, 없으면 가장 최근.
201
- const toIter = args.to !== undefined && args.to !== true ? Number(args.to) : null;
202
- const cp =
203
- toIter != null
204
- ? checkpoints.find((c) => c.iteration === toIter)
205
- : checkpoints[checkpoints.length - 1];
206
- if (!cp) {
207
- process.stderr.write(`${toIter}회차 체크포인트가 없습니다. 있는 회차: ${checkpoints.map((c) => c.iteration).join(', ')}\n`);
208
- return 1;
209
- }
210
- const res = await rollbackTo({ cwd, sha: cp.sha });
211
- if (!res.ok) {
212
- process.stderr.write(`롤백 실패: ${res.reason}\n`);
213
- return 1;
214
- }
215
- process.stdout.write(`✅ ${cp.iteration}회차 직전 상태로 추적 파일을 복원했습니다 (${cp.sha.slice(0, 8)}).\n`);
216
- // 되돌리지 못한 것을 되돌렸다고 말하지 않는다. git 은 추적 밖 파일을 트리 연산으로
217
- // 지우지 않고, `git clean` 은 사용자의 정상 파일까지 지운다 — 그래서 판단은 사람에게 준다.
218
- if (res.untrackedRemain?.length) {
219
- process.stdout.write(
220
- `⚠️ 추적되지 않는 파일 ${res.untrackedRemain.length}개는 그대로 남아 있습니다(에이전트가 새로 만든 것일 수 있습니다):\n` +
221
- res.untrackedRemain.slice(0, 10).map((f) => ` - ${f}\n`).join('') +
222
- (res.untrackedRemain.length > 10 ? ` … 외 ${res.untrackedRemain.length - 10}개\n` : '') +
223
- ' 필요하면 직접 지우세요. hi-loop 은 사용자의 정상 파일을 지울 수 없어 판단하지 않습니다.\n',
224
- );
225
- }
226
- if (res.safetySha) process.stdout.write(` 되돌리기 전 상태는 ${res.safetySha.slice(0, 8)} 에 스냅샷됨.\n`);
227
- return 0;
184
+ case 'answer':
185
+ case 'rollback':
186
+ case 'config': {
187
+ // 보조 명령의 실제 동작은 src/cli-commands.js 에 있다(NFR-5). bin 은 라우팅만 한다.
188
+ const c = await import('../src/cli-commands.js');
189
+ const fn = { answer: c.runAnswer, rollback: c.runRollback, config: c.runConfig }[command];
190
+ return fn({ cwd, args });
228
191
  }
229
192
  case 'setup': {
230
193
  const { runSetup } = await import('./setup.js');
@@ -232,38 +195,6 @@ export async function main(argv = process.argv.slice(2)) {
232
195
  process.stdout.write(`${report.lines.join('\n')}\n`);
233
196
  return 0;
234
197
  }
235
- case 'config': {
236
- // hi-loop config 현재 정책 출력
237
- // hi-loop config set <key> <value> 정책 변경 (branchPolicy|commitPolicy|commitStyle)
238
- // hi-loop config add-branch <name> 보호 브랜치 추가
239
- // hi-loop config remove-branch <name> 보호 브랜치 제거
240
- const cfg = await import('../src/config.js');
241
- const sub = args._[1];
242
- if (!sub) {
243
- process.stdout.write(`${JSON.stringify(cfg.loadConfig(cwd), null, 2)}\n`);
244
- return 0;
245
- }
246
- if (sub === 'set') {
247
- const r = cfg.setPolicy(cwd, args._[2], args._[3]);
248
- if (!r.ok) {
249
- process.stderr.write(`${r.reason}\n`);
250
- return 2;
251
- }
252
- process.stdout.write(`✓ ${args._[2]} = ${args._[3]}\n`);
253
- return 0;
254
- }
255
- if (sub === 'add-branch' || sub === 'remove-branch') {
256
- const r = sub === 'add-branch' ? cfg.addBranch(cwd, args._[2]) : cfg.removeBranch(cwd, args._[2]);
257
- if (!r.ok) {
258
- process.stderr.write(`${r.reason}\n`);
259
- return 2;
260
- }
261
- process.stdout.write(`✓ 보호 브랜치: ${r.config.protectedBranches.join(', ')}\n`);
262
- return 0;
263
- }
264
- process.stderr.write(`알 수 없는 config 하위명령: ${sub} (가능: set, add-branch, remove-branch)\n`);
265
- return 2;
266
- }
267
198
  default:
268
199
  process.stderr.write(`알 수 없는 명령: ${command}\n\n${USAGE}\n`);
269
200
  return 2;
@@ -276,7 +207,13 @@ if (isMain(import.meta.url)) {
276
207
  if (code !== null && code !== undefined) process.exit(code);
277
208
  })
278
209
  .catch((err) => {
279
- process.stderr.write(`[hi-loop] 치명적 오류: ${err?.stack ?? err}\n`);
210
+ // 환경/설정 오류(에이전트 미설치·인증 만료 등)는 우리 코드의 버그가 아니다.
211
+ // 스택트레이스는 잡음일 뿐이므로 행동지침 한 줄만 낸다. 진짜 버그만 스택을 노출한다.
212
+ if (err?.kind === 'environment') {
213
+ process.stderr.write(`[hi-loop] ${err.message}\n`);
214
+ } else {
215
+ process.stderr.write(`[hi-loop] 치명적 오류: ${err?.stack ?? err}\n`);
216
+ }
280
217
  process.exit(1);
281
218
  });
282
219
  }
package/bin/setup.js CHANGED
@@ -33,8 +33,36 @@ export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_BEGIN}
33
33
  프로젝트 셋업)가 없으면 **임의 설치하지 말고 스킵**한다 — 통과한 루프를 실패로 만들지 않는다.
34
34
  - 코드 변경 요청은 보호 브랜치(main/master/develop/dev)면 feature 로 분기하고, 테스트 통과 후
35
35
  **로컬 커밋**한다(정책은 커밋되는 \`.hi-loop.json\`; push/PR 은 안 함). "이번만"과 "앞으로"를 구분한다.
36
+ - 이 프로젝트의 표준 설계 문서는 \`docs/design.md\` 다. 코드 변경 루프는 이 문서와 정합해야 한다 —
37
+ 요청이 문서와 어긋날 수 있으면 \`--reconcile-spec docs/design.md\`(정합 게이트, 모순이면 사람에게
38
+ 물음)로, 이 문서를 스펙 오라클로 고정하려면 \`--spec docs/design.md\` 로 실행한다.
36
39
  ${CLAUDE_MD_END}`;
37
40
 
41
+ // 표준 설계 문서(FR-21 정합 오라클). setup 이 없으면 만들어 두고, 있으면 절대 덮어쓰지 않는다.
42
+ // PLAN 이 쓰는 spec.md 와 달리 이 문서는 **사람이 관리하는 진실**이라 엔진이 손대지 않는다.
43
+ export const DESIGN_DOC_PATH = 'docs/design.md';
44
+ export const DESIGN_DOC_TEMPLATE = `# 설계 문서 (Design Doc)
45
+
46
+ > 이 프로젝트의 **표준 설계 문서**다. hi-loop 의 문서↔소스 정합 장치가 이 문서를 오라클로 삼는다:
47
+ > - \`hi-loop run --goal "..." --reconcile-spec docs/design.md\` — 새 요청이 이 문서와 모순되면
48
+ > 구현 전에 멈춰 사람에게 묻는다(문서 고정 / 새 스펙 분기 / 중단).
49
+ > - \`hi-loop run --goal "..." --spec docs/design.md\` — 이 문서를 스펙 오라클로 고정한다
50
+ > (설계·코드 리뷰와 검증이 이 문서를 기준으로 판정).
51
+ >
52
+ > 아래를 프로젝트에 맞게 채워라. **비어 있으면 정합 게이트는 사실상 아무것도 막지 못한다.**
53
+
54
+ ## 목적 / 배경
55
+
56
+ ## 핵심 결정 — 제약·불변식
57
+ <!-- 예: 인증은 HS256 JWT, 만료 1시간. 결제 잔액은 음수 불가. 이 줄들이 정합 판정의 근거가 된다. -->
58
+
59
+ ## 범위 — 하는 것 / 안 하는 것
60
+
61
+ ## 공개 인터페이스 / API
62
+
63
+ ## 열린 질문
64
+ `;
65
+
38
66
  /** 이 setup 파일과 나란히 있는 실제 CLI 진입점의 절대 경로 */
39
67
  export function cliEntryPath() {
40
68
  return join(dirname(fileURLToPath(import.meta.url)), 'hi-loop.js');
@@ -146,7 +174,17 @@ export function runSetup({ cwd = process.cwd(), dryRun = false } = {}) {
146
174
  }
147
175
  }
148
176
 
149
- // 4) CLAUDE.md — 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침 (FR-6.5)
177
+ // 4) docs/design.md — 표준 설계 문서(FR-21 정합 오라클). 없으면 만들고, 있으면 내용을 보존한다.
178
+ const designPath = join(root, DESIGN_DOC_PATH);
179
+ if (!existsSync(designPath)) {
180
+ write(designPath, DESIGN_DOC_TEMPLATE);
181
+ changes.push(DESIGN_DOC_PATH);
182
+ lines.push(`${dryRun ? '[dry-run] ' : ''}✓ ${DESIGN_DOC_PATH} 표준 설계 문서를 만들었습니다 (--reconcile-spec / --spec 오라클).`);
183
+ } else {
184
+ lines.push(`· ${DESIGN_DOC_PATH} 이미 존재합니다 (내용 보존).`);
185
+ }
186
+
187
+ // 5) CLAUDE.md — 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침 (FR-6.5)
150
188
  const cmdPath = join(root, 'CLAUDE.md');
151
189
  const cmd = mergeClaudeMd(existsSync(cmdPath) ? readFileSync(cmdPath, 'utf8') : '');
152
190
  if (cmd.changed) {
@@ -157,7 +195,13 @@ export function runSetup({ cwd = process.cwd(), dryRun = false } = {}) {
157
195
  lines.push('· CLAUDE.md 지침은 이미 최신입니다.');
158
196
  }
159
197
 
160
- lines.push('', '이제 사용하세요:', ' hi-loop run --goal "요구사항" --test "npm test"', ' 또는 에이전트에서 MCP 도구 hiloop_run 호출 (스킬: /hi-loop)');
198
+ lines.push(
199
+ '',
200
+ '이제 사용하세요:',
201
+ ' hi-loop run --goal "요구사항" --test "npm test"',
202
+ ' 또는 에이전트에서 MCP 도구 hiloop_run 호출 (스킬: /hi-loop)',
203
+ ` · ${DESIGN_DOC_PATH} 를 채우면 --reconcile-spec/--spec 오라클로 문서↔소스 정합을 지킵니다.`,
204
+ );
161
205
  return { changes, lines, dryRun };
162
206
  }
163
207
 
package/docs/guide.md CHANGED
@@ -15,7 +15,7 @@ npm link # hi-loop, hi-loop-setup 명령 등록
15
15
 
16
16
  # 2) 사용할 프로젝트에서 설정 주입
17
17
  cd /path/to/my-project
18
- hi-loop-setup # .mcp.json / .gitignore / docs / tests
18
+ hi-loop-setup # .mcp.json / .gitignore / docs·tests / CLAUDE.md / docs/design.md
19
19
 
20
20
  # 3) 두 가지 방식 중 하나로 가동
21
21
  hi-loop run --goal "add 함수를 만들어라" --test "npm test" # 사람이 직접
@@ -91,8 +91,14 @@ hi-loop-setup # 실제 적용
91
91
  | 대상 | 동작 |
92
92
  |---|---|
93
93
  | `.mcp.json` | `hi-loop` MCP 서버 등록. **기존 다른 서버 항목은 보존** |
94
- | `.gitignore` | `.agent-state.json` 추가 (중복 없이) |
94
+ | `.gitignore` | `.agent-state.json` · `.agent-state.lock` 추가 (중복 없이) |
95
95
  | `docs/`, `tests/` | 없으면 생성 |
96
+ | `docs/design.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
97
+ | `CLAUDE.md` | 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침을 마커로 멱등 주입 |
98
+
99
+ > `docs/design.md` 는 이번 프로젝트의 **오라클**이다 — `--reconcile`(정합 게이트)·`--spec`(스펙 고정)이
100
+ > 이 문서를 기준으로 요청을 판정한다(§4-1 참조). 셋업 직후 이 문서를 채워두면, 이후 단말적 요청이
101
+ > 문서와 어긋날 때 엔진이 잡아준다. 비어 있으면 정합 게이트는 사실상 아무것도 막지 못한다.
96
102
 
97
103
  생성되는 `.mcp.json` 은 이렇게 **현재 설치본의 절대 경로**를 가리킨다:
98
104
 
@@ -132,6 +138,9 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
132
138
  | `--budget-usd` | — | (없음=무제한) | 누적 **비용** 상한(USD). 넘으면 다음 호출 전에 중단 |
133
139
  | `--stagnation` | — | `3` | 같은 실패가 이 횟수 연속이면 조기 종료. `--no-stagnation` 으로 끔 |
134
140
  | `--verify-spec` | — | (꺼짐) | 통과 후 별도 검증자로 스펙 대비 구현을 심판 (하향 전용, 비용 증가) |
141
+ | `--spec <path>` | — | (엔진이 결정) | 스펙 오라클을 이 문서로 **고정**. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다 |
142
+ | `--reconcile` / `--no-reconcile` | — | `--full` 이면 켜짐 | **문서 정합 게이트**: 요청이 표준 문서와 모순되면 구현 전에 멈춰 사람에게 묻는다 |
143
+ | `--reconcile-spec <path>` | — | design.md→spec.md | 정합 게이트가 대조할 문서 지정(지정하면 게이트가 켜진다) |
135
144
  | `--cwd` | `-c` | 현재 디렉터리 | 작업 대상 |
136
145
 
137
146
  > 세 상한의 관계: `--max-loops`(횟수) / `--budget-usd`(비용) / `--stagnation`(반복).
@@ -153,6 +162,14 @@ hi-loop status # 현재 루프 상태 요약 (누적 비용·정체
153
162
  hi-loop --help
154
163
  ```
155
164
 
165
+ > **문서↔소스 정합 (`--spec` / `--reconcile`)**: 기본적으로 이 엔진의 스펙·테스트는 에이전트
166
+ > **자신이** 쓴 것이라, 통과가 "자기 테스트를 통과"에 그칠 수 있다(완료 보고의 Gaps 가 이를 공개한다).
167
+ > 사람이 관리하는 표준 문서(`docs/design.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
168
+ > - `--spec docs/design.md` — 그 문서를 스펙 오라클로 **고정**(리뷰·검증이 이 문서 기준).
169
+ > - `--reconcile` — 새 요청이 그 문서와 **모순되면 구현 전에 멈춰** 묻는다(고정 / 분기 / 중단).
170
+ > 대상은 `--reconcile-spec` 명시 → `docs/design.md` → `docs/spec.md` 순. `hi-loop-setup` 을 했다면
171
+ > `--full`/`--reconcile` 만으로 design.md 가 자동 오라클이 된다.
172
+
156
173
  ### 4-1b. 전과정 라이프사이클 (`--full`) 과 배포·감시
157
174
 
158
175
  기본은 자가 치유 루프 하나다. 앞뒤 단계는 **켤 때만** 돈다 — 켜지 않은 사람의 비용이
@@ -238,17 +255,25 @@ hi-loop rollback --cwd . # 작업 대상 지정
238
255
 
239
256
  ### 4-2. MCP 서버 (클로드코드 / 커서)
240
257
 
241
- `hi-loop-setup` 후 에이전트를 재시작하면 도구 3개가 뜬다.
258
+ `hi-loop-setup` 후 에이전트를 재시작하면 도구 6개가 뜬다.
242
259
 
243
260
  | 도구 | 인자 | 용도 |
244
261
  |---|---|---|
245
- | `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `cwd` | 자가 치유 루프 실행 |
262
+ | `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `spec`, `reconcile`, `reconcileSpec`, `cwd` | 자가 치유(+전과정) 루프 실행 |
263
+ | `hiloop_answer` | `choice`, `note`, `cwd` | 대기 중인 질문에 답하고 재개 가능 상태로 되돌림 |
246
264
  | `hiloop_status` | `cwd` | 진행 상태 조회 (장시간 루프를 들여다보는 창) |
265
+ | `hiloop_rollback` | `to`, `cwd` | 체크포인트로 파일 복원 |
247
266
  | `hiloop_reset` | `cwd` | `.agent-state.json` 초기화 |
267
+ | `hiloop_setup` | `dryRun`, `cwd` | 프로젝트 설정 주입 |
248
268
 
249
269
  > MCP `hiloop_run`은 CLI와 달리 `--budget-usd`·`--stagnation`을 노출하지 않는다.
250
270
  > 예산 상한이 필요하면 CLI(`hi-loop run`)를 쓴다. 정체 감지는 기본값(3회)으로 동작한다.
251
271
 
272
+ > **cwd 기본값은 `CLAUDE_PROJECT_DIR || process.cwd()`.** MCP 는 호스트(클로드코드/커서)가 서버를
273
+ > 띄우는 경로라, "어느 프로젝트인가"의 정답은 호스트가 주입하는 `CLAUDE_PROJECT_DIR` 다. 각 도구에
274
+ > `cwd` 를 명시하면 그것이 최우선. (CLI 는 반대로 순수 `process.cwd()` — 터미널에서 cd 한 곳이 의도다.)
275
+ > 상위 폴더에서 여러 하위 프로젝트를 다룰 땐 `cwd` 를 명시하거나 프로젝트별로 `hi-loop-setup` 하라.
276
+
252
277
  수동 기동: `hi-loop mcp` (인자 없이 `hi-loop` 만 쳐도 서버로 뜬다)
253
278
 
254
279
  ### 4-3. 환경변수
@@ -260,6 +285,8 @@ hi-loop rollback --cwd . # 작업 대상 지정
260
285
  | `HILOOP_PERMISSION_MODE` | `acceptEdits` | 비대화형 편집 허용용. `bypassPermissions` 등으로 확대/축소 |
261
286
  | `HILOOP_VERIFY_CMD` | (`HILOOP_AGENT_CMD`→`claude`) | `--verify-spec` 검증자 실행 명령. 구현자와 다른 CLI로 검증 가능 |
262
287
  | `HILOOP_VERIFY_MODEL` | (구현자와 동일) | `--verify-spec` 검증자 모델 |
288
+ | `HILOOP_RECONCILE_CMD` | (`HILOOP_VERIFY_CMD`→구현자) | `--reconcile` 정합 판정자 실행 명령 |
289
+ | `HILOOP_RECONCILE_MODEL` | (`HILOOP_VERIFY_MODEL`→구현자) | `--reconcile` 정합 판정자 모델 |
263
290
  | `HILOOP_AGENT_TIMEOUT_MS` | `1800000` | 에이전트 호출 타임아웃(30분). 쓰레기값은 기본값으로 되돌림 |
264
291
  | `HILOOP_TEST_TIMEOUT_MS` | `600000` | 테스트 실행 타임아웃(10분). 〃 |
265
292
  | `TELEGRAM_BOT_TOKEN` | (없음) | 알림용. 미설정 시 조용히 생략 |
@@ -417,8 +444,9 @@ npm install -g @tuzi-ince/hi-loop # 명령 이름은 그대로 hi-loop / hi-l
417
444
  | 증상 | 원인 / 조치 |
418
445
  |---|---|
419
446
  | 에이전트가 파일을 하나도 안 쓰고 루프만 돈다 | 권한 모드 문제. `HILOOP_PERMISSION_MODE=bypassPermissions` 로 시도 |
420
- | `에이전트 실행 실패(claude): spawn ... ENOENT` | `claude`PATH 없음. `which claude` 확인 `HILOOP_AGENT_CMD` 에 절대 경로 지정 |
421
- | `spawn ... EACCES` | 커스텀 에이전트 스크립트에 실행 권한 없음 → `chmod +x` |
447
+ | `에이전트 명령 'claude' 찾을 없습니다` | preflight루프 진입 잡은 것. Claude Code 설치(`npm i -g @anthropic-ai/claude-code`) 또는 `HILOOP_AGENT_CMD` 에 절대 경로 지정 |
448
+ | `... 실행할 권한이 없습니다 (EACCES)` | 커스텀 에이전트 스크립트에 실행 권한 없음 → `chmod +x "$(command -v <명령>)"` |
449
+ | `API 키가 없거나 유효하지 않습니다 / 구독·크레딧 문제로 보입니다` | 에이전트 인증 문제. preflight 는 설치만 보고(비용 0), 인증은 첫 호출에서 감지된다 — `claude` 로그인 상태나 `ANTHROPIC_API_KEY` 를 확인 |
422
450
  | MCP 서버가 에이전트에 안 뜬다 | `.mcp.json` 경로가 실재하는지 확인(`hi-loop-setup` 재실행), 에이전트 재시작 |
423
451
  | MCP 응답이 깨진다 | stdout 은 프로토콜 채널이다. 커스텀 로거를 stdout 에 물리지 말 것 |
424
452
  | 같은 실패를 10회 반복하고 끝난다 | 알려진 한계(design.md L2). `--max-loops` 를 낮춰 비용부터 막고 goal 을 더 구체적으로 |
package/docs/spec.md CHANGED
@@ -222,11 +222,16 @@ runLoop({
222
222
  - FR-4.3 노출 도구:
223
223
  | tool | input | 동작 |
224
224
  |---|---|---|
225
- | `hiloop_run` | `goal`(필수), `testCommand?`, `maxLoops?`, `verifySpec?`, `cwd?` | 루프 실행 후 결과 텍스트 반환. `budgetUsd`·`stagnation`은 노출 안 함(CLI 전용) |
225
+ | `hiloop_run` | `goal`(필수), `testCommand?`, `maxLoops?`, `verifySpec?`, `spec?`, `reconcile?`, `cwd?` | 루프 실행 후 결과 텍스트 반환. `budgetUsd`·`stagnation`은 노출 안 함(CLI 전용) |
226
226
  | `hiloop_status` | `cwd?` | 현재 `.agent-state.json` 요약 반환 |
227
227
  | `hiloop_reset` | `cwd?` | 상태 파일 삭제 |
228
228
  - FR-4.4 도구 오류는 예외를 던지지 않고 `isError: true` + 메시지로 반환한다.
229
229
  - FR-4.5 SDK 미설치 시 친절한 에러 메시지 후 exit 1 (CLI 모드는 SDK 없이도 동작해야 함).
230
+ - FR-4.6 **cwd 기본값은 `CLAUDE_PROJECT_DIR || process.cwd()`.** MCP 는 호스트가 서버를 띄우는
231
+ 경로라 "어느 프로젝트인가"의 정답은 호스트가 주입하는 `CLAUDE_PROJECT_DIR` 다. 호출자가 `cwd`
232
+ 를 명시하면 그것이 최우선. (CLI 는 이와 달리 순수 `process.cwd()` — 터미널에서 cd 한 곳이
233
+ 사용자의 명시 의도이고, 통합 터미널에 새어든 env 가 그것을 덮으면 하위 프로젝트 겨냥이 깨진다.)
234
+ - FR-4.7 `hiloop_run` 은 루프 진입 전 에이전트 preflight(FR-20)를 거친다.
230
235
 
231
236
  ### FR-5. 텔레그램 알림 (`src/telegram.js`)
232
237
 
@@ -244,6 +249,11 @@ runLoop({
244
249
  - FR-6.3 `docs/`, `tests/` 디렉터리를 보장한다.
245
250
  - FR-6.4 **멱등(idempotent)**: 몇 번 돌려도 결과가 같고 기존 설정을 파괴하지 않는다.
246
251
  - FR-6.5 `--dry-run` 지원: 변경 없이 계획만 출력.
252
+ - FR-6.6 `docs/design.md` **표준 설계 문서**를 없으면 템플릿으로 만들고, 있으면 **절대 덮어쓰지
253
+ 않는다**(사람이 관리하는 진실). FR-21 정합 게이트·FR-19 스펙 고정의 오라클 대상이다. PLAN 이
254
+ 쓰는 `spec.md` 와 달리 엔진은 이 파일을 손대지 않는다.
255
+ - FR-6.7 `CLAUDE.md` 에 hi-loop 워크플로 라우팅 지침을 마커(`<!-- hi-loop:begin/end -->`)로 감싸
256
+ 멱등 주입한다. 코드 변경 루프가 `docs/design.md` 와 정합하도록(`--reconcile-spec`/`--spec`) 안내한다.
247
257
 
248
258
  ---
249
259
 
@@ -383,6 +393,55 @@ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW
383
393
  - FR-18.5 `assumptions`·`reviewFindings`·`answers` 도 컨텍스트 다이어트 대상이다(FR-3.1).
384
394
  프롬프트에 실려 나가므로 자르지 않으면 핸드오프가 압축한 만큼을 되돌려놓는다.
385
395
 
396
+ ### FR-19. 스펙 오라클 고정 (`--spec`) — 문서↔소스 drift 방지
397
+
398
+ 단말적 요청이 goal 마다 별도 스펙을 만들면(FR-3.5 의 goal 해시 분기) 프로젝트의 표준 문서와
399
+ 소스가 어긋나고, 그 갭이 환각의 빌미가 된다. `--spec <path>` 는 그 갭을 닫는다.
400
+
401
+ - FR-19.1 `--spec <path>` / MCP `hiloop_run(spec)` 로 스펙 오라클을 이 경로로 **고정**한다.
402
+ `state.specPath` 하나에서 DESIGN_REVIEW(FR-11)·CODE_REVIEW(FR-12)·스펙 검증(P6)이 모두
403
+ 읽으므로, 고정하면 세 오라클이 전부 이 문서를 기준으로 심판한다.
404
+ - FR-19.2 고정 경로는 **goal 해시로 비켜가지 않는다**(FR-3.5 의 분기를 끈다). 서로 다른 goal
405
+ 들이 같은 표준 문서를 오라클로 공유해, 요청이 문서와 어긋나면 오라클이 그것을 잡는다.
406
+ - FR-19.3 고정된 스펙에 **이미 내용이 있으면 PLAN 이 덮어쓰지 않는다**(`specPinned` 신호).
407
+ 사람이 관리하는 표준 문서를 진실로 받아들이고, 그에 맞춰 테스트만 작성한다. 비어 있을 때만 쓴다.
408
+ - FR-19.4 명시 고정은 저장된 `state.specPath` 보다 우선한다 — 사용자가 이번에 오라클을 바꾼 것이다.
409
+
410
+ ### FR-20. 에이전트 preflight & 실패 진단 (`src/preflight.js`)
411
+
412
+ 에이전트를 못 부르는 것은 이 엔진의 버그가 아니라 환경 문제다(미설치·인증·키). 루프 한복판의
413
+ 스택트레이스가 아니라 **행동지침 한 줄**로 바꾼다. CLI/MCP 공용.
414
+
415
+ - FR-20.1 에이전트를 부르는 명령(run/discover/plan/review)은 루프 진입 전 `claude --version`
416
+ 으로 실행 가능성을 확인한다(비용 0). 미설치(ENOENT)·권한(EACCES)이면 회차를 태우지 않고 멈춘다.
417
+ - FR-20.2 **오탐 방지**: `--version` 이 비정상 종료해도 막지 않는다(존재는 증명됨). 커스텀
418
+ 에이전트가 `--version` 을 지원 안 할 수 있다 — 멀쩡한 셋업을 죽이는 것이 더 나쁘다.
419
+ - FR-20.3 인증/구독/키 문제는 `--version` 으로 알 수 없다(비용을 들이지 않는다). 첫 실제 호출의
420
+ stderr 를 패턴으로 읽어 힌트로 번역한다. 못 알아본 실패는 원문을 그대로 보여준다(감추지 않는다).
421
+ - FR-20.4 환경 오류는 `kind==='environment'` 로 표시하고, CLI 는 스택 없이 메시지만, MCP 는
422
+ 접두 없이 그대로 전달한다. 진짜 버그만 스택트레이스를 노출한다.
423
+
424
+ ### FR-21. 문서 정합 게이트 (`--reconcile`, `src/reconcile.js`)
425
+
426
+ 단말적 요청이 기존 표준 문서와 상이하게 들어오면, 엔진은 goal 해시로 스펙을 **조용히 포크**해
427
+ (FR-3.5) 문서↔소스 갭을 만든다 — 그 갭이 환각의 빌미다. 이 게이트는 포크 직전에 모순을 잡아
428
+ 사람에게 표면화한다. 판정이 아니라 **표면화**다.
429
+
430
+ - FR-21.1 opt-in. `--reconcile` / `--no-reconcile`, `--full` 이면 자동 켜짐(FR-17 과 같은 삼상).
431
+ 게이트 발동 자체는 `docs/design.md` 존재만으로 켜지지 않는다 — 반드시 위 플래그로 opt-in 한다.
432
+ - FR-21.2 대조 대상 문서의 우선순위: **① `--reconcile-spec <path>`(명시) → ② `docs/design.md`
433
+ (setup 이 만드는 사람 관리 표준 문서, FR-6.6) → ③ `docs/spec.md`(폴백).** spec.md 는 PLAN 이 쓴
434
+ 에이전트 산출물이라 오라클로는 약하므로, 사람 문서가 있으면 그쪽을 기본으로 삼는다.
435
+ - FR-21.3 발동 조건: 켜졌고, 대상 문서가 있고, 이번 실행이 그것을 오라클로 쓰지 않고
436
+ (specPinned 아님) 별도 경로로 포크될 참일 때. 하나라도 아니면 판정기를 부르지 않는다(비용 0).
437
+ - FR-21.4 판정기는 별도 세션·plan 모드(쓰기 금지)로 goal 과 표준 문서를 대조해 `ok|conflict` 를
438
+ 낸다. 인프라 실패·파싱 실패는 `ok` 로 흘린다(하향 관대 — 애매한 것까지 막으면 정상 작업이 멈춘다).
439
+ - FR-21.5 `conflict` 면 상호배타 분기(ask, FR-16)로 멈춘다: (a) 표준 문서를 오라클로 **고정**하고
440
+ 진행(FR-19) / (b) 새 스펙으로 **분기** / (c) **중단**. `--ask never` 면 안 멈추고 포크로 진행하되
441
+ 미해결이 Gaps 에 남는다.
442
+ - FR-21.6 결정은 `state.reconciled` 로 기록해 같은 goal 재개 시 다시 묻지 않는다. 답한 재개는
443
+ 판정기를 재호출하지 않는다(gate 가 저장된 답을 돌려준다).
444
+
386
445
  ---
387
446
 
388
447
  ## 3. 비기능 요구사항
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuzi-ince/hi-loop",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI와 MCP 모드를 지원하는 초경량 자율형 자가 치유 엔진",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: hi-loop
2
+ name: flow
3
3
  description: >-
4
4
  기획·설계가 필요한 기능 개발/개선 요청을 소스만 보고 바로 구현하지 말고, PLAN→DESIGN→DO→CHECK→HEAL
5
5
  자율 루프(hiloop_run)로 처리한다. spec/설계를 먼저 세우고 테스트가 통과할 때까지 반복하며, full=true면