@su-record/vibe 3.2.23 → 3.2.24

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.
@@ -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', result?: 'ok'|'fail'|'stuck', summary?: string, discoverHash?: string }} opts
39
+ * @param {{ loop: string, event: 'start'|'discover'|'end'|'iteration', result?: 'ok'|'fail'|'stuck', summary?: string, discoverHash?: string, verified?: boolean }} opts
40
40
  * @returns {boolean} 성공 여부
41
41
  */
42
42
  export function appendLoopEvent(projectDir, opts) {
@@ -50,6 +50,7 @@ export function appendLoopEvent(projectDir, opts) {
50
50
  ...(opts.result !== undefined ? { result: opts.result } : {}),
51
51
  ...(opts.summary !== undefined ? { summary: opts.summary } : {}),
52
52
  ...(opts.discoverHash !== undefined ? { discoverHash: opts.discoverHash } : {}),
53
+ ...(opts.verified !== undefined ? { verified: opts.verified } : {}),
53
54
  };
54
55
  fs.appendFileSync(p, JSON.stringify(entry) + '\n', 'utf-8');
55
56
  return true;
@@ -116,3 +117,55 @@ export function isStuck(projectDir, loop, discoverHash) {
116
117
  return false;
117
118
  }
118
119
  }
120
+
121
+ /**
122
+ * 회전 1회를 기록한다.
123
+ *
124
+ * @param {string} projectDir
125
+ * @param {string} loop
126
+ * @param {boolean} verified - 이 회전이 검증을 통과했는가 (JUDGE 결정론 게이트 기준)
127
+ * @returns {boolean}
128
+ */
129
+ export function recordIteration(projectDir, loop, verified) {
130
+ return appendLoopEvent(projectDir, { loop, event: 'iteration', verified: Boolean(verified) });
131
+ }
132
+
133
+ /**
134
+ * 현재 루프 실행의 예산 상태.
135
+ *
136
+ * 두 축을 **따로** 센다:
137
+ * - `iterations` — 모든 회전. `max_iterations` 와 비교하는 폭주 방어 축이다.
138
+ * - `verified` — 검증을 통과한 회전만. 실제로 전진한 양이다.
139
+ *
140
+ * 둘을 하나로 뭉치면 "10회를 썼다" 는 알아도 "그중 8회가 헛돌았다" 는 모른다 —
141
+ * 헛도는 루프와 원래 큰 작업을 구분할 수 없다. loop-contract 는 폭주 방어가
142
+ * 모델의 양심이 아니라 코드여야 한다고 선언하는데, 정작 max_iterations 에는
143
+ * 런타임 계수가 없어 모델이 스스로 세고 있었다 (감사 2026-08-10).
144
+ *
145
+ * 직전 `start` 이후만 센다 — 새 실행은 예산도 새로 시작한다.
146
+ *
147
+ * @param {string} projectDir
148
+ * @param {string} loop
149
+ * @param {number} [maxIterations=10]
150
+ * @returns {{ iterations: number, verified: number, remaining: number, exhausted: boolean }}
151
+ */
152
+ export function readBudget(projectDir, loop, maxIterations = 10) {
153
+ const empty = { iterations: 0, verified: 0, remaining: maxIterations, exhausted: false };
154
+ try {
155
+ const raw = fs.readFileSync(historyPath(projectDir), 'utf-8');
156
+ const events = raw.split('\n')
157
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
158
+ .filter(e => e && e.loop === loop);
159
+
160
+ const lastStart = events.map(e => e.event).lastIndexOf('start');
161
+ const scoped = lastStart === -1 ? events : events.slice(lastStart);
162
+
163
+ const iters = scoped.filter(e => e.event === 'iteration');
164
+ const iterations = iters.length;
165
+ const verified = iters.filter(e => e.verified === true).length;
166
+ const remaining = Math.max(0, maxIterations - iterations);
167
+ return { iterations, verified, remaining, exhausted: remaining === 0 };
168
+ } catch {
169
+ return empty;
170
+ }
171
+ }
@@ -11,13 +11,15 @@
11
11
  * node hooks/scripts/loop-ledger.js gate open <id> <question> [option...]
12
12
  * node hooks/scripts/loop-ledger.js gate list
13
13
  * node hooks/scripts/loop-ledger.js gate answer <id> <answer>
14
+ * node hooks/scripts/loop-ledger.js iteration <name> <verified|unverified>
15
+ * node hooks/scripts/loop-ledger.js budget <name> [maxIterations]
14
16
  *
15
17
  * check-stuck: 'stuck' 또는 'ok'를 stdout에 출력하고 항상 exit 0.
