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,849 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { cp, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ import { getAgentReviewStatus } from './agent-review.js';
7
+ import {
8
+ buildExecutionDag,
9
+ buildManagedWorktreeCommands,
10
+ buildPendingManagedWorktree,
11
+ ensureManagedWorktree,
12
+ isManagedWorktreeCommandSafe,
13
+ readManagedExecutionState,
14
+ refreshManagedWorktree
15
+ } from './managed-worktree.js';
16
+ import { getWorkspaceDir, toWorkspaceRelative } from './workspace.js';
17
+
18
+ const SCHEMA_VERSION = '0.1.0';
19
+ const DEFAULT_TARGET = 'pr_create';
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ export async function startExecution(repoRoot, options = {}) {
23
+ const storyId = requireStoryId(options.storyId, 'execute start');
24
+ await assertWorkspaceInitialized(repoRoot, 'execute start');
25
+ const existing = await readManagedExecutionState(repoRoot, storyId);
26
+ const managedWorktree = await ensureManagedWorktree(repoRoot, {
27
+ storyId,
28
+ baseRef: options.baseRef,
29
+ branchName: options.branchName,
30
+ worktreePath: options.worktreePath
31
+ });
32
+ const state = await buildExecutionState(repoRoot, {
33
+ ...options,
34
+ storyId,
35
+ target: options.target ?? DEFAULT_TARGET,
36
+ startedAt: existing?.started_at,
37
+ managedWorktree,
38
+ preserveStartedAt: true
39
+ });
40
+ return writeExecutionStateWithLinkedCopies(repoRoot, state);
41
+ }
42
+
43
+ export async function getExecutionStatus(repoRoot, options = {}) {
44
+ const storyId = requireStoryId(options.storyId, 'execute status');
45
+ const existing = await readManagedExecutionState(repoRoot, storyId);
46
+ if (existing) {
47
+ const managedWorktree = await refreshManagedWorktree(repoRoot, existing.managed_worktree).catch(() => existing.managed_worktree ?? null);
48
+ const state = await buildExecutionState(repoRoot, {
49
+ ...options,
50
+ storyId,
51
+ target: options.target ?? existing.target ?? DEFAULT_TARGET,
52
+ startedAt: existing.started_at,
53
+ managedWorktree,
54
+ preserveStartedAt: true
55
+ });
56
+ return {
57
+ state,
58
+ artifact: toWorkspaceRelative(repoRoot, getExecutionStatePath(repoRoot, storyId)),
59
+ found: true
60
+ };
61
+ }
62
+ const managedWorktree = await buildPendingManagedWorktree(repoRoot, {
63
+ storyId,
64
+ baseRef: options.baseRef,
65
+ branchName: options.branchName,
66
+ worktreePath: options.worktreePath
67
+ });
68
+ const state = await buildExecutionState(repoRoot, {
69
+ ...options,
70
+ storyId,
71
+ target: options.target ?? DEFAULT_TARGET,
72
+ managedWorktree
73
+ });
74
+ return {
75
+ state,
76
+ artifact: toWorkspaceRelative(repoRoot, getExecutionStatePath(repoRoot, storyId)),
77
+ found: false
78
+ };
79
+ }
80
+
81
+ export async function getExecutionNext(repoRoot, options = {}) {
82
+ const result = await getExecutionStatus(repoRoot, options);
83
+ return {
84
+ ...result,
85
+ next: {
86
+ completion_status: result.state.completion_status,
87
+ current_phase: result.state.current_phase,
88
+ blocking_gate: result.state.blocking_gate,
89
+ next_actions: result.state.next_actions,
90
+ managed_worktree: result.state.managed_worktree,
91
+ execution_dag: result.state.execution_dag
92
+ }
93
+ };
94
+ }
95
+
96
+ export async function reconcileExecutionState(repoRoot, options = {}) {
97
+ const storyId = requireStoryId(options.storyId, 'execute reconcile');
98
+ await assertWorkspaceInitialized(repoRoot, 'execute reconcile');
99
+ const existing = await readManagedExecutionState(repoRoot, storyId);
100
+ const state = await buildExecutionState(repoRoot, {
101
+ ...options,
102
+ storyId,
103
+ target: options.target ?? existing?.target ?? DEFAULT_TARGET,
104
+ startedAt: existing?.started_at,
105
+ managedWorktree: await refreshManagedWorktree(repoRoot, existing?.managed_worktree).catch(() => existing?.managed_worktree ?? null),
106
+ preserveStartedAt: true
107
+ });
108
+ return writeExecutionStateWithLinkedCopies(repoRoot, state);
109
+ }
110
+
111
+ export async function updateExecutionStateFromPrPrepare(repoRoot, prepareResult, options = {}) {
112
+ const storyId = prepareResult?.preparation?.story?.story_id ?? options.storyId;
113
+ if (!storyId) return null;
114
+ if (prepareResult?.preparation?.workspace?.initialized !== true) return null;
115
+ return reconcileExecutionState(repoRoot, {
116
+ ...options,
117
+ storyId,
118
+ target: options.target ?? DEFAULT_TARGET
119
+ });
120
+ }
121
+
122
+ export async function updateExecutionStateFromPrCreate(repoRoot, createResult, options = {}) {
123
+ const storyId = createResult?.execution?.story?.story_id ?? options.storyId;
124
+ if (!storyId) return null;
125
+ if (createResult?.execution?.workspace_initialized !== true) return null;
126
+ const result = await reconcileExecutionState(repoRoot, {
127
+ ...options,
128
+ storyId,
129
+ target: options.target ?? DEFAULT_TARGET
130
+ });
131
+ const execution = createResult?.execution;
132
+ if (!execution || execution.dry_run) return result;
133
+ const state = {
134
+ ...result.state,
135
+ completion_status: execution.pr_url ? 'pr_created' : result.state.completion_status,
136
+ current_phase: execution.pr_url ? 'complete' : result.state.current_phase,
137
+ completed_phases: execution.pr_url
138
+ ? unique([...result.state.completed_phases, 'create_pr'])
139
+ : result.state.completed_phases,
140
+ pr_url: execution.pr_url ?? result.state.pr_url ?? null,
141
+ next_actions: execution.pr_url ? [] : result.state.next_actions,
142
+ blocking_gate: execution.pr_url ? null : result.state.blocking_gate,
143
+ updated_at: new Date().toISOString()
144
+ };
145
+ return writeExecutionStateWithLinkedCopies(repoRoot, state);
146
+ }
147
+
148
+ export async function updateExecutionStateFromPrMerge(repoRoot, mergeResult, options = {}) {
149
+ const storyId = mergeResult?.merge?.story?.story_id ?? options.storyId;
150
+ if (!storyId) return null;
151
+ const result = await reconcileExecutionState(repoRoot, {
152
+ ...options,
153
+ storyId,
154
+ target: options.target ?? DEFAULT_TARGET
155
+ });
156
+ const merge = mergeResult?.merge;
157
+ if (!merge || merge.status !== 'merged') return result;
158
+ const state = {
159
+ ...result.state,
160
+ completion_status: 'merged',
161
+ current_phase: 'complete',
162
+ completed_phases: unique([...result.state.completed_phases, 'merge_ready', 'merge']),
163
+ pr_url: merge.pr?.url ?? result.state.pr_url ?? null,
164
+ next_actions: [],
165
+ blocking_gate: null,
166
+ updated_at: new Date().toISOString()
167
+ };
168
+ return writeExecutionStateWithLinkedCopies(repoRoot, state);
169
+ }
170
+
171
+ export function renderExecutionStateSummary(result) {
172
+ const state = result.state ?? result;
173
+ const actions = state.next_actions?.length
174
+ ? state.next_actions.map((action) => `- ${action}`).join('\n')
175
+ : '- none';
176
+ const managedWorktree = formatManagedWorktreeSummary(state.managed_worktree);
177
+ const executionDag = formatExecutionDagSummary(state.execution_dag);
178
+ return `# VibePro Execution State
179
+
180
+ - story: ${state.story_id}
181
+ - target: ${state.target}
182
+ - status: ${state.completion_status}
183
+ - phase: ${state.current_phase}
184
+ - blocking_gate: ${state.blocking_gate?.id ?? 'none'}
185
+ - managed_worktree: ${managedWorktree.headline}
186
+ - execution_dag: ${executionDag.headline}
187
+ - artifact: ${result.artifact ?? '-'}
188
+
189
+ ## Managed Worktree
190
+
191
+ ${managedWorktree.details}
192
+
193
+ ## Execution DAG
194
+
195
+ ${executionDag.details}
196
+
197
+ ## Next Actions
198
+
199
+ ${actions}
200
+ `;
201
+ }
202
+
203
+ export function renderExecutionNextSummary(result) {
204
+ const next = result.next ?? result;
205
+ const state = result.state ?? result;
206
+ const actions = next.next_actions?.length
207
+ ? next.next_actions.map((action) => `- ${action}`).join('\n')
208
+ : '- none';
209
+ const managedWorktree = formatManagedWorktreeSummary(state.managed_worktree);
210
+ const executionDag = formatExecutionDagSummary(state.execution_dag);
211
+ return `# VibePro Next Action
212
+
213
+ - status: ${next.completion_status}
214
+ - phase: ${next.current_phase}
215
+ - blocking_gate: ${next.blocking_gate?.id ?? 'none'}
216
+ - managed_worktree: ${managedWorktree.headline}
217
+ - execution_dag: ${executionDag.headline}
218
+
219
+ ## Managed Worktree
220
+
221
+ ${managedWorktree.details}
222
+
223
+ ## Execution DAG
224
+
225
+ ${executionDag.details}
226
+
227
+ ${actions}
228
+ `;
229
+ }
230
+
231
+ function formatManagedWorktreeSummary(managedWorktree) {
232
+ if (!managedWorktree) {
233
+ return {
234
+ headline: 'not_recorded',
235
+ details: '- status: not_recorded'
236
+ };
237
+ }
238
+ const headline = `${managedWorktree.mode ?? 'unknown'}/${managedWorktree.status ?? 'unknown'}`;
239
+ return {
240
+ headline,
241
+ details: [
242
+ `- mode: ${managedWorktree.mode ?? '-'}`,
243
+ `- status: ${managedWorktree.status ?? '-'}`,
244
+ `- path: ${managedWorktree.path ?? '-'}`,
245
+ `- branch: ${managedWorktree.branch ?? '-'}`,
246
+ `- actual_branch: ${managedWorktree.actual_branch ?? '-'}`,
247
+ `- current_head_sha: ${managedWorktree.current_head_sha ?? '-'}`,
248
+ `- branch_match: ${managedWorktree.branch_match === false ? 'false' : managedWorktree.branch_match === true ? 'true' : '-'}`,
249
+ `- dirty: ${managedWorktree.dirty === true ? 'true' : managedWorktree.dirty === false ? 'false' : '-'}`
250
+ ].join('\n')
251
+ };
252
+ }
253
+
254
+ function formatExecutionDagSummary(executionDag) {
255
+ const nodes = Array.isArray(executionDag?.nodes) ? executionDag.nodes : [];
256
+ if (nodes.length === 0) {
257
+ return {
258
+ headline: 'not_recorded',
259
+ details: '- nodes: none'
260
+ };
261
+ }
262
+ const blockers = nodes.filter((node) => ['blocked', 'needs_evidence', 'failed'].includes(node.status));
263
+ return {
264
+ headline: `${nodes.length} nodes, ${blockers.length} blockers`,
265
+ details: nodes
266
+ .map((node) => `- ${node.id}: ${node.status}${node.reason ? ` (${node.reason})` : ''}`)
267
+ .join('\n')
268
+ };
269
+ }
270
+
271
+ async function buildExecutionState(repoRoot, options = {}) {
272
+ const root = path.resolve(repoRoot);
273
+ const storyId = requireStoryId(options.storyId, 'execution state');
274
+ const now = new Date().toISOString();
275
+ const [prPrepare, verificationEvidence, prCreate, prMerge, gateDagArtifact, agentReview] = await Promise.all([
276
+ readJsonIfExists(path.join(getWorkspaceDir(root), 'pr', storyId, 'pr-prepare.json')),
277
+ readJsonIfExists(path.join(getWorkspaceDir(root), 'pr', storyId, 'verification-evidence.json')),
278
+ readJsonIfExists(path.join(getWorkspaceDir(root), 'pr', storyId, 'pr-create.json')),
279
+ readJsonIfExists(path.join(getWorkspaceDir(root), 'pr', storyId, 'pr-merge.json')),
280
+ readJsonIfExists(path.join(getWorkspaceDir(root), 'pr', storyId, 'gate-dag.json')),
281
+ getAgentReviewStatus(root, { storyId }).catch(() => null)
282
+ ]);
283
+ const gateStatus = prPrepare?.gate_status ?? null;
284
+ const gateDag = gateDagArtifact ?? prPrepare?.pr_context?.gate_dag ?? prCreate?.gate_dag ?? null;
285
+ const unresolvedGates = collectUnresolvedRequiredGates(gateDag);
286
+ const blockingGates = unresolvedGates.filter(isCriticalUnresolvedGate);
287
+ const managedWorktree = options.managedWorktree
288
+ ? await refreshManagedWorktree(root, options.managedWorktree).catch(() => options.managedWorktree)
289
+ : null;
290
+ const currentHeadSha = await gitOptional(root, ['rev-parse', 'HEAD']);
291
+ const expectedHeadSha = await resolveExecutionExpectedHead(root, managedWorktree, currentHeadSha);
292
+ const executionBlockers = collectRequiredExecutionBlockers(
293
+ buildExecutionDag({
294
+ managedWorktree,
295
+ completedPhases: [],
296
+ completionStatus: 'not_prepared',
297
+ expectedHeadSha
298
+ }),
299
+ { storyId, baseRef: options.baseRef, managedWorktree, expectedHeadSha }
300
+ );
301
+ const executionBlockingGate = executionBlockers[0] ?? null;
302
+ const blockingGate = executionBlockingGate ?? pickBlockingGate(blockingGates);
303
+ const prCreated = Boolean(prCreate?.pr_url && prCreate?.dry_run !== true);
304
+ const merged = prMerge?.status === 'merged' || Boolean(prMerge?.merged_at || prMerge?.merge_commit_sha);
305
+ const gatesReadyForPrCreate = gateDag
306
+ ? Boolean(prPrepare && unresolvedGates.length === 0)
307
+ : gateStatus?.ready_for_pr_create === true && gateStatus?.execution_gate?.status !== 'waiver_required';
308
+ const readyForPrCreate = gatesReadyForPrCreate && !executionBlockingGate;
309
+ const waiverRequired = !prCreated && !readyForPrCreate && Boolean(prPrepare) && (
310
+ executionBlockingGate
311
+ ? false
312
+ : gateDag
313
+ ? unresolvedGates.length > 0 && blockingGates.length === 0
314
+ : gateStatus?.execution_gate?.status === 'waiver_required'
315
+ );
316
+ const completionStatus = merged
317
+ ? 'merged'
318
+ : prCreated
319
+ ? 'pr_created'
320
+ : readyForPrCreate
321
+ ? 'ready_for_pr_create'
322
+ : waiverRequired
323
+ ? 'waiver_required'
324
+ : executionBlockingGate
325
+ ? 'blocked'
326
+ : prPrepare
327
+ ? 'blocked'
328
+ : 'not_prepared';
329
+ const currentPhase = merged
330
+ ? 'complete'
331
+ : prCreated
332
+ ? 'complete'
333
+ : readyForPrCreate
334
+ ? 'create_pr'
335
+ : waiverRequired
336
+ ? 'verification'
337
+ : executionBlockingGate
338
+ ? 'prepare_pr'
339
+ : blockingGate?.id === 'gate:agent_review' || blockingGate?.id?.startsWith('review:')
340
+ ? 'agent_review'
341
+ : blockingGate
342
+ ? 'verification'
343
+ : 'prepare_pr';
344
+ const completedPhases = deriveCompletedPhases({
345
+ prPrepare,
346
+ verificationEvidence,
347
+ agentReview,
348
+ readyForPrCreate,
349
+ prCreated,
350
+ merged,
351
+ prMerge
352
+ });
353
+ const requiredCommands = buildManagedWorktreeCommands({
354
+ pr_prepare: buildPrPrepareCommand({ storyId, baseRef: options.baseRef }),
355
+ pr_create: buildPrCreateCommand({ storyId, baseRef: options.baseRef })
356
+ }, managedWorktree, { expectedHeadSha });
357
+ const nextActions = deriveNextActions({
358
+ storyId,
359
+ baseRef: options.baseRef,
360
+ managedWorktree,
361
+ expectedHeadSha,
362
+ prPrepare,
363
+ gateStatus,
364
+ unresolvedGates,
365
+ blockingGate,
366
+ waiverRequired,
367
+ readyForPrCreate,
368
+ prCreated,
369
+ merged
370
+ });
371
+ return {
372
+ schema_version: SCHEMA_VERSION,
373
+ story_id: storyId,
374
+ target: options.target ?? DEFAULT_TARGET,
375
+ started_at: options.preserveStartedAt ? (options.startedAt ?? now) : now,
376
+ updated_at: now,
377
+ current_phase: currentPhase,
378
+ completed_phases: completedPhases,
379
+ completion_status: completionStatus,
380
+ blocking_gate: blockingGate,
381
+ next_actions: nextActions,
382
+ required_commands: requiredCommands,
383
+ managed_worktree: managedWorktree,
384
+ execution_dag: buildExecutionDag({ managedWorktree, completedPhases, completionStatus, expectedHeadSha, prMerge }),
385
+ last_pr_prepare: prPrepare ? summarizePrPrepare(root, prPrepare) : null,
386
+ last_review_status: agentReview ? summarizeAgentReview(agentReview) : null,
387
+ last_verification_evidence: verificationEvidence ? summarizeVerificationEvidence(root, verificationEvidence) : null,
388
+ pr_url: prCreate?.pr_url ?? null
389
+ };
390
+ }
391
+
392
+ async function resolveExecutionExpectedHead(root, managedWorktree, currentHeadSha) {
393
+ if (!currentHeadSha || !managedWorktree?.current_head_sha || !managedWorktree?.path) return currentHeadSha;
394
+ const currentRoot = path.resolve(root);
395
+ const managedRoot = path.resolve(managedWorktree.path);
396
+ if (currentRoot === managedRoot) return currentHeadSha;
397
+ if (await gitIsAncestor(root, currentHeadSha, managedWorktree.current_head_sha)) {
398
+ return managedWorktree.current_head_sha;
399
+ }
400
+ return currentHeadSha;
401
+ }
402
+
403
+ function deriveCompletedPhases({ prPrepare, verificationEvidence, agentReview, readyForPrCreate, prCreated, merged, prMerge }) {
404
+ const phases = [];
405
+ if (prPrepare) phases.push('prepare_pr');
406
+ if ((verificationEvidence?.commands ?? []).length > 0) phases.push('verify');
407
+ if (agentReview?.summary?.required_review_count > 0 && agentReview.summary.unmet_required_review_count === 0) {
408
+ phases.push('agent_review');
409
+ }
410
+ if (readyForPrCreate) phases.push('ready_for_pr_create');
411
+ if (prCreated) phases.push('create_pr');
412
+ if (prMerge?.status === 'ready_to_merge' || prMerge?.status === 'merged') phases.push('merge_ready');
413
+ if (merged) phases.push('merge');
414
+ return phases;
415
+ }
416
+
417
+ function deriveNextActions({ storyId, baseRef, managedWorktree, expectedHeadSha, prPrepare, gateStatus, unresolvedGates = [], blockingGate, waiverRequired, readyForPrCreate, prCreated, merged }) {
418
+ const wrap = (command) => isManagedWorktreeCommandSafe(managedWorktree, { expectedHeadSha })
419
+ ? `cd ${shellQuote(managedWorktree.path)} && ${command}`
420
+ : command;
421
+ const routeAction = (action) => routeActionThroughManagedWorktree(action, wrap);
422
+ const wrapActions = (actions) => actions.map(routeAction);
423
+ if (merged) return [];
424
+ if (prCreated) return [wrap(buildExecuteMergeCommand({ storyId, baseRef }))];
425
+ if (!prPrepare && managedWorktree?.status === 'missing' && managedWorktree.mode !== 'disabled') {
426
+ return [buildExecuteStartCommand({ storyId, baseRef })];
427
+ }
428
+ if (!prPrepare) return [wrap(buildPrPrepareCommand({ storyId, baseRef }))];
429
+ if (readyForPrCreate) return [wrap(buildPrCreateCommand({ storyId, baseRef }))];
430
+ if (waiverRequired) {
431
+ const actions = unresolvedGates
432
+ .flatMap((gate) => gate.required_actions?.length ? gate.required_actions : [gate.reason])
433
+ .filter(Boolean);
434
+ if (actions.length > 0) return wrapActions(actions);
435
+ return ['Resolve unresolved non-critical gates or rerun PR create with an explicit verification waiver.'];
436
+ }
437
+ if (blockingGate) {
438
+ if (blockingGate.required_actions?.length) return wrapActions(blockingGate.required_actions);
439
+ return [blockingGate.reason ?? `Resolve ${blockingGate.label ?? blockingGate.id}`];
440
+ }
441
+ const actions = gateStatus?.next_required_actions?.length
442
+ ? gateStatus.next_required_actions
443
+ : gateStatus?.execution_gate?.required_actions ?? [];
444
+ if (actions.length > 0) return wrapActions(actions);
445
+ return [wrap(buildPrPrepareCommand({ storyId, baseRef }))];
446
+ }
447
+
448
+ function routeActionThroughManagedWorktree(action, wrap) {
449
+ const text = String(action ?? '');
450
+ if (/^vibepro\s+/.test(text.trim())) return wrap(text);
451
+ return text.replace(/`(vibepro\s+[^`]+)`/g, (_match, command) => `\`${wrap(command)}\``);
452
+ }
453
+
454
+ function summarizePrPrepare(root, prPrepare) {
455
+ return {
456
+ artifact: toWorkspaceRelative(root, path.join(getWorkspaceDir(root), 'pr', prPrepare.story?.story_id ?? prPrepare.story_id ?? 'unknown', 'pr-prepare.json')),
457
+ created_at: prPrepare.created_at ?? null,
458
+ overall_status: prPrepare.gate_status?.overall_status ?? prPrepare.pr_context?.gate_dag?.overall_status ?? null,
459
+ ready_for_pr_create: prPrepare.gate_status?.ready_for_pr_create === true,
460
+ head_sha: prPrepare.git?.head_sha ?? prPrepare.git?.head_ref ?? null
461
+ };
462
+ }
463
+
464
+ function summarizeAgentReview(agentReview) {
465
+ return {
466
+ status: agentReview.summary?.overall_status ?? agentReview.status ?? null,
467
+ required_review_count: agentReview.summary?.required_review_count ?? 0,
468
+ unmet_required_review_count: agentReview.summary?.unmet_required_review_count ?? 0,
469
+ stages: (agentReview.stages ?? []).map((stage) => ({
470
+ stage: stage.stage,
471
+ status: stage.status,
472
+ missing_count: stage.missing_count,
473
+ stale_count: stage.stale_count,
474
+ block_count: stage.block_count,
475
+ unverified_agent_count: stage.unverified_agent_count
476
+ }))
477
+ };
478
+ }
479
+
480
+ function summarizeVerificationEvidence(root, evidence) {
481
+ return {
482
+ artifact: toWorkspaceRelative(root, path.join(getWorkspaceDir(root), 'pr', evidence.story_id, 'verification-evidence.json')),
483
+ updated_at: evidence.updated_at ?? null,
484
+ command_count: (evidence.commands ?? []).length,
485
+ kinds: unique((evidence.commands ?? []).map((command) => command.kind).filter(Boolean))
486
+ };
487
+ }
488
+
489
+ function collectUnresolvedRequiredGates(gateDag) {
490
+ const nodes = Array.isArray(gateDag?.nodes) ? gateDag.nodes : [];
491
+ return nodes
492
+ .filter((node) => [
493
+ 'story',
494
+ 'pr_route_gate',
495
+ 'pr_body_contract_gate',
496
+ 'mirror_source_traceability_gate',
497
+ 'ci_status_or_waiver_gate',
498
+ 'vibepro_artifact_policy_gate',
499
+ 'split_resolution_gate',
500
+ 'managed_worktree_gate',
501
+ 'architecture_gate',
502
+ 'spec_gate',
503
+ 'decision_record_gate',
504
+ 'verification_gate',
505
+ 'requirement_gate',
506
+ 'visual_qa_gate',
507
+ 'design_quality_gate',
508
+ 'workflow_heavy_gate',
509
+ 'pr_freshness_gate',
510
+ 'agent_review_prepare_gate',
511
+ 'agent_review_role_gate',
512
+ 'agent_review_record_gate',
513
+ 'agent_review_stage_join_gate',
514
+ 'agent_review_gate'
515
+ ].includes(node.type))
516
+ .filter((node) => node.required)
517
+ .filter((node) => isUnresolvedStatus(node.status));
518
+ }
519
+
520
+ function collectRequiredExecutionBlockers(executionDag, { storyId, baseRef, managedWorktree, expectedHeadSha }) {
521
+ const nodes = Array.isArray(executionDag?.nodes) ? executionDag.nodes : [];
522
+ const blockers = nodes
523
+ .filter((node) => node.required)
524
+ .filter((node) => ['blocked', 'needs_evidence', 'failed'].includes(node.status))
525
+ .map((node) => ({
526
+ id: `execution:${node.id}`,
527
+ label: node.id,
528
+ status: node.status,
529
+ reason: node.reason ?? null,
530
+ required_actions: buildExecutionBlockerActions(node, { storyId, baseRef, managedWorktree })
531
+ }));
532
+ return blockers;
533
+ }
534
+
535
+ function buildExecutionBlockerActions(node, { storyId, baseRef, managedWorktree }) {
536
+ const executeStart = buildExecuteStartCommand({ storyId, baseRef });
537
+ if (node.id === 'worktree_created' && managedWorktree?.status === 'branch_mismatch') {
538
+ return [
539
+ `Restore ${managedWorktree.path} to ${managedWorktree.branch} or rerun ${executeStart} with a clean managed worktree path before PR preparation.`
540
+ ];
541
+ }
542
+ if (node.id === 'branch_bound' && managedWorktree?.branch_match === false) {
543
+ return [
544
+ `Resolve the managed worktree branch mismatch (${managedWorktree.actual_branch ?? 'detached'} != ${managedWorktree.branch}) before running PR preparation.`
545
+ ];
546
+ }
547
+ if (node.id === 'head_bound' && managedWorktree?.current_head_sha) {
548
+ return [
549
+ `Update the managed worktree at ${managedWorktree.path} to the current execution HEAD before PR preparation.`
550
+ ];
551
+ }
552
+ if (node.id === 'worktree_created') {
553
+ return [
554
+ `Run ${executeStart} to create or rebind the VibePro managed worktree before PR preparation.`
555
+ ];
556
+ }
557
+ return [
558
+ `Resolve Execution DAG node ${node.id} before PR preparation.`
559
+ ];
560
+ }
561
+
562
+ function isUnresolvedStatus(status) {
563
+ return [
564
+ 'candidate',
565
+ 'missing',
566
+ 'transient',
567
+ 'implicit',
568
+ 'inferred_empty',
569
+ 'needs_evidence',
570
+ 'needs_setup',
571
+ 'needs_review',
572
+ 'needs_rebase',
573
+ 'needs_changes',
574
+ 'contradicted',
575
+ 'stale',
576
+ 'block',
577
+ 'failed'
578
+ ].includes(status);
579
+ }
580
+
581
+ function pickBlockingGate(gates) {
582
+ if (!gates.length) return null;
583
+ const preferred = gates.find((gate) => gate.id === 'gate:agent_review')
584
+ ?? gates.find((gate) => gate.id?.startsWith('review:'))
585
+ ?? gates.find((gate) => gate.status === 'failed' || gate.status === 'block' || gate.status === 'contradicted')
586
+ ?? gates[0];
587
+ return {
588
+ id: preferred.id,
589
+ label: preferred.label ?? preferred.id,
590
+ status: preferred.status,
591
+ reason: preferred.reason ?? null,
592
+ required_actions: preferred.required_actions ?? []
593
+ };
594
+ }
595
+
596
+ function isCriticalUnresolvedGate(gate) {
597
+ if (gate.id === 'story' && gate.status === 'transient') return true;
598
+ if (gate.id === 'architecture' && gate.status === 'needs_review') return true;
599
+ if (gate.id === 'spec' && ['implicit', 'inferred_empty', 'needs_review'].includes(gate.status)) return true;
600
+ if (gate.id === 'gate:e2e' && gate.status !== 'passed') return true;
601
+ if (gate.id === 'gate:visual_qa' && gate.status !== 'ready_for_review') return true;
602
+ if (gate.id === 'gate:design_quality' && gate.status !== 'ready_for_review') return true;
603
+ if (gate.id === 'gate:requirement' && ['needs_review', 'contradicted'].includes(gate.status)) return true;
604
+ if (gate.id === 'gate:network_contract' && gate.status !== 'passed') return true;
605
+ if (gate.id === 'gate:pr_route_classification' && gate.status !== 'passed') return true;
606
+ if (gate.id === 'gate:pr_body_contract' && gate.status !== 'passed') return true;
607
+ if (gate.id === 'gate:mirror_source_traceability' && gate.status !== 'passed') return true;
608
+ if (gate.id === 'gate:ci_status_or_waiver' && gate.status !== 'passed') return true;
609
+ if (gate.id === 'gate:vibepro_artifact_policy' && gate.status !== 'passed') return true;
610
+ if (gate.id === 'gate:split_resolution' && gate.status !== 'passed') return true;
611
+ if (gate.id === 'gate:managed_worktree' && gate.required && gate.status !== 'satisfied') return true;
612
+ if (gate.id === 'gate:decision_record' && gate.status !== 'passed') return true;
613
+ if (gate.id === 'gate:pr_freshness' && gate.status !== 'passed') return true;
614
+ if (gate.type === 'workflow_heavy_gate' && gate.status !== 'passed') return true;
615
+ if (gate.id === 'gate:agent_review' && gate.status !== 'passed') return true;
616
+ return gate.status === 'failed' || gate.status === 'contradicted';
617
+ }
618
+
619
+ async function writeExecutionState(repoRoot, state) {
620
+ const root = path.resolve(repoRoot);
621
+ const filePath = getExecutionStatePath(root, state.story_id);
622
+ await mkdir(path.dirname(filePath), { recursive: true });
623
+ await writeJsonAtomic(filePath, state);
624
+ return {
625
+ state,
626
+ artifact: toWorkspaceRelative(root, filePath),
627
+ found: true
628
+ };
629
+ }
630
+
631
+ async function writeExecutionStateWithLinkedCopies(repoRoot, state) {
632
+ const result = await writeExecutionState(repoRoot, state);
633
+ await writeLinkedExecutionStateCopies(repoRoot, state);
634
+ await syncManagedWorktreeArtifactsToSource(repoRoot, state);
635
+ return result;
636
+ }
637
+
638
+ async function writeLinkedExecutionStateCopies(repoRoot, state) {
639
+ const currentRoot = path.resolve(repoRoot);
640
+ const targets = collectLinkedExecutionRoots(state)
641
+ .filter((target) => path.resolve(target) !== currentRoot);
642
+ for (const target of targets) {
643
+ const filePath = getExecutionStatePath(target, state.story_id);
644
+ await mkdir(path.dirname(filePath), { recursive: true });
645
+ await writeJsonAtomic(filePath, state);
646
+ }
647
+ }
648
+
649
+ async function syncManagedWorktreeArtifactsToSource(repoRoot, state) {
650
+ const currentRoot = path.resolve(repoRoot);
651
+ const managedPath = state?.managed_worktree?.path ? path.resolve(state.managed_worktree.path) : null;
652
+ const sourceRepo = state?.managed_worktree?.source_repo ? path.resolve(state.managed_worktree.source_repo) : null;
653
+ if (!managedPath || !sourceRepo || currentRoot !== managedPath || sourceRepo === currentRoot) return;
654
+ const workspace = getWorkspaceDir(currentRoot);
655
+ const sourceWorkspace = getWorkspaceDir(sourceRepo);
656
+ await copyDirectoryIfExists(
657
+ path.join(workspace, 'pr', state.story_id),
658
+ path.join(sourceWorkspace, 'pr', state.story_id)
659
+ );
660
+ await copyDirectoryIfExists(
661
+ path.join(workspace, 'reviews', state.story_id),
662
+ path.join(sourceWorkspace, 'reviews', state.story_id)
663
+ );
664
+ await copyDirectoryIfExists(
665
+ path.join(workspace, 'verification'),
666
+ path.join(sourceWorkspace, 'verification')
667
+ );
668
+ await mergeManifestFileIfExists(
669
+ path.join(workspace, 'vibepro-manifest.json'),
670
+ path.join(sourceWorkspace, 'vibepro-manifest.json')
671
+ );
672
+ }
673
+
674
+ function collectLinkedExecutionRoots(state) {
675
+ if (!state?.managed_worktree || state.managed_worktree.mode === 'disabled') return [];
676
+ return unique([
677
+ state.managed_worktree.path,
678
+ state.managed_worktree.source_repo
679
+ ].filter(Boolean).map((target) => path.resolve(target)));
680
+ }
681
+
682
+ async function copyDirectoryIfExists(source, target) {
683
+ try {
684
+ await cp(source, target, { recursive: true, force: true });
685
+ } catch (error) {
686
+ if (error.code === 'ENOENT') return;
687
+ throw error;
688
+ }
689
+ }
690
+
691
+ async function copyFileIfExists(source, target) {
692
+ try {
693
+ await mkdir(path.dirname(target), { recursive: true });
694
+ await cp(source, target, { force: true });
695
+ } catch (error) {
696
+ if (error.code === 'ENOENT') return;
697
+ throw error;
698
+ }
699
+ }
700
+
701
+ async function mergeManifestFileIfExists(source, target) {
702
+ const managed = await readJsonIfExists(source);
703
+ if (!managed) return;
704
+ const existing = await readJsonIfExists(target);
705
+ const merged = mergeWorkspaceManifest(existing, managed);
706
+ await mkdir(path.dirname(target), { recursive: true });
707
+ await writeJsonAtomic(target, merged);
708
+ }
709
+
710
+ function mergeWorkspaceManifest(existing, managed) {
711
+ const merged = {
712
+ ...(existing ?? {}),
713
+ ...managed
714
+ };
715
+ if (existing?.artifacts || managed?.artifacts) {
716
+ merged.artifacts = {
717
+ ...(existing?.artifacts ?? {}),
718
+ ...(managed?.artifacts ?? {})
719
+ };
720
+ }
721
+ if (Array.isArray(existing?.flow_verification_runs) || Array.isArray(managed?.flow_verification_runs)) {
722
+ merged.flow_verification_runs = mergeRunsById(
723
+ existing?.flow_verification_runs ?? [],
724
+ managed?.flow_verification_runs ?? []
725
+ );
726
+ }
727
+ return merged;
728
+ }
729
+
730
+ function mergeRunsById(existingRuns, managedRuns) {
731
+ const byId = new Map();
732
+ const anonymous = [];
733
+ for (const run of existingRuns) {
734
+ if (run?.run_id) byId.set(run.run_id, run);
735
+ else anonymous.push(run);
736
+ }
737
+ for (const run of managedRuns) {
738
+ if (run?.run_id) byId.set(run.run_id, { ...(byId.get(run.run_id) ?? {}), ...run });
739
+ else anonymous.push(run);
740
+ }
741
+ return [...anonymous, ...byId.values()];
742
+ }
743
+
744
+ async function readExecutionState(repoRoot, storyId) {
745
+ const filePath = getExecutionStatePath(repoRoot, storyId);
746
+ try {
747
+ return JSON.parse(await readFile(filePath, 'utf8'));
748
+ } catch (error) {
749
+ if (error.code === 'ENOENT') return null;
750
+ if (error instanceof SyntaxError) {
751
+ const backupPath = `${filePath}.corrupt-${Date.now()}-${process.pid}.bak`;
752
+ await rename(filePath, backupPath);
753
+ throw new Error(`execution state JSON is corrupt: ${toWorkspaceRelative(repoRoot, filePath)}. Moved it to ${toWorkspaceRelative(repoRoot, backupPath)}.`);
754
+ }
755
+ throw error;
756
+ }
757
+ }
758
+
759
+ async function readJsonIfExists(filePath) {
760
+ try {
761
+ return JSON.parse(await readFile(filePath, 'utf8'));
762
+ } catch (error) {
763
+ if (error.code === 'ENOENT') return null;
764
+ throw error;
765
+ }
766
+ }
767
+
768
+ async function writeJsonAtomic(filePath, value) {
769
+ const dir = path.dirname(filePath);
770
+ const base = path.basename(filePath);
771
+ const tempPath = path.join(dir, `.${base}.${process.pid}.${Date.now()}.tmp`);
772
+ try {
773
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`);
774
+ await rename(tempPath, filePath);
775
+ } catch (error) {
776
+ await rm(tempPath, { force: true });
777
+ throw error;
778
+ }
779
+ }
780
+
781
+ async function gitOptional(repoRoot, args) {
782
+ try {
783
+ const result = await execFileAsync('git', args, { cwd: repoRoot, encoding: 'utf8' });
784
+ return result.stdout.trim() || null;
785
+ } catch {
786
+ return null;
787
+ }
788
+ }
789
+
790
+ async function gitIsAncestor(repoRoot, ancestor, descendant) {
791
+ if (!ancestor || !descendant) return false;
792
+ if (ancestor === descendant) return true;
793
+ try {
794
+ await execFileAsync('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot, encoding: 'utf8' });
795
+ return true;
796
+ } catch {
797
+ return false;
798
+ }
799
+ }
800
+
801
+ function getExecutionStatePath(repoRoot, storyId) {
802
+ return path.join(getWorkspaceDir(repoRoot), 'executions', storyId, 'state.json');
803
+ }
804
+
805
+ async function assertWorkspaceInitialized(repoRoot, commandName) {
806
+ try {
807
+ await readFile(path.join(getWorkspaceDir(repoRoot), 'config.json'), 'utf8');
808
+ } catch (error) {
809
+ if (error.code === 'ENOENT') {
810
+ throw new Error(`${commandName} requires an initialized VibePro workspace. Run \`vibepro init <repo>\` first.`);
811
+ }
812
+ throw error;
813
+ }
814
+ }
815
+
816
+ function buildPrPrepareCommand({ storyId, baseRef }) {
817
+ const base = baseRef ? ` --base ${shellQuote(baseRef)}` : ' --base <base-ref>';
818
+ return `vibepro pr prepare . --story-id ${shellQuote(storyId)}${base}`;
819
+ }
820
+
821
+ function buildPrCreateCommand({ storyId, baseRef }) {
822
+ const base = baseRef ? ` --base ${shellQuote(baseRef)}` : ' --base <base-ref>';
823
+ return `vibepro pr create . --story-id ${shellQuote(storyId)}${base}`;
824
+ }
825
+
826
+ function buildExecuteMergeCommand({ storyId, baseRef }) {
827
+ const base = baseRef ? ` --base ${shellQuote(baseRef)}` : ' --base <base-ref>';
828
+ return `vibepro execute merge . --story-id ${shellQuote(storyId)}${base}`;
829
+ }
830
+
831
+ function buildExecuteStartCommand({ storyId, baseRef }) {
832
+ const base = baseRef ? ` --base ${shellQuote(baseRef)}` : ' --base <base-ref>';
833
+ return `vibepro execute start . --story-id ${shellQuote(storyId)}${base}`;
834
+ }
835
+
836
+ function shellQuote(value) {
837
+ const text = String(value);
838
+ if (/^[a-zA-Z0-9_./:=@+-]+$/.test(text)) return text;
839
+ return `'${text.replaceAll("'", "'\\''")}'`;
840
+ }
841
+
842
+ function requireStoryId(storyId, commandName) {
843
+ if (!storyId) throw new Error(`${commandName} requires --story-id <story-id>`);
844
+ return storyId;
845
+ }
846
+
847
+ function unique(values) {
848
+ return [...new Set(values)];
849
+ }