@imdeadpool/guardex 7.0.43 → 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.
Files changed (63) hide show
  1. package/README.md +26 -0
  2. package/package.json +2 -1
  3. package/skills/gx-act/SKILL.md +82 -0
  4. package/src/agents/inspect.js +17 -4
  5. package/src/agents/launch.js +10 -1
  6. package/src/agents/status.js +9 -6
  7. package/src/budget/index.js +2 -1
  8. package/src/cli/args.js +52 -2
  9. package/src/cli/commands/agents.js +364 -0
  10. package/src/cli/commands/bootstrap.js +92 -0
  11. package/src/cli/commands/branch.js +127 -0
  12. package/src/cli/commands/claude.js +674 -0
  13. package/src/cli/commands/doctor.js +268 -0
  14. package/src/cli/commands/finish.js +26 -0
  15. package/src/cli/commands/mcp.js +122 -0
  16. package/src/cli/commands/misc.js +304 -0
  17. package/src/cli/commands/pr.js +439 -0
  18. package/src/cli/commands/prompt.js +92 -0
  19. package/src/cli/commands/release.js +305 -0
  20. package/src/cli/commands/report.js +244 -0
  21. package/src/cli/commands/review.js +32 -0
  22. package/src/cli/commands/setup.js +242 -0
  23. package/src/cli/commands/status.js +338 -0
  24. package/src/cli/commands/watch.js +234 -0
  25. package/src/cli/main.js +68 -3726
  26. package/src/cli/shared/repo-env.js +161 -0
  27. package/src/cli/shared/sandbox.js +417 -0
  28. package/src/cli/shared/scaffolding.js +535 -0
  29. package/src/cli/shared/toolchain-shims.js +420 -0
  30. package/src/context.js +229 -11
  31. package/src/core/runtime.js +6 -1
  32. package/src/doctor/index.js +42 -13
  33. package/src/finish/index.js +147 -5
  34. package/src/finish/preflight.js +177 -0
  35. package/src/finish/review-gate.js +182 -0
  36. package/src/git/index.js +446 -4
  37. package/src/hooks/index.js +0 -64
  38. package/src/mcp/collect.js +370 -0
  39. package/src/mcp/server.js +157 -0
  40. package/src/output/index.js +67 -1
  41. package/src/pr-review.js +23 -0
  42. package/src/pr.js +381 -0
  43. package/src/sandbox/index.js +13 -2
  44. package/src/scaffold/agent-worktree-prep.js +213 -0
  45. package/src/scaffold/index.js +108 -10
  46. package/src/speckit/index.js +226 -0
  47. package/src/terminal/index.js +1 -76
  48. package/src/terminal/tmux.js +0 -1
  49. package/src/toolchain/index.js +20 -0
  50. package/templates/AGENTS.monorepo-apps.md +26 -0
  51. package/templates/AGENTS.multiagent-safety.md +61 -347
  52. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  53. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  54. package/templates/githooks/pre-commit +22 -19
  55. package/templates/scripts/agent-branch-finish.sh +8 -30
  56. package/templates/scripts/agent-branch-merge.sh +4 -1
  57. package/templates/scripts/agent-branch-start.sh +88 -3
  58. package/templates/scripts/agent-preflight.sh +31 -5
  59. package/templates/scripts/agent-worktree-prune.sh +1 -1
  60. package/templates/scripts/codex-agent.sh +0 -91
  61. package/src/agents/detect.js +0 -160
  62. package/src/cockpit/keybindings.js +0 -224
  63. package/src/cockpit/layout.js +0 -224
package/README.md CHANGED
@@ -308,6 +308,32 @@ Being honest about where this still has issues:
308
308
  <details open>
309
309
  <summary><strong>v7.x</strong></summary>
310
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
+
311
337
  ### v7.0.42
312
338
  - Bumped `@imdeadpool/guardex` from `7.0.41` to `7.0.42` so the current
