@tuzi-ince/hi-loop 0.4.1 → 0.6.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/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.4.1+)로 개발·검증하려면 아래 §2 의 로컬 설치를 쓴다.
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.4.1
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.4.1.tgz 생성 (~26kB)
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.4.1.tgz
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 참조). 셋업 직후 이 문서를 채워두면, 이후 단말적 요청이
@@ -122,7 +124,11 @@ hi-loop-setup # 실제 적용
122
124
 
123
125
  ## 4. 사용법
124
126
 
125
- ### 4-1. CLI 직접 실행 (사람 / 데몬 / 텔레그램 봇)
127
+ > **주 경로는 MCP(§4-2)다.** 대부분은 코드 어시스턴트(클로드코드·커서)가 `hiloop_run` 같은
128
+ > **MCP 도구로 hi-loop 을 부른다.** CLI(§4-1)는 사람이 터미널에서·데몬·봇·CI 로 직접 구동할 때 쓴다.
129
+ > 두 경로는 **같은 엔진·같은 상태 머신**을 쓰므로 동작은 동일하다 — 인터페이스만 다르다.
130
+
131
+ ### 4-1. CLI 직접 실행 (사람 / 데몬 / 텔레그램 봇 / CI)
126
132
 
127
133
  ```bash
128
134
  hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
@@ -135,7 +141,9 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
135
141
  |---|---|---|---|
136
142
  | `--goal` | `-g` | (필수) | 달성할 요구사항 |
137
143
  | `--test` | `-t` | `npm test` | 성패를 판정할 명령 |
138
- | `--max-loops` | `-m` | `10` | 최대 루프(=에이전트 호출) **횟수** 상한 |
144
+ | `--max-loops` | `-m` | `10` | 최대 루프(=에이전트 호출) **횟수** 상한 (무인 환경 백스톱) |
145
+ | `--on-fail` | — | `heal` | CHECK 실패/불능 시: `heal`(자동 치유) / `ask`(사람에게 재시도·종료 물음) / `stop`(즉시 종료). 대화형·비싼 e2e 에선 `ask` 권장 |
146
+ | `--step` | — | (꺼짐) | 스텝 모드. 매 단위 작업(BUILD 회차·각 stage) 후 멈춰 exit 3 으로 돌려준다 — 재실행으로 이어감. 진행 가시성·타임아웃 제거 (§4-2 스텝 모드) |
139
147
  | `--budget-usd` | — | (없음=무제한) | 누적 **비용** 상한(USD). 넘으면 다음 호출 전에 중단 |
140
148
  | `--stagnation` | — | `3` | 같은 실패가 이 횟수 연속이면 조기 종료. `--no-stagnation` 으로 끔 |
141
149
  | `--verify-spec` | — | (꺼짐) | 통과 후 별도 검증자로 스펙 대비 구현을 심판 (하향 전용, 비용 증가) |
@@ -153,16 +161,31 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
153
161
  > 돈을 막으려면 `--budget-usd` 를 써라. 자세한 실측치는 §7.
154
162
 
155
163
  exit code: `0` 통과 또는 `--stop-after` 로 정지 / `1` 실패(한도·예산 소진) /
156
- `2` 잘못된 사용 / `3` **사람의 답을 기다리는 중**(§4-1b).
164
+ `2` 잘못된 사용 / `3` **종료가 아니라 재호출이 필요**(사람의 답을 기다리는 §4-1b, 또는 `--step`
165
+ 스텝 완료 §4-2 — 둘 다 같은 goal 로 다시 실행하면 이어간다).
157
166
  → CI·스크립트에서 `hi-loop run … && echo OK` 로 바로 쓸 수 있다.
158
- 3 을 따로 둔 이유: 0 이면 `run && 배포` 가 질문에 답도 진행하고,
159
- 1 이면 진짜 실패와 구분되지 않는다.
167
+ 3 을 따로 둔 이유: 0 이면 `run && 배포` 가 미완인데 진행하고, 1 이면 진짜 실패와 구분되지 않는다.
160
168
 
161
169
  ```bash
162
170
  hi-loop status # 현재 루프 상태 요약 (누적 비용·정체·대기 질문 포함)
163
171
  hi-loop --help
