@tuzi-ince/hi-loop 0.3.4 → 0.4.0

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-영향도-분석--변경-위험-파악)).
@@ -308,7 +308,7 @@ hi-loop run --goal "..." --start-from BUILD --stop-after CODE_REVIEW
308
308
 
309
309
  코드 변경 작업을 **보호 브랜치에서 분기 → 구현·테스트 → 로컬 커밋**으로 감싼다. push/PR 은 하지 않는다.
310
310
 
311
- **정책은 커밋되는 `.hi-loop.json` 한 파일**에 산다(팀 공유, 스킬·엔진 공용). `.agent-state.json`
311
+ **정책은 커밋되는 `.hi-loop.json` 한 파일**에 산다(팀 공유, 스킬·엔진 공용). `.hi-loop/STATE.json`
312
312
  (임시·gitignore)과 다르다. 기본값:
313
313
 
314
314
  ```json
@@ -350,7 +350,7 @@ hi-loop config remove-branch dev # 보호 브랜치 제거
350
350
  새 goal로 깨끗이 다시 시작하려면 상태 파일을 지운다:
351
351
 
352
352
  ```bash
353
- hi-loop reset # .agent-state.json 삭제 (CLI)
353
+ hi-loop reset # .hi-loop/STATE.json 삭제 (CLI)
354
354
  # MCP: hiloop_reset
355
355
  ```
356
356
 
@@ -363,7 +363,7 @@ hi-loop reset # .agent-state.json 삭제 (CLI)
363
363
  ### 세션 핸드오프 & 컨텍스트 다이어트
364
364
 
365
365
  에이전트는 같은 세션에서 오래 굴릴수록 토큰이 쌓여 멍청해진다. 그래서 4회마다 세션을
366
- **버리고**, `.agent-state.json` 에 압축된 상태(goal / 스펙 요약 / 마지막 에러 / 최근 이력 5건)만
366
+ **버리고**, `.hi-loop/STATE.json` 에 압축된 상태(goal / 스펙 요약 / 마지막 에러 / 최근 이력 5건)만
367
367
  새 세션에 넘겨 이어서 작업한다(auto-resume). 같은 goal로 다시 실행하면 중단 지점부터 재개한다.
368
368
 
369
369
  ### TDD 자가 치유
@@ -415,7 +415,7 @@ false green을 disclosed green으로 바꾼다.
415
415
  **막지 못한다** — 스펙 파일만 여러 개로 늘 뿐이다. `--spec <path>` 가 이 구멍을 닫는다.
416
416
 
417
417
  ```bash
