@imdeadpool/guardex 7.0.16 → 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,47 +92,67 @@ 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/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-agent-git-hooks.sh',
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 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 = [
114
125
  'scripts/agent-branch-start.sh',
115
126
  'scripts/agent-branch-finish.sh',
116
127
  'scripts/agent-branch-merge.sh',
128
+ 'scripts/agent-session-state.js',
129
+ 'scripts/codex-agent.sh',
117
130
  'scripts/guardex-docker-loader.sh',
131
+ 'scripts/install-vscode-active-agents-extension.js',
132
+ 'scripts/review-bot-watch.sh',
118
133
  'scripts/agent-worktree-prune.sh',
119
134
  'scripts/agent-file-locks.py',
120
135
  'scripts/guardex-env.sh',
121
136
  'scripts/install-agent-git-hooks.sh',
137
+ 'scripts/openspec/init-plan-workspace.sh',
138
+ 'scripts/openspec/init-change-workspace.sh',
122
139
  '.githooks/pre-commit',
140
+ '.githooks/pre-push',
123
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)),
124
152
  '.omx/state/agent-file-locks.json',
125
153
  ];
126
154
 
127
- const REQUIRED_PACKAGE_SCRIPTS = {
155
+ const LEGACY_MANAGED_PACKAGE_SCRIPTS = {
128
156
  'agent:codex': 'bash ./scripts/codex-agent.sh',
129
157
  'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
130
158
  'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
@@ -149,30 +177,44 @@ const REQUIRED_PACKAGE_SCRIPTS = {
149
177
  'agent:finish': 'gx finish --all',
150
178
  };
151
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
+
152
207
  const EXECUTABLE_RELATIVE_PATHS = new Set([
153
- 'scripts/agent-branch-start.sh',
154
- 'scripts/agent-branch-finish.sh',
155
- 'scripts/agent-branch-merge.sh',
156
- 'scripts/codex-agent.sh',
208
+ 'scripts/agent-session-state.js',
157
209
  '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',
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)),
168
213
  ]);
169
214
 
