@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.
@@ -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 in the source-probe worktree; run rebase --continue or rebase --abort',
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
 
@@ -1,68 +1,317 @@
1
- function createSandboxApi(deps) {
2
- const {
3
- protectedBaseWriteBlock,
4
- runInstallInternal,
5
- ensureSetupProtectedBranches,
6
- ensureParentWorkspaceView,
7
- buildParentWorkspaceView,
8
- runFixInternal,
9
- } = deps;
10
-
11
- function assertProtectedMainWriteAllowed(options, commandName) {
12
- const blocked = protectedBaseWriteBlock(options);
13
- if (!blocked) {
14
- return;
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
- throw new Error(
18
- `${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
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
- function runSetupBootstrapInternal(options) {
26
- const installPayload = runInstallInternal(options);
27
- installPayload.operations.push(
28
- ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)),
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
- let parentWorkspace = null;
32
- if (options.parentWorkspaceView) {
33
- installPayload.operations.push(
34
- ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)),
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
- const fixPayload = runFixInternal({
42
- target: installPayload.repoRoot,
43
- dryRun: options.dryRun,
44
- force: options.force,
45
- forceManagedPaths: options.forceManagedPaths,
46
- dropStaleLocks: true,
47
- skipAgents: options.skipAgents,
48
- skipPackageJson: options.skipPackageJson,
49
- skipGitignore: options.skipGitignore,
50
- allowProtectedBaseWrite: options.allowProtectedBaseWrite,
51
- });
52
-
53
- return {
54
- installPayload,
55
- fixPayload,
56
- parentWorkspace,
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
- assertProtectedMainWriteAllowed,
62
- runSetupBootstrapInternal,
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
- createSandboxApi,
309
+ protectedBaseWriteBlock,
310
+ assertProtectedMainWriteAllowed,
311
+ extractAgentBranchStartMetadata,
312
+ resolveSandboxTarget,
313
+ buildSandboxSetupArgs,
314
+ isSpawnFailure,
315
+ startProtectedBaseSandbox,
316
+ cleanupProtectedBaseSandbox,
68
317
  };