@su-record/vibe 2.13.0 → 2.14.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.
Files changed (42) hide show
  1. package/CLAUDE.md +18 -15
  2. package/README.en.md +5 -3
  3. package/README.md +6 -4
  4. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  5. package/dist/cli/postinstall/constants.js +1 -0
  6. package/dist/cli/postinstall/constants.js.map +1 -1
  7. package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
  8. package/dist/cli/setup/ProjectSetup.js +4 -3
  9. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  10. package/dist/tools/index.d.ts +2 -0
  11. package/dist/tools/index.d.ts.map +1 -1
  12. package/dist/tools/index.js +2 -0
  13. package/dist/tools/index.js.map +1 -1
  14. package/dist/tools/loop/index.d.ts +6 -0
  15. package/dist/tools/loop/index.d.ts.map +1 -0
  16. package/dist/tools/loop/index.js +5 -0
  17. package/dist/tools/loop/index.js.map +1 -0
  18. package/dist/tools/loop/validateLoopDefinition.d.ts +38 -0
  19. package/dist/tools/loop/validateLoopDefinition.d.ts.map +1 -0
  20. package/dist/tools/loop/validateLoopDefinition.js +224 -0
  21. package/dist/tools/loop/validateLoopDefinition.js.map +1 -0
  22. package/dist/tools/loop/validateLoopDefinition.test.d.ts +14 -0
  23. package/dist/tools/loop/validateLoopDefinition.test.d.ts.map +1 -0
  24. package/dist/tools/loop/validateLoopDefinition.test.js +229 -0
  25. package/dist/tools/loop/validateLoopDefinition.test.js.map +1 -0
  26. package/hooks/scripts/__tests__/.vibe/command-log.txt +21 -0
  27. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  28. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  29. package/hooks/scripts/__tests__/keyword-detector.test.js +26 -18
  30. package/hooks/scripts/__tests__/loop-ledger.test.js +321 -0
  31. package/hooks/scripts/keyword-detector.js +22 -22
  32. package/hooks/scripts/lib/hook-context.js +31 -2
  33. package/hooks/scripts/lib/loop-ledger.js +118 -0
  34. package/hooks/scripts/loop-ledger.js +56 -0
  35. package/package.json +1 -1
  36. package/skills/vibe/SKILL.md +40 -23
  37. package/skills/vibe.loop/SKILL.md +116 -0
  38. package/skills/vibe.run/SKILL.md +22 -18
  39. package/skills/vibe.run/references/ralph-loop.md +18 -17
  40. package/skills/vibe.run/references/ultrawork-mode.md +36 -33
  41. package/vibe/rules/loop-contract.md +54 -0
  42. package/vibe/templates/loop-template.md +69 -0
