@imdeadpool/guardex 5.0.0 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,26 +50,29 @@ Related tools:
50
50
 
51
51
  - [oh-my-codex (OMX)](https://github.com/Yeachan-Heo/oh-my-codex)
52
52
  - [OpenSpec](https://github.com/Fission-AI/OpenSpec)
53
+ - [codex-account-switcher-cli](https://github.com/recodeecom/codex-account-switcher-cli)
53
54
 
54
55
  ## Fast setup (recommended)
55
56
 
56
57
  ```sh
57
58
  # inside your repo
58
59
  gx setup
60
+ # alias:
61
+ gx init
59
62
  ```
60
63
 
61
64
  That one command runs:
62
65
 
63
- 1. detects whether OMX/OpenSpec are already globally installed,
66
+ 1. detects whether OMX/OpenSpec/codex-auth are already globally installed,
64
67
  2. asks strict Y/N approval only if something is missing,
65
68
  3. installs guardrail scripts/hooks,
66
69
  4. repairs common safety problems,
67
70
  5. installs local Codex + Claude gx helper skill files if missing,
68
71
  6. scans and reports final status.
69
72
 
70
- ## Setup screenshot
73
+ ## Setup behavior screenshot
71
74
 
72
- ![gx setup success screenshot](https://raw.githubusercontent.com/recodeecom/multiagent-safety/main/docs/images/setup-success.svg)
75
+ ![gx status/setup behavior screenshot](https://raw.githubusercontent.com/recodeecom/multiagent-safety/main/docs/images/setup-success.svg)
73
76
 
74
77
  ## Status logs screenshot
75
78
 
@@ -217,10 +220,11 @@ Use this exact checklist to setup multi-agent safety in this repository for Code
217
220
 
218
221
  2) Bootstrap safety in this repo:
219
222
  gx setup
223
+ # alias: gx init
220
224
 
221
- - Setup detects global OMX/OpenSpec first.
225
+ - Setup detects global OMX/OpenSpec/codex-auth first.
222
226
  - If one is missing and setup asks for approval, reply explicitly:
223
- - y = run: npm i -g oh-my-codex @fission-ai/openspec (missing ones only)
227
+ - y = run: npm i -g oh-my-codex @fission-ai/openspec @imdeadpool/codex-account-switcher (missing ones only)
224
228
  - n = skip global installs
225
229
 
226
230
  3) If setup reports warnings/errors, repair + re-check:
@@ -231,6 +235,13 @@ Use this exact checklist to setup multi-agent safety in this repository for Code
231
235
  bash scripts/agent-branch-start.sh "task" "agent-name"
232
236
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
233
237
  bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
238
+ - For every new user message/task, repeat the same cycle:
239
+ start isolated agent branch/worktree -> claim file locks -> implement/verify ->
240
+ finish via PR/merge cleanup with scripts/agent-branch-finish.sh.
241
+ - `scripts/codex-agent.sh` now auto-runs this finish flow after Codex exits:
242
+ auto-commit changed files -> push/create PR -> merge attempt -> keep branch/worktree for follow-up.
243
+ - Remove merged branches when you are done reviewing:
244
+ gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
234
245
 
235
246
  5) Optional: create OpenSpec planning workspace:
236
247
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
@@ -250,8 +261,9 @@ Use this exact checklist to setup multi-agent safety in this repository for Code
250
261
 
251
262
  ```sh
252
263
  gx status [--target <path>] [--json]
253
- gx setup [--target <path>] [--dry-run] [--yes-global-install|--no-global-install] [--no-gitignore]
254
- gx doctor [--target <path>] [--dry-run] [--json] [--keep-stale-locks] [--no-gitignore]
264
+ gx setup [--target <path>] [--dry-run] [--yes-global-install|--no-global-install] [--no-gitignore] [--allow-protected-base-write]
265
+ gx init [--target <path>] [--dry-run] [--yes-global-install|--no-global-install] [--no-gitignore] [--allow-protected-base-write]
266
+ gx doctor [--target <path>] [--dry-run] [--json] [--keep-stale-locks] [--no-gitignore] [--allow-protected-base-write]
255
267
  gx copy-prompt
256
268
  gx copy-commands
257
269
  gx protect list [--target <path>]
@@ -261,25 +273,37 @@ gx protect set <branch...> [--target <path>]
261
273
  gx protect reset [--target <path>]
262
274
  gx sync --check [--target <path>] [--base <branch>] [--json]
263
275
  gx sync [--target <path>] [--base <branch>] [--strategy rebase|merge] [--ff-only]
276
+ gx cleanup [--target <path>] [--base <branch>] [--branch <agent/...>] [--dry-run] [--force-dirty] [--keep-remote]
264
277
  gx report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD]
265
- bash scripts/agent-worktree-prune.sh --base dev # manual stale worktree cleanup
278
+ bash scripts/agent-worktree-prune.sh # prune temporary worktrees only (keeps merged agent branches by default)
279
+ bash scripts/agent-worktree-prune.sh --delete-branches --delete-remote-branches # full merged-branch cleanup
280
+ bash scripts/agent-worktree-prune.sh --force-dirty --delete-branches # force-remove dirty merged worktrees too
266
281
  bash scripts/openspec/init-plan-workspace.sh <plan-slug> # optional OpenSpec plan scaffold
267
282
  ```
268
283
 
269
284
  No command defaults to `gx status` (non-mutating health/status view).
270
- `gx status` reports CLI/runtime info, global OMX/OpenSpec service status, and repo safety service state.
285
+ `gx status` reports CLI/runtime info, global OMX/OpenSpec/codex-auth service status, and repo safety service state.
286
+ `gx init` is an alias of `gx setup`.
271
287
  When run in an interactive terminal, default `GuardeX` checks npm for a newer version first
272
288
  and asks `[y/N]` whether to update immediately (default is `N`).
273
289
 
274
- - Interactive setup: prompts for Y/N approval before global OMX/OpenSpec install.
290
+ - Interactive setup: prompts for Y/N approval before global OMX/OpenSpec/codex-auth install.
275
291
  - Interactive prompt is strict (`[y/n]`) and waits for explicit answer.
276
292
  - Non-interactive setup: skips global installs by default; use `--yes-global-install` to force.
293
+ - In already-initialized repos, `setup` / `install` / `fix` block writes on protected `main` by default; start an agent branch first. Use `--allow-protected-base-write` only for emergency in-place maintenance.
294
+ - `gx doctor` on protected `main` auto-starts an isolated `agent/gx/...-gx-doctor` worktree branch and applies repairs there.
295
+ - `gx setup` and `gx doctor` always refresh `.githooks/pre-commit` from templates, so Codex sub-branch enforcement stays repaired.
296
+ - `scripts/codex-agent.sh` now auto-runs finish automation after a Codex session when `origin` exists:
297
+ auto-commit changed files, run PR/merge automation, and keep merged agent branches/worktrees by default.
298
+ It also auto-syncs each sandbox branch against the latest base branch before task execution.
299
+ If conflicts remain, it keeps the sandbox and prompts for a conflict-resolution review pass.
300
+ - use `gx cleanup` (or `gx cleanup --branch <agent/...>`) to remove merged branches/worktrees when done.
277
301
 
278
302
  ## Advanced commands
279
303
 
280
304
  ```sh
281
- gx install [--target <path>] [--force] [--skip-agents] [--skip-package-json] [--no-gitignore] [--dry-run]
282
- gx fix [--target <path>] [--dry-run] [--keep-stale-locks] [--no-gitignore]
305
+ gx install [--target <path>] [--force] [--skip-agents] [--skip-package-json] [--no-gitignore] [--dry-run] [--allow-protected-base-write]
306
+ gx fix [--target <path>] [--dry-run] [--keep-stale-locks] [--no-gitignore] [--allow-protected-base-write]
283
307
  gx scan [--target <path>] [--json]
284
308
  gx report help
285
309
  ```
@@ -350,7 +374,7 @@ multiagent.protectedBranches
350
374
  ## What is protected
351
375
 
352
376
  - direct commits to protected branches (defaults: `dev`, `main`, `master`; configurable via `gx protect ...`)
353
- - protected-branch commits are blocked regardless of commit client (including VS Code Source Control)
377
+ - protected-branch commits are blocked by default for all clients; Codex sessions only may commit protected branches when staged files are strictly `AGENTS.md` and/or `.gitignore`
354
378
  - Codex-session commits on non-`agent/*` branches are blocked by default (`multiagent.codexRequireAgentBranch=true`)
355
379
  - Codex commits attempted on protected branches trigger `guardex-preedit-guard` and require starting work via `scripts/codex-agent.sh`
356
380
  - overlapping file ownership between agents
@@ -10,7 +10,11 @@ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
10
10
  const TOOL_NAME = 'guardex';
11
11
  const SHORT_TOOL_NAME = 'gx';
12
12
  const LEGACY_NAMES = ['musafety', 'multiagent-safety'];
13
- const GLOBAL_TOOLCHAIN_PACKAGES = ['oh-my-codex', '@fission-ai/openspec'];
13
+ const GLOBAL_TOOLCHAIN_PACKAGES = [
14
+ 'oh-my-codex',
15
+ '@fission-ai/openspec',
16
+ '@imdeadpool/codex-account-switcher',
17
+ ];
14
18
  const MAINTAINER_RELEASE_REPO = path.resolve(
15
19
  process.env.MUSAFETY_RELEASE_REPO || '/tmp/multiagent-safety',
16
20
  );
@@ -54,6 +58,8 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
54
58
  '.githooks/pre-commit',
55
59
  'scripts/agent-branch-start.sh',
56
60
  'scripts/agent-branch-finish.sh',
61
+ 'scripts/agent-worktree-prune.sh',
62
+ 'scripts/codex-agent.sh',
57
63
  'scripts/agent-file-locks.py',
58
64
  ]);
