@su-record/vibe 3.1.0 → 3.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.
Files changed (183) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.en.md +9 -6
  3. package/README.md +9 -4
  4. package/dist/__tests__/wiring-integrity.test.d.ts +2 -0
  5. package/dist/__tests__/wiring-integrity.test.d.ts.map +1 -0
  6. package/dist/__tests__/wiring-integrity.test.js +160 -0
  7. package/dist/__tests__/wiring-integrity.test.js.map +1 -0
  8. package/dist/cli/commands/info.d.ts.map +1 -1
  9. package/dist/cli/commands/info.js +2 -1
  10. package/dist/cli/commands/info.js.map +1 -1
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +4 -2
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/design/design-md-parser.test.js +2 -2
  15. package/dist/cli/design/design-md-parser.test.js.map +1 -1
  16. package/dist/cli/index.js +102 -128
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
  19. package/dist/cli/postinstall/claude-agents.js +0 -5
  20. package/dist/cli/postinstall/claude-agents.js.map +1 -1
  21. package/dist/cli/postinstall/constants.d.ts +6 -0
  22. package/dist/cli/postinstall/constants.d.ts.map +1 -1
  23. package/dist/cli/postinstall/constants.js +42 -60
  24. package/dist/cli/postinstall/constants.js.map +1 -1
  25. package/dist/cli/postinstall/inline-skills.js +2 -2
  26. package/dist/cli/postinstall/inline-skills.js.map +1 -1
  27. package/dist/cli/postinstall/main.d.ts.map +1 -1
  28. package/dist/cli/postinstall/main.js +2 -0
  29. package/dist/cli/postinstall/main.js.map +1 -1
  30. package/dist/cli/postinstall.d.ts +6 -1
  31. package/dist/cli/postinstall.d.ts.map +1 -1
  32. package/dist/cli/postinstall.js +6 -1
  33. package/dist/cli/postinstall.js.map +1 -1
  34. package/dist/cli/setup/GlobalInstaller.d.ts +1 -15
  35. package/dist/cli/setup/GlobalInstaller.d.ts.map +1 -1
  36. package/dist/cli/setup/GlobalInstaller.js +16 -115
  37. package/dist/cli/setup/GlobalInstaller.js.map +1 -1
  38. package/dist/cli/setup/GlobalInstaller.test.d.ts +2 -0
  39. package/dist/cli/setup/GlobalInstaller.test.d.ts.map +1 -0
  40. package/dist/cli/setup/GlobalInstaller.test.js +16 -0
  41. package/dist/cli/setup/GlobalInstaller.test.js.map +1 -0
  42. package/dist/cli/setup/ProjectSetup.js +2 -2
  43. package/dist/cli/setup/ProjectSetup.js.map +1 -1
  44. package/dist/cli/setup/Provisioner.d.ts.map +1 -1
  45. package/dist/cli/setup/Provisioner.js +23 -7
  46. package/dist/cli/setup/Provisioner.js.map +1 -1
  47. package/dist/cli/setup.d.ts +1 -1
  48. package/dist/cli/setup.d.ts.map +1 -1
  49. package/dist/cli/setup.js +1 -1
  50. package/dist/cli/setup.js.map +1 -1
  51. package/dist/cli/utils/cli-detector.d.ts +3 -3
  52. package/dist/cli/utils/cli-detector.d.ts.map +1 -1
  53. package/dist/cli/utils/cli-detector.js +7 -27
  54. package/dist/cli/utils/cli-detector.js.map +1 -1
  55. package/dist/cli/utils/cli-detector.test.js +3 -2
  56. package/dist/cli/utils/cli-detector.test.js.map +1 -1
  57. package/dist/infra/lib/SkillRepository.js +1 -1
  58. package/dist/infra/lib/evolution/__tests__/integration.test.js +0 -42
  59. package/dist/infra/lib/evolution/__tests__/integration.test.js.map +1 -1
  60. package/dist/infra/lib/llm-availability.d.ts.map +1 -1
  61. package/dist/infra/lib/llm-availability.js +2 -10
  62. package/dist/infra/lib/llm-availability.js.map +1 -1
  63. package/dist/infra/lib/llm-availability.test.js +3 -2
  64. package/dist/infra/lib/llm-availability.test.js.map +1 -1
  65. package/dist/infra/lib/utils.d.ts +6 -0
  66. package/dist/infra/lib/utils.d.ts.map +1 -1
  67. package/dist/infra/lib/utils.js +33 -0
  68. package/dist/infra/lib/utils.js.map +1 -1
  69. package/dist/tools/convention/validateCodeQuality.d.ts +1 -0
  70. package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
  71. package/dist/tools/convention/validateCodeQuality.js +3 -2
  72. package/dist/tools/convention/validateCodeQuality.js.map +1 -1
  73. package/dist/tools/index.d.ts +2 -2
  74. package/dist/tools/index.d.ts.map +1 -1
  75. package/dist/tools/index.js +2 -0
  76. package/dist/tools/index.js.map +1 -1
  77. package/dist/tools/spec/executionPacket.d.ts +101 -0
  78. package/dist/tools/spec/executionPacket.d.ts.map +1 -0
  79. package/dist/tools/spec/executionPacket.js +394 -0
  80. package/dist/tools/spec/executionPacket.js.map +1 -0
  81. package/dist/tools/spec/executionPacket.test.d.ts +2 -0
  82. package/dist/tools/spec/executionPacket.test.d.ts.map +1 -0
  83. package/dist/tools/spec/executionPacket.test.js +360 -0
  84. package/dist/tools/spec/executionPacket.test.js.map +1 -0
  85. package/dist/tools/spec/index.d.ts +2 -0
  86. package/dist/tools/spec/index.d.ts.map +1 -1
  87. package/dist/tools/spec/index.js +2 -0
  88. package/dist/tools/spec/index.js.map +1 -1
  89. package/dist/tools/spec/specGenerator.d.ts +12 -0
  90. package/dist/tools/spec/specGenerator.d.ts.map +1 -1
  91. package/dist/tools/spec/specGenerator.js +96 -14
  92. package/dist/tools/spec/specGenerator.js.map +1 -1
  93. package/dist/tools/spec/specGenerator.test.d.ts +2 -0
  94. package/dist/tools/spec/specGenerator.test.d.ts.map +1 -0
  95. package/dist/tools/spec/specGenerator.test.js +130 -0
  96. package/dist/tools/spec/specGenerator.test.js.map +1 -0
  97. package/hooks/scripts/__tests__/.vibe/command-log.txt +3 -3
  98. package/hooks/scripts/__tests__/clone-behaviors.test.js +33 -1
  99. package/hooks/scripts/__tests__/code-check-memory-write.test.js +81 -0
  100. package/hooks/scripts/__tests__/llm-orchestrate-antigravity.test.js +5 -2
  101. package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +7 -2
  102. package/hooks/scripts/__tests__/run-ledger.test.js +161 -10
  103. package/hooks/scripts/__tests__/step-counter.test.js +21 -0
  104. package/hooks/scripts/__tests__/utils-npm-root.test.js +5 -4
  105. package/hooks/scripts/auto-format.js +2 -11
  106. package/hooks/scripts/auto-test.js +2 -11
  107. package/hooks/scripts/clone-extract.js +146 -10
  108. package/hooks/scripts/code-check.js +27 -55
  109. package/hooks/scripts/lib/glob.js +32 -0
  110. package/hooks/scripts/lib/hook-context.js +19 -2
  111. package/hooks/scripts/lib/run-ledger.js +123 -21
  112. package/hooks/scripts/post-edit.js +2 -3
  113. package/hooks/scripts/prompt-dispatcher.js +1 -1
  114. package/hooks/scripts/scope-guard.js +3 -42
  115. package/hooks/scripts/sentinel-guard.js +5 -20
  116. package/hooks/scripts/session-start.js +2 -2
  117. package/hooks/scripts/step-counter.js +35 -57
  118. package/hooks/scripts/verify-ledger.js +23 -3
  119. package/package.json +2 -3
  120. package/skills/clone/SKILL.md +10 -216
  121. package/skills/clone/references/capture-rules.md +48 -0
  122. package/skills/clone/references/legal-and-error-recovery.md +40 -0
  123. package/skills/clone/references/refine-rules.md +61 -0
  124. package/skills/clone/references/scaffold-phases.md +59 -0
  125. package/skills/clone/references/setup-and-layout.md +30 -0
  126. package/skills/docs/SKILL.md +39 -4
  127. package/skills/docs/references/api-docs-changelog.md +34 -0
  128. package/skills/docs/references/codemaps-output.md +23 -0
  129. package/{agents/diagrammer.md → skills/docs/references/diagram-spec.md} +2 -15
  130. package/skills/exec-plan/SKILL.md +1 -1
  131. package/skills/exec-plan/templates/plan.md +1 -1
  132. package/skills/handoff/SKILL.md +3 -3
  133. package/skills/handoff/templates/handoff.md +1 -1
  134. package/skills/spec/SKILL.md +7 -0
  135. package/skills/vibe/SKILL.md +3 -2
  136. package/skills/vibe.analyze/SKILL.md +12 -249
  137. package/skills/vibe.analyze/references/output-templates.md +188 -0
  138. package/skills/vibe.analyze/references/quality-gate.md +93 -0
  139. package/skills/vibe.clone/SKILL.md +14 -1
  140. package/skills/vibe.continue/SKILL.md +32 -0
  141. package/skills/vibe.design/SKILL.md +23 -1
  142. package/skills/vibe.design/references/ui-preview.md +20 -0
  143. package/skills/vibe.docs/SKILL.md +4 -2
  144. package/skills/vibe.figma/SKILL.md +33 -260
  145. package/skills/vibe.figma/references/branch-phases.md +113 -0
  146. package/skills/vibe.figma/references/state-schema.md +42 -0
  147. package/skills/vibe.figma/references/step-algorithms.md +204 -0
  148. package/skills/vibe.image/SKILL.md +62 -0
  149. package/skills/vibe.image/references/image-generation-examples.md +39 -0
  150. package/skills/vibe.reason/SKILL.md +3 -183
  151. package/skills/vibe.reason/references/output-format-template.md +57 -0
  152. package/skills/vibe.reason/references/quality-rubrics.md +94 -0
  153. package/skills/vibe.reason/references/worked-example.md +45 -0
  154. package/skills/vibe.review/SKILL.md +13 -348
  155. package/skills/vibe.review/references/boundary-check.md +25 -0
  156. package/skills/vibe.review/references/output-template.md +74 -0
  157. package/skills/vibe.review/references/quality-gate.md +84 -0
  158. package/skills/vibe.review/references/race-mode.md +117 -0
  159. package/skills/vibe.review/references/worked-examples.md +94 -0
  160. package/skills/vibe.run/SKILL.md +29 -6
  161. package/skills/vibe.trace/SKILL.md +3 -3
  162. package/skills/vibe.verify/SKILL.md +10 -4
  163. package/vibe/rules/loop-contract.md +7 -3
  164. package/vibe/templates/claudemd-template.md +1 -1
  165. package/vibe/templates/spec-template.md +32 -5
  166. package/agents/acceptance-tester.md +0 -56
  167. package/agents/documenter.md +0 -43
  168. package/agents/figma/figma-engineer.md +0 -76
  169. package/dist/cli/postinstall/index.d.ts +0 -23
  170. package/dist/cli/postinstall/index.d.ts.map +0 -1
  171. package/dist/cli/postinstall/index.js +0 -23
  172. package/dist/cli/postinstall/index.js.map +0 -1
  173. package/dist/cli/setup/index.d.ts +0 -9
  174. package/dist/cli/setup/index.d.ts.map +0 -1
  175. package/dist/cli/setup/index.js +0 -12
  176. package/dist/cli/setup/index.js.map +0 -1
  177. package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
  178. package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
  179. package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
  180. package/hooks/scripts/evolution-engine.js +0 -91
  181. package/hooks/scripts/hud-status.js +0 -321
  182. package/hooks/scripts/skill-injector.js +0 -83
  183. package/skills/vibe.utils/SKILL.md +0 -415
