@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 +55 -7
- package/bin/multiagent-safety.js +261 -15
- package/package.json +2 -2
- package/templates/AGENTS.multiagent-safety.md +3 -1
- package/templates/githooks/post-checkout +13 -0
- package/templates/githooks/post-merge +8 -0
- package/templates/githooks/pre-commit +13 -0
- package/templates/githooks/pre-push +13 -0
- package/templates/scripts/agent-branch-start.sh +24 -5
- package/templates/scripts/codex-agent.sh +23 -4
- package/templates/scripts/guardex-env.sh +74 -0
- package/templates/scripts/openspec/init-change-workspace.sh +6 -0
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
|
|
25
|
-
B[Agent B edits
|
|
26
|
-
C
|
|
27
|
-
D
|
|
28
|
-
E -->
|
|
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
|

|
|
97
107
|
|
|
98
|
-
###
|
|
108
|
+
### VS Code Source Control layout (agent + OpenSpec files)
|
|
99
109
|
|
|
100
|
-

|
|
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.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -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.
|
|
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 =
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
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.
|
|
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://
|
|
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
|
-
|
|
478
|
-
git -C "$repo_root"
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
278
|
-
git -C "$repo_root"
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|