164
172
  ```
165
173
 
174
+ > **실패 시 자동 반복 대신 사람에게 물음 (`--on-fail ask`)**: 대화형·비싼 e2e 에서 무인 자동
175
+ > 치유가 폭주하는 걸 막는다. CHECK 실패/불능에서 루프가 **멈추고 재시도/종료를 묻는다**(exit 3).
176
+ > `heal`(기본, 자동 치유) 동작은 그대로다. 무인(`--ask never`)에선 물을 수 없으니 안전하게 종료한다.
177
+ >
178
+ > ```bash
179
+ > hi-loop run --goal "..." --test "cd frontend && npx playwright test" --on-fail ask
180
+ > # ❌ 테스트 실패 → ⏸ 선택이 필요합니다 [ON_FAIL:2]
181
+ > # 계속 진행할까요? a) 재시도 b) 종료
182
+ > # → hi-loop answer <a|b> (exit 3)
183
+ >
184
+ > hi-loop answer a # 재시도 → 같은 goal 로 run 을 다시 호출하면 이어서 진행
185
+ > ```
186
+ > MCP 에선 `hiloop_run({ goal, onFail: "ask" })` → `awaiting` 반환 → `hiloop_answer` 로 답하고 재호출.
187
+ > 판정 불능(무결성·환경)일 땐 질문이 "실패"가 아니라 **"불능"**으로 표시돼 사람이 알아챈다.
188
+
166
189
  > **문서↔소스 정합 (`--spec` / `--reconcile`)**: 기본적으로 이 엔진의 스펙·테스트는 에이전트
167
190
  > **자신이** 쓴 것이라, 통과가 "자기 테스트를 통과"에 그칠 수 있다(완료 보고의 Gaps 가 이를 공개한다).
168
191
  > 사람이 관리하는 표준 문서(`docs/DESIGN.md`, `hi-loop-setup` 이 만든다)를 오라클로 세우면 그 한계를 좁힌다:
@@ -254,21 +277,82 @@ hi-loop rollback --cwd . # 작업 대상 지정
254
277
  안전망 스냅샷으로 한 번 더 떠두므로, 롤백 자체를 잘못 눌러도 다시 앞으로 갈 수 있다.
255
278
  - git 저장소가 아니면 체크포인트가 없으니 롤백도 조용히 no-op이다.
256
279
 
257
- ### 4-2. MCP 서버 (클로드코드 / 커서)
280
+ ### 4-2. MCP 서버 (클로드코드 / 커서) — **주 사용 경로 (권장)**
258
281
 
259
- `hi-loop-setup` 에이전트를 재시작하면 도구 6개가 뜬다.
282
+ 대부분의 사용은 CLI 아니라 **코드 어시스턴트(클로드코드·커서)가 MCP 도구로 hi-loop 을 부르는**
283
+ 방식이다. `hi-loop-setup` 후 에이전트를 재시작하면 도구 7개가 뜬다.
260
284
 
261
285
  | 도구 | 인자 | 용도 |
262
286
  |---|---|---|
263
- | `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `spec`, `reconcile`, `reconcileSpec`, `checks`, `cwd` | 자가 치유(+전과정) 루프 실행 |
287
+ | `hiloop_run` | `goal`(필수), `testCommand`, `maxLoops`, `verifySpec`, `full`, `ship`, `watch`, `stopAfter`, `step`, `spec`, `reconcile`, `reconcileSpec`, `checks`, `onFail`, `cwd` | 자가 치유(+전과정) 루프 실행. `step:true`=매 단위 후 `continue` 반환(대화형 권장) |
264
288
  | `hiloop_answer` | `choice`, `note`, `cwd` | 대기 중인 질문에 답하고 재개 가능 상태로 되돌림 |
265
289
  | `hiloop_status` | `cwd` | 진행 상태 조회 (장시간 루프를 들여다보는 창) |
266
290
  | `hiloop_rollback` | `to`, `cwd` | 체크포인트로 파일 복원 |
267
291
  | `hiloop_reset` | `cwd` | `.hi-loop/STATE.json` 초기화 |
268
292
  | `hiloop_setup` | `dryRun`, `cwd` | 프로젝트 설정 주입 |
