@tuzi-ince/hi-loop 0.3.4 → 0.4.1

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
@@ -5,8 +5,8 @@
5
5
  날것의 아이디어(goal)를 던지면, AI 에이전트를 PDCA 루프로 반복 구동해
6
6
  **테스트가 실제로 통과할 때까지** 스스로 고쳐 나간다.
7
7
 
8
- - 요구사항(무엇을): [`docs/spec.md`](docs/spec.md)
9
- - 설계(어떻게·왜): [`docs/design.md`](docs/design.md)
8
+ - 요구사항(무엇을): [`docs/SPEC.md`](docs/SPEC.md)
9
+ - 설계(어떻게·왜): [`docs/DESIGN.md`](docs/DESIGN.md)
10
10
  - 배포·설치·사용: [`docs/guide.md`](docs/guide.md)
11
11
 
12
12
  ## 루프 엔지니어링 — 우리의 개발 철학
@@ -33,10 +33,10 @@ 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 / CLAUDE.md / docs/design.md 자동 주입
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`** 를 함께 만든다(있으면 보존). 이 문서를 채우면
39
+ `hi-loop-setup` 은 **표준 설계 문서 `docs/DESIGN.md`** 를 함께 만든다(있으면 보존). 이 문서를 채우면
40
40
  문서↔소스 정합 장치(`--reconcile-spec` / `--spec`)의 오라클이 된다 — 아래 [문서↔소스 정합](#문서를-상시-오라클로--표준-문서-고정---spec) 참조.
41
41
 
42
42
  소스에서 개발용으로 설치하려면:
@@ -66,7 +66,7 @@ MCP 도구: `hiloop_run`, `hiloop_answer`, `hiloop_status`, `hiloop_reset`, `hil
66
66
  ## 동작 원리
67
67
 
68
68
  ```
69
- PLAN ──► spec.md + tests/app.test.js 를 먼저 강제 (구현 코드 금지)
69
+ PLAN ──► SPEC.md + tests/app.test.js 를 먼저 강제 (구현 코드 금지)
70
70
  DO ──► 테스트를 통과시킬 최소 구현
71
71
  CHECK ──► 엔진이 testCommand 를 직접 실행 (에이전트의 "다 됐어요"는 안 믿는다)
72
72
  ACT ──► stderr 를 그대로 에이전트에 들이밀고 "이 에러를 고쳐라" (최대 10회)
@@ -115,7 +115,7 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" --full --budget-usd
115
115
  - **DESIGN_REVIEW → BUILD → CODE_REVIEW** 순으로 만들고, 만든 것을 스스로 심판한다.
116
116
  - 상호배타 분기(예: "users 확장 vs auth_tokens 신설")를 만나면 거기서만 사람에게 묻는다.
117
117
 
118
- **기존 파일을 덮어쓰지 않는다.** `docs/spec.md` / `tests/app.test.js` 가 비어 있으면 그대로
118
+ **기존 파일을 덮어쓰지 않는다.** `docs/SPEC.md` / `tests/app.test.js` 가 비어 있으면 그대로
119
119
  쓰고, 이미 뭔가 있으면 goal 해시로 비켜간다(`docs/spec-5d88cc7f.md`). 같은 프로젝트에서
120
120
  goal만 바꿔 여러 번 돌려도 앞의 산출물이 살아남는다.
121
121
 
@@ -132,14 +132,14 @@ goal만 바꿔 여러 번 돌려도 앞의 산출물이 살아남는다.
132
132
  ```bash
133
133
  hi-loop run --goal "결제 모듈의 중복 검증 로직을 하나로 합쳐라" \
134
134
  --test "npm test" \
135
- --spec docs/spec.md \
135
+ --spec docs/SPEC.md \
136
136
  --verify-spec
137
137
  ```
138
138
 
139
139
  - 기존 `npm test` 가 회귀 방지선이다 — 개선하다 무언가 깨면 Tier 1에서 걸린다.
140
140
  - `--verify-spec` 은 테스트 통과 **후** 별도 검증자가 스펙 대비 구현을 심판한다.
141
141
  구조가 아니라 **의도**를 본다("리팩터링했다"는데 동작이 달라졌으면 기각).
142
- - `--spec docs/spec.md` 는 **사람이 관리하는 표준 문서를 오라클로 고정**한다 — 단말적 개선
142
+ - `--spec docs/SPEC.md` 는 **사람이 관리하는 표준 문서를 오라클로 고정**한다 — 단말적 개선
143
143
  요청이 문서와 어긋나는 것을 잡는다(→ [문서↔소스 정합](#문서를-상시-오라클로--표준-문서-고정---spec)).
144
144
  - 개선 중 락파일·설정·마이그레이션을 건드리면 보고서 Gaps에 ⚠️로 공개된다
145
145
  (→ [영향도 분석](#4-영향도-분석--변경-위험-파악)).
@@ -174,7 +174,9 @@ CI·배포 설정, 빌드·러너 설정, 환경 변수. "테스트는 통과했
174
174
  사람이 놓치지 않게 한다.
175
175
 
176
176
  **(b) 조건부 검사 — 바뀐 파일에 따라 게이트를 켠다.** 무거운 검사(e2e 등)를 매 회차 돌리는
177
- 대신, 특정 경로가 바뀐 회차에만 돌린다.
177
+ 대신, 특정 경로에 변경이 있을 때만 돌린다. `--when` 은 `git status`(HEAD 대비 워킹트리 변경)로
178
+ 매칭한다 — 즉 "그 경로를 건드린 그 회차만"이 아니라 **커밋 전 변경분에 그 경로가 있는 한 이후
179
+ 회차마다** 검사가 돈다(2회차에 e2e 가 사라지지 않게 하려는 의도).
178
180
 
179
181
  ```bash
180
182
  hi-loop run --goal "..." \
@@ -188,6 +190,22 @@ hi-loop run --goal "..." \
188
190
  - `--when` 글롭에 안 맞은 검사는 **건너뜀으로 기록**된다 — 안 돌린 것이 통과처럼 보이지 않게
189
191
  Gaps에 남는다.
190
192
 
193
+ **MCP 에서도 같은 걸 쓴다 — "UI 변경 회차에만 e2e"를 엔진이 강제한다.** `hiloop_run` 이 `checks`
194
+ 인자를 받는다(CLI `--check/--when` 의 MCP 노출):
195
+
196
+ ```json
197
+ "checks": [
198
+ { "cmd": "npm test" },
199
+ { "cmd": "npx playwright test", "when": "src/**/*.tsx" }
200
+ ]
201
+ ```
202
+
203
+ - e2e 를 "스킬이 사람에게 기억해서 실행"(제안)이 아니라 **엔진이 결정론적으로 강제**(메커니즘)하는 길이다.
204
+ UI 파일이 바뀐 회차마다 엔진이 e2e 를 돌리고 통과해야 pass 로 인정한다 — 조용히 빠지지 않는다.
205
+ - e2e 는 **셸 명령**이어야 한다. 엔진은 Playwright **MCP 도구**를 부를 수 없으므로, MCP 만 있는
206
+ 프로젝트는 `flow` 스킬이 루프 통과 후 MCP 로 돌린다(폴백). 셸 e2e 명령도 없으면 스킵.
207
+ - `flow` 스킬이 UI 작업을 감지하면 사용자에게 물은 뒤 이 `checks` 를 자동 구성해 `hiloop_run` 에 넘긴다.
208
+
191
209
  **diff만 리뷰하고 싶다면** — 지금 워킹트리 변경분을 정확성·보안·YAGNI 축으로 심판:
192
210
 
193
211
  ```bash
@@ -275,7 +293,7 @@ hi-loop run --goal "..." --start-from BUILD --stop-after CODE_REVIEW
275
293
 
276
294
  | 사용자가 이렇게 말하면 | LLM이 하는 일 |
277
295
  |---|---|
278
- | `/hi-loop 로그인 폼 만들어줘` (슬래시 스킬) | hi-loop 스킬 기동 → `hiloop_run(full=true)` 실행 |
296
+ | `/hi-loop:flow 로그인 폼 만들어줘` (슬래시 스킬) | `flow` 스킬 기동 → `hiloop_run(full=true)` 실행 (bare `/hi-loop` 은 명령이지 이 스킬이 아님) |
279
297
  | "hiloop_run 도구로 결제 버그 고쳐줘" | 지목된 MCP 도구를 로드해 바로 호출 |
280
298
  | 터미널에서 `hi-loop run --goal "..." --full` | 엔진을 CLI로 직접 구동(LLM 개입 없음) |
281
299
 
@@ -308,7 +326,7 @@ hi-loop run --goal "..." --start-from BUILD --stop-after CODE_REVIEW
308
326
 
309
327
  코드 변경 작업을 **보호 브랜치에서 분기 → 구현·테스트 → 로컬 커밋**으로 감싼다. push/PR 은 하지 않는다.
310
328
 
311
- **정책은 커밋되는 `.hi-loop.json` 한 파일**에 산다(팀 공유, 스킬·엔진 공용). `.agent-state.json`
329
+ **정책은 커밋되는 `.hi-loop.json` 한 파일**에 산다(팀 공유, 스킬·엔진 공용). `.hi-loop/STATE.json`
312
330
  (임시·gitignore)과 다르다. 기본값:
313
331
 
314
332
  ```json
@@ -350,7 +368,7 @@ hi-loop config remove-branch dev # 보호 브랜치 제거
350
368
  새 goal로 깨끗이 다시 시작하려면 상태 파일을 지운다:
351
369
 
352
370
  ```bash
353
- hi-loop reset # .agent-state.json 삭제 (CLI)
371
+ hi-loop reset # .hi-loop/STATE.json 삭제 (CLI)
354
372
  # MCP: hiloop_reset
355
373
  ```
356
374
 
@@ -363,7 +381,7 @@ hi-loop reset # .agent-state.json 삭제 (CLI)
363
381
  ### 세션 핸드오프 & 컨텍스트 다이어트
364
382
 
365
383
  에이전트는 같은 세션에서 오래 굴릴수록 토큰이 쌓여 멍청해진다. 그래서 4회마다 세션을
366
- **버리고**, `.agent-state.json` 에 압축된 상태(goal / 스펙 요약 / 마지막 에러 / 최근 이력 5건)만
384
+ **버리고**, `.hi-loop/STATE.json` 에 압축된 상태(goal / 스펙 요약 / 마지막 에러 / 최근 이력 5건)만
367
385
  새 세션에 넘겨 이어서 작업한다(auto-resume). 같은 goal로 다시 실행하면 중단 지점부터 재개한다.
368
386
 
369
387
  ### TDD 자가 치유
@@ -415,7 +433,7 @@ false green을 disclosed green으로 바꾼다.
415
433
  **막지 못한다** — 스펙 파일만 여러 개로 늘 뿐이다. `--spec <path>` 가 이 구멍을 닫는다.
416
434
 
417
435
  ```bash
418
- hi-loop run --goal "..." --spec docs/spec.md --verify-spec
436
+ hi-loop run --goal "..." --spec docs/SPEC.md --verify-spec
419
437
  ```
420
438
 
421
439
  - 스펙 오라클을 이 경로로 **고정**한다. DESIGN_REVIEW·CODE_REVIEW·`--verify-spec` 이 모두
@@ -438,9 +456,9 @@ hi-loop run --goal "결제에서 음수 잔액도 허용하라" --reconcile #
438
456
 
439
457
  - 기존 표준 문서가 있고 이번 요청이 그것과 별개 스펙으로 **포크될 참이면**, 포크 직전에
