@su-record/vibe 3.2.47 → 3.2.49
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/hooks/scripts/lib/loop-ledger.js +47 -1
- package/hooks/scripts/loop-ledger.js +25 -1
- package/package.json +1 -1
- package/skills/vibe.loop/SKILL.md +11 -1
- package/skills/vibe.loop/references/install-adapters.md +16 -1
- package/vibe/rules/loop-contract.md +10 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe",
|
|
3
3
|
"displayName": "Vibe",
|
|
4
|
-
"version": "3.2.
|
|
4
|
+
"version": "3.2.49",
|
|
5
5
|
"description": "Verification harness for AI coding agents — \"done\" is decided by deterministic gates (test exit codes, run-ledger, regression memory), not the model self-report.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "su-record",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.49",
|
|
4
4
|
"description": "Verification harness for AI coding agents — \"done\" is decided by deterministic gates (test exit codes, run-ledger, regression memory), not the model self-report.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "su-record",
|
|
@@ -36,7 +36,7 @@ export function hashDiscoverOutput(text) {
|
|
|
36
36
|
* 루프 이벤트를 jsonl 파일에 append한다.
|
|
37
37
|
*
|
|
38
38
|
* @param {string} projectDir
|
|
39
|
-
* @param {{ loop: string, event: 'start'|'discover'|'end'|'iteration', result?: 'ok'|'fail'|'stuck', summary?: string, discoverHash?: string, verified?: boolean }} opts
|
|
39
|
+
* @param {{ loop: string, event: 'start'|'discover'|'end'|'iteration'|'trial-approved', result?: 'ok'|'fail'|'stuck', summary?: string, discoverHash?: string, verified?: boolean }} opts
|
|
40
40
|
* @returns {boolean} 성공 여부
|
|
41
41
|
*/
|
|
42
42
|
export function appendLoopEvent(projectDir, opts) {
|
|
@@ -169,3 +169,49 @@ export function readBudget(projectDir, loop, maxIterations = 10) {
|
|
|
169
169
|
return empty;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 시운전 게이트 — 처음 거는 루프는 **두 바퀴만** 돌고 멈춘다.
|
|
175
|
+
*
|
|
176
|
+
* WHY: 자율 루프의 첫 실행은 정의가 맞는지 아무도 모르는 상태다. discover 가
|
|
177
|
+
* 엉뚱한 것을 긁거나 verify 기준이 틀려 있으면 루프는 그걸 **성실하게 반복**한다.
|
|
178
|
+
* 사람이 안 보는 동안 도는 것이 목적이므로, 틀린 채로 도는 것도 안 보인다.
|
|
179
|
+
* 그래서 처음에는 몇 바퀴만 돌려 기록을 눈으로 확인한 뒤 풀어준다.
|
|
180
|
+
*
|
|
181
|
+
* `max_iterations` 와는 다른 축이다 — 그건 **한 실행의 폭주**를 막고, 이건
|
|
182
|
+
* **정의가 검증되지 않은 루프**를 막는다. 폭주 예산은 매 `start` 마다 초기화되지만
|
|
183
|
+
* 시운전은 승인 전까지 계속 걸린다.
|
|
184
|
+
*
|
|
185
|
+
* 승인은 이력에 남긴다(`event: 'trial-approved'`) — 설정 파일이 아니라 원장에
|
|
186
|
+
* 두는 이유는 루프별로 다르고, 언제 누가 풀었는지가 감사 대상이기 때문이다.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} projectDir
|
|
189
|
+
* @param {string} loop
|
|
190
|
+
* @param {number} [trialIterations=2] 시운전 회전 수
|
|
191
|
+
* @returns {{ inTrial: boolean, cap: number|null, iterations: number, exhausted: boolean }}
|
|
192
|
+
*/
|
|
193
|
+
export function readTrialGate(projectDir, loop, trialIterations = 2) {
|
|
194
|
+
const cap = Math.max(1, Number(trialIterations) || 2);
|
|
195
|
+
try {
|
|
196
|
+
const raw = fs.readFileSync(historyPath(projectDir), 'utf-8');
|
|
197
|
+
const events = raw.split('\n')
|
|
198
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
199
|
+
.filter(e => e && e.loop === loop);
|
|
200
|
+
|
|
201
|
+
if (events.some(e => e.event === 'trial-approved')) {
|
|
202
|
+
return { inTrial: false, cap: null, iterations: 0, exhausted: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 시운전 회전은 실행을 가로질러 누적한다 — 매 start 마다 초기화되면
|
|
206
|
+
// 루프를 다시 걸기만 해도 시운전이 무한정 연장된다.
|
|
207
|
+
const iterations = events.filter(e => e.event === 'iteration').length;
|
|
208
|
+
return { inTrial: true, cap, iterations, exhausted: iterations >= cap };
|
|
209
|
+
} catch {
|
|
210
|
+
return { inTrial: true, cap, iterations: 0, exhausted: false };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 시운전을 풀어 이 루프를 정상 예산으로 돌린다 — 사람이 기록을 확인한 뒤 호출한다 */
|
|
215
|
+
export function approveTrial(projectDir, loop) {
|
|
216
|
+
return appendLoopEvent(projectDir, { loop, event: 'trial-approved' });
|
|
217
|
+
}
|
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
* node hooks/scripts/loop-ledger.js gate answer <id> <answer>
|
|
14
14
|
* node hooks/scripts/loop-ledger.js iteration <name> <verified|unverified>
|
|
15
15
|
* node hooks/scripts/loop-ledger.js budget <name> [maxIterations]
|
|
16
|
+
* node hooks/scripts/loop-ledger.js trial <name> [trialIterations]
|
|
17
|
+
* node hooks/scripts/loop-ledger.js trial-approve <name>
|
|
16
18
|
*
|
|
17
19
|
* check-stuck: 'stuck' 또는 'ok'를 stdout에 출력하고 항상 exit 0.
|
|
18
20
|
* anchor: 재고정 번들 JSON을 stdout에 출력한다 (loop-contract ANCHOR 절).
|
|
19
21
|
* 항상 exit 0 (fail-open).
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
appendLoopEvent, isStuck, recordIteration, readBudget, readTrialGate, approveTrial,
|
|
26
|
+
} from './lib/loop-ledger.js';
|
|
23
27
|
import { buildAnchor } from './lib/anchor.js';
|
|
24
28
|
import { prependInboxBlock } from './lib/inbox.js';
|
|
25
29
|
import { openGate, listOpenGates, answerGate, formatOpenGates } from './lib/gates.js';
|
|
@@ -96,6 +100,26 @@ if (subcommand === 'start') {
|
|
|
96
100
|
const b = readBudget(projectDir, loop, Number.isInteger(max) && max > 0 ? max : 10);
|
|
97
101
|
process.stdout.write(JSON.stringify(b) + '\n');
|
|
98
102
|
|
|
103
|
+
} else if (subcommand === 'trial') {
|
|
104
|
+
// 처음 거는 루프는 몇 바퀴만 돌고 멈춘다 — 정의가 틀린 채로 성실히 반복하는 것을 막는다
|
|
105
|
+
const [loop, capRaw] = args;
|
|
106
|
+
if (!loop) {
|
|
107
|
+
process.stdout.write('[loop-ledger] error: trial 에 루프 이름이 필요합니다\n');
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
const cap = Number.parseInt(capRaw ?? '2', 10);
|
|
111
|
+
const t = readTrialGate(projectDir, loop, Number.isInteger(cap) && cap > 0 ? cap : 2);
|
|
112
|
+
process.stdout.write(JSON.stringify(t) + '\n');
|
|
113
|
+
|
|
114
|
+
} else if (subcommand === 'trial-approve') {
|
|
115
|
+
const [loop] = args;
|
|
116
|
+
if (!loop) {
|
|
117
|
+
process.stdout.write('[loop-ledger] error: trial-approve 에 루프 이름이 필요합니다\n');
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
approveTrial(projectDir, loop);
|
|
121
|
+
process.stdout.write(`[loop-ledger] ${loop} 시운전 해제 — 이후 max_iterations 예산으로 돈다\n`);
|
|
122
|
+
|
|
99
123
|
} else if (subcommand === 'gate') {
|
|
100
124
|
// 사람 판단 지점을 디스크에 남긴다 — 세션이 죽어도 무엇을 묻고 있었는지 남는다
|
|
101
125
|
const [action, id, ...rest] = args;
|
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ HISTORY=".vibe/metrics/loop-history.jsonl"
|
|
|
52
52
|
3. **생성 직후 반드시 검증한다** — 실패 시 정의를 고치고 재검증, 통과 전에는 install/run 금지:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index.js').then(t => { const fs = require('fs'); const r = t.validateLoopDefinition(fs.readFileSync('.vibe/loops/<name>.md', 'utf-8')); console.log(JSON.stringify(r.errors.length ? r.errors : 'valid')); })"
|
|
55
|
+
node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/loop/index.js').then(t => { const fs = require('fs'); const r = t.validateLoopDefinition(fs.readFileSync('.vibe/loops/<name>.md', 'utf-8')); console.log(JSON.stringify(r.errors.length ? r.errors : 'valid')); })"
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**기본 제안 레시피 — nightly-triage** (사용자가 목표를 정하지 못하면 이것을 제안):
|
|
@@ -73,6 +73,9 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
|
|
|
73
73
|
→ missing[] 이 비어 있지 않으면 없는 아티팩트를 기억으로 메우지 않는다.
|
|
74
74
|
재고정 실패를 인박스에 남기고 종료한다.
|
|
75
75
|
1. 검증 validateLoopDefinition 통과 확인 (위 design 3의 명령) — 실패 시 인박스에 기록 후 종료
|
|
76
|
+
1-b. 시운전 node "$HOOKS_DIR/loop-ledger.js" trial <name> [trial_iterations]
|
|
77
|
+
→ `inTrial: true` 면 이 실행의 회전 상한은 `cap` 이다 (max_iterations 아님).
|
|
78
|
+
`exhausted: true` 면 **한 바퀴도 돌지 않고** 종료한다 — 6-c 참조.
|
|
76
79
|
2. 시작 기록 node "$HOOKS_DIR/loop-ledger.js" start <name>
|
|
77
80
|
3. DISCOVER 정의의 discover 지시 실행 → 일거리 목록 산출
|
|
78
81
|
4. STUCK 검사 DISCOVER_HASH=$(node -e "...hashDiscoverOutput...") # 발견 목록 텍스트의 해시
|
|
@@ -91,6 +94,13 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
|
|
|
91
94
|
node "$HOOKS_DIR/loop-ledger.js" budget <name> <max_iterations>
|
|
92
95
|
→ exhausted 면 잔여를 인박스로 이월하고 종료한다. 회전 수를 모델이
|
|
93
96
|
세지 않는다 — 폭주 방어는 코드가 판정한다 (loop-contract 예산 절).
|
|
97
|
+
6-c. 시운전 확인 시운전 중이면(1-b 의 `inTrial`) 매 회전 뒤 다시 확인한다:
|
|
98
|
+
node "$HOOKS_DIR/loop-ledger.js" trial <name> [trial_iterations]
|
|
99
|
+
→ exhausted 면 **거기서 멈춘다.** 인박스에 아래를 남기고 종료:
|
|
100
|
+
· 무엇을 발견했고 무엇을 고쳤는지 (회전별 한 줄)
|
|
101
|
+
· 확인할 것: `.vibe/metrics/loop-history.jsonl` 과 실제 변경 diff
|
|
102
|
+
· 이어서 돌리려면: loop-ledger.js trial-approve <name>
|
|
103
|
+
승인 없이 계속 도는 것 금지 — 정의가 맞는지 아직 아무도 모른다.
|
|
94
104
|
7. 종료 기록 node "$HOOKS_DIR/loop-ledger.js" end <name> <ok|fail|stuck> "<한 줄 요약>"
|
|
95
105
|
8. 인박스 node "$HOOKS_DIR/loop-ledger.js" inbox <name> <ok|fail|stuck> \
|
|
96
106
|
"발견: N건 / 처리: M건 / 검증: <기준과 결과>" \
|
|
@@ -6,5 +6,20 @@ Load only for `vibe.loop install`. Never register a schedule without the user's
|
|
|
6
6
|
|------|------|
|
|
7
7
|
| Claude Code (세션 루프) | `/loop <interval> "/vibe.loop run <name>"` |
|
|
8
8
|
| Claude Code (클라우드 루틴) | `/schedule`로 cron `<schedule>` + prompt `/vibe.loop run <name>` 등록 |
|
|
9
|
-
| OS cron fallback |
|
|
9
|
+
| OS cron fallback | 아래 참조 — **PATH 를 반드시 명시한다** |
|
|
10
|
+
|
|
11
|
+
### OS cron 등록
|
|
12
|
+
|
|
13
|
+
```cron
|
|
14
|
+
# PATH 를 적지 않으면 cron 은 /usr/bin:/bin 만 들고 실행한다 — claude 를 찾지 못해
|
|
15
|
+
# 루프가 **아무 소리 없이** 한 번도 돌지 않는다. 등록 전에 실제 경로를 확인할 것:
|
|
16
|
+
# which claude → 예: /usr/local/bin/claude, ~/.local/bin/claude, ~/.nvm/.../bin/claude
|
|
17
|
+
PATH=/usr/local/bin:/usr/bin:/bin:<claude 가 있는 디렉토리>
|
|
18
|
+
|
|
19
|
+
<schedule> cd <project> && claude -p "/vibe.loop run <name>" --permission-mode acceptEdits >> <project>/.vibe/loops/<name>.log 2>&1
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **PATH**: `which claude` 결과의 디렉토리를 반드시 포함시킨다. nvm·Homebrew·`~/.local/bin` 중 어디든 cron 기본 PATH 에는 없다
|
|
23
|
+
- **로그 리다이렉트**: cron 은 출력을 메일로 보내거나 버린다. 파일로 받아야 루프가 왜 멈췄는지 알 수 있다
|
|
24
|
+
- **등록 후 1회 검증**: 다음 실행 시각을 기다리지 말고 같은 명령을 `env -i PATH=... sh -c '...'` 로 한 번 돌려 본다 — cron 과 같은 최소 환경에서 되는지 확인하는 것이 목적이다
|
|
10
25
|
| Codex | Automations에 `<schedule>` + `$vibe.loop run <name>` 등록 |
|
|
@@ -195,4 +195,14 @@ JUDGE는 이번 feature의 **신규 생성 파일** 기준으로 검증 코드
|
|
|
195
195
|
| `ralplan` | 같은 계약을 계획 단계에 적용 |
|
|
196
196
|
| `ultrawork` / `ulw` | `automationLevel: autonomous` + 병렬 ACT — 루프 시맨틱이 아니라 자율성·병렬성 축. **병렬 항목이 파일을 수정하면 항목별 worktree 격리 필수** (`vibe.loop` 의 `isolation` 축과 같은 규칙) |
|
|
197
197
|
|
|
198
|
+
### 시운전 (처음 거는 루프)
|
|
199
|
+
승인 이력이 없는 루프는 **`trial_iterations`(기본 2) 회전만 돌고 멈춘다.** `max_iterations` 와 다른 축이다 — 그건 한 실행의 폭주를 막고, 이건 **정의가 검증되지 않은 루프**를 막는다. 폭주 예산은 매 `start` 마다 초기화되지만 시운전 회전은 실행을 가로질러 누적한다(다시 걸기만 해도 연장되면 게이트가 무의미하다).
|
|
200
|
+
|
|
201
|
+
| 판정 | 명령 |
|
|
202
|
+
|---|---|
|
|
203
|
+
| 시운전 상태 | `loop-ledger.js trial <name> [n]` → `{inTrial, cap, iterations, exhausted}` |
|
|
204
|
+
| 해제 | `loop-ledger.js trial-approve <name>` — 사람이 기록을 확인한 뒤 |
|
|
205
|
+
|
|
206
|
+
WHY: 자율 루프의 첫 실행은 정의가 맞는지 아무도 모르는 상태다. discover 가 엉뚱한 것을 긁거나 verify 기준이 틀려 있으면 루프는 그걸 **성실하게 반복한다.** 사람이 안 보는 동안 도는 것이 목적이므로 틀린 채로 도는 것도 안 보인다. 해제 기록을 설정이 아니라 원장에 남기는 이유는 루프별로 다르고 언제 풀렸는지가 감사 대상이기 때문이다.
|
|
207
|
+
|
|
198
208
|
> **폭이 수십 단위를 넘으면** Claude Code 네이티브 `Workflow`(누적 1000 에이전트 / 동시 min(16, cores-2))를 **제안**한다 — 자동 전환하지 않는다. 옵트인 도구이고, 조율 비용만 절약될 뿐 에이전트 사용량은 그대로 든다. Codex 에는 등가물이 없으므로 루프 계약 자체는 양쪽 동일하게 유지한다. 넘기더라도 위의 격리 규정과 검증자 컨텍스트 규정은 그대로 적용된다. 상세: `CLAUDE.md` "폭이 큰 작업 — 네이티브 workflow 로 라우팅".
|