293
+ | `hiloop_docsync` | `cwd` | 소스 변경(git diff)을 문서에 반영하는 단발 패스(§4-2 doc-sync) |
294
+
295
+ #### 기본 흐름 — 실행 → 진행 → (필요 시) 멈춤·재개
296
+
297
+ ```
298
+ hiloop_run({ goal, testCommand, cwd })
299
+ │ 루프가 도는 동안 회차마다 progress 알림을 보낸다(아래 참조) → 클라이언트가 안 끊긴다
300
+ ├─▶ ✅ 통과 → 결과 텍스트 + Gaps(검증 한계) 반환. 끝.
301
+ ├─▶ ❌ 실패 → 실패 요약 반환(heal 기본은 maxLoops 까지 자동 치유 후).
302
+ └─▶ ⏸ awaiting → **사람의 선택이 필요**. 질문을 사용자에게 그대로 제시하고,
303
+ hiloop_answer({ choice }) 로 답한 뒤 **같은 goal 로 hiloop_run 을 다시 호출**해 이어간다.
304
+ ```
305
+
306
+ **멈춤·재개(ask)는 MCP 의 핵심 패턴이다.** MCP 는 단일 요청/응답이라 서버가 사람을 기다릴 수 없다.
307
+ 그래서 hi-loop 은 **상태를 저장하고 `awaiting` 으로 반환**한다(차단 안 함). 멈추는 지점은 셋:
308
+ - **DISCOVER 분기**(상호배타 선택), **SHIP 직전**(비가역 배포), **on-fail ask**(테스트 실패/불능).
309
+ - 어느 경우든 `hiloop_run` 이 `⏸ 사용자의 선택이 필요합니다 …` 를 반환한다 → 그 질문을 사용자에게
310
+ 보여주고 → `hiloop_answer({ choice: "a" })` → **같은 goal 로 `hiloop_run` 재호출**하면 그 지점부터 잇는다.
311
+
312
+ #### 실패를 사람이 통제하기 — `onFail: "ask"` (대화형·비싼 e2e 권장)
313
+
314
+ 기본(`heal`)은 `maxLoops` 까지 무인 자동 치유다. 대화형에서 비싼 e2e 를 무인으로 반복시키지 않으려면:
315
+
316
+ ```js
317
+ hiloop_run({ goal: "...", testCommand: "cd frontend && npx playwright test", onFail: "ask" })
318
+ // ❌ 실패/불능 → ⏸ awaiting "재시도할까요 / 종료할까요?"
319
+ hiloop_answer({ choice: "a" }) // a=재시도, b=종료
320
+ // → 같은 goal 로 hiloop_run 재호출하면 한 회차 더 돌린다
321
+ ```
322
+ 판정 불능(무결성·환경)일 땐 질문이 "실패"가 아니라 **"불능"**으로 표시된다.
323
+
324
+ #### 긴 루프가 안 끊긴다 — progress 알림
325
+
326
+ `hiloop_run` 은 루프가 도는 동안 **회차마다 진행 알림(`notifications/progress`)을 보낸다.** 그래서
327
+ e2e 처럼 오래 걸리는 루프도 클라이언트 유휴 타임아웃(기본 30분)에 조용히 끊기지 않는다. 그럼에도
328
+ 매우 긴 무인 루프를 돌릴 땐, `hiloop_run` 대신 한 번 던지고 **`hiloop_status` 로 폴링**하는 편이 안전하다.
269
329
 
270
330
  > MCP `hiloop_run`은 CLI와 달리 `--budget-usd`·`--stagnation`을 노출하지 않는다.
271
- > 예산 상한이 필요하면 CLI(`hi-loop run`)를 쓴다. 정체 감지는 기본값(3회)으로 동작한다.
331
+ > 예산 상한이 필요하면 CLI(`hi-loop run`)를 쓴다. 정체 감지는 기본값(3회)으로 동작한다.
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 그대로다.
272
356
 
273
357
  > **UI 변경 회차에만 e2e 를 엔진이 강제하기 (`checks`).** `checks: [{cmd, when?}]` 를 주면
274
358
  > `testCommand` 대신 이 배열이 판정 기준이 되고, `when` 글롭에 맞는 파일이 바뀐 회차에만 그 검사를
@@ -289,12 +373,88 @@ hi-loop rollback --cwd . # 작업 대상 지정
289
373
 
290
374
  수동 기동: `hi-loop mcp` (인자 없이 `hi-loop` 만 쳐도 서버로 뜬다)
291
375
 