59
65
 
@@ -80,20 +86,24 @@ const COMMAND_TYPO_ALIASES = new Map([
80
86
  ['realaese', 'release'],
81
87
  ['relase', 'release'],
82
88
  ['setpu', 'setup'],
89
+ ['inti', 'init'],
83
90
  ['intsall', 'install'],
84
91
  ['docter', 'doctor'],
85
92
  ['doctro', 'doctor'],
93
+ ['cleunup', 'cleanup'],
86
94
  ['scna', 'scan'],
87
95
  ]);
88
96
  const SUGGESTIBLE_COMMANDS = [
89
97
  'status',
90
98
  'setup',
99
+ 'init',
91
100
  'doctor',
92
101
  'report',
93
102
  'copy-prompt',
94
103
  'copy-commands',
95
104
  'protect',
96
105
  'sync',
106
+ 'cleanup',
97
107
  'release',
98
108
  'install',
99
109
  'fix',
@@ -105,12 +115,14 @@ const SUGGESTIBLE_COMMANDS = [
105
115
  const CLI_COMMAND_DESCRIPTIONS = [
106
116
  ['status', 'Show GuardeX CLI + service health without modifying files'],
107
117
  ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore)'],
118
+ ['init', 'Alias of setup (bootstrap + repair guardrails in a git repo)'],
108
119
  ['doctor', 'Repair safety setup drift, then verify repo safety'],
109
120
  ['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'],
110
121
  ['copy-prompt', 'Print the AI-ready setup checklist'],
111
122
  ['copy-commands', 'Print setup checklist as executable commands only'],
112
123
  ['protect', 'Manage protected branches (list/add/remove/set/reset)'],
113
124
  ['sync', 'Check or sync agent branches with origin/<base>'],
125
+ ['cleanup', 'Cleanup merged agent branches/worktrees (local + remote)'],
114
126
  ['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
115
127
  ['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
116
128
  ['scan', 'Report safety issues and exit non-zero on findings'],
@@ -127,10 +139,11 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
127
139
 
128
140
  2) Bootstrap safety in this repo:
129
141
  gx setup
142
+ # alias: gx init
130
143
 
131
- - Setup detects global OMX/OpenSpec first.
144
+ - Setup detects global OMX/OpenSpec/codex-auth first.
132
145
  - If one is missing and setup asks for approval, reply explicitly:
133
- - y = run: npm i -g oh-my-codex @fission-ai/openspec (missing ones only)
146
+ - y = run: npm i -g oh-my-codex @fission-ai/openspec @imdeadpool/codex-account-switcher (missing ones only)
134
147
  - n = skip global installs
135
148
 
136
149
  3) If setup reports warnings/errors, repair + re-check:
@@ -141,6 +154,12 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
141
154
  bash scripts/agent-branch-start.sh "task" "agent-name"
142
155
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
143
156
  bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
157
+ - For every new user message/task, repeat the same cycle:
158
+ start isolated agent branch/worktree -> claim file locks -> implement/verify ->
159
+ finish via PR/merge cleanup with scripts/agent-branch-finish.sh.
160
+ - Finished branches stay available by default for audit/follow-up.
161
+ Remove them explicitly when done:
162
+ gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
144
163
 
145
164
  5) Optional: create OpenSpec planning workspace:
146
165
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
@@ -151,6 +170,9 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
151
170
  7) Optional: sync your current agent branch with latest base branch:
