@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 +36 -18
- package/bin/hi-loop.js +2 -2
- package/bin/setup.js +13 -11
- package/docs/{design.md → DESIGN.md} +10 -10
- package/docs/{spec.md → SPEC.md} +23 -23
- package/docs/guide.md +46 -32
- package/package.json +1 -1
- package/skills/flow/SKILL.md +27 -23
- package/src/cli-options.js +1 -1
- package/src/config.js +1 -1
- package/src/lock.js +11 -5
- package/src/loop.js +9 -6
- package/src/mcp-server.js +17 -4
- package/src/reconcile.js +1 -1
- package/src/resume.js +1 -1
- package/src/seal.js +1 -1
- package/src/state.js +31 -6
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
날것의 아이디어(goal)를 던지면, AI 에이전트를 PDCA 루프로 반복 구동해
|
|
6
6
|
**테스트가 실제로 통과할 때까지** 스스로 고쳐 나간다.
|
|
7
7
|
|
|
8
|
-
- 요구사항(무엇을): [`docs/
|
|
9
|
-
- 설계(어떻게·왜): [`docs/
|
|
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/
|
|
36
|
+
hi-loop-setup # .mcp.json / .gitignore / docs·tests / CLAUDE.md / docs/DESIGN.md 자동 주입
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`hi-loop-setup` 은 **표준 설계 문서 `docs/
|
|
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 ──►
|
|
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/
|
|
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/
|
|
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/
|
|
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 로그인 폼 만들어줘` (슬래시 스킬) |
|
|
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` 한 파일**에 산다(팀 공유, 스킬·엔진 공용). `.
|
|
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 # .
|
|
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
|
-
**버리고**, `.
|
|
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/
|
|
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/
|
|
442
|
-
(setup이 만드는 사람 관리 표준 문서)** → 없으면 `docs/
|
|
443
|
-
`--full`/`--reconcile`만으로 별도 지정 없이 **
|
|
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 .
|
|
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/
|
|
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/
|
|
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 = '.
|
|
14
|
-
// 런타임
|
|
15
|
-
|
|
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/
|
|
39
|
-
요청이 문서와 어긋날 수 있으면 \`--reconcile-spec docs/
|
|
40
|
-
물음)로, 이 문서를 스펙 오라클로 고정하려면 \`--spec docs/
|
|
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/
|
|
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/
|
|
50
|
+
> - \`hi-loop run --goal "..." --reconcile-spec docs/DESIGN.md\` — 새 요청이 이 문서와 모순되면
|
|
50
51
|
> 구현 전에 멈춰 사람에게 묻는다(문서 고정 / 새 스펙 분기 / 중단).
|
|
51
|
-
> - \`hi-loop run --goal "..." --spec docs/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
`.
|
|
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) — `.
|
|
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
|
-
| 단위/수용 테스트
|
|
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/
|
|
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/
|
|
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
|
-
- 확인할 것: 파일이 실제로 쓰였는가 / `.
|
|
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
|
|
package/docs/{spec.md → SPEC.md}
RENAMED
|
@@ -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]` | `.
|
|
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}/.
|
|
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/
|
|
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
|
-
가로챌 수 없다(
|
|
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 에서 두 루프가 `.
|
|
125
|
-
덮어쓰지 못하게 락 파일(`.
|
|
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 루프 상태는 `.
|
|
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 `.
|
|
179
|
+
- FR-3.5 `.hi-loop/STATE.json`이 이미 존재하고 goal이 동일하면 **auto-resume**:
|
|
180
180
|
마지막 iteration 다음부터 이어서 진행한다. goal이 다르면 상태를 새로 만든다.
|
|
181
181
|
단 이전 상태가 이미 `passed`면 완료된 작업이므로 새 상태로 시작한다.
|
|
182
182
|
손상된(파싱 불가) 상태 파일도 새 상태로 취급한다.
|
|
183
183
|
|
|
184
|
-
`.
|
|
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/
|
|
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?` | 현재 `.
|
|
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`에 `.
|
|
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/
|
|
252
|
+
- FR-6.6 `docs/DESIGN.md` **표준 설계 문서**를 없으면 템플릿으로 만들고, 있으면 **절대 덮어쓰지
|
|
253
253
|
않는다**(사람이 관리하는 진실). FR-21 정합 게이트·FR-19 스펙 고정의 오라클 대상이다. PLAN 이
|
|
254
|
-
쓰는 `
|
|
254
|
+
쓰는 `SPEC.md` 와 달리 엔진은 이 파일을 손대지 않는다.
|
|
255
255
|
- FR-6.7 `CLAUDE.md` 에 hi-loop 워크플로 라우팅 지침을 마커(`<!-- hi-loop:begin/end -->`)로 감싸
|
|
256
|
-
멱등 주입한다. 코드 변경 루프가 `docs/
|
|
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/
|
|
434
|
-
- FR-21.2 대조 대상 문서의 우선순위: **① `--reconcile-spec <path>`(명시) → ② `docs/
|
|
435
|
-
(setup 이 만드는 사람 관리 표준 문서, FR-6.6) → ③ `docs/
|
|
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 | `.
|
|
474
|
+
| AC-8 | `.hi-loop/STATE.json`이 매 iteration 갱신되고 스키마를 만족한다 |
|
|
475
475
|
| AC-9 | 동일 goal로 재시작 시 이전 iteration 다음부터 resume |
|
|
476
476
|
| AC-10 | 다른 goal로 재시작 시 상태 리셋 |
|
|
477
|
-
| AC-11 | PLAN 프롬프트에
|
|
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`에 `.
|
|
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/
|
|
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
|
-
> 요구사항은 [`
|
|
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/
|
|
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
|
-
>
|
|
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 #
|
|
49
|
+
npm test # 전부 통과 확인
|
|
49
50
|
npm link # 전역에 hi-loop / hi-loop-setup 심볼릭 링크
|
|
50
|
-
hi-loop --version # 0.1
|
|
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.
|
|
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.
|
|
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` | `.
|
|
95
|
+
| `.gitignore` | `.hi-loop/STATE.json` · `.hi-loop/STATE.lock` 추가 (중복 없이) |
|
|
95
96
|
| `docs/`, `tests/` | 없으면 생성 |
|
|
96
|
-
| `docs/
|
|
97
|
+
| `docs/DESIGN.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
|
|
97
98
|
| `CLAUDE.md` | 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침을 마커로 멱등 주입 |
|
|
98
99
|
|
|
99
|
-
> `docs/
|
|
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>` | — |
|
|
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/
|
|
168
|
-
> - `--spec docs/
|
|
168
|
+
> 사람이 관리하는 표준 문서(`docs/DESIGN.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
|
|
169
|
+
> - `--spec docs/DESIGN.md` — 그 문서를 스펙 오라클로 **고정**(리뷰·검증이 이 문서 기준).
|
|
169
170
|
> - `--reconcile` — 새 요청이 그 문서와 **모순되면 구현 전에 멈춰** 묻는다(고정 / 분기 / 중단).
|
|
170
|
-
> 대상은 `--reconcile-spec` 명시 → `docs/
|
|
171
|
-
> `--full`/`--reconcile` 만으로
|
|
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` | `.
|
|
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 #
|
|
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
|
-
이 절차는 이론이 아니다. **소스에서는
|
|
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` 통과 (
|
|
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회 반복하고 끝난다 | 알려진 한계(
|
|
453
|
-
| 테스트를 지워서 통과시켰다 | 알려진 한계(
|
|
454
|
-
| 이어서 하지 말고 처음부터 하고 싶다 | `rm .
|
|
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 .
|
|
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
|
-
정직하게 구분한다. (
|
|
480
|
+
정직하게 구분한다. (DESIGN.md §10 과 동일)
|
|
468
481
|
|
|
469
482
|
**실제로 실행해 확인함**
|
|
470
483
|
|
|
471
|
-
- `npm test`
|
|
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
|
|
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/
|
|
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
|
-
-
|
|
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
|
-
> 작업 디렉터리 이탈 방지는 여전히 프롬프트 규칙뿐이다(
|
|
541
|
+
> 작업 디렉터리 이탈 방지는 여전히 프롬프트 규칙뿐이다(DESIGN.md L8).
|
|
528
542
|
|
|
529
|
-
확인할 것: (1) 파일이 실제로 쓰였는가, (2) `.
|
|
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
package/skills/flow/SKILL.md
CHANGED
|
@@ -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. **
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
`.
|
|
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` — 현재 `.
|
|
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 규칙 주입
|
package/src/cli-options.js
CHANGED
|
@@ -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/
|
|
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
|
-
* `.
|
|
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 = '.
|
|
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
|
-
`끝나길 기다리거나, 그 프로세스가 죽었다면 ${
|
|
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/
|
|
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/
|
|
200
|
+
// FR-21: 문서 정합 게이트. 기존 표준 문서(docs/DESIGN.md)가 있고 이번 요청이 그것과 별개
|
|
201
201
|
// 스펙으로 포크될 참이면, 조용히 갈라지기 전에 모순 여부를 판정한다. 모순이면 사람에게 묻는다.
|
|
202
|
-
// 대조 대상: 명시(--reconcile-spec)가 최우선. 없으면
|
|
203
|
-
// 우선하고,
|
|
204
|
-
// 오라클로는 약하다 — 사람 문서가 있으면 그쪽이 옳다.
|
|
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
|
-
|| (
|
|
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: '현재 워크스페이스의 .
|
|
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: '.
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
* `.
|
|
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
|
-
* `.
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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 == 원자적
|