vibepro 0.1.0-alpha.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 (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
@@ -0,0 +1,230 @@
1
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { getWorkspaceDir, initWorkspace, toWorkspaceRelative } from './workspace.js';
5
+
6
+ const ENTRY_FILES = [
7
+ 'README.md',
8
+ 'README.ja.md',
9
+ 'AGENTS.md',
10
+ 'CLAUDE.md',
11
+ 'package.json',
12
+ 'pnpm-workspace.yaml',
13
+ 'turbo.json',
14
+ 'next.config.js',
15
+ 'next.config.mjs',
16
+ 'vite.config.ts',
17
+ 'tsconfig.json'
18
+ ];
19
+
20
+ const SOURCE_DIRS = [
21
+ 'src',
22
+ 'app',
23
+ 'pages',
24
+ 'components',
25
+ 'lib',
26
+ 'server',
27
+ 'api',
28
+ 'tests',
29
+ 'test',
30
+ 'e2e',
31
+ 'docs',
32
+ 'scripts'
33
+ ];
34
+
35
+ export async function generateAgentHarnessMap(repoRoot) {
36
+ await initWorkspace(repoRoot);
37
+ const root = path.resolve(repoRoot);
38
+ const harnessDir = path.join(getWorkspaceDir(root), 'harness');
39
+ await mkdir(harnessDir, { recursive: true });
40
+
41
+ const packageInfo = await readPackageInfo(root);
42
+ const entryFiles = await existingPaths(root, ENTRY_FILES);
43
+ const sourceDirs = await existingPaths(root, SOURCE_DIRS);
44
+ const testCommandMap = buildTestCommandMap(packageInfo);
45
+ const codebaseMap = renderCodebaseMap({ entryFiles, sourceDirs, packageInfo, testCommandMap });
46
+ const agentEntrypoints = renderAgentEntrypoints({ entryFiles, sourceDirs, testCommandMap });
47
+
48
+ const codebaseMapPath = path.join(harnessDir, 'codebase-map.md');
49
+ const agentEntrypointsPath = path.join(harnessDir, 'agent-entrypoints.md');
50
+ const testCommandMapPath = path.join(harnessDir, 'test-command-map.json');
51
+ await writeFile(codebaseMapPath, codebaseMap);
52
+ await writeFile(agentEntrypointsPath, agentEntrypoints);
53
+ await writeFile(testCommandMapPath, `${JSON.stringify(testCommandMap, null, 2)}\n`);
54
+
55
+ return {
56
+ schema_version: '0.1.0',
57
+ status: 'created',
58
+ repo: { root: '.' },
59
+ summary: {
60
+ entry_file_count: entryFiles.length,
61
+ source_dir_count: sourceDirs.length,
62
+ command_count: testCommandMap.commands.length
63
+ },
64
+ artifacts: {
65
+ codebase_map: toWorkspaceRelative(root, codebaseMapPath),
66
+ agent_entrypoints: toWorkspaceRelative(root, agentEntrypointsPath),
67
+ test_command_map: toWorkspaceRelative(root, testCommandMapPath)
68
+ },
69
+ entry_files: entryFiles,
70
+ source_dirs: sourceDirs,
71
+ test_command_map: testCommandMap
72
+ };
73
+ }
74
+
75
+ export function renderAgentHarnessMapSummary(result) {
76
+ return [
77
+ '# VibePro Agent Harness Map',
78
+ '',
79
+ `Status: ${result.status}`,
80
+ `Codebase map: ${result.artifacts.codebase_map}`,
81
+ `Agent entrypoints: ${result.artifacts.agent_entrypoints}`,
82
+ `Test command map: ${result.artifacts.test_command_map}`,
83
+ '',
84
+ `Entry files: ${result.summary.entry_file_count}`,
85
+ `Source dirs: ${result.summary.source_dir_count}`,
86
+ `Commands: ${result.summary.command_count}`
87
+ ].join('\n') + '\n';
88
+ }
89
+
90
+ async function readPackageInfo(root) {
91
+ const packagePath = path.join(root, 'package.json');
92
+ const content = await readOptional(packagePath);
93
+ if (content === null) return { exists: false, scripts: {}, workspaces: [] };
94
+ try {
95
+ const parsed = JSON.parse(content);
96
+ return {
97
+ exists: true,
98
+ name: parsed.name ?? null,
99
+ scripts: parsed.scripts ?? {},
100
+ workspaces: parsed.workspaces ?? []
101
+ };
102
+ } catch (error) {
103
+ return {
104
+ exists: true,
105
+ invalid: true,
106
+ error: error.message,
107
+ scripts: {},
108
+ workspaces: []
109
+ };
110
+ }
111
+ }
112
+
113
+ function buildTestCommandMap(packageInfo) {
114
+ const commands = Object.entries(packageInfo.scripts ?? {}).map(([name, command]) => ({
115
+ id: name,
116
+ command: `npm run ${name}`,
117
+ raw_script: command,
118
+ category: classifyScript(name, command)
119
+ }));
120
+ return {
121
+ schema_version: '0.1.0',
122
+ package_manager_hint: 'npm',
123
+ commands,
124
+ by_category: commands.reduce((groups, command) => {
125
+ const items = groups[command.category] ?? [];
126
+ items.push(command.id);
127
+ groups[command.category] = items;
128
+ return groups;
129
+ }, {})
130
+ };
131
+ }
132
+
133
+ function classifyScript(name, command) {
134
+ const text = `${name} ${command}`.toLowerCase();
135
+ if (/type(check)?|tsc/.test(text)) return 'typecheck';
136
+ if (/e2e|playwright|cypress/.test(text)) return 'e2e';
137
+ if (/test|vitest|jest|node --test/.test(text)) return 'unit';
138
+ if (/lint|eslint/.test(text)) return 'lint';
139
+ if (/build|next build|vite build/.test(text)) return 'build';
140
+ if (/dev|start/.test(text)) return 'runtime';
141
+ return 'other';
142
+ }
143
+
144
+ function renderCodebaseMap({ entryFiles, sourceDirs, packageInfo, testCommandMap }) {
145
+ const lines = [
146
+ '# Codebase Map',
147
+ '',
148
+ '## Entry Files',
149
+ '',
150
+ ...formatList(entryFiles),
151
+ '',
152
+ '## Source Directories',
153
+ '',
154
+ ...formatList(sourceDirs),
155
+ '',
156
+ '## Package Scripts',
157
+ '',
158
+ ...formatScripts(packageInfo.scripts ?? {}),
159
+ '',
160
+ '## Verification Commands',
161
+ '',
162
+ ...testCommandMap.commands.map((command) => `- ${command.category}: \`${command.command}\` (${command.raw_script})`)
163
+ ];
164
+ return `${lines.join('\n')}\n`;
165
+ }
166
+
167
+ function renderAgentEntrypoints({ entryFiles, sourceDirs, testCommandMap }) {
168
+ const lines = [
169
+ '# Agent Entrypoints',
170
+ '',
171
+ '## Read First',
172
+ '',
173
+ ...formatList(entryFiles.filter((file) => /README|AGENTS|CLAUDE|package\.json/.test(file))),
174
+ '',
175
+ '## Explore Next',
176
+ '',
177
+ ...formatList(sourceDirs),
178
+ '',
179
+ '## Avoid By Default',
180
+ '',
181
+ '- `.vibepro/` generated evidence',
182
+ '- `node_modules/` dependencies',
183
+ '- build outputs such as `.next/`, `dist/`, `build/`, and `coverage/`',
184
+ '',
185
+ '## Before Changing Code',
186
+ '',
187
+ ...testCommandMap.commands
188
+ .filter((command) => ['typecheck', 'unit', 'e2e', 'lint'].includes(command.category))
189
+ .map((command) => `- ${command.category}: \`${command.command}\``)
190
+ ];
191
+ return `${lines.join('\n')}\n`;
192
+ }
193
+
194
+ function formatList(items) {
195
+ return items.length === 0 ? ['- none detected'] : items.map((item) => `- ${item}`);
196
+ }
197
+
198
+ function formatScripts(scripts) {
199
+ const entries = Object.entries(scripts);
200
+ return entries.length === 0
201
+ ? ['- none detected']
202
+ : entries.map(([name, command]) => `- \`${name}\`: ${command}`);
203
+ }
204
+
205
+ async function existingPaths(root, candidates) {
206
+ const existing = [];
207
+ for (const candidate of candidates) {
208
+ if (await pathExists(path.join(root, candidate))) existing.push(candidate);
209
+ }
210
+ return existing;
211
+ }
212
+
213
+ async function readOptional(filePath) {
214
+ try {
215
+ return await readFile(filePath, 'utf8');
216
+ } catch (error) {
217
+ if (error.code === 'ENOENT') return null;
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ async function pathExists(filePath) {
223
+ try {
224
+ await stat(filePath);
225
+ return true;
226
+ } catch (error) {
227
+ if (error.code === 'ENOENT') return false;
228
+ throw error;
229
+ }
230
+ }
@@ -0,0 +1,337 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { verifyCodexInstructions } from './codex-manager.js';
5
+ import { verifyBundledSkills } from './skills-manager.js';
6
+
7
+ const GENERATED_IGNORE_PATTERNS = [
8
+ '.vibepro/',
9
+ 'node_modules/',
10
+ '.next/',
11
+ 'dist/',
12
+ 'build/',
13
+ 'coverage/'
14
+ ];
15
+
16
+ const HOOK_SETTINGS_FILES = new Set([
17
+ 'settings.json',
18
+ 'settings.local.json'
19
+ ]);
20
+
21
+ export async function scanAgentHarness(repoRoot) {
22
+ const root = path.resolve(repoRoot);
23
+ const codex = await verifyCodexInstructions(root);
24
+ const skills = await verifyBundledSkills(root);
25
+ const claude = await inspectClaudeHarness(root);
26
+ const hooks = await inspectClaudeHooks(root);
27
+ const ignoreNoise = await inspectIgnoreNoise(root);
28
+ const findings = [
29
+ ...codexFindings(codex),
30
+ ...skillsFindings(skills),
31
+ ...claudeFindings(claude),
32
+ ...hooksFindings(hooks),
33
+ ...ignoreNoiseFindings(ignoreNoise)
34
+ ];
35
+ const riskSummary = summarizeFindings(findings);
36
+ return {
37
+ schema_version: '0.1.0',
38
+ status: riskSummary.block > 0 ? 'fail' : riskSummary.review > 0 ? 'needs_review' : 'pass',
39
+ summary: {
40
+ findings: findings.length,
41
+ codex_status: codex.status,
42
+ claude_status: claude.status,
43
+ skills_status: skills.overall_status,
44
+ hook_findings: hooks.findings.length,
45
+ ignore_noise_status: ignoreNoise.status
46
+ },
47
+ codex,
48
+ claude,
49
+ skills,
50
+ hooks,
51
+ ignore_noise: ignoreNoise,
52
+ findings,
53
+ risk_summary: {
54
+ findings: riskSummary
55
+ },
56
+ next_actions: buildNextActions({ codex, claude, skills, hooks, ignoreNoise })
57
+ };
58
+ }
59
+
60
+ export function renderAgentHarnessStatus(result) {
61
+ const lines = [
62
+ '# VibePro Agent Harness Status',
63
+ '',
64
+ `Status: ${result.status}`,
65
+ '',
66
+ '| Area | Status | Detail |',
67
+ '| ---- | ------ | ------ |',
68
+ `| Codex instructions | ${normalizeHarnessStatus(result.codex?.status)} | ${result.codex?.target_path ?? 'AGENTS.md'} |`,
69
+ `| Claude Code instructions | ${normalizeHarnessStatus(result.claude?.has_claude_file ? 'ok' : 'missing')} | ${result.claude?.target_path ?? 'CLAUDE.md'} |`,
70
+ `| Claude Code skills dir | ${normalizeHarnessStatus(result.claude?.has_skills_dir ? 'ok' : 'missing')} | ${result.claude?.skills_dir ?? '.claude/skills'} |`,
71
+ `| VibePro bundled skills | ${normalizeHarnessStatus(result.skills?.overall_status)} | ${formatSkillSummary(result.skills?.summary)} |`,
72
+ `| Hooks | ${normalizeHarnessStatus(result.hooks?.status)} | ${(result.hooks?.settings_files ?? []).join(', ') || 'no hook settings'} |`,
73
+ `| Ignore noise | ${normalizeHarnessStatus(result.ignore_noise?.status)} | missing: ${(result.ignore_noise?.missing_patterns ?? []).join(', ') || '-'} |`,
74
+ ''
75
+ ];
76
+ if ((result.findings ?? []).length > 0) {
77
+ lines.push('## Findings', '');
78
+ for (const finding of result.findings) {
79
+ const detail = finding.skill ?? finding.file ?? finding.target ?? finding.area ?? finding.kind;
80
+ lines.push(`- [${finding.gate_effect ?? 'info'}] ${finding.kind}: ${detail}`);
81
+ }
82
+ lines.push('');
83
+ }
84
+ lines.push('## Next Actions', '');
85
+ if ((result.next_actions ?? []).length === 0) {
86
+ lines.push('- none');
87
+ } else {
88
+ for (const action of result.next_actions) lines.push(`- ${action}`);
89
+ }
90
+ return `${lines.join('\n')}\n`;
91
+ }
92
+
93
+ function normalizeHarnessStatus(status) {
94
+ if (['ok', 'pass'].includes(status)) return 'installed';
95
+ if (status === 'needs_install') return 'missing_or_outdated';
96
+ return status ?? 'unknown';
97
+ }
98
+
99
+ function formatSkillSummary(summary = {}) {
100
+ const entries = Object.entries(summary);
101
+ if (entries.length === 0) return '-';
102
+ return entries.map(([key, value]) => `${key}=${value}`).join(', ');
103
+ }
104
+
105
+ async function inspectClaudeHarness(root) {
106
+ const claudePath = path.join(root, 'CLAUDE.md');
107
+ const skillsDir = path.join(root, '.claude', 'skills');
108
+ const hasClaudeFile = await pathExists(claudePath);
109
+ const hasSkillsDir = await pathExists(skillsDir);
110
+ const installedSkillCount = hasSkillsDir ? (await listDirectories(skillsDir)).length : 0;
111
+ return {
112
+ status: hasClaudeFile && hasSkillsDir ? 'ok' : 'needs_install',
113
+ target_path: 'CLAUDE.md',
114
+ skills_dir: '.claude/skills',
115
+ has_claude_file: hasClaudeFile,
116
+ has_skills_dir: hasSkillsDir,
117
+ installed_skill_count: installedSkillCount
118
+ };
119
+ }
120
+
121
+ async function inspectClaudeHooks(root) {
122
+ const claudeDir = path.join(root, '.claude');
123
+ const files = await listHookSettingsFiles(claudeDir);
124
+ const findings = [];
125
+ for (const file of files) {
126
+ const relativeFile = path.relative(root, file);
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(await readFile(file, 'utf8'));
130
+ } catch (error) {
131
+ findings.push({
132
+ kind: 'invalid_hook_settings_json',
133
+ file: relativeFile,
134
+ message: error.message,
135
+ gate_effect: 'review'
136
+ });
137
+ continue;
138
+ }
139
+ for (const command of collectCommandStrings(parsed)) {
140
+ const target = extractLocalCommandTarget(command);
141
+ if (!target) continue;
142
+ const targetPath = path.resolve(root, target);
143
+ if (!(await pathExists(targetPath))) {
144
+ findings.push({
145
+ kind: 'hook_command_target_missing',
146
+ file: relativeFile,
147
+ command,
148
+ target,
149
+ gate_effect: 'review'
150
+ });
151
+ }
152
+ }
153
+ }
154
+ return {
155
+ status: findings.some((finding) => finding.gate_effect === 'review') ? 'needs_review' : 'pass',
156
+ settings_files: files.map((file) => path.relative(root, file)),
157
+ findings
158
+ };
159
+ }
160
+
161
+ async function inspectIgnoreNoise(root) {
162
+ const gitignore = await readOptional(path.join(root, '.gitignore'));
163
+ const configured_patterns = gitignore === null
164
+ ? []
165
+ : GENERATED_IGNORE_PATTERNS.filter((pattern) => gitignore.includes(pattern));
166
+ const missing_patterns = GENERATED_IGNORE_PATTERNS.filter((pattern) => !configured_patterns.includes(pattern));
167
+ return {
168
+ status: missing_patterns.includes('.vibepro/') ? 'needs_review' : 'pass',
169
+ target_path: '.gitignore',
170
+ configured_patterns,
171
+ missing_patterns
172
+ };
173
+ }
174
+
175
+ function codexFindings(codex) {
176
+ if (codex.overall_status === 'ok') return [];
177
+ return [{
178
+ kind: codex.status === 'outdated' ? 'codex_instructions_outdated' : 'codex_instructions_missing',
179
+ area: 'codex',
180
+ file: codex.target_path,
181
+ status: codex.status,
182
+ gate_effect: 'review'
183
+ }];
184
+ }
185
+
186
+ function skillsFindings(skills) {
187
+ return (skills.skills ?? [])
188
+ .filter((skill) => skill.status !== 'ok')
189
+ .map((skill) => ({
190
+ kind: skill.status === 'outdated' ? 'vibepro_skill_outdated' : 'vibepro_skill_missing',
191
+ area: 'skills',
192
+ skill: skill.name,
193
+ file: skill.target_path,
194
+ status: skill.status,
195
+ gate_effect: 'review'
196
+ }));
197
+ }
198
+
199
+ function claudeFindings(claude) {
200
+ const findings = [];
201
+ if (!claude.has_claude_file) {
202
+ findings.push({
203
+ kind: 'claude_instructions_missing',
204
+ area: 'claude_code',
205
+ file: claude.target_path,
206
+ gate_effect: 'review'
207
+ });
208
+ }
209
+ if (!claude.has_skills_dir) {
210
+ findings.push({
211
+ kind: 'claude_skills_dir_missing',
212
+ area: 'claude_code',
213
+ file: claude.skills_dir,
214
+ gate_effect: 'review'
215
+ });
216
+ }
217
+ return findings;
218
+ }
219
+
220
+ function hooksFindings(hooks) {
221
+ return hooks.findings.map((finding) => ({
222
+ ...finding,
223
+ area: 'hooks'
224
+ }));
225
+ }
226
+
227
+ function ignoreNoiseFindings(ignoreNoise) {
228
+ if (ignoreNoise.missing_patterns.length === 0) return [];
229
+ return [{
230
+ kind: 'ai_exploration_noise_ignores_incomplete',
231
+ area: 'ignore_noise',
232
+ file: ignoreNoise.target_path,
233
+ missing_patterns: ignoreNoise.missing_patterns,
234
+ gate_effect: ignoreNoise.missing_patterns.includes('.vibepro/') ? 'review' : 'info'
235
+ }];
236
+ }
237
+
238
+ function buildNextActions({ codex, claude, skills, hooks, ignoreNoise }) {
239
+ const actions = [];
240
+ if (codex.overall_status !== 'ok') actions.push('vibepro codex install <repo>');
241
+ if (skills.overall_status !== 'ok') actions.push('vibepro skills install <repo>');
242
+ if (claude.status !== 'ok') actions.push('Add or refresh CLAUDE.md and .claude/skills for Claude Code users.');
243
+ if (hooks.status !== 'pass') actions.push('Fix missing hook script targets before relying on automated hooks.');
244
+ if (ignoreNoise.status !== 'pass') actions.push('Add .vibepro/ to .gitignore so VibePro evidence stays out of product diffs.');
245
+ return actions;
246
+ }
247
+
248
+ async function listHookSettingsFiles(dir) {
249
+ const files = [];
250
+ try {
251
+ const entries = await readdir(dir, { withFileTypes: true });
252
+ for (const entry of entries) {
253
+ if (!entry.isFile()) continue;
254
+ if (HOOK_SETTINGS_FILES.has(entry.name) || /^settings\..+\.json$/.test(entry.name)) {
255
+ files.push(path.join(dir, entry.name));
256
+ }
257
+ }
258
+ } catch (error) {
259
+ if (error.code !== 'ENOENT') throw error;
260
+ }
261
+ return files;
262
+ }
263
+
264
+ function collectCommandStrings(value) {
265
+ const commands = [];
266
+ const visit = (item, key = '') => {
267
+ if (typeof item === 'string') {
268
+ if (key === 'command' || looksLikeCommand(item)) commands.push(item);
269
+ return;
270
+ }
271
+ if (Array.isArray(item)) {
272
+ for (const child of item) visit(child, key);
273
+ return;
274
+ }
275
+ if (item && typeof item === 'object') {
276
+ for (const [childKey, child] of Object.entries(item)) visit(child, childKey);
277
+ }
278
+ };
279
+ visit(value);
280
+ return [...new Set(commands)];
281
+ }
282
+
283
+ function looksLikeCommand(value) {
284
+ return /(?:^|\s)(?:node|npx|pnpm|npm|yarn|bash|sh|tsx)\s+/.test(value)
285
+ || /(?:^|\s)\.\/[^\s]+/.test(value);
286
+ }
287
+
288
+ function extractLocalCommandTarget(command) {
289
+ const patterns = [
290
+ /(?:^|\s)(?:node|bash|sh|tsx)\s+((?:\.\/)?(?:scripts|bin|tools)\/[^\s'"]+)/,
291
+ /(?:^|\s)npx\s+tsx\s+((?:\.\/)?(?:scripts|bin|tools)\/[^\s'"]+)/,
292
+ /(?:^|\s)(?:pnpm|npm|yarn)\s+(?:exec\s+)?tsx\s+((?:\.\/)?(?:scripts|bin|tools)\/[^\s'"]+)/,
293
+ /(?:^|\s)(\.\/(?:scripts|bin|tools)\/[^\s'"]+)/
294
+ ];
295
+ for (const pattern of patterns) {
296
+ const match = command.match(pattern);
297
+ if (match) return match[1].replace(/^\.\//, '');
298
+ }
299
+ return null;
300
+ }
301
+
302
+ async function listDirectories(dir) {
303
+ try {
304
+ const entries = await readdir(dir, { withFileTypes: true });
305
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
306
+ } catch (error) {
307
+ if (error.code === 'ENOENT') return [];
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ async function readOptional(filePath) {
313
+ try {
314
+ return await readFile(filePath, 'utf8');
315
+ } catch (error) {
316
+ if (error.code === 'ENOENT') return null;
317
+ throw error;
318
+ }
319
+ }
320
+
321
+ async function pathExists(filePath) {
322
+ try {
323
+ await stat(filePath);
324
+ return true;
325
+ } catch (error) {
326
+ if (error.code === 'ENOENT') return false;
327
+ throw error;
328
+ }
329
+ }
330
+
331
+ function summarizeFindings(findings) {
332
+ return findings.reduce((summary, finding) => {
333
+ const effect = ['block', 'review', 'info'].includes(finding.gate_effect) ? finding.gate_effect : 'info';
334
+ summary[effect] += 1;
335
+ return summary;
336
+ }, { block: 0, review: 0, info: 0 });
337
+ }