152
171
  gx sync --check
153
172
  gx sync
173
+
174
+ 8) Optional (GitHub remote cleanup): enable:
175
+ Settings -> General -> Pull Requests -> Automatically delete head branches
154
176
  `;
155
177
 
156
178
  const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
@@ -160,6 +182,7 @@ bash scripts/codex-agent.sh "task" "agent-name"
160
182
  bash scripts/agent-branch-start.sh "task" "agent-name"
161
183
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
162
184
  bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
185
+ gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
163
186
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
164
187
  gx protect add release staging
165
188
  gx sync --check
@@ -272,7 +295,12 @@ ${commandCatalogLines().join('\n')}
272
295
  NOTES
273
296
  - Running ${TOOL_NAME} with no command defaults to: ${SHORT_TOOL_NAME} status
274
297
  - Short alias: ${SHORT_TOOL_NAME}
298
+ - ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
275
299
  - ${TOOL_NAME} setup asks for Y/N approval before global installs
300
+ - In initialized repos, setup/install/fix block in-place writes on protected main by default
301
+ - doctor auto-starts a sandbox agent branch/worktree when run on protected main
302
+ - agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
303
+ - use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
276
304
  - Legacy command aliases are still supported: ${LEGACY_NAMES.join(', ')}`);
277
305
 