170
215
  const CRITICAL_GUARDRAIL_PATHS = new Set([
171
216
  'AGENTS.md',
172
- '.githooks/pre-commit',
173
- '.githooks/pre-push',
174
- '.githooks/post-merge',
175
- '.githooks/post-checkout',
217
+ ...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
176
218
  'scripts/agent-branch-start.sh',
177
219
  'scripts/agent-branch-finish.sh',
178
220
  'scripts/agent-branch-merge.sh',
@@ -202,9 +244,6 @@ const MANAGED_GITIGNORE_PATHS = [
202
244
  'scripts/agent-file-locks.py',
203
245
  '.githooks',
204
246
  '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
247
  LOCK_FILE_RELATIVE,
209
248
  ];
210
249
  const REPO_SCAFFOLD_DIRECTORIES = ['bin'];
@@ -237,6 +276,12 @@ const SUGGESTIBLE_COMMANDS = [
237
276
  'status',
238
277
  'setup',
239
278
  'doctor',
279
+ 'branch',
280
+ 'locks',
281
+ 'worktree',
282
+ 'hook',
283
+ 'migrate',
284
+ 'install-agent-skills',
240
285
  'agents',
241
286
  'merge',
242
287
  'finish',
@@ -262,6 +307,12 @@ const CLI_COMMAND_DESCRIPTIONS = [
262
307
  ['status', 'Show GitGuardex CLI + service health without modifying files'],
263
308
  ['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
264
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'],
265
316
  ['protect', 'Manage protected branches (list/add/remove/set/reset)'],
266
317
  ['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
267
318
  ['sync', 'Sync agent branches with origin/<base>'],
@@ -272,7 +323,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
272
323
  ['prompt', 'Print AI setup checklist (--exec, --snippet)'],
273
324
  ['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
274
325
  ['help', 'Show this help output'],
275
- ['version', 'Print GuardeX version'],
326
+ ['version', 'Print GitGuardex version'],
276
327
  ];
277
328
  const DEPRECATED_COMMAND_ALIASES = new Map([
278
329
  ['init', { target: 'setup', hint: 'gx setup' }],
@@ -306,11 +357,11 @@ function defaultAgentWorktreeRelativeDir(env = process.env) {
306
357
 
307
358
  const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in this repo.
308
359
 
309
- 1) Install: npm i -g @imdeadpool/guardex && gh --version
360
+ 1) Install: ${GLOBAL_INSTALL_COMMAND} && gh --version
310
361
  2) Bootstrap: gx setup
311
362
  3) Repair: gx doctor
312
- 4) Task loop: bash scripts/codex-agent.sh "<task>" "<agent>"
313
- or branch-start -> python3 scripts/agent-file-locks.py claim -> branch-finish
363
+ 4) Task loop: gx branch start "<task>" "<agent>"
364
+ then gx locks claim --branch "<agent-branch>" <file...> -> gx branch finish
314
365
  5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
315
366
  6) Finish: gx finish --all
316
367
  7) Cleanup: gx cleanup
@@ -321,12 +372,12 @@ const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in thi
321
372
  12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
322
373
  `;
323
374
 
324
- const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
375
+ const AI_SETUP_COMMANDS = `${GLOBAL_INSTALL_COMMAND}
325
376
  gh --version
326
377
  gx setup
327
378
  gx doctor
328
- bash scripts/codex-agent.sh "<task>" "<agent>"
329
- 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...>
330
381
  gx merge --branch "<agent-a>" --branch "<agent-b>"
331
382
  gx finish --all
332
383
  gx cleanup
@@ -357,7 +408,17 @@ function runtimeVersion() {
357
408
  }
358
409
 
359
410
  function supportsAnsiColors() {
360
- 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';
361
422
  }
362
423
 
363
424
  function colorize(text, colorCode) {
@@ -367,6 +428,56 @@ function colorize(text, colorCode) {
367
428
  return `\u001B[${colorCode}m${text}\u001B[0m`;
368
429
  }
369
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
+
370
481
  function statusDot(status) {
371
482
  if (status === 'active') {
372
483
  return colorize('●', '32'); // green
@@ -512,10 +623,74 @@ function run(cmd, args, options = {}) {
512
623
  encoding: 'utf8',
513
624
  stdio: options.stdio || 'pipe',
514
625
  cwd: options.cwd,
626
+ env: options.env ? { ...process.env, ...options.env } : process.env,
515
627
  timeout: options.timeout,
516
628
  });
517
629
  }
518
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',
678
+ timeout: options.timeout,
679
+ env: packageAssetEnv(options.env),
680
+ });
681
+ }
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
+
519
694
  function formatElapsedDuration(ms) {
520
695
  const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
521
696
  if (durationMs < 1000) {
@@ -604,22 +779,29 @@ function printAutoFinishSummary(summary, options = {}) {
604
779
 
605
780
  if (enabled) {
606
781
  console.log(
607
- `[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
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
+ ),
608
786
  );
609
787
  const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
610
788
  for (const detail of visibleDetails) {
611
- console.log(`[${TOOL_NAME}] ${detail}`);
789
+ console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
612
790
  }
613
791
  if (!verbose && details.length > detailLimit) {
614
792
  console.log(
615
- `[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
793
+ colorizeDoctorOutput(
794
+ `[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
795
+ 'warn',
796
+ ),
616
797
  );
617
798
  }
618
799
  return;
619
800
  }
620
801
 
621
802
  if (details.length > 0) {
622
- console.log(`[${TOOL_NAME}] ${verbose ? details[0] : summarizeAutoFinishDetail(details[0])}`);
803
+ const detail = verbose ? details[0] : summarizeAutoFinishDetail(details[0]);
804
+ console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
623
805
  }
624
806
  }
625
807
 
@@ -747,6 +929,9 @@ function toDestinationPath(relativeTemplatePath) {
747
929
  if (relativeTemplatePath.startsWith('github/')) {
748
930
  return `.${relativeTemplatePath}`;
749
931
  }
932
+ if (relativeTemplatePath.startsWith('vscode/')) {
933
+ return relativeTemplatePath;
934
+ }
750
935
  throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
751
936
  }
752
937
 
@@ -784,6 +969,111 @@ function isCriticalGuardrailPath(relativePath) {
784
969
  return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
785
970
  }
786
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
+
787
1077
  function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
788
1078
  const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
789
1079
  const destinationRelativePath = toDestinationPath(relativeTemplatePath);
@@ -961,8 +1251,7 @@ function writeLockState(repoRoot, payload, dryRun) {
961
1251
  fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
962
1252
  }
963
1253
 
964
- function ensurePackageScripts(repoRoot, dryRun, options = {}) {
965
- const force = Boolean(options.force);
1254
+ function removeLegacyPackageScripts(repoRoot, dryRun) {
966
1255
  const packagePath = path.join(repoRoot, 'package.json');
967
1256
  if (!fs.existsSync(packagePath)) {
968
1257
  return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
@@ -978,29 +1267,87 @@ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
978
1267
  const existingScripts = pkg.scripts && typeof pkg.scripts === 'object'
979
1268
  ? pkg.scripts
980
1269
  : {};
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
1270
  pkg.scripts = existingScripts;
987
1271
  let changed = false;
988
- for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
989
- if (pkg.scripts[key] !== value) {
990
- 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];
991
1275
  changed = true;
992
1276
  }
993
1277
  }
994
1278
 
995
1279
  if (!changed) {
996
- return { status: 'unchanged', file: 'package.json' };
1280
+ return { status: 'unchanged', file: 'package.json', note: 'no Guardex-managed agent:* scripts found' };
997
1281
  }
998
1282
 
999
1283
  if (!dryRun) {
1000
1284
  fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
1001
1285
  }
1002
1286
 
1003
- 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 };
1004
1351
  }
1005
1352
 
1006
1353
  function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
@@ -1366,7 +1713,7 @@ function assertProtectedMainWriteAllowed(options, commandName) {
1366
1713
  throw new Error(
1367
1714
  `${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
1368
1715
  `Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
1369
- ` bash scripts/agent-branch-start.sh "<task>" "codex"\n` +
1716
+ ` gx branch start "<task>" "codex"\n` +
1370
1717
  `Override once only when intentional: --allow-protected-base-write`,
1371
1718
  );
1372
1719
  }
@@ -1592,8 +1939,7 @@ function startProtectedBaseSandbox(blocked, { taskName, sandboxSuffix }) {
1592
1939
  return startProtectedBaseSandboxFallback(blocked, sandboxSuffix);
1593
1940
  }
1594
1941
 
1595
- const startResult = run('bash', [
1596
- startScript,
1942
+ const startResult = runPackageAsset('branchStart', [
1597
1943
  '--task',
1598
1944
  taskName,
1599
1945
  '--agent',
@@ -1742,8 +2088,7 @@ function collectWorktreeDirtyPaths(worktreePath) {
1742
2088
  }
1743
2089
 
1744
2090
  function collectDoctorForceAddPaths(worktreePath) {
1745
- return TEMPLATE_FILES
1746
- .map((entry) => toDestinationPath(entry))
2091
+ return REQUIRED_WORKFLOW_FILES
1747
2092
  .filter((relativePath) => relativePath.startsWith('scripts/') || relativePath.startsWith('.githooks/'))
1748
2093
  .filter((relativePath) => fs.existsSync(path.join(worktreePath, relativePath)));
1749
2094
  }
@@ -1795,13 +2140,13 @@ function claimDoctorChangedLocks(metadata) {
1795
2140
  ]));
1796
2141
  const deletedPaths = collectDoctorDeletedPaths(metadata.worktreePath);
1797
2142
  if (changedPaths.length > 0) {
1798
- run('python3', [lockScript, 'claim', '--branch', metadata.branch, ...changedPaths], {
2143
+ runPackageAsset('lockTool', ['claim', '--branch', metadata.branch, ...changedPaths], {
1799
2144
  cwd: metadata.worktreePath,
1800
2145
  timeout: 30_000,
1801
2146
  });
1802
2147
  }
1803
2148
  if (deletedPaths.length > 0) {
1804
- run('python3', [lockScript, 'allow-delete', '--branch', metadata.branch, ...deletedPaths], {
2149
+ runPackageAsset('lockTool', ['allow-delete', '--branch', metadata.branch, ...deletedPaths], {
1805
2150
  cwd: metadata.worktreePath,
1806
2151
  timeout: 30_000,
1807
2152
  });
@@ -1947,7 +2292,7 @@ function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
1947
2292
 
1948
2293
  const finishResult = run(
1949
2294
  'bash',
1950
- [finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg],
2295
+ [finishScript, '--branch', metadata.branch, '--base', blocked.branch, '--via-pr', waitForMergeArg, '--cleanup'],
1951
2296
  { cwd: metadata.worktreePath, timeout: finishTimeoutMs },
1952
2297
  );
1953
2298
  if (isSpawnFailure(finishResult)) {
@@ -2018,7 +2363,7 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
2018
2363
  ...(autoCommitResult.stagedFiles || []),
2019
2364
  ...OMX_SCAFFOLD_DIRECTORIES,
2020
2365
  ...Array.from(OMX_SCAFFOLD_FILES.keys()),
2021
- ...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
2366
+ ...REQUIRED_WORKFLOW_FILES,
2022
2367
  'bin',
2023
2368
  'package.json',
2024
2369
  '.gitignore',
@@ -2156,9 +2501,7 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
2156
2501
  }
2157
2502
 
2158
2503
  function syncDoctorLocalSupportFiles(repoRoot, dryRun) {
2159
- return TEMPLATE_FILES
2160
- .filter((entry) => entry.startsWith('codex/') || entry.startsWith('claude/'))
2161
- .map((entry) => ensureTemplateFilePresent(repoRoot, entry, dryRun));
2504
+ return [];
2162
2505
  }
2163
2506
 
2164
2507
  function runDoctorInSandbox(options, blocked) {
@@ -2438,7 +2781,7 @@ function runDoctorInSandbox(options, blocked) {
2438
2781
  if (finishResult.stderr) process.stderr.write(finishResult.stderr);
2439
2782
  } else if (finishResult.status === 'failed') {
2440
2783
  console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
2441
- console.log(`[guardex] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
2784
+ console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
2442
2785
  if (finishResult.stdout) process.stdout.write(finishResult.stdout);
2443
2786
  if (finishResult.stderr) process.stderr.write(finishResult.stderr);
2444
2787
  } else {
@@ -3116,7 +3459,6 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
3116
3459
 
3117
3460
  summary.attempted += 1;
3118
3461
  const finishArgs = [
3119
- finishScript,
3120
3462
  '--branch',
3121
3463
  branch,
3122
3464
  '--base',
@@ -3125,7 +3467,7 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
3125
3467
  waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
3126
3468
  '--cleanup',
3127
3469
  ];
3128
- const finishResult = run('bash', finishArgs, { cwd: repoRoot });
3470
+ const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot });
3129
3471
  const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
3130
3472
 
3131
3473
  if (finishResult.status === 0) {
@@ -3278,9 +3620,9 @@ function printSetupRepoHints(repoRoot, baseBranch, repoLabel = '') {
3278
3620
  console.log(`[${TOOL_NAME}] Bootstrap commit${label}: git add . && git commit -m "bootstrap gitguardex"`);
3279
3621
  console.log(
3280
3622
  `[${TOOL_NAME}] First agent flow${label}: ` +
3281
- `bash scripts/agent-branch-start.sh "<task>" "codex" -> ` +
3282
- `python3 scripts/agent-file-locks.py claim --branch "$(git branch --show-current)" <file...> -> ` +
3283
- `bash scripts/agent-branch-finish.sh --branch "$(git branch --show-current)" --base ${baseBranch} --via-pr --wait-for-merge`,
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`,
3284
3626
  );
3285
3627
  }
3286
3628
  if (!hasOrigin) {
@@ -3628,19 +3970,20 @@ function parseMergeArgs(rawArgs) {
3628
3970
  return options;
3629
3971
  }
3630
3972
 
3631
- function parseFinishArgs(rawArgs) {
3973
+ function parseFinishArgs(rawArgs, defaults = {}) {
3632
3974
  const options = {
3633
3975
  target: process.cwd(),
3634
3976
  base: '',
3635
3977
  branch: '',
3636
3978
  all: false,
3637
3979
  dryRun: false,
3638
- waitForMerge: true,
3639
- cleanup: true,
3980
+ waitForMerge: defaults.waitForMerge ?? true,
3981
+ cleanup: defaults.cleanup ?? true,
3640
3982
  keepRemote: false,
3641
3983
  noAutoCommit: false,
3642
3984
  failFast: false,
3643
3985
  commitMessage: '',
3986
+ mergeMode: defaults.mergeMode || 'pr',
3644
3987
  };
3645
3988
 
3646
3989
  for (let index = 0; index < rawArgs.length; index += 1) {
@@ -3697,6 +4040,26 @@ function parseFinishArgs(rawArgs) {
3697
4040
  options.waitForMerge = false;
3698
4041
  continue;
3699
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
+ }
3700
4063
  if (arg === '--cleanup') {
3701
4064
  options.cleanup = true;
3702
4065
  continue;
@@ -3861,7 +4224,7 @@ function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
3861
4224
  ]);
3862
4225
 
3863
4226
  if (changedFiles.length > 0) {
3864
- const claim = run('python3', [lockScript, 'claim', '--branch', branch, ...changedFiles], {
4227
+ const claim = runPackageAsset('lockTool', ['claim', '--branch', branch, ...changedFiles], {
3865
4228
  cwd: repoRoot,
3866
4229
  stdio: 'pipe',
3867
4230
  });
@@ -3895,7 +4258,7 @@ function claimLocksForAutoCommit(repoRoot, worktreePath, branch) {
3895
4258
  ]);
3896
4259
 
3897
4260
  if (deletedFiles.length > 0) {
3898
- const allowDelete = run('python3', [lockScript, 'allow-delete', '--branch', branch, ...deletedFiles], {
4261
+ const allowDelete = runPackageAsset('lockTool', ['allow-delete', '--branch', branch, ...deletedFiles], {
3899
4262
  cwd: repoRoot,
3900
4263
  stdio: 'pipe',
3901
4264
  });
@@ -4673,6 +5036,16 @@ function askGlobalInstallForMissing(options, missingPackages, missingLocalTools)
4673
5036
  }
4674
5037
 
4675
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
+
4676
5049
  if (options.dryRun) {
4677
5050
  return { status: 'dry-run-skip' };
4678
5051
  }
@@ -4701,11 +5074,11 @@ function installGlobalToolchain(options) {
4701
5074
 
4702
5075
  const missingPackages = detection.ok ? detection.missing : [...GLOBAL_TOOLCHAIN_PACKAGES];
4703
5076
  const missingLocalTools = localCompanionTools.filter((tool) => tool.status !== 'active');
4704
- const approval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
4705
- if (!approval.approved) {
5077
+ const installApproval = askGlobalInstallForMissing(options, missingPackages, missingLocalTools);
5078
+ if (!installApproval.approved) {
4706
5079
  return {
4707
5080
  status: 'skipped',
4708
- reason: approval.source,
5081
+ reason: installApproval.source,
4709
5082
  missingPackages,
4710
5083
  missingLocalTools,
4711
5084
  };
@@ -4800,13 +5173,15 @@ function runInstallInternal(options) {
4800
5173
  for (const templateFile of TEMPLATE_FILES) {
4801
5174
  operations.push(copyTemplateFile(repoRoot, templateFile, Boolean(options.force), Boolean(options.dryRun)));
4802
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
+ }
4803
5182
 
4804
5183
  operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
4805
5184
 
4806
- if (!options.skipPackageJson) {
4807
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
4808
- }
4809
-
4810
5185
  if (!options.skipAgents) {
4811
5186
  operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
4812
5187
  }
@@ -4845,6 +5220,12 @@ function runFixInternal(options) {
4845
5220
  for (const templateFile of TEMPLATE_FILES) {
4846
5221
  operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
4847
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
+ }
4848
5229
 
4849
5230
  operations.push(ensureLockRegistry(repoRoot, Boolean(options.dryRun)));
4850
5231
 
@@ -4874,10 +5255,6 @@ function runFixInternal(options) {
4874
5255
  }
4875
5256
  }
4876
5257
 
4877
- if (!options.skipPackageJson) {
4878
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
4879
- }
4880
-
4881
5258
  if (!options.skipAgents) {
4882
5259
  operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
4883
5260
  }
@@ -4907,8 +5284,7 @@ function runScanInternal(options) {
4907
5284
  const requiredPaths = [
4908
5285
  ...OMX_SCAFFOLD_DIRECTORIES,
4909
5286
  ...Array.from(OMX_SCAFFOLD_FILES.keys()),
4910
- ...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
4911
- LOCK_FILE_RELATIVE,
5287
+ ...REQUIRED_WORKFLOW_FILES,
4912
5288
  ];
4913
5289
 
4914
5290
  for (const relativePath of requiredPaths) {
@@ -5043,21 +5419,34 @@ function printScanResult(scan, json = false) {
5043
5419
 
5044
5420
  if (scan.guardexEnabled === false) {
5045
5421
  console.log(
5046
- `[${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
+ ),
5047
5426
  );
5048
5427
  return;
5049
5428
  }
5050
5429
 
5051
5430
  if (scan.findings.length === 0) {
5052
- console.log(`[${TOOL_NAME}] ✅ No safety issues detected.`);
5431
+ console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ No safety issues detected.`, 'safe'));
5053
5432
  return;
5054
5433
  }
5055
5434
 
5056
5435
  for (const item of scan.findings) {
5057
5436
  const target = item.path ? ` (${item.path})` : '';
5058
- 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
+ );
5059
5443
  }
5060
- 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
+ );
5061
5450
  }
5062
5451
 
5063
5452
  function setExitCodeFromScan(scan) {
@@ -5498,10 +5887,13 @@ function doctor(rawArgs) {
5498
5887
  verbose: singleRepoOptions.verboseAutoFinish,
5499
5888
  });
5500
5889
  if (safe) {
5501
- console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
5890
+ console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ✅ Repo is fully safe.`, 'safe'));
5502
5891
  } else {
5503
5892
  console.log(
5504
- `[${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
+ ),
5505
5897
  );
5506
5898
  }
5507
5899
  setExitCodeFromScan(scanResult);
@@ -6511,17 +6903,18 @@ function doctorAudit(rawArgs) {
6511
6903
 
6512
6904
  const packagePath = path.join(repoRoot, 'package.json');
6513
6905
  if (!fs.existsSync(packagePath)) {
6514
- warn('package.json not found (npm helper scripts cannot be verified)');
6906
+ warn('package.json not found (legacy agent:* script drift cannot be checked)');
6515
6907
  } else {
6516
6908
  try {
6517
6909
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
6518
6910
  const scripts = pkg.scripts || {};
6519
- for (const [name, expectedValue] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
6520
- if (scripts[name] !== expectedValue) {
6521
- fail(`package.json script mismatch for "${name}"`);
6522
- } else {
6523
- ok(`package.json script "${name}" is configured`);
6524
- }
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');
6525
6918
  }
6526
6919
  } catch (error) {
6527
6920
  fail(`package.json is invalid JSON: ${error.message}`);
@@ -6603,6 +6996,167 @@ function prompt(rawArgs) {
6603
6996
  return copyPrompt();
6604
6997
  }
6605
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
+
6606
7160
  function cleanup(rawArgs) {
6607
7161
  const options = parseCleanupArgs(rawArgs);
6608
7162
  const repoRoot = resolveRepoRoot(options.target);
@@ -6611,7 +7165,7 @@ function cleanup(rawArgs) {
6611
7165
  throw new Error(`Missing cleanup script: ${pruneScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
6612
7166
  }
6613
7167
 
6614
- const args = [pruneScript];
7168
+ const args = [];
6615
7169
  if (options.base) {
6616
7170
  args.push('--base', options.base);
6617
7171
  }
@@ -6642,7 +7196,7 @@ function cleanup(rawArgs) {
6642
7196
  }
6643
7197
 
6644
7198
  const runCleanupCycle = () => {
6645
- const runResult = run('bash', args, { cwd: repoRoot, stdio: 'inherit' });
7199
+ const runResult = runPackageAsset('worktreePrune', args, { cwd: repoRoot, stdio: 'inherit' });
6646
7200
  if (runResult.status !== 0) {
6647
7201
  throw new Error('Cleanup command failed');
6648
7202
  }
@@ -6681,7 +7235,7 @@ function merge(rawArgs) {
6681
7235
  throw new Error(`Missing merge script: ${mergeScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
6682
7236
  }
6683
7237
 
6684
- const args = [mergeScript];
7238
+ const args = [];
6685
7239
  if (options.base) {
6686
7240
  args.push('--base', options.base);
6687
7241
  }
@@ -6698,7 +7252,7 @@ function merge(rawArgs) {
6698
7252
  args.push('--branch', branch);
6699
7253
  }
6700
7254
 
6701
- const mergeResult = run('bash', args, { cwd: repoRoot, stdio: 'pipe' });
7255
+ const mergeResult = runPackageAsset('branchMerge', args, { cwd: repoRoot, stdio: 'pipe' });
6702
7256
  if (mergeResult.stdout) {
6703
7257
  process.stdout.write(mergeResult.stdout);
6704
7258
  }
@@ -6712,8 +7266,8 @@ function merge(rawArgs) {
6712
7266
  process.exitCode = 0;
6713
7267
  }
6714
7268
 
6715
- function finish(rawArgs) {
6716
- const options = parseFinishArgs(rawArgs);
7269
+ function finish(rawArgs, defaults = {}) {
7270
+ const options = parseFinishArgs(rawArgs, defaults);
6717
7271
  const repoRoot = resolveRepoRoot(options.target);
6718
7272
  const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
6719
7273
 
@@ -6784,26 +7338,31 @@ function finish(rawArgs) {
6784
7338
  }
6785
7339
 
6786
7340
  const finishArgs = [
6787
- finishScript,
6788
7341
  '--branch',
6789
7342
  branch,
6790
7343
  '--base',
6791
7344
  baseBranch,
6792
- '--via-pr',
6793
7345
  options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge',
6794
7346
  options.cleanup ? '--cleanup' : '--no-cleanup',
6795
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
+ }
6796
7355
  if (options.keepRemote) {
6797
7356
  finishArgs.push('--keep-remote-branch');
6798
7357
  }
6799
7358
 
6800
7359
  if (options.dryRun) {
6801
- 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(' ')}`);
6802
7361
  succeeded += 1;
6803
7362
  continue;
6804
7363
  }
6805
7364
 
6806
- const finishResult = run('bash', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
7365
+ const finishResult = runPackageAsset('branchFinish', finishArgs, { cwd: repoRoot, stdio: 'pipe' });
6807
7366
  if (finishResult.stdout) {
6808
7367
  process.stdout.write(finishResult.stdout);
6809
7368
  }
@@ -7197,6 +7756,13 @@ function main() {
7197
7756
 
7198
7757
  if (command === 'prompt') return prompt(rest);
7199
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);
7200
7766
  if (command === 'agents') return agents(rest);
7201
7767
  if (command === 'merge') return merge(rest);
7202
7768
  if (command === 'finish') return finish(rest);