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