376
+ #### `/flow` 스킬 — 단계 지정 (`/flow plan`, `/flow do` …)
377
+
378
+ Claude Code 에선 `flow` 스킬을 슬래시로 부른다(`/hi-loop:flow`, 또는 bare `/flow`). **첫 단어에 단계어**를
379
+ 붙이면 그 단계까지만 돌고 멈춰 **다음 단계를 제안**한다(bkit 의 `/pdca plan|do` 와 유사). 단계어가 없으면
380
+ 완료까지 돈다. 스킬이 단계어를 엔진의 `stopAfter` 로 옮긴다:
381
+
382
+ | 입력 | 동작 |
383
+ |---|---|
384
+ | `/flow <요청>` | 완료까지 TDD 반복(+UI면 e2e 제안) |
385
+ | `/flow plan <요청>` (=`spec`) | 스펙+테스트만 만들고 멈춤 → 구현 제안 |
386
+ | `/flow do <요청>` (=`build`) | 테스트 통과까지 구현(검증·치유 포함) → 리뷰/배포 제안 |
387
+ | `/flow discover`·`review`·`ship`·`watch` | 각 단계까지 |
388
+
389
+ > ⚠️ hi-loop 은 **DO→CHECK→ACT(HEAL) 를 하나의 자가치유 BUILD 루프로 합친다** — 검증(CHECK)은 엔진이
390
+ > 매 회차 자동 판정하므로 bkit 처럼 `check` 가 **별도 단계가 아니다**. `do`(=build) 하나가 "구현→검증→치유를
391
+ > 통과까지" 담당한다. 그래서 단계어는 바깥 단계(discover/plan/do/review/ship/watch)뿐이다.
392
+
393
+ CLI 에는 같은 단계가 서브커맨드로 있다: `hi-loop plan|discover|review|ship|watch`(각각 `--stop-after` 상당).
394
+
395
+ #### 문서↔소스 맞추기 — `hiloop_docsync` / `/flow doc-sync` / `hi-loop docsync`
396
+
397
+ 소스를 바꾼 뒤 **관련 문서를 소스에 맞추는 단발 패스**다. 자가 치유 루프가 아니라 한 번의
398
+ 에이전트 패스라 짧고(타임아웃 없음), 진행 알림을 보낸다. 동작:
399
+
400
+ 1. `git diff`로 **소스(비-문서) 변경**을 읽는다(커밋 안 된 변경 우선, 없으면 마지막 커밋). 소스 변경이 없으면 아무것도 안 한다.
401
+ 2. 일꾼 에이전트가 그 diff 에 맞춰 **문서만** 갱신한다 — `README.md`·`docs/*.md`.
402
+ 3. **`docs/DESIGN.md`(사람 오라클)와 소스·테스트는 건드리지 않는다.** 문서 외 파일이 바뀌면 `stray` 로 경고한다.
403
+
404
+ ```
405
+ hiloop_docsync({ cwd })
406
+ 🔎 소스 변경 대조 중… → 📝 문서 갱신 중… → ✅ 문서 2개 갱신: README.md, docs/guide.md
407
+ ```
408
+
409
+ > diff 로 확인되는 것만 반영한다(없는 내용을 지어내면 그게 환각이다). 갱신 후 어시스턴트가
410
+ > `git diff <문서>`로 실제 변경을 보여주고, 커밋은 사람이 판단한다(문서를 소스와 **같은 변경**으로 커밋).
411
+ > CLI: `hi-loop docsync`. 스킬: `/flow doc-sync`(goal 불필요).
412
+
413
+ #### 다른 MCP 호스트에 등록하기 (Cursor / opencode / Cline …)
414
+
415
+ hi-loop 의 MCP 서버는 **표준 stdio MCP 서버**라 MCP 를 지원하는 어떤 호스트에든 등록된다.
416
+ `hi-loop-setup --host <호스트>` 가 호스트별 설정 파일에 알아서 써 준다:
417
+
418
+ ```bash
419
+ hi-loop-setup --host cursor # → .cursor/mcp.json (Claude 와 같은 mcpServers 스키마)
420
+ hi-loop-setup --host opencode # → opencode.json (mcp/local 스키마)
421
+ ```
422
+
423
+ 수동으로 넣고 싶으면 (또는 다른 호스트) 아래 형태를 그 호스트 설정에 직접 추가한다:
424
+
425
+ ```jsonc
426
+ // opencode — opencode.json
427
+ { "mcp": { "hi-loop": { "type": "local", "command": ["hi-loop", "mcp"], "environment": {} } } }
428
+
429
+ // Cursor — .cursor/mcp.json (Claude Code 의 .mcp.json 과 같은 스키마)
430
+ { "mcpServers": { "hi-loop": { "command": "hi-loop", "args": ["mcp"] } } }
431
+ ```
432
+
433
+ 전역 설치가 안 됐으면 `command` 를 절대 경로로: `node /path/to/hi-loop/bin/hi-loop.js mcp`
434
+ (`--host` 로 생성하면 현재 설치본의 절대 경로가 자동으로 박힌다).
435
+
436
+ > ⚠️ **중요 — 루프의 실제 일꾼은 여전히 `claude` 다.** hi-loop 도구는 어느 호스트에서든 뜨지만,
437
+ > 엔진은 구현 에이전트로 **`claude -p …` 를 서브프로세스로 부른다**(기본값). 즉 opencode 안에서 hi-loop 을
438
+ > 써도 코딩은 claude 가 한다(claude 설치·인증 필요). 다른 에이전트 CLI 를 루프의 일꾼으로 쓰려면
439
+ > `HILOOP_AGENT_CMD` + **에이전트 어댑터**(§4-3의 `HILOOP_AGENT_PROVIDER`)를 함께 준다. 예: opencode 를
440
+ > 일꾼으로 —
441
+ > ```bash
442
+ > HILOOP_AGENT_PROVIDER=generic HILOOP_AGENT_CMD=opencode HILOOP_AGENT_ARGS=run
443
+ > # → 루프가 매 회차 `opencode run "<프롬프트>"` 를 부르고 stdout 을 결과로 읽는다.
444
+ > ```
445
+ > `generic` 어댑터는 **세션 재개·비용 집계를 지원하지 않는다**(claude 전용 계약이라 매 회차 새 세션).
446
+ > 기능이 온전한 기본 경로는 claude 이며, generic 은 이식용 최소 계약이다.
447
+ >
448
+ > 또한 **자동 라우팅(스킬 `flow`·CLAUDE.md)은 Claude Code 전용**이다. 다른 호스트에서는 `hiloop_run`
449
+ > 같은 **도구를 직접 호출**한다(도구 동작은 동일).
450
+
292
451
  ### 4-3. 환경변수