418
- hi-loop run --goal "..." --spec docs/spec.md --verify-spec
418
+ hi-loop run --goal "..." --spec docs/SPEC.md --verify-spec
419
419
  ```
420
420
 
421
421
  - 스펙 오라클을 이 경로로 **고정**한다. DESIGN_REVIEW·CODE_REVIEW·`--verify-spec` 이 모두
@@ -438,9 +438,9 @@ hi-loop run --goal "결제에서 음수 잔액도 허용하라" --reconcile #
438
438
 
439
439
  - 기존 표준 문서가 있고 이번 요청이 그것과 별개 스펙으로 **포크될 참이면**, 포크 직전에
440
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는
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
444
  PLAN이 쓴 에이전트 산출물이라 오라클로 약하다 — 사람 문서가 있으면 그쪽이 옳다.)
445
445
  - **모순이면 멈추고 사람에게 묻는다**(상호배타 3지선다): **(a)** 표준 문서를 오라클로 고정하고
446
446
  진행 / **(b)** 새 스펙으로 분기(요청이 문서를 갱신) / **(c)** 중단하고 문서를 먼저 정리.
@@ -520,7 +520,7 @@ src/checks.js 다중 조건 검사 — --check/--when, short-circuit 없음
520
520
  src/blast.js 블라스트 반경 — 변경의 위험 분류를 공개
521
521
  src/report.js 완료 보고 — Gaps 공개 + 상태 요약
522
522
  src/cli-options.js CLI 인자 → runLoop 옵션 번역 (잘못된 값은 조용히 흘리지 않는다)
523
- src/state.js .agent-state.json (원자적 저장 + 다이어트 + 산출물 경로 + resume + Gaps)
523
+ src/state.js .hi-loop/STATE.json (원자적 저장 + 다이어트 + 산출물 경로 + resume + Gaps)
524
524
  src/runners.js 에이전트/테스트 실행 어댑터 (주입 가능) + 비용 파싱 + 타임아웃
525
525
  src/preflight.js 에이전트 실행 preflight + 실패 진단 (미설치·인증·키 → 행동지침)
526
526
  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). **차단 사유였던 "개발 환경 실행 가드"는 사실이 아니었다** — 막고 있던 것은 기술적 불가능이 아니라 실행되지 않은 절차였다 |
@@ -577,10 +577,10 @@ try/catch 로 감싼다. **텔레그램 장애로 빌드가 죽는 것은 설계
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자)",
@@ -223,7 +223,7 @@ runLoop({
223
223
  | tool | input | 동작 |
224
224
  |---|---|---|
225
225
  | `hiloop_run` | `goal`(필수), `testCommand?`, `maxLoops?`, `verifySpec?`, `spec?`, `reconcile?`, `cwd?` | 루프 실행 후 결과 텍스트 반환. `budgetUsd`·`stagnation`은 노출 안 함(CLI 전용) |
226
- | `hiloop_status` | `cwd?` | 현재 `.agent-state.json` 요약 반환 |
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" # 사람이 직접
@@ -91,12 +91,12 @@ hi-loop-setup # 실제 적용
91
91
  | 대상 | 동작 |
92
92
  |---|---|
93
93
  | `.mcp.json` | `hi-loop` MCP 서버 등록. **기존 다른 서버 항목은 보존** |
94
- | `.gitignore` | `.agent-state.json` · `.agent-state.lock` 추가 (중복 없이) |
94
+ | `.gitignore` | `.hi-loop/STATE.json` · `.hi-loop/STATE.lock` 추가 (중복 없이) |
95
95
  | `docs/`, `tests/` | 없으면 생성 |
96
- | `docs/design.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
96
+ | `docs/DESIGN.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
97
97
  | `CLAUDE.md` | 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침을 마커로 멱등 주입 |
98
98
 
99
- > `docs/design.md` 는 이번 프로젝트의 **오라클**이다 — `--reconcile`(정합 게이트)·`--spec`(스펙 고정)이
99
+ > `docs/DESIGN.md` 는 이번 프로젝트의 **오라클**이다 — `--reconcile`(정합 게이트)·`--spec`(스펙 고정)이
100
100
  > 이 문서를 기준으로 요청을 판정한다(§4-1 참조). 셋업 직후 이 문서를 채워두면, 이후 단말적 요청이
101
101
  > 문서와 어긋날 때 엔진이 잡아준다. 비어 있으면 정합 게이트는 사실상 아무것도 막지 못한다.
102
102
 
@@ -140,7 +140,7 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
140
140
  | `--verify-spec` | — | (꺼짐) | 통과 후 별도 검증자로 스펙 대비 구현을 심판 (하향 전용, 비용 증가) |
141
141
  | `--spec <path>` | — | (엔진이 결정) | 스펙 오라클을 이 문서로 **고정**. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다 |
142
142
  | `--reconcile` / `--no-reconcile` | — | `--full` 이면 켜짐 | **문서 정합 게이트**: 요청이 표준 문서와 모순되면 구현 전에 멈춰 사람에게 묻는다 |
143
- | `--reconcile-spec <path>` | — | design.md→spec.md | 정합 게이트가 대조할 문서 지정(지정하면 게이트가 켜진다) |
143
+ | `--reconcile-spec <path>` | — | DESIGN.md→SPEC.md | 정합 게이트가 대조할 문서 지정(지정하면 게이트가 켜진다) |
144
144
  | `--cwd` | `-c` | 현재 디렉터리 | 작업 대상 |
145
145
 
146
146
  > 세 상한의 관계: `--max-loops`(횟수) / `--budget-usd`(비용) / `--stagnation`(반복).
@@ -164,11 +164,11 @@ hi-loop --help
164
164
 
165
165
  > **문서↔소스 정합 (`--spec` / `--reconcile`)**: 기본적으로 이 엔진의 스펙·테스트는 에이전트
166
166
  > **자신이** 쓴 것이라, 통과가 "자기 테스트를 통과"에 그칠 수 있다(완료 보고의 Gaps 가 이를 공개한다).
167
- > 사람이 관리하는 표준 문서(`docs/design.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
168
- > - `--spec docs/design.md` — 그 문서를 스펙 오라클로 **고정**(리뷰·검증이 이 문서 기준).
167
+ > 사람이 관리하는 표준 문서(`docs/DESIGN.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
168
+ > - `--spec docs/DESIGN.md` — 그 문서를 스펙 오라클로 **고정**(리뷰·검증이 이 문서 기준).
169
169
  > - `--reconcile` — 새 요청이 그 문서와 **모순되면 구현 전에 멈춰** 묻는다(고정 / 분기 / 중단).
170
- > 대상은 `--reconcile-spec` 명시 → `docs/design.md` → `docs/spec.md` 순. `hi-loop-setup` 을 했다면
171
- > `--full`/`--reconcile` 만으로 design.md 가 자동 오라클이 된다.
170
+ > 대상은 `--reconcile-spec` 명시 → `docs/DESIGN.md` → `docs/SPEC.md` 순. `hi-loop-setup` 을 했다면
171
+ > `--full`/`--reconcile` 만으로 DESIGN.md 가 자동 오라클이 된다.
172
172
 
173
173
  ### 4-1b. 전과정 라이프사이클 (`--full`) 과 배포·감시
174
174
 
@@ -263,7 +263,7 @@ hi-loop rollback --cwd . # 작업 대상 지정
263
263
  | `hiloop_answer` | `choice`, `note`, `cwd` | 대기 중인 질문에 답하고 재개 가능 상태로 되돌림 |
264
264
  | `hiloop_status` | `cwd` | 진행 상태 조회 (장시간 루프를 들여다보는 창) |
265
265
  | `hiloop_rollback` | `to`, `cwd` | 체크포인트로 파일 복원 |
266
- | `hiloop_reset` | `cwd` | `.agent-state.json` 초기화 |
266
+ | `hiloop_reset` | `cwd` | `.hi-loop/STATE.json` 초기화 |
267
267
  | `hiloop_setup` | `dryRun`, `cwd` | 프로젝트 설정 주입 |
268
268
 
269
269
  > MCP `hiloop_run`은 CLI와 달리 `--budget-usd`·`--stagnation`을 노출하지 않는다.
@@ -449,22 +449,22 @@ npm install -g @tuzi-ince/hi-loop # 명령 이름은 그대로 hi-loop / hi-l
449
449
  | `API 키가 없거나 유효하지 않습니다 / 구독·크레딧 문제로 보입니다` | 에이전트 인증 문제. preflight 는 설치만 보고(비용 0), 인증은 첫 호출에서 감지된다 — `claude` 로그인 상태나 `ANTHROPIC_API_KEY` 를 확인 |
450
450
  | MCP 서버가 에이전트에 안 뜬다 | `.mcp.json` 경로가 실재하는지 확인(`hi-loop-setup` 재실행), 에이전트 재시작 |
451
451
  | 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` |
452
+ | 같은 실패를 10회 반복하고 끝난다 | 알려진 한계(DESIGN.md L2). `--max-loops` 를 낮춰 비용부터 막고 goal 을 더 구체적으로 |
453
+ | 테스트를 지워서 통과시켰다 | 알려진 한계(DESIGN.md L1). 프롬프트 규칙이 1차 방어선일 뿐 — `git diff tests/` 로 반드시 확인 |
454
+ | 이어서 하지 말고 처음부터 하고 싶다 | `rm .hi-loop/STATE.json` 또는 `hiloop_reset` |
455
455
  | `hi-loop` 를 옮긴 뒤 MCP 가 깨졌다 | `.mcp.json` 이 절대 경로라서 그렇다. `hi-loop-setup` 재실행 |
456
456
 
457
457
  상태 파일을 직접 들여다보는 게 가장 빠른 디버깅이다:
458
458
 
459
459
  ```bash
460
- cat .agent-state.json | jq '{status, phase, iteration, sessionId, sessionSerial, lastError}'
460
+ cat .hi-loop/STATE.json | jq '{status, phase, iteration, sessionId, sessionSerial, lastError}'
461
461
  ```
462
462
 
463
463
  ---
464
464
 
465
465
  ## 7. ⚠️ 이 가이드에서 검증된 것 / 안 된 것
466
466
 
467
- 정직하게 구분한다. (design.md §10 과 동일)
467
+ 정직하게 구분한다. (DESIGN.md §10 과 동일)
468
468
 
469
469
  **실제로 실행해 확인함**
470
470
 
@@ -483,7 +483,7 @@ cat .agent-state.json | jq '{status, phase, iteration, sessionId, sessionSerial,
483
483
  **사실이 아니었다.** 막고 있던 것은 기술적 불가능이 아니라 실행되지 않은 절차였다.
484
484
 
485
485
  - **에이전트 왕복 전체**: PLAN→CHECK(실패)→DO→CHECK(통과)→exit 0
486
- - **`acceptEdits` 실효**: 에이전트가 실제로 `docs/spec.md`·`tests/app.test.js`·`src/add.js` 를 썼다
486
+ - **`acceptEdits` 실효**: 에이전트가 실제로 `docs/SPEC.md`·`tests/app.test.js`·`src/add.js` 를 썼다
487
487
  - **세션 재개(`--resume`)**: 세션 파일 하나에 PLAN 프롬프트와 DO 프롬프트가 둘 다 들어 있다
488
488
  - **예산 상한**: `--budget-usd 1` → `maxLoops 5` 중 2회차에 중단, 누적 $1.61
489
489
  - **경로 회피**: 보호 대상 파일이 있는 디렉터리에서 원본 무수정, 해시 경로에 산출
@@ -524,8 +524,8 @@ hi-loop run --goal "두 수를 더하는 add 함수를 만들어라" --test "npm
524
524
  > 그러면 "권한 모드가 되는가" 대신 "에이전트가 함정을 눈치채는가"를 측정하게 된다.
525
525
 
526
526
  > ⚠️ **일회용 디렉터리에서 돌려라.** 산출물 경로 회피가 기존 파일은 지켜주지만,
527
- > 작업 디렉터리 이탈 방지는 여전히 프롬프트 규칙뿐이다(design.md L8).
527
+ > 작업 디렉터리 이탈 방지는 여전히 프롬프트 규칙뿐이다(DESIGN.md L8).
528
528
 
529
- 확인할 것: (1) 파일이 실제로 쓰였는가, (2) `.agent-state.json` 의 `sessionId`·`costUsd`,
529
+ 확인할 것: (1) 파일이 실제로 쓰였는가, (2) `.hi-loop/STATE.json` 의 `sessionId`·`costUsd`,
530
530
  (3) 최종 exit code. 핸드오프까지 보려면 `--max-loops 5` + 에이전트가 못 고치는
531
531
  실패 명령(`--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.0",
4
4
  "description": "CLI와 MCP 모드를 지원하는 초경량 자율형 자가 치유 엔진",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -89,7 +89,7 @@ user-invocable: true
89
89
  ## 정책 관리 (.hi-loop.json)
90
90
 
91
91
  브랜치·커밋 정책은 프로젝트 루트의 **커밋되는** `.hi-loop.json` 하나에 산다(팀 공유, 스킬·엔진 공용).
92
- `.agent-state.json`(임시·gitignore)과 다르다.
92
+ `.hi-loop/STATE.json`(임시·gitignore)과 다르다.
93
93
 
94
94
  ### 최초 1회 (지연 발동)
95
95
  코드변경 요청인데 `.hi-loop.json` 이 **없으면**, 진행 전에 딱 한 번 묻고 저장한다:
@@ -113,7 +113,7 @@ user-invocable: true
113
113
  우선순위: **이번 요청 지시 > `.hi-loop.json` > 기본값**.
114
114
 
115
115
  ## 보조 도구
116
- - `hiloop_status` — 현재 `.agent-state.json` 루프 상태 요약
116
+ - `hiloop_status` — 현재 `.hi-loop/STATE.json` 루프 상태 요약
117
117
  - `hiloop_rollback` — git 체크포인트로 파일 복원(회차 지정 가능)
118
118
  - `hiloop_reset` — 루프 상태 초기화
119
119
  - `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
@@ -100,11 +100,11 @@ export const tools = {
100
100
  },
101
101
  },
102
102
  hiloop_status: {
103
- description: '현재 워크스페이스의 .agent-state.json 루프 상태 요약을 반환한다.',
103
+ description: '현재 워크스페이스의 .hi-loop/STATE.json 루프 상태 요약을 반환한다.',
104
104
  handler: async ({ cwd = defaultCwd() } = {}) => text(summarizeState(loadState(statePathFor(cwd)))),
105
105
  },
106
106
  hiloop_reset: {
107
- description: '.agent-state.json 을 삭제해 루프 상태를 초기화한다.',
107
+ description: '.hi-loop/STATE.json 을 삭제해 루프 상태를 초기화한다.',
108
108
  handler: async ({ cwd = defaultCwd() } = {}) =>
109
109
  text(resetState(statePathFor(cwd)) ? '상태를 초기화했습니다.' : '초기화할 상태가 없습니다.'),
110
110
  },
@@ -195,7 +195,7 @@ export async function startMcpServer({ version = '0.1.0' } = {}) {
195
195
  spec: z
196
196
  .string()
197
197
  .optional()
198
- .describe('스펙 오라클을 이 파일로 고정한다(예: docs/spec.md). 사람이 관리하는 표준 문서를 여러 요청이 공유하는 오라클로 삼아 문서↔소스 drift 를 막는다. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다.'),
198
+ .describe('스펙 오라클을 이 파일로 고정한다(예: docs/DESIGN.md). 사람이 관리하는 표준 문서를 여러 요청이 공유하는 오라클로 삼아 문서↔소스 drift 를 막는다. 이미 내용이 있으면 PLAN 이 덮어쓰지 않는다.'),
199
199
  reconcile: z
200
200
  .boolean()
201
201
  .optional()
@@ -203,7 +203,7 @@ export async function startMcpServer({ version = '0.1.0' } = {}) {
203
203
  reconcileSpec: z
204
204
  .string()
205
205
  .optional()
206
- .describe('정합 게이트가 대조할 표준 문서 경로(기본 docs/spec.md). 예: docs/design.md. 지정하면 게이트가 켜진다.'),
206
+ .describe('정합 게이트가 대조할 표준 문서 경로(기본 docs/DESIGN.md). 예: docs/DESIGN.md. 지정하면 게이트가 켜진다.'),
207
207
  cwd: cwdSchema,
208
208
  },
209
209
  },
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 == 원자적