440
458
  별도 판정자(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는
459
+ - **대조 대상 우선순위**: `--reconcile-spec <path>`(명시) → `docs/SPEC.md`가 아니라 **`docs/DESIGN.md`
460
+ (setup이 만드는 사람 관리 표준 문서)** → 없으면 `docs/SPEC.md` 폴백. 즉 `hi-loop-setup`을 했다면
461
+ `--full`/`--reconcile`만으로 별도 지정 없이 **DESIGN.md가 자동으로 오라클이 된다.** (SPEC.md는
444
462
  PLAN이 쓴 에이전트 산출물이라 오라클로 약하다 — 사람 문서가 있으면 그쪽이 옳다.)
445
463
  - **모순이면 멈추고 사람에게 묻는다**(상호배타 3지선다): **(a)** 표준 문서를 오라클로 고정하고
446
464
  진행 / **(b)** 새 스펙으로 분기(요청이 문서를 갱신) / **(c)** 중단하고 문서를 먼저 정리.
@@ -520,7 +538,7 @@ src/checks.js 다중 조건 검사 — --check/--when, short-circuit 없음
520
538
  src/blast.js 블라스트 반경 — 변경의 위험 분류를 공개
521
539
  src/report.js 완료 보고 — Gaps 공개 + 상태 요약
522
540
  src/cli-options.js CLI 인자 → runLoop 옵션 번역 (잘못된 값은 조용히 흘리지 않는다)
523
- src/state.js .agent-state.json (원자적 저장 + 다이어트 + 산출물 경로 + resume + Gaps)
541
+ src/state.js .hi-loop/STATE.json (원자적 저장 + 다이어트 + 산출물 경로 + resume + Gaps)
524
542
  src/runners.js 에이전트/테스트 실행 어댑터 (주입 가능) + 비용 파싱 + 타임아웃
525
543
  src/preflight.js 에이전트 실행 preflight + 실패 진단 (미설치·인증·키 → 행동지침)
526
544
  src/reconcile.js 문서 정합 게이트 — 요청↔표준 문서 모순 판정 + ask 표면화 (FR-21)
package/bin/hi-loop.js CHANGED
@@ -27,12 +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] [--spec docs/spec.md] [--cwd .]
30
+ [--stagnation 3 | --no-stagnation] [--verify-spec] [--spec docs/DESIGN.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
+ [--reconcile | --no-reconcile] [--reconcile-spec docs/DESIGN.md]
36
36
  [--watch "<헬스체크 명령>"] [--watch-for 5m] [--watch-every 30s]
37
37
  [--watch-tolerate 1] [--on-watch-fail stop|rollback|heal]
38
38
  [--branch [name]] [--commit] [--commit-message "..."]
package/bin/setup.js CHANGED
@@ -10,9 +10,10 @@ import { parseArgs } from '../src/args.js';
10
10
  import { isMain } from '../src/is-main.js';
11
11
 
12
12
  export const MCP_SERVER_KEY = 'hi-loop';
13
- export const GITIGNORE_ENTRY = '.agent-state.json';
14
- // 런타임 부산물 커밋되면 된다. 락(L4) pid 마다 다르고, 상태는 캐시다.
15
- export const GITIGNORE_ENTRIES = ['.agent-state.json', '.agent-state.lock'];
13
+ export const GITIGNORE_ENTRY = '.hi-loop/';
14
+ // 런타임 부산물은 전용 폴더 .hi-loop/ 아래(STATE.json·STATE.lock) 모여 있으므로 폴더째 무시한다.
15
+ // 상태는 캐시고 락은 pid 마다 달라 커밋되면 안 된다. 정책(.hi-loop.json)은 이 폴더 밖이라 커밋된다.
16
+ export const GITIGNORE_ENTRIES = ['.hi-loop/'];
16
17
 
17
18
  // CLAUDE.md 에 심는 상시 지침 — LLM 이 "기획/설계가 필요한 요청"을 hi-loop 으로
18
19
  // 라우팅하도록 하는 push 표면이다. MCP 도구는 pull(모델이 먼저 떠올려야 호출)이라
@@ -35,20 +36,20 @@ export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_BEGIN}
35
36
  프로젝트 셋업)가 없으면 **임의 설치하지 말고 스킵**한다 — 통과한 루프를 실패로 만들지 않는다.
36
37
  - 코드 변경 요청은 보호 브랜치(main/master/develop/dev)면 feature 로 분기하고, 테스트 통과 후