293
452
 
294
453
  | 변수 | 기본값 | 설명 |
295
454
  |---|---|---|
296
455
  | `HILOOP_AGENT_CMD` | `claude` | 에이전트 실행 명령. 다른 CLI 로 교체 가능 |
297
- | `HILOOP_AGENT_ARGS` | (없음) | 에이전트에 덧붙일 인자 (공백 구분) |
456
+ | `HILOOP_AGENT_PROVIDER` | `claude` | 에이전트 CLI 계약. `claude`(세션·비용 지원) 또는 `generic`(프롬프트=마지막 위치인자, stdout=결과, 세션·비용 없음). 미상값은 `claude` 로 되돌림 |
457
+ | `HILOOP_AGENT_ARGS` | (없음) | 에이전트에 덧붙일 인자 (공백 구분). `generic` 에선 프롬프트 **앞** 서브커맨드로 쓰임(예: `run`) |
298
458
  | `HILOOP_PERMISSION_MODE` | `acceptEdits` | 비대화형 편집 허용용. `bypassPermissions` 등으로 확대/축소 |
299
459
  | `HILOOP_VERIFY_CMD` | (`HILOOP_AGENT_CMD`→`claude`) | `--verify-spec` 검증자 실행 명령. 구현자와 다른 CLI로 검증 가능 |
300
460
  | `HILOOP_VERIFY_MODEL` | (구현자와 동일) | `--verify-spec` 검증자 모델 |
@@ -485,7 +645,7 @@ cat .hi-loop/STATE.json | jq '{status, phase, iteration, sessionId, sessionSeria
485
645
  - `npm pack` → tarball 생성 (bin/src/docs/README 포함)
486
646
  - **`npm install -g --prefix /tmp/lev-prefix ./tarball` 로 진짜 설치 → 설치본 실행 확인**
487
647
  - `bin/hi-loop` 가 심링크로 생성됨을 `ls -l` 로 확인
488
- - **심링크 bin 경유** `hi-loop --version` → `0.4.1`, `run`(goal 없음) → exit 2
648
+ - **심링크 bin 경유** `hi-loop --version` → `0.6.0`, `run`(goal 없음) → exit 2
489
649
  - 설치본으로 전체 루프 E2E: PLAN→실패→DO→통과→exit 0
490
650
  - 설치본 `hi-loop-setup` → `.mcp.json` 이 설치 위치를 정확히 가리킴
491
651
  - `hi-loop-setup` 멱등, `--dry-run` 무기록
@@ -520,8 +680,9 @@ cat .hi-loop/STATE.json | jq '{status, phase, iteration, sessionId, sessionSeria
520
680
  - **MCP 모드에서 실제 에이전트 spawn** — 도구 왕복(initialize/tools/call)만 확인
521
681
  - 장시간(10회) 실제 루프
522
682
  - 텔레그램 실제 발송
523
- - 레지스트리에는 **0.4.0** 이 게시돼 있다 — 이번 세션의 변경(0.4.1: `.hi-loop/` 상태·대문자 규약·
524
- MCP `checks`)을 레지스트리로 받으려면 **0.4.1 재게시**가 필요하다. 그전까지 최신은 로컬 설치(§2)로만.
683
+ - 레지스트리에는 **0.4.0** 이 게시돼 있다 — 이후 변경(`.hi-loop/` 상태·대문자 규약·MCP `checks`·
684
+ `--on-fail`·MCP 진행 알림·에이전트 프로바이더 어댑터·스텝 모드·flow 라우팅 수정·`/flow` 단계어·doc-sync,
685
+ 현재 **0.6.0**)을 레지스트리로 받으려면 **0.6.0 재게시**가 필요하다. 그전까지 최신은 로컬 설치(§2)로만.
525
686
 
