@imdeadpool/guardex 7.0.16 → 7.0.19
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 +187 -53
- package/bin/multiagent-safety.js +863 -220
- package/package.json +2 -2
- package/templates/AGENTS.multiagent-safety.md +7 -4
- package/templates/codex/skills/gitguardex/SKILL.md +1 -1
- package/templates/codex/skills/guardex-merge-skills-to-dev/SKILL.md +3 -3
- package/templates/githooks/post-checkout +1 -1
- package/templates/githooks/post-merge +19 -6
- package/templates/githooks/pre-commit +29 -10
- package/templates/scripts/agent-branch-finish.sh +32 -19
- package/templates/scripts/agent-branch-merge.sh +24 -5
- package/templates/scripts/agent-branch-start.sh +23 -29
- package/templates/scripts/agent-file-locks.py +11 -11
- package/templates/scripts/agent-session-state.js +110 -0
- package/templates/scripts/codex-agent.sh +113 -54
- 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 +576 -74
- package/templates/scripts/review-bot-watch.sh +30 -7
- package/templates/vscode/guardex-active-agents/README.md +22 -0
- package/templates/vscode/guardex-active-agents/extension.js +357 -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,47 +92,65 @@ 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/agent-branch-merge.sh',
|
|
93
|
-
'scripts/codex-agent.sh',
|
|
100
|
+
'scripts/agent-session-state.js',
|
|
94
101
|
'scripts/guardex-docker-loader.sh',
|
|
95
|
-
'scripts/review-bot-watch.sh',
|
|
96
|
-
'scripts/agent-worktree-prune.sh',
|
|
97
|
-
'scripts/agent-file-locks.py',
|
|
98
102
|
'scripts/guardex-env.sh',
|
|
99
|
-
'scripts/install-
|
|
100
|
-
'scripts/openspec/init-plan-workspace.sh',
|
|
101
|
-
'scripts/openspec/init-change-workspace.sh',
|
|
102
|
-
'githooks/pre-commit',
|
|
103
|
-
'githooks/pre-push',
|
|
104
|
-
'githooks/post-merge',
|
|
105
|
-
'githooks/post-checkout',
|
|
106
|
-
'codex/skills/gitguardex/SKILL.md',
|
|
107
|
-
'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
108
|
-
'claude/commands/gitguardex.md',
|
|
103
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
109
104
|
'github/pull.yml.example',
|
|
110
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',
|
|
111
110
|
];
|
|
112
111
|
|
|
113
|
-
const
|
|
114
|
-
'scripts/agent-branch-start.sh',
|
|
115
|
-
'scripts/agent-branch-finish.sh',
|
|
116
|
-
'scripts/agent-branch-merge.sh',
|
|
112
|
+
const LEGACY_WORKFLOW_SHIM_SPECS = [
|
|
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_WORKFLOW_SHIMS = LEGACY_WORKFLOW_SHIM_SPECS.map((entry) => entry.relativePath);
|
|
125
|
+
|
|
126
|
+
const MANAGED_TEMPLATE_DESTINATIONS = TEMPLATE_FILES.map((entry) => toDestinationPath(entry));
|
|
127
|
+
const MANAGED_TEMPLATE_SCRIPT_FILES = MANAGED_TEMPLATE_DESTINATIONS.filter((entry) =>
|
|
128
|
+
entry.startsWith('scripts/'),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const LEGACY_MANAGED_REPO_FILES = [
|
|
132
|
+
...LEGACY_WORKFLOW_SHIMS,
|
|
133
|
+
'scripts/agent-session-state.js',
|
|
117
134
|
'scripts/guardex-docker-loader.sh',
|
|
118
|
-
'scripts/
|
|
119
|
-
'scripts/agent-file-locks.py',
|
|
135
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
120
136
|
'scripts/guardex-env.sh',
|
|
121
137
|
'scripts/install-agent-git-hooks.sh',
|
|
122
138
|
'.githooks/pre-commit',
|
|
139
|
+
'.githooks/pre-push',
|
|
123
140
|
'.githooks/post-merge',
|
|
141
|
+
'.githooks/post-checkout',
|
|
142
|
+
'.codex/skills/gitguardex/SKILL.md',
|
|
143
|
+
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
144
|
+
'.claude/commands/gitguardex.md',
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const REQUIRED_MANAGED_REPO_FILES = [
|
|
148
|
+
...MANAGED_TEMPLATE_DESTINATIONS,
|
|
149
|
+
...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
|
|
124
150
|
'.omx/state/agent-file-locks.json',
|
|
125
151
|
];
|
|
126
152
|
|
|
127
|
-
const
|
|
153
|
+
const LEGACY_MANAGED_PACKAGE_SCRIPTS = {
|
|
128
154
|
'agent:codex': 'bash ./scripts/codex-agent.sh',
|
|
129
155
|
'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
|
|
130
156
|
'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
|
|
@@ -149,36 +175,41 @@ const REQUIRED_PACKAGE_SCRIPTS = {
|
|
|
149
175
|
'agent:finish': 'gx finish --all',
|
|
150
176
|
};
|
|
151
177
|
|
|
178
|
+
const PACKAGE_SCRIPT_ASSETS = {
|
|
179
|
+
branchStart: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-start.sh'),
|
|
180
|
+
branchFinish: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-finish.sh'),
|
|
181
|
+
branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'),
|
|
182
|
+
codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'),
|
|
183
|
+
reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'),
|
|
184
|
+
worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'),
|
|
185
|
+
lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'),
|
|
186
|
+
planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'),
|
|
187
|
+
changeInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-change-workspace.sh'),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const USER_LEVEL_SKILL_ASSETS = [
|
|
191
|
+
{
|
|
192
|
+
source: path.join(TEMPLATE_ROOT, 'codex', 'skills', 'gitguardex', 'SKILL.md'),
|
|
193
|
+
destination: path.join('.codex', 'skills', 'gitguardex', 'SKILL.md'),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
source: path.join(TEMPLATE_ROOT, 'codex', 'skills', 'guardex-merge-skills-to-dev', 'SKILL.md'),
|
|
197
|
+
destination: path.join('.codex', 'skills', 'guardex-merge-skills-to-dev', 'SKILL.md'),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
source: path.join(TEMPLATE_ROOT, 'claude', 'commands', 'gitguardex.md'),
|
|
201
|
+
destination: path.join('.claude', 'commands', 'gitguardex.md'),
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
152
205
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
153
|
-
|
|
154
|
-
'
|
|
155
|
-
'scripts/agent-branch-merge.sh',
|
|
156
|
-
'scripts/codex-agent.sh',
|
|
157
|
-
'scripts/guardex-docker-loader.sh',
|
|
158
|
-
'scripts/review-bot-watch.sh',
|
|
159
|
-
'scripts/agent-worktree-prune.sh',
|
|
160
|
-
'scripts/agent-file-locks.py',
|
|
161
|
-
'scripts/install-agent-git-hooks.sh',
|
|
162
|
-
'scripts/openspec/init-plan-workspace.sh',
|
|
163
|
-
'scripts/openspec/init-change-workspace.sh',
|
|
164
|
-
'.githooks/pre-commit',
|
|
165
|
-
'.githooks/pre-push',
|
|
166
|
-
'.githooks/post-merge',
|
|
167
|
-
'.githooks/post-checkout',
|
|
206
|
+
...MANAGED_TEMPLATE_SCRIPT_FILES,
|
|
207
|
+
...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
|
|
168
208
|
]);
|
|
169
209
|
|
|
170
210
|
const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
171
211
|
'AGENTS.md',
|
|
172
|
-
'.githooks
|
|
173
|
-
'.githooks/pre-push',
|
|
174
|
-
'.githooks/post-merge',
|
|
175
|
-
'.githooks/post-checkout',
|
|
176
|
-
'scripts/agent-branch-start.sh',
|
|
177
|
-
'scripts/agent-branch-finish.sh',
|
|
178
|
-
'scripts/agent-branch-merge.sh',
|
|
179
|
-
'scripts/agent-worktree-prune.sh',
|
|
180
|
-
'scripts/codex-agent.sh',
|
|
181
|
-
'scripts/agent-file-locks.py',
|
|
212
|
+
...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
|
|
182
213
|
'scripts/guardex-env.sh',
|
|
183
214
|
]);
|
|
184
215
|
|
|
@@ -197,14 +228,12 @@ const AGENT_WORKTREE_RELATIVE_DIRS = [
|
|
|
197
228
|
const MANAGED_GITIGNORE_PATHS = [
|
|
198
229
|
'.omx/',
|
|
199
230
|
'.omc/',
|
|
200
|
-
'scripts
|
|
201
|
-
'scripts/
|
|
202
|
-
'scripts/
|
|
231
|
+
'scripts/agent-session-state.js',
|
|
232
|
+
'scripts/guardex-docker-loader.sh',
|
|
233
|
+
'scripts/guardex-env.sh',
|
|
234
|
+
'scripts/install-vscode-active-agents-extension.js',
|
|
203
235
|
'.githooks',
|
|
204
236
|
'oh-my-codex/',
|
|
205
|
-
'.codex/skills/gitguardex/SKILL.md',
|
|
206
|
-
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
207
|
-
'.claude/commands/gitguardex.md',
|
|
208
237
|
LOCK_FILE_RELATIVE,
|
|
209
238
|
];
|
|
210
239
|
const REPO_SCAFFOLD_DIRECTORIES = ['bin'];
|
|
@@ -221,6 +250,13 @@ const OMX_SCAFFOLD_FILES = new Map([
|
|
|
221
250
|
['.omx/notepad.md', '\n\n## WORKING MEMORY\n'],
|
|
222
251
|
['.omx/project-memory.json', '{}\n'],
|
|
223
252
|
]);
|
|
253
|
+
const TARGETED_FORCEABLE_MANAGED_PATHS = new Set([
|
|
254
|
+
'AGENTS.md',
|
|
255
|
+
'.gitignore',
|
|
256
|
+
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
257
|
+
...REQUIRED_MANAGED_REPO_FILES,
|
|
258
|
+
...LEGACY_WORKFLOW_SHIMS,
|
|
259
|
+
]);
|
|
224
260
|
const COMMAND_TYPO_ALIASES = new Map([
|
|
225
261
|
['relaese', 'release'],
|
|
226
262
|
['realaese', 'release'],
|
|
@@ -237,6 +273,12 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
237
273
|
'status',
|
|
238
274
|
'setup',
|
|
239
275
|
'doctor',
|
|
276
|
+
'branch',
|
|
277
|
+
'locks',
|
|
278
|
+
'worktree',
|
|
279
|
+
'hook',
|
|
280
|
+
'migrate',
|
|
281
|
+
'install-agent-skills',
|
|
240
282
|
'agents',
|
|
241
283
|
'merge',
|
|
242
284
|
'finish',
|
|
@@ -262,6 +304,12 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
262
304
|
['status', 'Show GitGuardex CLI + service health without modifying files'],
|
|
263
305
|
['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
|
|
264
306
|
['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
|
|
307
|
+
['branch', 'CLI-owned branch workflow surface (start/finish/merge)'],
|
|
308
|
+
['locks', 'CLI-owned file lock surface (claim/allow-delete/release/status/validate)'],
|
|
309
|
+
['worktree', 'CLI-owned worktree cleanup surface (prune)'],
|
|
310
|
+
['hook', 'Hook dispatch/install surface used by managed shims'],
|
|
311
|
+
['migrate', 'Convert legacy repo-local installs to the zero-copy CLI-owned surface'],
|
|
312
|
+
['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
|
|
265
313
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
266
314
|
['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
|
|
267
315
|
['sync', 'Sync agent branches with origin/<base>'],
|
|
@@ -272,7 +320,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
272
320
|
['prompt', 'Print AI setup checklist (--exec, --snippet)'],
|
|
273
321
|
['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
|
|
274
322
|
['help', 'Show this help output'],
|
|
275
|
-
['version', 'Print
|
|
323
|
+
['version', 'Print GitGuardex version'],
|
|
276
324
|
];
|
|
277
325
|
const DEPRECATED_COMMAND_ALIASES = new Map([
|
|
278
326
|
['init', { target: 'setup', hint: 'gx setup' }],
|
|
@@ -306,11 +354,11 @@ function defaultAgentWorktreeRelativeDir(env = process.env) {
|
|
|
306
354
|
|
|
307
355
|
const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in this repo.
|
|
308
356
|
|
|
309
|
-
1) Install:
|
|
357
|
+
1) Install: ${GLOBAL_INSTALL_COMMAND} && gh --version
|
|
310
358
|
2) Bootstrap: gx setup
|
|
311
359
|
3) Repair: gx doctor
|
|
312
|
-
4) Task loop:
|
|
313
|
-
|
|
360
|
+
4) Task loop: gx branch start "<task>" "<agent>"
|
|
361
|
+
then gx locks claim --branch "<agent-branch>" <file...> -> gx branch finish
|
|
314
362
|
5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
|
|
315
363
|
6) Finish: gx finish --all
|
|
316
364
|
7) Cleanup: gx cleanup
|
|
@@ -321,12 +369,12 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
|
|
|
321
369
|
12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
|
|
322
370
|
`;
|
|
323
371
|
|
|
324
|
-
const AI_SETUP_COMMANDS =
|
|
372
|
+
const AI_SETUP_COMMANDS = `${GLOBAL_INSTALL_COMMAND}
|
|
325
373
|
gh --version
|
|
326
374
|
gx setup
|
|
327
375
|
gx doctor
|
|
328
|
-
|
|
329
|
-
|
|
376
|
+
gx branch start "<task>" "<agent>"
|
|
377
|
+
gx locks claim --branch "<agent-branch>" <file...>
|
|
330
378
|
gx merge --branch "<agent-a>" --branch "<agent-b>"
|
|
331
379
|
gx finish --all
|
|
332
380
|
gx cleanup
|
|
@@ -357,7 +405,17 @@ function runtimeVersion() {
|
|
|
357
405
|
}
|
|
358
406
|
|
|
359
407
|
function supportsAnsiColors() {
|
|
360
|
-
|
|
408
|
+
const forced = String(process.env.FORCE_COLOR || '').trim().toLowerCase();
|
|
409
|
+
if (['0', 'false', 'no', 'off'].includes(forced)) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
if (forced.length > 0) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
if (process.env.NO_COLOR) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
return Boolean(process.stdout.isTTY) && process.env.TERM !== 'dumb';
|
|
361
419
|
}
|
|
362
420
|
|
|
363
421
|
function colorize(text, colorCode) {
|
|
@@ -367,6 +425,56 @@ function colorize(text, colorCode) {
|
|
|
367
425
|
return `\u001B[${colorCode}m${text}\u001B[0m`;
|
|
368
426
|
}
|
|
369
427
|
|
|
428
|
+
function doctorOutputColorCode(status) {
|
|
429
|
+
const normalized = String(status || '').trim().toLowerCase();
|
|
430
|
+
if (['active', 'done', 'ok', 'safe', 'success'].includes(normalized)) {
|
|
431
|
+
return '32';
|
|
432
|
+
}
|
|
433
|
+
if (normalized === 'disabled') {
|
|
434
|
+
return '36';
|
|
435
|
+
}
|
|
436
|
+
if (['degraded', 'pending', 'skip', 'warn', 'warning'].includes(normalized)) {
|
|
437
|
+
return '33';
|
|
438
|
+
}
|
|
439
|
+
if (['error', 'fail', 'inactive', 'unsafe'].includes(normalized)) {
|
|
440
|
+
return '31';
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function colorizeDoctorOutput(text, status) {
|
|
446
|
+
const colorCode = doctorOutputColorCode(status);
|
|
447
|
+
return colorCode ? colorize(text, colorCode) : text;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function detectAutoFinishDetailStatus(detail) {
|
|
451
|
+
const trimmed = String(detail || '').trim();
|
|
452
|
+
const match = trimmed.match(/^\[(\w+)\]/);
|
|
453
|
+
if (match) {
|
|
454
|
+
return match[1].toLowerCase();
|
|
455
|
+
}
|
|
456
|
+
if (/^Skipped\b/i.test(trimmed) || /^No local agent branches found\b/i.test(trimmed)) {
|
|
457
|
+
return 'skip';
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function detectAutoFinishSummaryStatus(summary) {
|
|
463
|
+
if (!summary || summary.enabled === false) {
|
|
464
|
+
return detectAutoFinishDetailStatus(summary?.details?.[0]);
|
|
465
|
+
}
|
|
466
|
+
if ((summary.failed || 0) > 0) {
|
|
467
|
+
return 'fail';
|
|
468
|
+
}
|
|
469
|
+
if ((summary.completed || 0) > 0) {
|
|
470
|
+
return 'done';
|
|
471
|
+
}
|
|
472
|
+
if ((summary.skipped || 0) > 0) {
|
|
473
|
+
return 'skip';
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
370
478
|
function statusDot(status) {
|
|
371
479
|
if (status === 'active') {
|
|
372
480
|
return colorize('●', '32'); // green
|
|
@@ -512,10 +620,95 @@ function run(cmd, args, options = {}) {
|
|
|
512
620
|
encoding: 'utf8',
|
|
513
621
|
stdio: options.stdio || 'pipe',
|
|
514
622
|
cwd: options.cwd,
|
|
623
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
624
|
+
timeout: options.timeout,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function extractTargetedArgs(rawArgs, defaultTarget = process.cwd()) {
|
|
629
|
+
const passthrough = [];
|
|
630
|
+
let target = defaultTarget;
|
|
631
|
+
|
|
632
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
633
|
+
const arg = rawArgs[index];
|
|
634
|
+
if (arg === '--target' || arg === '-t') {
|
|
635
|
+
target = requireValue(rawArgs, index, '--target');
|
|
636
|
+
index += 1;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
passthrough.push(arg);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return { target, passthrough };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function packageAssetEnv(extraEnv = {}) {
|
|
646
|
+
return {
|
|
647
|
+
GUARDEX_CLI_ENTRY: __filename,
|
|
648
|
+
GUARDEX_NODE_BIN: process.execPath,
|
|
649
|
+
...extraEnv,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function packageAssetPath(assetKey) {
|
|
654
|
+
const assetPath = PACKAGE_SCRIPT_ASSETS[assetKey];
|
|
655
|
+
if (!assetPath) {
|
|
656
|
+
throw new Error(`Unknown package asset: ${assetKey}`);
|
|
657
|
+
}
|
|
658
|
+
if (!fs.existsSync(assetPath)) {
|
|
659
|
+
throw new Error(`Missing package asset: ${assetPath}`);
|
|
660
|
+
}
|
|
661
|
+
return assetPath;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function runPackageAsset(assetKey, rawArgs, options = {}) {
|
|
665
|
+
const assetPath = packageAssetPath(assetKey);
|
|
666
|
+
let cmd = 'bash';
|
|
667
|
+
if (assetPath.endsWith('.py')) {
|
|
668
|
+
cmd = 'python3';
|
|
669
|
+
} else if (assetPath.endsWith('.js')) {
|
|
670
|
+
cmd = process.execPath;
|
|
671
|
+
}
|
|
672
|
+
return run(cmd, [assetPath, ...rawArgs], {
|
|
673
|
+
cwd: options.cwd || process.cwd(),
|
|
674
|
+
stdio: options.stdio || 'pipe',
|
|
515
675
|
timeout: options.timeout,
|
|
676
|
+
env: packageAssetEnv(options.env),
|
|
516
677
|
});
|
|
517
678
|
}
|
|
518
679
|
|
|
680
|
+
function repoLocalLegacyScriptPath(repoRoot, relativePath) {
|
|
681
|
+
const assetPath = path.join(repoRoot, relativePath);
|
|
682
|
+
return fs.existsSync(assetPath) ? assetPath : null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function runReviewBotCommand(repoRoot, rawArgs, options = {}) {
|
|
686
|
+
const legacyScript = repoLocalLegacyScriptPath(repoRoot, 'scripts/review-bot-watch.sh');
|
|
687
|
+
if (legacyScript) {
|
|
688
|
+
return run('bash', [legacyScript, ...rawArgs], {
|
|
689
|
+
cwd: repoRoot,
|
|
690
|
+
stdio: options.stdio || 'pipe',
|
|
691
|
+
timeout: options.timeout,
|
|
692
|
+
env: packageAssetEnv(options.env),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return runPackageAsset('reviewBot', rawArgs, {
|
|
696
|
+
...options,
|
|
697
|
+
cwd: repoRoot,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function invokePackageAsset(assetKey, rawArgs, options = {}) {
|
|
702
|
+
const result = runPackageAsset(assetKey, rawArgs, options);
|
|
703
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
704
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
705
|
+
if (result.status !== 0) {
|
|
706
|
+
throw new Error(`${assetKey} command failed with status ${result.status}`);
|
|
707
|
+
}
|
|
708
|
+
process.exitCode = 0;
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
|
|
519
712
|
function formatElapsedDuration(ms) {
|
|
520
713
|
const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
521
714
|
if (durationMs < 1000) {
|
|
@@ -604,22 +797,29 @@ function printAutoFinishSummary(summary, options = {}) {
|
|
|
604
797
|
|
|
605
798
|
if (enabled) {
|
|
606
799
|
console.log(
|
|
607
|
-
|
|
800
|
+
colorizeDoctorOutput(
|
|
801
|
+
`[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
|
|
802
|
+
detectAutoFinishSummaryStatus(summary),
|
|
803
|
+
),
|
|
608
804
|
);
|
|
609
805
|
const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
|
|
610
806
|
for (const detail of visibleDetails) {
|
|
611
|
-
console.log(`[${TOOL_NAME}] ${detail}
|
|
807
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
|
|
612
808
|
}
|
|
613
809
|
if (!verbose && details.length > detailLimit) {
|
|
614
810
|
console.log(
|
|
615
|
-
|
|
811
|
+
colorizeDoctorOutput(
|
|
812
|
+
`[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
|
|
813
|
+
'warn',
|
|
814
|
+
),
|
|
616
815
|
);
|
|
617
816
|
}
|
|
618
817
|
return;
|
|
619
818
|
}
|
|
620
819
|
|
|
621
820
|
if (details.length > 0) {
|
|
622
|
-
|
|
821
|
+
const detail = verbose ? details[0] : summarizeAutoFinishDetail(details[0]);
|
|
822
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
|
|
623
823
|
}
|
|
624
824
|
}
|
|
625
825
|
|
|
@@ -747,6 +947,9 @@ function toDestinationPath(relativeTemplatePath) {
|
|
|
747
947
|
if (relativeTemplatePath.startsWith('github/')) {
|
|
748
948
|
return `.${relativeTemplatePath}`;
|
|
749
949
|
}
|
|
950
|
+
if (relativeTemplatePath.startsWith('vscode/')) {
|
|
951
|
+
return relativeTemplatePath;
|
|
952
|
+
}
|
|
750
953
|
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
751
954
|
}
|
|
752
955
|
|
|
@@ -784,6 +987,118 @@ function isCriticalGuardrailPath(relativePath) {
|
|
|
784
987
|
return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
|
|
785
988
|
}
|
|
786
989
|
|
|
990
|
+
function shellSingleQuote(value) {
|
|
991
|
+
return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function renderShellDispatchShim(commandParts) {
|
|
995
|
+
const rendered = commandParts.map((part) => shellSingleQuote(part)).join(' ');
|
|
996
|
+
return (
|
|
997
|
+
'#!/usr/bin/env bash\n' +
|
|
998
|
+
'set -euo pipefail\n' +
|
|
999
|
+
'\n' +
|
|
1000
|
+
'if [[ -n "${GUARDEX_CLI_ENTRY:-}" ]]; then\n' +
|
|
1001
|
+
' node_bin="${GUARDEX_NODE_BIN:-node}"\n' +
|
|
1002
|
+
` exec "$node_bin" "$GUARDEX_CLI_ENTRY" ${rendered} "$@"\n` +
|
|
1003
|
+
'fi\n' +
|
|
1004
|
+
'\n' +
|
|
1005
|
+
'resolve_guardex_cli() {\n' +
|
|
1006
|
+
' if [[ -n "${GUARDEX_CLI_BIN:-}" ]]; then\n' +
|
|
1007
|
+
' printf \'%s\' "$GUARDEX_CLI_BIN"\n' +
|
|
1008
|
+
' return 0\n' +
|
|
1009
|
+
' fi\n' +
|
|
1010
|
+
' if command -v gx >/dev/null 2>&1; then\n' +
|
|
1011
|
+
' printf \'%s\' "gx"\n' +
|
|
1012
|
+
' return 0\n' +
|
|
1013
|
+
' fi\n' +
|
|
1014
|
+
' if command -v gitguardex >/dev/null 2>&1; then\n' +
|
|
1015
|
+
' printf \'%s\' "gitguardex"\n' +
|
|
1016
|
+
' return 0\n' +
|
|
1017
|
+
' fi\n' +
|
|
1018
|
+
' echo "[gitguardex-shim] Missing gx CLI in PATH." >&2\n' +
|
|
1019
|
+
' exit 1\n' +
|
|
1020
|
+
'}\n' +
|
|
1021
|
+
'\n' +
|
|
1022
|
+
'cli_bin="$(resolve_guardex_cli)"\n' +
|
|
1023
|
+
`exec "$cli_bin" ${rendered} "$@"\n`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function renderPythonDispatchShim(commandParts) {
|
|
1028
|
+
return (
|
|
1029
|
+
'#!/usr/bin/env python3\n' +
|
|
1030
|
+
'import os\n' +
|
|
1031
|
+
'import shutil\n' +
|
|
1032
|
+
'import subprocess\n' +
|
|
1033
|
+
'import sys\n' +
|
|
1034
|
+
'\n' +
|
|
1035
|
+
`COMMAND = ${JSON.stringify(commandParts)}\n` +
|
|
1036
|
+
'\n' +
|
|
1037
|
+
'entry = os.environ.get("GUARDEX_CLI_ENTRY")\n' +
|
|
1038
|
+
'if entry:\n' +
|
|
1039
|
+
' node_bin = os.environ.get("GUARDEX_NODE_BIN") or shutil.which("node") or "node"\n' +
|
|
1040
|
+
' raise SystemExit(subprocess.call([node_bin, entry, *COMMAND, *sys.argv[1:]]))\n' +
|
|
1041
|
+
'cli = os.environ.get("GUARDEX_CLI_BIN") or shutil.which("gx") or shutil.which("gitguardex")\n' +
|
|
1042
|
+
'if not cli:\n' +
|
|
1043
|
+
' sys.stderr.write("[gitguardex-shim] Missing gx CLI in PATH.\\n")\n' +
|
|
1044
|
+
' raise SystemExit(1)\n' +
|
|
1045
|
+
'raise SystemExit(subprocess.call([cli, *COMMAND, *sys.argv[1:]]))\n'
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function managedForceConflictMessage(relativePath) {
|
|
1050
|
+
return (
|
|
1051
|
+
`Refusing to overwrite existing file without --force: ${relativePath}\n` +
|
|
1052
|
+
`Use '--force ${relativePath}' to rewrite only this managed file, or '--force' to rewrite all managed files.`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function renderManagedFile(repoRoot, relativePath, content, options = {}) {
|
|
1057
|
+
const destinationPath = path.join(repoRoot, relativePath);
|
|
1058
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
1059
|
+
const force = Boolean(options.force);
|
|
1060
|
+
const dryRun = Boolean(options.dryRun);
|
|
1061
|
+
|
|
1062
|
+
if (destinationExists) {
|
|
1063
|
+
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
1064
|
+
if (existingContent === content) {
|
|
1065
|
+
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
1066
|
+
return { status: 'unchanged', file: relativePath };
|
|
1067
|
+
}
|
|
1068
|
+
if (!force && !isCriticalGuardrailPath(relativePath)) {
|
|
1069
|
+
throw new Error(managedForceConflictMessage(relativePath));
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
ensureParentDir(repoRoot, destinationPath, dryRun);
|
|
1074
|
+
if (!dryRun) {
|
|
1075
|
+
fs.writeFileSync(destinationPath, content, 'utf8');
|
|
1076
|
+
ensureExecutable(destinationPath, relativePath, dryRun);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (destinationExists && !force && isCriticalGuardrailPath(relativePath)) {
|
|
1080
|
+
return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: relativePath };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return { status: destinationExists ? 'overwritten' : 'created', file: relativePath };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function ensureGeneratedScriptShim(repoRoot, spec, options = {}) {
|
|
1087
|
+
const content = spec.kind === 'python'
|
|
1088
|
+
? renderPythonDispatchShim(spec.command)
|
|
1089
|
+
: renderShellDispatchShim(spec.command);
|
|
1090
|
+
return renderManagedFile(repoRoot, spec.relativePath, content, options);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function ensureHookShim(repoRoot, hookName, options = {}) {
|
|
1094
|
+
return renderManagedFile(
|
|
1095
|
+
repoRoot,
|
|
1096
|
+
path.posix.join('.githooks', hookName),
|
|
1097
|
+
renderShellDispatchShim(['hook', 'run', hookName]),
|
|
1098
|
+
options,
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
787
1102
|
function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
788
1103
|
const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
|
|
789
1104
|
const destinationRelativePath = toDestinationPath(relativeTemplatePath);
|
|
@@ -799,9 +1114,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
|
|
|
799
1114
|
return { status: 'unchanged', file: destinationRelativePath };
|
|
800
1115
|
}
|
|
801
1116
|
if (!force && !isCriticalGuardrailPath(destinationRelativePath)) {
|
|
802
|
-
throw new Error(
|
|
803
|
-
`Refusing to overwrite existing file without --force: ${destinationRelativePath}`,
|
|
804
|
-
);
|
|
1117
|
+
throw new Error(managedForceConflictMessage(destinationRelativePath));
|
|
805
1118
|
}
|
|
806
1119
|
}
|
|
807
1120
|
|
|
@@ -852,6 +1165,22 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
|
|
|
852
1165
|
return { status: 'created', file: destinationRelativePath };
|
|
853
1166
|
}
|
|
854
1167
|
|
|
1168
|
+
function ensureTargetedLegacyWorkflowShims(repoRoot, options) {
|
|
1169
|
+
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
1170
|
+
if (targetedPaths.length === 0) {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const operations = [];
|
|
1175
|
+
for (const shim of LEGACY_WORKFLOW_SHIM_SPECS) {
|
|
1176
|
+
if (!shouldForceManagedPath(options, shim.relativePath)) {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
operations.push(ensureGeneratedScriptShim(repoRoot, shim, { dryRun: options.dryRun, force: true }));
|
|
1180
|
+
}
|
|
1181
|
+
return operations;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
855
1184
|
function lockFilePath(repoRoot) {
|
|
856
1185
|
return path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
857
1186
|
}
|
|
@@ -961,8 +1290,7 @@ function writeLockState(repoRoot, payload, dryRun) {
|
|
|
961
1290
|
fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
962
1291
|
}
|
|
963
1292
|
|
|
964
|
-
function
|
|
965
|
-
const force = Boolean(options.force);
|
|
1293
|
+
function removeLegacyPackageScripts(repoRoot, dryRun) {
|
|
966
1294
|
const packagePath = path.join(repoRoot, 'package.json');
|
|
967
1295
|
if (!fs.existsSync(packagePath)) {
|
|
968
1296
|
return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
|
|
@@ -978,29 +1306,87 @@ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
|
|
|
978
1306
|
const existingScripts = pkg.scripts && typeof pkg.scripts === 'object'
|
|
979
1307
|
? pkg.scripts
|
|
980
1308
|
: {};
|
|
981
|
-
const hasExistingAgentScripts = Object.keys(existingScripts).some((key) => key.startsWith('agent:'));
|
|
982
|
-
if (hasExistingAgentScripts && !force) {
|
|
983
|
-
return { status: 'unchanged', file: 'package.json', note: 'preserved existing agent:* scripts' };
|
|
984
|
-
}
|
|
985
|
-
|
|
986
1309
|
pkg.scripts = existingScripts;
|
|
987
1310
|
let changed = false;
|
|
988
|
-
for (const [key, value] of Object.entries(
|
|
989
|
-
if (
|
|
990
|
-
|
|
1311
|
+
for (const [key, value] of Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)) {
|
|
1312
|
+
if (existingScripts[key] === value) {
|
|
1313
|
+
delete existingScripts[key];
|
|
991
1314
|
changed = true;
|
|
992
1315
|
}
|
|
993
1316
|
}
|
|
994
1317
|
|
|
995
1318
|
if (!changed) {
|
|
996
|
-
return { status: 'unchanged', file: 'package.json' };
|
|
1319
|
+
return { status: 'unchanged', file: 'package.json', note: 'no Guardex-managed agent:* scripts found' };
|
|
997
1320
|
}
|
|
998
1321
|
|
|
999
1322
|
if (!dryRun) {
|
|
1000
1323
|
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
1001
1324
|
}
|
|
1002
1325
|
|
|
1003
|
-
return { status: 'updated', file: 'package.json' };
|
|
1326
|
+
return { status: dryRun ? 'would-update' : 'updated', file: 'package.json', note: 'removed Guardex-managed agent:* scripts' };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function installUserLevelAsset(asset, options = {}) {
|
|
1330
|
+
const dryRun = Boolean(options.dryRun);
|
|
1331
|
+
const force = Boolean(options.force);
|
|
1332
|
+
const destinationPath = path.join(GUARDEX_HOME_DIR, asset.destination);
|
|
1333
|
+
const sourceContent = fs.readFileSync(asset.source, 'utf8');
|
|
1334
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
1335
|
+
|
|
1336
|
+
if (destinationExists) {
|
|
1337
|
+
const existingContent = fs.readFileSync(destinationPath, 'utf8');
|
|
1338
|
+
if (existingContent === sourceContent) {
|
|
1339
|
+
return { status: 'unchanged', file: asset.destination };
|
|
1340
|
+
}
|
|
1341
|
+
if (!force) {
|
|
1342
|
+
return { status: 'skipped-conflict', file: asset.destination };
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!dryRun) {
|
|
1347
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
1348
|
+
fs.writeFileSync(destinationPath, sourceContent, 'utf8');
|
|
1349
|
+
}
|
|
1350
|
+
return { status: destinationExists ? (dryRun ? 'would-update' : 'updated') : 'created', file: asset.destination };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function removeLegacyManagedRepoFile(repoRoot, relativePath, options = {}) {
|
|
1354
|
+
const dryRun = Boolean(options.dryRun);
|
|
1355
|
+
const force = Boolean(options.force);
|
|
1356
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
1357
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1358
|
+
return { status: 'unchanged', file: relativePath, note: 'not present' };
|
|
1359
|
+
}
|
|
1360
|
+
if (!fs.statSync(absolutePath).isFile()) {
|
|
1361
|
+
return { status: 'skipped-conflict', file: relativePath, note: 'not a regular file' };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const skillAsset = USER_LEVEL_SKILL_ASSETS.find((asset) => asset.destination === relativePath);
|
|
1365
|
+
if (skillAsset) {
|
|
1366
|
+
const userLevelPath = path.join(GUARDEX_HOME_DIR, skillAsset.destination);
|
|
1367
|
+
if (!fs.existsSync(userLevelPath)) {
|
|
1368
|
+
return { status: 'skipped', file: relativePath, note: 'user-level replacement not installed' };
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const templateRelative = skillAsset
|
|
1373
|
+
? skillAsset.source.slice(TEMPLATE_ROOT.length + 1)
|
|
1374
|
+
: relativePath.replace(/^\./, '');
|
|
1375
|
+
const sourcePath = path.join(TEMPLATE_ROOT, templateRelative);
|
|
1376
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1377
|
+
return { status: 'skipped', file: relativePath, note: 'template source missing' };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
1381
|
+
const existingContent = fs.readFileSync(absolutePath, 'utf8');
|
|
1382
|
+
if (existingContent !== sourceContent && !force) {
|
|
1383
|
+
return { status: 'skipped-conflict', file: relativePath, note: 'local edits differ from managed template' };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (!dryRun) {
|
|
1387
|
+
fs.rmSync(absolutePath, { force: true });
|
|
1388
|
+
}
|
|
1389
|
+
return { status: dryRun ? 'would-remove' : 'removed', file: relativePath };
|
|
1004
1390
|
}
|
|
1005
1391
|
|
|
1006
1392
|
function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
|
|
@@ -1101,8 +1487,65 @@ function requireValue(rawArgs, index, flagName) {
|
|
|
1101
1487
|
return value;
|
|
1102
1488
|
}
|
|
1103
1489
|
|
|
1490
|
+
function normalizeManagedForcePath(rawPath) {
|
|
1491
|
+
if (typeof rawPath !== 'string') {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
const normalized = path.posix.normalize(rawPath.replace(/\\/g, '/'));
|
|
1495
|
+
if (!normalized || normalized === '.' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
return normalized.startsWith('./') ? normalized.slice(2) : normalized;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function collectForceManagedPaths(rawArgs, startIndex) {
|
|
1502
|
+
const forceManagedPaths = [];
|
|
1503
|
+
let nextIndex = startIndex;
|
|
1504
|
+
|
|
1505
|
+
while (nextIndex + 1 < rawArgs.length) {
|
|
1506
|
+
const candidate = rawArgs[nextIndex + 1];
|
|
1507
|
+
if (!candidate || candidate.startsWith('-')) {
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
const normalized = normalizeManagedForcePath(candidate);
|
|
1511
|
+
if (!normalized || !TARGETED_FORCEABLE_MANAGED_PATHS.has(normalized)) {
|
|
1512
|
+
throw new Error(`Unknown managed path after --force: ${candidate}`);
|
|
1513
|
+
}
|
|
1514
|
+
forceManagedPaths.push(normalized);
|
|
1515
|
+
nextIndex += 1;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return { forceManagedPaths, nextIndex };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function appendForceArgs(args, options) {
|
|
1522
|
+
if (!options.force) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
args.push('--force');
|
|
1526
|
+
for (const managedPath of options.forceManagedPaths || []) {
|
|
1527
|
+
args.push(managedPath);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function shouldForceManagedPath(options, relativePath) {
|
|
1532
|
+
if (!options.force) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
|
|
1536
|
+
if (targetedPaths.length === 0) {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
const normalized = normalizeManagedForcePath(relativePath);
|
|
1540
|
+
return normalized !== null && targetedPaths.includes(normalized);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1104
1543
|
function parseCommonArgs(rawArgs, defaults) {
|
|
1105
1544
|
const options = { ...defaults };
|
|
1545
|
+
const supportsForce = Object.prototype.hasOwnProperty.call(options, 'force');
|
|
1546
|
+
if (supportsForce && !Array.isArray(options.forceManagedPaths)) {
|
|
1547
|
+
options.forceManagedPaths = [];
|
|
1548
|
+
}
|
|
1106
1549
|
|
|
1107
1550
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1108
1551
|
const arg = rawArgs[index];
|
|
@@ -1124,7 +1567,17 @@ function parseCommonArgs(rawArgs, defaults) {
|
|
|
1124
1567
|
continue;
|
|
1125
1568
|
}
|
|
1126
1569
|
if (arg === '--force') {
|
|
1570
|
+
if (!supportsForce) {
|
|
1571
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1572
|
+
}
|
|
1127
1573
|
options.force = true;
|
|
1574
|
+
const parsed = collectForceManagedPaths(rawArgs, index);
|
|
1575
|
+
if (parsed.forceManagedPaths.length > 0) {
|
|
1576
|
+
options.forceManagedPaths = Array.from(
|
|
1577
|
+
new Set([...(options.forceManagedPaths || []), ...parsed.forceManagedPaths]),
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
index = parsed.nextIndex;
|
|
1128
1581
|
continue;
|
|
1129
1582
|
}
|
|
1130
1583
|
if (arg === '--keep-stale-locks') {
|
|
@@ -1242,6 +1695,7 @@ function parseSetupArgs(rawArgs, defaults) {
|
|
|
1242
1695
|
function parseDoctorArgs(rawArgs) {
|
|
1243
1696
|
const doctorDefaults = {
|
|
1244
1697
|
target: process.cwd(),
|
|
1698
|
+
force: false,
|
|
1245
1699
|
dropStaleLocks: true,
|
|
1246
1700
|
skipAgents: false,
|
|
1247
1701
|
skipPackageJson: false,
|
|
@@ -1323,7 +1777,6 @@ function ensureParentWorkspaceView(repoRoot, dryRun) {
|
|
|
1323
1777
|
function hasGuardexBootstrapFiles(repoRoot) {
|
|
1324
1778
|
const required = [
|
|
1325
1779
|
'AGENTS.md',
|
|
1326
|
-
'scripts/agent-branch-start.sh',
|
|
1327
1780
|
'.githooks/pre-commit',
|
|
1328
1781
|
'.githooks/pre-push',
|
|
1329
1782
|
LOCK_FILE_RELATIVE,
|
|
@@ -1366,7 +1819,7 @@ function assertProtectedMainWriteAllowed(options, commandName) {
|
|
|
1366
1819
|
throw new Error(
|
|
1367
1820
|
`${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
|
|
1368
1821
|
`Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
|
|
1369
|
-
`
|
|
1822
|
+
` gx branch start "<task>" "codex"\n` +
|
|
1370
1823
|
`Override once only when intentional: --allow-protected-base-write`,
|
|
1371
1824
|
);
|
|
1372
1825
|
}
|
|
@@ -1391,6 +1844,7 @@ function runSetupBootstrapInternal(options) {
|
|
|
1391
1844
|
target: installPayload.repoRoot,
|
|
1392
1845
|
dryRun: options.dryRun,
|
|
1393
1846
|
force: options.force,
|
|
1847
|
+
forceManagedPaths: options.forceManagedPaths,
|
|
1394
1848
|
dropStaleLocks: true,
|
|
1395
1849
|
skipAgents: options.skipAgents,
|
|
1396
1850
|
skipPackageJson: options.skipPackageJson,
|
|
@@ -1428,7 +1882,7 @@ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
|
|
|
1428
1882
|
|
|
1429
1883
|
function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
1430
1884
|
const args = ['setup', '--target', sandboxTarget, '--no-global-install', '--no-recursive'];
|
|
1431
|
-
|
|
1885
|
+
appendForceArgs(args, options);
|
|
1432
1886
|
if (options.skipAgents) args.push('--skip-agents');
|
|
1433
1887
|
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
1434
1888
|
if (options.skipGitignore) args.push('--no-gitignore');
|
|
@@ -1439,7 +1893,7 @@ function buildSandboxSetupArgs(options, sandboxTarget) {
|
|
|
1439
1893
|
function buildSandboxDoctorArgs(options, sandboxTarget) {
|
|
1440
1894
|
const args = ['doctor', '--target', sandboxTarget];
|
|
1441
1895
|
if (options.dryRun) args.push('--dry-run');
|
|
1442
|
-
|
|
1896
|
+
appendForceArgs(args, options);
|
|
1443
1897
|
if (options.skipAgents) args.push('--skip-agents');
|
|
1444
1898
|
if (options.skipPackageJson) args.push('--skip-package-json');
|
|
1445
1899
|
if (options.skipGitignore) args.push('--no-gitignore');
|
|
@@ -1587,13 +2041,7 @@ function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
|
|
|
1587
2041
|
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1588
2042
|
}
|
|
1589
2043
|
|
|
1590
|
-
const
|
|
1591
|
-
if (!fs.existsSync(startScript)) {
|
|
1592
|
-
return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
const startResult = run('bash', [
|
|
1596
|
-
startScript,
|
|
2044
|
+
const startResult = runPackageAsset('branchStart', [
|
|
1597
2045
|
'--task',
|
|
1598
2046
|
taskName,
|
|
1599
2047
|
'--agent',
|
|
@@ -1742,8 +2190,7 @@ function collectWorktreeDirtyPaths(worktreePath) {
|
|
|
1742
2190
|
}
|
|
1743
2191
|
|
|
1744
2192
|
function collectDoctorForceAddPaths(worktreePath) {
|
|
1745
|
-
return
|
|
1746
|
-
.map((entry) => toDestinationPath(entry))
|
|
2193
|
+
return REQUIRED_MANAGED_REPO_FILES
|
|
1747
2194
|
.filter((relativePath) => relativePath.startsWith('scripts/') || relativePath.startsWith('.githooks/'))
|
|
1748
2195
|
.filter((relativePath) => fs.existsSync(path.join(worktreePath, relativePath)));
|
|
1749
2196
|
}
|
|
@@ -1779,11 +2226,10 @@ function stripDoctorSandboxLocks(rawContent, branchName) {
|
|
|
1779
2226
|
}
|
|
1780
2227
|
|
|
1781
2228
|
function claimDoctorChangedLocks(metadata) {
|
|
1782
|
-
|
|
1783
|
-
if (!fs.existsSync(lockScript) || !metadata.branch) {
|
|
2229
|
+
if (!metadata.branch) {
|
|
1784
2230
|
return {
|
|
1785
2231
|
status: 'skipped',
|
|
1786
|
-
note: '
|
|
2232
|
+
note: 'missing sandbox branch metadata',
|
|
1787
2233
|
changedCount: 0,
|
|
1788
2234
|
deletedCount: 0,
|
|
1789
2235
|
};
|
|
@@ -1795,13 +2241,13 @@ function claimDoctorChangedLocks(metadata) {
|
|
|
1795
2241
|
]));
|
|
1796
2242
|
const deletedPaths = collectDoctorDeletedPaths(metadata.worktreePath);
|
|
1797
2243
|
if (changedPaths.length > 0) {
|
|
1798
|
-
|
|
2244
|
+
runPackageAsset('lockTool', ['claim', '--branch', metadata.branch, ...changedPaths], {
|
|
1799
2245
|
cwd: metadata.worktreePath,
|
|
1800
2246
|
timeout: 30_000,
|
|
1801
2247
|
});
|
|
1802
2248
|
}
|
|
1803
2249
|
if (deletedPaths.length > 0) {
|
|
1804
|
-
|
|
2250
|
+
runPackageAsset('lockTool', ['allow-delete', '--branch', metadata.branch, ...deletedPaths], {
|
|
1805
2251
|
cwd: metadata.worktreePath,
|
|
1806
2252
|
timeout: 30_000,
|
|
1807
2253
|
});
|
|
@@ -1902,13 +2348,6 @@ function doctorFinishFlowIsPending(output) {
|
|
|
1902
2348
|
}
|
|
1903
2349
|
|
|
1904
2350
|
function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
|
|
1905
|
-
const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
|
|
1906
|
-
if (!fs.existsSync(finishScript)) {
|
|
1907
|
-
return {
|
|
1908
|
-
status: 'skipped',
|
|
1909
|
-
note: `${path.relative(metadata.worktreePath, finishScript)} missing in sandbox`,
|
|
1910
|
-
};
|
|
1911
|
-
}
|
|
1912
2351
|
if (!hasOriginRemote(blocked.repoRoot)) {
|
|
1913
2352
|
return {
|
|
1914
2353
|
status: 'skipped',
|
|
@@ -1945,9 +2384,9 @@ function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
|
|
|
1945
2384
|
const finishTimeoutMs = Math.max(180_000, (waitTimeoutSeconds + 60) * 1000);
|
|
1946
2385
|
const waitForMergeArg = options.waitForMerge === false ? '--no-wait-for-merge' : '--wait-for-merge';
|
|
1947
2386
|
|
|
1948
|
-
const finishResult =
|
|
1949
|
-
'
|
|
1950
|
-
[
|
|
2387
|
+
const finishResult = runPackageAsset(
|
|
2388
|
+
'branchFinish',
|
|
2389
|
+
['--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg, '--cleanup'],
|
|
1951
2390
|
{ cwd: metadata.worktreePath, timeout: finishTimeoutMs },
|
|
1952
2391
|
);
|
|
1953
2392
|
if (isSpawnFailure(finishResult)) {
|
|
@@ -2018,7 +2457,7 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
|
|
|
2018
2457
|
...(autoCommitResult.stagedFiles || []),
|
|
2019
2458
|
...OMX_SCAFFOLD_DIRECTORIES,
|
|
2020
2459
|
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
2021
|
-
...
|
|
2460
|
+
...REQUIRED_MANAGED_REPO_FILES,
|
|
2022
2461
|
'bin',
|
|
2023
2462
|
'package.json',
|
|
2024
2463
|
'.gitignore',
|
|
@@ -2156,9 +2595,7 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
|
|
|
2156
2595
|
}
|
|
2157
2596
|
|
|
2158
2597
|
function syncDoctorLocalSupportFiles(repoRoot, dryRun) {
|
|
2159
|
-
return
|
|
2160
|
-
.filter((entry) => entry.startsWith('codex/') || entry.startsWith('claude/'))
|
|
2161
|
-
.map((entry) => ensureTemplateFilePresent(repoRoot, entry, dryRun));
|
|
2598
|
+
return [];
|
|
2162
2599
|
}
|
|
2163
2600
|
|
|
2164
2601
|
function runDoctorInSandbox(options, blocked) {
|
|
@@ -2438,7 +2875,7 @@ function runDoctorInSandbox(options, blocked) {
|
|
|
2438
2875
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
2439
2876
|
} else if (finishResult.status === 'failed') {
|
|
2440
2877
|
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
2441
|
-
console.log(`[
|
|
2878
|
+
console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
|
|
2442
2879
|
if (finishResult.stdout) process.stdout.write(finishResult.stdout);
|
|
2443
2880
|
if (finishResult.stderr) process.stderr.write(finishResult.stderr);
|
|
2444
2881
|
} else {
|
|
@@ -3044,13 +3481,6 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
3044
3481
|
return summary;
|
|
3045
3482
|
}
|
|
3046
3483
|
|
|
3047
|
-
const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
|
|
3048
|
-
if (!fs.existsSync(finishScript)) {
|
|
3049
|
-
summary.enabled = false;
|
|
3050
|
-
summary.details.push(`Skipped auto-finish sweep (missing ${path.relative(repoRoot, finishScript)}).`);
|
|
3051
|
-
return summary;
|
|
3052
|
-
}
|
|
3053
|
-
|
|
3054
3484
|
const hasOrigin = gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
|
|
3055
3485
|
if (!hasOrigin) {
|
|
3056
3486
|
summary.enabled = false;
|
|
@@ -3116,7 +3546,6 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
3116
3546
|
|
|
3117
3547
|
summary.attempted += 1;
|
|
3118
3548
|
const finishArgs = [
|
|
3119
|
-
finishScript,
|
|
3120
3549
|
'--branch',
|
|
3121
3550
|
branch,
|
|
3122
3551
|
'--base',
|
|
@@ -3125,7 +3554,7 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
|
|
|
3125
3554
|
waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
3126
3555
|
'--cleanup',
|
|
3127
3556
|
];
|
|
3128
|
-
const finishResult =
|
|
3557
|
+
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot });
|
|
3129
3558
|
const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
|
|
3130
3559
|
|
|
3131
3560
|
if (finishResult.status === 0) {
|
|
@@ -3278,9 +3707,9 @@ function printSetupRepoHints(repoRoot, baseBranch, repoLabel = '') {
|
|
|
3278
3707
|
console.log(`[${TOOL_NAME}] Bootstrap commit${label}: git add . && git commit -m "bootstrap gitguardex"`);
|
|
3279
3708
|
console.log(
|
|
3280
3709
|
`[${TOOL_NAME}] First agent flow${label}: ` +
|
|
3281
|
-
`
|
|
3282
|
-
`
|
|
3283
|
-
`
|
|
3710
|
+
`gx branch start "<task>" "codex" -> ` +
|
|
3711
|
+
`gx locks claim --branch "$(git branch --show-current)" <file...> -> ` +
|
|
3712
|
+
`gx branch finish --branch "$(git branch --show-current)" --base ${baseBranch} --via-pr --wait-for-merge`,
|
|
3284
3713
|
);
|
|
3285
3714
|
}
|
|
3286
3715
|
if (!hasOrigin) {
|
|
@@ -3628,19 +4057,20 @@ function parseMergeArgs(rawArgs) {
|
|
|
3628
4057
|
return options;
|
|
3629
4058
|
}
|
|
3630
4059
|
|
|
3631
|
-
function parseFinishArgs(rawArgs) {
|
|
4060
|
+
function parseFinishArgs(rawArgs, defaults = {}) {
|
|
3632
4061
|
const options = {
|
|
3633
4062
|
target: process.cwd(),
|
|
3634
4063
|
base: '',
|
|
3635
4064
|
branch: '',
|
|
3636
4065
|
all: false,
|
|
3637
4066
|
dryRun: false,
|
|
3638
|
-
waitForMerge: true,
|
|
3639
|
-
cleanup: true,
|
|
4067
|
+
waitForMerge: defaults.waitForMerge ?? true,
|
|
4068
|
+
cleanup: defaults.cleanup ?? true,
|
|
3640
4069
|
keepRemote: false,
|
|
3641
4070
|
noAutoCommit: false,
|
|
3642
4071
|
failFast: false,
|
|
3643
4072
|
commitMessage: '',
|
|
4073
|
+
mergeMode: defaults.mergeMode || 'pr',
|
|
3644
4074
|
};
|
|
3645
4075
|
|
|
3646
4076
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -3697,6 +4127,26 @@ function parseFinishArgs(rawArgs) {
|
|
|
3697
4127
|
options.waitForMerge = false;
|
|
3698
4128
|
continue;
|
|
3699
4129
|
}
|
|
4130
|
+
if (arg === '--via-pr') {
|
|
4131
|
+
options.mergeMode = 'pr';
|
|
4132
|
+
continue;
|
|
4133
|
+
}
|
|
4134
|
+
if (arg === '--direct-only') {
|
|
4135
|
+
options.mergeMode = 'direct';
|
|
4136
|
+
continue;
|
|
4137
|
+
}
|
|
4138
|
+
if (arg === '--mode') {
|
|
4139
|
+
const next = rawArgs[index + 1];
|
|
4140
|
+
if (!next) {
|
|
4141
|
+
throw new Error('--mode requires a value');
|
|
4142
|
+
}
|
|
4143
|
+
if (!['auto', 'direct', 'pr'].includes(next)) {
|
|
4144
|
+
throw new Error(`Invalid --mode value: ${next} (expected auto|direct|pr)`);
|
|
4145
|
+
}
|
|
4146
|
+
options.mergeMode = next;
|
|
4147
|
+
index += 1;
|
|
4148
|
+
continue;
|
|
4149
|
+
}
|
|
3700
4150
|
if (arg === '--cleanup') {
|
|
3701
4151
|
options.cleanup = true;
|
|
3702
4152
|
continue;
|
|
@@ -3849,11 +4299,6 @@ function gitOutputLines(worktreePath, args) {
|
|
|
3849
4299
|
}
|
|
3850
4300
|
|
|
3851
4301
|
function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
3852
|
-
const lockScript = path.join(repoRoot, 'scripts', 'agent-file-locks.py');
|
|
3853
|
-
if (!fs.existsSync(lockScript)) {
|
|
3854
|
-
return;
|
|
3855
|
-
}
|
|
3856
|
-
|
|
3857
4302
|
const changedFiles = uniquePreserveOrder([
|
|
3858
4303
|
...gitOutputLines(worktreePath, ['diff', '--name-only', '--', '.', ':(exclude).omx/state/agent-file-locks.json']),
|
|
3859
4304
|
...gitOutputLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', ':(exclude).omx/state/agent-file-locks.json']),
|
|
@@ -3861,7 +4306,7 @@ function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
|
3861
4306
|
]);
|
|
3862
4307
|
|
|
3863
4308
|
if (changedFiles.length > 0) {
|
|
3864
|
-
const claim =
|
|
4309
|
+
const claim = runPackageAsset('lockTool', ['claim', '--branch', branch, ...changedFiles], {
|
|
3865
4310
|
cwd: repoRoot,
|
|
3866
4311
|
stdio: 'pipe',
|
|
3867
4312
|
});
|
|
@@ -3895,7 +4340,7 @@ function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
|
|
|
3895
4340
|
]);
|
|
3896
4341
|
|
|
3897
4342
|
if (deletedFiles.length > 0) {
|
|
3898
|
-
const allowDelete =
|
|
4343
|
+
const allowDelete = runPackageAsset('lockTool', ['allow-delete', '--branch', branch, ...deletedFiles], {
|
|
3899
4344
|
cwd: repoRoot,
|
|
3900
4345
|
stdio: 'pipe',
|
|
3901
4346
|
});
|
|
@@ -4673,6 +5118,16 @@ function askGlobalInstallForMissing(options, missingPackages, missingLocalTools)
|
|
|
4673
5118
|
}
|
|
4674
5119
|
|
|
4675
5120
|
function installGlobalToolchain(options) {
|
|
5121
|
+
const approval = resolveGlobalInstallApproval(options);
|
|
5122
|
+
if (approval.source === 'flag' && !approval.approved) {
|
|
5123
|
+
return {
|
|
5124
|
+
status: 'skipped',
|
|
5125
|
+
reason: approval.source,
|
|
5126
|
+
missingPackages: [],
|
|
5127
|
+
missingLocalTools: [],
|
|
5128
|
+
};
|
|
5129
|
+
}
|
|
5130
|
+
|
|
4676
5131
|
if (options.dryRun) {
|
|
4677
5132
|
return { status: 'dry-run-skip' };
|
|
4678
5133
|
}
|
|
@@ -4701,11 +5156,11 @@ function installGlobalToolchain(options) {
|
|
|
4701
5156
|
|
|
4702
5157
|
const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
|
|
4703
5158
|
const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
|
|
4704
|
-
const
|
|
4705
|
-
if (!
|
|
5159
|
+
const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
|
|
5160
|
+
if (!installApproval.approved) {
|
|
4706
5161
|
return {
|
|
4707
5162
|
status: 'skipped',
|
|
4708
|
-
reason:
|
|
5163
|
+
reason: installApproval.source,
|
|
4709
5164
|
missingPackages,
|
|
4710
5165
|
missingLocalTools,
|
|
4711
5166
|
};
|
|
@@ -4798,15 +5253,28 @@ function runInstallInternal(options) {
|
|
|
4798
5253
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
4799
5254
|
|
|
4800
5255
|
for (const templateFile of TEMPLATE_FILES) {
|
|
4801
|
-
operations.push(
|
|
5256
|
+
operations.push(
|
|
5257
|
+
copyTemplateFile(
|
|
5258
|
+
repoRoot,
|
|
5259
|
+
templateFile,
|
|
5260
|
+
shouldForceManagedPath(options, toDestinationPath(templateFile)),
|
|
5261
|
+
Boolean(options.dryRun),
|
|
5262
|
+
),
|
|
5263
|
+
);
|
|
5264
|
+
}
|
|
5265
|
+
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
5266
|
+
for (const hookName of HOOK_NAMES) {
|
|
5267
|
+
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
5268
|
+
operations.push(
|
|
5269
|
+
ensureHookShim(repoRoot, hookName, {
|
|
5270
|
+
dryRun: options.dryRun,
|
|
5271
|
+
force: shouldForceManagedPath(options, hookRelativePath),
|
|
5272
|
+
}),
|
|
5273
|
+
);
|
|
4802
5274
|
}
|
|
4803
5275
|
|
|
4804
5276
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
4805
5277
|
|
|
4806
|
-
if (!options.skipPackageJson) {
|
|
4807
|
-
operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4808
|
-
}
|
|
4809
|
-
|
|
4810
5278
|
if (!options.skipAgents) {
|
|
4811
5279
|
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4812
5280
|
}
|
|
@@ -4843,8 +5311,22 @@ function runFixInternal(options) {
|
|
|
4843
5311
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
4844
5312
|
|
|
4845
5313
|
for (const templateFile of TEMPLATE_FILES) {
|
|
5314
|
+
if (shouldForceManagedPath(options, toDestinationPath(templateFile))) {
|
|
5315
|
+
operations.push(copyTemplateFile(repoRoot, templateFile, true, Boolean(options.dryRun)));
|
|
5316
|
+
continue;
|
|
5317
|
+
}
|
|
4846
5318
|
operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
|
|
4847
5319
|
}
|
|
5320
|
+
operations.push(...ensureTargetedLegacyWorkflowShims(repoRoot, options));
|
|
5321
|
+
for (const hookName of HOOK_NAMES) {
|
|
5322
|
+
const hookRelativePath = path.posix.join('.githooks', hookName);
|
|
5323
|
+
operations.push(
|
|
5324
|
+
ensureHookShim(repoRoot, hookName, {
|
|
5325
|
+
dryRun: options.dryRun,
|
|
5326
|
+
force: shouldForceManagedPath(options, hookRelativePath),
|
|
5327
|
+
}),
|
|
5328
|
+
);
|
|
5329
|
+
}
|
|
4848
5330
|
|
|
4849
5331
|
operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
|
|
4850
5332
|
|
|
@@ -4874,10 +5356,6 @@ function runFixInternal(options) {
|
|
|
4874
5356
|
}
|
|
4875
5357
|
}
|
|
4876
5358
|
|
|
4877
|
-
if (!options.skipPackageJson) {
|
|
4878
|
-
operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4879
|
-
}
|
|
4880
|
-
|
|
4881
5359
|
if (!options.skipAgents) {
|
|
4882
5360
|
operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
|
|
4883
5361
|
}
|
|
@@ -4907,8 +5385,7 @@ function runScanInternal(options) {
|
|
|
4907
5385
|
const requiredPaths = [
|
|
4908
5386
|
...OMX_SCAFFOLD_DIRECTORIES,
|
|
4909
5387
|
...Array.from(OMX_SCAFFOLD_FILES.keys()),
|
|
4910
|
-
...
|
|
4911
|
-
LOCK_FILE_RELATIVE,
|
|
5388
|
+
...REQUIRED_MANAGED_REPO_FILES,
|
|
4912
5389
|
];
|
|
4913
5390
|
|
|
4914
5391
|
for (const relativePath of requiredPaths) {
|
|
@@ -4918,7 +5395,7 @@ function runScanInternal(options) {
|
|
|
4918
5395
|
level: 'error',
|
|
4919
5396
|
code: 'missing-managed-file',
|
|
4920
5397
|
path: relativePath,
|
|
4921
|
-
message: `Missing managed
|
|
5398
|
+
message: `Missing managed repo file: ${relativePath}`,
|
|
4922
5399
|
});
|
|
4923
5400
|
}
|
|
4924
5401
|
}
|
|
@@ -5043,21 +5520,34 @@ function printScanResult(scan, json = false) {
|
|
|
5043
5520
|
|
|
5044
5521
|
if (scan.guardexEnabled === false) {
|
|
5045
5522
|
console.log(
|
|
5046
|
-
|
|
5523
|
+
colorizeDoctorOutput(
|
|
5524
|
+
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
|
|
5525
|
+
'disabled',
|
|
5526
|
+
),
|
|
5047
5527
|
);
|
|
5048
5528
|
return;
|
|
5049
5529
|
}
|
|
5050
5530
|
|
|
5051
5531
|
if (scan.findings.length === 0) {
|
|
5052
|
-
console.log(`[${TOOL_NAME}] ✅ No safety issues detected
|
|
5532
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ No safety issues detected.`, 'safe'));
|
|
5053
5533
|
return;
|
|
5054
5534
|
}
|
|
5055
5535
|
|
|
5056
5536
|
for (const item of scan.findings) {
|
|
5057
5537
|
const target = item.path ? ` (${item.path})` : '';
|
|
5058
|
-
console.log(
|
|
5538
|
+
console.log(
|
|
5539
|
+
colorizeDoctorOutput(
|
|
5540
|
+
`[${item.level.toUpperCase()}] ${item.code}${target}: ${item.message}`,
|
|
5541
|
+
item.level,
|
|
5542
|
+
),
|
|
5543
|
+
);
|
|
5059
5544
|
}
|
|
5060
|
-
console.log(
|
|
5545
|
+
console.log(
|
|
5546
|
+
colorizeDoctorOutput(
|
|
5547
|
+
`[${TOOL_NAME}] Summary: ${scan.errors} error(s), ${scan.warnings} warning(s).`,
|
|
5548
|
+
scan.errors > 0 ? 'error' : 'warn',
|
|
5549
|
+
),
|
|
5550
|
+
);
|
|
5061
5551
|
}
|
|
5062
5552
|
|
|
5063
5553
|
function setExitCodeFromScan(scan) {
|
|
@@ -5349,6 +5839,7 @@ function doctor(rawArgs) {
|
|
|
5349
5839
|
'--single-repo',
|
|
5350
5840
|
'--target',
|
|
5351
5841
|
repoPath,
|
|
5842
|
+
...(options.force ? ['--force', ...(options.forceManagedPaths || [])] : []),
|
|
5352
5843
|
...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
|
|
5353
5844
|
...(options.skipAgents ? ['--skip-agents'] : []),
|
|
5354
5845
|
...(options.skipPackageJson ? ['--skip-package-json'] : []),
|
|
@@ -5498,10 +5989,13 @@ function doctor(rawArgs) {
|
|
|
5498
5989
|
verbose: singleRepoOptions.verboseAutoFinish,
|
|
5499
5990
|
});
|
|
5500
5991
|
if (safe) {
|
|
5501
|
-
console.log(`[${TOOL_NAME}] ✅ Repo is fully safe
|
|
5992
|
+
console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ Repo is fully safe.`, 'safe'));
|
|
5502
5993
|
} else {
|
|
5503
5994
|
console.log(
|
|
5504
|
-
|
|
5995
|
+
colorizeDoctorOutput(
|
|
5996
|
+
`[${TOOL_NAME}] ⚠️ Repo is not fully safe yet (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
|
|
5997
|
+
scanResult.errors > 0 ? 'unsafe' : 'warn',
|
|
5998
|
+
),
|
|
5505
5999
|
);
|
|
5506
6000
|
}
|
|
5507
6001
|
setExitCodeFromScan(scanResult);
|
|
@@ -5510,15 +6004,7 @@ function doctor(rawArgs) {
|
|
|
5510
6004
|
function review(rawArgs) {
|
|
5511
6005
|
const options = parseReviewArgs(rawArgs);
|
|
5512
6006
|
const repoRoot = resolveRepoRoot(options.target);
|
|
5513
|
-
const
|
|
5514
|
-
if (!fs.existsSync(reviewScriptPath)) {
|
|
5515
|
-
throw new Error(
|
|
5516
|
-
`Missing review bot script: ${reviewScriptPath}\n` +
|
|
5517
|
-
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
5518
|
-
);
|
|
5519
|
-
}
|
|
5520
|
-
|
|
5521
|
-
const result = run('bash', [reviewScriptPath, ...options.passthroughArgs], { cwd: repoRoot });
|
|
6007
|
+
const result = runReviewBotCommand(repoRoot, options.passthroughArgs);
|
|
5522
6008
|
if (isSpawnFailure(result)) {
|
|
5523
6009
|
throw result.error;
|
|
5524
6010
|
}
|
|
@@ -5657,24 +6143,9 @@ function spawnDetachedAgentProcess({ command, args, cwd, logPath }) {
|
|
|
5657
6143
|
function agents(rawArgs) {
|
|
5658
6144
|
const options = parseAgentsArgs(rawArgs);
|
|
5659
6145
|
const repoRoot = resolveRepoRoot(options.target);
|
|
5660
|
-
const reviewScriptPath = path.join(repoRoot, 'scripts', 'review-bot-watch.sh');
|
|
5661
|
-
const pruneScriptPath = path.join(repoRoot, 'scripts', 'agent-worktree-prune.sh');
|
|
5662
6146
|
const statePath = agentsStatePathForRepo(repoRoot);
|
|
5663
6147
|
|
|
5664
6148
|
if (options.subcommand === 'start') {
|
|
5665
|
-
if (!fs.existsSync(reviewScriptPath)) {
|
|
5666
|
-
throw new Error(
|
|
5667
|
-
`Missing review bot script: ${reviewScriptPath}\n` +
|
|
5668
|
-
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
5669
|
-
);
|
|
5670
|
-
}
|
|
5671
|
-
if (!fs.existsSync(pruneScriptPath)) {
|
|
5672
|
-
throw new Error(
|
|
5673
|
-
`Missing cleanup script: ${pruneScriptPath}\n` +
|
|
5674
|
-
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
5675
|
-
);
|
|
5676
|
-
}
|
|
5677
|
-
|
|
5678
6149
|
const existingState = readAgentsState(repoRoot);
|
|
5679
6150
|
const existingReviewPid = Number.parseInt(String(existingState?.review?.pid || ''), 10);
|
|
5680
6151
|
const existingCleanupPid = Number.parseInt(String(existingState?.cleanup?.pid || ''), 10);
|
|
@@ -5699,8 +6170,17 @@ function agents(rawArgs) {
|
|
|
5699
6170
|
|
|
5700
6171
|
if (!reviewRunning) {
|
|
5701
6172
|
reviewPid = spawnDetachedAgentProcess({
|
|
5702
|
-
command:
|
|
5703
|
-
args: [
|
|
6173
|
+
command: process.execPath,
|
|
6174
|
+
args: [
|
|
6175
|
+
path.resolve(__filename),
|
|
6176
|
+
'internal',
|
|
6177
|
+
'run-shell',
|
|
6178
|
+
'reviewBot',
|
|
6179
|
+
'--target',
|
|
6180
|
+
repoRoot,
|
|
6181
|
+
'--interval',
|
|
6182
|
+
String(options.reviewIntervalSeconds),
|
|
6183
|
+
],
|
|
5704
6184
|
cwd: repoRoot,
|
|
5705
6185
|
logPath: reviewLogPath,
|
|
5706
6186
|
});
|
|
@@ -5751,7 +6231,7 @@ function agents(rawArgs) {
|
|
|
5751
6231
|
review: {
|
|
5752
6232
|
pid: reviewPid,
|
|
5753
6233
|
intervalSeconds: reviewIntervalSeconds,
|
|
5754
|
-
script:
|
|
6234
|
+
script: path.resolve(__filename),
|
|
5755
6235
|
logPath: reviewLogPath,
|
|
5756
6236
|
},
|
|
5757
6237
|
cleanup: {
|
|
@@ -5782,7 +6262,7 @@ function agents(rawArgs) {
|
|
|
5782
6262
|
return;
|
|
5783
6263
|
}
|
|
5784
6264
|
|
|
5785
|
-
const reviewStop = stopAgentProcessByPid(existingState?.review?.pid, '
|
|
6265
|
+
const reviewStop = stopAgentProcessByPid(existingState?.review?.pid, 'internal run-shell reviewBot');
|
|
5786
6266
|
const cleanupStop = stopAgentProcessByPid(existingState?.cleanup?.pid, `${path.basename(__filename)} cleanup`);
|
|
5787
6267
|
|
|
5788
6268
|
if (fs.existsSync(statePath)) {
|
|
@@ -6478,7 +6958,7 @@ function doctorAudit(rawArgs) {
|
|
|
6478
6958
|
ok('git core.hooksPath is .githooks');
|
|
6479
6959
|
}
|
|
6480
6960
|
|
|
6481
|
-
for (const relativePath of
|
|
6961
|
+
for (const relativePath of REQUIRED_MANAGED_REPO_FILES) {
|
|
6482
6962
|
const absolutePath = path.join(repoRoot, relativePath);
|
|
6483
6963
|
if (!fs.existsSync(absolutePath)) {
|
|
6484
6964
|
fail(`missing ${relativePath}`);
|
|
@@ -6511,17 +6991,18 @@ function doctorAudit(rawArgs) {
|
|
|
6511
6991
|
|
|
6512
6992
|
const packagePath = path.join(repoRoot, 'package.json');
|
|
6513
6993
|
if (!fs.existsSync(packagePath)) {
|
|
6514
|
-
warn('package.json not found (
|
|
6994
|
+
warn('package.json not found (legacy agent:* script drift cannot be checked)');
|
|
6515
6995
|
} else {
|
|
6516
6996
|
try {
|
|
6517
6997
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
6518
6998
|
const scripts = pkg.scripts || {};
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6999
|
+
const legacyAgentScripts = Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)
|
|
7000
|
+
.filter(([name, expectedValue]) => scripts[name] === expectedValue)
|
|
7001
|
+
.map(([name]) => name);
|
|
7002
|
+
if (legacyAgentScripts.length > 0) {
|
|
7003
|
+
warn(`legacy agent:* package.json scripts remain (${legacyAgentScripts.join(', ')}); run '${SHORT_TOOL_NAME} migrate' to remove them`);
|
|
7004
|
+
} else {
|
|
7005
|
+
ok('package.json does not contain Guardex-managed agent:* helper scripts');
|
|
6525
7006
|
}
|
|
6526
7007
|
} catch (error) {
|
|
6527
7008
|
fail(`package.json is invalid JSON: ${error.message}`);
|
|
@@ -6603,15 +7084,175 @@ function prompt(rawArgs) {
|
|
|
6603
7084
|
return copyPrompt();
|
|
6604
7085
|
}
|
|
6605
7086
|
|
|
7087
|
+
function printStandaloneOperations(title, rootLabel, operations, dryRun = false) {
|
|
7088
|
+
console.log(`[${TOOL_NAME}] ${title}: ${rootLabel}`);
|
|
7089
|
+
for (const operation of operations) {
|
|
7090
|
+
const note = operation.note ? ` (${operation.note})` : '';
|
|
7091
|
+
console.log(` - ${operation.status.padEnd(12)} ${operation.file}${note}`);
|
|
7092
|
+
}
|
|
7093
|
+
if (dryRun) {
|
|
7094
|
+
console.log(`[${TOOL_NAME}] Dry run complete. No files were modified.`);
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
|
|
7098
|
+
function branch(rawArgs) {
|
|
7099
|
+
const [subcommand, ...rest] = rawArgs;
|
|
7100
|
+
if (subcommand === 'start') {
|
|
7101
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7102
|
+
invokePackageAsset('branchStart', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7103
|
+
return;
|
|
7104
|
+
}
|
|
7105
|
+
if (subcommand === 'finish') {
|
|
7106
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7107
|
+
invokePackageAsset('branchFinish', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7108
|
+
return;
|
|
7109
|
+
}
|
|
7110
|
+
if (subcommand === 'merge') return merge(rest);
|
|
7111
|
+
throw new Error(
|
|
7112
|
+
`Usage: ${SHORT_TOOL_NAME} branch <start|finish|merge> [options] ` +
|
|
7113
|
+
`(examples: '${SHORT_TOOL_NAME} branch start "<task>" "<agent>"', '${SHORT_TOOL_NAME} branch finish --branch <agent/...>')`,
|
|
7114
|
+
);
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7117
|
+
function locks(rawArgs) {
|
|
7118
|
+
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
7119
|
+
const result = runPackageAsset('lockTool', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7120
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7121
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7122
|
+
process.exitCode = result.status;
|
|
7123
|
+
}
|
|
7124
|
+
|
|
7125
|
+
function worktree(rawArgs) {
|
|
7126
|
+
const [subcommand, ...rest] = rawArgs;
|
|
7127
|
+
if (subcommand === 'prune') {
|
|
7128
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7129
|
+
invokePackageAsset('worktreePrune', passthrough, { cwd: resolveRepoRoot(target) });
|
|
7130
|
+
return;
|
|
7131
|
+
}
|
|
7132
|
+
throw new Error(`Usage: ${SHORT_TOOL_NAME} worktree prune [cleanup-options]`);
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
function hook(rawArgs) {
|
|
7136
|
+
const [subcommand, ...rest] = rawArgs;
|
|
7137
|
+
if (subcommand === 'run') {
|
|
7138
|
+
const [hookName, ...hookArgs] = rest;
|
|
7139
|
+
if (!HOOK_NAMES.includes(hookName)) {
|
|
7140
|
+
throw new Error(`Unknown hook name: ${hookName || '(missing)'}`);
|
|
7141
|
+
}
|
|
7142
|
+
const { target, passthrough } = extractTargetedArgs(hookArgs);
|
|
7143
|
+
const hookAssetPath = path.join(TEMPLATE_ROOT, 'githooks', hookName);
|
|
7144
|
+
const result = run('bash', [hookAssetPath, ...passthrough], {
|
|
7145
|
+
cwd: resolveRepoRoot(target),
|
|
7146
|
+
stdio: hookName === 'pre-push' ? 'inherit' : 'pipe',
|
|
7147
|
+
env: packageAssetEnv(),
|
|
7148
|
+
});
|
|
7149
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7150
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7151
|
+
process.exitCode = result.status;
|
|
7152
|
+
return;
|
|
7153
|
+
}
|
|
7154
|
+
if (subcommand === 'install') {
|
|
7155
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7156
|
+
if (passthrough.length > 0) {
|
|
7157
|
+
throw new Error(`Unknown hook install option: ${passthrough[0]}`);
|
|
7158
|
+
}
|
|
7159
|
+
const repoRoot = resolveRepoRoot(target);
|
|
7160
|
+
const hookResult = configureHooks(repoRoot, false);
|
|
7161
|
+
console.log(`[${TOOL_NAME}] Hook install target: ${repoRoot}`);
|
|
7162
|
+
console.log(` - hooksPath ${hookResult.status} ${hookResult.key}=${hookResult.value}`);
|
|
7163
|
+
process.exitCode = 0;
|
|
7164
|
+
return;
|
|
7165
|
+
}
|
|
7166
|
+
throw new Error(`Usage: ${SHORT_TOOL_NAME} hook <run|install> ...`);
|
|
7167
|
+
}
|
|
7168
|
+
|
|
7169
|
+
function internal(rawArgs) {
|
|
7170
|
+
const [subcommand, assetKey, ...rest] = rawArgs;
|
|
7171
|
+
if (subcommand !== 'run-shell') {
|
|
7172
|
+
throw new Error(`Unknown internal command: ${subcommand || '(missing)'}`);
|
|
7173
|
+
}
|
|
7174
|
+
const { target, passthrough } = extractTargetedArgs(rest);
|
|
7175
|
+
const repoRoot = resolveRepoRoot(target);
|
|
7176
|
+
const result = assetKey === 'reviewBot'
|
|
7177
|
+
? runReviewBotCommand(repoRoot, passthrough)
|
|
7178
|
+
: runPackageAsset(assetKey, passthrough, { cwd: repoRoot });
|
|
7179
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7180
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7181
|
+
process.exitCode = result.status;
|
|
7182
|
+
}
|
|
7183
|
+
|
|
7184
|
+
function installAgentSkills(rawArgs) {
|
|
7185
|
+
let dryRun = false;
|
|
7186
|
+
let force = false;
|
|
7187
|
+
for (const arg of rawArgs) {
|
|
7188
|
+
if (arg === '--dry-run') {
|
|
7189
|
+
dryRun = true;
|
|
7190
|
+
continue;
|
|
7191
|
+
}
|
|
7192
|
+
if (arg === '--force') {
|
|
7193
|
+
force = true;
|
|
7194
|
+
continue;
|
|
7195
|
+
}
|
|
7196
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7199
|
+
const operations = USER_LEVEL_SKILL_ASSETS.map((asset) => installUserLevelAsset(asset, { dryRun, force }));
|
|
7200
|
+
printStandaloneOperations('User-level Guardex skills', GUARDEX_HOME_DIR, operations, dryRun);
|
|
7201
|
+
process.exitCode = 0;
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
function migrate(rawArgs) {
|
|
7205
|
+
const { target, passthrough } = extractTargetedArgs(rawArgs);
|
|
7206
|
+
let dryRun = false;
|
|
7207
|
+
let force = false;
|
|
7208
|
+
let installSkills = false;
|
|
7209
|
+
for (const arg of passthrough) {
|
|
7210
|
+
if (arg === '--dry-run') {
|
|
7211
|
+
dryRun = true;
|
|
7212
|
+
continue;
|
|
7213
|
+
}
|
|
7214
|
+
if (arg === '--force') {
|
|
7215
|
+
force = true;
|
|
7216
|
+
continue;
|
|
7217
|
+
}
|
|
7218
|
+
if (arg === '--install-agent-skills') {
|
|
7219
|
+
installSkills = true;
|
|
7220
|
+
continue;
|
|
7221
|
+
}
|
|
7222
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
7223
|
+
}
|
|
7224
|
+
|
|
7225
|
+
const repoRoot = resolveRepoRoot(target);
|
|
7226
|
+
const fixPayload = runFixInternal({
|
|
7227
|
+
target: repoRoot,
|
|
7228
|
+
dryRun,
|
|
7229
|
+
force,
|
|
7230
|
+
skipAgents: false,
|
|
7231
|
+
skipPackageJson: true,
|
|
7232
|
+
skipGitignore: false,
|
|
7233
|
+
dropStaleLocks: true,
|
|
7234
|
+
});
|
|
7235
|
+
printOperations('Migrate/fix', fixPayload, dryRun);
|
|
7236
|
+
|
|
7237
|
+
if (installSkills) {
|
|
7238
|
+
const skillOps = USER_LEVEL_SKILL_ASSETS.map((asset) => installUserLevelAsset(asset, { dryRun, force }));
|
|
7239
|
+
printStandaloneOperations('Migrate/install-agent-skills', GUARDEX_HOME_DIR, skillOps, dryRun);
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
const removableLegacyFiles = LEGACY_MANAGED_REPO_FILES.filter(
|
|
7243
|
+
(relativePath) => !REQUIRED_MANAGED_REPO_FILES.includes(relativePath),
|
|
7244
|
+
);
|
|
7245
|
+
const removalOps = removableLegacyFiles.map((relativePath) => removeLegacyManagedRepoFile(repoRoot, relativePath, { dryRun, force }));
|
|
7246
|
+
removalOps.push(removeLegacyPackageScripts(repoRoot, dryRun));
|
|
7247
|
+
printStandaloneOperations('Migrate/cleanup', repoRoot, removalOps, dryRun);
|
|
7248
|
+
process.exitCode = 0;
|
|
7249
|
+
}
|
|
7250
|
+
|
|
6606
7251
|
function cleanup(rawArgs) {
|
|
6607
7252
|
const options = parseCleanupArgs(rawArgs);
|
|
6608
7253
|
const repoRoot = resolveRepoRoot(options.target);
|
|
6609
|
-
const pruneScript = path.join(repoRoot, 'scripts', 'agent-worktree-prune.sh');
|
|
6610
|
-
if (!fs.existsSync(pruneScript)) {
|
|
6611
|
-
throw new Error(`Missing cleanup script: ${pruneScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
6612
|
-
}
|
|
6613
7254
|
|
|
6614
|
-
const args = [
|
|
7255
|
+
const args = [];
|
|
6615
7256
|
if (options.base) {
|
|
6616
7257
|
args.push('--base', options.base);
|
|
6617
7258
|
}
|
|
@@ -6642,7 +7283,7 @@ function cleanup(rawArgs) {
|
|
|
6642
7283
|
}
|
|
6643
7284
|
|
|
6644
7285
|
const runCleanupCycle = () => {
|
|
6645
|
-
const runResult =
|
|
7286
|
+
const runResult = runPackageAsset('worktreePrune', args, { cwd: repoRoot, stdio: 'inherit' });
|
|
6646
7287
|
if (runResult.status !== 0) {
|
|
6647
7288
|
throw new Error('Cleanup command failed');
|
|
6648
7289
|
}
|
|
@@ -6675,13 +7316,8 @@ function cleanup(rawArgs) {
|
|
|
6675
7316
|
function merge(rawArgs) {
|
|
6676
7317
|
const options = parseMergeArgs(rawArgs);
|
|
6677
7318
|
const repoRoot = resolveRepoRoot(options.target);
|
|
6678
|
-
const mergeScript = path.join(repoRoot, 'scripts', 'agent-branch-merge.sh');
|
|
6679
7319
|
|
|
6680
|
-
|
|
6681
|
-
throw new Error(`Missing merge script: ${mergeScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
6682
|
-
}
|
|
6683
|
-
|
|
6684
|
-
const args = [mergeScript];
|
|
7320
|
+
const args = [];
|
|
6685
7321
|
if (options.base) {
|
|
6686
7322
|
args.push('--base', options.base);
|
|
6687
7323
|
}
|
|
@@ -6698,7 +7334,7 @@ function merge(rawArgs) {
|
|
|
6698
7334
|
args.push('--branch', branch);
|
|
6699
7335
|
}
|
|
6700
7336
|
|
|
6701
|
-
const mergeResult =
|
|
7337
|
+
const mergeResult = runPackageAsset('branchMerge', args, { cwd: repoRoot, stdio: 'pipe' });
|
|
6702
7338
|
if (mergeResult.stdout) {
|
|
6703
7339
|
process.stdout.write(mergeResult.stdout);
|
|
6704
7340
|
}
|
|
@@ -6712,14 +7348,9 @@ function merge(rawArgs) {
|
|
|
6712
7348
|
process.exitCode = 0;
|
|
6713
7349
|
}
|
|
6714
7350
|
|
|
6715
|
-
function finish(rawArgs) {
|
|
6716
|
-
const options = parseFinishArgs(rawArgs);
|
|
7351
|
+
function finish(rawArgs, defaults = {}) {
|
|
7352
|
+
const options = parseFinishArgs(rawArgs, defaults);
|
|
6717
7353
|
const repoRoot = resolveRepoRoot(options.target);
|
|
6718
|
-
const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
|
|
6719
|
-
|
|
6720
|
-
if (!fs.existsSync(finishScript)) {
|
|
6721
|
-
throw new Error(`Missing finish script: ${finishScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
|
|
6722
|
-
}
|
|
6723
7354
|
|
|
6724
7355
|
const worktreeEntries = listAgentWorktrees(repoRoot);
|
|
6725
7356
|
const worktreeByBranch = new Map(worktreeEntries.map((entry) => [entry.branch, entry.worktreePath]));
|
|
@@ -6784,26 +7415,31 @@ function finish(rawArgs) {
|
|
|
6784
7415
|
}
|
|
6785
7416
|
|
|
6786
7417
|
const finishArgs = [
|
|
6787
|
-
finishScript,
|
|
6788
7418
|
'--branch',
|
|
6789
7419
|
branch,
|
|
6790
7420
|
'--base',
|
|
6791
7421
|
baseBranch,
|
|
6792
|
-
'--via-pr',
|
|
6793
7422
|
options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
|
|
6794
7423
|
options.cleanup ? '--cleanup' : '--no-cleanup',
|
|
6795
7424
|
];
|
|
7425
|
+
if (options.mergeMode === 'pr') {
|
|
7426
|
+
finishArgs.push('--via-pr');
|
|
7427
|
+
} else if (options.mergeMode === 'direct') {
|
|
7428
|
+
finishArgs.push('--direct-only');
|
|
7429
|
+
} else {
|
|
7430
|
+
finishArgs.push('--mode', 'auto');
|
|
7431
|
+
}
|
|
6796
7432
|
if (options.keepRemote) {
|
|
6797
7433
|
finishArgs.push('--keep-remote-branch');
|
|
6798
7434
|
}
|
|
6799
7435
|
|
|
6800
7436
|
if (options.dryRun) {
|
|
6801
|
-
console.log(`[${TOOL_NAME}] [dry-run] Would run:
|
|
7437
|
+
console.log(`[${TOOL_NAME}] [dry-run] Would run: gx branch finish ${finishArgs.join(' ')}`);
|
|
6802
7438
|
succeeded += 1;
|
|
6803
7439
|
continue;
|
|
6804
7440
|
}
|
|
6805
7441
|
|
|
6806
|
-
const finishResult =
|
|
7442
|
+
const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
|
|
6807
7443
|
if (finishResult.stdout) {
|
|
6808
7444
|
process.stdout.write(finishResult.stdout);
|
|
6809
7445
|
}
|
|
@@ -7197,6 +7833,13 @@ function main() {
|
|
|
7197
7833
|
|
|
7198
7834
|
if (command === 'prompt') return prompt(rest);
|
|
7199
7835
|
if (command === 'doctor') return doctor(rest);
|
|
7836
|
+
if (command === 'branch') return branch(rest);
|
|
7837
|
+
if (command === 'locks') return locks(rest);
|
|
7838
|
+
if (command === 'worktree') return worktree(rest);
|
|
7839
|
+
if (command === 'hook') return hook(rest);
|
|
7840
|
+
if (command === 'migrate') return migrate(rest);
|
|
7841
|
+
if (command === 'install-agent-skills') return installAgentSkills(rest);
|
|
7842
|
+
if (command === 'internal') return internal(rest);
|
|
7200
7843
|
if (command === 'agents') return agents(rest);
|
|
7201
7844
|
if (command === 'merge') return merge(rest);
|
|
7202
7845
|
if (command === 'finish') return finish(rest);
|