37
38
  **로컬 커밋**한다(정책은 커밋되는 \`.hi-loop.json\`; push/PR 은 안 함). "이번만"과 "앞으로"를 구분한다.
38
- - 이 프로젝트의 표준 설계 문서는 \`docs/design.md\` 다. 코드 변경 루프는 이 문서와 정합해야 한다 —
39
- 요청이 문서와 어긋날 수 있으면 \`--reconcile-spec docs/design.md\`(정합 게이트, 모순이면 사람에게
40
- 물음)로, 이 문서를 스펙 오라클로 고정하려면 \`--spec docs/design.md\` 로 실행한다.
39
+ - 이 프로젝트의 표준 설계 문서는 \`docs/DESIGN.md\` 다. 코드 변경 루프는 이 문서와 정합해야 한다 —
40
+ 요청이 문서와 어긋날 수 있으면 \`--reconcile-spec docs/DESIGN.md\`(정합 게이트, 모순이면 사람에게
41
+ 물음)로, 이 문서를 스펙 오라클로 고정하려면 \`--spec docs/DESIGN.md\` 로 실행한다.
41
42
  ${CLAUDE_MD_END}`;
42
43
 
43
44
  // 표준 설계 문서(FR-21 정합 오라클). setup 이 없으면 만들어 두고, 있으면 절대 덮어쓰지 않는다.
44
45
  // PLAN 이 쓰는 spec.md 와 달리 이 문서는 **사람이 관리하는 진실**이라 엔진이 손대지 않는다.
45
- export const DESIGN_DOC_PATH = 'docs/design.md';
46
+ export const DESIGN_DOC_PATH = 'docs/DESIGN.md';
46
47
  export const DESIGN_DOC_TEMPLATE = `# 설계 문서 (Design Doc)
47
48
 
48
49
  > 이 프로젝트의 **표준 설계 문서**다. hi-loop 의 문서↔소스 정합 장치가 이 문서를 오라클로 삼는다:
49
- > - \`hi-loop run --goal "..." --reconcile-spec docs/design.md\` — 새 요청이 이 문서와 모순되면
50
+ > - \`hi-loop run --goal "..." --reconcile-spec docs/DESIGN.md\` — 새 요청이 이 문서와 모순되면
50
51
  > 구현 전에 멈춰 사람에게 묻는다(문서 고정 / 새 스펙 분기 / 중단).
51
- > - \`hi-loop run --goal "..." --spec docs/design.md\` — 이 문서를 스펙 오라클로 고정한다
52
+ > - \`hi-loop run --goal "..." --spec docs/DESIGN.md\` — 이 문서를 스펙 오라클로 고정한다
52
53
  > (설계·코드 리뷰와 검증이 이 문서를 기준으로 판정).
53
54
  >
54
55
  > 아래를 프로젝트에 맞게 채워라. **비어 있으면 정합 게이트는 사실상 아무것도 막지 못한다.**
@@ -176,9 +177,10 @@ export function runSetup({ cwd = process.cwd(), dryRun = false } = {}) {
176
177
  }
177
178
  }
178
179
 
179
- // 4) docs/design.md — 표준 설계 문서(FR-21 정합 오라클). 없으면 만들고, 있으면 내용을 보존한다.
180
+ // 4) docs/DESIGN.md — 표준 설계 문서(FR-21 정합 오라클). 없으면 만들고, 있으면 내용을 보존한다.
181
+ // 구버전 소문자(docs/design.md)가 있으면 그것도 존중해 새로 만들지 않는다(대소문자 구분 FS 대비).
180
182
  const designPath = join(root, DESIGN_DOC_PATH);
181
- if (!existsSync(designPath)) {
183
+ if (!existsSync(designPath) && !existsSync(join(root, 'docs/design.md'))) {
182
184
  write(designPath, DESIGN_DOC_TEMPLATE);
183
185
  changes.push(DESIGN_DOC_PATH);
184
186
  lines.push(`${dryRun ? '[dry-run] ' : ''}✓ ${DESIGN_DOC_PATH} 표준 설계 문서를 만들었습니다 (--reconcile-spec / --spec 오라클).`);
@@ -1,6 +1,6 @@
1
1
  # hi-loop — 설계 문서 (Design v1.0)
2
2
 
3
- > 대상: `docs/spec.md` (요구사항 정의서) 를 무엇으로/어떻게 구현했는가.
3
+ > 대상: `docs/SPEC.md` (요구사항 정의서) 를 무엇으로/어떻게 구현했는가.
4
4
  > 요구사항이 **무엇을(What)** 이라면 이 문서는 **어떻게(How)와 왜(Why)** 다.
5
5
  > 구현 기준 커밋: `auto/handoff-engine`
6
6
 
@@ -210,7 +210,7 @@ state.iteration += 1;
210
210
 
211
211
  ### 4.7 산출물 경로: 프롬프트에서 빼앗아 엔진으로 (G7)
212
212
 
213
- 원래 PLAN 프롬프트는 `docs/spec.md`와 `tests/app.test.js`를 **문자열로 박아** 지시했다.
213
+ 원래 PLAN 프롬프트는 `docs/SPEC.md`와 `tests/app.test.js`를 **문자열로 박아** 지시했다.
214
214
  그 결과 두 가지가 깨진다:
215
215
 
216
216
  1. 그 파일들이 이미 있는 프로젝트에서 돌리면 **남의 스펙·테스트를 덮어쓴다.**
@@ -227,13 +227,13 @@ state.iteration += 1;
227
227
 
228
228
  ```js
229
229
  planPathsFor({ cwd, goal })
230
- // docs/spec.md 가 비었으면 → 'docs/spec.md'
230
+ // docs/SPEC.md 가 비었으면 → 'docs/SPEC.md'
231
231
  // 이미 있으면 → 'docs/spec-<sha1(goal).slice(0,8)>.md'
232
232
  ```
233
233
 
234
234
  - **왜 해시인가**: 같은 goal이면 같은 경로여야 resume이 이어진다. 다른 goal끼리는 충돌하지
235
235
  않는다. 한국어 goal에서도 파일명이 깨지지 않는다(슬러그화하면 깨진다).
236
- - **왜 최초 1회만 계산하나**: PLAN이 `docs/spec.md`를 만든 뒤 재계산하면 "이미 있음"으로
236
+ - **왜 최초 1회만 계산하나**: PLAN이 `docs/SPEC.md`를 만든 뒤 재계산하면 "이미 있음"으로
237
237
  판정돼 2회차부터 경로가 밀려난다. 그래서 경로는 상태에 박아두고 resume은 그걸 재사용한다.
238
238
  - **한계**: 이것은 **회피**지 강제가 아니다. 엔진은 에이전트의 쓰기를 가로챌 수 없다(CLI를
239
239
  spawn할 뿐이다). 지정 경로 밖을 건드리지 말라는 것은 여전히 프롬프트 규칙이다(L1과 동류).
@@ -408,7 +408,7 @@ MCP `hiloop_run` 결과에도 붙는다. 에이전트가 이 도구를 호출해
408
408
 
409
409
  ### 4.13 동시 실행 락: 배타는 원자적 생성으로 (L4)
410
410
 
411
- `.agent-state.json`은 단일 루프를 가정한다. 같은 디렉터리에서 두 번째 `hi-loop run`이
411
+ `.hi-loop/STATE.json`은 단일 루프를 가정한다. 같은 디렉터리에서 두 번째 `hi-loop run`이
412
412
  뜨면 두 프로세스가 같은 파일을 번갈아 저장해 resume이 깨진다. 락 파일 하나로 막는다.
413
413
 
414
414
  **설계 결정 — 왜 `wx` 인가?** 락 획득은 `writeFileSync(path, ..., { flag: 'wx' })` —
@@ -546,7 +546,7 @@ try/catch 로 감싼다. **텔레그램 장애로 빌드가 죽는 것은 설계
546
546
  | ~~L1~~ | ~~에이전트가 테스트를 무력화해 통과시킬 수 있다~~ | ✅ **해소**(§4.8) — CHECK 2단 판정 + 지문 비교. 가드 발동은 유닛테스트로 고정, 1차 방어(프롬프트)는 실제 에이전트로 버팀 확인. 다만 관측이지 강제는 아니다(L8) |
547
547
  | ~~L2~~ | ~~같은 오답을 10회 반복해도 엔진은 모른다~~ | ✅ **해소**(§4.9) — `errorSignature` 연속 동일 3회 시 `stopReason: 'stagnated'`. 정체 > 예산 우선순위 |
548
548
  | ~~L3~~ | ~~롤백이 없다 — 에이전트가 코드를 망가뜨리면 그대로 남는다~~ | ✅ **해소**(§4.11) — 호출 전 `git stash create` 비파괴 체크포인트 + `hi-loop rollback [--to N]`. 자동 롤백은 없음(전경 실행). 실제 git 으로 복원 실측 |
549
- | ~~L4~~ | ~~단일 루프 가정(락 없음)~~ | ✅ **해소**(§4.13) — `.agent-state.lock` pid 락. 산 pid는 거부, 죽은 pid는 stale 회수. runLoop 이 finally 로 확실히 푼다 |
549
+ | ~~L4~~ | ~~단일 루프 가정(락 없음)~~ | ✅ **해소**(§4.13) — `.hi-loop/STATE.lock` pid 락. 산 pid는 거부, 죽은 pid는 stale 회수. runLoop 이 finally 로 확실히 푼다 |
550
550
  | ~~L5~~ | ~~비용/토큰 추적 없음~~ | ✅ **해소**(§4.6) — `total_cost_usd` 누적 + `--budget-usd`. 실제 에이전트로 검증됨(§10) |
551
551
  | ~~L6~~ | ~~타임아웃 하드코딩~~ | ✅ **해소** — `HILOOP_AGENT_TIMEOUT_MS` / `HILOOP_TEST_TIMEOUT_MS`. 쓰레기값은 기본값으로 되돌림 |
552
552
  | ~~L7~~ | ~~실제 `claude` 에이전트와의 통합이 미검증~~ | ✅ **해소** — claude 2.1.212 로 실측. 편집 권한·세션 재개·JSON 파싱·전체 루프 전부 확인(§10). **차단 사유였던 "개발 환경 실행 가드"는 사실이 아니었다** — 막고 있던 것은 기술적 불가능이 아니라 실행되지 않은 절차였다 |
@@ -571,16 +571,16 @@ try/catch 로 감싼다. **텔레그램 장애로 빌드가 죽는 것은 설계
571
571
 
572
572
  | 항목 | 상태 | 근거 |
573
573
  |---|---|---|
574
- | 단위/수용 테스트 104개 | ✅ 통과 | `npm test` (네트워크·실제 LLM 없이) |
574
+ | 단위/수용 테스트 343개 | ✅ 통과 | `npm test` (네트워크·실제 LLM 없이) |
575
575
  | **설치본(tarball) 실행** | ✅ 실측 | `npm pack` → `npm install -g --prefix /tmp/lev-prefix ./tgz` → **심링크 bin 경유** 실행 및 전체 루프 E2E 통과. §4.5 버그를 잡아낸 경로 |
576
576
  | CLI 루프 E2E (가짜 에이전트) | ✅ 실측 | PLAN→실패→DO→통과→exit 0, 2회차 프롬프트에 실제 AssertionError 주입 확인 |
577
577
  | MCP 서버 (도구 왕복) | ✅ 실측 | stdio 로 initialize → tools/list(3종) → tools/call |
578
578
  | setup 멱등성 | ✅ 테스트 | 2회 실행 시 changes=[] |
579
579
  | **실제 `claude` 에이전트 왕복** | ✅ **실측** | claude 2.1.212. PLAN→CHECK(fail)→DO→CHECK(pass)→exit 0, 2회차 통과 |
580
- | **`--permission-mode acceptEdits` 실효성** | ✅ **실측** | 에이전트가 실제로 `docs/spec.md`(81줄) + `tests/app.test.js`(105줄) + `src/add.js`를 썼다. 이전의 "⚠️ 추론"이 사실로 확인됨 |
580
+ | **`--permission-mode acceptEdits` 실효성** | ✅ **실측** | 에이전트가 실제로 `docs/SPEC.md`(81줄) + `tests/app.test.js`(105줄) + `src/add.js`를 썼다. 이전의 "⚠️ 추론"이 사실로 확인됨 |
581
581
  | **세션 재개(`--resume`)** | ✅ **실측** | 세션 파일 하나(`~/.claude/projects/…/<id>.jsonl`)에 PLAN 프롬프트와 DO 프롬프트가 **둘 다** 존재. 재개 실패 시 2회차는 새 세션 ID를 받았을 것 |
582
582
  | **비용 관측 / 예산 상한** | ✅ **실측** | 예산 $1 지정 → `maxLoops 5` 중 **2회차에 중단**, `stopReason: 'budget'`, `costUsd: 1.606411` |
583
- | **경로 회피 (덮어쓰기 방지)** | ✅ **실측** | 보호 대상 `docs/spec.md`·`tests/app.test.js` 가 있는 디렉터리에서 실행 → 원본 무수정(`git status` 비어 있음), 에이전트는 `docs/spec-5d88cc7f.md`·`tests/app-5d88cc7f.test.js` 에 씀 |
583
+ | **경로 회피 (덮어쓰기 방지)** | ✅ **실측** | 보호 대상 `docs/SPEC.md`·`tests/app.test.js` 가 있는 디렉터리에서 실행 → 원본 무수정(`git status` 비어 있음), 에이전트는 `docs/spec-5d88cc7f.md`·`tests/app-5d88cc7f.test.js` 에 씀 |
584
584
  | CLI 플래그 4종 존재 | ✅ 실측 | claude 2.1.212 `--help`: `-p`, `--output-format`, `-r/--resume`, `--permission-mode`(choices 에 `acceptEdits`·`plan` 포함) |
585
585
  | **핸드오프(4회차 세션 폐기) — 실제 에이전트** | ❌ **미검증** | 가짜 에이전트로만 확인(AC-6). 관측하려면 `--max-loops 5` 이상 + 계속 실패해야 한다 — `iteration % 4 === 0 && iteration < maxLoops` 이므로 `maxLoops 3` 으로는 **산술적으로 불가능** |
586
586
  | **MCP 모드에서 실제 에이전트 spawn** | ❌ 미검증 | 도구 왕복만 확인. `hiloop_run` 이 실제 claude 를 띄우는 경로는 미확인 |
@@ -614,7 +614,7 @@ hi-loop run --goal "두 수를 더하는 add 함수를 만들어라" --test "npm
614
614
  그러면 "권한 모드가 되는가"가 아니라 "에이전트가 함정을 눈치채는가"를 측정하게 된다.
615
615
  - **일회용 디렉터리에서 돌려라.** 경로 회피(§4.7)가 있어도 작업 디렉터리 이탈 방지는
616
616
  프롬프트 규칙뿐이다(L8).
617
- - 확인할 것: 파일이 실제로 쓰였는가 / `.agent-state.json` 의 `sessionId`·`costUsd` /
617
+ - 확인할 것: 파일이 실제로 쓰였는가 / `.hi-loop/STATE.json` 의 `sessionId`·`costUsd` /
618
618
  최종 exit code. 핸드오프까지 보려면 `--max-loops 5` 이상 + 계속 실패하는 testCommand
619
619
  (예: `--test "node -e 'process.exit(1)'"` — 에이전트가 수정할 수 없다).
620
620
 
@@ -39,7 +39,7 @@
39
39
  | `hi-loop rollback [--to N] [--cwd DIR]` | N회차 직전 체크포인트로 파일 복원 (기본: 최신). L3 |
40
40
  | `hi-loop mcp` | MCP 서버를 stdio 전송으로 기동 |
41
41
  | `hi-loop` (인자 없음) | `mcp`와 동일 (에이전트가 그냥 실행해도 서버로 뜬다) |
42
- | `hi-loop status [--cwd DIR]` | `.agent-state.json` 요약 출력 |
42
+ | `hi-loop status [--cwd DIR]` | `.hi-loop/STATE.json` 요약 출력 |
43
43
  | `hi-loop setup` / `hi-loop-setup` | 프로젝트/클로드코드 설정 자동 주입 |
44
44
  | `hi-loop --help`, `hi-loop --version` | 도움말/버전 |
45
45
 
@@ -66,7 +66,7 @@ runLoop({
66
66
  testRunner, // async ({command, cwd}) => {ok, code, stdout, stderr}
67
67
  notifier, // async (event) => void
68
68
  logger, // (line) => void
69
- statePath, // 기본 `${cwd}/.agent-state.json`
69
+ statePath, // 기본 `${cwd}/.hi-loop/STATE.json`
70
70
  }) => Promise<Result>
71
71
  ```
72
72
 
@@ -75,7 +75,7 @@ runLoop({
75
75
  - FR-2.1 **PLAN**: 첫 iteration에서 spec 문서 + 테스트를 **먼저** 만들도록 에이전트에
76
76
  지시한다. 코드 작성 금지를 프롬프트에 명시한다.
77
77
  - FR-2.1a **산출물 경로는 엔진이 정한다**(프롬프트에 하드코딩하지 않는다).
78
- 기본값은 `docs/spec.md` / `tests/app.test.js`이며, **그 경로에 이미 파일이 있으면
78
+ 기본값은 `docs/SPEC.md` / `tests/app.test.js`이며, **그 경로에 이미 파일이 있으면
79
79
  goal 해시를 붙여 비켜간다**(`docs/spec-<hash8>.md` / `tests/app-<hash8>.test.js`).
80
80
  - 왜: 하드코딩하면 (a) 그 파일이 이미 있는 프로젝트의 스펙·테스트를 덮어쓰고,
81
81
  (b) 같은 프로젝트에서 goal만 바꿔 두 번 돌리면 1회차 산출물이 파괴된다. 롤백은 없다.
@@ -98,7 +98,7 @@ runLoop({
98
98
  - 위반 사실을 에러 뒤에 붙여 다음 루프에 먹인다(차단은 대안과 함께 배송 — "되돌리고
99
99
  구현을 고쳐라, 테스트가 틀렸으면 약화 말고 더 정확한 단언으로 교체하라").
100
100
  - **한계**: 이것은 관측이지 강제가 아니다. 엔진은 CLI를 spawn할 뿐 에이전트의 쓰기를
101
- 가로챌 수 없다(design.md L8).
101
+ 가로챌 수 없다(DESIGN.md L8).
102
102
  - FR-2.4 **ACT(HEAL)**: 실패 시 stdout/stderr 마지막 4000자를 잘라 에이전트에 들이밀고
103
103
  "이 에러를 고쳐라"로 재지시. `maxLoops`(기본 10)까지 반복.
104
104
  - FR-2.5 CHECK가 통과하면 즉시 종료하고 `status: 'passed'`.
@@ -121,8 +121,8 @@ runLoop({
121
121
  - 왜 필요한가: 실측상 새 세션은 사소한 호출도 바닥값 ~$0.43(캐시 생성)이고,
122
122
  동일 goal·동일 2회전이 $2.58과 $4.67로 1.8배 갈렸다. **`maxLoops`는 비용 상한이 아니다.**
123
123
  - 러너가 비용을 주지 않으면 0으로 취급한다(누적이 `NaN`으로 오염되지 않는다).
124
- - FR-2.12 **동시 실행 락 (L4).** 같은 cwd 에서 두 루프가 `.agent-state.json`을 서로
125
- 덮어쓰지 못하게 락 파일(`.agent-state.lock`)로 배타를 강제한다.
124
+ - FR-2.12 **동시 실행 락 (L4).** 같은 cwd 에서 두 루프가 `.hi-loop/STATE.json`을 서로
125
+ 덮어쓰지 못하게 락 파일(`.hi-loop/STATE.lock`)로 배타를 강제한다.
126
126
  - 원자적 생성(`wx` 플래그): 이미 있으면 실패 — 이것이 배타의 원자성.
127
127
  - 락에 pid 를 기록하고, 주인이 살아 있으면 거부, 죽었으면(크래시/SIGKILL) stale 로 보고
128
128
  회수한다. 남은 락 때문에 영영 못 도는 것이 더 나쁘다. 손상된 락도 stale 로 본다.
@@ -169,19 +169,19 @@ runLoop({
169
169
 
170
170
  ### FR-3. 세션 핸드오프 & 컨텍스트 다이어트 (`bkit`)
171
171
 
172
- - FR-3.1 루프 상태는 `.agent-state.json`에 매 phase 전이마다 **원자적으로**(tmp→rename) 저장한다.
172
+ - FR-3.1 루프 상태는 `.hi-loop/STATE.json`에 매 phase 전이마다 **원자적으로**(tmp→rename) 저장한다.
173
173
  - FR-3.2 `iteration % handoffEvery === 0`이면 세션을 폐기(`sessionId = null`)하고
174
174
  `sessionSerial`을 1 올린다. 다음 호출은 새 세션에서 시작한다.
175
175
  - FR-3.3 새 세션에 전달하는 컨텍스트는 **압축본만**:
176
176
  goal, specSummary(≤1200자), lastError(≤2000자), 최근 history 5건 요약, 현재 iteration.
177
177
  전체 대화 로그는 절대 넘기지 않는다.
178
178
  - FR-3.4 `specSummary`는 PLAN 단계 에이전트 출력의 요약(≤1200자)으로 갱신한다.
179
- - FR-3.5 `.agent-state.json`이 이미 존재하고 goal이 동일하면 **auto-resume**:
179
+ - FR-3.5 `.hi-loop/STATE.json`이 이미 존재하고 goal이 동일하면 **auto-resume**:
180
180
  마지막 iteration 다음부터 이어서 진행한다. goal이 다르면 상태를 새로 만든다.
181
181
  단 이전 상태가 이미 `passed`면 완료된 작업이므로 새 상태로 시작한다.
182
182
  손상된(파싱 불가) 상태 파일도 새 상태로 취급한다.
183
183
 
184
- `.agent-state.json` 스키마:
184
+ `.hi-loop/STATE.json` 스키마:
185
185
 
186
186
  ```jsonc
187
187
  {
@@ -195,7 +195,7 @@ runLoop({
195
195
  "maxLoops": 10,
196
196
  "sessionId": "abc-123 | null",
197
197
  "sessionSerial": 1,
198
- "specPath": "docs/spec.md", // FR-2.1a. 최초 1회 결정 후 고정.
198
+ "specPath": "docs/SPEC.md", // FR-2.1a. 최초 1회 결정 후 고정.
199
199
  "testPath": "tests/app.test.js", // FR-2.1a. resume 시 재계산하지 않는다.
200
200
  "specSummary": "…(≤1200자)",
201
201
  "lastError": "…(≤2000자)",
@@ -222,8 +222,8 @@ runLoop({
222
222
  - FR-4.3 노출 도구:
223
223
  | tool | input | 동작 |
224
224
  |---|---|---|
225
- | `hiloop_run` | `goal`(필수), `testCommand?`, `maxLoops?`, `verifySpec?`, `spec?`, `reconcile?`, `cwd?` | 루프 실행 후 결과 텍스트 반환. `budgetUsd`·`stagnation`은 노출 안 함(CLI 전용) |
226
- | `hiloop_status` | `cwd?` | 현재 `.agent-state.json` 요약 반환 |
225
+ | `hiloop_run` | `goal`(필수), `testCommand?`, `maxLoops?`, `verifySpec?`, `spec?`, `reconcile?`, `reconcileSpec?`, `checks?`, `cwd?` | 루프 실행 후 결과 텍스트 반환. `budgetUsd`·`stagnation`은 노출 안 함(CLI 전용). `checks`=`[{cmd,when?}]` 조건부 검사(CLI `--check/--when` 의 MCP 노출) — UI 변경 회차에만 e2e 같은 게이트를 **엔진이 강제**한다 |
226
+ | `hiloop_status` | `cwd?` | 현재 `.hi-loop/STATE.json` 요약 반환 |
227
227
  | `hiloop_reset` | `cwd?` | 상태 파일 삭제 |
228
228
  - FR-4.4 도구 오류는 예외를 던지지 않고 `isError: true` + 메시지로 반환한다.
229
229
  - FR-4.5 SDK 미설치 시 친절한 에러 메시지 후 exit 1 (CLI 모드는 SDK 없이도 동작해야 함).
@@ -245,15 +245,15 @@ runLoop({
245
245
 
246
246
  - FR-6.1 대상 프로젝트에 `.mcp.json`을 생성/병합하여 `hi-loop` MCP 서버를 등록한다.
247
247
  기존 다른 서버 항목은 보존한다.
248
- - FR-6.2 `.gitignore`에 `.agent-state.json` 과 `.agent-state.lock`(L4) 항목을 없으면 추가한다.
248
+ - FR-6.2 `.gitignore`에 `.hi-loop/STATE.json` 과 `.hi-loop/STATE.lock`(L4) 항목을 없으면 추가한다.
249
249
  - FR-6.3 `docs/`, `tests/` 디렉터리를 보장한다.
250
250
  - FR-6.4 **멱등(idempotent)**: 몇 번 돌려도 결과가 같고 기존 설정을 파괴하지 않는다.
251
251
  - FR-6.5 `--dry-run` 지원: 변경 없이 계획만 출력.
252
- - FR-6.6 `docs/design.md` **표준 설계 문서**를 없으면 템플릿으로 만들고, 있으면 **절대 덮어쓰지
252
+ - FR-6.6 `docs/DESIGN.md` **표준 설계 문서**를 없으면 템플릿으로 만들고, 있으면 **절대 덮어쓰지
253
253
  않는다**(사람이 관리하는 진실). FR-21 정합 게이트·FR-19 스펙 고정의 오라클 대상이다. PLAN 이
254
- 쓰는 `spec.md` 와 달리 엔진은 이 파일을 손대지 않는다.
254
+ 쓰는 `SPEC.md` 와 달리 엔진은 이 파일을 손대지 않는다.
255
255
  - FR-6.7 `CLAUDE.md` 에 hi-loop 워크플로 라우팅 지침을 마커(`<!-- hi-loop:begin/end -->`)로 감싸
256
- 멱등 주입한다. 코드 변경 루프가 `docs/design.md` 와 정합하도록(`--reconcile-spec`/`--spec`) 안내하고,
256
+ 멱등 주입한다. 코드 변경 루프가 `docs/DESIGN.md` 와 정합하도록(`--reconcile-spec`/`--spec`) 안내하고,
257
257
  **응답 언어를 사용자의 요청 언어에 맞추라는 지시**를 포함한다 — 영어 스킬/도구/프레임워크 표면이
258
258
  많아도 한국어 요청엔 한국어로 답하도록(언어 드리프트 방지).
259
259
 
@@ -430,9 +430,9 @@ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW
430
430
  사람에게 표면화한다. 판정이 아니라 **표면화**다.
431
431
 
432
432
  - FR-21.1 opt-in. `--reconcile` / `--no-reconcile`, `--full` 이면 자동 켜짐(FR-17 과 같은 삼상).
433
- 게이트 발동 자체는 `docs/design.md` 존재만으로 켜지지 않는다 — 반드시 위 플래그로 opt-in 한다.
434
- - FR-21.2 대조 대상 문서의 우선순위: **① `--reconcile-spec <path>`(명시) → ② `docs/design.md`
435
- (setup 이 만드는 사람 관리 표준 문서, FR-6.6) → ③ `docs/spec.md`(폴백).** spec.md 는 PLAN 이 쓴
433
+ 게이트 발동 자체는 `docs/DESIGN.md` 존재만으로 켜지지 않는다 — 반드시 위 플래그로 opt-in 한다.
434
+ - FR-21.2 대조 대상 문서의 우선순위: **① `--reconcile-spec <path>`(명시) → ② `docs/DESIGN.md`
435
+ (setup 이 만드는 사람 관리 표준 문서, FR-6.6) → ③ `docs/SPEC.md`(폴백).** SPEC.md 는 PLAN 이 쓴
436
436
  에이전트 산출물이라 오라클로는 약하므로, 사람 문서가 있으면 그쪽을 기본으로 삼는다.
437
437
  - FR-21.3 발동 조건: 켜졌고, 대상 문서가 있고, 이번 실행이 그것을 오라클로 쓰지 않고
438
438
  (specPinned 아님) 별도 경로로 포크될 참일 때. 하나라도 아니면 판정기를 부르지 않는다(비용 0).
@@ -471,17 +471,17 @@ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW
471
471
  | AC-5 | 실패한 CHECK의 stderr가 다음 에이전트 프롬프트에 포함된다 |
472
472
  | AC-6 | iteration 4 이후 호출은 sessionId=null(새 세션)로 시작한다 |
473
473
  | AC-7 | 핸드오프 프롬프트에 goal/specSummary/lastError가 들어가고 전체 로그는 없다 |
474
- | AC-8 | `.agent-state.json`이 매 iteration 갱신되고 스키마를 만족한다 |
474
+ | AC-8 | `.hi-loop/STATE.json`이 매 iteration 갱신되고 스키마를 만족한다 |
475
475
  | AC-9 | 동일 goal로 재시작 시 이전 iteration 다음부터 resume |
476
476
  | AC-10 | 다른 goal로 재시작 시 상태 리셋 |
477
- | AC-11 | PLAN 프롬프트에 spec.md/tests 강제 문구 포함, 코드 작성 금지 명시 |
477
+ | AC-11 | PLAN 프롬프트에 SPEC.md/tests 강제 문구 포함, 코드 작성 금지 명시 |
478
478
  | AC-12 | 텔레그램 env 미설정 시 `{skipped:true}`, 루프 정상 진행 |
479
479
  | AC-13 | 텔레그램 fetch가 throw해도 `{ok:false}` 반환하고 전파 안 함 |
480
480
  | AC-14 | 4096자 초과 메시지 절단 |
481
481
  | AC-15 | CLI 인자 파서가 `--k v`, `--k=v`, `-g v`를 처리 |
482
482
  | AC-16 | `run` with no goal → exit code 2 |
483
483
  | AC-17 | setup이 `.mcp.json`을 병합하고 기존 서버를 보존 (멱등) |
484
- | AC-18 | setup이 `.gitignore`에 `.agent-state.json`을 중복 없이 추가 |
484
+ | AC-18 | setup이 `.gitignore`에 `.hi-loop/STATE.json`을 중복 없이 추가 |
485
485
  | AC-19 | 상태 저장이 원자적(tmp 파일 잔존 없음) |
486
486
  | AC-20 | `hiloop_status` 도구가 상태 요약 텍스트 반환 |
487
487
  | AC-21 | `total_cost_usd`를 파싱해 `costUsd`로 노출하고, 없거나 숫자가 아니면 0 |
@@ -490,7 +490,7 @@ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW
490
490
  | AC-24 | 예산 미지정이면 기존 동작(무제한) 유지 |
491
491
  | AC-25 | 잘못된 `budgetUsd`(0/음수/문자열)는 `TypeError` — 조용한 무제한 금지 |
492
492
  | AC-26 | 비용을 주지 않는 러너와도 동작(`costUsd`가 `NaN`이 되지 않는다) |
493
- | AC-27 | 기본 경로가 비어 있으면 `docs/spec.md` / `tests/app.test.js` 사용 |
493
+ | AC-27 | 기본 경로가 비어 있으면 `docs/SPEC.md` / `tests/app.test.js` 사용 |
494
494
  | AC-28 | 기본 경로에 파일이 있으면 goal 해시 경로로 비켜가고 **원본을 수정하지 않는다** |
495
495
  | AC-29 | 같은 goal이면 같은 경로, 다른 goal이면 다른 경로 (한국어 goal 포함) |
496
496
  | AC-30 | resume은 저장된 경로를 재사용한다(PLAN 산출물 때문에 밀려나지 않는다) |
package/docs/guide.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # hi-loop — 배포·설치·사용 가이드
2
2
 
3
3
  > 이 문서는 **실제로 실행해 확인한 절차**만 담는다. 확인 못 한 것은 §7 에 그렇다고 적어뒀다.
4
- > 요구사항은 [`spec.md`](spec.md), 설계 근거는 [`design.md`](design.md) 참조.
4
+ > 요구사항은 [`SPEC.md`](SPEC.md), 설계 근거는 [`DESIGN.md`](DESIGN.md) 참조.
5
5
 
6
6
  ---
7
7
 
@@ -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 / CLAUDE.md / docs/design.md
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" # 사람이 직접
@@ -33,8 +33,9 @@ hi-loop run --goal "add 함수를 만들어라" --test "npm test" # 사람이
33
33
  | Git | 선택 (롤백/리뷰용) | `git --version` |
34
34
 
35
35
  > ⚠️ **`npm install -g handoff` 를 하지 마라.** 무스코프 `handoff` 는 이 프로젝트와
36
- > 무관한 **제3자의 redis lua 래퍼 패키지**(v0.1.3)다. 이 프로젝트의 이름은
37
- > **`@tuzi-ince/hi-loop`** 이며 아직 배포 전이다. 설치는 아래 §2 방법을 쓴다.
36
+ > 무관한 **제3자의 redis lua 래퍼 패키지**(v0.1.3)다. 이 프로젝트는 **스코프 이름
37
+ > `@tuzi-ince/hi-loop`** 으로 npm 레지스트리에 게시돼 있다(현재 0.4.0). 스코프를 반드시 붙여라.
38
+ > 최신 소스(0.4.1+)로 개발·검증하려면 아래 §2 의 로컬 설치를 쓴다.
38
39
 
39
40
  ---
40
41
 
@@ -45,9 +46,9 @@ hi-loop run --goal "add 함수를 만들어라" --test "npm test" # 사람이
45
46
  ```bash
46
47
  git clone <이 저장소> hi-loop && cd hi-loop
47
48
  npm install # @modelcontextprotocol/sdk, zod
48
- npm test # 104개 통과 확인
49
+ npm test # 전부 통과 확인
49
50
  npm link # 전역에 hi-loop / hi-loop-setup 심볼릭 링크
50
- hi-loop --version # 0.1.0
51
+ hi-loop --version # 0.4.1
51
52
  ```
52
53
 
53
54
  해제: `npm unlink -g @tuzi-ince/hi-loop`
@@ -62,10 +63,10 @@ npm install -g /absolute/path/to/hi-loop
62
63
 
63
64
  ```bash
64
65
  # 보내는 쪽
65
- npm pack # tuzi-ince-hi-loop-0.1.0.tgz 생성 (~26kB)
66
+ npm pack # tuzi-ince-hi-loop-0.4.1.tgz 생성 (~26kB)
66
67
 
67
68
  # 받는 쪽
68
- npm install -g ./tuzi-ince-hi-loop-0.1.0.tgz
69
+ npm install -g ./tuzi-ince-hi-loop-0.4.1.tgz
69
70
  ```
70
71
 
71
72
  ### 2-D. 설치 없이 직접 실행
@@ -91,12 +92,12 @@ hi-loop-setup # 실제 적용
91
92
  | 대상 | 동작 |
92
93
  |---|---|
93
94
  | `.mcp.json` | `hi-loop` MCP 서버 등록. **기존 다른 서버 항목은 보존** |
94
- | `.gitignore` | `.agent-state.json` · `.agent-state.lock` 추가 (중복 없이) |
95
+ | `.gitignore` | `.hi-loop/STATE.json` · `.hi-loop/STATE.lock` 추가 (중복 없이) |
95
96
  | `docs/`, `tests/` | 없으면 생성 |
96
- | `docs/design.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
97
+ | `docs/DESIGN.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
97
98
  | `CLAUDE.md` | 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침을 마커로 멱등 주입 |
98
99
 
99
- > `docs/design.md` 는 이번 프로젝트의 **오라클**이다 — `--reconcile`(정합 게이트)·`--spec`(스펙 고정)이
100
+ > `docs/DESIGN.md` 는 이번 프로젝트의 **오라클**이다 — `--reconcile`(정합 게이트)·`--spec`(스펙 고정)이
100
101
  > 이 문서를 기준으로 요청을 판정한다(§4-1 참조). 셋업 직후 이 문서를 채워두면, 이후 단말적 요청이
101
102
  > 문서와 어긋날 때 엔진이 잡아준다. 비어 있으면 정합 게이트는 사실상 아무것도 막지 못한다.
102
103
 
@@ -140,7 +141,7 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
140
141
  | `--verify-spec` | — | (꺼짐) | 통과 후 별도 검증자로 스펙 대비 구현을 심판 (하향 전용, 비용 증가) |
141
142
  | `--spec <path>` | — | (엔진이 결정) | 스펙 오라클을 이 문서로 **고정**. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다 |
142
143
  | `--reconcile` / `--no-reconcile` | — | `--full` 이면 켜짐 | **문서 정합 게이트**: 요청이 표준 문서와 모순되면 구현 전에 멈춰 사람에게 묻는다 |
143
- | `--reconcile-spec <path>` | — | design.md→spec.md | 정합 게이트가 대조할 문서 지정(지정하면 게이트가 켜진다) |
144
+ | `--reconcile-spec <path>` | — | DESIGN.md→SPEC.md | 정합 게이트가 대조할 문서 지정(지정하면 게이트가 켜진다) |
144
145
  | `--cwd` | `-c` | 현재 디렉터리 | 작업 대상 |
145
146
 
146
147
  > 세 상한의 관계: `--max-loops`(횟수) / `--budget-usd`(비용) / `--stagnation`(반복).
@@ -164,11 +165,11 @@ hi-loop --help
164
165
 
165
166
  > **문서↔소스 정합 (`--spec` / `--reconcile`)**: 기본적으로 이 엔진의 스펙·테스트는 에이전트
166
167
  > **자신이** 쓴 것이라, 통과가 "자기 테스트를 통과"에 그칠 수 있다(완료 보고의 Gaps 가 이를 공개한다).
167
- > 사람이 관리하는 표준 문서(`docs/design.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
168
- > - `--spec docs/design.md` — 그 문서를 스펙 오라클로 **고정**(리뷰·검증이 이 문서 기준).
168
+ > 사람이 관리하는 표준 문서(`docs/DESIGN.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
169
+ > - `--spec docs/DESIGN.md` — 그 문서를 스펙 오라클로 **고정**(리뷰·검증이 이 문서 기준).
169
170
  > - `--reconcile` — 새 요청이 그 문서와 **모순되면 구현 전에 멈춰** 묻는다(고정 / 분기 / 중단).
170
- > 대상은 `--reconcile-spec` 명시 → `docs/design.md` → `docs/spec.md` 순. `hi-loop-setup` 을 했다면
171
- > `--full`/`--reconcile` 만으로 design.md 가 자동 오라클이 된다.
171
+ > 대상은 `--reconcile-spec` 명시 → `docs/DESIGN.md` → `docs/SPEC.md` 순. `hi-loop-setup` 을 했다면
172
+ > `--full`/`--reconcile` 만으로 DESIGN.md 가 자동 오라클이 된다.
172
173
 
173
174
  ### 4-1b. 전과정 라이프사이클 (`--full`) 과 배포·감시
174
175
 
@@ -259,16 +260,28 @@ hi-loop rollback --cwd . # 작업 대상 지정
259
260
 
260
261
  | 도구 | 인자 | 용도 |
261
262
  |---|---|---|
262
- | `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `spec`, `reconcile`, `reconcileSpec`, `cwd` | 자가 치유(+전과정) 루프 실행 |
263
+ | `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `spec`, `reconcile`, `reconcileSpec`, `checks`, `cwd` | 자가 치유(+전과정) 루프 실행 |
263
264
  | `hiloop_answer` | `choice`, `note`, `cwd` | 대기 중인 질문에 답하고 재개 가능 상태로 되돌림 |
264
265
  | `hiloop_status` | `cwd` | 진행 상태 조회 (장시간 루프를 들여다보는 창) |
265
266
  | `hiloop_rollback` | `to`, `cwd` | 체크포인트로 파일 복원 |
266
- | `hiloop_reset` | `cwd` | `.agent-state.json` 초기화 |
267
+ | `hiloop_reset` | `cwd` | `.hi-loop/STATE.json` 초기화 |
267
268
  | `hiloop_setup` | `dryRun`, `cwd` | 프로젝트 설정 주입 |
268
269
 
269
270
  > MCP `hiloop_run`은 CLI와 달리 `--budget-usd`·`--stagnation`을 노출하지 않는다.
270
271
  > 예산 상한이 필요하면 CLI(`hi-loop run`)를 쓴다. 정체 감지는 기본값(3회)으로 동작한다.
271
272
 
273
+ > **UI 변경 회차에만 e2e 를 엔진이 강제하기 (`checks`).** `checks: [{cmd, when?}]` 를 주면
274
+ > `testCommand` 대신 이 배열이 판정 기준이 되고, `when` 글롭에 맞는 파일이 바뀐 회차에만 그 검사를
275
+ > 돌린다. e2e 를 스킬의 자연어 지시(사람이 기억해서 실행)가 아니라 **엔진이 결정론적으로 강제**하는 길이다:
276
+ > ```json
277
+ > "checks": [
278
+ > { "cmd": "npm test" },
279
+ > { "cmd": "npx playwright test", "when": "src/**/*.tsx" }
280
+ > ]
281
+ > ```
282
+ > e2e 는 **셸 명령**이어야 한다(엔진은 Playwright MCP 도구를 부를 수 없다 — 그 경우는 스킬이 루프 후
283
+ > MCP 로 돌린다). `flow` 스킬이 UI 작업을 감지하면 이 `checks` 를 자동으로 구성해 넘긴다.
284
+
272
285
  > **cwd 기본값은 `CLAUDE_PROJECT_DIR || process.cwd()`.** MCP 는 호스트(클로드코드/커서)가 서버를
273
286
  > 띄우는 경로라, "어느 프로젝트인가"의 정답은 호스트가 주입하는 `CLAUDE_PROJECT_DIR` 다. 각 도구에
274
287
  > `cwd` 를 명시하면 그것이 최우선. (CLI 는 반대로 순수 `process.cwd()` — 터미널에서 cd 한 곳이 의도다.)
@@ -346,7 +359,7 @@ hi-loop 의 CHECK 단계는 **에이전트가 "다 됐다"고 말해도 믿지
346
359
  npm version patch --no-git-tag-version # 0.1.0 -> 0.1.1
347
360
 
348
361
  # ── CHECK 1: 소스에서 테스트
349
- npm test # 104
362
+ npm test # 343
350
363
 
351
364
  # ── CHECK 2: 아티팩트로 만든다 (레지스트리 안 건드림)
352
365
  npm pack # tuzi-ince-hi-loop-0.1.1.tgz
@@ -372,7 +385,7 @@ rm -rf /tmp/lev-prefix # 정리
372
385
 
373
386
  ### 이 루프가 실제로 잡아낸 버그
374
387
 
375
- 이 절차는 이론이 아니다. **소스에서는 104개 테스트가 다 통과하는데 설치본에서는
388
+ 이 절차는 이론이 아니다. **소스에서는 343개 테스트가 다 통과하는데 설치본에서는
376
389
  모든 명령이 아무 일도 안 하고 조용히 `exit 0` 으로 끝나는** 버그를 이 루프가 잡았다.
377
390
 
378
391
  원인: `bin/*.js` 의 "직접 실행인가?" 판정이
@@ -431,7 +444,7 @@ npm install -g @tuzi-ince/hi-loop # 명령 이름은 그대로 hi-loop / hi-l
431
444
 
432
445
  배포 전 체크리스트:
433
446
 
434
- - [ ] `npm test` 통과 (104개)
447
+ - [ ] `npm test` 통과 (343개)
435
448
  - [ ] **§7 의 실제 에이전트 스모크 테스트 통과** ← 아직 안 된 항목. **이게 통과하기 전엔 배포하지 마라**
436
449
  - [ ] `npm pack --dry-run` 으로 포함 파일 확인 (bin, src, docs, README)
437
450
  - [ ] `version` 갱신
@@ -449,30 +462,30 @@ npm install -g @tuzi-ince/hi-loop # 명령 이름은 그대로 hi-loop / hi-l
449
462
  | `API 키가 없거나 유효하지 않습니다 / 구독·크레딧 문제로 보입니다` | 에이전트 인증 문제. preflight 는 설치만 보고(비용 0), 인증은 첫 호출에서 감지된다 — `claude` 로그인 상태나 `ANTHROPIC_API_KEY` 를 확인 |
450
463
  | MCP 서버가 에이전트에 안 뜬다 | `.mcp.json` 경로가 실재하는지 확인(`hi-loop-setup` 재실행), 에이전트 재시작 |
451
464
  | MCP 응답이 깨진다 | stdout 은 프로토콜 채널이다. 커스텀 로거를 stdout 에 물리지 말 것 |
452
- | 같은 실패를 10회 반복하고 끝난다 | 알려진 한계(design.md L2). `--max-loops` 를 낮춰 비용부터 막고 goal 을 더 구체적으로 |
453
- | 테스트를 지워서 통과시켰다 | 알려진 한계(design.md L1). 프롬프트 규칙이 1차 방어선일 뿐 — `git diff tests/` 로 반드시 확인 |
454
- | 이어서 하지 말고 처음부터 하고 싶다 | `rm .agent-state.json` 또는 `hiloop_reset` |
465
+ | 같은 실패를 10회 반복하고 끝난다 | 알려진 한계(DESIGN.md L2). `--max-loops` 를 낮춰 비용부터 막고 goal 을 더 구체적으로 |
466
+ | 테스트를 지워서 통과시켰다 | 알려진 한계(DESIGN.md L1). 프롬프트 규칙이 1차 방어선일 뿐 — `git diff tests/` 로 반드시 확인 |
467
+ | 이어서 하지 말고 처음부터 하고 싶다 | `rm .hi-loop/STATE.json` 또는 `hiloop_reset` |
455
468
  | `hi-loop` 를 옮긴 뒤 MCP 가 깨졌다 | `.mcp.json` 이 절대 경로라서 그렇다. `hi-loop-setup` 재실행 |
456
469
 
457
470
  상태 파일을 직접 들여다보는 게 가장 빠른 디버깅이다:
458
471
 
459
472
  ```bash
460
- cat .agent-state.json | jq '{status, phase, iteration, sessionId, sessionSerial, lastError}'
473
+ cat .hi-loop/STATE.json | jq '{status, phase, iteration, sessionId, sessionSerial, lastError}'
461
474
  ```
462
475
 
463
476
  ---
464
477
 
465
478
  ## 7. ⚠️ 이 가이드에서 검증된 것 / 안 된 것
466
479
 
467
- 정직하게 구분한다. (design.md §10 과 동일)
480
+ 정직하게 구분한다. (DESIGN.md §10 과 동일)
468
481
 
469
482
  **실제로 실행해 확인함**
470
483
 
471
- - `npm test` 104개 통과
484
+ - `npm test` 전부 통과
472
485
  - `npm pack` → tarball 생성 (bin/src/docs/README 포함)
473
486
  - **`npm install -g --prefix /tmp/lev-prefix ./tarball` 로 진짜 설치 → 설치본 실행 확인**
474
487
  - `bin/hi-loop` 가 심링크로 생성됨을 `ls -l` 로 확인
475
- - **심링크 bin 경유** `hi-loop --version` → `0.1.0`, `run`(goal 없음) → exit 2
488
+ - **심링크 bin 경유** `hi-loop --version` → `0.4.1`, `run`(goal 없음) → exit 2
476
489
  - 설치본으로 전체 루프 E2E: PLAN→실패→DO→통과→exit 0
477
490
  - 설치본 `hi-loop-setup` → `.mcp.json` 이 설치 위치를 정확히 가리킴
478
491
  - `hi-loop-setup` 멱등, `--dry-run` 무기록
@@ -483,7 +496,7 @@ cat .agent-state.json | jq '{status, phase, iteration, sessionId, sessionSerial,
483
496
  **사실이 아니었다.** 막고 있던 것은 기술적 불가능이 아니라 실행되지 않은 절차였다.
484
497
 
485
498
  - **에이전트 왕복 전체**: PLAN→CHECK(실패)→DO→CHECK(통과)→exit 0
486
- - **`acceptEdits` 실효**: 에이전트가 실제로 `docs/spec.md`·`tests/app.test.js`·`src/add.js` 를 썼다
499
+ - **`acceptEdits` 실효**: 에이전트가 실제로 `docs/SPEC.md`·`tests/app.test.js`·`src/add.js` 를 썼다
487
500
  - **세션 재개(`--resume`)**: 세션 파일 하나에 PLAN 프롬프트와 DO 프롬프트가 둘 다 들어 있다
488
501
  - **예산 상한**: `--budget-usd 1` → `maxLoops 5` 중 2회차에 중단, 누적 $1.61
489
502
  - **경로 회피**: 보호 대상 파일이 있는 디렉터리에서 원본 무수정, 해시 경로에 산출
@@ -507,7 +520,8 @@ cat .agent-state.json | jq '{status, phase, iteration, sessionId, sessionSerial,
507
520
  - **MCP 모드에서 실제 에이전트 spawn** — 도구 왕복(initialize/tools/call)만 확인
508
521
  - 장시간(10회) 실제 루프
509
522
  - 텔레그램 실제 발송
510
- - 레지스트리 경유 설치(`npm install -g @tuzi-ince/hi-loop`) 배포 후에만 가능
523
+ - 레지스트리에는 **0.4.0** 게시돼 있다 — 이번 세션의 변경(0.4.1: `.hi-loop/` 상태·대문자 규약·
524
+ MCP `checks`)을 레지스트리로 받으려면 **0.4.1 재게시**가 필요하다. 그전까지 최신은 로컬 설치(§2)로만.
511
525
 
512
526
  **재현 절차**
513
527
 
@@ -524,8 +538,8 @@ hi-loop run --goal "두 수를 더하는 add 함수를 만들어라" --test "npm
524
538
  > 그러면 "권한 모드가 되는가" 대신 "에이전트가 함정을 눈치채는가"를 측정하게 된다.
525
539
 
526
540
  > ⚠️ **일회용 디렉터리에서 돌려라.** 산출물 경로 회피가 기존 파일은 지켜주지만,
527
- > 작업 디렉터리 이탈 방지는 여전히 프롬프트 규칙뿐이다(design.md L8).
541
+ > 작업 디렉터리 이탈 방지는 여전히 프롬프트 규칙뿐이다(DESIGN.md L8).
528
542
 
529
- 확인할 것: (1) 파일이 실제로 쓰였는가, (2) `.agent-state.json` 의 `sessionId`·`costUsd`,
543
+ 확인할 것: (1) 파일이 실제로 쓰였는가, (2) `.hi-loop/STATE.json` 의 `sessionId`·`costUsd`,
530
544
  (3) 최종 exit code. 핸드오프까지 보려면 `--max-loops 5` + 에이전트가 못 고치는
531
545
  실패 명령(`--test "node -e 'process.exit(1)'"`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuzi-ince/hi-loop",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "description": "CLI와 MCP 모드를 지원하는 초경량 자율형 자가 치유 엔진",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,19 @@ user-invocable: true
46
46
  - `full: true` — 발굴·설계리뷰·코드리뷰를 포함한 전체 라이프사이클
47
47
  - `testCommand`: 프로젝트의 테스트 명령(기본 `npm test`)
48
48
  - 필요 시 `maxLoops`, `ship`, `watch`
49
+ - **UI 작업이면 e2e 를 엔진 게이트로 넣는다(권장 — LLM 기억에 의존하지 않는 결정론적 강제).**
50
+ goal 이 UI(화면·컴포넌트·라우팅·스타일)를 건드릴 것 같고 프로젝트에 **셸 e2e 명령**
51
+ (package.json `test:e2e` 스크립트, 또는 `playwright.config.*` → `npx playwright test`)이 있으면,
52
+ **먼저 사용자에게 e2e 게이트 여부를 묻고**, 켜기로 하면 `checks` 로 넘긴다:
53
+ ```json
54
+ "checks": [
55
+ { "cmd": "<기본 테스트, 예: npm test>" },
56
+ { "cmd": "<e2e 셸 명령>", "when": "<UI 글롭, 예: src/**/*.tsx>" }
57
+ ]
58
+ ```
59
+ → 엔진이 **UI 변경이 (커밋 전) 워킹트리에 있는 회차마다** e2e 를 돌리고, 통과해야 pass 로 인정한다(조용히 빠지지 않는다).
60
+ - 셸 e2e 명령이 없고 **Playwright MCP 만** 있으면 `checks` 를 쓰지 않는다(엔진은 MCP 도구를 못 부른다)
61
+ — 루프 통과 후 7단계에서 MCP 로 돌린다. **둘 다 없으면** e2e 는 스킵한다(도구를 임의 설치하지 않는다).
49
62
 
50
63
  5. **사용자 선택 처리.** 응답이 `⏸ 사용자의 선택이 필요합니다` 로 오면,
51
64
  그 질문을 **그대로 사용자에게 제시**하고 답을 받는다. 답을 `hiloop_answer`
@@ -54,27 +67,18 @@ user-invocable: true
54
67
  6. **결과 보고.** `✅ 통과` / `❌ 실패` 와 반복 횟수, 그리고 함께 온 Gaps(검증 한계)를
55
68
  사용자에게 전한다. false green(테스트만 초록이고 실제 미완)을 통과로 포장하지 않는다.
56
69
 
57
- 7. **UI 변경 e2e 게이트.** 문법·기능 테스트가 통과(✅)한 뒤, 이번 변경이
58
- **UI(프론트엔드 화면·컴포넌트·라우팅·스타일)** 건드렸는지 판단한다.
59
- - UI 변경이 **없으면** 단계를 건너뛴다(그대로 종료).
60
- - UI 변경이 **있으면** 사용자에게 **먼저 묻는다**: "UI 변경이 있었습니다. e2e 테스트를
61
- 진행할까요?" 임의로 실행하지 않는다.
62
-
63
- **e2e 실행 능력을 순서대로 탐지한다(강등 사다리).** e2e는 이미 통과한 루프의 *부가* 게이트다 —
64
- 없다고 루프를 실패로 만들지 않는다.
65
- 1. **Playwright MCP 도구가 세션에 있으면** 그걸로 실행한다(브라우저 구동·스크린샷 네이티브 지원).
66
- 2. 없지만 **프로젝트에 e2e 셋업이 있으면**(`@playwright/test` devDep + `test:e2e` 스크립트,
67
- 또는 `playwright.config.*`, 또는 `e2e` 스킬) 경로로 실행한다.
68
- 3. **둘 없으면** e2e 도구를 **임의로 설치하지 않는다** Playwright + 브라우저 바이너리는
69
- 수백 MB다. 사용자에게 상황을 알리고 선택지를 준다: (a) 설치 후 진행, (b) 이미 있는 다른
70
- 도구 사용, (c) 건너뛰기. 어느 쪽도 강요하지 않고, **이 게이트를 "스킵"으로 기록**한다.
71
-
72
- - e2e를 실제로 실행하기로 했으면 **결과 화면(스크린샷) 저장 여부도 사용자에게 묻는다**.
73
- 스크린샷은 **브라우저를 구동하는 도구(1·2)가 있을 때만** 가능하다 — 없으면 저장할 화면이
74
- 없음을 밝힌다.
75
- - 완료되면 **테스트 결과서와 스크린샷을 프로젝트의 test 폴더에 저장**한다
76
- (예: 리포트 `tests/e2e/report-<날짜시각>.md`, 스크린샷 `tests/e2e/screenshots/`).
77
- 날짜·시각은 실제 값으로 채운다. 저장 경로를 사용자에게 알린다.
70
+ 7. **e2e 후처리 (스크린샷·리포트, MCP 폴백).** 루프가 통과(✅)한 뒤, 4단계에서 정한 e2e 전략에 따라:
71
+ - **엔진 `checks` 로 돌린 경우**: e2e 는 이미 게이트로 통과했다(엔진이 강제). 결과 화면(스크린샷)
72
+ 저장이 필요하면 사용자에게 물어 저장한다. **여기서 다시 e2e 를 돌릴 필요는 없다.**
73
+ - **Playwright MCP 폴백 경우**(셸 e2e 명령이 없어 4단계에서 checks 못 건 경우): 사용자에게
74
+ "UI 변경이 있었습니다. 지금 Playwright MCP 로 e2e 를 돌릴까요?" 물어, 승인 시 MCP 로 실행한다.
75
+ - **스킵된 경우**(도구 없음): e2e 도구가 없어 스킵했음을 밝힌다 — 통과한 루프를 실패로 만들지 않는다.
76
+ - 실행한 경우 **결과서·스크린샷을 test 폴더에 저장**한다(예: 리포트 `tests/e2e/report-<날짜시각>.md`,
77
+ 스크린샷 `tests/e2e/screenshots/`). 날짜·시각은 실제 값으로 채우고 저장 경로를 알린다.
78
+ 스크린샷은 브라우저를 구동하는 도구(Playwright MCP/셸)가 있을 때만 가능하다.
79
+
80
+ > 이렇게 나누나: e2e "실행"은 결정론적 게이트라 **엔진(`checks`)**에 맡기는 최신 정설이다
81
+ > (프롬프트 지시는 "제안"이지 "강제"가 아니다). 엔진이 부르는 Playwright MCP LLM 이 후처리한다.
78
82
 
79
83
  8. **커밋 게이트 (git 워크플로).** 테스트가 통과(✅)하고 e2e 게이트까지 끝나면 `commitPolicy` 대로
80
84
  **로컬** 커밋한다. **push/PR 은 하지 않는다.** git 저장소가 아니면 건너뛴다.
@@ -89,7 +93,7 @@ user-invocable: true
89
93
  ## 정책 관리 (.hi-loop.json)
90
94
 
91
95
  브랜치·커밋 정책은 프로젝트 루트의 **커밋되는** `.hi-loop.json` 하나에 산다(팀 공유, 스킬·엔진 공용).
92
- `.agent-state.json`(임시·gitignore)과 다르다.
96
+ `.hi-loop/STATE.json`(임시·gitignore)과 다르다.
93
97
 
94
98
  ### 최초 1회 (지연 발동)
95
99
  코드변경 요청인데 `.hi-loop.json` 이 **없으면**, 진행 전에 딱 한 번 묻고 저장한다:
@@ -113,7 +117,7 @@ user-invocable: true
113
117
  우선순위: **이번 요청 지시 > `.hi-loop.json` > 기본값**.
114
118
 
115
119
  ## 보조 도구
116
- - `hiloop_status` — 현재 `.agent-state.json` 루프 상태 요약
120
+ - `hiloop_status` — 현재 `.hi-loop/STATE.json` 루프 상태 요약
117
121
  - `hiloop_rollback` — git 체크포인트로 파일 복원(회차 지정 가능)
118
122
  - `hiloop_reset` — 루프 상태 초기화
119
123
  - `hiloop_setup` — 프로젝트에 `.mcp.json`·`.gitignore`·`docs/`·`tests/`·CLAUDE.md 규칙 주입
@@ -145,7 +145,7 @@ export function parseRunOptions(args, { staged = {} } = {}) {
145
145
  designReview: triState('design-review', 'no-design-review'),
146
146
  discover: triState('discover', 'no-discover'),
147
147
  reconcile: triState('reconcile', 'no-reconcile'),
148
- // FR-21: 정합 게이트가 대조할 표준 문서 경로(기본 docs/spec.md). 지정하면 게이트가 켜진다.
148
+ // FR-21: 정합 게이트가 대조할 표준 문서 경로(기본 docs/DESIGN.md). 지정하면 게이트가 켜진다.
149
149
  reconcileSpec: str('reconcile-spec'),
150
150
  startFrom,
151
151
  stopAfter,
package/src/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 프로젝트 정책 저장소 `.hi-loop.json` (FR-16) — 브랜치·커밋 워크플로의 단일 진실 원천.
3
3
  *
4
- * `.agent-state.json`(임시·gitignore, goal 마다 리셋)과 **다르다**: 이 파일은 팀이 공유하는
4
+ * `.hi-loop/STATE.json`(임시·gitignore, goal 마다 리셋)과 **다르다**: 이 파일은 팀이 공유하는
5
5
  * **영속 정책**이라 커밋 대상이다(그래서 .gitignore 에 넣지 않는다).
6
6
  *
7
7
  * 스킬 층(대화형)과 엔진 층(헤드리스)이 같은 파일을 읽어 정책을 일관되게 적용한다.
package/src/lock.js CHANGED
@@ -12,13 +12,15 @@
12
12
  * - 락 해제는 finally 에서. 하지만 SIGKILL 은 finally 를 안 태우므로 stale 회수가
13
13
  * 최종 안전망이다.
14
14
  */
15
- import { writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
16
- import { join } from 'node:path';
15
+ import { writeFileSync, readFileSync, rmSync, existsSync, mkdirSync } from 'node:fs';
16
+ import { join, dirname } from 'node:path';
17
+ import { STATE_DIR } from './state.js';
17
18
 
18
- export const LOCK_FILENAME = '.agent-state.lock';
19
+ export const LOCK_FILENAME = 'STATE.lock'; // STATE_DIR 안의 락 파일
20
+ export const LEGACY_LOCK_FILENAME = '.agent-state.lock'; // 구버전 루트 락
19
21
 
20
22
  export function lockPathFor(cwd) {
21
- return join(cwd, LOCK_FILENAME);
23
+ return join(cwd, STATE_DIR, LOCK_FILENAME);
22
24
  }
23
25
 
24
26
  /** pid 가 살아 있는가. 신호 0 은 프로세스를 안 건드리고 존재만 확인한다. */
@@ -40,6 +42,10 @@ function isAlive(pid) {
40
42
  export function acquireLock(cwd, { pid = process.pid, now = () => new Date().toISOString() } = {}) {
41
43
  const path = lockPathFor(cwd);
42
44
  const payload = `${JSON.stringify({ pid, at: now() })}\n`;
45
+ mkdirSync(dirname(path), { recursive: true }); // .hi-loop/ 보장
46
+ // 구버전 루트 락(.agent-state.lock)이 남아 있으면 정리한다(새 락은 .hi-loop/ 안이다).
47
+ const legacy = join(cwd, LEGACY_LOCK_FILENAME);
48
+ if (existsSync(legacy)) rmSync(legacy, { force: true });
43
49
 
44
50
  const tryCreate = () => {
45
51
  writeFileSync(path, payload, { flag: 'wx' }); // wx: 이미 있으면 EEXIST
@@ -55,7 +61,7 @@ export function acquireLock(cwd, { pid = process.pid, now = () => new Date().toI
55
61
  if (holder && isAlive(holder.pid) && holder.pid !== pid) {
56
62
  throw new Error(
57
63
  `다른 hi-loop 루프가 이미 이 디렉터리에서 실행 중입니다 (pid ${holder.pid}). ` +
58
- `끝나길 기다리거나, 그 프로세스가 죽었다면 ${LOCK_FILENAME} 을 지우세요.`,
64
+ `끝나길 기다리거나, 그 프로세스가 죽었다면 ${path} 을 지우세요.`,
59
65
  );
60
66
  }
61
67
  // stale 이거나 우리 자신의 락 — 뺏어서 다시 만든다.
package/src/loop.js CHANGED
@@ -76,7 +76,7 @@ async function runLoopBody({
76
76
  maxDesignRounds = 2,
77
77
  discover = null,
78
78
  reconcile = null, // FR-21: 문서 정합 게이트. null=자동(--full 또는 reconcileSpec 지정 시 켜짐).
79
- reconcileSpec = null, // 대조할 표준 문서 경로. null=기본 docs/spec.md.
79
+ reconcileSpec = null, // 대조할 표준 문서 경로. null=기본 docs/DESIGN.md.
80
80
  full = false,
81
81
  // FR-15: 어느 경로로 들어와도 **같은 상태 머신**을 쓴다. 별도 코드 경로를 만들지 않는다
82
82
  // — 분기가 둘이면 반드시 한쪽이 썩는다.
@@ -197,13 +197,16 @@ async function runLoopBody({
197
197
  return { pause: true };
198
198
  };
199
199
 
200
- // FR-21: 문서 정합 게이트. 기존 표준 문서(docs/spec.md)가 있고 이번 요청이 그것과 별개
200
+ // FR-21: 문서 정합 게이트. 기존 표준 문서(docs/DESIGN.md)가 있고 이번 요청이 그것과 별개
201
201
  // 스펙으로 포크될 참이면, 조용히 갈라지기 전에 모순 여부를 판정한다. 모순이면 사람에게 묻는다.
202
- // 대조 대상: 명시(--reconcile-spec)가 최우선. 없으면 사람이 관리하는 표준 문서(docs/design.md)를
203
- // 우선하고, 그것도 없으면 docs/spec.md 폴백한다. spec.md PLAN 쓴 에이전트 산출물이라
204
- // 오라클로는 약하다 — 사람 문서가 있으면 그쪽이 옳다. (게이트 발동 자체는 여전히 opt-in 이다.)
202
+ // 대조 대상: 명시(--reconcile-spec)가 최우선. 없으면 사람 관리 표준 문서(docs/DESIGN.md)를
203
+ // 우선하고, 그다음 스펙 순으로 폴백한다. 대문자=정본, 소문자=구버전(대소문자 구분 FS 대비).
204
+ // spec 은 PLAN 이 쓴 에이전트 산출물이라 오라클로는 약하다 — 사람 문서가 있으면 그쪽이 옳다.
205
+ // (게이트 발동 자체는 여전히 opt-in 이다.)
206
+ const firstExisting = (...ps) => ps.find((p) => existsSync(join(cwd, p)));
205
207
  const STANDING_SPEC = reconcileSpec
206
- || (existsSync(join(cwd, 'docs/design.md')) ? 'docs/design.md' : 'docs/spec.md');
208
+ || firstExisting('docs/DESIGN.md', 'docs/DESIGN.md', 'docs/SPEC.md', 'docs/DESIGN.md')
209
+ || 'docs/DESIGN.md';
207
210
  if (reconcileEnabled && !state.reconciled && !state.specPinned
208
211
  && state.specPath !== STANDING_SPEC && existsSync(join(cwd, STANDING_SPEC))) {
209
212
  // 이미 답한 재개면 판정기를 다시 부르지 않는다(비용 0). gate 가 저장된 답을 돌려준다.
package/src/mcp-server.js CHANGED
@@ -43,6 +43,7 @@ export const tools = {
43
43
  spec,
44
44
  reconcile,
45
45
  reconcileSpec,
46
+ checks,
46
47
  cwd = defaultCwd(),
47
48
  }) => {
48
49
  if (!goal) return fail('goal 은 필수입니다.');
@@ -62,6 +63,9 @@ export const tools = {
62
63
  specPath: spec || null,
63
64
  reconcile: reconcile === undefined ? null : Boolean(reconcile),
64
65
  reconcileSpec: reconcileSpec || null,
66
+ // 조건부 검사(--check/--when 의 MCP 노출): 주면 testCommand 대신 이 배열이 판정 기준이 된다.
67
+ // when 글롭에 맞는 파일이 바뀐 회차에만 그 검사를 돌린다(UI 변경 회차에만 e2e 등).
68
+ checks: Array.isArray(checks) && checks.length ? checks : null,
65
69
  cwd,
66
70
  logger: log,
67
71
  });
@@ -100,11 +104,11 @@ export const tools = {
100
104
  },
101
105
  },
102
106
  hiloop_status: {
103
- description: '현재 워크스페이스의 .agent-state.json 루프 상태 요약을 반환한다.',
107
+ description: '현재 워크스페이스의 .hi-loop/STATE.json 루프 상태 요약을 반환한다.',
104
108
  handler: async ({ cwd = defaultCwd() } = {}) => text(summarizeState(loadState(statePathFor(cwd)))),
105
109
  },
106
110
  hiloop_reset: {
107
- description: '.agent-state.json 을 삭제해 루프 상태를 초기화한다.',
111
+ description: '.hi-loop/STATE.json 을 삭제해 루프 상태를 초기화한다.',
108
112
  handler: async ({ cwd = defaultCwd() } = {}) =>
109
113
  text(resetState(statePathFor(cwd)) ? '상태를 초기화했습니다.' : '초기화할 상태가 없습니다.'),
110
114
  },
@@ -195,7 +199,7 @@ export async function startMcpServer({ version = '0.1.0' } = {}) {
195
199
  spec: z
196
200
  .string()
197
201
  .optional()
198
- .describe('스펙 오라클을 이 파일로 고정한다(예: docs/spec.md). 사람이 관리하는 표준 문서를 여러 요청이 공유하는 오라클로 삼아 문서↔소스 drift 를 막는다. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다.'),
202
+ .describe('스펙 오라클을 이 파일로 고정한다(예: docs/DESIGN.md). 사람이 관리하는 표준 문서를 여러 요청이 공유하는 오라클로 삼아 문서↔소스 drift 를 막는다. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다.'),
199
203
  reconcile: z
200
204
  .boolean()
201
205
  .optional()
@@ -203,7 +207,16 @@ export async function startMcpServer({ version = '0.1.0' } = {}) {
203
207
  reconcileSpec: z
204
208
  .string()
205
209
  .optional()
206
- .describe('정합 게이트가 대조할 표준 문서 경로(기본 docs/spec.md). 예: docs/design.md. 지정하면 게이트가 켜진다.'),
210
+ .describe('정합 게이트가 대조할 표준 문서 경로(기본 docs/DESIGN.md). 예: docs/DESIGN.md. 지정하면 게이트가 켜진다.'),
211
+ checks: z
212
+ .array(z.object({ cmd: z.string(), when: z.string().optional() }))
213
+ .optional()
214
+ .describe(
215
+ '조건부 판정 검사. 주면 testCommand 대신 이 배열이 판정 기준이 된다(첫 항목에 기본 테스트를 넣을 것). ' +
216
+ 'when 글롭(예: "src/**/*.tsx")이 있으면 그 경로가 바뀐 회차에만 그 검사를 돌린다 — ' +
217
+ '"UI 를 고친 회차에만 e2e" 를 엔진이 강제하는 용도. e2e 는 셸 명령이어야 한다(예: {cmd:"npx playwright test", when:"src/**/*.tsx"}). ' +
218
+ 'short-circuit 하지 않아 유닛이 깨져도 e2e 결과를 같은 회차에 함께 본다.',
219
+ ),
207
220
  cwd: cwdSchema,
208
221
  },
209
222
  },
package/src/reconcile.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * 문서 정합 게이트 (FR-21) — 새 요청이 기존 표준 문서와 모순되는가.
3
3
  *
4
4
  * 기본 동작은 goal 마다 스펙을 새로 포크한다(state.planPathsFor). 그래서 단말적 요청이
5
- * 기존 `docs/spec.md` 와 어긋나도 조용히 갈라져 나갈 뿐, 아무도 그 모순을 보지 못한다 —
5
+ * 기존 `docs/DESIGN.md` 와 어긋나도 조용히 갈라져 나갈 뿐, 아무도 그 모순을 보지 못한다 —
6
6
  * 문서↔소스 갭의 근원이고, 그 갭이 환각의 빌미가 된다.
7
7
  *
8
8
  * 판정이 아니라 **표면화**다. 모순이면 상호배타 분기(ask, FR-16)로 멈춰 사람이 고른다:
package/src/resume.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * 재개 규칙 (FR-3.5) — 디스크의 과거를 이번 실행에 이어 붙일 때 **무엇까지 믿을 것인가**.
3
3
  *
4
4
  * state.js 에서 뺀 이유는 크기(NFR-5)만이 아니다. 이 파일이 다루는 것은 신뢰 경계다.
5
- * `.agent-state.json` 은 gitignore 돼 있고 에이전트의 쓰기 범위 안에 있다 — 게이트를 여는
5
+ * `.hi-loop/STATE.json` 은 gitignore 돼 있고 에이전트의 쓰기 범위 안에 있다 — 게이트를 여는
6
6
  * 값들이 에이전트가 만질 수 있는 파일에서 온다. 그 값을 어디까지 믿을지 정하는 규칙을
7
7
  * 저장·직렬화와 섞어두면 필드가 하나 늘 때 검증을 빠뜨리기 쉽다.
8
8
  */
package/src/seal.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 상태 봉인 (L16) — 게이트를 여는 값이 에이전트의 쓰기 범위 안에 있는 문제.
3
3
  *
4
- * `.agent-state.json` 은 cwd 에 있고 gitignore 돼 있으며, 에이전트는 그 cwd 에서
4
+ * `.hi-loop/STATE.json` 은 cwd 에 있고 gitignore 돼 있으며, 에이전트는 그 cwd 에서
5
5
  * `acceptEdits` 권한으로 돈다. 즉 **게이트의 기준선이 피고인이 편집할 수 있는 파일에 산다.**
6
6
  * 한 줄이면 된다:
7
7
  *
package/src/state.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * - 원자적 쓰기(tmp -> rename)
4
4
  * - 컨텍스트 다이어트: 저장 시 필드 길이를 강제로 자른다.
5
5
  */
6
- import { readFileSync, writeFileSync, renameSync, existsSync, rmSync } from 'node:fs';
7
- import { join, dirname } from 'node:path';
6
+ import { readFileSync, writeFileSync, renameSync, existsSync, rmSync, mkdirSync } from 'node:fs';
7
+ import { join, dirname, basename } from 'node:path';
8
8
  import { createHash } from 'node:crypto';
9
9
  import { loadOrCreateKey, sealOf } from './seal.js';
10
10
 
@@ -15,9 +15,14 @@ export { reportGaps, summarizeState } from './report.js';
15
15
  export { resumeOrCreate } from './resume.js';
16
16
 
17
17
  export const STATE_VERSION = 2;
18
- export const STATE_FILENAME = '.agent-state.json';
18
+ // 런타임 상태는 docs 가 아니라 전용 숨김 폴더 `.hi-loop/` 아래에 대문자 STATE.json 으로 둔다.
19
+ // (사람이 검토하는 문서 vs 내부 상태를 위치로 가른다.) 구 위치는 첫 로드 때 무손실 이관한다.
20
+ export const STATE_DIR = '.hi-loop';
21
+ export const STATE_FILENAME = 'STATE.json'; // STATE_DIR 안의 파일명
22
+ export const LEGACY_STATE_FILENAME = '.agent-state.json'; // 이관 대상(구버전 루트 파일)
19
23
 
20
- export const DEFAULT_SPEC_PATH = 'docs/spec.md';
24
+ // 사람이 관리하는 표준 문서는 **대문자**(필수·단일 진실 원천), 엔진 생성물은 소문자.
25
+ export const DEFAULT_SPEC_PATH = 'docs/SPEC.md';
21
26
  export const DEFAULT_TEST_PATH = 'tests/app.test.js';
22
27
 
23
28
  /**
@@ -70,7 +75,16 @@ export function errorSignature(text) {
70
75
  }
71
76
 
72
77
  export function statePathFor(cwd) {
73
- return join(cwd, STATE_FILENAME);
78
+ return join(cwd, STATE_DIR, STATE_FILENAME);
79
+ }
80
+
81
+ /** 새 상태 경로에서 구 위치(.agent-state.json)를 역산한다. 형태가 안 맞으면 null(이관 안 함). */
82
+ function legacyStatePathOf(path) {
83
+ const dir = dirname(path);
84
+ if (basename(path) === STATE_FILENAME && basename(dir) === STATE_DIR) {
85
+ return join(dirname(dir), LEGACY_STATE_FILENAME);
86
+ }
87
+ return null;
74
88
  }
75
89
 
76
90
  /**
@@ -210,7 +224,17 @@ export function migrateState(prev) {
210
224
  */
211
225
 
212
226
  export function loadState(path) {
213
- if (!existsSync(path)) return null;
227
+ if (!existsSync(path)) {
228
+ // 구 위치(.agent-state.json)가 있으면 새 위치(.hi-loop/STATE.json)로 무손실 이관한다.
229
+ // 이게 없으면 "업데이트했더니 진행 중이던 루프가 사라졌다"가 된다(FR-18.2 와 같은 이유).
230
+ const legacy = legacyStatePathOf(path);
231
+ if (legacy && existsSync(legacy)) {
232
+ mkdirSync(dirname(path), { recursive: true });
233
+ renameSync(legacy, path);
234
+ } else {
235
+ return null;
236
+ }
237
+ }
214
238
  try {
215
239
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
216
240
  if (!parsed) return null;
@@ -248,6 +272,7 @@ export function saveState(path, state, { now = new Date().toISOString() } = {})
248
272
  // 다음 로드에서 checked=false 로 나타나고 보고서가 밝힌다.
249
273
  const seal = sealOf(compact, loadOrCreateKey());
250
274
  if (seal) compact.seal = seal;
275
+ mkdirSync(dirname(path), { recursive: true }); // .hi-loop/ 이 없으면 만든다
251
276
  const tmp = join(dirname(path), `.${STATE_FILENAME}.${process.pid}.tmp`);
252
277
  writeFileSync(tmp, `${JSON.stringify(compact, null, 2)}\n`, 'utf8');
253
278
  renameSync(tmp, path); // 같은 디렉터리 내 rename == 원자적