@tuzi-ince/hi-loop 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ hi-loop mcp
40
40
  hi-loop status # 현재 루프 상태 요약
41
41
  ```
42
42
 
43
- MCP 도구: `hiloop_run`, `hiloop_status`, `hiloop_reset`.
43
+ MCP 도구: `hiloop_run`, `hiloop_answer`, `hiloop_status`, `hiloop_reset`, `hiloop_rollback`, `hiloop_setup`.
44
44
 
45
45
  ## 동작 원리
46
46
 
@@ -51,6 +51,87 @@ CHECK ──► 엔진이 testCommand 를 직접 실행 (에이전트의 "다
51
51
  ACT ──► stderr 를 그대로 에이전트에 들이밀고 "이 에러를 고쳐라" (최대 10회)
52
52
  ```
53
53
 
54
+ ## 전과정 라이프사이클 (`--full`)
55
+
56
+ 기본은 위의 자가 치유 루프 하나다. `--full` 을 붙이면 앞뒤 단계까지 돈다.
57
+
58
+ ```
59
+ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW → SHIP → WATCH → DONE
60
+ ▲ │ │ │
61
+ └── reject ┘ reject ──┘ fail ─────┘
62
+ ```
63
+
64
+ ```bash
65
+ # 아이디어 한 줄 → 발굴 → 스펙 → 구현 → 리뷰 → 배포 → 감시. 사람 개입 최소.
66
+ hi-loop run --goal "JWT 인증 미들웨어" --full \
67
+ --ship "npm publish --access public" \
68
+ --watch "curl -f https://api.example.com/health" --watch-for 5m
69
+ ```
70
+
71
+ **추가되는 모든 단계는 기계가 판정할 종료 조건을 가진다.** 없으면 그건 엔진의 단계가
72
+ 아니라 문서 생성기다.
73
+
74
+ | 단계 | 무엇을 하는가 | 종료 조건 |
75
+ |---|---|---|
76
+ | DISCOVER | goal 의 모호함을 **가정으로 확정**하고 문서화 | 가정 목록 확정 |
77
+ | DESIGN_REVIEW | 구현 착수 **전** 스펙을 심판 | 독립 검증자 pass |
78
+ | CODE_REVIEW | diff 를 정확성·보안·YAGNI 축으로 심판 | 독립 검증자 pass |
79
+ | SHIP | 배포 명령 실행 | **exit code 0** |
80
+ | WATCH | 헬스체크 반복 | 지정 시간 동안 버팀 |
81
+
82
+ 배포·감시는 **에이전트를 부르지 않는다.** "배포했다고 모델이 말했다"가 아니라
83
+ "헬스체크 exit code 가 0이다"가 통과 조건이다. 그래서 이 두 단계의 비용은 0이다.
84
+
85
+ ### 기본은 무중단, 필연적 선택에서만 묻는다
86
+
87
+ 리뷰 단계들은 **모델이 심판하고 엔진이 되돌린다.** 사람을 안 부른다 —
88
+ 설계 리뷰가 기각하면 PLAN 을 다시 쓰고, 코드 리뷰가 기각하면 그 지적이 HEAL 의 연료가 된다.
89
+
90
+ 사람이 멈춰 서는 곳은 둘뿐이다:
91
+ 1. DISCOVER 가 **상호배타 분기**를 만났을 때 (가정으로 답할 수 없는 지점)
92
+ 2. SHIP 직전 (비가역 외부 행위. `--yes` 로 생략)
93
+
94
+ ```
95
+ $ hi-loop run --goal "..." --full
96
+ ⏸ 선택이 필요합니다 [DISCOVER]
97
+ 인증 저장소를 어디에 둘까?
98
+ a) 기존 users 테이블 확장 — 마이그레이션 필요
99
+ b) 별도 auth_tokens 테이블 신규 — 조인 비용
100
+ → hi-loop answer <a|b> [--note "..."]
101
+
102
+ $ hi-loop answer a # 고른 것도 "가정"으로 기록돼 이후 프롬프트에 실린다
103
+ ```
104
+
105
+ 차단형 입력을 쓰지 않고 **상태를 저장하고 종료**한다(exit 3). 그래서 MCP·백그라운드
106
+ 데몬·텔레그램 봇에서도 그대로 성립한다. MCP 에서는 호스트 LLM 이 질문을 사용자에게
107
+ 제시하고 `hiloop_answer` 로 답을 돌려준다.
108
+
109
+ `--ask never` 면 분기에서도 안 멈춘다(CI·봇용). 대신 **미해결이라는 사실이 Gaps 에 남는다.**
110
+
111
+ ### 단계별로 따로 부를 수도 있다
112
+
113
+ ```bash
114
+ hi-loop discover --goal "..." # 발굴만 (가정 확정 + 문서)
115
+ hi-loop plan --goal "..." # 스펙·테스트까지만 (구현 전)
116
+ hi-loop review # 현재 변경분만 코드 리뷰
117
+ hi-loop ship --ship "npm publish" # 배포 단계만
118
+ hi-loop watch --watch "curl -f ..."
119
+ ```
120
+
121
+ 어느 경로로 들어와도 **같은 상태 머신**을 쓴다. 후반 단계는 `--goal` 을 다시 안 받는다 —
122
+ 저장된 상태에서 읽는다(한 글자만 달라도 새 루프로 인식돼 진행 상황이 버려지기 때문).
123
+
124
+ ### 왜 기본값이 "꺼짐"인가
125
+
126
+ 새 단계는 각각 에이전트 호출을 1회씩 더한다. 전역 기본 ON 으로 하면 업데이트만 한
127
+ 기존 사용자의 실행 비용이 조용히 는다. 그래서:
128
+
129
+ - 플래그가 없으면 **종전 그대로** 자가 치유 루프 하나만 돈다.
130
+ - `--full` → 발굴·설계리뷰·코드리뷰 켜짐.
131
+ - `--ship` → 리뷰들만 자동 켜짐 ("리뷰 없는 자동 배포가 위험하다"는 근거가 여기서만 성립).
132
+ - `--review/--no-review`, `--design-review/--no-design-review`, `--discover/--no-discover`
133
+ 가 위 자동을 양방향으로 덮는다.
134
+
54
135
  ### 세션 핸드오프 & 컨텍스트 다이어트 (bkit 철학)
