@imdeadpool/guardex 7.0.6 → 7.0.8
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 +56 -7
- package/bin/multiagent-safety.js +222 -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,16 @@ npm pack --dry-run
|
|
|
373
412
|
|
|
374
413
|
## Release notes
|
|
375
414
|
|
|
415
|
+
### v7.0.8
|
|
416
|
+
|
|
417
|
+
- **Added: repo toggle guidance in `gx` status/help output.** The command summary now shows a dedicated `REPO TOGGLE` section so operators can see the repo-local switch immediately: `GUARDEX_ON=0` disables Guardex for a repo and `GUARDEX_ON=1` turns it back on.
|
|
418
|
+
- **Changed: package metadata advanced to the next publishable release.** Bumped `@imdeadpool/guardex` from `7.0.7` to `7.0.8` so the current `main` branch state can be published without colliding with the existing release number.
|
|
419
|
+
|
|
420
|
+
### v7.0.7
|
|
421
|
+
|
|
422
|
+
- **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.
|
|
423
|
+
- **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`.
|
|
424
|
+
|
|
376
425
|
### v7.0.6
|
|
377
426
|
|
|
378
427
|
- **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.
|
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
|
|
|
@@ -312,10 +320,17 @@ function agentBotCatalogLines(indent = ' ') {
|
|
|
312
320
|
);
|
|
313
321
|
}
|
|
314
322
|
|
|
323
|
+
function repoToggleLines(indent = ' ') {
|
|
324
|
+
return [
|
|
325
|
+
`${indent}Set repo-root .env: ${GUARDEX_REPO_TOGGLE_ENV}=0 disables Guardex, ${GUARDEX_REPO_TOGGLE_ENV}=1 enables it again`,
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
|
|
315
329
|
function printToolLogsSummary() {
|
|
316
330
|
const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
|
|
317
331
|
const commandDetails = commandCatalogLines(' ');
|
|
318
332
|
const agentBotDetails = agentBotCatalogLines(' ');
|
|
333
|
+
const repoToggleDetails = repoToggleLines(' ');
|
|
319
334
|
|
|
320
335
|
if (!supportsAnsiColors()) {
|
|
321
336
|
console.log(`${TOOL_NAME}-tools logs:`);
|
|
@@ -329,6 +344,10 @@ function printToolLogsSummary() {
|
|
|
329
344
|
for (const line of agentBotDetails) {
|
|
330
345
|
console.log(line);
|
|
331
346
|
}
|
|
347
|
+
console.log(' REPO TOGGLE');
|
|
348
|
+
for (const line of repoToggleDetails) {
|
|
349
|
+
console.log(line);
|
|
350
|
+
}
|
|
332
351
|
return;
|
|
333
352
|
}
|
|
334
353
|
|
|
@@ -336,6 +355,7 @@ function printToolLogsSummary() {
|
|
|
336
355
|
const usageHeader = colorize('USAGE', '1');
|
|
337
356
|
const commandsHeader = colorize('COMMANDS', '1');
|
|
338
357
|
const agentBotHeader = colorize('AGENT BOT', '1');
|
|
358
|
+
const repoToggleHeader = colorize('REPO TOGGLE', '1');
|
|
339
359
|
const pipe = colorize('│', '90');
|
|
340
360
|
const tee = colorize('├', '90');
|
|
341
361
|
const corner = colorize('└', '90');
|
|
@@ -359,6 +379,14 @@ function printToolLogsSummary() {
|
|
|
359
379
|
}
|
|
360
380
|
console.log(` ${pipe}${line.slice(2)}`);
|
|
361
381
|
}
|
|
382
|
+
console.log(` ${tee}─ ${repoToggleHeader}`);
|
|
383
|
+
for (const line of repoToggleDetails) {
|
|
384
|
+
if (!line) {
|
|
385
|
+
console.log(` ${pipe}`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
console.log(` ${pipe}${line.slice(2)}`);
|
|
389
|
+
}
|
|
362
390
|
console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
|
|
363
391
|
}
|
|
364
392
|
|
|
@@ -379,6 +407,9 @@ ${commandCatalogLines().join('\n')}
|
|
|
379
407
|
AGENT BOT
|
|
380
408
|
${agentBotCatalogLines().join('\n')}
|
|
381
409
|
|
|
410
|
+
REPO TOGGLE
|
|
411
|
+
${repoToggleLines().join('\n')}
|
|
412
|
+
|
|
382
413
|
NOTES
|
|
383
414
|
- No command = ${SHORT_TOOL_NAME} status. ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup.
|
|
384
415
|
- Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
|
|
@@ -754,7 +785,6 @@ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
|
|
|
754
785
|
}
|
|
755
786
|
|
|
756
787
|
function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
|
|
757
|
-
const force = Boolean(options.force);
|
|
758
788
|
const agentsPath = path.join(repoRoot, 'AGENTS.md');
|
|
759
789
|
const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
|
|
760
790
|
const managedRegex = new RegExp(
|
|
@@ -771,9 +801,6 @@ function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
|
|
|
771
801
|
|
|
772
802
|
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
773
803
|
if (managedRegex.test(existing)) {
|
|
774
|
-
if (!force) {
|
|
775
|
-
return { status: 'unchanged', file: 'AGENTS.md', note: 'preserved existing guardex-managed block' };
|
|
776
|
-
}
|
|
777
804
|
const next = existing.replace(managedRegex, snippet);
|
|
778
805
|
if (next === existing) {
|
|
779
806
|
return { status: 'unchanged', file: 'AGENTS.md' };
|
|
@@ -3155,6 +3182,75 @@ function parseAutoApproval(name) {
|
|
|
3155
3182
|
return null;
|
|
3156
3183
|
}
|
|
3157
3184
|
|
|
3185
|
+
function parseBooleanLike(raw) {
|
|
3186
|
+
if (raw == null) return null;
|
|
3187
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
3188
|
+
if (!normalized) return null;
|
|
3189
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
3190
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
function parseDotenvAssignmentValue(raw) {
|
|
3195
|
+
let value = String(raw || '').trim();
|
|
3196
|
+
if (!value) return '';
|
|
3197
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
3198
|
+
return value.slice(1, -1).trim();
|
|
3199
|
+
}
|
|
3200
|
+
value = value.replace(/\s+#.*$/, '').trim();
|
|
3201
|
+
return value;
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
function readRepoDotenvValue(repoRoot, name) {
|
|
3205
|
+
const envPath = path.join(repoRoot, '.env');
|
|
3206
|
+
if (!fs.existsSync(envPath)) return null;
|
|
3207
|
+
const pattern = new RegExp(`^\\s*(?:export\\s+)?${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*(.*)$`);
|
|
3208
|
+
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
|
|
3209
|
+
for (const line of lines) {
|
|
3210
|
+
const trimmed = line.trim();
|
|
3211
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
3212
|
+
const match = line.match(pattern);
|
|
3213
|
+
if (!match) continue;
|
|
3214
|
+
return parseDotenvAssignmentValue(match[1]);
|
|
3215
|
+
}
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function resolveGuardexRepoToggle(repoRoot, env = process.env) {
|
|
3220
|
+
const envRaw = env[GUARDEX_REPO_TOGGLE_ENV];
|
|
3221
|
+
const envEnabled = parseBooleanLike(envRaw);
|
|
3222
|
+
if (envEnabled !== null) {
|
|
3223
|
+
return {
|
|
3224
|
+
enabled: envEnabled,
|
|
3225
|
+
source: 'process environment',
|
|
3226
|
+
raw: String(envRaw).trim(),
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
const dotenvRaw = readRepoDotenvValue(repoRoot, GUARDEX_REPO_TOGGLE_ENV);
|
|
3231
|
+
const dotenvEnabled = parseBooleanLike(dotenvRaw);
|
|
3232
|
+
if (dotenvEnabled !== null) {
|
|
3233
|
+
return {
|
|
3234
|
+
enabled: dotenvEnabled,
|
|
3235
|
+
source: 'repo .env',
|
|
3236
|
+
raw: String(dotenvRaw).trim(),
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
return {
|
|
3241
|
+
enabled: true,
|
|
3242
|
+
source: 'default',
|
|
3243
|
+
raw: '',
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
function describeGuardexRepoToggle(toggle) {
|
|
3248
|
+
if (!toggle || toggle.source === 'default') {
|
|
3249
|
+
return 'default enabled mode';
|
|
3250
|
+
}
|
|
3251
|
+
return `${toggle.source} (${GUARDEX_REPO_TOGGLE_ENV}=${toggle.raw})`;
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3158
3254
|
function parseVersionString(version) {
|
|
3159
3255
|
const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
3160
3256
|
if (!match) return null;
|
|
@@ -3605,6 +3701,22 @@ function findStaleLockPaths(repoRoot, locks) {
|
|
|
3605
3701
|
|
|
3606
3702
|
function runInstallInternal(options) {
|
|
3607
3703
|
const repoRoot = resolveRepoRoot(options.target);
|
|
3704
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
3705
|
+
if (!guardexToggle.enabled) {
|
|
3706
|
+
return {
|
|
3707
|
+
repoRoot,
|
|
3708
|
+
operations: [
|
|
3709
|
+
{
|
|
3710
|
+
status: 'skipped',
|
|
3711
|
+
file: '.env',
|
|
3712
|
+
note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
|
|
3713
|
+
},
|
|
3714
|
+
],
|
|
3715
|
+
hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
|
|
3716
|
+
guardexEnabled: false,
|
|
3717
|
+
guardexToggle,
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3608
3720
|
const operations = [];
|
|
3609
3721
|
|
|
3610
3722
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
@@ -3628,11 +3740,27 @@ function runInstallInternal(options) {
|
|
|
3628
3740
|
|
|
3629
3741
|
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
|
|
3630
3742
|
|
|
3631
|
-
return { repoRoot, operations, hookResult };
|
|
3743
|
+
return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
|
|
3632
3744
|
}
|
|
3633
3745
|
|
|
3634
3746
|
function runFixInternal(options) {
|
|
3635
3747
|
const repoRoot = resolveRepoRoot(options.target);
|
|
3748
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
3749
|
+
if (!guardexToggle.enabled) {
|
|
3750
|
+
return {
|
|
3751
|
+
repoRoot,
|
|
3752
|
+
operations: [
|
|
3753
|
+
{
|
|
3754
|
+
status: 'skipped',
|
|
3755
|
+
file: '.env',
|
|
3756
|
+
note: `Guardex disabled by ${describeGuardexRepoToggle(guardexToggle)}`,
|
|
3757
|
+
},
|
|
3758
|
+
],
|
|
3759
|
+
hookResult: { status: 'skipped', key: 'core.hooksPath', value: '(unchanged)' },
|
|
3760
|
+
guardexEnabled: false,
|
|
3761
|
+
guardexToggle,
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3636
3764
|
const operations = [];
|
|
3637
3765
|
|
|
3638
3766
|
operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
|
|
@@ -3682,11 +3810,25 @@ function runFixInternal(options) {
|
|
|
3682
3810
|
|
|
3683
3811
|
const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
|
|
3684
3812
|
|
|
3685
|
-
return { repoRoot, operations, hookResult };
|
|
3813
|
+
return { repoRoot, operations, hookResult, guardexEnabled: true, guardexToggle };
|
|
3686
3814
|
}
|
|
3687
3815
|
|
|
3688
3816
|
function runScanInternal(options) {
|
|
3689
3817
|
const repoRoot = resolveRepoRoot(options.target);
|
|
3818
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
3819
|
+
const currentBranchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
|
|
3820
|
+
const branch = currentBranchResult.status === 0 ? currentBranchResult.stdout.trim() : '(unknown)';
|
|
3821
|
+
if (!guardexToggle.enabled) {
|
|
3822
|
+
return {
|
|
3823
|
+
repoRoot,
|
|
3824
|
+
branch,
|
|
3825
|
+
findings: [],
|
|
3826
|
+
errors: 0,
|
|
3827
|
+
warnings: 0,
|
|
3828
|
+
guardexEnabled: false,
|
|
3829
|
+
guardexToggle,
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3690
3832
|
const findings = [];
|
|
3691
3833
|
|
|
3692
3834
|
const requiredPaths = [
|
|
@@ -3777,15 +3919,14 @@ function runScanInternal(options) {
|
|
|
3777
3919
|
const errors = findings.filter((item) => item.level === 'error');
|
|
3778
3920
|
const warnings = findings.filter((item) => item.level === 'warn');
|
|
3779
3921
|
|
|
3780
|
-
const currentBranchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
|
|
3781
|
-
const branch = currentBranchResult.status === 0 ? currentBranchResult.stdout.trim() : '(unknown)';
|
|
3782
|
-
|
|
3783
3922
|
return {
|
|
3784
3923
|
repoRoot,
|
|
3785
3924
|
branch,
|
|
3786
3925
|
findings,
|
|
3787
3926
|
errors: errors.length,
|
|
3788
3927
|
warnings: warnings.length,
|
|
3928
|
+
guardexEnabled: true,
|
|
3929
|
+
guardexToggle,
|
|
3789
3930
|
};
|
|
3790
3931
|
}
|
|
3791
3932
|
|
|
@@ -3811,6 +3952,8 @@ function printScanResult(scan, json = false) {
|
|
|
3811
3952
|
{
|
|
3812
3953
|
repoRoot: scan.repoRoot,
|
|
3813
3954
|
branch: scan.branch,
|
|
3955
|
+
guardexEnabled: scan.guardexEnabled !== false,
|
|
3956
|
+
guardexToggle: scan.guardexToggle || null,
|
|
3814
3957
|
errors: scan.errors,
|
|
3815
3958
|
warnings: scan.warnings,
|
|
3816
3959
|
findings: scan.findings,
|
|
@@ -3825,6 +3968,13 @@ function printScanResult(scan, json = false) {
|
|
|
3825
3968
|
console.log(`[${TOOL_NAME}] Scan target: ${scan.repoRoot}`);
|
|
3826
3969
|
console.log(`[${TOOL_NAME}] Branch: ${scan.branch}`);
|
|
3827
3970
|
|
|
3971
|
+
if (scan.guardexEnabled === false) {
|
|
3972
|
+
console.log(
|
|
3973
|
+
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(scan.guardexToggle)}).`,
|
|
3974
|
+
);
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3828
3978
|
if (scan.findings.length === 0) {
|
|
3829
3979
|
console.log(`[${TOOL_NAME}] ✅ No safety issues detected.`);
|
|
3830
3980
|
return;
|
|
@@ -3838,6 +3988,10 @@ function printScanResult(scan, json = false) {
|
|
|
3838
3988
|
}
|
|
3839
3989
|
|
|
3840
3990
|
function setExitCodeFromScan(scan) {
|
|
3991
|
+
if (scan.guardexEnabled === false) {
|
|
3992
|
+
process.exitCode = 0;
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3841
3995
|
if (scan.errors > 0) {
|
|
3842
3996
|
process.exitCode = 2;
|
|
3843
3997
|
return;
|
|
@@ -3879,7 +4033,9 @@ function status(rawArgs) {
|
|
|
3879
4033
|
const inGitRepo = isGitRepo(targetPath);
|
|
3880
4034
|
const scanResult = inGitRepo ? runScanInternal({ target: targetPath, json: false }) : null;
|
|
3881
4035
|
const repoServiceStatus = scanResult
|
|
3882
|
-
? (scanResult.
|
|
4036
|
+
? (scanResult.guardexEnabled === false
|
|
4037
|
+
? 'disabled'
|
|
4038
|
+
: (scanResult.errors === 0 && scanResult.warnings === 0 ? 'active' : 'degraded'))
|
|
3883
4039
|
: 'inactive';
|
|
3884
4040
|
|
|
3885
4041
|
const payload = {
|
|
@@ -3893,6 +4049,8 @@ function status(rawArgs) {
|
|
|
3893
4049
|
target: targetPath,
|
|
3894
4050
|
inGitRepo,
|
|
3895
4051
|
serviceStatus: repoServiceStatus,
|
|
4052
|
+
guardexEnabled: scanResult ? scanResult.guardexEnabled !== false : null,
|
|
4053
|
+
guardexToggle: scanResult ? scanResult.guardexToggle || null : null,
|
|
3896
4054
|
scan: scanResult
|
|
3897
4055
|
? {
|
|
3898
4056
|
repoRoot: scanResult.repoRoot,
|
|
@@ -3942,6 +4100,17 @@ function status(rawArgs) {
|
|
|
3942
4100
|
return;
|
|
3943
4101
|
}
|
|
3944
4102
|
|
|
4103
|
+
if (scanResult.guardexEnabled === false) {
|
|
4104
|
+
console.log(
|
|
4105
|
+
`[${TOOL_NAME}] Repo safety service: ${statusDot('disabled')} disabled (${describeGuardexRepoToggle(scanResult.guardexToggle)}).`,
|
|
4106
|
+
);
|
|
4107
|
+
console.log(`[${TOOL_NAME}] Repo: ${scanResult.repoRoot}`);
|
|
4108
|
+
console.log(`[${TOOL_NAME}] Branch: ${scanResult.branch}`);
|
|
4109
|
+
printToolLogsSummary();
|
|
4110
|
+
process.exitCode = 0;
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
3945
4114
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
3946
4115
|
console.log(`[${TOOL_NAME}] Repo safety service: ${statusDot('active')} active.`);
|
|
3947
4116
|
} else if (scanResult.errors === 0) {
|
|
@@ -3983,6 +4152,13 @@ function install(rawArgs) {
|
|
|
3983
4152
|
printOperations('Install target', payload, options.dryRun);
|
|
3984
4153
|
|
|
3985
4154
|
if (!options.dryRun) {
|
|
4155
|
+
if (payload.guardexEnabled === false) {
|
|
4156
|
+
console.log(
|
|
4157
|
+
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(payload.guardexToggle)}). Skipping repo bootstrap.`,
|
|
4158
|
+
);
|
|
4159
|
+
process.exitCode = 0;
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
3986
4162
|
if (!options.skipAgents) {
|
|
3987
4163
|
console.log(`[${TOOL_NAME}] AGENTS.md managed policy block is configured by install.`);
|
|
3988
4164
|
}
|
|
@@ -4008,6 +4184,13 @@ function fix(rawArgs) {
|
|
|
4008
4184
|
printOperations('Fix target', payload, options.dryRun);
|
|
4009
4185
|
|
|
4010
4186
|
if (!options.dryRun) {
|
|
4187
|
+
if (payload.guardexEnabled === false) {
|
|
4188
|
+
console.log(
|
|
4189
|
+
`[${TOOL_NAME}] Guardex is disabled for this repo (${describeGuardexRepoToggle(payload.guardexToggle)}). Skipping repo repair.`,
|
|
4190
|
+
);
|
|
4191
|
+
process.exitCode = 0;
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4011
4194
|
console.log(`[${TOOL_NAME}] Repair complete. Next step: ${TOOL_NAME} scan`);
|
|
4012
4195
|
}
|
|
4013
4196
|
|
|
@@ -4047,11 +4230,20 @@ function doctor(rawArgs) {
|
|
|
4047
4230
|
const fixPayload = runFixInternal(options);
|
|
4048
4231
|
const scanResult = runScanInternal({ target: options.target, json: false });
|
|
4049
4232
|
const currentBaseBranch = currentBranchName(scanResult.repoRoot);
|
|
4050
|
-
const autoFinishSummary =
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4233
|
+
const autoFinishSummary = scanResult.guardexEnabled === false
|
|
4234
|
+
? {
|
|
4235
|
+
enabled: false,
|
|
4236
|
+
attempted: 0,
|
|
4237
|
+
completed: 0,
|
|
4238
|
+
skipped: 0,
|
|
4239
|
+
failed: 0,
|
|
4240
|
+
details: [],
|
|
4241
|
+
}
|
|
4242
|
+
: autoFinishReadyAgentBranches(scanResult.repoRoot, {
|
|
4243
|
+
baseBranch: currentBaseBranch,
|
|
4244
|
+
dryRun: options.dryRun,
|
|
4245
|
+
});
|
|
4246
|
+
const safe = scanResult.guardexEnabled === false || (scanResult.errors === 0 && scanResult.warnings === 0);
|
|
4055
4247
|
const musafe = safe;
|
|
4056
4248
|
|
|
4057
4249
|
if (options.json) {
|
|
@@ -4068,6 +4260,8 @@ function doctor(rawArgs) {
|
|
|
4068
4260
|
dryRun: Boolean(options.dryRun),
|
|
4069
4261
|
},
|
|
4070
4262
|
scan: {
|
|
4263
|
+
guardexEnabled: scanResult.guardexEnabled !== false,
|
|
4264
|
+
guardexToggle: scanResult.guardexToggle || null,
|
|
4071
4265
|
errors: scanResult.errors,
|
|
4072
4266
|
warnings: scanResult.warnings,
|
|
4073
4267
|
findings: scanResult.findings,
|
|
@@ -4084,6 +4278,11 @@ function doctor(rawArgs) {
|
|
|
4084
4278
|
|
|
4085
4279
|
printOperations('Doctor/fix', fixPayload, options.dryRun);
|
|
4086
4280
|
printScanResult(scanResult, false);
|
|
4281
|
+
if (scanResult.guardexEnabled === false) {
|
|
4282
|
+
console.log(`[${TOOL_NAME}] Repo-local Guardex enforcement is intentionally disabled.`);
|
|
4283
|
+
setExitCodeFromScan(scanResult);
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4087
4286
|
if (autoFinishSummary.enabled) {
|
|
4088
4287
|
console.log(
|
|
4089
4288
|
`[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
|
|
@@ -4825,6 +5024,7 @@ function initWorkspace(rawArgs) {
|
|
|
4825
5024
|
function doctorAudit(rawArgs) {
|
|
4826
5025
|
const options = parseDoctorArgs(rawArgs);
|
|
4827
5026
|
const repoRoot = resolveRepoRoot(options.target);
|
|
5027
|
+
const guardexToggle = resolveGuardexRepoToggle(repoRoot);
|
|
4828
5028
|
const failures = [];
|
|
4829
5029
|
const warnings = [];
|
|
4830
5030
|
|
|
@@ -4841,6 +5041,13 @@ function doctorAudit(rawArgs) {
|
|
|
4841
5041
|
}
|
|
4842
5042
|
|
|
4843
5043
|
console.log(`[multiagent-safety] doctor target: ${repoRoot}`);
|
|
5044
|
+
if (!guardexToggle.enabled) {
|
|
5045
|
+
console.log(
|
|
5046
|
+
`[multiagent-safety] Guardex is disabled for this repo (${describeGuardexRepoToggle(guardexToggle)}).`,
|
|
5047
|
+
);
|
|
5048
|
+
console.log('[multiagent-safety] doctor passed.');
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
4844
5051
|
|
|
4845
5052
|
const hooksPath = run('git', ['-C', repoRoot, 'config', '--get', 'core.hooksPath']);
|
|
4846
5053
|
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.8",
|
|
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
|
|