@imdeadpool/guardex 7.0.5 → 7.0.7

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
@@ -9,6 +9,11 @@ GuardeX is a safety layer for parallel Codex/agent work in git repos.
9
9
  > [!WARNING]
10
10
  > Not affiliated with OpenAI or Codex. Not an official tool.
11
11
 
12
+ ## Frontend Repo
13
+
14
+ - Standalone frontend repository: https://github.com/Webu-PRO/guardex-frontend
15
+ - This repository tracks/mirrors the frontend under `frontend/` as documented below.
16
+
12
17
  ## The problem (what was going wrong)
13
18
 
14
19
  Multiple Codex agents worked on the same files at the same time.
@@ -21,11 +26,16 @@ GuardeX exists to stop that loop.
21
26
 
22
27
  ```mermaid
23
28
  flowchart LR
24
- A[Agent A edits file X] --> C[Conflict / overwrite]
25
- B[Agent B edits file X] --> C
26
- C --> D[Deleted or lost code]
27
- D --> E[Rework and confusion]
28
- E --> C
29
+ A[Agent A edits shared files] --> S[Same target surface]
30
+ B[Agent B edits shared files] --> S
31
+ C[Agent C edits shared files] --> S
32
+ D[Agent D edits shared files] --> S
33
+ E[Agent E edits shared files] --> S
34
+ S --> F[Conflict / overwrite churn]
35
+ F --> G[Deleted or lost code]
36
+ G --> H[Rework and confusion]
37
+ H --> I[Regression risk grows]
38
+ I --> F
29
39
  ```
30
40
 
31
41
  ## What GuardeX enforces
@@ -95,9 +105,9 @@ gx finish --all
95
105
 