313
339
  `main` payload can publish under a fresh npm version after `7.0.41` reached
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.43",
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",
@@ -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.
@@ -45,10 +45,22 @@ function parseWorktreeList(outputText) {
45
45
  return worktrees;
46
46
  }
47
47
 
48
- function worktreePathForBranch(repoRoot, branch) {
48
+ function listWorktrees(repoRoot) {
49
49
  const result = git(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
50
- if (result.status !== 0) return { worktreePath: repoRoot, worktreeFound: false };
51
- const match = parseWorktreeList(result.stdout).find((entry) => entry.branch === branch);
50
+ return result.status === 0 ? parseWorktreeList(result.stdout) : null;
51
+ }
52
+
53
+ // `worktrees` (pre-parsed via listWorktrees) lets callers iterating many branches
54
+ // in one pass hoist the invariant `git worktree list` out of their loop. When
55
+ // omitted, the list is fetched on demand — unchanged behavior.
56
+ function worktreePathForBranch(repoRoot, branch, worktrees = null) {
57
+ let entries = worktrees;
58
+ if (entries == null) {
59
+ const result = git(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
60
+ if (result.status !== 0) return { worktreePath: repoRoot, worktreeFound: false };
61
+ entries = parseWorktreeList(result.stdout);
62
+ }
63
+ const match = entries.find((entry) => entry.branch === branch);
52
64
  return {
53
65
  worktreePath: match?.path || repoRoot,
54
66
  worktreeFound: Boolean(match?.path),
@@ -119,7 +131,7 @@ function inspectAgentBranch(options) {
119
131
 
120
132
  const baseBranch = resolveBaseBranch(repoRoot, branch);
121
133
  const compareRef = compareRefForBase(repoRoot, baseBranch);
122
- const { worktreePath, worktreeFound } = worktreePathForBranch(repoRoot, branch);
134
+ const { worktreePath, worktreeFound } = worktreePathForBranch(repoRoot, branch, options.worktrees);
123
135
  return { repoRoot, branch, baseBranch, compareRef, worktreePath, worktreeFound };
124
136
  }
125
137
 
@@ -179,6 +191,7 @@ module.exports = {
179
191
  branchLocks,
180
192
  changedFiles,
181
193
  inspectAgentBranch,
194
+ listWorktrees,
182
195
  parseWorktreeList,
183
196
  readLockRegistry,
184
197
  renderDiff,
@@ -185,6 +185,14 @@ function buildPromptCommand(parts, agent, prompt) {
185
185
  return commandToShell([...parts, prompt]);
186
186
  }
187
187
 
188
+ function buildResourceEnv() {
189
+ const cpus = require('node:os').cpus().length || 8;
190
+ // Cap each agent's cargo parallelism to avoid overwhelming the system
191
+ // when multiple agents build concurrently.
192
+ const jobs = Math.max(2, Math.floor(cpus / 4));
193
+ return [`CARGO_BUILD_JOBS=${jobs}`];
194
+ }
195
+
188
196
  function buildAgentLaunchCommand(options) {
189
197
  if (!options || typeof options !== 'object') {
190
198
  throw new TypeError('options are required');
@@ -207,7 +215,8 @@ function buildAgentLaunchCommand(options) {
207
215
  }
208
216
 
209
217
  const launchCommand = buildPromptCommand(baseParts, agent, prompt);
210
- const envPrefix = buildSessionEnv(agent, sessionId).join(' ');
218
+ const envParts = [...buildResourceEnv(), ...buildSessionEnv(agent, sessionId)];
219
+ const envPrefix = envParts.join(' ');
211
220
  const launchWithEnv = envPrefix ? `${envPrefix} ${launchCommand}` : launchCommand;
212
221
  if (!worktreePath) return launchWithEnv;
213
222
  return `cd ${shellQuote(worktreePath)} && ${launchWithEnv}`;
@@ -1,5 +1,5 @@
1
1
  const { fs, path, LOCK_FILE_RELATIVE, TOOL_NAME } = require('../context');
2
- const { changedFiles } = require('./inspect');
2
+ const { changedFiles, listWorktrees } = require('./inspect');
3
3
  const { listAgentSessions } = require('./sessions');
4
4
 
5
5
  function uniqueSorted(values) {
@@ -35,10 +35,10 @@ function readLockDetails(repoRoot) {
35
35
  return { counts, files };
36
36
  }
37
37
 
38
- function readChangedFiles(repoRoot, branch) {
38
+ function readChangedFiles(repoRoot, branch, worktrees) {
39
39
  if (!branch) return [];
40
40
  try {
41
- return changedFiles({ target: repoRoot, branch }).files || [];
41
+ return changedFiles({ target: repoRoot, branch, worktrees }).files || [];
42
42
  } catch (_error) {
43
43
  return [];
44
44
  }
@@ -53,7 +53,7 @@ function normalizePr(session) {
53
53
  };
54
54
  }
55
55
 
56
- function normalizeSessionForStatus(session, lockDetails, repoRoot) {
56
+ function normalizeSessionForStatus(session, lockDetails, repoRoot, worktrees) {
57
57
  const branch = session.branch || '';
58
58
  const worktreePath = session.worktreePath || '';
59
59
  const claimedFiles = uniqueSorted([
@@ -73,7 +73,7 @@ function normalizeSessionForStatus(session, lockDetails, repoRoot) {
73
73
  worktreeExists: worktreePath ? fs.existsSync(worktreePath) : false,
74
74
  lockCount: lockDetails.counts.get(branch) || 0,
75
75
  claimedFiles,
76
- changedFiles: readChangedFiles(repoRoot, branch),
76
+ changedFiles: readChangedFiles(repoRoot, branch, worktrees),
77
77
  metadata: session.metadata && typeof session.metadata === 'object' ? session.metadata : {},
78
78
  launchCommand: session.launchCommand || '',
79
79
  tmux: session.tmux && typeof session.tmux === 'object' ? session.tmux : null,
@@ -85,10 +85,13 @@ function normalizeSessionForStatus(session, lockDetails, repoRoot) {
85
85
 
86
86
  function buildAgentsStatusPayload(repoRoot) {
87
87
  const lockDetails = readLockDetails(repoRoot);
88
+ // Hoist the invariant `git worktree list` out of the per-session loop: fetch it
89
+ // once here instead of once inside every session's changedFiles() (~6N -> ~6+N).
90
+ const worktrees = listWorktrees(repoRoot);
88
91
  return {
89
92
  schemaVersion: 1,
90
93
  repoRoot,
91
- sessions: listAgentSessions(repoRoot).map((session) => normalizeSessionForStatus(session, lockDetails, repoRoot)),
94
+ sessions: listAgentSessions(repoRoot).map((session) => normalizeSessionForStatus(session, lockDetails, repoRoot, worktrees)),
92
95
  };
93
96
  }
94
97
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const cp = require('node:child_process');
4
+ const { GH_BIN } = require('../context');
4
5
 
5
6
  const TOOL_NAME = 'gx';
6
7
 
@@ -8,7 +9,7 @@ const DEFAULT_WARN_NET_USD = 1; // any paid spend at all
8
9
  const DEFAULT_CRITICAL_NET_USD = 10; // paid spend that has caused merge blocks before
9
10
 
10
11
  function runGh(args) {
11
- const result = cp.spawnSync('gh', args, { encoding: 'utf8' });
12
+ const result = cp.spawnSync(GH_BIN, args, { encoding: 'utf8' });
12
13
  if (result.error) {
13
14
  const err = new Error(`gh binary not found: ${result.error.message}`);
14
15
  err.code = 'GH_BIN_MISSING';
package/src/cli/args.js CHANGED
@@ -108,6 +108,14 @@ function parseCommonArgs(rawArgs, defaults) {
108
108
  options.allowProtectedBaseWrite = true;
109
109
  continue;
110
110
  }
111
+ if (arg === '--contract' || arg === '--full') {
112
+ options.contract = true;
113
+ continue;
114
+ }
115
+ if (arg === '--minimal' || arg === '--no-contract') {
116
+ options.contract = false;
117
+ continue;
118
+ }
111
119
  if (Object.prototype.hasOwnProperty.call(options, 'waitForMerge') && arg === '--wait-for-merge') {
112
120
  options.waitForMerge = true;
113
121
  continue;
@@ -177,6 +185,8 @@ function parseSetupArgs(rawArgs, defaults) {
177
185
  const setupDefaults = {
178
186
  ...defaults,
179
187
  parentWorkspaceView: false,
188
+ speckit: true,
189
+ speckitForce: false,
180
190
  };
181
191
  const forwardedArgs = [];
182
192
 
@@ -190,6 +200,19 @@ function parseSetupArgs(rawArgs, defaults) {
190
200
  setupDefaults.parentWorkspaceView = false;
191
201
  continue;
192
202
  }
203
+ if (arg === '--no-speckit' || arg === '--skip-speckit') {
204
+ setupDefaults.speckit = false;
205
+ continue;
206
+ }
207
+ if (arg === '--speckit') {
208
+ setupDefaults.speckit = true;
209
+ continue;
210
+ }
211
+ if (arg === '--speckit-force' || arg === '--reinstall-speckit') {
212
+ setupDefaults.speckit = true;
213
+ setupDefaults.speckitForce = true;
214
+ continue;
215
+ }
193
216
  forwardedArgs.push(arg);
194
217
  }
195
218
 
@@ -1069,6 +1092,10 @@ function parseFinishArgs(rawArgs, defaults = {}) {
1069
1092
  failFast: false,
1070
1093
  commitMessage: '',
1071
1094
  mergeMode: defaults.mergeMode || 'pr',
1095
+ skipPreflight: false,
1096
+ gateReview: defaults.gateReview ?? false,
1097
+ reviewProvider: defaults.reviewProvider || 'codex',
1098
+ allowNoChecks: false,
1072
1099
  };
1073
1100
 
1074
1101
  for (let index = 0; index < rawArgs.length; index += 1) {
@@ -1181,6 +1208,31 @@ function parseFinishArgs(rawArgs, defaults = {}) {
1181
1208
  options.advanceSubmodules = false;
1182
1209
  continue;
1183
1210
  }
1211
+ if (arg === '--skip-preflight') {
1212
+ options.skipPreflight = true;
1213
+ continue;
1214
+ }
1215
+ if (arg === '--gate-review') {
1216
+ options.gateReview = true;
1217
+ continue;
1218
+ }
1219
+ if (arg === '--no-gate-review' || arg === '--skip-review-gate') {
1220
+ options.gateReview = false;
1221
+ continue;
1222
+ }
1223
+ if (arg === '--allow-no-checks') {
1224
+ options.allowNoChecks = true;
1225
+ continue;
1226
+ }
1227
+ if (arg === '--review-provider') {
1228
+ const next = rawArgs[index + 1];
1229
+ if (!next || !['codex', 'claude'].includes(next)) {
1230
+ throw new Error('--review-provider requires a value of codex|claude');
1231
+ }
1232
+ options.reviewProvider = next;
1233
+ index += 1;
1234
+ continue;
1235
+ }
1184
1236
  throw new Error(`Unknown option: ${arg}`);
1185
1237
  }
1186
1238
 
@@ -1194,9 +1246,7 @@ function parseFinishArgs(rawArgs, defaults = {}) {
1194
1246
  module.exports = {
1195
1247
  requireValue,
1196
1248
  normalizeManagedForcePath,
1197
- collectForceManagedPaths,
1198
1249
  parseCommonArgs,
1199
- parseRepoTraversalArgs,
1200
1250
  parseSetupArgs,
1201
1251
  parseDoctorArgs,
1202
1252
  parseTargetFlag,