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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- 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
|
+
}
|