@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/README.md
CHANGED
|
@@ -34,8 +34,14 @@ npm install -g @tuzi-ince/hi-loop
|
|
|
34
34
|
# 2) 프로젝트에 주입 (멱등 — 여러 번 돌려도 안전)
|
|
35
35
|
cd /path/to/my-project
|
|
36
36
|
hi-loop-setup # .mcp.json / .gitignore / docs·tests / CLAUDE.md / docs/DESIGN.md 자동 주입
|
|
37
|
+
hi-loop-setup --host cursor # Cursor 용 .cursor/mcp.json 에 등록
|
|
38
|
+
hi-loop-setup --host opencode # opencode 용 opencode.json 에 등록
|
|
37
39
|
```
|
|
38
40
|
|
|
41
|
+
> MCP 서버는 표준 stdio 라 Cursor·opencode 등 다른 호스트에도 `--host` 로 등록된다. 단 스킬·CLAUDE.md
|
|
42
|
+
> 자동 라우팅은 **Claude Code 전용**이고, 다른 호스트에선 루프의 일꾼도 claude 가 아니라면 `HILOOP_AGENT_PROVIDER=generic`
|
|
43
|
+
> 이 필요하다(claude 외 CLI 이식 — 세션·비용 미지원). 자세히는 [가이드 §4-2/§4-3](docs/guide.md).
|
|
44
|
+
|
|
39
45
|
`hi-loop-setup` 은 **표준 설계 문서 `docs/DESIGN.md`** 를 함께 만든다(있으면 보존). 이 문서를 채우면
|
|
40
46
|
문서↔소스 정합 장치(`--reconcile-spec` / `--spec`)의 오라클이 된다 — 아래 [문서↔소스 정합](#문서를-상시-오라클로--표준-문서-고정---spec) 참조.
|
|
41
47
|
|
|
@@ -61,7 +67,7 @@ hi-loop mcp
|
|
|
61
67
|
hi-loop status # 현재 루프 상태 요약
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
MCP 도구: `hiloop_run`, `hiloop_answer`, `hiloop_status`, `hiloop_reset`, `hiloop_rollback`, `hiloop_setup`.
|
|
70
|
+
MCP 도구: `hiloop_run`, `hiloop_answer`, `hiloop_status`, `hiloop_reset`, `hiloop_rollback`, `hiloop_setup`, `hiloop_docsync`.
|
|
65
71
|
|
|
66
72
|
## 동작 원리
|
|
67
73
|
|
|
@@ -293,7 +299,8 @@ hi-loop run --goal "..." --start-from BUILD --stop-after CODE_REVIEW
|
|
|
293
299
|
|
|
294
300
|
| 사용자가 이렇게 말하면 | LLM이 하는 일 |
|
|
295
301
|
|---|---|
|
|
296
|
-
| `/
|
|
302
|
+
| `/flow 로그인 폼 만들어줘` (슬래시 스킬, `/hi-loop:flow` 도 동일) | `flow` 스킬 기동 → `hiloop_run(full=true, step=true)` 실행 — 대화형이라 스텝 모드로 매 스텝을 보이며 진행 |
|
|
303
|
+
| `/flow plan 로그인 폼` (단계어) | 첫 단어가 단계면 그 단계까지만: `stopAfter:"PLAN"` → 스펙·테스트만 만들고 멈춰 **다음(구현) 제안**. `do`/`review`/`ship` 등도 동일 |
|
|
297
304
|
| "hiloop_run 도구로 결제 버그 고쳐줘" | 지목된 MCP 도구를 로드해 바로 호출 |
|
|
298
305
|
| 터미널에서 `hi-loop run --goal "..." --full` | 엔진을 CLI로 직접 구동(LLM 개입 없음) |
|
|
299
306
|
|
|
@@ -304,7 +311,7 @@ hi-loop run --goal "..." --start-from BUILD --stop-after CODE_REVIEW
|
|
|
304
311
|
|
|
305
312
|
| 사용자가 이렇게 말하면 | 어디에 걸리나 | LLM이 하는 일 |
|
|
306
313
|
|---|---|---|
|
|
307
|
-
| "워크스페이스 폴더 드래그앤드롭 **개선**해줘" | 트리거 `개선` |
|
|
314
|
+
| "워크스페이스 폴더 드래그앤드롭 **개선**해줘" | 트리거 `개선` | `flow` 스킬 후보로 뜸 → 기획·설계 필요 판단 → `flow` 실행(대화형이라 `step` 으로 매 스텝 보이며 진행) |
|
|
308
315
|
| "장바구니 **기능** 하나 **만들어**줘" | 트리거 `기능/design` | 발굴→스펙·테스트 우선(PLAN)→구현·리뷰 루프 |
|
|
309
316
|
| "결제 모듈 **리팩터**해줘" | 트리거 `리팩터` | 기존 테스트를 가드레일로 `--verify-spec` 성격의 개선 루프 |
|
|
310
317
|
| "동시 요청 시 잔액 음수 **버그** 고쳐줘" | 트리거 `구현/버그성` | **재현 테스트 먼저** → HEAL 루프로 수정 |
|
|
@@ -468,6 +475,20 @@ hi-loop run --goal "결제에서 음수 잔액도 허용하라" --reconcile #
|
|
|
468
475
|
> 이것이 "단말적 요청이 문서와 상이하게 들어와 갭이 벌어지는" 상황을 **엔진 차원에서** 막는
|
|
469
476
|
> 장치다. `--spec`(고정)과 `--reconcile`(감지)은 짝으로 쓰면 문서↔소스 정합이 가장 단단하다.
|
|
470
477
|
|
|
478
|
+
### 소스를 바꿨으면 문서도 맞춘다 — `hiloop_docsync` / `hi-loop docsync` / `/flow doc-sync`
|
|
479
|
+
|
|
480
|
+
`--spec`/`--reconcile`이 drift를 **예방**하는 게이트라면, doc-sync는 **이미 바뀐 소스에 맞춰 문서를
|
|
481
|
+
갱신**하는 단발 패스다. `git diff`로 소스 변경을 읽어 일꾼 에이전트가 **문서만**(README·docs/*) 고친다.
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
hi-loop docsync # 소스 변경 → 문서 반영 (한 번의 패스, 진행 표시)
|
|
485
|
+
# 또는 스킬: /flow doc-sync · MCP: hiloop_docsync
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
- **`docs/DESIGN.md`(사람 오라클)와 소스·테스트는 건드리지 않는다.** 문서 외 편집은 `stray`로 경고.
|
|
489
|
+
- diff로 확인되는 것만 반영한다(없는 내용을 지어내지 않음). 커밋은 사람이 판단한다.
|
|
490
|
+
- 자가 치유 루프가 아니라 짧은 단발이라 타임아웃/블랙박스가 없다. MCP/CLI라 소비 프로젝트에서도 동작.
|
|
491
|
+
|
|
471
492
|
### 에이전트를 못 부르면 스택이 아니라 지침을 준다 (preflight)
|
|
472
493
|
|
|
473
494
|
에이전트(claude) 미설치·인증 만료·API 키 부재는 이 엔진의 버그가 아니라 환경 문제다.
|
|
@@ -501,6 +522,24 @@ MCP 도구의 `cwd` 기본값은 `CLAUDE_PROJECT_DIR || process.cwd()` 다 —
|
|
|
501
522
|
`ask` 는 비싼 e2e 루프에 특히 유용하다 — 8.5분·수달러짜리 회차를 무인으로 반복하지 않고,
|
|
502
523
|
**판정 불능(무결성·환경)**까지 사람에게 표면화한다. 무인(`--ask never`)에선 물을 수 없으니 안전하게 종료한다.
|
|
503
524
|
|
|
525
|
+
### ⏭ 스텝 모드 — LLM 이 루프를 몰고, 매 스텝이 보인다 (`step`)
|
|
526
|
+
|
|
527
|
+
기본은 **한 콜에서 엔진이 루프 전체**를 돈다(모델 A: 무인·결정론에 최적). 대화형에선 이게
|
|
528
|
+
불투명하고(진행이 콘솔에 안 보임) 길어서 타임아웃 위험이 있으며 에이전트를 두 번(바깥 LLM +
|
|
529
|
+
서브프로세스 claude) 쓴다. `step` 을 주면 **매 단위 작업(BUILD 회차·각 stage) 후 제어를 LLM 에게
|
|
530
|
+
돌려준다**(모델 B):
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
hiloop_run({ goal, step: true })
|
|
534
|
+
└▶ ⏭ 스텝 완료 (PLAN) → LLM 이 진행을 알리고 같은 goal 로 재호출
|
|
535
|
+
└▶ ⏭ 스텝 완료 (DO) … └▶ ✅ 통과 / ❌ 실패 / ⏸ awaiting
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
매 스텝이 콘솔에 보이고, 콜당 에이전트 1콜이라 타임아웃이 없다. 상태가 STATE.json 에 저장돼
|
|
539
|
+
**재개는 자동**이며, 통과 판정은 그대로 엄격하다(`continue` 는 통과가 아니다). `flow` 스킬이
|
|
540
|
+
대화형에서 이 모드를 기본으로 쓴다. CLI 는 `--step`(스텝 후 exit 3, 재실행으로 이어감).
|
|
541
|
+
무인·완료까지 자동은 `step` 을 빼면 모델 A 그대로다.
|
|
542
|
+
|
|
504
543
|
### 💸 비용은 엔진이 센다 — 그리고 싸지 않다
|
|
505
544
|
|
|
506
545
|
**`--max-loops` 는 비용 상한이 아니다.** 실패 루프가 10회를 다 쓰면 비용이 크게 뛸 수 있다.
|
|
@@ -519,7 +558,8 @@ hi-loop run --goal "..." --max-loops 5 --budget-usd 1 --no-stagnation
|
|
|
519
558
|
| 변수 | 설명 |
|
|
520
559
|
|---|---|
|
|
521
560
|
| `HILOOP_AGENT_CMD` | 에이전트 실행 명령 (기본 `claude`) |
|
|
522
|
-
| `
|
|
561
|
+
| `HILOOP_AGENT_PROVIDER` | 에이전트 CLI 계약 (기본 `claude`). `generic` 은 프롬프트를 마지막 위치인자로, stdout 을 결과로 읽는 최소 계약 — opencode 등 claude 외 CLI 를 일꾼으로 쓸 때. 단 **세션 재개·비용 집계는 없다** |
|
|
562
|
+
| `HILOOP_AGENT_ARGS` | 에이전트에 덧붙일 인자 (공백 구분). 예: `--model sonnet` — 비용에 직결된다. `generic` 에선 프롬프트 앞 서브커맨드로 쓰임(예: `run`) |
|
|
523
563
|
| `HILOOP_PERMISSION_MODE` | 권한 모드 (기본 `acceptEdits`). 편집 허용이 이 엔진의 전제다 |
|
|
524
564
|
| `HILOOP_VERIFY_CMD` / `HILOOP_VERIFY_MODEL` | `--verify-spec` 검증자의 실행 명령·모델 (미설정 시 구현자와 동일). 검증에 더 센 모델을 쓸 수 있다 |
|
|
525
565
|
| `HILOOP_REVIEW_CMD` / `HILOOP_REVIEW_MODEL` | 설계·코드 리뷰어의 실행 명령·모델 (미설정 시 `HILOOP_VERIFY_*` → 구현자 순으로 폴백) |
|
package/bin/hi-loop.js
CHANGED
|
@@ -28,7 +28,7 @@ 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] [--on-fail heal|ask|stop] [--verify-spec]
|
|
31
|
-
[--spec docs/DESIGN.md] [--cwd .]
|
|
31
|
+
[--step] [--spec docs/DESIGN.md] [--cwd .]
|
|
32
32
|
[--ship "<배포 명령>"] [--on-ship-fail stop|heal]
|
|
33
33
|
[--review | --no-review] [--max-review-rounds 2]
|
|
34
34
|
[--design-review | --no-design-review] [--max-design-rounds 2]
|
|
@@ -53,6 +53,7 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
|
|
|
53
53
|
hi-loop answer <선택> [--note "..."] 대기 중인 질문에 답하고 루프를 재개 가능 상태로 되돌림
|
|
54
54
|
hi-loop rollback [--to N] [--cwd .] N회차 직전 체크포인트로 파일 복원 (기본: 최신)
|
|
55
55
|
hi-loop setup [--dry-run] 프로젝트/클로드코드 설정 자동 주입
|
|
56
|
+
hi-loop docsync [--cwd .] 소스 변경(git diff)을 문서에 반영하는 단발 패스(DESIGN.md 제외)
|
|
56
57
|
hi-loop config [set <k> <v> | add-branch <b> | remove-branch <b>]
|
|
57
58
|
브랜치·커밋 정책(.hi-loop.json) 조회·변경
|
|
58
59
|
hi-loop --help | --version
|
|
@@ -81,7 +82,7 @@ const USAGE = `hi-loop — 초경량 자율형 자가 치유 엔진
|
|
|
81
82
|
export async function main(argv = process.argv.slice(2)) {
|
|
82
83
|
const args = parseArgs(argv, {
|
|
83
84
|
alias: { g: 'goal', t: 'test', c: 'cwd', h: 'help', v: 'version', m: 'max-loops' },
|
|
84
|
-
boolean: ['help', 'version', 'dry-run', 'no-stagnation', 'verify-spec', 'flaky-probe', 'yes', 'review', 'no-review', 'design-review', 'no-design-review', 'full', 'discover', 'no-discover', 'reconcile', 'no-reconcile', 'commit'],
|
|
85
|
+
boolean: ['help', 'version', 'dry-run', 'no-stagnation', 'verify-spec', 'flaky-probe', 'yes', 'review', 'no-review', 'design-review', 'no-design-review', 'full', 'discover', 'no-discover', 'reconcile', 'no-reconcile', 'commit', 'step'],
|
|
85
86
|
// 개수가 정해지지 않은 입력. `--check A --when X --check B` 처럼 위치로 짝을 맺는다.
|
|
86
87
|
repeat: ['check', 'when'],
|
|
87
88
|
});
|
|
@@ -165,9 +166,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
165
166
|
...options,
|
|
166
167
|
logger: (line) => process.stdout.write(`${line}\n`),
|
|
167
168
|
});
|
|
168
|
-
// awaiting
|
|
169
|
-
//
|
|
170
|
-
if (result.status === 'awaiting') return 3;
|
|
169
|
+
// awaiting/continue 는 "종료가 아니라 재호출이 필요"한 상태다(전자는 답을, 후자는 다음 스텝을).
|
|
170
|
+
// 0 을 주면 `hi-loop run && 배포` 가 미완인데 넘어가고, 1 을 주면 진짜 실패와 구분이 안 된다.
|
|
171
|
+
if (result.status === 'awaiting' || result.status === 'continue') return 3;
|
|
171
172
|
// stopped 는 시킨 것을 다 한 것이다. 실패가 아니다.
|
|
172
173
|
if (result.status === 'stopped') return 0;
|
|
173
174
|
return result.status === 'passed' ? 0 : 1;
|
|
@@ -196,6 +197,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
196
197
|
process.stdout.write(`${report.lines.join('\n')}\n`);
|
|
197
198
|
return 0;
|
|
198
199
|
}
|
|
200
|
+
case 'docsync': {
|
|
201
|
+
// FR-25: 소스 변경을 문서에 반영하는 단발 패스(루프 아님). 문서만 고친다.
|
|
202
|
+
const { runDocSync } = await import('../src/docsync.js');
|
|
203
|
+
const res = await runDocSync({ cwd, logger: (l) => process.stdout.write(`${l}\n`) });
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
process.stderr.write(`${res.reason}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
if (res.stray?.length) process.stdout.write(`⚠️ 문서 외 파일 변경(검토 요망): ${res.stray.join(', ')}\n`);
|
|
209
|
+
if (res.note) process.stdout.write(`\n${res.note}\n`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
199
212
|
default:
|
|
200
213
|
process.stderr.write(`알 수 없는 명령: ${command}\n\n${USAGE}\n`);
|
|
201
214
|
return 2;
|
package/bin/setup.js
CHANGED
|
@@ -24,7 +24,10 @@ export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_BEGIN}
|
|
|
24
24
|
## hi-loop 워크플로
|
|
25
25
|
|
|
26
26
|
기능 추가·개선·리팩터처럼 **기획/설계가 선행되어야 하는 요청**은 소스만 보고 바로 구현하지 말고,
|
|
27
|
-
\`hi-loop
|
|
27
|
+
**\`flow\` 스킬(\`/hi-loop:flow\`)** 로 PLAN→DESIGN→DO→CHECK→HEAL 자율 루프를 돌린다 — 스킬이 브랜치
|
|
28
|
+
게이트·스텝 진행·ask 재개·e2e·커밋까지 프로토콜로 감싼다. 스킬을 거치지 않고 MCP 도구 \`hiloop_run\`
|
|
29
|
+
을 직접 부를 땐 **대화형이면 \`step: true\` 를 반드시 준다**(매 단위 후 제어를 돌려받아 진행을 사용자에게
|
|
30
|
+
보이며 이어 호출 — 빼면 루프 전체가 한 콜에서 조용히 돌아 진행이 안 보이고 타임아웃 위험). 전과정은 \`full: true\`.
|
|
28
31
|
|
|
29
32
|
**응답 언어는 사용자의 요청 언어에 맞춘다** — 한국어 요청엔 한국어로, 영어 요청엔 영어로 답한다. hi-loop 도구·서브에이전트 결과(리포트·Gaps 등)를 옮길 때도 사용자 언어로 전한다.
|
|
30
33
|
|
|
@@ -91,13 +94,36 @@ function readJson(path) {
|
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
/** `.mcp.json` 병합 — 기존 서버 항목 보존 (FR-6.1, FR-6.4) */
|
|
97
|
+
/** `.mcp.json` 병합 — 기존 서버 항목 보존 (FR-6.1, FR-6.4). Claude Code·Cursor 공용 스키마 */
|
|
95
98
|
export function mergeMcpConfig(existing) {
|
|
96
99
|
const base = existing && typeof existing === 'object' ? existing : {};
|
|
97
100
|
const servers = base.mcpServers && typeof base.mcpServers === 'object' ? base.mcpServers : {};
|
|
98
101
|
return { ...base, mcpServers: { ...servers, [MCP_SERVER_KEY]: serverEntry() } };
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
/** opencode 는 `mcp` 키에 `type:'local'` + command 배열 스키마를 쓴다 (FR-6.8) */
|
|
105
|
+
export const opencodeServerEntry = (entry = cliEntryPath()) => ({
|
|
106
|
+
type: 'local',
|
|
107
|
+
command: [process.execPath, entry, 'mcp'],
|
|
108
|
+
environment: {},
|
|
109
|
+
});
|
|
110
|
+
export function mergeOpencodeConfig(existing) {
|
|
111
|
+
const base = existing && typeof existing === 'object' ? existing : {};
|
|
112
|
+
const mcp = base.mcp && typeof base.mcp === 'object' ? base.mcp : {};
|
|
113
|
+
return { ...base, mcp: { ...mcp, [MCP_SERVER_KEY]: opencodeServerEntry() } };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* MCP 서버를 어느 호스트 설정에 쓸지 (FR-6.8, `--host`).
|
|
118
|
+
* `claudeMd`=true 인 호스트만 CLAUDE.md 자동 라우팅 지침을 심는다 — 스킬·CLAUDE.md 는
|
|
119
|
+
* Claude Code 전용이라 다른 호스트에선 도구를 직접 호출한다.
|
|
120
|
+
*/
|
|
121
|
+
export const HOSTS = {
|
|
122
|
+
claude: { label: 'Claude Code', path: '.mcp.json', merge: mergeMcpConfig, claudeMd: true },
|
|
123
|
+
cursor: { label: 'Cursor', path: '.cursor/mcp.json', merge: mergeMcpConfig, claudeMd: false },
|
|
124
|
+
opencode: { label: 'opencode', path: 'opencode.json', merge: mergeOpencodeConfig, claudeMd: false },
|
|
125
|
+
};
|
|
126
|
+
|
|
101
127
|
/** `.gitignore` 항목 추가 — 중복 없이 (FR-6.2) */
|
|
102
128
|
export function mergeGitignore(content) {
|
|
103
129
|
let text = typeof content === 'string' ? content : '';
|
|
@@ -133,25 +159,28 @@ export function mergeClaudeMd(content) {
|
|
|
133
159
|
return { changed: true, content: `${text}${prefix}${CLAUDE_MD_BLOCK}\n` };
|
|
134
160
|
}
|
|
135
161
|
|
|
136
|
-
export function runSetup({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
162
|
+
export function runSetup({ cwd = process.cwd(), dryRun = false, host = 'claude' } = {}) {
|
|
137
163
|
const root = resolve(cwd);
|
|
164
|
+
const spec = HOSTS[host] || HOSTS.claude;
|
|
138
165
|
const lines = [];
|
|
139
166
|
const changes = [];
|
|
140
167
|
const write = (path, content) => {
|
|
141
|
-
if (
|
|
168
|
+
if (dryRun) return;
|
|
169
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
170
|
+
writeFileSync(path, content, 'utf8');
|
|
142
171
|
};
|
|
143
172
|
|
|
144
|
-
// 1) .mcp.json
|
|
145
|
-
const mcpPath = join(root,
|
|
173
|
+
// 1) 호스트 MCP 설정 (--host: claude=.mcp.json / cursor=.cursor/mcp.json / opencode=opencode.json)
|
|
174
|
+
const mcpPath = join(root, spec.path);
|
|
146
175
|
const before = readJson(mcpPath);
|
|
147
|
-
const merged =
|
|
176
|
+
const merged = spec.merge(before);
|
|
148
177
|
const mcpChanged = JSON.stringify(before) !== JSON.stringify(merged);
|
|
149
178
|
if (mcpChanged) {
|
|
150
179
|
write(mcpPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
151
|
-
changes.push(
|
|
152
|
-
lines.push(`${dryRun ? '[dry-run] ' : ''}✓ .
|
|
180
|
+
changes.push(spec.path);
|
|
181
|
+
lines.push(`${dryRun ? '[dry-run] ' : ''}✓ ${spec.path} 에 hi-loop MCP 서버를 등록했습니다 (${spec.label}).`);
|
|
153
182
|
} else {
|
|
154
|
-
lines.push(
|
|
183
|
+
lines.push(`· ${spec.path} 은 이미 최신입니다 (${spec.label}).`);
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
// 2) .gitignore
|
|
@@ -188,22 +217,29 @@ export function runSetup({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
|
188
217
|
lines.push(`· ${DESIGN_DOC_PATH} 이미 존재합니다 (내용 보존).`);
|
|
189
218
|
}
|
|
190
219
|
|
|
191
|
-
// 5) CLAUDE.md — 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침 (FR-6.5)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
220
|
+
// 5) CLAUDE.md — 기획/설계 요청을 hi-loop 으로 라우팅하는 상시 지침 (FR-6.5).
|
|
221
|
+
// 스킬·CLAUDE.md 자동 라우팅은 Claude Code 전용이라 다른 호스트에선 건너뛴다.
|
|
222
|
+
if (spec.claudeMd) {
|
|
223
|
+
const cmdPath = join(root, 'CLAUDE.md');
|
|
224
|
+
const cmd = mergeClaudeMd(existsSync(cmdPath) ? readFileSync(cmdPath, 'utf8') : '');
|
|
225
|
+
if (cmd.changed) {
|
|
226
|
+
write(cmdPath, cmd.content);
|
|
227
|
+
changes.push('CLAUDE.md');
|
|
228
|
+
lines.push(`${dryRun ? '[dry-run] ' : ''}✓ CLAUDE.md 에 hi-loop 워크플로 지침을 주입했습니다.`);
|
|
229
|
+
} else {
|
|
230
|
+
lines.push('· CLAUDE.md 지침은 이미 최신입니다.');
|
|
231
|
+
}
|
|
198
232
|
} else {
|
|
199
|
-
lines.push(
|
|
233
|
+
lines.push(`· ${spec.label} 은 CLAUDE.md 자동 라우팅 대상이 아닙니다 — 에이전트에서 hiloop_run 을 직접 호출하세요.`);
|
|
200
234
|
}
|
|
201
235
|
|
|
202
236
|
lines.push(
|
|
203
237
|
'',
|
|
204
238
|
'이제 사용하세요:',
|
|
205
239
|
' hi-loop run --goal "요구사항" --test "npm test"',
|
|
206
|
-
|
|
240
|
+
spec.claudeMd
|
|
241
|
+
? ' 또는 에이전트에서 MCP 도구 hiloop_run 호출 (스킬: /hi-loop:flow)'
|
|
242
|
+
: ` 또는 ${spec.label} 에서 MCP 도구 hiloop_run 을 직접 호출`,
|
|
207
243
|
` · ${DESIGN_DOC_PATH} 를 채우면 --reconcile-spec/--spec 오라클로 문서↔소스 정합을 지킵니다.`,
|
|
208
244
|
);
|
|
209
245
|
return { changes, lines, dryRun };
|
|
@@ -211,6 +247,10 @@ export function runSetup({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
|
211
247
|
|
|
212
248
|
if (isMain(import.meta.url)) {
|
|
213
249
|
const args = parseArgs(process.argv.slice(2), { alias: { c: 'cwd' }, boolean: ['dry-run'] });
|
|
214
|
-
const
|
|
250
|
+
const host = args.host && HOSTS[args.host] ? args.host : 'claude';
|
|
251
|
+
if (args.host && !HOSTS[args.host]) {
|
|
252
|
+
process.stderr.write(`알 수 없는 --host '${args.host}'. 지원: ${Object.keys(HOSTS).join(', ')}. claude 로 진행합니다.\n`);
|
|
253
|
+
}
|
|
254
|
+
const report = runSetup({ cwd: args.cwd ?? process.cwd(), dryRun: Boolean(args['dry-run']), host });
|
|
215
255
|
process.stdout.write(`${report.lines.join('\n')}\n`);
|
|
216
256
|
}
|
package/docs/DESIGN.md
CHANGED
|
@@ -56,9 +56,10 @@
|
|
|
56
56
|
| `src/state.js` | 상태 스키마·저장·절단·산출물 경로·resume·에러 지문·Gaps 보고 | 230 |
|
|
57
57
|
| `bin/hi-loop.js` | CLI 진입점 (+rollback) | 161 |
|
|
58
58
|
| `src/integrity.js` | 테스트 무결성 지문·비교 (L1) | 131 |
|
|
59
|
-
| `src/runners.js` | 프로세스 spawn (+타임아웃 env), 응답·비용 파싱 (L6) |
|
|
60
|
-
| `src/prompts.js` | 단계별 프롬프트 + 검증자 프롬프트 조립 |
|
|
59
|
+
| `src/runners.js` | 프로세스 spawn (+타임아웃 env), 응답·비용 파싱 (L6), 프로바이더 어댑터 (FR-23) | 163 |
|
|
60
|
+
| `src/prompts.js` | 단계별 프롬프트 + 검증자 + doc-sync 프롬프트 조립 | 296 |
|
|
61
61
|
| `bin/setup.js` | 설정 주입 진입점 | 115 |
|
|
62
|
+
| `src/docsync.js` | DOC-SYNC 단발 패스 — diff 수집·문서 갱신·stray 검증 (FR-25) | 100 |
|
|
62
63
|
| `src/mcp-server.js` | MCP 도구 등록/응답 포맷 | 102 |
|
|
63
64
|
| `src/verify.js` | 스펙 오라클 — Tier 2 검증자 (L9) | 96 |
|
|
64
65
|
| `src/checkpoint.js` | git stash 체크포인트·롤백 (L3) + 삭제 관측 (L8) | 94 |
|
|
@@ -495,13 +496,17 @@ runLoop({ agentRunner, testRunner, notifier, logger, statePath })
|
|
|
495
496
|
|
|
496
497
|
| 포트 | 계약 | 프로덕션 어댑터 | 테스트 스텁 |
|
|
497
498
|
|---|---|---|---|
|
|
498
|
-
| `agentRunner` | `({prompt, sessionId, cwd}) => {text, sessionId}` | `claude
|
|
499
|
+
| `agentRunner` | `({prompt, sessionId, cwd}) => {text, sessionId}` | 프로바이더 어댑터(`claude` 기본 / `generic`), spawn·타임아웃은 공유 | 호출 기록 배열 |
|
|
499
500
|
| `testRunner` | `({command, cwd}) => {ok, code, stdout, stderr}` | `spawn(cmd, {shell:true})` | 정해둔 성패 시퀀스 |
|
|
500
501
|
| `notifier` | `(event) => void` | 텔레그램 | 이벤트 타입 수집 |
|
|
501
502
|
|
|
502
503
|
- **세션 왕복**: 러너가 `sessionId` 를 돌려주고 엔진이 그것을 다음 호출에 넣는다.
|
|
503
504
|
엔진은 세션이 CLI 플래그인지 HTTP 헤더인지 모른다 → `claude` 외 다른 에이전트로 교체 가능
|
|
504
505
|
(`HILOOP_AGENT_CMD`).
|
|
506
|
+
- **프로바이더 어댑터**(FR-23): 인자 구성·출력 파싱을 `HILOOP_AGENT_PROVIDER` 로 고르는
|
|
507
|
+
`{buildArgs, parseOutput}` 쌍으로 분리. `claude`(세션·비용 지원, 기본)와 `generic`(프롬프트를
|
|
508
|
+
마지막 위치인자로, stdout=텍스트, 세션·비용 없음 — opencode 등). `makeAgentRunner` 는 어댑터
|
|
509
|
+
두 함수만 호출하고 spawn·타임아웃·에러 분기는 공유한다.
|
|
505
510
|
- **에이전트 응답 파싱**: `--output-format json` 의 `{result, session_id}` 를 읽되,
|
|
506
511
|
JSON 이 아니면 원문을 텍스트로 쓰고 기존 세션을 유지한다(관대한 파싱).
|
|
507
512
|
- **실패 처리 비대칭**: `testRunner` 의 실패는 **정상 입력**(치유할 재료)이라 resolve 로 흘리고,
|
|
@@ -638,6 +643,9 @@ hi-loop run --goal "두 수를 더하는 add 함수를 만들어라" --test "npm
|
|
|
638
643
|
| FR-4 MCP 서버 | §7 | `src/mcp-server.js` | AC-20 |
|
|
639
644
|
| FR-5 텔레그램 | §8 | `src/telegram.js` | AC-12~14 |
|
|
640
645
|
| FR-6 setup | — | `bin/setup.js` | AC-17, 18 |
|
|
646
|
+
| FR-23 에이전트 프로바이더 어댑터 | §9 | `src/runners.js` (`AGENT_ADAPTERS`, `getAgentAdapter`) | app.test.js 어댑터 4종 |
|
|
647
|
+
| FR-24 스텝 모드(LLM 드리븐) | §4 | `src/loop.js`·`src/build.js`·`src/outcome.js` (`stepResult`) | tests/step.test.js |
|
|
648
|
+
| FR-25 DOC-SYNC(소스→문서 단발 패스) | §9 | `src/docsync.js`·`src/prompts.js`(`docSyncPrompt`)·`src/mcp-server.js`·`bin/hi-loop.js` | tests/docsync.test.js |
|
|
641
649
|
| NFR-3 주입 가능 | §6 | `src/runners.js` | 전 테스트가 네트워크 없이 동작 |
|
|
642
650
|
| NFR-4 원자적 저장 | §4.4 | `src/state.js` | AC-19 |
|
|
643
651
|
|
package/docs/SPEC.md
CHANGED
|
@@ -258,9 +258,18 @@ runLoop({
|
|
|
258
258
|
않는다**(사람이 관리하는 진실). FR-21 정합 게이트·FR-19 스펙 고정의 오라클 대상이다. PLAN 이
|
|
259
259
|
쓰는 `SPEC.md` 와 달리 엔진은 이 파일을 손대지 않는다.
|
|
260
260
|
- FR-6.7 `CLAUDE.md` 에 hi-loop 워크플로 라우팅 지침을 마커(`<!-- hi-loop:begin/end -->`)로 감싸
|
|
261
|
-
멱등 주입한다.
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
멱등 주입한다. **`flow` 스킬(`/hi-loop:flow`)을 1순위 경로로 지목**한다 — 스킬명을 실제와 일치시켜야
|
|
262
|
+
LLM 이 "스킬을 못 찾아 도구로 새는" 오라우팅을 막는다(실측 근본원인: 존재하지 않는 "hi-loop 스킬"을
|
|
263
|
+
가리켜 `hiloop_run` 직접 호출로 샜다). 스킬을 거치지 않고 도구를 직접 부를 땐 **대화형이면 `step:true`
|
|
264
|
+
를 강제**한다(모델 B — 진행 가시성·타임아웃 제거, FR-24). 코드 변경 루프가 `docs/DESIGN.md` 와
|
|
265
|
+
정합하도록(`--reconcile-spec`/`--spec`) 안내하고, **응답 언어를 사용자의 요청 언어에 맞추라는 지시**를
|
|
266
|
+
포함한다 — 영어 스킬/도구/프레임워크 표면이 많아도 한국어 요청엔 한국어로 답하도록(언어 드리프트 방지).
|
|
267
|
+
- FR-6.8 `--host claude|cursor|opencode` — MCP 서버를 어느 호스트 설정에 등록할지 고른다(기본 `claude`).
|
|
268
|
+
- `claude`: `.mcp.json`(`mcpServers` 스키마) + CLAUDE.md 라우팅 지침(FR-6.7).
|
|
269
|
+
- `cursor`: `.cursor/mcp.json`(claude 와 **같은** `mcpServers` 스키마). CLAUDE.md 는 건너뛴다.
|
|
270
|
+
- `opencode`: `opencode.json` 의 `mcp` 키에 `{type:'local', command:[node, entry, 'mcp'], environment:{}}`.
|
|
271
|
+
- CLAUDE.md·스킬 자동 라우팅은 **Claude Code 전용**이라 claude 외 호스트에선 심지 않는다(도구 직접 호출).
|
|
272
|
+
기존 `mcp`/`mcpServers` 항목은 보존(멱등). 미상값은 `claude` 로 폴백. gitignore·docs·DESIGN.md 는 공통.
|
|
264
273
|
|
|
265
274
|
---
|
|
266
275
|
|
|
@@ -466,6 +475,59 @@ DISCOVER → PLAN → DESIGN_REVIEW → DO ⇄ CHECK ⇄ HEAL → CODE_REVIEW
|
|
|
466
475
|
- FR-22.4 **무인 안전**: `--ask never`(물을 사람 없음)에서 `on-fail ask` 는 pause 하지 않고
|
|
467
476
|
안전하게 종료한다 — 답을 못 받는데 멈춰 있으면 무한/폭주가 된다.
|
|
468
477
|
|
|
478
|
+
### FR-23. 에이전트 프로바이더 어댑터 — claude 외 CLI 이식 (`HILOOP_AGENT_PROVIDER`, `src/runners.js`)
|
|
479
|
+
|
|
480
|
+
MCP 서버는 표준 stdio 라 어느 호스트에든 뜨지만, 루프의 **실제 일꾼**은 `claude -p …` 서브프로세스에
|
|
481
|
+
고정돼 있었다(인자 구성·출력 파싱이 claude 계약 전용). 그래서 opencode 같은 다른 에이전트 CLI 를
|
|
482
|
+
일꾼으로 쓸 수 없었다. 이 결합을 프로바이더 어댑터로 끊는다.
|
|
483
|
+
|
|
484
|
+
- FR-23.1 `HILOOP_AGENT_PROVIDER` 로 어댑터 선택(기본 `claude`). 어댑터는 `{buildArgs, parseOutput}`
|
|
485
|
+
한 쌍이다 — `makeAgentRunner` 는 이 둘만 호출하고 spawn·타임아웃·에러 처리는 공유한다.
|
|
486
|
+
- FR-23.2 `claude` 어댑터 = 종전 `buildAgentArgs`/`parseAgentOutput` **그대로**(기존 동작 불변):
|
|
487
|
+
`-p <prompt> --output-format json [--resume sid] [--permission-mode]`, `result/session_id/total_cost_usd` 파싱.
|
|
488
|
+
- FR-23.3 `generic` 어댑터 = 프롬프트를 **마지막 위치인자**로(`[...extraArgs, prompt]`), stdout 을 그대로
|
|
489
|
+
텍스트로 본다. **세션 재개·비용 집계는 지원 안 함**(매 호출 새 세션, `costUsd:0`). 예: `HILOOP_AGENT_CMD=opencode`
|
|
490
|
+
`HILOOP_AGENT_ARGS=run` `HILOOP_AGENT_PROVIDER=generic` → `opencode run "<prompt>"`.
|
|
491
|
+
- FR-23.4 이름이 없거나 미상값이면 `claude` 로 조용히 되돌린다(기본 경로 보존). 기능이 온전한 경로는
|
|
492
|
+
claude 이며 generic 은 이식용 최소 계약임을 문서로 명시한다(guide §4-2/§4-3).
|
|
493
|
+
|
|
494
|
+
### FR-24. 스텝 모드 — LLM 이 루프를 모는 실행 (`step`, `src/loop.js`·`src/build.js`·`src/outcome.js`)
|
|
495
|
+
|
|
496
|
+
기본 실행(모델 A)은 `hiloop_run` **한 콜 안에서 엔진이 루프 전체**를 돈다(BUILD 는 PLAN+DO+HEAL×
|
|
497
|
+
maxLoops 를 통짜로). 이 구조의 대가: (1) 진행이 콘솔에 안 보임(바깥 LLM 이 콜 반환까지 정지),
|
|
498
|
+
(2) 한 콜이 길어 MCP 클라이언트 타임아웃에 걸림(고아 루프·비용 폭주), (3) 바깥 LLM 과 서브프로세스
|
|
499
|
+
claude 로 **에이전트를 두 번** 씀. 스텝 모드(모델 B)는 제어를 매 단위마다 LLM 에게 돌려준다.
|
|
500
|
+
|
|
501
|
+
- FR-24.1 `step` (기본 false). true 면 **매 단위 작업 후** `status:'continue'` 로 반환한다. 단위 =
|
|
502
|
+
BUILD 안의 **한 회차**(에이전트 1콜: PLAN/DO/HEAL 각각), 그리고 outer stage 머신의 **각 stage**
|
|
503
|
+
(DISCOVER/CODE_REVIEW/SHIP/WATCH/COMMIT). 다음이 DONE 이면 양보하지 않고 그대로 통과 처리한다.
|
|
504
|
+
- FR-24.2 **재개는 새 기능이 아니다** — 상태는 이미 매 지점 STATE.json 에 저장되므로, `continue` 후
|
|
505
|
+
같은 goal 로 재호출하면 기존 resume 경로가 정확히 다음 단위를 이어간다. 별도 코드 경로 없음(FR-15).
|
|
506
|
+
- FR-24.3 **통과 판정 우회 금지**: `continue` 는 실패도 통과도 아니다. `stepResult` 는 상태만 저장하고
|
|
507
|
+
돌려주며, 진짜 통과는 여전히 `finish('passed')` 한 곳만 지나 checkVerdictBinding(false green 게이트)을
|
|
508
|
+
통과해야 한다. 스텝이 늘어도 통과 주장 지점은 하나다.
|
|
509
|
+
- FR-24.4 **기존 동작 불변**: 기본 false 라 모델 A(무인·완료까지 자동)와 전체 회귀 스위트가 그대로다.
|
|
510
|
+
CLI `--step`, MCP `step`, 그리고 flow 스킬이 대화형에서 이 모드를 기본으로 쓴다(진행 가시성).
|
|
511
|
+
- FR-24.5 MCP 응답은 `continue` 를 "⏭ 스텝 완료 … 같은 goal 로 다시 호출" 로 표면화해 LLM 이
|
|
512
|
+
진행을 사용자에게 알리고 이어 호출하게 한다. CLI 는 awaiting 과 같은 계열(종료 아님)로 exit 3.
|
|
513
|
+
|
|
514
|
+
### FR-25. DOC-SYNC — 소스 변경을 문서에 반영하는 단발 패스 (`hiloop_docsync`, `src/docsync.js`)
|
|
515
|
+
|
|
516
|
+
hi-loop 은 소스↔문서 drift 를 **예방**하는 게이트(FR-19/21)는 있었지만, 이미 바뀐 소스에 맞춰 문서를
|
|
517
|
+
**갱신**하는 장치는 없었다(문서 동기화는 사람/어시스턴트의 수동 규율에 의존). doc-sync 는 이걸 엔진
|
|
518
|
+
동작으로 만든다 — MCP/CLI 라 소비 프로젝트에서도, claude 외 일꾼으로도 동작한다(FR-23).
|
|
519
|
+
|
|
520
|
+
- FR-25.1 **단발 패스**(자가 치유 루프 아님). `git diff` 로 소스 변경을 모아 일꾼 에이전트에게 주고
|
|
521
|
+
문서만 갱신하게 한다. 통과시킬 테스트가 없으므로 회차·정체·예산 개념이 없다. 짧아서 타임아웃/블랙박스가 없고 진행 알림을 보낸다.
|
|
522
|
+
- FR-25.2 **diff 범위**: 커밋 안 된 변경(HEAD 대비) 우선, 없으면 마지막 커밋(HEAD~1..HEAD). 문서(.md)만
|
|
523
|
+
바뀌었으면 트리거가 없으므로(반영할 소스 변경 없음) 에이전트를 부르지 않고 종료한다(비용 0).
|
|
524
|
+
- FR-25.3 **갱신 대상은 문서뿐**: `README.md`·`docs/*.md`. **`docs/DESIGN.md`(사람 오라클)와 소스·테스트는
|
|
525
|
+
건드리지 않는다**(프롬프트로 명시 + 사후 분류로 강제). diff 로 확인되는 것만 반영한다(환각 방지).
|
|
526
|
+
- FR-25.4 **stray 검증**: doc-sync 실행 전후 변경 파일 집합을 비교해, 이번에 새로 바뀐 것 중 문서가
|
|
527
|
+
아닌 파일(소스·테스트·오라클)은 `stray` 로 분리 보고한다 — 어시스턴트가 경고로 표면화한다.
|
|
528
|
+
- FR-25.5 표면: MCP `hiloop_docsync`, CLI `hi-loop docsync`, 스킬 `/flow doc-sync`(goal 불필요).
|
|
529
|
+
안전망으로 실행 전 체크포인트를 뜬다(잘못 고치면 `hiloop_rollback`). 커밋은 하지 않는다(사람이 판단).
|
|
530
|
+
|
|
469
531
|
---
|
|
470
532
|
|
|
471
533
|
## 3. 비기능 요구사항
|