278
306
  if (outsideGitRepo) {
@@ -346,6 +374,10 @@ function ensureExecutable(destinationPath, relativePath, dryRun) {
346
374
  }
347
375
  }
348
376
 
377
+ function isCriticalGuardrailPath(relativePath) {
378
+ return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
379
+ }
380
+
349
381
  function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
350
382
  const sourcePath = path.join(TEMPLATE_ROOT, relativeTemplatePath);
351
383
  const destinationRelativePath = toDestinationPath(relativeTemplatePath);
@@ -360,7 +392,7 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
360
392
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
361
393
  return { status: 'unchanged', file: destinationRelativePath };
362
394
  }
363
- if (!force) {
395
+ if (!force && !isCriticalGuardrailPath(destinationRelativePath)) {
364
396
  throw new Error(
365
397
  `Refusing to overwrite existing file without --force: ${destinationRelativePath}`,
366
398
  );
@@ -373,6 +405,10 @@ function copyTemplateFile(repoRoot, relativeTemplatePath, force, dryRun) {
373
405
  ensureExecutable(destinationPath, destinationRelativePath, dryRun);
374
406
  }
375
407
 
408
+ if (destinationExists && !force && isCriticalGuardrailPath(destinationRelativePath)) {
409
+ return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
410
+ }
411
+
376
412
  return { status: destinationExists ? 'overwritten' : 'created', file: destinationRelativePath };
377
413
  }
378
414
 
@@ -389,6 +425,14 @@ function ensureTemplateFilePresent(repoRoot, relativeTemplatePath, dryRun) {
389
425
  return { status: 'unchanged', file: destinationRelativePath };
390
426
  }
391
427
 
428
+ if (isCriticalGuardrailPath(destinationRelativePath)) {
429
+ if (!dryRun) {
430
+ fs.writeFileSync(destinationPath, sourceContent, 'utf8');
431
+ ensureExecutable(destinationPath, destinationRelativePath, dryRun);
432
+ }
433
+ return { status: dryRun ? 'would-repair-critical' : 'repaired-critical', file: destinationRelativePath };
434
+ }
435
+
392
436
  // In fix mode, avoid silently replacing local customizations.
393
437
  return { status: 'skipped-conflict', file: destinationRelativePath };
394
438
  }
@@ -473,7 +517,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
473
517
  'agent:codex': 'bash ./scripts/codex-agent.sh',
474
518
  'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
475
519
  'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