96
106
  ![gx lock and delete guard screenshot](https://raw.githubusercontent.com/recodeee/guardex/main/docs/images/workflow-lock-guard.svg)
97
107
 
98
- ### Real VS Code Source Control layout (exact screenshot)
108
+ ### VS Code Source Control layout (agent + OpenSpec files)
99
109
 
100
- ![Real VS Code Source Control layout](https://raw.githubusercontent.com/recodeee/guardex/main/docs/images/workflow-vscode-source-control-exact.png)
110
+ ![VS Code Source Control layout with OpenSpec files](https://raw.githubusercontent.com/recodeee/guardex/main/docs/images/workflow-source-control.svg)
101
111
 
102
112
  ## Copy-paste: common commands
103
113
 
@@ -213,6 +223,7 @@ gx agents stop
213
223
  - Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
214
224
  - On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
215
225
  - In-place agent branching is disabled; `scripts/agent-branch-start.sh` always creates a separate worktree to keep your visible local/base branch unchanged.
226
+ - Fresh sandbox branches intentionally start without any git upstream; guardex records the protected base in `branch.<name>.guardexBase`, and the first `git push -u` publishes the real upstream branch.
216
227
  - `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
217
228
 
218
229
  ## Configure protected branches
@@ -282,6 +293,34 @@ Then in your repo:
282
293
 
283
294
  After that, the app reviews new and updated pull requests automatically.
284
295
 
296
+ ## Frontend mirror sync (`Webu-PRO/guardex-frontend`)
297
+
298
+ This repo includes `.github/workflows/sync-frontend-mirror.yml`, which mirrors
299
+ the `frontend/` subtree to a separate repository whenever `main` receives
300
+ changes under `frontend/**`.
301
+
302
+ Default target:
303
+
304
+ - repo: `Webu-PRO/guardex-frontend`
305
+ - branch: `main`
306
+
307
+ Required setup (in this repository):
308
+
309
+ 1. `Settings -> Secrets and variables -> Actions`
310
+ 2. Add repository secret `GUARDEX_FRONTEND_MIRROR_PAT`
311
+ - value must be a token with `contents:write` access to `Webu-PRO/guardex-frontend`
312
+
313
+ Optional overrides (Actions Variables):
314
+
315
+ - `GUARDEX_FRONTEND_MIRROR_REPO` (default `Webu-PRO/guardex-frontend`)
316
+ - `GUARDEX_FRONTEND_MIRROR_BRANCH` (default `main`)
317
+
318
+ Manual run:
319
+
320
+ ```sh
321
+ gh workflow run sync-frontend-mirror.yml
322
+ ```
323
+
285
324
  ## Companion dependency: `codex-auth` account switcher
286
325
 
287
326
  For multi-identity Codex workflows, GuardeX pairs with
@@ -373,6 +412,15 @@ npm pack --dry-run
373
412
 
374
413
  ## Release notes
375
414
 
415
+ ### v7.0.7
416
+
417
+ - **Fixed: next publish target now advances past npm.** Bumped `@imdeadpool/guardex` from `7.0.6` to `7.0.7` so the next `npm publish` does not collide with the already-published registry version.
418
+ - **Fixed: root package metadata drift in `package-lock.json`.** The lockfile root version had fallen behind the package manifest (`7.0.4` vs. `7.0.6`), which made release metadata inconsistent. The bump resynchronized `package.json` and `package-lock.json` on `7.0.7`.
419
+
420
+ ### v7.0.6
421
+
422
+ - **Fixed: self-updater lied about success.** `gx`'s update prompt runs `npm i -g @imdeadpool/guardex@latest` and previously trusted npm's exit code. When npm's resolution cache made it report "changed 1 package" without actually overwriting the files (a known quirk triggered when the user just bumped from N-1 → N in the same session, or with a warm metadata cache), the prompt kept re-firing on every subsequent `gx` invocation because the on-disk `package.json` was still stale. `gx` now re-reads the globally installed `package.json` after the `@latest` install returns, compares its `version` field to the advertised latest, and if they don't match runs a pinned retry `npm i -g @imdeadpool/guardex@<latest>` to force the cache past the obstructing entry. If the pinned retry also fails to advance the on-disk version, the user gets a clear hint (`npm root -g && npm cache verify`) instead of a silent loop.
423
+
376
424
  ### v7.0.5
377
425
 
378
426
  - **Added: `oh-my-claude` to `gx status` global-toolchain check.** The Claude-side mirror of `oh-my-codex` is now reported alongside the existing services (`oh-my-codex`, `@fission-ai/openspec`, `@imdeadpool/codex-account-switcher`, `gh`). Users who have not yet installed it will see a clear "inactive" line instead of silent omission, matching the existing codex detection contract.
@@ -35,6 +35,7 @@ const SCORECARD_BIN = process.env.GUARDEX_SCORECARD_BIN || 'scorecard';
35
35
  const GIT_PROTECTED_BRANCHES_KEY = 'multiagent.protectedBranches';
36
36
  const GIT_BASE_BRANCH_KEY = 'multiagent.baseBranch';
37
37
  const GIT_SYNC_STRATEGY_KEY = 'multiagent.sync.strategy';
38
+ const GUARDEX_REPO_TOGGLE_ENV = 'GUARDEX_ON';
38
39
  const DEFAULT_PROTECTED_BRANCHES = ['dev', 'main', 'master'];
39
40
  const DEFAULT_BASE_BRANCH = 'dev';
40
41
  const DEFAULT_SYNC_STRATEGY = 'rebase';
@@ -49,6 +50,7 @@ const TEMPLATE_FILES = [
49
50
  'scripts/review-bot-watch.sh',
50
51
  'scripts/agent-worktree-prune.sh',
51
52
  'scripts/agent-file-locks.py',
53
+ 'scripts/guardex-env.sh',
52
54
  'scripts/install-agent-git-hooks.sh',
53
55
  'scripts/openspec/init-plan-workspace.sh',
54
56
  'scripts/openspec/init-change-workspace.sh',
@@ -68,6 +70,7 @@ const REQUIRED_WORKFLOW_FILES = [
68
70
  'scripts/agent-branch-finish.sh',
69
71
  'scripts/agent-worktree-prune.sh',
70
72
  'scripts/agent-file-locks.py',
73
+ 'scripts/guardex-env.sh',
71
74
  'scripts/install-agent-git-hooks.sh',
72
75
  '.githooks/pre-commit',
73
76
  '.githooks/post-merge',
@@ -113,6 +116,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
113
116
  'scripts/agent-worktree-prune.sh',
114
117
  'scripts/codex-agent.sh',
115
118
  'scripts/agent-file-locks.py',
119
+ 'scripts/guardex-env.sh',
116
120
  ]);
117
121
 
118
122
  const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
@@ -130,6 +134,7 @@ const MANAGED_GITIGNORE_PATHS = [
130
134
  'scripts/review-bot-watch.sh',
131
135
  'scripts/agent-worktree-prune.sh',
132
136
  'scripts/agent-file-locks.py',
137
+ 'scripts/guardex-env.sh',
133
138
  'scripts/install-agent-git-hooks.sh',
134
139
  'scripts/openspec/init-plan-workspace.sh',
135
140
  'scripts/openspec/init-change-workspace.sh',
@@ -289,6 +294,9 @@ function statusDot(status) {
289
294
  if (status === 'inactive') {
290
295
  return colorize('●', '31'); // red
291
296
  }
297
+ if (status === 'disabled') {
298
+ return colorize('●', '36'); // cyan
299
+ }
292
300
  return colorize('●', '33'); // yellow for degraded/unknown
293
301
  }
294
302
 
@@ -754,7 +762,6 @@ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
754
762
  }
755
763
 
756
764
  function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
757
- const force = Boolean(options.force);
758
765
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
759
766
  const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
760
767
  const managedRegex = new RegExp(
@@ -771,9 +778,6 @@ function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
771
778
 
772
779
  const existing = fs.readFileSync(agentsPath, 'utf8');
773
780
  if (managedRegex.test(existing)) {
774
- if (!force) {
775
- return { status: 'unchanged', file: 'AGENTS.md', note: 'preserved existing guardex-managed block' };
776
- }
777
781
  const next = existing.replace(managedRegex, snippet);
778
782
  if (next === existing) {
779
783
  return { status: 'unchanged', file: 'AGENTS.md' };
@@ -3155,6 +3159,75 @@ function parseAutoApproval(name) {
3155
3159
  return null;
3156
3160
  }
3157
3161
 
3162
+ function parseBooleanLike(raw) {
3163
+ if (raw == null) return null;
3164
+ const normalized = String(raw).trim().toLowerCase();
3165
+ if (!normalized) return null;
3166
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
3167
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
3168
+ return null;
3169
+ }
3170
+
3171
+ function parseDotenvAssignmentValue(raw) {
3172
+ let value = String(raw || '').trim();
3173
+ if (!value) return '';
3174
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
3175
+ return value.slice(1, -1).trim();
3176
+ }
3177
+ value = value.replace(/\s+#.*$/, '').trim();
3178
+ return value;
3179
+ }
3180
+
3181
+ function readRepoDotenvValue(repoRoot, name) {
3182
+ const envPath = path.join(repoRoot, '.env');
3183
+ if (!fs.existsSync(envPath)) return null;
3184
+ const pattern = new RegExp(`^\\s*(?:export\\s+)?${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*(.*)$`);
3185
+ const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
3186
+ for (const line of lines) {
3187
+ const trimmed = line.trim();
3188
+ if (!trimmed || trimmed.startsWith('#')) continue;
3189
+ const match = line.match(pattern);
3190
+ if (!match) continue;
3191
+ return parseDotenvAssignmentValue(match[1]);
3192
+ }
3193
+ return null;
3194
+ }
3195
+
3196
+ function resolveGuardexRepoToggle(repoRoot, env = process.env) {
3197
+ const envRaw = env[GUARDEX_REPO_TOGGLE_ENV];
3198
+ const envEnabled = parseBooleanLike(envRaw);
3199
+ if (envEnabled !== null) {
3200
+ return {
3201
+ enabled: envEnabled,
3202
+ source: 'process environment',
3203
+ raw: String(envRaw).trim(),
3204
+ };
3205
+ }
3206
+
3207
+ const dotenvRaw = readRepoDotenvValue(repoRoot, GUARDEX_REPO_TOGGLE_ENV);
3208
+ const dotenvEnabled = parseBooleanLike(dotenvRaw);
3209
+ if (dotenvEnabled !== null) {
3210
+ return {
3211
+ enabled: dotenvEnabled,
3212
+ source: 'repo .env',
3213
+ raw: String(dotenvRaw).trim(),
3214
+ };
3215
+ }
3216
+
3217
+ return {
3218
+ enabled: true,
3219
+ source: 'default',
3220
+ raw: '',
3221
+ };
3222
+ }
3223
+
3224
+ function describeGuardexRepoToggle(toggle) {
3225
+ if (!toggle || toggle.source === 'default') {
3226
+ return 'default enabled mode';
3227
+ }
3228
+ return `${toggle.source} (${GUARDEX_REPO_TOGGLE_ENV}=${toggle.raw})`;
3229
+ }
3230
+
3158
3231
  function parseVersionString(version) {
3159
3232
  const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
3160
3233
  if (!match) return null;
@@ -3265,9 +3338,71 @@ function maybeSelfUpdateBeforeStatus() {
3265
3338
  return;
3266
3339
  }
3267
3340
 
3341
+ // Verify the install actually advanced the on-disk version. npm sometimes
3342
+ // reports "changed 1 package" with status 0 while leaving the old files
3343
+ // in place (version resolution cache / dedupe quirks). If the installed
3344
+ // version doesn't match check.latest, retry with the pinned version so
3345
+ // npm bypasses whatever heuristic made it skip the upgrade.
3346
+ const postInstallVersion = readInstalledGuardexVersion();
3347
+ if (postInstallVersion != null && postInstallVersion !== check.latest) {
3348
+ console.log(
3349
+ `[${TOOL_NAME}] Installed version is still ${postInstallVersion} (expected ${check.latest}). ` +
3350
+ `Retrying with pinned version ${check.latest}…`,
3351
+ );
3352
+ const pinnedResult = run(
3353
+ NPM_BIN,
3354
+ ['i', '-g', `${packageJson.name}@${check.latest}`],
3355
+ { stdio: 'inherit' },
3356
+ );
3357
+ if (pinnedResult.status !== 0) {
3358
+ console.log(
3359
+ `[${TOOL_NAME}] ⚠️ Pinned retry failed. Run manually: ${NPM_BIN} i -g ${packageJson.name}@${check.latest}`,
3360
+ );
3361
+ return;
3362
+ }
3363
+ const pinnedVersion = readInstalledGuardexVersion();
3364
+ if (pinnedVersion != null && pinnedVersion !== check.latest) {
3365
+ console.log(
3366
+ `[${TOOL_NAME}] ⚠️ On-disk version still ${pinnedVersion} after pinned retry. ` +
3367
+ `Investigate: ${NPM_BIN} root -g && ${NPM_BIN} cache verify`,
3368
+ );
3369
+ return;
3370
+ }
3371
+ }
3372
+
3268
3373
  console.log(`[${TOOL_NAME}] ✅ Updated to latest published version.`);
3269
3374
  }
3270
3375
 
3376
+ function readInstalledGuardexVersion() {
3377
+ // Resolves the globally-installed package's on-disk version so we can
3378
+ // verify npm actually wrote new bytes. Uses `npm root -g` to locate the
3379
+ // global install root so we don't accidentally read the running source
3380
+ // tree (which is the file the CLI was spawned from — that IS the global
3381
+ // copy in the normal case, but a bump should be visible via a fresh read
3382
+ // either way). Returns null if we can't determine it.
3383
+ try {
3384
+ const rootResult = run(NPM_BIN, ['root', '-g'], { timeout: 5000 });
3385
+ if (rootResult.status !== 0) {
3386
+ return null;
3387
+ }
3388
+ const globalRoot = String(rootResult.stdout || '').trim();
3389
+ if (!globalRoot) {
3390
+ return null;
3391
+ }
3392
+ const installedPkgPath = path.join(globalRoot, packageJson.name, 'package.json');
3393
+ if (!fs.existsSync(installedPkgPath)) {
3394
+ return null;
3395
+ }
3396
+ const parsed = JSON.parse(fs.readFileSync(installedPkgPath, 'utf8'));
3397
+ if (parsed && typeof parsed.version === 'string') {
3398
+ return parsed.version;
3399
+ }
3400
+ } catch (error) {
3401
+ return null;
3402
+ }
3403
+ return null;
3404
+ }
3405
+
3271
3406
  function checkForOpenSpecPackageUpdate() {
3272
3407
  if (envFlagEnabled('GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK')) {
3273
3408
  return { checked: false, reason: 'disabled' };
@@ -3543,6 +3678,22 @@ function findStaleLockPaths(repoRoot, locks) {
3543
3678
 
3544
3679
  function runInstallInternal(options) {
3545
3680
  const repoRoot = resolveRepoRoot(options.target);
3681
+ const guardexToggle = resolveGuardexRepoToggle(repoRoot);
3682
+ if (!guardexToggle.enabled) {
3683
+ return {
3684
+ repoRoot,
3685
+ operations: [
3686
+ {
3687
+ status: 'skipped',
3688
+ file: '.env',
3689
+ note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
3690
+ },
3691
+ ],
3692
+ hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
3693
+ guardexEnabled: false,
3694
+ guardexToggle,
3695
+ };
3696
+ }
3546
3697
  const operations = [];
3547
3698
 
3548
3699
  operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
@@ -3566,11 +3717,27 @@ function runInstallInternal(options) {
3566
3717
 
3567
3718
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
3568
3719
 
3569
- return { repoRoot, operations, hookResult };
3720
+ return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
3570
3721
  }
3571
3722
 
3572
3723
  function runFixInternal(options) {
3573
3724
  const repoRoot = resolveRepoRoot(options.target);
3725
+ const guardexToggle = resolveGuardexRepoToggle(repoRoot);
3726
+ if (!guardexToggle.enabled) {
3727
+ return {
3728
+ repoRoot,
3729
+ operations: [
3730
+ {
3731
+ status: 'skipped',
3732
+ file: '.env',
3733
+ note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
3734
+ },
3735
+ ],
3736
+ hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
3737
+ guardexEnabled: false,
3738
+ guardexToggle,
3739
+ };
3740
+ }
3574
3741
  const operations = [];
3575
3742
 
3576
3743
  operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
@@ -3620,11 +3787,25 @@ function runFixInternal(options) {
3620
3787
 
3621
3788
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
3622
3789
 
3623
- return { repoRoot, operations, hookResult };
3790
+ return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
3624
3791
  }
3625
3792
 
3626
3793
  function runScanInternal(options) {
3627
3794
  const repoRoot = resolveRepoRoot(options.target);
3795
+ const guardexToggle = resolveGuardexRepoToggle(repoRoot);
3796
+ const currentBranchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
3797
+ const branch = currentBranchResult.status === 0 ? currentBranchResult.stdout.trim() : '(unknown)';
3798
+ if (!guardexToggle.enabled) {
3799
+ return {
3800
+ repoRoot,
3801
+ branch,
3802
+ findings: [],
3803
+ errors: 0,
3804
+ warnings: 0,
3805
+ guardexEnabled: false,
3806
+ guardexToggle,
3807
+ };
3808
+ }
3628
3809
  const findings = [];
3629
3810
 
3630
3811
  const requiredPaths = [
@@ -3715,15 +3896,14 @@ function runScanInternal(options) {
3715
3896
  const errors = findings.filter((item) => item.level === 'error');
3716
3897
  const warnings = findings.filter((item) => item.level === 'warn');
3717
3898
 
3718
- const currentBranchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
3719
- const branch = currentBranchResult.status === 0 ? currentBranchResult.stdout.trim() : '(unknown)';
3720
-
3721
3899
  return {
3722
3900
  repoRoot,
3723
3901
  branch,
3724
3902
  findings,
3725
3903
  errors: errors.length,
3726
3904
  warnings: warnings.length,
3905
+ guardexEnabled: true,
3906
+ guardexToggle,
3727
3907
  };
3728
3908
  }
3729
3909
 
@@ -3749,6 +3929,8 @@ function printScanResult(scan, json = false) {
3749
3929
  {
3750
3930
  repoRoot: scan.repoRoot,
3751
3931
  branch: scan.branch,
3932
+ guardexEnabled: scan.guardexEnabled !== false,
3933
+ guardexToggle: scan.guardexToggle || null,
3752
3934
  errors: scan.errors,
3753
3935
  warnings: scan.warnings,
3754
3936
  findings: scan.findings,
@@ -3763,6 +3945,13 @@ function printScanResult(scan, json = false) {
3763
3945
  console.log(`[${TOOL_NAME}] Scan target: ${scan.repoRoot}`);
3764
3946
  console.log(`[${TOOL_NAME}] Branch: ${scan.branch}`);
3765
3947
 
3948
+ if (scan.guardexEnabled === false) {
3949
+ console.log(
3950
+ `[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
3951
+ );
3952
+ return;
3953
+ }
3954
+
3766
3955
  if (scan.findings.length === 0) {
3767
3956
  console.log(`[${TOOL_NAME}] ✅ No safety issues detected.`);
3768
3957
  return;
@@ -3776,6 +3965,10 @@ function printScanResult(scan, json = false) {
3776
3965
  }
3777
3966
 
3778
3967
  function setExitCodeFromScan(scan) {
3968
+ if (scan.guardexEnabled === false) {
3969
+ process.exitCode = 0;
3970
+ return;
3971
+ }
3779
3972
  if (scan.errors > 0) {
3780
3973
  process.exitCode = 2;
3781
3974
  return;
@@ -3817,7 +4010,9 @@ function status(rawArgs) {
3817
4010
  const inGitRepo = isGitRepo(targetPath);
3818
4011
  const scanResult = inGitRepo ? runScanInternal({ target: targetPath, json: false }) : null;
3819
4012
  const repoServiceStatus = scanResult
3820
- ? (scanResult.errors === 0 && scanResult.warnings === 0 ? 'active' : 'degraded')
4013
+ ? (scanResult.guardexEnabled === false
4014
+ ? 'disabled'
4015
+ : (scanResult.errors === 0 && scanResult.warnings === 0 ? 'active' : 'degraded'))
3821
4016
  : 'inactive';
3822
4017
 
3823
4018
  const payload = {
@@ -3831,6 +4026,8 @@ function status(rawArgs) {
3831
4026
  target: targetPath,
3832
4027
  inGitRepo,
3833
4028
  serviceStatus: repoServiceStatus,
4029
+ guardexEnabled: scanResult ? scanResult.guardexEnabled !== false : null,
4030
+ guardexToggle: scanResult ? scanResult.guardexToggle || null : null,
3834
4031
  scan: scanResult
3835
4032
  ? {
3836
4033
  repoRoot: scanResult.repoRoot,
@@ -3880,6 +4077,17 @@ function status(rawArgs) {
3880
4077
  return;
3881
4078
  }
3882
4079
 
4080
+ if (scanResult.guardexEnabled === false) {
4081
+ console.log(
4082
+ `[${TOOL_NAME}] Repo safety service: ${statusDot('disabled')} disabled (${describeGuardexRepoToggle(scanResult.guardexToggle)}).`,
4083
+ );
4084
+ console.log(`[${TOOL_NAME}] Repo: ${scanResult.repoRoot}`);
4085
+ console.log(`[${TOOL_NAME}] Branch: ${scanResult.branch}`);
4086
+ printToolLogsSummary();
4087
+ process.exitCode = 0;
4088
+ return;
4089
+ }
4090
+
3883
4091
  if (scanResult.errors === 0 && scanResult.warnings === 0) {
3884
4092
  console.log(`[${TOOL_NAME}] Repo safety service: ${statusDot('active')} active.`);
3885
4093
  } else if (scanResult.errors === 0) {
@@ -3921,6 +4129,13 @@ function install(rawArgs) {
3921
4129
  printOperations('Install target', payload, options.dryRun);
3922
4130
 
3923
4131
  if (!options.dryRun) {
4132
+ if (payload.guardexEnabled === false) {
4133
+ console.log(
4134
+ `[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(payload.guardexToggle)}). Skipping repo bootstrap.`,
4135
+ );
4136
+ process.exitCode = 0;
4137
+ return;
4138
+ }
3924
4139
  if (!options.skipAgents) {
3925
4140
  console.log(`[${TOOL_NAME}] AGENTS.md managed policy block is configured by install.`);
3926
4141
  }
@@ -3946,6 +4161,13 @@ function fix(rawArgs) {
3946
4161
  printOperations('Fix target', payload, options.dryRun);
3947
4162
 
3948
4163
  if (!options.dryRun) {
4164
+ if (payload.guardexEnabled === false) {
4165
+ console.log(
4166
+ `[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(payload.guardexToggle)}). Skipping repo repair.`,
4167
+ );
4168
+ process.exitCode = 0;
4169
+ return;
4170
+ }
3949
4171
  console.log(`[${TOOL_NAME}] Repair complete. Next step: ${TOOL_NAME} scan`);
3950
4172
  }
3951
4173
 
@@ -3985,11 +4207,20 @@ function doctor(rawArgs) {
3985
4207
  const fixPayload = runFixInternal(options);
3986
4208
  const scanResult = runScanInternal({ target: options.target, json: false });
3987
4209
  const currentBaseBranch = currentBranchName(scanResult.repoRoot);
3988
- const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
3989
- baseBranch: currentBaseBranch,
3990
- dryRun: options.dryRun,
3991
- });
3992
- const safe = scanResult.errors === 0 && scanResult.warnings === 0;
4210
+ const autoFinishSummary = scanResult.guardexEnabled === false
4211
+ ? {
4212
+ enabled: false,
4213
+ attempted: 0,
4214
+ completed: 0,
4215
+ skipped: 0,
4216
+ failed: 0,
4217
+ details: [],
4218
+ }
4219
+ : autoFinishReadyAgentBranches(scanResult.repoRoot, {
4220
+ baseBranch: currentBaseBranch,
4221
+ dryRun: options.dryRun,
4222
+ });
4223
+ const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
3993
4224
  const musafe = safe;
3994
4225
 
3995
4226
  if (options.json) {
@@ -4006,6 +4237,8 @@ function doctor(rawArgs) {
4006
4237
  dryRun: Boolean(options.dryRun),
4007
4238
  },
4008
4239
  scan: {
4240
+ guardexEnabled: scanResult.guardexEnabled !== false,
4241
+ guardexToggle: scanResult.guardexToggle || null,
4009
4242
  errors: scanResult.errors,
4010
4243
  warnings: scanResult.warnings,
4011
4244
  findings: scanResult.findings,
@@ -4022,6 +4255,11 @@ function doctor(rawArgs) {
4022
4255
 
4023
4256
  printOperations('Doctor/fix', fixPayload, options.dryRun);
4024
4257
  printScanResult(scanResult, false);
4258
+ if (scanResult.guardexEnabled === false) {
4259
+ console.log(`[${TOOL_NAME}] Repo-local Guardex enforcement is intentionally disabled.`);
4260
+ setExitCodeFromScan(scanResult);
4261
+ return;
4262
+ }
4025
4263
  if (autoFinishSummary.enabled) {
4026
4264
  console.log(
4027
4265
  `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
@@ -4763,6 +5001,7 @@ function initWorkspace(rawArgs) {
4763
5001
  function doctorAudit(rawArgs) {
4764
5002
  const options = parseDoctorArgs(rawArgs);
4765
5003
  const repoRoot = resolveRepoRoot(options.target);
5004
+ const guardexToggle = resolveGuardexRepoToggle(repoRoot);
4766
5005
  const failures = [];
4767
5006
  const warnings = [];
4768
5007
 
@@ -4779,6 +5018,13 @@ function doctorAudit(rawArgs) {
4779
5018
  }
4780
5019
 
4781
5020
  console.log(`[multiagent-safety] doctor target: ${repoRoot}`);
5021
+ if (!guardexToggle.enabled) {
5022
+ console.log(
5023
+ `[multiagent-safety] Guardex is disabled for this repo (${describeGuardexRepoToggle(guardexToggle)}).`,
5024
+ );
5025
+ console.log('[multiagent-safety] doctor passed.');
5026
+ return;
5027
+ }
4782
5028
 
4783
5029
  const hooksPath = run('git', ['-C', repoRoot, 'config', '--get', 'core.hooksPath']);
4784
5030
  if (hooksPath.status !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.5",
3
+ "version": "7.0.7",
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,
@@ -58,7 +58,7 @@
58
58
  "bugs": {
59
59
  "url": "https://github.com/recodeee/guardex/issues"
60
60
  },
61
- "homepage": "https://github.com/recodeee/guardex#readme",
61
+ "homepage": "https://guardextutorial.com",
62
62
  "funding": "https://github.com/sponsors/recodeecom",
63
63
  "publishConfig": {
64
64
  "access": "public"
@@ -1,6 +1,8 @@
1
1
  <!-- multiagent-safety:START -->
2
2
  ## Multi-Agent Safety Contract
3
3
 
4
+ **Repo toggle.** Guardex is enabled by default. If the repo root `.env` sets `GUARDEX_ON=0`, `false`, `no`, or `off`, treat this entire Guardex contract as disabled for the repo and do not require Guardex worktrees, lock claims, completion flow, or OpenSpec workflow until `GUARDEX_ON` is re-enabled.
5
+
4
6
  **Isolation.** Every task runs on a dedicated `agent/*` branch + worktree. Start with `scripts/agent-branch-start.sh "<task>" "<agent-name>"`. Treat the base branch (`main`/`dev`) as read-only while an agent branch is active. Never `git checkout <branch>` on a primary working tree (including nested repos); use `git worktree add` instead. The `.githooks/post-checkout` hook auto-reverts primary-branch switches during agent sessions — bypass only with `GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH=1`.
5
7
 
6
8
  **Ownership.** Before editing, claim files: `scripts/agent-file-locks.py claim --branch "<agent-branch>" <file...>`. Before deleting, confirm the path is in your claim. Don't edit outside your scope unless reassigned.
@@ -13,7 +15,7 @@
13
15
 
14
16
  **Reporting.** Every completion handoff includes: files changed, behavior touched, verification commands + results, risks/follow-ups.
15
17
 
16
- **OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
18
+ **OpenSpec (when change-driven).** Keep `openspec/changes/<slug>/tasks.md` checkboxes current during work, not batched at the end. Task scaffolds and manual task edits must include an explicit final completion/cleanup section that ends with PR merge + sandbox cleanup (`gx finish --via-pr --wait-for-merge --cleanup` or `scripts/agent-branch-finish.sh ... --cleanup`) and records PR URL + final `MERGED` evidence. Verify specs with `openspec validate --specs` before archive. Don't archive unverified.
17
19
 
18
20
  **Version bumps.** If a change bumps a published version, the same PR updates release notes/changelog.
19
21
  <!-- multiagent-safety:END -->
@@ -9,6 +9,19 @@ if [[ "${GUARDEX_ALLOW_PRIMARY_BRANCH_SWITCH:-0}" == "1" ]]; then
9
9
  exit 0
10
10
  fi
11
11
 
12
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
13
+ if [[ -z "$repo_root" ]]; then
14
+ exit 0
15
+ fi
16
+ guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
17
+ if [[ -f "$guardex_env_helper" ]]; then
18
+ # shellcheck source=/dev/null
19
+ source "$guardex_env_helper"
20
+ fi
21
+ if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
22
+ exit 0
23
+ fi
24
+
12
25
  # Skip in secondary worktrees — only the primary checkout is guarded.
13
26
  git_dir_abs="$(cd "$(git rev-parse --git-dir)" && pwd -P)"
14
27
  common_dir_abs="$(cd "$(git rev-parse --git-common-dir)" && pwd -P)"
@@ -9,6 +9,14 @@ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
9
9
  if [[ -z "$repo_root" ]]; then
10
10
  exit 0
11
11
  fi
12
+ guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
13
+ if [[ -f "$guardex_env_helper" ]]; then
14
+ # shellcheck source=/dev/null
15
+ source "$guardex_env_helper"
16
+ fi
17
+ if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
18
+ exit 0
19
+ fi
12
20
 
13
21
  branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
14
22
  if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
@@ -9,6 +9,19 @@ if [[ -z "$branch" ]]; then
9
9
  exit 0
10
10
  fi
11
11
 
12
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
13
+ if [[ -z "$repo_root" ]]; then
14
+ exit 0
15
+ fi
16
+ guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
17
+ if [[ -f "$guardex_env_helper" ]]; then
18
+ # shellcheck source=/dev/null
19
+ source "$guardex_env_helper"
20
+ fi
21
+ if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
22
+ exit 0
23
+ fi
24
+
12
25
  if [[ "${ALLOW_COMMIT_ON_PROTECTED_BRANCH:-0}" == "1" ]]; then
13
26
  exit 0
14
27
  fi
@@ -5,6 +5,19 @@ if [[ "${ALLOW_PUSH_ON_PROTECTED_BRANCH:-0}" == "1" || "${ALLOW_COMMIT_ON_PROTEC
5
5
  exit 0
6
6
  fi
7
7
 
8
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
9
+ if [[ -z "$repo_root" ]]; then
10
+ exit 0
11
+ fi
12
+ guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
13
+ if [[ -f "$guardex_env_helper" ]]; then
14
+ # shellcheck source=/dev/null
15
+ source "$guardex_env_helper"
16
+ fi
17
+ if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
18
+ exit 0
19
+ fi
20
+
8
21
  is_vscode_git_context=0
9
22
  if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
10
23
  is_vscode_git_context=1
@@ -389,6 +389,23 @@ fi
389
389
 
390
390
  repo_root="$(git rev-parse --show-toplevel)"
391
391
 
392
+ guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
393
+ if [[ -f "$guardex_env_helper" ]]; then
394
+ # shellcheck source=/dev/null
395
+ source "$guardex_env_helper"
396
+ fi
397
+ if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
398
+ toggle_source="$(guardex_repo_toggle_source "$repo_root" || true)"
399
+ toggle_raw="$(guardex_repo_toggle_raw "$repo_root" || true)"
400
+ if [[ -n "$toggle_source" && -n "$toggle_raw" ]]; then
401
+ echo "[agent-branch-start] Guardex is disabled for this repo (${toggle_source}: GUARDEX_ON=${toggle_raw})." >&2
402
+ else
403
+ echo "[agent-branch-start] Guardex is disabled for this repo." >&2
404
+ fi
405
+ echo "[agent-branch-start] Skip Guardex worktree/OpenSpec flow or re-enable with GUARDEX_ON=1." >&2
406
+ exit 1
407
+ fi
408
+
392
409
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
393
410
  echo "[agent-branch-start] --base requires a non-empty branch name." >&2
394
411
  exit 1
@@ -474,12 +491,14 @@ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_bra
474
491
  fi
475
492
  fi
476
493
 
477
- git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref"
478
- git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
479
-
480
- if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
481
- git -C "$worktree_path" branch --set-upstream-to="origin/${BASE_BRANCH}" "$branch_name" >/dev/null 2>&1 || true
494
+ worktree_add_output=""
495
+ if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" 2>&1)"; then
496
+ printf '%s\n' "$worktree_add_output" >&2
497
+ exit 1
482
498
  fi
499
+ git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
500
+ # Fresh agent branches should start unpublished; clear any inherited base-branch tracking.
501
+ git -C "$worktree_path" branch --unset-upstream "$branch_name" >/dev/null 2>&1 || true
483
502
 
484
503
  if [[ -n "$auto_transfer_stash_ref" ]]; then
485
504
  if git -C "$worktree_path" stash apply "$auto_transfer_stash_ref" >/dev/null 2>&1; then
@@ -130,6 +130,23 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
130
130
  fi
131
131
  repo_root="$(git rev-parse --show-toplevel)"
132
132
 
133
+ guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
134
+ if [[ -f "$guardex_env_helper" ]]; then
135
+ # shellcheck source=/dev/null
136
+ source "$guardex_env_helper"
137
+ fi
138
+ if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
139
+ toggle_source="$(guardex_repo_toggle_source "$repo_root" || true)"
140
+ toggle_raw="$(guardex_repo_toggle_raw "$repo_root" || true)"
141
+ if [[ -n "$toggle_source" && -n "$toggle_raw" ]]; then
142
+ echo "[codex-agent] Guardex is disabled for this repo (${toggle_source}: GUARDEX_ON=${toggle_raw})." >&2
143
+ else
144
+ echo "[codex-agent] Guardex is disabled for this repo." >&2
145
+ fi
146
+ echo "[codex-agent] Skip Guardex sandbox flow or re-enable with GUARDEX_ON=1." >&2
147
+ exit 1
148
+ fi
149
+
133
150
  sanitize_slug() {
134
151
  local raw="$1"
135
152
  local fallback="${2:-task}"
@@ -274,11 +291,13 @@ start_sandbox_fallback() {
274
291
  return 1
275
292
  fi
276
293
 
277
- git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
278
- git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$base_branch" >/dev/null 2>&1 || true
279
- if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
280
- git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
294
+ local worktree_add_output=""
295
+ if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" 2>&1)"; then
296
+ printf '%s\n' "$worktree_add_output" >&2
297
+ return 1
281
298
  fi
299
+ git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$base_branch" >/dev/null 2>&1 || true
300
+ git -C "$worktree_path" branch --unset-upstream "$branch_name" >/dev/null 2>&1 || true
282
301
 
283
302
  printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
284
303
  printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bash
2
+
3
+ guardex_normalize_bool() {
4
+ local raw="${1:-}"
5
+ local fallback="${2:-}"
6
+ local lowered
7
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
8
+ case "$lowered" in
9
+ 1|true|yes|on) printf '1' ;;
10
+ 0|false|no|off) printf '0' ;;
11
+ '') printf '%s' "$fallback" ;;
12
+ *) printf '%s' "$fallback" ;;
13
+ esac
14
+ }
15
+
16
+ guardex_read_repo_dotenv_var() {
17
+ local repo_root="$1"
18
+ local key="${2:-GUARDEX_ON}"
19
+ local env_file="${repo_root}/.env"
20
+ local line value
21
+
22
+ [[ -f "$env_file" ]] || return 1
23
+
24
+ while IFS= read -r line || [[ -n "$line" ]]; do
25
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
26
+ if [[ "$line" =~ ^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=(.*)$ ]]; then
27
+ value="${BASH_REMATCH[2]}"
28
+ value="$(printf '%s' "$value" | sed -E 's/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//')"
29
+ if [[ "$value" == \"*\" && "$value" == *\" ]]; then
30
+ value="${value:1:${#value}-2}"
31
+ elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
32
+ value="${value:1:${#value}-2}"
33
+ fi
34
+ printf '%s' "$value"
35
+ return 0
36
+ fi
37
+ done < "$env_file"
38
+
39
+ return 1
40
+ }
41
+
42
+ guardex_repo_toggle_raw() {
43
+ local repo_root="$1"
44
+ if [[ -n "${GUARDEX_ON:-}" ]]; then
45
+ printf '%s' "$GUARDEX_ON"
46
+ return 0
47
+ fi
48
+ guardex_read_repo_dotenv_var "$repo_root" "GUARDEX_ON"
49
+ }
50
+
51
+ guardex_repo_toggle_source() {
52
+ local repo_root="$1"
53
+ if [[ -n "${GUARDEX_ON:-}" ]]; then
54
+ printf 'process environment'
55
+ return 0
56
+ fi
57
+ if guardex_read_repo_dotenv_var "$repo_root" "GUARDEX_ON" >/dev/null; then
58
+ printf 'repo .env'
59
+ return 0
60
+ fi
61
+ return 1
62
+ }
63
+
64
+ guardex_repo_is_enabled() {
65
+ local repo_root="$1"
66
+ local raw normalized
67
+ if raw="$(guardex_repo_toggle_raw "$repo_root")"; then
68
+ normalized="$(guardex_normalize_bool "$raw" "")"
69
+ if [[ "$normalized" == "0" ]]; then
70
+ return 1
71
+ fi
72
+ fi
73
+ return 0
74
+ }
@@ -66,6 +66,12 @@ if [[ ! -f "${CHANGE_DIR}/tasks.md" ]]; then
66
66
  - [ ] 3.1 Run targeted project verification commands.
67
67
  - [ ] 3.2 Run \`openspec validate ${CHANGE_SLUG} --type change --strict\`.
68
68
  - [ ] 3.3 Run \`openspec validate --specs\`.
69
+
70
+ ## 4. Completion
71
+
72
+ - [ ] 4.1 Finish the agent branch via PR merge + cleanup (\`gx finish --via-pr --wait-for-merge --cleanup\` or \`bash scripts/agent-branch-finish.sh --branch <agent-branch> --base <base-branch> --via-pr --wait-for-merge --cleanup\`).
73
+ - [ ] 4.2 Record PR URL + final \`MERGED\` state in the completion handoff.
74
+ - [ ] 4.3 Confirm sandbox cleanup (\`git worktree list\`, \`git branch -a\`) or capture a \`BLOCKED:\` handoff if merge/cleanup is pending.
69
75
  TASKSEOF
70
76
  fi
71
77