526
687
  **재현 절차**
527
688
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuzi-ince/hi-loop",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "CLI와 MCP 모드를 지원하는 초경량 자율형 자가 치유 엔진",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,10 +3,12 @@ name: flow
3
3
  description: >-
4
4
  기획·설계가 필요한 기능 개발/개선 요청을 소스만 보고 바로 구현하지 말고, PLAN→DESIGN→DO→CHECK→HEAL
5
5
  자율 루프(hiloop_run)로 처리한다. spec/설계를 먼저 세우고 테스트가 통과할 때까지 반복하며, full=true면
6
- 발굴·설계리뷰·코드리뷰까지 전체 라이프사이클을 돈다. Use this whenever the user asks to build or improve
7
- a feature that benefits from planning/design before coding, or mentions the hi-loop/PDCA/TDD lifecycle.
8
- Triggers: 기획, 설계, 구현, 개선, 기능 추가, 리팩터, plan, design, implement, feature, improve, refactor,
9
- PDCA, TDD, lifecycle, 자가치유, self-healing, hi-loop, hiloop.
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) 확정.** 사용자의 요청을 문장의 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,13 +74,16 @@ 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
- - `full: true` — 발굴·설계리뷰·코드리뷰를 포함한 전체 라이프사이클
81
+ - `step: true` — **대화형 기본값.** 단위 작업 후 제어를 돌려받아(진행이 사용자에게 보이고,
82
+ 콜이 짧아 타임아웃이 없다) 이어 호출한다(아래 5-a 참조). 무인·완료까지 한 번에 돌려야 할
83
+ 특별한 이유가 있을 때만 뺀다.
84
+ - **단계어가 있으면** 위 "인자 문법" 표대로 `stopAfter`(및 필요한 `full`/`ship`/`watch`)를 준다.
85
+ **없으면** `full: true` — 발굴·설계리뷰·코드리뷰를 포함한 전체 라이프사이클로 완료까지.
47
86
  - `testCommand`: 프로젝트의 테스트 명령(기본 `npm test`)
48
- - 필요 시 `maxLoops`, `ship`, `watch`
49
87
  - **UI 작업이면 e2e 를 엔진 게이트로 넣는다(권장 — LLM 기억에 의존하지 않는 결정론적 강제).**
50
88
  goal 이 UI(화면·컴포넌트·라우팅·스타일)를 건드릴 것 같고 프로젝트에 **셸 e2e 명령**
51
89
  (package.json `test:e2e` 스크립트, 또는 `playwright.config.*` → `npx playwright test`)이 있으면,
@@ -60,7 +98,12 @@ user-invocable: true
60
98
  - 셸 e2e 명령이 없고 **Playwright MCP 만** 있으면 `checks` 를 쓰지 않는다(엔진은 MCP 도구를 못 부른다)
61
99
  — 루프 통과 후 7단계에서 MCP 로 돌린다. **둘 다 없으면** e2e 는 스킵한다(도구를 임의 설치하지 않는다).
62
100
 
63
- 5. **사용자 선택 처리.** 응답이 `⏸ 사용자의 선택이 필요합니다`오면,
101
+ 5-a. **스텝 이어가기 (step 모드).** 응답이 `⏭ 스텝 완료 …`오면 루프가 **아직 미완**이다.
102
+ 그 한 줄 진행(어느 회차·어느 단계인지)을 **사용자에게 중계**한 뒤, **같은 인자로 `hiloop_run` 을
103
+ 다시 호출**해 다음 스텝을 진행한다. `✅ 통과` / `❌ 실패` / `⏸ 선택 필요` 가 나올 때까지 반복한다.
104
+ 이렇게 해야 매 스텝이 콘솔에 보이고 긴 콜의 타임아웃이 없다(백그라운드에서 조용히 도는 일이 없다).
105
+
106
+ 5-b. **사용자 선택 처리.** 응답이 `⏸ 사용자의 선택이 필요합니다` 로 오면,
64
107
  그 질문을 **그대로 사용자에게 제시**하고 답을 받는다. 답을 `hiloop_answer`
65
108
  (`choice`, 필요하면 `note`)로 전달한 뒤, **같은 goal 로 `hiloop_run` 을 다시 호출**해 이어간다.
66
109
 
@@ -121,3 +164,4 @@ user-invocable: true
121
164
  - `hiloop_rollback` — git 체크포인트로 파일 복원(회차 지정 가능)