55
136
 
56
137
  에이전트는 같은 세션에서 오래 굴릴수록 토큰이 쌓여 멍청해진다.
@@ -146,6 +227,8 @@ hi-loop rollback --to 3 # 3회차 직전 상태로
146
227
  | `HILOOP_AGENT_ARGS` | 에이전트에 덧붙일 인자 (공백 구분). 예: `--model sonnet` — 비용에 직결된다 |
147
228
  | `HILOOP_PERMISSION_MODE` | 권한 모드 (기본 `acceptEdits`). 편집 허용이 이 엔진의 전제다 |
148
229
  | `HILOOP_VERIFY_CMD` / `HILOOP_VERIFY_MODEL` | `--verify-spec` 검증자의 실행 명령·모델 (미설정 시 구현자와 동일). bkit처럼 검증에 더 센 모델을 쓸 수 있다 |
230
+ | `HILOOP_REVIEW_CMD` / `HILOOP_REVIEW_MODEL` | 설계·코드 리뷰어의 실행 명령·모델 (미설정 시 `HILOOP_VERIFY_*` → 구현자 순으로 폴백) |
231
+ | `HILOOP_DISCOVER_CMD` / `HILOOP_DISCOVER_MODEL` | 발굴 단계의 실행 명령·모델 |
149
232
  | `HILOOP_AGENT_TIMEOUT_MS` / `HILOOP_TEST_TIMEOUT_MS` | 에이전트(30분)·테스트(10분) 타임아웃. 쓰레기값은 기본값으로 되돌림 |
150
233
  | `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | 시작/핸드오프/성공/실패 알림. 미설정 시 조용히 생략 |
151
234
 
@@ -158,9 +241,17 @@ npm test # node:test, devDependency 0개
158
241
  ## 구조
159
242
 
160
243
  ```
161
- bin/hi-loop.js CLI 엔트리포인트 (CLI / MCP 분기 + rollback)
244
+ bin/hi-loop.js CLI 엔트리포인트 (CLI / MCP 분기 + rollback + answer + 단계별 실행)
162
245
  bin/setup.js 프로젝트·클로드코드 설정 자동 주입
163
- src/loop.js 자가 치유 오케스트레이션 루프 + 예산·정체·2단 판정·락·삭제 관측 집행
246
+ src/loop.js 라이프사이클 stage 머신 다음 stage 정하는 권한은 여기 한 곳뿐
247
+ src/build.js 안쪽 자가 치유 루프 — 예산·정체·무결성·2단 판정·핸드오프 집행
248
+ src/stages.js DISCOVER / CODE_REVIEW / SHIP / WATCH 핸들러 (지시만 반환)
249
+ src/discover.js DISCOVER — 요구사항 발굴, 가정 확정, 분기 감지 (FR-10)
250
+ src/review.js DESIGN_REVIEW / CODE_REVIEW — 격리된 Tier 2 심판 (FR-11, FR-12)
251
+ src/ship.js SHIP / WATCH — 배포·헬스체크. 에이전트를 부르지 않는다 (FR-13, FR-14)
252
+ src/ask.js ask 모드 — pause/resume 상태머신, 가정 기록 (FR-16)
253
+ src/report.js 완료 보고 — Gaps 공개(L10) + 상태 요약
254
+ src/cli-options.js CLI 인자 → runLoop 옵션 번역 (잘못된 값은 조용히 흘리지 않는다)
164
255
  src/state.js .agent-state.json (원자적 저장 + 다이어트 + 산출물 경로 + resume + Gaps)
165
256
  src/runners.js 에이전트/테스트 실행 어댑터 (주입 가능) + 비용 파싱 + 타임아웃 (L6)
166
257
  src/prompts.js PDCA 단계별 프롬프트 + 검증자 프롬프트
package/bin/hi-loop.js CHANGED
@@ -28,8 +28,26 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
28
28
  사용법:
29
29
  hi-loop run --goal "<요구사항>" [--test "npm test"] [--max-loops 10] [--budget-usd 5]
30
30
  [--stagnation 3 | --no-stagnation] [--verify-spec] [--cwd .]
31
+ [--ship "<배포 명령>"] [--on-ship-fail stop|heal]
32
+ [--review | --no-review] [--max-review-rounds 2]
33
+ [--design-review | --no-design-review] [--max-design-rounds 2]
34
+ [--full] [--discover | --no-discover]
35
+ [--watch "<헬스체크 명령>"] [--watch-for 5m] [--watch-every 30s]
36
+ [--watch-tolerate 1] [--on-watch-fail stop|rollback|heal]
37
+ [--ask auto|never|always] [--yes]
38
+ [--start-from <stage>] [--stop-after <stage>]
39
+ stage: DISCOVER | PLAN | BUILD | CODE_REVIEW | SHIP | WATCH
40
+
41
+ 단계별 실행 (전부 같은 상태 머신을 쓴다):
42
+ hi-loop discover --goal "<요구사항>" 발굴만 하고 멈춤 (가정 확정 + 문서 생성)
43
+ hi-loop plan --goal "<요구사항>" 스펙·테스트까지 만들고 멈춤 (구현 전)
44
+ hi-loop review 현재 변경분만 코드 리뷰
45
+ hi-loop ship --ship "<명령>" 배포 단계만 실행
46
+ hi-loop watch --watch "<명령>" 헬스체크 단계만 실행
47
+
31
48
  hi-loop mcp MCP 서버(stdio)로 기동. 인자가 없으면 기본 동작.
32
49
  hi-loop status [--cwd .] 현재 루프 상태 요약
50
+ hi-loop answer <선택> [--note "..."] 대기 중인 질문에 답하고 루프를 재개 가능 상태로 되돌림
33
51
  hi-loop rollback [--to N] [--cwd .] N회차 직전 체크포인트로 파일 복원 (기본: 최신)
