@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.
Files changed (118) hide show
  1. package/README.md +94 -13
  2. package/package.json +3 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/skills/gx-act/SKILL.md +82 -0
  6. package/src/agents/cleanup-sessions.js +126 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +202 -0
  9. package/src/agents/launch.js +249 -0
  10. package/src/agents/registry.js +133 -0
  11. package/src/agents/selection-panel.js +571 -0
  12. package/src/agents/sessions.js +151 -0
  13. package/src/agents/start.js +591 -0
  14. package/src/agents/status.js +146 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +344 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +357 -3
  19. package/src/cli/commands/agents.js +364 -0
  20. package/src/cli/commands/bootstrap.js +92 -0
  21. package/src/cli/commands/branch.js +127 -0
  22. package/src/cli/commands/claude.js +674 -0
  23. package/src/cli/commands/doctor.js +268 -0
  24. package/src/cli/commands/finish.js +26 -0
  25. package/src/cli/commands/mcp.js +122 -0
  26. package/src/cli/commands/misc.js +304 -0
  27. package/src/cli/commands/pr.js +439 -0
  28. package/src/cli/commands/prompt.js +92 -0
  29. package/src/cli/commands/release.js +305 -0
  30. package/src/cli/commands/report.js +244 -0
  31. package/src/cli/commands/review.js +32 -0
  32. package/src/cli/commands/setup.js +242 -0
  33. package/src/cli/commands/status.js +338 -0
  34. package/src/cli/commands/watch.js +234 -0
  35. package/src/cli/main.js +85 -3613
  36. package/src/cli/shared/repo-env.js +161 -0
  37. package/src/cli/shared/sandbox.js +417 -0
  38. package/src/cli/shared/scaffolding.js +535 -0
  39. package/src/cli/shared/toolchain-shims.js +420 -0
  40. package/src/cockpit/action-runner.js +3 -0
  41. package/src/cockpit/actions.js +80 -0
  42. package/src/cockpit/control.js +1121 -0
  43. package/src/cockpit/index.js +426 -0
  44. package/src/cockpit/kitty-layout.js +549 -0
  45. package/src/cockpit/kitty-tree.js +144 -0
  46. package/src/cockpit/logs-reader.js +182 -0
  47. package/src/cockpit/menu.js +204 -0
  48. package/src/cockpit/pane-actions.js +597 -0
  49. package/src/cockpit/pane-menu.js +387 -0
  50. package/src/cockpit/projects-finder.js +178 -0
  51. package/src/cockpit/render.js +215 -0
  52. package/src/cockpit/settings-render.js +128 -0
  53. package/src/cockpit/settings.js +124 -0
  54. package/src/cockpit/shortcuts.js +24 -0
  55. package/src/cockpit/sidebar.js +311 -0
  56. package/src/cockpit/state.js +72 -0
  57. package/src/cockpit/theme.js +128 -0
  58. package/src/cockpit/welcome.js +266 -0
  59. package/src/context.js +304 -43
  60. package/src/core/runtime.js +6 -1
  61. package/src/doctor/index.js +45 -15
  62. package/src/finish/index.js +186 -7
  63. package/src/finish/preflight.js +177 -0
  64. package/src/finish/review-gate.js +182 -0
  65. package/src/git/index.js +511 -4
  66. package/src/hooks/index.js +0 -64
  67. package/src/kitty/command.js +101 -0
  68. package/src/kitty/runtime.js +250 -0
  69. package/src/mcp/collect.js +370 -0
  70. package/src/mcp/server.js +157 -0
  71. package/src/output/index.js +68 -2
  72. package/src/pr-review.js +264 -0
  73. package/src/pr.js +381 -0
  74. package/src/sandbox/index.js +13 -2
  75. package/src/scaffold/agent-worktree-prep.js +213 -0
  76. package/src/scaffold/index.js +127 -10
  77. package/src/speckit/index.js +226 -0
  78. package/src/submodule/index.js +288 -0
  79. package/src/terminal/index.js +45 -0
  80. package/src/terminal/kitty.js +622 -0
  81. package/src/terminal/tmux.js +125 -0
  82. package/src/tmux/command.js +27 -0
  83. package/src/tmux/session.js +89 -0
  84. package/src/toolchain/index.js +20 -0
  85. package/templates/AGENTS.monorepo-apps.md +26 -0
  86. package/templates/AGENTS.multiagent-safety.md +63 -323
  87. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  88. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  89. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  90. package/templates/githooks/pre-commit +44 -20
  91. package/templates/github/workflows/README.md +87 -0
  92. package/templates/github/workflows/ci-full.yml +55 -0
  93. package/templates/github/workflows/ci.yml +56 -0
  94. package/templates/github/workflows/cr.yml +20 -1
  95. package/templates/scripts/agent-branch-finish.sh +519 -23
  96. package/templates/scripts/agent-branch-merge.sh +4 -1
  97. package/templates/scripts/agent-branch-start.sh +176 -24
  98. package/templates/scripts/agent-preflight.sh +115 -0
  99. package/templates/scripts/agent-worktree-prune.sh +96 -5
  100. package/templates/scripts/codex-agent.sh +41 -97
  101. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  102. package/templates/scripts/review-bot-watch.sh +31 -2
  103. package/templates/scripts/agent-session-state.js +0 -171
  104. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  105. package/templates/vscode/guardex-active-agents/README.md +0 -34
  106. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  107. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  108. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  109. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  110. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  111. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  112. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  113. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  114. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  115. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  116. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  117. package/templates/vscode/guardex-active-agents/package.json +0 -169
  118. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