122
165
  - `hiloop_reset` — 루프 상태 초기화
123
166
  - `hiloop_setup` — 프로젝트에 `.mcp.json`·`.gitignore`·`docs/`·`tests/`·CLAUDE.md 규칙 주입
167
+ - `hiloop_docsync` — 소스 변경(git diff)을 문서에 반영하는 단발 패스(`/flow doc-sync` 가 이걸 부른다)
package/src/ask.js CHANGED
@@ -123,6 +123,18 @@ export function applyAnswer(state, { choice, note = '', now = new Date().toISOSt
123
123
  };
124
124
  }
125
125
 
126
+ /** FR-22 on-fail 게이트 질문 스펙 (재시도/종료). createAsk 에 그대로 넘긴다. */
127
+ export function onFailAsk({ stage, blocked, summary }) {
128
+ return {
129
+ stage,
130
+ question: `${blocked ? '⚠️ 테스트를 판정할 수 없습니다(불능).' : '테스트가 실패했습니다.'}\n${summary}\n계속 진행할까요?`,
131
+ options: [
132
+ { key: 'a', label: '재시도 — 수정하고 한 번 더 돌린다', impact: '회차·비용 추가' },
133
+ { key: 'b', label: '종료 — 여기서 멈춘다', impact: '지금까지의 산출물은 남는다' },
134
+ ],
135
+ };
136
+ }
137
+
126
138
  /** 에이전트가 스스로 세운 가정을 기록한다 (policy=never 또는 분기가 아닌 모호성). */
127
139
  export function recordAssumption(state, { text, stage, now = new Date().toISOString() }) {
128
140
  const clean = String(text ?? '').trim();
package/src/build.js CHANGED
@@ -46,10 +46,12 @@ 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 } = 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;
53
+ // FR-22: on-fail ask 로 사용자가 "재시도"를 고른 만큼 회차 상한이 늘어난다(사람이 곧 상한).
54
+ const cap = () => maxLoops + (state.extraLoops || 0);
53
55
 
54
56
  state.stage = 'BUILD';
55
57
  state.status = 'running';
@@ -61,7 +63,14 @@ export async function runBuildLoop(ctx) {
61
63
  }
62
64
  saveState(statePath, state);
63
65
 