34
52
  hi-loop setup [--dry-run] 프로젝트/클로드코드 설정 자동 주입
35
53
  hi-loop --help | --version
@@ -39,6 +57,8 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
39
57
  HILOOP_AGENT_ARGS 에이전트에 덧붙일 인자 (공백 구분, 예: --model sonnet)
40
58
  HILOOP_PERMISSION_MODE 권한 모드 (기본: acceptEdits)
41
59
  HILOOP_VERIFY_CMD/MODEL --verify-spec 검증자의 실행 명령·모델 (기본: 구현자와 동일)
60
+ HILOOP_REVIEW_CMD/MODEL 설계·코드 리뷰어의 실행 명령·모델
61
+ HILOOP_DISCOVER_CMD/MODEL 발굴 단계의 실행 명령·모델
42
62
  HILOOP_AGENT_TIMEOUT_MS 에이전트 타임아웃 (기본: 1800000 = 30분)
43
63
  HILOOP_TEST_TIMEOUT_MS 테스트 타임아웃 (기본: 600000 = 10분)
44
64
  TELEGRAM_BOT_TOKEN/CHAT_ID 텔레그램 알림 (미설정 시 생략)`;
@@ -46,7 +66,7 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
46
66
  export async function main(argv = process.argv.slice(2)) {
47
67
  const args = parseArgs(argv, {
48
68
  alias: { g: 'goal', t: 'test', c: 'cwd', h: 'help', v: 'version', m: 'max-loops' },
49
- boolean: ['help', 'version', 'dry-run', 'no-stagnation', 'verify-spec'],
69
+ boolean: ['help', 'version', 'dry-run', 'no-stagnation', 'verify-spec', 'yes', 'review', 'no-review', 'design-review', 'no-design-review', 'full', 'discover', 'no-discover'],
50
70
  });
51
71
  const command = args._[0] ?? (args.help || args.version ? null : 'mcp');
52
72
 
@@ -61,45 +81,58 @@ export async function main(argv = process.argv.slice(2)) {
61
81
 
62
82
  const cwd = resolve(args.cwd ?? process.cwd());
63
83
 
84
+ /**
85
+ * 단계별 서브커맨드 → run 의 인자로 번역한다 (FR-15.3).
86
+ * 별도 실행 경로를 만들지 않는 이유: 경로가 둘이면 한쪽은 반드시 뒤처져 썩는다.
87
+ */
88
+ const STAGE_COMMANDS = {
89
+ discover: { startFrom: 'DISCOVER', stopAfter: 'DISCOVER', force: { discover: true } },
90
+ plan: { stopAfter: 'PLAN' },
91
+ review: { startFrom: 'CODE_REVIEW', stopAfter: 'CODE_REVIEW', force: { codeReview: true } },
92
+ ship: { startFrom: 'SHIP', stopAfter: 'SHIP' },
93
+ watch: { startFrom: 'WATCH', stopAfter: 'WATCH' },
94
+ };
95
+
64
96
  switch (command) {
65
- case 'run': {
66
- if (!args.goal || args.goal === true) {
67
- process.stderr.write(`--goal 이 필요합니다.\n\n${USAGE}\n`);
68
- return 2;
97
+ case 'run':
98
+ case 'discover':
99
+ case 'plan':
100
+ case 'review':
101
+ case 'ship':
102
+ case 'watch': {
103
+ const staged = STAGE_COMMANDS[command] ?? {};
104
+ // 후반 단계는 goal 을 다시 받을 이유가 없다 — 이미 상태 파일에 있다.
105
+ // 여기서 goal 을 요구하면 사용자가 매번 같은 문자열을 정확히 다시 쳐야 하고,
106
+ // 한 글자만 달라도 resumeOrCreate 가 **새 루프**로 인식해 진행 상황을 버린다.
107
+ let goal = args.goal && args.goal !== true ? String(args.goal) : null;
108
+ if (!goal) {
109
+ const { loadState, statePathFor } = await import('../src/state.js');
110
+ goal = loadState(statePathFor(cwd))?.goal ?? null;
69
111
  }
70
- const { runLoop } = await import('../src/loop.js');
71
- const maxLoops = Number(args['max-loops'] ?? 10);
72
- // 예산은 명시할 때만 건다. 잘못된 값으로 조용히 무제한이 되지 않도록 exit 2 로 거른다.
73
- const budgetRaw = args['budget-usd'];
74
- let budgetUsd = null;
75
- if (budgetRaw !== undefined && budgetRaw !== true) {
76
- budgetUsd = Number(budgetRaw);
77
- if (!(Number.isFinite(budgetUsd) && budgetUsd > 0)) {
78
- process.stderr.write(`--budget-usd 는 0 보다 큰 숫자여야 합니다: ${budgetRaw}\n`);
79
- return 2;
80
- }
112
+ if (!goal) {
113
+ process.stderr.write(`--goal 필요합니다 (저장된 루프 상태도 없습니다).\n\n${USAGE}\n`);
114
+ return 2;
81
115
  }
82
- // 정체 상한: --no-stagnation 으로 끄거나 --stagnation N 으로 조절. 기본 3.
83
- let stagnationLimit = 3;
84
- if (args['no-stagnation']) stagnationLimit = null;
85
- else if (args.stagnation !== undefined && args.stagnation !== true) {
86
- const n = Number(args.stagnation);
87
- if (!(Number.isInteger(n) && n > 0)) {
88
- process.stderr.write(`--stagnation 은 1 이상의 정수여야 합니다: ${args.stagnation}\n`);
89
- return 2;
90
- }
91
- stagnationLimit = n;
116
+
117
+ const { parseRunOptions } = await import('../src/cli-options.js');
118
+ const parsed = parseRunOptions(args, { staged });
119
+ if (!parsed.ok) {
120
+ process.stderr.write(`${parsed.message}\n`);
121
+ return 2;
92
122
  }
123
+
124
+ const { runLoop } = await import('../src/loop.js');
93
125
  const result = await runLoop({
94
- goal: String(args.goal),
95
- testCommand: args.test && args.test !== true ? String(args.test) : 'npm test',
96
- maxLoops: Number.isInteger(maxLoops) && maxLoops > 0 ? maxLoops : 10,
97
- budgetUsd,
98
- stagnationLimit,
99
- verifySpec: Boolean(args['verify-spec']),
126
+ goal,
100
127
  cwd,
128
+ ...parsed.options,
101
129
  logger: (line) => process.stdout.write(`${line}\n`),
102
130
  });
131
+ // awaiting 은 성공도 실패도 아니다. 0 을 주면 `hi-loop run && 배포` 같은 스크립트가
132
+ // 질문에 답도 안 한 채 다음 단계로 넘어간다. 1 을 주면 진짜 실패와 구분이 안 된다.
133
+ if (result.status === 'awaiting') return 3;
134
+ // stopped 는 시킨 것을 다 한 것이다. 실패가 아니다.
135
+ if (result.status === 'stopped') return 0;
103
136
  return result.status === 'passed' ? 0 : 1;
104
137
  }
105
138
  case 'mcp': {
@@ -112,6 +145,26 @@ export async function main(argv = process.argv.slice(2)) {
112
145
  process.stdout.write(`${summarizeState(loadState(statePathFor(cwd)))}\n`);
113
146
  return 0;
114
147
  }
148
+ case 'answer': {
149
+ const { loadState, saveState, statePathFor } = await import('../src/state.js');
150
+ const { applyAnswer } = await import('../src/ask.js');
151
+ const statePath = statePathFor(cwd);
152
+ const state = loadState(statePath);
153
+ if (!state?.ask) {
154
+ process.stderr.write('대기 중인 질문이 없습니다.\n');
155
+ return 1;
156
+ }
157
+ const note = args.note && args.note !== true ? String(args.note) : '';
158
+ const res = applyAnswer(state, { choice: args._[1], note });
159
+ if (!res.ok) {
160
+ process.stderr.write(`${res.reason}\n`);
161
+ return 2;
162
+ }
163
+ saveState(statePath, res.state);
164
+ const last = res.state.assumptions[res.state.assumptions.length - 1];
165
+ process.stdout.write(`✅ 기록했습니다: ${last.text}\n 이어서 진행하려면 같은 goal 로 hi-loop run 을 다시 실행하세요.\n`);
166
+ return 0;
167
+ }
115
168
  case 'rollback': {
116
169
  const { loadState, statePathFor } = await import('../src/state.js');
117
170
  const { rollbackTo } = await import('../src/checkpoint.js');
package/docs/design.md CHANGED
@@ -629,3 +629,80 @@ hi-loop run --goal "두 수를 더하는 add 함수를 만들어라" --test "npm
629
629
  | FR-6 setup | — | `bin/setup.js` | AC-17, 18 |
630
630
  | NFR-3 주입 가능 | §6 | `src/runners.js` | 전 테스트가 네트워크 없이 동작 |
631
631
  | NFR-4 원자적 저장 | §4.4 | `src/state.js` | AC-19 |
632
+
633
+ ---
634
+
635
+ ## 12. 라이프사이클 확장 (v0.2) — 설계 근거
636
+
637
+ 전체 근거와 대안 검토는 [`claudedocs/lifecycle-proposal.md`](../claudedocs/lifecycle-proposal.md).
638
+ 여기에는 **왜 그렇게 갈랐는지**만 남긴다.
639
+
640
+ ### 12.1 확장의 규칙 하나 (G-NEW)
641
+
642
+ > 새 단계는 기계가 판정할 종료 조건을 가져야 한다.
643
+
644
+ G1("판정 주체는 엔진")의 연장이다. 이 규칙이 없으면 확장은 "에이전트를 한 번 더 부르고
645
+ 그 말을 믿는" 단계들의 나열이 되고, 그건 이 엔진이 CHECK 에서 이미 거부한 방식이다.
646
+ 그래서 배포·감시는 exit code 로, 리뷰들은 **기각만 가능한 하향 전용 심판**으로 설계했다.
647
+
648
+ ### 12.2 되돌림은 전부 기존 경로 재사용
649
+
650
+ | 기각 주체 | 되돌아가는 곳 | 연료 |
651
+ |---|---|---|
652
+ | DESIGN_REVIEW | PLAN (회차를 되돌린다) | 기각 사유 |
653
+ | CODE_REVIEW | HEAL | findings |
654
+ | SHIP (`--on-ship-fail heal`) | HEAL | 배포 로그 |
655
+ | WATCH (`--on-watch-fail heal`) | HEAL | 헬스체크 에러 |
656
+
657
+ 새 치유 경로를 하나도 만들지 않았다. 안쪽 루프는 "에러 문자열을 받아 고친다"는 인터페이스
658
+ 하나로 이미 일반적이었고, 새 단계들은 그 인터페이스에 문자열을 넣어줄 뿐이다.
659
+ 덕분에 무결성 가드·정체 감지·체크포인트·예산이 새 단계에도 그대로 적용된다.
660
+
661
+ ### 12.3 DESIGN_REVIEW 가 PLAN 을 되돌릴 때 CHECK 를 건너뛰는 이유
662
+
663
+ `state.iteration -= 1` 후 `continue` 한다. 이때 테스트를 돌리지 않는다 —
664
+ 구현이 없는 상태의 테스트 실패는 **확정적**이고, 확정적으로 아는 것을 확인하려고
665
+ 테스트 명령을 한 번 더 돌리는 것은 순수한 낭비다.
666
+
667
+ ### 12.4 ask 가 차단형 stdin 이 아닌 이유
668
+
669
+ 세 가지 구동 형태(CLI 전경 / MCP 서버 / 백그라운드·봇) 중 stdin 이 성립하는 것은 하나뿐이다.
670
+ MCP 는 단일 요청/응답이라 서버가 사람을 기다릴 자리가 없고, 봇에는 터미널이 없다.
671
+ 그래서 **상태를 저장하고 종료**한다. 이 선택의 부수 효과가 좋다:
672
+ - 세션이 끊겨도 질문이 파일에 남는다.
673
+ - MCP 에서는 호스트 LLM 이 질문을 사용자에게 대신 제시한다 — 오히려 자연스럽다.
674
+ - exit 3 으로 `run && 배포` 류 스크립트가 답 없이 진행하는 것을 막는다.
675
+
676
+ ### 12.5 stage vs phase 를 분리한 이유
677
+
678
+ `phase` 의 의미(안쪽 루프의 회차별 위치)를 바꾸지 않았다. 바꿨다면 기존 상태 파일·테스트·
679
+ 문서·프롬프트가 전부 흔들린다. 새 축(`stage`)을 하나 더 두는 쪽이 싸다.
680
+
681
+ ### 12.6 모듈 분리 (NFR-5)
682
+
683
+ 확장 과정에서 `loop.js` 가 653줄까지 불어 NFR-5(파일당 300줄)를 어겼다. 되돌린 방식:
684
+
685
+ | 파일 | 책임 |
686
+ |---|---|
687
+ | `loop.js` | **다음 stage 를 정한다.** 그것만 한다 |
688
+ | `build.js` | 안쪽 자가 치유 루프 (검증된 심장, 로직 무변경) |
689
+ | `stages.js` | 각 stage 핸들러 — 상태를 진행시키지 않고 **지시**만 반환 |
690
+ | `report.js` | 사람이 읽는 보고 (상태를 바꾸지 않는다) |
691
+ | `cli-options.js` | 인자 검증 — 잘못된 값을 조용히 기본값으로 흘리지 않는다 |
692
+
693
+ 핸들러가 다음 stage 를 직접 정하지 않게 한 것이 핵심이다. 여러 곳이 `state.stage` 를
694
+ 옮기기 시작하면 흐름을 한눈에 볼 수 없게 된다.
695
+
696
+ ### 12.7 요구 → 구현 대응 (추가분)
697
+
698
+ | 요구 | 구현 | 검증 |
699
+ |---|---|---|
700
+ | FR-10 DISCOVER | `src/discover.js`, `src/stages.js` | `tests/lifecycle.test.js` |
701
+ | FR-11 DESIGN_REVIEW | `src/review.js`, `src/build.js`, `src/prompts.js` | 〃 |
702
+ | FR-12 CODE_REVIEW | `src/review.js`, `src/stages.js` | 〃 |
703
+ | FR-13 SHIP | `src/ship.js`, `src/stages.js` | 〃 |
704
+ | FR-14 WATCH | `src/ship.js`, `src/stages.js` | 〃 |
705
+ | FR-15 단계별 실행 | `src/loop.js`, `bin/hi-loop.js`, `src/cli-options.js` | 〃 |
706
+ | FR-16 ask 모드 | `src/ask.js`, `src/loop.js`, `src/mcp-server.js` | 〃 |
707
+ | FR-17 단계 기본값 | `src/loop.js` | 〃 |
708
+ | FR-18 상태 v2 | `src/state.js` (`migrateState`) | 〃 |
package/docs/guide.md CHANGED
@@ -142,14 +142,85 @@ hi-loop run --goal "JWT 인증 미들웨어를 만들어라" \
142
142
  > 한 번은 $2.58, 한 번은 $4.67 이었다(컨텍스트 크기 차이). 호출 횟수로는 비용을 못 막는다.
143
143
  > 돈을 막으려면 `--budget-usd` 를 써라. 자세한 실측치는 §7.
144
144
 
145
- exit code: `0` 통과 / `1` 실패(한도 소진 또는 예산 소진) / `2` 잘못된 사용.
145
+ exit code: `0` 통과 또는 `--stop-after` 로 정지 / `1` 실패(한도·예산 소진) /
146
+ `2` 잘못된 사용 / `3` **사람의 답을 기다리는 중**(§4-1b).
146
147
  → CI·스크립트에서 `hi-loop run … && echo OK` 로 바로 쓸 수 있다.
148
+ 3 을 따로 둔 이유: 0 이면 `run && 배포` 가 질문에 답도 안 한 채 진행하고,
149
+ 1 이면 진짜 실패와 구분되지 않는다.
147
150
 
148
151
  ```bash