package/src/context.js CHANGED
@@ -21,7 +21,14 @@ const GLOBAL_INSTALL_COMMAND = `npm i -g ${packageJson.name}`;
21
21
  const OPENSPEC_PACKAGE = '@fission-ai/openspec';
22
22
  const OMC_PACKAGE = 'oh-my-claude-sisyphus';
23
23
  const OMC_REPO_URL = 'https://github.com/Yeachan-Heo/oh-my-claudecode';
24
- const COLONY_PACKAGE = '@imdeadpool/colony-cli';
24
+ // Colony was published under @imdeadpool/colony-cli historically; the new
25
+ // canonical npm name is `colonyq`. The companion-install prompt that
26
+ // gx status / gx setup show now reads `npm i -g colonyq`. Post-install
27
+ // setup the user runs themselves (gitguardex only owns the `npm i -g` step):
28
+ // colony install --ide codex
29
+ // npx skills add recodeee/colony/skills/colony-mcp
30
+ // colony health
31
+ const COLONY_PACKAGE = 'colonyq';
25
32
  const NPX_BIN = process.env.GUARDEX_NPX_BIN || 'npx';
26
33
  const GUARDEX_HOME_DIR = path.resolve(process.env.GUARDEX_HOME_DIR || os.homedir());
27
34
  const GLOBAL_TOOLCHAIN_SERVICES = [
@@ -61,14 +68,34 @@ const OPTIONAL_LOCAL_COMPANION_TOOLS = [
61
68
  installArgs: ['skills', 'add', 'JuliusBrussee/caveman'],
62
69
  },
63
70
  ];
64
- const GH_BIN = process.env.GUARDEX_GH_BIN || 'gh';
71
+ function commandAvailable(command) {
72
+ const result = cp.spawnSync(command, ['--version'], { stdio: 'ignore' });
73
+ return result.status === 0;
74
+ }
75
+
76
+ function resolveGithubCliBin(env = process.env) {
77
+ const explicit = String(env.GUARDEX_GH_BIN || '').trim();
78
+ if (explicit) {
79
+ return explicit;
80
+ }
81
+ return commandAvailable('ghx') ? 'ghx' : 'gh';
82
+ }
83
+
84
+ const GH_BIN = resolveGithubCliBin();
85
+ const RTK_BIN = process.env.GUARDEX_RTK_BIN || 'rtk';
65
86
  const REQUIRED_SYSTEM_TOOLS = [
66
87
  {
67
88
  name: 'gh',
68
- displayName: 'GitHub (gh)',
89
+ displayName: GH_BIN === 'ghx' ? 'GitHub (ghx proxy)' : 'GitHub (gh)',
69
90
  command: GH_BIN,
70
91
  installHint: 'https://cli.github.com/',
71
92
  },
93
+ {
94
+ name: 'rtk',
95
+ displayName: 'RTK (rtk)',
96
+ command: RTK_BIN,
97
+ installHint: 'Install RTK and ensure `rtk` is on PATH.',
98
+ },
72
99
  ];
