@imdeadpool/guardex 5.0.9 → 5.0.12
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 +117 -9
- package/bin/multiagent-safety.js +456 -8
- package/package.json +6 -5
- package/templates/AGENTS.multiagent-safety.md +3 -2
- package/templates/codex/skills/guardex/SKILL.md +48 -0
- package/templates/codex/skills/guardex-merge-skills-to-dev/SKILL.md +58 -0
- package/templates/github/pull.yml.example +6 -0
- package/templates/github/workflows/cr.yml +21 -0
- package/templates/scripts/agent-branch-start.sh +23 -0
- package/templates/scripts/agent-worktree-prune.sh +102 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# GuardeX — Guardian T-Rex for your repo
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@imdeadpool/guardex)
|
|
4
|
-
[](https://github.com/recodeee/guardex/actions/workflows/ci.yml)
|
|
5
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/recodeee/guardex)
|
|
6
6
|
|
|
7
7
|
GuardeX is a safety layer for parallel Codex/agent work in git repos.
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ Progress became **de-progressive**: more activity, less real forward movement.
|
|
|
17
17
|
|
|
18
18
|
GuardeX exists to stop that loop.
|
|
19
19
|
|
|
20
|
-

|
|
21
21
|
|
|
22
22
|
```mermaid
|
|
23
23
|
flowchart LR
|
|
@@ -81,23 +81,23 @@ gx finish --all
|
|
|
81
81
|
|
|
82
82
|
### Setup status
|
|
83
83
|
|
|
84
|
-

|
|
85
85
|
|
|
86
86
|
### Service logs/status
|
|
87
87
|
|
|
88
|
-

|
|
89
89
|
|
|
90
90
|
### Branch/worktree start protocol
|
|
91
91
|
|
|
92
|
-

|
|
93
93
|
|
|
94
94
|
### Lock + delete guard protocol
|
|
95
95
|
|
|
96
|
-

|
|
97
97
|
|
|
98
98
|
### Real VS Code Source Control layout (exact screenshot)
|
|
99
99
|
|
|
100
|
-

|
|
101
101
|
|
|
102
102
|
## Copy-paste: common commands
|
|
103
103
|
|
|
@@ -124,15 +124,24 @@ gx sync
|
|
|
124
124
|
# continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
|
|
125
125
|
gx review --interval 30
|
|
126
126
|
|
|
127
|
+
# start both background bots for this repo (review + cleanup)
|
|
128
|
+
gx agents start
|
|
129
|
+
|
|
130
|
+
# stop both background bots for this repo
|
|
131
|
+
gx agents stop
|
|
132
|
+
|
|
127
133
|
# auto-commit finished agent branches and open/merge PR flow in one pass
|
|
128
134
|
gx finish --all
|
|
129
135
|
|
|
130
136
|
# cleanup merged agent branches and hide clean stale agent worktrees
|
|
131
137
|
gx cleanup
|
|
132
138
|
|
|
139
|
+
# run continuous stale-branch cleanup bot (default idle threshold: 10 minutes)
|
|
140
|
+
gx cleanup --watch --interval 60
|
|
141
|
+
|
|
133
142
|
# scan/report
|
|
134
143
|
gx scan
|
|
135
|
-
gx report scorecard --repo github.com/
|
|
144
|
+
gx report scorecard --repo github.com/recodeee/guardex
|
|
136
145
|
```
|
|
137
146
|
|
|
138
147
|
### Continuous Codex PR monitor (local codex-auth session)
|
|
@@ -152,6 +161,37 @@ Useful flags:
|
|
|
152
161
|
|
|
153
162
|
Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flags for compatibility with both older and newer `scripts/codex-agent.sh` argument parsing.
|
|
154
163
|
|
|
164
|
+
### Continuous stale branch cleanup bot
|
|
165
|
+
|
|
166
|
+
Use this to auto-prune idle `agent/*` worktrees created by Codex while keeping active worktrees untouched.
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
# watch cleanup loop every minute (default idle threshold is 10 minutes when --watch is enabled)
|
|
170
|
+
gx cleanup --watch --interval 60
|
|
171
|
+
|
|
172
|
+
# one-shot cleanup for branches idle at least 10 minutes
|
|
173
|
+
gx cleanup --idle-minutes 10
|
|
174
|
+
|
|
175
|
+
# run a single watch cycle (helpful for cron/CI checks)
|
|
176
|
+
gx cleanup --watch --once --interval 60
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Repo Agent Supervisor (start both bots with one command)
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
# starts review bot + cleanup bot in background for the current repo
|
|
183
|
+
gx agents start
|
|
184
|
+
|
|
185
|
+
# optional tuning
|
|
186
|
+
gx agents start --review-interval 30 --cleanup-interval 60 --idle-minutes 10
|
|
187
|
+
|
|
188
|
+
# show whether both bots are running for this repo
|
|
189
|
+
gx agents status
|
|
190
|
+
|
|
191
|
+
# stop both bots and clear repo-local state
|
|
192
|
+
gx agents stop
|
|
193
|
+
```
|
|
194
|
+
|
|
155
195
|
## Important behavior defaults
|
|
156
196
|
|
|
157
197
|
- No command defaults to `gx status`.
|
|
@@ -201,6 +241,40 @@ gh --version
|
|
|
201
241
|
gh auth status
|
|
202
242
|
```
|
|
203
243
|
|
|
244
|
+
## Optional GitHub Apps: fork sync + PR review
|
|
245
|
+
|
|
246
|
+
### Pull app (Probot fork sync)
|
|
247
|
+
|
|
248
|
+
GuardeX setup now installs a starter file at `.github/pull.yml.example`.
|
|
249
|
+
|
|
250
|
+
To enable fork auto-sync:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
cp .github/pull.yml.example .github/pull.yml
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Then edit `.github/pull.yml`:
|
|
257
|
+
|
|
258
|
+
- set `rules[].base` to your fork branch (`main`, `master`, or `dev`)
|
|
259
|
+
- set `rules[].upstream` to `<upstream-owner>:<branch>`
|
|
260
|
+
|
|
261
|
+
Install the app: <https://github.com/apps/pull>
|
|
262
|
+
Validate config: `https://pull.git.ci/check/<owner>/<repo>`
|
|
263
|
+
|
|
264
|
+
### CR-GPT code review app
|
|
265
|
+
|
|
266
|
+
Install app: <https://github.com/apps/cr-gpt>
|
|
267
|
+
|
|
268
|
+
`gx setup` also installs `.github/workflows/cr.yml` (GitHub Actions review workflow).
|
|
269
|
+
|
|
270
|
+
Then in your repo:
|
|
271
|
+
|
|
272
|
+
1. `Settings -> Secrets and variables -> Actions`
|
|
273
|
+
2. open `Variables`
|
|
274
|
+
3. add `OPENAI_API_KEY`
|
|
275
|
+
|
|
276
|
+
After that, the app reviews new and updated pull requests automatically.
|
|
277
|
+
|
|
204
278
|
## Companion dependency: `codex-auth` account switcher
|
|
205
279
|
|
|
206
280
|
For multi-identity Codex workflows, GuardeX pairs with
|
|
@@ -236,6 +310,8 @@ scripts/openspec/init-plan-workspace.sh
|
|
|
236
310
|
.githooks/pre-push
|
|
237
311
|
.codex/skills/guardex/SKILL.md
|
|
238
312
|
.claude/commands/guardex.md
|
|
313
|
+
.github/pull.yml.example
|
|
314
|
+
.github/workflows/cr.yml
|
|
239
315
|
.omx/state/agent-file-locks.json
|
|
240
316
|
```
|
|
241
317
|
|
|
@@ -247,6 +323,23 @@ If you enabled global OpenSpec install during setup (`@fission-ai/openspec`), us
|
|
|
247
323
|
|
|
248
324
|
- [`docs/openspec-getting-started.md`](./docs/openspec-getting-started.md)
|
|
249
325
|
|
|
326
|
+
Default core flow:
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
/opsx:propose <change-name> -> /opsx:apply -> /opsx:archive
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Optional expanded flow:
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
openspec config profile <profile-name>
|
|
336
|
+
openspec update
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
/opsx:new <change-name> -> /opsx:ff or /opsx:continue -> /opsx:apply -> /opsx:verify -> /opsx:archive
|
|
341
|
+
```
|
|
342
|
+
|
|
250
343
|
### OpenSpec in agent sub-branches
|
|
251
344
|
|
|
252
345
|
- `scripts/codex-agent.sh` enforces an OpenSpec workspace before it launches Codex in each sandbox branch/worktree.
|
|
@@ -271,6 +364,21 @@ npm pack --dry-run
|
|
|
271
364
|
|
|
272
365
|
## Release notes
|
|
273
366
|
|
|
367
|
+
### v5.0.12
|
|
368
|
+
|
|
369
|
+
- Bumped package version from `5.0.11` to `5.0.12` for the next npm publish.
|
|
370
|
+
- Updated repository metadata and README links to the renamed GitHub repository (`recodeee/guardex`).
|
|
371
|
+
|
|
372
|
+
### v5.0.11
|
|
373
|
+
|
|
374
|
+
- Updated the managed AGENTS contract wording to use `GX` naming and added an explicit OMX completion policy requiring commit + push + PR creation/update at task completion.
|
|
375
|
+
- Ensured `gx install` explicitly configures the managed `AGENTS.md` policy block and added regression coverage for this install-path behavior.
|
|
376
|
+
- Bumped package version from `5.0.10` to `5.0.11` for the next npm publish.
|
|
377
|
+
|
|
378
|
+
### v5.0.10
|
|
379
|
+
|
|
380
|
+
- Bumped package version from `5.0.9` to `5.0.10` for the next npm publish.
|
|
381
|
+
|
|
274
382
|
### v5.0.9
|
|
275
383
|
|
|
276
384
|
- Enforced OpenSpec workspace bootstrap for sandbox agent execution: `scripts/codex-agent.sh` now initializes `openspec/plan/<agent-branch-slug>/` before launching Codex, and `scripts/agent-branch-start.sh` supports `MUSAFETY_OPENSPEC_AUTO_INIT` plus `MUSAFETY_OPENSPEC_PLAN_SLUG`.
|
package/bin/multiagent-safety.js
CHANGED
|
@@ -50,7 +50,10 @@ const TEMPLATE_FILES = [
|
|
|
50
50
|
'githooks/pre-commit',
|
|
51
51
|
'githooks/pre-push',
|
|
52
52
|
'codex/skills/guardex/SKILL.md',
|
|
53
|
+
'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
53
54
|
'claude/commands/guardex.md',
|
|
55
|
+
'github/pull.yml.example',
|
|
56
|
+
'github/workflows/cr.yml',
|
|
54
57
|
];
|
|
55
58
|
|
|
56
59
|
const EXECUTABLE_RELATIVE_PATHS = new Set([
|
|
@@ -78,6 +81,7 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
|
|
|
78
81
|
]);
|
|
79
82
|
|
|
80
83
|
const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
|
|
84
|
+
const AGENTS_BOTS_STATE_RELATIVE = '.omx/state/agents-bots.json';
|
|
81
85
|
const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
|
|
82
86
|
const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
|
|
83
87
|
const GITIGNORE_MARKER_START = '# multiagent-safety:START';
|
|
@@ -96,6 +100,7 @@ const MANAGED_GITIGNORE_PATHS = [
|
|
|
96
100
|
'.githooks/pre-push',
|
|
97
101
|
'oh-my-codex/',
|
|
98
102
|
'.codex/skills/guardex/SKILL.md',
|
|
103
|
+
'.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
|
|
99
104
|
'.claude/commands/guardex.md',
|
|
100
105
|
LOCK_FILE_RELATIVE,
|
|
101
106
|
];
|
|
@@ -128,6 +133,7 @@ const SUGGESTIBLE_COMMANDS = [
|
|
|
128
133
|
'init',
|
|
129
134
|
'doctor',
|
|
130
135
|
'review',
|
|
136
|
+
'agents',
|
|
131
137
|
'finish',
|
|
132
138
|
'report',
|
|
133
139
|
'copy-prompt',
|
|
@@ -154,7 +160,8 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
154
160
|
['copy-commands', 'Print setup checklist as executable commands only'],
|
|
155
161
|
['protect', 'Manage protected branches (list/add/remove/set/reset)'],
|
|
156
162
|
['sync', 'Check or sync agent branches with origin/<base>'],
|
|
157
|
-
['cleanup', 'Cleanup
|
|
163
|
+
['cleanup', 'Cleanup agent branches/worktrees (supports idle watch mode)'],
|
|
164
|
+
['agents', 'Start/stop repo-scoped review + cleanup bots'],
|
|
158
165
|
['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
|
|
159
166
|
['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
|
|
160
167
|
['scan', 'Report safety issues and exit non-zero on findings'],
|
|
@@ -165,6 +172,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
|
|
|
165
172
|
];
|
|
166
173
|
const AGENT_BOT_DESCRIPTIONS = [
|
|
167
174
|
['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
|
|
175
|
+
['agents', 'Start/stop both review and cleanup bots for this repo'],
|
|
168
176
|
];
|
|
169
177
|
|
|
170
178
|
const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
|
|
@@ -203,18 +211,48 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
|
|
|
203
211
|
- To finalize all completed agent branches in one pass:
|
|
204
212
|
gx finish --all
|
|
205
213
|
|
|
206
|
-
6)
|
|
214
|
+
6) OpenSpec default change flow (core profile):
|
|
215
|
+
/opsx:propose <change-name>
|
|
216
|
+
/opsx:apply
|
|
217
|
+
/opsx:archive
|
|
218
|
+
- Full guide: docs/openspec-getting-started.md
|
|
219
|
+
|
|
220
|
+
7) Optional: enable expanded OpenSpec workflow commands:
|
|
221
|
+
openspec config profile <profile-name>
|
|
222
|
+
openspec update
|
|
223
|
+
- Expanded path: /opsx:new -> /opsx:ff or /opsx:continue -> /opsx:apply -> /opsx:verify -> /opsx:archive
|
|
224
|
+
|
|
225
|
+
8) Optional: create OpenSpec planning workspace:
|
|
207
226
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
208
227
|
|
|
209
|
-
|
|
228
|
+
9) Optional: protect extra branches:
|
|
210
229
|
gx protect add release staging
|
|
211
230
|
|
|
212
|
-
|
|
231
|
+
10) Optional: sync your current agent branch with latest base branch:
|
|
213
232
|
gx sync --check
|
|
214
233
|
gx sync
|
|
215
234
|
|
|
216
|
-
|
|
235
|
+
11) Optional (GitHub remote cleanup): enable:
|
|
217
236
|
Settings -> General -> Pull Requests -> Automatically delete head branches
|
|
237
|
+
|
|
238
|
+
12) Optional (fork sync with Pull app):
|
|
239
|
+
cp .github/pull.yml.example .github/pull.yml
|
|
240
|
+
# then edit .github/pull.yml:
|
|
241
|
+
# - set rules[].base to your fork branch (main/master/dev)
|
|
242
|
+
# - set rules[].upstream to upstream-owner:branch
|
|
243
|
+
# install app: https://github.com/apps/pull
|
|
244
|
+
# validate config: https://pull.git.ci/check/<owner>/<repo>
|
|
245
|
+
|
|
246
|
+
13) Optional (PR review bot with cr-gpt GitHub App):
|
|
247
|
+
- install app: https://github.com/apps/cr-gpt
|
|
248
|
+
- in GitHub repo Settings -> Secrets and variables -> Actions -> Variables:
|
|
249
|
+
add OPENAI_API_KEY (your API key)
|
|
250
|
+
- the app reviews new/updated pull requests automatically
|
|
251
|
+
|
|
252
|
+
14) Optional: test PR review action workflow
|
|
253
|
+
- gx setup installs .github/workflows/cr.yml
|
|
254
|
+
- open or update a PR
|
|
255
|
+
- check Actions -> "Code Review" run logs + PR timeline comments
|
|
218
256
|
`;
|
|
219
257
|
|
|
220
258
|
const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
|
|
@@ -229,9 +267,12 @@ bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)
|
|
|
229
267
|
gx finish --all
|
|
230
268
|
gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
|
|
231
269
|
bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
|
|
270
|
+
openspec config profile <profile-name>
|
|
271
|
+
openspec update
|
|
232
272
|
gx protect add release staging
|
|
233
273
|
gx sync --check
|
|
234
274
|
gx sync
|
|
275
|
+
cp .github/pull.yml.example .github/pull.yml
|
|
235
276
|
`;
|
|
236
277
|
|
|
237
278
|
const SCORECARD_RISK_BY_CHECK = {
|
|
@@ -435,6 +476,9 @@ function toDestinationPath(relativeTemplatePath) {
|
|
|
435
476
|
if (relativeTemplatePath.startsWith('claude/')) {
|
|
436
477
|
return `.${relativeTemplatePath}`;
|
|
437
478
|
}
|
|
479
|
+
if (relativeTemplatePath.startsWith('github/')) {
|
|
480
|
+
return `.${relativeTemplatePath}`;
|
|
481
|
+
}
|
|
438
482
|
throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
|
|
439
483
|
}
|
|
440
484
|
|
|
@@ -1567,6 +1611,69 @@ function parseReviewArgs(rawArgs) {
|
|
|
1567
1611
|
};
|
|
1568
1612
|
}
|
|
1569
1613
|
|
|
1614
|
+
function parseAgentsArgs(rawArgs) {
|
|
1615
|
+
const parsed = parseTargetFlag(rawArgs, process.cwd());
|
|
1616
|
+
const [subcommandRaw = '', ...rest] = parsed.args;
|
|
1617
|
+
const subcommand = subcommandRaw || 'status';
|
|
1618
|
+
const options = {
|
|
1619
|
+
target: parsed.target,
|
|
1620
|
+
subcommand,
|
|
1621
|
+
reviewIntervalSeconds: 30,
|
|
1622
|
+
cleanupIntervalSeconds: 60,
|
|
1623
|
+
idleMinutes: 10,
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
1627
|
+
const arg = rest[index];
|
|
1628
|
+
if (arg === '--review-interval') {
|
|
1629
|
+
const next = rest[index + 1];
|
|
1630
|
+
if (!next) {
|
|
1631
|
+
throw new Error('--review-interval requires an integer seconds value');
|
|
1632
|
+
}
|
|
1633
|
+
const parsedValue = Number.parseInt(next, 10);
|
|
1634
|
+
if (!Number.isInteger(parsedValue) || parsedValue < 5) {
|
|
1635
|
+
throw new Error('--review-interval must be an integer >= 5 seconds');
|
|
1636
|
+
}
|
|
1637
|
+
options.reviewIntervalSeconds = parsedValue;
|
|
1638
|
+
index += 1;
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (arg === '--cleanup-interval') {
|
|
1642
|
+
const next = rest[index + 1];
|
|
1643
|
+
if (!next) {
|
|
1644
|
+
throw new Error('--cleanup-interval requires an integer seconds value');
|
|
1645
|
+
}
|
|
1646
|
+
const parsedValue = Number.parseInt(next, 10);
|
|
1647
|
+
if (!Number.isInteger(parsedValue) || parsedValue < 5) {
|
|
1648
|
+
throw new Error('--cleanup-interval must be an integer >= 5 seconds');
|
|
1649
|
+
}
|
|
1650
|
+
options.cleanupIntervalSeconds = parsedValue;
|
|
1651
|
+
index += 1;
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (arg === '--idle-minutes') {
|
|
1655
|
+
const next = rest[index + 1];
|
|
1656
|
+
if (!next) {
|
|
1657
|
+
throw new Error('--idle-minutes requires an integer minutes value');
|
|
1658
|
+
}
|
|
1659
|
+
const parsedValue = Number.parseInt(next, 10);
|
|
1660
|
+
if (!Number.isInteger(parsedValue) || parsedValue < 1) {
|
|
1661
|
+
throw new Error('--idle-minutes must be an integer >= 1');
|
|
1662
|
+
}
|
|
1663
|
+
options.idleMinutes = parsedValue;
|
|
1664
|
+
index += 1;
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (!['start', 'stop', 'status'].includes(options.subcommand)) {
|
|
1671
|
+
throw new Error(`Unknown agents subcommand: ${options.subcommand}`);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
return options;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1570
1677
|
function parseReportArgs(rawArgs) {
|
|
1571
1678
|
const options = {
|
|
1572
1679
|
target: process.cwd(),
|
|
@@ -2276,6 +2383,10 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2276
2383
|
forceDirty: false,
|
|
2277
2384
|
keepRemote: false,
|
|
2278
2385
|
keepCleanWorktrees: false,
|
|
2386
|
+
idleMinutes: 0,
|
|
2387
|
+
watch: false,
|
|
2388
|
+
intervalSeconds: 60,
|
|
2389
|
+
once: false,
|
|
2279
2390
|
};
|
|
2280
2391
|
|
|
2281
2392
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -2323,9 +2434,47 @@ function parseCleanupArgs(rawArgs) {
|
|
|
2323
2434
|
options.keepCleanWorktrees = true;
|
|
2324
2435
|
continue;
|
|
2325
2436
|
}
|
|
2437
|
+
if (arg === '--idle-minutes') {
|
|
2438
|
+
const next = rawArgs[index + 1];
|
|
2439
|
+
if (!next) {
|
|
2440
|
+
throw new Error('--idle-minutes requires an integer value');
|
|
2441
|
+
}
|
|
2442
|
+
const parsed = Number.parseInt(next, 10);
|
|
2443
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
2444
|
+
throw new Error('--idle-minutes must be an integer >= 0');
|
|
2445
|
+
}
|
|
2446
|
+
options.idleMinutes = parsed;
|
|
2447
|
+
index += 1;
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if (arg === '--watch') {
|
|
2451
|
+
options.watch = true;
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
if (arg === '--interval') {
|
|
2455
|
+
const next = rawArgs[index + 1];
|
|
2456
|
+
if (!next) {
|
|
2457
|
+
throw new Error('--interval requires an integer seconds value');
|
|
2458
|
+
}
|
|
2459
|
+
const parsed = Number.parseInt(next, 10);
|
|
2460
|
+
if (!Number.isInteger(parsed) || parsed < 5) {
|
|
2461
|
+
throw new Error('--interval must be an integer >= 5 seconds');
|
|
2462
|
+
}
|
|
2463
|
+
options.intervalSeconds = parsed;
|
|
2464
|
+
index += 1;
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
if (arg === '--once') {
|
|
2468
|
+
options.once = true;
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2326
2471
|
throw new Error(`Unknown option: ${arg}`);
|
|
2327
2472
|
}
|
|
2328
2473
|
|
|
2474
|
+
if (options.watch && options.idleMinutes === 0) {
|
|
2475
|
+
options.idleMinutes = 10;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2329
2478
|
return options;
|
|
2330
2479
|
}
|
|
2331
2480
|
|
|
@@ -3485,6 +3634,9 @@ function install(rawArgs) {
|
|
|
3485
3634
|
printOperations('Install target', payload, options.dryRun);
|
|
3486
3635
|
|
|
3487
3636
|
if (!options.dryRun) {
|
|
3637
|
+
if (!options.skipAgents) {
|
|
3638
|
+
console.log(`[${TOOL_NAME}] AGENTS.md managed policy block is configured by install.`);
|
|
3639
|
+
}
|
|
3488
3640
|
console.log(`[${TOOL_NAME}] Installed. Next step: ${TOOL_NAME} setup`);
|
|
3489
3641
|
}
|
|
3490
3642
|
|
|
@@ -3624,6 +3776,263 @@ function review(rawArgs) {
|
|
|
3624
3776
|
process.exitCode = typeof result.status === 'number' ? result.status : 1;
|
|
3625
3777
|
}
|
|
3626
3778
|
|
|
3779
|
+
function agentsStatePathForRepo(repoRoot) {
|
|
3780
|
+
return path.join(repoRoot, AGENTS_BOTS_STATE_RELATIVE);
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
function readAgentsState(repoRoot) {
|
|
3784
|
+
const statePath = agentsStatePathForRepo(repoRoot);
|
|
3785
|
+
if (!fs.existsSync(statePath)) {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
try {
|
|
3789
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
3790
|
+
} catch (_error) {
|
|
3791
|
+
return null;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
function writeAgentsState(repoRoot, state) {
|
|
3796
|
+
const statePath = agentsStatePathForRepo(repoRoot);
|
|
3797
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
3798
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
function processAlive(pid) {
|
|
3802
|
+
const normalizedPid = Number.parseInt(String(pid || ''), 10);
|
|
3803
|
+
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
|
|
3804
|
+
return false;
|
|
3805
|
+
}
|
|
3806
|
+
try {
|
|
3807
|
+
process.kill(normalizedPid, 0);
|
|
3808
|
+
return true;
|
|
3809
|
+
} catch (_error) {
|
|
3810
|
+
return false;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
function sleepSeconds(seconds) {
|
|
3815
|
+
const result = run('sleep', [String(seconds)]);
|
|
3816
|
+
if (isSpawnFailure(result) || result.status !== 0) {
|
|
3817
|
+
throw new Error(`sleep command failed for ${seconds}s`);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
function readProcessCommand(pid) {
|
|
3822
|
+
const result = run('ps', ['-o', 'command=', '-p', String(pid)]);
|
|
3823
|
+
if (isSpawnFailure(result) || result.status !== 0) {
|
|
3824
|
+
return '';
|
|
3825
|
+
}
|
|
3826
|
+
return String(result.stdout || '').trim();
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
function stopAgentProcessByPid(pid, expectedToken = '') {
|
|
3830
|
+
const normalizedPid = Number.parseInt(String(pid || ''), 10);
|
|
3831
|
+
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
|
|
3832
|
+
return { status: 'invalid', pid: normalizedPid };
|
|
3833
|
+
}
|
|
3834
|
+
if (!processAlive(normalizedPid)) {
|
|
3835
|
+
return { status: 'not-running', pid: normalizedPid };
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
if (expectedToken) {
|
|
3839
|
+
const cmdline = readProcessCommand(normalizedPid);
|
|
3840
|
+
if (cmdline && !cmdline.includes(expectedToken)) {
|
|
3841
|
+
return { status: 'mismatch', pid: normalizedPid, command: cmdline };
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
try {
|
|
3846
|
+
process.kill(-normalizedPid, 'SIGTERM');
|
|
3847
|
+
} catch (_error) {
|
|
3848
|
+
try {
|
|
3849
|
+
process.kill(normalizedPid, 'SIGTERM');
|
|
3850
|
+
} catch (_err) {
|
|
3851
|
+
return { status: 'term-failed', pid: normalizedPid };
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
const deadline = Date.now() + 3_000;
|
|
3856
|
+
while (Date.now() < deadline) {
|
|
3857
|
+
if (!processAlive(normalizedPid)) {
|
|
3858
|
+
return { status: 'stopped', pid: normalizedPid };
|
|
3859
|
+
}
|
|
3860
|
+
sleepSeconds(0.1);
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
try {
|
|
3864
|
+
process.kill(-normalizedPid, 'SIGKILL');
|
|
3865
|
+
} catch (_error) {
|
|
3866
|
+
try {
|
|
3867
|
+
process.kill(normalizedPid, 'SIGKILL');
|
|
3868
|
+
} catch (_err) {
|
|
3869
|
+
return { status: 'kill-failed', pid: normalizedPid };
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
sleepSeconds(0.1);
|
|
3873
|
+
|
|
3874
|
+
return {
|
|
3875
|
+
status: processAlive(normalizedPid) ? 'kill-failed' : 'stopped',
|
|
3876
|
+
pid: normalizedPid,
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
function spawnDetachedAgentProcess({ command, args, cwd, logPath }) {
|
|
3881
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
3882
|
+
const logHandle = fs.openSync(logPath, 'a');
|
|
3883
|
+
fs.writeSync(
|
|
3884
|
+
logHandle,
|
|
3885
|
+
`[${new Date().toISOString()}] spawn: ${command} ${args.join(' ')}\n`,
|
|
3886
|
+
);
|
|
3887
|
+
const child = cp.spawn(command, args, {
|
|
3888
|
+
cwd,
|
|
3889
|
+
detached: true,
|
|
3890
|
+
stdio: ['ignore', logHandle, logHandle],
|
|
3891
|
+
env: process.env,
|
|
3892
|
+
});
|
|
3893
|
+
fs.closeSync(logHandle);
|
|
3894
|
+
if (child.error) {
|
|
3895
|
+
throw child.error;
|
|
3896
|
+
}
|
|
3897
|
+
child.unref();
|
|
3898
|
+
const pid = Number.parseInt(String(child.pid || ''), 10);
|
|
3899
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
3900
|
+
throw new Error(`Failed to spawn detached process for ${command}`);
|
|
3901
|
+
}
|
|
3902
|
+
return pid;
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
function agents(rawArgs) {
|
|
3906
|
+
const options = parseAgentsArgs(rawArgs);
|
|
3907
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
3908
|
+
const reviewScriptPath = path.join(repoRoot, 'scripts', 'review-bot-watch.sh');
|
|
3909
|
+
const pruneScriptPath = path.join(repoRoot, 'scripts', 'agent-worktree-prune.sh');
|
|
3910
|
+
const statePath = agentsStatePathForRepo(repoRoot);
|
|
3911
|
+
|
|
3912
|
+
if (options.subcommand === 'start') {
|
|
3913
|
+
if (!fs.existsSync(reviewScriptPath)) {
|
|
3914
|
+
throw new Error(
|
|
3915
|
+
`Missing review bot script: ${reviewScriptPath}\n` +
|
|
3916
|
+
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
if (!fs.existsSync(pruneScriptPath)) {
|
|
3920
|
+
throw new Error(
|
|
3921
|
+
`Missing cleanup script: ${pruneScriptPath}\n` +
|
|
3922
|
+
`Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
const existingState = readAgentsState(repoRoot);
|
|
3927
|
+
const existingReviewPid = Number.parseInt(String(existingState?.review?.pid || ''), 10);
|
|
3928
|
+
const existingCleanupPid = Number.parseInt(String(existingState?.cleanup?.pid || ''), 10);
|
|
3929
|
+
const reviewRunning = processAlive(existingReviewPid);
|
|
3930
|
+
const cleanupRunning = processAlive(existingCleanupPid);
|
|
3931
|
+
|
|
3932
|
+
if (reviewRunning && cleanupRunning) {
|
|
3933
|
+
console.log(
|
|
3934
|
+
`[${TOOL_NAME}] Repo agents already running (review pid=${existingReviewPid}, cleanup pid=${existingCleanupPid}).`,
|
|
3935
|
+
);
|
|
3936
|
+
process.exitCode = 0;
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
if (reviewRunning) {
|
|
3941
|
+
stopAgentProcessByPid(existingReviewPid, 'review-bot-watch.sh');
|
|
3942
|
+
}
|
|
3943
|
+
if (cleanupRunning) {
|
|
3944
|
+
stopAgentProcessByPid(existingCleanupPid, `${path.basename(__filename)} cleanup`);
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
const reviewLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-review.log');
|
|
3948
|
+
const cleanupLogPath = path.join(repoRoot, '.omx', 'logs', 'agent-cleanup.log');
|
|
3949
|
+
const reviewPid = spawnDetachedAgentProcess({
|
|
3950
|
+
command: 'bash',
|
|
3951
|
+
args: [reviewScriptPath, '--interval', String(options.reviewIntervalSeconds)],
|
|
3952
|
+
cwd: repoRoot,
|
|
3953
|
+
logPath: reviewLogPath,
|
|
3954
|
+
});
|
|
3955
|
+
const cleanupPid = spawnDetachedAgentProcess({
|
|
3956
|
+
command: process.execPath,
|
|
3957
|
+
args: [
|
|
3958
|
+
path.resolve(__filename),
|
|
3959
|
+
'cleanup',
|
|
3960
|
+
'--target',
|
|
3961
|
+
repoRoot,
|
|
3962
|
+
'--watch',
|
|
3963
|
+
'--interval',
|
|
3964
|
+
String(options.cleanupIntervalSeconds),
|
|
3965
|
+
'--idle-minutes',
|
|
3966
|
+
String(options.idleMinutes),
|
|
3967
|
+
],
|
|
3968
|
+
cwd: repoRoot,
|
|
3969
|
+
logPath: cleanupLogPath,
|
|
3970
|
+
});
|
|
3971
|
+
|
|
3972
|
+
writeAgentsState(repoRoot, {
|
|
3973
|
+
schemaVersion: 1,
|
|
3974
|
+
repoRoot,
|
|
3975
|
+
startedAt: new Date().toISOString(),
|
|
3976
|
+
review: {
|
|
3977
|
+
pid: reviewPid,
|
|
3978
|
+
intervalSeconds: options.reviewIntervalSeconds,
|
|
3979
|
+
script: reviewScriptPath,
|
|
3980
|
+
logPath: reviewLogPath,
|
|
3981
|
+
},
|
|
3982
|
+
cleanup: {
|
|
3983
|
+
pid: cleanupPid,
|
|
3984
|
+
intervalSeconds: options.cleanupIntervalSeconds,
|
|
3985
|
+
idleMinutes: options.idleMinutes,
|
|
3986
|
+
script: path.resolve(__filename),
|
|
3987
|
+
logPath: cleanupLogPath,
|
|
3988
|
+
},
|
|
3989
|
+
});
|
|
3990
|
+
|
|
3991
|
+
console.log(
|
|
3992
|
+
`[${TOOL_NAME}] Started repo agents in ${repoRoot} (review pid=${reviewPid}, cleanup pid=${cleanupPid}).`,
|
|
3993
|
+
);
|
|
3994
|
+
console.log(`[${TOOL_NAME}] Logs: ${reviewLogPath}, ${cleanupLogPath}`);
|
|
3995
|
+
process.exitCode = 0;
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
if (options.subcommand === 'stop') {
|
|
4000
|
+
const existingState = readAgentsState(repoRoot);
|
|
4001
|
+
if (!existingState) {
|
|
4002
|
+
console.log(`[${TOOL_NAME}] Repo agents are not running for ${repoRoot}.`);
|
|
4003
|
+
process.exitCode = 0;
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
const reviewStop = stopAgentProcessByPid(existingState?.review?.pid, 'review-bot-watch.sh');
|
|
4008
|
+
const cleanupStop = stopAgentProcessByPid(existingState?.cleanup?.pid, `${path.basename(__filename)} cleanup`);
|
|
4009
|
+
|
|
4010
|
+
if (fs.existsSync(statePath)) {
|
|
4011
|
+
fs.unlinkSync(statePath);
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
console.log(
|
|
4015
|
+
`[${TOOL_NAME}] Stopped repo agents in ${repoRoot} (review=${reviewStop.status}, cleanup=${cleanupStop.status}).`,
|
|
4016
|
+
);
|
|
4017
|
+
process.exitCode = 0;
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
const existingState = readAgentsState(repoRoot);
|
|
4022
|
+
if (!existingState) {
|
|
4023
|
+
console.log(`[${TOOL_NAME}] Repo agents status: inactive (${repoRoot})`);
|
|
4024
|
+
process.exitCode = 0;
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
const reviewPid = Number.parseInt(String(existingState?.review?.pid || ''), 10);
|
|
4029
|
+
const cleanupPid = Number.parseInt(String(existingState?.cleanup?.pid || ''), 10);
|
|
4030
|
+
console.log(
|
|
4031
|
+
`[${TOOL_NAME}] Repo agents status: review=${processAlive(reviewPid) ? 'running' : 'stopped'}(pid=${reviewPid || 0}), cleanup=${processAlive(cleanupPid) ? 'running' : 'stopped'}(pid=${cleanupPid || 0})`,
|
|
4032
|
+
);
|
|
4033
|
+
process.exitCode = 0;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
3627
4036
|
function report(rawArgs) {
|
|
3628
4037
|
const options = parseReportArgs(rawArgs);
|
|
3629
4038
|
const subcommand = options.subcommand || 'help';
|
|
@@ -3802,6 +4211,13 @@ function setup(rawArgs) {
|
|
|
3802
4211
|
if (scanResult.errors === 0 && scanResult.warnings === 0) {
|
|
3803
4212
|
console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
|
|
3804
4213
|
console.log(`[${TOOL_NAME}] Copy AI setup prompt with: ${SHORT_TOOL_NAME} copy-prompt`);
|
|
4214
|
+
console.log(
|
|
4215
|
+
`[${TOOL_NAME}] OpenSpec core workflow: /opsx:propose -> /opsx:apply -> /opsx:archive`,
|
|
4216
|
+
);
|
|
4217
|
+
console.log(
|
|
4218
|
+
`[${TOOL_NAME}] Optional expanded OpenSpec profile: openspec config profile <profile-name> && openspec update`,
|
|
4219
|
+
);
|
|
4220
|
+
console.log(`[${TOOL_NAME}] OpenSpec guide: docs/openspec-getting-started.md`);
|
|
3805
4221
|
}
|
|
3806
4222
|
|
|
3807
4223
|
setExitCodeFromScan(scanResult);
|
|
@@ -3895,15 +4311,42 @@ function cleanup(rawArgs) {
|
|
|
3895
4311
|
if (!options.keepCleanWorktrees) {
|
|
3896
4312
|
args.push('--only-dirty-worktrees');
|
|
3897
4313
|
}
|
|
4314
|
+
if (options.idleMinutes > 0) {
|
|
4315
|
+
args.push('--idle-minutes', String(options.idleMinutes));
|
|
4316
|
+
}
|
|
3898
4317
|
args.push('--delete-branches');
|
|
3899
4318
|
if (!options.keepRemote) {
|
|
3900
4319
|
args.push('--delete-remote-branches');
|
|
3901
4320
|
}
|
|
3902
4321
|
|
|
3903
|
-
const
|
|
3904
|
-
|
|
3905
|
-
|
|
4322
|
+
const runCleanupCycle = () => {
|
|
4323
|
+
const runResult = run('bash', args, { cwd: repoRoot, stdio: 'inherit' });
|
|
4324
|
+
if (runResult.status !== 0) {
|
|
4325
|
+
throw new Error('Cleanup command failed');
|
|
4326
|
+
}
|
|
4327
|
+
};
|
|
4328
|
+
|
|
4329
|
+
if (options.watch) {
|
|
4330
|
+
let cycle = 0;
|
|
4331
|
+
while (true) {
|
|
4332
|
+
cycle += 1;
|
|
4333
|
+
console.log(
|
|
4334
|
+
`[${TOOL_NAME}] Cleanup watch cycle=${cycle} (interval=${options.intervalSeconds}s, idleMinutes=${options.idleMinutes}).`,
|
|
4335
|
+
);
|
|
4336
|
+
runCleanupCycle();
|
|
4337
|
+
if (options.once) {
|
|
4338
|
+
break;
|
|
4339
|
+
}
|
|
4340
|
+
const sleepResult = run('sleep', [String(options.intervalSeconds)], { cwd: repoRoot });
|
|
4341
|
+
if (sleepResult.status !== 0) {
|
|
4342
|
+
throw new Error(`Cleanup watch sleep failed (interval=${options.intervalSeconds}s)`);
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
process.exitCode = 0;
|
|
4346
|
+
return;
|
|
3906
4347
|
}
|
|
4348
|
+
|
|
4349
|
+
runCleanupCycle();
|
|
3907
4350
|
process.exitCode = 0;
|
|
3908
4351
|
}
|
|
3909
4352
|
|
|
@@ -4358,6 +4801,11 @@ function main() {
|
|
|
4358
4801
|
return;
|
|
4359
4802
|
}
|
|
4360
4803
|
|
|
4804
|
+
if (command === 'agents') {
|
|
4805
|
+
agents(rest);
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4361
4809
|
if (command === 'finish') {
|
|
4362
4810
|
finish(rest);
|
|
4363
4811
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.12",
|
|
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,
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"agent:safety:scan": "gx scan",
|
|
30
30
|
"agent:safety:fix": "gx fix",
|
|
31
31
|
"agent:safety:doctor": "gx doctor",
|
|
32
|
-
"agent:review:watch": "bash ./scripts/review-bot-watch.sh"
|
|
32
|
+
"agent:review:watch": "bash ./scripts/review-bot-watch.sh",
|
|
33
|
+
"agent:finish": "gx finish --all"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=18"
|
|
@@ -53,12 +54,12 @@
|
|
|
53
54
|
"author": "recodeecom",
|
|
54
55
|
"repository": {
|
|
55
56
|
"type": "git",
|
|
56
|
-
"url": "git+https://github.com/
|
|
57
|
+
"url": "git+https://github.com/recodeee/guardex.git"
|
|
57
58
|
},
|
|
58
59
|
"bugs": {
|
|
59
|
-
"url": "https://github.com/
|
|
60
|
+
"url": "https://github.com/recodeee/guardex/issues"
|
|
60
61
|
},
|
|
61
|
-
"homepage": "https://github.com/
|
|
62
|
+
"homepage": "https://github.com/recodeee/guardex#readme",
|
|
62
63
|
"funding": "https://github.com/sponsors/recodeecom",
|
|
63
64
|
"publishConfig": {
|
|
64
65
|
"access": "public"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- multiagent-safety:START -->
|
|
2
|
-
## Multi-Agent Execution Contract (
|
|
2
|
+
## Multi-Agent Execution Contract (GX)
|
|
3
3
|
|
|
4
4
|
0. Session plan comment + read gate (required)
|
|
5
5
|
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
- In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
|
|
14
14
|
- Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
|
|
15
15
|
- 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).
|
|
16
|
+
- OMX completion policy: when a task is done, the agent must commit the task changes, push the agent branch, and create/update a PR for those changes (via `codex-agent` or `agent-branch-finish`).
|
|
16
17
|
- Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
|
|
17
18
|
- Use `--no-cleanup` only when you explicitly need to keep a merged sandbox for audit/debug follow-up.
|
|
18
19
|
- If codex-agent auto-finish cannot complete, immediately run `scripts/agent-branch-finish.sh --branch "<agent-branch>" --via-pr --wait-for-merge` and keep the branch open until checks/review pass.
|
|
19
20
|
- 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.
|
|
20
21
|
- Completion is not valid until these are true: commit exists on the agent branch, branch is pushed to `origin`, and PR/merge status is produced by `agent-branch-finish.sh` or `codex-agent`.
|
|
21
|
-
- For every new task, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch; otherwise create a fresh one from the current local base snapshot with `scripts/agent-branch-start.sh`.
|
|
22
|
+
- For every new task, including follow-up work in the same chat/session, if an assigned agent sub-branch/worktree is already open, continue in that sub-branch; otherwise create a fresh one from the current local base snapshot with `scripts/agent-branch-start.sh`.
|
|
22
23
|
- Never implement directly on the local/base branch checkout; keep it unchanged and perform all edits in the agent sub-branch/worktree.
|
|
23
24
|
- If the change publishes or bumps a version, the same change must also update release notes/changelog entries.
|
|
24
25
|
|
|
@@ -38,4 +38,52 @@ gx scan
|
|
|
38
38
|
- For one-command Codex sandbox startup, use `bash scripts/codex-agent.sh "<task>" "<agent-name>"`.
|
|
39
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
40
|
- Auto-finish keeps the branch/worktree by default; remove merged branches explicitly with `gx cleanup` (or `gx cleanup --branch "<agent-branch>"`).
|
|
41
|
+
- For skill-file-only merges into the local base branch (`dev` by default), use `$guardex-merge-skills-to-dev`.
|
|
41
42
|
- Do not bypass protected branch safeguards unless explicitly required.
|
|
43
|
+
|
|
44
|
+
## Bulk merge runbook (changed agent branches)
|
|
45
|
+
|
|
46
|
+
Use this when a repo has many `agent/*` branches/worktrees with pending changes and you need them merged into the base branch quickly.
|
|
47
|
+
|
|
48
|
+
1. Confirm base and guardrails are healthy:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
git status --short --branch
|
|
52
|
+
git pull --ff-only origin "$(git config --get multiagent.baseBranch || echo dev)"
|
|
53
|
+
gx scan
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
2. Run bulk finish first:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
gx finish --all
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. If a branch fails with `already used by worktree` or stale rebase hints, clear the stale state in that worktree, then retry targeted finish:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
git -C "<worktree>" rebase --abort || true
|
|
66
|
+
gx finish --branch "<agent-branch>" --base "$(git config --get multiagent.baseBranch || echo dev)" --no-wait-for-merge --cleanup
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
4. If `gh pr merge` exits non-zero due local branch deletion but PR is already merged, treat it as merged and verify with:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
gh pr view "<pr-number>" --json state,mergedAt,url
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
5. If a branch is still ahead of base with no open PR, create and merge a follow-up PR manually:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
gh pr create --base "<base-branch>" --head "<agent-branch>" --title "Auto-finish: <agent-branch>" --body "Follow-up merge for pending branch commits."
|
|
79
|
+
gh pr merge "<pr-number>" --squash --delete-branch
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
6. Final verification:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
gh pr list --state open --search "head:agent/ base:<base-branch>"
|
|
86
|
+
git pull --ff-only origin "<base-branch>"
|
|
87
|
+
gx cleanup
|
|
88
|
+
gx scan
|
|
89
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: guardex-merge-skills-to-dev
|
|
3
|
+
description: "Use when you need to merge SKILL.md updates from agent branches/worktrees into the local base branch (default: dev) with the multiagent-safety flow."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GuardeX Merge Skills to dev
|
|
7
|
+
|
|
8
|
+
Use this skill when you only want to promote Codex skill file updates into the base branch (normally `dev`) without editing the visible base checkout directly.
|
|
9
|
+
|
|
10
|
+
## What this merges
|
|
11
|
+
|
|
12
|
+
- `.codex/skills/**/SKILL.md`
|
|
13
|
+
- `templates/codex/skills/**/SKILL.md`
|
|
14
|
+
|
|
15
|
+
## Merge runbook (safe path)
|
|
16
|
+
|
|
17
|
+
1. Resolve the base branch:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
BASE_BRANCH="$(git config --get multiagent.baseBranch || echo dev)"
|
|
21
|
+
echo "$BASE_BRANCH"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. Start a dedicated integration sandbox from base:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bash scripts/agent-branch-start.sh "merge-skill-files-to-${BASE_BRANCH}" "skill-merge" "$BASE_BRANCH"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Enter the sandbox worktree printed by the command above.
|
|
31
|
+
|
|
32
|
+
4. Pull only skill files from each source agent branch:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
SOURCE_BRANCH="<agent-branch>"
|
|
36
|
+
git checkout "$SOURCE_BRANCH" -- ':(glob).codex/skills/**/SKILL.md' ':(glob)templates/codex/skills/**/SKILL.md'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
5. Verify scope before commit:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
git status --short
|
|
43
|
+
git diff --name-only
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
6. Commit and merge back to base using guardex finish flow:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
git add .codex/skills templates/codex/skills
|
|
50
|
+
git commit -m "Merge skill file updates into ${BASE_BRANCH}"
|
|
51
|
+
bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --base "$BASE_BRANCH" --via-pr --wait-for-merge --cleanup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
|
|
56
|
+
- If a source branch has non-skill changes, this runbook keeps them out of the merge.
|
|
57
|
+
- If merge conflicts occur, resolve only within the skill files, then rerun `agent-branch-finish.sh`.
|
|
58
|
+
- Do not commit directly on `dev`/`main`; always merge through an agent branch/worktree.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Code Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, reopened, synchronize]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
review:
|
|
13
|
+
if: ${{ secrets.OPENAI_API_KEY != '' }}
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: anc95/ChatGPT-CodeReview@main
|
|
17
|
+
env:
|
|
18
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
19
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
20
|
+
OPENAI_API_ENDPOINT: https://api.openai.com/v1
|
|
21
|
+
MODEL: gpt-4o-mini
|
|
@@ -193,6 +193,26 @@ hydrate_local_helper_in_worktree() {
|
|
|
193
193
|
echo "[agent-branch-start] Hydrated local helper in worktree: ${relative_path}"
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
hydrate_dependency_dir_symlink_in_worktree() {
|
|
197
|
+
local repo="$1"
|
|
198
|
+
local worktree="$2"
|
|
199
|
+
local relative_path="$3"
|
|
200
|
+
local source_path="${repo}/${relative_path}"
|
|
201
|
+
local target_path="${worktree}/${relative_path}"
|
|
202
|
+
|
|
203
|
+
if [[ ! -d "$source_path" ]]; then
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [[ -e "$target_path" ]]; then
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
mkdir -p "$(dirname "$target_path")"
|
|
212
|
+
ln -s "$source_path" "$target_path"
|
|
213
|
+
echo "[agent-branch-start] Linked dependency dir in worktree: ${relative_path}"
|
|
214
|
+
}
|
|
215
|
+
|
|
196
216
|
initialize_openspec_plan_workspace() {
|
|
197
217
|
local repo="$1"
|
|
198
218
|
local worktree="$2"
|
|
@@ -341,6 +361,9 @@ if [[ -n "$auto_transfer_stash_ref" ]]; then
|
|
|
341
361
|
fi
|
|
342
362
|
|
|
343
363
|
hydrate_local_helper_in_worktree "$repo_root" "$worktree_path" "scripts/codex-agent.sh"
|
|
364
|
+
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "node_modules"
|
|
365
|
+
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/frontend/node_modules"
|
|
366
|
+
hydrate_dependency_dir_symlink_in_worktree "$repo_root" "$worktree_path" "apps/backend/node_modules"
|
|
344
367
|
if ! initialize_openspec_plan_workspace "$repo_root" "$worktree_path" "$openspec_plan_slug"; then
|
|
345
368
|
exit 1
|
|
346
369
|
fi
|
|
@@ -9,6 +9,10 @@ DELETE_BRANCHES=0
|
|
|
9
9
|
DELETE_REMOTE_BRANCHES=0
|
|
10
10
|
ONLY_DIRTY_WORKTREES=0
|
|
11
11
|
TARGET_BRANCH=""
|
|
12
|
+
IDLE_MINUTES=0
|
|
13
|
+
NOW_EPOCH_RAW="${MUSAFETY_PRUNE_NOW_EPOCH:-}"
|
|
14
|
+
IDLE_SECONDS=0
|
|
15
|
+
NOW_EPOCH=0
|
|
12
16
|
|
|
13
17
|
if [[ -n "$BASE_BRANCH" ]]; then
|
|
14
18
|
BASE_BRANCH_EXPLICIT=1
|
|
@@ -45,9 +49,13 @@ while [[ $# -gt 0 ]]; do
|
|
|
45
49
|
TARGET_BRANCH="${2:-}"
|
|
46
50
|
shift 2
|
|
47
51
|
;;
|
|
52
|
+
--idle-minutes)
|
|
53
|
+
IDLE_MINUTES="${2:-}"
|
|
54
|
+
shift 2
|
|
55
|
+
;;
|
|
48
56
|
*)
|
|
49
57
|
echo "[agent-worktree-prune] Unknown argument: $1" >&2
|
|
50
|
-
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>]" >&2
|
|
58
|
+
echo "Usage: $0 [--base <branch>] [--dry-run] [--force-dirty] [--delete-branches] [--delete-remote-branches] [--only-dirty-worktrees] [--branch <agent/...>] [--idle-minutes <minutes>]" >&2
|
|
51
59
|
exit 1
|
|
52
60
|
;;
|
|
53
61
|
esac
|
|
@@ -103,6 +111,16 @@ if [[ -n "$TARGET_BRANCH" && "$TARGET_BRANCH" != agent/* ]]; then
|
|
|
103
111
|
exit 1
|
|
104
112
|
fi
|
|
105
113
|
|
|
114
|
+
if [[ ! "$IDLE_MINUTES" =~ ^[0-9]+$ ]]; then
|
|
115
|
+
echo "[agent-worktree-prune] --idle-minutes must be an integer >= 0." >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if [[ -n "$NOW_EPOCH_RAW" && ! "$NOW_EPOCH_RAW" =~ ^[0-9]+$ ]]; then
|
|
120
|
+
echo "[agent-worktree-prune] MUSAFETY_PRUNE_NOW_EPOCH must be a unix timestamp integer." >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
106
124
|
if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
|
|
107
125
|
BASE_BRANCH="$(resolve_base_branch)"
|
|
108
126
|
fi
|
|
@@ -117,6 +135,13 @@ if ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${BASE_BRANCH}";
|
|
|
117
135
|
exit 1
|
|
118
136
|
fi
|
|
119
137
|
|
|
138
|
+
IDLE_SECONDS=$((IDLE_MINUTES * 60))
|
|
139
|
+
if [[ -n "$NOW_EPOCH_RAW" ]]; then
|
|
140
|
+
NOW_EPOCH="$NOW_EPOCH_RAW"
|
|
141
|
+
else
|
|
142
|
+
NOW_EPOCH="$(date +%s)"
|
|
143
|
+
fi
|
|
144
|
+
|
|
120
145
|
run_cmd() {
|
|
121
146
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
122
147
|
echo "[agent-worktree-prune] [dry-run] $*"
|
|
@@ -167,6 +192,71 @@ select_unique_worktree_path() {
|
|
|
167
192
|
printf '%s' "$candidate"
|
|
168
193
|
}
|
|
169
194
|
|
|
195
|
+
read_branch_activity_epoch() {
|
|
196
|
+
local branch="$1"
|
|
197
|
+
local wt="${2:-}"
|
|
198
|
+
local activity_epoch=""
|
|
199
|
+
|
|
200
|
+
activity_epoch="$(
|
|
201
|
+
git -C "$repo_root" reflog show --format='%ct' -n 1 "refs/heads/${branch}" 2>/dev/null \
|
|
202
|
+
| head -n 1 \
|
|
203
|
+
| tr -d '[:space:]'
|
|
204
|
+
)"
|
|
205
|
+
if [[ -z "$activity_epoch" ]]; then
|
|
206
|
+
activity_epoch="$(
|
|
207
|
+
git -C "$repo_root" log -1 --format='%ct' "$branch" 2>/dev/null \
|
|
208
|
+
| head -n 1 \
|
|
209
|
+
| tr -d '[:space:]'
|
|
210
|
+
)"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if [[ -n "$wt" && -d "$wt" ]]; then
|
|
214
|
+
local lock_file="${wt}/.omx/state/agent-file-locks.json"
|
|
215
|
+
if [[ -f "$lock_file" ]]; then
|
|
216
|
+
local lock_mtime=""
|
|
217
|
+
lock_mtime="$(stat -c %Y "$lock_file" 2>/dev/null || stat -f %m "$lock_file" 2>/dev/null || true)"
|
|
218
|
+
if [[ "$lock_mtime" =~ ^[0-9]+$ ]]; then
|
|
219
|
+
if [[ -z "$activity_epoch" || "$lock_mtime" -gt "$activity_epoch" ]]; then
|
|
220
|
+
activity_epoch="$lock_mtime"
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
printf '%s' "$activity_epoch"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
skipped_recent=0
|
|
230
|
+
|
|
231
|
+
branch_idle_gate() {
|
|
232
|
+
local branch="$1"
|
|
233
|
+
local wt="$2"
|
|
234
|
+
local reason="$3"
|
|
235
|
+
if [[ "$IDLE_SECONDS" -le 0 ]]; then
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
if [[ -z "$branch" ]]; then
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
local last_activity_epoch=""
|
|
243
|
+
last_activity_epoch="$(read_branch_activity_epoch "$branch" "$wt")"
|
|
244
|
+
if [[ ! "$last_activity_epoch" =~ ^[0-9]+$ ]]; then
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
local idle_age=$((NOW_EPOCH - last_activity_epoch))
|
|
249
|
+
if [[ "$idle_age" -lt 0 ]]; then
|
|
250
|
+
idle_age=0
|
|
251
|
+
fi
|
|
252
|
+
if [[ "$idle_age" -lt "$IDLE_SECONDS" ]]; then
|
|
253
|
+
skipped_recent=$((skipped_recent + 1))
|
|
254
|
+
echo "[agent-worktree-prune] Skipping recent branch (${reason}): ${branch} (idle=${idle_age}s < ${IDLE_SECONDS}s)"
|
|
255
|
+
return 1
|
|
256
|
+
fi
|
|
257
|
+
return 0
|
|
258
|
+
}
|
|
259
|
+
|
|
170
260
|
relocated_foreign=0
|
|
171
261
|
skipped_foreign=0
|
|
172
262
|
|
|
@@ -273,6 +363,10 @@ process_entry() {
|
|
|
273
363
|
return
|
|
274
364
|
fi
|
|
275
365
|
|
|
366
|
+
if ! branch_idle_gate "$branch" "$wt" "$remove_reason"; then
|
|
367
|
+
return
|
|
368
|
+
fi
|
|
369
|
+
|
|
276
370
|
if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
|
|
277
371
|
skipped_dirty=$((skipped_dirty + 1))
|
|
278
372
|
echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
|
|
@@ -339,6 +433,9 @@ if [[ "$DELETE_BRANCHES" -eq 1 ]]; then
|
|
|
339
433
|
if branch_has_worktree "$branch"; then
|
|
340
434
|
continue
|
|
341
435
|
fi
|
|
436
|
+
if ! branch_idle_gate "$branch" "" "stale-merged-branch"; then
|
|
437
|
+
continue
|
|
438
|
+
fi
|
|
342
439
|
if git -C "$repo_root" merge-base --is-ancestor "$branch" "$BASE_BRANCH"; then
|
|
343
440
|
if run_cmd git -C "$repo_root" branch -d "$branch" >/dev/null 2>&1; then
|
|
344
441
|
removed_branches=$((removed_branches + 1))
|
|
@@ -356,7 +453,7 @@ fi
|
|
|
356
453
|
|
|
357
454
|
run_cmd git -C "$repo_root" worktree prune
|
|
358
455
|
|
|
359
|
-
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}"
|
|
456
|
+
echo "[agent-worktree-prune] Summary: base=${BASE_BRANCH}, idle_minutes=${IDLE_MINUTES}, removed_worktrees=${removed_worktrees}, removed_branches=${removed_branches}, skipped_active=${skipped_active}, skipped_dirty=${skipped_dirty}, skipped_recent=${skipped_recent}"
|
|
360
457
|
if [[ "$relocated_foreign" -gt 0 || "$skipped_foreign" -gt 0 ]]; then
|
|
361
458
|
echo "[agent-worktree-prune] Foreign routing: relocated=${relocated_foreign}, skipped=${skipped_foreign}"
|
|
362
459
|
fi
|
|
@@ -366,3 +463,6 @@ fi
|
|
|
366
463
|
if [[ "$skipped_dirty" -gt 0 ]]; then
|
|
367
464
|
echo "[agent-worktree-prune] Tip: dirty worktrees were preserved. Clean/finish them first, or pass --force-dirty to remove anyway." >&2
|
|
368
465
|
fi
|
|
466
|
+
if [[ "$IDLE_SECONDS" -gt 0 && "$skipped_recent" -gt 0 ]]; then
|
|
467
|
+
echo "[agent-worktree-prune] Tip: recent branches were preserved by --idle-minutes=${IDLE_MINUTES}. Re-run later or lower the threshold." >&2
|
|
468
|
+
fi
|