149
- hi-loop status # 현재 루프 상태 요약 (누적 비용·정체 포함)
152
+ hi-loop status # 현재 루프 상태 요약 (누적 비용·정체·대기 질문 포함)
150
153
  hi-loop --help
151
154
  ```
152
155
 
156
+ ### 4-1b. 전과정 라이프사이클 (`--full`) 과 배포·감시
157
+
158
+ 기본은 자가 치유 루프 하나다. 앞뒤 단계는 **켤 때만** 돈다 — 켜지 않은 사람의 비용이
159
+ 업데이트만으로 늘어나면 안 되기 때문이다.
160
+
161
+ ```bash
162
+ # 발굴 → 스펙 → 설계리뷰 → 구현 → 코드리뷰 → 배포 → 감시
163
+ hi-loop run --goal "JWT 인증 미들웨어" --full \
164
+ --ship "npm publish --access public" \
165
+ --watch "curl -f https://api.example.com/health" --watch-for 5m
166
+ ```
167
+
168
+ | 플래그 | 기본값 | 설명 |
169
+ |---|---|---|
170
+ | `--full` | (꺼짐) | 발굴·설계리뷰·코드리뷰를 한 번에 켠다 |
171
+ | `--discover` / `--no-discover` | `--full` 이면 켜짐 | goal 의 모호함을 가정으로 확정하고 문서화 |
172
+ | `--design-review` / `--no-design-review` | `--full`·`--ship` 이면 켜짐 | 구현 착수 **전** 스펙 심판 |
173
+ | `--review` / `--no-review` | `--full`·`--ship` 이면 켜짐 | diff 를 정확성·보안·YAGNI 축으로 심판 |
174
+ | `--max-design-rounds` / `--max-review-rounds` | `2` | 각 리뷰의 재시도 상한 |
175
+ | `--ship "<cmd>"` | (없음) | 배포 명령. 판정은 exit code 뿐 |
176
+ | `--on-ship-fail` | `stop` | `heal` 이면 배포 로그를 연료로 루프가 고친다 |
177
+ | `--watch "<cmd>"` | (없음) | 배포 후 헬스체크 명령 |
178
+ | `--watch-for` / `--watch-every` | `5m` / `30s` | 감시 총 시간 / 검사 간격 |
179
+ | `--watch-tolerate` | `1` | 이 횟수까지의 실패는 봐준다(워밍업 구간) |
180
+ | `--on-watch-fail` | `stop` | `rollback` 은 체크포인트로 복원, `heal` 은 루프로 되돌림 |
181
+ | `--ask` | `auto` | `never` = 절대 안 물음(CI·봇) / `always` = 모든 경계에서 확인 |
182
+ | `--yes` | (꺼짐) | 배포 확인을 생략 |
183
+
184
+ > ⚠️ `--ship` 을 쓰면 **리뷰가 자동으로 켜진다.** 리뷰 없는 자동 배포가 위험하기 때문이다.
185
+ > 끄려면 `--no-review --no-design-review` 를 명시하라.
186
+
187
+ **배포는 항상 확인을 거친다** — 되돌릴 수 없는 외부 행위이므로 기본 무중단의 예외다.
188
+
189
+ ```
190
+ $ hi-loop run --goal "..." --ship "npm publish"
191
+ ✅ 3회차에 통과했습니다.
192
+ 🔎 CODE_REVIEW 1/2 … ✅ 코드 리뷰 통과.
193
+ ⏸ 선택이 필요합니다 [SHIP]
194
+ 배포 명령을 실행할까요? npm publish
195
+ a) 진행 — 외부에 나가면 되돌릴 수 없습니다
196
+ b) 중단 — 배포 없이 여기서 종료
197
+ → hi-loop answer <a|b> [--note "..."]
198
+ (exit 3)
199
+
200
+ $ hi-loop answer a
201
+ $ hi-loop run --goal "..." --ship "npm publish" # 같은 goal 로 재개
202
+ ```
203
+
204
+ 재개해도 **빌드를 다시 돌리지 않는다** — 통과한 빌드를 또 돌리면 비용을 두 번 낸다.
205
+
206
+ `--ask never` 를 쓰면 질문 없이 끝까지 간다. 대신 정하지 못한 분기는 **미해결로 기록되어
207
+ 완료 보고의 Gaps 에 드러난다.** 정하지 않은 것을 정했다고 말하지 않는다.
208
+
209
+ ### 4-1c. 단계별로 따로 부르기
210
+
211
+ ```bash
212
+ hi-loop discover --goal "..." # 발굴만
213
+ hi-loop plan --goal "..." # 스펙·테스트까지 (구현 전)
214
+ hi-loop review # 현재 변경분만 코드 리뷰
215
+ hi-loop ship --ship "npm publish" # 배포 단계만
216
+ hi-loop watch --watch "curl -f ..." # 헬스체크만
217
+
218
+ hi-loop run --goal "..." --stop-after CODE_REVIEW # 구간 지정도 가능
219
+ ```
220
+
221
+ 후반 단계는 `--goal` 을 다시 안 받는다(저장된 상태에서 읽는다). 한 글자만 달라도
222
+ **새 루프로 인식되어 진행 상황이 버려지기** 때문이다.
223
+
153
224
  ### 4-1a. 롤백 — 에이전트가 망가뜨린 것을 되돌린다 (L3)
154
225
 
155
226
  각 에이전트 호출 **전에** `git stash create`로 워킹트리를 비파괴 스냅샷한다. 에이전트가
package/docs/spec.md CHANGED
@@ -247,6 +247,144 @@ runLoop({
247
247
 
248
248
  ---
249
249
 
250
+ ## 2-B. 라이프사이클 확장 (v0.2)
251
+
252
+ hi-loop 은 독립 패키지다. 받는 사람에게 다른 방법론 도구가 있다고 가정할 수 없으므로,
253
+ 개발 프로세스의 앞단(발굴·설계 리뷰)과 뒷단(코드 리뷰·배포·모니터링)을 이 패키지 안에서 완결시킨다.
254
+
255
+ **확장의 유일한 규칙 (G-NEW)**: 새 단계는 **기계가 판정할 종료 조건**을 가져야 한다.
256
+ 없으면 그건 엔진의 단계가 아니라 문서 생성기다. G1(판정 주체는 엔진)의 연장이다.
257
+
258
+ ```
259
+ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW → SHIP → WATCH → DONE
260
+ ▲ │ │ │
261
+ └── reject ┘ reject ──┘ fail ─────┘
262
+ ```
263
+
264
+ 안쪽 루프(FR-2)는 불변이다. 새 단계는 전부 그 앞뒤에 붙는 껍질이며, 되돌림은 모두
265
+ **기존 HEAL/PLAN 재사용**으로 처리한다 — 새 치유 경로를 만들지 않는다.
266
+
267
+ 상태에는 `stage`(라이프사이클)와 `phase`(안쪽 루프 회차)가 공존한다. `phase` 의 의미는 종전과 같다.
268
+
269
+ ### FR-10. DISCOVER — 요구사항 발굴/기획 (`src/discover.js`)
270
+
271
+ - FR-10.1 goal 의 모호성을 분석해 `docs/discovery-<hash8>.md` 와 `state.assumptions[]` 를 만든다.
272
+ 문서는 **엔진이 쓴다** — 에이전트에게 맡기면 "썼다고 말했지만 안 썼다"를 검증할 수 없다.
273
+ 발굴 에이전트는 쓰기 권한 없이(plan) 돌고 판정 가능한 JSON 만 반환한다.
274
+ - FR-10.2 **기본은 중단 없음.** 모호한 지점은 에이전트가 가정을 세우고 **기록한 뒤 진행**한다.
275
+ 기록되지 않은 가정이 없다는 것이 이 단계의 산출물이다.
276
+ - FR-10.3 예외: **상호 배타적이고 스펙을 실질적으로 가르는 분기**면 ask 모드(FR-16).
277
+ 분기가 여럿이어도 **하나만** 묻는다 — 질문을 쌓으면 무중단이라는 기본이 무너진다.
278
+ - FR-10.4 `--discover` / `--no-discover` 로 켜고 끈다. 기본값은 FR-17.
279
+ - FR-10.5 확정된 가정은 `contextBlock` 을 통해 이후 모든 단계의 프롬프트에 실린다.
280
+ - FR-10.6 발굴 실패(파싱 불가·프로세스 오류)는 루프를 막지 않는다. 발굴은 정확도를
281
+ 높이는 단계지 통과 조건이 아니다.
282
+ - FR-10.7 `state.discovery` 가 있으면 재개 시 다시 돌지 않는다(비용 이중 지불 방지).
283
+
284
+ ### FR-11. DESIGN_REVIEW — 설계 리뷰 (`src/review.js`)
285
+
286
+ - FR-11.1 PLAN 직후, 구현 착수 **전**에 독립 검증자가 스펙을 goal·assumptions 대비 심판한다.
287
+ 여기가 가장 값싼 교정 지점이다 — 스펙이 틀리면 그 뒤 전부가 틀린 것을 향해 가는데,
288
+ CHECK 는 이걸 못 잡는다(테스트도 같은 스펙에서 나왔으므로 사이좋게 통과한다).
289
+ - FR-11.2 격리는 FR-9(스펙 오라클)와 동일: `--permission-mode plan` + 새 세션.
290
+ - FR-11.3 `reject` 면 사유를 연료로 **PLAN 을 다시 돈다**(회차를 되돌린다). 사람에게 묻지 않는다.
291
+ 구현이 없는 상태이므로 이때 CHECK 는 건너뛴다 — 확정적으로 실패하고 아무것도 알려주지 않는다.
292
+ - FR-11.4 `maxDesignRounds`(기본 2) 소진 시 그대로 진행한다.
293
+ - FR-11.5 심사 축은 둘뿐이다: **해석 정합**(goal 대비 누락·왜곡·범위 초과)과
294
+ **검증 가능성**(각 수용 기준이 테스트로 판정되는가). 후자가 핵심이다 —
295
+ 기계가 판정할 수 없는 수용 기준은 나중에 반드시 false green 을 만든다.
296
+
297
+ ### FR-12. CODE_REVIEW — 코드 리뷰 (`src/review.js`)
298
+
299
+ - FR-12.1 CHECK 통과 직후 `git diff HEAD` 를 심판한다. 격리는 FR-11.2 와 동일.
300
+ - FR-12.2 심사 축: 정확성 / 보안 / 과잉구현(YAGNI) / 명백한 성능 함정.
301
+ **스타일은 심사하지 않는다** — 린터의 일이고, 모델이 하면 진짜 결함이 소음에 묻힌다.
302
+ - FR-12.3 `reject` 면 findings 를 `lastError` 에 넣고 **HEAL 로 되돌린다**(안쪽 루프 재사용).
303
+ - FR-12.4 `maxReviewRounds`(기본 2) 소진 시 **막지 않고 Gaps 에 공개**한다.
304
+ 여기서 실패로 처리하면 통과한 테스트를 모델 심판이 뒤집는 셈이라 하향 전용 원칙을 넘어선다.
305
+ - FR-12.5 하향 전용(L9 유지). Tier1(exit code + 무결성) 실패를 통과로 올릴 수 없다.
306
+ - FR-12.6 지적이 하나도 없는 `reject` 는 통과로 친다 — 고칠 대상 없는 기각은 무한 루프만 만든다.
307
+
308
+ ### FR-13. SHIP — 배포 (`src/ship.js`)
309
+
310
+ - FR-13.1 `--ship "<cmd>"` 지정 시 전 단계 통과 후 엔진이 그 명령을 실행한다.
311
+ - FR-13.2 판정은 **exit code 뿐**. 에이전트를 부르지 않으므로 **비용은 0**이다.
312
+ - FR-13.3 실패 시 `--on-ship-fail stop|heal` (기본 `stop`).
313
+ **`rollback` 을 기본으로 두지 않는다** — 이미 외부에 나간 것을 로컬에서 되돌려도
314
+ 돌아오지 않고, 코드와 배포본이 어긋난 상태만 남는다.
315
+ - FR-13.4 SHIP 은 **항상 확인을 거친다**(비가역 외부 행위). `--yes` 또는 `--ask never` 로만 생략된다.
316
+ 기본 무중단의 예외 둘 중 하나다.
317
+
318
+ ### FR-14. WATCH — 모니터링 (`src/ship.js`)
319
+
320
+ - FR-14.1 `--watch "<cmd>" [--watch-for 5m] [--watch-every 30s] [--watch-tolerate 1]`.
321
+ - FR-14.2 `watchFor` 동안 **버텼는가**가 판정이다. 1회 실패로 죽지 않는다 —
322
+ 배포 직후 워밍업·롤링 교체 구간에서는 정상 배포도 한두 번 깨진다.
323
+ 누적 실패가 `tolerate` 를 넘을 때만 실패다. 최소 1회는 반드시 검사한다.
324
+ - FR-14.3 실패 시 `--on-watch-fail stop|rollback|heal` (기본 `stop`).
325
+ `rollback` 은 FR-8 의 기존 체크포인트를 재사용한다.
326
+ - FR-14.4 벽시계 시간만 쓰고 에이전트를 부르지 않는다. `maxLoops`·`budgetUsd` 와 무관하다.
327
+ - FR-14.5 기간 문자열(`5m`, `30s`, `1500`)이 잘못되면 **exit 2**. 조용히 기본값으로 흘리면
328
+ "5분"이라 쓴 것이 5ms 로 해석돼 감시했다는 착각만 남는다.
329
+
330
+ ### FR-15. 단계별 실행
331
+
332
+ - FR-15.1 `--start-from <stage>` / `--stop-after <stage>`
333
+ (`DISCOVER|PLAN|BUILD|CODE_REVIEW|SHIP|WATCH`).
334
+ - FR-15.2 축약 서브커맨드: `hi-loop discover|plan|review|ship|watch`.
335
+ - FR-15.3 어느 경로로 들어와도 **같은 상태 머신**을 쓴다. 별도 실행 경로를 만들지 않는다 —
336
+ 경로가 둘이면 반드시 한쪽이 뒤처져 썩는다.
337
+ - FR-15.4 후반 단계는 `--goal` 없이 저장된 상태의 goal 을 쓴다. 매번 다시 치게 하면
338
+ 한 글자만 달라도 `resumeOrCreate` 가 새 루프로 인식해 진행 상황을 버린다.
339
+ - FR-15.5 `--start-from PLAN` 은 거부한다(exit 2). PLAN 은 안쪽 루프의 회차이며,
340
+ BUILD 로 시작하면 자연히 PLAN 부터 돈다.
341
+ - FR-15.6 정지 결과의 상태는 `stopped` 다. `passed` 로 하면 "테스트가 통과했다"는 거짓말이 되고,
342
+ `failed` 로 하면 시킨 것을 다 한 실행을 실패로 만든다. CLI exit code 는 0.
343
+
344
+ ### FR-16. ask 모드 (`src/ask.js`)
345
+
346
+ - FR-16.1 `--ask auto|never|always` (기본 `auto`).
347
+ `auto` 는 상호배타 분기(FR-10.3)와 배포 직전(FR-13.4)에만 묻는다.
348
+ `never` 는 절대 묻지 않는다(CI·백그라운드 데몬·봇용). `always` 는 모든 경계에서 묻는다.
349
+ - FR-16.2 **차단형 stdin 을 쓰지 않는다.** MCP 는 단일 요청/응답이라 서버가 사람을 기다릴
350
+ 자리가 없고, 백그라운드 구동에는 답할 터미널이 없다. 대신 상태를 저장하고 종료한다.
351
+ - FR-16.3 대기 중이면 `status='awaiting'`, `state.ask={id,stage,question,options[]}`.
352
+ CLI exit code 는 **3** — 0 이면 `run && 배포` 류 스크립트가 답도 안 한 채 진행하고,
353
+ 1 이면 진짜 실패와 구분되지 않는다.
354
+ - FR-16.4 `hi-loop answer <키> [--note "..."]` / MCP `hiloop_answer` 로 재개한다.
355
+ MCP 에서는 호스트 LLM 이 질문을 사용자에게 제시하고 답을 받아 도구를 호출한다.
356
+ - FR-16.5 답이 남아 있는 상태로는 `run` 이 재개되지 않는다. 이 관문이 없으면 run 재호출만으로
357
+ 사람이 고르라던 분기를 건너뛴다.
358
+ - FR-16.6 선택지가 있는 질문에서 목록에 없는 답은 거부한다(`--note` 자유 서술은 예외).
359
+ 오타를 임의 해석하면 ask 의 존재 이유가 사라진다.
360
+ - FR-16.7 **사람이 고른 것도 가정으로 기록된다**(`decidedBy: 'human'`).
361
+
362
+ ### FR-17. 단계 기본값 — 조용히 비용을 얹지 않는다
363
+
364
+ 새 단계는 각각 에이전트 호출을 1회씩 더한다(SHIP/WATCH 는 0). 전역 기본 ON 으로 하면
365
+ 업데이트만 한 기존 사용자의 실행 비용이 조용히 는다. 그래서:
366
+
367
+ - FR-17.1 아무 플래그도 없으면 **종전 그대로** BUILD 하나만 돈다.
368
+ - FR-17.2 `--full` 이면 발굴·설계리뷰·코드리뷰가 켜진다("전과정" 스위치 하나).
369
+ - FR-17.3 `--ship` 은 **리뷰들을 자동으로 켠다** — "리뷰 없는 자동 배포가 위험하다"는
370
+ 근거가 정확히 여기서만 성립하기 때문이다. 발굴은 켜지 않는다(배포 안전과 무관).
371
+ - FR-17.4 개별 플래그가 위 자동을 양방향으로 덮는다.
372
+ - FR-17.5 서브커맨드(FR-15.2)는 해당 단계를 **하라는 명령**이므로 자동 기본값을 무시하고 켠다.
373
+
374
+ ### FR-18. 상태 스키마 v2
375
+
376
+ - FR-18.1 `STATE_VERSION` 2. 신규 필드: `stage`, `assumptions[]`, `ask`, `answers[]`,
377
+ `designRounds`, `reviewRounds`, `reviewFindings[]`, `discovery`, `ship`, `watch`.
378
+ - FR-18.2 **v1 → v2 마이그레이션 필수.** `loadState` 는 버전 불일치 시 null 을 반환하므로,
379
+ 마이그레이션 없이 버전만 올리면 진행 중이던 루프가 조용히 버려진다
380
+ (사용자에겐 "업데이트했더니 작업이 사라졌다"). 신규 필드를 기본값으로 채워 승격한다.
381
+ - FR-18.3 v1 의 `stage` 는 `BUILD`(이미 PLAN 을 지났다), 통과한 상태는 `DONE`.
382
+ - FR-18.4 미래 버전(>2)은 읽지 않는다 — 구버전이 신버전 상태를 해석하면 조용히 망가뜨린다.
383
+ - FR-18.5 `assumptions`·`reviewFindings`·`answers` 도 컨텍스트 다이어트 대상이다(FR-3.1).
384
+ 프롬프트에 실려 나가므로 자르지 않으면 핸드오프가 압축한 만큼을 되돌려놓는다.
385
+
386
+ ---
387
+
250
388
  ## 3. 비기능 요구사항
251
389
 
252
390
  - NFR-1 런타임 의존성은 `@modelcontextprotocol/sdk`(+ 그 스키마 짝인 `zod`) 뿐.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuzi-ince/hi-loop",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "CLI와 MCP 모드를 지원하는 초경량 자율형 자가 치유 엔진",
5
5
  "type": "module",
6
6
  "license": "MIT",