476
- 'agent:cleanup': 'bash ./scripts/agent-worktree-prune.sh --base dev',
520
+ 'agent:cleanup': `${SHORT_TOOL_NAME} cleanup`,
477
521
  'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
478
522
  'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
479
523
  'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
@@ -630,6 +674,10 @@ function parseCommonArgs(rawArgs, defaults) {
630
674
  options.skipGitignore = true;
631
675
  continue;
632
676
  }
677
+ if (arg === '--allow-protected-base-write') {
678
+ options.allowProtectedBaseWrite = true;
679
+ continue;
680
+ }
633
681
 
634
682
  throw new Error(`Unknown option: ${arg}`);
635
683
  }
@@ -641,6 +689,149 @@ function parseCommonArgs(rawArgs, defaults) {
641
689
  return options;
642
690
  }
643
691
 
692
+ function hasGuardexBootstrapFiles(repoRoot) {
693
+ const required = [
694
+ 'AGENTS.md',
695
+ 'scripts/agent-branch-start.sh',
696
+ '.githooks/pre-commit',
697
+ LOCK_FILE_RELATIVE,
698
+ ];
699
+ return required.every((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
700
+ }
701
+
702
+ function protectedBaseWriteBlock(options) {
703
+ if (options.dryRun || options.allowProtectedBaseWrite) {
704
+ return null;
705
+ }
706
+
707
+ const repoRoot = resolveRepoRoot(options.target);
708
+ if (!hasGuardexBootstrapFiles(repoRoot)) {
709
+ return null;
710
+ }
711
+
712
+ const branch = currentBranchName(repoRoot);
713
+ if (branch !== 'main') {
714
+ return null;
715
+ }
716
+
717
+ const protectedBranches = readProtectedBranches(repoRoot);
718
+ if (!protectedBranches.includes(branch)) {
719
+ return null;
720
+ }
721
+
722
+ return {
723
+ repoRoot,
724
+ branch,
725
+ };
726
+ }
727
+
728
+ function assertProtectedMainWriteAllowed(options, commandName) {
729
+ const blocked = protectedBaseWriteBlock(options);
730
+ if (!blocked) {
731
+ return;
732
+ }
733
+
734
+ throw new Error(
735
+ `${commandName} blocked on protected branch '${blocked.branch}' in an initialized repo.\n` +
736
+ `Keep local '${blocked.branch}' pull-only: start an agent branch/worktree first:\n` +
737
+ ` bash scripts/agent-branch-start.sh "<task>" "codex"\n` +
738
+ `Override once only when intentional: --allow-protected-base-write`,
739
+ );
740
+ }
741
+
742
+ function extractAgentBranchStartMetadata(output) {
743
+ const branchMatch = String(output || '').match(/^\[agent-branch-start\] Created branch: (.+)$/m);
744
+ const worktreeMatch = String(output || '').match(/^\[agent-branch-start\] Worktree: (.+)$/m);
745
+ return {
746
+ branch: branchMatch ? branchMatch[1].trim() : '',
747
+ worktreePath: worktreeMatch ? worktreeMatch[1].trim() : '',
748
+ };
749
+ }
750
+
751
+ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
752
+ const resolvedTarget = path.resolve(targetPath);
753
+ const relativeTarget = path.relative(repoRoot, resolvedTarget);
754
+ if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
755
+ throw new Error(`doctor target must stay inside repo root when sandboxing: ${resolvedTarget}`);
756
+ }
757
+ if (!relativeTarget || relativeTarget === '.') {
758
+ return worktreePath;
759
+ }
760
+ return path.join(worktreePath, relativeTarget);
761
+ }
762
+
763
+ function buildSandboxDoctorArgs(options, sandboxTarget) {
764
+ const args = ['doctor', '--target', sandboxTarget];
765
+ if (options.dryRun) args.push('--dry-run');
766
+ if (options.skipAgents) args.push('--skip-agents');
767
+ if (options.skipPackageJson) args.push('--skip-package-json');
768
+ if (options.skipGitignore) args.push('--no-gitignore');
769
+ if (!options.dropStaleLocks) args.push('--keep-stale-locks');
770
+ if (options.json) args.push('--json');
771
+ return args;
772
+ }
773
+
774
+ function runDoctorInSandbox(options, blocked) {
775
+ const startScript = path.join(blocked.repoRoot, 'scripts', 'agent-branch-start.sh');
776
+ if (!fs.existsSync(startScript)) {
777
+ throw new Error(
778
+ `doctor sandbox fallback is unavailable because '${startScript}' is missing.\n` +
779
+ `Run '${SHORT_TOOL_NAME} setup --allow-protected-base-write' once to restore branch-start tooling.`,
780
+ );
781
+ }
782
+
783
+ const startResult = run('bash', [
784
+ startScript,
785
+ '--task',
786
+ `${SHORT_TOOL_NAME}-doctor`,
787
+ '--agent',
788
+ SHORT_TOOL_NAME,
789
+ '--base',
790
+ blocked.branch,
791
+ ], { cwd: blocked.repoRoot });
792
+ if (startResult.error) {
793
+ throw startResult.error;
794
+ }
795
+ if (startResult.status !== 0) {
796
+ throw new Error((startResult.stderr || startResult.stdout || 'failed to start doctor sandbox').trim());
797
+ }
798
+
799
+ const metadata = extractAgentBranchStartMetadata(startResult.stdout);
800
+ if (!metadata.worktreePath) {
801
+ throw new Error(`Failed to parse sandbox worktree from agent-branch-start output:\n${startResult.stdout}`);
802
+ }
803
+
804
+ const sandboxTarget = resolveSandboxTarget(blocked.repoRoot, metadata.worktreePath, options.target);
805
+ const nestedResult = run(
806
+ process.execPath,
807
+ [__filename, ...buildSandboxDoctorArgs(options, sandboxTarget)],
808
+ { cwd: metadata.worktreePath },
809
+ );
810
+ if (nestedResult.error) {
811
+ throw nestedResult.error;
812
+ }
813
+
814
+ if (options.json) {
815
+ if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
816
+ if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
817
+ } else {
818
+ console.log(
819
+ `[${TOOL_NAME}] doctor detected protected branch '${blocked.branch}'. ` +
820
+ `Running repairs in sandbox branch '${metadata.branch || 'agent/<auto>'}'.`,
821
+ );
822
+ if (startResult.stdout) process.stdout.write(startResult.stdout);
823
+ if (startResult.stderr) process.stderr.write(startResult.stderr);
824
+ if (nestedResult.stdout) process.stdout.write(nestedResult.stdout);
825
+ if (nestedResult.stderr) process.stderr.write(nestedResult.stderr);
826
+ }
827
+
828
+ if (typeof nestedResult.status === 'number') {
829
+ process.exitCode = nestedResult.status;
830
+ return;
831
+ }
832
+ process.exitCode = 1;
833
+ }
834
+
644
835
  function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