64
- while (state.iteration < maxLoops) {
66
+ // FR-22 재개: 직전 실패에서 on-fail ask 로 물었던 답을 처리한다('a'=재시도→회차 +1, 'b'=종료).
67
+ const pendingFail = [...(state.answers ?? [])].find((a) => a.stage === `ON_FAIL:${state.iteration}`);
68
+ if (pendingFail) {
69
+ if (pendingFail.choice === 'a') state.extraLoops = (state.extraLoops || 0) + 1;
70
+ else return { ok: false, stopReason: 'user-stop', detail: '사용자가 종료를 선택했습니다.' };
71
+ }
72
+
73
+ while (state.iteration < cap()) {
65
74
  // 예산은 **다음 호출 앞**에서 막는다. 이미 쓴 돈은 되돌릴 수 없으므로
66
75
  // 할 수 있는 일은 "더 쓰지 않는 것"뿐이다.
67
76
  if (budgetUsd != null && state.costUsd >= budgetUsd) {
@@ -127,6 +136,7 @@ export async function runBuildLoop(ctx) {
127
136
  state.iteration -= 1;
128
137
  saveState(statePath, state);
129
138
  logger(`[hi-loop] 📐 설계 리뷰 기각 (${state.designRounds}/${maxDesignRounds}) — 스펙을 다시 씁니다: ${truncate(verdict.reason ?? '', 200)}`);
139
+ if (step) return { ok: false, continue: true, phase: 'PLAN', iteration: state.iteration };
130
140
  continue;
131
141
  }
132
142
  logger('[hi-loop] 📐 설계 리뷰 통과.');
@@ -206,6 +216,21 @@ export async function runBuildLoop(ctx) {
206
216
  state.lastError = truncate(failure, LIMITS.lastError);
207
217
  state.phase = 'ACT';
208
218
 
219
+ // ---- FR-22 on-fail 게이트 ----
220
+ // 실패/불능을 사람에게 물을지·즉시 멈출지. PLAN 회차(1)는 제외한다(구현 전이라 실패가 당연).
221
+ // heal(기본)은 이 블록을 건너뛰어 종전대로 자동 치유한다 — 기존 동작 불변.
222
+ if (state.iteration >= 2 && onFail !== 'heal') {
223
+ const blocked = !integrity.ok || regressed.length > 0; // 판정 불능/메타 문제(무결성·비회귀)
224
+ // stop, 또는 물을 수 없는 환경(ask never)은 안전하게 종료 — 무인 폭주 방지.
225
+ if (onFail === 'stop' || askPolicy === 'never') {
226
+ saveState(statePath, state);
227
+ return { ok: false, stopReason: 'on-fail-stop', detail: truncate(state.lastError, 500) };
228
+ }
229
+ // ask: 사람에게 재시도/종료를 묻는다. pause 신호만 반환하고 loop.js 가 ask 를 세팅한다.
230
+ saveState(statePath, state);
231
+ return { paused: true, onFail: { stage: `ON_FAIL:${state.iteration}`, blocked, summary: truncate(state.lastError, 400) } };
232
+ }
233
+
209
234
  // ---- 정체 감지 (L2) ----
210
235
  // 같은 실패가 stagnationLimit 회 연속이면 이 접근으로는 못 고친다는 뜻 —
211
236
  // 같은 값을 태우며 maxLoops 를 다 쓰는 것은 순수한 낭비다(실측: 함정에 $3.07).
@@ -236,7 +261,7 @@ export async function runBuildLoop(ctx) {
236
261
  logger(`[hi-loop] ❌ 테스트 실패 (code ${check?.code}) — 치유를 시도합니다.`);
237
262
  }
238
263
 
239
- if (stagnationLimit != null && state.stagnantRuns >= stagnationLimit && state.iteration < maxLoops) {
264
+ if (stagnationLimit != null && state.stagnantRuns >= stagnationLimit && state.iteration < cap()) {
240
265
  logger(`[hi-loop] 🔁 같은 실패가 ${state.stagnantRuns}회 반복 — 접근이 막혔습니다. ${state.iteration}회차에서 중단합니다. (누적 $${state.costUsd.toFixed(2)})`);
241
266
  return {
242
267
  ok: false,
@@ -246,7 +271,7 @@ export async function runBuildLoop(ctx) {
246
271
  }
247
272
 
248
273
  // ---- 세션 핸드오프 & 컨텍스트 다이어트 (FR-3.2) ----
249
- if (state.iteration % handoffEvery === 0 && state.iteration < maxLoops) {
274
+ if (state.iteration % handoffEvery === 0 && state.iteration < cap()) {
250
275
  state.sessionId = null;
251
276
  state.sessionSerial += 1;
252
277
  saveState(statePath, state);
@@ -257,6 +282,12 @@ export async function runBuildLoop(ctx) {
257
282
  detail: `session #${state.sessionSerial} 로 컨텍스트 다이어트 후 재개`,
258
283
  });
259
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 };
260
291
  }
261
292
 
262
293
  logger(`[hi-loop] 한도(${maxLoops}회)를 소진했지만 테스트가 통과하지 못했습니다. (누적 $${state.costUsd.toFixed(2)})`);
@@ -90,6 +90,9 @@ export function parseRunOptions(args, { staged = {} } = {}) {
90
90
 
91
91
  const onShipFail = enumOpt('on-ship-fail', ['stop', 'heal'], 'stop');
92
92
  if (onShipFail?.error) return bad(onShipFail.error);
93
+ // FR-22: CHECK 실패/불능 시 안쪽 루프를 어떻게 다룰까. heal=자동수정(기본) / ask=사람에게 물음 / stop=즉시 중단.
94
+ const onFail = enumOpt('on-fail', ['heal', 'ask', 'stop'], 'heal');
95
+ if (onFail?.error) return bad(onFail.error);
93
96
  const onWatchFail = enumOpt('on-watch-fail', ['stop', 'rollback', 'heal'], 'stop');
94
97
  if (onWatchFail?.error) return bad(onWatchFail.error);
95
98
 
@@ -138,6 +141,7 @@ export function parseRunOptions(args, { staged = {} } = {}) {
138
141
  commitMessage: str('commit-message'),
139
142
  onShipFail,
140
143
  onWatchFail,
144
+ onFail,
141
145
  askPolicy,
142
146
  yes: Boolean(args.yes),
143
147
  full: Boolean(args.full),
@@ -149,6 +153,8 @@ export function parseRunOptions(args, { staged = {} } = {}) {
149
153
  reconcileSpec: str('reconcile-spec'),
150
154
  startFrom,
151
155
  stopAfter,
156
+ // FR-24: 스텝 모드. 매 단위 작업 후 status:continue 로 제어를 돌려준다(호출자가 이어 호출).
157
+ step: Boolean(args.step),
152
158
  ...durations,
153
159
  ...numbers,
154
160
  // 서브커맨드는 그 단계를 **하라는 명령**이다. 자동 기본값이 그걸 끄면 안 된다.