@@ -18,6 +18,11 @@ import { execFileSync, spawnSync } from 'child_process';
18
18
  import { fileURLToPath } from 'url';
19
19
 
20
20
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const PASS_RESULTS = [{ command: 'npm test', exitCode: 0 }];
22
+
23
+ function verifyOptions(ledger, results = PASS_RESULTS) {
24
+ return { runId: ledger.runId, verificationResults: results };
25
+ }
21
26
 
22
27
  // 테스트 격리용 임시 디렉토리
23
28
  let tmpDir;
@@ -59,7 +64,7 @@ describe('run-ledger: round-trip', () => {
59
64
  it('recordRunStart는 verifyPassed를 false로 리셋', async () => {
60
65
  const { recordRunStart, recordVerify, readLedger } = await importLedger();
61
66
  recordRunStart(tmpDir, 'f1');
62
- recordVerify(tmpDir, true);
67
+ recordVerify(tmpDir, true, verifyOptions(readLedger(tmpDir)));
63
68
  // 두 번째 run — 리셋
64
69
  recordRunStart(tmpDir, 'f2');
65
70
  const ledger = readLedger(tmpDir);
@@ -72,7 +77,7 @@ describe('run-ledger: round-trip', () => {
72
77
  const { recordRunStart, recordVerify, readLedger } = await importLedger();
73
78
  recordRunStart(tmpDir, 'feat');
74
79
  const before = new Date().toISOString();
75
- recordVerify(tmpDir, true);
80
+ recordVerify(tmpDir, true, verifyOptions(readLedger(tmpDir)));
76
81
  const ledger = readLedger(tmpDir);
77
82
  expect(ledger.verifyPassed).toBe(true);
78
83
  expect(ledger.verifyAt).toBeDefined();
@@ -82,10 +87,133 @@ describe('run-ledger: round-trip', () => {
82
87
  it('recordVerify(fail) → verifyPassed=false', async () => {
83
88
  const { recordRunStart, recordVerify, readLedger } = await importLedger();
84
89
  recordRunStart(tmpDir, 'feat');
85
- recordVerify(tmpDir, false);
90
+ recordVerify(tmpDir, false, verifyOptions(readLedger(tmpDir), []));
86
91
  const ledger = readLedger(tmpDir);
87
92
  expect(ledger.verifyPassed).toBe(false);
88
93
  });
94
+
95
+ it('recordVerify → 안정된 runId 경로에 evidence.json 원자 기록', async () => {
96
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
97
+ recordRunStart(tmpDir, 'feat');
98
+ const started = readLedger(tmpDir);
99
+ fs.mkdirSync(path.join(tmpDir, '.vibe', 'specs'), { recursive: true });
100
+ fs.writeFileSync(path.join(tmpDir, '.vibe', 'specs', 'feat.md'), '# SPEC');
101
+ expect(recordVerify(tmpDir, true, verifyOptions(started))).toBe(true);
102
+
103
+ const after = readLedger(tmpDir);
104
+ expect(after.runId).toBe(started.runId);
105
+ const evidencePath = path.join(tmpDir, '.vibe', 'runs', after.runId, 'evidence.json');
106
+ expect(fs.existsSync(evidencePath)).toBe(true);
107
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
108
+ expect(evidence).toMatchObject({
109
+ schemaVersion: '1.0.0',
110
+ runId: after.runId,
111
+ specPath: '.vibe/specs/feat.md',
112
+ judges: {
113
+ deterministic: { authority: 'blocking', verifyPassed: true },
114
+ model: { authority: 'advisory-only', canComplete: false },
115
+ humanTaste: { authority: 'release-only', canComplete: false },
116
+ },
117
+ });
118
+ });
119
+
120
+ it('레거시 ledger도 runId를 보강하고 누락된 검증 결과는 빈 배열로 기록', async () => {
121
+ const p = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
122
+ fs.mkdirSync(path.dirname(p), { recursive: true });
123
+ fs.writeFileSync(p, JSON.stringify({ runStarted: '2026-01-01T00:00:00.000Z' }));
124
+ const { recordVerify, readLedger } = await importLedger();
125
+ expect(recordVerify(tmpDir, false)).toBe(true);
126
+ const ledger = readLedger(tmpDir);
127
+ const evidencePath = path.join(tmpDir, '.vibe', 'runs', ledger.runId, 'evidence.json');
128
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
129
+ expect(evidence.specPath).toBeNull();
130
+ expect(evidence.judges.deterministic.verificationResults).toEqual([]);
131
+ });
132
+
133
+ it('기존 verificationResults의 command와 exitCode를 evidence에 보존', async () => {
134
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
135
+ recordRunStart(tmpDir, 'feat');
136
+ const p = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
137
+ const ledger = readLedger(tmpDir);
138
+ fs.writeFileSync(p, JSON.stringify({
139
+ ...ledger,
140
+ verificationResults: [{ command: 'npm test', exitCode: 0 }],
141
+ }));
142
+ recordVerify(tmpDir, true, verifyOptions(ledger, [
143
+ { command: 'npm test', exitCode: 0 },
144
+ ]));
145
+ const evidencePath = path.join(tmpDir, '.vibe', 'runs', ledger.runId, 'evidence.json');
146
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
147
+ expect(evidence.judges.deterministic.verificationResults)
148
+ .toEqual([{ command: 'npm test', exitCode: 0 }]);
149
+ });
150
+
151
+ it('새 run은 이전 run의 검증 결과를 제거', async () => {
152
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
153
+ recordRunStart(tmpDir, 'first');
154
+ const first = readLedger(tmpDir);
155
+ recordVerify(tmpDir, true, verifyOptions(first, [{ command: 'old-test', exitCode: 0 }]));
156
+
157
+ recordRunStart(tmpDir, 'second');
158
+ const second = readLedger(tmpDir);
159
+ expect(second.verificationResults).toBeUndefined();
160
+ recordVerify(tmpDir, true, verifyOptions(second));
161
+ const evidencePath = path.join(tmpDir, '.vibe', 'runs', second.runId, 'evidence.json');
162
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
163
+ expect(evidence.judges.deterministic.verificationResults).toEqual(PASS_RESULTS);
164
+ });
165
+
166
+ it('조작된 runId는 프로젝트 밖에 evidence를 기록하지 않음', async () => {
167
+ const ledgerPath = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
168
+ const escapedDir = path.join(path.dirname(tmpDir), `escaped-${path.basename(tmpDir)}`);
169
+ fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
170
+ fs.writeFileSync(ledgerPath, JSON.stringify({
171
+ runId: `../../../${path.basename(escapedDir)}`,
172
+ runStarted: new Date().toISOString(),
173
+ verifyPassed: false,
174
+ }));
175
+ const { recordVerify } = await importLedger();
176
+ expect(recordVerify(tmpDir, true, {
177
+ runId: `../../../${path.basename(escapedDir)}`,
178
+ verificationResults: PASS_RESULTS,
179
+ })).toBe(false);
180
+ expect(fs.existsSync(path.join(escapedDir, 'evidence.json'))).toBe(false);
181
+ });
182
+
183
+ it('evidence 기록 실패 시 ledger를 통과 상태로 바꾸지 않음', async () => {
184
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
185
+ recordRunStart(tmpDir, 'feat');
186
+ const ledger = readLedger(tmpDir);
187
+ const runPath = path.join(tmpDir, '.vibe', 'runs', ledger.runId);
188
+ fs.mkdirSync(path.dirname(runPath), { recursive: true });
189
+ fs.writeFileSync(runPath, 'occupied');
190
+
191
+ expect(recordVerify(tmpDir, true, verifyOptions(ledger))).toBe(false);
192
+ expect(readLedger(tmpDir).verifyPassed).toBe(false);
193
+ });
194
+
195
+ it('다른 runId의 verify 결과를 현재 run에 적용하지 않음', async () => {
196
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
197
+ recordRunStart(tmpDir, 'feat');
198
+ expect(recordVerify(tmpDir, true, {
199
+ runId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
200
+ verificationResults: PASS_RESULTS,
201
+ })).toBe(false);
202
+ expect(readLedger(tmpDir).verifyPassed).toBe(false);
203
+ });
204
+
205
+ it('분할 SPEC의 _index.md를 evidence 경로로 기록', async () => {
206
+ const { recordRunStart, recordVerify, readLedger } = await importLedger();
207
+ const specDir = path.join(tmpDir, '.vibe', 'specs', 'large-feature');
208
+ fs.mkdirSync(specDir, { recursive: true });
209
+ fs.writeFileSync(path.join(specDir, '_index.md'), '# Master SPEC');
210
+ recordRunStart(tmpDir, 'large-feature');
211
+ const ledger = readLedger(tmpDir);
212
+ recordVerify(tmpDir, true, verifyOptions(ledger));
213
+ const evidencePath = path.join(tmpDir, '.vibe', 'runs', ledger.runId, 'evidence.json');
214
+ const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
215
+ expect(evidence.specPath).toBe('.vibe/specs/large-feature/_index.md');
216
+ });
89
217
  });
90
218
 
91
219
  // ──────────────────────────────────────────────────────────────────
@@ -101,7 +229,7 @@ describe('run-ledger: verifyAt > runStarted', () => {
101
229
  const patched = { ...ledger, runStarted: new Date(Date.now() - 1000).toISOString() };
102
230
  fs.writeFileSync(p, JSON.stringify(patched), 'utf-8');
103
231
 
104
- recordVerify(tmpDir, true);
232
+ recordVerify(tmpDir, true, verifyOptions(readLedger(tmpDir)));
105
233
  const after = readLedger(tmpDir);
106
234
  expect(after.verifyAt > after.runStarted).toBe(true);
107
235
  expect(after.verifyPassed).toBe(true);
@@ -170,6 +298,12 @@ describe('run-ledger: vibe.run 감지 헬퍼', () => {
170
298
  expect(extractRunFeature('$vibe.run auth-login ultrawork')).toBe('auth-login');
171
299
  });
172
300
 
301
+ it('extractRunFeature: 따옴표를 제거해 SPEC 경로용 기능명을 반환', async () => {
302
+ const { extractRunFeature } = await importLedger();
303
+ expect(extractRunFeature('/vibe.run "auth-login"')).toBe('auth-login');
304
+ expect(extractRunFeature("$vibe.run 'auth-login'")).toBe('auth-login');
305
+ });
306
+
173
307
  it('extractRunFeature: 기능명 없으면 null', async () => {
174
308
  const { extractRunFeature } = await importLedger();
175
309
  expect(extractRunFeature('/vibe.run --phase 1')).toBeNull();
@@ -200,11 +334,17 @@ describe('run-ledger: stopWarned', () => {
200
334
  describe('verify-ledger CLI', () => {
201
335
  const CLI = path.resolve(__dirname, '..', 'verify-ledger.js');
202
336
 
337
+ function cliArgs(status, ledger, results = PASS_RESULTS) {
338
+ const resultsPath = path.join(tmpDir, 'verification-results.json');
339
+ fs.writeFileSync(resultsPath, JSON.stringify(results));
340
+ return [CLI, status, ledger.runId, resultsPath];
341
+ }
342
+
203
343
  it('pass 인자 → verifyPassed=true, exit 0', async () => {
204
- const { recordRunStart } = await importLedger();
344
+ const { recordRunStart, readLedger } = await importLedger();
205
345
  recordRunStart(tmpDir, 'feat');
206
346
 
207
- const result = spawnSync('node', [CLI, 'pass'], {
347
+ const result = spawnSync('node', cliArgs('pass', readLedger(tmpDir)), {
208
348
  encoding: 'utf-8',
209
349
  timeout: 5000,
210
350
  env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
@@ -212,16 +352,15 @@ describe('verify-ledger CLI', () => {
212
352
  expect(result.status).toBe(0);
213
353
  expect(result.stdout).toContain('verifyPassed=true');
214
354
 
215
- const { readLedger } = await importLedger();
216
355
  const ledger = readLedger(tmpDir);
217
356
  expect(ledger.verifyPassed).toBe(true);
218
357
  });
219
358
 
220
359
  it('fail 인자 → verifyPassed=false, exit 0', async () => {
221
- const { recordRunStart } = await importLedger();
360
+ const { recordRunStart, readLedger } = await importLedger();
222
361
  recordRunStart(tmpDir, 'feat');
223
362
 
224
- const result = spawnSync('node', [CLI, 'fail'], {
363
+ const result = spawnSync('node', cliArgs('fail', readLedger(tmpDir), []), {
225
364
  encoding: 'utf-8',
226
365
  timeout: 5000,
227
366
  env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
@@ -229,7 +368,6 @@ describe('verify-ledger CLI', () => {
229
368
  expect(result.status).toBe(0);
230
369
  expect(result.stdout).toContain('verifyPassed=false');
231
370
 
232
- const { readLedger } = await importLedger();
233
371
  const ledger = readLedger(tmpDir);
234
372
  expect(ledger.verifyPassed).toBe(false);
235
373
  });
@@ -242,6 +380,19 @@ describe('verify-ledger CLI', () => {
242
380
  });
243
381
  expect(result.status).toBe(0);
244
382
  });
383
+
384
+ it('기록 실패는 exit 0을 유지하되 stderr 경고로 노출', () => {
385
+ const projectFile = path.join(tmpDir, 'not-a-directory');
386
+ fs.writeFileSync(projectFile, 'occupied');
387
+ const result = spawnSync('node', [CLI, 'pass'], {
388
+ encoding: 'utf-8',
389
+ timeout: 5000,
390
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectFile },
391
+ });
392
+ expect(result.status).toBe(0);
393
+ expect(result.stderr).toContain('evidence.json write failed');
394
+ expect(result.stdout).not.toContain('recorded:');
395
+ });
245
396
  });
246
397
 
247
398
  // ──────────────────────────────────────────────────────────────────
@@ -435,4 +435,25 @@ describe('step-counter PostToolUse hook', () => {
435
435
  expect(files).toHaveLength(1);
436
436
  });
437
437
  });
438
+
439
+ // ───────── A2/D2: 64KB 초과 stdin 페이로드 (구 단일 버퍼 리더 절단 버그 회귀) ─────────
440
+ describe('stdin 64KB 초과 페이로드 — 절단 없이 처리', () => {
441
+ it('페이로드가 64KB를 넘어도 JSON 전체를 읽어 target_file을 정확히 기록', () => {
442
+ // 구현이 64KB 단일 버퍼로 되돌아가면 JSON이 중간에 잘려 파싱 실패 →
443
+ // stdinPayload=null → jsonl 라인 자체가 기록되지 않는다.
444
+ const padding = 'x'.repeat(80000); // 총 페이로드 > 64KB 보장
445
+ runCounter({
446
+ payload: {
447
+ tool_name: 'Edit',
448
+ tool_input: { file_path: 'src/foo.ts', padding },
449
+ tool_response: {},
450
+ },
451
+ projectDir,
452
+ });
453
+ const lines = readJsonl(runJsonl);
454
+ expect(lines).toHaveLength(1);
455
+ expect(lines[0].target_file).toBe('src/foo.ts');
456
+ expect(lines[0].tool).toBe('Edit');
457
+ });
458
+ });
438
459
  });
@@ -15,10 +15,11 @@ import { spawnSync } from 'child_process';
15
15
  import fs from 'fs';
16
16
  import os from 'os';
17
17
  import path from 'path';
18
- import { fileURLToPath } from 'url';
18
+ import { fileURLToPath, pathToFileURL } from 'url';
19
19
 
20
20
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
- const UTILS_PATH = path.resolve(__dirname, '..', 'utils.js');
21
+ // Windows 절대경로(C:\...)는 ESM import 지정자가 될 수 없으므로 file:// URL로 변환
22
+ const UTILS_URL = pathToFileURL(path.resolve(__dirname, '..', 'utils.js')).href;
22
23
 
23
24
  function makeTempCacheFile() {
24
25
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-npm-root-test-'));
@@ -30,8 +31,8 @@ function makeTempCacheFile() {
30
31
  * VIBE_NPM_ROOT_CACHE_FILE 환경 변수로 캐시 파일 경로를 주입한다.
31
32
  */
32
33
  function runGetNpmRoot(cacheFilePath) {
33
- return spawnSync('node', ['--input-type=module', '--eval',
34
- `import { getGlobalNpmPath } from '${UTILS_PATH}';
34
+ return spawnSync(process.execPath, ['--input-type=module', '--eval',
35
+ `import { getGlobalNpmPath } from '${UTILS_URL}';
35
36
  process.stdout.write(getGlobalNpmPath() || '');`
36
37
  ], {
37
38
  encoding: 'utf-8',
@@ -24,15 +24,6 @@ const PYTHON_EXT_RE = /\.py$/;
24
24
  const GO_EXT_RE = /\.go$/;
25
25
  const FORMAT_TIMEOUT_MS = 5000;
26
26
 
27
- function getFilePath(ctx) {
28
- try {
29
- const input = JSON.parse(ctx.toolInput || '{}');
30
- return input.file_path || input.path || '';
31
- } catch {
32
- return '';
33
- }
34
- }
35
-
36
27
  // PATH 직접 스캔 — `which` execSync는 매 파일 저장마다 자식 프로세스를 동기
37
28
  // spawn하므로, fs.existsSync로 대체하고 프로세스 내 캐싱한다.
38
29
  const _binCache = new Map();
@@ -109,13 +100,13 @@ async function formatFile(filePath) {
109
100
 
110
101
  /**
111
102
  * in-process 진입점 — 포맷 실행. finding 문자열 배열 반환.
112
- * @param {{ toolInput: string }} ctx
103
+ * @param {{ filePath: string }} ctx
113
104
  * @returns {Promise<{ exitCode: number, findings: string[] }>}
114
105
  */
115
106
  export async function run(ctx) {
116
107
  const findings = [];
117
108
  try {
118
- const filePath = getFilePath(ctx);
109
+ const filePath = ctx.filePath;
119
110
  if (filePath) {
120
111
  const finding = await formatFile(filePath);
121
112
  if (finding) findings.push(finding);
@@ -33,15 +33,6 @@ const DEBOUNCE_COOLDOWN_MS = 120_000;
33
33
  /** debounce 상태 파일 경로 */
34
34
  const DEBOUNCE_STATE_FILE = path.join(PROJECT_DIR, '.vibe', 'metrics', 'auto-test-state.json');
35
35
 
36
- function getFilePath(ctx) {
37
- try {
38
- const input = JSON.parse(ctx.toolInput || '{}');
39
- return input.file_path || input.path || '';
40
- } catch {
41
- return '';
42
- }
43
- }
44
-
45
36
  function isTestFile(filePath) {
46
37
  return TEST_SUFFIXES.some(s => filePath.includes(s));
47
38
  }
@@ -163,13 +154,13 @@ function updateDebounceState(testFile, srcFile) {
163
154
 
164
155
  /**
165
156
  * in-process 진입점 — 테스트 실행. findings 반환.
166
- * @param {{ toolInput: string }} ctx
157
+ * @param {{ filePath: string }} ctx
167
158
  * @returns {Promise<{ exitCode: number, findings: string[] }>}
168
159
  */
169
160
  export async function run(ctx) {
170
161
  const findings = [];
171
162
  try {
172
- const filePath = getFilePath(ctx);
163
+ const filePath = ctx.filePath;
173
164
  if (!filePath || !CODE_EXT_RE.test(filePath)) return { exitCode: 0, findings };
174
165
 
175
166
  const mode = getTestMode();
@@ -78,6 +78,11 @@ const CSS_PROPS = [
78
78
  'mask-image', 'mask-size', 'mask-position', 'clip-path',
79
79
  ];
80
80
 
81
+ const SUB_URL_LIMIT = 200;
82
+ const SUB_EXCLUDE_TEXT_RE = /^(?:검색|search|top|home|kor|eng|chn|vtn|language|언어|개인정보|privacy|문의|제안|sitemap|사이트맵|youtube|뉴스룸|newsroom|cookie|닫기|partner|파트너)$/i;
83
+ const SUB_EXCLUDE_PATH_RE = /\/(?:search|sitemap|privacy|contact|inquiry|cookie|login|auth|policy)(?:\/|$)/i;
84
+ const SUB_EXCLUDE_EXT_RE = /\.(?:pdf|zip|jpe?g|png|gif|webp|svg|mp4|mov|avi|docx?|xlsx?|pptx?)$/i;
85
+
81
86
  // ─── CLI parse ──────────────────────────────────────────────────────
82
87
  function parseArgs(argv) {
83
88
  const [, , cmd, urlArg, ...rest] = argv;
@@ -104,6 +109,74 @@ function parseViewport(v) {
104
109
  };
105
110
  }
106
111
 
112
+ function localePrefixOf(rawUrl) {
113
+ const first = new URL(rawUrl).pathname.split('/').filter(Boolean)[0];
114
+ if (!first || !/^[a-z]{2,3}(?:-[a-z]{2})?$/i.test(first)) return null;
115
+ return `/${first}`;
116
+ }
117
+
118
+ function normalizeSubUrl(startUrl, href) {
119
+ if (!href || typeof href !== 'string') return null;
120
+ try {
121
+ const target = new URL(href, startUrl);
122
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') return null;
123
+ target.hash = '';
124
+ target.search = '';
125
+ target.pathname = target.pathname.replace(/\/+$/, '') || '/';
126
+ return target;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function isSameMenuScope(target, start, localePrefix) {
133
+ if (target.origin !== start.origin) return false;
134
+ if (!localePrefix) return true;
135
+ return target.pathname === localePrefix || target.pathname.startsWith(`${localePrefix}/`);
136
+ }
137
+
138
+ function isExcludedSubLink(target, text) {
139
+ const label = String(text || '').trim();
140
+ if (SUB_EXCLUDE_TEXT_RE.test(label)) return true;
141
+ if (SUB_EXCLUDE_PATH_RE.test(target.pathname)) return true;
142
+ return SUB_EXCLUDE_EXT_RE.test(target.pathname);
143
+ }
144
+
145
+ function collectSubUrls(startUrl, links, limit = SUB_URL_LIMIT) {
146
+ const start = normalizeSubUrl(startUrl, startUrl);
147
+ if (!start) return [];
148
+ const localePrefix = localePrefixOf(start.href);
149
+ const urls = [start.href];
150
+ const seen = new Set(urls);
151
+
152
+ for (const link of links) {
153
+ if (urls.length >= limit) break;
154
+ const target = normalizeSubUrl(start.href, link.href);
155
+ if (!target || seen.has(target.href)) continue;
156
+ if (!isSameMenuScope(target, start, localePrefix)) continue;
157
+ if (isExcludedSubLink(target, link.text)) continue;
158
+ seen.add(target.href);
159
+ urls.push(target.href);
160
+ }
161
+
162
+ return urls;
163
+ }
164
+
165
+ function findSitemapUrl(startUrl, links) {
166
+ const start = normalizeSubUrl(startUrl, startUrl);
167
+ if (!start) return null;
168
+ const localePrefix = localePrefixOf(start.href);
169
+
170
+ for (const link of links) {
171
+ const label = String(link.text || '').trim();
172
+ if (!/sitemap|사이트맵/i.test(`${label} ${link.href}`)) continue;
173
+ const target = normalizeSubUrl(start.href, link.href);
174
+ if (target && isSameMenuScope(target, start, localePrefix)) return target.href;
175
+ }
176
+
177
+ return null;
178
+ }
179
+
107
180
  // ─── Puppeteer dynamic loader (optional dep) ────────────────────────
108
181
  async function loadPuppeteer() {
109
182
  try {
@@ -905,6 +978,14 @@ const SEO_EXTRACT = `(function () {
905
978
  return Array.from(new Set(icons.concat(og)));
906
979
  })`;
907
980
 
981
+ const SUB_LINK_EXTRACT = `(function () {
982
+ const links = Array.from(document.querySelectorAll('a[href]')).map((a) => ({
983
+ href: a.getAttribute('href'),
984
+ text: (a.innerText || a.textContent || '').replace(/\\s+/g, ' ').trim(),
985
+ }));
986
+ return { links };
987
+ })`;
988
+
908
989
  // HTML sanitization: strip scripts/analytics before saving
909
990
  function sanitizeHtml(html) {
910
991
  return html
@@ -914,6 +995,65 @@ function sanitizeHtml(html) {
914
995
  .replace(/\son[a-z]+='[^']*'/gi, '');
915
996
  }
916
997
 
998
+ async function newClonePage(browser) {
999
+ const page = await browser.newPage();
1000
+ await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
1001
+ await page.setUserAgent(
1002
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
1003
+ '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
1004
+ );
1005
+ await page.setCacheEnabled(false);
1006
+ return page;
1007
+ }
1008
+
1009
+ async function discoverSubUrls({ url, opts }) {
1010
+ if (!url) throw new Error('URL is required');
1011
+ if (!opts.out) throw new Error('--out=<file> is required');
1012
+
1013
+ if (!opts.ignoreRobots) {
1014
+ const blocked = await checkRobots(url);
1015
+ if (blocked) {
1016
+ throw new Error(
1017
+ `robots.txt disallows ${url}. Pass --ignore-robots only with the site owner's permission.`,
1018
+ );
1019
+ }
1020
+ }
1021
+
1022
+ const puppeteer = await loadPuppeteer();
1023
+ const browser = await puppeteer.launch({
1024
+ headless: 'new',
1025
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
1026
+ });
1027
+
1028
+ try {
1029
+ const page = await newClonePage(browser);
1030
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 });
1031
+ await wait(800);
1032
+
1033
+ let source = 'menu';
1034
+ let sourceUrl = url;
1035
+ let { links } = await page.evaluate(`(${SUB_LINK_EXTRACT})()`);
1036
+ const sitemapUrl = findSitemapUrl(url, links);
1037
+ if (sitemapUrl) {
1038
+ await page.goto(sitemapUrl, { waitUntil: 'networkidle2', timeout: 90000 });
1039
+ await wait(800);
1040
+ source = 'sitemap';
1041
+ sourceUrl = sitemapUrl;
1042
+ ({ links } = await page.evaluate(`(${SUB_LINK_EXTRACT})()`));
1043
+ }
1044
+
1045
+ const urls = collectSubUrls(url, links);
1046
+ fs.mkdirSync(path.dirname(opts.out), { recursive: true });
1047
+ fs.writeFileSync(
1048
+ opts.out,
1049
+ JSON.stringify({ meta: { url, source, sourceUrl, count: urls.length }, urls }, null, 2),
1050
+ );
1051
+ process.stdout.write(`[clone-extract] suburls ${urls.length} → ${opts.out}\n`);
1052
+ } finally {
1053
+ await browser.close();
1054
+ }
1055
+ }
1056
+
917
1057
  // ─── Main capture flow ──────────────────────────────────────────────
918
1058
  async function capture({ url, opts }) {
919
1059
  if (!url) throw new Error('URL is required');
@@ -954,14 +1094,8 @@ async function capture({ url, opts }) {
954
1094
  });
955
1095
 
956
1096
  try {
957
- const page = await browser.newPage();
1097
+ const page = await newClonePage(browser);
958
1098
  await page.setViewport(viewport);
959
- await page.setUserAgent(
960
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
961
- '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
962
- );
963
- // disable cache to get fresh assets
964
- await page.setCacheEnabled(false);
965
1099
 
966
1100
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 });
967
1101
  await scrollToBottom(page);
@@ -1140,16 +1274,18 @@ const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('
1140
1274
  if (isMain) {
1141
1275
  const { cmd, url, opts } = parseArgs(process.argv);
1142
1276
 
1143
- if (cmd !== 'capture') {
1277
+ if (cmd !== 'capture' && cmd !== 'suburls') {
1144
1278
  console.error('Usage: node clone-extract.js capture <URL> --out=<dir> --viewport=WxH[@DPR] --bp=mo|pc [--stealth] [--ignore-robots] [--no-interact] [--wait=ms]');
1279
+ console.error(' node clone-extract.js suburls <URL> --out=<file> [--ignore-robots]');
1145
1280
  process.exit(1);
1146
1281
  }
1147
1282
 
1148
- capture({ url, opts }).catch((err) => {
1283
+ const run = cmd === 'suburls' ? discoverSubUrls : capture;
1284
+ run({ url, opts }).catch((err) => {
1149
1285
  console.error(`[clone-extract] FAIL: ${err.message}`);
1150
1286
  if (process.env.DEBUG) console.error(err.stack);
1151
1287
  process.exit(1);
1152
1288
  });
1153
1289
  }
1154
1290
 
1155
- export { diffStyles };
1291
+ export { collectSubUrls, diffStyles };