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,48 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { getWorkspaceDir, initWorkspace } from './workspace.js';
5
+
6
+ export const DEFAULT_OUTPUT_LANGUAGE = 'ja';
7
+ export const SUPPORTED_OUTPUT_LANGUAGES = new Set(['ja', 'en']);
8
+
9
+ export function normalizeOutputLanguage(value) {
10
+ return SUPPORTED_OUTPUT_LANGUAGES.has(value) ? value : DEFAULT_OUTPUT_LANGUAGE;
11
+ }
12
+
13
+ export function assertOutputLanguage(value) {
14
+ if (SUPPORTED_OUTPUT_LANGUAGES.has(value)) return value;
15
+ throw new Error(`Unsupported output language: ${value}. Supported languages: ja, en`);
16
+ }
17
+
18
+ export function resolveOutputLanguage(config, override = null) {
19
+ if (override) return assertOutputLanguage(override);
20
+ return normalizeOutputLanguage(config?.output?.language);
21
+ }
22
+
23
+ export async function resolveHumanOutputLanguage(repoRoot, options = {}) {
24
+ const override = typeof options === 'string' ? options : options.language;
25
+ if (override) return assertOutputLanguage(override);
26
+ await initWorkspace(repoRoot);
27
+ const configPath = path.join(getWorkspaceDir(repoRoot), 'config.json');
28
+ const config = JSON.parse(await readFile(configPath, 'utf8'));
29
+ return resolveOutputLanguage(config);
30
+ }
31
+
32
+ export function localizedText(language, values) {
33
+ const normalized = normalizeOutputLanguage(language);
34
+ return values[normalized] ?? values[DEFAULT_OUTPUT_LANGUAGE] ?? values.en ?? '';
35
+ }
36
+
37
+ export async function setOutputLanguage(repoRoot, language) {
38
+ const normalized = assertOutputLanguage(language);
39
+ await initWorkspace(repoRoot);
40
+ const configPath = path.join(getWorkspaceDir(repoRoot), 'config.json');
41
+ const config = JSON.parse(await readFile(configPath, 'utf8'));
42
+ config.output = {
43
+ ...(config.output ?? {}),
44
+ language: normalized
45
+ };
46
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
47
+ return { language: normalized, config };
48
+ }
@@ -0,0 +1,182 @@
1
+ const SCHEMA_VERSION = '0.1.0';
2
+ const SNIPPET_MAX = 120;
3
+
4
+ const TEST_PATH_PATTERNS = [
5
+ /(^|\/)tests?\//,
6
+ /(^|\/)__tests?__\//,
7
+ /\.(test|spec)\.[cm]?[jt]sx?$/,
8
+ /\.(test|spec)\.py$/,
9
+ /_test\.(go|rs)$/
10
+ ];
11
+
12
+ const PATTERNS = [
13
+ {
14
+ id: 'merge_conflict_marker',
15
+ severity: 'critical',
16
+ applies_to: () => true,
17
+ message: 'Merge conflict markers present in committed text.',
18
+ fix_hint: 'Resolve the merge conflict and remove the <<<<<<<, =======, and >>>>>>> markers before committing.',
19
+ file_check(addedLines) {
20
+ let openMarker = null;
21
+ let separator = null;
22
+ let close = null;
23
+ for (const entry of addedLines) {
24
+ if (entry.content.startsWith('<<<<<<<')) openMarker = entry;
25
+ else if (entry.content.startsWith('=======') && openMarker && !separator) separator = entry;
26
+ else if (entry.content.startsWith('>>>>>>>') && openMarker && separator) close = entry;
27
+ }
28
+ if (openMarker && separator && close) {
29
+ return { line_number: openMarker.line_number, snippet: trimSnippet(openMarker.content) };
30
+ }
31
+ return null;
32
+ }
33
+ },
34
+ {
35
+ id: 'commit_marker_left_in',
36
+ severity: 'high',
37
+ applies_to: (filePath) => !isTestPath(filePath),
38
+ message: 'Self-addressed commit marker left in the change.',
39
+ fix_hint: 'Remove the marker; if the work is truly unfinished, split it into a follow-up PR or mark the entire PR as a draft.',
40
+ pattern: /\b(REMOVE BEFORE COMMIT|DO NOT COMMIT|DEBUG ONLY)\b/
41
+ },
42
+ {
43
+ id: 'debug_print_left_in',
44
+ severity: 'high',
45
+ applies_to: (filePath) => {
46
+ if (isTestPath(filePath)) return false;
47
+ return /\.(js|ts|jsx|tsx|mjs|cjs|py|rs)$/.test(filePath);
48
+ },
49
+ message: 'Debug print or logging statement left in non-test source.',
50
+ fix_hint: 'Remove the debug line, or replace it with a real logger call wired through the project\'s logging facility.',
51
+ match(line, filePath) {
52
+ if (/\bconsole\.(log|debug)\s*\(/.test(line)) return { snippet: trimSnippet(line), fix_hint: null };
53
+ if (/\bdebugger\s*;/.test(line)) return { snippet: trimSnippet(line), fix_hint: null };
54
+ if (filePath.endsWith('.py') && /(^|\s)print\s*\(/.test(line)) return { snippet: trimSnippet(line), fix_hint: null };
55
+ if (filePath.endsWith('.rs') && /\bdbg!\s*\(/.test(line)) return { snippet: trimSnippet(line), fix_hint: null };
56
+ return null;
57
+ }
58
+ },
59
+ {
60
+ id: 'silent_catch',
61
+ severity: 'high',
62
+ applies_to: (filePath) => !isTestPath(filePath),
63
+ message: 'Empty catch block silently swallows errors.',
64
+ fix_hint: 'Log the error, rethrow, or document explicitly with a comment why the failure is being ignored.',
65
+ match(line, filePath) {
66
+ if (/\}\s*catch\s*(?:\(\s*\w*\s*\))?\s*\{\s*\}/.test(line)) return { snippet: trimSnippet(line) };
67
+ if (filePath.endsWith('.py') && /except[^:]*:\s*pass\b/.test(line)) return { snippet: trimSnippet(line) };
68
+ return null;
69
+ }
70
+ },
71
+ {
72
+ id: 'todo_fixme_marker',
73
+ severity: 'medium',
74
+ applies_to: (filePath) => !/\.md$/i.test(filePath),
75
+ message: 'TODO/FIXME/XXX marker left in committed source.',
76
+ fix_hint: 'Either resolve the marker now or link it to a tracked issue and convert it into a concrete reference.',
77
+ pattern: /\b(TODO|FIXME|XXX)\b/
78
+ },
79
+ {
80
+ id: 'eslint_disable_without_reason',
81
+ severity: 'medium',
82
+ applies_to: (filePath) => /\.(js|ts|jsx|tsx|mjs|cjs)$/.test(filePath),
83
+ message: 'eslint-disable directive used without a trailing justification.',
84
+ fix_hint: 'Add a short `-- because <reason>` clause so a future reviewer knows why the rule is suppressed.',
85
+ match(line) {
86
+ const m = line.match(/eslint-disable(?:-next-line|-line)?(?:\s+([^*\n]*))?/);
87
+ if (!m) return null;
88
+ const tail = (m[1] ?? '').trim();
89
+ if (!tail) return { snippet: trimSnippet(line) };
90
+ const afterRule = tail.replace(/^[\w@/-]+(?:,\s*[\w@/-]+)*/, '').trim();
91
+ if (!afterRule || /^[*/]+$/.test(afterRule)) return { snippet: trimSnippet(line) };
92
+ return null;
93
+ }
94
+ },
95
+ {
96
+ id: 'placeholder_text',
97
+ severity: 'medium',
98
+ applies_to: () => true,
99
+ message: 'Generic placeholder text left in the change.',
100
+ fix_hint: 'Replace with a real value, or remove the snippet entirely if it was only filler.',
101
+ pattern: /\b(lorem ipsum|Acme Corp|Acme Corporation|John Doe|Jane Doe|foo@bar\.com|user@example\.com|example\.com)\b/i
102
+ },
103
+ {
104
+ id: 'em_dash_in_prose',
105
+ severity: 'low',
106
+ applies_to: (filePath) => /\.md$/i.test(filePath),
107
+ message: 'Em dash (—) detected in Markdown prose — a common AI tell.',
108
+ fix_hint: 'Prefer a comma, period, or " - " hyphen-space depending on intent; em dashes are rare in human-edited prose.',
109
+ pattern: /—/
110
+ }
111
+ ];
112
+
113
+ export function detectLazyPatterns({ files } = {}) {
114
+ const findings = [];
115
+ if (!Array.isArray(files)) {
116
+ return { schema_version: SCHEMA_VERSION, findings };
117
+ }
118
+ for (const file of files) {
119
+ if (!file || typeof file.path !== 'string') continue;
120
+ const addedLines = Array.isArray(file.added_lines) ? file.added_lines : [];
121
+ for (const pattern of PATTERNS) {
122
+ try {
123
+ if (!pattern.applies_to(file.path)) continue;
124
+ if (typeof pattern.file_check === 'function') {
125
+ const hit = pattern.file_check(addedLines);
126
+ if (hit) {
127
+ findings.push({
128
+ pattern_id: pattern.id,
129
+ severity: pattern.severity,
130
+ file: file.path,
131
+ line_number: hit.line_number,
132
+ snippet: hit.snippet,
133
+ message: pattern.message,
134
+ fix_hint: hit.fix_hint ?? pattern.fix_hint
135
+ });
136
+ }
137
+ continue;
138
+ }
139
+ for (const entry of addedLines) {
140
+ if (!entry || typeof entry.content !== 'string') continue;
141
+ let hit = null;
142
+ if (typeof pattern.match === 'function') {
143
+ hit = pattern.match(entry.content, file.path);
144
+ } else if (pattern.pattern instanceof RegExp && pattern.pattern.test(entry.content)) {
145
+ hit = { snippet: trimSnippet(entry.content) };
146
+ }
147
+ if (hit) {
148
+ findings.push({
149
+ pattern_id: pattern.id,
150
+ severity: pattern.severity,
151
+ file: file.path,
152
+ line_number: entry.line_number,
153
+ snippet: hit.snippet,
154
+ message: pattern.message,
155
+ fix_hint: hit.fix_hint ?? pattern.fix_hint
156
+ });
157
+ }
158
+ }
159
+ } catch {
160
+ continue;
161
+ }
162
+ }
163
+ }
164
+ return { schema_version: SCHEMA_VERSION, findings };
165
+ }
166
+
167
+ export function listLazyPatternCatalog() {
168
+ return PATTERNS.map((pattern) => ({
169
+ id: pattern.id,
170
+ severity: pattern.severity,
171
+ message: pattern.message
172
+ }));
173
+ }
174
+
175
+ function isTestPath(filePath) {
176
+ return TEST_PATH_PATTERNS.some((re) => re.test(filePath));
177
+ }
178
+
179
+ function trimSnippet(text) {
180
+ const trimmed = String(text).trim();
181
+ return trimmed.length > SNIPPET_MAX ? `${trimmed.slice(0, SNIPPET_MAX - 1)}…` : trimmed;
182
+ }
@@ -0,0 +1,135 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export async function scanLocalDev(repoRoot) {
5
+ const packageJson = await readPackageJson(repoRoot);
6
+ const scripts = packageJson?.scripts ?? {};
7
+ const heavyDevScripts = Object.entries(scripts)
8
+ .filter(([name]) => name === 'dev' || name.startsWith('dev:'))
9
+ .map(([name, command]) => analyzeDevScript(name, command, scripts))
10
+ .filter(Boolean);
11
+
12
+ return {
13
+ package_json_found: Boolean(packageJson),
14
+ scanned_scripts: Object.keys(scripts).filter((name) => name === 'dev' || name.startsWith('dev:')).length,
15
+ heavy_dev_scripts: heavyDevScripts,
16
+ runtime_probe_plan: buildRuntimeProbePlan(scripts, heavyDevScripts),
17
+ risk_summary: {
18
+ heavy_dev_scripts: summarizeGateEffects(heavyDevScripts)
19
+ }
20
+ };
21
+ }
22
+
23
+ async function readPackageJson(repoRoot) {
24
+ try {
25
+ return JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'));
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') return null;
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function analyzeDevScript(name, command, scripts) {
33
+ if (typeof command !== 'string' || command.trim() === '') return null;
34
+ const concurrentlyCommands = extractConcurrentlyCommands(command);
35
+ const workerScriptRefs = countWorkerScriptRefs(command, scripts);
36
+ const hasNextDev = /\bnext\s+dev\b/.test(command);
37
+ const preDevCommands = countPreDevCommands(command);
38
+ const processCount = concurrentlyCommands.length > 0
39
+ ? concurrentlyCommands.length
40
+ : 1 + workerScriptRefs;
41
+
42
+ if (processCount < 4 && workerScriptRefs < 3 && preDevCommands < 2) return null;
43
+
44
+ return {
45
+ file: 'package.json',
46
+ script_name: name,
47
+ command,
48
+ kind: 'heavy_local_dev_script',
49
+ process_count: processCount,
50
+ worker_script_refs: workerScriptRefs,
51
+ pre_dev_command_count: preDevCommands,
52
+ has_next_dev: hasNextDev,
53
+ has_concurrently: concurrentlyCommands.length > 0,
54
+ gate_effect: 'review',
55
+ confidence: concurrentlyCommands.length > 0 && workerScriptRefs > 0 ? 'high' : 'medium',
56
+ recommendation: 'UI確認用のweb-only dev scriptとworker起動scriptを分離し、必要なworkerだけを明示起動できるようにする。'
57
+ };
58
+ }
59
+
60
+ function extractConcurrentlyCommands(command) {
61
+ if (!/\bconcurrently\b/.test(command)) return [];
62
+ return [...command.matchAll(/"([^"]+)"|'([^']+)'/g)]
63
+ .map((match) => match[1] ?? match[2])
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function countWorkerScriptRefs(command, scripts) {
68
+ const directRefs = (command.match(/\bnpm:worker[\w:-]*/g) ?? []).length;
69
+ const npmRunRefs = [...command.matchAll(/\bnpm\s+run\s+([A-Za-z0-9:_-]+)/g)]
70
+ .map((match) => match[1])
71
+ .filter((scriptName) => scriptName.includes('worker') || /worker/i.test(scripts[scriptName] ?? ''))
72
+ .length;
73
+ return directRefs + npmRunRefs;
74
+ }
75
+
76
+ function countPreDevCommands(command) {
77
+ const nextDevIndex = command.search(/\bnext\s+dev\b/);
78
+ if (nextDevIndex === -1) return 0;
79
+ const beforeNextDev = command.slice(0, nextDevIndex);
80
+ return (beforeNextDev.match(/&&|\|\|/g) ?? []).length;
81
+ }
82
+
83
+ function buildRuntimeProbePlan(scripts, heavyDevScripts) {
84
+ const webScript = resolveWebDevScript(scripts);
85
+ const fullScript = resolveFullDevScript(scripts, heavyDevScripts);
86
+ const commands = [
87
+ webScript ? {
88
+ id: 'web-dev-startup',
89
+ command: `npm run ${webScript}`,
90
+ metrics: ['startup_seconds', 'next_ready_log_time', 'process_rss_mb', 'process_cpu_percent'],
91
+ purpose: 'UI確認だけに必要なNext.js dev serverの起動時間と常駐メモリを測る。'
92
+ } : null,
93
+ fullScript ? {
94
+ id: 'full-local-startup',
95
+ command: `npm run ${fullScript}`,
96
+ metrics: ['startup_seconds', 'process_count', 'total_rss_mb', 'worker_rss_mb'],
97
+ purpose: 'worker込みのフルローカル起動が必要な時の上限コストを測る。'
98
+ } : null,
99
+ {
100
+ id: 'api-latency-projects',
101
+ command: 'curl -w "time_total=%{time_total}\\n" -o /dev/null -s http://localhost:${PORT:-3000}/api/projects',
102
+ metrics: ['api_latency_ms', 'http_status'],
103
+ purpose: 'ローカル初期表示に近いAPI latencyを測る。認証が必要な場合はブラウザ計測に置き換える。'
104
+ }
105
+ ].filter(Boolean);
106
+
107
+ return {
108
+ status: commands.length > 0 ? 'available' : 'unavailable',
109
+ auto_run: false,
110
+ reason: '診断は対象リポジトリのserverやworkerを自動起動しない。計測はtask実行時の明示操作として残す。',
111
+ commands
112
+ };
113
+ }
114
+
115
+ function resolveWebDevScript(scripts) {
116
+ if (scripts['dev:web']) return 'dev:web';
117
+ if (scripts.dev && /\bnext\s+dev\b/.test(scripts.dev) && !/\bconcurrently\b/.test(scripts.dev)) return 'dev';
118
+ if (scripts['dev:turbo'] && /\bnext\s+dev\b/.test(scripts['dev:turbo']) && !/\bconcurrently\b/.test(scripts['dev:turbo'])) return 'dev:turbo';
119
+ return null;
120
+ }
121
+
122
+ function resolveFullDevScript(scripts, heavyDevScripts) {
123
+ if (scripts['local:full']) return 'local:full';
124
+ if (scripts['dev:full']) return 'dev:full';
125
+ if (scripts['workers:dev']) return 'workers:dev';
126
+ return heavyDevScripts[0]?.script_name ?? null;
127
+ }
128
+
129
+ function summarizeGateEffects(hits) {
130
+ return {
131
+ block: hits.filter((hit) => hit.gate_effect === 'block').length,
132
+ review: hits.filter((hit) => hit.gate_effect === 'review').length,
133
+ info: hits.filter((hit) => hit.gate_effect === 'info').length
134
+ };
135
+ }
@@ -0,0 +1,187 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ import { readDecisionRecordsIfExists } from './decision-records.js';
7
+ import { resolveManagedWorktreeMode } from './managed-worktree.js';
8
+ import { getWorkspaceDir } from './workspace.js';
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ export async function buildManagedWorktreeGate(repoRoot, options = {}) {
13
+ const root = path.resolve(repoRoot);
14
+ const storyId = options.storyId;
15
+ const state = storyId ? await readBoundExecutionState(root, storyId) : null;
16
+ let managedWorktree = state?.managed_worktree ?? null;
17
+ if (managedWorktree?.path) {
18
+ managedWorktree = {
19
+ ...managedWorktree,
20
+ current_head_sha: await gitOptional(managedWorktree.path, ['rev-parse', 'HEAD']) ?? managedWorktree.current_head_sha ?? null
21
+ };
22
+ }
23
+ const mode = await resolveManagedWorktreeMode(managedWorktree?.source_repo ?? root);
24
+ const expectedPath = managedWorktree?.path ? path.resolve(managedWorktree.path) : null;
25
+ const currentPath = root;
26
+ const insideManagedWorktree = expectedPath ? isSameOrInside(currentPath, expectedPath) : false;
27
+ const decisionRecords = options.decisionRecords
28
+ ?? (storyId ? await readDecisionRecordsIfExists(root, storyId) : null);
29
+ const bypass = findAcceptedManagedWorktreeBypass(decisionRecords);
30
+ const missingReason = expectedPath
31
+ ? `current repo ${formatPath(currentPath)} is outside VibePro managed worktree ${formatPath(expectedPath)}`
32
+ : 'VibePro managed worktree execution state is missing';
33
+
34
+ if (mode === 'disabled') {
35
+ return {
36
+ id: 'gate:managed_worktree',
37
+ type: 'managed_worktree_gate',
38
+ label: 'Managed Worktree Gate',
39
+ status: 'not_applicable',
40
+ required: false,
41
+ mode,
42
+ current_repo: currentPath,
43
+ managed_worktree_path: expectedPath,
44
+ reason: 'managed worktree mode is disabled'
45
+ };
46
+ }
47
+
48
+ if (bypass) {
49
+ return {
50
+ id: 'gate:managed_worktree',
51
+ type: 'managed_worktree_gate',
52
+ label: 'Managed Worktree Gate',
53
+ status: 'bypassed',
54
+ required: mode === 'required',
55
+ mode,
56
+ current_repo: currentPath,
57
+ managed_worktree_path: expectedPath,
58
+ branch: managedWorktree?.branch ?? null,
59
+ current_head_sha: managedWorktree?.current_head_sha ?? null,
60
+ managed_worktree: managedWorktree,
61
+ decision_id: bypass.decision_id,
62
+ reason: `accepted bypass decision recorded: ${bypass.reason ?? bypass.summary ?? bypass.decision_id}`
63
+ };
64
+ }
65
+
66
+ if (insideManagedWorktree) {
67
+ return {
68
+ id: 'gate:managed_worktree',
69
+ type: 'managed_worktree_gate',
70
+ label: 'Managed Worktree Gate',
71
+ status: 'passed',
72
+ required: mode === 'required',
73
+ mode,
74
+ current_repo: currentPath,
75
+ managed_worktree_path: expectedPath,
76
+ branch: managedWorktree?.branch ?? null,
77
+ current_head_sha: managedWorktree?.current_head_sha ?? null,
78
+ managed_worktree: managedWorktree,
79
+ reason: 'command is running inside the VibePro managed worktree'
80
+ };
81
+ }
82
+
83
+ return {
84
+ id: 'gate:managed_worktree',
85
+ type: 'managed_worktree_gate',
86
+ label: 'Managed Worktree Gate',
87
+ status: mode === 'required' ? 'block' : 'needs_review',
88
+ required: mode === 'required',
89
+ mode,
90
+ current_repo: currentPath,
91
+ managed_worktree_path: expectedPath,
92
+ branch: managedWorktree?.branch ?? null,
93
+ current_head_sha: managedWorktree?.current_head_sha ?? null,
94
+ managed_worktree: managedWorktree,
95
+ reason: missingReason
96
+ };
97
+ }
98
+
99
+ export async function assertManagedWorktreeCommandAllowed(repoRoot, options = {}) {
100
+ const gate = await buildManagedWorktreeGate(repoRoot, options);
101
+ if (gate.mode !== 'required' || ['passed', 'bypassed'].includes(gate.status)) {
102
+ return gate;
103
+ }
104
+ const commandName = options.commandName ?? 'command';
105
+ throw new Error(
106
+ `${commandName} is blocked by gate:managed_worktree. ${gate.reason}. ` +
107
+ 'Run the command from `vibepro execute start` managed_worktree.path or record an accepted waiver with ' +
108
+ '`vibepro decision record --type waiver --source gate:managed_worktree --reason <reason>`.'
109
+ );
110
+ }
111
+
112
+ export function formatManagedWorktreePrStatus(gate) {
113
+ if (!gate) return 'unknown';
114
+ if (gate.status === 'passed') return 'passed';
115
+ if (gate.status === 'bypassed') return 'bypassed';
116
+ if (gate.status === 'not_applicable') return 'disabled';
117
+ if (gate.status === 'block') return 'needs_review';
118
+ return gate.status ?? 'unknown';
119
+ }
120
+
121
+ async function readExecutionStateIfExists(repoRoot, storyId) {
122
+ try {
123
+ return JSON.parse(await readFile(path.join(getWorkspaceDir(repoRoot), 'executions', storyId, 'state.json'), 'utf8'));
124
+ } catch (error) {
125
+ if (error.code === 'ENOENT') return null;
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ async function readBoundExecutionState(repoRoot, storyId) {
131
+ const localState = await readExecutionStateIfExists(repoRoot, storyId);
132
+ if (localState?.managed_worktree) return localState;
133
+
134
+ const worktrees = await listGitWorktrees(repoRoot);
135
+ for (const worktreePath of worktrees) {
136
+ if (path.resolve(worktreePath) === path.resolve(repoRoot)) continue;
137
+ const state = await readExecutionStateIfExists(worktreePath, storyId);
138
+ const managedPath = state?.managed_worktree?.path ? path.resolve(state.managed_worktree.path) : null;
139
+ if (managedPath && isSameOrInside(path.resolve(repoRoot), managedPath)) return state;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ async function listGitWorktrees(repoRoot) {
145
+ try {
146
+ const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {
147
+ cwd: repoRoot,
148
+ encoding: 'utf8'
149
+ });
150
+ return stdout
151
+ .split('\n')
152
+ .map((line) => line.startsWith('worktree ') ? line.slice('worktree '.length) : null)
153
+ .filter(Boolean);
154
+ } catch {
155
+ return [];
156
+ }
157
+ }
158
+
159
+ async function gitOptional(repoRoot, args) {
160
+ try {
161
+ const { stdout } = await execFileAsync('git', args, {
162
+ cwd: repoRoot,
163
+ encoding: 'utf8'
164
+ });
165
+ return stdout.trim() || null;
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ function findAcceptedManagedWorktreeBypass(decisionRecords) {
172
+ const decisions = Array.isArray(decisionRecords?.decisions) ? decisionRecords.decisions : [];
173
+ return decisions.find((decision) => decision
174
+ && decision.type === 'waiver'
175
+ && decision.status === 'accepted'
176
+ && decision.source === 'gate:managed_worktree') ?? null;
177
+ }
178
+
179
+ function isSameOrInside(candidate, parent) {
180
+ const relative = path.relative(parent, candidate);
181
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
182
+ }
183
+
184
+ function formatPath(filePath) {
185
+ if (!filePath) return '-';
186
+ return filePath;
187
+ }