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,501 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
import { renderPrMergeHtml } from './html-report.js';
|
|
8
|
+
import { getWorkspaceDir, readManifest, toWorkspaceRelative, writeManifest } from './workspace.js';
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const SUCCESSFUL_CHECK_CONCLUSIONS = new Set(['SUCCESS', 'SKIPPED', 'NEUTRAL']);
|
|
12
|
+
const VALID_MERGE_STRATEGIES = new Set(['merge', 'squash', 'rebase']);
|
|
13
|
+
|
|
14
|
+
export async function executeMerge(repoRoot, options = {}) {
|
|
15
|
+
const root = path.resolve(repoRoot);
|
|
16
|
+
const storyId = options.storyId;
|
|
17
|
+
if (!storyId) throw new Error('execute merge requires --story-id <id>');
|
|
18
|
+
|
|
19
|
+
const prDir = path.join(getWorkspaceDir(root), 'pr', storyId);
|
|
20
|
+
const [prPrepare, prCreate, executionState] = await Promise.all([
|
|
21
|
+
readJsonIfExists(path.join(prDir, 'pr-prepare.json')),
|
|
22
|
+
readJsonIfExists(path.join(prDir, 'pr-create.json')),
|
|
23
|
+
readJsonIfExists(path.join(getWorkspaceDir(root), 'executions', storyId, 'state.json'))
|
|
24
|
+
]);
|
|
25
|
+
const story = prCreate?.story ?? prPrepare?.story ?? executionState?.story ?? { story_id: storyId };
|
|
26
|
+
const strategy = normalizeMergeStrategy(options.strategy);
|
|
27
|
+
const deleteBranch = options.deleteBranch === true;
|
|
28
|
+
const dryRun = options.dryRun === true;
|
|
29
|
+
const currentHeadSha = await gitOptional(root, ['rev-parse', 'HEAD']);
|
|
30
|
+
const currentBranch = await gitOptional(root, ['branch', '--show-current']);
|
|
31
|
+
const nonWorkspaceDirtyFiles = await collectNonWorkspaceDirtyFiles(root);
|
|
32
|
+
const gateDag = prCreate?.gate_dag ?? prPrepare?.pr_context?.gate_dag ?? null;
|
|
33
|
+
const baseBranch = stripRemote(options.baseRef ?? prCreate?.base ?? prPrepare?.git?.base_ref ?? 'main');
|
|
34
|
+
const prSelector = options.pr ?? prCreate?.pr_url ?? executionState?.pr_url ?? null;
|
|
35
|
+
const repositorySlug = await resolveGitHubRepositorySlug(root, { prCreate, prPrepare, executionState });
|
|
36
|
+
const createdAt = new Date().toISOString();
|
|
37
|
+
const merge = {
|
|
38
|
+
schema_version: '0.1.0',
|
|
39
|
+
created_at: createdAt,
|
|
40
|
+
mode: 'execute_merge',
|
|
41
|
+
dry_run: dryRun,
|
|
42
|
+
workspace_initialized: true,
|
|
43
|
+
story,
|
|
44
|
+
output: prCreate?.output ?? prPrepare?.output ?? { language: 'ja' },
|
|
45
|
+
strategy,
|
|
46
|
+
delete_branch: deleteBranch,
|
|
47
|
+
base: baseBranch,
|
|
48
|
+
current_branch: currentBranch,
|
|
49
|
+
current_head_sha: currentHeadSha,
|
|
50
|
+
repository_slug: repositorySlug,
|
|
51
|
+
pr: {
|
|
52
|
+
selector: prSelector,
|
|
53
|
+
url: null,
|
|
54
|
+
state: null,
|
|
55
|
+
is_draft: null,
|
|
56
|
+
merge_state_status: null,
|
|
57
|
+
review_decision: null,
|
|
58
|
+
head_ref_name: null,
|
|
59
|
+
head_ref_oid: null,
|
|
60
|
+
base_ref_name: null,
|
|
61
|
+
checks: []
|
|
62
|
+
},
|
|
63
|
+
gate_dag: gateDag,
|
|
64
|
+
preconditions: {
|
|
65
|
+
pr_selector_resolved: Boolean(prSelector),
|
|
66
|
+
gate_ready: gateDag?.overall_status === 'ready_for_review',
|
|
67
|
+
clean_worktree: nonWorkspaceDirtyFiles.length === 0,
|
|
68
|
+
base_freshness: {
|
|
69
|
+
status: 'unknown',
|
|
70
|
+
required: true,
|
|
71
|
+
base_ref: baseBranch,
|
|
72
|
+
merge_base_contains_base: null,
|
|
73
|
+
fetched_ref: `origin/${baseBranch}`
|
|
74
|
+
},
|
|
75
|
+
remote_head_match: {
|
|
76
|
+
status: 'unknown',
|
|
77
|
+
required: true,
|
|
78
|
+
local_head_sha: currentHeadSha,
|
|
79
|
+
remote_head_sha: null
|
|
80
|
+
},
|
|
81
|
+
checks_ready: {
|
|
82
|
+
status: 'unknown',
|
|
83
|
+
required: true,
|
|
84
|
+
pending_count: 0,
|
|
85
|
+
failing_count: 0
|
|
86
|
+
},
|
|
87
|
+
review_policy: {
|
|
88
|
+
status: 'unknown',
|
|
89
|
+
required: true
|
|
90
|
+
},
|
|
91
|
+
open_pull_request: {
|
|
92
|
+
status: 'unknown',
|
|
93
|
+
required: true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
warnings: [],
|
|
97
|
+
commands: [],
|
|
98
|
+
results: [],
|
|
99
|
+
branch_cleanup: {
|
|
100
|
+
requested: deleteBranch,
|
|
101
|
+
remote: {
|
|
102
|
+
attempted: false,
|
|
103
|
+
deleted: false,
|
|
104
|
+
command: null
|
|
105
|
+
},
|
|
106
|
+
local: {
|
|
107
|
+
attempted: false,
|
|
108
|
+
deleted: false,
|
|
109
|
+
command: null
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
status: 'blocked',
|
|
113
|
+
stop_reason: null,
|
|
114
|
+
merge_commit_sha: null,
|
|
115
|
+
merged_at: null
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!prSelector) {
|
|
119
|
+
merge.stop_reason = 'pr_selector_missing';
|
|
120
|
+
merge.warnings.push('PR selector could not be resolved from --pr, pr-create artifact, or execution state.');
|
|
121
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
122
|
+
return { merge, artifacts };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
merge.commands.push(formatCommand(['git', ['fetch', 'origin', baseBranch]]));
|
|
126
|
+
merge.commands.push(formatCommand(['gh', buildPrViewArgs(prSelector, repositorySlug, PR_VIEW_FIELDS)]));
|
|
127
|
+
merge.commands.push(formatCommand(['gh', buildMergeArgs(prSelector, strategy, repositorySlug, currentHeadSha)]));
|
|
128
|
+
if (deleteBranch) {
|
|
129
|
+
merge.commands.push(formatCommand(['git', ['push', 'origin', '--delete', currentBranch || 'HEAD']]));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fetchResult = await runCommand(root, ['git', ['fetch', 'origin', baseBranch]], options);
|
|
133
|
+
merge.results.push(fetchResult);
|
|
134
|
+
if (fetchResult.exit_code !== 0) {
|
|
135
|
+
merge.stop_reason = 'base_fetch_failed';
|
|
136
|
+
merge.preconditions.base_freshness.status = 'blocked';
|
|
137
|
+
merge.error = `Command failed: ${fetchResult.command}`;
|
|
138
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
139
|
+
return { merge, artifacts };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const containsBase = await gitIsAncestor(root, `origin/${baseBranch}`, currentHeadSha);
|
|
143
|
+
merge.preconditions.base_freshness.status = containsBase ? 'passed' : 'blocked';
|
|
144
|
+
merge.preconditions.base_freshness.merge_base_contains_base = containsBase;
|
|
145
|
+
|
|
146
|
+
const prViewResult = await runCommand(root, ['gh', buildPrViewArgs(prSelector, repositorySlug, PR_VIEW_FIELDS)], options);
|
|
147
|
+
merge.results.push(prViewResult);
|
|
148
|
+
if (prViewResult.exit_code !== 0) {
|
|
149
|
+
merge.stop_reason = 'pr_view_failed';
|
|
150
|
+
merge.error = `Command failed: ${prViewResult.command}`;
|
|
151
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
152
|
+
return { merge, artifacts };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const prView = JSON.parse(prViewResult.stdout || '{}');
|
|
156
|
+
const checks = normalizeChecks(prView.statusCheckRollup);
|
|
157
|
+
const checkSummary = summarizeChecks(checks);
|
|
158
|
+
merge.pr = {
|
|
159
|
+
selector: prSelector,
|
|
160
|
+
url: prView.url ?? ((typeof prSelector === 'string' && /^https?:\/\//.test(prSelector)) ? prSelector : null),
|
|
161
|
+
state: prView.state ?? null,
|
|
162
|
+
is_draft: prView.isDraft ?? null,
|
|
163
|
+
merge_state_status: prView.mergeStateStatus ?? null,
|
|
164
|
+
review_decision: prView.reviewDecision ?? null,
|
|
165
|
+
head_ref_name: prView.headRefName ?? null,
|
|
166
|
+
head_ref_oid: prView.headRefOid ?? null,
|
|
167
|
+
base_ref_name: prView.baseRefName ?? null,
|
|
168
|
+
checks
|
|
169
|
+
};
|
|
170
|
+
merge.preconditions.remote_head_match.remote_head_sha = prView.headRefOid ?? null;
|
|
171
|
+
merge.preconditions.remote_head_match.status = currentHeadSha && prView.headRefOid && currentHeadSha === prView.headRefOid
|
|
172
|
+
? 'passed'
|
|
173
|
+
: 'blocked';
|
|
174
|
+
merge.preconditions.checks_ready.status = checkSummary.pending_count === 0 && checkSummary.failing_count === 0
|
|
175
|
+
? 'passed'
|
|
176
|
+
: 'blocked';
|
|
177
|
+
merge.preconditions.checks_ready.pending_count = checkSummary.pending_count;
|
|
178
|
+
merge.preconditions.checks_ready.failing_count = checkSummary.failing_count;
|
|
179
|
+
merge.preconditions.review_policy.status = (
|
|
180
|
+
prView.reviewDecision === 'CHANGES_REQUESTED' || prView.reviewDecision === 'REVIEW_REQUIRED'
|
|
181
|
+
) ? 'blocked' : 'passed';
|
|
182
|
+
merge.preconditions.open_pull_request.status = (
|
|
183
|
+
prView.state === 'OPEN' && prView.isDraft !== true && prView.mergeStateStatus === 'CLEAN'
|
|
184
|
+
) ? 'passed' : 'blocked';
|
|
185
|
+
|
|
186
|
+
const blockingReasons = [];
|
|
187
|
+
if (merge.preconditions.gate_ready !== true) blockingReasons.push('gate_not_ready');
|
|
188
|
+
if (!merge.preconditions.clean_worktree) blockingReasons.push('dirty_worktree');
|
|
189
|
+
if (merge.preconditions.base_freshness.status !== 'passed') blockingReasons.push('base_not_fresh');
|
|
190
|
+
if (merge.preconditions.remote_head_match.status !== 'passed') blockingReasons.push('remote_head_mismatch');
|
|
191
|
+
if (merge.preconditions.checks_ready.status !== 'passed') blockingReasons.push('checks_not_ready');
|
|
192
|
+
if (merge.preconditions.review_policy.status !== 'passed') blockingReasons.push('review_policy_not_satisfied');
|
|
193
|
+
if (merge.preconditions.open_pull_request.status !== 'passed') blockingReasons.push('pr_not_mergeable');
|
|
194
|
+
if (nonWorkspaceDirtyFiles.length > 0) {
|
|
195
|
+
merge.warnings.push(`Non-workspace dirty files: ${nonWorkspaceDirtyFiles.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (blockingReasons.length > 0) {
|
|
199
|
+
merge.status = 'blocked';
|
|
200
|
+
merge.stop_reason = blockingReasons.join(',');
|
|
201
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
202
|
+
return { merge, artifacts };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (dryRun) {
|
|
206
|
+
merge.status = 'ready_to_merge';
|
|
207
|
+
merge.stop_reason = 'ready_to_merge_dry_run';
|
|
208
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
209
|
+
return { merge, artifacts };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const mergeResult = await runCommand(
|
|
213
|
+
root,
|
|
214
|
+
['gh', buildMergeArgs(prSelector, strategy, repositorySlug, currentHeadSha)],
|
|
215
|
+
options,
|
|
216
|
+
{ cwd: os.tmpdir() }
|
|
217
|
+
);
|
|
218
|
+
merge.results.push(mergeResult);
|
|
219
|
+
if (mergeResult.exit_code !== 0) {
|
|
220
|
+
merge.status = 'failed';
|
|
221
|
+
merge.stop_reason = 'gh_merge_failed';
|
|
222
|
+
merge.error = `Command failed: ${mergeResult.command}`;
|
|
223
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
224
|
+
return { merge, artifacts };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (deleteBranch && merge.pr.head_ref_name) {
|
|
228
|
+
const remoteDeleteArgs = ['git', ['push', 'origin', '--delete', merge.pr.head_ref_name]];
|
|
229
|
+
merge.branch_cleanup.remote.attempted = true;
|
|
230
|
+
merge.branch_cleanup.remote.command = formatCommand(remoteDeleteArgs);
|
|
231
|
+
const remoteDeleteResult = await runCommand(root, remoteDeleteArgs, options);
|
|
232
|
+
merge.results.push(remoteDeleteResult);
|
|
233
|
+
merge.branch_cleanup.remote.deleted = remoteDeleteResult.exit_code === 0;
|
|
234
|
+
if (!merge.branch_cleanup.remote.deleted) {
|
|
235
|
+
merge.warnings.push(`Remote branch deletion failed: ${remoteDeleteResult.command}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (currentBranch && currentBranch !== merge.pr.head_ref_name) {
|
|
239
|
+
const localDeleteArgs = ['git', ['branch', '-d', merge.pr.head_ref_name]];
|
|
240
|
+
merge.branch_cleanup.local.attempted = true;
|
|
241
|
+
merge.branch_cleanup.local.command = formatCommand(localDeleteArgs);
|
|
242
|
+
const localDeleteResult = await runCommand(root, localDeleteArgs, options);
|
|
243
|
+
merge.results.push(localDeleteResult);
|
|
244
|
+
merge.branch_cleanup.local.deleted = localDeleteResult.exit_code === 0;
|
|
245
|
+
if (!merge.branch_cleanup.local.deleted) {
|
|
246
|
+
merge.warnings.push(`Local branch deletion failed: ${localDeleteResult.command}`);
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
merge.warnings.push('Local branch deletion skipped because the merged branch is checked out in the current worktree.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const mergedViewResult = await runCommand(
|
|
254
|
+
root,
|
|
255
|
+
['gh', buildPrViewArgs(prSelector, repositorySlug, 'url,state,mergedAt,mergeCommit')],
|
|
256
|
+
options,
|
|
257
|
+
{ cwd: os.tmpdir() }
|
|
258
|
+
);
|
|
259
|
+
merge.results.push(mergedViewResult);
|
|
260
|
+
if (mergedViewResult.exit_code === 0) {
|
|
261
|
+
const mergedView = JSON.parse(mergedViewResult.stdout || '{}');
|
|
262
|
+
merge.pr.url = mergedView.url ?? merge.pr.url;
|
|
263
|
+
merge.merge_commit_sha = mergedView.mergeCommit?.oid ?? null;
|
|
264
|
+
merge.merged_at = mergedView.mergedAt ?? null;
|
|
265
|
+
} else {
|
|
266
|
+
merge.warnings.push(`Post-merge PR view failed: ${mergedViewResult.command}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
merge.status = 'merged';
|
|
270
|
+
merge.stop_reason = null;
|
|
271
|
+
const artifacts = await writePrMergeArtifacts(root, storyId, merge);
|
|
272
|
+
return { merge, artifacts };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function renderPrMergeSummary(result) {
|
|
276
|
+
const merge = result.merge ?? result;
|
|
277
|
+
const checks = merge.pr?.checks ?? [];
|
|
278
|
+
const failures = checks.filter((check) => check.status !== 'COMPLETED' || !SUCCESSFUL_CHECK_CONCLUSIONS.has(check.conclusion));
|
|
279
|
+
return `# Execute Merge
|
|
280
|
+
|
|
281
|
+
- story: ${merge.story?.story_id ?? '-'}
|
|
282
|
+
- status: ${merge.status}
|
|
283
|
+
- strategy: ${merge.strategy}
|
|
284
|
+
- pr: ${merge.pr?.url ?? merge.pr?.selector ?? '-'}
|
|
285
|
+
- stop_reason: ${merge.stop_reason ?? 'none'}
|
|
286
|
+
- merge_commit: ${merge.merge_commit_sha ?? '-'}
|
|
287
|
+
- merged_at: ${merge.merged_at ?? '-'}
|
|
288
|
+
|
|
289
|
+
## Preconditions
|
|
290
|
+
|
|
291
|
+
- gate_ready: ${merge.preconditions.gate_ready ? 'passed' : 'blocked'}
|
|
292
|
+
- clean_worktree: ${merge.preconditions.clean_worktree ? 'passed' : 'blocked'}
|
|
293
|
+
- base_freshness: ${merge.preconditions.base_freshness.status}
|
|
294
|
+
- remote_head_match: ${merge.preconditions.remote_head_match.status}
|
|
295
|
+
- checks_ready: ${merge.preconditions.checks_ready.status}
|
|
296
|
+
- review_policy: ${merge.preconditions.review_policy.status}
|
|
297
|
+
- open_pull_request: ${merge.preconditions.open_pull_request.status}
|
|
298
|
+
|
|
299
|
+
## Commands
|
|
300
|
+
|
|
301
|
+
${merge.commands.map((command) => `- ${command}`).join('\n')}
|
|
302
|
+
|
|
303
|
+
## Check Summary
|
|
304
|
+
|
|
305
|
+
- checks: ${checks.length}
|
|
306
|
+
- failing_or_pending: ${failures.length}
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeMergeStrategy(strategy) {
|
|
311
|
+
const normalized = strategy ?? 'merge';
|
|
312
|
+
if (!VALID_MERGE_STRATEGIES.has(normalized)) {
|
|
313
|
+
throw new Error(`Unsupported merge strategy: ${normalized}. Use merge, squash, or rebase.`);
|
|
314
|
+
}
|
|
315
|
+
return normalized;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildMergeArgs(prSelector, strategy, repositorySlug, matchHeadCommit) {
|
|
319
|
+
const args = ['pr', 'merge', String(prSelector), `--${strategy}`];
|
|
320
|
+
if (repositorySlug) args.push('--repo', repositorySlug);
|
|
321
|
+
if (matchHeadCommit) args.push('--match-head-commit', matchHeadCommit);
|
|
322
|
+
return args;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function buildPrViewArgs(prSelector, repositorySlug, fields) {
|
|
326
|
+
const args = ['pr', 'view', String(prSelector), '--json', fields];
|
|
327
|
+
if (repositorySlug) args.push('--repo', repositorySlug);
|
|
328
|
+
return args;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeChecks(checks) {
|
|
332
|
+
if (!Array.isArray(checks)) return [];
|
|
333
|
+
return checks.map((check) => ({
|
|
334
|
+
name: check.name ?? check.context ?? 'unknown',
|
|
335
|
+
status: check.status ?? 'UNKNOWN',
|
|
336
|
+
conclusion: check.conclusion ?? '',
|
|
337
|
+
workflow_name: check.workflowName ?? '',
|
|
338
|
+
details_url: check.detailsUrl ?? null
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function summarizeChecks(checks) {
|
|
343
|
+
return checks.reduce((summary, check) => {
|
|
344
|
+
if (check.status !== 'COMPLETED') summary.pending_count += 1;
|
|
345
|
+
else if (!SUCCESSFUL_CHECK_CONCLUSIONS.has(check.conclusion)) summary.failing_count += 1;
|
|
346
|
+
return summary;
|
|
347
|
+
}, { pending_count: 0, failing_count: 0 });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function collectNonWorkspaceDirtyFiles(repoRoot) {
|
|
351
|
+
const output = await gitOptional(repoRoot, ['status', '--porcelain']);
|
|
352
|
+
const files = String(output ?? '')
|
|
353
|
+
.split('\n')
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
.map((line) => line.slice(3).trim())
|
|
356
|
+
.filter(Boolean)
|
|
357
|
+
.filter((file) => !file.startsWith('.vibepro/'));
|
|
358
|
+
return [...new Set(files)];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function stripRemote(ref) {
|
|
362
|
+
return typeof ref === 'string' ? ref.replace(/^origin\//, '') : ref;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function formatCommand(command) {
|
|
366
|
+
const [bin, args] = command;
|
|
367
|
+
return [bin, ...args.map(shellQuote)].join(' ');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function shellQuote(value) {
|
|
371
|
+
if (/^[a-zA-Z0-9_./:=@+-]+$/.test(value)) return value;
|
|
372
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runCommand(repoRoot, command, options = {}, execution = {}) {
|
|
376
|
+
const [bin, args] = command;
|
|
377
|
+
const startedAt = new Date().toISOString();
|
|
378
|
+
try {
|
|
379
|
+
const result = await execFileAsync(bin, args, {
|
|
380
|
+
cwd: execution.cwd ?? repoRoot,
|
|
381
|
+
encoding: 'utf8',
|
|
382
|
+
env: options.env
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
command: formatCommand(command),
|
|
386
|
+
started_at: startedAt,
|
|
387
|
+
finished_at: new Date().toISOString(),
|
|
388
|
+
exit_code: 0,
|
|
389
|
+
stdout: result.stdout.trim(),
|
|
390
|
+
stderr: result.stderr.trim()
|
|
391
|
+
};
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return {
|
|
394
|
+
command: formatCommand(command),
|
|
395
|
+
started_at: startedAt,
|
|
396
|
+
finished_at: new Date().toISOString(),
|
|
397
|
+
exit_code: Number.isInteger(error.code) ? error.code : 1,
|
|
398
|
+
stdout: String(error.stdout ?? '').trim(),
|
|
399
|
+
stderr: String(error.stderr ?? error.message ?? '').trim()
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function gitOptional(repoRoot, args) {
|
|
405
|
+
try {
|
|
406
|
+
const result = await execFileAsync('git', args, { cwd: repoRoot, encoding: 'utf8' });
|
|
407
|
+
return result.stdout.trim() || null;
|
|
408
|
+
} catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function gitIsAncestor(repoRoot, ancestor, descendant) {
|
|
414
|
+
if (!ancestor || !descendant) return false;
|
|
415
|
+
if (ancestor === descendant) return true;
|
|
416
|
+
try {
|
|
417
|
+
await execFileAsync('git', ['merge-base', '--is-ancestor', ancestor, descendant], { cwd: repoRoot, encoding: 'utf8' });
|
|
418
|
+
return true;
|
|
419
|
+
} catch {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function readJsonIfExists(filePath) {
|
|
425
|
+
try {
|
|
426
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error.code === 'ENOENT') return null;
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function writePrMergeArtifacts(repoRoot, storyId, merge) {
|
|
434
|
+
const prDir = path.join(getWorkspaceDir(repoRoot), 'pr', storyId);
|
|
435
|
+
await mkdir(prDir, { recursive: true });
|
|
436
|
+
const jsonPath = path.join(prDir, 'pr-merge.json');
|
|
437
|
+
const reportPath = path.join(prDir, 'pr-merge.html');
|
|
438
|
+
await writeFile(jsonPath, `${JSON.stringify(merge, null, 2)}\n`);
|
|
439
|
+
await writeFile(reportPath, renderPrMergeHtml(merge, {
|
|
440
|
+
language: merge.output?.language ?? 'ja'
|
|
441
|
+
}));
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const manifest = await readManifest(repoRoot);
|
|
445
|
+
manifest.pr_merges = {
|
|
446
|
+
...(manifest.pr_merges ?? {}),
|
|
447
|
+
[storyId]: {
|
|
448
|
+
latest_merge: toWorkspaceRelative(repoRoot, jsonPath),
|
|
449
|
+
latest_report: toWorkspaceRelative(repoRoot, reportPath),
|
|
450
|
+
latest_pr_url: merge.pr?.url ?? null,
|
|
451
|
+
latest_merge_commit: merge.merge_commit_sha ?? null,
|
|
452
|
+
latest_merged_at: merge.merged_at ?? null,
|
|
453
|
+
latest_dry_run: merge.dry_run
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
await writeManifest(repoRoot, manifest);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (error.code !== 'ENOENT') throw error;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
pr_merge_json: jsonPath,
|
|
463
|
+
pr_merge_report: reportPath
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const PR_VIEW_FIELDS = [
|
|
468
|
+
'url',
|
|
469
|
+
'state',
|
|
470
|
+
'isDraft',
|
|
471
|
+
'mergeStateStatus',
|
|
472
|
+
'reviewDecision',
|
|
473
|
+
'headRefName',
|
|
474
|
+
'headRefOid',
|
|
475
|
+
'baseRefName',
|
|
476
|
+
'statusCheckRollup'
|
|
477
|
+
].join(',');
|
|
478
|
+
|
|
479
|
+
async function resolveGitHubRepositorySlug(repoRoot, context = {}) {
|
|
480
|
+
const candidates = [
|
|
481
|
+
context.prCreate?.toolchain?.source_git?.origin_url,
|
|
482
|
+
context.prPrepare?.toolchain?.source_git?.origin_url,
|
|
483
|
+
await gitOptional(repoRoot, ['config', '--get', 'remote.origin.url']),
|
|
484
|
+
context.prCreate?.pr_url,
|
|
485
|
+
context.executionState?.pr_url
|
|
486
|
+
];
|
|
487
|
+
for (const candidate of candidates) {
|
|
488
|
+
const slug = githubRepositorySlug(candidate);
|
|
489
|
+
if (slug) return slug;
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function githubRepositorySlug(value) {
|
|
495
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
496
|
+
const sshMatch = value.match(/github\.com[:/]([^/]+\/[^/.]+?)(?:\.git)?$/i);
|
|
497
|
+
if (sshMatch) return sshMatch[1];
|
|
498
|
+
const httpMatch = value.match(/^https?:\/\/github\.com\/([^/]+\/[^/.]+?)(?:\.git)?(?:\/pull\/\d+)?\/?$/i);
|
|
499
|
+
if (httpMatch) return httpMatch[1];
|
|
500
|
+
return null;
|
|
501
|
+
}
|