@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.
@@ -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 packageJsonPath = path.resolve(__dirname, '..', 'package.json');
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.resolve(__dirname, '..', 'templates');
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-branch-start.sh',
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-agent-git-hooks.sh',
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 REQUIRED_WORKFLOW_FILES = [
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 REQUIRED_PACKAGE_SCRIPTS = {
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-branch-start.sh',
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/review-bot-watch.sh',
155
- 'scripts/agent-worktree-prune.sh',
156
- 'scripts/agent-file-locks.py',
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/pre-commit',
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 GuardeX version'],
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: npm i -g @imdeadpool/guardex && gh --version
360
+ 1) Install: ${GLOBAL_INSTALL_COMMAND} && gh --version
300
361
  2) Bootstrap: gx setup
301
362
  3) Repair: gx doctor
302
- 4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
303
- or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
304
- 5) Finish: gx finish --all
305
- 6) Cleanup: gx cleanup
306
- 7) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
307
- 8) Optional: gx protect add release staging
308
- 9) Optional: gx sync --check && gx sync
309
- 10) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
310
- 11) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
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 = `npm i -g @imdeadpool/guardex
375
+ const AI_SETUP_COMMANDS = `${GLOBAL_INSTALL_COMMAND}
314
376
  gh --version
315
377
  gx setup
316
378
  gx doctor
317
- bash scripts/codex-agent.sh "<task>" "<agent>"
318
- python3 scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>
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
- return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
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 ensurePackageScripts(repoRoot, dryRun, options = {}) {
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(REQUIRED_PACKAGE_SCRIPTS)) {
870
- if (pkg.scripts[key] !== value) {
871
- pkg.scripts[key] = value;
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
- return parseRepoTraversalArgs(rawArgs, {
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
- ` bash scripts/agent-branch-start.sh "<task>" "codex"\n` +
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 = run('bash', [
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 TEMPLATE_FILES
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
- run('python3', [lockScript, 'claim', '--branch', metadata.branch, ...changedPaths], {
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
- run('python3', [lockScript, 'allow-delete', '--branch', metadata.branch, ...deletedPaths], {
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
- ...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
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 TEMPLATE_FILES
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(`[guardex] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
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
- if (postSandboxAutoFinishSummary.enabled) {
2311
- console.log(
2312
- `[${TOOL_NAME}] Auto-finish sweep (base=${blocked.branch}): attempted=${postSandboxAutoFinishSummary.attempted}, completed=${postSandboxAutoFinishSummary.completed}, skipped=${postSandboxAutoFinishSummary.skipped}, failed=${postSandboxAutoFinishSummary.failed}`,
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 = run('bash', finishArgs, { cwd: repoRoot });
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
- `bash scripts/agent-branch-start.sh "<task>" "codex" -> ` +
3149
- `python3 scripts/agent-file-locks.py claim --branch "$(git branch --show-current)" <file...> -> ` +
3150
- `bash scripts/agent-branch-finish.sh --branch "$(git branch --show-current)" --base ${baseBranch} --via-pr --wait-for-merge`,
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 parseFinishArgs(rawArgs) {
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 = run('python3', [lockScript, 'claim', '--branch', branch, ...changedFiles], {
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 = run('python3', [lockScript, 'allow-delete', '--branch', branch, ...deletedFiles], {
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 approval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
4496
- if (!approval.approved) {
5077
+ const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
5078
+ if (!installApproval.approved) {
4497
5079
  return {
4498
5080
  status: 'skipped',
4499
- reason: approval.source,
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
- ...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
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
- `[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
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(`[${item.level.toUpperCase()}] ${item.code}${target}: ${item.message}`);
5437
+ console.log(
5438
+ colorizeDoctorOutput(
5439
+ `[${item.level.toUpperCase()}] ${item.code}${target}: ${item.message}`,
5440
+ item.level,
5441
+ ),
5442
+ );
4850
5443
  }
4851
- console.log(`[${TOOL_NAME}] Summary: ${scan.errors} error(s), ${scan.warnings} warning(s).`);
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 (const repoPath of discoveredRepos) {
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 nestedResult = run(
5136
- process.execPath,
5137
- [
5138
- path.resolve(__filename),
5139
- 'doctor',
5140
- '--single-repo',
5141
- '--target',
5142
- repoPath,
5143
- ...(options.dropStaleLocks ? [] : ['--keep-stale-locks']),
5144
- ...(options.skipAgents ? ['--skip-agents'] : []),
5145
- ...(options.skipPackageJson ? ['--skip-package-json'] : []),
5146
- ...(options.skipGitignore ? ['--no-gitignore'] : []),
5147
- ...(options.dryRun ? ['--dry-run'] : []),
5148
- // Recursive child doctor runs should report pending PR state immediately instead of blocking the parent loop.
5149
- '--no-wait-for-merge',
5150
- ...(options.json ? ['--json'] : []),
5151
- ...(options.allowProtectedBaseWrite ? ['--allow-protected-base-write'] : []),
5152
- ],
5153
- { cwd: topRepoRoot },
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
- if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
5185
- if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
5186
- process.stdout.write('\n');
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
- if (autoFinishSummary.enabled) {
5277
- console.log(
5278
- `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
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
- `[${TOOL_NAME}] ⚠️ Repo is not fully safe yet (${scanResult.errors} error(s), ${scanResult.warnings} warning(s)).`,
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 (npm helper scripts cannot be verified)');
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
- for (const [name, expectedValue] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
6306
- if (scripts[name] !== expectedValue) {
6307
- fail(`package.json script mismatch for "${name}"`);
6308
- } else {
6309
- ok(`package.json script "${name}" is configured`);
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 = [pruneScript];
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 = run('bash', args, { cwd: repoRoot, stdio: 'inherit' });
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 finish(rawArgs) {
6462
- const options = parseFinishArgs(rawArgs);
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: bash ${finishArgs.join(' ')}`);
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 = run('bash', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
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);