@tuzi-ince/hi-loop 0.4.3 → 0.6.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 +44 -4
- package/bin/hi-loop.js +18 -5
- package/bin/setup.js +60 -20
- package/docs/DESIGN.md +11 -3
- package/docs/SPEC.md +65 -3
- package/docs/guide.md +125 -18
- package/package.json +1 -1
- package/skills/flow/SKILL.md +74 -31
- package/src/build.js +8 -1
- package/src/cli-options.js +2 -0
- package/src/docsync.js +100 -0
- package/src/loop.js +13 -13
- package/src/mcp-server.js +61 -62
- package/src/outcome.js +15 -1
- package/src/prompts.js +27 -0
- package/src/runners.js +29 -1
package/docs/guide.md
CHANGED
|
@@ -35,7 +35,7 @@ hi-loop run --goal "add 함수를 만들어라" --test "npm test" # 사람이
|
|
|
35
35
|
> ⚠️ **`npm install -g handoff` 를 하지 마라.** 무스코프 `handoff` 는 이 프로젝트와
|
|
36
36
|
> 무관한 **제3자의 redis lua 래퍼 패키지**(v0.1.3)다. 이 프로젝트는 **스코프 이름
|
|
37
37
|
> `@tuzi-ince/hi-loop`** 으로 npm 레지스트리에 게시돼 있다(현재 0.4.0). 스코프를 반드시 붙여라.
|
|
38
|
-
> 최신 소스(0.
|
|
38
|
+
> 최신 소스(0.6.0+)로 개발·검증하려면 아래 §2 의 로컬 설치를 쓴다.
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -48,7 +48,7 @@ git clone <이 저장소> hi-loop && cd hi-loop
|
|
|
48
48
|
npm install # @modelcontextprotocol/sdk, zod
|
|
49
49
|
npm test # 전부 통과 확인
|
|
50
50
|
npm link # 전역에 hi-loop / hi-loop-setup 심볼릭 링크
|
|
51
|
-
hi-loop --version # 0.
|
|
51
|
+
hi-loop --version # 0.6.0
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
해제: `npm unlink -g @tuzi-ince/hi-loop`
|
|
@@ -63,10 +63,10 @@ npm install -g /absolute/path/to/hi-loop
|
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
# 보내는 쪽
|
|
66
|
-
npm pack # tuzi-ince-hi-loop-0.
|
|
66
|
+
npm pack # tuzi-ince-hi-loop-0.6.0.tgz 생성 (~26kB)
|
|
67
67
|
|
|
68
68
|
# 받는 쪽
|
|
69
|
-
npm install -g ./tuzi-ince-hi-loop-0.
|
|
69
|
+
npm install -g ./tuzi-ince-hi-loop-0.6.0.tgz
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
### 2-D. 설치 없이 직접 실행
|
|
@@ -84,18 +84,20 @@ node /path/to/hi-loop/bin/hi-loop.js run --goal "..." --test "npm test"
|
|
|
84
84
|
```bash
|
|
85
85
|
cd /path/to/my-project
|
|
86
86
|
hi-loop-setup --dry-run # 무엇이 바뀔지 먼저 보기
|
|
87
|
-
hi-loop-setup # 실제 적용
|
|
87
|
+
hi-loop-setup # 실제 적용 (기본 --host claude)
|
|
88
|
+
hi-loop-setup --host cursor # Cursor 용 .cursor/mcp.json 에 등록
|
|
89
|
+
hi-loop-setup --host opencode # opencode 용 opencode.json 에 등록
|
|
88
90
|
```
|
|
89
91
|
|
|
90
|
-
하는
|
|
92
|
+
하는 일(기본 `--host claude`):
|
|
91
93
|
|
|
92
94
|
| 대상 | 동작 |
|
|
93
95
|
|---|---|
|
|
94
|
-
| `.mcp.json` | `hi-loop` MCP 서버 등록. **기존 다른 서버 항목은
|
|
96
|
+
| `.mcp.json` | `hi-loop` MCP 서버 등록. **기존 다른 서버 항목은 보존**. (`--host cursor`→`.cursor/mcp.json`, `--host opencode`→`opencode.json`) |
|
|
95
97
|
| `.gitignore` | `.hi-loop/STATE.json` · `.hi-loop/STATE.lock` 추가 (중복 없이) |
|
|
96
98
|
| `docs/`, `tests/` | 없으면 생성 |
|
|
97
99
|
| `docs/DESIGN.md` | **표준 설계 문서**를 템플릿으로 생성. **있으면 절대 덮어쓰지 않음**(사람이 관리하는 진실) |
|
|
98
|
-
| `CLAUDE.md` | 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침을 마커로 멱등
|
|
100
|
+
| `CLAUDE.md` | 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침을 마커로 멱등 주입. **`--host claude` 에서만** — 스킬·CLAUDE.md 라우팅은 Claude Code 전용이라 cursor/opencode 에선 건너뛴다(도구 직접 호출) |
|
|
99
101
|
|
|
100
102
|
> `docs/DESIGN.md` 는 이번 프로젝트의 **오라클**이다 — `--reconcile`(정합 게이트)·`--spec`(스펙 고정)이
|
|
101
103
|
> 이 문서를 기준으로 요청을 판정한다(§4-1 참조). 셋업 직후 이 문서를 채워두면, 이후 단말적 요청이
|
|
@@ -141,6 +143,7 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
|
|
|
141
143
|
| `--test` | `-t` | `npm test` | 성패를 판정할 명령 |
|
|
142
144
|
| `--max-loops` | `-m` | `10` | 최대 루프(=에이전트 호출) **횟수** 상한 (무인 환경 백스톱) |
|
|
143
145
|
| `--on-fail` | — | `heal` | CHECK 실패/불능 시: `heal`(자동 치유) / `ask`(사람에게 재시도·종료 물음) / `stop`(즉시 종료). 대화형·비싼 e2e 에선 `ask` 권장 |
|
|
146
|
+
| `--step` | — | (꺼짐) | 스텝 모드. 매 단위 작업(BUILD 회차·각 stage) 후 멈춰 exit 3 으로 돌려준다 — 재실행으로 이어감. 진행 가시성·타임아웃 제거 (§4-2 스텝 모드) |
|
|
144
147
|
| `--budget-usd` | — | (없음=무제한) | 누적 **비용** 상한(USD). 넘으면 다음 호출 전에 중단 |
|
|
145
148
|
| `--stagnation` | — | `3` | 같은 실패가 이 횟수 연속이면 조기 종료. `--no-stagnation` 으로 끔 |
|
|
146
149
|
| `--verify-spec` | — | (꺼짐) | 통과 후 별도 검증자로 스펙 대비 구현을 심판 (하향 전용, 비용 증가) |
|
|
@@ -158,10 +161,10 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
|
|
|
158
161
|
> 돈을 막으려면 `--budget-usd` 를 써라. 자세한 실측치는 §7.
|
|
159
162
|
|
|
160
163
|
exit code: `0` 통과 또는 `--stop-after` 로 정지 / `1` 실패(한도·예산 소진) /
|
|
161
|
-
`2` 잘못된 사용 / `3`
|
|
164
|
+
`2` 잘못된 사용 / `3` **종료가 아니라 재호출이 필요**(사람의 답을 기다리는 중 §4-1b, 또는 `--step`
|
|
165
|
+
스텝 완료 §4-2 — 둘 다 같은 goal 로 다시 실행하면 이어간다).
|
|
162
166
|
→ CI·스크립트에서 `hi-loop run … && echo OK` 로 바로 쓸 수 있다.
|
|
163
|
-
3 을 따로 둔 이유: 0 이면 `run && 배포` 가
|
|
164
|
-
1 이면 진짜 실패와 구분되지 않는다.
|
|
167
|
+
3 을 따로 둔 이유: 0 이면 `run && 배포` 가 미완인데 진행하고, 1 이면 진짜 실패와 구분되지 않는다.
|
|
165
168
|
|
|
166
169
|
```bash
|
|
167
170
|
hi-loop status # 현재 루프 상태 요약 (누적 비용·정체·대기 질문 포함)
|
|
@@ -277,16 +280,17 @@ hi-loop rollback --cwd . # 작업 대상 지정
|
|
|
277
280
|
### 4-2. MCP 서버 (클로드코드 / 커서) — **주 사용 경로 (권장)**
|
|
278
281
|
|
|
279
282
|
대부분의 사용은 CLI 가 아니라 **코드 어시스턴트(클로드코드·커서)가 MCP 도구로 hi-loop 을 부르는**
|
|
280
|
-
방식이다. `hi-loop-setup` 후 에이전트를 재시작하면 도구
|
|
283
|
+
방식이다. `hi-loop-setup` 후 에이전트를 재시작하면 도구 7개가 뜬다.
|
|
281
284
|
|
|
282
285
|
| 도구 | 인자 | 용도 |
|
|
283
286
|
|---|---|---|
|
|
284
|
-
| `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `spec`, `reconcile`, `reconcileSpec`, `checks`, `onFail`, `cwd` | 자가 치유(+전과정) 루프
|
|
287
|
+
| `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `step`, `spec`, `reconcile`, `reconcileSpec`, `checks`, `onFail`, `cwd` | 자가 치유(+전과정) 루프 실행. `step:true`=매 단위 후 `continue` 반환(대화형 권장) |
|
|
285
288
|
| `hiloop_answer` | `choice`, `note`, `cwd` | 대기 중인 질문에 답하고 재개 가능 상태로 되돌림 |
|
|
286
289
|
| `hiloop_status` | `cwd` | 진행 상태 조회 (장시간 루프를 들여다보는 창) |
|
|
287
290
|
| `hiloop_rollback` | `to`, `cwd` | 체크포인트로 파일 복원 |
|
|
288
291
|
| `hiloop_reset` | `cwd` | `.hi-loop/STATE.json` 초기화 |
|
|
289
292
|
| `hiloop_setup` | `dryRun`, `cwd` | 프로젝트 설정 주입 |
|
|
293
|
+
| `hiloop_docsync` | `cwd` | 소스 변경(git diff)을 문서에 반영하는 단발 패스(§4-2 doc-sync) |
|
|
290
294
|
|
|
291
295
|
#### 기본 흐름 — 실행 → 진행 → (필요 시) 멈춤·재개
|
|
292
296
|
|
|
@@ -326,6 +330,30 @@ e2e 처럼 오래 걸리는 루프도 클라이언트 유휴 타임아웃(기본
|
|
|
326
330
|
> MCP `hiloop_run`은 CLI와 달리 `--budget-usd`·`--stagnation`을 노출하지 않는다.
|
|
327
331
|
> 예산 상한이 꼭 필요하면 CLI(`hi-loop run`)를 쓴다. 정체 감지는 기본값(3회)으로 동작한다.
|
|
328
332
|
|
|
333
|
+
#### 스텝 모드 — LLM 이 루프를 몰고, 매 스텝이 보인다 (`step: true`, 대화형 권장)
|
|
334
|
+
|
|
335
|
+
기본 `hiloop_run` 은 **한 콜 안에서 엔진이 루프 전체**를 돈다(모델 A). 무인 실행엔 좋지만 대화형에선
|
|
336
|
+
세 가지가 불편하다: ① 콜이 반환될 때까지 **진행이 콘솔에 안 보이고**(바깥 LLM 이 정지), ② 한 콜이
|
|
337
|
+
길어 **타임아웃** 위험, ③ 바깥 LLM + 서브프로세스 claude 로 **에이전트를 두 번** 쓴다.
|
|
338
|
+
|
|
339
|
+
`step: true` 를 주면 **매 단위 작업(BUILD 한 회차 · 각 stage) 후 제어를 LLM 에게 돌려준다**(모델 B):
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
hiloop_run({ goal, step: true })
|
|
343
|
+
└─▶ ⏭ 스텝 완료 (PLAN) — 1회차. 아직 미완 → 같은 goal 로 다시 호출
|
|
344
|
+
LLM 이 이 진행을 사용자에게 알리고 hiloop_run 을 다시 호출
|
|
345
|
+
└─▶ ⏭ 스텝 완료 (DO) — 2회차 …
|
|
346
|
+
└─▶ ✅ 통과 (또는 ❌ 실패 / ⏸ awaiting)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- **매 스텝이 콘솔에 보인다** — LLM 이 스텝마다 제어를 돌려받아 사용자에게 중계한다.
|
|
350
|
+
- **타임아웃이 없다** — 콜당 에이전트 1콜뿐이라 짧다.
|
|
351
|
+
- **재개는 자동** — 상태가 STATE.json 에 저장돼 있어 재호출이 정확히 다음 스텝을 이어간다.
|
|
352
|
+
- 통과 판정은 그대로 엄격하다 — `continue` 는 통과가 아니며, false green 게이트를 우회하지 않는다.
|
|
353
|
+
|
|
354
|
+
`flow` 스킬은 대화형에서 이 모드를 기본으로 쓴다. CLI 는 `--step`(스텝 후 exit 3, 재실행으로 이어감).
|
|
355
|
+
무인·완료까지 자동으로 돌릴 땐 `step` 을 빼면 종전 모델 A 그대로다.
|
|
356
|
+
|
|
329
357
|
> **UI 변경 회차에만 e2e 를 엔진이 강제하기 (`checks`).** `checks: [{cmd, when?}]` 를 주면
|
|
330
358
|
> `testCommand` 대신 이 배열이 판정 기준이 되고, `when` 글롭에 맞는 파일이 바뀐 회차에만 그 검사를
|
|
331
359
|
> 돌린다. e2e 를 스킬의 자연어 지시(사람이 기억해서 실행)가 아니라 **엔진이 결정론적으로 강제**하는 길이다:
|
|
@@ -336,7 +364,9 @@ e2e 처럼 오래 걸리는 루프도 클라이언트 유휴 타임아웃(기본
|
|
|
336
364
|
> ]
|
|
337
365
|
> ```
|
|
338
366
|
> e2e 는 **셸 명령**이어야 한다(엔진은 Playwright MCP 도구를 부를 수 없다 — 그 경우는 스킬이 루프 후
|
|
339
|
-
> MCP 로 돌린다). `
|
|
367
|
+
> MCP 로 돌린다). `checks` 엔진 게이트는 **opt-in**이다: `flow` 스킬은 기본적으로 시작 시 e2e 를 걸지
|
|
368
|
+
> 않고 **루프 통과 후 UI 변경이 있을 때** e2e 여부를 한 번 묻는다. 매 회차 결정론적 강제가 필요할 때
|
|
369
|
+
> (무인·CI, 또는 "매 회차 e2e 로 막아줘"라고 명시)만 이 `checks` 를 시작 시 건다.
|
|
340
370
|
|
|
341
371
|
> **cwd 기본값은 `CLAUDE_PROJECT_DIR || process.cwd()`.** MCP 는 호스트(클로드코드/커서)가 서버를
|
|
342
372
|
> 띄우는 경로라, "어느 프로젝트인가"의 정답은 호스트가 주입하는 `CLAUDE_PROJECT_DIR` 다. 각 도구에
|
|
@@ -345,12 +375,88 @@ e2e 처럼 오래 걸리는 루프도 클라이언트 유휴 타임아웃(기본
|
|
|
345
375
|
|
|
346
376
|
수동 기동: `hi-loop mcp` (인자 없이 `hi-loop` 만 쳐도 서버로 뜬다)
|
|
347
377
|
|
|
378
|
+
#### `/flow` 스킬 — 단계 지정 (`/flow plan`, `/flow do` …)
|
|
379
|
+
|
|
380
|
+
Claude Code 에선 `flow` 스킬을 슬래시로 부른다(`/hi-loop:flow`, 또는 bare `/flow`). **첫 단어에 단계어**를
|
|
381
|
+
붙이면 그 단계까지만 돌고 멈춰 **다음 단계를 제안**한다(bkit 의 `/pdca plan|do` 와 유사). 단계어가 없으면
|
|
382
|
+
완료까지 돈다. 스킬이 단계어를 엔진의 `stopAfter` 로 옮긴다:
|
|
383
|
+
|
|
384
|
+
| 입력 | 동작 |
|
|
385
|
+
|---|---|
|
|
386
|
+
| `/flow <요청>` | 완료까지 TDD 반복(+UI면 e2e 제안) |
|
|
387
|
+
| `/flow plan <요청>` (=`spec`) | 스펙+테스트만 만들고 멈춤 → 구현 제안 |
|
|
388
|
+
| `/flow do <요청>` (=`build`) | 테스트 통과까지 구현(검증·치유 포함) → 리뷰/배포 제안 |
|
|
389
|
+
| `/flow discover`·`review`·`ship`·`watch` | 각 단계까지 |
|
|
390
|
+
|
|
391
|
+
> ⚠️ hi-loop 은 **DO→CHECK→ACT(HEAL) 를 하나의 자가치유 BUILD 루프로 합친다** — 검증(CHECK)은 엔진이
|
|
392
|
+
> 매 회차 자동 판정하므로 bkit 처럼 `check` 가 **별도 단계가 아니다**. `do`(=build) 하나가 "구현→검증→치유를
|
|
393
|
+
> 통과까지" 담당한다. 그래서 단계어는 바깥 단계(discover/plan/do/review/ship/watch)뿐이다.
|
|
394
|
+
|
|
395
|
+
CLI 에는 같은 단계가 서브커맨드로 있다: `hi-loop plan|discover|review|ship|watch`(각각 `--stop-after` 상당).
|
|
396
|
+
|
|
397
|
+
#### 문서↔소스 맞추기 — `hiloop_docsync` / `/flow doc-sync` / `hi-loop docsync`
|
|
398
|
+
|
|
399
|
+
소스를 바꾼 뒤 **관련 문서를 소스에 맞추는 단발 패스**다. 자가 치유 루프가 아니라 한 번의
|
|
400
|
+
에이전트 패스라 짧고(타임아웃 없음), 진행 알림을 보낸다. 동작:
|
|
401
|
+
|
|
402
|
+
1. `git diff`로 **소스(비-문서) 변경**을 읽는다(커밋 안 된 변경 우선, 없으면 마지막 커밋). 소스 변경이 없으면 아무것도 안 한다.
|
|
403
|
+
2. 일꾼 에이전트가 그 diff 에 맞춰 **문서만** 갱신한다 — `README.md`·`docs/*.md`.
|
|
404
|
+
3. **`docs/DESIGN.md`(사람 오라클)와 소스·테스트는 건드리지 않는다.** 문서 외 파일이 바뀌면 `stray` 로 경고한다.
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
hiloop_docsync({ cwd })
|
|
408
|
+
🔎 소스 변경 대조 중… → 📝 문서 갱신 중… → ✅ 문서 2개 갱신: README.md, docs/guide.md
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
> diff 로 확인되는 것만 반영한다(없는 내용을 지어내면 그게 환각이다). 갱신 후 어시스턴트가
|
|
412
|
+
> `git diff <문서>`로 실제 변경을 보여주고, 커밋은 사람이 판단한다(문서를 소스와 **같은 변경**으로 커밋).
|
|
413
|
+
> CLI: `hi-loop docsync`. 스킬: `/flow doc-sync`(goal 불필요).
|
|
414
|
+
|
|
415
|
+
#### 다른 MCP 호스트에 등록하기 (Cursor / opencode / Cline …)
|
|
416
|
+
|
|
417
|
+
hi-loop 의 MCP 서버는 **표준 stdio MCP 서버**라 MCP 를 지원하는 어떤 호스트에든 등록된다.
|
|
418
|
+
`hi-loop-setup --host <호스트>` 가 호스트별 설정 파일에 알아서 써 준다:
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
hi-loop-setup --host cursor # → .cursor/mcp.json (Claude 와 같은 mcpServers 스키마)
|
|
422
|
+
hi-loop-setup --host opencode # → opencode.json (mcp/local 스키마)
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
수동으로 넣고 싶으면 (또는 다른 호스트) 아래 형태를 그 호스트 설정에 직접 추가한다:
|
|
426
|
+
|
|
427
|
+
```jsonc
|
|
428
|
+
// opencode — opencode.json
|
|
429
|
+
{ "mcp": { "hi-loop": { "type": "local", "command": ["hi-loop", "mcp"], "environment": {} } } }
|
|
430
|
+
|
|
431
|
+
// Cursor — .cursor/mcp.json (Claude Code 의 .mcp.json 과 같은 스키마)
|
|
432
|
+
{ "mcpServers": { "hi-loop": { "command": "hi-loop", "args": ["mcp"] } } }
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
전역 설치가 안 됐으면 `command` 를 절대 경로로: `node /path/to/hi-loop/bin/hi-loop.js mcp`
|
|
436
|
+
(`--host` 로 생성하면 현재 설치본의 절대 경로가 자동으로 박힌다).
|
|
437
|
+
|
|
438
|
+
> ⚠️ **중요 — 루프의 실제 일꾼은 여전히 `claude` 다.** hi-loop 도구는 어느 호스트에서든 뜨지만,
|
|
439
|
+
> 엔진은 구현 에이전트로 **`claude -p …` 를 서브프로세스로 부른다**(기본값). 즉 opencode 안에서 hi-loop 을
|
|
440
|
+
> 써도 코딩은 claude 가 한다(claude 설치·인증 필요). 다른 에이전트 CLI 를 루프의 일꾼으로 쓰려면
|
|
441
|
+
> `HILOOP_AGENT_CMD` + **에이전트 어댑터**(§4-3의 `HILOOP_AGENT_PROVIDER`)를 함께 준다. 예: opencode 를
|
|
442
|
+
> 일꾼으로 —
|
|
443
|
+
> ```bash
|
|
444
|
+
> HILOOP_AGENT_PROVIDER=generic HILOOP_AGENT_CMD=opencode HILOOP_AGENT_ARGS=run
|
|
445
|
+
> # → 루프가 매 회차 `opencode run "<프롬프트>"` 를 부르고 stdout 을 결과로 읽는다.
|
|
446
|
+
> ```
|
|
447
|
+
> `generic` 어댑터는 **세션 재개·비용 집계를 지원하지 않는다**(claude 전용 계약이라 매 회차 새 세션).
|
|
448
|
+
> 기능이 온전한 기본 경로는 claude 이며, generic 은 이식용 최소 계약이다.
|
|
449
|
+
>
|
|
450
|
+
> 또한 **자동 라우팅(스킬 `flow`·CLAUDE.md)은 Claude Code 전용**이다. 다른 호스트에서는 `hiloop_run`
|
|
451
|
+
> 같은 **도구를 직접 호출**한다(도구 동작은 동일).
|
|
452
|
+
|
|
348
453
|
### 4-3. 환경변수
|
|
349
454
|
|
|
350
455
|
| 변수 | 기본값 | 설명 |
|
|
351
456
|
|---|---|---|
|
|
352
457
|
| `HILOOP_AGENT_CMD` | `claude` | 에이전트 실행 명령. 다른 CLI 로 교체 가능 |
|
|
353
|
-
| `
|
|
458
|
+
| `HILOOP_AGENT_PROVIDER` | `claude` | 에이전트 CLI 계약. `claude`(세션·비용 지원) 또는 `generic`(프롬프트=마지막 위치인자, stdout=결과, 세션·비용 없음). 미상값은 `claude` 로 되돌림 |
|
|
459
|
+
| `HILOOP_AGENT_ARGS` | (없음) | 에이전트에 덧붙일 인자 (공백 구분). `generic` 에선 프롬프트 **앞** 서브커맨드로 쓰임(예: `run`) |
|
|
354
460
|
| `HILOOP_PERMISSION_MODE` | `acceptEdits` | 비대화형 편집 허용용. `bypassPermissions` 등으로 확대/축소 |
|
|
355
461
|
| `HILOOP_VERIFY_CMD` | (`HILOOP_AGENT_CMD`→`claude`) | `--verify-spec` 검증자 실행 명령. 구현자와 다른 CLI로 검증 가능 |
|
|
356
462
|
| `HILOOP_VERIFY_MODEL` | (구현자와 동일) | `--verify-spec` 검증자 모델 |
|
|
@@ -541,7 +647,7 @@ cat .hi-loop/STATE.json | jq '{status, phase, iteration, sessionId, sessionSeria
|
|
|
541
647
|
- `npm pack` → tarball 생성 (bin/src/docs/README 포함)
|
|
542
648
|
- **`npm install -g --prefix /tmp/lev-prefix ./tarball` 로 진짜 설치 → 설치본 실행 확인**
|
|
543
649
|
- `bin/hi-loop` 가 심링크로 생성됨을 `ls -l` 로 확인
|
|
544
|
-
- **심링크 bin 경유** `hi-loop --version` → `0.
|
|
650
|
+
- **심링크 bin 경유** `hi-loop --version` → `0.6.0`, `run`(goal 없음) → exit 2
|
|
545
651
|
- 설치본으로 전체 루프 E2E: PLAN→실패→DO→통과→exit 0
|
|
546
652
|
- 설치본 `hi-loop-setup` → `.mcp.json` 이 설치 위치를 정확히 가리킴
|
|
547
653
|
- `hi-loop-setup` 멱등, `--dry-run` 무기록
|
|
@@ -576,8 +682,9 @@ cat .hi-loop/STATE.json | jq '{status, phase, iteration, sessionId, sessionSeria
|
|
|
576
682
|
- **MCP 모드에서 실제 에이전트 spawn** — 도구 왕복(initialize/tools/call)만 확인
|
|
577
683
|
- 장시간(10회) 실제 루프
|
|
578
684
|
- 텔레그램 실제 발송
|
|
579
|
-
- 레지스트리에는 **0.4.0** 이 게시돼 있다 —
|
|
580
|
-
MCP
|
|
685
|
+
- 레지스트리에는 **0.4.0** 이 게시돼 있다 — 이후 변경(`.hi-loop/` 상태·대문자 규약·MCP `checks`·
|
|
686
|
+
`--on-fail`·MCP 진행 알림·에이전트 프로바이더 어댑터·스텝 모드·flow 라우팅 수정·`/flow` 단계어·doc-sync,
|
|
687
|
+
현재 **0.6.0**)을 레지스트리로 받으려면 **0.6.0 재게시**가 필요하다. 그전까지 최신은 로컬 설치(§2)로만.
|
|
581
688
|
|
|
582
689
|
**재현 절차**
|
|
583
690
|
|
package/package.json
CHANGED
package/skills/flow/SKILL.md
CHANGED
|
@@ -3,10 +3,12 @@ name: flow
|
|
|
3
3
|
description: >-
|
|
4
4
|
기획·설계가 필요한 기능 개발/개선 요청을 소스만 보고 바로 구현하지 말고, PLAN→DESIGN→DO→CHECK→HEAL
|
|
5
5
|
자율 루프(hiloop_run)로 처리한다. spec/설계를 먼저 세우고 테스트가 통과할 때까지 반복하며, full=true면
|
|
6
|
-
발굴·설계리뷰·코드리뷰까지 전체 라이프사이클을 돈다.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
PDCA
|
|
6
|
+
발굴·설계리뷰·코드리뷰까지 전체 라이프사이클을 돈다. 인자 첫 단어로 단계를 지정할 수 있다
|
|
7
|
+
(예: `/flow plan …`=스펙까지, `/flow do …`=구현까지) — 그 단계에서 멈춰 다음을 제안한다. Use this
|
|
8
|
+
whenever the user asks to build or improve a feature that benefits from planning/design before coding,
|
|
9
|
+
or mentions the hi-loop/PDCA/TDD lifecycle, or invokes a stage like plan/spec/do/build/review/ship.
|
|
10
|
+
Triggers: 기획, 설계, 구현, 개선, 기능 추가, 리팩터, plan, spec, design, do, build, implement, feature,
|
|
11
|
+
improve, refactor, review, ship, PDCA, TDD, lifecycle, 자가치유, self-healing, hi-loop, hiloop.
|
|
10
12
|
user-invocable: true
|
|
11
13
|
---
|
|
12
14
|
|
|
@@ -20,10 +22,43 @@ user-invocable: true
|
|
|
20
22
|
- 사용자가 hi-loop / PDCA / TDD 라이프사이클을 명시했을 때.
|
|
21
23
|
- 예외(루프 없이 바로 처리): 오타·1줄 수정·문구 변경 같은 사소한 작업.
|
|
22
24
|
|
|
25
|
+
## `/flow` 인자 — 단계 지정 (선택)
|
|
26
|
+
|
|
27
|
+
인자의 **첫 단어**가 단계어면 그 단계까지만 돌리고 멈춰 **다음 단계를 제안**한다(나머지 단어가 goal).
|
|
28
|
+
단계어가 없으면 인자 전체가 goal 이고 **완료까지** 돈다. `hiloop_run` 은 항상 `step: true` 로 부른다.
|
|
29
|
+
|
|
30
|
+
| 입력 | 별칭 | `hiloop_run` 인자 | 동작 |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| `/flow <요청>` | (단계어 없음) | `full:true, step:true` | 완료까지 TDD 반복. UI면 e2e 제안(4·7단계) |
|
|
33
|
+
| `/flow plan <요청>` | `spec`, `설계` | `stopAfter:"PLAN", step:true` | 스펙+테스트만 만들고 멈춤 → **구현** 제안 |
|
|
34
|
+
| `/flow do <요청>` | `build`, `구현` | `stopAfter:"BUILD", step:true` | 테스트 통과까지 구현(검증·치유 포함) → **리뷰/배포** 제안 |
|
|
35
|
+
| `/flow discover <요청>` | `발굴` | `full:true, stopAfter:"DISCOVER", step:true` | 요구 발굴만 → **설계(plan)** 제안 |
|
|
36
|
+
| `/flow review <요청>` | `리뷰` | `full:true, stopAfter:"CODE_REVIEW", step:true` | 구현→코드리뷰까지 → **배포** 제안 |
|
|
37
|
+
| `/flow ship <요청>` | `배포` | `ship:"<명령>", stopAfter:"SHIP", step:true` | 배포까지(배포 명령 없으면 먼저 물음) |
|
|
38
|
+
| `/flow watch <요청>` | `감시` | `watch:"<명령>", stopAfter:"WATCH"` | 배포 후 헬스체크까지 |
|
|
39
|
+
| `/flow doc-sync` | `docsync`, `문서동기화` | **`hiloop_docsync` 호출**(hiloop_run 아님) | 소스 변경(git diff)을 문서에 반영하는 단발 패스 |
|
|
40
|
+
|
|
41
|
+
> **`doc-sync` 는 특수 단계어**다: 루프(`hiloop_run`)가 아니라 **`hiloop_docsync`** 를 부른다.
|
|
42
|
+
> 소스 변경 후 문서↔소스를 맞추는 단발 작업이라 goal·테스트가 없다. 결과로 온 "갱신 문서 목록"을
|
|
43
|
+
> 사용자에게 알리고, 필요하면 `git diff <문서>` 로 실제 변경을 보여준다. `stray`(문서 외 파일 변경)가
|
|
44
|
+
> 보고되면 **경고로 표면화**한다 — doc-sync 는 소스·`docs/DESIGN.md` 를 건드리면 안 된다.
|
|
45
|
+
|
|
46
|
+
> ⚠️ **hi-loop 의 단계는 bkit 의 이산 PDCA 와 다르다.** hi-loop 은 **DO→CHECK→ACT(HEAL) 를 하나의
|
|
47
|
+
> 자가치유 BUILD 루프로 합친다** — 검증(CHECK)은 엔진이 매 회차 testCommand 로 자동 판정하므로
|
|
48
|
+
> `check` 는 별도 단계가 아니다. 그래서 단계어는 바깥 단계(discover/plan/do/review/ship/watch)만 있고,
|
|
49
|
+
> `do`(=build) 하나가 "구현→검증→치유를 통과까지" 전부 담당한다.
|
|
50
|
+
|
|
51
|
+
- **이어가기(resume).** 단계어로 멈춘 뒤 사용자가 다음을 승인하면, **같은 goal 로 `hiloop_run` 을 다시
|
|
52
|
+
호출**한다 — 상태가 저장돼 있어 멈춘 지점부터 이어간다. 다음 단계까지만 가려면 그 단계의 `stopAfter` 를,
|
|
53
|
+
끝까지 가려면 `stopAfter` 를 빼고 부른다. 인자에 goal 없이 단계어만 오면(`/flow do`) 진행 중인 goal 을 잇는다.
|
|
54
|
+
- 멈춘 응답(`⏹ … 까지 진행 후 정지`)이 오면 **무엇이 만들어졌는지(스펙/테스트 경로·구현 결과)를 요약**하고,
|
|
55
|
+
**다음 단계를 구체적 예/아니오로 제안**한다: "다음: 구현(DO) 진행할까요?".
|
|
56
|
+
|
|
23
57
|
## 실행 절차
|
|
24
58
|
|
|
25
|
-
1. **목표(goal) 확정.**
|
|
26
|
-
|
|
59
|
+
1. **목표(goal)·단계 확정.** 먼저 인자의 첫 단어가 **단계어**(plan/spec/do/build/discover/review/ship/watch/doc-sync,
|
|
60
|
+
위 "인자 문법" 표)인지 본다 — 맞으면 그 단계로 범위를 좁히고 나머지를 goal 로, 아니면 인자 전체를 goal 로
|
|
61
|
+
한다(`doc-sync` 는 goal 이 필요 없다). 요청을 한 문장의 goal로 정리한다(모호하면 짧게 확인 — 무엇을·어디서·성공 기준).
|
|
27
62
|
|
|
28
63
|
2. **정책 확인 & 브랜치 게이트 (git 워크플로).** 코드를 바꾸는 요청이면 루프 전에 정책을 본다.
|
|
29
64
|
git 저장소가 아니면 이 단계를 건너뛴다.
|
|
@@ -39,46 +74,53 @@ user-invocable: true
|
|
|
39
74
|
정확한 도구명 **접두사는 등록 방식(플러그인 vs 프로젝트 `.mcp.json`)에 따라 다르다**
|
|
40
75
|
(`mcp__hi-loop__hiloop_run` 또는 `mcp__plugin_...__hiloop_run`). 접두사를 하드코딩하지 말고
|
|
41
76
|
**이름으로 검색**해 매칭된 정확한 도구를 호출한다:
|
|
42
|
-
`ToolSearch` → 쿼리 `hiloop_run hiloop_answer` (키워드 검색)
|
|
77
|
+
`ToolSearch` → 쿼리 `hiloop_run hiloop_answer hiloop_docsync` (키워드 검색)
|
|
43
78
|
|
|
44
79
|
4. **루프 실행.** `hiloop_run` 을 호출한다:
|
|
45
80
|
- `goal`: 위에서 정한 목표
|
|
46
|
-
- `
|
|
81
|
+
- `step: true` — **대화형 기본값.** 매 단위 작업 후 제어를 돌려받아(진행이 사용자에게 보이고,
|
|
82
|
+
콜이 짧아 타임아웃이 없다) 이어 호출한다(아래 5-a 참조). 무인·완료까지 한 번에 돌려야 할
|
|
83
|
+
특별한 이유가 있을 때만 뺀다.
|
|
84
|
+
- **단계어가 있으면** 위 "인자 문법" 표대로 `stopAfter`(및 필요한 `full`/`ship`/`watch`)를 준다.
|
|
85
|
+
**없으면** `full: true` — 발굴·설계리뷰·코드리뷰를 포함한 전체 라이프사이클로 완료까지.
|
|
47
86
|
- `testCommand`: 프로젝트의 테스트 명령(기본 `npm test`)
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
- **e2e 는 시작 시 묻지 않는다.** UI 작업이어도 우선 기본 테스트(`testCommand`)로 루프를 돌린다.
|
|
88
|
+
e2e 확인은 **통과 후(7단계)** — 구현이 실제로 되고 확인할 UI 변경이 생겼을 때 — 로 미룬다.
|
|
89
|
+
(구현이 되는지도 모르는 시작 시점에 헷갈리는 e2e 결정을 강요하지 않는다.)
|
|
90
|
+
- **(고급, opt-in) 매 회차 e2e 강제**: 사용자가 "매 회차 e2e 로 막아줘" 같이 **명시적으로** 원할 때만
|
|
91
|
+
시작 시 `checks` 를 건다(무인·CI 처럼 결정론적 게이트가 필요한 경우). 셸 e2e 명령과 실행 환경(서버 등)이
|
|
92
|
+
있어야 한다. 느리고 비싸므로 **기본이 아니다**:
|
|
53
93
|
```json
|
|
54
|
-
"checks": [
|
|
55
|
-
{ "cmd": "<기본 테스트, 예: npm test>" },
|
|
56
|
-
{ "cmd": "<e2e 셸 명령>", "when": "<UI 글롭, 예: src/**/*.tsx>" }
|
|
57
|
-
]
|
|
94
|
+
"checks": [{ "cmd": "<기본 테스트>" }, { "cmd": "<e2e 셸 명령>", "when": "<UI 글롭, 예: src/**/*.tsx>" }]
|
|
58
95
|
```
|
|
59
|
-
→ 엔진이
|
|
60
|
-
|
|
61
|
-
|
|
96
|
+
→ 엔진이 UI 변경 회차마다 e2e 를 돌려 통과해야 pass 로 인정한다.
|
|
97
|
+
|
|
98
|
+
5-a. **스텝 이어가기 (step 모드).** 응답이 `⏭ 스텝 완료 …` 로 오면 루프가 **아직 미완**이다.
|
|
99
|
+
그 한 줄 진행(어느 회차·어느 단계인지)을 **사용자에게 중계**한 뒤, **같은 인자로 `hiloop_run` 을
|
|
100
|
+
다시 호출**해 다음 스텝을 진행한다. `✅ 통과` / `❌ 실패` / `⏸ 선택 필요` 가 나올 때까지 반복한다.
|
|
101
|
+
이렇게 해야 매 스텝이 콘솔에 보이고 긴 콜의 타임아웃이 없다(백그라운드에서 조용히 도는 일이 없다).
|
|
62
102
|
|
|
63
|
-
5. **사용자 선택 처리.** 응답이 `⏸ 사용자의 선택이 필요합니다` 로 오면,
|
|
103
|
+
5-b. **사용자 선택 처리.** 응답이 `⏸ 사용자의 선택이 필요합니다` 로 오면,
|
|
64
104
|
그 질문을 **그대로 사용자에게 제시**하고 답을 받는다. 답을 `hiloop_answer`
|
|
65
105
|
(`choice`, 필요하면 `note`)로 전달한 뒤, **같은 goal 로 `hiloop_run` 을 다시 호출**해 이어간다.
|
|
66
106
|
|
|
67
107
|
6. **결과 보고.** `✅ 통과` / `❌ 실패` 와 반복 횟수, 그리고 함께 온 Gaps(검증 한계)를
|
|
68
108
|
사용자에게 전한다. false green(테스트만 초록이고 실제 미완)을 통과로 포장하지 않는다.
|
|
69
109
|
|
|
70
|
-
7. **e2e
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
110
|
+
7. **e2e 확인 (통과 후 · UI 변경이 있을 때만).** 여기가 e2e 를 정하는 **기본 지점**이다. 루프가 통과(✅)했고
|
|
111
|
+
**UI(화면·컴포넌트·라우팅·스타일)가 바뀌었으며** e2e 도구가 있으면, **간단히 예/아니오로** 묻는다:
|
|
112
|
+
> "UI 가 바뀌었어요. 지금 e2e 로 실제 화면을 확인할까요? (예 / 아니오)"
|
|
113
|
+
- **예** → e2e 실행. **셸 e2e 명령**(package.json `test:e2e`, 또는 `playwright.config.*` → `npx playwright test`)이
|
|
114
|
+
있으면 그걸, 없고 **Playwright MCP** 만 있으면 MCP 로 돌린다. 서버 등 환경이 필요하면 먼저 확인·기동한다.
|
|
115
|
+
- **아니오** → 기본 테스트로 충분하다고 보고 넘어간다.
|
|
116
|
+
- e2e 도구가 **둘 다 없으면** 묻지 말고 스킵했음을 밝힌다(도구를 임의 설치하지 않는다 — 통과한 루프를 실패로 만들지 않는다).
|
|
117
|
+
- UI 변경이 없으면 e2e 는 해당 없음(묻지 않는다).
|
|
118
|
+
- **4단계에서 `checks` 엔진 게이트를 이미 걸었다면** e2e 는 이미 게이트로 통과했으니 다시 묻지 않는다(스크린샷 저장만 필요하면 물어 저장).
|
|
119
|
+
- 실행한 경우 **결과서·스크린샷을 test 폴더에 저장**한다(리포트 `tests/e2e/report-<날짜시각>.md`,
|
|
77
120
|
스크린샷 `tests/e2e/screenshots/`). 날짜·시각은 실제 값으로 채우고 저장 경로를 알린다.
|
|
78
|
-
스크린샷은 브라우저를 구동하는 도구(Playwright MCP/셸)가 있을 때만 가능하다.
|
|
79
121
|
|
|
80
|
-
> 왜
|
|
81
|
-
>
|
|
122
|
+
> 왜 통과 후에 묻나: 구현이 되기 전엔 e2e 결정이 이르고 헷갈린다. **일이 됐고 확인할 UI가 있을 때**
|
|
123
|
+
> 한 번 묻는 게 자연스럽다. 매 회차 결정론적 강제가 꼭 필요하면(무인·CI) 4단계의 opt-in `checks` 를 쓴다.
|
|
82
124
|
|
|
83
125
|
8. **커밋 게이트 (git 워크플로).** 테스트가 통과(✅)하고 e2e 게이트까지 끝나면 `commitPolicy` 대로
|
|
84
126
|
**로컬** 커밋한다. **push/PR 은 하지 않는다.** git 저장소가 아니면 건너뛴다.
|
|
@@ -121,3 +163,4 @@ user-invocable: true
|
|
|
121
163
|
- `hiloop_rollback` — git 체크포인트로 파일 복원(회차 지정 가능)
|
|
122
164
|
- `hiloop_reset` — 루프 상태 초기화
|
|
123
165
|
- `hiloop_setup` — 프로젝트에 `.mcp.json`·`.gitignore`·`docs/`·`tests/`·CLAUDE.md 규칙 주입
|
|
166
|
+
- `hiloop_docsync` — 소스 변경(git diff)을 문서에 반영하는 단발 패스(`/flow doc-sync` 가 이걸 부른다)
|
package/src/build.js
CHANGED
|
@@ -46,7 +46,7 @@ function addCost(prev, delta) {
|
|
|
46
46
|
|
|
47
47
|
export async function runBuildLoop(ctx) {
|
|
48
48
|
const { goal, cwd, logger, notify, statePath, baselineFiles, designReviewEnabled, ports, opts } = ctx;
|
|
49
|
-
const { maxLoops, budgetUsd, testCommand, stagnationLimit, verifySpec, handoffEvery, maxDesignRounds, stopAfter, onFail, askPolicy } = opts;
|
|
49
|
+
const { maxLoops, budgetUsd, testCommand, stagnationLimit, verifySpec, handoffEvery, maxDesignRounds, stopAfter, onFail, askPolicy, step } = opts;
|
|
50
50
|
const { agentRunner, testRunner, integrityChecker, checkpointer, fileLister, specVerifier, designReviewer, treeKeyReader } = ports;
|
|
51
51
|
// state 는 이 루프에서 속성만 바뀌고 재대입되지 않는다 — 참조를 그대로 잡아도 안전하다.
|
|
52
52
|
const state = ctx.state;
|
|
@@ -136,6 +136,7 @@ export async function runBuildLoop(ctx) {
|
|
|
136
136
|
state.iteration -= 1;
|
|
137
137
|
saveState(statePath, state);
|
|
138
138
|
logger(`[hi-loop] 📐 설계 리뷰 기각 (${state.designRounds}/${maxDesignRounds}) — 스펙을 다시 씁니다: ${truncate(verdict.reason ?? '', 200)}`);
|
|
139
|
+
if (step) return { ok: false, continue: true, phase: 'PLAN', iteration: state.iteration };
|
|
139
140
|
continue;
|
|
140
141
|
}
|
|
141
142
|
logger('[hi-loop] 📐 설계 리뷰 통과.');
|
|
@@ -281,6 +282,12 @@ export async function runBuildLoop(ctx) {
|
|
|
281
282
|
detail: `session #${state.sessionSerial} 로 컨텍스트 다이어트 후 재개`,
|
|
282
283
|
});
|
|
283
284
|
}
|
|
285
|
+
|
|
286
|
+
// FR-24 스텝 모드: 이 회차(에이전트 1콜)를 끝냈고 아직 통과 전이다. 제어를 호출자에게
|
|
287
|
+
// 돌려준다 — 상태는 저장돼 있어 다음 hiloop_run 이 resume 으로 이 지점을 이어간다.
|
|
288
|
+
// phase 는 이 회차의 의미(PLAN/DO/ACT)로 보고한다 — state.phase 는 실패 후 ACT 로 바뀐 뒤라
|
|
289
|
+
// 매 스텝이 ACT 로만 찍힌다(무의미). phaseForIteration 이 회차→단계의 정본이다.
|
|
290
|
+
if (step) return { ok: false, continue: true, phase: phaseForIteration(state.iteration), iteration: state.iteration };
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
logger(`[hi-loop] 한도(${maxLoops}회)를 소진했지만 테스트가 통과하지 못했습니다. (누적 $${state.costUsd.toFixed(2)})`);
|
package/src/cli-options.js
CHANGED
|
@@ -153,6 +153,8 @@ export function parseRunOptions(args, { staged = {} } = {}) {
|
|
|
153
153
|
reconcileSpec: str('reconcile-spec'),
|
|
154
154
|
startFrom,
|
|
155
155
|
stopAfter,
|
|
156
|
+
// FR-24: 스텝 모드. 매 단위 작업 후 status:continue 로 제어를 돌려준다(호출자가 이어 호출).
|
|
157
|
+
step: Boolean(args.step),
|
|
156
158
|
...durations,
|
|
157
159
|
...numbers,
|
|
158
160
|
// 서브커맨드는 그 단계를 **하라는 명령**이다. 자동 기본값이 그걸 끄면 안 된다.
|
package/src/docsync.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOC-SYNC (FR-25) — 소스 변경을 문서에 반영하는 단발 패스.
|
|
3
|
+
*
|
|
4
|
+
* 자가 치유 루프가 아니다(통과시킬 테스트가 없다). git diff 로 소스 변경을 모아 일꾼
|
|
5
|
+
* 에이전트에게 주고, **문서만** 소스에 맞춰 갱신하게 한다. 사람이 관리하는 표준 오라클
|
|
6
|
+
* (docs/DESIGN.md)은 절대 건드리지 않는다. 짧은 단발이라 긴 루프의 블랙박스/타임아웃이 없다.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { makeAgentRunner } from './runners.js';
|
|
10
|
+
import { makeCheckpointer, makeChangeLister, isGitRepo } from './checkpoint.js';
|
|
11
|
+
import { docSyncPrompt } from './prompts.js';
|
|
12
|
+
|
|
13
|
+
function git(args, cwd) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
execFile('git', args, { cwd, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
16
|
+
resolve(err ? null : String(stdout));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isMd = (f) => /\.md$/i.test(f);
|
|
22
|
+
/** 사람이 관리하는 표준 오라클 — doc-sync 도 손대면 안 된다. */
|
|
23
|
+
export const isOracleDoc = (f) => /(^|\/)(DESIGN|design)\.md$/.test(f);
|
|
24
|
+
/** doc-sync 가 갱신해도 되는 문서인가: 마크다운이되 오라클은 제외. */
|
|
25
|
+
export const isEditableDoc = (f) => isMd(f) && !isOracleDoc(f);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 반영할 **소스(비-문서) 변경**의 diff 를 읽는다. 커밋 안 된 변경(HEAD 대비)을 우선하고,
|
|
29
|
+
* 없으면 마지막 커밋을 본다. 문서(.md)만 바뀌었으면 트리거가 없으므로 빈 문자열.
|
|
30
|
+
*/
|
|
31
|
+
export function makeSourceDiffReader() {
|
|
32
|
+
return async ({ cwd }) => {
|
|
33
|
+
const uncommitted = (await git(['diff', 'HEAD', '--name-only'], cwd)) || '';
|
|
34
|
+
let range = ['HEAD'];
|
|
35
|
+
let files = uncommitted.split('\n').filter(Boolean);
|
|
36
|
+
if (!files.length) {
|
|
37
|
+
const last = (await git(['diff', 'HEAD~1', 'HEAD', '--name-only'], cwd)) || '';
|
|
38
|
+
files = last.split('\n').filter(Boolean);
|
|
39
|
+
range = ['HEAD~1', 'HEAD'];
|
|
40
|
+
}
|
|
41
|
+
const sourceFiles = files.filter((f) => !isMd(f));
|
|
42
|
+
if (!sourceFiles.length) return '';
|
|
43
|
+
return (await git(['diff', ...range, '--', ...sourceFiles], cwd)) || '';
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* doc-sync 한 번 실행. 반환:
|
|
49
|
+
* { ok:true, changed:[문서], stray:[문서 외 편집], note, costUsd }
|
|
50
|
+
* { ok:false, reason } — 비-git 등 실행 불가
|
|
51
|
+
*/
|
|
52
|
+
export async function runDocSync({
|
|
53
|
+
cwd = process.cwd(),
|
|
54
|
+
logger = () => {},
|
|
55
|
+
agentRunner = makeAgentRunner(),
|
|
56
|
+
checkpointer = makeCheckpointer(),
|
|
57
|
+
changeLister = makeChangeLister(),
|
|
58
|
+
diffReader = makeSourceDiffReader(),
|
|
59
|
+
isRepo = isGitRepo,
|
|
60
|
+
docTargets = null,
|
|
61
|
+
} = {}) {
|
|
62
|
+
if (!(await isRepo(cwd))) {
|
|
63
|
+
return { ok: false, reason: 'git 저장소가 아니라 소스 변경(diff)을 읽을 수 없습니다.' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger('[hi-loop] 🔎 소스 변경 대조 중…');
|
|
67
|
+
const diffText = await diffReader({ cwd });
|
|
68
|
+
if (!diffText.trim()) {
|
|
69
|
+
return { ok: true, changed: [], stray: [], note: '반영할 소스 변경이 없습니다(문서만 바뀌었거나 변경 없음).' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 안전망: 에이전트가 손대기 전 스냅샷. 잘못 고치면 rollback 으로 되돌린다.
|
|
73
|
+
await checkpointer({ cwd });
|
|
74
|
+
const before = new Set((await changeLister({ cwd })) ?? []);
|
|
75
|
+
|
|
76
|
+
logger('[hi-loop] 📝 문서 갱신 중…');
|
|
77
|
+
const result = await agentRunner({ prompt: docSyncPrompt({ diffText, docTargets }), cwd });
|
|
78
|
+
|
|
79
|
+
const after = (await changeLister({ cwd })) ?? [];
|
|
80
|
+
const touched = after.filter((f) => !before.has(f));
|
|
81
|
+
const changed = touched.filter(isEditableDoc);
|
|
82
|
+
const stray = touched.filter((f) => !isEditableDoc(f));
|
|
83
|
+
|
|
84
|
+
logger(
|
|
85
|
+
changed.length
|
|
86
|
+
? `[hi-loop] ✅ 문서 ${changed.length}개 갱신: ${changed.join(', ')}`
|
|
87
|
+
: '[hi-loop] ✅ 갱신할 문서 변경이 없었습니다.',
|
|
88
|
+
);
|
|
89
|
+
if (stray.length) logger(`[hi-loop] ⚠️ 문서 외 파일이 변경됨(검토 요망): ${stray.join(', ')}`);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
changed,
|
|
94
|
+
stray,
|
|
95
|
+
note: typeof result?.text === 'string' ? result.text.slice(0, 800) : '',
|
|
96
|
+
costUsd: Number.isFinite(result?.costUsd) ? result.costUsd : 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default runDocSync;
|