645
836
  const remaining = [];
646
837
  let target = defaultTarget;
@@ -1076,6 +1267,63 @@ function parseSyncArgs(rawArgs) {
1076
1267
  return options;
1077
1268
  }
1078
1269
 
1270
+ function parseCleanupArgs(rawArgs) {
1271
+ const options = {
1272
+ target: process.cwd(),
1273
+ base: '',
1274
+ branch: '',
1275
+ dryRun: false,
1276
+ forceDirty: false,
1277
+ keepRemote: false,
1278
+ };
1279
+
1280
+ for (let index = 0; index < rawArgs.length; index += 1) {
1281
+ const arg = rawArgs[index];
1282
+ if (arg === '--target') {
1283
+ const next = rawArgs[index + 1];
1284
+ if (!next) {
1285
+ throw new Error('--target requires a path value');
1286
+ }
1287
+ options.target = next;
1288
+ index += 1;
1289
+ continue;
1290
+ }
1291
+ if (arg === '--base') {
1292
+ const next = rawArgs[index + 1];
1293
+ if (!next) {
1294
+ throw new Error('--base requires a branch value');
1295
+ }
1296
+ options.base = next;
1297
+ index += 1;
1298
+ continue;
1299
+ }
1300
+ if (arg === '--branch') {
1301
+ const next = rawArgs[index + 1];
1302
+ if (!next) {
1303
+ throw new Error('--branch requires an agent branch value');
1304
+ }
1305
+ options.branch = next;
1306
+ index += 1;
1307
+ continue;
1308
+ }
1309
+ if (arg === '--dry-run') {
1310
+ options.dryRun = true;
1311
+ continue;
1312
+ }
1313
+ if (arg === '--force-dirty') {
1314
+ options.forceDirty = true;
1315
+ continue;
1316
+ }
1317
+ if (arg === '--keep-remote') {
1318
+ options.keepRemote = true;
1319
+ continue;
1320
+ }
1321
+ throw new Error(`Unknown option: ${arg}`);
1322
+ }
1323
+
1324
+ return options;
1325
+ }
1326
+
1079
1327
  function syncOperation(repoRoot, strategy, baseRef, ffOnly) {
1080
1328
  if (strategy === 'rebase') {
1081
1329
  if (ffOnly) {
@@ -1777,8 +2025,10 @@ function install(rawArgs) {
1777
2025
  skipPackageJson: false,
1778
2026
  skipGitignore: false,
1779
2027
  dryRun: false,
2028
+ allowProtectedBaseWrite: false,
1780
2029
  });
1781
2030
 
2031
+ assertProtectedMainWriteAllowed(options, 'install');
1782
2032
  const payload = runInstallInternal(options);
1783
2033
  printOperations('Install target', payload, options.dryRun);
1784
2034
 
@@ -1797,8 +2047,10 @@ function fix(rawArgs) {
1797
2047
  skipPackageJson: false,
1798
2048
  skipGitignore: false,
1799
2049
  dryRun: false,
2050
+ allowProtectedBaseWrite: false,
1800
2051
  });
1801
2052
 
2053
+ assertProtectedMainWriteAllowed(options, 'fix');
1802
2054
  const payload = runFixInternal(options);
1803
2055
  printOperations('Fix target', payload, options.dryRun);
1804
2056
 
@@ -1829,8 +2081,16 @@ function doctor(rawArgs) {
1829
2081
  skipGitignore: false,
1830
2082
  dryRun: false,
1831
2083
  json: false,
2084
+ allowProtectedBaseWrite: false,
1832
2085
  });
