@imdeadpool/guardex 7.0.41 → 7.1.0
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 +94 -13
- package/package.json +3 -1
- package/skills/gitguardex/SKILL.md +13 -0
- package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
- package/skills/gx-act/SKILL.md +82 -0
- package/src/agents/cleanup-sessions.js +126 -0
- package/src/agents/finish.js +172 -0
- package/src/agents/inspect.js +202 -0
- package/src/agents/launch.js +249 -0
- package/src/agents/registry.js +133 -0
- package/src/agents/selection-panel.js +571 -0
- package/src/agents/sessions.js +151 -0
- package/src/agents/start.js +591 -0
- package/src/agents/status.js +146 -0
- package/src/agents/terminal.js +152 -0
- package/src/budget/index.js +344 -0
- package/src/ci-init/index.js +265 -0
- package/src/cli/args.js +357 -3
- package/src/cli/commands/agents.js +364 -0
- package/src/cli/commands/bootstrap.js +92 -0
- package/src/cli/commands/branch.js +127 -0
- package/src/cli/commands/claude.js +674 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/finish.js +26 -0
- package/src/cli/commands/mcp.js +122 -0
- package/src/cli/commands/misc.js +304 -0
- package/src/cli/commands/pr.js +439 -0
- package/src/cli/commands/prompt.js +92 -0
- package/src/cli/commands/release.js +305 -0
- package/src/cli/commands/report.js +244 -0
- package/src/cli/commands/review.js +32 -0
- package/src/cli/commands/setup.js +242 -0
- package/src/cli/commands/status.js +338 -0
- package/src/cli/commands/watch.js +234 -0
- package/src/cli/main.js +85 -3613
- package/src/cli/shared/repo-env.js +161 -0
- package/src/cli/shared/sandbox.js +417 -0
- package/src/cli/shared/scaffolding.js +535 -0
- package/src/cli/shared/toolchain-shims.js +420 -0
- package/src/cockpit/action-runner.js +3 -0
- package/src/cockpit/actions.js +80 -0
- package/src/cockpit/control.js +1121 -0
- package/src/cockpit/index.js +426 -0
- package/src/cockpit/kitty-layout.js +549 -0
- package/src/cockpit/kitty-tree.js +144 -0
- package/src/cockpit/logs-reader.js +182 -0
- package/src/cockpit/menu.js +204 -0
- package/src/cockpit/pane-actions.js +597 -0
- package/src/cockpit/pane-menu.js +387 -0
- package/src/cockpit/projects-finder.js +178 -0
- package/src/cockpit/render.js +215 -0
- package/src/cockpit/settings-render.js +128 -0
- package/src/cockpit/settings.js +124 -0
- package/src/cockpit/shortcuts.js +24 -0
- package/src/cockpit/sidebar.js +311 -0
- package/src/cockpit/state.js +72 -0
- package/src/cockpit/theme.js +128 -0
- package/src/cockpit/welcome.js +266 -0
- package/src/context.js +304 -43
- package/src/core/runtime.js +6 -1
- package/src/doctor/index.js +45 -15
- package/src/finish/index.js +186 -7
- package/src/finish/preflight.js +177 -0
- package/src/finish/review-gate.js +182 -0
- package/src/git/index.js +511 -4
- package/src/hooks/index.js +0 -64
- package/src/kitty/command.js +101 -0
- package/src/kitty/runtime.js +250 -0
- package/src/mcp/collect.js +370 -0
- package/src/mcp/server.js +157 -0
- package/src/output/index.js +68 -2
- package/src/pr-review.js +264 -0
- package/src/pr.js +381 -0
- package/src/sandbox/index.js +13 -2
- package/src/scaffold/agent-worktree-prep.js +213 -0
- package/src/scaffold/index.js +127 -10
- package/src/speckit/index.js +226 -0
- package/src/submodule/index.js +288 -0
- package/src/terminal/index.js +45 -0
- package/src/terminal/kitty.js +622 -0
- package/src/terminal/tmux.js +125 -0
- package/src/tmux/command.js +27 -0
- package/src/tmux/session.js +89 -0
- package/src/toolchain/index.js +20 -0
- package/templates/AGENTS.monorepo-apps.md +26 -0
- package/templates/AGENTS.multiagent-safety.md +63 -323
- package/templates/AGENTS.multiagent-safety.min.md +11 -0
- package/templates/codex/skills/gitguardex/SKILL.md +2 -0
- package/templates/codex/skills/gx-act/SKILL.md +82 -0
- package/templates/githooks/pre-commit +44 -20
- package/templates/github/workflows/README.md +87 -0
- package/templates/github/workflows/ci-full.yml +55 -0
- package/templates/github/workflows/ci.yml +56 -0
- package/templates/github/workflows/cr.yml +20 -1
- package/templates/scripts/agent-branch-finish.sh +519 -23
- package/templates/scripts/agent-branch-merge.sh +4 -1
- package/templates/scripts/agent-branch-start.sh +176 -24
- package/templates/scripts/agent-preflight.sh +115 -0
- package/templates/scripts/agent-worktree-prune.sh +96 -5
- package/templates/scripts/codex-agent.sh +41 -97
- package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
- package/templates/scripts/review-bot-watch.sh +31 -2
- package/templates/scripts/agent-session-state.js +0 -171
- package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
- package/templates/vscode/guardex-active-agents/README.md +0 -34
- package/templates/vscode/guardex-active-agents/extension.js +0 -3782
- package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
- package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
- package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
- package/templates/vscode/guardex-active-agents/package.json +0 -169
- package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
package/README.md
CHANGED
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
<a href="#01--install-in-one-line">Install</a> ·
|
|
25
25
|
<a href="#03--what-it-does">What it does</a> ·
|
|
26
26
|
<a href="#04--daily-workflow">Workflow</a> ·
|
|
27
|
-
<a href="#05--
|
|
28
|
-
<a href="#
|
|
29
|
-
<a href="#08--
|
|
30
|
-
<a href="#
|
|
27
|
+
<a href="#05--dmux-style-multi-agent-cockpit">Cockpit</a> ·
|
|
28
|
+
<a href="#06--what-gx-shows-first">gx status</a> ·
|
|
29
|
+
<a href="#08--commands">Commands</a> ·
|
|
30
|
+
<a href="#09--v6--v7-migration">Migration</a> ·
|
|
31
|
+
<a href="#11--companion-tools">Companions</a>
|
|
31
32
|
</p>
|
|
32
33
|
|
|
33
34
|
---
|
|
@@ -56,6 +57,12 @@ gx setup # hooks, state, OMX / OpenSpec / caveman wiring — one shot
|
|
|
56
57
|
> feels rough — especially around **cleanup**, **finish**, **merge**, or
|
|
57
58
|
> **recovery** flows — sorry. We're patching as we find things.
|
|
58
59
|
|
|
60
|
+
> [!CAUTION]
|
|
61
|
+
> **Recommended default: macOS or Linux with Codex CLI.** OMX is primarily
|
|
62
|
+
> designed and actively tuned for that path. Native Windows and Codex App are
|
|
63
|
+
> not the default experience, may break or behave inconsistently, and currently
|
|
64
|
+
> receive less support.
|
|
65
|
+
|
|
59
66
|
---
|
|
60
67
|
|
|
61
68
|
## The problem
|
|
@@ -135,7 +142,30 @@ gx branch finish --branch "$(git rev-parse --abbrev-ref HEAD)" \
|
|
|
135
142
|
|
|
136
143
|
---
|
|
137
144
|
|
|
138
|
-
## `05`
|
|
145
|
+
## `05` dmux-style multi-agent cockpit
|
|
146
|
+
|
|
147
|
+
GitGuardex now has a dmux-style cockpit for starting, inspecting, and
|
|
148
|
+
finishing isolated agent lanes from one terminal workspace. It is not a
|
|
149
|
+
dmux clone: GitGuardex keeps safety as the core and adds orchestration
|
|
150
|
+
on top of isolated worktrees, file locks, protected base branches, and
|
|
151
|
+
PR-only finish.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
gx cockpit
|
|
155
|
+
gx agents start "fix auth tests" --agent codex --base main --claim test/auth.test.js
|
|
156
|
+
gx agents start "fix auth tests" --panel --codex-accounts 3 --base main
|
|
157
|
+
gx agents start "update setup docs" --agent claude --base main --claim README.md
|
|
158
|
+
gx agents status
|
|
159
|
+
gx agents files --branch agent/codex/fix-auth-tests-2026-04-29-21-30
|
|
160
|
+
gx agents diff --branch agent/claude/update-setup-docs-2026-04-29-21-31
|
|
161
|
+
gx agents finish --branch agent/codex/fix-auth-tests-2026-04-29-21-30
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Long-form guide: [docs/agents-cockpit.md](./docs/agents-cockpit.md).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## `06` What `gx` shows first
|
|
139
169
|
|
|
140
170
|
Before you branch, repair, or start agents, run plain `gx`. It gives you
|
|
141
171
|
a one-screen status for the CLI, global helpers, repo safety service,
|
|
@@ -170,7 +200,7 @@ the compact layout everywhere.
|
|
|
170
200
|
|
|
171
201
|
---
|
|
172
202
|
|
|
173
|
-
## `
|
|
203
|
+
## `07` How `AGENTS.md` is handled
|
|
174
204
|
|
|
175
205
|
> [!IMPORTANT]
|
|
176
206
|
> **GitGuardex never overwrites your guidance.** Only content between
|
|
@@ -182,12 +212,13 @@ the compact layout everywhere.
|
|
|
182
212
|
| --- | --- |
|
|
183
213
|
| `AGENTS.md` **with** markers | Refreshes **only** the managed block. |
|
|
184
214
|
| `AGENTS.md` **without** markers | Appends the managed block to the end. |
|
|
185
|
-
| No `AGENTS.md` | Creates it with the managed block. |
|
|
215
|
+
| No `AGENTS.md` | Creates it with the managed block, then links `CLAUDE.md` to it. |
|
|
216
|
+
| No root `CLAUDE.md` | Creates a `CLAUDE.md` symlink to `AGENTS.md`. |
|
|
186
217
|
| A root `CLAUDE.md` | Leaves it alone. |
|
|
187
218
|
|
|
188
219
|
---
|
|
189
220
|
|
|
190
|
-
## `
|
|
221
|
+
## `08` Commands
|
|
191
222
|
|
|
192
223
|
### Core
|
|
193
224
|
|
|
@@ -209,6 +240,18 @@ the compact layout everywhere.
|
|
|
209
240
|
| `gx sync` | Sync current agent branch against base. |
|
|
210
241
|
| `gx release` | Update the GitHub release from README notes. |
|
|
211
242
|
|
|
243
|
+
### Multi-agent cockpit
|
|
244
|
+
|
|
245
|
+
| command | does |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `gx cockpit` | Create or attach to a repo tmux cockpit session with a status pane. |
|
|
248
|
+
| `gx agents start "<task>" --agent codex` | Start an isolated Codex lane for a task. |
|
|
249
|
+
| `gx agents start "<task>" --agent claude` | Start an isolated Claude Code lane for a task. |
|
|
250
|
+
| `gx agents status` | Show repo agent service status. |
|
|
251
|
+
| `gx agents files --branch <agent/...>` | List files changed by one agent lane. |
|
|
252
|
+
| `gx agents diff --branch <agent/...>` | Show the diff for one agent lane. |
|
|
253
|
+
| `gx agents finish --branch <agent/...>` | Finish one agent session through the existing PR flow. |
|
|
254
|
+
|
|
212
255
|
```bash
|
|
213
256
|
gx release # create/update the current GitHub release from README notes
|
|
214
257
|
```
|
|
@@ -227,7 +270,7 @@ gx protect reset # back to: dev · main · master
|
|
|
227
270
|
|
|
228
271
|
---
|
|
229
272
|
|
|
230
|
-
## `
|
|
273
|
+
## `09` v6 → v7 migration
|
|
231
274
|
|
|
232
275
|
Five commands were consolidated into flags. Old names still work and
|
|
233
276
|
print a deprecation notice; they'll be removed in v8.
|
|
@@ -245,7 +288,7 @@ print a deprecation notice; they'll be removed in v8.
|
|
|
245
288
|
|
|
246
289
|
---
|
|
247
290
|
|
|
248
|
-
## `
|
|
291
|
+
## `10` Known rough edges
|
|
249
292
|
|
|
250
293
|
Being honest about where this still has issues:
|
|
251
294
|
|
|
@@ -265,6 +308,44 @@ Being honest about where this still has issues:
|
|
|
265
308
|
<details open>
|
|
266
309
|
<summary><strong>v7.x</strong></summary>
|
|
267
310
|
|
|
311
|
+
### v7.0.43
|
|
312
|
+
- Budget-friendly CI defaults for gitguardex-managed projects: live
|
|
313
|
+
workflows drop `push: main`, gate per-PR jobs on `pull_request.draft
|
|
314
|
+
== false`, add `concurrency: cancel-in-progress`, and split per-runtime
|
|
315
|
+
matrix coverage into a weekly `ci-full.yml`. CodeQL and Scorecard run
|
|
316
|
+
on the weekly schedule + `workflow_dispatch` only. Templates under
|
|
317
|
+
`templates/github/workflows/` carry the same posture so downstream
|
|
318
|
+
projects inherit it via `gx setup`.
|
|
319
|
+
- New `gx ci-init` subcommand scaffolds `ci.yml`, `ci-full.yml`, `cr.yml`,
|
|
320
|
+
and a `README.md` budget-posture guide into a target repo's
|
|
321
|
+
`.github/workflows/` directory. Supports `--target`, `--dry-run`,
|
|
322
|
+
`--force`, `--no-stage`, and `--json`.
|
|
323
|
+
- New `gx budget` subcommand wraps the new GitHub
|
|
324
|
+
`/settings/billing/usage` endpoint (the legacy
|
|
325
|
+
`/settings/billing/actions` endpoint was retired in early 2026) and
|
|
326
|
+
reports monthly Actions minute spend with warn/critical USD thresholds
|
|
327
|
+
per `--org` or `--user`.
|
|
328
|
+
- Per-PR label opt-in for `agent/*` lanes: `needs-review` runs AI code
|
|
329
|
+
review on an otherwise-skipped agent PR; `needs-ci-full` triggers the
|
|
330
|
+
full cross-runtime matrix without waiting for the weekly schedule.
|
|
331
|
+
- `gx branch finish` runs `scripts/agent-preflight.sh` in the worktree
|
|
332
|
+
before pushing. Default script auto-detects pnpm/npm, Rust, and Python
|
|
333
|
+
stacks and refuses the push on verification failure. After pre-flight
|
|
334
|
+
passes, draft PRs are promoted to ready-for-review so the
|
|
335
|
+
budget-friendly CI defaults fire once on a known-passing commit.
|
|
336
|
+
|
|
337
|
+
### v7.0.42
|
|
338
|
+
- Bumped `@imdeadpool/guardex` from `7.0.41` to `7.0.42` so the current
|
|
339
|
+
`main` payload can publish under a fresh npm version after `7.0.41` reached
|
|
340
|
+
the registry.
|
|
341
|
+
- Improves the agent-session and cockpit workflow: `gx agents start/status`
|
|
342
|
+
now share canonical session storage, agent lanes can be previewed, claimed,
|
|
343
|
+
finished by session, and launched through safer supported-agent metadata,
|
|
344
|
+
and cockpit can render live session state through tmux-backed panes.
|
|
345
|
+
- Hardens setup and status hygiene by ignoring local `.codex/` state during
|
|
346
|
+
setup/doctor, avoiding generic OpenSpec dirty-worktree reuse, and pruning
|
|
347
|
+
stale agent sessions from user-facing status surfaces.
|
|
348
|
+
|
|
268
349
|
### v7.0.41
|
|
269
350
|
- Bumped `@imdeadpool/guardex` from `7.0.40` to `7.0.41` so the current
|
|
270
351
|
`main` payload can publish under a fresh npm version after the `v7.0.40`
|
|
@@ -316,13 +397,13 @@ Being honest about where this still has issues:
|
|
|
316
397
|
|
|
317
398
|
---
|
|
318
399
|
|
|
319
|
-
## `
|
|
400
|
+
## `11` Companion tools
|
|
320
401
|
|
|
321
402
|
All optional — but if you're running many agents, you probably want them.
|
|
322
403
|
`gx status` auto-detects each one and reports it in the `Global services`
|
|
323
404
|
block.
|
|
324
405
|
|
|
325
|
-
Install repo skills with `npx skills add recodee/gitguardex`; `npx skills add recodee/` opens the recodee namespace. `gx setup` does not auto-run `npx skills add ...`. If the picker does not show a separate `guardex` skill, that is expected.
|
|
406
|
+
Install repo skills with `npx skills add recodee/gitguardex`; the npm package also ships the root `skills/` catalog so the npx installer can read the GitGuardex skill from the published tarball. `npx skills add recodee/` opens the recodee namespace. `gx setup` does not auto-run `npx skills add ...`. If the picker does not show a separate `guardex` skill, that is expected.
|
|
326
407
|
|
|
327
408
|
| Tool | What it does | Stars |
|
|
328
409
|
| --- | --- | --- |
|
|
@@ -333,7 +414,7 @@ Install repo skills with `npx skills add recodee/gitguardex`; `npx skills add re
|
|
|
333
414
|
| [**cavekit**](https://github.com/JuliusBrussee/cavekit) — `npx skills add JuliusBrussee/cavekit` | Spec-driven build loop with `spec`, `build`, `check`, `caveman`, `backprop` skills bundled in. | [](https://github.com/JuliusBrussee/cavekit) |
|
|
334
415
|
| [**caveman**](https://github.com/JuliusBrussee/caveman) — `npx skills add JuliusBrussee/caveman` | Ultra-compressed response mode for Claude / Codex. Less output-token churn on long reviews and debug loops. | [](https://github.com/JuliusBrussee/caveman) |
|
|
335
416
|
| [**codex-account-switcher**](https://github.com/recodeecom/codex-account-switcher-cli) — `npm i -g @imdeadpool/codex-account-switcher` | Multi-identity Codex account switcher. Auto-registers accounts on `codex login`; switch with one command. | [](https://github.com/recodeecom/codex-account-switcher-cli) |
|
|
336
|
-
| [**GitHub CLI (`gh`)**](https://github.com/cli/cli) — see [cli.github.com](https://cli.github.com/) | Required for PR / merge automation. `gx branch finish --via-pr --wait-for-merge` depends on it. | [](https://github.com/cli/cli) |
|
|
417
|
+
| [**GitHub CLI (`gh`)**](https://github.com/cli/cli) — see [cli.github.com](https://cli.github.com/) | Required for PR / merge automation. `gx branch finish --via-pr --wait-for-merge` depends on it. If `ghx` is on `PATH`, Guardex uses that GitHub CLI cache proxy automatically; set `GUARDEX_GH_BIN=gh` to force direct `gh`. | [](https://github.com/cli/cli) |
|
|
337
418
|
|
|
338
419
|
---
|
|
339
420
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/guardex",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Guardian T-Rex for your multi-agent repo. Isolated worktrees, file locks, and PR-only merges stop parallel Codex & Claude agents from overwriting each other's work. Auto-wires Oh My Codex, Oh My Claude, OpenSpec, and Caveman.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"preferGlobal": true,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"agent:branch:merge": "bash ./scripts/agent-branch-merge.sh",
|
|
19
19
|
"agent:cleanup": "gx cleanup",
|
|
20
20
|
"agent:hooks:install": "bash ./scripts/install-agent-git-hooks.sh",
|
|
21
|
+
"guardex:install-global": "bash ./scripts/install-global-hooks.sh",
|
|
21
22
|
"agent:locks:claim": "python3 ./scripts/agent-file-locks.py claim",
|
|
22
23
|
"agent:locks:allow-delete": "python3 ./scripts/agent-file-locks.py allow-delete",
|
|
23
24
|
"agent:locks:release": "python3 ./scripts/agent-file-locks.py release",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"files": [
|
|
40
41
|
"bin",
|
|
41
42
|
"src",
|
|
43
|
+
"skills",
|
|
42
44
|
"templates",
|
|
43
45
|
"README.md",
|
|
44
46
|
"LICENSE",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gitguardex
|
|
3
|
+
description: "Repo guardrail check and repair."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use when repo safety may be broken.
|
|
7
|
+
|
|
8
|
+
`gx status` -> `gx doctor` -> `gx status --strict`
|
|
9
|
+
|
|
10
|
+
Bootstrap: `gx setup`
|
|
11
|
+
Ops: `gx branch start "<task>" "<agent>"`, `gx locks claim --branch "<agent-branch>" <file...>`, `gx branch finish --branch "<agent-branch>" --base <base> --via-pr --wait-for-merge --cleanup`, `gx finish --all`, `gx cleanup`
|
|
12
|
+
|
|
13
|
+
When inspecting or verifying, prefer `rtk` compact wrappers if available (`rtk git status`, `rtk grep`, `rtk test <cmd>`). Do not wrap commands whose stdout is parsed by scripts.
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
- `skills/**/SKILL.md`
|
|
13
|
+
- `.codex/skills/**/SKILL.md`
|
|
14
|
+
- `templates/codex/skills/**/SKILL.md`
|
|
15
|
+
|
|
16
|
+
## Merge runbook (safe path)
|
|
17
|
+
|
|
18
|
+
1. Resolve the base branch:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
BASE_BRANCH="$(git config --get multiagent.baseBranch || echo dev)"
|
|
22
|
+
echo "$BASE_BRANCH"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. Start a dedicated integration sandbox from base:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
gx branch start "merge-skill-files-to-${BASE_BRANCH}" "skill-merge" "$BASE_BRANCH"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. Enter the sandbox worktree printed by the command above.
|
|
32
|
+
|
|
33
|
+
4. Pull only skill files from each source agent branch:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
SOURCE_BRANCH="<agent-branch>"
|
|
37
|
+
git checkout "$SOURCE_BRANCH" -- ':(glob)skills/**/SKILL.md' ':(glob).codex/skills/**/SKILL.md' ':(glob)templates/codex/skills/**/SKILL.md'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
5. Verify scope before commit:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
git status --short
|
|
44
|
+
git diff --name-only
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
6. Commit and merge back to base using guardex finish flow:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
git add skills .codex/skills templates/codex/skills
|
|
51
|
+
git commit -m "Merge skill file updates into ${BASE_BRANCH}"
|
|
52
|
+
gx branch finish --branch "$(git rev-parse --abbrev-ref HEAD)" --base "$BASE_BRANCH" --via-pr --wait-for-merge --cleanup
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- If a source branch has non-skill changes, this runbook keeps them out of the merge.
|
|
58
|
+
- If merge conflicts occur, resolve only within the skill files, then rerun `gx branch finish`.
|
|
59
|
+
- Do not commit directly on `dev`/`main`; always merge through an agent branch/worktree.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gx-act
|
|
3
|
+
description: "Run GitHub Actions workflows locally with nektos/act before pushing, so CI failures are caught on the laptop and the PR can be squash-merged on the first remote run."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gx-act — local GitHub Actions
|
|
7
|
+
|
|
8
|
+
Use whenever a change touches code that would trigger CI on GitHub. Run the workflows locally with `act` first; only push the branch when the local run is green, then squash-merge the PR on GitHub.
|
|
9
|
+
|
|
10
|
+
## When to invoke
|
|
11
|
+
|
|
12
|
+
- Before `gx pr open` / `gx pr sync` / `gx branch finish --via-pr`.
|
|
13
|
+
- Before re-pushing after a CI failure.
|
|
14
|
+
- When iterating on `.github/workflows/*.yml` itself.
|
|
15
|
+
|
|
16
|
+
## Install `act`
|
|
17
|
+
|
|
18
|
+
`act` requires Docker (or Podman). Check the binary:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
command -v act || echo "act not installed"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install one way:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Linux/macOS via the upstream installer
|
|
28
|
+
curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -s -- -b "$HOME/.local/bin"
|
|
29
|
+
|
|
30
|
+
# macOS via Homebrew
|
|
31
|
+
brew install act
|
|
32
|
+
|
|
33
|
+
# Arch
|
|
34
|
+
sudo pacman -S act
|
|
35
|
+
|
|
36
|
+
# Or use the GitHub CLI extension
|
|
37
|
+
gh extension install https://github.com/nektos/gh-act
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Upstream: https://github.com/nektos/act
|
|
41
|
+
|
|
42
|
+
## Quick commands
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
# List jobs the local runner would execute for the push event
|
|
46
|
+
act -l
|
|
47
|
+
|
|
48
|
+
# Run the default push workflows (what GitHub runs on a normal push)
|
|
49
|
+
act push
|
|
50
|
+
|
|
51
|
+
# Run a specific event
|
|
52
|
+
act pull_request
|
|
53
|
+
act workflow_dispatch -W .github/workflows/release.yml
|
|
54
|
+
|
|
55
|
+
# Run a single job
|
|
56
|
+
act -j test
|
|
57
|
+
|
|
58
|
+
# Pin a runner image (medium is the act default; large matches real GH closer)
|
|
59
|
+
act -P ubuntu-latest=catthehacker/ubuntu:act-latest
|
|
60
|
+
|
|
61
|
+
# Pass secrets / env without committing them
|
|
62
|
+
act -s GITHUB_TOKEN="$GITHUB_TOKEN" --env-file .env.act
|
|
63
|
+
|
|
64
|
+
# Reuse containers between runs (faster iteration)
|
|
65
|
+
act --reuse
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Workflow (local CI → squash-merge on GitHub)
|
|
69
|
+
|
|
70
|
+
1. Implement the change in the agent worktree.
|
|
71
|
+
2. `act -l` to confirm which jobs will fire for the event you care about.
|
|
72
|
+
3. `act push` (or the specific event/job) until it is green locally.
|
|
73
|
+
4. `gx branch finish --branch "<agent-branch>" --base main --via-pr --wait-for-merge --cleanup`.
|
|
74
|
+
- Or `gx pr open` then `gx pr sync --auto-merge --merge-strategy squash` for explicit PR control.
|
|
75
|
+
5. On GitHub: squash-merge once the remote run mirrors the local one.
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- `act` does not reproduce GitHub-hosted services exactly (no real secrets, different runner image, no concurrency groups). Treat a green `act` run as a strong signal, not a proof — the remote run is still authoritative.
|
|
80
|
+
- Keep `act` config in `.actrc` at the repo root so every agent uses the same runner image.
|
|
81
|
+
- If a workflow uses `GITHUB_TOKEN` for API calls, pass a PAT via `-s GITHUB_TOKEN=...`; do not commit it.
|
|
82
|
+
- Add `.actrc`, `.cache/act`, and any `act`-specific event payloads to `.gitignore` if they appear.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
listAgentSessions,
|
|
5
|
+
removeAgentSession,
|
|
6
|
+
} = require('./sessions');
|
|
7
|
+
const { branchExists: defaultBranchExists } = require('../git');
|
|
8
|
+
const { TOOL_NAME } = require('../context');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_STALE_AGE_MINUTES = 24 * 60;
|
|
11
|
+
const TERMINAL_STATUSES = new Set(['finished', 'pr-opened', 'failed']);
|
|
12
|
+
|
|
13
|
+
function parseTimestamp(value) {
|
|
14
|
+
if (!value) return null;
|
|
15
|
+
const timestamp = Date.parse(value);
|
|
16
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sessionAgeMinutes(session, nowMs) {
|
|
20
|
+
const timestamp = parseTimestamp(session.updatedAt) ?? parseTimestamp(session.createdAt);
|
|
21
|
+
if (timestamp === null) return null;
|
|
22
|
+
return Math.max(0, Math.floor((nowMs - timestamp) / 60000));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function evaluateSession(session, repoRoot, options) {
|
|
26
|
+
const reasons = [];
|
|
27
|
+
const worktreePath = session.worktreePath || '';
|
|
28
|
+
const branch = session.branch || '';
|
|
29
|
+
|
|
30
|
+
if (worktreePath && !options.existsSync(worktreePath)) {
|
|
31
|
+
reasons.push('missing-worktree');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (branch && !options.branchExists(repoRoot, branch)) {
|
|
35
|
+
reasons.push('missing-branch');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const status = session.status || '';
|
|
39
|
+
const ageMinutes = sessionAgeMinutes(session, options.nowMs);
|
|
40
|
+
if (
|
|
41
|
+
TERMINAL_STATUSES.has(status)
|
|
42
|
+
&& ageMinutes !== null
|
|
43
|
+
&& ageMinutes >= options.staleAgeMinutes
|
|
44
|
+
) {
|
|
45
|
+
reasons.push('terminal-status-old');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...session,
|
|
50
|
+
ageMinutes,
|
|
51
|
+
reasons,
|
|
52
|
+
stale: reasons.length > 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cleanupAgentSessions(repoRoot, rawOptions = {}) {
|
|
57
|
+
const options = {
|
|
58
|
+
dryRun: Boolean(rawOptions.dryRun),
|
|
59
|
+
staleAgeMinutes: rawOptions.staleAgeMinutes ?? DEFAULT_STALE_AGE_MINUTES,
|
|
60
|
+
nowMs: rawOptions.nowMs ?? Date.now(),
|
|
61
|
+
existsSync: rawOptions.existsSync || fs.existsSync,
|
|
62
|
+
branchExists: rawOptions.branchExists || defaultBranchExists,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const sessions = listAgentSessions(repoRoot);
|
|
66
|
+
const candidates = sessions
|
|
67
|
+
.map((session) => evaluateSession(session, repoRoot, options))
|
|
68
|
+
.filter((session) => session.stale);
|
|
69
|
+
|
|
70
|
+
const removed = [];
|
|
71
|
+
if (!options.dryRun) {
|
|
72
|
+
for (const session of candidates) {
|
|
73
|
+
if (removeAgentSession(repoRoot, session.id)) {
|
|
74
|
+
removed.push(session.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
schemaVersion: 1,
|
|
81
|
+
repoRoot,
|
|
82
|
+
dryRun: options.dryRun,
|
|
83
|
+
staleAgeMinutes: options.staleAgeMinutes,
|
|
84
|
+
inspected: sessions.length,
|
|
85
|
+
candidates,
|
|
86
|
+
removed,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatSessionLine(session, verb) {
|
|
91
|
+
const reasonText = session.reasons.join(',');
|
|
92
|
+
return `- ${verb} ${session.id} status=${session.status || '-'} branch=${session.branch || '-'} ` +
|
|
93
|
+
`worktree=${session.worktreePath || '-'} reasons=${reasonText}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderCleanupSessionsResult(result, options = {}) {
|
|
97
|
+
if (options.json) return `${JSON.stringify(result, null, 2)}\n`;
|
|
98
|
+
|
|
99
|
+
const action = result.dryRun ? 'would remove' : 'removed';
|
|
100
|
+
const lines = [
|
|
101
|
+
`[${TOOL_NAME}] Agent session cleanup: ${action} ${result.dryRun ? result.candidates.length : result.removed.length} ` +
|
|
102
|
+
`of ${result.inspected} (${result.repoRoot})`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (result.candidates.length === 0) {
|
|
106
|
+
lines.push('- no stale session metadata found');
|
|
107
|
+
} else {
|
|
108
|
+
for (const session of result.candidates) {
|
|
109
|
+
lines.push(formatSessionLine(session, action));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `${lines.join('\n')}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runCleanupSessionsCommand(repoRoot, options = {}) {
|
|
117
|
+
return renderCleanupSessionsResult(cleanupAgentSessions(repoRoot, options), options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
DEFAULT_STALE_AGE_MINUTES,
|
|
122
|
+
TERMINAL_STATUSES,
|
|
123
|
+
cleanupAgentSessions,
|
|
124
|
+
renderCleanupSessionsResult,
|
|
125
|
+
runCleanupSessionsCommand,
|
|
126
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const { TOOL_NAME } = require('../context');
|
|
2
|
+
const finishCommands = require('../finish');
|
|
3
|
+
const {
|
|
4
|
+
readAgentSession,
|
|
5
|
+
updateAgentSession,
|
|
6
|
+
listAgentSessions,
|
|
7
|
+
} = require('./sessions');
|
|
8
|
+
|
|
9
|
+
function resolveSessionByBranch(repoRoot, branch) {
|
|
10
|
+
const matches = listAgentSessions(repoRoot).filter((session) => session.branch === branch);
|
|
11
|
+
if (matches.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (matches.length > 1) {
|
|
15
|
+
throw new Error(`Multiple agent sessions found for branch: ${branch}`);
|
|
16
|
+
}
|
|
17
|
+
return matches[0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveAgentSessionForFinish(repoRoot, options) {
|
|
21
|
+
if (options.sessionId) {
|
|
22
|
+
const session = readAgentSession(repoRoot, options.sessionId);
|
|
23
|
+
if (!session) {
|
|
24
|
+
throw new Error(`Agent session not found: ${options.sessionId}`);
|
|
25
|
+
}
|
|
26
|
+
return session;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.branch) {
|
|
30
|
+
const session = resolveSessionByBranch(repoRoot, options.branch);
|
|
31
|
+
if (!session) {
|
|
32
|
+
throw new Error(`Agent session not found for branch: ${options.branch}`);
|
|
33
|
+
}
|
|
34
|
+
return session;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new Error('agents finish requires --session <id> or --branch <agent/...>');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sessionStatusAfterFinish(finishArgs) {
|
|
41
|
+
const modeIndex = finishArgs.indexOf('--mode');
|
|
42
|
+
const directMode = finishArgs.includes('--direct-only') || finishArgs[modeIndex + 1] === 'direct';
|
|
43
|
+
return finishArgs.includes('--no-wait-for-merge') && !directMode ? 'pr-opened' : 'finished';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanupResultAfterFinish(finishArgs, status) {
|
|
47
|
+
if (status === 'failed') return 'failed';
|
|
48
|
+
if (finishArgs.includes('--no-cleanup')) return 'skipped';
|
|
49
|
+
if (finishArgs.includes('--cleanup')) return status === 'finished' ? 'completed' : 'pending';
|
|
50
|
+
return 'unknown';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function firstMatch(text, patterns) {
|
|
54
|
+
for (const pattern of patterns) {
|
|
55
|
+
const match = text.match(pattern);
|
|
56
|
+
if (match && match[1]) return match[1].trim();
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function finishOutputText(result, captured = {}) {
|
|
62
|
+
return [
|
|
63
|
+
captured.stdout,
|
|
64
|
+
captured.stderr,
|
|
65
|
+
result?.stdout,
|
|
66
|
+
result?.stderr,
|
|
67
|
+
].map((value) => String(value || '')).join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildFinishEvidence(session, finishArgs, status, result, captured = {}) {
|
|
71
|
+
const outputText = finishOutputText(result, captured);
|
|
72
|
+
const prUrl = firstMatch(outputText, [
|
|
73
|
+
/\[agent-branch-finish\] (?:Merged PR|PR):\s+(https?:\/\/\S+)/,
|
|
74
|
+
/\b(https?:\/\/\S+\/pull\/\d+)\b/,
|
|
75
|
+
]);
|
|
76
|
+
const mergeState = status === 'finished' ? 'MERGED' : status === 'pr-opened' ? 'OPEN' : status.toUpperCase();
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
sessionId: session.id || '',
|
|
80
|
+
branch: session.branch || '',
|
|
81
|
+
prUrl,
|
|
82
|
+
mergeState,
|
|
83
|
+
cleanupResult: cleanupResultAfterFinish(finishArgs, status),
|
|
84
|
+
status,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function captureProcessOutput(fn) {
|
|
89
|
+
let stdout = '';
|
|
90
|
+
let stderr = '';
|
|
91
|
+
const originalStdoutWrite = process.stdout.write;
|
|
92
|
+
const originalStderrWrite = process.stderr.write;
|
|
93
|
+
process.stdout.write = function captureStdout(chunk, encoding, callback) {
|
|
94
|
+
stdout += Buffer.isBuffer(chunk) ? chunk.toString(encoding || 'utf8') : String(chunk || '');
|
|
95
|
+
if (typeof encoding === 'function') encoding();
|
|
96
|
+
if (typeof callback === 'function') callback();
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
99
|
+
process.stderr.write = function captureStderr(chunk, encoding, callback) {
|
|
100
|
+
stderr += Buffer.isBuffer(chunk) ? chunk.toString(encoding || 'utf8') : String(chunk || '');
|
|
101
|
+
if (typeof encoding === 'function') encoding();
|
|
102
|
+
if (typeof callback === 'function') callback();
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
return { result: fn(), captured: { stdout, stderr } };
|
|
107
|
+
} finally {
|
|
108
|
+
process.stdout.write = originalStdoutWrite;
|
|
109
|
+
process.stderr.write = originalStderrWrite;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function finishAgentSession(repoRoot, options, deps = {}) {
|
|
114
|
+
const finishRunner = deps.finishRunner || finishCommands.finish;
|
|
115
|
+
const output = deps.output || process.stdout;
|
|
116
|
+
const session = resolveAgentSessionForFinish(repoRoot, options);
|
|
117
|
+
const jsonMode = Boolean(options.json);
|
|
118
|
+
|
|
119
|
+
if (!session.branch) {
|
|
120
|
+
throw new Error(`Agent session '${session.id}' has no branch metadata.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
updateAgentSession(repoRoot, session.id, { status: 'finishing' });
|
|
124
|
+
|
|
125
|
+
const finishArgs = [
|
|
126
|
+
'--target',
|
|
127
|
+
repoRoot,
|
|
128
|
+
'--branch',
|
|
129
|
+
session.branch,
|
|
130
|
+
...options.finishArgs,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
if (!jsonMode) {
|
|
134
|
+
output.write(`[${TOOL_NAME}] Agent session: ${session.id}\n`);
|
|
135
|
+
output.write(`[${TOOL_NAME}] Branch: ${session.branch}\n`);
|
|
136
|
+
output.write(`[${TOOL_NAME}] Worktree: ${session.worktreePath || '(unknown)'}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const runnerResult = jsonMode
|
|
141
|
+
? captureProcessOutput(() => finishRunner(finishArgs))
|
|
142
|
+
: { result: finishRunner(finishArgs), captured: {} };
|
|
143
|
+
const result = runnerResult.result;
|
|
144
|
+
const status = sessionStatusAfterFinish(finishArgs);
|
|
145
|
+
const evidence = buildFinishEvidence(session, finishArgs, status, result, runnerResult.captured);
|
|
146
|
+
updateAgentSession(repoRoot, session.id, {
|
|
147
|
+
status,
|
|
148
|
+
pr: { url: evidence.prUrl, state: evidence.mergeState },
|
|
149
|
+
finishEvidence: evidence,
|
|
150
|
+
});
|
|
151
|
+
if (!jsonMode) {
|
|
152
|
+
output.write(`[${TOOL_NAME}] Finish result: ${status}\n`);
|
|
153
|
+
}
|
|
154
|
+
return { session, status, result, finishArgs, evidence };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const evidence = buildFinishEvidence(session, finishArgs, 'failed', null);
|
|
157
|
+
updateAgentSession(repoRoot, session.id, {
|
|
158
|
+
status: 'failed',
|
|
159
|
+
finishEvidence: evidence,
|
|
160
|
+
});
|
|
161
|
+
if (!jsonMode) {
|
|
162
|
+
output.write(`[${TOOL_NAME}] Finish result: failed\n`);
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
buildFinishEvidence,
|
|
170
|
+
finishAgentSession,
|
|
171
|
+
resolveAgentSessionForFinish,
|
|
172
|
+
};
|