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,766 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, readFile, realpath, rename, writeFile } 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 { MANIFEST_FILE, getWorkspaceDir, toWorkspaceRelative } from './workspace.js';
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const VALID_MODES = new Set(['required', 'preferred', 'disabled']);
11
+
12
+ export async function resolveManagedWorktreeMode(repoRoot) {
13
+ const config = await readConfig(repoRoot);
14
+ const mode = config?.execution?.managed_worktree ?? 'disabled';
15
+ return VALID_MODES.has(mode) ? mode : 'disabled';
16
+ }
17
+
18
+ export async function ensureManagedWorktree(repoRoot, options = {}) {
19
+ const root = path.resolve(repoRoot);
20
+ const mode = options.mode ?? await resolveManagedWorktreeMode(root);
21
+ if (mode === 'disabled') {
22
+ return {
23
+ mode,
24
+ status: 'disabled',
25
+ required: false,
26
+ source_repo: root,
27
+ path: null,
28
+ branch: null,
29
+ base_ref: options.baseRef ?? null,
30
+ created_from_sha: null,
31
+ current_head_sha: null,
32
+ dirty: null,
33
+ dirty_fingerprint: null
34
+ };
35
+ }
36
+
37
+ const storyId = options.storyId;
38
+ if (!storyId) throw new Error('managed worktree requires storyId');
39
+ const baseRef = options.baseRef ?? 'HEAD';
40
+ const createdFromSha = await gitOptional(root, ['rev-parse', baseRef]);
41
+ const shortId = buildShortId(storyId, createdFromSha || baseRef);
42
+ const worktreePath = path.resolve(options.worktreePath ?? path.join(root, '.worktrees', 'vibepro', `${storyId}-${shortId}`));
43
+ const branch = options.branchName ?? `vibepro/${storyId}-${shortId}`;
44
+ const existing = await findWorktree(root, worktreePath);
45
+
46
+ if (!existing) {
47
+ await mkdir(path.dirname(worktreePath), { recursive: true });
48
+ try {
49
+ await git(root, ['worktree', 'add', worktreePath, '-b', branch, baseRef]);
50
+ } catch (error) {
51
+ return buildUnavailableManagedWorktree({
52
+ mode,
53
+ root,
54
+ worktreePath,
55
+ branch,
56
+ baseRef,
57
+ createdFromSha,
58
+ reason: normalizeErrorMessage(error)
59
+ });
60
+ }
61
+ } else if (!isBranchMatch(existing.branch, branch)) {
62
+ throw new Error(`managed worktree branch mismatch at ${worktreePath}: expected ${branch}, found ${existing.branch ?? 'detached'}`);
63
+ }
64
+ await copyWorkspaceControlFiles(root, worktreePath);
65
+ await ensureManagedWorktreeGitExclude(worktreePath);
66
+
67
+ const currentHeadSha = await gitOptional(worktreePath, ['rev-parse', 'HEAD']);
68
+ const actualBranch = await gitOptional(worktreePath, ['branch', '--show-current']);
69
+ const dirty = await collectDirty(worktreePath);
70
+ return {
71
+ mode,
72
+ status: existing ? 'reused' : 'created',
73
+ required: mode === 'required',
74
+ source_repo: root,
75
+ source_relative_path: toWorkspaceRelative(root, root),
76
+ path: worktreePath,
77
+ relative_path: toWorkspaceRelative(root, worktreePath),
78
+ branch,
79
+ actual_branch: actualBranch || existing?.branch || null,
80
+ branch_match: isBranchMatch(actualBranch || existing?.branch, branch),
81
+ base_ref: baseRef,
82
+ created_from_sha: createdFromSha || null,
83
+ current_head_sha: currentHeadSha || null,
84
+ dirty: dirty.dirty,
85
+ dirty_fingerprint: dirty.fingerprint
86
+ };
87
+ }
88
+
89
+ export async function buildPendingManagedWorktree(repoRoot, options = {}) {
90
+ const root = path.resolve(repoRoot);
91
+ const mode = options.mode ?? await resolveManagedWorktreeMode(root);
92
+ if (mode === 'disabled') {
93
+ return {
94
+ mode,
95
+ status: 'disabled',
96
+ required: false,
97
+ source_repo: root,
98
+ path: null,
99
+ branch: null,
100
+ base_ref: options.baseRef ?? null,
101
+ created_from_sha: null,
102
+ current_head_sha: null,
103
+ dirty: null,
104
+ dirty_fingerprint: null
105
+ };
106
+ }
107
+
108
+ const storyId = options.storyId;
109
+ if (!storyId) throw new Error('managed worktree requires storyId');
110
+ const baseRef = options.baseRef ?? 'HEAD';
111
+ const createdFromSha = await gitOptional(root, ['rev-parse', baseRef]);
112
+ const shortId = buildShortId(storyId, createdFromSha || baseRef);
113
+ const worktreePath = path.resolve(options.worktreePath ?? path.join(root, '.worktrees', 'vibepro', `${storyId}-${shortId}`));
114
+ const branch = options.branchName ?? `vibepro/${storyId}-${shortId}`;
115
+ return {
116
+ mode,
117
+ status: 'missing',
118
+ required: mode === 'required',
119
+ source_repo: root,
120
+ source_relative_path: toWorkspaceRelative(root, root),
121
+ path: worktreePath,
122
+ relative_path: toWorkspaceRelative(root, worktreePath),
123
+ branch,
124
+ actual_branch: null,
125
+ branch_match: null,
126
+ base_ref: baseRef,
127
+ created_from_sha: createdFromSha || null,
128
+ current_head_sha: null,
129
+ dirty: null,
130
+ dirty_fingerprint: null
131
+ };
132
+ }
133
+
134
+ function buildUnavailableManagedWorktree({ mode, root, worktreePath, branch, baseRef, createdFromSha, reason }) {
135
+ return {
136
+ mode,
137
+ status: 'unavailable',
138
+ required: mode === 'required',
139
+ source_repo: root,
140
+ source_relative_path: toWorkspaceRelative(root, root),
141
+ path: worktreePath,
142
+ relative_path: toWorkspaceRelative(root, worktreePath),
143
+ branch,
144
+ actual_branch: null,
145
+ branch_match: false,
146
+ base_ref: baseRef,
147
+ created_from_sha: createdFromSha || null,
148
+ current_head_sha: null,
149
+ dirty: null,
150
+ dirty_fingerprint: null,
151
+ failure_reason: reason
152
+ };
153
+ }
154
+
155
+ export async function refreshManagedWorktree(repoRoot, managedWorktree) {
156
+ if (!managedWorktree?.path || managedWorktree.mode === 'disabled') return managedWorktree ?? null;
157
+ const root = path.resolve(repoRoot);
158
+ const worktreePath = path.resolve(managedWorktree.path);
159
+ const existing = await findWorktree(root, worktreePath);
160
+ const currentHeadSha = await gitOptional(worktreePath, ['rev-parse', 'HEAD']);
161
+ if (currentHeadSha || existing) await ensureManagedWorktreeGitExclude(worktreePath);
162
+ const actualBranch = await gitOptional(worktreePath, ['branch', '--show-current']) || existing?.branch || null;
163
+ const dirty = await collectDirty(worktreePath);
164
+ const exists = Boolean(currentHeadSha || existing);
165
+ const branchMatch = isBranchMatch(actualBranch, managedWorktree.branch);
166
+ const availableStatus = ['created', 'reused'].includes(managedWorktree.status)
167
+ ? managedWorktree.status
168
+ : 'available';
169
+ const missingStatus = managedWorktree.status === 'unavailable' && managedWorktree.failure_reason
170
+ ? 'unavailable'
171
+ : 'missing';
172
+ return {
173
+ ...managedWorktree,
174
+ status: exists ? branchMatch ? availableStatus : 'branch_mismatch' : missingStatus,
175
+ actual_branch: actualBranch,
176
+ branch_match: branchMatch,
177
+ current_head_sha: currentHeadSha || managedWorktree.current_head_sha || null,
178
+ dirty: dirty.dirty,
179
+ dirty_fingerprint: dirty.fingerprint
180
+ };
181
+ }
182
+
183
+ export function buildManagedWorktreeCommands(commands, managedWorktree, options = {}) {
184
+ if (!isManagedWorktreeCommandSafe(managedWorktree, options)) return commands;
185
+ return Object.fromEntries(Object.entries(commands).map(([key, command]) => [
186
+ key,
187
+ `cd ${shellQuote(managedWorktree.path)} && ${command}`
188
+ ]));
189
+ }
190
+
191
+ export function isManagedWorktreeCommandSafe(managedWorktree, options = {}) {
192
+ if (!managedWorktree?.path || managedWorktree.mode === 'disabled') return false;
193
+ if (!['created', 'reused', 'available'].includes(managedWorktree.status)) return false;
194
+ if (managedWorktree.branch_match === false) return false;
195
+ if (options.expectedHeadSha && managedWorktree.current_head_sha && managedWorktree.current_head_sha !== options.expectedHeadSha) return false;
196
+ return true;
197
+ }
198
+
199
+ export async function evaluateManagedWorktreeCommandContext(repoRoot, options = {}) {
200
+ const root = path.resolve(repoRoot);
201
+ const storyId = options.storyId;
202
+ if (!storyId) {
203
+ const mode = await resolveManagedWorktreeMode(root);
204
+ const currentHeadSha = await gitOptional(root, ['rev-parse', 'HEAD']);
205
+ const actualRoot = await canonicalPath(root);
206
+ return {
207
+ status: mode === 'required' ? 'blocked' : mode === 'preferred' ? 'needs_review' : 'not_applicable',
208
+ mode,
209
+ required: mode === 'required',
210
+ reason: mode === 'disabled'
211
+ ? 'managed worktree mode is disabled'
212
+ : 'story id is required to evaluate managed worktree locality before protected commands',
213
+ command_name: options.commandName ?? null,
214
+ repo_root: root,
215
+ actual_root: actualRoot,
216
+ expected_root: null,
217
+ expected_head_sha: options.expectedHeadSha ?? currentHeadSha ?? null,
218
+ current_head_sha: currentHeadSha,
219
+ managed_worktree: null
220
+ };
221
+ }
222
+ const state = await readManagedExecutionState(root, storyId);
223
+ const configuredMode = await resolveManagedWorktreeModeForState(root, state);
224
+ if (!state?.managed_worktree) {
225
+ const currentHeadSha = await gitOptional(root, ['rev-parse', 'HEAD']);
226
+ const actualRoot = await canonicalPath(root);
227
+ return {
228
+ status: configuredMode === 'required' ? 'blocked' : configuredMode === 'preferred' ? 'needs_review' : 'not_applicable',
229
+ mode: configuredMode,
230
+ required: configuredMode === 'required',
231
+ reason: configuredMode === 'disabled'
232
+ ? 'managed worktree mode is disabled'
233
+ : 'no managed worktree execution state is recorded for this checkout; run vibepro execute start before managed worktree protected commands',
234
+ command_name: options.commandName ?? null,
235
+ repo_root: root,
236
+ actual_root: actualRoot,
237
+ expected_root: null,
238
+ expected_head_sha: options.expectedHeadSha ?? currentHeadSha ?? null,
239
+ current_head_sha: currentHeadSha,
240
+ managed_worktree: null
241
+ };
242
+ }
243
+ if (configuredMode === 'disabled') {
244
+ return {
245
+ status: 'not_applicable',
246
+ mode: configuredMode,
247
+ required: false,
248
+ reason: 'managed worktree mode is disabled',
249
+ command_name: options.commandName ?? null,
250
+ repo_root: root,
251
+ actual_root: await canonicalPath(root),
252
+ expected_root: null,
253
+ expected_head_sha: null,
254
+ current_head_sha: await gitOptional(root, ['rev-parse', 'HEAD']),
255
+ managed_worktree: {
256
+ ...state.managed_worktree,
257
+ mode: configuredMode,
258
+ required: false
259
+ }
260
+ };
261
+ }
262
+ const managedWorktree = {
263
+ ...await refreshManagedWorktree(root, state.managed_worktree).catch(() => state.managed_worktree),
264
+ mode: configuredMode,
265
+ required: configuredMode === 'required'
266
+ };
267
+ const actualRoot = await canonicalPath(root);
268
+ const expectedRoot = managedWorktree.path ? await canonicalPath(managedWorktree.path) : null;
269
+ const localityMatches = Boolean(expectedRoot && actualRoot === expectedRoot);
270
+ const currentHeadSha = await gitOptional(root, ['rev-parse', 'HEAD']);
271
+ const expectedHeadSha = options.expectedHeadSha ?? currentHeadSha ?? null;
272
+ const headMatches = !expectedHeadSha || !managedWorktree.current_head_sha || managedWorktree.current_head_sha === expectedHeadSha;
273
+ const branchMatches = managedWorktree.branch_match !== false;
274
+ const status = localityMatches && branchMatches && headMatches ? 'satisfied' : configuredMode === 'required' ? 'blocked' : 'needs_review';
275
+ return {
276
+ status,
277
+ mode: configuredMode,
278
+ required: configuredMode === 'required',
279
+ reason: buildManagedWorktreeContextReason({
280
+ commandName: options.commandName,
281
+ localityMatches,
282
+ branchMatches,
283
+ headMatches,
284
+ expectedHeadSha,
285
+ currentHeadSha,
286
+ actualRoot,
287
+ expectedRoot,
288
+ managedWorktree
289
+ }),
290
+ command_name: options.commandName ?? null,
291
+ repo_root: root,
292
+ actual_root: actualRoot,
293
+ expected_root: expectedRoot,
294
+ expected_head_sha: expectedHeadSha,
295
+ current_head_sha: currentHeadSha,
296
+ managed_worktree: managedWorktree
297
+ };
298
+ }
299
+
300
+ export async function readManagedExecutionState(repoRoot, storyId) {
301
+ const root = path.resolve(repoRoot);
302
+ const localState = await readExecutionState(root, storyId);
303
+ if (localState?.managed_worktree) return localState;
304
+ const linkedState = await findLinkedExecutionState(root, storyId);
305
+ return linkedState ?? localState;
306
+ }
307
+
308
+ async function resolveManagedWorktreeModeForState(root, state) {
309
+ const sourceRepo = state?.managed_worktree?.source_repo;
310
+ if (sourceRepo) {
311
+ return resolveManagedWorktreeMode(sourceRepo);
312
+ }
313
+ return resolveManagedWorktreeMode(root);
314
+ }
315
+
316
+ export async function assertManagedWorktreeCommandAllowed(repoRoot, options = {}) {
317
+ const context = await evaluateManagedWorktreeCommandContext(repoRoot, options);
318
+ if (context.status === 'blocked') {
319
+ const bypass = options.storyId
320
+ ? findAcceptedManagedWorktreeBypass(await readDecisionRecordsIfExists(path.resolve(repoRoot), options.storyId))
321
+ : null;
322
+ if (bypass) {
323
+ return {
324
+ ...context,
325
+ status: 'bypassed',
326
+ reason: `accepted bypass decision recorded: ${bypass.reason ?? bypass.summary ?? bypass.decision_id}`,
327
+ decision_id: bypass.decision_id
328
+ };
329
+ }
330
+ throw new Error(`managed worktree required for ${options.commandName ?? 'this command'}: ${context.reason}`);
331
+ }
332
+ return context;
333
+ }
334
+
335
+ function findAcceptedManagedWorktreeBypass(decisionRecords) {
336
+ const decisions = Array.isArray(decisionRecords?.decisions) ? decisionRecords.decisions : [];
337
+ return decisions.find((decision) => decision
338
+ && decision.type === 'waiver'
339
+ && decision.status === 'accepted'
340
+ && decision.source === 'gate:managed_worktree') ?? null;
341
+ }
342
+
343
+ export function buildManagedWorktreeCommandWarning(context) {
344
+ if (context?.status !== 'needs_review') return null;
345
+ const binding = buildManagedWorktreeCommandBinding(context);
346
+ return {
347
+ id: 'managed_worktree_locality',
348
+ status: binding.status,
349
+ mode: binding.mode,
350
+ required: false,
351
+ command_name: binding.command_name,
352
+ reason: binding.reason,
353
+ action: 'Run the command from the recorded VibePro managed worktree, update the managed worktree to the current HEAD, or explicitly disable managed_worktree for this repository.',
354
+ repo_root: binding.repo_root,
355
+ actual_root: binding.actual_root,
356
+ expected_root: binding.expected_root,
357
+ expected_head_sha: binding.expected_head_sha,
358
+ current_head_sha: binding.current_head_sha,
359
+ managed_worktree: binding.managed_worktree
360
+ };
361
+ }
362
+
363
+ export function buildManagedWorktreeCommandBinding(context) {
364
+ if (!context || context.status === 'not_applicable') return null;
365
+ return {
366
+ status: context.status,
367
+ mode: context.mode,
368
+ required: context.required === true,
369
+ command_name: context.command_name ?? null,
370
+ reason: context.reason ?? null,
371
+ repo_root: context.repo_root ?? null,
372
+ actual_root: context.actual_root ?? null,
373
+ expected_root: context.expected_root ?? null,
374
+ expected_head_sha: context.expected_head_sha ?? null,
375
+ current_head_sha: context.current_head_sha ?? null,
376
+ managed_worktree: context.managed_worktree ? {
377
+ source_repo: context.managed_worktree.source_repo ?? null,
378
+ path: context.managed_worktree.path ?? null,
379
+ branch: context.managed_worktree.branch ?? null,
380
+ actual_branch: context.managed_worktree.actual_branch ?? null,
381
+ current_head_sha: context.managed_worktree.current_head_sha ?? null,
382
+ dirty: context.managed_worktree.dirty ?? null,
383
+ dirty_fingerprint: context.managed_worktree.dirty_fingerprint ?? null
384
+ } : null
385
+ };
386
+ }
387
+
388
+ export function buildExecutionDag({ managedWorktree, completedPhases = [], completionStatus = 'not_prepared', expectedHeadSha = null, prMerge = null }) {
389
+ const hasWorktree = Boolean(managedWorktree?.path && managedWorktree.mode !== 'disabled');
390
+ const worktreeAvailable = ['created', 'reused', 'available'].includes(managedWorktree?.status);
391
+ const branchBound = worktreeAvailable && managedWorktree.branch && managedWorktree.branch_match !== false;
392
+ const headBound = branchBound
393
+ && (!expectedHeadSha || !managedWorktree.current_head_sha || managedWorktree.current_head_sha === expectedHeadSha);
394
+ const mergeReady = prMerge?.status === 'ready_to_merge' || prMerge?.status === 'merged';
395
+ const merged = prMerge?.status === 'merged' || Boolean(prMerge?.merged_at || prMerge?.merge_commit_sha);
396
+ const nodes = [
397
+ {
398
+ id: 'story_selected',
399
+ status: 'passed',
400
+ required: true,
401
+ reason: 'Story id is bound to this execution state'
402
+ },
403
+ {
404
+ id: 'worktree_created',
405
+ status: managedWorktree?.mode === 'disabled'
406
+ ? 'not_applicable'
407
+ : worktreeAvailable
408
+ ? 'passed'
409
+ : managedWorktree?.mode === 'required'
410
+ ? 'blocked'
411
+ : 'needs_evidence',
412
+ required: managedWorktree?.mode === 'required',
413
+ reason: managedWorktree?.mode === 'disabled'
414
+ ? 'managed worktree mode is disabled'
415
+ : worktreeAvailable
416
+ ? 'VibePro managed worktree is available'
417
+ : managedWorktree?.status === 'branch_mismatch'
418
+ ? 'VibePro managed worktree branch does not match the recorded branch'
419
+ : managedWorktree?.status === 'unavailable'
420
+ ? `VibePro managed worktree could not be created: ${managedWorktree.failure_reason ?? 'unknown error'}`
421
+ : 'VibePro managed worktree is missing',
422
+ evidence: hasWorktree ? {
423
+ path: managedWorktree.path,
424
+ branch: managedWorktree.branch,
425
+ actual_branch: managedWorktree.actual_branch ?? null,
426
+ failure_reason: managedWorktree.failure_reason ?? null
427
+ } : null
428
+ },
429
+ {
430
+ id: 'branch_bound',
431
+ status: branchBound ? 'passed' : managedWorktree?.mode === 'disabled' ? 'not_applicable' : 'needs_evidence',
432
+ required: managedWorktree?.mode === 'required',
433
+ reason: branchBound
434
+ ? 'managed branch is recorded and matches the worktree branch'
435
+ : hasWorktree && managedWorktree.branch_match === false
436
+ ? 'managed branch does not match the worktree branch'
437
+ : 'no managed branch recorded',
438
+ evidence: hasWorktree ? { branch: managedWorktree.branch, actual_branch: managedWorktree.actual_branch ?? null, head_sha: managedWorktree.current_head_sha } : null
439
+ },
440
+ {
441
+ id: 'head_bound',
442
+ status: managedWorktree?.mode === 'disabled'
443
+ ? 'not_applicable'
444
+ : headBound
445
+ ? 'passed'
446
+ : managedWorktree?.mode === 'required'
447
+ ? 'blocked'
448
+ : 'needs_evidence',
449
+ required: managedWorktree?.mode === 'required',
450
+ reason: headBound
451
+ ? 'managed worktree HEAD matches the current execution HEAD'
452
+ : hasWorktree && expectedHeadSha && managedWorktree.current_head_sha
453
+ ? 'managed worktree HEAD does not match the current execution HEAD'
454
+ : 'managed worktree HEAD binding is not recorded',
455
+ evidence: hasWorktree ? {
456
+ head_sha: managedWorktree.current_head_sha,
457
+ expected_head_sha: expectedHeadSha
458
+ } : null
459
+ },
460
+ {
461
+ id: 'implementation_started',
462
+ status: branchBound || managedWorktree?.mode === 'disabled' ? 'passed' : 'pending',
463
+ required: false,
464
+ reason: branchBound
465
+ ? 'managed branch is ready for implementation work'
466
+ : managedWorktree?.mode === 'disabled'
467
+ ? 'implementation starts in the current checkout because managed worktree mode is disabled'
468
+ : 'implementation has not started in a bound managed branch yet'
469
+ },
470
+ {
471
+ id: 'implementation_complete',
472
+ status: completedPhases.length > 0 || ['ready_for_pr_create', 'pr_created'].includes(completionStatus) ? 'passed' : 'pending',
473
+ required: false,
474
+ reason: completedPhases.length > 0 || ['ready_for_pr_create', 'pr_created'].includes(completionStatus)
475
+ ? 'implementation has produced PR preparation or verification evidence'
476
+ : 'implementation completion evidence has not been recorded yet'
477
+ },
478
+ {
479
+ id: 'verification_recorded',
480
+ status: completedPhases.includes('verify') ? 'passed' : 'pending',
481
+ required: false,
482
+ reason: completedPhases.includes('verify') ? 'verification evidence exists' : 'verification evidence has not been recorded yet'
483
+ },
484
+ {
485
+ id: 'agent_review_recorded',
486
+ status: completedPhases.includes('agent_review') ? 'passed' : 'pending',
487
+ required: false,
488
+ reason: completedPhases.includes('agent_review') ? 'required agent review evidence is complete' : 'agent review is not complete yet'
489
+ },
490
+ {
491
+ id: 'pr_prepare_ready',
492
+ status: completedPhases.includes('ready_for_pr_create') ? 'passed' : 'pending',
493
+ required: true,
494
+ reason: completedPhases.includes('ready_for_pr_create') ? 'Gate DAG is ready for PR creation' : 'PR prepare is not ready yet'
495
+ },
496
+ {
497
+ id: 'pr_created',
498
+ status: completionStatus === 'pr_created' ? 'passed' : 'pending',
499
+ required: true,
500
+ reason: completionStatus === 'pr_created' ? 'PR URL is recorded' : 'PR has not been created yet'
501
+ },
502
+ {
503
+ id: 'merge_ready',
504
+ status: completionStatus === 'merged'
505
+ ? 'passed'
506
+ : mergeReady
507
+ ? 'passed'
508
+ : prMerge?.status === 'blocked'
509
+ ? 'blocked'
510
+ : completionStatus === 'pr_created'
511
+ ? 'pending'
512
+ : 'not_applicable',
513
+ required: false,
514
+ reason: completionStatus === 'merged'
515
+ ? 'merge preconditions were satisfied before the recorded merge'
516
+ : mergeReady
517
+ ? 'execute merge preconditions were satisfied for the current PR'
518
+ : prMerge?.status === 'blocked'
519
+ ? prMerge.stop_reason ?? 'execute merge recorded blocking preconditions'
520
+ : completionStatus === 'pr_created'
521
+ ? 'PR exists but execute merge has not recorded merge readiness yet'
522
+ : 'PR has not been created yet'
523
+ },
524
+ {
525
+ id: 'merged_or_closed',
526
+ status: merged ? 'passed' : completionStatus === 'pr_created' ? 'pending' : 'not_applicable',
527
+ required: false,
528
+ reason: merged
529
+ ? 'merge commit and merged_at are recorded'
530
+ : completionStatus === 'pr_created'
531
+ ? 'PR is still open or merge result has not been recorded yet'
532
+ : 'PR has not been created yet'
533
+ },
534
+ {
535
+ id: 'worktree_cleaned',
536
+ status: 'not_applicable',
537
+ required: false,
538
+ reason: 'managed worktree cleanup is outside this MVP implementation scope'
539
+ }
540
+ ];
541
+ return {
542
+ schema_version: '0.1.0',
543
+ nodes,
544
+ edges: [
545
+ ['story_selected', 'worktree_created'],
546
+ ['worktree_created', 'branch_bound'],
547
+ ['branch_bound', 'head_bound'],
548
+ ['head_bound', 'implementation_started'],
549
+ ['implementation_started', 'implementation_complete'],
550
+ ['implementation_complete', 'verification_recorded'],
551
+ ['verification_recorded', 'agent_review_recorded'],
552
+ ['agent_review_recorded', 'pr_prepare_ready'],
553
+ ['pr_prepare_ready', 'pr_created'],
554
+ ['pr_created', 'merge_ready'],
555
+ ['merge_ready', 'merged_or_closed'],
556
+ ['merged_or_closed', 'worktree_cleaned']
557
+ ].map(([from, to]) => ({ from, to }))
558
+ };
559
+ }
560
+
561
+ async function readConfig(repoRoot) {
562
+ try {
563
+ return JSON.parse(await readFile(path.join(getWorkspaceDir(repoRoot), 'config.json'), 'utf8'));
564
+ } catch (error) {
565
+ if (error.code === 'ENOENT') return null;
566
+ throw error;
567
+ }
568
+ }
569
+
570
+ async function readExecutionState(repoRoot, storyId) {
571
+ const filePath = path.join(getWorkspaceDir(repoRoot), 'executions', storyId, 'state.json');
572
+ try {
573
+ return JSON.parse(await readFile(filePath, 'utf8'));
574
+ } catch (error) {
575
+ if (error.code === 'ENOENT') return null;
576
+ if (error instanceof SyntaxError) {
577
+ const backupPath = `${filePath}.corrupt-${Date.now()}-${process.pid}.bak`;
578
+ await rename(filePath, backupPath);
579
+ throw new Error(`execution state JSON is corrupt: ${toWorkspaceRelative(repoRoot, filePath)}. Moved it to ${toWorkspaceRelative(repoRoot, backupPath)}.`);
580
+ }
581
+ throw error;
582
+ }
583
+ }
584
+
585
+ async function findLinkedExecutionState(repoRoot, storyId) {
586
+ const root = path.resolve(repoRoot);
587
+ const rootRealpath = await canonicalPath(root);
588
+ const output = await gitOptional(root, ['worktree', 'list', '--porcelain']);
589
+ if (!output) return null;
590
+ for (const item of parseWorktreeList(output)) {
591
+ const candidateRoot = path.resolve(item.path);
592
+ if (await canonicalPath(candidateRoot) === rootRealpath) continue;
593
+ const candidate = await readExecutionState(candidateRoot, storyId);
594
+ if (!candidate?.managed_worktree?.path) continue;
595
+ const managedPath = await canonicalPath(candidate.managed_worktree.path);
596
+ const sourceRepo = candidate.managed_worktree.source_repo
597
+ ? await canonicalPath(candidate.managed_worktree.source_repo)
598
+ : null;
599
+ if (managedPath === rootRealpath || sourceRepo === rootRealpath) return candidate;
600
+ }
601
+ return null;
602
+ }
603
+
604
+ function buildManagedWorktreeContextReason({
605
+ commandName,
606
+ localityMatches,
607
+ branchMatches,
608
+ headMatches,
609
+ expectedHeadSha,
610
+ currentHeadSha,
611
+ actualRoot,
612
+ expectedRoot,
613
+ managedWorktree
614
+ }) {
615
+ const label = commandName ?? 'command';
616
+ if (localityMatches && branchMatches && headMatches) {
617
+ return `${label} is running inside the recorded managed worktree`;
618
+ }
619
+ const issues = [];
620
+ if (!localityMatches) issues.push(`repo root ${actualRoot} is not recorded managed worktree ${expectedRoot ?? '-'}`);
621
+ if (!branchMatches) issues.push(`managed branch mismatch: expected ${managedWorktree?.branch ?? '-'}, found ${managedWorktree?.actual_branch ?? '-'}`);
622
+ if (!headMatches) issues.push(`managed worktree HEAD ${managedWorktree?.current_head_sha ?? '-'} does not match expected HEAD ${expectedHeadSha ?? currentHeadSha ?? '-'}`);
623
+ return issues.join('; ');
624
+ }
625
+
626
+ async function copyWorkspaceControlFiles(repoRoot, worktreePath) {
627
+ const targetDir = getWorkspaceDir(worktreePath);
628
+ await mkdir(targetDir, { recursive: true });
629
+ await copyWorkspaceJsonFile(repoRoot, targetDir, 'config.json', { required: true });
630
+ await copyWorkspaceJsonFile(repoRoot, targetDir, MANIFEST_FILE, { required: true });
631
+ }
632
+
633
+ async function copyWorkspaceJsonFile(repoRoot, targetDir, fileName, options = {}) {
634
+ const source = path.join(getWorkspaceDir(repoRoot), fileName);
635
+ try {
636
+ const parsed = JSON.parse(await readFile(source, 'utf8'));
637
+ await writeFile(path.join(targetDir, fileName), `${JSON.stringify(parsed, null, 2)}\n`);
638
+ } catch (error) {
639
+ if (error.code === 'ENOENT' && !options.required) return;
640
+ throw error;
641
+ }
642
+ }
643
+
644
+ async function ensureManagedWorktreeGitExclude(worktreePath) {
645
+ const excludePathText = await gitOptional(worktreePath, ['rev-parse', '--git-path', 'info/exclude']);
646
+ if (!excludePathText) return;
647
+ const excludePath = path.isAbsolute(excludePathText)
648
+ ? excludePathText
649
+ : path.resolve(worktreePath, excludePathText);
650
+ await mkdir(path.dirname(excludePath), { recursive: true });
651
+ const required = [
652
+ '# VibePro managed worktree control files',
653
+ '/.vibepro/config.json',
654
+ `/.vibepro/${MANIFEST_FILE}`,
655
+ '/.vibepro/executions/'
656
+ ];
657
+
658
+ let existing = '';
659
+ try {
660
+ existing = await readFile(excludePath, 'utf8');
661
+ } catch (error) {
662
+ if (error.code !== 'ENOENT') throw error;
663
+ }
664
+
665
+ const missing = required.filter((line) => !existing.includes(line));
666
+ if (missing.length === 0) return;
667
+ const prefix = existing.trim().length > 0 ? `${existing.trimEnd()}\n` : '';
668
+ await writeFile(excludePath, `${prefix}${missing.join('\n')}\n`);
669
+ }
670
+
671
+ async function findWorktree(repoRoot, worktreePath) {
672
+ const output = await gitOptional(repoRoot, ['worktree', 'list', '--porcelain']);
673
+ if (!output) return null;
674
+ const normalized = await canonicalPath(worktreePath);
675
+ for (const item of parseWorktreeList(output)) {
676
+ const itemRealpath = await canonicalPath(item.path);
677
+ if (item.path === path.resolve(worktreePath) || item.path === normalized || itemRealpath === normalized) {
678
+ return { ...item, realpath: itemRealpath };
679
+ }
680
+ }
681
+ return null;
682
+ }
683
+
684
+ function parseWorktreeList(output) {
685
+ const entries = [];
686
+ let current = null;
687
+ for (const line of output.split('\n')) {
688
+ if (!line.trim()) {
689
+ if (current) entries.push(current);
690
+ current = null;
691
+ continue;
692
+ }
693
+ if (line.startsWith('worktree ')) {
694
+ if (current) entries.push(current);
695
+ const worktreePath = line.slice('worktree '.length);
696
+ current = { path: path.resolve(worktreePath), realpath: path.resolve(worktreePath), branch: null, head: null };
697
+ continue;
698
+ }
699
+ if (!current) continue;
700
+ if (line.startsWith('HEAD ')) current.head = line.slice('HEAD '.length);
701
+ if (line.startsWith('branch ')) current.branch = normalizeBranchName(line.slice('branch '.length));
702
+ }
703
+ if (current) entries.push(current);
704
+ return entries;
705
+ }
706
+
707
+ async function canonicalPath(filePath) {
708
+ const resolved = path.resolve(filePath);
709
+ try {
710
+ return path.resolve(await realpath(resolved));
711
+ } catch {
712
+ return resolved;
713
+ }
714
+ }
715
+
716
+ async function collectDirty(repoRoot) {
717
+ const status = await gitOptional(repoRoot, ['status', '--porcelain', '-uall']);
718
+ const lines = status ? status.split('\n').filter(Boolean) : [];
719
+ return {
720
+ dirty: lines.length > 0,
721
+ fingerprint: lines.length === 0 ? 'clean' : lines.join('\n')
722
+ };
723
+ }
724
+
725
+ function buildShortId(storyId, seed) {
726
+ const text = `${storyId}:${seed}`;
727
+ let hash = 0;
728
+ for (let i = 0; i < text.length; i += 1) {
729
+ hash = ((hash << 5) - hash + text.charCodeAt(i)) >>> 0;
730
+ }
731
+ return hash.toString(36).slice(0, 6);
732
+ }
733
+
734
+ function normalizeBranchName(branch) {
735
+ if (!branch) return null;
736
+ return branch.startsWith('refs/heads/') ? branch.slice('refs/heads/'.length) : branch;
737
+ }
738
+
739
+ function isBranchMatch(actual, expected) {
740
+ if (!expected) return true;
741
+ return normalizeBranchName(actual) === normalizeBranchName(expected);
742
+ }
743
+
744
+ function normalizeErrorMessage(error) {
745
+ const message = error?.stderr || error?.message || String(error);
746
+ return message.trim().split('\n').filter(Boolean).slice(-1)[0] ?? 'unknown error';
747
+ }
748
+
749
+ async function git(repoRoot, args) {
750
+ await execFileAsync('git', args, { cwd: repoRoot, encoding: 'utf8' });
751
+ }
752
+
753
+ async function gitOptional(repoRoot, args) {
754
+ try {
755
+ const { stdout } = await execFileAsync('git', args, { cwd: repoRoot, encoding: 'utf8' });
756
+ return stdout.trim();
757
+ } catch {
758
+ return '';
759
+ }
760
+ }
761
+
762
+ function shellQuote(value) {
763
+ const text = String(value);
764
+ if (/^[a-zA-Z0-9_./:=@+-]+$/.test(text)) return text;
765
+ return `'${text.replaceAll("'", "'\\''")}'`;
766
+ }