@imdeadpool/guardex 7.0.21 → 7.0.23
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/README.md +39 -29
- package/package.json +5 -1
- package/src/cli/args.js +89 -0
- package/src/cli/main.js +645 -2873
- package/src/context.js +195 -31
- package/src/core/stdin.js +52 -0
- package/src/core/versions.js +33 -0
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +604 -1
- package/src/hooks/index.js +64 -0
- package/src/output/index.js +72 -5
- package/src/report/session-severity.js +213 -0
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +627 -0
- package/src/toolchain/index.js +559 -179
- package/templates/AGENTS.multiagent-safety.md +25 -0
- package/templates/scripts/agent-branch-finish.sh +86 -6
- package/templates/scripts/agent-session-state.js +62 -1
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +38 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +38 -11
- package/templates/scripts/openspec/init-plan-workspace.sh +34 -3
- package/templates/vscode/guardex-active-agents/README.md +9 -6
- package/templates/vscode/guardex-active-agents/extension.js +805 -77
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +15 -3
- package/templates/vscode/guardex-active-agents/session-schema.js +311 -4
package/src/cli/main.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const hooksModule = require('../hooks');
|
|
4
4
|
const sandboxModule = require('../sandbox');
|
|
5
5
|
const toolchainModule = require('../toolchain');
|
|
6
|
-
const
|
|
6
|
+
const finishCommands = require('../finish');
|
|
7
|
+
const doctorModule = require('../doctor');
|
|
8
|
+
const sessionSeverityReport = require('../report/session-severity');
|
|
7
9
|
const {
|
|
8
10
|
fs,
|
|
9
11
|
path,
|
|
@@ -60,6 +62,9 @@ const {
|
|
|
60
62
|
DEPRECATED_COMMAND_ALIASES,
|
|
61
63
|
envFlagIsTruthy,
|
|
62
64
|
defaultAgentWorktreeRelativeDir,
|
|
65
|
+
listAiSetupPartNames,
|
|
66
|
+
parseAiSetupPartNames,
|
|
67
|
+
renderAiSetupPrompt,
|
|
63
68
|
AI_SETUP_PROMPT,
|
|
64
69
|
AI_SETUP_COMMANDS,
|
|
65
70
|
SCORECARD_RISK_BY_CHECK,
|
|
@@ -69,6 +74,37 @@ const {
|
|
|
69
74
|
resolveRepoRoot,
|
|
70
75
|
isGitRepo,
|
|
71
76
|
discoverNestedGitRepos,
|
|
77
|
+
uniquePreserveOrder,
|
|
78
|
+
listLocalUserBranches,
|
|
79
|
+
listLocalAgentBranches,
|
|
80
|
+
mapWorktreePathsByBranch,
|
|
81
|
+
gitRefExists,
|
|
82
|
+
hasSignificantWorkingTreeChanges,
|
|
83
|
+
readConfiguredProtectedBranches,
|
|
84
|
+
readProtectedBranches,
|
|
85
|
+
ensureSetupProtectedBranches,
|
|
86
|
+
writeProtectedBranches,
|
|
87
|
+
readGitConfig,
|
|
88
|
+
resolveBaseBranch,
|
|
89
|
+
resolveSyncStrategy,
|
|
90
|
+
currentBranchName,
|
|
91
|
+
repoHasHeadCommit,
|
|
92
|
+
readBranchDisplayName,
|
|
93
|
+
hasOriginRemote: repoHasOriginRemote,
|
|
94
|
+
detectComposeHintFiles,
|
|
95
|
+
printSetupRepoHints,
|
|
96
|
+
ensureRepoBranch,
|
|
97
|
+
ensureOriginBaseRef,
|
|
98
|
+
workingTreeIsDirty,
|
|
99
|
+
aheadBehind,
|
|
100
|
+
lockRegistryStatus,
|
|
101
|
+
listAgentWorktrees,
|
|
102
|
+
listLocalAgentBranchesForFinish,
|
|
103
|
+
worktreeHasLocalChanges,
|
|
104
|
+
branchExists,
|
|
105
|
+
resolveFinishBaseBranch,
|
|
106
|
+
branchMergedIntoBase,
|
|
107
|
+
syncOperation,
|
|
72
108
|
} = require('../git');
|
|
73
109
|
const {
|
|
74
110
|
run,
|
|
@@ -78,6 +114,12 @@ const {
|
|
|
78
114
|
runReviewBotCommand,
|
|
79
115
|
invokePackageAsset,
|
|
80
116
|
} = require('../core/runtime');
|
|
117
|
+
const {
|
|
118
|
+
parseVersionString,
|
|
119
|
+
compareParsedVersions,
|
|
120
|
+
isNewerVersion,
|
|
121
|
+
} = require('../core/versions');
|
|
122
|
+
const { readSingleLineFromStdin } = require('../core/stdin');
|
|
81
123
|
const {
|
|
82
124
|
normalizeManagedForcePath,
|
|
83
125
|
parseCommonArgs,
|
|
@@ -119,94 +161,28 @@ const {
|
|
|
119
161
|
renderShellDispatchShim,
|
|
120
162
|
renderPythonDispatchShim,
|
|
121
163
|
managedForceConflictMessage,
|
|
164
|
+
renderManagedFile,
|
|
165
|
+
ensureGeneratedScriptShim,
|
|
166
|
+
ensureHookShim,
|
|
167
|
+
copyTemplateFile,
|
|
168
|
+
ensureTemplateFilePresent,
|
|
169
|
+
materializePackageRepoTemplateFiles,
|
|
170
|
+
ensureOmxScaffold,
|
|
171
|
+
ensureLockRegistry,
|
|
172
|
+
lockStateOrError,
|
|
173
|
+
writeLockState,
|
|
174
|
+
removeLegacyPackageScripts,
|
|
175
|
+
installUserLevelAsset,
|
|
176
|
+
removeLegacyManagedRepoFile,
|
|
177
|
+
ensureAgentsSnippet,
|
|
178
|
+
ensureManagedGitignore,
|
|
179
|
+
buildRepoVscodeSettings,
|
|
180
|
+
ensureRepoVscodeSettings,
|
|
181
|
+
configureHooks,
|
|
122
182
|
printOperations,
|
|
123
183
|
printStandaloneOperations,
|
|
124
184
|
} = require('../scaffold');
|
|
125
185
|
|
|
126
|
-
let sandboxApi;
|
|
127
|
-
let toolchainApi;
|
|
128
|
-
let finishApi;
|
|
129
|
-
|
|
130
|
-
function getSandboxApi() {
|
|
131
|
-
if (!sandboxApi) {
|
|
132
|
-
sandboxApi = sandboxModule.createSandboxApi({
|
|
133
|
-
protectedBaseWriteBlock,
|
|
134
|
-
runInstallInternal,
|
|
135
|
-
ensureSetupProtectedBranches,
|
|
136
|
-
ensureParentWorkspaceView,
|
|
137
|
-
buildParentWorkspaceView,
|
|
138
|
-
runFixInternal,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return sandboxApi;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function getToolchainApi() {
|
|
145
|
-
if (!toolchainApi) {
|
|
146
|
-
toolchainApi = toolchainModule.createToolchainApi({
|
|
147
|
-
TOOL_NAME,
|
|
148
|
-
NPM_BIN,
|
|
149
|
-
NPX_BIN,
|
|
150
|
-
packageJson,
|
|
151
|
-
OPENSPEC_PACKAGE,
|
|
152
|
-
OPENSPEC_BIN,
|
|
153
|
-
GLOBAL_TOOLCHAIN_PACKAGES,
|
|
154
|
-
parseAutoApproval,
|
|
155
|
-
isInteractiveTerminal,
|
|
156
|
-
promptYesNoStrict,
|
|
157
|
-
run,
|
|
158
|
-
checkForGuardexUpdate,
|
|
159
|
-
printUpdateAvailableBanner,
|
|
160
|
-
readInstalledGuardexVersion,
|
|
161
|
-
restartIntoUpdatedGuardex,
|
|
162
|
-
checkForOpenSpecPackageUpdate,
|
|
163
|
-
printOpenSpecUpdateAvailableBanner,
|
|
164
|
-
resolveGlobalInstallApproval,
|
|
165
|
-
detectGlobalToolchainPackages,
|
|
166
|
-
detectOptionalLocalCompanionTools,
|
|
167
|
-
formatGlobalToolchainServiceName,
|
|
168
|
-
askGlobalInstallForMissing,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return toolchainApi;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function getFinishApi() {
|
|
175
|
-
if (!finishApi) {
|
|
176
|
-
finishApi = finishModule.createFinishApi({
|
|
177
|
-
TOOL_NAME,
|
|
178
|
-
LOCK_FILE_RELATIVE,
|
|
179
|
-
path,
|
|
180
|
-
fs,
|
|
181
|
-
run,
|
|
182
|
-
runPackageAsset,
|
|
183
|
-
resolveRepoRoot,
|
|
184
|
-
parseCleanupArgs,
|
|
185
|
-
parseMergeArgs,
|
|
186
|
-
parseFinishArgs,
|
|
187
|
-
parseSyncArgs,
|
|
188
|
-
listAgentWorktrees,
|
|
189
|
-
listLocalAgentBranchesForFinish,
|
|
190
|
-
uniquePreserveOrder,
|
|
191
|
-
branchExists,
|
|
192
|
-
resolveFinishBaseBranch,
|
|
193
|
-
worktreeHasLocalChanges,
|
|
194
|
-
branchMergedIntoBase,
|
|
195
|
-
autoCommitWorktreeForFinish,
|
|
196
|
-
resolveBaseBranch,
|
|
197
|
-
resolveSyncStrategy,
|
|
198
|
-
ensureOriginBaseRef,
|
|
199
|
-
gitRun,
|
|
200
|
-
currentBranchName,
|
|
201
|
-
workingTreeIsDirty,
|
|
202
|
-
aheadBehind,
|
|
203
|
-
lockRegistryStatus,
|
|
204
|
-
syncOperation,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
return finishApi;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
186
|
/**
|
|
211
187
|
* @typedef {Object} AutoFinishSummary
|
|
212
188
|
* @property {boolean} [enabled]
|
|
@@ -261,116 +237,27 @@ function getFinishApi() {
|
|
|
261
237
|
* @property {AutoFinishSummary} autoFinish
|
|
262
238
|
* @property {string | null} sandboxLockContent
|
|
263
239
|
*/
|
|
264
|
-
function renderManagedFile(repoRoot, relativePath, content, options = {}) {
|
|
265
|
-
const destinationPath = path.join(repoRoot, relativePath);
|
|
266
|
-
const destinationExists = fs.existsSync(destinationPath);
|
|
267
|
-
const force = Boolean(options.force);
|
|
268
|
-
const dryRun = Boolean(options.dryRun);
|
|
269
|
-
|
|
270
|
-
if (destinationExists) {
|
|
271
|
-
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
272
|
-
if (existingContent === content) {
|
|
273
|
-
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
274
|
-
return { status: 'unchanged', file: relativePath };
|
|
275
|
-
}
|
|
276
|
-
if (!force && !isCriticalGuardrailPath(relativePath)) {
|
|
277
|
-
throw new Error(managedForceConflictMessage(relativePath));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
282
|
-
if (!dryRun) {
|
|
283
|
-
fs.writeFileSync(destinationPath, content, 'utf8');
|
|
284
|
-
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (destinationExists && !force && isCriticalGuardrailPath(relativePath)) {
|
|
288
|
-
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: relativePath };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return { status: destinationExists ? 'overwritten' : 'created', file: relativePath };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function ensureGeneratedScriptShim(repoRoot, spec, options = {}) {
|
|
295
|
-
const content = spec.kind === 'python'
|
|
296
|
-
? renderPythonDispatchShim(spec.command)
|
|
297
|
-
: renderShellDispatchShim(spec.command);
|
|
298
|
-
return renderManagedFile(repoRoot, spec.relativePath, content, options);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function ensureHookShim(repoRoot, hookName, options = {}) {
|
|
302
|
-
return renderManagedFile(
|
|
303
|
-
repoRoot,
|
|
304
|
-
path.posix.join('.githooks', hookName),
|
|
305
|
-
renderShellDispatchShim(['hook', 'run', hookName]),
|
|
306
|
-
options,
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
311
|
-
const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
|
|
312
|
-
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
|
|
313
|
-
const destinationPath = path.join(repoRoot, destinationRelativePath);
|
|
314
|
-
|
|
315
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
316
|
-
const destinationExists = fs.existsSync(destinationPath);
|
|
317
240
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
322
|
-
return { status: 'unchanged', file: destinationRelativePath };
|
|
323
|
-
}
|
|
324
|
-
if (!force && !isCriticalGuardrailPath(destinationRelativePath)) {
|
|
325
|
-
throw new Error(managedForceConflictMessage(destinationRelativePath));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
330
|
-
if (!dryRun) {
|
|
331
|
-
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
332
|
-
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
241
|
+
function appendForceArgs(args, options) {
|
|
242
|
+
if (!options.force) {
|
|
243
|
+
return;
|
|
333
244
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
245
|
+
args.push('--force');
|
|
246
|
+
for (const managedPath of options.forceManagedPaths || []) {
|
|
247
|
+
args.push(managedPath);
|
|
337
248
|
}
|
|
338
|
-
|
|
339
|
-
return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
|
|
340
249
|
}
|
|
341
250
|
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const destinationPath = path.join(repoRoot, destinationRelativePath);
|
|
346
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
347
|
-
|
|
348
|
-
if (fs.existsSync(destinationPath)) {
|
|
349
|
-
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
350
|
-
if (existingContent === sourceContent) {
|
|
351
|
-
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
352
|
-
return { status: 'unchanged', file: destinationRelativePath };
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (isCriticalGuardrailPath(destinationRelativePath)) {
|
|
356
|
-
if (!dryRun) {
|
|
357
|
-
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
358
|
-
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
359
|
-
}
|
|
360
|
-
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// In fix mode, avoid silently replacing local customizations.
|
|
364
|
-
return { status: 'skipped-conflict', file: destinationRelativePath };
|
|
251
|
+
function shouldForceManagedPath(options, relativePath) {
|
|
252
|
+
if (!options.force) {
|
|
253
|
+
return false;
|
|
365
254
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
370
|
-
ensureExecutable(destinationPath, destinationRelativePath, dryRun);
|
|
255
|
+
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
256
|
+
if (targetedPaths.length === 0) {
|
|
257
|
+
return true;
|
|
371
258
|
}
|
|
372
|
-
|
|
373
|
-
return
|
|
259
|
+
const normalized = normalizeManagedForcePath(relativePath);
|
|
260
|
+
return normalized !== null && targetedPaths.includes(normalized);
|
|
374
261
|
}
|
|
375
262
|
|
|
376
263
|
function ensureTargetedLegacyWorkflowShims(repoRoot, options) {
|
|
@@ -389,2760 +276,599 @@ function ensureTargetedLegacyWorkflowShims(repoRoot, options) {
|
|
|
389
276
|
return operations;
|
|
390
277
|
}
|
|
391
278
|
|
|
392
|
-
function
|
|
393
|
-
return
|
|
279
|
+
function normalizeWorkspacePath(relativePath) {
|
|
280
|
+
return String(relativePath || '.').replace(/\\/g, '/');
|
|
394
281
|
}
|
|
395
282
|
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
for (const relativeDir of REPO_SCAFFOLD_DIRECTORIES) {
|
|
400
|
-
const absoluteDir = path.join(repoRoot, relativeDir);
|
|
401
|
-
if (fs.existsSync(absoluteDir)) {
|
|
402
|
-
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
403
|
-
throw new Error(`Expected directory at ${relativeDir} but found a file.`);
|
|
404
|
-
}
|
|
405
|
-
operations.push({ status: 'unchanged', file: relativeDir });
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (!dryRun) {
|
|
410
|
-
fs.mkdirSync(absoluteDir, { recursive: true });
|
|
411
|
-
}
|
|
412
|
-
operations.push({ status: 'created', file: relativeDir });
|
|
413
|
-
}
|
|
283
|
+
function isCommandAvailable(commandName) {
|
|
284
|
+
return run('which', [commandName]).status === 0;
|
|
285
|
+
}
|
|
414
286
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
operations.push({ status: 'unchanged', file: relativeDir });
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
287
|
+
function buildParentWorkspaceView(repoRoot) {
|
|
288
|
+
const parentDir = path.dirname(repoRoot);
|
|
289
|
+
const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`;
|
|
290
|
+
const workspacePath = path.join(parentDir, workspaceFileName);
|
|
291
|
+
const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.');
|
|
424
292
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
293
|
+
return {
|
|
294
|
+
workspacePath,
|
|
295
|
+
payload: {
|
|
296
|
+
folders: [
|
|
297
|
+
{ path: repoRelativePath },
|
|
298
|
+
...AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => ({
|
|
299
|
+
path: normalizeWorkspacePath(path.join(repoRelativePath === '.' ? '' : repoRelativePath, relativeDir)),
|
|
300
|
+
})),
|
|
301
|
+
],
|
|
302
|
+
settings: {
|
|
303
|
+
'scm.alwaysShowRepositories': true,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
430
308
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
operations.push({ status: 'unchanged', file: relativeFile });
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
309
|
+
function ensureParentWorkspaceView(repoRoot, dryRun) {
|
|
310
|
+
const { workspacePath, payload } = buildParentWorkspaceView(repoRoot);
|
|
311
|
+
const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath);
|
|
312
|
+
const nextContent = `${JSON.stringify(payload, null, 2)}\n`;
|
|
313
|
+
const note = 'parent VS Code workspace view';
|
|
440
314
|
|
|
315
|
+
if (!fs.existsSync(workspacePath)) {
|
|
441
316
|
if (!dryRun) {
|
|
442
|
-
fs.
|
|
443
|
-
fs.writeFileSync(absoluteFile, defaultContent, 'utf8');
|
|
317
|
+
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
444
318
|
}
|
|
445
|
-
|
|
319
|
+
return { status: dryRun ? 'would-create' : 'created', file: operationFile, note };
|
|
446
320
|
}
|
|
447
321
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
function ensureLockRegistry(repoRoot, dryRun) {
|
|
452
|
-
const absolutePath = lockFilePath(repoRoot);
|
|
453
|
-
if (fs.existsSync(absolutePath)) {
|
|
454
|
-
return { status: 'unchanged', file: LOCK_FILE_RELATIVE };
|
|
322
|
+
const currentContent = fs.readFileSync(workspacePath, 'utf8');
|
|
323
|
+
if (currentContent === nextContent) {
|
|
324
|
+
return { status: 'unchanged', file: operationFile, note };
|
|
455
325
|
}
|
|
456
326
|
|
|
457
327
|
if (!dryRun) {
|
|
458
|
-
fs.
|
|
459
|
-
fs.writeFileSync(absolutePath, JSON.stringify({ locks: {} }, null, 2) + '\n', 'utf8');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return { status: 'created', file: LOCK_FILE_RELATIVE };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function lockStateOrError(repoRoot) {
|
|
466
|
-
const lockPath = lockFilePath(repoRoot);
|
|
467
|
-
if (!fs.existsSync(lockPath)) {
|
|
468
|
-
return { ok: false, error: `${LOCK_FILE_RELATIVE} is missing` };
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
473
|
-
if (!parsed || typeof parsed !== 'object' || typeof parsed.locks !== 'object' || parsed.locks === null) {
|
|
474
|
-
return { ok: false, error: `${LOCK_FILE_RELATIVE} has invalid schema (expected { locks: {} })` };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Normalize older schema entries.
|
|
478
|
-
for (const [filePath, entry] of Object.entries(parsed.locks)) {
|
|
479
|
-
if (!entry || typeof entry !== 'object') {
|
|
480
|
-
parsed.locks[filePath] = { branch: '', claimed_at: '', allow_delete: false };
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
if (!Object.prototype.hasOwnProperty.call(entry, 'allow_delete')) {
|
|
484
|
-
entry.allow_delete = false;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return { ok: true, raw: parsed, locks: parsed.locks };
|
|
489
|
-
} catch (error) {
|
|
490
|
-
return { ok: false, error: `${LOCK_FILE_RELATIVE} is invalid JSON: ${error.message}` };
|
|
328
|
+
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
491
329
|
}
|
|
330
|
+
return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note };
|
|
492
331
|
}
|
|
493
332
|
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
333
|
+
function hasGuardexBootstrapFiles(repoRoot) {
|
|
334
|
+
const required = [
|
|
335
|
+
'AGENTS.md',
|
|
336
|
+
'.githooks/pre-commit',
|
|
337
|
+
'.githooks/pre-push',
|
|
338
|
+
LOCK_FILE_RELATIVE,
|
|
339
|
+
];
|
|
340
|
+
return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
499
341
|
}
|
|
500
342
|
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
|
|
343
|
+
function protectedBaseWriteBlock(options, { requireBootstrap = true } = {}) {
|
|
344
|
+
if (options.dryRun || options.allowProtectedBaseWrite) {
|
|
345
|
+
return null;
|
|
505
346
|
}
|
|
506
347
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
} catch (error) {
|
|
511
|
-
throw new Error(`Unable to parse package.json in target repo: ${error.message}`);
|
|
348
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
349
|
+
if (requireBootstrap && !hasGuardexBootstrapFiles(repoRoot)) {
|
|
350
|
+
return null;
|
|
512
351
|
}
|
|
513
352
|
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
pkg.scripts = existingScripts;
|
|
518
|
-
let changed = false;
|
|
519
|
-
for (const [key, value] of Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)) {
|
|
520
|
-
if (existingScripts[key] === value) {
|
|
521
|
-
delete existingScripts[key];
|
|
522
|
-
changed = true;
|
|
523
|
-
}
|
|
353
|
+
const branch = currentBranchName(repoRoot);
|
|
354
|
+
if (branch !== 'main') {
|
|
355
|
+
return null;
|
|
524
356
|
}
|
|
525
357
|
|
|
526
|
-
|
|
527
|
-
|
|
358
|
+
const protectedBranches = readProtectedBranches(repoRoot);
|
|
359
|
+
if (!protectedBranches.includes(branch)) {
|
|
360
|
+
return null;
|
|
528
361
|
}
|
|
529
362
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
363
|
+
return {
|
|
364
|
+
repoRoot,
|
|
365
|
+
branch,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
533
368
|
|
|
534
|
-
|
|
369
|
+
function assertProtectedMainWriteAllowed(options, commandName) {
|
|
370
|
+
return sandboxModule.assertProtectedMainWriteAllowed(options, commandName);
|
|
535
371
|
}
|
|
536
372
|
|
|
537
|
-
function
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
return { status: 'skipped-conflict', file: asset.destination };
|
|
373
|
+
function runSetupBootstrapInternal(options) {
|
|
374
|
+
const installPayload = runInstallInternal(options);
|
|
375
|
+
installPayload.operations.push(
|
|
376
|
+
ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
let parentWorkspace = null;
|
|
380
|
+
if (options.parentWorkspaceView) {
|
|
381
|
+
installPayload.operations.push(
|
|
382
|
+
ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
383
|
+
);
|
|
384
|
+
if (!options.dryRun) {
|
|
385
|
+
parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
|
|
551
386
|
}
|
|
552
387
|
}
|
|
553
388
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
389
|
+
const fixPayload = runFixInternal({
|
|
390
|
+
target: installPayload.repoRoot,
|
|
391
|
+
dryRun: options.dryRun,
|
|
392
|
+
force: options.force,
|
|
393
|
+
forceManagedPaths: options.forceManagedPaths,
|
|
394
|
+
dropStaleLocks: true,
|
|
395
|
+
skipAgents: options.skipAgents,
|
|
396
|
+
skipPackageJson: options.skipPackageJson,
|
|
397
|
+
skipGitignore: options.skipGitignore,
|
|
398
|
+
allowProtectedBaseWrite: options.allowProtectedBaseWrite,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
installPayload,
|
|
403
|
+
fixPayload,
|
|
404
|
+
parentWorkspace,
|
|
405
|
+
};
|
|
559
406
|
}
|
|
560
407
|
|
|
561
|
-
function
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return { status: 'skipped-conflict', file: relativePath, note: 'not a regular file' };
|
|
570
|
-
}
|
|
408
|
+
function extractAgentBranchStartMetadata(output) {
|
|
409
|
+
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
|
|
410
|
+
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
|
|
411
|
+
return {
|
|
412
|
+
branch: branchMatch ? branchMatch[1].trim() : '',
|
|
413
|
+
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
|
|
414
|
+
};
|
|
415
|
+
}
|
|
571
416
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
417
|
+
function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
418
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
419
|
+
const relativeTarget = path.relative(repoRoot, resolvedTarget);
|
|
420
|
+
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
421
|
+
throw new Error(`sandbox target must stay inside repo root: ${resolvedTarget}`);
|
|
578
422
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
? skillAsset.source.slice(TEMPLATE_ROOT.length + 1)
|
|
582
|
-
: relativePath.replace(/^\./, '');
|
|
583
|
-
const sourcePath = path.join(TEMPLATE_ROOT, templateRelative);
|
|
584
|
-
if (!fs.existsSync(sourcePath)) {
|
|
585
|
-
return { status: 'skipped', file: relativePath, note: 'template source missing' };
|
|
423
|
+
if (!relativeTarget || relativeTarget === '.') {
|
|
424
|
+
return worktreePath;
|
|
586
425
|
}
|
|
426
|
+
return path.join(worktreePath, relativeTarget);
|
|
427
|
+
}
|
|
587
428
|
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
429
|
+
function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
430
|
+
const args = ['setup', '--target', sandboxTarget, '--no-global-install', '--no-recursive'];
|
|
431
|
+
appendForceArgs(args, options);
|
|
432
|
+
if (options.skipAgents) args.push('--skip-agents');
|
|
433
|
+
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
434
|
+
if (options.skipGitignore) args.push('--no-gitignore');
|
|
435
|
+
if (options.dryRun) args.push('--dry-run');
|
|
436
|
+
return args;
|
|
437
|
+
}
|
|
593
438
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
return { status: dryRun ? 'would-remove' : 'removed', file: relativePath };
|
|
439
|
+
function isSpawnFailure(result) {
|
|
440
|
+
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
598
441
|
}
|
|
599
442
|
|
|
600
|
-
function
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
'
|
|
606
|
-
)
|
|
443
|
+
function protectedBaseSandboxBranchPrefix() {
|
|
444
|
+
const now = new Date();
|
|
445
|
+
const stamp = [
|
|
446
|
+
now.getUTCFullYear(),
|
|
447
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
448
|
+
String(now.getUTCDate()).padStart(2, '0'),
|
|
449
|
+
].join('') + '-' + [
|
|
450
|
+
String(now.getUTCHours()).padStart(2, '0'),
|
|
451
|
+
String(now.getUTCMinutes()).padStart(2, '0'),
|
|
452
|
+
String(now.getUTCSeconds()).padStart(2, '0'),
|
|
453
|
+
].join('');
|
|
454
|
+
return `agent/gx/${stamp}`;
|
|
455
|
+
}
|
|
607
456
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
return { status: 'created', file: 'AGENTS.md' };
|
|
613
|
-
}
|
|
457
|
+
function protectedBaseSandboxWorktreePath(repoRoot, branchName) {
|
|
458
|
+
return path.join(repoRoot, defaultAgentWorktreeRelativeDir(), branchName.replace(/\//g, '__'));
|
|
459
|
+
}
|
|
614
460
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
620
|
-
}
|
|
621
|
-
if (!dryRun) {
|
|
622
|
-
fs.writeFileSync(agentsPath, next, 'utf8');
|
|
623
|
-
}
|
|
624
|
-
return { status: 'updated', file: 'AGENTS.md', note: 'refreshed gitguardex-managed block' };
|
|
461
|
+
function resolveProtectedBaseSandboxStartRef(repoRoot, baseBranch) {
|
|
462
|
+
run('git', ['-C', repoRoot, 'fetch', 'origin', baseBranch, '--quiet'], { timeout: 20_000 });
|
|
463
|
+
if (gitRefExists(repoRoot, `refs/remotes/origin/${baseBranch}`)) {
|
|
464
|
+
return `origin/${baseBranch}`;
|
|
625
465
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
|
|
466
|
+
if (gitRefExists(repoRoot, `refs/heads/${baseBranch}`)) {
|
|
467
|
+
return baseBranch;
|
|
629
468
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (!dryRun) {
|
|
633
|
-
fs.writeFileSync(agentsPath, `${existing}${separator}${snippet}\n`, 'utf8');
|
|
469
|
+
if (currentBranchName(repoRoot) === baseBranch) {
|
|
470
|
+
return null;
|
|
634
471
|
}
|
|
635
|
-
|
|
636
|
-
return { status: 'updated', file: 'AGENTS.md' };
|
|
472
|
+
throw new Error(`Unable to find base ref for sandbox bootstrap: ${baseBranch}`);
|
|
637
473
|
}
|
|
638
474
|
|
|
639
|
-
function
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
...MANAGED_GITIGNORE_PATHS,
|
|
644
|
-
GITIGNORE_MARKER_END,
|
|
645
|
-
].join('\n');
|
|
646
|
-
const managedRegex = new RegExp(
|
|
647
|
-
`${GITIGNORE_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${GITIGNORE_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
648
|
-
'm',
|
|
649
|
-
);
|
|
650
|
-
|
|
651
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
652
|
-
if (!dryRun) {
|
|
653
|
-
fs.writeFileSync(gitignorePath, `${managedBlock}\n`, 'utf8');
|
|
654
|
-
}
|
|
655
|
-
return { status: 'created', file: '.gitignore', note: 'added gitguardex-managed entries' };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
659
|
-
if (managedRegex.test(existing)) {
|
|
660
|
-
const next = existing.replace(managedRegex, managedBlock);
|
|
661
|
-
if (next === existing) {
|
|
662
|
-
return { status: 'unchanged', file: '.gitignore' };
|
|
663
|
-
}
|
|
664
|
-
if (!dryRun) {
|
|
665
|
-
fs.writeFileSync(gitignorePath, next, 'utf8');
|
|
666
|
-
}
|
|
667
|
-
return { status: 'updated', file: '.gitignore', note: 'refreshed gitguardex-managed entries' };
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
671
|
-
if (!dryRun) {
|
|
672
|
-
fs.writeFileSync(gitignorePath, `${existing}${separator}${managedBlock}\n`, 'utf8');
|
|
673
|
-
}
|
|
674
|
-
return { status: 'updated', file: '.gitignore', note: 'appended gitguardex-managed entries' };
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function stripJsonComments(source) {
|
|
678
|
-
let result = '';
|
|
679
|
-
let inString = false;
|
|
680
|
-
let escapeNext = false;
|
|
681
|
-
let inLineComment = false;
|
|
682
|
-
let inBlockComment = false;
|
|
683
|
-
|
|
684
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
685
|
-
const current = source[index];
|
|
686
|
-
const next = source[index + 1];
|
|
687
|
-
|
|
688
|
-
if (inLineComment) {
|
|
689
|
-
if (current === '\n' || current === '\r') {
|
|
690
|
-
inLineComment = false;
|
|
691
|
-
result += current;
|
|
692
|
-
}
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (inBlockComment) {
|
|
697
|
-
if (current === '*' && next === '/') {
|
|
698
|
-
inBlockComment = false;
|
|
699
|
-
index += 1;
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (current === '\n' || current === '\r') {
|
|
703
|
-
result += current;
|
|
704
|
-
}
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (inString) {
|
|
709
|
-
result += current;
|
|
710
|
-
if (escapeNext) {
|
|
711
|
-
escapeNext = false;
|
|
712
|
-
} else if (current === '\\') {
|
|
713
|
-
escapeNext = true;
|
|
714
|
-
} else if (current === '"') {
|
|
715
|
-
inString = false;
|
|
716
|
-
}
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (current === '"') {
|
|
721
|
-
inString = true;
|
|
722
|
-
result += current;
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (current === '/' && next === '/') {
|
|
727
|
-
inLineComment = true;
|
|
728
|
-
index += 1;
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (current === '/' && next === '*') {
|
|
733
|
-
inBlockComment = true;
|
|
734
|
-
index += 1;
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
result += current;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return result;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function stripJsonTrailingCommas(source) {
|
|
745
|
-
let result = '';
|
|
746
|
-
let inString = false;
|
|
747
|
-
let escapeNext = false;
|
|
748
|
-
|
|
749
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
750
|
-
const current = source[index];
|
|
751
|
-
|
|
752
|
-
if (inString) {
|
|
753
|
-
result += current;
|
|
754
|
-
if (escapeNext) {
|
|
755
|
-
escapeNext = false;
|
|
756
|
-
} else if (current === '\\') {
|
|
757
|
-
escapeNext = true;
|
|
758
|
-
} else if (current === '"') {
|
|
759
|
-
inString = false;
|
|
760
|
-
}
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (current === '"') {
|
|
765
|
-
inString = true;
|
|
766
|
-
result += current;
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (current === ',') {
|
|
771
|
-
let lookahead = index + 1;
|
|
772
|
-
while (lookahead < source.length && /\s/.test(source[lookahead])) {
|
|
773
|
-
lookahead += 1;
|
|
774
|
-
}
|
|
775
|
-
if (source[lookahead] === '}' || source[lookahead] === ']') {
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
result += current;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
return result;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function parseJsonObjectLikeFile(source, relativePath) {
|
|
787
|
-
let parsed;
|
|
788
|
-
try {
|
|
789
|
-
parsed = JSON.parse(stripJsonTrailingCommas(stripJsonComments(source)));
|
|
790
|
-
} catch (error) {
|
|
791
|
-
throw new Error(`Unable to parse ${relativePath} as JSON or JSONC: ${error.message}`);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
795
|
-
throw new Error(`${relativePath} must contain a top-level object.`);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
return parsed;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function uniqueStringList(values) {
|
|
802
|
-
const seen = new Set();
|
|
803
|
-
const result = [];
|
|
804
|
-
|
|
805
|
-
for (const value of values) {
|
|
806
|
-
if (typeof value !== 'string' || seen.has(value)) {
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
seen.add(value);
|
|
810
|
-
result.push(value);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return result;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function buildRepoVscodeSettings(existingSettings = {}) {
|
|
817
|
-
const nextSettings = { ...existingSettings };
|
|
818
|
-
const existingIgnoredFolders = Array.isArray(existingSettings[REPO_SCAN_IGNORED_FOLDERS_SETTING])
|
|
819
|
-
? existingSettings[REPO_SCAN_IGNORED_FOLDERS_SETTING]
|
|
820
|
-
: [];
|
|
821
|
-
|
|
822
|
-
nextSettings[REPO_SCAN_IGNORED_FOLDERS_SETTING] = uniqueStringList([
|
|
823
|
-
...existingIgnoredFolders,
|
|
824
|
-
...MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
825
|
-
]);
|
|
826
|
-
|
|
827
|
-
return nextSettings;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function ensureRepoVscodeSettings(repoRoot, dryRun) {
|
|
831
|
-
const settingsPath = path.join(repoRoot, SHARED_VSCODE_SETTINGS_RELATIVE);
|
|
832
|
-
const destinationExists = fs.existsSync(settingsPath);
|
|
833
|
-
const existingContent = destinationExists ? fs.readFileSync(settingsPath, 'utf8') : '';
|
|
834
|
-
const existingSettings = destinationExists
|
|
835
|
-
? parseJsonObjectLikeFile(existingContent, SHARED_VSCODE_SETTINGS_RELATIVE)
|
|
836
|
-
: {};
|
|
837
|
-
const nextContent = `${JSON.stringify(buildRepoVscodeSettings(existingSettings), null, 2)}\n`;
|
|
838
|
-
|
|
839
|
-
if (destinationExists && existingContent === nextContent) {
|
|
840
|
-
return { status: 'unchanged', file: SHARED_VSCODE_SETTINGS_RELATIVE };
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
ensureParentDir(repoRoot, settingsPath, dryRun);
|
|
844
|
-
if (!dryRun) {
|
|
845
|
-
fs.writeFileSync(settingsPath, nextContent, 'utf8');
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
return {
|
|
849
|
-
status: destinationExists ? 'updated' : 'created',
|
|
850
|
-
file: SHARED_VSCODE_SETTINGS_RELATIVE,
|
|
851
|
-
note: 'shared VS Code repo scan ignores for Guardex worktrees',
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
function configureHooks(repoRoot, dryRun) {
|
|
856
|
-
if (dryRun) {
|
|
857
|
-
return { status: 'would-set', key: 'core.hooksPath', value: '.githooks' };
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const result = run('git', ['-C', repoRoot, 'config', 'core.hooksPath', '.githooks']);
|
|
861
|
-
if (result.status !== 0) {
|
|
862
|
-
throw new Error(`Failed to set git hooksPath: ${(result.stderr || '').trim()}`);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return { status: 'set', key: 'core.hooksPath', value: '.githooks' };
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function appendForceArgs(args, options) {
|
|
869
|
-
if (!options.force) {
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
args.push('--force');
|
|
873
|
-
for (const managedPath of options.forceManagedPaths || []) {
|
|
874
|
-
args.push(managedPath);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function shouldForceManagedPath(options, relativePath) {
|
|
879
|
-
if (!options.force) {
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
883
|
-
if (targetedPaths.length === 0) {
|
|
884
|
-
return true;
|
|
885
|
-
}
|
|
886
|
-
const normalized = normalizeManagedForcePath(relativePath);
|
|
887
|
-
return normalized !== null && targetedPaths.includes(normalized);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function normalizeWorkspacePath(relativePath) {
|
|
891
|
-
return String(relativePath || '.').replace(/\\/g, '/');
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function buildParentWorkspaceView(repoRoot) {
|
|
895
|
-
const parentDir = path.dirname(repoRoot);
|
|
896
|
-
const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`;
|
|
897
|
-
const workspacePath = path.join(parentDir, workspaceFileName);
|
|
898
|
-
const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.');
|
|
899
|
-
|
|
900
|
-
return {
|
|
901
|
-
workspacePath,
|
|
902
|
-
payload: {
|
|
903
|
-
folders: [
|
|
904
|
-
{ path: repoRelativePath },
|
|
905
|
-
...AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => ({
|
|
906
|
-
path: normalizeWorkspacePath(path.join(repoRelativePath === '.' ? '' : repoRelativePath, relativeDir)),
|
|
907
|
-
})),
|
|
908
|
-
],
|
|
909
|
-
settings: {
|
|
910
|
-
'scm.alwaysShowRepositories': true,
|
|
911
|
-
},
|
|
912
|
-
},
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function ensureParentWorkspaceView(repoRoot, dryRun) {
|
|
917
|
-
const { workspacePath, payload } = buildParentWorkspaceView(repoRoot);
|
|
918
|
-
const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath);
|
|
919
|
-
const nextContent = `${JSON.stringify(payload, null, 2)}\n`;
|
|
920
|
-
const note = 'parent VS Code workspace view';
|
|
921
|
-
|
|
922
|
-
if (!fs.existsSync(workspacePath)) {
|
|
923
|
-
if (!dryRun) {
|
|
924
|
-
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
925
|
-
}
|
|
926
|
-
return { status: dryRun ? 'would-create' : 'created', file: operationFile, note };
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const currentContent = fs.readFileSync(workspacePath, 'utf8');
|
|
930
|
-
if (currentContent === nextContent) {
|
|
931
|
-
return { status: 'unchanged', file: operationFile, note };
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (!dryRun) {
|
|
935
|
-
fs.writeFileSync(workspacePath, nextContent, 'utf8');
|
|
936
|
-
}
|
|
937
|
-
return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note };
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
function hasGuardexBootstrapFiles(repoRoot) {
|
|
941
|
-
const required = [
|
|
942
|
-
'AGENTS.md',
|
|
943
|
-
'.githooks/pre-commit',
|
|
944
|
-
'.githooks/pre-push',
|
|
945
|
-
LOCK_FILE_RELATIVE,
|
|
946
|
-
];
|
|
947
|
-
return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function protectedBaseWriteBlock(options, { requireBootstrap = true } = {}) {
|
|
951
|
-
if (options.dryRun || options.allowProtectedBaseWrite) {
|
|
952
|
-
return null;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const repoRoot = resolveRepoRoot(options.target);
|
|
956
|
-
if (requireBootstrap && !hasGuardexBootstrapFiles(repoRoot)) {
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const branch = currentBranchName(repoRoot);
|
|
961
|
-
if (branch !== 'main') {
|
|
962
|
-
return null;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
const protectedBranches = readProtectedBranches(repoRoot);
|
|
966
|
-
if (!protectedBranches.includes(branch)) {
|
|
967
|
-
return null;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
return {
|
|
971
|
-
repoRoot,
|
|
972
|
-
branch,
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function assertProtectedMainWriteAllowed(options, commandName) {
|
|
977
|
-
return getSandboxApi().assertProtectedMainWriteAllowed(options, commandName);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function runSetupBootstrapInternal(options) {
|
|
981
|
-
return getSandboxApi().runSetupBootstrapInternal(options);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function extractAgentBranchStartMetadata(output) {
|
|
985
|
-
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
|
|
986
|
-
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
|
|
987
|
-
return {
|
|
988
|
-
branch: branchMatch ? branchMatch[1].trim() : '',
|
|
989
|
-
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
994
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
995
|
-
const relativeTarget = path.relative(repoRoot, resolvedTarget);
|
|
996
|
-
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
997
|
-
throw new Error(`sandbox target must stay inside repo root: ${resolvedTarget}`);
|
|
998
|
-
}
|
|
999
|
-
if (!relativeTarget || relativeTarget === '.') {
|
|
1000
|
-
return worktreePath;
|
|
1001
|
-
}
|
|
1002
|
-
return path.join(worktreePath, relativeTarget);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
1006
|
-
const args = ['setup', '--target', sandboxTarget, '--no-global-install', '--no-recursive'];
|
|
1007
|
-
appendForceArgs(args, options);
|
|
1008
|
-
if (options.skipAgents) args.push('--skip-agents');
|
|
1009
|
-
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
1010
|
-
if (options.skipGitignore) args.push('--no-gitignore');
|
|
1011
|
-
if (options.dryRun) args.push('--dry-run');
|
|
1012
|
-
return args;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
1016
|
-
const args = ['doctor', '--target', sandboxTarget];
|
|
1017
|
-
if (options.dryRun) args.push('--dry-run');
|
|
1018
|
-
appendForceArgs(args, options);
|
|
1019
|
-
if (options.skipAgents) args.push('--skip-agents');
|
|
1020
|
-
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
1021
|
-
if (options.skipGitignore) args.push('--no-gitignore');
|
|
1022
|
-
if (!options.dropStaleLocks) args.push('--keep-stale-locks');
|
|
1023
|
-
args.push(options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
|
|
1024
|
-
if (options.verboseAutoFinish) args.push('--verbose-auto-finish');
|
|
1025
|
-
if (options.json) args.push('--json');
|
|
1026
|
-
return args;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function isSpawnFailure(result) {
|
|
1030
|
-
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function ensureRepoBranch(repoRoot, branch) {
|
|
1034
|
-
const current = currentBranchName(repoRoot);
|
|
1035
|
-
if (current === branch) {
|
|
1036
|
-
return { ok: true, changed: false };
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
const checkoutResult = run('git', ['-C', repoRoot, 'checkout', branch], { timeout: 20_000 });
|
|
1040
|
-
if (isSpawnFailure(checkoutResult)) {
|
|
1041
|
-
return {
|
|
1042
|
-
ok: false,
|
|
1043
|
-
changed: false,
|
|
1044
|
-
stdout: checkoutResult.stdout || '',
|
|
1045
|
-
stderr: checkoutResult.stderr || '',
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
if (checkoutResult.status !== 0) {
|
|
1049
|
-
return {
|
|
1050
|
-
ok: false,
|
|
1051
|
-
changed: false,
|
|
1052
|
-
stdout: checkoutResult.stdout || '',
|
|
1053
|
-
stderr: checkoutResult.stderr || '',
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
return { ok: true, changed: true };
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function protectedBaseSandboxBranchPrefix() {
|
|
1061
|
-
const now = new Date();
|
|
1062
|
-
const stamp = [
|
|
1063
|
-
now.getUTCFullYear(),
|
|
1064
|
-
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
1065
|
-
String(now.getUTCDate()).padStart(2, '0'),
|
|
1066
|
-
].join('') + '-' + [
|
|
1067
|
-
String(now.getUTCHours()).padStart(2, '0'),
|
|
1068
|
-
String(now.getUTCMinutes()).padStart(2, '0'),
|
|
1069
|
-
String(now.getUTCSeconds()).padStart(2, '0'),
|
|
1070
|
-
].join('');
|
|
1071
|
-
return `agent/gx/${stamp}`;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function protectedBaseSandboxWorktreePath(repoRoot, branchName) {
|
|
1075
|
-
return path.join(repoRoot, defaultAgentWorktreeRelativeDir(), branchName.replace(/\//g, '__'));
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function gitRefExists(repoRoot, ref) {
|
|
1079
|
-
return run('git', ['-C', repoRoot, 'show-ref', '--verify', '--quiet', ref]).status === 0;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function resolveProtectedBaseSandboxStartRef(repoRoot, baseBranch) {
|
|
1083
|
-
run('git', ['-C', repoRoot, 'fetch', 'origin', baseBranch, '--quiet'], { timeout: 20_000 });
|
|
1084
|
-
if (gitRefExists(repoRoot, `refs/remotes/origin/${baseBranch}`)) {
|
|
1085
|
-
return `origin/${baseBranch}`;
|
|
1086
|
-
}
|
|
1087
|
-
if (gitRefExists(repoRoot, `refs/heads/${baseBranch}`)) {
|
|
1088
|
-
return baseBranch;
|
|
1089
|
-
}
|
|
1090
|
-
if (currentBranchName(repoRoot) === baseBranch) {
|
|
1091
|
-
return null;
|
|
1092
|
-
}
|
|
1093
|
-
throw new Error(`Unable to find base ref for sandbox bootstrap: ${baseBranch}`);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
function startProtectedBaseSandboxFallback(blocked, sandboxSuffix) {
|
|
1097
|
-
const branchPrefix = protectedBaseSandboxBranchPrefix();
|
|
1098
|
-
let selectedBranch = '';
|
|
1099
|
-
let selectedWorktreePath = '';
|
|
1100
|
-
|
|
1101
|
-
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
1102
|
-
const suffix = attempt === 0 ? sandboxSuffix : `${attempt + 1}-${sandboxSuffix}`;
|
|
1103
|
-
const candidateBranch = `${branchPrefix}-${suffix}`;
|
|
1104
|
-
const candidateWorktreePath = protectedBaseSandboxWorktreePath(blocked.repoRoot, candidateBranch);
|
|
1105
|
-
if (gitRefExists(blocked.repoRoot, `refs/heads/${candidateBranch}`)) {
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
if (fs.existsSync(candidateWorktreePath)) {
|
|
1109
|
-
continue;
|
|
1110
|
-
}
|
|
1111
|
-
selectedBranch = candidateBranch;
|
|
1112
|
-
selectedWorktreePath = candidateWorktreePath;
|
|
1113
|
-
break;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if (!selectedBranch || !selectedWorktreePath) {
|
|
1117
|
-
throw new Error('Unable to allocate unique sandbox branch/worktree');
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
fs.mkdirSync(path.dirname(selectedWorktreePath), { recursive: true });
|
|
1121
|
-
const startRef = resolveProtectedBaseSandboxStartRef(blocked.repoRoot, blocked.branch);
|
|
1122
|
-
const addArgs = startRef
|
|
1123
|
-
? ['-C', blocked.repoRoot, 'worktree', 'add', '-b', selectedBranch, selectedWorktreePath, startRef]
|
|
1124
|
-
: ['-C', blocked.repoRoot, 'worktree', 'add', '--orphan', selectedWorktreePath];
|
|
1125
|
-
const addResult = run('git', addArgs);
|
|
1126
|
-
if (isSpawnFailure(addResult)) {
|
|
1127
|
-
throw addResult.error;
|
|
1128
|
-
}
|
|
1129
|
-
if (addResult.status !== 0) {
|
|
1130
|
-
throw new Error((addResult.stderr || addResult.stdout || 'failed to create sandbox').trim());
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (!startRef) {
|
|
1134
|
-
const renameResult = run(
|
|
1135
|
-
'git',
|
|
1136
|
-
['-C', selectedWorktreePath, 'branch', '-m', selectedBranch],
|
|
1137
|
-
{ timeout: 20_000 },
|
|
1138
|
-
);
|
|
1139
|
-
if (isSpawnFailure(renameResult)) {
|
|
1140
|
-
throw renameResult.error;
|
|
1141
|
-
}
|
|
1142
|
-
if (renameResult.status !== 0) {
|
|
1143
|
-
throw new Error(
|
|
1144
|
-
(renameResult.stderr || renameResult.stdout || 'failed to name orphan sandbox branch').trim(),
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
return {
|
|
1150
|
-
metadata: {
|
|
1151
|
-
branch: selectedBranch,
|
|
1152
|
-
worktreePath: selectedWorktreePath,
|
|
1153
|
-
},
|
|
1154
|
-
stdout:
|
|
1155
|
-
`[agent-branch-start] Created branch: ${selectedBranch}\n` +
|
|
1156
|
-
`[agent-branch-start] Worktree: ${selectedWorktreePath}\n`,
|
|
1157
|
-
stderr: addResult.stderr || '',
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
1162
|
-
if (sandboxSuffix === 'gx-doctor') {
|
|
1163
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
const startResult = runPackageAsset('branchStart', [
|
|
1167
|
-
'--task',
|
|
1168
|
-
taskName,
|
|
1169
|
-
'--agent',
|
|
1170
|
-
SHORT_TOOL_NAME,
|
|
1171
|
-
'--base',
|
|
1172
|
-
blocked.branch,
|
|
1173
|
-
], { cwd: blocked.repoRoot });
|
|
1174
|
-
if (isSpawnFailure(startResult)) {
|
|
1175
|
-
throw startResult.error;
|
|
1176
|
-
}
|
|
1177
|
-
if (startResult.status !== 0) {
|
|
1178
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
1182
|
-
const currentBranch = currentBranchName(blocked.repoRoot);
|
|
1183
|
-
const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
|
|
1184
|
-
const repoRootPath = path.resolve(blocked.repoRoot);
|
|
1185
|
-
const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
|
|
1186
|
-
const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
|
|
1187
|
-
|
|
1188
|
-
if (!hasSafeWorktree || branchChanged) {
|
|
1189
|
-
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
1190
|
-
if (!restoreResult.ok) {
|
|
1191
|
-
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
1192
|
-
throw new Error(
|
|
1193
|
-
`sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
1194
|
-
(detail ? `\n${detail}` : ''),
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return {
|
|
1201
|
-
metadata,
|
|
1202
|
-
stdout: startResult.stdout || '',
|
|
1203
|
-
stderr: startResult.stderr || '',
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function cleanupProtectedBaseSandbox(repoRoot, metadata) {
|
|
1208
|
-
const result = {
|
|
1209
|
-
worktree: 'skipped',
|
|
1210
|
-
branch: 'skipped',
|
|
1211
|
-
note: 'missing sandbox metadata',
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
|
-
if (!metadata?.worktreePath || !metadata?.branch) {
|
|
1215
|
-
return result;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
if (fs.existsSync(metadata.worktreePath)) {
|
|
1219
|
-
const removeResult = run(
|
|
1220
|
-
'git',
|
|
1221
|
-
['-C', repoRoot, 'worktree', 'remove', '--force', metadata.worktreePath],
|
|
1222
|
-
{ timeout: 30_000 },
|
|
1223
|
-
);
|
|
1224
|
-
if (isSpawnFailure(removeResult)) {
|
|
1225
|
-
throw removeResult.error;
|
|
1226
|
-
}
|
|
1227
|
-
if (removeResult.status !== 0) {
|
|
1228
|
-
throw new Error(
|
|
1229
|
-
(removeResult.stderr || removeResult.stdout || 'failed to remove sandbox worktree').trim(),
|
|
1230
|
-
);
|
|
1231
|
-
}
|
|
1232
|
-
result.worktree = 'removed';
|
|
1233
|
-
} else {
|
|
1234
|
-
result.worktree = 'missing';
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
if (gitRefExists(repoRoot, `refs/heads/${metadata.branch}`)) {
|
|
1238
|
-
const branchDeleteResult = run(
|
|
1239
|
-
'git',
|
|
1240
|
-
['-C', repoRoot, 'branch', '-D', metadata.branch],
|
|
1241
|
-
{ timeout: 20_000 },
|
|
1242
|
-
);
|
|
1243
|
-
if (isSpawnFailure(branchDeleteResult)) {
|
|
1244
|
-
throw branchDeleteResult.error;
|
|
1245
|
-
}
|
|
1246
|
-
if (branchDeleteResult.status !== 0) {
|
|
1247
|
-
throw new Error(
|
|
1248
|
-
(branchDeleteResult.stderr || branchDeleteResult.stdout || 'failed to delete sandbox branch').trim(),
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
result.branch = 'deleted';
|
|
1252
|
-
} else {
|
|
1253
|
-
result.branch = 'missing';
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
result.note = 'sandbox worktree pruned';
|
|
1257
|
-
return result;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
function parseGitPathList(output) {
|
|
1261
|
-
return String(output || '')
|
|
1262
|
-
.split('\n')
|
|
1263
|
-
.map((line) => line.trim())
|
|
1264
|
-
.filter((line) => line && line !== LOCK_FILE_RELATIVE);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function collectDoctorChangedPaths(worktreePath) {
|
|
1268
|
-
const changed = new Set();
|
|
1269
|
-
const commands = [
|
|
1270
|
-
['diff', '--name-only'],
|
|
1271
|
-
['diff', '--cached', '--name-only'],
|
|
1272
|
-
['ls-files', '--others', '--exclude-standard'],
|
|
1273
|
-
];
|
|
1274
|
-
for (const gitArgs of commands) {
|
|
1275
|
-
const result = run('git', ['-C', worktreePath, ...gitArgs], { timeout: 20_000 });
|
|
1276
|
-
for (const filePath of parseGitPathList(result.stdout)) {
|
|
1277
|
-
changed.add(filePath);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
return Array.from(changed);
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
function collectDoctorDeletedPaths(worktreePath) {
|
|
1284
|
-
const deleted = new Set();
|
|
1285
|
-
const commands = [
|
|
1286
|
-
['diff', '--name-only', '--diff-filter=D'],
|
|
1287
|
-
['diff', '--cached', '--name-only', '--diff-filter=D'],
|
|
1288
|
-
];
|
|
1289
|
-
for (const gitArgs of commands) {
|
|
1290
|
-
const result = run('git', ['-C', worktreePath, ...gitArgs], { timeout: 20_000 });
|
|
1291
|
-
for (const filePath of parseGitPathList(result.stdout)) {
|
|
1292
|
-
deleted.add(filePath);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
return Array.from(deleted);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function collectWorktreeDirtyPaths(worktreePath) {
|
|
1299
|
-
const dirty = new Set();
|
|
1300
|
-
const commands = [
|
|
1301
|
-
['diff', '--name-only'],
|
|
1302
|
-
['diff', '--cached', '--name-only'],
|
|
1303
|
-
['ls-files', '--others', '--exclude-standard'],
|
|
1304
|
-
];
|
|
1305
|
-
for (const gitArgs of commands) {
|
|
1306
|
-
const result = run('git', ['-C', worktreePath, ...gitArgs], { timeout: 20_000 });
|
|
1307
|
-
for (const filePath of parseGitPathList(result.stdout)) {
|
|
1308
|
-
dirty.add(filePath);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
return Array.from(dirty);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function collectDoctorForceAddPaths(worktreePath) {
|
|
1315
|
-
return REQUIRED_MANAGED_REPO_FILES
|
|
1316
|
-
.filter((relativePath) => relativePath.startsWith('scripts/') || relativePath.startsWith('.githooks/'))
|
|
1317
|
-
.filter((relativePath) => fs.existsSync(path.join(worktreePath, relativePath)));
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function stripDoctorSandboxLocks(rawContent, branchName) {
|
|
1321
|
-
if (!rawContent || !branchName) {
|
|
1322
|
-
return rawContent;
|
|
1323
|
-
}
|
|
1324
|
-
try {
|
|
1325
|
-
const parsed = JSON.parse(rawContent);
|
|
1326
|
-
const locks = parsed && typeof parsed === 'object' && parsed.locks && typeof parsed.locks === 'object'
|
|
1327
|
-
? parsed.locks
|
|
1328
|
-
: null;
|
|
1329
|
-
if (!locks) {
|
|
1330
|
-
return rawContent;
|
|
1331
|
-
}
|
|
1332
|
-
let changed = false;
|
|
1333
|
-
const filteredLocks = {};
|
|
1334
|
-
for (const [filePath, lockInfo] of Object.entries(locks)) {
|
|
1335
|
-
if (lockInfo && lockInfo.branch === branchName) {
|
|
1336
|
-
changed = true;
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
filteredLocks[filePath] = lockInfo;
|
|
1340
|
-
}
|
|
1341
|
-
if (!changed) {
|
|
1342
|
-
return rawContent;
|
|
1343
|
-
}
|
|
1344
|
-
return `${JSON.stringify({ ...parsed, locks: filteredLocks }, null, 2)}\n`;
|
|
1345
|
-
} catch {
|
|
1346
|
-
return rawContent;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
function claimDoctorChangedLocks(metadata) {
|
|
1351
|
-
if (!metadata.branch) {
|
|
1352
|
-
return {
|
|
1353
|
-
status: 'skipped',
|
|
1354
|
-
note: 'missing sandbox branch metadata',
|
|
1355
|
-
changedCount: 0,
|
|
1356
|
-
deletedCount: 0,
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
const changedPaths = Array.from(new Set([
|
|
1361
|
-
...collectDoctorChangedPaths(metadata.worktreePath),
|
|
1362
|
-
...collectDoctorForceAddPaths(metadata.worktreePath),
|
|
1363
|
-
]));
|
|
1364
|
-
const deletedPaths = collectDoctorDeletedPaths(metadata.worktreePath);
|
|
1365
|
-
if (changedPaths.length > 0) {
|
|
1366
|
-
runPackageAsset('lockTool', ['claim', '--branch', metadata.branch, ...changedPaths], {
|
|
1367
|
-
cwd: metadata.worktreePath,
|
|
1368
|
-
timeout: 30_000,
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
if (deletedPaths.length > 0) {
|
|
1372
|
-
runPackageAsset('lockTool', ['allow-delete', '--branch', metadata.branch, ...deletedPaths], {
|
|
1373
|
-
cwd: metadata.worktreePath,
|
|
1374
|
-
timeout: 30_000,
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
return {
|
|
1379
|
-
status: 'claimed',
|
|
1380
|
-
note: 'claimed locks for doctor auto-commit',
|
|
1381
|
-
changedCount: changedPaths.length,
|
|
1382
|
-
deletedCount: deletedPaths.length,
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
function autoCommitDoctorSandboxChanges(metadata) {
|
|
1387
|
-
if (!metadata.worktreePath || !metadata.branch) {
|
|
1388
|
-
return {
|
|
1389
|
-
status: 'skipped',
|
|
1390
|
-
note: 'missing sandbox branch metadata',
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
claimDoctorChangedLocks(metadata);
|
|
1395
|
-
run(
|
|
1396
|
-
'git',
|
|
1397
|
-
['-C', metadata.worktreePath, 'add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`],
|
|
1398
|
-
{ timeout: 20_000 },
|
|
1399
|
-
);
|
|
1400
|
-
const forceAddPaths = collectDoctorForceAddPaths(metadata.worktreePath);
|
|
1401
|
-
if (forceAddPaths.length > 0) {
|
|
1402
|
-
run(
|
|
1403
|
-
'git',
|
|
1404
|
-
['-C', metadata.worktreePath, 'add', '-f', '--', ...forceAddPaths],
|
|
1405
|
-
{ timeout: 20_000 },
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
const staged = run(
|
|
1409
|
-
'git',
|
|
1410
|
-
['-C', metadata.worktreePath, 'diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`],
|
|
1411
|
-
{ timeout: 20_000 },
|
|
1412
|
-
);
|
|
1413
|
-
const stagedFiles = parseGitPathList(staged.stdout);
|
|
1414
|
-
if (stagedFiles.length === 0) {
|
|
1415
|
-
return {
|
|
1416
|
-
status: 'no-changes',
|
|
1417
|
-
note: 'no committable doctor changes found in sandbox',
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const commitResult = run(
|
|
1422
|
-
'git',
|
|
1423
|
-
['-C', metadata.worktreePath, 'commit', '-m', 'Auto-finish: gx doctor repairs'],
|
|
1424
|
-
{ timeout: 30_000 },
|
|
1425
|
-
);
|
|
1426
|
-
if (commitResult.status !== 0) {
|
|
1427
|
-
return {
|
|
1428
|
-
status: 'failed',
|
|
1429
|
-
note: 'doctor sandbox auto-commit failed',
|
|
1430
|
-
stdout: commitResult.stdout || '',
|
|
1431
|
-
stderr: commitResult.stderr || '',
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
return {
|
|
1436
|
-
status: 'committed',
|
|
1437
|
-
note: 'doctor sandbox repairs committed',
|
|
1438
|
-
commitMessage: 'Auto-finish: gx doctor repairs',
|
|
1439
|
-
stagedFiles,
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
function hasOriginRemote(repoRoot) {
|
|
1444
|
-
return run('git', ['-C', repoRoot, 'remote', 'get-url', 'origin']).status === 0;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function originRemoteLooksLikeGithub(repoRoot) {
|
|
1448
|
-
const originUrl = readGitConfig(repoRoot, 'remote.origin.url');
|
|
1449
|
-
if (!originUrl) {
|
|
1450
|
-
return false;
|
|
1451
|
-
}
|
|
1452
|
-
return /github\.com[:/]/i.test(originUrl);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function isCommandAvailable(commandName) {
|
|
1456
|
-
return run('which', [commandName]).status === 0;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
function extractAgentBranchFinishPrUrl(output) {
|
|
1460
|
-
const match = String(output || '').match(/\[agent-branch-finish\] PR:\s*(\S+)/);
|
|
1461
|
-
return match ? match[1] : '';
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
function doctorFinishFlowIsPending(output) {
|
|
1465
|
-
return (
|
|
1466
|
-
/\[agent-branch-finish\] PR merge not completed yet; leaving PR open\./.test(output) ||
|
|
1467
|
-
/\[agent-branch-finish\] Merge pending review\/check policy\. Branch cleanup skipped for now\./.test(output) ||
|
|
1468
|
-
/\[agent-branch-finish\] PR auto-merge enabled; waiting for required checks\/reviews\./.test(output)
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
|
|
1473
|
-
if (!hasOriginRemote(blocked.repoRoot)) {
|
|
1474
|
-
return {
|
|
1475
|
-
status: 'skipped',
|
|
1476
|
-
note: 'origin remote missing; skipped auto-finish',
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
|
|
1480
|
-
if (!explicitGhBin && !originRemoteLooksLikeGithub(blocked.repoRoot)) {
|
|
1481
|
-
return {
|
|
1482
|
-
status: 'skipped',
|
|
1483
|
-
note: 'origin remote is not GitHub; skipped auto-finish PR flow',
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
|
|
1488
|
-
if (!isCommandAvailable(ghBin)) {
|
|
1489
|
-
return {
|
|
1490
|
-
status: 'skipped',
|
|
1491
|
-
note: `'${ghBin}' not available; skipped auto-finish PR flow`,
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
const ghAuthStatus = run(ghBin, ['auth', 'status'], { timeout: 20_000 });
|
|
1495
|
-
if (ghAuthStatus.status !== 0) {
|
|
1496
|
-
return {
|
|
1497
|
-
status: 'skipped',
|
|
1498
|
-
note: `'${ghBin}' auth unavailable; skipped auto-finish PR flow`,
|
|
1499
|
-
stderr: ghAuthStatus.stderr || '',
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
const rawWaitTimeoutSeconds = Number.parseInt(process.env.GUARDEX_FINISH_WAIT_TIMEOUT_SECONDS || '1800', 10);
|
|
1504
|
-
const waitTimeoutSeconds =
|
|
1505
|
-
Number.isFinite(rawWaitTimeoutSeconds) && rawWaitTimeoutSeconds >= 30 ? rawWaitTimeoutSeconds : 1800;
|
|
1506
|
-
const finishTimeoutMs = Math.max(180_000, (waitTimeoutSeconds + 60) * 1000);
|
|
1507
|
-
const waitForMergeArg = options.waitForMerge === false ? '--no-wait-for-merge' : '--wait-for-merge';
|
|
1508
|
-
|
|
1509
|
-
const finishResult = runPackageAsset(
|
|
1510
|
-
'branchFinish',
|
|
1511
|
-
['--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg, '--cleanup'],
|
|
1512
|
-
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
|
|
1513
|
-
);
|
|
1514
|
-
if (isSpawnFailure(finishResult)) {
|
|
1515
|
-
return {
|
|
1516
|
-
status: 'failed',
|
|
1517
|
-
note: 'doctor sandbox finish flow errored',
|
|
1518
|
-
stdout: finishResult.stdout || '',
|
|
1519
|
-
stderr: finishResult.stderr || '',
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
if (finishResult.status !== 0) {
|
|
1523
|
-
return {
|
|
1524
|
-
status: 'failed',
|
|
1525
|
-
note: 'doctor sandbox finish flow failed',
|
|
1526
|
-
stdout: finishResult.stdout || '',
|
|
1527
|
-
stderr: finishResult.stderr || '',
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
const combinedOutput = `${finishResult.stdout || ''}\n${finishResult.stderr || ''}`;
|
|
1532
|
-
if (doctorFinishFlowIsPending(combinedOutput)) {
|
|
1533
|
-
return {
|
|
1534
|
-
status: 'pending',
|
|
1535
|
-
note: 'PR created and waiting for merge policy/checks',
|
|
1536
|
-
prUrl: extractAgentBranchFinishPrUrl(combinedOutput),
|
|
1537
|
-
stdout: finishResult.stdout || '',
|
|
1538
|
-
stderr: finishResult.stderr || '',
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
return {
|
|
1543
|
-
status: 'completed',
|
|
1544
|
-
note: 'doctor sandbox finish flow completed',
|
|
1545
|
-
stdout: finishResult.stdout || '',
|
|
1546
|
-
stderr: finishResult.stderr || '',
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata, autoCommitResult, finishResult) {
|
|
1551
|
-
if (options.dryRun) {
|
|
1552
|
-
return {
|
|
1553
|
-
status: autoCommitResult.status === 'committed' ? 'would-merge' : 'skipped',
|
|
1554
|
-
note: autoCommitResult.status === 'committed'
|
|
1555
|
-
? 'dry run: would fast-forward tracked doctor repairs into the protected base workspace'
|
|
1556
|
-
: 'dry run skips tracked repair merge',
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
if (autoCommitResult.status !== 'committed') {
|
|
1561
|
-
return {
|
|
1562
|
-
status: autoCommitResult.status === 'no-changes' ? 'unchanged' : 'skipped',
|
|
1563
|
-
note: autoCommitResult.status === 'no-changes'
|
|
1564
|
-
? 'no tracked doctor repairs needed in the protected base workspace'
|
|
1565
|
-
: 'tracked doctor repair merge skipped',
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
if (finishResult.status !== 'skipped') {
|
|
1570
|
-
return {
|
|
1571
|
-
status: 'skipped',
|
|
1572
|
-
note: finishResult.status === 'failed'
|
|
1573
|
-
? 'tracked doctor repairs remain in the sandbox after finish failure'
|
|
1574
|
-
: 'tracked doctor repairs are being delivered through the sandbox finish flow',
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
const allowedPaths = new Set([
|
|
1579
|
-
...(autoCommitResult.stagedFiles || []),
|
|
1580
|
-
...OMX_SCAFFOLD_DIRECTORIES,
|
|
1581
|
-
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
1582
|
-
...REQUIRED_MANAGED_REPO_FILES,
|
|
1583
|
-
'bin',
|
|
1584
|
-
'package.json',
|
|
1585
|
-
'.gitignore',
|
|
1586
|
-
'AGENTS.md',
|
|
1587
|
-
]);
|
|
1588
|
-
const dirtyPaths = collectWorktreeDirtyPaths(blocked.repoRoot);
|
|
1589
|
-
let stashRef = '';
|
|
1590
|
-
if (dirtyPaths.length > 0) {
|
|
1591
|
-
const unexpectedPaths = dirtyPaths.filter((filePath) => {
|
|
1592
|
-
if (allowedPaths.has(filePath)) {
|
|
1593
|
-
return false;
|
|
1594
|
-
}
|
|
1595
|
-
return !AGENT_WORKTREE_RELATIVE_DIRS.some(
|
|
1596
|
-
(relativeDir) => filePath === relativeDir || filePath.startsWith(`${relativeDir}/`),
|
|
1597
|
-
);
|
|
1598
|
-
});
|
|
1599
|
-
if (unexpectedPaths.length > 0) {
|
|
1600
|
-
return {
|
|
1601
|
-
status: 'failed',
|
|
1602
|
-
note: `protected branch workspace has unrelated local changes: ${unexpectedPaths.join(', ')}`,
|
|
1603
|
-
};
|
|
1604
|
-
}
|
|
1605
|
-
const stashMessage = `guardex-doctor-merge-${Date.now()}`;
|
|
1606
|
-
const stashResult = run(
|
|
1607
|
-
'git',
|
|
1608
|
-
['-C', blocked.repoRoot, 'stash', 'push', '--all', '--message', stashMessage],
|
|
1609
|
-
{ timeout: 30_000 },
|
|
1610
|
-
);
|
|
1611
|
-
if (isSpawnFailure(stashResult)) {
|
|
1612
|
-
return {
|
|
1613
|
-
status: 'failed',
|
|
1614
|
-
note: 'could not stash protected branch doctor drift before merge',
|
|
1615
|
-
stdout: stashResult.stdout || '',
|
|
1616
|
-
stderr: stashResult.stderr || '',
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
if (stashResult.status !== 0) {
|
|
1620
|
-
return {
|
|
1621
|
-
status: 'failed',
|
|
1622
|
-
note: 'stashing protected branch doctor drift failed',
|
|
1623
|
-
stdout: stashResult.stdout || '',
|
|
1624
|
-
stderr: stashResult.stderr || '',
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const stashLookup = run(
|
|
1629
|
-
'git',
|
|
1630
|
-
['-C', blocked.repoRoot, 'stash', 'list'],
|
|
1631
|
-
{ timeout: 20_000 },
|
|
1632
|
-
);
|
|
1633
|
-
stashRef = String(stashLookup.stdout || '')
|
|
1634
|
-
.split('\n')
|
|
1635
|
-
.find((line) => line.includes(stashMessage))
|
|
1636
|
-
?.split(':')[0]
|
|
1637
|
-
?.trim() || '';
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
1641
|
-
if (!restoreResult.ok) {
|
|
1642
|
-
if (stashRef) {
|
|
1643
|
-
run('git', ['-C', blocked.repoRoot, 'stash', 'apply', stashRef], { timeout: 30_000 });
|
|
1644
|
-
}
|
|
1645
|
-
return {
|
|
1646
|
-
status: 'failed',
|
|
1647
|
-
note: `could not restore protected branch '${blocked.branch}' before applying sandbox repairs`,
|
|
1648
|
-
stdout: restoreResult.stdout || '',
|
|
1649
|
-
stderr: restoreResult.stderr || '',
|
|
1650
|
-
};
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
const mergeResult = run(
|
|
1654
|
-
'git',
|
|
1655
|
-
['-C', blocked.repoRoot, 'merge', '--ff-only', metadata.branch],
|
|
1656
|
-
{ timeout: 30_000 },
|
|
1657
|
-
);
|
|
1658
|
-
if (isSpawnFailure(mergeResult)) {
|
|
1659
|
-
if (stashRef) {
|
|
1660
|
-
run('git', ['-C', blocked.repoRoot, 'stash', 'apply', stashRef], { timeout: 30_000 });
|
|
1661
|
-
}
|
|
1662
|
-
return {
|
|
1663
|
-
status: 'failed',
|
|
1664
|
-
note: 'tracked doctor repair merge errored',
|
|
1665
|
-
stdout: mergeResult.stdout || '',
|
|
1666
|
-
stderr: mergeResult.stderr || '',
|
|
1667
|
-
};
|
|
1668
|
-
}
|
|
1669
|
-
if (mergeResult.status !== 0) {
|
|
1670
|
-
if (stashRef) {
|
|
1671
|
-
run('git', ['-C', blocked.repoRoot, 'stash', 'apply', stashRef], { timeout: 30_000 });
|
|
1672
|
-
}
|
|
1673
|
-
return {
|
|
1674
|
-
status: 'failed',
|
|
1675
|
-
note: 'tracked doctor repair merge failed',
|
|
1676
|
-
stdout: mergeResult.stdout || '',
|
|
1677
|
-
stderr: mergeResult.stderr || '',
|
|
1678
|
-
};
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
let cleanupResult;
|
|
1682
|
-
try {
|
|
1683
|
-
cleanupResult = cleanupProtectedBaseSandbox(blocked.repoRoot, metadata);
|
|
1684
|
-
} catch (error) {
|
|
1685
|
-
return {
|
|
1686
|
-
status: 'failed',
|
|
1687
|
-
note: `tracked doctor repair merge succeeded but sandbox cleanup failed: ${error.message}`,
|
|
1688
|
-
stdout: mergeResult.stdout || '',
|
|
1689
|
-
stderr: mergeResult.stderr || '',
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
let hookRefreshResult;
|
|
1694
|
-
try {
|
|
1695
|
-
hookRefreshResult = configureHooks(blocked.repoRoot, false);
|
|
1696
|
-
} catch (error) {
|
|
1697
|
-
return {
|
|
1698
|
-
status: 'failed',
|
|
1699
|
-
note: `tracked doctor repair merge succeeded but local hook refresh failed: ${error.message}`,
|
|
1700
|
-
stdout: mergeResult.stdout || '',
|
|
1701
|
-
stderr: mergeResult.stderr || '',
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
if (stashRef) {
|
|
1706
|
-
run('git', ['-C', blocked.repoRoot, 'stash', 'drop', stashRef], { timeout: 20_000 });
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
return {
|
|
1710
|
-
status: 'merged',
|
|
1711
|
-
note: 'fast-forwarded tracked doctor repairs into the protected base workspace',
|
|
1712
|
-
stdout: mergeResult.stdout || '',
|
|
1713
|
-
stderr: mergeResult.stderr || '',
|
|
1714
|
-
cleanup: cleanupResult,
|
|
1715
|
-
hookRefresh: hookRefreshResult,
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
/**
|
|
1720
|
-
* @param {string} [note]
|
|
1721
|
-
* @returns {OperationResult}
|
|
1722
|
-
*/
|
|
1723
|
-
function createDoctorSkippedOperation(note = 'sandbox doctor did not complete successfully') {
|
|
1724
|
-
return {
|
|
1725
|
-
status: 'skipped',
|
|
1726
|
-
note,
|
|
1727
|
-
};
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
/**
|
|
1731
|
-
* @param {string} [note]
|
|
1732
|
-
* @returns {AutoFinishSummary}
|
|
1733
|
-
*/
|
|
1734
|
-
function createSkippedDoctorAutoFinishSummary(note = 'sandbox doctor did not complete successfully') {
|
|
1735
|
-
return {
|
|
1736
|
-
enabled: false,
|
|
1737
|
-
attempted: 0,
|
|
1738
|
-
completed: 0,
|
|
1739
|
-
skipped: 0,
|
|
1740
|
-
failed: 0,
|
|
1741
|
-
details: [`Skipped auto-finish sweep (${note}).`],
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
/**
|
|
1746
|
-
* Default the lifecycle to skipped states until the nested doctor run succeeds.
|
|
1747
|
-
*
|
|
1748
|
-
* @param {string} [note]
|
|
1749
|
-
* @returns {DoctorSandboxExecution}
|
|
1750
|
-
*/
|
|
1751
|
-
function createDoctorSandboxExecutionState(note = 'sandbox doctor did not complete successfully') {
|
|
1752
|
-
return {
|
|
1753
|
-
autoCommit: createDoctorSkippedOperation(note),
|
|
1754
|
-
finish: createDoctorSkippedOperation(note),
|
|
1755
|
-
protectedBaseRepairSync: createDoctorSkippedOperation(note),
|
|
1756
|
-
lockSync: createDoctorSkippedOperation(note),
|
|
1757
|
-
omxScaffoldSync: createDoctorSkippedOperation(note),
|
|
1758
|
-
autoFinish: createSkippedDoctorAutoFinishSummary(note),
|
|
1759
|
-
sandboxLockContent: null,
|
|
1760
|
-
};
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
/**
|
|
1764
|
-
* @param {string} repoRoot
|
|
1765
|
-
* @param {boolean} dryRun
|
|
1766
|
-
* @returns {OperationResult}
|
|
1767
|
-
*/
|
|
1768
|
-
function summarizeDoctorOmxScaffoldSync(repoRoot, dryRun) {
|
|
1769
|
-
const omxScaffoldOps = ensureOmxScaffold(repoRoot, dryRun);
|
|
1770
|
-
const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
1771
|
-
if (changedOmxPaths.length === 0) {
|
|
1772
|
-
return {
|
|
1773
|
-
status: 'unchanged',
|
|
1774
|
-
note: '.omx scaffold already in sync',
|
|
1775
|
-
operations: omxScaffoldOps,
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
return {
|
|
1779
|
-
status: dryRun ? 'would-sync' : 'synced',
|
|
1780
|
-
note: `${dryRun ? 'would sync' : 'synced'} ${changedOmxPaths.length} .omx path(s)`,
|
|
1781
|
-
operations: omxScaffoldOps,
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
/**
|
|
1786
|
-
* @param {string} repoRoot
|
|
1787
|
-
* @param {SandboxMetadata} metadata
|
|
1788
|
-
* @returns {DoctorLockSyncState}
|
|
1789
|
-
*/
|
|
1790
|
-
function syncDoctorLockRegistryBeforeMerge(repoRoot, metadata) {
|
|
1791
|
-
const sandboxLockPath = path.join(metadata.worktreePath, LOCK_FILE_RELATIVE);
|
|
1792
|
-
const baseLockPath = path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
1793
|
-
if (!fs.existsSync(baseLockPath)) {
|
|
1794
|
-
return {
|
|
1795
|
-
result: {
|
|
1796
|
-
status: 'skipped',
|
|
1797
|
-
note: `${LOCK_FILE_RELATIVE} missing in protected base workspace`,
|
|
1798
|
-
},
|
|
1799
|
-
sandboxLockContent: null,
|
|
1800
|
-
};
|
|
1801
|
-
}
|
|
1802
|
-
if (!fs.existsSync(sandboxLockPath)) {
|
|
1803
|
-
return {
|
|
1804
|
-
result: {
|
|
1805
|
-
status: 'skipped',
|
|
1806
|
-
note: `${LOCK_FILE_RELATIVE} missing in sandbox worktree`,
|
|
1807
|
-
},
|
|
1808
|
-
sandboxLockContent: null,
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
const sourceContent = stripDoctorSandboxLocks(
|
|
1813
|
-
fs.readFileSync(sandboxLockPath, 'utf8'),
|
|
1814
|
-
metadata.branch,
|
|
1815
|
-
);
|
|
1816
|
-
const destinationContent = fs.readFileSync(baseLockPath, 'utf8');
|
|
1817
|
-
if (sourceContent === destinationContent) {
|
|
1818
|
-
return {
|
|
1819
|
-
result: {
|
|
1820
|
-
status: 'unchanged',
|
|
1821
|
-
note: `${LOCK_FILE_RELATIVE} already in sync`,
|
|
1822
|
-
},
|
|
1823
|
-
sandboxLockContent: sourceContent,
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
fs.mkdirSync(path.dirname(baseLockPath), { recursive: true });
|
|
1828
|
-
fs.writeFileSync(baseLockPath, sourceContent, 'utf8');
|
|
1829
|
-
return {
|
|
1830
|
-
result: {
|
|
1831
|
-
status: 'synced',
|
|
1832
|
-
note: `${LOCK_FILE_RELATIVE} synced from sandbox`,
|
|
1833
|
-
},
|
|
1834
|
-
sandboxLockContent: sourceContent,
|
|
1835
|
-
};
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
/**
|
|
1839
|
-
* @param {string} repoRoot
|
|
1840
|
-
* @param {string | null} sandboxLockContent
|
|
1841
|
-
* @returns {OperationResult}
|
|
1842
|
-
*/
|
|
1843
|
-
function syncDoctorLockRegistryAfterMerge(repoRoot, sandboxLockContent) {
|
|
1844
|
-
if (sandboxLockContent === null) {
|
|
1845
|
-
return {
|
|
1846
|
-
status: 'skipped',
|
|
1847
|
-
note: `${LOCK_FILE_RELATIVE} missing in sandbox worktree`,
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
const baseLockPath = path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
1852
|
-
if (!fs.existsSync(baseLockPath)) {
|
|
1853
|
-
fs.mkdirSync(path.dirname(baseLockPath), { recursive: true });
|
|
1854
|
-
fs.writeFileSync(baseLockPath, sandboxLockContent, 'utf8');
|
|
1855
|
-
return {
|
|
1856
|
-
status: 'synced',
|
|
1857
|
-
note: `${LOCK_FILE_RELATIVE} recreated from sandbox`,
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
const destinationContent = fs.readFileSync(baseLockPath, 'utf8');
|
|
1862
|
-
if (sandboxLockContent === destinationContent) {
|
|
1863
|
-
return {
|
|
1864
|
-
status: 'unchanged',
|
|
1865
|
-
note: `${LOCK_FILE_RELATIVE} already in sync`,
|
|
1866
|
-
};
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
fs.mkdirSync(path.dirname(baseLockPath), { recursive: true });
|
|
1870
|
-
fs.writeFileSync(baseLockPath, sandboxLockContent, 'utf8');
|
|
1871
|
-
return {
|
|
1872
|
-
status: 'synced',
|
|
1873
|
-
note: `${LOCK_FILE_RELATIVE} synced from sandbox`,
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
/**
|
|
1878
|
-
* @param {object} options
|
|
1879
|
-
* @param {{ repoRoot: string, branch: string }} blocked
|
|
1880
|
-
* @param {SandboxMetadata} metadata
|
|
1881
|
-
* @returns {DoctorSandboxExecution}
|
|
1882
|
-
*/
|
|
1883
|
-
function executeDoctorSandboxLifecycle(options, blocked, metadata) {
|
|
1884
|
-
const execution = createDoctorSandboxExecutionState();
|
|
1885
|
-
const dryRun = Boolean(options.dryRun);
|
|
1886
|
-
|
|
1887
|
-
execution.omxScaffoldSync = summarizeDoctorOmxScaffoldSync(blocked.repoRoot, dryRun);
|
|
1888
|
-
|
|
1889
|
-
if (!dryRun) {
|
|
1890
|
-
execution.autoCommit = autoCommitDoctorSandboxChanges(metadata);
|
|
1891
|
-
if (execution.autoCommit.status === 'committed') {
|
|
1892
|
-
execution.finish = finishDoctorSandboxBranch(blocked, metadata, options);
|
|
1893
|
-
} else if (execution.autoCommit.status === 'no-changes') {
|
|
1894
|
-
execution.finish = createDoctorSkippedOperation('no doctor changes to auto-finish');
|
|
1895
|
-
} else if (execution.autoCommit.status !== 'failed') {
|
|
1896
|
-
execution.finish = createDoctorSkippedOperation('auto-commit did not run');
|
|
1897
|
-
}
|
|
1898
|
-
} else {
|
|
1899
|
-
execution.autoCommit = createDoctorSkippedOperation('dry-run skips doctor sandbox auto-commit');
|
|
1900
|
-
execution.finish = createDoctorSkippedOperation('dry-run skips doctor sandbox finish flow');
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
const lockSyncState = syncDoctorLockRegistryBeforeMerge(blocked.repoRoot, metadata);
|
|
1904
|
-
execution.lockSync = lockSyncState.result;
|
|
1905
|
-
execution.sandboxLockContent = lockSyncState.sandboxLockContent;
|
|
1906
|
-
|
|
1907
|
-
execution.protectedBaseRepairSync = mergeDoctorSandboxRepairsBackToProtectedBase(
|
|
1908
|
-
options,
|
|
1909
|
-
blocked,
|
|
1910
|
-
metadata,
|
|
1911
|
-
execution.autoCommit,
|
|
1912
|
-
execution.finish,
|
|
1913
|
-
);
|
|
1914
|
-
|
|
1915
|
-
execution.omxScaffoldSync = summarizeDoctorOmxScaffoldSync(blocked.repoRoot, dryRun);
|
|
1916
|
-
execution.lockSync = syncDoctorLockRegistryAfterMerge(
|
|
1917
|
-
blocked.repoRoot,
|
|
1918
|
-
execution.sandboxLockContent,
|
|
1919
|
-
);
|
|
1920
|
-
execution.autoFinish = autoFinishReadyAgentBranches(blocked.repoRoot, {
|
|
1921
|
-
baseBranch: blocked.branch,
|
|
1922
|
-
dryRun: options.dryRun,
|
|
1923
|
-
waitForMerge: options.waitForMerge,
|
|
1924
|
-
excludeBranches: [metadata.branch],
|
|
1925
|
-
});
|
|
1926
|
-
|
|
1927
|
-
return execution;
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
function emitDoctorSandboxJsonOutput(nestedResult, execution) {
|
|
1931
|
-
if (nestedResult.stdout) {
|
|
1932
|
-
if (nestedResult.status === 0) {
|
|
1933
|
-
try {
|
|
1934
|
-
const parsed = JSON.parse(nestedResult.stdout);
|
|
1935
|
-
process.stdout.write(
|
|
1936
|
-
JSON.stringify(
|
|
1937
|
-
{
|
|
1938
|
-
...parsed,
|
|
1939
|
-
protectedBaseRepairSync: execution.protectedBaseRepairSync,
|
|
1940
|
-
sandboxOmxScaffoldSync: execution.omxScaffoldSync,
|
|
1941
|
-
sandboxLockSync: execution.lockSync,
|
|
1942
|
-
sandboxAutoCommit: execution.autoCommit,
|
|
1943
|
-
sandboxFinish: execution.finish,
|
|
1944
|
-
autoFinish: execution.autoFinish,
|
|
1945
|
-
},
|
|
1946
|
-
null,
|
|
1947
|
-
2,
|
|
1948
|
-
) + '\n',
|
|
1949
|
-
);
|
|
1950
|
-
} catch {
|
|
1951
|
-
process.stdout.write(nestedResult.stdout);
|
|
1952
|
-
}
|
|
1953
|
-
} else {
|
|
1954
|
-
process.stdout.write(nestedResult.stdout);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
/**
|
|
1961
|
-
* @param {object} options
|
|
1962
|
-
* @param {{ branch: string }} blocked
|
|
1963
|
-
* @param {SandboxMetadata} metadata
|
|
1964
|
-
* @param {SandboxStartResult} startResult
|
|
1965
|
-
* @param {any} nestedResult
|
|
1966
|
-
* @param {DoctorSandboxExecution} execution
|
|
1967
|
-
*/
|
|
1968
|
-
function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult, nestedResult, execution) {
|
|
1969
|
-
console.log(
|
|
1970
|
-
`[${TOOL_NAME}] doctor detected protected branch '${blocked.branch}'. ` +
|
|
1971
|
-
`Running repairs in sandbox branch '${metadata.branch || 'agent/<auto>'}'.`,
|
|
1972
|
-
);
|
|
1973
|
-
if (startResult.stdout) process.stdout.write(startResult.stdout);
|
|
1974
|
-
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
1975
|
-
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
1976
|
-
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
1977
|
-
if (nestedResult.status !== 0) {
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
if (execution.autoCommit.status === 'committed') {
|
|
1982
|
-
console.log(
|
|
1983
|
-
`[${TOOL_NAME}] Auto-committed doctor repairs in sandbox branch '${metadata.branch}'.`,
|
|
1984
|
-
);
|
|
1985
|
-
} else if (execution.autoCommit.status === 'failed') {
|
|
1986
|
-
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit failed; branch left for manual follow-up.`);
|
|
1987
|
-
if (execution.autoCommit.stdout) process.stdout.write(execution.autoCommit.stdout);
|
|
1988
|
-
if (execution.autoCommit.stderr) process.stderr.write(execution.autoCommit.stderr);
|
|
1989
|
-
} else {
|
|
1990
|
-
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${execution.autoCommit.note}.`);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
if (execution.protectedBaseRepairSync.status === 'merged') {
|
|
1994
|
-
console.log(`[${TOOL_NAME}] Fast-forwarded tracked doctor repairs into the protected branch workspace.`);
|
|
1995
|
-
} else if (execution.protectedBaseRepairSync.status === 'unchanged') {
|
|
1996
|
-
console.log(`[${TOOL_NAME}] Protected branch workspace already had the tracked doctor repairs.`);
|
|
1997
|
-
} else if (execution.protectedBaseRepairSync.status === 'would-merge') {
|
|
1998
|
-
console.log(`[${TOOL_NAME}] Dry run: would fast-forward tracked doctor repairs into the protected branch workspace.`);
|
|
1999
|
-
} else if (execution.protectedBaseRepairSync.status === 'failed') {
|
|
2000
|
-
console.log(`[${TOOL_NAME}] Protected branch tracked repair merge failed: ${execution.protectedBaseRepairSync.note}.`);
|
|
2001
|
-
if (execution.protectedBaseRepairSync.stdout) process.stdout.write(execution.protectedBaseRepairSync.stdout);
|
|
2002
|
-
if (execution.protectedBaseRepairSync.stderr) process.stderr.write(execution.protectedBaseRepairSync.stderr);
|
|
2003
|
-
} else {
|
|
2004
|
-
console.log(`[${TOOL_NAME}] Protected branch tracked repair merge skipped: ${execution.protectedBaseRepairSync.note}.`);
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
if (execution.lockSync.status === 'synced') {
|
|
2008
|
-
console.log(
|
|
2009
|
-
`[${TOOL_NAME}] Synced repaired lock registry back to protected branch workspace (${LOCK_FILE_RELATIVE}).`,
|
|
2010
|
-
);
|
|
2011
|
-
} else if (execution.lockSync.status === 'unchanged') {
|
|
2012
|
-
console.log(`[${TOOL_NAME}] Lock registry already synced in protected branch workspace.`);
|
|
2013
|
-
} else {
|
|
2014
|
-
console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${execution.lockSync.note}.`);
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
if (execution.finish.status === 'completed') {
|
|
2018
|
-
console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
|
|
2019
|
-
if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
|
|
2020
|
-
if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
|
|
2021
|
-
} else if (execution.finish.status === 'pending') {
|
|
2022
|
-
console.log(
|
|
2023
|
-
`[${TOOL_NAME}] Auto-finish pending for sandbox branch '${metadata.branch}': ${execution.finish.note}.`,
|
|
2024
|
-
);
|
|
2025
|
-
if (execution.finish.prUrl) {
|
|
2026
|
-
console.log(`[${TOOL_NAME}] PR: ${execution.finish.prUrl}`);
|
|
2027
|
-
}
|
|
2028
|
-
if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
|
|
2029
|
-
if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
|
|
2030
|
-
} else if (execution.finish.status === 'failed') {
|
|
2031
|
-
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
2032
|
-
if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
|
|
2033
|
-
if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
|
|
2034
|
-
} else {
|
|
2035
|
-
console.log(`[${TOOL_NAME}] Auto-finish skipped: ${execution.finish.note}.`);
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
printAutoFinishSummary(execution.autoFinish, {
|
|
2039
|
-
baseBranch: blocked.branch,
|
|
2040
|
-
verbose: options.verboseAutoFinish,
|
|
2041
|
-
});
|
|
2042
|
-
if (execution.omxScaffoldSync.status === 'synced') {
|
|
2043
|
-
console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
|
|
2044
|
-
} else if (execution.omxScaffoldSync.status === 'unchanged') {
|
|
2045
|
-
console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
|
|
2046
|
-
} else if (execution.omxScaffoldSync.status === 'would-sync') {
|
|
2047
|
-
console.log(`[${TOOL_NAME}] Dry run: would sync .omx scaffold back to protected branch workspace.`);
|
|
2048
|
-
} else {
|
|
2049
|
-
console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${execution.omxScaffoldSync.note}.`);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
function setDoctorSandboxExitCode(nestedResult, execution) {
|
|
2054
|
-
if (typeof nestedResult.status === 'number') {
|
|
2055
|
-
let exitCode = nestedResult.status;
|
|
2056
|
-
if (exitCode === 0 && execution.autoCommit.status === 'failed') {
|
|
2057
|
-
exitCode = 1;
|
|
2058
|
-
}
|
|
2059
|
-
if (
|
|
2060
|
-
exitCode === 0 &&
|
|
2061
|
-
execution.autoCommit.status === 'committed' &&
|
|
2062
|
-
(execution.finish.status === 'failed' || execution.finish.status === 'pending')
|
|
2063
|
-
) {
|
|
2064
|
-
exitCode = 1;
|
|
2065
|
-
}
|
|
2066
|
-
if (exitCode === 0 && execution.protectedBaseRepairSync.status === 'failed') {
|
|
2067
|
-
exitCode = 1;
|
|
2068
|
-
}
|
|
2069
|
-
process.exitCode = exitCode;
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
process.exitCode = 1;
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function runDoctorInSandbox(options, blocked) {
|
|
2076
|
-
/** @type {SandboxStartResult} */
|
|
2077
|
-
const startResult = startProtectedBaseSandbox(blocked, {
|
|
2078
|
-
taskName: `${SHORT_TOOL_NAME}-doctor`,
|
|
2079
|
-
sandboxSuffix: 'gx-doctor',
|
|
2080
|
-
});
|
|
2081
|
-
const metadata = startResult.metadata;
|
|
2082
|
-
|
|
2083
|
-
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
2084
|
-
const nestedResult = run(
|
|
2085
|
-
process.execPath,
|
|
2086
|
-
[__filename, ...buildSandboxDoctorArgs(options, sandboxTarget)],
|
|
2087
|
-
{ cwd: metadata.worktreePath },
|
|
2088
|
-
);
|
|
2089
|
-
if (isSpawnFailure(nestedResult)) {
|
|
2090
|
-
throw nestedResult.error;
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
const execution = nestedResult.status === 0
|
|
2094
|
-
? executeDoctorSandboxLifecycle(options, blocked, metadata)
|
|
2095
|
-
: createDoctorSandboxExecutionState();
|
|
2096
|
-
|
|
2097
|
-
if (options.json) {
|
|
2098
|
-
emitDoctorSandboxJsonOutput(nestedResult, execution);
|
|
2099
|
-
} else {
|
|
2100
|
-
emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult, nestedResult, execution);
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
setDoctorSandboxExitCode(nestedResult, execution);
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
function runSetupInSandbox(options, blocked, repoLabel = '') {
|
|
2107
|
-
const startResult = startProtectedBaseSandbox(blocked, {
|
|
2108
|
-
taskName: `${SHORT_TOOL_NAME}-setup`,
|
|
2109
|
-
sandboxSuffix: 'gx-setup',
|
|
2110
|
-
});
|
|
2111
|
-
const metadata = startResult.metadata;
|
|
2112
|
-
|
|
2113
|
-
if (startResult.stdout) process.stdout.write(startResult.stdout);
|
|
2114
|
-
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
2115
|
-
console.log(
|
|
2116
|
-
`[${TOOL_NAME}] setup blocked on protected branch '${blocked.branch}' in an initialized repo; ` +
|
|
2117
|
-
'refreshing through a sandbox worktree and syncing managed bootstrap files back locally.',
|
|
2118
|
-
);
|
|
2119
|
-
|
|
2120
|
-
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
2121
|
-
const nestedResult = run(
|
|
2122
|
-
process.execPath,
|
|
2123
|
-
[__filename, ...buildSandboxSetupArgs(options, sandboxTarget)],
|
|
2124
|
-
{ cwd: metadata.worktreePath },
|
|
2125
|
-
);
|
|
2126
|
-
if (isSpawnFailure(nestedResult)) {
|
|
2127
|
-
throw nestedResult.error;
|
|
2128
|
-
}
|
|
2129
|
-
if (nestedResult.status !== 0) {
|
|
2130
|
-
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
2131
|
-
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
2132
|
-
throw new Error(
|
|
2133
|
-
`sandboxed setup failed for protected branch '${blocked.branch}'. ` +
|
|
2134
|
-
`Inspect sandbox at ${metadata.worktreePath}`,
|
|
2135
|
-
);
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
const syncOptions = {
|
|
2139
|
-
...options,
|
|
2140
|
-
target: blocked.repoRoot,
|
|
2141
|
-
recursive: false,
|
|
2142
|
-
allowProtectedBaseWrite: true,
|
|
2143
|
-
};
|
|
2144
|
-
const { installPayload, fixPayload, parentWorkspace } = runSetupBootstrapInternal(syncOptions);
|
|
2145
|
-
printOperations(`Setup/install${repoLabel}`, installPayload, syncOptions.dryRun);
|
|
2146
|
-
printOperations(`Setup/fix${repoLabel}`, fixPayload, syncOptions.dryRun);
|
|
2147
|
-
if (!syncOptions.dryRun && parentWorkspace) {
|
|
2148
|
-
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
const scanResult = runScanInternal({ target: blocked.repoRoot, json: false });
|
|
2152
|
-
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
2153
|
-
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
2154
|
-
baseBranch: currentBaseBranch,
|
|
2155
|
-
dryRun: syncOptions.dryRun,
|
|
2156
|
-
});
|
|
2157
|
-
printScanResult(scanResult, false);
|
|
2158
|
-
if (autoFinishSummary.enabled) {
|
|
2159
|
-
console.log(
|
|
2160
|
-
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
2161
|
-
);
|
|
2162
|
-
for (const detail of autoFinishSummary.details) {
|
|
2163
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
2164
|
-
}
|
|
2165
|
-
} else if (autoFinishSummary.details.length > 0) {
|
|
2166
|
-
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
const cleanupResult = cleanupProtectedBaseSandbox(blocked.repoRoot, metadata);
|
|
2170
|
-
console.log(
|
|
2171
|
-
`[${TOOL_NAME}] Protected-base setup sandbox cleanup: ${cleanupResult.note} ` +
|
|
2172
|
-
`(worktree=${cleanupResult.worktree}, branch=${cleanupResult.branch}).`,
|
|
2173
|
-
);
|
|
2174
|
-
|
|
2175
|
-
return {
|
|
2176
|
-
scanResult,
|
|
2177
|
-
};
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
function todayDateStamp() {
|
|
2182
|
-
return new Date().toISOString().slice(0, 10);
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
function inferGithubRepoFromOrigin(repoRoot) {
|
|
2186
|
-
const rawOrigin = readGitConfig(repoRoot, 'remote.origin.url');
|
|
2187
|
-
if (!rawOrigin) return '';
|
|
2188
|
-
|
|
2189
|
-
const httpsMatch = rawOrigin.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
2190
|
-
if (!httpsMatch) return '';
|
|
2191
|
-
const slug = (httpsMatch[1] || '').replace(/^\/+/, '').trim();
|
|
2192
|
-
if (!slug || !slug.includes('/')) return '';
|
|
2193
|
-
return `github.com/${slug}`;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
function inferGithubRepoSlug(rawValue) {
|
|
2197
|
-
const raw = String(rawValue || '').trim();
|
|
2198
|
-
if (!raw) return '';
|
|
2199
|
-
const match = raw.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
2200
|
-
if (!match) return '';
|
|
2201
|
-
const slug = String(match[1] || '')
|
|
2202
|
-
.replace(/^\/+/, '')
|
|
2203
|
-
.replace(/^github\.com\//i, '')
|
|
2204
|
-
.trim();
|
|
2205
|
-
if (!slug || !slug.includes('/')) return '';
|
|
2206
|
-
return slug;
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
function resolveScorecardRepo(repoRoot, explicitRepo) {
|
|
2210
|
-
if (explicitRepo) {
|
|
2211
|
-
return explicitRepo.trim();
|
|
2212
|
-
}
|
|
2213
|
-
const inferred = inferGithubRepoFromOrigin(repoRoot);
|
|
2214
|
-
if (inferred) return inferred;
|
|
2215
|
-
throw new Error(
|
|
2216
|
-
'Unable to infer GitHub repo from origin remote. Pass --repo github.com/<owner>/<repo>.',
|
|
2217
|
-
);
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
function runScorecardJson(repo) {
|
|
2221
|
-
const result = run(SCORECARD_BIN, ['--repo', repo, '--format', 'json'], { allowFailure: true });
|
|
2222
|
-
if (result.status !== 0) {
|
|
2223
|
-
const details = (result.stderr || result.stdout || '').trim();
|
|
2224
|
-
throw new Error(
|
|
2225
|
-
`Failed to run scorecard CLI ('${SCORECARD_BIN} --repo ${repo} --format json').${details ? `\n${details}` : ''}`,
|
|
2226
|
-
);
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
try {
|
|
2230
|
-
return JSON.parse(result.stdout || '{}');
|
|
2231
|
-
} catch (error) {
|
|
2232
|
-
throw new Error(`Unable to parse scorecard JSON output: ${error.message}`);
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
function readScorecardJsonFile(filePath) {
|
|
2237
|
-
const absolute = path.resolve(filePath);
|
|
2238
|
-
if (!fs.existsSync(absolute)) {
|
|
2239
|
-
throw new Error(`scorecard JSON file not found: ${absolute}`);
|
|
2240
|
-
}
|
|
2241
|
-
try {
|
|
2242
|
-
return JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
|
2243
|
-
} catch (error) {
|
|
2244
|
-
throw new Error(`Unable to parse scorecard JSON file: ${error.message}`);
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
function normalizeScorecardChecks(payload) {
|
|
2249
|
-
const rawChecks = Array.isArray(payload?.checks) ? payload.checks : [];
|
|
2250
|
-
return rawChecks.map((check) => {
|
|
2251
|
-
const name = String(check?.name || 'Unknown');
|
|
2252
|
-
const rawScore = Number(check?.score);
|
|
2253
|
-
const score = Number.isFinite(rawScore) ? rawScore : 0;
|
|
2254
|
-
return {
|
|
2255
|
-
name,
|
|
2256
|
-
score,
|
|
2257
|
-
risk: SCORECARD_RISK_BY_CHECK[name] || 'Unknown',
|
|
2258
|
-
};
|
|
2259
|
-
});
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
function renderScorecardBaselineMarkdown({ repo, score, checks, capturedAt, scorecardVersion, reportDate }) {
|
|
2263
|
-
const rows = checks
|
|
2264
|
-
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
2265
|
-
.join('\n');
|
|
2266
|
-
|
|
2267
|
-
return [
|
|
2268
|
-
'# OpenSSF Scorecard Baseline Report',
|
|
2269
|
-
'',
|
|
2270
|
-
`- **Repository:** \`${repo}\``,
|
|
2271
|
-
'- **Source:** generated by `gx report scorecard`',
|
|
2272
|
-
`- **Captured at:** ${capturedAt}`,
|
|
2273
|
-
`- **Scorecard version:** \`${scorecardVersion}\``,
|
|
2274
|
-
`- **Overall score:** **${score} / 10**`,
|
|
2275
|
-
'',
|
|
2276
|
-
'## Check breakdown',
|
|
2277
|
-
'',
|
|
2278
|
-
'| Check | Score | Risk |',
|
|
2279
|
-
'|---|---:|---|',
|
|
2280
|
-
rows || '| (none) | 0 | Unknown |',
|
|
2281
|
-
'',
|
|
2282
|
-
`## Report date`,
|
|
2283
|
-
'',
|
|
2284
|
-
`- ${reportDate}`,
|
|
2285
|
-
'',
|
|
2286
|
-
].join('\n');
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
function renderScorecardRemediationPlanMarkdown({ baselineRelativePath, checks }) {
|
|
2290
|
-
const failing = checks.filter((item) => item.score < 10);
|
|
2291
|
-
const failingRows = failing
|
|
2292
|
-
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
|
|
2293
|
-
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
2294
|
-
.join('\n');
|
|
2295
|
-
|
|
2296
|
-
return [
|
|
2297
|
-
'# OpenSSF Scorecard Remediation Plan',
|
|
2298
|
-
'',
|
|
2299
|
-
`Based on baseline report: \`${baselineRelativePath}\`.`,
|
|
2300
|
-
'',
|
|
2301
|
-
'## Failing checks',
|
|
2302
|
-
'',
|
|
2303
|
-
'| Check | Score | Risk |',
|
|
2304
|
-
'|---|---:|---|',
|
|
2305
|
-
(failingRows || '| None | 10 | N/A |'),
|
|
2306
|
-
'',
|
|
2307
|
-
'## Priority order',
|
|
2308
|
-
'',
|
|
2309
|
-
'1. Fix **High** risk checks first (especially score 0 items).',
|
|
2310
|
-
'2. Then close **Medium** risk checks with score < 10.',
|
|
2311
|
-
'3. Finally address **Low** risk ecosystem/process checks.',
|
|
2312
|
-
'',
|
|
2313
|
-
'## Verification loop',
|
|
2314
|
-
'',
|
|
2315
|
-
'1. Run scorecard again.',
|
|
2316
|
-
'2. Re-generate baseline + remediation files.',
|
|
2317
|
-
'3. Compare score deltas and track improved checks.',
|
|
2318
|
-
'',
|
|
2319
|
-
].join('\n');
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
function parseBranchList(rawValue) {
|
|
2323
|
-
return String(rawValue || '')
|
|
2324
|
-
.split(/[\s,]+/)
|
|
2325
|
-
.map((item) => item.trim())
|
|
2326
|
-
.filter(Boolean);
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
function uniquePreserveOrder(items) {
|
|
2330
|
-
const seen = new Set();
|
|
2331
|
-
const result = [];
|
|
2332
|
-
for (const item of items) {
|
|
2333
|
-
if (seen.has(item)) continue;
|
|
2334
|
-
seen.add(item);
|
|
2335
|
-
result.push(item);
|
|
2336
|
-
}
|
|
2337
|
-
return result;
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
function readConfiguredProtectedBranches(repoRoot) {
|
|
2341
|
-
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
2342
|
-
if (result.status !== 0) {
|
|
2343
|
-
return null;
|
|
2344
|
-
}
|
|
2345
|
-
const parsed = uniquePreserveOrder(parseBranchList(result.stdout.trim()));
|
|
2346
|
-
if (parsed.length === 0) {
|
|
2347
|
-
return null;
|
|
2348
|
-
}
|
|
2349
|
-
return parsed;
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
function listLocalUserBranches(repoRoot) {
|
|
2353
|
-
const result = gitRun(repoRoot, ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], { allowFailure: true });
|
|
2354
|
-
const branchNames = result.status === 0
|
|
2355
|
-
? uniquePreserveOrder(
|
|
2356
|
-
String(result.stdout || '')
|
|
2357
|
-
.split('\n')
|
|
2358
|
-
.map((item) => item.trim())
|
|
2359
|
-
.filter(Boolean),
|
|
2360
|
-
)
|
|
2361
|
-
: [];
|
|
2362
|
-
|
|
2363
|
-
const additionalUserBranches = branchNames.filter(
|
|
2364
|
-
(branchName) =>
|
|
2365
|
-
!branchName.startsWith('agent/') &&
|
|
2366
|
-
!DEFAULT_PROTECTED_BRANCHES.includes(branchName),
|
|
2367
|
-
);
|
|
2368
|
-
if (additionalUserBranches.length > 0) {
|
|
2369
|
-
return additionalUserBranches;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
const current = gitRun(repoRoot, ['branch', '--show-current'], { allowFailure: true });
|
|
2373
|
-
if (current.status !== 0) {
|
|
2374
|
-
return [];
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
const branchName = String(current.stdout || '').trim();
|
|
2378
|
-
if (
|
|
2379
|
-
!branchName ||
|
|
2380
|
-
branchName.startsWith('agent/') ||
|
|
2381
|
-
DEFAULT_PROTECTED_BRANCHES.includes(branchName)
|
|
2382
|
-
) {
|
|
2383
|
-
return [];
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
return [branchName];
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
function listLocalAgentBranches(repoRoot) {
|
|
2390
|
-
const result = gitRun(
|
|
2391
|
-
repoRoot,
|
|
2392
|
-
['for-each-ref', '--format=%(refname:short)', 'refs/heads/agent/'],
|
|
2393
|
-
{ allowFailure: true },
|
|
2394
|
-
);
|
|
2395
|
-
if (result.status !== 0) {
|
|
2396
|
-
return [];
|
|
2397
|
-
}
|
|
2398
|
-
return uniquePreserveOrder(
|
|
2399
|
-
String(result.stdout || '')
|
|
2400
|
-
.split('\n')
|
|
2401
|
-
.map((item) => item.trim())
|
|
2402
|
-
.filter(Boolean),
|
|
2403
|
-
);
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
function mapWorktreePathsByBranch(repoRoot) {
|
|
2407
|
-
const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
|
|
2408
|
-
const map = new Map();
|
|
2409
|
-
if (result.status !== 0) {
|
|
2410
|
-
return map;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
const lines = String(result.stdout || '').split('\n');
|
|
2414
|
-
let currentWorktree = '';
|
|
2415
|
-
for (const line of lines) {
|
|
2416
|
-
if (line.startsWith('worktree ')) {
|
|
2417
|
-
currentWorktree = line.slice('worktree '.length).trim();
|
|
2418
|
-
continue;
|
|
2419
|
-
}
|
|
2420
|
-
if (line.startsWith('branch refs/heads/')) {
|
|
2421
|
-
const branchName = line.slice('branch refs/heads/'.length).trim();
|
|
2422
|
-
if (currentWorktree && branchName) {
|
|
2423
|
-
map.set(branchName, currentWorktree);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
return map;
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
2431
|
-
const result = run('git', [
|
|
2432
|
-
'-C',
|
|
2433
|
-
worktreePath,
|
|
2434
|
-
'status',
|
|
2435
|
-
'--porcelain',
|
|
2436
|
-
'--untracked-files=normal',
|
|
2437
|
-
'--',
|
|
2438
|
-
]);
|
|
2439
|
-
if (result.status !== 0) {
|
|
2440
|
-
return true;
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
const lines = String(result.stdout || '')
|
|
2444
|
-
.split('\n')
|
|
2445
|
-
.map((line) => line.trimEnd())
|
|
2446
|
-
.filter((line) => line.length > 0);
|
|
2447
|
-
|
|
2448
|
-
for (const line of lines) {
|
|
2449
|
-
const pathPart = (line.length > 3 ? line.slice(3) : '').trim();
|
|
2450
|
-
if (!pathPart) continue;
|
|
2451
|
-
if (pathPart === LOCK_FILE_RELATIVE) continue;
|
|
2452
|
-
if (pathPart.startsWith(`${LOCK_FILE_RELATIVE} -> `)) continue;
|
|
2453
|
-
if (pathPart.endsWith(` -> ${LOCK_FILE_RELATIVE}`)) continue;
|
|
2454
|
-
return true;
|
|
2455
|
-
}
|
|
2456
|
-
return false;
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
2460
|
-
const baseBranch = String(options.baseBranch || '').trim();
|
|
2461
|
-
const dryRun = Boolean(options.dryRun);
|
|
2462
|
-
const waitForMerge = options.waitForMerge !== false;
|
|
2463
|
-
const excludedBranches = new Set(
|
|
2464
|
-
Array.isArray(options.excludeBranches)
|
|
2465
|
-
? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
|
|
2466
|
-
: [],
|
|
2467
|
-
);
|
|
2468
|
-
|
|
2469
|
-
const summary = {
|
|
2470
|
-
enabled: true,
|
|
2471
|
-
baseBranch,
|
|
2472
|
-
attempted: 0,
|
|
2473
|
-
completed: 0,
|
|
2474
|
-
skipped: 0,
|
|
2475
|
-
failed: 0,
|
|
2476
|
-
details: [],
|
|
2477
|
-
};
|
|
2478
|
-
|
|
2479
|
-
if (!baseBranch || baseBranch === 'HEAD' || baseBranch.startsWith('agent/')) {
|
|
2480
|
-
summary.enabled = false;
|
|
2481
|
-
summary.details.push('Skipped auto-finish sweep (base branch is missing or not a non-agent local branch).');
|
|
2482
|
-
return summary;
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
if (String(process.env.GUARDEX_DOCTOR_SANDBOX || '') === '1') {
|
|
2486
|
-
summary.enabled = false;
|
|
2487
|
-
summary.details.push('Skipped auto-finish sweep inside doctor sandbox pass.');
|
|
2488
|
-
return summary;
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
if (String(process.env.GUARDEX_SKIP_AUTO_FINISH_READY_BRANCHES || '') === '1') {
|
|
2492
|
-
summary.enabled = false;
|
|
2493
|
-
summary.details.push('Skipped auto-finish sweep (GUARDEX_SKIP_AUTO_FINISH_READY_BRANCHES=1).');
|
|
2494
|
-
return summary;
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
if (dryRun) {
|
|
2498
|
-
summary.enabled = false;
|
|
2499
|
-
summary.details.push('Skipped auto-finish sweep in dry-run mode.');
|
|
2500
|
-
return summary;
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
const hasOrigin = gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
|
|
2504
|
-
if (!hasOrigin) {
|
|
2505
|
-
summary.enabled = false;
|
|
2506
|
-
summary.details.push('Skipped auto-finish sweep (origin remote missing).');
|
|
2507
|
-
return summary;
|
|
2508
|
-
}
|
|
2509
|
-
const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
|
|
2510
|
-
if (!explicitGhBin && !originRemoteLooksLikeGithub(repoRoot)) {
|
|
2511
|
-
summary.enabled = false;
|
|
2512
|
-
summary.details.push('Skipped auto-finish sweep (origin remote is not GitHub).');
|
|
2513
|
-
return summary;
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
|
|
2517
|
-
if (run(ghBin, ['--version']).status !== 0) {
|
|
2518
|
-
summary.enabled = false;
|
|
2519
|
-
summary.details.push(`Skipped auto-finish sweep (${ghBin} not available).`);
|
|
2520
|
-
return summary;
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
const branchWorktrees = mapWorktreePathsByBranch(repoRoot);
|
|
2524
|
-
const agentBranches = listLocalAgentBranches(repoRoot);
|
|
2525
|
-
if (agentBranches.length === 0) {
|
|
2526
|
-
summary.enabled = false;
|
|
2527
|
-
summary.details.push('No local agent branches found for auto-finish sweep.');
|
|
2528
|
-
return summary;
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
for (const branch of agentBranches) {
|
|
2532
|
-
if (excludedBranches.has(branch)) {
|
|
2533
|
-
summary.skipped += 1;
|
|
2534
|
-
summary.details.push(`[skip] ${branch}: excluded from this auto-finish sweep.`);
|
|
2535
|
-
continue;
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
if (branch === baseBranch) {
|
|
2539
|
-
summary.skipped += 1;
|
|
2540
|
-
summary.details.push(`[skip] ${branch}: source branch equals base branch.`);
|
|
2541
|
-
continue;
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
let counts;
|
|
2545
|
-
try {
|
|
2546
|
-
counts = aheadBehind(repoRoot, branch, baseBranch);
|
|
2547
|
-
} catch (error) {
|
|
2548
|
-
summary.failed += 1;
|
|
2549
|
-
summary.details.push(`[fail] ${branch}: unable to compute ahead/behind (${error.message}).`);
|
|
2550
|
-
continue;
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
if (counts.ahead <= 0) {
|
|
2554
|
-
summary.skipped += 1;
|
|
2555
|
-
summary.details.push(`[skip] ${branch}: already merged into ${baseBranch}.`);
|
|
2556
|
-
continue;
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
const branchWorktree = branchWorktrees.get(branch) || '';
|
|
2560
|
-
if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
|
|
2561
|
-
summary.skipped += 1;
|
|
2562
|
-
summary.details.push(`[skip] ${branch}: dirty worktree (${branchWorktree}).`);
|
|
2563
|
-
continue;
|
|
2564
|
-
}
|
|
475
|
+
function startProtectedBaseSandboxFallback(blocked, sandboxSuffix) {
|
|
476
|
+
const branchPrefix = protectedBaseSandboxBranchPrefix();
|
|
477
|
+
let selectedBranch = '';
|
|
478
|
+
let selectedWorktreePath = '';
|
|
2565
479
|
|
|
2566
|
-
|
|
2567
|
-
const
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
baseBranch,
|
|
2572
|
-
'--via-pr',
|
|
2573
|
-
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
2574
|
-
'--cleanup',
|
|
2575
|
-
];
|
|
2576
|
-
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot });
|
|
2577
|
-
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
2578
|
-
|
|
2579
|
-
if (finishResult.status === 0) {
|
|
2580
|
-
summary.completed += 1;
|
|
2581
|
-
summary.details.push(`[done] ${branch}: auto-finish completed.`);
|
|
480
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
481
|
+
const suffix = attempt === 0 ? sandboxSuffix : `${attempt + 1}-${sandboxSuffix}`;
|
|
482
|
+
const candidateBranch = `${branchPrefix}-${suffix}`;
|
|
483
|
+
const candidateWorktreePath = protectedBaseSandboxWorktreePath(blocked.repoRoot, candidateBranch);
|
|
484
|
+
if (gitRefExists(blocked.repoRoot, `refs/heads/${candidateBranch}`)) {
|
|
2582
485
|
continue;
|
|
2583
486
|
}
|
|
2584
|
-
|
|
2585
|
-
const recoverableConflict = detectRecoverableAutoFinishConflict(combinedOutput);
|
|
2586
|
-
if (recoverableConflict) {
|
|
2587
|
-
summary.skipped += 1;
|
|
2588
|
-
const tail = combinedOutput ? ` ${combinedOutput.split('\n').slice(-2).join(' | ')}` : '';
|
|
2589
|
-
summary.details.push(`[skip] ${branch}: ${recoverableConflict.rawLabel}${tail}`);
|
|
487
|
+
if (fs.existsSync(candidateWorktreePath)) {
|
|
2590
488
|
continue;
|
|
2591
489
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
summary.details.push(`[fail] ${branch}: auto-finish failed.${tail}`);
|
|
490
|
+
selectedBranch = candidateBranch;
|
|
491
|
+
selectedWorktreePath = candidateWorktreePath;
|
|
492
|
+
break;
|
|
2596
493
|
}
|
|
2597
494
|
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
2602
|
-
const localUserBranches = listLocalUserBranches(repoRoot);
|
|
2603
|
-
if (localUserBranches.length === 0) {
|
|
2604
|
-
return {
|
|
2605
|
-
status: 'unchanged',
|
|
2606
|
-
file: `git config ${GIT_PROTECTED_BRANCHES_KEY}`,
|
|
2607
|
-
note: 'no additional local user branches detected',
|
|
2608
|
-
};
|
|
495
|
+
if (!selectedBranch || !selectedWorktreePath) {
|
|
496
|
+
throw new Error('Unable to allocate unique sandbox branch/worktree');
|
|
2609
497
|
}
|
|
2610
498
|
|
|
2611
|
-
|
|
2612
|
-
const
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
499
|
+
fs.mkdirSync(path.dirname(selectedWorktreePath), { recursive: true });
|
|
500
|
+
const startRef = resolveProtectedBaseSandboxStartRef(blocked.repoRoot, blocked.branch);
|
|
501
|
+
const addArgs = startRef
|
|
502
|
+
? ['-C', blocked.repoRoot, 'worktree', 'add', '-b', selectedBranch, selectedWorktreePath, startRef]
|
|
503
|
+
: ['-C', blocked.repoRoot, 'worktree', 'add', '--orphan', selectedWorktreePath];
|
|
504
|
+
const addResult = run('git', addArgs);
|
|
505
|
+
if (isSpawnFailure(addResult)) {
|
|
506
|
+
throw addResult.error;
|
|
507
|
+
}
|
|
508
|
+
if (addResult.status !== 0) {
|
|
509
|
+
throw new Error((addResult.stderr || addResult.stdout || 'failed to create sandbox').trim());
|
|
2620
510
|
}
|
|
2621
511
|
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
512
|
+
if (!startRef) {
|
|
513
|
+
const renameResult = run(
|
|
514
|
+
'git',
|
|
515
|
+
['-C', selectedWorktreePath, 'branch', '-m', selectedBranch],
|
|
516
|
+
{ timeout: 20_000 },
|
|
517
|
+
);
|
|
518
|
+
if (isSpawnFailure(renameResult)) {
|
|
519
|
+
throw renameResult.error;
|
|
520
|
+
}
|
|
521
|
+
if (renameResult.status !== 0) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
(renameResult.stderr || renameResult.stdout || 'failed to name orphan sandbox branch').trim(),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
2625
526
|
}
|
|
2626
527
|
|
|
2627
528
|
return {
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
529
|
+
metadata: {
|
|
530
|
+
branch: selectedBranch,
|
|
531
|
+
worktreePath: selectedWorktreePath,
|
|
532
|
+
},
|
|
533
|
+
stdout:
|
|
534
|
+
`[agent-branch-start] Created branch: ${selectedBranch}\n` +
|
|
535
|
+
`[agent-branch-start] Worktree: ${selectedWorktreePath}\n`,
|
|
536
|
+
stderr: addResult.stderr || '',
|
|
2631
537
|
};
|
|
2632
538
|
}
|
|
2633
539
|
|
|
2634
|
-
function
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
return [...DEFAULT_PROTECTED_BRANCHES];
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
const parsed = uniquePreserveOrder(parseBranchList(result.stdout.trim()));
|
|
2641
|
-
if (parsed.length === 0) {
|
|
2642
|
-
return [...DEFAULT_PROTECTED_BRANCHES];
|
|
2643
|
-
}
|
|
2644
|
-
return parsed;
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
function writeProtectedBranches(repoRoot, branches) {
|
|
2648
|
-
if (branches.length === 0) {
|
|
2649
|
-
gitRun(repoRoot, ['config', '--unset-all', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
2650
|
-
return;
|
|
2651
|
-
}
|
|
2652
|
-
gitRun(repoRoot, ['config', GIT_PROTECTED_BRANCHES_KEY, branches.join(' ')]);
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
function readGitConfig(repoRoot, key) {
|
|
2656
|
-
const result = gitRun(repoRoot, ['config', '--get', key], { allowFailure: true });
|
|
2657
|
-
if (result.status !== 0) {
|
|
2658
|
-
return '';
|
|
2659
|
-
}
|
|
2660
|
-
return (result.stdout || '').trim();
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
function resolveBaseBranch(repoRoot, explicitBase) {
|
|
2664
|
-
if (explicitBase) {
|
|
2665
|
-
return explicitBase;
|
|
2666
|
-
}
|
|
2667
|
-
const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
|
|
2668
|
-
return configured || DEFAULT_BASE_BRANCH;
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
function resolveSyncStrategy(repoRoot, explicitStrategy) {
|
|
2672
|
-
const strategy = (explicitStrategy || readGitConfig(repoRoot, GIT_SYNC_STRATEGY_KEY) || DEFAULT_SYNC_STRATEGY)
|
|
2673
|
-
.trim()
|
|
2674
|
-
.toLowerCase();
|
|
2675
|
-
if (strategy !== 'rebase' && strategy !== 'merge') {
|
|
2676
|
-
throw new Error(`Invalid sync strategy '${strategy}' (expected: rebase or merge)`);
|
|
540
|
+
function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
541
|
+
if (sandboxSuffix === 'gx-doctor') {
|
|
542
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
2677
543
|
}
|
|
2678
|
-
return strategy;
|
|
2679
|
-
}
|
|
2680
544
|
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
545
|
+
const startResult = runPackageAsset('branchStart', [
|
|
546
|
+
'--task',
|
|
547
|
+
taskName,
|
|
548
|
+
'--agent',
|
|
549
|
+
SHORT_TOOL_NAME,
|
|
550
|
+
'--base',
|
|
551
|
+
blocked.branch,
|
|
552
|
+
], { cwd: blocked.repoRoot });
|
|
553
|
+
if (isSpawnFailure(startResult)) {
|
|
554
|
+
throw startResult.error;
|
|
2685
555
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
throw new Error('Detached HEAD is not supported for sync operations');
|
|
556
|
+
if (startResult.status !== 0) {
|
|
557
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
2689
558
|
}
|
|
2690
|
-
return branch;
|
|
2691
|
-
}
|
|
2692
559
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
560
|
+
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
561
|
+
const currentBranch = currentBranchName(blocked.repoRoot);
|
|
562
|
+
const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
|
|
563
|
+
const repoRootPath = path.resolve(blocked.repoRoot);
|
|
564
|
+
const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
|
|
565
|
+
const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
|
|
2696
566
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
567
|
+
if (!hasSafeWorktree || branchChanged) {
|
|
568
|
+
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
569
|
+
if (!restoreResult.ok) {
|
|
570
|
+
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
571
|
+
throw new Error(
|
|
572
|
+
`sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
573
|
+
(detail ? `\n${detail}` : ''),
|
|
574
|
+
);
|
|
2703
575
|
}
|
|
2704
|
-
return
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
const detached = gitRun(repoRoot, ['rev-parse', '--short', 'HEAD'], { allowFailure: true });
|
|
2708
|
-
if (detached.status === 0) {
|
|
2709
|
-
return `(detached at ${String(detached.stdout || '').trim()})`;
|
|
576
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
2710
577
|
}
|
|
2711
|
-
return '(unknown)';
|
|
2712
|
-
}
|
|
2713
578
|
|
|
2714
|
-
|
|
2715
|
-
|
|
579
|
+
return {
|
|
580
|
+
metadata,
|
|
581
|
+
stdout: startResult.stdout || '',
|
|
582
|
+
stderr: startResult.stderr || '',
|
|
583
|
+
};
|
|
2716
584
|
}
|
|
2717
585
|
|
|
2718
|
-
function
|
|
2719
|
-
|
|
2720
|
-
|
|
586
|
+
function cleanupProtectedBaseSandbox(repoRoot, metadata) {
|
|
587
|
+
const result = {
|
|
588
|
+
worktree: 'skipped',
|
|
589
|
+
branch: 'skipped',
|
|
590
|
+
note: 'missing sandbox metadata',
|
|
591
|
+
};
|
|
2721
592
|
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
const hasHeadCommit = repoHasHeadCommit(repoRoot);
|
|
2725
|
-
const hasOrigin = repoHasOriginRemote(repoRoot);
|
|
2726
|
-
const composeFiles = detectComposeHintFiles(repoRoot);
|
|
2727
|
-
if (hasHeadCommit && hasOrigin && composeFiles.length === 0) {
|
|
2728
|
-
return;
|
|
593
|
+
if (!metadata?.worktreePath || !metadata?.branch) {
|
|
594
|
+
return result;
|
|
2729
595
|
}
|
|
2730
596
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
`[${TOOL_NAME}] First agent flow${label}: ` +
|
|
2737
|
-
`gx branch start "<task>" "codex" -> ` +
|
|
2738
|
-
`gx locks claim --branch "$(git branch --show-current)" <file...> -> ` +
|
|
2739
|
-
`gx branch finish --branch "$(git branch --show-current)" --base ${baseBranch} --via-pr --wait-for-merge`,
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2742
|
-
if (!hasOrigin) {
|
|
2743
|
-
console.log(`[${TOOL_NAME}] No origin remote${label}: finish and auto-merge flows stay local until you add one.`);
|
|
2744
|
-
}
|
|
2745
|
-
if (composeFiles.length > 0) {
|
|
2746
|
-
console.log(
|
|
2747
|
-
`[${TOOL_NAME}] Docker Compose helper${label}: detected ${composeFiles.join(', ')}. ` +
|
|
2748
|
-
`Set GUARDEX_DOCKER_SERVICE and run 'bash scripts/guardex-docker-loader.sh -- <command...>'.`,
|
|
597
|
+
if (fs.existsSync(metadata.worktreePath)) {
|
|
598
|
+
const removeResult = run(
|
|
599
|
+
'git',
|
|
600
|
+
['-C', repoRoot, 'worktree', 'remove', '--force', metadata.worktreePath],
|
|
601
|
+
{ timeout: 30_000 },
|
|
2749
602
|
);
|
|
603
|
+
if (isSpawnFailure(removeResult)) {
|
|
604
|
+
throw removeResult.error;
|
|
605
|
+
}
|
|
606
|
+
if (removeResult.status !== 0) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
(removeResult.stderr || removeResult.stdout || 'failed to remove sandbox worktree').trim(),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
result.worktree = 'removed';
|
|
612
|
+
} else {
|
|
613
|
+
result.worktree = 'missing';
|
|
2750
614
|
}
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
function workingTreeIsDirty(repoRoot) {
|
|
2754
|
-
const result = gitRun(repoRoot, ['status', '--porcelain'], { allowFailure: true });
|
|
2755
|
-
if (result.status !== 0) {
|
|
2756
|
-
throw new Error('Unable to inspect git working tree status');
|
|
2757
|
-
}
|
|
2758
|
-
const lines = (result.stdout || '').split('\n').filter((line) => line.length > 0);
|
|
2759
|
-
const significant = lines.filter((line) => {
|
|
2760
|
-
const pathPart = (line.length > 3 ? line.slice(3) : '').trim();
|
|
2761
|
-
if (!pathPart) return false;
|
|
2762
|
-
if (pathPart === LOCK_FILE_RELATIVE) return false;
|
|
2763
|
-
if (pathPart.startsWith(`${LOCK_FILE_RELATIVE} -> `)) return false;
|
|
2764
|
-
if (pathPart.endsWith(` -> ${LOCK_FILE_RELATIVE}`)) return false;
|
|
2765
|
-
return true;
|
|
2766
|
-
});
|
|
2767
|
-
return significant.length > 0;
|
|
2768
|
-
}
|
|
2769
615
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
616
|
+
if (gitRefExists(repoRoot, `refs/heads/${metadata.branch}`)) {
|
|
617
|
+
const branchDeleteResult = run(
|
|
618
|
+
'git',
|
|
619
|
+
['-C', repoRoot, 'branch', '-D', metadata.branch],
|
|
620
|
+
{ timeout: 20_000 },
|
|
2775
621
|
);
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
allowFailure: true,
|
|
2779
|
-
});
|
|
2780
|
-
if (hasRemoteBase.status !== 0) {
|
|
2781
|
-
throw new Error(`Remote base branch not found: origin/${baseBranch}`);
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
|
|
2785
|
-
function aheadBehind(repoRoot, branchRef, baseRef) {
|
|
2786
|
-
const result = gitRun(repoRoot, ['rev-list', '--left-right', '--count', `${branchRef}...${baseRef}`], {
|
|
2787
|
-
allowFailure: true,
|
|
2788
|
-
});
|
|
2789
|
-
if (result.status !== 0) {
|
|
2790
|
-
throw new Error(`Unable to compute ahead/behind for ${branchRef} vs ${baseRef}`);
|
|
2791
|
-
}
|
|
2792
|
-
const parts = (result.stdout || '').trim().split(/\s+/).filter(Boolean);
|
|
2793
|
-
const ahead = Number.parseInt(parts[0] || '0', 10);
|
|
2794
|
-
const behind = Number.parseInt(parts[1] || '0', 10);
|
|
2795
|
-
return { ahead: Number.isFinite(ahead) ? ahead : 0, behind: Number.isFinite(behind) ? behind : 0 };
|
|
2796
|
-
}
|
|
2797
|
-
|
|
2798
|
-
function lockRegistryStatus(repoRoot) {
|
|
2799
|
-
const result = gitRun(repoRoot, ['status', '--porcelain', '--', LOCK_FILE_RELATIVE], { allowFailure: true });
|
|
2800
|
-
if (result.status !== 0) {
|
|
2801
|
-
return { dirty: false, untracked: false };
|
|
2802
|
-
}
|
|
2803
|
-
const lines = (result.stdout || '').split('\n').filter((line) => line.length > 0);
|
|
2804
|
-
if (lines.length === 0) {
|
|
2805
|
-
return { dirty: false, untracked: false };
|
|
2806
|
-
}
|
|
2807
|
-
const untracked = lines.some((line) => line.startsWith('??'));
|
|
2808
|
-
return { dirty: true, untracked };
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
function listAgentWorktrees(repoRoot) {
|
|
2813
|
-
const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
|
|
2814
|
-
if (result.status !== 0) {
|
|
2815
|
-
throw new Error('Unable to list git worktrees for finish command');
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
const entries = [];
|
|
2819
|
-
let currentPath = '';
|
|
2820
|
-
let currentBranchRef = '';
|
|
2821
|
-
const lines = String(result.stdout || '').split('\n');
|
|
2822
|
-
for (const line of lines) {
|
|
2823
|
-
if (!line.trim()) {
|
|
2824
|
-
if (currentPath && currentBranchRef.startsWith('refs/heads/agent/')) {
|
|
2825
|
-
entries.push({
|
|
2826
|
-
worktreePath: currentPath,
|
|
2827
|
-
branch: currentBranchRef.replace(/^refs\/heads\//, ''),
|
|
2828
|
-
});
|
|
2829
|
-
}
|
|
2830
|
-
currentPath = '';
|
|
2831
|
-
currentBranchRef = '';
|
|
2832
|
-
continue;
|
|
2833
|
-
}
|
|
2834
|
-
if (line.startsWith('worktree ')) {
|
|
2835
|
-
currentPath = line.slice('worktree '.length).trim();
|
|
2836
|
-
continue;
|
|
622
|
+
if (isSpawnFailure(branchDeleteResult)) {
|
|
623
|
+
throw branchDeleteResult.error;
|
|
2837
624
|
}
|
|
2838
|
-
if (
|
|
2839
|
-
|
|
2840
|
-
|
|
625
|
+
if (branchDeleteResult.status !== 0) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
(branchDeleteResult.stderr || branchDeleteResult.stdout || 'failed to delete sandbox branch').trim(),
|
|
628
|
+
);
|
|
2841
629
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
worktreePath: currentPath,
|
|
2846
|
-
branch: currentBranchRef.replace(/^refs\/heads\//, ''),
|
|
2847
|
-
});
|
|
630
|
+
result.branch = 'deleted';
|
|
631
|
+
} else {
|
|
632
|
+
result.branch = 'missing';
|
|
2848
633
|
}
|
|
2849
634
|
|
|
2850
|
-
|
|
635
|
+
result.note = 'sandbox worktree pruned';
|
|
636
|
+
return result;
|
|
2851
637
|
}
|
|
2852
638
|
|
|
2853
|
-
function
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
if (result.status !== 0) {
|
|
2860
|
-
throw new Error('Unable to list local agent branches');
|
|
2861
|
-
}
|
|
2862
|
-
return uniquePreserveOrder(
|
|
2863
|
-
String(result.stdout || '')
|
|
2864
|
-
.split('\n')
|
|
2865
|
-
.map((line) => line.trim())
|
|
2866
|
-
.filter((line) => line.startsWith('agent/')),
|
|
2867
|
-
);
|
|
2868
|
-
}
|
|
639
|
+
function runSetupInSandbox(options, blocked, repoLabel = '') {
|
|
640
|
+
const startResult = startProtectedBaseSandbox(blocked, {
|
|
641
|
+
taskName: `${SHORT_TOOL_NAME}-setup`,
|
|
642
|
+
sandboxSuffix: 'gx-setup',
|
|
643
|
+
});
|
|
644
|
+
const metadata = startResult.metadata;
|
|
2869
645
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
if (result.status === 1) {
|
|
2876
|
-
return true;
|
|
2877
|
-
}
|
|
2878
|
-
throw new Error(
|
|
2879
|
-
`git ${args.join(' ')} failed in ${worktreePath}: ${(
|
|
2880
|
-
result.stderr || result.stdout || ''
|
|
2881
|
-
).trim()}`,
|
|
646
|
+
if (startResult.stdout) process.stdout.write(startResult.stdout);
|
|
647
|
+
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
648
|
+
console.log(
|
|
649
|
+
`[${TOOL_NAME}] setup blocked on protected branch '${blocked.branch}' in an initialized repo; ` +
|
|
650
|
+
'refreshing through a sandbox worktree and syncing managed bootstrap files back locally.',
|
|
2882
651
|
);
|
|
2883
|
-
}
|
|
2884
652
|
|
|
2885
|
-
|
|
2886
|
-
const
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
653
|
+
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
654
|
+
const nestedResult = run(
|
|
655
|
+
process.execPath,
|
|
656
|
+
[__filename, ...buildSandboxSetupArgs(options, sandboxTarget)],
|
|
657
|
+
{ cwd: metadata.worktreePath },
|
|
658
|
+
);
|
|
659
|
+
if (isSpawnFailure(nestedResult)) {
|
|
660
|
+
throw nestedResult.error;
|
|
661
|
+
}
|
|
662
|
+
if (nestedResult.status !== 0) {
|
|
663
|
+
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
664
|
+
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
665
|
+
throw new Error(
|
|
666
|
+
`sandboxed setup failed for protected branch '${blocked.branch}'. ` +
|
|
667
|
+
`Inspect sandbox at ${metadata.worktreePath}`,
|
|
668
|
+
);
|
|
2895
669
|
}
|
|
2896
670
|
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
671
|
+
const syncOptions = {
|
|
672
|
+
...options,
|
|
673
|
+
target: blocked.repoRoot,
|
|
674
|
+
recursive: false,
|
|
675
|
+
allowProtectedBaseWrite: true,
|
|
676
|
+
};
|
|
677
|
+
const { installPayload, fixPayload, parentWorkspace } = runSetupBootstrapInternal(syncOptions);
|
|
678
|
+
printOperations(`Setup/install${repoLabel}`, installPayload, syncOptions.dryRun);
|
|
679
|
+
printOperations(`Setup/fix${repoLabel}`, fixPayload, syncOptions.dryRun);
|
|
680
|
+
if (!syncOptions.dryRun && parentWorkspace) {
|
|
681
|
+
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
2907
682
|
}
|
|
2908
683
|
|
|
2909
|
-
const
|
|
2910
|
-
|
|
684
|
+
const scanResult = runScanInternal({ target: blocked.repoRoot, json: false });
|
|
685
|
+
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
686
|
+
const autoFinishSummary = doctorModule.autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
687
|
+
baseBranch: currentBaseBranch,
|
|
688
|
+
dryRun: syncOptions.dryRun,
|
|
2911
689
|
});
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
function gitOutputLines(worktreePath, args) {
|
|
2919
|
-
const result = run('git', ['-C', worktreePath, ...args], { stdio: 'pipe' });
|
|
2920
|
-
if (result.status !== 0) {
|
|
2921
|
-
throw new Error(
|
|
2922
|
-
`git ${args.join(' ')} failed in ${worktreePath}: ${(
|
|
2923
|
-
result.stderr || result.stdout || ''
|
|
2924
|
-
).trim()}`,
|
|
690
|
+
printScanResult(scanResult, false);
|
|
691
|
+
if (autoFinishSummary.enabled) {
|
|
692
|
+
console.log(
|
|
693
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
2925
694
|
);
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
.split('\n')
|
|
2929
|
-
.map((line) => line.trim())
|
|
2930
|
-
.filter(Boolean);
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
2934
|
-
const changedFiles = uniquePreserveOrder([
|
|
2935
|
-
...gitOutputLines(worktreePath, ['diff', '--name-only', '--', '.', ':(exclude).omx/state/agent-file-locks.json']),
|
|
2936
|
-
...gitOutputLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', ':(exclude).omx/state/agent-file-locks.json']),
|
|
2937
|
-
...gitOutputLines(worktreePath, ['ls-files', '--others', '--exclude-standard']),
|
|
2938
|
-
]);
|
|
2939
|
-
|
|
2940
|
-
if (changedFiles.length > 0) {
|
|
2941
|
-
const claim = runPackageAsset('lockTool', ['claim', '--branch', branch, ...changedFiles], {
|
|
2942
|
-
cwd: repoRoot,
|
|
2943
|
-
stdio: 'pipe',
|
|
2944
|
-
});
|
|
2945
|
-
if (claim.status !== 0) {
|
|
2946
|
-
throw new Error(
|
|
2947
|
-
`Lock claim failed for ${branch}: ${(
|
|
2948
|
-
claim.stderr || claim.stdout || ''
|
|
2949
|
-
).trim()}`,
|
|
2950
|
-
);
|
|
695
|
+
for (const detail of autoFinishSummary.details) {
|
|
696
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
2951
697
|
}
|
|
698
|
+
} else if (autoFinishSummary.details.length > 0) {
|
|
699
|
+
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
2952
700
|
}
|
|
2953
701
|
|
|
2954
|
-
const
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
'--',
|
|
2960
|
-
'.',
|
|
2961
|
-
':(exclude).omx/state/agent-file-locks.json',
|
|
2962
|
-
]),
|
|
2963
|
-
...gitOutputLines(worktreePath, [
|
|
2964
|
-
'diff',
|
|
2965
|
-
'--cached',
|
|
2966
|
-
'--name-only',
|
|
2967
|
-
'--diff-filter=D',
|
|
2968
|
-
'--',
|
|
2969
|
-
'.',
|
|
2970
|
-
':(exclude).omx/state/agent-file-locks.json',
|
|
2971
|
-
]),
|
|
2972
|
-
]);
|
|
2973
|
-
|
|
2974
|
-
if (deletedFiles.length > 0) {
|
|
2975
|
-
const allowDelete = runPackageAsset('lockTool', ['allow-delete', '--branch', branch, ...deletedFiles], {
|
|
2976
|
-
cwd: repoRoot,
|
|
2977
|
-
stdio: 'pipe',
|
|
2978
|
-
});
|
|
2979
|
-
if (allowDelete.status !== 0) {
|
|
2980
|
-
throw new Error(
|
|
2981
|
-
`Delete-lock grant failed for ${branch}: ${(
|
|
2982
|
-
allowDelete.stderr || allowDelete.stdout || ''
|
|
2983
|
-
).trim()}`,
|
|
2984
|
-
);
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
702
|
+
const cleanupResult = cleanupProtectedBaseSandbox(blocked.repoRoot, metadata);
|
|
703
|
+
console.log(
|
|
704
|
+
`[${TOOL_NAME}] Protected-base setup sandbox cleanup: ${cleanupResult.note} ` +
|
|
705
|
+
`(worktree=${cleanupResult.worktree}, branch=${cleanupResult.branch}).`,
|
|
706
|
+
);
|
|
2988
707
|
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
});
|
|
2993
|
-
return result.status === 0;
|
|
708
|
+
return {
|
|
709
|
+
scanResult,
|
|
710
|
+
};
|
|
2994
711
|
}
|
|
2995
712
|
|
|
2996
|
-
function resolveFinishBaseBranch(repoRoot, _sourceBranch, explicitBase) {
|
|
2997
|
-
if (explicitBase) {
|
|
2998
|
-
return explicitBase;
|
|
2999
|
-
}
|
|
3000
713
|
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
714
|
+
function todayDateStamp() {
|
|
715
|
+
return new Date().toISOString().slice(0, 10);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function inferGithubRepoFromOrigin(repoRoot) {
|
|
719
|
+
const rawOrigin = readGitConfig(repoRoot, 'remote.origin.url');
|
|
720
|
+
if (!rawOrigin) return '';
|
|
3005
721
|
|
|
3006
|
-
|
|
722
|
+
const httpsMatch = rawOrigin.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
723
|
+
if (!httpsMatch) return '';
|
|
724
|
+
const slug = (httpsMatch[1] || '').replace(/^\/+/, '').trim();
|
|
725
|
+
if (!slug || !slug.includes('/')) return '';
|
|
726
|
+
return `github.com/${slug}`;
|
|
3007
727
|
}
|
|
3008
728
|
|
|
3009
|
-
function
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
return false;
|
|
3021
|
-
}
|
|
3022
|
-
throw new Error(`Unable to determine merge status for ${branch} -> ${baseBranch}`);
|
|
729
|
+
function inferGithubRepoSlug(rawValue) {
|
|
730
|
+
const raw = String(rawValue || '').trim();
|
|
731
|
+
if (!raw) return '';
|
|
732
|
+
const match = raw.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
733
|
+
if (!match) return '';
|
|
734
|
+
const slug = String(match[1] || '')
|
|
735
|
+
.replace(/^\/+/, '')
|
|
736
|
+
.replace(/^github\.com\//i, '')
|
|
737
|
+
.trim();
|
|
738
|
+
if (!slug || !slug.includes('/')) return '';
|
|
739
|
+
return slug;
|
|
3023
740
|
}
|
|
3024
741
|
|
|
3025
|
-
function
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
return { changed: false, committed: false };
|
|
742
|
+
function resolveScorecardRepo(repoRoot, explicitRepo) {
|
|
743
|
+
if (explicitRepo) {
|
|
744
|
+
return explicitRepo.trim();
|
|
3029
745
|
}
|
|
746
|
+
const inferred = inferGithubRepoFromOrigin(repoRoot);
|
|
747
|
+
if (inferred) return inferred;
|
|
748
|
+
throw new Error(
|
|
749
|
+
'Unable to infer GitHub repo from origin remote. Pass --repo github.com/<owner>/<repo>.',
|
|
750
|
+
);
|
|
751
|
+
}
|
|
3030
752
|
|
|
3031
|
-
|
|
753
|
+
function runScorecardJson(repo) {
|
|
754
|
+
const result = run(SCORECARD_BIN, ['--repo', repo, '--format', 'json'], { allowFailure: true });
|
|
755
|
+
if (result.status !== 0) {
|
|
756
|
+
const details = (result.stderr || result.stdout || '').trim();
|
|
3032
757
|
throw new Error(
|
|
3033
|
-
`
|
|
758
|
+
`Failed to run scorecard CLI ('${SCORECARD_BIN} --repo ${repo} --format json').${details ? `\n${details}` : ''}`,
|
|
3034
759
|
);
|
|
3035
760
|
}
|
|
3036
761
|
|
|
3037
|
-
|
|
3038
|
-
return
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
claimLocksForAutoCommit(repoRoot, worktreePath, branch);
|
|
3042
|
-
|
|
3043
|
-
const addResult = run('git', ['-C', worktreePath, 'add', '-A'], { stdio: 'pipe' });
|
|
3044
|
-
if (addResult.status !== 0) {
|
|
3045
|
-
throw new Error(`git add failed in ${worktreePath}: ${(addResult.stderr || addResult.stdout || '').trim()}`);
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
const stagedHasChanges = gitQuietChangeResult(worktreePath, [
|
|
3049
|
-
'diff',
|
|
3050
|
-
'--cached',
|
|
3051
|
-
'--quiet',
|
|
3052
|
-
'--',
|
|
3053
|
-
'.',
|
|
3054
|
-
':(exclude).omx/state/agent-file-locks.json',
|
|
3055
|
-
]);
|
|
3056
|
-
if (!stagedHasChanges) {
|
|
3057
|
-
return { changed: true, committed: false };
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
const commitMessage = options.commitMessage || `Auto-finish: ${branch}`;
|
|
3061
|
-
const commitResult = run('git', ['-C', worktreePath, 'commit', '-m', commitMessage], { stdio: 'pipe' });
|
|
3062
|
-
if (commitResult.status !== 0) {
|
|
3063
|
-
throw new Error(
|
|
3064
|
-
`Auto-commit failed on '${branch}': ${(
|
|
3065
|
-
commitResult.stderr || commitResult.stdout || ''
|
|
3066
|
-
).trim()}`,
|
|
3067
|
-
);
|
|
762
|
+
try {
|
|
763
|
+
return JSON.parse(result.stdout || '{}');
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw new Error(`Unable to parse scorecard JSON output: ${error.message}`);
|
|
3068
766
|
}
|
|
3069
|
-
|
|
3070
|
-
return { changed: true, committed: true, message: commitMessage };
|
|
3071
767
|
}
|
|
3072
768
|
|
|
3073
|
-
function
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
}
|
|
3078
|
-
const rebased = run('git', ['-C', repoRoot, 'rebase', baseRef], { stdio: 'pipe' });
|
|
3079
|
-
if (rebased.status !== 0) {
|
|
3080
|
-
const details = (rebased.stderr || rebased.stdout || '').trim();
|
|
3081
|
-
const gitDir = path.join(repoRoot, '.git');
|
|
3082
|
-
const rebaseActive = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
|
|
3083
|
-
const help = rebaseActive
|
|
3084
|
-
? '\nResolve conflicts, then run: git rebase --continue\nOr abort: git rebase --abort'
|
|
3085
|
-
: '';
|
|
3086
|
-
throw new Error(`Sync failed during rebase onto ${baseRef}.${details ? `\n${details}` : ''}${help}`);
|
|
3087
|
-
}
|
|
3088
|
-
return;
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
const mergeArgs = ['-C', repoRoot, 'merge', '--no-edit'];
|
|
3092
|
-
if (ffOnly) {
|
|
3093
|
-
mergeArgs.push('--ff-only');
|
|
769
|
+
function readScorecardJsonFile(filePath) {
|
|
770
|
+
const absolute = path.resolve(filePath);
|
|
771
|
+
if (!fs.existsSync(absolute)) {
|
|
772
|
+
throw new Error(`scorecard JSON file not found: ${absolute}`);
|
|
3094
773
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
const gitDir = path.join(repoRoot, '.git');
|
|
3100
|
-
const mergeActive = fs.existsSync(path.join(gitDir, 'MERGE_HEAD'));
|
|
3101
|
-
const help = mergeActive ? '\nResolve conflicts, then run: git commit\nOr abort: git merge --abort' : '';
|
|
3102
|
-
throw new Error(`Sync failed during merge from ${baseRef}.${details ? `\n${details}` : ''}${help}`);
|
|
774
|
+
try {
|
|
775
|
+
return JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
|
776
|
+
} catch (error) {
|
|
777
|
+
throw new Error(`Unable to parse scorecard JSON file: ${error.message}`);
|
|
3103
778
|
}
|
|
3104
779
|
}
|
|
3105
780
|
|
|
3106
|
-
function
|
|
3107
|
-
|
|
781
|
+
function normalizeScorecardChecks(payload) {
|
|
782
|
+
const rawChecks = Array.isArray(payload?.checks) ? payload.checks : [];
|
|
783
|
+
return rawChecks.map((check) => {
|
|
784
|
+
const name = String(check?.name || 'Unknown');
|
|
785
|
+
const rawScore = Number(check?.score);
|
|
786
|
+
const score = Number.isFinite(rawScore) ? rawScore : 0;
|
|
787
|
+
return {
|
|
788
|
+
name,
|
|
789
|
+
score,
|
|
790
|
+
risk: SCORECARD_RISK_BY_CHECK[name] || 'Unknown',
|
|
791
|
+
};
|
|
792
|
+
});
|
|
3108
793
|
}
|
|
3109
794
|
|
|
3110
|
-
|
|
795
|
+
function renderScorecardBaselineMarkdown({ repo, score, checks, capturedAt, scorecardVersion, reportDate }) {
|
|
796
|
+
const rows = checks
|
|
797
|
+
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
798
|
+
.join('\n');
|
|
3111
799
|
|
|
3112
|
-
|
|
3113
|
-
|
|
800
|
+
return [
|
|
801
|
+
'# OpenSSF Scorecard Baseline Report',
|
|
802
|
+
'',
|
|
803
|
+
`- **Repository:** \`${repo}\``,
|
|
804
|
+
'- **Source:** generated by `gx report scorecard`',
|
|
805
|
+
`- **Captured at:** ${capturedAt}`,
|
|
806
|
+
`- **Scorecard version:** \`${scorecardVersion}\``,
|
|
807
|
+
`- **Overall score:** **${score} / 10**`,
|
|
808
|
+
'',
|
|
809
|
+
'## Check breakdown',
|
|
810
|
+
'',
|
|
811
|
+
'| Check | Score | Risk |',
|
|
812
|
+
'|---|---:|---|',
|
|
813
|
+
rows || '| (none) | 0 | Unknown |',
|
|
814
|
+
'',
|
|
815
|
+
`## Report date`,
|
|
816
|
+
'',
|
|
817
|
+
`- ${reportDate}`,
|
|
818
|
+
'',
|
|
819
|
+
].join('\n');
|
|
3114
820
|
}
|
|
3115
821
|
|
|
3116
|
-
function
|
|
3117
|
-
|
|
3118
|
-
const
|
|
822
|
+
function renderScorecardRemediationPlanMarkdown({ baselineRelativePath, checks }) {
|
|
823
|
+
const failing = checks.filter((item) => item.score < 10);
|
|
824
|
+
const failingRows = failing
|
|
825
|
+
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
|
|
826
|
+
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
827
|
+
.join('\n');
|
|
3119
828
|
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
829
|
+
return [
|
|
830
|
+
'# OpenSSF Scorecard Remediation Plan',
|
|
831
|
+
'',
|
|
832
|
+
`Based on baseline report: \`${baselineRelativePath}\`.`,
|
|
833
|
+
'',
|
|
834
|
+
'## Failing checks',
|
|
835
|
+
'',
|
|
836
|
+
'| Check | Score | Risk |',
|
|
837
|
+
'|---|---:|---|',
|
|
838
|
+
(failingRows || '| None | 10 | N/A |'),
|
|
839
|
+
'',
|
|
840
|
+
'## Priority order',
|
|
841
|
+
'',
|
|
842
|
+
'1. Fix **High** risk checks first (especially score 0 items).',
|
|
843
|
+
'2. Then close **Medium** risk checks with score < 10.',
|
|
844
|
+
'3. Finally address **Low** risk ecosystem/process checks.',
|
|
845
|
+
'',
|
|
846
|
+
'## Verification loop',
|
|
847
|
+
'',
|
|
848
|
+
'1. Run scorecard again.',
|
|
849
|
+
'2. Re-generate baseline + remediation files.',
|
|
850
|
+
'3. Compare score deltas and track improved checks.',
|
|
851
|
+
'',
|
|
852
|
+
].join('\n');
|
|
853
|
+
}
|
|
3131
854
|
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
}
|
|
855
|
+
function parseBranchList(rawValue) {
|
|
856
|
+
return String(rawValue || '')
|
|
857
|
+
.split(/[\s,]+/)
|
|
858
|
+
.map((item) => item.trim())
|
|
859
|
+
.filter(Boolean);
|
|
860
|
+
}
|
|
3139
861
|
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
input += char;
|
|
862
|
+
function originRemoteLooksLikeGithub(repoRoot) {
|
|
863
|
+
const originUrl = readGitConfig(repoRoot, 'remote.origin.url');
|
|
864
|
+
if (!originUrl) {
|
|
865
|
+
return false;
|
|
3145
866
|
}
|
|
867
|
+
return /github\.com[:/]/i.test(originUrl);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function isInteractiveTerminal() {
|
|
871
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3146
872
|
}
|
|
3147
873
|
|
|
3148
874
|
function parseAutoApproval(name) {
|
|
@@ -3223,38 +949,6 @@ function describeGuardexRepoToggle(toggle) {
|
|
|
3223
949
|
return `${toggle.source} (${GUARDEX_REPO_TOGGLE_ENV}=${toggle.raw})`;
|
|
3224
950
|
}
|
|
3225
951
|
|
|
3226
|
-
function parseVersionString(version) {
|
|
3227
|
-
const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
3228
|
-
if (!match) return null;
|
|
3229
|
-
return [
|
|
3230
|
-
Number.parseInt(match[1], 10),
|
|
3231
|
-
Number.parseInt(match[2], 10),
|
|
3232
|
-
Number.parseInt(match[3], 10),
|
|
3233
|
-
];
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
function compareParsedVersions(left, right) {
|
|
3237
|
-
if (!left || !right) return 0;
|
|
3238
|
-
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
3239
|
-
const leftValue = left[index] || 0;
|
|
3240
|
-
const rightValue = right[index] || 0;
|
|
3241
|
-
if (leftValue > rightValue) return 1;
|
|
3242
|
-
if (leftValue < rightValue) return -1;
|
|
3243
|
-
}
|
|
3244
|
-
return 0;
|
|
3245
|
-
}
|
|
3246
|
-
|
|
3247
|
-
function isNewerVersion(latest, current) {
|
|
3248
|
-
const latestParts = parseVersionString(latest);
|
|
3249
|
-
const currentParts = parseVersionString(current);
|
|
3250
|
-
|
|
3251
|
-
if (!latestParts || !currentParts) {
|
|
3252
|
-
return String(latest || '').trim() !== String(current || '').trim();
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
return compareParsedVersions(latestParts, currentParts) > 0;
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
952
|
function parseNpmVersionOutput(stdout) {
|
|
3259
953
|
const trimmed = String(stdout || '').trim();
|
|
3260
954
|
if (!trimmed) return '';
|
|
@@ -3308,7 +1002,7 @@ function printUpdateAvailableBanner(current, latest) {
|
|
|
3308
1002
|
}
|
|
3309
1003
|
|
|
3310
1004
|
function maybeSelfUpdateBeforeStatus() {
|
|
3311
|
-
return
|
|
1005
|
+
return toolchainModule.maybeSelfUpdateBeforeStatus();
|
|
3312
1006
|
}
|
|
3313
1007
|
|
|
3314
1008
|
function readInstalledGuardexVersion() {
|
|
@@ -3443,7 +1137,7 @@ function printOpenSpecUpdateAvailableBanner(current, latest) {
|
|
|
3443
1137
|
}
|
|
3444
1138
|
|
|
3445
1139
|
function maybeOpenSpecUpdateBeforeStatus() {
|
|
3446
|
-
return
|
|
1140
|
+
return toolchainModule.maybeOpenSpecUpdateBeforeStatus();
|
|
3447
1141
|
}
|
|
3448
1142
|
|
|
3449
1143
|
function promptYesNoStrict(question) {
|
|
@@ -3622,7 +1316,7 @@ function askGlobalInstallForMissing(options, missingPackages, missingLocalTools)
|
|
|
3622
1316
|
}
|
|
3623
1317
|
|
|
3624
1318
|
function installGlobalToolchain(options) {
|
|
3625
|
-
return
|
|
1319
|
+
return toolchainModule.installGlobalToolchain(options);
|
|
3626
1320
|
}
|
|
3627
1321
|
|
|
3628
1322
|
function findStaleLockPaths(repoRoot, locks) {
|
|
@@ -3686,6 +1380,7 @@ function runInstallInternal(options) {
|
|
|
3686
1380
|
),
|
|
3687
1381
|
);
|
|
3688
1382
|
}
|
|
1383
|
+
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
3689
1384
|
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
3690
1385
|
for (const hookName of HOOK_NAMES) {
|
|
3691
1386
|
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
@@ -3742,6 +1437,7 @@ function runFixInternal(options) {
|
|
|
3742
1437
|
}
|
|
3743
1438
|
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
3744
1439
|
}
|
|
1440
|
+
operations.push(...materializePackageRepoTemplateFiles(repoRoot, TEMPLATE_FILES, Boolean(options.dryRun)));
|
|
3745
1441
|
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
3746
1442
|
for (const hookName of HOOK_NAMES) {
|
|
3747
1443
|
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
@@ -3982,9 +1678,9 @@ function status(rawArgs) {
|
|
|
3982
1678
|
json: false,
|
|
3983
1679
|
});
|
|
3984
1680
|
|
|
3985
|
-
const toolchain = detectGlobalToolchainPackages();
|
|
1681
|
+
const toolchain = toolchainModule.detectGlobalToolchainPackages();
|
|
3986
1682
|
const npmServices = GLOBAL_TOOLCHAIN_PACKAGES.map((pkg) => {
|
|
3987
|
-
const service = getGlobalToolchainService(pkg);
|
|
1683
|
+
const service = toolchainModule.getGlobalToolchainService(pkg);
|
|
3988
1684
|
if (!toolchain.ok) {
|
|
3989
1685
|
return {
|
|
3990
1686
|
name: service.name,
|
|
@@ -4002,12 +1698,12 @@ function status(rawArgs) {
|
|
|
4002
1698
|
status: toolchain.installed.includes(pkg) ? 'active' : 'inactive',
|
|
4003
1699
|
};
|
|
4004
1700
|
});
|
|
4005
|
-
const localCompanionServices = detectOptionalLocalCompanionTools().map((tool) => ({
|
|
1701
|
+
const localCompanionServices = toolchainModule.detectOptionalLocalCompanionTools().map((tool) => ({
|
|
4006
1702
|
name: tool.name,
|
|
4007
1703
|
displayName: tool.displayName || tool.name,
|
|
4008
1704
|
status: tool.status,
|
|
4009
1705
|
}));
|
|
4010
|
-
const requiredSystemTools = detectRequiredSystemTools();
|
|
1706
|
+
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
|
|
4011
1707
|
const services = [
|
|
4012
1708
|
...npmServices,
|
|
4013
1709
|
...localCompanionServices,
|
|
@@ -4076,7 +1772,7 @@ function status(rawArgs) {
|
|
|
4076
1772
|
console.log(
|
|
4077
1773
|
`[${TOOL_NAME}] Optional companion tools inactive: ${inactiveOptionalCompanions.join(', ')}`,
|
|
4078
1774
|
);
|
|
4079
|
-
for (const warning of describeMissingGlobalDependencyWarnings(
|
|
1775
|
+
for (const warning of toolchainModule.describeMissingGlobalDependencyWarnings(
|
|
4080
1776
|
npmServices
|
|
4081
1777
|
.filter((service) => service.status === 'inactive')
|
|
4082
1778
|
.map((service) => service.packageName),
|
|
@@ -4333,7 +2029,13 @@ function doctor(rawArgs) {
|
|
|
4333
2029
|
|
|
4334
2030
|
const blocked = protectedBaseWriteBlock(singleRepoOptions, { requireBootstrap: false });
|
|
4335
2031
|
if (blocked) {
|
|
4336
|
-
runDoctorInSandbox(singleRepoOptions, blocked
|
|
2032
|
+
doctorModule.runDoctorInSandbox(singleRepoOptions, blocked, {
|
|
2033
|
+
startProtectedBaseSandbox,
|
|
2034
|
+
cleanupProtectedBaseSandbox,
|
|
2035
|
+
ensureOmxScaffold,
|
|
2036
|
+
configureHooks,
|
|
2037
|
+
autoFinishReadyAgentBranches: doctorModule.autoFinishReadyAgentBranches,
|
|
2038
|
+
});
|
|
4337
2039
|
return;
|
|
4338
2040
|
}
|
|
4339
2041
|
|
|
@@ -4350,7 +2052,7 @@ function doctor(rawArgs) {
|
|
|
4350
2052
|
failed: 0,
|
|
4351
2053
|
details: [],
|
|
4352
2054
|
}
|
|
4353
|
-
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
2055
|
+
: doctorModule.autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
4354
2056
|
baseBranch: currentBaseBranch,
|
|
4355
2057
|
dryRun: singleRepoOptions.dryRun,
|
|
4356
2058
|
waitForMerge: singleRepoOptions.waitForMerge,
|
|
@@ -4454,10 +2156,15 @@ function processAlive(pid) {
|
|
|
4454
2156
|
}
|
|
4455
2157
|
try {
|
|
4456
2158
|
process.kill(normalizedPid, 0);
|
|
4457
|
-
return true;
|
|
4458
2159
|
} catch (_error) {
|
|
4459
2160
|
return false;
|
|
4460
2161
|
}
|
|
2162
|
+
|
|
2163
|
+
const state = readProcessState(normalizedPid);
|
|
2164
|
+
if (state.startsWith('Z')) {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
return true;
|
|
4461
2168
|
}
|
|
4462
2169
|
|
|
4463
2170
|
function sleepSeconds(seconds) {
|
|
@@ -4475,6 +2182,14 @@ function readProcessCommand(pid) {
|
|
|
4475
2182
|
return String(result.stdout || '').trim();
|
|
4476
2183
|
}
|
|
4477
2184
|
|
|
2185
|
+
function readProcessState(pid) {
|
|
2186
|
+
const result = run('ps', ['-o', 'stat=', '-p', String(pid)]);
|
|
2187
|
+
if (isSpawnFailure(result) || result.status !== 0) {
|
|
2188
|
+
return '';
|
|
2189
|
+
}
|
|
2190
|
+
return String(result.stdout || '').trim();
|
|
2191
|
+
}
|
|
2192
|
+
|
|
4478
2193
|
function stopAgentProcessByPid(pid, expectedToken = '') {
|
|
4479
2194
|
const normalizedPid = Number.parseInt(String(pid || ''), 10);
|
|
4480
2195
|
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
|
|
@@ -4666,6 +2381,16 @@ function agents(rawArgs) {
|
|
|
4666
2381
|
}
|
|
4667
2382
|
|
|
4668
2383
|
if (options.subcommand === 'stop') {
|
|
2384
|
+
if (options.pid) {
|
|
2385
|
+
const stopResult = stopAgentProcessByPid(options.pid);
|
|
2386
|
+
const success = ['stopped', 'not-running'].includes(stopResult.status);
|
|
2387
|
+
console.log(
|
|
2388
|
+
`[${TOOL_NAME}] Stopped agent pid ${options.pid} (${stopResult.status}).`,
|
|
2389
|
+
);
|
|
2390
|
+
process.exitCode = success ? 0 : 1;
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
4669
2394
|
const existingState = readAgentsState(repoRoot);
|
|
4670
2395
|
if (!existingState) {
|
|
4671
2396
|
console.log(`[${TOOL_NAME}] Repo agents are not running for ${repoRoot}.`);
|
|
@@ -4709,15 +2434,29 @@ function report(rawArgs) {
|
|
|
4709
2434
|
console.log(
|
|
4710
2435
|
`${TOOL_NAME} report commands:\n` +
|
|
4711
2436
|
` ${TOOL_NAME} report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD] [--dry-run] [--json]\n` +
|
|
2437
|
+
` ${TOOL_NAME} report session-severity --task-size <narrow-patch|medium-change|large-change> --tokens <count> --exec-count <count> --write-stdin-count <count> --completion-before-tail <yes|no> [--expected-bound <count>] [--fragmentation <preset|0-25>] [--finish-path <preset|0-15>] [--post-proof <preset|0-15>] [--json]\n` +
|
|
4712
2438
|
`\n` +
|
|
4713
2439
|
`Examples:\n` +
|
|
4714
2440
|
` ${TOOL_NAME} report scorecard --repo github.com/recodeecom/multiagent-safety\n` +
|
|
4715
|
-
` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10
|
|
2441
|
+
` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10\n` +
|
|
2442
|
+
` ${TOOL_NAME} report session-severity --task-size narrow-patch --tokens 3850000 --exec-count 18 --write-stdin-count 6 --completion-before-tail yes --fragmentation 14 --finish-path 6 --post-proof 4`,
|
|
4716
2443
|
);
|
|
4717
2444
|
process.exitCode = 0;
|
|
4718
2445
|
return;
|
|
4719
2446
|
}
|
|
4720
2447
|
|
|
2448
|
+
if (subcommand === 'session-severity') {
|
|
2449
|
+
const payload = sessionSeverityReport.buildSessionSeverityReport(options);
|
|
2450
|
+
if (options.json) {
|
|
2451
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
2452
|
+
process.exitCode = 0;
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
console.log(sessionSeverityReport.renderSessionSeverityReport(payload));
|
|
2456
|
+
process.exitCode = 0;
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
4721
2460
|
if (subcommand !== 'scorecard') {
|
|
4722
2461
|
throw new Error(`Unknown report subcommand: ${subcommand}`);
|
|
4723
2462
|
}
|
|
@@ -4806,7 +2545,7 @@ function setup(rawArgs) {
|
|
|
4806
2545
|
allowProtectedBaseWrite: false,
|
|
4807
2546
|
});
|
|
4808
2547
|
|
|
4809
|
-
const globalInstallStatus = installGlobalToolchain(options);
|
|
2548
|
+
const globalInstallStatus = toolchainModule.installGlobalToolchain(options);
|
|
4810
2549
|
if (globalInstallStatus.status === 'installed') {
|
|
4811
2550
|
console.log(
|
|
4812
2551
|
`[${TOOL_NAME}] ✅ Companion tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
|
|
@@ -4814,7 +2553,7 @@ function setup(rawArgs) {
|
|
|
4814
2553
|
} else if (globalInstallStatus.status === 'already-installed') {
|
|
4815
2554
|
console.log(`[${TOOL_NAME}] ✅ Companion tools already installed. Skipping.`);
|
|
4816
2555
|
} else if (globalInstallStatus.status === 'failed') {
|
|
4817
|
-
const installCommands = describeCompanionInstallCommands(
|
|
2556
|
+
const installCommands = toolchainModule.describeCompanionInstallCommands(
|
|
4818
2557
|
GLOBAL_TOOLCHAIN_PACKAGES,
|
|
4819
2558
|
OPTIONAL_LOCAL_COMPANION_TOOLS,
|
|
4820
2559
|
);
|
|
@@ -4830,13 +2569,13 @@ function setup(rawArgs) {
|
|
|
4830
2569
|
);
|
|
4831
2570
|
} else if (globalInstallStatus.status === 'skipped') {
|
|
4832
2571
|
console.log(`[${TOOL_NAME}] ⚠️ Companion installs skipped by user choice.`);
|
|
4833
|
-
for (const warning of describeMissingGlobalDependencyWarnings(
|
|
2572
|
+
for (const warning of toolchainModule.describeMissingGlobalDependencyWarnings(
|
|
4834
2573
|
globalInstallStatus.missingPackages || [],
|
|
4835
2574
|
)) {
|
|
4836
2575
|
console.log(`[${TOOL_NAME}] ⚠️ ${warning}`);
|
|
4837
2576
|
}
|
|
4838
2577
|
}
|
|
4839
|
-
const requiredSystemTools = detectRequiredSystemTools();
|
|
2578
|
+
const requiredSystemTools = toolchainModule.detectRequiredSystemTools();
|
|
4840
2579
|
const missingSystemTools = requiredSystemTools.filter((tool) => tool.status !== 'active');
|
|
4841
2580
|
if (missingSystemTools.length === 0) {
|
|
4842
2581
|
console.log(`[${TOOL_NAME}] ✅ Required system tools available (${requiredSystemTools.map((tool) => tool.name).join(', ')}).`);
|
|
@@ -4904,7 +2643,7 @@ function setup(rawArgs) {
|
|
|
4904
2643
|
|
|
4905
2644
|
const scanResult = runScanInternal({ target: repoPath, json: false });
|
|
4906
2645
|
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
4907
|
-
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
2646
|
+
const autoFinishSummary = doctorModule.autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
4908
2647
|
baseBranch: currentBaseBranch,
|
|
4909
2648
|
dryRun: perRepoOptions.dryRun,
|
|
4910
2649
|
});
|
|
@@ -5226,26 +2965,59 @@ function copyCommands() {
|
|
|
5226
2965
|
function prompt(rawArgs) {
|
|
5227
2966
|
const args = Array.isArray(rawArgs) ? rawArgs : [];
|
|
5228
2967
|
let variant = 'prompt';
|
|
5229
|
-
|
|
2968
|
+
let listParts = false;
|
|
2969
|
+
const selectedParts = [];
|
|
2970
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2971
|
+
const arg = args[index];
|
|
5230
2972
|
if (arg === '--exec' || arg === '--commands') variant = 'exec';
|
|
5231
2973
|
else if (arg === '--snippet' || arg === '--agents') variant = 'snippet';
|
|
5232
2974
|
else if (arg === '--prompt' || arg === '--full') variant = 'prompt';
|
|
2975
|
+
else if (arg === '--list-parts') listParts = true;
|
|
2976
|
+
else if (arg === '--part' || arg === '--parts') {
|
|
2977
|
+
const rawValue = args[index + 1];
|
|
2978
|
+
if (!rawValue || rawValue.startsWith('--')) {
|
|
2979
|
+
throw new Error(`${arg} requires a value`);
|
|
2980
|
+
}
|
|
2981
|
+
selectedParts.push(...parseAiSetupPartNames(rawValue));
|
|
2982
|
+
index += 1;
|
|
2983
|
+
} else if (arg.startsWith('--part=')) {
|
|
2984
|
+
selectedParts.push(...parseAiSetupPartNames(arg.slice('--part='.length)));
|
|
2985
|
+
} else if (arg.startsWith('--parts=')) {
|
|
2986
|
+
selectedParts.push(...parseAiSetupPartNames(arg.slice('--parts='.length)));
|
|
2987
|
+
}
|
|
5233
2988
|
else if (arg === '-h' || arg === '--help') variant = 'help';
|
|
5234
2989
|
else throw new Error(`Unknown option: ${arg}`);
|
|
5235
2990
|
}
|
|
5236
2991
|
if (variant === 'help') {
|
|
5237
2992
|
console.log(
|
|
5238
2993
|
`${SHORT_TOOL_NAME} prompt commands:\n` +
|
|
5239
|
-
` ${SHORT_TOOL_NAME} prompt
|
|
5240
|
-
` ${SHORT_TOOL_NAME} prompt --exec
|
|
5241
|
-
` ${SHORT_TOOL_NAME} prompt --
|
|
2994
|
+
` ${SHORT_TOOL_NAME} prompt Print AI setup checklist\n` +
|
|
2995
|
+
` ${SHORT_TOOL_NAME} prompt --exec Print setup commands only (shell-ready)\n` +
|
|
2996
|
+
` ${SHORT_TOOL_NAME} prompt --part <name> Print only the named checklist slice(s)\n` +
|
|
2997
|
+
` ${SHORT_TOOL_NAME} prompt --exec --part <name> Print only the named exec-capable slice(s)\n` +
|
|
2998
|
+
` ${SHORT_TOOL_NAME} prompt --list-parts List prompt part names\n` +
|
|
2999
|
+
` ${SHORT_TOOL_NAME} prompt --exec --list-parts List exec-capable prompt part names\n` +
|
|
3000
|
+
` ${SHORT_TOOL_NAME} prompt --snippet Print the AGENTS.md managed-block template`,
|
|
5242
3001
|
);
|
|
5243
3002
|
process.exitCode = 0;
|
|
5244
3003
|
return;
|
|
5245
3004
|
}
|
|
5246
|
-
if (variant === '
|
|
5247
|
-
|
|
5248
|
-
|
|
3005
|
+
if (variant === 'snippet') {
|
|
3006
|
+
if (listParts || selectedParts.length > 0) {
|
|
3007
|
+
throw new Error('--snippet does not support --list-parts or --part');
|
|
3008
|
+
}
|
|
3009
|
+
return printAgentsSnippet();
|
|
3010
|
+
}
|
|
3011
|
+
if (listParts) {
|
|
3012
|
+
if (selectedParts.length > 0) {
|
|
3013
|
+
throw new Error('--list-parts does not support --part');
|
|
3014
|
+
}
|
|
3015
|
+
process.stdout.write(`${listAiSetupPartNames({ execOnly: variant === 'exec' }).join('\n')}\n`);
|
|
3016
|
+
process.exitCode = 0;
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
process.stdout.write(renderAiSetupPrompt({ exec: variant === 'exec', parts: selectedParts }));
|
|
3020
|
+
process.exitCode = 0;
|
|
5249
3021
|
}
|
|
5250
3022
|
|
|
5251
3023
|
function branch(rawArgs) {
|
|
@@ -5376,19 +3148,19 @@ function migrate(rawArgs) {
|
|
|
5376
3148
|
}
|
|
5377
3149
|
|
|
5378
3150
|
function cleanup(rawArgs) {
|
|
5379
|
-
return
|
|
3151
|
+
return finishCommands.cleanup(rawArgs);
|
|
5380
3152
|
}
|
|
5381
3153
|
|
|
5382
3154
|
function merge(rawArgs) {
|
|
5383
|
-
return
|
|
3155
|
+
return finishCommands.merge(rawArgs);
|
|
5384
3156
|
}
|
|
5385
3157
|
|
|
5386
3158
|
function finish(rawArgs, defaults = {}) {
|
|
5387
|
-
return
|
|
3159
|
+
return finishCommands.finish(rawArgs, defaults);
|
|
5388
3160
|
}
|
|
5389
3161
|
|
|
5390
3162
|
function sync(rawArgs) {
|
|
5391
|
-
return
|
|
3163
|
+
return finishCommands.sync(rawArgs);
|
|
5392
3164
|
}
|
|
5393
3165
|
|
|
5394
3166
|
function protect(rawArgs) {
|
|
@@ -5473,8 +3245,8 @@ function main() {
|
|
|
5473
3245
|
const args = process.argv.slice(2);
|
|
5474
3246
|
|
|
5475
3247
|
if (args.length === 0) {
|
|
5476
|
-
maybeSelfUpdateBeforeStatus();
|
|
5477
|
-
maybeOpenSpecUpdateBeforeStatus();
|
|
3248
|
+
toolchainModule.maybeSelfUpdateBeforeStatus();
|
|
3249
|
+
toolchainModule.maybeOpenSpecUpdateBeforeStatus();
|
|
5478
3250
|
status([]);
|
|
5479
3251
|
return;
|
|
5480
3252
|
}
|
|
@@ -5488,7 +3260,7 @@ function main() {
|
|
|
5488
3260
|
}
|
|
5489
3261
|
|
|
5490
3262
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
5491
|
-
maybeSelfUpdateBeforeStatus();
|
|
3263
|
+
toolchainModule.maybeSelfUpdateBeforeStatus();
|
|
5492
3264
|
console.log(packageJson.version);
|
|
5493
3265
|
return;
|
|
5494
3266
|
}
|