@imdeadpool/guardex 7.0.14 → 7.0.16
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 +37 -4
- package/bin/multiagent-safety.js +1236 -171
- package/package.json +3 -2
- package/templates/scripts/agent-branch-finish.sh +35 -6
- package/templates/scripts/agent-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +93 -15
- package/templates/scripts/agent-worktree-prune.sh +78 -44
- package/templates/scripts/codex-agent.sh +96 -4
- package/templates/scripts/guardex-docker-loader.sh +123 -0
- package/templates/scripts/openspec/init-plan-workspace.sh +42 -0
package/bin/multiagent-safety.js
CHANGED
|
@@ -64,7 +64,7 @@ const REQUIRED_SYSTEM_TOOLS = [
|
|
|
64
64
|
},
|
|
65
65
|
];
|
|
66
66
|
const MAINTAINER_RELEASE_REPO = path.resolve(
|
|
67
|
-
process.env.GUARDEX_RELEASE_REPO || '
|
|
67
|
+
process.env.GUARDEX_RELEASE_REPO || path.resolve(__dirname, '..'),
|
|
68
68
|
);
|
|
69
69
|
const NPM_BIN = process.env.GUARDEX_NPM_BIN || 'npm';
|
|
70
70
|
const OPENSPEC_BIN = process.env.GUARDEX_OPENSPEC_BIN || 'openspec';
|
|
@@ -77,13 +77,21 @@ const DEFAULT_PROTECTED_BRANCHES = ['dev', 'main', 'master'];
|
|
|
77
77
|
const DEFAULT_BASE_BRANCH = 'dev';
|
|
78
78
|
const DEFAULT_SYNC_STRATEGY = 'rebase';
|
|
79
79
|
const DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES = 60;
|
|
80
|
+
const COMPOSE_HINT_FILES = [
|
|
81
|
+
'docker-compose.yml',
|
|
82
|
+
'docker-compose.yaml',
|
|
83
|
+
'compose.yml',
|
|
84
|
+
'compose.yaml',
|
|
85
|
+
];
|
|
80
86
|
|
|
81
87
|
const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates');
|
|
82
88
|
|
|
83
89
|
const TEMPLATE_FILES = [
|
|
84
90
|
'scripts/agent-branch-start.sh',
|
|
85
91
|
'scripts/agent-branch-finish.sh',
|
|
92
|
+
'scripts/agent-branch-merge.sh',
|
|
86
93
|
'scripts/codex-agent.sh',
|
|
94
|
+
'scripts/guardex-docker-loader.sh',
|
|
87
95
|
'scripts/review-bot-watch.sh',
|
|
88
96
|
'scripts/agent-worktree-prune.sh',
|
|
89
97
|
'scripts/agent-file-locks.py',
|
|
@@ -105,6 +113,8 @@ const TEMPLATE_FILES = [
|
|
|
105
113
|
const REQUIRED_WORKFLOW_FILES = [
|
|
106
114
|
'scripts/agent-branch-start.sh',
|
|
107
115
|
'scripts/agent-branch-finish.sh',
|
|
116
|
+
'scripts/agent-branch-merge.sh',
|
|
117
|
+
'scripts/guardex-docker-loader.sh',
|
|
108
118
|
'scripts/agent-worktree-prune.sh',
|
|
109
119
|
'scripts/agent-file-locks.py',
|
|
110
120
|
'scripts/guardex-env.sh',
|
|
@@ -118,6 +128,7 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
118
128
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
119
129
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
120
130
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
131
|
+
'agent:branch:merge': 'bash ./scripts/agent-branch-merge.sh',
|
|
121
132
|
'agent:cleanup': 'gx cleanup',
|
|
122
133
|
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
123
134
|
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
@@ -133,6 +144,7 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
133
144
|
'agent:safety:scan': 'gx status --strict',
|
|
134
145
|
'agent:safety:fix': 'gx setup --repair',
|
|
135
146
|
'agent:safety:doctor': 'gx doctor',
|
|
147
|
+
'agent:docker:load': 'bash ./scripts/guardex-docker-loader.sh',
|
|
136
148
|
'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
|
|
137
149
|
'agent:finish': 'gx finish --all',
|
|
138
150
|
};
|
|
@@ -140,7 +152,9 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
140
152
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
141
153
|
'scripts/agent-branch-start.sh',
|
|
142
154
|
'scripts/agent-branch-finish.sh',
|
|
155
|
+
'scripts/agent-branch-merge.sh',
|
|
143
156
|
'scripts/codex-agent.sh',
|
|
157
|
+
'scripts/guardex-docker-loader.sh',
|
|
144
158
|
'scripts/review-bot-watch.sh',
|
|
145
159
|
'scripts/agent-worktree-prune.sh',
|
|
146
160
|
'scripts/agent-file-locks.py',
|
|
@@ -161,6 +175,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
|
161
175
|
'.githooks/post-checkout',
|
|
162
176
|
'scripts/agent-branch-start.sh',
|
|
163
177
|
'scripts/agent-branch-finish.sh',
|
|
178
|
+
'scripts/agent-branch-merge.sh',
|
|
164
179
|
'scripts/agent-worktree-prune.sh',
|
|
165
180
|
'scripts/codex-agent.sh',
|
|
166
181
|
'scripts/agent-file-locks.py',
|
|
@@ -173,10 +188,18 @@ const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
|
|
|
173
188
|
const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
|
|
174
189
|
const GITIGNORE_MARKER_START = '# multiagent-safety:START';
|
|
175
190
|
const GITIGNORE_MARKER_END = '# multiagent-safety:END';
|
|
191
|
+
const CODEX_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
|
|
192
|
+
const CLAUDE_WORKTREE_RELATIVE_DIR = path.join('.omc', 'agent-worktrees');
|
|
193
|
+
const AGENT_WORKTREE_RELATIVE_DIRS = [
|
|
194
|
+
CODEX_WORKTREE_RELATIVE_DIR,
|
|
195
|
+
CLAUDE_WORKTREE_RELATIVE_DIR,
|
|
196
|
+
];
|
|
176
197
|
const MANAGED_GITIGNORE_PATHS = [
|
|
177
198
|
'.omx/',
|
|
178
199
|
'.omc/',
|
|
179
200
|
'scripts/*',
|
|
201
|
+
'scripts/agent-branch-start.sh',
|
|
202
|
+
'scripts/agent-file-locks.py',
|
|
180
203
|
'.githooks',
|
|
181
204
|
'oh-my-codex/',
|
|
182
205
|
'.codex/skills/gitguardex/SKILL.md',
|
|
@@ -190,7 +213,9 @@ const OMX_SCAFFOLD_DIRECTORIES = [
|
|
|
190
213
|
'.omx/state',
|
|
191
214
|
'.omx/logs',
|
|
192
215
|
'.omx/plans',
|
|
193
|
-
|
|
216
|
+
CODEX_WORKTREE_RELATIVE_DIR,
|
|
217
|
+
'.omc',
|
|
218
|
+
CLAUDE_WORKTREE_RELATIVE_DIR,
|
|
194
219
|
];
|
|
195
220
|
const OMX_SCAFFOLD_FILES = new Map([
|
|
196
221
|
['.omx/notepad.md', '\n\n## WORKING MEMORY\n'],
|
|
@@ -213,6 +238,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
213
238
|
'setup',
|
|
214
239
|
'doctor',
|
|
215
240
|
'agents',
|
|
241
|
+
'merge',
|
|
216
242
|
'finish',
|
|
217
243
|
'report',
|
|
218
244
|
'protect',
|
|
@@ -237,9 +263,11 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
237
263
|
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
|
|
238
264
|
['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
|
|
239
265
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
266
|
+
['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
|
|
240
267
|
['sync', 'Sync agent branches with origin/<base>'],
|
|
241
268
|
['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
|
|
242
269
|
['cleanup', 'Prune merged/stale agent branches and worktrees'],
|
|
270
|
+
['release', 'Create or update the current GitHub release with README-generated notes'],
|
|
243
271
|
['agents', 'Start/stop repo-scoped review + cleanup bots'],
|
|
244
272
|
['prompt', 'Print AI setup checklist (--exec, --snippet)'],
|
|
245
273
|
['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
|
|
@@ -259,6 +287,22 @@ const DEPRECATED_COMMAND_ALIASES = new Map([
|
|
|
259
287
|
const AGENT_BOT_DESCRIPTIONS = [
|
|
260
288
|
['agents', 'Start/stop review + cleanup bots for this repo'],
|
|
261
289
|
];
|
|
290
|
+
const DOCTOR_AUTO_FINISH_DETAIL_LIMIT = 6;
|
|
291
|
+
const DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX = 72;
|
|
292
|
+
const DOCTOR_AUTO_FINISH_MESSAGE_MAX = 160;
|
|
293
|
+
|
|
294
|
+
function envFlagIsTruthy(raw) {
|
|
295
|
+
const lowered = String(raw || '').trim().toLowerCase();
|
|
296
|
+
return lowered === '1' || lowered === 'true' || lowered === 'yes' || lowered === 'on';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isClaudeCodeSession(env = process.env) {
|
|
300
|
+
return envFlagIsTruthy(env.CLAUDECODE) || Boolean(env.CLAUDE_CODE_SESSION_ID);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function defaultAgentWorktreeRelativeDir(env = process.env) {
|
|
304
|
+
return isClaudeCodeSession(env) ? CLAUDE_WORKTREE_RELATIVE_DIR : CODEX_WORKTREE_RELATIVE_DIR;
|
|
305
|
+
}
|
|
262
306
|
|
|
263
307
|
const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in this repo.
|
|
264
308
|
|
|
@@ -267,13 +311,14 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
|
|
|
267
311
|
3) Repair: gx doctor
|
|
268
312
|
4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
269
313
|
or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
|
|
270
|
-
5)
|
|
271
|
-
6)
|
|
272
|
-
7)
|
|
273
|
-
8)
|
|
274
|
-
9) Optional: gx
|
|
275
|
-
10)
|
|
276
|
-
11)
|
|
314
|
+
5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
|
|
315
|
+
6) Finish: gx finish --all
|
|
316
|
+
7) Cleanup: gx cleanup
|
|
317
|
+
8) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
|
|
318
|
+
9) Optional: gx protect add release staging
|
|
319
|
+
10) Optional: gx sync --check && gx sync
|
|
320
|
+
11) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
|
|
321
|
+
12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
|
|
277
322
|
`;
|
|
278
323
|
|
|
279
324
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
@@ -282,6 +327,7 @@ gx setup
|
|
|
282
327
|
gx doctor
|
|
283
328
|
bash scripts/codex-agent.sh "<task>" "<agent>"
|
|
284
329
|
python3 scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>
|
|
330
|
+
gx merge --branch "<agent-a>" --branch "<agent-b>"
|
|
285
331
|
gx finish --all
|
|
286
332
|
gx cleanup
|
|
287
333
|
gx protect add release staging
|
|
@@ -470,6 +516,113 @@ function run(cmd, args, options = {}) {
|
|
|
470
516
|
});
|
|
471
517
|
}
|
|
472
518
|
|
|
519
|
+
function formatElapsedDuration(ms) {
|
|
520
|
+
const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
521
|
+
if (durationMs < 1000) {
|
|
522
|
+
return `${Math.round(durationMs)}ms`;
|
|
523
|
+
}
|
|
524
|
+
if (durationMs < 10_000) {
|
|
525
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
526
|
+
}
|
|
527
|
+
return `${Math.round(durationMs / 1000)}s`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function truncateMiddle(value, maxLength) {
|
|
531
|
+
const text = String(value || '');
|
|
532
|
+
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
533
|
+
if (!limit || text.length <= limit) {
|
|
534
|
+
return text;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const visible = limit - 1;
|
|
538
|
+
const headLength = Math.ceil(visible / 2);
|
|
539
|
+
const tailLength = Math.floor(visible / 2);
|
|
540
|
+
return `${text.slice(0, headLength)}…${text.slice(text.length - tailLength)}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function truncateTail(value, maxLength) {
|
|
544
|
+
const text = String(value || '');
|
|
545
|
+
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
546
|
+
if (!limit || text.length <= limit) {
|
|
547
|
+
return text;
|
|
548
|
+
}
|
|
549
|
+
return `${text.slice(0, limit - 1)}…`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function compactAutoFinishPathSegments(message) {
|
|
553
|
+
return String(message || '').replace(/\((\/[^)]+)\)/g, (_, rawPath) => {
|
|
554
|
+
if (
|
|
555
|
+
rawPath.includes(`${path.sep}.omx${path.sep}agent-worktrees${path.sep}`) ||
|
|
556
|
+
rawPath.includes(`${path.sep}.omc${path.sep}agent-worktrees${path.sep}`)
|
|
557
|
+
) {
|
|
558
|
+
return `(${path.basename(rawPath)})`;
|
|
559
|
+
}
|
|
560
|
+
return `(${truncateMiddle(rawPath, 72)})`;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function summarizeAutoFinishDetail(detail) {
|
|
565
|
+
const trimmed = String(detail || '').trim();
|
|
566
|
+
const match = trimmed.match(/^\[(\w+)\]\s+([^:]+):\s*(.*)$/);
|
|
567
|
+
if (!match) {
|
|
568
|
+
return truncateTail(compactAutoFinishPathSegments(trimmed), DOCTOR_AUTO_FINISH_MESSAGE_MAX);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const [, status, rawBranch, rawMessage] = match;
|
|
572
|
+
const branch = truncateMiddle(rawBranch, DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX);
|
|
573
|
+
let message = String(rawMessage || '').trim();
|
|
574
|
+
|
|
575
|
+
if (status === 'fail') {
|
|
576
|
+
message = message.replace(/^auto-finish failed\.?\s*/i, '');
|
|
577
|
+
if (/\[agent-sync-guard\]/.test(message) && /Resolve conflicts/i.test(message)) {
|
|
578
|
+
message = 'rebase conflict in finish flow; run rebase --continue or rebase --abort in the source-probe worktree';
|
|
579
|
+
} else if (/unable to compute ahead\/behind/i.test(message)) {
|
|
580
|
+
const aheadBehindMatch = message.match(/unable to compute ahead\/behind(?: \([^)]+\))?/i);
|
|
581
|
+
if (aheadBehindMatch) {
|
|
582
|
+
message = aheadBehindMatch[0];
|
|
583
|
+
}
|
|
584
|
+
} else if (/remote ref does not exist/i.test(message)) {
|
|
585
|
+
message = 'branch merged, but the remote ref was already removed during cleanup';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
message = compactAutoFinishPathSegments(message)
|
|
590
|
+
.replace(/\s+\|\s+/g, '; ')
|
|
591
|
+
.trim();
|
|
592
|
+
|
|
593
|
+
return `[${status}] ${branch}: ${truncateTail(message, DOCTOR_AUTO_FINISH_MESSAGE_MAX)}`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function printAutoFinishSummary(summary, options = {}) {
|
|
597
|
+
const enabled = Boolean(summary && summary.enabled);
|
|
598
|
+
const details = Array.isArray(summary && summary.details) ? summary.details : [];
|
|
599
|
+
const baseBranch = String(options.baseBranch || summary?.baseBranch || '').trim();
|
|
600
|
+
const verbose = Boolean(options.verbose);
|
|
601
|
+
const detailLimit = Number.isFinite(options.detailLimit)
|
|
602
|
+
? Math.max(0, options.detailLimit)
|
|
603
|
+
: DOCTOR_AUTO_FINISH_DETAIL_LIMIT;
|
|
604
|
+
|
|
605
|
+
if (enabled) {
|
|
606
|
+
console.log(
|
|
607
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
|
|
608
|
+
);
|
|
609
|
+
const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
|
|
610
|
+
for (const detail of visibleDetails) {
|
|
611
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
612
|
+
}
|
|
613
|
+
if (!verbose && details.length > detailLimit) {
|
|
614
|
+
console.log(
|
|
615
|
+
`[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (details.length > 0) {
|
|
622
|
+
console.log(`[${TOOL_NAME}] ${verbose ? details[0] : summarizeAutoFinishDetail(details[0])}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
473
626
|
function gitRun(repoRoot, args, { allowFailure = false } = {}) {
|
|
474
627
|
const result = run('git', ['-C', repoRoot, ...args]);
|
|
475
628
|
if (!allowFailure && result.status !== 0) {
|
|
@@ -509,8 +662,6 @@ const NESTED_REPO_DEFAULT_SKIP_DIRS = new Set([
|
|
|
509
662
|
'.venv',
|
|
510
663
|
'.pnpm-store',
|
|
511
664
|
]);
|
|
512
|
-
const NESTED_REPO_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
|
|
513
|
-
|
|
514
665
|
function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
515
666
|
const maxDepth = Number.isFinite(opts.maxDepth) ? Math.max(1, opts.maxDepth) : NESTED_REPO_DEFAULT_MAX_DEPTH;
|
|
516
667
|
const extraSkip = new Set(Array.isArray(opts.extraSkip) ? opts.extraSkip : []);
|
|
@@ -525,7 +676,7 @@ function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
|
525
676
|
return path.resolve(resolvedRoot, raw);
|
|
526
677
|
})();
|
|
527
678
|
|
|
528
|
-
const
|
|
679
|
+
const worktreeSkipAbsolutes = AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => path.join(resolvedRoot, relativeDir));
|
|
529
680
|
const found = new Set();
|
|
530
681
|
found.add(resolvedRoot);
|
|
531
682
|
|
|
@@ -557,7 +708,7 @@ function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
|
557
708
|
|
|
558
709
|
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
559
710
|
if (shouldSkipDir(entry.name)) continue;
|
|
560
|
-
if (entryPath
|
|
711
|
+
if (worktreeSkipAbsolutes.includes(entryPath)) continue;
|
|
561
712
|
walk(entryPath, depth + 1);
|
|
562
713
|
}
|
|
563
714
|
}
|
|
@@ -1000,6 +1151,14 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
1000
1151
|
options.allowProtectedBaseWrite = true;
|
|
1001
1152
|
continue;
|
|
1002
1153
|
}
|
|
1154
|
+
if (Object.prototype.hasOwnProperty.call(options, 'waitForMerge') && arg === '--wait-for-merge') {
|
|
1155
|
+
options.waitForMerge = true;
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (Object.prototype.hasOwnProperty.call(options, 'waitForMerge') && arg === '--no-wait-for-merge') {
|
|
1159
|
+
options.waitForMerge = false;
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1003
1162
|
|
|
1004
1163
|
throw new Error(`Unknown option: ${arg}`);
|
|
1005
1164
|
}
|
|
@@ -1081,7 +1240,7 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
1081
1240
|
}
|
|
1082
1241
|
|
|
1083
1242
|
function parseDoctorArgs(rawArgs) {
|
|
1084
|
-
|
|
1243
|
+
const doctorDefaults = {
|
|
1085
1244
|
target: process.cwd(),
|
|
1086
1245
|
dropStaleLocks: true,
|
|
1087
1246
|
skipAgents: false,
|
|
@@ -1090,7 +1249,25 @@ function parseDoctorArgs(rawArgs) {
|
|
|
1090
1249
|
dryRun: false,
|
|
1091
1250
|
json: false,
|
|
1092
1251
|
allowProtectedBaseWrite: false,
|
|
1093
|
-
|
|
1252
|
+
waitForMerge: true,
|
|
1253
|
+
verboseAutoFinish: false,
|
|
1254
|
+
};
|
|
1255
|
+
const forwardedArgs = [];
|
|
1256
|
+
|
|
1257
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1258
|
+
const arg = rawArgs[index];
|
|
1259
|
+
if (arg === '--verbose-auto-finish') {
|
|
1260
|
+
doctorDefaults.verboseAutoFinish = true;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (arg === '--compact-auto-finish') {
|
|
1264
|
+
doctorDefaults.verboseAutoFinish = false;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
forwardedArgs.push(arg);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return parseRepoTraversalArgs(forwardedArgs, doctorDefaults);
|
|
1094
1271
|
}
|
|
1095
1272
|
|
|
1096
1273
|
function normalizeWorkspacePath(relativePath) {
|
|
@@ -1102,16 +1279,15 @@ function buildParentWorkspaceView(repoRoot) {
|
|
|
1102
1279
|
const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`;
|
|
1103
1280
|
const workspacePath = path.join(parentDir, workspaceFileName);
|
|
1104
1281
|
const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.');
|
|
1105
|
-
const worktreesRelativePath = normalizeWorkspacePath(
|
|
1106
|
-
path.join(repoRelativePath === '.' ? '' : repoRelativePath, '.omx', 'agent-worktrees'),
|
|
1107
|
-
);
|
|
1108
1282
|
|
|
1109
1283
|
return {
|
|
1110
1284
|
workspacePath,
|
|
1111
1285
|
payload: {
|
|
1112
1286
|
folders: [
|
|
1113
1287
|
{ path: repoRelativePath },
|
|
1114
|
-
|
|
1288
|
+
...AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => ({
|
|
1289
|
+
path: normalizeWorkspacePath(path.join(repoRelativePath === '.' ? '' : repoRelativePath, relativeDir)),
|
|
1290
|
+
})),
|
|
1115
1291
|
],
|
|
1116
1292
|
settings: {
|
|
1117
1293
|
'scm.alwaysShowRepositories': true,
|
|
@@ -1195,6 +1371,40 @@ function assertProtectedMainWriteAllowed(options, commandName) {
|
|
|
1195
1371
|
);
|
|
1196
1372
|
}
|
|
1197
1373
|
|
|
1374
|
+
function runSetupBootstrapInternal(options) {
|
|
1375
|
+
const installPayload = runInstallInternal(options);
|
|
1376
|
+
installPayload.operations.push(
|
|
1377
|
+
ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
let parentWorkspace = null;
|
|
1381
|
+
if (options.parentWorkspaceView) {
|
|
1382
|
+
installPayload.operations.push(
|
|
1383
|
+
ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)),
|
|
1384
|
+
);
|
|
1385
|
+
if (!options.dryRun) {
|
|
1386
|
+
parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const fixPayload = runFixInternal({
|
|
1391
|
+
target: installPayload.repoRoot,
|
|
1392
|
+
dryRun: options.dryRun,
|
|
1393
|
+
force: options.force,
|
|
1394
|
+
dropStaleLocks: true,
|
|
1395
|
+
skipAgents: options.skipAgents,
|
|
1396
|
+
skipPackageJson: options.skipPackageJson,
|
|
1397
|
+
skipGitignore: options.skipGitignore,
|
|
1398
|
+
allowProtectedBaseWrite: options.allowProtectedBaseWrite,
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
return {
|
|
1402
|
+
installPayload,
|
|
1403
|
+
fixPayload,
|
|
1404
|
+
parentWorkspace,
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1198
1408
|
function extractAgentBranchStartMetadata(output) {
|
|
1199
1409
|
const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
|
|
1200
1410
|
const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
|
|
@@ -1208,7 +1418,7 @@ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
|
1208
1418
|
const resolvedTarget = path.resolve(targetPath);
|
|
1209
1419
|
const relativeTarget = path.relative(repoRoot, resolvedTarget);
|
|
1210
1420
|
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
1211
|
-
throw new Error(`
|
|
1421
|
+
throw new Error(`sandbox target must stay inside repo root: ${resolvedTarget}`);
|
|
1212
1422
|
}
|
|
1213
1423
|
if (!relativeTarget || relativeTarget === '.') {
|
|
1214
1424
|
return worktreePath;
|
|
@@ -1216,6 +1426,16 @@ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
|
1216
1426
|
return path.join(worktreePath, relativeTarget);
|
|
1217
1427
|
}
|
|
1218
1428
|
|
|
1429
|
+
function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
1430
|
+
const args = ['setup', '--target', sandboxTarget, '--no-global-install', '--no-recursive'];
|
|
1431
|
+
if (options.force) args.push('--force');
|
|
1432
|
+
if (options.skipAgents) args.push('--skip-agents');
|
|
1433
|
+
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
1434
|
+
if (options.skipGitignore) args.push('--no-gitignore');
|
|
1435
|
+
if (options.dryRun) args.push('--dry-run');
|
|
1436
|
+
return args;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1219
1439
|
function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
1220
1440
|
const args = ['doctor', '--target', sandboxTarget];
|
|
1221
1441
|
if (options.dryRun) args.push('--dry-run');
|
|
@@ -1224,6 +1444,8 @@ function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
|
1224
1444
|
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
1225
1445
|
if (options.skipGitignore) args.push('--no-gitignore');
|
|
1226
1446
|
if (!options.dropStaleLocks) args.push('--keep-stale-locks');
|
|
1447
|
+
args.push(options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
|
|
1448
|
+
if (options.verboseAutoFinish) args.push('--verbose-auto-finish');
|
|
1227
1449
|
if (options.json) args.push('--json');
|
|
1228
1450
|
return args;
|
|
1229
1451
|
}
|
|
@@ -1259,7 +1481,7 @@ function ensureRepoBranch(repoRoot, branch) {
|
|
|
1259
1481
|
return { ok: true, changed: true };
|
|
1260
1482
|
}
|
|
1261
1483
|
|
|
1262
|
-
function
|
|
1484
|
+
function protectedBaseSandboxBranchPrefix() {
|
|
1263
1485
|
const now = new Date();
|
|
1264
1486
|
const stamp = [
|
|
1265
1487
|
now.getUTCFullYear(),
|
|
@@ -1273,15 +1495,15 @@ function doctorSandboxBranchPrefix() {
|
|
|
1273
1495
|
return `agent/gx/${stamp}`;
|
|
1274
1496
|
}
|
|
1275
1497
|
|
|
1276
|
-
function
|
|
1277
|
-
return path.join(repoRoot,
|
|
1498
|
+
function protectedBaseSandboxWorktreePath(repoRoot, branchName) {
|
|
1499
|
+
return path.join(repoRoot, defaultAgentWorktreeRelativeDir(), branchName.replace(/\//g, '__'));
|
|
1278
1500
|
}
|
|
1279
1501
|
|
|
1280
1502
|
function gitRefExists(repoRoot, ref) {
|
|
1281
1503
|
return run('git', ['-C', repoRoot, 'show-ref', '--verify', '--quiet', ref]).status === 0;
|
|
1282
1504
|
}
|
|
1283
1505
|
|
|
1284
|
-
function
|
|
1506
|
+
function resolveProtectedBaseSandboxStartRef(repoRoot, baseBranch) {
|
|
1285
1507
|
run('git', ['-C', repoRoot, 'fetch', 'origin', baseBranch, '--quiet'], { timeout: 20_000 });
|
|
1286
1508
|
if (gitRefExists(repoRoot, `refs/remotes/origin/${baseBranch}`)) {
|
|
1287
1509
|
return `origin/${baseBranch}`;
|
|
@@ -1289,18 +1511,21 @@ function resolveDoctorSandboxStartRef(repoRoot, baseBranch) {
|
|
|
1289
1511
|
if (gitRefExists(repoRoot, `refs/heads/${baseBranch}`)) {
|
|
1290
1512
|
return baseBranch;
|
|
1291
1513
|
}
|
|
1292
|
-
|
|
1514
|
+
if (currentBranchName(repoRoot) === baseBranch) {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
throw new Error(`Unable to find base ref for sandbox bootstrap: ${baseBranch}`);
|
|
1293
1518
|
}
|
|
1294
1519
|
|
|
1295
|
-
function
|
|
1296
|
-
const branchPrefix =
|
|
1520
|
+
function startProtectedBaseSandboxFallback(blocked, sandboxSuffix) {
|
|
1521
|
+
const branchPrefix = protectedBaseSandboxBranchPrefix();
|
|
1297
1522
|
let selectedBranch = '';
|
|
1298
1523
|
let selectedWorktreePath = '';
|
|
1299
1524
|
|
|
1300
1525
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
1301
|
-
const suffix = attempt === 0 ?
|
|
1526
|
+
const suffix = attempt === 0 ? sandboxSuffix : `${attempt + 1}-${sandboxSuffix}`;
|
|
1302
1527
|
const candidateBranch = `${branchPrefix}-${suffix}`;
|
|
1303
|
-
const candidateWorktreePath =
|
|
1528
|
+
const candidateWorktreePath = protectedBaseSandboxWorktreePath(blocked.repoRoot, candidateBranch);
|
|
1304
1529
|
if (gitRefExists(blocked.repoRoot, `refs/heads/${candidateBranch}`)) {
|
|
1305
1530
|
continue;
|
|
1306
1531
|
}
|
|
@@ -1313,20 +1538,36 @@ function startDoctorSandboxFallback(blocked) {
|
|
|
1313
1538
|
}
|
|
1314
1539
|
|
|
1315
1540
|
if (!selectedBranch || !selectedWorktreePath) {
|
|
1316
|
-
throw new Error('Unable to allocate unique sandbox branch/worktree
|
|
1541
|
+
throw new Error('Unable to allocate unique sandbox branch/worktree');
|
|
1317
1542
|
}
|
|
1318
1543
|
|
|
1319
1544
|
fs.mkdirSync(path.dirname(selectedWorktreePath), { recursive: true });
|
|
1320
|
-
const startRef =
|
|
1321
|
-
const
|
|
1322
|
-
'
|
|
1323
|
-
['-C', blocked.repoRoot, 'worktree', 'add', '
|
|
1324
|
-
);
|
|
1545
|
+
const startRef = resolveProtectedBaseSandboxStartRef(blocked.repoRoot, blocked.branch);
|
|
1546
|
+
const addArgs = startRef
|
|
1547
|
+
? ['-C', blocked.repoRoot, 'worktree', 'add', '-b', selectedBranch, selectedWorktreePath, startRef]
|
|
1548
|
+
: ['-C', blocked.repoRoot, 'worktree', 'add', '--orphan', selectedWorktreePath];
|
|
1549
|
+
const addResult = run('git', addArgs);
|
|
1325
1550
|
if (isSpawnFailure(addResult)) {
|
|
1326
1551
|
throw addResult.error;
|
|
1327
1552
|
}
|
|
1328
1553
|
if (addResult.status !== 0) {
|
|
1329
|
-
throw new Error((addResult.stderr || addResult.stdout || 'failed to create
|
|
1554
|
+
throw new Error((addResult.stderr || addResult.stdout || 'failed to create sandbox').trim());
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (!startRef) {
|
|
1558
|
+
const renameResult = run(
|
|
1559
|
+
'git',
|
|
1560
|
+
['-C', selectedWorktreePath, 'branch', '-m', selectedBranch],
|
|
1561
|
+
{ timeout: 20_000 },
|
|
1562
|
+
);
|
|
1563
|
+
if (isSpawnFailure(renameResult)) {
|
|
1564
|
+
throw renameResult.error;
|
|
1565
|
+
}
|
|
1566
|
+
if (renameResult.status !== 0) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
(renameResult.stderr || renameResult.stdout || 'failed to name orphan sandbox branch').trim(),
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1330
1571
|
}
|
|
1331
1572
|
|
|
1332
1573
|
return {
|
|
@@ -1341,16 +1582,20 @@ function startDoctorSandboxFallback(blocked) {
|
|
|
1341
1582
|
};
|
|
1342
1583
|
}
|
|
1343
1584
|
|
|
1344
|
-
function
|
|
1585
|
+
function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
1586
|
+
if (sandboxSuffix === 'gx-doctor') {
|
|
1587
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1345
1590
|
const startScript = path.join(blocked.repoRoot, 'scripts', 'agent-branch-start.sh');
|
|
1346
1591
|
if (!fs.existsSync(startScript)) {
|
|
1347
|
-
return
|
|
1592
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1348
1593
|
}
|
|
1349
1594
|
|
|
1350
1595
|
const startResult = run('bash', [
|
|
1351
1596
|
startScript,
|
|
1352
1597
|
'--task',
|
|
1353
|
-
|
|
1598
|
+
taskName,
|
|
1354
1599
|
'--agent',
|
|
1355
1600
|
SHORT_TOOL_NAME,
|
|
1356
1601
|
'--base',
|
|
@@ -1360,7 +1605,7 @@ function startDoctorSandbox(blocked) {
|
|
|
1360
1605
|
throw startResult.error;
|
|
1361
1606
|
}
|
|
1362
1607
|
if (startResult.status !== 0) {
|
|
1363
|
-
return
|
|
1608
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1364
1609
|
}
|
|
1365
1610
|
|
|
1366
1611
|
const metadata = extractAgentBranchStartMetadata(startResult.stdout);
|
|
@@ -1375,11 +1620,11 @@ function startDoctorSandbox(blocked) {
|
|
|
1375
1620
|
if (!restoreResult.ok) {
|
|
1376
1621
|
const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
|
|
1377
1622
|
throw new Error(
|
|
1378
|
-
`
|
|
1623
|
+
`sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
|
|
1379
1624
|
(detail ? `\n${detail}` : ''),
|
|
1380
1625
|
);
|
|
1381
1626
|
}
|
|
1382
|
-
return
|
|
1627
|
+
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1383
1628
|
}
|
|
1384
1629
|
|
|
1385
1630
|
return {
|
|
@@ -1389,6 +1634,59 @@ function startDoctorSandbox(blocked) {
|
|
|
1389
1634
|
};
|
|
1390
1635
|
}
|
|
1391
1636
|
|
|
1637
|
+
function cleanupProtectedBaseSandbox(repoRoot, metadata) {
|
|
1638
|
+
const result = {
|
|
1639
|
+
worktree: 'skipped',
|
|
1640
|
+
branch: 'skipped',
|
|
1641
|
+
note: 'missing sandbox metadata',
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
if (!metadata?.worktreePath || !metadata?.branch) {
|
|
1645
|
+
return result;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (fs.existsSync(metadata.worktreePath)) {
|
|
1649
|
+
const removeResult = run(
|
|
1650
|
+
'git',
|
|
1651
|
+
['-C', repoRoot, 'worktree', 'remove', '--force', metadata.worktreePath],
|
|
1652
|
+
{ timeout: 30_000 },
|
|
1653
|
+
);
|
|
1654
|
+
if (isSpawnFailure(removeResult)) {
|
|
1655
|
+
throw removeResult.error;
|
|
1656
|
+
}
|
|
1657
|
+
if (removeResult.status !== 0) {
|
|
1658
|
+
throw new Error(
|
|
1659
|
+
(removeResult.stderr || removeResult.stdout || 'failed to remove sandbox worktree').trim(),
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
result.worktree = 'removed';
|
|
1663
|
+
} else {
|
|
1664
|
+
result.worktree = 'missing';
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (gitRefExists(repoRoot, `refs/heads/${metadata.branch}`)) {
|
|
1668
|
+
const branchDeleteResult = run(
|
|
1669
|
+
'git',
|
|
1670
|
+
['-C', repoRoot, 'branch', '-D', metadata.branch],
|
|
1671
|
+
{ timeout: 20_000 },
|
|
1672
|
+
);
|
|
1673
|
+
if (isSpawnFailure(branchDeleteResult)) {
|
|
1674
|
+
throw branchDeleteResult.error;
|
|
1675
|
+
}
|
|
1676
|
+
if (branchDeleteResult.status !== 0) {
|
|
1677
|
+
throw new Error(
|
|
1678
|
+
(branchDeleteResult.stderr || branchDeleteResult.stdout || 'failed to delete sandbox branch').trim(),
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
result.branch = 'deleted';
|
|
1682
|
+
} else {
|
|
1683
|
+
result.branch = 'missing';
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
result.note = 'sandbox worktree pruned';
|
|
1687
|
+
return result;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1392
1690
|
function parseGitPathList(output) {
|
|
1393
1691
|
return String(output || '')
|
|
1394
1692
|
.split('\n')
|
|
@@ -1427,6 +1725,59 @@ function collectDoctorDeletedPaths(worktreePath) {
|
|
|
1427
1725
|
return Array.from(deleted);
|
|
1428
1726
|
}
|
|
1429
1727
|
|
|
1728
|
+
function collectWorktreeDirtyPaths(worktreePath) {
|
|
1729
|
+
const dirty = new Set();
|
|
1730
|
+
const commands = [
|
|
1731
|
+
['diff', '--name-only'],
|
|
1732
|
+
['diff', '--cached', '--name-only'],
|
|
1733
|
+
['ls-files', '--others', '--exclude-standard'],
|
|
1734
|
+
];
|
|
1735
|
+
for (const gitArgs of commands) {
|
|
1736
|
+
const result = run('git', ['-C', worktreePath, ...gitArgs], { timeout: 20_000 });
|
|
1737
|
+
for (const filePath of parseGitPathList(result.stdout)) {
|
|
1738
|
+
dirty.add(filePath);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return Array.from(dirty);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function collectDoctorForceAddPaths(worktreePath) {
|
|
1745
|
+
return TEMPLATE_FILES
|
|
1746
|
+
.map((entry) => toDestinationPath(entry))
|
|
1747
|
+
.filter((relativePath) => relativePath.startsWith('scripts/') || relativePath.startsWith('.githooks/'))
|
|
1748
|
+
.filter((relativePath) => fs.existsSync(path.join(worktreePath, relativePath)));
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function stripDoctorSandboxLocks(rawContent, branchName) {
|
|
1752
|
+
if (!rawContent || !branchName) {
|
|
1753
|
+
return rawContent;
|
|
1754
|
+
}
|
|
1755
|
+
try {
|
|
1756
|
+
const parsed = JSON.parse(rawContent);
|
|
1757
|
+
const locks = parsed && typeof parsed === 'object' && parsed.locks && typeof parsed.locks === 'object'
|
|
1758
|
+
? parsed.locks
|
|
1759
|
+
: null;
|
|
1760
|
+
if (!locks) {
|
|
1761
|
+
return rawContent;
|
|
1762
|
+
}
|
|
1763
|
+
let changed = false;
|
|
1764
|
+
const filteredLocks = {};
|
|
1765
|
+
for (const [filePath, lockInfo] of Object.entries(locks)) {
|
|
1766
|
+
if (lockInfo && lockInfo.branch === branchName) {
|
|
1767
|
+
changed = true;
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
filteredLocks[filePath] = lockInfo;
|
|
1771
|
+
}
|
|
1772
|
+
if (!changed) {
|
|
1773
|
+
return rawContent;
|
|
1774
|
+
}
|
|
1775
|
+
return `${JSON.stringify({ ...parsed, locks: filteredLocks }, null, 2)}\n`;
|
|
1776
|
+
} catch {
|
|
1777
|
+
return rawContent;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1430
1781
|
function claimDoctorChangedLocks(metadata) {
|
|
1431
1782
|
const lockScript = path.join(metadata.worktreePath, 'scripts', 'agent-file-locks.py');
|
|
1432
1783
|
if (!fs.existsSync(lockScript) || !metadata.branch) {
|
|
@@ -1438,7 +1789,10 @@ function claimDoctorChangedLocks(metadata) {
|
|
|
1438
1789
|
};
|
|
1439
1790
|
}
|
|
1440
1791
|
|
|
1441
|
-
const changedPaths =
|
|
1792
|
+
const changedPaths = Array.from(new Set([
|
|
1793
|
+
...collectDoctorChangedPaths(metadata.worktreePath),
|
|
1794
|
+
...collectDoctorForceAddPaths(metadata.worktreePath),
|
|
1795
|
+
]));
|
|
1442
1796
|
const deletedPaths = collectDoctorDeletedPaths(metadata.worktreePath);
|
|
1443
1797
|
if (changedPaths.length > 0) {
|
|
1444
1798
|
run('python3', [lockScript, 'claim', '--branch', metadata.branch, ...changedPaths], {
|
|
@@ -1470,7 +1824,19 @@ function autoCommitDoctorSandboxChanges(metadata) {
|
|
|
1470
1824
|
}
|
|
1471
1825
|
|
|
1472
1826
|
claimDoctorChangedLocks(metadata);
|
|
1473
|
-
run(
|
|
1827
|
+
run(
|
|
1828
|
+
'git',
|
|
1829
|
+
['-C', metadata.worktreePath, 'add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`],
|
|
1830
|
+
{ timeout: 20_000 },
|
|
1831
|
+
);
|
|
1832
|
+
const forceAddPaths = collectDoctorForceAddPaths(metadata.worktreePath);
|
|
1833
|
+
if (forceAddPaths.length > 0) {
|
|
1834
|
+
run(
|
|
1835
|
+
'git',
|
|
1836
|
+
['-C', metadata.worktreePath, 'add', '-f', '--', ...forceAddPaths],
|
|
1837
|
+
{ timeout: 20_000 },
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1474
1840
|
const staged = run(
|
|
1475
1841
|
'git',
|
|
1476
1842
|
['-C', metadata.worktreePath, 'diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`],
|
|
@@ -1535,7 +1901,7 @@ function doctorFinishFlowIsPending(output) {
|
|
|
1535
1901
|
);
|
|
1536
1902
|
}
|
|
1537
1903
|
|
|
1538
|
-
function finishDoctorSandboxBranch(blocked, metadata) {
|
|
1904
|
+
function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
|
|
1539
1905
|
const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
|
|
1540
1906
|
if (!fs.existsSync(finishScript)) {
|
|
1541
1907
|
return {
|
|
@@ -1577,10 +1943,11 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1577
1943
|
const waitTimeoutSeconds =
|
|
1578
1944
|
Number.isFinite(rawWaitTimeoutSeconds) && rawWaitTimeoutSeconds >= 30 ? rawWaitTimeoutSeconds : 1800;
|
|
1579
1945
|
const finishTimeoutMs = Math.max(180_000, (waitTimeoutSeconds + 60) * 1000);
|
|
1946
|
+
const waitForMergeArg = options.waitForMerge === false ? '--no-wait-for-merge' : '--wait-for-merge';
|
|
1580
1947
|
|
|
1581
1948
|
const finishResult = run(
|
|
1582
1949
|
'bash',
|
|
1583
|
-
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr',
|
|
1950
|
+
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg],
|
|
1584
1951
|
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
|
|
1585
1952
|
);
|
|
1586
1953
|
if (isSpawnFailure(finishResult)) {
|
|
@@ -1619,35 +1986,186 @@ function finishDoctorSandboxBranch(blocked, metadata) {
|
|
|
1619
1986
|
};
|
|
1620
1987
|
}
|
|
1621
1988
|
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1989
|
+
function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata, autoCommitResult, finishResult) {
|
|
1990
|
+
if (options.dryRun) {
|
|
1991
|
+
return {
|
|
1992
|
+
status: autoCommitResult.status === 'committed' ? 'would-merge' : 'skipped',
|
|
1993
|
+
note: autoCommitResult.status === 'committed'
|
|
1994
|
+
? 'dry run: would fast-forward tracked doctor repairs into the protected base workspace'
|
|
1995
|
+
: 'dry run skips tracked repair merge',
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (autoCommitResult.status !== 'committed') {
|
|
2000
|
+
return {
|
|
2001
|
+
status: autoCommitResult.status === 'no-changes' ? 'unchanged' : 'skipped',
|
|
2002
|
+
note: autoCommitResult.status === 'no-changes'
|
|
2003
|
+
? 'no tracked doctor repairs needed in the protected base workspace'
|
|
2004
|
+
: 'tracked doctor repair merge skipped',
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
if (finishResult.status !== 'skipped') {
|
|
2009
|
+
return {
|
|
2010
|
+
status: 'skipped',
|
|
2011
|
+
note: finishResult.status === 'failed'
|
|
2012
|
+
? 'tracked doctor repairs remain in the sandbox after finish failure'
|
|
2013
|
+
: 'tracked doctor repairs are being delivered through the sandbox finish flow',
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const allowedPaths = new Set([
|
|
2018
|
+
...(autoCommitResult.stagedFiles || []),
|
|
2019
|
+
...OMX_SCAFFOLD_DIRECTORIES,
|
|
2020
|
+
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
2021
|
+
...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
|
|
2022
|
+
'bin',
|
|
2023
|
+
'package.json',
|
|
2024
|
+
'.gitignore',
|
|
2025
|
+
'AGENTS.md',
|
|
2026
|
+
]);
|
|
2027
|
+
const dirtyPaths = collectWorktreeDirtyPaths(blocked.repoRoot);
|
|
2028
|
+
let stashRef = '';
|
|
2029
|
+
if (dirtyPaths.length > 0) {
|
|
2030
|
+
const unexpectedPaths = dirtyPaths.filter((filePath) => {
|
|
2031
|
+
if (allowedPaths.has(filePath)) {
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
return !AGENT_WORKTREE_RELATIVE_DIRS.some(
|
|
2035
|
+
(relativeDir) => filePath === relativeDir || filePath.startsWith(`${relativeDir}/`),
|
|
2036
|
+
);
|
|
2037
|
+
});
|
|
2038
|
+
if (unexpectedPaths.length > 0) {
|
|
2039
|
+
return {
|
|
2040
|
+
status: 'failed',
|
|
2041
|
+
note: `protected branch workspace has unrelated local changes: ${unexpectedPaths.join(', ')}`,
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
const stashMessage = `guardex-doctor-merge-${Date.now()}`;
|
|
2045
|
+
const stashResult = run(
|
|
2046
|
+
'git',
|
|
2047
|
+
['-C', blocked.repoRoot, 'stash', 'push', '--all', '--message', stashMessage],
|
|
2048
|
+
{ timeout: 30_000 },
|
|
2049
|
+
);
|
|
2050
|
+
if (isSpawnFailure(stashResult)) {
|
|
2051
|
+
return {
|
|
2052
|
+
status: 'failed',
|
|
2053
|
+
note: 'could not stash protected branch doctor drift before merge',
|
|
2054
|
+
stdout: stashResult.stdout || '',
|
|
2055
|
+
stderr: stashResult.stderr || '',
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
if (stashResult.status !== 0) {
|
|
2059
|
+
return {
|
|
2060
|
+
status: 'failed',
|
|
2061
|
+
note: 'stashing protected branch doctor drift failed',
|
|
2062
|
+
stdout: stashResult.stdout || '',
|
|
2063
|
+
stderr: stashResult.stderr || '',
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const stashLookup = run(
|
|
2068
|
+
'git',
|
|
2069
|
+
['-C', blocked.repoRoot, 'stash', 'list'],
|
|
2070
|
+
{ timeout: 20_000 },
|
|
2071
|
+
);
|
|
2072
|
+
stashRef = String(stashLookup.stdout || '')
|
|
2073
|
+
.split('\n')
|
|
2074
|
+
.find((line) => line.includes(stashMessage))
|
|
2075
|
+
?.split(':')[0]
|
|
2076
|
+
?.trim() || '';
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
|
|
2080
|
+
if (!restoreResult.ok) {
|
|
2081
|
+
if (stashRef) {
|
|
2082
|
+
run('git', ['-C', blocked.repoRoot, 'stash', 'apply', stashRef], { timeout: 30_000 });
|
|
2083
|
+
}
|
|
2084
|
+
return {
|
|
2085
|
+
status: 'failed',
|
|
2086
|
+
note: `could not restore protected branch '${blocked.branch}' before applying sandbox repairs`,
|
|
2087
|
+
stdout: restoreResult.stdout || '',
|
|
2088
|
+
stderr: restoreResult.stderr || '',
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const mergeResult = run(
|
|
2093
|
+
'git',
|
|
2094
|
+
['-C', blocked.repoRoot, 'merge', '--ff-only', metadata.branch],
|
|
2095
|
+
{ timeout: 30_000 },
|
|
1630
2096
|
);
|
|
1631
|
-
|
|
1632
|
-
|
|
2097
|
+
if (isSpawnFailure(mergeResult)) {
|
|
2098
|
+
if (stashRef) {
|
|
2099
|
+
run('git', ['-C', blocked.repoRoot, 'stash', 'apply', stashRef], { timeout: 30_000 });
|
|
2100
|
+
}
|
|
2101
|
+
return {
|
|
2102
|
+
status: 'failed',
|
|
2103
|
+
note: 'tracked doctor repair merge errored',
|
|
2104
|
+
stdout: mergeResult.stdout || '',
|
|
2105
|
+
stderr: mergeResult.stderr || '',
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
if (mergeResult.status !== 0) {
|
|
2109
|
+
if (stashRef) {
|
|
2110
|
+
run('git', ['-C', blocked.repoRoot, 'stash', 'apply', stashRef], { timeout: 30_000 });
|
|
2111
|
+
}
|
|
2112
|
+
return {
|
|
2113
|
+
status: 'failed',
|
|
2114
|
+
note: 'tracked doctor repair merge failed',
|
|
2115
|
+
stdout: mergeResult.stdout || '',
|
|
2116
|
+
stderr: mergeResult.stderr || '',
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
let cleanupResult;
|
|
2121
|
+
try {
|
|
2122
|
+
cleanupResult = cleanupProtectedBaseSandbox(blocked.repoRoot, metadata);
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
return {
|
|
2125
|
+
status: 'failed',
|
|
2126
|
+
note: `tracked doctor repair merge succeeded but sandbox cleanup failed: ${error.message}`,
|
|
2127
|
+
stdout: mergeResult.stdout || '',
|
|
2128
|
+
stderr: mergeResult.stderr || '',
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
1633
2131
|
|
|
1634
|
-
|
|
2132
|
+
let hookRefreshResult;
|
|
2133
|
+
try {
|
|
2134
|
+
hookRefreshResult = configureHooks(blocked.repoRoot, false);
|
|
2135
|
+
} catch (error) {
|
|
1635
2136
|
return {
|
|
1636
|
-
status: '
|
|
1637
|
-
note:
|
|
1638
|
-
|
|
2137
|
+
status: 'failed',
|
|
2138
|
+
note: `tracked doctor repair merge succeeded but local hook refresh failed: ${error.message}`,
|
|
2139
|
+
stdout: mergeResult.stdout || '',
|
|
2140
|
+
stderr: mergeResult.stderr || '',
|
|
1639
2141
|
};
|
|
1640
2142
|
}
|
|
1641
2143
|
|
|
2144
|
+
if (stashRef) {
|
|
2145
|
+
run('git', ['-C', blocked.repoRoot, 'stash', 'drop', stashRef], { timeout: 20_000 });
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1642
2148
|
return {
|
|
1643
|
-
status:
|
|
1644
|
-
note:
|
|
1645
|
-
|
|
2149
|
+
status: 'merged',
|
|
2150
|
+
note: 'fast-forwarded tracked doctor repairs into the protected base workspace',
|
|
2151
|
+
stdout: mergeResult.stdout || '',
|
|
2152
|
+
stderr: mergeResult.stderr || '',
|
|
2153
|
+
cleanup: cleanupResult,
|
|
2154
|
+
hookRefresh: hookRefreshResult,
|
|
1646
2155
|
};
|
|
1647
2156
|
}
|
|
1648
2157
|
|
|
2158
|
+
function syncDoctorLocalSupportFiles(repoRoot, dryRun) {
|
|
2159
|
+
return TEMPLATE_FILES
|
|
2160
|
+
.filter((entry) => entry.startsWith('codex/') || entry.startsWith('claude/'))
|
|
2161
|
+
.map((entry) => ensureTemplateFilePresent(repoRoot, entry, dryRun));
|
|
2162
|
+
}
|
|
2163
|
+
|
|
1649
2164
|
function runDoctorInSandbox(options, blocked) {
|
|
1650
|
-
const startResult =
|
|
2165
|
+
const startResult = startProtectedBaseSandbox(blocked, {
|
|
2166
|
+
taskName: `${SHORT_TOOL_NAME}-doctor`,
|
|
2167
|
+
sandboxSuffix: 'gx-doctor',
|
|
2168
|
+
});
|
|
1651
2169
|
const metadata = startResult.metadata;
|
|
1652
2170
|
|
|
1653
2171
|
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
@@ -1677,6 +2195,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1677
2195
|
status: 'skipped',
|
|
1678
2196
|
note: 'sandbox doctor did not complete successfully',
|
|
1679
2197
|
};
|
|
2198
|
+
let sandboxLockContent = null;
|
|
1680
2199
|
let postSandboxAutoFinishSummary = {
|
|
1681
2200
|
enabled: false,
|
|
1682
2201
|
attempted: 0,
|
|
@@ -1690,7 +2209,6 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1690
2209
|
note: 'sandbox doctor did not complete successfully',
|
|
1691
2210
|
};
|
|
1692
2211
|
if (nestedResult.status === 0) {
|
|
1693
|
-
protectedBaseRepairSyncResult = syncProtectedBaseDoctorRepairs(options, blocked);
|
|
1694
2212
|
const omxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
|
|
1695
2213
|
const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
1696
2214
|
if (changedOmxPaths.length === 0) {
|
|
@@ -1710,7 +2228,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1710
2228
|
if (!options.dryRun) {
|
|
1711
2229
|
autoCommitResult = autoCommitDoctorSandboxChanges(metadata);
|
|
1712
2230
|
if (autoCommitResult.status === 'committed') {
|
|
1713
|
-
finishResult = finishDoctorSandboxBranch(blocked, metadata);
|
|
2231
|
+
finishResult = finishDoctorSandboxBranch(blocked, metadata, options);
|
|
1714
2232
|
} else if (autoCommitResult.status === 'no-changes') {
|
|
1715
2233
|
finishResult = {
|
|
1716
2234
|
status: 'skipped',
|
|
@@ -1746,7 +2264,11 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1746
2264
|
note: `${LOCK_FILE_RELATIVE} missing in sandbox worktree`,
|
|
1747
2265
|
};
|
|
1748
2266
|
} else {
|
|
1749
|
-
const sourceContent =
|
|
2267
|
+
const sourceContent = stripDoctorSandboxLocks(
|
|
2268
|
+
fs.readFileSync(sandboxLockPath, 'utf8'),
|
|
2269
|
+
metadata.branch,
|
|
2270
|
+
);
|
|
2271
|
+
sandboxLockContent = sourceContent;
|
|
1750
2272
|
const destinationContent = fs.readFileSync(baseLockPath, 'utf8');
|
|
1751
2273
|
if (sourceContent === destinationContent) {
|
|
1752
2274
|
lockSyncResult = {
|
|
@@ -1763,9 +2285,66 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1763
2285
|
}
|
|
1764
2286
|
}
|
|
1765
2287
|
|
|
2288
|
+
protectedBaseRepairSyncResult = mergeDoctorSandboxRepairsBackToProtectedBase(
|
|
2289
|
+
options,
|
|
2290
|
+
blocked,
|
|
2291
|
+
metadata,
|
|
2292
|
+
autoCommitResult,
|
|
2293
|
+
finishResult,
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
syncDoctorLocalSupportFiles(blocked.repoRoot, Boolean(options.dryRun));
|
|
2297
|
+
|
|
2298
|
+
const postMergeOmxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
|
|
2299
|
+
const postMergeChangedOmxPaths = postMergeOmxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
|
|
2300
|
+
if (postMergeChangedOmxPaths.length === 0) {
|
|
2301
|
+
omxScaffoldSyncResult = {
|
|
2302
|
+
status: 'unchanged',
|
|
2303
|
+
note: '.omx scaffold already in sync',
|
|
2304
|
+
operations: postMergeOmxScaffoldOps,
|
|
2305
|
+
};
|
|
2306
|
+
} else {
|
|
2307
|
+
omxScaffoldSyncResult = {
|
|
2308
|
+
status: options.dryRun ? 'would-sync' : 'synced',
|
|
2309
|
+
note: `${options.dryRun ? 'would sync' : 'synced'} ${postMergeChangedOmxPaths.length} .omx path(s)`,
|
|
2310
|
+
operations: postMergeOmxScaffoldOps,
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const postMergeBaseLockPath = path.join(blocked.repoRoot, LOCK_FILE_RELATIVE);
|
|
2315
|
+
if (sandboxLockContent === null) {
|
|
2316
|
+
lockSyncResult = {
|
|
2317
|
+
status: 'skipped',
|
|
2318
|
+
note: `${LOCK_FILE_RELATIVE} missing in sandbox worktree`,
|
|
2319
|
+
};
|
|
2320
|
+
} else if (!fs.existsSync(postMergeBaseLockPath)) {
|
|
2321
|
+
fs.mkdirSync(path.dirname(postMergeBaseLockPath), { recursive: true });
|
|
2322
|
+
fs.writeFileSync(postMergeBaseLockPath, sandboxLockContent, 'utf8');
|
|
2323
|
+
lockSyncResult = {
|
|
2324
|
+
status: 'synced',
|
|
2325
|
+
note: `${LOCK_FILE_RELATIVE} recreated from sandbox`,
|
|
2326
|
+
};
|
|
2327
|
+
} else {
|
|
2328
|
+
const destinationContent = fs.readFileSync(postMergeBaseLockPath, 'utf8');
|
|
2329
|
+
if (sandboxLockContent === destinationContent) {
|
|
2330
|
+
lockSyncResult = {
|
|
2331
|
+
status: 'unchanged',
|
|
2332
|
+
note: `${LOCK_FILE_RELATIVE} already in sync`,
|
|
2333
|
+
};
|
|
2334
|
+
} else {
|
|
2335
|
+
fs.mkdirSync(path.dirname(postMergeBaseLockPath), { recursive: true });
|
|
2336
|
+
fs.writeFileSync(postMergeBaseLockPath, sandboxLockContent, 'utf8');
|
|
2337
|
+
lockSyncResult = {
|
|
2338
|
+
status: 'synced',
|
|
2339
|
+
note: `${LOCK_FILE_RELATIVE} synced from sandbox`,
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
1766
2344
|
postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
|
|
1767
2345
|
baseBranch: blocked.branch,
|
|
1768
2346
|
dryRun: options.dryRun,
|
|
2347
|
+
waitForMerge: options.waitForMerge,
|
|
1769
2348
|
excludeBranches: [metadata.branch],
|
|
1770
2349
|
});
|
|
1771
2350
|
}
|
|
@@ -1820,14 +2399,28 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1820
2399
|
console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${autoCommitResult.note}.`);
|
|
1821
2400
|
}
|
|
1822
2401
|
|
|
1823
|
-
if (protectedBaseRepairSyncResult.status === '
|
|
1824
|
-
console.log(`[${TOOL_NAME}]
|
|
2402
|
+
if (protectedBaseRepairSyncResult.status === 'merged') {
|
|
2403
|
+
console.log(`[${TOOL_NAME}] Fast-forwarded tracked doctor repairs into the protected branch workspace.`);
|
|
1825
2404
|
} else if (protectedBaseRepairSyncResult.status === 'unchanged') {
|
|
1826
|
-
console.log(`[${TOOL_NAME}] Protected branch workspace already had the
|
|
1827
|
-
} else if (protectedBaseRepairSyncResult.status === 'would-
|
|
1828
|
-
console.log(`[${TOOL_NAME}] Dry run: would
|
|
2405
|
+
console.log(`[${TOOL_NAME}] Protected branch workspace already had the tracked doctor repairs.`);
|
|
2406
|
+
} else if (protectedBaseRepairSyncResult.status === 'would-merge') {
|
|
2407
|
+
console.log(`[${TOOL_NAME}] Dry run: would fast-forward tracked doctor repairs into the protected branch workspace.`);
|
|
2408
|
+
} else if (protectedBaseRepairSyncResult.status === 'failed') {
|
|
2409
|
+
console.log(`[${TOOL_NAME}] Protected branch tracked repair merge failed: ${protectedBaseRepairSyncResult.note}.`);
|
|
2410
|
+
if (protectedBaseRepairSyncResult.stdout) process.stdout.write(protectedBaseRepairSyncResult.stdout);
|
|
2411
|
+
if (protectedBaseRepairSyncResult.stderr) process.stderr.write(protectedBaseRepairSyncResult.stderr);
|
|
2412
|
+
} else {
|
|
2413
|
+
console.log(`[${TOOL_NAME}] Protected branch tracked repair merge skipped: ${protectedBaseRepairSyncResult.note}.`);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
if (lockSyncResult.status === 'synced') {
|
|
2417
|
+
console.log(
|
|
2418
|
+
`[${TOOL_NAME}] Synced repaired lock registry back to protected branch workspace (${LOCK_FILE_RELATIVE}).`,
|
|
2419
|
+
);
|
|
2420
|
+
} else if (lockSyncResult.status === 'unchanged') {
|
|
2421
|
+
console.log(`[${TOOL_NAME}] Lock registry already synced in protected branch workspace.`);
|
|
1829
2422
|
} else {
|
|
1830
|
-
console.log(`[${TOOL_NAME}]
|
|
2423
|
+
console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
|
|
1831
2424
|
}
|
|
1832
2425
|
|
|
1833
2426
|
if (finishResult.status === 'completed') {
|
|
@@ -1845,32 +2438,17 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1845
2438
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1846
2439
|
} else if (finishResult.status === 'failed') {
|
|
1847
2440
|
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
2441
|
+
console.log(`[guardex] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
1848
2442
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
1849
2443
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
1850
2444
|
} else {
|
|
1851
2445
|
console.log(`[${TOOL_NAME}] Auto-finish skipped: ${finishResult.note}.`);
|
|
1852
2446
|
}
|
|
1853
2447
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
} else if (lockSyncResult.status === 'unchanged') {
|
|
1859
|
-
console.log(`[${TOOL_NAME}] Lock registry already synced in protected branch workspace.`);
|
|
1860
|
-
} else {
|
|
1861
|
-
console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
if (postSandboxAutoFinishSummary.enabled) {
|
|
1865
|
-
console.log(
|
|
1866
|
-
`[${TOOL_NAME}] Auto-finish sweep (base=${blocked.branch}): attempted=${postSandboxAutoFinishSummary.attempted}, completed=${postSandboxAutoFinishSummary.completed}, skipped=${postSandboxAutoFinishSummary.skipped}, failed=${postSandboxAutoFinishSummary.failed}`,
|
|
1867
|
-
);
|
|
1868
|
-
for (const detail of postSandboxAutoFinishSummary.details) {
|
|
1869
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
1870
|
-
}
|
|
1871
|
-
} else if (postSandboxAutoFinishSummary.details.length > 0) {
|
|
1872
|
-
console.log(`[${TOOL_NAME}] ${postSandboxAutoFinishSummary.details[0]}`);
|
|
1873
|
-
}
|
|
2448
|
+
printAutoFinishSummary(postSandboxAutoFinishSummary, {
|
|
2449
|
+
baseBranch: blocked.branch,
|
|
2450
|
+
verbose: options.verboseAutoFinish,
|
|
2451
|
+
});
|
|
1874
2452
|
if (omxScaffoldSyncResult.status === 'synced') {
|
|
1875
2453
|
console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
|
|
1876
2454
|
} else if (omxScaffoldSyncResult.status === 'unchanged') {
|
|
@@ -1883,22 +2461,99 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
1883
2461
|
}
|
|
1884
2462
|
}
|
|
1885
2463
|
|
|
1886
|
-
if (typeof nestedResult.status === 'number') {
|
|
1887
|
-
let exitCode = nestedResult.status;
|
|
1888
|
-
if (exitCode === 0 && autoCommitResult.status === 'failed') {
|
|
1889
|
-
exitCode = 1;
|
|
1890
|
-
}
|
|
1891
|
-
if (
|
|
1892
|
-
exitCode === 0 &&
|
|
1893
|
-
autoCommitResult.status === 'committed' &&
|
|
1894
|
-
(finishResult.status === 'failed' || finishResult.status === 'pending')
|
|
1895
|
-
) {
|
|
1896
|
-
exitCode = 1;
|
|
2464
|
+
if (typeof nestedResult.status === 'number') {
|
|
2465
|
+
let exitCode = nestedResult.status;
|
|
2466
|
+
if (exitCode === 0 && autoCommitResult.status === 'failed') {
|
|
2467
|
+
exitCode = 1;
|
|
2468
|
+
}
|
|
2469
|
+
if (
|
|
2470
|
+
exitCode === 0 &&
|
|
2471
|
+
autoCommitResult.status === 'committed' &&
|
|
2472
|
+
(finishResult.status === 'failed' || finishResult.status === 'pending')
|
|
2473
|
+
) {
|
|
2474
|
+
exitCode = 1;
|
|
2475
|
+
}
|
|
2476
|
+
if (exitCode === 0 && protectedBaseRepairSyncResult.status === 'failed') {
|
|
2477
|
+
exitCode = 1;
|
|
2478
|
+
}
|
|
2479
|
+
process.exitCode = exitCode;
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
process.exitCode = 1;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function runSetupInSandbox(options, blocked, repoLabel = '') {
|
|
2486
|
+
const startResult = startProtectedBaseSandbox(blocked, {
|
|
2487
|
+
taskName: `${SHORT_TOOL_NAME}-setup`,
|
|
2488
|
+
sandboxSuffix: 'gx-setup',
|
|
2489
|
+
});
|
|
2490
|
+
const metadata = startResult.metadata;
|
|
2491
|
+
|
|
2492
|
+
if (startResult.stdout) process.stdout.write(startResult.stdout);
|
|
2493
|
+
if (startResult.stderr) process.stderr.write(startResult.stderr);
|
|
2494
|
+
console.log(
|
|
2495
|
+
`[${TOOL_NAME}] setup blocked on protected branch '${blocked.branch}' in an initialized repo; ` +
|
|
2496
|
+
'refreshing through a sandbox worktree and syncing managed bootstrap files back locally.',
|
|
2497
|
+
);
|
|
2498
|
+
|
|
2499
|
+
const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
|
|
2500
|
+
const nestedResult = run(
|
|
2501
|
+
process.execPath,
|
|
2502
|
+
[__filename, ...buildSandboxSetupArgs(options, sandboxTarget)],
|
|
2503
|
+
{ cwd: metadata.worktreePath },
|
|
2504
|
+
);
|
|
2505
|
+
if (isSpawnFailure(nestedResult)) {
|
|
2506
|
+
throw nestedResult.error;
|
|
2507
|
+
}
|
|
2508
|
+
if (nestedResult.status !== 0) {
|
|
2509
|
+
if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
|
|
2510
|
+
if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
|
|
2511
|
+
throw new Error(
|
|
2512
|
+
`sandboxed setup failed for protected branch '${blocked.branch}'. ` +
|
|
2513
|
+
`Inspect sandbox at ${metadata.worktreePath}`,
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
const syncOptions = {
|
|
2518
|
+
...options,
|
|
2519
|
+
target: blocked.repoRoot,
|
|
2520
|
+
recursive: false,
|
|
2521
|
+
allowProtectedBaseWrite: true,
|
|
2522
|
+
};
|
|
2523
|
+
const { installPayload, fixPayload, parentWorkspace } = runSetupBootstrapInternal(syncOptions);
|
|
2524
|
+
printOperations(`Setup/install${repoLabel}`, installPayload, syncOptions.dryRun);
|
|
2525
|
+
printOperations(`Setup/fix${repoLabel}`, fixPayload, syncOptions.dryRun);
|
|
2526
|
+
if (!syncOptions.dryRun && parentWorkspace) {
|
|
2527
|
+
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
const scanResult = runScanInternal({ target: blocked.repoRoot, json: false });
|
|
2531
|
+
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
2532
|
+
const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
2533
|
+
baseBranch: currentBaseBranch,
|
|
2534
|
+
dryRun: syncOptions.dryRun,
|
|
2535
|
+
});
|
|
2536
|
+
printScanResult(scanResult, false);
|
|
2537
|
+
if (autoFinishSummary.enabled) {
|
|
2538
|
+
console.log(
|
|
2539
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
2540
|
+
);
|
|
2541
|
+
for (const detail of autoFinishSummary.details) {
|
|
2542
|
+
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
1897
2543
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
2544
|
+
} else if (autoFinishSummary.details.length > 0) {
|
|
2545
|
+
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
1900
2546
|
}
|
|
1901
|
-
|
|
2547
|
+
|
|
2548
|
+
const cleanupResult = cleanupProtectedBaseSandbox(blocked.repoRoot, metadata);
|
|
2549
|
+
console.log(
|
|
2550
|
+
`[${TOOL_NAME}] Protected-base setup sandbox cleanup: ${cleanupResult.note} ` +
|
|
2551
|
+
`(worktree=${cleanupResult.worktree}, branch=${cleanupResult.branch}).`,
|
|
2552
|
+
);
|
|
2553
|
+
|
|
2554
|
+
return {
|
|
2555
|
+
scanResult,
|
|
2556
|
+
};
|
|
1902
2557
|
}
|
|
1903
2558
|
|
|
1904
2559
|
function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
|
|
@@ -2082,6 +2737,19 @@ function inferGithubRepoFromOrigin(repoRoot) {
|
|
|
2082
2737
|
return `github.com/${slug}`;
|
|
2083
2738
|
}
|
|
2084
2739
|
|
|
2740
|
+
function inferGithubRepoSlug(rawValue) {
|
|
2741
|
+
const raw = String(rawValue || '').trim();
|
|
2742
|
+
if (!raw) return '';
|
|
2743
|
+
const match = raw.match(/github\.com[:/](.+?)(?:\.git)?$/i);
|
|
2744
|
+
if (!match) return '';
|
|
2745
|
+
const slug = String(match[1] || '')
|
|
2746
|
+
.replace(/^\/+/, '')
|
|
2747
|
+
.replace(/^github\.com\//i, '')
|
|
2748
|
+
.trim();
|
|
2749
|
+
if (!slug || !slug.includes('/')) return '';
|
|
2750
|
+
return slug;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2085
2753
|
function resolveScorecardRepo(repoRoot, explicitRepo) {
|
|
2086
2754
|
if (explicitRepo) {
|
|
2087
2755
|
return explicitRepo.trim();
|
|
@@ -2335,6 +3003,7 @@ function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
|
2335
3003
|
function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
2336
3004
|
const baseBranch = String(options.baseBranch || '').trim();
|
|
2337
3005
|
const dryRun = Boolean(options.dryRun);
|
|
3006
|
+
const waitForMerge = options.waitForMerge !== false;
|
|
2338
3007
|
const excludedBranches = new Set(
|
|
2339
3008
|
Array.isArray(options.excludeBranches)
|
|
2340
3009
|
? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
|
|
@@ -2453,7 +3122,7 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
2453
3122
|
'--base',
|
|
2454
3123
|
baseBranch,
|
|
2455
3124
|
'--via-pr',
|
|
2456
|
-
'--wait-for-merge',
|
|
3125
|
+
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
2457
3126
|
'--cleanup',
|
|
2458
3127
|
];
|
|
2459
3128
|
const finishResult = run('bash', finishArgs, { cwd: repoRoot });
|
|
@@ -2565,6 +3234,66 @@ function currentBranchName(repoRoot) {
|
|
|
2565
3234
|
return branch;
|
|
2566
3235
|
}
|
|
2567
3236
|
|
|
3237
|
+
function repoHasHeadCommit(repoRoot) {
|
|
3238
|
+
return gitRun(repoRoot, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true }).status === 0;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function readBranchDisplayName(repoRoot) {
|
|
3242
|
+
const symbolic = gitRun(repoRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { allowFailure: true });
|
|
3243
|
+
if (symbolic.status === 0) {
|
|
3244
|
+
const branch = String(symbolic.stdout || '').trim();
|
|
3245
|
+
if (!branch) {
|
|
3246
|
+
return '(unknown)';
|
|
3247
|
+
}
|
|
3248
|
+
return repoHasHeadCommit(repoRoot) ? branch : `${branch} (unborn; no commits yet)`;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
const detached = gitRun(repoRoot, ['rev-parse', '--short', 'HEAD'], { allowFailure: true });
|
|
3252
|
+
if (detached.status === 0) {
|
|
3253
|
+
return `(detached at ${String(detached.stdout || '').trim()})`;
|
|
3254
|
+
}
|
|
3255
|
+
return '(unknown)';
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
function repoHasOriginRemote(repoRoot) {
|
|
3259
|
+
return gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
function detectComposeHintFiles(repoRoot) {
|
|
3263
|
+
return COMPOSE_HINT_FILES.filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
function printSetupRepoHints(repoRoot, baseBranch, repoLabel = '') {
|
|
3267
|
+
const branchDisplay = readBranchDisplayName(repoRoot);
|
|
3268
|
+
const hasHeadCommit = repoHasHeadCommit(repoRoot);
|
|
3269
|
+
const hasOrigin = repoHasOriginRemote(repoRoot);
|
|
3270
|
+
const composeFiles = detectComposeHintFiles(repoRoot);
|
|
3271
|
+
if (hasHeadCommit && hasOrigin && composeFiles.length === 0) {
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const label = repoLabel ? ` ${repoLabel}` : '';
|
|
3276
|
+
if (!hasHeadCommit) {
|
|
3277
|
+
console.log(`[${TOOL_NAME}] Fresh repo onboarding${label}: current branch is ${branchDisplay}.`);
|
|
3278
|
+
console.log(`[${TOOL_NAME}] Bootstrap commit${label}: git add . && git commit -m "bootstrap gitguardex"`);
|
|
3279
|
+
console.log(
|
|
3280
|
+
`[${TOOL_NAME}] First agent flow${label}: ` +
|
|
3281
|
+
`bash scripts/agent-branch-start.sh "<task>" "codex" -> ` +
|
|
3282
|
+
`python3 scripts/agent-file-locks.py claim --branch "$(git branch --show-current)" <file...> -> ` +
|
|
3283
|
+
`bash scripts/agent-branch-finish.sh --branch "$(git branch --show-current)" --base ${baseBranch} --via-pr --wait-for-merge`,
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
if (!hasOrigin) {
|
|
3287
|
+
console.log(`[${TOOL_NAME}] No origin remote${label}: finish and auto-merge flows stay local until you add one.`);
|
|
3288
|
+
}
|
|
3289
|
+
if (composeFiles.length > 0) {
|
|
3290
|
+
console.log(
|
|
3291
|
+
`[${TOOL_NAME}] Docker Compose helper${label}: detected ${composeFiles.join(', ')}. ` +
|
|
3292
|
+
`Set GUARDEX_DOCKER_SERVICE and run 'bash scripts/guardex-docker-loader.sh -- <command...>'.`,
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
|
|
2568
3297
|
function workingTreeIsDirty(repoRoot) {
|
|
2569
3298
|
const result = gitRun(repoRoot, ['status', '--porcelain'], { allowFailure: true });
|
|
2570
3299
|
if (result.status !== 0) {
|
|
@@ -2823,6 +3552,82 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2823
3552
|
return options;
|
|
2824
3553
|
}
|
|
2825
3554
|
|
|
3555
|
+
function parseMergeArgs(rawArgs) {
|
|
3556
|
+
const options = {
|
|
3557
|
+
target: process.cwd(),
|
|
3558
|
+
base: '',
|
|
3559
|
+
into: '',
|
|
3560
|
+
branches: [],
|
|
3561
|
+
task: '',
|
|
3562
|
+
agent: '',
|
|
3563
|
+
};
|
|
3564
|
+
|
|
3565
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
3566
|
+
const arg = rawArgs[index];
|
|
3567
|
+
if (arg === '--target') {
|
|
3568
|
+
const next = rawArgs[index + 1];
|
|
3569
|
+
if (!next) {
|
|
3570
|
+
throw new Error('--target requires a path value');
|
|
3571
|
+
}
|
|
3572
|
+
options.target = next;
|
|
3573
|
+
index += 1;
|
|
3574
|
+
continue;
|
|
3575
|
+
}
|
|
3576
|
+
if (arg === '--base') {
|
|
3577
|
+
const next = rawArgs[index + 1];
|
|
3578
|
+
if (!next) {
|
|
3579
|
+
throw new Error('--base requires a branch value');
|
|
3580
|
+
}
|
|
3581
|
+
options.base = next;
|
|
3582
|
+
index += 1;
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
if (arg === '--into') {
|
|
3586
|
+
const next = rawArgs[index + 1];
|
|
3587
|
+
if (!next) {
|
|
3588
|
+
throw new Error('--into requires an agent/* branch value');
|
|
3589
|
+
}
|
|
3590
|
+
options.into = next;
|
|
3591
|
+
index += 1;
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
if (arg === '--branch') {
|
|
3595
|
+
const next = rawArgs[index + 1];
|
|
3596
|
+
if (!next) {
|
|
3597
|
+
throw new Error('--branch requires an agent/* branch value');
|
|
3598
|
+
}
|
|
3599
|
+
options.branches.push(next);
|
|
3600
|
+
index += 1;
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
if (arg === '--task') {
|
|
3604
|
+
const next = rawArgs[index + 1];
|
|
3605
|
+
if (!next) {
|
|
3606
|
+
throw new Error('--task requires a task value');
|
|
3607
|
+
}
|
|
3608
|
+
options.task = next;
|
|
3609
|
+
index += 1;
|
|
3610
|
+
continue;
|
|
3611
|
+
}
|
|
3612
|
+
if (arg === '--agent') {
|
|
3613
|
+
const next = rawArgs[index + 1];
|
|
3614
|
+
if (!next) {
|
|
3615
|
+
throw new Error('--agent requires an agent value');
|
|
3616
|
+
}
|
|
3617
|
+
options.agent = next;
|
|
3618
|
+
index += 1;
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
if (options.branches.length === 0) {
|
|
3625
|
+
throw new Error('merge requires at least one --branch <agent/*> input');
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
return options;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
2826
3631
|
function parseFinishArgs(rawArgs) {
|
|
2827
3632
|
const options = {
|
|
2828
3633
|
target: process.cwd(),
|
|
@@ -3376,6 +4181,17 @@ function parseVersionString(version) {
|
|
|
3376
4181
|
];
|
|
3377
4182
|
}
|
|
3378
4183
|
|
|
4184
|
+
function compareParsedVersions(left, right) {
|
|
4185
|
+
if (!left || !right) return 0;
|
|
4186
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
4187
|
+
const leftValue = left[index] || 0;
|
|
4188
|
+
const rightValue = right[index] || 0;
|
|
4189
|
+
if (leftValue > rightValue) return 1;
|
|
4190
|
+
if (leftValue < rightValue) return -1;
|
|
4191
|
+
}
|
|
4192
|
+
return 0;
|
|
4193
|
+
}
|
|
4194
|
+
|
|
3379
4195
|
function isNewerVersion(latest, current) {
|
|
3380
4196
|
const latestParts = parseVersionString(latest);
|
|
3381
4197
|
const currentParts = parseVersionString(current);
|
|
@@ -3384,11 +4200,7 @@ function isNewerVersion(latest, current) {
|
|
|
3384
4200
|
return String(latest || '').trim() !== String(current || '').trim();
|
|
3385
4201
|
}
|
|
3386
4202
|
|
|
3387
|
-
|
|
3388
|
-
if (latestParts[index] > currentParts[index]) return true;
|
|
3389
|
-
if (latestParts[index] < currentParts[index]) return false;
|
|
3390
|
-
}
|
|
3391
|
-
return false;
|
|
4203
|
+
return compareParsedVersions(latestParts, currentParts) > 0;
|
|
3392
4204
|
}
|
|
3393
4205
|
|
|
3394
4206
|
function parseNpmVersionOutput(stdout) {
|
|
@@ -4078,8 +4890,7 @@ function runFixInternal(options) {
|
|
|
4078
4890
|
function runScanInternal(options) {
|
|
4079
4891
|
const repoRoot = resolveRepoRoot(options.target);
|
|
4080
4892
|
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
4081
|
-
const
|
|
4082
|
-
const branch = currentBranchResult.status === 0 ? currentBranchResult.stdout.trim() : '(unknown)';
|
|
4893
|
+
const branch = readBranchDisplayName(repoRoot);
|
|
4083
4894
|
if (!guardexToggle.enabled) {
|
|
4084
4895
|
return {
|
|
4085
4896
|
repoRoot,
|
|
@@ -4525,29 +5336,38 @@ function doctor(rawArgs) {
|
|
|
4525
5336
|
|
|
4526
5337
|
const repoResults = [];
|
|
4527
5338
|
let aggregateExitCode = 0;
|
|
4528
|
-
for (
|
|
5339
|
+
for (let repoIndex = 0; repoIndex < discoveredRepos.length; repoIndex += 1) {
|
|
5340
|
+
const repoPath = discoveredRepos[repoIndex];
|
|
5341
|
+
const progressLabel = `${repoIndex + 1}/${discoveredRepos.length}`;
|
|
4529
5342
|
if (!options.json) {
|
|
4530
|
-
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} ──`);
|
|
5343
|
+
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} [${progressLabel}] ──`);
|
|
4531
5344
|
}
|
|
4532
5345
|
|
|
4533
|
-
const
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
],
|
|
4549
|
-
|
|
4550
|
-
);
|
|
5346
|
+
const childArgs = [
|
|
5347
|
+
path.resolve(__filename),
|
|
5348
|
+
'doctor',
|
|
5349
|
+
'--single-repo',
|
|
5350
|
+
'--target',
|
|
5351
|
+
repoPath,
|
|
5352
|
+
...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
|
|
5353
|
+
...(options.skipAgents ? ['--skip-agents'] : []),
|
|
5354
|
+
...(options.skipPackageJson ? ['--skip-package-json'] : []),
|
|
5355
|
+
...(options.skipGitignore ? ['--no-gitignore'] : []),
|
|
5356
|
+
...(options.dryRun ? ['--dry-run'] : []),
|
|
5357
|
+
// Recursive child doctor runs should report pending PR state immediately instead of blocking the parent loop.
|
|
5358
|
+
'--no-wait-for-merge',
|
|
5359
|
+
...(options.verboseAutoFinish ? ['--verbose-auto-finish'] : []),
|
|
5360
|
+
...(options.json ? ['--json'] : []),
|
|
5361
|
+
...(options.allowProtectedBaseWrite ? ['--allow-protected-base-write'] : []),
|
|
5362
|
+
];
|
|
5363
|
+
const startedAt = Date.now();
|
|
5364
|
+
const nestedResult = options.json
|
|
5365
|
+
? run(process.execPath, childArgs, { cwd: topRepoRoot })
|
|
5366
|
+
: cp.spawnSync(process.execPath, childArgs, {
|
|
5367
|
+
cwd: topRepoRoot,
|
|
5368
|
+
encoding: 'utf8',
|
|
5369
|
+
stdio: 'inherit',
|
|
5370
|
+
});
|
|
4551
5371
|
if (isSpawnFailure(nestedResult)) {
|
|
4552
5372
|
throw nestedResult.error;
|
|
4553
5373
|
}
|
|
@@ -4577,9 +5397,12 @@ function doctor(rawArgs) {
|
|
|
4577
5397
|
},
|
|
4578
5398
|
);
|
|
4579
5399
|
} else {
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
5400
|
+
console.log(
|
|
5401
|
+
`[${TOOL_NAME}] Doctor target complete: ${repoPath} [${progressLabel}] in ${formatElapsedDuration(Date.now() - startedAt)}.`,
|
|
5402
|
+
);
|
|
5403
|
+
if (repoIndex < discoveredRepos.length - 1) {
|
|
5404
|
+
process.stdout.write('\n');
|
|
5405
|
+
}
|
|
4583
5406
|
}
|
|
4584
5407
|
}
|
|
4585
5408
|
|
|
@@ -4628,6 +5451,7 @@ function doctor(rawArgs) {
|
|
|
4628
5451
|
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
4629
5452
|
baseBranch: currentBaseBranch,
|
|
4630
5453
|
dryRun: singleRepoOptions.dryRun,
|
|
5454
|
+
waitForMerge: singleRepoOptions.waitForMerge,
|
|
4631
5455
|
});
|
|
4632
5456
|
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
4633
5457
|
const musafe = safe;
|
|
@@ -4662,23 +5486,17 @@ function doctor(rawArgs) {
|
|
|
4662
5486
|
return;
|
|
4663
5487
|
}
|
|
4664
5488
|
|
|
4665
|
-
printOperations('Doctor/fix', fixPayload,
|
|
5489
|
+
printOperations('Doctor/fix', fixPayload, options.dryRun);
|
|
4666
5490
|
printScanResult(scanResult, false);
|
|
4667
5491
|
if (scanResult.guardexEnabled === false) {
|
|
4668
5492
|
console.log(`[${TOOL_NAME}] Repo-local Guardex enforcement is intentionally disabled.`);
|
|
4669
5493
|
setExitCodeFromScan(scanResult);
|
|
4670
5494
|
return;
|
|
4671
5495
|
}
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
for (const detail of autoFinishSummary.details) {
|
|
4677
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
4678
|
-
}
|
|
4679
|
-
} else if (autoFinishSummary.details.length > 0) {
|
|
4680
|
-
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
4681
|
-
}
|
|
5496
|
+
printAutoFinishSummary(autoFinishSummary, {
|
|
5497
|
+
baseBranch: currentBaseBranch,
|
|
5498
|
+
verbose: singleRepoOptions.verboseAutoFinish,
|
|
5499
|
+
});
|
|
4682
5500
|
if (safe) {
|
|
4683
5501
|
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
|
|
4684
5502
|
} else {
|
|
@@ -5171,31 +5989,24 @@ function setup(rawArgs) {
|
|
|
5171
5989
|
console.log(`[${TOOL_NAME}] ── Setup target: ${repoPath} ──`);
|
|
5172
5990
|
}
|
|
5173
5991
|
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5992
|
+
const blocked = protectedBaseWriteBlock(perRepoOptions);
|
|
5993
|
+
if (blocked) {
|
|
5994
|
+
const sandboxResult = runSetupInSandbox(perRepoOptions, blocked, repoLabel);
|
|
5995
|
+
aggregateErrors += sandboxResult.scanResult.errors;
|
|
5996
|
+
aggregateWarnings += sandboxResult.scanResult.warnings;
|
|
5997
|
+
lastScanResult = sandboxResult.scanResult;
|
|
5998
|
+
continue;
|
|
5179
5999
|
}
|
|
5180
|
-
printOperations(`Setup/install${repoLabel}`, installPayload, perRepoOptions.dryRun);
|
|
5181
6000
|
|
|
5182
|
-
const fixPayload =
|
|
5183
|
-
|
|
5184
|
-
dryRun: perRepoOptions.dryRun,
|
|
5185
|
-
force: perRepoOptions.force,
|
|
5186
|
-
dropStaleLocks: true,
|
|
5187
|
-
skipAgents: perRepoOptions.skipAgents,
|
|
5188
|
-
skipPackageJson: perRepoOptions.skipPackageJson,
|
|
5189
|
-
skipGitignore: perRepoOptions.skipGitignore,
|
|
5190
|
-
});
|
|
6001
|
+
const { installPayload, fixPayload, parentWorkspace } = runSetupBootstrapInternal(perRepoOptions);
|
|
6002
|
+
printOperations(`Setup/install${repoLabel}`, installPayload, perRepoOptions.dryRun);
|
|
5191
6003
|
printOperations(`Setup/fix${repoLabel}`, fixPayload, perRepoOptions.dryRun);
|
|
5192
6004
|
|
|
5193
6005
|
if (perRepoOptions.dryRun) {
|
|
5194
6006
|
continue;
|
|
5195
6007
|
}
|
|
5196
6008
|
|
|
5197
|
-
if (
|
|
5198
|
-
const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
|
|
6009
|
+
if (parentWorkspace) {
|
|
5199
6010
|
console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
|
|
5200
6011
|
}
|
|
5201
6012
|
|
|
@@ -5216,6 +6027,7 @@ function setup(rawArgs) {
|
|
|
5216
6027
|
} else if (autoFinishSummary.details.length > 0) {
|
|
5217
6028
|
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
5218
6029
|
}
|
|
6030
|
+
printSetupRepoHints(scanResult.repoRoot, currentBaseBranch, repoLabel);
|
|
5219
6031
|
|
|
5220
6032
|
aggregateErrors += scanResult.errors;
|
|
5221
6033
|
aggregateWarnings += scanResult.warnings;
|
|
@@ -5275,6 +6087,156 @@ function ensureCleanWorkingTree(repoRoot) {
|
|
|
5275
6087
|
}
|
|
5276
6088
|
}
|
|
5277
6089
|
|
|
6090
|
+
function readReleaseRepoPackageJson(repoRoot) {
|
|
6091
|
+
const manifestPath = path.join(repoRoot, 'package.json');
|
|
6092
|
+
if (!fs.existsSync(manifestPath)) {
|
|
6093
|
+
throw new Error(`Release blocked: package.json missing in ${repoRoot}`);
|
|
6094
|
+
}
|
|
6095
|
+
|
|
6096
|
+
try {
|
|
6097
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
6098
|
+
} catch (error) {
|
|
6099
|
+
throw new Error(`Release blocked: unable to parse package.json in ${repoRoot}: ${error.message}`);
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
function resolveReleaseGithubRepo(repoRoot) {
|
|
6104
|
+
const releasePackageJson = readReleaseRepoPackageJson(repoRoot);
|
|
6105
|
+
const fromManifest = inferGithubRepoSlug(
|
|
6106
|
+
releasePackageJson.repository &&
|
|
6107
|
+
(releasePackageJson.repository.url || releasePackageJson.repository),
|
|
6108
|
+
);
|
|
6109
|
+
if (fromManifest) {
|
|
6110
|
+
return fromManifest;
|
|
6111
|
+
}
|
|
6112
|
+
|
|
6113
|
+
const fromOrigin = inferGithubRepoSlug(readGitConfig(repoRoot, 'remote.origin.url'));
|
|
6114
|
+
if (fromOrigin) {
|
|
6115
|
+
return fromOrigin;
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
throw new Error(
|
|
6119
|
+
'Release blocked: unable to resolve GitHub repo from package.json repository URL or origin remote.',
|
|
6120
|
+
);
|
|
6121
|
+
}
|
|
6122
|
+
|
|
6123
|
+
function readRepoReadme(repoRoot) {
|
|
6124
|
+
const readmePath = path.join(repoRoot, 'README.md');
|
|
6125
|
+
if (!fs.existsSync(readmePath)) {
|
|
6126
|
+
throw new Error(`Release blocked: README.md missing in ${repoRoot}`);
|
|
6127
|
+
}
|
|
6128
|
+
return fs.readFileSync(readmePath, 'utf8');
|
|
6129
|
+
}
|
|
6130
|
+
|
|
6131
|
+
function parseReadmeReleaseEntries(readmeContent) {
|
|
6132
|
+
const releaseNotesIndex = String(readmeContent || '').indexOf('## Release notes');
|
|
6133
|
+
if (releaseNotesIndex < 0) {
|
|
6134
|
+
throw new Error('Release blocked: README.md is missing the "## Release notes" section');
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
const releaseNotesContent = String(readmeContent || '').slice(releaseNotesIndex);
|
|
6138
|
+
const entries = [];
|
|
6139
|
+
const lines = releaseNotesContent.split(/\r?\n/);
|
|
6140
|
+
let currentTag = '';
|
|
6141
|
+
let currentLines = [];
|
|
6142
|
+
|
|
6143
|
+
function flushEntry() {
|
|
6144
|
+
if (!currentTag) {
|
|
6145
|
+
return;
|
|
6146
|
+
}
|
|
6147
|
+
const body = currentLines.join('\n').trim();
|
|
6148
|
+
if (body) {
|
|
6149
|
+
entries.push({ tag: currentTag, body, version: parseVersionString(currentTag) });
|
|
6150
|
+
}
|
|
6151
|
+
currentTag = '';
|
|
6152
|
+
currentLines = [];
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
for (const line of lines) {
|
|
6156
|
+
const headingMatch = line.match(/^###\s+(v\d+\.\d+\.\d+)\s*$/);
|
|
6157
|
+
if (headingMatch) {
|
|
6158
|
+
flushEntry();
|
|
6159
|
+
currentTag = headingMatch[1];
|
|
6160
|
+
continue;
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6163
|
+
if (!currentTag) {
|
|
6164
|
+
continue;
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
if (/^<\/details>\s*$/.test(line) || /^##\s+/.test(line)) {
|
|
6168
|
+
flushEntry();
|
|
6169
|
+
continue;
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
currentLines.push(line);
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
flushEntry();
|
|
6176
|
+
|
|
6177
|
+
if (entries.length === 0) {
|
|
6178
|
+
throw new Error('Release blocked: README.md did not yield any versioned release-note sections');
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
return entries;
|
|
6182
|
+
}
|
|
6183
|
+
|
|
6184
|
+
function resolvePreviousPublishedReleaseTag(repoSlug, currentTag) {
|
|
6185
|
+
const result = run(GH_BIN, ['release', 'list', '--repo', repoSlug, '--limit', '20'], {
|
|
6186
|
+
timeout: 20_000,
|
|
6187
|
+
});
|
|
6188
|
+
if (result.error) {
|
|
6189
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} release list': ${result.error.message}`);
|
|
6190
|
+
}
|
|
6191
|
+
if (result.status !== 0) {
|
|
6192
|
+
const details = (result.stderr || result.stdout || '').trim();
|
|
6193
|
+
throw new Error(`Release blocked: unable to list GitHub releases.${details ? `\n${details}` : ''}`);
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
const tags = String(result.stdout || '')
|
|
6197
|
+
.split('\n')
|
|
6198
|
+
.map((line) => line.split('\t')[0].trim())
|
|
6199
|
+
.filter(Boolean);
|
|
6200
|
+
|
|
6201
|
+
return tags.find((tag) => tag !== currentTag) || '';
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
function selectReleaseEntriesForWindow(entries, currentTag, previousTag) {
|
|
6205
|
+
const currentVersion = parseVersionString(currentTag);
|
|
6206
|
+
if (!currentVersion) {
|
|
6207
|
+
throw new Error(`Release blocked: invalid current version tag '${currentTag}'`);
|
|
6208
|
+
}
|
|
6209
|
+
const previousVersion = previousTag ? parseVersionString(previousTag) : null;
|
|
6210
|
+
|
|
6211
|
+
const selected = entries.filter((entry) => {
|
|
6212
|
+
if (!entry.version) return false;
|
|
6213
|
+
if (compareParsedVersions(entry.version, currentVersion) > 0) return false;
|
|
6214
|
+
if (!previousVersion) return entry.tag === currentTag;
|
|
6215
|
+
return compareParsedVersions(entry.version, previousVersion) > 0;
|
|
6216
|
+
});
|
|
6217
|
+
|
|
6218
|
+
if (!selected.some((entry) => entry.tag === currentTag)) {
|
|
6219
|
+
throw new Error(`Release blocked: README.md is missing release notes for ${currentTag}`);
|
|
6220
|
+
}
|
|
6221
|
+
|
|
6222
|
+
return selected;
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
function renderGeneratedReleaseNotes(entries, currentTag, previousTag) {
|
|
6226
|
+
const intro = previousTag ? `Changes since ${previousTag}.` : `Changes in ${currentTag}.`;
|
|
6227
|
+
const sections = entries
|
|
6228
|
+
.map((entry) => `### ${entry.tag}\n${entry.body}`)
|
|
6229
|
+
.join('\n\n');
|
|
6230
|
+
return `GitGuardex ${currentTag}\n\n${intro}\n\n${sections}`;
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
function buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag) {
|
|
6234
|
+
const readme = readRepoReadme(repoRoot);
|
|
6235
|
+
const entries = parseReadmeReleaseEntries(readme);
|
|
6236
|
+
const selected = selectReleaseEntriesForWindow(entries, currentTag, previousTag);
|
|
6237
|
+
return renderGeneratedReleaseNotes(selected, currentTag, previousTag);
|
|
6238
|
+
}
|
|
6239
|
+
|
|
5278
6240
|
function release(rawArgs) {
|
|
5279
6241
|
if (rawArgs.length > 0) {
|
|
5280
6242
|
throw new Error(`Unknown option: ${rawArgs[0]}`);
|
|
@@ -5290,13 +6252,74 @@ function release(rawArgs) {
|
|
|
5290
6252
|
ensureMainBranch(repoRoot);
|
|
5291
6253
|
ensureCleanWorkingTree(repoRoot);
|
|
5292
6254
|
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
6255
|
+
if (!isCommandAvailable(GH_BIN)) {
|
|
6256
|
+
throw new Error(`Release blocked: '${GH_BIN}' is not available`);
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
const ghAuthStatus = run(GH_BIN, ['auth', 'status'], { timeout: 20_000 });
|
|
6260
|
+
if (ghAuthStatus.error) {
|
|
6261
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} auth status': ${ghAuthStatus.error.message}`);
|
|
6262
|
+
}
|
|
6263
|
+
if (ghAuthStatus.status !== 0) {
|
|
6264
|
+
const details = (ghAuthStatus.stderr || ghAuthStatus.stdout || '').trim();
|
|
6265
|
+
throw new Error(`Release blocked: '${GH_BIN}' auth is unavailable.${details ? `\n${details}` : ''}`);
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
const releasePackageJson = readReleaseRepoPackageJson(repoRoot);
|
|
6269
|
+
const repoSlug = resolveReleaseGithubRepo(repoRoot);
|
|
6270
|
+
const currentTag = `v${releasePackageJson.version}`;
|
|
6271
|
+
const previousTag = resolvePreviousPublishedReleaseTag(repoSlug, currentTag);
|
|
6272
|
+
const notes = buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag);
|
|
6273
|
+
const headCommit = gitRun(repoRoot, ['rev-parse', 'HEAD']).stdout.trim();
|
|
6274
|
+
|
|
6275
|
+
const existingRelease = run(GH_BIN, ['release', 'view', currentTag, '--repo', repoSlug], {
|
|
6276
|
+
timeout: 20_000,
|
|
6277
|
+
});
|
|
6278
|
+
if (existingRelease.error) {
|
|
6279
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} release view': ${existingRelease.error.message}`);
|
|
6280
|
+
}
|
|
6281
|
+
|
|
6282
|
+
const releaseArgs =
|
|
6283
|
+
existingRelease.status === 0
|
|
6284
|
+
? ['release', 'edit', currentTag, '--repo', repoSlug, '--title', currentTag, '--notes', notes]
|
|
6285
|
+
: [
|
|
6286
|
+
'release',
|
|
6287
|
+
'create',
|
|
6288
|
+
currentTag,
|
|
6289
|
+
'--repo',
|
|
6290
|
+
repoSlug,
|
|
6291
|
+
'--target',
|
|
6292
|
+
headCommit,
|
|
6293
|
+
'--title',
|
|
6294
|
+
currentTag,
|
|
6295
|
+
'--notes',
|
|
6296
|
+
notes,
|
|
6297
|
+
];
|
|
6298
|
+
|
|
6299
|
+
console.log(
|
|
6300
|
+
`[${TOOL_NAME}] ${existingRelease.status === 0 ? 'Updating' : 'Creating'} GitHub release ${currentTag} on ${repoSlug}`,
|
|
6301
|
+
);
|
|
6302
|
+
if (previousTag) {
|
|
6303
|
+
console.log(`[${TOOL_NAME}] Aggregating README release notes newer than ${previousTag}.`);
|
|
6304
|
+
} else {
|
|
6305
|
+
console.log(`[${TOOL_NAME}] No earlier published GitHub release found; using only ${currentTag}.`);
|
|
6306
|
+
}
|
|
6307
|
+
|
|
6308
|
+
const releaseResult = run(GH_BIN, releaseArgs, { cwd: repoRoot, timeout: 60_000 });
|
|
6309
|
+
if (releaseResult.error) {
|
|
6310
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} release': ${releaseResult.error.message}`);
|
|
6311
|
+
}
|
|
6312
|
+
if (releaseResult.status !== 0) {
|
|
6313
|
+
const details = (releaseResult.stderr || releaseResult.stdout || '').trim();
|
|
6314
|
+
throw new Error(`GitHub release command failed.${details ? `\n${details}` : ''}`);
|
|
6315
|
+
}
|
|
6316
|
+
|
|
6317
|
+
const releaseUrl = String(releaseResult.stdout || '').trim();
|
|
6318
|
+
if (releaseUrl) {
|
|
6319
|
+
console.log(releaseUrl);
|
|
5297
6320
|
}
|
|
5298
6321
|
|
|
5299
|
-
console.log(`[${TOOL_NAME}] ✅
|
|
6322
|
+
console.log(`[${TOOL_NAME}] ✅ GitHub release ${currentTag} is synced to the README history.`);
|
|
5300
6323
|
process.exitCode = 0;
|
|
5301
6324
|
}
|
|
5302
6325
|
|
|
@@ -5649,6 +6672,46 @@ function cleanup(rawArgs) {
|
|
|
5649
6672
|
process.exitCode = 0;
|
|
5650
6673
|
}
|
|
5651
6674
|
|
|
6675
|
+
function merge(rawArgs) {
|
|
6676
|
+
const options = parseMergeArgs(rawArgs);
|
|
6677
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
6678
|
+
const mergeScript = path.join(repoRoot, 'scripts', 'agent-branch-merge.sh');
|
|
6679
|
+
|
|
6680
|
+
if (!fs.existsSync(mergeScript)) {
|
|
6681
|
+
throw new Error(`Missing merge script: ${mergeScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6684
|
+
const args = [mergeScript];
|
|
6685
|
+
if (options.base) {
|
|
6686
|
+
args.push('--base', options.base);
|
|
6687
|
+
}
|
|
6688
|
+
if (options.into) {
|
|
6689
|
+
args.push('--into', options.into);
|
|
6690
|
+
}
|
|
6691
|
+
if (options.task) {
|
|
6692
|
+
args.push('--task', options.task);
|
|
6693
|
+
}
|
|
6694
|
+
if (options.agent) {
|
|
6695
|
+
args.push('--agent', options.agent);
|
|
6696
|
+
}
|
|
6697
|
+
for (const branch of options.branches) {
|
|
6698
|
+
args.push('--branch', branch);
|
|
6699
|
+
}
|
|
6700
|
+
|
|
6701
|
+
const mergeResult = run('bash', args, { cwd: repoRoot, stdio: 'pipe' });
|
|
6702
|
+
if (mergeResult.stdout) {
|
|
6703
|
+
process.stdout.write(mergeResult.stdout);
|
|
6704
|
+
}
|
|
6705
|
+
if (mergeResult.stderr) {
|
|
6706
|
+
process.stderr.write(mergeResult.stderr);
|
|
6707
|
+
}
|
|
6708
|
+
if (mergeResult.status !== 0) {
|
|
6709
|
+
throw new Error(`merge command failed with status ${mergeResult.status}`);
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6712
|
+
process.exitCode = 0;
|
|
6713
|
+
}
|
|
6714
|
+
|
|
5652
6715
|
function finish(rawArgs) {
|
|
5653
6716
|
const options = parseFinishArgs(rawArgs);
|
|
5654
6717
|
const repoRoot = resolveRepoRoot(options.target);
|
|
@@ -6100,6 +7163,7 @@ function main() {
|
|
|
6100
7163
|
}
|
|
6101
7164
|
|
|
6102
7165
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
7166
|
+
maybeSelfUpdateBeforeStatus();
|
|
6103
7167
|
console.log(packageJson.version);
|
|
6104
7168
|
return;
|
|
6105
7169
|
}
|
|
@@ -6134,6 +7198,7 @@ function main() {
|
|
|
6134
7198
|
if (command === 'prompt') return prompt(rest);
|
|
6135
7199
|
if (command === 'doctor') return doctor(rest);
|
|
6136
7200
|
if (command === 'agents') return agents(rest);
|
|
7201
|
+
if (command === 'merge') return merge(rest);
|
|
6137
7202
|
if (command === 'finish') return finish(rest);
|
|
6138
7203
|
if (command === 'report') return report(rest);
|
|
6139
7204
|
if (command === 'protect') return protect(rest);
|