16
18
  * anchor: 재고정 번들 JSON을 stdout에 출력한다 (loop-contract ANCHOR 절).
17
19
  * 항상 exit 0 (fail-open).
18
20
  */
19
21
 
20
- import { appendLoopEvent, isStuck } from './lib/loop-ledger.js';
22
+ import { appendLoopEvent, isStuck, recordIteration, readBudget } from './lib/loop-ledger.js';
21
23
  import { buildAnchor } from './lib/anchor.js';
22
24
  import { prependInboxBlock } from './lib/inbox.js';
23
25
  import { openGate, listOpenGates, answerGate, formatOpenGates } from './lib/gates.js';
@@ -73,6 +75,27 @@ if (subcommand === 'start') {
73
75
  : '[loop-ledger] WARNING: inbox write failed\n'
74
76
  );
75
77
 
78
+ } else if (subcommand === 'iteration') {
79
+ // 회전 계수는 코드가 한다 — max_iterations 를 모델이 세면 폭주 방어가 양심이 된다
80
+ const [loop, verifiedArg] = args;
81
+ if (!loop || !['verified', 'unverified'].includes(verifiedArg || '')) {
82
+ process.stdout.write('[loop-ledger] error: iteration 에 루프 이름과 verified|unverified 가 필요합니다\n');
83
+ process.exit(0);
84
+ }
85
+ recordIteration(projectDir, loop, verifiedArg === 'verified');
86
+ const b = readBudget(projectDir, loop);
87
+ process.stdout.write(`[loop-ledger] iteration recorded: ${loop} ${verifiedArg} (${b.iterations} 회전 / 검증 ${b.verified})\n`);
88
+
89
+ } else if (subcommand === 'budget') {
90
+ const [loop, maxRaw] = args;
91
+ if (!loop) {
92
+ process.stdout.write('[loop-ledger] error: budget 에 루프 이름이 필요합니다\n');
93
+ process.exit(0);
94
+ }
95
+ const max = Number.parseInt(maxRaw ?? '10', 10);
96
+ const b = readBudget(projectDir, loop, Number.isInteger(max) && max > 0 ? max : 10);
97
+ process.stdout.write(JSON.stringify(b) + '\n');
98
+
76
99
  } else if (subcommand === 'gate') {
77
100
  // 사람 판단 지점을 디스크에 남긴다 — 세션이 죽어도 무엇을 묻고 있었는지 남는다
78
101
  const [action, id, ...rest] = args;
@@ -105,7 +128,7 @@ if (subcommand === 'start') {
105
128
  } else {
106
129
  process.stdout.write(
107
130
  '[loop-ledger] 사용법: start <name> | end <name> <ok|fail|stuck> [summary] | '
108
- + 'check-stuck <name> <hash> | anchor [feature] | inbox <name> <ok|fail|stuck> [line...] | gate <open|list|answer> …\n'
131
+ + 'check-stuck <name> <hash> | anchor [feature] | inbox <name> <ok|fail|stuck> [line...] | gate <open|list|answer> | iteration <name> <verified|unverified> | budget <name> [max]\n'
109
132
  );
110
133
  }
111
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "3.2.23",
3
+ "version": "3.2.24",
4
4
  "description": "AI Coding Framework for Claude Code — 7+ agents, 52 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -86,6 +86,11 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
86
86
  · ledger: cat .vibe/metrics/run-ledger.json → verifyPassed === true 만 성공
87
87
  · tests: 정의의 test_command 실행 → exit 0 만 성공
88
88
  · none: 판정 생략(보고만). "코드를 보니 잘 된 것 같다"는 판정이 아니다.
89
+ 6-b. 회전 기록 node "$HOOKS_DIR/loop-ledger.js" iteration <name> <verified|unverified>
90
+ VERIFY 결과를 그대로 넣는다. 이어서 예산을 확인한다:
91
+ node "$HOOKS_DIR/loop-ledger.js" budget <name> <max_iterations>
92
+ → exhausted 면 잔여를 인박스로 이월하고 종료한다. 회전 수를 모델이
93
+ 세지 않는다 — 폭주 방어는 코드가 판정한다 (loop-contract 예산 절).
89
94
  7. 종료 기록 node "$HOOKS_DIR/loop-ledger.js" end <name> <ok|fail|stuck> "<한 줄 요약>"
90
95
  8. 인박스 node "$HOOKS_DIR/loop-ledger.js" inbox <name> <ok|fail|stuck> \
91
96
  "발견: N건 / 처리: M건 / 검증: <기준과 결과>" \
@@ -111,6 +116,7 @@ node -e "import('{{VIBE_PATH_URL}}/node_modules/@su-record/vibe/dist/tools/index
111
116
  |------|------|
112
117
  | 완료는 게이트가 판정 | run-ledger `verifyPassed` / 테스트 exit code (REQ-005) |
113
118
  | stuck은 해시가 판정 | loop-ledger check-stuck, 동일 발견 2회 연속 (REQ-009) |
119
+ | 회전 수도 코드가 판정 | loop-ledger iteration/budget — iterations(폭주 방어) 와 verified(전진량) 를 분리 |
114
120
  | 이해 부채 가드 | 인박스 리뷰 큐 + push/release 금지 (REQ-008) |
115
121
  | 기록 없는 실행 없음 | loop-history.jsonl start/end 의무 (REQ-006) |
116
122
 
@@ -62,6 +62,27 @@ node "$HOOKS_DIR/loop-ledger.js" anchor [feature]
62
62
 
63
63
  > `autonomous` 의 "계속" 은 **stuck 난 루프를 더 돌린다는 뜻이 아니다** — 2회 연속 동일 발견은 정의상 재시도가 무의미하다. 같은 목표를 붙잡지 않고 다음 단위로 넘어간다는 뜻이며, 미달은 TODO/인박스에 남는다. 미달 상태를 **완료로 기록하지 않는다.**
64
64
 
65
+ ### 예산 — 회전은 코드가 세고, 전진과 헛돎을 구분한다
66
+
67
+ 이 문서는 서두에서 "폭주 방어가 모델의 양심이 아니라 결정론적 가드(코드)" 라고 선언한다. 그런데 정작 폭주 방어인 `max_iterations` 에는 런타임 계수가 없었다 — stuck 만 명령으로 판정되고 회전 수는 모델이 스스로 셌다. 선언과 구현이 어긋난 지점이었다 (감사 2026-08-10).
68
+
69
+ ```bash
70
+ node "$HOOKS_DIR/loop-ledger.js" iteration <name> <verified|unverified> # 회전 종료 시 1회
71
+ node "$HOOKS_DIR/loop-ledger.js" budget <name> [max] # → {iterations, verified, remaining, exhausted}
72
+ ```
73
+
74
+ **두 축을 따로 센다:**
75
+
76
+ | 축 | 의미 | 쓰임 |
77
+ |---|---|---|
78
+ | `iterations` | 모든 회전 | `max_iterations` 와 비교하는 **폭주 방어** |
79
+ | `verified` | JUDGE 결정론 게이트를 통과한 회전만 | 실제로 **전진한 양** |
80
+
81
+ 둘을 하나로 뭉치면 "10회를 썼다" 는 알아도 "그중 8회가 헛돌았다" 는 모른다 — 헛도는 루프와 원래 큰 작업을 구분할 수 없다. `exhausted: true` 면 잔여를 인박스로 이월하고 종료한다.
82
+
83
+ - **실행 실패(error)는 회전을 소비하지 않는다** — 위 실행 실패 절대로 루프가 즉시 종료되므로 예산을 갉아먹지 않는다. 재시도가 예산을 태우는 형태를 만들지 않는다.
84
+ - 계수는 직전 `start` 이후만 센다 — 새 실행은 예산도 새로 시작한다.
85
+
65
86
  ### 게이트 객체 — 사람을 기다리는 이유는 디스크에 산다
66
87
 
67
88
  사람 개입 지점의 질문이 컨텍스트에만 있으면, 세션이 죽거나 compact 로 소실될 때 **무엇을 묻고 있었는지가 사라진다.** 사람은 돌아왔는데 답할 대상이 없다. run-ledger·loop-history·인박스가 전부 디스크에 사는데 "지금 왜 멈춰 있는가"만 컨텍스트에 있었다.
@@ -131,7 +152,7 @@ stuck 은 **같은 발견이 반복되는** 상태다. 스킬이 로드되지
131
152
 
132
153
  | 파라미터 | 기본 | 의미 |
133
154
  |---|---|---|
134
- | `max_iterations` | 10 | 회전 상한. 도달 시 잔여를 인박스로 이월 |
155
+ | `max_iterations` | 10 | 회전 상한. 도달 시 잔여를 인박스로 이월. **계수는 코드가 한다** — 아래 예산 절 |
135
156
  | `exit` | 게이트 통과 (**측정된** P1=0 ∧ verifyPassed) | 종료 기준. coverage 100% 등으로 상향 가능. 판정된 P1 은 위 Judge 권한 경계 표를 따른다 |
136
157
  | `--interactive` | off | 단계별 확인 모드 (회전마다 사람 승인 — 과거의 기본값) |
137
158
  | `--max-iter N` | — | 회전 상한 명시 (N=1이면 1회 시도) |