@imdeadpool/guardex 7.0.20 → 7.0.22
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 +66 -30
- package/package.json +1 -1
- package/src/cli/args.js +804 -2
- package/src/cli/main.js +744 -5101
- package/src/context.js +197 -33
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +645 -32
- package/src/output/index.js +8 -1
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +681 -22
- package/src/toolchain/index.js +622 -178
- package/templates/scripts/agent-branch-finish.sh +56 -5
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +14 -2
- package/templates/vscode/guardex-active-agents/README.md +3 -1
- package/templates/vscode/guardex-active-agents/extension.js +321 -12
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +5 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +233 -29
package/src/output/index.js
CHANGED
|
@@ -281,7 +281,14 @@ function detectRecoverableAutoFinishConflict(message) {
|
|
|
281
281
|
if (/rebase --continue/i.test(text) && /rebase --abort/i.test(text)) {
|
|
282
282
|
return {
|
|
283
283
|
rawLabel: 'auto-finish requires manual rebase.',
|
|
284
|
-
summary: 'manual rebase required
|
|
284
|
+
summary: 'manual rebase required on the branch before auto-finish can continue',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (/Reattach '.+' in a regular worktree, then rebase it onto origin\/.+ manually\./i.test(text)) {
|
|
289
|
+
return {
|
|
290
|
+
rawLabel: 'auto-finish requires manual rebase.',
|
|
291
|
+
summary: 'manual rebase required on the branch before auto-finish can continue',
|
|
285
292
|
};
|
|
286
293
|
}
|
|
287
294
|
|
package/src/sandbox/index.js
CHANGED
|
@@ -1,68 +1,317 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
const {
|
|
2
|
+
fs,
|
|
3
|
+
path,
|
|
4
|
+
SHORT_TOOL_NAME,
|
|
5
|
+
LOCK_FILE_RELATIVE,
|
|
6
|
+
defaultAgentWorktreeRelativeDir,
|
|
7
|
+
} = require('../context');
|
|
8
|
+
const { run, runPackageAsset } = require('../core/runtime');
|
|
9
|
+
const {
|
|
10
|
+
resolveRepoRoot,
|
|
11
|
+
currentBranchName,
|
|
12
|
+
readProtectedBranches,
|
|
13
|
+
gitRefExists,
|
|
14
|
+
ensureRepoBranch,
|
|
15
|
+
} = require('../git');
|
|
16
|
+
|
|
17
|
+
function hasGuardexBootstrapFiles(repoRoot) {
|
|
18
|
+
const required = [
|
|
19
|
+
'AGENTS.md',
|
|
20
|
+
'.githooks/pre-commit',
|
|
21
|
+
'.githooks/pre-push',
|
|
22
|
+
LOCK_FILE_RELATIVE,
|
|
23
|
+
];
|
|
24
|
+
return required.every((relativePath) => require('../context').fs.existsSync(path.join(repoRoot, relativePath)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function protectedBaseWriteBlock(options, { requireBootstrap = true } = {}) {
|
|
28
|
+
if (options.dryRun || options.allowProtectedBaseWrite) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
33
|
+
if (requireBootstrap && !hasGuardexBootstrapFiles(repoRoot)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const branch = currentBranchName(repoRoot);
|
|
38
|
+
if (branch !== 'main') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const protectedBranches = readProtectedBranches(repoRoot);
|
|
43
|
+
if (!protectedBranches.includes(branch)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
repoRoot,
|
|
49
|
+
branch,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertProtectedMainWriteAllowed(options, commandName) {
|
|
54
|
+
const blocked = protectedBaseWriteBlock(options);
|
|
55
|
+
if (!blocked) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
|
|
61
|
+
`Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
|
|
62
|
+
` gx branch start "<task>" "codex"\n` +
|
|
63
|
+
`Override once only when intentional: --allow-protected-base-write`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractAgentBranchStartMetadata(output) {
|
|
68
|
+
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
|
|
69
|
+
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
|
|
70
|
+
return {
|
|
71
|
+
branch: branchMatch ? branchMatch[1].trim() : '',
|
|
72
|
+
worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
77
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
78
|
+
const relativeTarget = path.relative(repoRoot, resolvedTarget);
|
|
79
|
+
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
80
|
+
throw new Error(`sandbox target must stay inside repo root: ${resolvedTarget}`);
|
|
81
|
+
}
|
|
82
|
+
if (!relativeTarget || relativeTarget === '.') {
|
|
83
|
+
return worktreePath;
|
|
84
|
+
}
|
|
85
|
+
return path.join(worktreePath, relativeTarget);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendManagedForceArgs(args, options) {
|
|
89
|
+
if (!options.force) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
args.push('--force');
|
|
93
|
+
for (const managedPath of options.forceManagedPaths || []) {
|
|
94
|
+
args.push(managedPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
99
|
+
const args = ['setup', '--target', sandboxTarget, '--no-global-install', '--no-recursive'];
|
|
100
|
+
appendManagedForceArgs(args, options);
|
|
101
|
+
if (options.skipAgents) args.push('--skip-agents');
|
|
102
|
+
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
103
|
+
if (options.skipGitignore) args.push('--no-gitignore');
|
|
104
|
+
if (options.dryRun) args.push('--dry-run');
|
|
105
|
+
return args;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isSpawnFailure(result) {
|
|
109
|
+
return Boolean(result?.error) && typeof result?.status !== 'number';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function protectedBaseSandboxBranchPrefix() {
|
|
113
|
+
const now = new Date();
|
|
114
|
+
const stamp = [
|
|
115
|
+
now.getUTCFullYear(),
|
|
116
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
117
|
+
String(now.getUTCDate()).padStart(2, '0'),
|
|
118
|
+
].join('') + '-' + [
|
|
119
|
+
String(now.getUTCHours()).padStart(2, '0'),
|
|
120
|
+
String(now.getUTCMinutes()).padStart(2, '0'),
|
|
121
|
+
String(now.getUTCSeconds()).padStart(2, '0'),
|
|
122
|
+
].join('');
|
|
123
|
+
return `agent/gx/${stamp}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function protectedBaseSandboxWorktreePath(repoRoot, branchName) {
|
|
127
|
+
return path.join(repoRoot, defaultAgentWorktreeRelativeDir(), branchName.replace(/\//g, '__'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveProtectedBaseSandboxStartRef(repoRoot, baseBranch) {
|
|
131
|
+
run('git', ['-C', repoRoot, 'fetch', 'origin', baseBranch, '--quiet'], { timeout: 20_000 });
|
|
132
|
+
if (gitRefExists(repoRoot, `refs/remotes/origin/${baseBranch}`)) {
|
|
133
|
+
return `origin/${baseBranch}`;
|
|
134
|
+
}
|
|
135
|
+
if (gitRefExists(repoRoot, `refs/heads/${baseBranch}`)) {
|
|
136
|
+
return baseBranch;
|
|
137
|
+
}
|
|
138
|
+
if (currentBranchName(repoRoot) === baseBranch) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`Unable to find base ref for sandbox bootstrap: ${baseBranch}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function startProtectedBaseSandboxFallback(blocked, sandboxSuffix) {
|
|
145
|
+
const branchPrefix = protectedBaseSandboxBranchPrefix();
|
|
146
|
+
let selectedBranch = '';
|
|
147
|
+
let selectedWorktreePath = '';
|
|
148
|
+
|
|
149
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
150
|
+
const suffix = attempt === 0 ? sandboxSuffix : `${attempt + 1}-${sandboxSuffix}`;
|
|
151
|
+
const candidateBranch = `${branchPrefix}-${suffix}`;
|
|
152
|
+
const candidateWorktreePath = protectedBaseSandboxWorktreePath(blocked.repoRoot, candidateBranch);
|
|
153
|
+
if (gitRefExists(blocked.repoRoot, `refs/heads/${candidateBranch}`)) {
|
|
154
|
+
continue;
|
|
15
155
|
}
|
|
156
|
+
if (fs.existsSync(candidateWorktreePath)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
selectedBranch = candidateBranch;
|
|
160
|
+
selectedWorktreePath = candidateWorktreePath;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
16
163
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
|
|
20
|
-
` gx branch start "<task>" "codex"\n` +
|
|
21
|
-
`Override once only when intentional: --allow-protected-base-write`,
|
|
22
|
-
);
|
|
164
|
+
if (!selectedBranch || !selectedWorktreePath) {
|
|
165
|
+
throw new Error('Unable to allocate unique sandbox branch/worktree');
|
|
23
166
|
}
|
|
24
167
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
168
|
+
fs.mkdirSync(path.dirname(selectedWorktreePath), { recursive: true });
|
|
169
|
+
const startRef = resolveProtectedBaseSandboxStartRef(blocked.repoRoot, blocked.branch);
|
|
170
|
+
const addArgs = startRef
|
|
171
|
+
? ['-C', blocked.repoRoot, 'worktree', 'add', '-b', selectedBranch, selectedWorktreePath, startRef]
|
|
172
|
+
: ['-C', blocked.repoRoot, 'worktree', 'add', '--orphan', selectedWorktreePath];
|
|
173
|
+
const addResult = run('git', addArgs);
|
|
174
|
+
if (isSpawnFailure(addResult)) {
|
|
175
|
+
throw addResult.error;
|
|
176
|
+
}
|
|
177
|
+
if (addResult.status !== 0) {
|
|
178
|
+
throw new Error((addResult.stderr || addResult.stdout || 'failed to create sandbox').trim());
|
|
179
|
+
}
|
|
30
180
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
181
|
+
if (!startRef) {
|
|
182
|
+
const renameResult = run(
|
|
183
|
+
'git',
|
|
184
|
+
['-C', selectedWorktreePath, 'branch', '-m', selectedBranch],
|
|
185
|
+
{ timeout: 20_000 },
|
|
186
|
+
);
|
|
187
|
+
if (isSpawnFailure(renameResult)) {
|
|
188
|
+
throw renameResult.error;
|
|
189
|
+
}
|
|
190
|
+
if (renameResult.status !== 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
(renameResult.stderr || renameResult.stdout || 'failed to name orphan sandbox branch').trim(),
|
|
35
193
|
);
|
|
36
|
-
if (!options.dryRun) {
|
|
37
|
-
parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
|
|
38
|
-
}
|
|
39
194
|
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
metadata: {
|
|
199
|
+
branch: selectedBranch,
|
|
200
|
+
worktreePath: selectedWorktreePath,
|
|
201
|
+
},
|
|
202
|
+
stdout:
|
|
203
|
+
`[agent-branch-start] Created branch: ${selectedBranch}\n` +
|
|
204
|
+
`[agent-branch-start] Worktree: ${selectedWorktreePath}\n`,
|
|
205
|
+
stderr: addResult.stderr || '',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
210
|
+
if (sandboxSuffix === 'gx-doctor') {
|
|
211
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const startResult = runPackageAsset('branchStart', [
|
|
215
|
+
'--task',
|
|
216
|
+
taskName,
|
|
217
|
+
'--agent',
|
|
218
|
+
SHORT_TOOL_NAME,
|
|
219
|
+
'--base',
|
|
220
|
+
blocked.branch,
|
|
221
|
+
], { cwd: blocked.repoRoot });
|
|
222
|
+
if (isSpawnFailure(startResult)) {
|
|
223
|
+
throw startResult.error;
|
|
224
|
+
}
|
|
225
|
+
if (startResult.status !== 0) {
|
|
226
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
227
|
+
}
|
|
40
228
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
229
|
+
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
230
|
+
const currentBranch = currentBranchName(blocked.repoRoot);
|
|
231
|
+
const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
|
|
232
|
+
const repoRootPath = path.resolve(blocked.repoRoot);
|
|
233
|
+
const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
|
|
234
|
+
const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
|
|
235
|
+
|
|
236
|
+
if (!hasSafeWorktree || branchChanged) {
|
|
237
|
+
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
238
|
+
if (!restoreResult.ok) {
|
|
239
|
+
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
240
|
+
throw new Error(
|
|
241
|
+
`sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
242
|
+
(detail ? `\n${detail}` : ''),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
58
246
|
}
|
|
59
247
|
|
|
60
248
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
249
|
+
metadata,
|
|
250
|
+
stdout: startResult.stdout || '',
|
|
251
|
+
stderr: startResult.stderr || '',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cleanupProtectedBaseSandbox(repoRoot, metadata) {
|
|
256
|
+
const result = {
|
|
257
|
+
worktree: 'skipped',
|
|
258
|
+
branch: 'skipped',
|
|
259
|
+
note: 'missing sandbox metadata',
|
|
63
260
|
};
|
|
261
|
+
|
|
262
|
+
if (!metadata?.worktreePath || !metadata?.branch) {
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (fs.existsSync(metadata.worktreePath)) {
|
|
267
|
+
const removeResult = run(
|
|
268
|
+
'git',
|
|
269
|
+
['-C', repoRoot, 'worktree', 'remove', '--force', metadata.worktreePath],
|
|
270
|
+
{ timeout: 30_000 },
|
|
271
|
+
);
|
|
272
|
+
if (isSpawnFailure(removeResult)) {
|
|
273
|
+
throw removeResult.error;
|
|
274
|
+
}
|
|
275
|
+
if (removeResult.status !== 0) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
(removeResult.stderr || removeResult.stdout || 'failed to remove sandbox worktree').trim(),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
result.worktree = 'removed';
|
|
281
|
+
} else {
|
|
282
|
+
result.worktree = 'missing';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (gitRefExists(repoRoot, `refs/heads/${metadata.branch}`)) {
|
|
286
|
+
const branchDeleteResult = run(
|
|
287
|
+
'git',
|
|
288
|
+
['-C', repoRoot, 'branch', '-D', metadata.branch],
|
|
289
|
+
{ timeout: 20_000 },
|
|
290
|
+
);
|
|
291
|
+
if (isSpawnFailure(branchDeleteResult)) {
|
|
292
|
+
throw branchDeleteResult.error;
|
|
293
|
+
}
|
|
294
|
+
if (branchDeleteResult.status !== 0) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
(branchDeleteResult.stderr || branchDeleteResult.stdout || 'failed to delete sandbox branch').trim(),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
result.branch = 'deleted';
|
|
300
|
+
} else {
|
|
301
|
+
result.branch = 'missing';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
result.note = 'sandbox worktree pruned';
|
|
305
|
+
return result;
|
|
64
306
|
}
|
|
65
307
|
|
|
66
308
|
module.exports = {
|
|
67
|
-
|
|
309
|
+
protectedBaseWriteBlock,
|
|
310
|
+
assertProtectedMainWriteAllowed,
|
|
311
|
+
extractAgentBranchStartMetadata,
|
|
312
|
+
resolveSandboxTarget,
|
|
313
|
+
buildSandboxSetupArgs,
|
|
314
|
+
isSpawnFailure,
|
|
315
|
+
startProtectedBaseSandbox,
|
|
316
|
+
cleanupProtectedBaseSandbox,
|
|
68
317
|
};
|