@imdeadpool/guardex 5.0.0 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -13
- package/bin/multiagent-safety.js +307 -7
- package/package.json +1 -1
- package/templates/AGENTS.multiagent-safety.md +7 -1
- package/templates/codex/skills/guardex/SKILL.md +5 -0
- package/templates/githooks/pre-commit +22 -0
- package/templates/scripts/agent-branch-finish.sh +81 -31
- package/templates/scripts/agent-worktree-prune.sh +128 -17
- package/templates/scripts/codex-agent.sh +336 -3
package/README.md
CHANGED
|
@@ -50,26 +50,29 @@ Related tools:
|
|
|
50
50
|
|
|
51
51
|
- [oh-my-codex (OMX)](https://github.com/Yeachan-Heo/oh-my-codex)
|
|
52
52
|
- [OpenSpec](https://github.com/Fission-AI/OpenSpec)
|
|
53
|
+
- [codex-account-switcher-cli](https://github.com/recodeecom/codex-account-switcher-cli)
|
|
53
54
|
|
|
54
55
|
## Fast setup (recommended)
|
|
55
56
|
|
|
56
57
|
```sh
|
|
57
58
|
# inside your repo
|
|
58
59
|
gx setup
|
|
60
|
+
# alias:
|
|
61
|
+
gx init
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
That one command runs:
|
|
62
65
|
|
|
63
|
-
1. detects whether OMX/OpenSpec are already globally installed,
|
|
66
|
+
1. detects whether OMX/OpenSpec/codex-auth are already globally installed,
|
|
64
67
|
2. asks strict Y/N approval only if something is missing,
|
|
65
68
|
3. installs guardrail scripts/hooks,
|
|
66
69
|
4. repairs common safety problems,
|
|
67
70
|
5. installs local Codex + Claude gx helper skill files if missing,
|
|
68
71
|
6. scans and reports final status.
|
|
69
72
|
|
|
70
|
-
## Setup screenshot
|
|
73
|
+
## Setup behavior screenshot
|
|
71
74
|
|
|
72
|
-

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