@@ -27,28 +27,30 @@ function runDetector(text) {
27
27
  // ══════════════════════════════════════════════════
28
28
  describe('keyword-detector', () => {
29
29
  describe('ralph keyword', () => {
30
- it('should detect ralph keyword', () => {
30
+ it('should detect ralph keyword and emit deprecation hint', () => {
31
31
  const result = runDetector('implement the login feature ralph');
32
- expect(result.stdout).toContain('[RALPH MODE]');
32
+ expect(result.stdout).toContain('[vibe]');
33
+ expect(result.stdout).toContain('deprecated');
33
34
  expect(result.stdout).toContain('persistence');
34
35
  });
35
36
 
36
37
  it('should detect ralph case-insensitively', () => {
37
38
  const result = runDetector('RALPH fix all the bugs');
38
- expect(result.stdout).toContain('[RALPH MODE]');
39
+ expect(result.stdout).toContain('[vibe]');
40
+ expect(result.stdout).toContain('deprecated');
39
41
  });
40
42
  });
41
43
 
42
44
  describe('ultrawork keyword', () => {
43
- it('should detect ultrawork keyword', () => {
45
+ it('should detect ultrawork keyword and emit automationLevel banner', () => {
44
46
  const result = runDetector('ultrawork build the entire app');
45
- expect(result.stdout).toContain('[ULTRAWORK MODE]');
47
+ expect(result.stdout).toContain('[ULTRAWORK]');
46
48
  expect(result.stdout).toContain('parallel');
47
49
  });
48
50
 
49
51
  it('should detect ulw alias', () => {
50
52
  const result = runDetector('ulw refactor the codebase');
51
- expect(result.stdout).toContain('[ULTRAWORK MODE]');
53
+ expect(result.stdout).toContain('[ULTRAWORK]');
52
54
  });
53
55
 
54
56
  it('should detect Korean alias when word boundary matches', () => {
@@ -74,28 +76,32 @@ describe('keyword-detector', () => {
74
76
  // strict 키워드(일상어): 명령 끝 위치 또는 --flag 에서만 발동.
75
77
  // 일상 영어("please verify", "quick question")의 오탐을 막기 위함.
76
78
  describe('verify keyword (strict)', () => {
77
- it('should detect verify at command tail', () => {
79
+ it('should detect verify at command tail and emit deprecation hint', () => {
78
80
  const result = runDetector('make the implementation correct, verify');
79
- expect(result.stdout).toContain('[VERIFY MODE]');
81
+ expect(result.stdout).toContain('[vibe]');
82
+ expect(result.stdout).toContain('deprecated');
80
83
  expect(result.stdout).toContain('verification');
81
84
  });
82
85
 
83
86
  it('should detect --verify flag', () => {
84
87
  const result = runDetector('fix the bug --verify');
85
- expect(result.stdout).toContain('[VERIFY MODE]');
88
+ expect(result.stdout).toContain('[vibe]');
89
+ expect(result.stdout).toContain('deprecated');
86
90
  });
87
91
  });
88
92
 
89
93
  describe('quick keyword (strict)', () => {
90
- it('should detect quick at command tail', () => {
94
+ it('should detect quick at command tail and emit --max-iter 1 hint', () => {
91
95
  const result = runDetector('fix this typo quick');
92
- expect(result.stdout).toContain('[QUICK MODE]');
96
+ expect(result.stdout).toContain('[vibe]');
97
+ expect(result.stdout).toContain('--max-iter 1');
93
98
  expect(result.stdout).toContain('fast');
94
99
  });
95
100
 
96
101
  it('should detect --quick flag', () => {
97
102
  const result = runDetector('build the payment API --quick');
98
- expect(result.stdout).toContain('[QUICK MODE]');
103
+ expect(result.stdout).toContain('[vibe]');
104
+ expect(result.stdout).toContain('--max-iter 1');
99
105
  });
100
106
  });
101
107
 
@@ -140,17 +146,19 @@ describe('keyword-detector', () => {
140
146
  // Keyword combinations / synergies
141
147
  // ══════════════════════════════════════════════════
142
148
  describe('keyword combinations', () => {
143
- it('should detect ralph+ultrawork synergy', () => {
149
+ it('should detect ralph+ultrawork synergy and emit deprecation hint', () => {
144
150
  const result = runDetector('ralph ultrawork build everything from scratch');
145
- expect(result.stdout).toContain('[RALPH+ULTRAWORK]');
151
+ expect(result.stdout).toContain('[vibe]');
152
+ expect(result.stdout).toContain('deprecated');
146
153
  expect(result.stdout).toContain('persistence');
147
154
  expect(result.stdout).toContain('parallel');
148
155
  });
149
156
 
150
- it('should detect ralph+verify synergy', () => {
157
+ it('should detect ralph+verify synergy and emit deprecation hint', () => {
151
158
  // verify is strict → use --verify flag (ralph stays bare)
152
159
  const result = runDetector('ralph fix each step --verify');
153
- expect(result.stdout).toContain('[RALPH+VERIFY]');
160
+ expect(result.stdout).toContain('[vibe]');
161
+ expect(result.stdout).toContain('deprecated');
154
162
  });
155
163
 
156
164
  it('should output both keywords when no synergy key matches sorted order', () => {
@@ -158,7 +166,7 @@ describe('keyword-detector', () => {
158
166
  // sorts keywords alphabetically → tries 'explore+ultrawork' which has no match.
159
167
  // So individual outputs are emitted instead. explore is strict → --explore.
160
168
  const result = runDetector('ultrawork analyze the entire project --explore');
161
- expect(result.stdout).toContain('[ULTRAWORK MODE]');
169
+ expect(result.stdout).toContain('[ULTRAWORK]');
162
170
  expect(result.stdout).toContain('[EXPLORE MODE]');
163
171
  expect(result.stdout).toContain('[FLAGS]');
164
172
  });
@@ -224,7 +232,7 @@ describe('keyword-detector', () => {
224
232
  });
225
233
 
226
234
  it('should merge flags from multiple keywords', () => {
227
- // quick is strict → place at command tail
235
+ // quick is strict → place at command tail; ralph stays bare
228
236
  const result = runDetector('ralph finish this quick');
229
237
  expect(result.stdout).toContain('[FLAGS]');
230
238
  expect(result.stdout).toContain('persistence');
@@ -0,0 +1,321 @@
1
+ /**
2
+ * loop-ledger 라이브러리 + CLI 테스트
3
+ *
4
+ * 커버리지:
5
+ * - appendLoopEvent round-trip
6
+ * - isStuck: 2회 연속 동일 hash → true
7
+ * - isStuck: 다른 hash → false
8
+ * - isStuck: 이력 없음 → false
9
+ * - fail-open: 손상된 jsonl 줄 무시
10
+ * - hashDiscoverOutput: 공백 정규화
11
+ * - CLI: start / end / check-stuck 서브커맨드
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import { spawnSync } from 'child_process';
19
+ import { fileURLToPath } from 'url';
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ let tmpDir;
24
+ beforeEach(() => {
25
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-loop-ledger-test-'));
26
+ });
27
+ afterEach(() => {
28
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
29
+ });
30
+
31
+ async function importLedger() {
32
+ const p = path.resolve(__dirname, '..', 'lib', 'loop-ledger.js');
33
+ return import(p);
34
+ }
35
+
36
+ const CLI = path.resolve(__dirname, '..', 'loop-ledger.js');
37
+
38
+ function historyPath(dir) {
39
+ return path.join(dir, '.vibe', 'metrics', 'loop-history.jsonl');
40
+ }
41
+
42
+ // ─── appendLoopEvent round-trip ───────────────────────────────────────
43
+
44
+ describe('loop-ledger: appendLoopEvent round-trip', () => {
45
+ it('start 이벤트를 append 하면 jsonl 파일에 기록된다', async () => {
46
+ const { appendLoopEvent } = await importLedger();
47
+ appendLoopEvent(tmpDir, { loop: 'nightly', event: 'start' });
48
+
49
+ const p = historyPath(tmpDir);
50
+ expect(fs.existsSync(p)).toBe(true);
51
+ const line = fs.readFileSync(p, 'utf-8').trim();
52
+ const obj = JSON.parse(line);
53
+ expect(obj.loop).toBe('nightly');
54
+ expect(obj.event).toBe('start');
55
+ expect(obj.ts).toBeDefined();
56
+ });
57
+
58
+ it('end 이벤트에 result, summary, discoverHash가 포함된다', async () => {
59
+ const { appendLoopEvent } = await importLedger();
60
+ appendLoopEvent(tmpDir, {
61
+ loop: 'nightly',
62
+ event: 'end',
63
+ result: 'ok',
64
+ summary: '3 items processed',
65
+ discoverHash: 'abc123',
66
+ });
67
+
68
+ const p = historyPath(tmpDir);
69
+ const line = fs.readFileSync(p, 'utf-8').trim();
70
+ const obj = JSON.parse(line);
71
+ expect(obj.event).toBe('end');
72
+ expect(obj.result).toBe('ok');
73
+ expect(obj.summary).toBe('3 items processed');
74
+ expect(obj.discoverHash).toBe('abc123');
75
+ });
76
+
77
+ it('여러 이벤트가 순서대로 append 된다', async () => {
78
+ const { appendLoopEvent } = await importLedger();
79
+ appendLoopEvent(tmpDir, { loop: 'loop-a', event: 'start' });
80
+ appendLoopEvent(tmpDir, { loop: 'loop-a', event: 'end', result: 'ok' });
81
+ appendLoopEvent(tmpDir, { loop: 'loop-b', event: 'start' });
82
+
83
+ const p = historyPath(tmpDir);
84
+ const lines = fs.readFileSync(p, 'utf-8').trim().split('\n');
85
+ expect(lines).toHaveLength(3);
86
+ expect(JSON.parse(lines[0]).loop).toBe('loop-a');
87
+ expect(JSON.parse(lines[1]).event).toBe('end');
88
+ expect(JSON.parse(lines[2]).loop).toBe('loop-b');
89
+ });
90
+
91
+ it('appendLoopEvent는 true를 반환한다', async () => {
92
+ const { appendLoopEvent } = await importLedger();
93
+ const result = appendLoopEvent(tmpDir, { loop: 'x', event: 'start' });
94
+ expect(result).toBe(true);
95
+ });
96
+ });
97
+
98
+ // ─── isStuck ──────────────────────────────────────────────────────────
99
+
100
+ describe('loop-ledger: isStuck', () => {
101
+ it('이력 없으면 false', async () => {
102
+ const { isStuck } = await importLedger();
103
+ expect(isStuck(tmpDir, 'nightly', 'hash-abc')).toBe(false);
104
+ });
105
+
106
+ it('직전 discover 이벤트와 동일 hash → true (2회 연속 성립)', async () => {
107
+ const { appendLoopEvent, isStuck } = await importLedger();
108
+ // 이전 반복의 discover 이벤트 (같은 hash)
109
+ appendLoopEvent(tmpDir, {
110
+ loop: 'nightly',
111
+ event: 'discover',
112
+ discoverHash: 'hash-same',
113
+ });
114
+ // 신규 반복이 같은 hash를 갖고 오면 stuck
115
+ expect(isStuck(tmpDir, 'nightly', 'hash-same')).toBe(true);
116
+ });
117
+
118
+ it('직전 discover 이벤트와 다른 hash → false', async () => {
119
+ const { appendLoopEvent, isStuck } = await importLedger();
120
+ appendLoopEvent(tmpDir, {
121
+ loop: 'nightly',
122
+ event: 'discover',
123
+ discoverHash: 'hash-old',
124
+ });
125
+ expect(isStuck(tmpDir, 'nightly', 'hash-new')).toBe(false);
126
+ });
127
+
128
+ it('end 이벤트의 hash는 비교 대상이 아님 (discover만 본다)', async () => {
129
+ const { appendLoopEvent, isStuck } = await importLedger();
130
+ appendLoopEvent(tmpDir, {
131
+ loop: 'nightly',
132
+ event: 'end',
133
+ result: 'ok',
134
+ discoverHash: 'hash-same',
135
+ });
136
+ expect(isStuck(tmpDir, 'nightly', 'hash-same')).toBe(false);
137
+ });
138
+
139
+ it('다른 루프 이름의 이벤트는 영향 안 줌', async () => {
140
+ const { appendLoopEvent, isStuck } = await importLedger();
141
+ appendLoopEvent(tmpDir, {
142
+ loop: 'other-loop',
143
+ event: 'discover',
144
+ discoverHash: 'hash-same',
145
+ });
146
+ // 'nightly' 에 대한 이력 없음 → false
147
+ expect(isStuck(tmpDir, 'nightly', 'hash-same')).toBe(false);
148
+ });
149
+
150
+ it('start 이벤트만 있고 discover 없으면 false', async () => {
151
+ const { appendLoopEvent, isStuck } = await importLedger();
152
+ appendLoopEvent(tmpDir, { loop: 'nightly', event: 'start' });
153
+ expect(isStuck(tmpDir, 'nightly', 'hash-abc')).toBe(false);
154
+ });
155
+
156
+ it('discoverHash 가 빈 문자열이면 false', async () => {
157
+ const { appendLoopEvent, isStuck } = await importLedger();
158
+ appendLoopEvent(tmpDir, {
159
+ loop: 'nightly',
160
+ event: 'discover',
161
+ discoverHash: '',
162
+ });
163
+ expect(isStuck(tmpDir, 'nightly', '')).toBe(false);
164
+ });
165
+ });
166
+
167
+ // ─── fail-open: 손상된 jsonl 줄 ──────────────────────────────────────
168
+
169
+ describe('loop-ledger: fail-open on corrupt jsonl', () => {
170
+ it('손상된 줄이 있어도 isStuck이 충돌하지 않고 false 반환', async () => {
171
+ const { appendLoopEvent, isStuck } = await importLedger();
172
+
173
+ // 정상 줄 + 손상 줄 + 정상 줄 혼합
174
+ const p = historyPath(tmpDir);
175
+ fs.mkdirSync(path.dirname(p), { recursive: true });
176
+ fs.writeFileSync(
177
+ p,
178
+ [
179
+ JSON.stringify({ ts: '2026-01-01T00:00:00.000Z', loop: 'nightly', event: 'discover', discoverHash: 'abc' }),
180
+ 'CORRUPTED LINE {{{',
181
+ JSON.stringify({ ts: '2026-01-01T01:00:00.000Z', loop: 'nightly', event: 'discover', discoverHash: 'xyz' }),
182
+ ].join('\n') + '\n',
183
+ 'utf-8'
184
+ );
185
+
186
+ // 손상 줄은 건너뛰고, 가장 최근 discover 이벤트(최신)의 hash는 'xyz'
187
+ expect(isStuck(tmpDir, 'nightly', 'xyz')).toBe(true);
188
+ expect(isStuck(tmpDir, 'nightly', 'abc')).toBe(false);
189
+ });
190
+
191
+ it('appendLoopEvent: 전체 파일이 corrupt 여도 추가 후 정상 동작', async () => {
192
+ const { appendLoopEvent } = await importLedger();
193
+ const p = historyPath(tmpDir);
194
+ fs.mkdirSync(path.dirname(p), { recursive: true });
195
+ fs.writeFileSync(p, 'NOT JSON\n', 'utf-8');
196
+
197
+ const ok = appendLoopEvent(tmpDir, { loop: 'x', event: 'start' });
198
+ expect(ok).toBe(true);
199
+ const lines = fs.readFileSync(p, 'utf-8').split('\n').filter(Boolean);
200
+ // 손상 줄 + 새로 추가된 정상 줄
201
+ expect(lines.length).toBeGreaterThanOrEqual(2);
202
+ const last = JSON.parse(lines[lines.length - 1]);
203
+ expect(last.event).toBe('start');
204
+ });
205
+ });
206
+
207
+ // ─── hashDiscoverOutput ───────────────────────────────────────────────
208
+
209
+ describe('loop-ledger: hashDiscoverOutput', () => {
210
+ it('동일 텍스트는 동일 해시를 반환한다', async () => {
211
+ const { hashDiscoverOutput } = await importLedger();
212
+ const h1 = hashDiscoverOutput('hello world');
213
+ const h2 = hashDiscoverOutput('hello world');
214
+ expect(h1).toBe(h2);
215
+ });
216
+
217
+ it('공백 정규화: 여분 공백/줄바꿈이 달라도 동일 해시', async () => {
218
+ const { hashDiscoverOutput } = await importLedger();
219
+ const h1 = hashDiscoverOutput('item a\nitem b');
220
+ const h2 = hashDiscoverOutput('item a item b');
221
+ const h3 = hashDiscoverOutput(' item a item b ');
222
+ expect(h1).toBe(h2);
223
+ expect(h2).toBe(h3);
224
+ });
225
+
226
+ it('다른 텍스트는 다른 해시', async () => {
227
+ const { hashDiscoverOutput } = await importLedger();
228
+ expect(hashDiscoverOutput('abc')).not.toBe(hashDiscoverOutput('xyz'));
229
+ });
230
+
231
+ it('sha256 hex 64자 반환', async () => {
232
+ const { hashDiscoverOutput } = await importLedger();
233
+ expect(hashDiscoverOutput('test')).toMatch(/^[0-9a-f]{64}$/);
234
+ });
235
+ });
236
+
237
+ // ─── CLI: start / end / check-stuck ─────────────────────────────────
238
+
239
+ describe('loop-ledger CLI', () => {
240
+ it('start → exit 0, 확인 메시지 출력, jsonl 생성', () => {
241
+ const result = spawnSync('node', [CLI, 'start', 'nightly'], {
242
+ encoding: 'utf-8',
243
+ timeout: 5000,
244
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
245
+ });
246
+ expect(result.status).toBe(0);
247
+ expect(result.stdout).toContain('start recorded');
248
+ expect(result.stdout).toContain('nightly');
249
+ expect(fs.existsSync(historyPath(tmpDir))).toBe(true);
250
+ });
251
+
252
+ it('end → exit 0, jsonl에 end 이벤트 기록', () => {
253
+ const result = spawnSync('node', [CLI, 'end', 'nightly', 'ok', 'summary text'], {
254
+ encoding: 'utf-8',
255
+ timeout: 5000,
256
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
257
+ });
258
+ expect(result.status).toBe(0);
259
+ expect(result.stdout).toContain('end recorded');
260
+
261
+ const line = fs.readFileSync(historyPath(tmpDir), 'utf-8').trim();
262
+ const obj = JSON.parse(line);
263
+ expect(obj.event).toBe('end');
264
+ expect(obj.result).toBe('ok');
265
+ expect(obj.summary).toBe('summary text');
266
+ });
267
+
268
+ it('check-stuck: 직전 discover hash와 동일 → stdout "stuck"', async () => {
269
+ const { appendLoopEvent } = await importLedger();
270
+ appendLoopEvent(tmpDir, {
271
+ loop: 'nightly',
272
+ event: 'discover',
273
+ discoverHash: 'deadbeef',
274
+ });
275
+
276
+ const result = spawnSync('node', [CLI, 'check-stuck', 'nightly', 'deadbeef'], {
277
+ encoding: 'utf-8',
278
+ timeout: 5000,
279
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
280
+ });
281
+ expect(result.status).toBe(0);
282
+ expect(result.stdout.trim()).toBe('stuck');
283
+ });
284
+
285
+ it('check-stuck: 새 hash → stdout "ok"', async () => {
286
+ const { appendLoopEvent } = await importLedger();
287
+ appendLoopEvent(tmpDir, {
288
+ loop: 'nightly',
289
+ event: 'end',
290
+ result: 'ok',
291
+ discoverHash: 'old-hash',
292
+ });
293
+
294
+ const result = spawnSync('node', [CLI, 'check-stuck', 'nightly', 'new-hash'], {
295
+ encoding: 'utf-8',
296
+ timeout: 5000,
297
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
298
+ });
299
+ expect(result.status).toBe(0);
300
+ expect(result.stdout.trim()).toBe('ok');
301
+ });
302
+
303
+ it('check-stuck: 이력 없으면 "ok"', () => {
304
+ const result = spawnSync('node', [CLI, 'check-stuck', 'nightly', 'anyhash'], {
305
+ encoding: 'utf-8',
306
+ timeout: 5000,
307
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
308
+ });
309
+ expect(result.status).toBe(0);
310
+ expect(result.stdout.trim()).toBe('ok');
311
+ });
312
+
313
+ it('인수 없이 실행해도 exit 0 (fail-open)', () => {
314
+ const result = spawnSync('node', [CLI], {
315
+ encoding: 'utf-8',
316
+ timeout: 5000,
317
+ env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
318
+ });
319
+ expect(result.status).toBe(0);
320
+ });
321
+ });
@@ -8,20 +8,20 @@ import { VIBE_PATH, PROJECT_DIR } from './utils.js';
8
8
 
9
9
  // 매직 키워드 정의
10
10
  const MAGIC_KEYWORDS = {
11
- // 지속성 모드 (완료까지 계속)
11
+ // Deprecated: 기본 루프 동작과 동일 (no-op). exit=coverage-100으로 해석.
12
12
  ralph: {
13
- name: 'Ralph Loop',
14
- description: 'Continue until task is verified complete',
13
+ name: 'Ralph (deprecated alias)',
14
+ description: '[deprecated] Looping to convergence is the default; alias mapped',
15
15
  flags: ['persistence', 'verification'],
16
- output: '[RALPH MODE] Self-referential completion loop activated. Will continue until ALL tasks verified complete. NO early stopping.',
16
+ output: '[vibe] \'ralph\' is deprecated looping to convergence is the default; alias mapped.',
17
17
  },
18
18
 
19
- // 울트라워크 모드 (병렬 + 자동 계속)
19
+ // 울트라워크 모드 (automationLevel: autonomous + 병렬 ACT)
20
20
  ultrawork: {
21
21
  name: 'Ultrawork',
22
- description: 'Maximum parallel execution, no pause',
22
+ description: 'automationLevel: autonomous + parallel ACT (deprecated alias)',
23
23
  flags: ['parallel', 'auto_continue', 'no_confirmation'],
24
- output: '[ULTRAWORK MODE] Use PARALLEL Task calls. Auto-continue through ALL phases. Auto-retry on errors up to 3 times. Do NOT ask for confirmation between phases.',
24
+ output: '[ULTRAWORK] automationLevel: autonomous + parallel ACT. Loop runs to convergence; stuck auto-TODO (no confirmation).',
25
25
  },
26
26
  ulw: {
27
27
  alias: 'ultrawork',
@@ -47,12 +47,12 @@ const MAGIC_KEYWORDS = {
47
47
  output: '[RALPLAN MODE] Iterative planning with consensus. Will refine plan until approved, then execute with Ralph persistence.',
48
48
  },
49
49
 
50
- // 검증 모드
50
+ // Deprecated: 기본 JUDGE는 항상 결정론 검증 (no-op)
51
51
  verify: {
52
- name: 'Verify Mode',
53
- description: 'Strict verification after each step',
52
+ name: 'Verify (deprecated alias)',
53
+ description: '[deprecated] Deterministic verification is the default; alias mapped',
54
54
  flags: ['verification', 'strict'],
55
- output: '[VERIFY MODE] Strict verification enabled. Every change must be verified before proceeding.',
55
+ output: '[vibe] \'verify\' is deprecated deterministic JUDGE is the default; alias mapped.',
56
56
  strict: true, // 일상어 ("please verify the fix" 오탐 방지)
57
57
  },
58
58
 
@@ -65,29 +65,29 @@ const MAGIC_KEYWORDS = {
65
65
  strict: true, // 일상어 ("let me explore the options" 오탐 방지)
66
66
  },
67
67
 
68
- // 빠른 모드
68
+ // Deprecated: --max-iter 1 매핑
69
69
  quick: {
70
- name: 'Quick Mode',
71
- description: 'Fast execution, minimal verification',
70
+ name: 'Quick (deprecated alias)',
71
+ description: '[deprecated] Maps to --max-iter 1; use --max-iter 1 explicitly',
72
72
  flags: ['fast', 'minimal_verification'],
73
- output: '[QUICK MODE] Fast execution mode. Minimal verification, single round reviews.',
73
+ output: '[vibe] \'quick\' maps to --max-iter 1 (single-pass, minimal JUDGE).',
74
74
  strict: true, // 일상어 ("quick question on auth" 오탐 방지)
75
75
  },
76
76
  };
77
77
 
78
- // 키워드 조합 시너지
78
+ // 키워드 조합 시너지 (deprecated alias 조합도 매핑 유지)
79
79
  const KEYWORD_SYNERGIES = {
80
80
  'ralph+ultrawork': {
81
- name: 'Ralph Ultrawork',
82
- output: '[RALPH+ULTRAWORK] Maximum persistence AND parallel execution. Will NOT stop until ALL phases complete with verification.',
81
+ name: 'Ralph+Ultrawork (deprecated)',
82
+ output: '[vibe] \'ralph\'+\'ultrawork\' deprecated: automationLevel: autonomous + parallel ACT, exit=coverage-100.',
83
83
  },
84
84
  'ralph+verify': {
85
- name: 'Ralph Verify',
86
- output: '[RALPH+VERIFY] Persistent completion with strict verification at each step.',
85
+ name: 'Ralph+Verify (deprecated)',
86
+ output: '[vibe] \'ralph\'+\'verify\' deprecated: both are default behavior; alias mapped.',
87
87
  },
88
88
  'ultrawork+explore': {
89
- name: 'Ultrawork Explore',
90
- output: '[ULTRAWORK+EXPLORE] Parallel exploration agents for maximum coverage.',
89
+ name: 'Ultrawork+Explore',
90
+ output: '[ULTRAWORK+EXPLORE] automationLevel: autonomous + parallel exploration agents.',
91
91
  },
92
92
  };
93
93
 
@@ -19,6 +19,26 @@ import { pathToFileURL } from 'url';
19
19
  /** stdin을 EOF까지 읽을 때 허용하는 최대 바이트 수 (10MB). */
20
20
  const STDIN_MAX_BYTES = 10 * 1024 * 1024;
21
21
 
22
+ /** EAGAIN 재시도 간격/데드라인 — non-blocking pipe에서 writer가 아직 flush 전일 수 있다. */
23
+ const STDIN_EAGAIN_RETRY_MS = 5;
24
+ const STDIN_EAGAIN_DEADLINE_MS = 500;
25
+
26
+ /** 동기 sleep (Atomics.wait — 이벤트 루프 없이 대기). */
27
+ const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
28
+ function sleepSync(ms) {
29
+ Atomics.wait(sleepBuf, 0, 0, ms);
30
+ }
31
+
32
+ /** 누적 청크가 완전한 JSON인지 검사 — EAGAIN 시 조기 종료 판단용. */
33
+ function isCompleteJson(chunks) {
34
+ try {
35
+ JSON.parse(Buffer.concat(chunks).toString('utf-8'));
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
22
42
  /**
23
43
  * stdin에서 JSON 페이로드 동기 읽기 (Claude Code 하네스 호환).
24
44
  * fd 0 직접 사용 — Windows는 '/dev/stdin'이 없음.
@@ -37,16 +57,25 @@ export function readStdinSync() {
37
57
  const chunkSize = 65536;
38
58
  const chunkBuf = Buffer.alloc(chunkSize);
39
59
 
60
+ let lastProgress = Date.now();
40
61
  while (true) {
41
62
  let bytesRead;
42
63
  try {
43
64
  bytesRead = fs.readSync(0, chunkBuf, 0, chunkSize, null);
44
65
  } catch (err) {
45
- // EAGAIN: non-blocking stdin에 데이터 없음 루프 종료
46
- if (err.code === 'EAGAIN') break;
66
+ // EAGAIN: 파이프가 *지금* 비었을 EOF가 아닐 수 있다 (writer가 flush 전).
67
+ // EOF로 취급하면 대용량 페이로드가 중간에 끊겨 가드가 fail-open된다 —
68
+ // 완전한 JSON이 모였거나 데드라인이 지나기 전까지 재시도한다.
69
+ if (err.code === 'EAGAIN') {
70
+ if (chunks.length > 0 && isCompleteJson(chunks)) break;
71
+ if (Date.now() - lastProgress > STDIN_EAGAIN_DEADLINE_MS) break;
72
+ sleepSync(STDIN_EAGAIN_RETRY_MS);
73
+ continue;
74
+ }
47
75
  throw err;
48
76
  }
49
77
  if (bytesRead === 0) break;
78
+ lastProgress = Date.now();
50
79
  totalBytes += bytesRead;
51
80
  if (totalBytes > STDIN_MAX_BYTES) {
52
81
  truncated = true;