@imdeadpool/guardex 7.0.16 → 7.0.19

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