@imdeadpool/guardex 7.0.15 → 7.0.18
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/CONTRIBUTING.md +1 -1
- package/README.md +182 -51
- package/bin/multiagent-safety.js +993 -172
- package/package.json +3 -2
- package/templates/AGENTS.multiagent-safety.md +4 -4
- package/templates/githooks/post-checkout +1 -1
- package/templates/githooks/post-merge +19 -6
- package/templates/githooks/pre-commit +8 -8
- package/templates/scripts/agent-branch-merge.sh +421 -0
- package/templates/scripts/agent-branch-start.sh +43 -3
- package/templates/scripts/agent-session-state.js +110 -0
- package/templates/scripts/codex-agent.sh +124 -2
- package/templates/scripts/install-vscode-active-agents-extension.js +92 -0
- package/templates/scripts/openspec/init-change-workspace.sh +77 -9
- package/templates/scripts/openspec/init-plan-workspace.sh +592 -48
- package/templates/vscode/guardex-active-agents/README.md +21 -0
- package/templates/vscode/guardex-active-agents/extension.js +317 -0
- package/templates/vscode/guardex-active-agents/package.json +57 -0
- package/templates/vscode/guardex-active-agents/session-schema.js +407 -0
package/bin/multiagent-safety.js
CHANGED
|
@@ -5,12 +5,20 @@ const os = require('node:os');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const cp = require('node:child_process');
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
const packageJsonPath = path.join(PACKAGE_ROOT, 'package.json');
|
|
9
10
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
10
11
|
|
|
11
12
|
const TOOL_NAME = 'gitguardex';
|
|
12
13
|
const SHORT_TOOL_NAME = 'gx';
|
|
14
|
+
if (!process.env.GUARDEX_CLI_ENTRY) {
|
|
15
|
+
process.env.GUARDEX_CLI_ENTRY = __filename;
|
|
16
|
+
}
|
|
17
|
+
if (!process.env.GUARDEX_NODE_BIN) {
|
|
18
|
+
process.env.GUARDEX_NODE_BIN = process.execPath;
|
|
19
|
+
}
|
|
13
20
|
const LEGACY_NAMES = ['guardex', 'multiagent-safety'];
|
|
21
|
+
const GLOBAL_INSTALL_COMMAND = `npm i -g ${packageJson.name}`;
|
|
14
22
|
const OPENSPEC_PACKAGE = '@fission-ai/openspec';
|
|
15
23
|
const OMC_PACKAGE = 'oh-my-claude-sisyphus';
|
|
16
24
|
const OMC_REPO_URL = 'https://github.com/Yeachan-Heo/oh-my-claudecode';
|
|
@@ -84,48 +92,71 @@ const COMPOSE_HINT_FILES = [
|
|
|
84
92
|
'compose.yaml',
|
|
85
93
|
];
|
|
86
94
|
|
|
87
|
-
const TEMPLATE_ROOT = path.
|
|
95
|
+
const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
|
|
96
|
+
|
|
97
|
+
const HOOK_NAMES = ['pre-commit', 'pre-push', 'post-merge', 'post-checkout'];
|
|
88
98
|
|
|
89
99
|
const TEMPLATE_FILES = [
|
|
90
|
-
'scripts/agent-
|
|
91
|
-
'scripts/agent-branch-finish.sh',
|
|
92
|
-
'scripts/codex-agent.sh',
|
|
100
|
+
'scripts/agent-session-state.js',
|
|
93
101
|
'scripts/guardex-docker-loader.sh',
|
|
94
|
-
'scripts/review-bot-watch.sh',
|
|
95
|
-
'scripts/agent-worktree-prune.sh',
|
|
96
|
-
'scripts/agent-file-locks.py',
|
|
97
102
|
'scripts/guardex-env.sh',
|
|
98
|
-
'scripts/install-
|
|
99
|
-
'scripts/openspec/init-plan-workspace.sh',
|
|
100
|
-
'scripts/openspec/init-change-workspace.sh',
|
|
101
|
-
'githooks/pre-commit',
|
|
102
|
-
'githooks/pre-push',
|
|
103
|
-
'githooks/post-merge',
|
|
104
|
-
'githooks/post-checkout',
|
|
105
|
-
'codex/skills/gitguardex/SKILL.md',
|
|
106
|
-
'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
107
|
-
'claude/commands/gitguardex.md',
|
|
103
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
108
104
|
'github/pull.yml.example',
|
|
109
105
|
'github/workflows/cr.yml',
|
|
106
|
+
'vscode/guardex-active-agents/package.json',
|
|
107
|
+
'vscode/guardex-active-agents/extension.js',
|
|
108
|
+
'vscode/guardex-active-agents/session-schema.js',
|
|
109
|
+
'vscode/guardex-active-agents/README.md',
|
|
110
110
|
];
|
|
111
111
|
|
|
112
|
-
const
|
|
112
|
+
const SCRIPT_SHIMS = [
|
|
113
|
+
{ relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] },
|
|
114
|
+
{ relativePath: 'scripts/agent-branch-finish.sh', kind: 'shell', command: ['branch', 'finish'] },
|
|
115
|
+
{ relativePath: 'scripts/agent-branch-merge.sh', kind: 'shell', command: ['branch', 'merge'] },
|
|
116
|
+
{ relativePath: 'scripts/codex-agent.sh', kind: 'shell', command: ['internal', 'run-shell', 'codexAgent'] },
|
|
117
|
+
{ relativePath: 'scripts/review-bot-watch.sh', kind: 'shell', command: ['internal', 'run-shell', 'reviewBot'] },
|
|
118
|
+
{ relativePath: 'scripts/agent-worktree-prune.sh', kind: 'shell', command: ['worktree', 'prune'] },
|
|
119
|
+
{ relativePath: 'scripts/agent-file-locks.py', kind: 'python', command: ['locks'] },
|
|
120
|
+
{ relativePath: 'scripts/openspec/init-plan-workspace.sh', kind: 'shell', command: ['internal', 'run-shell', 'planInit'] },
|
|
121
|
+
{ relativePath: 'scripts/openspec/init-change-workspace.sh', kind: 'shell', command: ['internal', 'run-shell', 'changeInit'] },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const LEGACY_MANAGED_REPO_FILES = [
|
|
113
125
|
'scripts/agent-branch-start.sh',
|
|
114
126
|
'scripts/agent-branch-finish.sh',
|
|
127
|
+
'scripts/agent-branch-merge.sh',
|
|
128
|
+
'scripts/agent-session-state.js',
|
|
129
|
+
'scripts/codex-agent.sh',
|
|
115
130
|
'scripts/guardex-docker-loader.sh',
|
|
131
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
132
|
+
'scripts/review-bot-watch.sh',
|
|
116
133
|
'scripts/agent-worktree-prune.sh',
|
|
117
134
|
'scripts/agent-file-locks.py',
|
|
118
135
|
'scripts/guardex-env.sh',
|
|
119
136
|
'scripts/install-agent-git-hooks.sh',
|
|
137
|
+
'scripts/openspec/init-plan-workspace.sh',
|
|
138
|
+
'scripts/openspec/init-change-workspace.sh',
|
|
120
139
|
'.githooks/pre-commit',
|
|
140
|
+
'.githooks/pre-push',
|
|
121
141
|
'.githooks/post-merge',
|
|
142
|
+
'.githooks/post-checkout',
|
|
143
|
+
'.codex/skills/gitguardex/SKILL.md',
|
|
144
|
+
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
145
|
+
'.claude/commands/gitguardex.md',
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const REQUIRED_WORKFLOW_FILES = [
|
|
149
|
+
...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
|
|
150
|
+
...SCRIPT_SHIMS.map((entry) => entry.relativePath),
|
|
151
|
+
...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
|
|
122
152
|
'.omx/state/agent-file-locks.json',
|
|
123
153
|
];
|
|
124
154
|
|
|
125
|
-
const
|
|
155
|
+
const LEGACY_MANAGED_PACKAGE_SCRIPTS = {
|
|
126
156
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
127
157
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
128
158
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
159
|
+
'agent:branch:merge': 'bash ./scripts/agent-branch-merge.sh',
|
|
129
160
|
'agent:cleanup': 'gx cleanup',
|
|
130
161
|
'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
|
|
131
162
|
'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
|
|
@@ -146,31 +177,47 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
146
177
|
'agent:finish': 'gx finish --all',
|
|
147
178
|
};
|
|
148
179
|
|
|
180
|
+
const PACKAGE_SCRIPT_ASSETS = {
|
|
181
|
+
branchStart: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-start.sh'),
|
|
182
|
+
branchFinish: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-finish.sh'),
|
|
183
|
+
branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'),
|
|
184
|
+
codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'),
|
|
185
|
+
reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'),
|
|
186
|
+
worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'),
|
|
187
|
+
lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'),
|
|
188
|
+
planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'),
|
|
189
|
+
changeInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-change-workspace.sh'),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const USER_LEVEL_SKILL_ASSETS = [
|
|
193
|
+
{
|
|
194
|
+
source: path.join(TEMPLATE_ROOT, 'codex', 'skills', 'gitguardex', 'SKILL.md'),
|
|
195
|
+
destination: path.join('.codex', 'skills', 'gitguardex', 'SKILL.md'),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
source: path.join(TEMPLATE_ROOT, 'codex', 'skills', 'guardex-merge-skills-to-dev', 'SKILL.md'),
|
|
199
|
+
destination: path.join('.codex', 'skills', 'guardex-merge-skills-to-dev', 'SKILL.md'),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
source: path.join(TEMPLATE_ROOT, 'claude', 'commands', 'gitguardex.md'),
|
|
203
|
+
destination: path.join('.claude', 'commands', 'gitguardex.md'),
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
149
207
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
150
|
-
'scripts/agent-
|
|
151
|
-
'scripts/agent-branch-finish.sh',
|
|
152
|
-
'scripts/codex-agent.sh',
|
|
208
|
+
'scripts/agent-session-state.js',
|
|
153
209
|
'scripts/guardex-docker-loader.sh',
|
|
154
|
-
'scripts/
|
|
155
|
-
|
|
156
|
-
'
|
|
157
|
-
'scripts/install-agent-git-hooks.sh',
|
|
158
|
-
'scripts/openspec/init-plan-workspace.sh',
|
|
159
|
-
'scripts/openspec/init-change-workspace.sh',
|
|
160
|
-
'.githooks/pre-commit',
|
|
161
|
-
'.githooks/pre-push',
|
|
162
|
-
'.githooks/post-merge',
|
|
163
|
-
'.githooks/post-checkout',
|
|
210
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
211
|
+
...SCRIPT_SHIMS.map((entry) => entry.relativePath),
|
|
212
|
+
...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
|
|
164
213
|
]);
|
|
165
214
|
|
|
166
215
|
const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
167
216
|
'AGENTS.md',
|
|
168
|
-
'.githooks
|
|
169
|
-
'.githooks/pre-push',
|
|
170
|
-
'.githooks/post-merge',
|
|
171
|
-
'.githooks/post-checkout',
|
|
217
|
+
...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
|
|
172
218
|
'scripts/agent-branch-start.sh',
|
|
173
219
|
'scripts/agent-branch-finish.sh',
|
|
220
|
+
'scripts/agent-branch-merge.sh',
|
|
174
221
|
'scripts/agent-worktree-prune.sh',
|
|
175
222
|
'scripts/codex-agent.sh',
|
|
176
223
|
'scripts/agent-file-locks.py',
|
|
@@ -197,9 +244,6 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
197
244
|
'scripts/agent-file-locks.py',
|
|
198
245
|
'.githooks',
|
|
199
246
|
'oh-my-codex/',
|
|
200
|
-
'.codex/skills/gitguardex/SKILL.md',
|
|
201
|
-
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
202
|
-
'.claude/commands/gitguardex.md',
|
|
203
247
|
LOCK_FILE_RELATIVE,
|
|
204
248
|
];
|
|
205
249
|
const REPO_SCAFFOLD_DIRECTORIES = ['bin'];
|
|
@@ -232,7 +276,14 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
232
276
|
'status',
|
|
233
277
|
'setup',
|
|
234
278
|
'doctor',
|
|
279
|
+
'branch',
|
|
280
|
+
'locks',
|
|
281
|
+
'worktree',
|
|
282
|
+
'hook',
|
|
283
|
+
'migrate',
|
|
284
|
+
'install-agent-skills',
|
|
235
285
|
'agents',
|
|
286
|
+
'merge',
|
|
236
287
|
'finish',
|
|
237
288
|
'report',
|
|
238
289
|
'protect',
|
|
@@ -256,7 +307,14 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
256
307
|
['status', 'Show GitGuardex CLI + service health without modifying files'],
|
|
257
308
|
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
|
|
258
309
|
['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
|
|
310
|
+
['branch', 'CLI-owned branch workflow surface (start/finish/merge)'],
|
|
311
|
+
['locks', 'CLI-owned file lock surface (claim/allow-delete/release/status/validate)'],
|
|
312
|
+
['worktree', 'CLI-owned worktree cleanup surface (prune)'],
|
|
313
|
+
['hook', 'Hook dispatch/install surface used by managed shims'],
|
|
314
|
+
['migrate', 'Convert legacy repo-local installs to the new shim-based CLI-owned surface'],
|
|
315
|
+
['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
|
|
259
316
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
317
|
+
['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
|
|
260
318
|
['sync', 'Sync agent branches with origin/<base>'],
|
|
261
319
|
['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
|
|
262
320
|
['cleanup', 'Prune merged/stale agent branches and worktrees'],
|
|
@@ -265,7 +323,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
265
323
|
['prompt', 'Print AI setup checklist (--exec, --snippet)'],
|
|
266
324
|
['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
|
|
267
325
|
['help', 'Show this help output'],
|
|
268
|
-
['version', 'Print
|
|
326
|
+
['version', 'Print GitGuardex version'],
|
|
269
327
|
];
|
|
270
328
|
const DEPRECATED_COMMAND_ALIASES = new Map([
|
|
271
329
|
['init', { target: 'setup', hint: 'gx setup' }],
|
|
@@ -280,6 +338,9 @@ const DEPRECATED_COMMAND_ALIASES = new Map([
|
|
|
280
338
|
const AGENT_BOT_DESCRIPTIONS = [
|
|
281
339
|
['agents', 'Start/stop review + cleanup bots for this repo'],
|
|
282
340
|
];
|
|
341
|
+
const DOCTOR_AUTO_FINISH_DETAIL_LIMIT = 6;
|
|
342
|
+
const DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX = 72;
|
|
343
|
+
const DOCTOR_AUTO_FINISH_MESSAGE_MAX = 160;
|
|
283
344
|
|
|
284
345
|
function envFlagIsTruthy(raw) {
|
|
285
346
|
const lowered = String(raw || '').trim().toLowerCase();
|
|
@@ -296,26 +357,28 @@ function defaultAgentWorktreeRelativeDir(env = process.env) {
|
|
|
296
357
|
|
|
297
358
|
const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in this repo.
|
|
298
359
|
|
|
299
|
-
1) Install:
|
|
360
|
+
1) Install: ${GLOBAL_INSTALL_COMMAND} && gh --version
|
|
300
361
|
2) Bootstrap: gx setup
|
|
301
362
|
3) Repair: gx doctor
|
|
302
|
-
4) Task loop:
|
|
303
|
-
|
|
304
|
-
5)
|
|
305
|
-
6)
|
|
306
|
-
7)
|
|
307
|
-
8)
|
|
308
|
-
9) Optional: gx
|
|
309
|
-
10)
|
|
310
|
-
11)
|
|
363
|
+
4) Task loop: gx branch start "<task>" "<agent>"
|
|
364
|
+
then gx locks claim --branch "<agent-branch>" <file...> -> gx branch finish
|
|
365
|
+
5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
|
|
366
|
+
6) Finish: gx finish --all
|
|
367
|
+
7) Cleanup: gx cleanup
|
|
368
|
+
8) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
|
|
369
|
+
9) Optional: gx protect add release staging
|
|
370
|
+
10) Optional: gx sync --check && gx sync
|
|
371
|
+
11) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
|
|
372
|
+
12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
|
|
311
373
|
`;
|
|
312
374
|
|
|
313
|
-
const AI_SETUP_COMMANDS =
|
|
375
|
+
const AI_SETUP_COMMANDS = `${GLOBAL_INSTALL_COMMAND}
|
|
314
376
|
gh --version
|
|
315
377
|
gx setup
|
|
316
378
|
gx doctor
|
|
317
|
-
|
|
318
|
-
|
|
379
|
+
gx branch start "<task>" "<agent>"
|
|
380
|
+
gx locks claim --branch "<agent-branch>" <file...>
|
|
381
|
+
gx merge --branch "<agent-a>" --branch "<agent-b>"
|
|
319
382
|
gx finish --all
|
|
320
383
|
gx cleanup
|
|
321
384
|
gx protect add release staging
|
|
@@ -345,7 +408,17 @@ function runtimeVersion() {
|
|
|
345
408
|
}
|
|
346
409
|
|
|
347
410
|
function supportsAnsiColors() {
|
|
348
|
-
|
|
411
|
+
const forced = String(process.env.FORCE_COLOR || '').trim().toLowerCase();
|
|
412
|
+
if (['0', 'false', 'no', 'off'].includes(forced)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (forced.length > 0) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (process.env.NO_COLOR) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
return Boolean(process.stdout.isTTY) && process.env.TERM !== 'dumb';
|
|
349
422
|
}
|
|
350
423
|
|
|
351
424
|
function colorize(text, colorCode) {
|
|
@@ -355,6 +428,56 @@ function colorize(text, colorCode) {
|
|
|
355
428
|
return `\u001B[${colorCode}m${text}\u001B[0m`;
|
|
356
429
|
}
|
|
357
430
|
|
|
431
|
+
function doctorOutputColorCode(status) {
|
|
432
|
+
const normalized = String(status || '').trim().toLowerCase();
|
|
433
|
+
if (['active', 'done', 'ok', 'safe', 'success'].includes(normalized)) {
|
|
434
|
+
return '32';
|
|
435
|
+
}
|
|
436
|
+
if (normalized === 'disabled') {
|
|
437
|
+
return '36';
|
|
438
|
+
}
|
|
439
|
+
if (['degraded', 'pending', 'skip', 'warn', 'warning'].includes(normalized)) {
|
|
440
|
+
return '33';
|
|
441
|
+
}
|
|
442
|
+
if (['error', 'fail', 'inactive', 'unsafe'].includes(normalized)) {
|
|
443
|
+
return '31';
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function colorizeDoctorOutput(text, status) {
|
|
449
|
+
const colorCode = doctorOutputColorCode(status);
|
|
450
|
+
return colorCode ? colorize(text, colorCode) : text;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function detectAutoFinishDetailStatus(detail) {
|
|
454
|
+
const trimmed = String(detail || '').trim();
|
|
455
|
+
const match = trimmed.match(/^\[(\w+)\]/);
|
|
456
|
+
if (match) {
|
|
457
|
+
return match[1].toLowerCase();
|
|
458
|
+
}
|
|
459
|
+
if (/^Skipped\b/i.test(trimmed) || /^No local agent branches found\b/i.test(trimmed)) {
|
|
460
|
+
return 'skip';
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function detectAutoFinishSummaryStatus(summary) {
|
|
466
|
+
if (!summary || summary.enabled === false) {
|
|
467
|
+
return detectAutoFinishDetailStatus(summary?.details?.[0]);
|
|
468
|
+
}
|
|
469
|
+
if ((summary.failed || 0) > 0) {
|
|
470
|
+
return 'fail';
|
|
471
|
+
}
|
|
472
|
+
if ((summary.completed || 0) > 0) {
|
|
473
|
+
return 'done';
|
|
474
|
+
}
|
|
475
|
+
if ((summary.skipped || 0) > 0) {
|
|
476
|
+
return 'skip';
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
358
481
|
function statusDot(status) {
|
|
359
482
|
if (status === 'active') {
|
|
360
483
|
return colorize('●', '32'); // green
|
|
@@ -500,10 +623,188 @@ function run(cmd, args, options = {}) {
|
|
|
500
623
|
encoding: 'utf8',
|
|
501
624
|
stdio: options.stdio || 'pipe',
|
|
502
625
|
cwd: options.cwd,
|
|
626
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
627
|
+
timeout: options.timeout,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function extractTargetedArgs(rawArgs, defaultTarget = process.cwd()) {
|
|
632
|
+
const passthrough = [];
|
|
633
|
+
let target = defaultTarget;
|
|
634
|
+
|
|
635
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
636
|
+
const arg = rawArgs[index];
|
|
637
|
+
if (arg === '--target' || arg === '-t') {
|
|
638
|
+
target = requireValue(rawArgs, index, '--target');
|
|
639
|
+
index += 1;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
passthrough.push(arg);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return { target, passthrough };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function packageAssetEnv(extraEnv = {}) {
|
|
649
|
+
return {
|
|
650
|
+
GUARDEX_CLI_ENTRY: __filename,
|
|
651
|
+
GUARDEX_NODE_BIN: process.execPath,
|
|
652
|
+
...extraEnv,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function packageAssetPath(assetKey) {
|
|
657
|
+
const assetPath = PACKAGE_SCRIPT_ASSETS[assetKey];
|
|
658
|
+
if (!assetPath) {
|
|
659
|
+
throw new Error(`Unknown package asset: ${assetKey}`);
|
|
660
|
+
}
|
|
661
|
+
if (!fs.existsSync(assetPath)) {
|
|
662
|
+
throw new Error(`Missing package asset: ${assetPath}`);
|
|
663
|
+
}
|
|
664
|
+
return assetPath;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function runPackageAsset(assetKey, rawArgs, options = {}) {
|
|
668
|
+
const assetPath = packageAssetPath(assetKey);
|
|
669
|
+
let cmd = 'bash';
|
|
670
|
+
if (assetPath.endsWith('.py')) {
|
|
671
|
+
cmd = 'python3';
|
|
672
|
+
} else if (assetPath.endsWith('.js')) {
|
|
673
|
+
cmd = process.execPath;
|
|
674
|
+
}
|
|
675
|
+
return run(cmd, [assetPath, ...rawArgs], {
|
|
676
|
+
cwd: options.cwd || process.cwd(),
|
|
677
|
+
stdio: options.stdio || 'pipe',
|
|
503
678
|
timeout: options.timeout,
|
|
679
|
+
env: packageAssetEnv(options.env),
|
|
504
680
|
});
|
|
505
681
|
}
|
|
506
682
|
|
|
683
|
+
function invokePackageAsset(assetKey, rawArgs, options = {}) {
|
|
684
|
+
const result = runPackageAsset(assetKey, rawArgs, options);
|
|
685
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
686
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
687
|
+
if (result.status !== 0) {
|
|
688
|
+
throw new Error(`${assetKey} command failed with status ${result.status}`);
|
|
689
|
+
}
|
|
690
|
+
process.exitCode = 0;
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function formatElapsedDuration(ms) {
|
|
695
|
+
const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
696
|
+
if (durationMs < 1000) {
|
|
697
|
+
return `${Math.round(durationMs)}ms`;
|
|
698
|
+
}
|
|
699
|
+
if (durationMs < 10_000) {
|
|
700
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
701
|
+
}
|
|
702
|
+
return `${Math.round(durationMs / 1000)}s`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function truncateMiddle(value, maxLength) {
|
|
706
|
+
const text = String(value || '');
|
|
707
|
+
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
708
|
+
if (!limit || text.length <= limit) {
|
|
709
|
+
return text;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const visible = limit - 1;
|
|
713
|
+
const headLength = Math.ceil(visible / 2);
|
|
714
|
+
const tailLength = Math.floor(visible / 2);
|
|
715
|
+
return `${text.slice(0, headLength)}…${text.slice(text.length - tailLength)}`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function truncateTail(value, maxLength) {
|
|
719
|
+
const text = String(value || '');
|
|
720
|
+
const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
|
|
721
|
+
if (!limit || text.length <= limit) {
|
|
722
|
+
return text;
|
|
723
|
+
}
|
|
724
|
+
return `${text.slice(0, limit - 1)}…`;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function compactAutoFinishPathSegments(message) {
|
|
728
|
+
return String(message || '').replace(/\((\/[^)]+)\)/g, (_, rawPath) => {
|
|
729
|
+
if (
|
|
730
|
+
rawPath.includes(`${path.sep}.omx${path.sep}agent-worktrees${path.sep}`) ||
|
|
731
|
+
rawPath.includes(`${path.sep}.omc${path.sep}agent-worktrees${path.sep}`)
|
|
732
|
+
) {
|
|
733
|
+
return `(${path.basename(rawPath)})`;
|
|
734
|
+
}
|
|
735
|
+
return `(${truncateMiddle(rawPath, 72)})`;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function summarizeAutoFinishDetail(detail) {
|
|
740
|
+
const trimmed = String(detail || '').trim();
|
|
741
|
+
const match = trimmed.match(/^\[(\w+)\]\s+([^:]+):\s*(.*)$/);
|
|
742
|
+
if (!match) {
|
|
743
|
+
return truncateTail(compactAutoFinishPathSegments(trimmed), DOCTOR_AUTO_FINISH_MESSAGE_MAX);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const [, status, rawBranch, rawMessage] = match;
|
|
747
|
+
const branch = truncateMiddle(rawBranch, DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX);
|
|
748
|
+
let message = String(rawMessage || '').trim();
|
|
749
|
+
|
|
750
|
+
if (status === 'fail') {
|
|
751
|
+
message = message.replace(/^auto-finish failed\.?\s*/i, '');
|
|
752
|
+
if (/\[agent-sync-guard\]/.test(message) && /Resolve conflicts/i.test(message)) {
|
|
753
|
+
message = 'rebase conflict in finish flow; run rebase --continue or rebase --abort in the source-probe worktree';
|
|
754
|
+
} else if (/unable to compute ahead\/behind/i.test(message)) {
|
|
755
|
+
const aheadBehindMatch = message.match(/unable to compute ahead\/behind(?: \([^)]+\))?/i);
|
|
756
|
+
if (aheadBehindMatch) {
|
|
757
|
+
message = aheadBehindMatch[0];
|
|
758
|
+
}
|
|
759
|
+
} else if (/remote ref does not exist/i.test(message)) {
|
|
760
|
+
message = 'branch merged, but the remote ref was already removed during cleanup';
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
message = compactAutoFinishPathSegments(message)
|
|
765
|
+
.replace(/\s+\|\s+/g, '; ')
|
|
766
|
+
.trim();
|
|
767
|
+
|
|
768
|
+
return `[${status}] ${branch}: ${truncateTail(message, DOCTOR_AUTO_FINISH_MESSAGE_MAX)}`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function printAutoFinishSummary(summary, options = {}) {
|
|
772
|
+
const enabled = Boolean(summary && summary.enabled);
|
|
773
|
+
const details = Array.isArray(summary && summary.details) ? summary.details : [];
|
|
774
|
+
const baseBranch = String(options.baseBranch || summary?.baseBranch || '').trim();
|
|
775
|
+
const verbose = Boolean(options.verbose);
|
|
776
|
+
const detailLimit = Number.isFinite(options.detailLimit)
|
|
777
|
+
? Math.max(0, options.detailLimit)
|
|
778
|
+
: DOCTOR_AUTO_FINISH_DETAIL_LIMIT;
|
|
779
|
+
|
|
780
|
+
if (enabled) {
|
|
781
|
+
console.log(
|
|
782
|
+
colorizeDoctorOutput(
|
|
783
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
|
|
784
|
+
detectAutoFinishSummaryStatus(summary),
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
|
|
788
|
+
for (const detail of visibleDetails) {
|
|
789
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
|
|
790
|
+
}
|
|
791
|
+
if (!verbose && details.length > detailLimit) {
|
|
792
|
+
console.log(
|
|
793
|
+
colorizeDoctorOutput(
|
|
794
|
+
`[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
|
|
795
|
+
'warn',
|
|
796
|
+
),
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (details.length > 0) {
|
|
803
|
+
const detail = verbose ? details[0] : summarizeAutoFinishDetail(details[0]);
|
|
804
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
507
808
|
function gitRun(repoRoot, args, { allowFailure = false } = {}) {
|
|
508
809
|
const result = run('git', ['-C', repoRoot, ...args]);
|
|
509
810
|
if (!allowFailure && result.status !== 0) {
|
|
@@ -628,6 +929,9 @@ function toDestinationPath(relativeTemplatePath) {
|
|
|
628
929
|
if (relativeTemplatePath.startsWith('github/')) {
|
|
629
930
|
return `.${relativeTemplatePath}`;
|
|
630
931
|
}
|
|
932
|
+
if (relativeTemplatePath.startsWith('vscode/')) {
|
|
933
|
+
return relativeTemplatePath;
|
|
934
|
+
}
|
|
631
935
|
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
632
936
|
}
|
|
633
937
|
|
|
@@ -665,6 +969,111 @@ function isCriticalGuardrailPath(relativePath) {
|
|
|
665
969
|
return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
|
|
666
970
|
}
|
|
667
971
|
|
|
972
|
+
function shellSingleQuote(value) {
|
|
973
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function renderShellDispatchShim(commandParts) {
|
|
977
|
+
const rendered = commandParts.map((part) => shellSingleQuote(part)).join(' ');
|
|
978
|
+
return (
|
|
979
|
+
'#!/usr/bin/env bash\n' +
|
|
980
|
+
'set -euo pipefail\n' +
|
|
981
|
+
'\n' +
|
|
982
|
+
'if [[ -n "${GUARDEX_CLI_ENTRY:-}" ]]; then\n' +
|
|
983
|
+
' node_bin="${GUARDEX_NODE_BIN:-node}"\n' +
|
|
984
|
+
` exec "$node_bin" "$GUARDEX_CLI_ENTRY" ${rendered} "$@"\n` +
|
|
985
|
+
'fi\n' +
|
|
986
|
+
'\n' +
|
|
987
|
+
'resolve_guardex_cli() {\n' +
|
|
988
|
+
' if [[ -n "${GUARDEX_CLI_BIN:-}" ]]; then\n' +
|
|
989
|
+
' printf \'%s\' "$GUARDEX_CLI_BIN"\n' +
|
|
990
|
+
' return 0\n' +
|
|
991
|
+
' fi\n' +
|
|
992
|
+
' if command -v gx >/dev/null 2>&1; then\n' +
|
|
993
|
+
' printf \'%s\' "gx"\n' +
|
|
994
|
+
' return 0\n' +
|
|
995
|
+
' fi\n' +
|
|
996
|
+
' if command -v gitguardex >/dev/null 2>&1; then\n' +
|
|
997
|
+
' printf \'%s\' "gitguardex"\n' +
|
|
998
|
+
' return 0\n' +
|
|
999
|
+
' fi\n' +
|
|
1000
|
+
' echo "[gitguardex-shim] Missing gx CLI in PATH." >&2\n' +
|
|
1001
|
+
' exit 1\n' +
|
|
1002
|
+
'}\n' +
|
|
1003
|
+
'\n' +
|
|
1004
|
+
'cli_bin="$(resolve_guardex_cli)"\n' +
|
|
1005
|
+
`exec "$cli_bin" ${rendered} "$@"\n`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function renderPythonDispatchShim(commandParts) {
|
|
1010
|
+
return (
|
|
1011
|
+
'#!/usr/bin/env python3\n' +
|
|
1012
|
+
'import os\n' +
|
|
1013
|
+
'import shutil\n' +
|
|
1014
|
+
'import subprocess\n' +
|
|
1015
|
+
'import sys\n' +
|
|
1016
|
+
'\n' +
|
|
1017
|
+
`COMMAND = ${JSON.stringify(commandParts)}\n` +
|
|
1018
|
+
'\n' +
|
|
1019
|
+
'entry = os.environ.get("GUARDEX_CLI_ENTRY")\n' +
|
|
1020
|
+
'if entry:\n' +
|
|
1021
|
+
' node_bin = os.environ.get("GUARDEX_NODE_BIN") or shutil.which("node") or "node"\n' +
|
|
1022
|
+
' raise SystemExit(subprocess.call([node_bin, entry, *COMMAND, *sys.argv[1:]]))\n' +
|
|
1023
|
+
'cli = os.environ.get("GUARDEX_CLI_BIN") or shutil.which("gx") or shutil.which("gitguardex")\n' +
|
|
1024
|
+
'if not cli:\n' +
|
|
1025
|
+
' sys.stderr.write("[gitguardex-shim] Missing gx CLI in PATH.\\n")\n' +
|
|
1026
|
+
' raise SystemExit(1)\n' +
|
|
1027
|
+
'raise SystemExit(subprocess.call([cli, *COMMAND, *sys.argv[1:]]))\n'
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function renderManagedFile(repoRoot, relativePath, content, options = {}) {
|
|
1032
|
+
const destinationPath = path.join(repoRoot, relativePath);
|
|
1033
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
1034
|
+
const force = Boolean(options.force);
|
|
1035
|
+
const dryRun = Boolean(options.dryRun);
|
|
1036
|
+
|
|
1037
|
+
if (destinationExists) {
|
|
1038
|
+
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
1039
|
+
if (existingContent === content) {
|
|
1040
|
+
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
1041
|
+
return { status: 'unchanged', file: relativePath };
|
|
1042
|
+
}
|
|
1043
|
+
if (!force && !isCriticalGuardrailPath(relativePath)) {
|
|
1044
|
+
throw new Error(`Refusing to overwrite existing file without --force: ${relativePath}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
1049
|
+
if (!dryRun) {
|
|
1050
|
+
fs.writeFileSync(destinationPath, content, 'utf8');
|
|
1051
|
+
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (destinationExists && !force && isCriticalGuardrailPath(relativePath)) {
|
|
1055
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: relativePath };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return { status: destinationExists ? 'overwritten' : 'created', file: relativePath };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function ensureGeneratedScriptShim(repoRoot, spec, options = {}) {
|
|
1062
|
+
const content = spec.kind === 'python'
|
|
1063
|
+
? renderPythonDispatchShim(spec.command)
|
|
1064
|
+
: renderShellDispatchShim(spec.command);
|
|
1065
|
+
return renderManagedFile(repoRoot, spec.relativePath, content, options);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function ensureHookShim(repoRoot, hookName, options = {}) {
|
|
1069
|
+
return renderManagedFile(
|
|
1070
|
+
repoRoot,
|
|
1071
|
+
path.posix.join('.githooks', hookName),
|
|
1072
|
+
renderShellDispatchShim(['hook', 'run', hookName]),
|
|
1073
|
+
options,
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
668
1077
|
function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
669
1078
|
const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
|
|
670
1079
|
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
|
|
@@ -842,8 +1251,7 @@ function writeLockState(repoRoot, payload, dryRun) {
|
|
|
842
1251
|
fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
843
1252
|
}
|
|
844
1253
|
|
|
845
|
-
function
|
|
846
|
-
const force = Boolean(options.force);
|
|
1254
|
+
function removeLegacyPackageScripts(repoRoot, dryRun) {
|
|
847
1255
|
const packagePath = path.join(repoRoot, 'package.json');
|
|
848
1256
|
if (!fs.existsSync(packagePath)) {
|
|
849
1257
|
return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
|
|
@@ -859,29 +1267,87 @@ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
|
|
|
859
1267
|
const existingScripts = pkg.scripts && typeof pkg.scripts === 'object'
|
|
860
1268
|
? pkg.scripts
|
|
861
1269
|
: {};
|
|
862
|
-
const hasExistingAgentScripts = Object.keys(existingScripts).some((key) => key.startsWith('agent:'));
|
|
863
|
-
if (hasExistingAgentScripts && !force) {
|
|
864
|
-
return { status: 'unchanged', file: 'package.json', note: 'preserved existing agent:* scripts' };
|
|
865
|
-
}
|
|
866
|
-
|
|
867
1270
|
pkg.scripts = existingScripts;
|
|
868
1271
|
let changed = false;
|
|
869
|
-
for (const [key, value] of Object.entries(
|
|
870
|
-
if (
|
|
871
|
-
|
|
1272
|
+
for (const [key, value] of Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)) {
|
|
1273
|
+
if (existingScripts[key] === value) {
|
|
1274
|
+
delete existingScripts[key];
|
|
872
1275
|
changed = true;
|
|
873
1276
|
}
|
|
874
1277
|
}
|
|
875
1278
|
|
|
876
1279
|
if (!changed) {
|
|
877
|
-
return { status: 'unchanged', file: 'package.json' };
|
|
1280
|
+
return { status: 'unchanged', file: 'package.json', note: 'no Guardex-managed agent:* scripts found' };
|
|
878
1281
|
}
|
|
879
1282
|
|
|
880
1283
|
if (!dryRun) {
|
|
881
1284
|
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
882
1285
|
}
|
|
883
1286
|
|
|
884
|
-
return { status: 'updated', file: 'package.json' };
|
|
1287
|
+
return { status: dryRun ? 'would-update' : 'updated', file: 'package.json', note: 'removed Guardex-managed agent:* scripts' };
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function installUserLevelAsset(asset, options = {}) {
|
|
1291
|
+
const dryRun = Boolean(options.dryRun);
|
|
1292
|
+
const force = Boolean(options.force);
|
|
1293
|
+
const destinationPath = path.join(GUARDEX_HOME_DIR, asset.destination);
|
|
1294
|
+
const sourceContent = fs.readFileSync(asset.source, 'utf8');
|
|
1295
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
1296
|
+
|
|
1297
|
+
if (destinationExists) {
|
|
1298
|
+
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
1299
|
+
if (existingContent === sourceContent) {
|
|
1300
|
+
return { status: 'unchanged', file: asset.destination };
|
|
1301
|
+
}
|
|
1302
|
+
if (!force) {
|
|
1303
|
+
return { status: 'skipped-conflict', file: asset.destination };
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (!dryRun) {
|
|
1308
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
1309
|
+
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
1310
|
+
}
|
|
1311
|
+
return { status: destinationExists ? (dryRun ? 'would-update' : 'updated') : 'created', file: asset.destination };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function removeLegacyManagedRepoFile(repoRoot, relativePath, options = {}) {
|
|
1315
|
+
const dryRun = Boolean(options.dryRun);
|
|
1316
|
+
const force = Boolean(options.force);
|
|
1317
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
1318
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1319
|
+
return { status: 'unchanged', file: relativePath, note: 'not present' };
|
|
1320
|
+
}
|
|
1321
|
+
if (!fs.statSync(absolutePath).isFile()) {
|
|
1322
|
+
return { status: 'skipped-conflict', file: relativePath, note: 'not a regular file' };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const skillAsset = USER_LEVEL_SKILL_ASSETS.find((asset) => asset.destination === relativePath);
|
|
1326
|
+
if (skillAsset) {
|
|
1327
|
+
const userLevelPath = path.join(GUARDEX_HOME_DIR, skillAsset.destination);
|
|
1328
|
+
if (!fs.existsSync(userLevelPath)) {
|
|
1329
|
+
return { status: 'skipped', file: relativePath, note: 'user-level replacement not installed' };
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const templateRelative = skillAsset
|
|
1334
|
+
? skillAsset.source.slice(TEMPLATE_ROOT.length + 1)
|
|
1335
|
+
: relativePath.replace(/^\./, '');
|
|
1336
|
+
const sourcePath = path.join(TEMPLATE_ROOT, templateRelative);
|
|
1337
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1338
|
+
return { status: 'skipped', file: relativePath, note: 'template source missing' };
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
1342
|
+
const existingContent = fs.readFileSync(absolutePath, 'utf8');
|
|
1343
|
+
if (existingContent !== sourceContent && !force) {
|
|
1344
|
+
return { status: 'skipped-conflict', file: relativePath, note: 'local edits differ from managed template' };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (!dryRun) {
|
|
1348
|
+
fs.rmSync(absolutePath, { force: true });
|
|
1349
|
+
}
|
|
1350
|
+
return { status: dryRun ? 'would-remove' : 'removed', file: relativePath };
|
|
885
1351
|
}
|
|
886
1352
|
|
|
887
1353
|
function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
|
|
@@ -1121,7 +1587,7 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
1121
1587
|
}
|
|
1122
1588
|
|
|
1123
1589
|
function parseDoctorArgs(rawArgs) {
|
|
1124
|
-
|
|
1590
|
+
const doctorDefaults = {
|
|
1125
1591
|
target: process.cwd(),
|
|
1126
1592
|
dropStaleLocks: true,
|
|
1127
1593
|
skipAgents: false,
|
|
@@ -1131,7 +1597,24 @@ function parseDoctorArgs(rawArgs) {
|
|
|
1131
1597
|
json: false,
|
|
1132
1598
|
allowProtectedBaseWrite: false,
|
|
1133
1599
|
waitForMerge: true,
|
|
1134
|
-
|
|
1600
|
+
verboseAutoFinish: false,
|
|
1601
|
+
};
|
|
1602
|
+
const forwardedArgs = [];
|
|
1603
|
+
|
|
1604
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1605
|
+
const arg = rawArgs[index];
|
|
1606
|
+
if (arg === '--verbose-auto-finish') {
|
|
1607
|
+
doctorDefaults.verboseAutoFinish = true;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (arg === '--compact-auto-finish') {
|
|
1611
|
+
doctorDefaults.verboseAutoFinish = false;
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
forwardedArgs.push(arg);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return parseRepoTraversalArgs(forwardedArgs, doctorDefaults);
|
|
1135
1618
|
}
|
|
1136
1619
|
|
|
1137
1620
|
function normalizeWorkspacePath(relativePath) {
|
|
@@ -1230,7 +1713,7 @@ function assertProtectedMainWriteAllowed(options, commandName) {
|
|
|
1230
1713
|
throw new Error(
|
|
1231
1714
|
`${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
|
|
1232
1715
|
`Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
|
|
1233
|
-
`
|
|
1716
|
+
` gx branch start "<task>" "codex"\n` +
|
|
1234
1717
|
`Override once only when intentional: --allow-protected-base-write`,
|
|
1235
1718
|
);
|
|
1236
1719
|
}
|
|
@@ -1309,6 +1792,7 @@ function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
|
1309
1792
|
if (options.skipGitignore) args.push('--no-gitignore');
|
|
1310
1793
|
if (!options.dropStaleLocks) args.push('--keep-stale-locks');
|
|
1311
1794
|
args.push(options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
|
|
1795
|
+
if (options.verboseAutoFinish) args.push('--verbose-auto-finish');
|
|
1312
1796
|
if (options.json) args.push('--json');
|
|
1313
1797
|
return args;
|
|
1314
1798
|
}
|
|
@@ -1455,8 +1939,7 @@ function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
|
1455
1939
|
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1456
1940
|
}
|
|
1457
1941
|
|
|
1458
|
-
const startResult =
|
|
1459
|
-
startScript,
|
|
1942
|
+
const startResult = runPackageAsset('branchStart', [
|
|
1460
1943
|
'--task',
|
|
1461
1944
|
taskName,
|
|
1462
1945
|
'--agent',
|
|
@@ -1605,8 +2088,7 @@ function collectWorktreeDirtyPaths(worktreePath) {
|
|
|
1605
2088
|
}
|
|
1606
2089
|
|
|
1607
2090
|
function collectDoctorForceAddPaths(worktreePath) {
|
|
1608
|
-
return
|
|
1609
|
-
.map((entry) => toDestinationPath(entry))
|
|
2091
|
+
return REQUIRED_WORKFLOW_FILES
|
|
1610
2092
|
.filter((relativePath) => relativePath.startsWith('scripts/') || relativePath.startsWith('.githooks/'))
|
|
1611
2093
|
.filter((relativePath) => fs.existsSync(path.join(worktreePath, relativePath)));
|
|
1612
2094
|
}
|
|
@@ -1658,13 +2140,13 @@ function claimDoctorChangedLocks(metadata) {
|
|
|
1658
2140
|
]));
|
|
1659
2141
|
const deletedPaths = collectDoctorDeletedPaths(metadata.worktreePath);
|
|
1660
2142
|
if (changedPaths.length > 0) {
|
|
1661
|
-
|
|
2143
|
+
runPackageAsset('lockTool', ['claim', '--branch', metadata.branch, ...changedPaths], {
|
|
1662
2144
|
cwd: metadata.worktreePath,
|
|
1663
2145
|
timeout: 30_000,
|
|
1664
2146
|
});
|
|
1665
2147
|
}
|
|
1666
2148
|
if (deletedPaths.length > 0) {
|
|
1667
|
-
|
|
2149
|
+
runPackageAsset('lockTool', ['allow-delete', '--branch', metadata.branch, ...deletedPaths], {
|
|
1668
2150
|
cwd: metadata.worktreePath,
|
|
1669
2151
|
timeout: 30_000,
|
|
1670
2152
|
});
|
|
@@ -1810,7 +2292,7 @@ function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
|
|
|
1810
2292
|
|
|
1811
2293
|
const finishResult = run(
|
|
1812
2294
|
'bash',
|
|
1813
|
-
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg],
|
|
2295
|
+
[finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg, '--cleanup'],
|
|
1814
2296
|
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
|
|
1815
2297
|
);
|
|
1816
2298
|
if (isSpawnFailure(finishResult)) {
|
|
@@ -1881,7 +2363,7 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
|
|
|
1881
2363
|
...(autoCommitResult.stagedFiles || []),
|
|
1882
2364
|
...OMX_SCAFFOLD_DIRECTORIES,
|
|
1883
2365
|
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
1884
|
-
...
|
|
2366
|
+
...REQUIRED_WORKFLOW_FILES,
|
|
1885
2367
|
'bin',
|
|
1886
2368
|
'package.json',
|
|
1887
2369
|
'.gitignore',
|
|
@@ -2019,9 +2501,7 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
|
|
|
2019
2501
|
}
|
|
2020
2502
|
|
|
2021
2503
|
function syncDoctorLocalSupportFiles(repoRoot, dryRun) {
|
|
2022
|
-
return
|
|
2023
|
-
.filter((entry) => entry.startsWith('codex/') || entry.startsWith('claude/'))
|
|
2024
|
-
.map((entry) => ensureTemplateFilePresent(repoRoot, entry, dryRun));
|
|
2504
|
+
return [];
|
|
2025
2505
|
}
|
|
2026
2506
|
|
|
2027
2507
|
function runDoctorInSandbox(options, blocked) {
|
|
@@ -2207,6 +2687,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
2207
2687
|
postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
|
|
2208
2688
|
baseBranch: blocked.branch,
|
|
2209
2689
|
dryRun: options.dryRun,
|
|
2690
|
+
waitForMerge: options.waitForMerge,
|
|
2210
2691
|
excludeBranches: [metadata.branch],
|
|
2211
2692
|
});
|
|
2212
2693
|
}
|
|
@@ -2300,23 +2781,17 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
2300
2781
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
2301
2782
|
} else if (finishResult.status === 'failed') {
|
|
2302
2783
|
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
2303
|
-
console.log(`[
|
|
2784
|
+
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
2304
2785
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
2305
2786
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
2306
2787
|
} else {
|
|
2307
2788
|
console.log(`[${TOOL_NAME}] Auto-finish skipped: ${finishResult.note}.`);
|
|
2308
2789
|
}
|
|
2309
2790
|
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
for (const detail of postSandboxAutoFinishSummary.details) {
|
|
2315
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
2316
|
-
}
|
|
2317
|
-
} else if (postSandboxAutoFinishSummary.details.length > 0) {
|
|
2318
|
-
console.log(`[${TOOL_NAME}] ${postSandboxAutoFinishSummary.details[0]}`);
|
|
2319
|
-
}
|
|
2791
|
+
printAutoFinishSummary(postSandboxAutoFinishSummary, {
|
|
2792
|
+
baseBranch: blocked.branch,
|
|
2793
|
+
verbose: options.verboseAutoFinish,
|
|
2794
|
+
});
|
|
2320
2795
|
if (omxScaffoldSyncResult.status === 'synced') {
|
|
2321
2796
|
console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
|
|
2322
2797
|
} else if (omxScaffoldSyncResult.status === 'unchanged') {
|
|
@@ -2871,6 +3346,7 @@ function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
|
2871
3346
|
function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
2872
3347
|
const baseBranch = String(options.baseBranch || '').trim();
|
|
2873
3348
|
const dryRun = Boolean(options.dryRun);
|
|
3349
|
+
const waitForMerge = options.waitForMerge !== false;
|
|
2874
3350
|
const excludedBranches = new Set(
|
|
2875
3351
|
Array.isArray(options.excludeBranches)
|
|
2876
3352
|
? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
|
|
@@ -2983,16 +3459,15 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
2983
3459
|
|
|
2984
3460
|
summary.attempted += 1;
|
|
2985
3461
|
const finishArgs = [
|
|
2986
|
-
finishScript,
|
|
2987
3462
|
'--branch',
|
|
2988
3463
|
branch,
|
|
2989
3464
|
'--base',
|
|
2990
3465
|
baseBranch,
|
|
2991
3466
|
'--via-pr',
|
|
2992
|
-
'--wait-for-merge',
|
|
3467
|
+
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
2993
3468
|
'--cleanup',
|
|
2994
3469
|
];
|
|
2995
|
-
const finishResult =
|
|
3470
|
+
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot });
|
|
2996
3471
|
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
2997
3472
|
|
|
2998
3473
|
if (finishResult.status === 0) {
|
|
@@ -3145,9 +3620,9 @@ function printSetupRepoHints(repoRoot, baseBranch, repoLabel = '') {
|
|
|
3145
3620
|
console.log(`[${TOOL_NAME}] Bootstrap commit${label}: git add . && git commit -m "bootstrap gitguardex"`);
|
|
3146
3621
|
console.log(
|
|
3147
3622
|
`[${TOOL_NAME}] First agent flow${label}: ` +
|
|
3148
|
-
`
|
|
3149
|
-
`
|
|
3150
|
-
`
|
|
3623
|
+
`gx branch start "<task>" "codex" -> ` +
|
|
3624
|
+
`gx locks claim --branch "$(git branch --show-current)" <file...> -> ` +
|
|
3625
|
+
`gx branch finish --branch "$(git branch --show-current)" --base ${baseBranch} --via-pr --wait-for-merge`,
|
|
3151
3626
|
);
|
|
3152
3627
|
}
|
|
3153
3628
|
if (!hasOrigin) {
|
|
@@ -3419,19 +3894,96 @@ function parseCleanupArgs(rawArgs) {
|
|
|
3419
3894
|
return options;
|
|
3420
3895
|
}
|
|
3421
3896
|
|
|
3422
|
-
function
|
|
3897
|
+
function parseMergeArgs(rawArgs) {
|
|
3898
|
+
const options = {
|
|
3899
|
+
target: process.cwd(),
|
|
3900
|
+
base: '',
|
|
3901
|
+
into: '',
|
|
3902
|
+
branches: [],
|
|
3903
|
+
task: '',
|
|
3904
|
+
agent: '',
|
|
3905
|
+
};
|
|
3906
|
+
|
|
3907
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
3908
|
+
const arg = rawArgs[index];
|
|
3909
|
+
if (arg === '--target') {
|
|
3910
|
+
const next = rawArgs[index + 1];
|
|
3911
|
+
if (!next) {
|
|
3912
|
+
throw new Error('--target requires a path value');
|
|
3913
|
+
}
|
|
3914
|
+
options.target = next;
|
|
3915
|
+
index += 1;
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
if (arg === '--base') {
|
|
3919
|
+
const next = rawArgs[index + 1];
|
|
3920
|
+
if (!next) {
|
|
3921
|
+
throw new Error('--base requires a branch value');
|
|
3922
|
+
}
|
|
3923
|
+
options.base = next;
|
|
3924
|
+
index += 1;
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
if (arg === '--into') {
|
|
3928
|
+
const next = rawArgs[index + 1];
|
|
3929
|
+
if (!next) {
|
|
3930
|
+
throw new Error('--into requires an agent/* branch value');
|
|
3931
|
+
}
|
|
3932
|
+
options.into = next;
|
|
3933
|
+
index += 1;
|
|
3934
|
+
continue;
|
|
3935
|
+
}
|
|
3936
|
+
if (arg === '--branch') {
|
|
3937
|
+
const next = rawArgs[index + 1];
|
|
3938
|
+
if (!next) {
|
|
3939
|
+
throw new Error('--branch requires an agent/* branch value');
|
|
3940
|
+
}
|
|
3941
|
+
options.branches.push(next);
|
|
3942
|
+
index += 1;
|
|
3943
|
+
continue;
|
|
3944
|
+
}
|
|
3945
|
+
if (arg === '--task') {
|
|
3946
|
+
const next = rawArgs[index + 1];
|
|
3947
|
+
if (!next) {
|
|
3948
|
+
throw new Error('--task requires a task value');
|
|
3949
|
+
}
|
|
3950
|
+
options.task = next;
|
|
3951
|
+
index += 1;
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3954
|
+
if (arg === '--agent') {
|
|
3955
|
+
const next = rawArgs[index + 1];
|
|
3956
|
+
if (!next) {
|
|
3957
|
+
throw new Error('--agent requires an agent value');
|
|
3958
|
+
}
|
|
3959
|
+
options.agent = next;
|
|
3960
|
+
index += 1;
|
|
3961
|
+
continue;
|
|
3962
|
+
}
|
|
3963
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
if (options.branches.length === 0) {
|
|
3967
|
+
throw new Error('merge requires at least one --branch <agent/*> input');
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
return options;
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
function parseFinishArgs(rawArgs, defaults = {}) {
|
|
3423
3974
|
const options = {
|
|
3424
3975
|
target: process.cwd(),
|
|
3425
3976
|
base: '',
|
|
3426
3977
|
branch: '',
|
|
3427
3978
|
all: false,
|
|
3428
3979
|
dryRun: false,
|
|
3429
|
-
waitForMerge: true,
|
|
3430
|
-
cleanup: true,
|
|
3980
|
+
waitForMerge: defaults.waitForMerge ?? true,
|
|
3981
|
+
cleanup: defaults.cleanup ?? true,
|
|
3431
3982
|
keepRemote: false,
|
|
3432
3983
|
noAutoCommit: false,
|
|
3433
3984
|
failFast: false,
|
|
3434
3985
|
commitMessage: '',
|
|
3986
|
+
mergeMode: defaults.mergeMode || 'pr',
|
|
3435
3987
|
};
|
|
3436
3988
|
|
|
3437
3989
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -3488,6 +4040,26 @@ function parseFinishArgs(rawArgs) {
|
|
|
3488
4040
|
options.waitForMerge = false;
|
|
3489
4041
|
continue;
|
|
3490
4042
|
}
|
|
4043
|
+
if (arg === '--via-pr') {
|
|
4044
|
+
options.mergeMode = 'pr';
|
|
4045
|
+
continue;
|
|
4046
|
+
}
|
|
4047
|
+
if (arg === '--direct-only') {
|
|
4048
|
+
options.mergeMode = 'direct';
|
|
4049
|
+
continue;
|
|
4050
|
+
}
|
|
4051
|
+
if (arg === '--mode') {
|
|
4052
|
+
const next = rawArgs[index + 1];
|
|
4053
|
+
if (!next) {
|
|
4054
|
+
throw new Error('--mode requires a value');
|
|
4055
|
+
}
|
|
4056
|
+
if (!['auto', 'direct', 'pr'].includes(next)) {
|
|
4057
|
+
throw new Error(`Invalid --mode value: ${next} (expected auto|direct|pr)`);
|
|
4058
|
+
}
|
|
4059
|
+
options.mergeMode = next;
|
|
4060
|
+
index += 1;
|
|
4061
|
+
continue;
|
|
4062
|
+
}
|
|
3491
4063
|
if (arg === '--cleanup') {
|
|
3492
4064
|
options.cleanup = true;
|
|
3493
4065
|
continue;
|
|
@@ -3652,7 +4224,7 @@ function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
|
3652
4224
|
]);
|
|
3653
4225
|
|
|
3654
4226
|
if (changedFiles.length > 0) {
|
|
3655
|
-
const claim =
|
|
4227
|
+
const claim = runPackageAsset('lockTool', ['claim', '--branch', branch, ...changedFiles], {
|
|
3656
4228
|
cwd: repoRoot,
|
|
3657
4229
|
stdio: 'pipe',
|
|
3658
4230
|
});
|
|
@@ -3686,7 +4258,7 @@ function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
|
3686
4258
|
]);
|
|
3687
4259
|
|
|
3688
4260
|
if (deletedFiles.length > 0) {
|
|
3689
|
-
const allowDelete =
|
|
4261
|
+
const allowDelete = runPackageAsset('lockTool', ['allow-delete', '--branch', branch, ...deletedFiles], {
|
|
3690
4262
|
cwd: repoRoot,
|
|
3691
4263
|
stdio: 'pipe',
|
|
3692
4264
|
});
|
|
@@ -4464,6 +5036,16 @@ function askGlobalInstallForMissing(options, missingPackages, missingLocalTools)
|
|
|
4464
5036
|
}
|
|
4465
5037
|
|
|
4466
5038
|
function installGlobalToolchain(options) {
|
|
5039
|
+
const approval = resolveGlobalInstallApproval(options);
|
|
5040
|
+
if (approval.source === 'flag' && !approval.approved) {
|
|
5041
|
+
return {
|
|
5042
|
+
status: 'skipped',
|
|
5043
|
+
reason: approval.source,
|
|
5044
|
+
missingPackages: [],
|
|
5045
|
+
missingLocalTools: [],
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
|
|
4467
5049
|
if (options.dryRun) {
|
|
4468
5050
|
return { status: 'dry-run-skip' };
|
|
4469
5051
|
}
|
|
@@ -4492,11 +5074,11 @@ function installGlobalToolchain(options) {
|
|
|
4492
5074
|
|
|
4493
5075
|
const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
|
|
4494
5076
|
const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
|
|
4495
|
-
const
|
|
4496
|
-
if (!
|
|
5077
|
+
const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
|
|
5078
|
+
if (!installApproval.approved) {
|
|
4497
5079
|
return {
|
|
4498
5080
|
status: 'skipped',
|
|
4499
|
-
reason:
|
|
5081
|
+
reason: installApproval.source,
|
|
4500
5082
|
missingPackages,
|
|
4501
5083
|
missingLocalTools,
|
|
4502
5084
|
};
|
|
@@ -4591,13 +5173,15 @@ function runInstallInternal(options) {
|
|
|
4591
5173
|
for (const templateFile of TEMPLATE_FILES) {
|
|
4592
5174
|
operations.push(copyTemplateFile(repoRoot, templateFile, Boolean(options.force), Boolean(options.dryRun)));
|
|
4593
5175
|
}
|
|
5176
|
+
for (const shim of SCRIPT_SHIMS) {
|
|
5177
|
+
operations.push(ensureGeneratedScriptShim(repoRoot, shim, options));
|
|
5178
|
+
}
|
|
5179
|
+
for (const hookName of HOOK_NAMES) {
|
|
5180
|
+
operations.push(ensureHookShim(repoRoot, hookName, options));
|
|
5181
|
+
}
|
|
4594
5182
|
|
|
4595
5183
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
4596
5184
|
|
|
4597
|
-
if (!options.skipPackageJson) {
|
|
4598
|
-
operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4599
|
-
}
|
|
4600
|
-
|
|
4601
5185
|
if (!options.skipAgents) {
|
|
4602
5186
|
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4603
5187
|
}
|
|
@@ -4636,6 +5220,12 @@ function runFixInternal(options) {
|
|
|
4636
5220
|
for (const templateFile of TEMPLATE_FILES) {
|
|
4637
5221
|
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
4638
5222
|
}
|
|
5223
|
+
for (const shim of SCRIPT_SHIMS) {
|
|
5224
|
+
operations.push(ensureGeneratedScriptShim(repoRoot, shim, options));
|
|
5225
|
+
}
|
|
5226
|
+
for (const hookName of HOOK_NAMES) {
|
|
5227
|
+
operations.push(ensureHookShim(repoRoot, hookName, options));
|
|
5228
|
+
}
|
|
4639
5229
|
|
|
4640
5230
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
4641
5231
|
|
|
@@ -4665,10 +5255,6 @@ function runFixInternal(options) {
|
|
|
4665
5255
|
}
|
|
4666
5256
|
}
|
|
4667
5257
|
|
|
4668
|
-
if (!options.skipPackageJson) {
|
|
4669
|
-
operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4670
|
-
}
|
|
4671
|
-
|
|
4672
5258
|
if (!options.skipAgents) {
|
|
4673
5259
|
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4674
5260
|
}
|
|
@@ -4698,8 +5284,7 @@ function runScanInternal(options) {
|
|
|
4698
5284
|
const requiredPaths = [
|
|
4699
5285
|
...OMX_SCAFFOLD_DIRECTORIES,
|
|
4700
5286
|
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
4701
|
-
...
|
|
4702
|
-
LOCK_FILE_RELATIVE,
|
|
5287
|
+
...REQUIRED_WORKFLOW_FILES,
|
|
4703
5288
|
];
|
|
4704
5289
|
|
|
4705
5290
|
for (const relativePath of requiredPaths) {
|
|
@@ -4834,21 +5419,34 @@ function printScanResult(scan, json = false) {
|
|
|
4834
5419
|
|
|
4835
5420
|
if (scan.guardexEnabled === false) {
|
|
4836
5421
|
console.log(
|
|
4837
|
-
|
|
5422
|
+
colorizeDoctorOutput(
|
|
5423
|
+
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
|
|
5424
|
+
'disabled',
|
|
5425
|
+
),
|
|
4838
5426
|
);
|
|
4839
5427
|
return;
|
|
4840
5428
|
}
|
|
4841
5429
|
|
|
4842
5430
|
if (scan.findings.length === 0) {
|
|
4843
|
-
console.log(`[${TOOL_NAME}] ✅ No safety issues detected
|
|
5431
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ No safety issues detected.`, 'safe'));
|
|
4844
5432
|
return;
|
|
4845
5433
|
}
|
|
4846
5434
|
|
|
4847
5435
|
for (const item of scan.findings) {
|
|
4848
5436
|
const target = item.path ? ` (${item.path})` : '';
|
|
4849
|
-
console.log(
|
|
5437
|
+
console.log(
|
|
5438
|
+
colorizeDoctorOutput(
|
|
5439
|
+
`[${item.level.toUpperCase()}] ${item.code}${target}: ${item.message}`,
|
|
5440
|
+
item.level,
|
|
5441
|
+
),
|
|
5442
|
+
);
|
|
4850
5443
|
}
|
|
4851
|
-
console.log(
|
|
5444
|
+
console.log(
|
|
5445
|
+
colorizeDoctorOutput(
|
|
5446
|
+
`[${TOOL_NAME}] Summary: ${scan.errors} error(s), ${scan.warnings} warning(s).`,
|
|
5447
|
+
scan.errors > 0 ? 'error' : 'warn',
|
|
5448
|
+
),
|
|
5449
|
+
);
|
|
4852
5450
|
}
|
|
4853
5451
|
|
|
4854
5452
|
function setExitCodeFromScan(scan) {
|
|
@@ -5127,31 +5725,38 @@ function doctor(rawArgs) {
|
|
|
5127
5725
|
|
|
5128
5726
|
const repoResults = [];
|
|
5129
5727
|
let aggregateExitCode = 0;
|
|
5130
|
-
for (
|
|
5728
|
+
for (let repoIndex = 0; repoIndex < discoveredRepos.length; repoIndex += 1) {
|
|
5729
|
+
const repoPath = discoveredRepos[repoIndex];
|
|
5730
|
+
const progressLabel = `${repoIndex + 1}/${discoveredRepos.length}`;
|
|
5131
5731
|
if (!options.json) {
|
|
5132
|
-
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} ──`);
|
|
5732
|
+
console.log(`[${TOOL_NAME}] ── Doctor target: ${repoPath} [${progressLabel}] ──`);
|
|
5133
5733
|
}
|
|
5134
5734
|
|
|
5135
|
-
const
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5735
|
+
const childArgs = [
|
|
5736
|
+
path.resolve(__filename),
|
|
5737
|
+
'doctor',
|
|
5738
|
+
'--single-repo',
|
|
5739
|
+
'--target',
|
|
5740
|
+
repoPath,
|
|
5741
|
+
...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
|
|
5742
|
+
...(options.skipAgents ? ['--skip-agents'] : []),
|
|
5743
|
+
...(options.skipPackageJson ? ['--skip-package-json'] : []),
|
|
5744
|
+
...(options.skipGitignore ? ['--no-gitignore'] : []),
|
|
5745
|
+
...(options.dryRun ? ['--dry-run'] : []),
|
|
5746
|
+
// Recursive child doctor runs should report pending PR state immediately instead of blocking the parent loop.
|
|
5747
|
+
'--no-wait-for-merge',
|
|
5748
|
+
...(options.verboseAutoFinish ? ['--verbose-auto-finish'] : []),
|
|
5749
|
+
...(options.json ? ['--json'] : []),
|
|
5750
|
+
...(options.allowProtectedBaseWrite ? ['--allow-protected-base-write'] : []),
|
|
5751
|
+
];
|
|
5752
|
+
const startedAt = Date.now();
|
|
5753
|
+
const nestedResult = options.json
|
|
5754
|
+
? run(process.execPath, childArgs, { cwd: topRepoRoot })
|
|
5755
|
+
: cp.spawnSync(process.execPath, childArgs, {
|
|
5756
|
+
cwd: topRepoRoot,
|
|
5757
|
+
encoding: 'utf8',
|
|
5758
|
+
stdio: 'inherit',
|
|
5759
|
+
});
|
|
5155
5760
|
if (isSpawnFailure(nestedResult)) {
|
|
5156
5761
|
throw nestedResult.error;
|
|
5157
5762
|
}
|
|
@@ -5181,9 +5786,12 @@ function doctor(rawArgs) {
|
|
|
5181
5786
|
},
|
|
5182
5787
|
);
|
|
5183
5788
|
} else {
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5789
|
+
console.log(
|
|
5790
|
+
`[${TOOL_NAME}] Doctor target complete: ${repoPath} [${progressLabel}] in ${formatElapsedDuration(Date.now() - startedAt)}.`,
|
|
5791
|
+
);
|
|
5792
|
+
if (repoIndex < discoveredRepos.length - 1) {
|
|
5793
|
+
process.stdout.write('\n');
|
|
5794
|
+
}
|
|
5187
5795
|
}
|
|
5188
5796
|
}
|
|
5189
5797
|
|
|
@@ -5232,6 +5840,7 @@ function doctor(rawArgs) {
|
|
|
5232
5840
|
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
5233
5841
|
baseBranch: currentBaseBranch,
|
|
5234
5842
|
dryRun: singleRepoOptions.dryRun,
|
|
5843
|
+
waitForMerge: singleRepoOptions.waitForMerge,
|
|
5235
5844
|
});
|
|
5236
5845
|
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
5237
5846
|
const musafe = safe;
|
|
@@ -5273,21 +5882,18 @@ function doctor(rawArgs) {
|
|
|
5273
5882
|
setExitCodeFromScan(scanResult);
|
|
5274
5883
|
return;
|
|
5275
5884
|
}
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
for (const detail of autoFinishSummary.details) {
|
|
5281
|
-
console.log(`[${TOOL_NAME}] ${detail}`);
|
|
5282
|
-
}
|
|
5283
|
-
} else if (autoFinishSummary.details.length > 0) {
|
|
5284
|
-
console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
|
|
5285
|
-
}
|
|
5885
|
+
printAutoFinishSummary(autoFinishSummary, {
|
|
5886
|
+
baseBranch: currentBaseBranch,
|
|
5887
|
+
verbose: singleRepoOptions.verboseAutoFinish,
|
|
5888
|
+
});
|
|
5286
5889
|
if (safe) {
|
|
5287
|
-
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe
|
|
5890
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ Repo is fully safe.`, 'safe'));
|
|
5288
5891
|
} else {
|
|
5289
5892
|
console.log(
|
|
5290
|
-
|
|
5893
|
+
colorizeDoctorOutput(
|
|
5894
|
+
`[${TOOL_NAME}] ⚠️ Repo is not fully safe yet (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
5895
|
+
scanResult.errors > 0 ? 'unsafe' : 'warn',
|
|
5896
|
+
),
|
|
5291
5897
|
);
|
|
5292
5898
|
}
|
|
5293
5899
|
setExitCodeFromScan(scanResult);
|
|
@@ -6297,17 +6903,18 @@ function doctorAudit(rawArgs) {
|
|
|
6297
6903
|
|
|
6298
6904
|
const packagePath = path.join(repoRoot, 'package.json');
|
|
6299
6905
|
if (!fs.existsSync(packagePath)) {
|
|
6300
|
-
warn('package.json not found (
|
|
6906
|
+
warn('package.json not found (legacy agent:* script drift cannot be checked)');
|
|
6301
6907
|
} else {
|
|
6302
6908
|
try {
|
|
6303
6909
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
6304
6910
|
const scripts = pkg.scripts || {};
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6911
|
+
const legacyAgentScripts = Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)
|
|
6912
|
+
.filter(([name, expectedValue]) => scripts[name] === expectedValue)
|
|
6913
|
+
.map(([name]) => name);
|
|
6914
|
+
if (legacyAgentScripts.length > 0) {
|
|
6915
|
+
warn(`legacy agent:* package.json scripts remain (${legacyAgentScripts.join(', ')}); run '${SHORT_TOOL_NAME} migrate' to remove them`);
|
|
6916
|
+
} else {
|
|
6917
|
+
ok('package.json does not contain Guardex-managed agent:* helper scripts');
|
|
6311
6918
|
}
|
|
6312
6919
|
} catch (error) {
|
|
6313
6920
|
fail(`package.json is invalid JSON: ${error.message}`);
|
|
@@ -6389,6 +6996,167 @@ function prompt(rawArgs) {
|
|
|
6389
6996
|
return copyPrompt();
|
|
6390
6997
|
}
|
|
6391
6998
|
|
|
6999
|
+
function printStandaloneOperations(title, rootLabel, operations, dryRun = false) {
|
|
7000
|
+
console.log(`[${TOOL_NAME}] ${title}: ${rootLabel}`);
|
|
7001
|
+
for (const operation of operations) {
|
|
7002
|
+
const note = operation.note ? ` (${operation.note})` : '';
|
|
7003
|
+
console.log(` - ${operation.status.padEnd(12)} ${operation.file}${note}`);
|
|
7004
|
+
}
|
|
7005
|
+
if (dryRun) {
|
|
7006
|
+
console.log(`[${TOOL_NAME}] Dry run complete. No files were modified.`);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
|
|
7010
|
+
function branch(rawArgs) {
|
|
7011
|
+
const [subcommand, ...rest] = rawArgs;
|
|
7012
|
+
if (subcommand === 'start') {
|
|
7013
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7014
|
+
invokePackageAsset('branchStart', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
if (subcommand === 'finish') {
|
|
7018
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7019
|
+
invokePackageAsset('branchFinish', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7020
|
+
return;
|
|
7021
|
+
}
|
|
7022
|
+
if (subcommand === 'merge') return merge(rest);
|
|
7023
|
+
throw new Error(
|
|
7024
|
+
`Usage: ${SHORT_TOOL_NAME} branch <start|finish|merge> [options] ` +
|
|
7025
|
+
`(examples: '${SHORT_TOOL_NAME} branch start "<task>" "<agent>"', '${SHORT_TOOL_NAME} branch finish --branch <agent/...>')`,
|
|
7026
|
+
);
|
|
7027
|
+
}
|
|
7028
|
+
|
|
7029
|
+
function locks(rawArgs) {
|
|
7030
|
+
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
7031
|
+
const result = runPackageAsset('lockTool', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7032
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7033
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7034
|
+
process.exitCode = result.status;
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
function worktree(rawArgs) {
|
|
7038
|
+
const [subcommand, ...rest] = rawArgs;
|
|
7039
|
+
if (subcommand === 'prune') {
|
|
7040
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7041
|
+
invokePackageAsset('worktreePrune', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7042
|
+
return;
|
|
7043
|
+
}
|
|
7044
|
+
throw new Error(`Usage: ${SHORT_TOOL_NAME} worktree prune [cleanup-options]`);
|
|
7045
|
+
}
|
|
7046
|
+
|
|
7047
|
+
function hook(rawArgs) {
|
|
7048
|
+
const [subcommand, ...rest] = rawArgs;
|
|
7049
|
+
if (subcommand === 'run') {
|
|
7050
|
+
const [hookName, ...hookArgs] = rest;
|
|
7051
|
+
if (!HOOK_NAMES.includes(hookName)) {
|
|
7052
|
+
throw new Error(`Unknown hook name: ${hookName || '(missing)'}`);
|
|
7053
|
+
}
|
|
7054
|
+
const { target, passthrough } = extractTargetedArgs(hookArgs);
|
|
7055
|
+
const hookAssetPath = path.join(TEMPLATE_ROOT, 'githooks', hookName);
|
|
7056
|
+
const result = run('bash', [hookAssetPath, ...passthrough], {
|
|
7057
|
+
cwd: resolveRepoRoot(target),
|
|
7058
|
+
stdio: hookName === 'pre-push' ? 'inherit' : 'pipe',
|
|
7059
|
+
env: packageAssetEnv(),
|
|
7060
|
+
});
|
|
7061
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7062
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7063
|
+
process.exitCode = result.status;
|
|
7064
|
+
return;
|
|
7065
|
+
}
|
|
7066
|
+
if (subcommand === 'install') {
|
|
7067
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7068
|
+
if (passthrough.length > 0) {
|
|
7069
|
+
throw new Error(`Unknown hook install option: ${passthrough[0]}`);
|
|
7070
|
+
}
|
|
7071
|
+
const repoRoot = resolveRepoRoot(target);
|
|
7072
|
+
const hookResult = configureHooks(repoRoot, false);
|
|
7073
|
+
console.log(`[${TOOL_NAME}] Hook install target: ${repoRoot}`);
|
|
7074
|
+
console.log(` - hooksPath ${hookResult.status} ${hookResult.key}=${hookResult.value}`);
|
|
7075
|
+
process.exitCode = 0;
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
throw new Error(`Usage: ${SHORT_TOOL_NAME} hook <run|install> ...`);
|
|
7079
|
+
}
|
|
7080
|
+
|
|
7081
|
+
function internal(rawArgs) {
|
|
7082
|
+
const [subcommand, assetKey, ...rest] = rawArgs;
|
|
7083
|
+
if (subcommand !== 'run-shell') {
|
|
7084
|
+
throw new Error(`Unknown internal command: ${subcommand || '(missing)'}`);
|
|
7085
|
+
}
|
|
7086
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7087
|
+
const result = runPackageAsset(assetKey, passthrough, { cwd: resolveRepoRoot(target) });
|
|
7088
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7089
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7090
|
+
process.exitCode = result.status;
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
function installAgentSkills(rawArgs) {
|
|
7094
|
+
let dryRun = false;
|
|
7095
|
+
let force = false;
|
|
7096
|
+
for (const arg of rawArgs) {
|
|
7097
|
+
if (arg === '--dry-run') {
|
|
7098
|
+
dryRun = true;
|
|
7099
|
+
continue;
|
|
7100
|
+
}
|
|
7101
|
+
if (arg === '--force') {
|
|
7102
|
+
force = true;
|
|
7103
|
+
continue;
|
|
7104
|
+
}
|
|
7105
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
7106
|
+
}
|
|
7107
|
+
|
|
7108
|
+
const operations = USER_LEVEL_SKILL_ASSETS.map((asset) => installUserLevelAsset(asset, { dryRun, force }));
|
|
7109
|
+
printStandaloneOperations('User-level Guardex skills', GUARDEX_HOME_DIR, operations, dryRun);
|
|
7110
|
+
process.exitCode = 0;
|
|
7111
|
+
}
|
|
7112
|
+
|
|
7113
|
+
function migrate(rawArgs) {
|
|
7114
|
+
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
7115
|
+
let dryRun = false;
|
|
7116
|
+
let force = false;
|
|
7117
|
+
let installSkills = false;
|
|
7118
|
+
for (const arg of passthrough) {
|
|
7119
|
+
if (arg === '--dry-run') {
|
|
7120
|
+
dryRun = true;
|
|
7121
|
+
continue;
|
|
7122
|
+
}
|
|
7123
|
+
if (arg === '--force') {
|
|
7124
|
+
force = true;
|
|
7125
|
+
continue;
|
|
7126
|
+
}
|
|
7127
|
+
if (arg === '--install-agent-skills') {
|
|
7128
|
+
installSkills = true;
|
|
7129
|
+
continue;
|
|
7130
|
+
}
|
|
7131
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
7132
|
+
}
|
|
7133
|
+
|
|
7134
|
+
const repoRoot = resolveRepoRoot(target);
|
|
7135
|
+
const fixPayload = runFixInternal({
|
|
7136
|
+
target: repoRoot,
|
|
7137
|
+
dryRun,
|
|
7138
|
+
force,
|
|
7139
|
+
skipAgents: false,
|
|
7140
|
+
skipPackageJson: true,
|
|
7141
|
+
skipGitignore: false,
|
|
7142
|
+
dropStaleLocks: true,
|
|
7143
|
+
});
|
|
7144
|
+
printOperations('Migrate/fix', fixPayload, dryRun);
|
|
7145
|
+
|
|
7146
|
+
if (installSkills) {
|
|
7147
|
+
const skillOps = USER_LEVEL_SKILL_ASSETS.map((asset) => installUserLevelAsset(asset, { dryRun, force }));
|
|
7148
|
+
printStandaloneOperations('Migrate/install-agent-skills', GUARDEX_HOME_DIR, skillOps, dryRun);
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
const removableLegacyFiles = LEGACY_MANAGED_REPO_FILES.filter(
|
|
7152
|
+
(relativePath) => !REQUIRED_WORKFLOW_FILES.includes(relativePath),
|
|
7153
|
+
);
|
|
7154
|
+
const removalOps = removableLegacyFiles.map((relativePath) => removeLegacyManagedRepoFile(repoRoot, relativePath, { dryRun, force }));
|
|
7155
|
+
removalOps.push(removeLegacyPackageScripts(repoRoot, dryRun));
|
|
7156
|
+
printStandaloneOperations('Migrate/cleanup', repoRoot, removalOps, dryRun);
|
|
7157
|
+
process.exitCode = 0;
|
|
7158
|
+
}
|
|
7159
|
+
|
|
6392
7160
|
function cleanup(rawArgs) {
|
|
6393
7161
|
const options = parseCleanupArgs(rawArgs);
|
|
6394
7162
|
const repoRoot = resolveRepoRoot(options.target);
|
|
@@ -6397,7 +7165,7 @@ function cleanup(rawArgs) {
|
|
|
6397
7165
|
throw new Error(`Missing cleanup script: ${pruneScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
6398
7166
|
}
|
|
6399
7167
|
|
|
6400
|
-
const args = [
|
|
7168
|
+
const args = [];
|
|
6401
7169
|
if (options.base) {
|
|
6402
7170
|
args.push('--base', options.base);
|
|
6403
7171
|
}
|
|
@@ -6428,7 +7196,7 @@ function cleanup(rawArgs) {
|
|
|
6428
7196
|
}
|
|
6429
7197
|
|
|
6430
7198
|
const runCleanupCycle = () => {
|
|
6431
|
-
const runResult =
|
|
7199
|
+
const runResult = runPackageAsset('worktreePrune', args, { cwd: repoRoot, stdio: 'inherit' });
|
|
6432
7200
|
if (runResult.status !== 0) {
|
|
6433
7201
|
throw new Error('Cleanup command failed');
|
|
6434
7202
|
}
|
|
@@ -6458,8 +7226,48 @@ function cleanup(rawArgs) {
|
|
|
6458
7226
|
process.exitCode = 0;
|
|
6459
7227
|
}
|
|
6460
7228
|
|
|
6461
|
-
function
|
|
6462
|
-
const options =
|
|
7229
|
+
function merge(rawArgs) {
|
|
7230
|
+
const options = parseMergeArgs(rawArgs);
|
|
7231
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
7232
|
+
const mergeScript = path.join(repoRoot, 'scripts', 'agent-branch-merge.sh');
|
|
7233
|
+
|
|
7234
|
+
if (!fs.existsSync(mergeScript)) {
|
|
7235
|
+
throw new Error(`Missing merge script: ${mergeScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
const args = [];
|
|
7239
|
+
if (options.base) {
|
|
7240
|
+
args.push('--base', options.base);
|
|
7241
|
+
}
|
|
7242
|
+
if (options.into) {
|
|
7243
|
+
args.push('--into', options.into);
|
|
7244
|
+
}
|
|
7245
|
+
if (options.task) {
|
|
7246
|
+
args.push('--task', options.task);
|
|
7247
|
+
}
|
|
7248
|
+
if (options.agent) {
|
|
7249
|
+
args.push('--agent', options.agent);
|
|
7250
|
+
}
|
|
7251
|
+
for (const branch of options.branches) {
|
|
7252
|
+
args.push('--branch', branch);
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
const mergeResult = runPackageAsset('branchMerge', args, { cwd: repoRoot, stdio: 'pipe' });
|
|
7256
|
+
if (mergeResult.stdout) {
|
|
7257
|
+
process.stdout.write(mergeResult.stdout);
|
|
7258
|
+
}
|
|
7259
|
+
if (mergeResult.stderr) {
|
|
7260
|
+
process.stderr.write(mergeResult.stderr);
|
|
7261
|
+
}
|
|
7262
|
+
if (mergeResult.status !== 0) {
|
|
7263
|
+
throw new Error(`merge command failed with status ${mergeResult.status}`);
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
process.exitCode = 0;
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
function finish(rawArgs, defaults = {}) {
|
|
7270
|
+
const options = parseFinishArgs(rawArgs, defaults);
|
|
6463
7271
|
const repoRoot = resolveRepoRoot(options.target);
|
|
6464
7272
|
const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
|
|
6465
7273
|
|
|
@@ -6530,26 +7338,31 @@ function finish(rawArgs) {
|
|
|
6530
7338
|
}
|
|
6531
7339
|
|
|
6532
7340
|
const finishArgs = [
|
|
6533
|
-
finishScript,
|
|
6534
7341
|
'--branch',
|
|
6535
7342
|
branch,
|
|
6536
7343
|
'--base',
|
|
6537
7344
|
baseBranch,
|
|
6538
|
-
'--via-pr',
|
|
6539
7345
|
options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
6540
7346
|
options.cleanup ? '--cleanup' : '--no-cleanup',
|
|
6541
7347
|
];
|
|
7348
|
+
if (options.mergeMode === 'pr') {
|
|
7349
|
+
finishArgs.push('--via-pr');
|
|
7350
|
+
} else if (options.mergeMode === 'direct') {
|
|
7351
|
+
finishArgs.push('--direct-only');
|
|
7352
|
+
} else {
|
|
7353
|
+
finishArgs.push('--mode', 'auto');
|
|
7354
|
+
}
|
|
6542
7355
|
if (options.keepRemote) {
|
|
6543
7356
|
finishArgs.push('--keep-remote-branch');
|
|
6544
7357
|
}
|
|
6545
7358
|
|
|
6546
7359
|
if (options.dryRun) {
|
|
6547
|
-
console.log(`[${TOOL_NAME}] [dry-run] Would run:
|
|
7360
|
+
console.log(`[${TOOL_NAME}] [dry-run] Would run: gx branch finish ${finishArgs.join(' ')}`);
|
|
6548
7361
|
succeeded += 1;
|
|
6549
7362
|
continue;
|
|
6550
7363
|
}
|
|
6551
7364
|
|
|
6552
|
-
const finishResult =
|
|
7365
|
+
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
|
|
6553
7366
|
if (finishResult.stdout) {
|
|
6554
7367
|
process.stdout.write(finishResult.stdout);
|
|
6555
7368
|
}
|
|
@@ -6943,7 +7756,15 @@ function main() {
|
|
|
6943
7756
|
|
|
6944
7757
|
if (command === 'prompt') return prompt(rest);
|
|
6945
7758
|
if (command === 'doctor') return doctor(rest);
|
|
7759
|
+
if (command === 'branch') return branch(rest);
|
|
7760
|
+
if (command === 'locks') return locks(rest);
|
|
7761
|
+
if (command === 'worktree') return worktree(rest);
|
|
7762
|
+
if (command === 'hook') return hook(rest);
|
|
7763
|
+
if (command === 'migrate') return migrate(rest);
|
|
7764
|
+
if (command === 'install-agent-skills') return installAgentSkills(rest);
|
|
7765
|
+
if (command === 'internal') return internal(rest);
|
|
6946
7766
|
if (command === 'agents') return agents(rest);
|
|
7767
|
+
if (command === 'merge') return merge(rest);
|
|
6947
7768
|
if (command === 'finish') return finish(rest);
|
|
6948
7769
|
if (command === 'report') return report(rest);
|
|
6949
7770
|
if (command === 'protect') return protect(rest);
|