1833
2086
 
2087
+ const blocked = protectedBaseWriteBlock(options);
2088
+ if (blocked) {
2089
+ runDoctorInSandbox(options, blocked);
2090
+ return;
2091
+ }
2092
+
2093
+ assertProtectedMainWriteAllowed(options, 'doctor');
1834
2094
  const fixPayload = runFixInternal(options);
1835
2095
  const scanResult = runScanInternal({ target: options.target, json: false });
1836
2096
  const musafe = scanResult.errors === 0 && scanResult.warnings === 0;
@@ -1974,6 +2234,7 @@ function setup(rawArgs) {
1974
2234
  dryRun: false,
1975
2235
  yesGlobalInstall: false,
1976
2236
  noGlobalInstall: false,
2237
+ allowProtectedBaseWrite: false,
1977
2238
  });
1978
2239
 
1979
2240
  const globalInstallStatus = installGlobalToolchain(options);
@@ -1982,7 +2243,7 @@ function setup(rawArgs) {
1982
2243
  `[${TOOL_NAME}] ✅ Global tools installed (${(globalInstallStatus.packages || []).join(', ')}).`,
1983
2244
  );
1984
2245
  } else if (globalInstallStatus.status === 'already-installed') {
1985
- console.log(`[${TOOL_NAME}] ✅ OMX/OpenSpec global tools already installed. Skipping.`);
2246
+ console.log(`[${TOOL_NAME}] ✅ OMX/OpenSpec/codex-auth global tools already installed. Skipping.`);
1986
2247
  } else if (globalInstallStatus.status === 'failed') {
1987
2248
  console.log(
1988
2249
  `[${TOOL_NAME}] ⚠️ Global install failed: ${globalInstallStatus.reason}\n` +
@@ -1996,6 +2257,7 @@ function setup(rawArgs) {
1996
2257
  );
1997
2258
  }
1998
2259
 
2260
+ assertProtectedMainWriteAllowed(options, 'setup');
1999
2261
  const installPayload = runInstallInternal(options);
2000
2262
  printOperations('Setup/install', installPayload, options.dryRun);
2001
2263
 
@@ -2090,6 +2352,39 @@ function copyCommands() {
2090
2352
  process.exitCode = 0;
2091
2353
  }
2092
2354
 
2355
+ function cleanup(rawArgs) {
2356
+ const options = parseCleanupArgs(rawArgs);
2357
+ const repoRoot = resolveRepoRoot(options.target);
2358
+ const pruneScript = path.join(repoRoot, 'scripts', 'agent-worktree-prune.sh');
2359
+ if (!fs.existsSync(pruneScript)) {
2360
+ throw new Error(`Missing cleanup script: ${pruneScript}. Run '${SHORT_TOOL_NAME} setup' first.`);
2361
+ }
2362
+
2363
+ const args = [pruneScript];
2364
+ if (options.base) {
2365
+ args.push('--base', options.base);
2366
+ }
2367
+ if (options.branch) {
2368
+ args.push('--branch', options.branch);
2369
+ }
2370
+ if (options.forceDirty) {
2371
+ args.push('--force-dirty');
2372
+ }
2373
+ if (options.dryRun) {
2374
+ args.push('--dry-run');
2375
+ }
2376
+ args.push('--delete-branches');
2377
+ if (!options.keepRemote) {
2378
+ args.push('--delete-remote-branches');
2379
+ }
2380
+
2381
+ const runResult = run('bash', args, { cwd: repoRoot, stdio: 'inherit' });
2382
+ if (runResult.status !== 0) {
2383
+ throw new Error('Cleanup command failed');
2384
+ }
2385
+ process.exitCode = 0;
2386
+ }
2387
+
2093
2388
  function sync(rawArgs) {
2094
2389
  const options = parseSyncArgs(rawArgs);
2095
2390
  const repoRoot = resolveRepoRoot(options.target);
@@ -2403,7 +2698,7 @@ function main() {
2403
2698
  return;
2404
2699
  }
2405
2700
 
2406
- if (command === 'setup') {
2701
+ if (command === 'setup' || command === 'init') {
2407
2702
  setup(rest);
2408
2703
  return;
2409
2704
  }
@@ -2438,6 +2733,11 @@ function main() {
2438
2733
  return;
2439
2734
  }
2440
2735
 
2736
+ if (command === 'cleanup') {
2737
+ cleanup(rest);
2738
+ return;
2739
+ }
2740
+
2441
2741
  if (command === 'release') {
2442
2742
  release(rest);
2443
2743
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,
@@ -10,7 +10,13 @@
10
10
  - Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
11
11
  - If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
12
12
  - For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
13
- - Agent completion must use `scripts/agent-branch-finish.sh` (direct merge to base when allowed; auto PR fallback for protected bases, then cleanup after merge).
13
+ - Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
14
+ - Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, and pull the local base branch after merge).
15
+ - Auto-finish keeps the sandbox branch/worktree by default so conflict follow-ups and audits stay reproducible.
16
+ - Use explicit cleanup when done: `gx cleanup --branch "<agent-branch>"` (or `gx cleanup` for all merged agent branches).
17
+ - If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr` and keep the branch open until checks/review pass.
18
+ - If merge/rebase conflicts block auto-finish, run a conflict-resolution review pass in that sandbox branch, then rerun `agent-branch-finish.sh --via-pr` until merged.
19
+ - Per-message loop is mandatory: for every new user message/task, start a fresh agent branch/worktree, claim ownership locks, implement and verify, finish via PR/merge cleanup, then repeat for the next message/task.
14
20
 
15
21
  1. Explicit ownership before edits
16
22
 
@@ -19,6 +19,8 @@ If guardrails are missing entirely, run:
19
19
 
20
20
  ```sh
21
21
  gx setup
22
+ # alias
23
+ gx init
22
24
  ```
23
25
 
24
26
  Then verify:
@@ -32,5 +34,8 @@ gx scan
32
34
 
33
35
  - Prefer `gx doctor` for one-step repair + verification.
34
36
  - Keep agent work isolated (`agent/*` branches + lock claims).
37
+ - For every new user message/task, restart the full loop on a fresh agent branch/worktree.
35
38
  - For one-command Codex sandbox startup, use `bash scripts/codex-agent.sh "<task>" "<agent-name>"`.
39
+ - `scripts/codex-agent.sh` auto-syncs the sandbox branch against base before each task and auto-finishes merge/PR flow after Codex exits.
40
+ - Auto-finish keeps the branch/worktree by default; remove merged branches explicitly with `gx cleanup` (or `gx cleanup --branch "<agent-branch>"`).
36
41
  - Do not bypass protected branch safeguards unless explicitly required.