73
100
  const MAINTAINER_RELEASE_REPO = path.resolve(
74
101
  process.env.GUARDEX_RELEASE_REPO || PACKAGE_ROOT,
@@ -111,51 +138,41 @@ function toDestinationPath(relativeTemplatePath) {
111
138
  if (relativeTemplatePath.startsWith('github/')) {
112
139
  return `.${relativeTemplatePath}`;
113
140
  }
114
- if (relativeTemplatePath.startsWith('vscode/')) {
115
- return relativeTemplatePath;
116
- }
117
141
  throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
118
142
  }
119
143
 
144
+ // scripts/ ↔ templates/scripts/ layout convention (single source of truth):
145
+ //
146
+ // 1. PAIRED files (10): tracked on both sides; scripts/<file> is a symlink
147
+ // to ../templates/scripts/<file> per PR #548. See
148
+ // scripts/check-script-symlinks.sh for the exact list. CI + the
149
+ // .githooks/pre-commit shim both enforce that no symlink is ever
150
+ // replaced with a regular file. Edit only the templates/scripts/ copy;
151
+ // the symlink propagates.
152
+ //
153
+ // 2. SCAFFOLD-ONLY files (the 3 below + workflows):
154
+ // tracked only under templates/; scaffolded into gitignored
155
+ // scripts/<file> (or .githooks/<file>, etc.) by `gx setup`. Consumer
156
+ // repos receive a regular file copy at the destination; gitguardex
157
+ // itself receives the same copy and ignores it via the
158
+ // multiagent-safety .gitignore block. Edit only the templates/ copy.
159
+ //
160
+ // If a file you're about to add fits pattern (1), also add it to
161
+ // scripts/check-script-symlinks.sh's required_symlinks list. If it fits
162
+ // pattern (2), append the destination path to .gitignore's multiagent-
163
+ // safety block (auto-managed by syncManagedGitignoreLines below).
120
164
  const TEMPLATE_FILES = [
121
- 'scripts/agent-session-state.js',
165
+ 'scripts/agent-preflight.sh',
122
166
  'scripts/guardex-docker-loader.sh',
123
167
  'scripts/guardex-env.sh',
124
- 'scripts/install-vscode-active-agents-extension.js',
125
168
  'github/pull.yml.example',
169
+ 'github/workflows/ci.yml',
170
+ 'github/workflows/ci-full.yml',
126
171
  'github/workflows/cr.yml',
127
- 'vscode/guardex-active-agents/package.json',
128
- 'vscode/guardex-active-agents/extension.js',
129
- 'vscode/guardex-active-agents/session-schema.js',
130
- 'vscode/guardex-active-agents/README.md',
131
- 'vscode/guardex-active-agents/icon.png',
132
- 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json',
133
- 'vscode/guardex-active-agents/fileicons/icons/agent.svg',
134
- 'vscode/guardex-active-agents/fileicons/icons/branch.svg',
135
- 'vscode/guardex-active-agents/fileicons/icons/config.svg',
136
- 'vscode/guardex-active-agents/fileicons/icons/hook.svg',
137
- 'vscode/guardex-active-agents/fileicons/icons/openspec.svg',
138
- 'vscode/guardex-active-agents/fileicons/icons/plan.svg',
139
- 'vscode/guardex-active-agents/fileicons/icons/spec.svg',
172
+ 'github/workflows/README.md',
140
173
  ];
141
174
 
142
- const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set([
143
- 'scripts/agent-session-state.js',
144
- 'scripts/install-vscode-active-agents-extension.js',
145
- 'vscode/guardex-active-agents/package.json',
146
- 'vscode/guardex-active-agents/extension.js',
147
- 'vscode/guardex-active-agents/session-schema.js',
148
- 'vscode/guardex-active-agents/README.md',
149
- 'vscode/guardex-active-agents/icon.png',
150
- 'vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json',
151
- 'vscode/guardex-active-agents/fileicons/icons/agent.svg',
152
- 'vscode/guardex-active-agents/fileicons/icons/branch.svg',
153
- 'vscode/guardex-active-agents/fileicons/icons/config.svg',
154
- 'vscode/guardex-active-agents/fileicons/icons/hook.svg',
155
- 'vscode/guardex-active-agents/fileicons/icons/openspec.svg',
156
- 'vscode/guardex-active-agents/fileicons/icons/plan.svg',
157
- 'vscode/guardex-active-agents/fileicons/icons/spec.svg',
158
- ]);
175
+ const PACKAGE_ROOT_SOURCE_OVERRIDES = new Set();
159
176
 
160
177
  const LEGACY_WORKFLOW_SHIM_SPECS = [
161
178
  { relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] },
@@ -178,9 +195,7 @@ const MANAGED_TEMPLATE_SCRIPT_FILES = MANAGED_TEMPLATE_DESTINATIONS.filter((entr
178
195
 
179
196
  const LEGACY_MANAGED_REPO_FILES = [
180
197
  ...LEGACY_WORKFLOW_SHIMS,
181
- 'scripts/agent-session-state.js',
182
198
  'scripts/guardex-docker-loader.sh',
183
- 'scripts/install-vscode-active-agents-extension.js',
184
199
  'scripts/guardex-env.sh',
185
200
  'scripts/install-agent-git-hooks.sh',
186
201
  '.githooks/pre-commit',
@@ -229,7 +244,6 @@ const PACKAGE_SCRIPT_ASSETS = {
229
244
  branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'),
230
245
  codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'),
231
246
  reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'),
232
- sessionState: path.join(TEMPLATE_ROOT, 'scripts', 'agent-session-state.js'),
233
247
  worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'),
234
248
  lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'),
235
249
  planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'),
@@ -266,6 +280,8 @@ const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
266
280
  const AGENTS_BOTS_STATE_RELATIVE = '.omx/state/agents-bots.json';
267
281
  const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
268
282
  const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
283
+ const MONOREPO_MARKER_START = '<!-- monorepo-apps:START -->';
284
+ const MONOREPO_MARKER_END = '<!-- monorepo-apps:END -->';
269
285
  const GITIGNORE_MARKER_START = '# multiagent-safety:START';
270
286
  const GITIGNORE_MARKER_END = '# multiagent-safety:END';
271
287
  const CODEX_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
@@ -289,13 +305,12 @@ const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
289
305
  const MANAGED_GITIGNORE_PATHS = [
290
306
  '.omx/',
291
307
  '.omc/',
308
+ '.codex/',
292
309
  '!.vscode/',
293
310
  '.vscode/*',
294
311
  '!.vscode/settings.json',
295
- 'scripts/agent-session-state.js',
296
312
  'scripts/guardex-docker-loader.sh',
297
313
  'scripts/guardex-env.sh',
298
- 'scripts/install-vscode-active-agents-extension.js',
299
314
  '.githooks',
300
315
  'oh-my-codex/',
301
316
  LOCK_FILE_RELATIVE,
@@ -345,6 +360,7 @@ const SUGGESTIBLE_COMMANDS = [
345
360
  'hook',
346
361
  'migrate',
347
362
  'install-agent-skills',
363
+ 'cockpit',
348
364
  'agents',
349
365
  'merge',
350
366
  'finish',
@@ -364,6 +380,9 @@ const SUGGESTIBLE_COMMANDS = [
364
380
  'copy-commands',
365
381
  'print-agents-snippet',
366
382
  'release',
383
+ 'budget',
384
+ 'ci-init',
385
+ 'speckit',
367
386
  ];
368
387
  // CLI_COMMAND_GROUPS is the grouped source of truth the `gx --help` /
369
388
  // `gx` no-args renderer uses. Each group is ordered roughly by how often a
@@ -409,7 +428,10 @@ const CLI_COMMAND_GROUPS = [
409
428
  description: 'Review / cleanup bots, AI setup prompts, and safety reports.',
410
429
  commands: [
411
430
  ['agents', 'Start/stop repo-scoped review + cleanup bots'],
431
+ ['pr-review', 'Run local Codex/Claude PR review and post inline GitHub comments or write an artifact'],
432
+ ['cockpit', 'Create or attach to a repo tmux cockpit session'],
412
433
  ['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
434
+ ['speckit', 'Install Spec Kit (specify-cli) SDD slash skills (/speckit-specify, /speckit-plan, ...) into the current repo'],
413
435
  ['prompt', 'Print AI setup checklist or named slices (--exec, --part, --list-parts, --snippet)'],
414
436
  ['report', 'Security/safety reports (e.g. OpenSSF scorecard, session severity)'],
415
437
  ['release', 'Create or update the current GitHub release with README-generated notes'],
@@ -449,6 +471,9 @@ const DOCTOR_AUTO_FINISH_MESSAGE_MAX = 160;
449
471
  const AI_SETUP_PART_ALIASES = new Map([
450
472
  ['task', 'task-loop'],
451
473
  ['loop', 'task-loop'],
474
+ ['compact-commands', 'rtk'],
475
+ ['command-compression', 'rtk'],
476
+ ['token-commands', 'rtk'],
452
477
  ['reviewbot', 'review-bot'],
453
478
  ['forksync', 'fork-sync'],
454
479
  ]);
@@ -500,6 +525,18 @@ const AI_SETUP_PARTS = [
500
525
  'gx locks claim --branch "<agent-branch>" <file...>',
501
526
  ],
502
527
  },
528
+ {
529
+ name: 'rtk',
530
+ label: 'RTK command compression',
531
+ promptLines: [
532
+ 'Prefer RTK wrappers for noisy shell discovery and verification when `rtk` is available; fall back to raw commands when missing.',
533
+ 'Files: `rtk ls .`, `rtk read <file>`, `rtk read <file> -l aggressive`, `rtk smart <file>`, `rtk find "<glob>" .`, `rtk grep "<pattern>" .`, `rtk diff <a> <b>`.',
534
+ 'Git/GitHub: `rtk git status`, `rtk git diff`, `rtk git log -n 10`, `rtk gh pr list`, `rtk gh pr view <id>`.',
535
+ 'Tests/build: `rtk test <cmd>`, `rtk err <cmd>`, `rtk jest`, `rtk vitest`, `rtk playwright test`, `rtk pytest`, `rtk cargo test`, `rtk tsc`, `rtk lint`.',
536
+ 'Use `rtk gain`, `rtk discover`, and `rtk session` to audit savings; use `rtk proxy <command>` only when raw passthrough is required.',
537
+ 'Do not wrap machine-readable commands with RTK when code parses stdout (`--porcelain`, `--json`, NUL-delimited output, or exact stdout contracts).',
538
+ ],
539
+ },
503
540
  {
504
541
  name: 'integrate',
505
542
  label: 'Integrate',
@@ -658,11 +695,232 @@ const SCORECARD_RISK_BY_CHECK = {
658
695
  License: 'Low',
659
696
  };
660
697
 
698
+ // ---------------------------------------------------------------------------
699
+ // Process-scoped memoization for idempotent git/gh probes.
700
+ //
701
+ // Many gx commands (notably `gx doctor`, `gx status`, preflight checks) ask
702
+ // git/gh the same read-only questions multiple times within a single Node
703
+ // process (current branch, remote URL, worktree list, config values, PR
704
+ // state). spawnSync is cheap individually but adds up across 20+ probes.
705
+ //
706
+ // Rules:
707
+ // * Cache only idempotent reads. Strict allowlist below.
708
+ // * Never cache writes, network mutations, or anything passing stdin.
709
+ // * Lifetime = this Node process only (no disk cache, no TTL beyond the
710
+ // process). A fresh `gx ...` invocation always starts cold.
711
+ // * Honors GUARDEX_PROBE_TRACE=1 to print `[probe]` / `[probe-hit]` lines
712
+ // on stderr so duplicate calls are observable.
713
+ // * Honors GUARDEX_PROBE_CACHE=0 to disable the cache entirely
714
+ // (escape hatch).
715
+ // ---------------------------------------------------------------------------
716
+
717
+ const PROBE_CACHE = new Map();
718
+ const PROBE_TRACE_ENABLED = (() => {
719
+ const raw = String(process.env.GUARDEX_PROBE_TRACE || '').trim().toLowerCase();
720
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
721
+ })();
722
+ const PROBE_CACHE_DISABLED = (() => {
723
+ const raw = String(process.env.GUARDEX_PROBE_CACHE || '').trim().toLowerCase();
724
+ return raw === '0' || raw === 'false' || raw === 'no' || raw === 'off';
725
+ })();
726
+
727
+ // Env vars that, if they differ between calls, must invalidate cache (because
728
+ // they change git/gh's actual answer). Most env doesn't matter for read probes
729
+ // so we deliberately key only on a small slice to keep cache hits high.
730
+ const PROBE_CACHE_ENV_KEYS = [
731
+ 'GIT_DIR',
732
+ 'GIT_WORK_TREE',
733
+ 'GIT_COMMON_DIR',
734
+ 'GIT_INDEX_FILE',
735
+ 'GITHUB_TOKEN',
736
+ 'GH_TOKEN',
737
+ 'GH_HOST',
738
+ ];
739
+
740
+ function envSubsetKey(envOverride) {
741
+ const env = envOverride || process.env;
742
+ const parts = [];
743
+ for (const key of PROBE_CACHE_ENV_KEYS) {
744
+ if (env[key] !== undefined) parts.push(`${key}=${env[key]}`);
745
+ }
746
+ return parts.join('');
747
+ }
748
+
749
+ // Strip a leading `-C <path>` (or `-c key=value`) pair from git args so we
750
+ // look at the real verb after global options.
751
+ function gitVerbAndRest(args) {
752
+ if (!Array.isArray(args) || args.length === 0) return { verb: '', rest: [] };
753
+ let i = 0;
754
+ while (i < args.length) {
755
+ const a = args[i];
756
+ if (a === '-C' && i + 1 < args.length) {
757
+ i += 2;
758
+ continue;
759
+ }
760
+ if (a === '-c' && i + 1 < args.length) {
761
+ i += 2;
762
+ continue;
763
+ }
764
+ if (typeof a === 'string' && a.startsWith('-c') && a !== '-c') {
765
+ i += 1;
766
+ continue;
767
+ }
768
+ break;
769
+ }
770
+ return { verb: args[i] || '', rest: args.slice(i + 1) };
771
+ }
772
+
773
+ // A git command is cacheable only if its answer is invariant for the lifetime
774
+ // of the process under our own commands' behavior. Many git "reads"
775
+ // (`status`, `diff`, `for-each-ref`, `rev-list`, `ls-files`, `worktree list`,
776
+ // `branch --show-current`, `merge-base --is-ancestor`) can change mid-run
777
+ // because gx itself writes (commits, branch ops, worktree add/remove, config
778
+ // writes). Caching those would feed callers stale answers and break command
779
+ // flows. The narrow allowlist below intentionally covers only:
780
+ // * filesystem geometry that git itself treats as fixed for a given repo
781
+ // checkout (`rev-parse --show-toplevel|--git-common-dir|--git-dir|
782
+ // --show-cdup|--show-superproject-working-tree|--is-inside-work-tree|
783
+ // --is-bare-repository`)
784
+ // * tool version banners (`--version`)
785
+ // Everything else falls through to a real spawn each time.
786
+ function gitIsCacheableRead(args) {
787
+ const { verb, rest } = gitVerbAndRest(args);
788
+ if (!verb) return false;
789
+
790
+ if (verb === '--version' || verb === 'version') return true;
791
+
792
+ if (verb === 'rev-parse') {
793
+ // Allow ONLY the geometry-probe forms whose answer never depends on the
794
+ // working-tree, ref state, or config. Concrete ref resolution
795
+ // (`rev-parse HEAD`, `rev-parse --verify <ref>`) is intentionally NOT
796
+ // cached because gx writes refs.
797
+ for (const a of rest) {
798
+ if (typeof a !== 'string') continue;
799
+ if (a.startsWith('-')) {
800
+ if (
801
+ a === '--show-toplevel' ||
802
+ a === '--git-common-dir' ||
803
+ a === '--git-dir' ||
804
+ a === '--show-cdup' ||
805
+ a === '--show-superproject-working-tree' ||
806
+ a === '--is-inside-work-tree' ||
807
+ a === '--is-inside-git-dir' ||
808
+ a === '--is-bare-repository' ||
809
+ a === '--show-prefix'
810
+ ) {
811
+ // continue scanning to make sure no ref-probe is also present
812
+ continue;
813
+ }
814
+ // Any other flag (--verify, --short, --abbrev-ref, etc.) means we're
815
+ // resolving a ref, which is mutable.
816
+ return false;
817
+ }
818
+ // Bare positional (e.g. a ref name) -> ref resolution, mutable.
819
+ return false;
820
+ }
821
+ return rest.length > 0;
822
+ }
823
+ return false;
824
+ }
825
+
826
+ // gh probes that ARE safe to cache within a single process: only the version
827
+ // banner. Auth state can change (login/logout in another shell), PR state
828
+ // can change (a merge can land mid-poll). The instruction's allowlist
829
+ // included `gh auth status`, `gh api -X GET`, `gh pr view --json`, etc.;
830
+ // those are read-only from gh's side but their underlying truth shifts under
831
+ // gx's own writes (`gh pr create`, `gh pr merge`, `gh auth refresh`, server
832
+ // state). Within a single doctor sweep we explicitly want to re-query auth
833
+ // and PR state, so caching them would mask freshly-changed reality.
834
+ function ghIsCacheableRead(args) {
835
+ if (!Array.isArray(args) || args.length === 0) return false;
836
+ const verb = args[0] || '';
837
+ if (verb === '--version' || verb === 'version') return true;
838
+ return false;
839
+ }
840
+
841
+ function isCacheableSpawn(cmd, args, options) {
842
+ if (PROBE_CACHE_DISABLED) return false;
843
+ if (!cmd || typeof cmd !== 'string') return false;
844
+ if (options && options.input !== undefined && options.input !== null) return false;
845
+ // Pass-through if stdio is anything other than fully pipe/ignore (callers
846
+ // that inherit stdio need the real child to print, not a cached payload).
847
+ if (options && options.stdio && options.stdio !== 'pipe' && options.stdio !== 'ignore') {
848
+ if (Array.isArray(options.stdio)) {
849
+ for (const leg of options.stdio) {
850
+ if (leg && leg !== 'pipe' && leg !== 'ignore' && leg !== null) return false;
851
+ }
852
+ } else {
853
+ return false;
854
+ }
855
+ }
856
+ const base = path.basename(cmd);
857
+ if (base === 'git') return gitIsCacheableRead(args || []);
858
+ if (base === 'gh' || base === 'ghx') return ghIsCacheableRead(args || []);
859
+ if (base === 'which' || base === 'command' || base === 'type') {
860
+ return Array.isArray(args) && args.length >= 1;
861
+ }
862
+ return false;
863
+ }
864
+
865
+ function probeTrace(prefix, cmd, args) {
866
+ if (!PROBE_TRACE_ENABLED) return;
867
+ try {
868
+ process.stderr.write(`[${prefix}] ${cmd} ${(args || []).join(' ')}\n`);
869
+ } catch {
870
+ // Tracing must never break the probe.
871
+ }
872
+ }
873
+
874
+ function cachedSpawn(cmd, args, options) {
875
+ const cacheable = isCacheableSpawn(cmd, args, options);
876
+ if (!cacheable) {
877
+ probeTrace('probe', cmd, args);
878
+ return cp.spawnSync(cmd, args, options);
879
+ }
880
+ const cwdKey = (options && options.cwd) || process.cwd();
881
+ const envKey = options && options.env
882
+ ? envSubsetKey({ ...process.env, ...options.env })
883
+ : envSubsetKey(process.env);
884
+ const key = `${cmd} ${JSON.stringify(args || [])} ${cwdKey} ${envKey}`;
885
+ const cached = PROBE_CACHE.get(key);
886
+ if (cached) {
887
+ probeTrace('probe-hit', cmd, args);
888
+ // Clone so callers that mutate the result don't poison the cache.
889
+ return {
890
+ pid: cached.pid,
891
+ status: cached.status,
892
+ signal: cached.signal,
893
+ stdout: cached.stdout,
894
+ stderr: cached.stderr,
895
+ output: cached.output ? cached.output.slice() : cached.output,
896
+ error: cached.error,
897
+ };
898
+ }
899
+ probeTrace('probe', cmd, args);
900
+ const result = cp.spawnSync(cmd, args, options);
901
+ // Don't cache spawn errors (ENOENT etc.) — they may resolve on retry with a
902
+ // different binary path.
903
+ if (result && result.error) {
904
+ return result;
905
+ }
906
+ PROBE_CACHE.set(key, {
907
+ pid: result && result.pid,
908
+ status: result && result.status,
909
+ signal: result && result.signal,
910
+ stdout: result && result.stdout,
911
+ stderr: result && result.stderr,
912
+ output: result && result.output,
913
+ error: result && result.error,
914
+ });
915
+ return result;
916
+ }
917
+
661
918
  module.exports = {
662
919
  fs,
663
920
  os,
664
921
  path,
665
922
  cp,
923
+ cachedSpawn,
666
924
  PACKAGE_ROOT,
667
925
  CLI_ENTRY_PATH,
668
926
  packageJsonPath,
@@ -680,6 +938,7 @@ module.exports = {
680
938
  GLOBAL_TOOLCHAIN_SERVICES,
681
939
  GLOBAL_TOOLCHAIN_PACKAGES,
682
940
  OPTIONAL_LOCAL_COMPANION_TOOLS,
941
+ resolveGithubCliBin,
683
942
  GH_BIN,
684
943
  REQUIRED_SYSTEM_TOOLS,
685
944
  MAINTAINER_RELEASE_REPO,
@@ -715,6 +974,8 @@ module.exports = {
715
974
  AGENTS_BOTS_STATE_RELATIVE,
716
975
  AGENTS_MARKER_START,
717
976
  AGENTS_MARKER_END,
977
+ MONOREPO_MARKER_START,
978
+ MONOREPO_MARKER_END,
718
979
  GITIGNORE_MARKER_START,
719
980
  GITIGNORE_MARKER_END,
720
981
  CODEX_WORKTREE_RELATIVE_DIR,
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  fs,
3
3
  path,
4
+ cachedSpawn,
4
5
  CLI_ENTRY_PATH,
5
6
  PACKAGE_SCRIPT_ASSETS,
6
7
  } = require('../context');
@@ -13,8 +14,12 @@ function requireValue(rawArgs, index, flagName) {
13
14
  return value;
14
15
  }
15
16
 
17
+ // Route reads through the process-scoped probe cache. cachedSpawn caches ONLY a
18
+ // strict allowlist (git geometry probes, git/gh `--version`, `which`) and falls
19
+ // through to a real spawn for everything else — writes, ref resolution, npm,
20
+ // gh auth/pr — so observable behavior is unchanged, only redundant probes drop.
16
21
  function run(cmd, args, options = {}) {
17
- return require('node:child_process').spawnSync(cmd, args, {
22
+ return cachedSpawn(cmd, args, {
18
23
  encoding: 'utf8',
19
24
  stdio: options.stdio || 'pipe',
20
25
  cwd: options.cwd,
@@ -1,8 +1,10 @@
1
1
  const {
2
2
  fs,
3
3
  path,
4
+ cachedSpawn,
4
5
  TOOL_NAME,
5
6
  SHORT_TOOL_NAME,
7
+ GH_BIN,
6
8
  LOCK_FILE_RELATIVE,
7
9
  REQUIRED_MANAGED_REPO_FILES,
8
10
  OMX_SCAFFOLD_DIRECTORIES,
@@ -10,7 +12,23 @@ const {
10
12
  AGENT_WORKTREE_RELATIVE_DIRS,
11
13
  defaultAgentWorktreeRelativeDir,
12
14
  } = require('../context');
13
- const { run, runPackageAsset } = require('../core/runtime');
15
+ const { runPackageAsset } = require('../core/runtime');
16
+
17
+ // Route doctor probe-running calls through the process-scoped probe cache.
18
+ // cachedSpawn falls through to cp.spawnSync for any non-allowlisted call
19
+ // (git commit/push/stash/checkout, gh auth login, etc.), so writes are never
20
+ // cached. Doctor fires the same read questions many times within one run
21
+ // (current branch, remote URL, worktree list, gh auth status) — caching
22
+ // those is a pure perf win.
23
+ function run(cmd, args, options = {}) {
24
+ return cachedSpawn(cmd, args, {
25
+ encoding: 'utf8',
26
+ stdio: options.stdio || 'pipe',
27
+ cwd: options.cwd,
28
+ env: options.env ? { ...process.env, ...options.env } : process.env,
29
+ timeout: options.timeout,
30
+ });
31
+ }
14
32
  const {
15
33
  currentBranchName,
16
34
  gitRefExists,
@@ -30,7 +48,7 @@ const {
30
48
  cleanupProtectedBaseSandbox,
31
49
  } = require('../sandbox');
32
50
  const { ensureOmxScaffold, configureHooks } = require('../scaffold');
33
- const { detectRecoverableAutoFinishConflict, printAutoFinishSummary } = require('../output');
51
+ const { detectRecoverableAutoFinishConflict, printAutoFinishSummary, isTerseMode } = require('../output');
34
52
  const { autoCommitWorktreeForFinish } = require('../finish');
35
53
 
36
54
  /**
@@ -100,6 +118,7 @@ function buildSandboxDoctorArgs(options, sandboxTarget) {
100
118
  if (options.skipAgents) args.push('--skip-agents');
101
119
  if (options.skipPackageJson) args.push('--skip-package-json');
102
120
  if (options.skipGitignore) args.push('--no-gitignore');
121
+ if (options.contract) args.push('--contract');
103
122
  if (!options.dropStaleLocks) args.push('--keep-stale-locks');
104
123
  args.push(options.waitForMerge ? '--wait-for-merge' : '--no-wait-for-merge');
105
124
  if (options.verboseAutoFinish) args.push('--verbose-auto-finish');
@@ -442,7 +461,7 @@ function finishDoctorSandboxBranch(blocked, metadata, options = {}) {
442
461
  };
443
462
  }
444
463
 
445
- const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
464
+ const ghBin = GH_BIN;
446
465
  if (!isCommandAvailable(ghBin)) {
447
466
  return {
448
467
  status: 'skipped',
@@ -890,7 +909,7 @@ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
890
909
 
891
910
  const originAvailable = hasOriginRemote(repoRoot);
892
911
  const explicitGhBin = Boolean(String(process.env.GUARDEX_GH_BIN || '').trim());
893
- const ghBin = process.env.GUARDEX_GH_BIN || 'gh';
912
+ const ghBin = GH_BIN;
894
913
  const ghAvailable =
895
914
  originAvailable &&
896
915
  (explicitGhBin || originRemoteLooksLikeGithub(repoRoot)) &&
@@ -1151,6 +1170,7 @@ function emitDoctorSandboxJsonOutput(nestedResult, execution) {
1151
1170
  }
1152
1171
 
1153
1172
  function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult, nestedResult, execution) {
1173
+ const terse = isTerseMode();
1154
1174
  console.log(
1155
1175
  `[${TOOL_NAME}] doctor detected protected branch '${blocked.branch}'. ` +
1156
1176
  `Running repairs in sandbox branch '${metadata.branch || 'agent/<auto>'}'.`,
@@ -1163,6 +1183,10 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1163
1183
  return;
1164
1184
  }
1165
1185
 
1186
+ // Terse mode: drop "[OK] X skipped because of Y" / "already in sync"
1187
+ // confirmations. Keep committed/failed/pending/merged states verbose so
1188
+ // operators still see action-required hints, PR URLs, branch names, and
1189
+ // file paths.
1166
1190
  if (execution.autoCommit.status === 'committed') {
1167
1191
  console.log(
1168
1192
  `[${TOOL_NAME}] Auto-committed doctor repairs in sandbox branch '${metadata.branch}'.`,
@@ -1171,22 +1195,24 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1171
1195
  console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit failed; branch left for manual follow-up.`);
1172
1196
  if (execution.autoCommit.stdout) process.stdout.write(execution.autoCommit.stdout);
1173
1197
  if (execution.autoCommit.stderr) process.stderr.write(execution.autoCommit.stderr);
1174
- } else {
1198
+ } else if (!terse) {
1175
1199
  console.log(`[${TOOL_NAME}] Doctor sandbox auto-commit skipped: ${execution.autoCommit.note}.`);
1176
1200
  }
1177
1201
 
1178
1202
  if (execution.protectedBaseRepairSync.status === 'merged') {
1179
1203
  console.log(`[${TOOL_NAME}] Fast-forwarded tracked doctor repairs into the protected branch workspace.`);
1180
- } else if (execution.protectedBaseRepairSync.status === 'unchanged') {
1181
- console.log(`[${TOOL_NAME}] Protected branch workspace already had the tracked doctor repairs.`);
1182
1204
  } else if (execution.protectedBaseRepairSync.status === 'would-merge') {
1183
1205
  console.log(`[${TOOL_NAME}] Dry run: would fast-forward tracked doctor repairs into the protected branch workspace.`);
1184
1206
  } else if (execution.protectedBaseRepairSync.status === 'failed') {
1185
1207
  console.log(`[${TOOL_NAME}] Protected branch tracked repair merge failed: ${execution.protectedBaseRepairSync.note}.`);
1186
1208
  if (execution.protectedBaseRepairSync.stdout) process.stdout.write(execution.protectedBaseRepairSync.stdout);
1187
1209
  if (execution.protectedBaseRepairSync.stderr) process.stderr.write(execution.protectedBaseRepairSync.stderr);
1188
- } else {
1189
- console.log(`[${TOOL_NAME}] Protected branch tracked repair merge skipped: ${execution.protectedBaseRepairSync.note}.`);
1210
+ } else if (!terse) {
1211
+ if (execution.protectedBaseRepairSync.status === 'unchanged') {
1212
+ console.log(`[${TOOL_NAME}] Protected branch workspace already had the tracked doctor repairs.`);
1213
+ } else {
1214
+ console.log(`[${TOOL_NAME}] Protected branch tracked repair merge skipped: ${execution.protectedBaseRepairSync.note}.`);
1215
+ }
1190
1216
  }
1191
1217
 
1192
1218
  if (execution.lockSync.status === 'synced') {
@@ -1194,8 +1220,10 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1194
1220
  `[${TOOL_NAME}] Synced repaired lock registry back to protected branch workspace (${LOCK_FILE_RELATIVE}).`,
1195
1221
  );
1196
1222
  } else if (execution.lockSync.status === 'unchanged') {
1223
+ // Kept verbose in terse mode too: downstream consumers (and tests) rely
1224
+ // on seeing the lock-registry sync stage reach a terminal state line.
1197
1225
  console.log(`[${TOOL_NAME}] Lock registry already synced in protected branch workspace.`);
1198
- } else {
1226
+ } else if (!terse) {
1199
1227
  console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${execution.lockSync.note}.`);
1200
1228
  }
1201
1229
 
@@ -1216,7 +1244,7 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1216
1244
  console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
1217
1245
  if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
1218
1246
  if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
1219
- } else {
1247
+ } else if (!terse) {
1220
1248
  console.log(`[${TOOL_NAME}] Auto-finish skipped: ${execution.finish.note}.`);
1221
1249
  }
1222
1250
 
@@ -1226,12 +1254,14 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
1226
1254
  });
1227
1255
  if (execution.omxScaffoldSync.status === 'synced') {
1228
1256
  console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
1229
- } else if (execution.omxScaffoldSync.status === 'unchanged') {
1230
- console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
1231
1257
  } else if (execution.omxScaffoldSync.status === 'would-sync') {
1232
1258
  console.log(`[${TOOL_NAME}] Dry run: would sync .omx scaffold back to protected branch workspace.`);
1233
- } else {
1234
- console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${execution.omxScaffoldSync.note}.`);
1259
+ } else if (!terse) {
1260
+ if (execution.omxScaffoldSync.status === 'unchanged') {
1261
+ console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
1262
+ } else {
1263
+ console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${execution.omxScaffoldSync.note}.`);
1264
+ }
1235
1265
  }
1236
1266
  }
1237
1267