@imdeadpool/guardex 6.1.0 → 7.0.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.
@@ -54,7 +54,6 @@ const TEMPLATE_FILES = [
54
54
  'githooks/pre-commit',
55
55
  'githooks/pre-push',
56
56
  'githooks/post-merge',
57
- 'githooks/post-checkout',
58
57
  'codex/skills/guardex/SKILL.md',
59
58
  'codex/skills/guardex-merge-skills-to-dev/SKILL.md',
60
59
  'claude/commands/guardex.md',
@@ -98,7 +97,6 @@ const EXECUTABLE_RELATIVE_PATHS = new Set([
98
97
  '.githooks/pre-commit',
99
98
  '.githooks/pre-push',
100
99
  '.githooks/post-merge',
101
- '.githooks/post-checkout',
102
100
  ]);
103
101
 
104
102
  const CRITICAL_GUARDRAIL_PATHS = new Set([
@@ -106,7 +104,6 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
106
104
  '.githooks/pre-commit',
107
105
  '.githooks/pre-push',
108
106
  '.githooks/post-merge',
109
- '.githooks/post-checkout',
110
107
  'scripts/agent-branch-start.sh',
111
108
  'scripts/agent-branch-finish.sh',
112
109
  'scripts/agent-worktree-prune.sh',
@@ -134,7 +131,6 @@ const MANAGED_GITIGNORE_PATHS = [
134
131
  '.githooks/pre-commit',
135
132
  '.githooks/pre-push',
136
133
  '.githooks/post-merge',
137
- '.githooks/post-checkout',
138
134
  'oh-my-codex/',
139
135
  '.codex/skills/guardex/SKILL.md',
140
136
  '.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
@@ -167,158 +163,84 @@ const COMMAND_TYPO_ALIASES = new Map([
167
163
  const SUGGESTIBLE_COMMANDS = [
168
164
  'status',
169
165
  'setup',
170
- 'init',
171
166
  'doctor',
172
- 'review',
173
167
  'agents',
174
168
  'finish',
175
169
  'report',
176
- 'copy-prompt',
177
- 'copy-commands',
178
170
  'protect',
179
171
  'sync',
180
172
  'cleanup',
181
- 'release',
173
+ 'prompt',
174
+ 'help',
175
+ 'version',
176
+ // deprecated aliases still routable with a warning
177
+ 'init',
182
178
  'install',
183
179
  'fix',
184
180
  'scan',
181
+ 'review',
182
+ 'copy-prompt',
183
+ 'copy-commands',
185
184
  'print-agents-snippet',
186
- 'help',
187
- 'version',
185
+ 'release',
188
186
  ];
189
187
  const CLI_COMMAND_DESCRIPTIONS = [
190
188
  ['status', 'Show GuardeX CLI + service health without modifying files'],
191
- ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore, --parent-workspace-view)'],
192
- ['init', 'Alias of setup (bootstrap + repair guardrails in a git repo)'],
193
- ['doctor', 'Repair safety setup drift, then verify repo safety'],
194
- ['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'],
195
- ['finish', 'Auto-commit completed agent branches, then run PR finish flow'],
196
- ['copy-prompt', 'Print the AI-ready setup checklist'],
197
- ['copy-commands', 'Print setup checklist as executable commands only'],
189
+ ['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
190
+ ['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
198
191
  ['protect', 'Manage protected branches (list/add/remove/set/reset)'],
199
- ['sync', 'Check or sync agent branches with origin/<base>'],
200
- ['cleanup', 'Cleanup agent branches/worktrees (watch mode defaults to 60-minute idle threshold)'],
192
+ ['sync', 'Sync agent branches with origin/<base>'],
193
+ ['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
194
+ ['cleanup', 'Prune merged/stale agent branches and worktrees'],
201
195
  ['agents', 'Start/stop repo-scoped review + cleanup bots'],
202
- ['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
203
- ['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
204
- ['scan', 'Report safety issues and exit non-zero on findings'],
205
- ['print-agents-snippet', 'Print the AGENTS.md snippet template'],
206
- ['release', 'Publish GuardeX from maintainer release repo'],
196
+ ['prompt', 'Print AI setup checklist (--exec, --snippet)'],
197
+ ['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
207
198
  ['help', 'Show this help output'],
208
199
  ['version', 'Print GuardeX version'],
209
200
  ];
210
- const CORE_COMMAND_NAMES = new Set([
211
- 'setup',
212
- 'doctor',
213
- 'status',
214
- 'finish',
215
- 'cleanup',
216
- 'sync',
217
- 'scan',
201
+ const DEPRECATED_COMMAND_ALIASES = new Map([
202
+ ['init', { target: 'setup', hint: 'gx setup' }],
203
+ ['install', { target: 'setup', hint: 'gx setup --install-only' }],
204
+ ['fix', { target: 'setup', hint: 'gx setup --repair' }],
205
+ ['scan', { target: 'status', hint: 'gx status --strict' }],
206
+ ['copy-prompt', { target: 'prompt', hint: 'gx prompt' }],
207
+ ['copy-commands', { target: 'prompt', hint: 'gx prompt --exec' }],
208
+ ['print-agents-snippet', { target: 'prompt', hint: 'gx prompt --snippet' }],
209
+ ['review', { target: 'agents', hint: 'gx agents start (runs review + cleanup)' }],
218
210
  ]);
219
211
  const AGENT_BOT_DESCRIPTIONS = [
220
- ['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
221
- ['agents', 'Start/stop both review and cleanup bots for this repo'],
212
+ ['agents', 'Start/stop review + cleanup bots for this repo'],
222
213
  ];
223
214
 
224
- const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
225
-
226
- 1) Install (if missing):
227
- npm i -g @imdeadpool/guardex
228
-
229
- 2) Bootstrap safety in this repo:
230
- gx setup
231
- # alias: gx init
232
-
233
- - Setup detects global OMX/OpenSpec/codex-auth npm packages first.
234
- - If one is missing and setup asks for approval, reply explicitly:
235
- - y = run: npm i -g oh-my-codex @fission-ai/openspec @imdeadpool/codex-account-switcher (missing ones only)
236
- - n = skip global installs
237
- - Setup also checks GitHub CLI (gh), required for PR/merge automation.
238
- - If gh is missing: install it from https://cli.github.com/ and rerun gx setup.
239
-
240
- 3) If setup reports warnings/errors, repair + re-check:
241
- gx doctor
242
-
243
- 4) Optional: start continuous PR monitor from this repo:
244
- gx review --interval 30
245
-
246
- 5) Confirm next safe agent workflow commands:
247
- bash scripts/codex-agent.sh "task" "agent-name"
248
- bash scripts/agent-branch-start.sh "task" "agent-name"
249
- python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
250
- bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --base dev --via-pr --wait-for-merge
251
- - For every new user message/task, repeat the same cycle:
252
- start isolated agent branch/worktree -> claim file locks -> implement/verify ->
253
- finish via PR/merge cleanup into dev with scripts/agent-branch-finish.sh.
254
- - Finished branches stay available by default for audit/follow-up.
255
- Remove them explicitly when done:
256
- gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
257
- - To finalize all completed agent branches in one pass:
258
- gx finish --all
259
-
260
- 6) OpenSpec default change flow (core profile):
261
- /opsx:propose <change-name>
262
- /opsx:apply
263
- /opsx:archive
264
- - Full guide: docs/openspec-getting-started.md
265
-
266
- 7) Optional: enable expanded OpenSpec workflow commands:
267
- openspec config profile <profile-name>
268
- openspec update
269
- - Expanded path: /opsx:new -> /opsx:ff or /opsx:continue -> /opsx:apply -> /opsx:verify -> /opsx:archive
270
-
271
- 8) Optional: create OpenSpec planning workspace:
272
- bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
273
-
274
- 9) Optional: protect extra branches:
275
- gx protect add release staging
276
-
277
- 10) Optional: sync your current agent branch with latest base branch:
278
- gx sync --check
279
- gx sync
280
-
281
- 11) Optional (GitHub remote cleanup): enable:
282
- Settings -> General -> Pull Requests -> Automatically delete head branches
283
-
284
- 12) Optional (fork sync with Pull app):
285
- cp .github/pull.yml.example .github/pull.yml
286
- # then edit .github/pull.yml:
287
- # - set rules[].base to your fork branch (main/master/dev)
288
- # - set rules[].upstream to upstream-owner:branch
289
- # install app: https://github.com/apps/pull
290
- # validate config: https://pull.git.ci/check/<owner>/<repo>
291
-
292
- 13) Optional (PR review bot with cr-gpt GitHub App):
293
- - install app: https://github.com/apps/cr-gpt
294
- - in GitHub repo Settings -> Secrets and variables -> Actions -> Variables:
295
- add OPENAI_API_KEY (your API key)
296
- - the app reviews new/updated pull requests automatically
297
-
298
- 14) Optional: test PR review action workflow
299
- - gx setup installs .github/workflows/cr.yml
300
- - open or update a PR
301
- - check Actions -> "Code Review" run logs + PR timeline comments
215
+ const AI_SETUP_PROMPT = `GuardeX (gx) setup checklist for Codex/Claude in this repo.
216
+
217
+ 1) Install: npm i -g @imdeadpool/guardex && gh --version
218
+ 2) Bootstrap: gx setup # installs hooks/templates + verifies; prompts Y/N for global OMX/OpenSpec/codex-auth
219
+ 3) If degraded: gx doctor # repair + re-verify
220
+ 4) Per task: bash scripts/codex-agent.sh "<task>" "<agent>"
221
+ # or manual:
222
+ # bash scripts/agent-branch-start.sh "<task>" "<agent>"
223
+ # python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
224
+ # bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --via-pr --wait-for-merge
225
+ 5) Finalize all: gx finish --all
226
+ 6) Cleanup: gx cleanup
227
+ 7) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive (see docs/openspec-getting-started.md)
228
+ 8) Protect: gx protect add release staging (optional)
229
+ 9) Sync: gx sync --check && gx sync (optional; rebase onto base)
230
+ 10) Fork sync: cp .github/pull.yml.example .github/pull.yml (optional; install https://github.com/apps/pull)
231
+ 11) PR review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY in Actions variables (uses .github/workflows/cr.yml)
232
+ 12) GitHub repo: enable Settings -> PRs -> Automatically delete head branches
302
233
  `;
303
234
 
304
235
  const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
305
236
  gh --version
306
237
  gx setup
307
238
  gx doctor
308
- gx review --interval 30
309
- bash scripts/codex-agent.sh "task" "agent-name"
310
- bash scripts/agent-branch-start.sh "task" "agent-name"
311
- python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
312
- bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --base dev --via-pr --wait-for-merge
239
+ bash scripts/codex-agent.sh "<task>" "<agent>"
313
240
  gx finish --all
314
- gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
315
- bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
316
- openspec config profile <profile-name>
317
- openspec update
241
+ gx cleanup
318
242
  gx protect add release staging
319
- gx sync --check
320
243
  gx sync
321
- cp .github/pull.yml.example .github/pull.yml
322
244
  `;
323
245
 
324
246
  const SCORECARD_RISK_BY_CHECK = {
@@ -364,15 +286,12 @@ function statusDot(status) {
364
286
  return colorize('●', '33'); // yellow for degraded/unknown
365
287
  }
366
288
 
367
- function commandCatalogLines(indent = ' ', { coreOnly = false } = {}) {
368
- const entries = coreOnly
369
- ? CLI_COMMAND_DESCRIPTIONS.filter(([name]) => CORE_COMMAND_NAMES.has(name))
370
- : CLI_COMMAND_DESCRIPTIONS;
371
- const maxCommandLength = entries.reduce(
289
+ function commandCatalogLines(indent = ' ') {
290
+ const maxCommandLength = CLI_COMMAND_DESCRIPTIONS.reduce(
372
291
  (max, [command]) => Math.max(max, command.length),
373
292
  0,
374
293
  );
375
- return entries.map(
294
+ return CLI_COMMAND_DESCRIPTIONS.map(
376
295
  ([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
377
296
  );
378
297
  }
@@ -389,7 +308,7 @@ function agentBotCatalogLines(indent = ' ') {
389
308
 
390
309
  function printToolLogsSummary() {
391
310
  const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
392
- const commandDetails = commandCatalogLines(' ', { coreOnly: true });
311
+ const commandDetails = commandCatalogLines(' ');
393
312
  const agentBotDetails = agentBotCatalogLines(' ');
394
313
 
395
314
  if (!supportsAnsiColors()) {
@@ -434,7 +353,7 @@ function printToolLogsSummary() {
434
353
  }
435
354
  console.log(` ${pipe}${line.slice(2)}`);
436
355
  }
437
- console.log(` ${corner}─ ${colorize(`Try '${SHORT_TOOL_NAME} doctor' to repair drift, or '${SHORT_TOOL_NAME} help' for the full command list.`, '2')}`);
356
+ console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
438
357
  }
439
358
 
440
359
  function usage(options = {}) {
@@ -455,19 +374,12 @@ AGENT BOT
455
374
  ${agentBotCatalogLines().join('\n')}
456
375
 
457
376
  NOTES
458
- - Running ${TOOL_NAME} with no command defaults to: ${SHORT_TOOL_NAME} status
459
- - Short alias: ${SHORT_TOOL_NAME}
460
- - ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
461
- - ${TOOL_NAME} setup asks for Y/N approval before global installs
462
- - ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
463
- - For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
464
- - Optional parent-folder Source Control view: ${SHORT_TOOL_NAME} setup --target <repo-path> --parent-workspace-view
465
- - In initialized repos, setup/install/fix block in-place writes on protected main by default
466
- - setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch
467
- - doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
468
- - agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
469
- - use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
470
- - Legacy command aliases are still supported: ${LEGACY_NAMES.join(', ')}`);
377
+ - No command = ${SHORT_TOOL_NAME} status. ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup.
378
+ - Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
379
+ - Target another repo: ${SHORT_TOOL_NAME} <cmd> --target <repo-path>.
380
+ - On protected main, setup/install/fix/doctor auto-sandbox via agent branch + PR flow.
381
+ - Run '${SHORT_TOOL_NAME} cleanup' to prune merged agent branches/worktrees.
382
+ - Legacy aliases: ${LEGACY_NAMES.join(', ')}.`);
471
383
 
472
384
  if (outsideGitRepo) {
473
385
  console.log(`
@@ -513,6 +425,90 @@ function isGitRepo(targetPath) {
513
425
  return result.status === 0;
514
426
  }
515
427
 
428
+ const NESTED_REPO_DEFAULT_MAX_DEPTH = 6;
429
+ const NESTED_REPO_DEFAULT_SKIP_DIRS = new Set([
430
+ 'node_modules',
431
+ '.git',
432
+ 'dist',
433
+ 'build',
434
+ '.next',
435
+ '.cache',
436
+ 'target',
437
+ 'vendor',
438
+ '.venv',
439
+ '.pnpm-store',
440
+ ]);
441
+ const NESTED_REPO_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
442
+
443
+ function discoverNestedGitRepos(rootPath, opts = {}) {
444
+ const maxDepth = Number.isFinite(opts.maxDepth) ? Math.max(1, opts.maxDepth) : NESTED_REPO_DEFAULT_MAX_DEPTH;
445
+ const extraSkip = new Set(Array.isArray(opts.extraSkip) ? opts.extraSkip : []);
446
+ const includeSubmodules = Boolean(opts.includeSubmodules);
447
+ const resolvedRoot = path.resolve(rootPath);
448
+
449
+ const rootCommonDir = (() => {
450
+ const result = run('git', ['-C', resolvedRoot, 'rev-parse', '--git-common-dir'], { cwd: resolvedRoot });
451
+ if (result.status !== 0) return null;
452
+ const raw = result.stdout.trim();
453
+ if (!raw) return null;
454
+ return path.resolve(resolvedRoot, raw);
455
+ })();
456
+
457
+ const workreeSkipAbsolute = path.join(resolvedRoot, NESTED_REPO_WORKTREE_RELATIVE_DIR);
458
+ const found = new Set();
459
+ found.add(resolvedRoot);
460
+
461
+ function shouldSkipDir(dirName) {
462
+ return NESTED_REPO_DEFAULT_SKIP_DIRS.has(dirName) || extraSkip.has(dirName);
463
+ }
464
+
465
+ function walk(currentPath, depth) {
466
+ if (depth > maxDepth) return;
467
+ let entries;
468
+ try {
469
+ entries = fs.readdirSync(currentPath, { withFileTypes: true });
470
+ } catch {
471
+ return;
472
+ }
473
+
474
+ for (const entry of entries) {
475
+ const entryPath = path.join(currentPath, entry.name);
476
+
477
+ if (entry.name === '.git') {
478
+ if (entry.isDirectory()) {
479
+ if (entryPath === path.join(resolvedRoot, '.git')) continue;
480
+ found.add(path.dirname(entryPath));
481
+ } else if (includeSubmodules && entry.isFile()) {
482
+ found.add(path.dirname(entryPath));
483
+ }
484
+ continue;
485
+ }
486
+
487
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
488
+ if (shouldSkipDir(entry.name)) continue;
489
+ if (entryPath === workreeSkipAbsolute) continue;
490
+ walk(entryPath, depth + 1);
491
+ }
492
+ }
493
+
494
+ walk(resolvedRoot, 0);
495
+
496
+ const filtered = Array.from(found).filter((repoPath) => {
497
+ if (repoPath === resolvedRoot) return true;
498
+ if (!rootCommonDir) return true;
499
+ const childResult = run('git', ['-C', repoPath, 'rev-parse', '--git-common-dir'], { cwd: repoPath });
500
+ if (childResult.status !== 0) return true;
501
+ const childCommonDirRaw = childResult.stdout.trim();
502
+ if (!childCommonDirRaw) return true;
503
+ const childCommonDir = path.resolve(repoPath, childCommonDirRaw);
504
+ return childCommonDir !== rootCommonDir;
505
+ });
506
+
507
+ const [root, ...rest] = filtered;
508
+ rest.sort((a, b) => a.localeCompare(b));
509
+ return [root, ...rest];
510
+ }
511
+
516
512
  function toDestinationPath(relativeTemplatePath) {
517
513
  if (relativeTemplatePath.startsWith('scripts/')) {
518
514
  return relativeTemplatePath;
@@ -709,7 +705,8 @@ function writeLockState(repoRoot, payload, dryRun) {
709
705
  fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
710
706
  }
711
707
 
712
- function ensurePackageScripts(repoRoot, dryRun) {
708
+ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
709
+ const force = Boolean(options.force);
713
710
  const packagePath = path.join(repoRoot, 'package.json');
714
711
  if (!fs.existsSync(packagePath)) {
715
712
  return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
@@ -722,30 +719,15 @@ function ensurePackageScripts(repoRoot, dryRun) {
722
719
  throw new Error(`Unable to parse package.json in target repo: ${error.message}`);
723
720
  }
724
721
 
725
- const wantedScripts = {
726
- 'agent:codex': 'bash ./scripts/codex-agent.sh',
727
- 'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
728
- 'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
729
- 'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
730
- 'agent:finish': `${SHORT_TOOL_NAME} finish --all`,
731
- 'agent:cleanup': `${SHORT_TOOL_NAME} cleanup`,
732
- 'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
733
- 'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
734
- 'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
735
- 'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
736
- 'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
737
- 'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
738
- 'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
739
- 'agent:protect:list': `${SHORT_TOOL_NAME} protect list`,
740
- 'agent:branch:sync': `${SHORT_TOOL_NAME} sync`,
741
- 'agent:branch:sync:check': `${SHORT_TOOL_NAME} sync --check`,
742
- 'agent:safety:setup': `${SHORT_TOOL_NAME} setup`,
743
- 'agent:safety:scan': `${SHORT_TOOL_NAME} scan`,
744
- 'agent:safety:fix': `${SHORT_TOOL_NAME} fix`,
745
- 'agent:safety:doctor': `${SHORT_TOOL_NAME} doctor`,
746
- };
722
+ const existingScripts = pkg.scripts && typeof pkg.scripts === 'object'
723
+ ? pkg.scripts
724
+ : {};
725
+ const hasExistingAgentScripts = Object.keys(existingScripts).some((key) => key.startsWith('agent:'));
726
+ if (hasExistingAgentScripts && !force) {
727
+ return { status: 'unchanged', file: 'package.json', note: 'preserved existing agent:* scripts' };
728
+ }
747
729
 
748
- pkg.scripts = pkg.scripts || {};
730
+ pkg.scripts = existingScripts;
749
731
  let changed = false;
750
732
  for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
751
733
  if (pkg.scripts[key] !== value) {
@@ -765,7 +747,8 @@ function ensurePackageScripts(repoRoot, dryRun) {
765
747
  return { status: 'updated', file: 'package.json' };
766
748
  }
767
749
 
768
- function ensureAgentsSnippet(repoRoot, dryRun) {
750
+ function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
751
+ const force = Boolean(options.force);
769
752
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
770
753
  const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
771
754
  const managedRegex = new RegExp(
@@ -782,6 +765,9 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
782
765
 
783
766
  const existing = fs.readFileSync(agentsPath, 'utf8');
784
767
  if (managedRegex.test(existing)) {
768
+ if (!force) {
769
+ return { status: 'unchanged', file: 'AGENTS.md', note: 'preserved existing guardex-managed block' };
770
+ }
785
771
  const next = existing.replace(managedRegex, snippet);
786
772
  if (next === existing) {
787
773
  return { status: 'unchanged', file: 'AGENTS.md' };
@@ -925,10 +911,18 @@ function parseCommonArgs(rawArgs, defaults) {
925
911
  }
926
912
 
927
913
  function parseSetupArgs(rawArgs, defaults) {
928
- const setupDefaults = { ...defaults, parentWorkspaceView: false };
914
+ const setupDefaults = {
915
+ ...defaults,
916
+ parentWorkspaceView: false,
917
+ recursive: true,
918
+ nestedMaxDepth: NESTED_REPO_DEFAULT_MAX_DEPTH,
919
+ nestedSkipDirs: [],
920
+ includeSubmodules: false,
921
+ };
929
922
  const forwardedArgs = [];
930
923
 
931
- for (const arg of rawArgs) {
924
+ for (let index = 0; index < rawArgs.length; index += 1) {
925
+ const arg = rawArgs[index];
932
926
  if (arg === '--parent-workspace-view') {
933
927
  setupDefaults.parentWorkspaceView = true;
934
928
  continue;
@@ -937,6 +931,34 @@ function parseSetupArgs(rawArgs, defaults) {
937
931
  setupDefaults.parentWorkspaceView = false;
938
932
  continue;
939
933
  }
934
+ if (arg === '--no-recursive' || arg === '--no-nested' || arg === '--single-repo') {
935
+ setupDefaults.recursive = false;
936
+ continue;
937
+ }
938
+ if (arg === '--recursive' || arg === '--nested') {
939
+ setupDefaults.recursive = true;
940
+ continue;
941
+ }
942
+ if (arg === '--max-depth') {
943
+ const raw = requireValue(rawArgs, index, '--max-depth');
944
+ const parsed = Number.parseInt(raw, 10);
945
+ if (!Number.isFinite(parsed) || parsed < 1) {
946
+ throw new Error('--max-depth requires a positive integer');
947
+ }
948
+ setupDefaults.nestedMaxDepth = parsed;
949
+ index += 1;
950
+ continue;
951
+ }
952
+ if (arg === '--skip-nested') {
953
+ const raw = requireValue(rawArgs, index, '--skip-nested');
954
+ setupDefaults.nestedSkipDirs.push(raw);
955
+ index += 1;
956
+ continue;
957
+ }
958
+ if (arg === '--include-submodules') {
959
+ setupDefaults.includeSubmodules = true;
960
+ continue;
961
+ }
940
962
  forwardedArgs.push(arg);
941
963
  }
942
964
 
@@ -1092,6 +1114,7 @@ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
1092
1114
  function buildSandboxDoctorArgs(options, sandboxTarget) {
1093
1115
  const args = ['doctor', '--target', sandboxTarget];
1094
1116
  if (options.dryRun) args.push('--dry-run');
1117
+ if (options.force) args.push('--force');
1095
1118
  if (options.skipAgents) args.push('--skip-agents');
1096
1119
  if (options.skipPackageJson) args.push('--skip-package-json');
1097
1120
  if (options.skipGitignore) args.push('--no-gitignore');
@@ -3528,11 +3551,11 @@ function runInstallInternal(options) {
3528
3551
  }
3529
3552
 
3530
3553
  if (!options.skipPackageJson) {
3531
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun)));
3554
+ operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3532
3555
  }
3533
3556
 
3534
3557
  if (!options.skipAgents) {
3535
- operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun)));
3558
+ operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3536
3559
  }
3537
3560
 
3538
3561
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -3582,11 +3605,11 @@ function runFixInternal(options) {
3582
3605
  }
3583
3606
 
3584
3607
  if (!options.skipPackageJson) {
3585
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun)));
3608
+ operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3586
3609
  }
3587
3610
 
3588
3611
  if (!options.skipAgents) {
3589
- operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun)));
3612
+ operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3590
3613
  }
3591
3614
 
3592
3615
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -4453,23 +4476,87 @@ function setup(rawArgs) {
4453
4476
  }
4454
4477
  }
4455
4478
 
4456
- assertProtectedMainWriteAllowed(options, 'setup');
4457
- const installPayload = runInstallInternal(options);
4458
- installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)));
4459
- if (options.parentWorkspaceView) {
4460
- installPayload.operations.push(ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun)));
4479
+ const topRepoRoot = resolveRepoRoot(options.target);
4480
+ const discoveredRepos = options.recursive
4481
+ ? discoverNestedGitRepos(topRepoRoot, {
4482
+ maxDepth: options.nestedMaxDepth,
4483
+ extraSkip: options.nestedSkipDirs,
4484
+ includeSubmodules: options.includeSubmodules,
4485
+ })
4486
+ : [topRepoRoot];
4487
+
4488
+ if (discoveredRepos.length > 1) {
4489
+ console.log(
4490
+ `[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. Installing into each (use --no-recursive to limit to the top-level).`,
4491
+ );
4492
+ for (const repoPath of discoveredRepos) {
4493
+ const marker = repoPath === topRepoRoot ? ' (top-level)' : '';
4494
+ console.log(`[${TOOL_NAME}] - ${repoPath}${marker}`);
4495
+ }
4461
4496
  }
4462
- printOperations('Setup/install', installPayload, options.dryRun);
4463
4497
 
4464
- const fixPayload = runFixInternal({
4465
- target: options.target,
4466
- dryRun: options.dryRun,
4467
- dropStaleLocks: true,
4468
- skipAgents: options.skipAgents,
4469
- skipPackageJson: options.skipPackageJson,
4470
- skipGitignore: options.skipGitignore,
4471
- });
4472
- printOperations('Setup/fix', fixPayload, options.dryRun);
4498
+ let aggregateErrors = 0;
4499
+ let aggregateWarnings = 0;
4500
+ let lastScanResult = null;
4501
+
4502
+ for (const repoPath of discoveredRepos) {
4503
+ const perRepoOptions = { ...options, target: repoPath };
4504
+ const repoLabel = discoveredRepos.length > 1 ? ` [${path.relative(topRepoRoot, repoPath) || '.'}]` : '';
4505
+
4506
+ if (discoveredRepos.length > 1) {
4507
+ console.log(`[${TOOL_NAME}] ── Setup target: ${repoPath} ──`);
4508
+ }
4509
+
4510
+ assertProtectedMainWriteAllowed(perRepoOptions, 'setup');
4511
+ const installPayload = runInstallInternal(perRepoOptions);
4512
+ installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(perRepoOptions.dryRun)));
4513
+ if (perRepoOptions.parentWorkspaceView) {
4514
+ installPayload.operations.push(ensureParentWorkspaceView(installPayload.repoRoot, Boolean(perRepoOptions.dryRun)));
4515
+ }
4516
+ printOperations(`Setup/install${repoLabel}`, installPayload, perRepoOptions.dryRun);
4517
+
4518
+ const fixPayload = runFixInternal({
4519
+ target: repoPath,
4520
+ dryRun: perRepoOptions.dryRun,
4521
+ force: perRepoOptions.force,
4522
+ dropStaleLocks: true,
4523
+ skipAgents: perRepoOptions.skipAgents,
4524
+ skipPackageJson: perRepoOptions.skipPackageJson,
4525
+ skipGitignore: perRepoOptions.skipGitignore,
4526
+ });
4527
+ printOperations(`Setup/fix${repoLabel}`, fixPayload, perRepoOptions.dryRun);
4528
+
4529
+ if (perRepoOptions.dryRun) {
4530
+ continue;
4531
+ }
4532
+
4533
+ if (perRepoOptions.parentWorkspaceView) {
4534
+ const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
4535
+ console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
4536
+ }
4537
+
4538
+ const scanResult = runScanInternal({ target: repoPath, json: false });
4539
+ const currentBaseBranch = currentBranchName(scanResult.repoRoot);
4540
+ const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
4541
+ baseBranch: currentBaseBranch,
4542
+ dryRun: perRepoOptions.dryRun,
4543
+ });
4544
+ printScanResult(scanResult, false);
4545
+ if (autoFinishSummary.enabled) {
4546
+ console.log(
4547
+ `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
4548
+ );
4549
+ for (const detail of autoFinishSummary.details) {
4550
+ console.log(`[${TOOL_NAME}] ${detail}`);
4551
+ }
4552
+ } else if (autoFinishSummary.details.length > 0) {
4553
+ console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
4554
+ }
4555
+
4556
+ aggregateErrors += scanResult.errors;
4557
+ aggregateWarnings += scanResult.warnings;
4558
+ lastScanResult = scanResult;
4559
+ }
4473
4560
 
4474
4561
  if (options.dryRun) {
4475
4562
  console.log(`[${TOOL_NAME}] Dry run setup done.`);
@@ -4477,32 +4564,11 @@ function setup(rawArgs) {
4477
4564
  return;
4478
4565
  }
4479
4566
 
4480
- if (options.parentWorkspaceView) {
4481
- const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
4482
- console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
4483
- }
4484
-
4485
- const scanResult = runScanInternal({ target: options.target, json: false });
4486
- const currentBaseBranch = currentBranchName(scanResult.repoRoot);
4487
- const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
4488
- baseBranch: currentBaseBranch,
4489
- dryRun: options.dryRun,
4490
- });
4491
- printScanResult(scanResult, false);
4492
- if (autoFinishSummary.enabled) {
4493
- console.log(
4494
- `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
4495
- );
4496
- for (const detail of autoFinishSummary.details) {
4497
- console.log(`[${TOOL_NAME}] ${detail}`);
4498
- }
4499
- } else if (autoFinishSummary.details.length > 0) {
4500
- console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
4501
- }
4502
-
4503
- if (scanResult.errors === 0 && scanResult.warnings === 0) {
4504
- console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
4505
- console.log(`[${TOOL_NAME}] Copy AI setup prompt with: ${SHORT_TOOL_NAME} copy-prompt`);
4567
+ if (aggregateErrors === 0 && aggregateWarnings === 0) {
4568
+ const repoCount = discoveredRepos.length;
4569
+ const suffix = repoCount > 1 ? ` (${repoCount} repos)` : '';
4570
+ console.log(`[${TOOL_NAME}] ✅ Setup complete.${suffix}`);
4571
+ console.log(`[${TOOL_NAME}] Copy AI setup prompt with: ${SHORT_TOOL_NAME} prompt`);
4506
4572
  console.log(
4507
4573
  `[${TOOL_NAME}] OpenSpec core workflow: /opsx:propose -> /opsx:apply -> /opsx:archive`,
4508
4574
  );
@@ -4512,7 +4578,13 @@ function setup(rawArgs) {
4512
4578
  console.log(`[${TOOL_NAME}] OpenSpec guide: docs/openspec-getting-started.md`);
4513
4579
  }
4514
4580
 
4515
- setExitCodeFromScan(scanResult);
4581
+ if (lastScanResult) {
4582
+ setExitCodeFromScan({
4583
+ ...lastScanResult,
4584
+ errors: aggregateErrors,
4585
+ warnings: aggregateWarnings,
4586
+ });
4587
+ }
4516
4588
  }
4517
4589
 
4518
4590
  function ensureMainBranch(repoRoot) {
@@ -4811,6 +4883,31 @@ function copyCommands() {
4811
4883
  process.exitCode = 0;
4812
4884
  }
4813
4885
 
4886
+ function prompt(rawArgs) {
4887
+ const args = Array.isArray(rawArgs) ? rawArgs : [];
4888
+ let variant = 'prompt';
4889
+ for (const arg of args) {
4890
+ if (arg === '--exec' || arg === '--commands') variant = 'exec';
4891
+ else if (arg === '--snippet' || arg === '--agents') variant = 'snippet';
4892
+ else if (arg === '--prompt' || arg === '--full') variant = 'prompt';
4893
+ else if (arg === '-h' || arg === '--help') variant = 'help';
4894
+ else throw new Error(`Unknown option: ${arg}`);
4895
+ }
4896
+ if (variant === 'help') {
4897
+ console.log(
4898
+ `${SHORT_TOOL_NAME} prompt commands:\n` +
4899
+ ` ${SHORT_TOOL_NAME} prompt Print AI setup checklist\n` +
4900
+ ` ${SHORT_TOOL_NAME} prompt --exec Print setup commands only (shell-ready)\n` +
4901
+ ` ${SHORT_TOOL_NAME} prompt --snippet Print the AGENTS.md managed-block template`,
4902
+ );
4903
+ process.exitCode = 0;
4904
+ return;
4905
+ }
4906
+ if (variant === 'exec') return copyCommands();
4907
+ if (variant === 'snippet') return printAgentsSnippet();
4908
+ return copyPrompt();
4909
+ }
4910
+
4814
4911
  function cleanup(rawArgs) {
4815
4912
  const options = parseCleanupArgs(rawArgs);
4816
4913
  const repoRoot = resolveRepoRoot(options.target);
@@ -5289,6 +5386,29 @@ function normalizeCommandOrThrow(command) {
5289
5386
  return command;
5290
5387
  }
5291
5388
 
5389
+ function warnDeprecatedAlias(aliasName) {
5390
+ const entry = DEPRECATED_COMMAND_ALIASES.get(aliasName);
5391
+ if (!entry) return;
5392
+ console.error(
5393
+ `[${TOOL_NAME}] '${aliasName}' is deprecated and will be removed in a future major release. ` +
5394
+ `Use: ${entry.hint}`,
5395
+ );
5396
+ }
5397
+
5398
+ function extractFlag(args, ...names) {
5399
+ const flagSet = new Set(names);
5400
+ let found = false;
5401
+ const remaining = [];
5402
+ for (const arg of args) {
5403
+ if (flagSet.has(arg)) {
5404
+ found = true;
5405
+ } else {
5406
+ remaining.push(arg);
5407
+ }
5408
+ }
5409
+ return { found, remaining };
5410
+ }
5411
+
5292
5412
  function main() {
5293
5413
  const args = process.argv.slice(2);
5294
5414
 
@@ -5312,90 +5432,42 @@ function main() {
5312
5432
  return;
5313
5433
  }
5314
5434
 
5315
- if (command === 'status') {
5316
- status(rest);
5317
- return;
5318
- }
5319
-
5320
- if (command === 'setup' || command === 'init') {
5321
- setup(rest);
5322
- return;
5435
+ // Deprecated direct aliases — route to new surface and warn once.
5436
+ if (DEPRECATED_COMMAND_ALIASES.has(command)) {
5437
+ warnDeprecatedAlias(command);
5438
+ if (command === 'init') return setup(rest);
5439
+ if (command === 'install') return install(rest);
5440
+ if (command === 'fix') return fix(rest);
5441
+ if (command === 'scan') return scan(rest);
5442
+ if (command === 'copy-prompt') return copyPrompt();
5443
+ if (command === 'copy-commands') return copyCommands();
5444
+ if (command === 'print-agents-snippet') return printAgentsSnippet();
5445
+ if (command === 'review') return review(rest);
5323
5446
  }
5324
5447
 
5325
- if (command === 'doctor') {
5326
- doctor(rest);
5327
- return;
5328
- }
5329
-
5330
- if (command === 'review') {
5331
- review(rest);
5332
- return;
5333
- }
5334
-
5335
- if (command === 'agents') {
5336
- agents(rest);
5337
- return;
5338
- }
5339
-
5340
- if (command === 'finish') {
5341
- finish(rest);
5342
- return;
5343
- }
5344
-
5345
- if (command === 'report') {
5346
- report(rest);
5347
- return;
5348
- }
5349
-
5350
- if (command === 'copy-prompt') {
5351
- copyPrompt();
5352
- return;
5353
- }
5354
-
5355
- if (command === 'copy-commands') {
5356
- copyCommands();
5357
- return;
5358
- }
5359
-
5360
- if (command === 'protect') {
5361
- protect(rest);
5362
- return;
5363
- }
5364
-
5365
- if (command === 'sync') {
5366
- sync(rest);
5367
- return;
5368
- }
5369
-
5370
- if (command === 'cleanup') {
5371
- cleanup(rest);
5372
- return;
5373
- }
5374
-
5375
- if (command === 'release') {
5376
- release(rest);
5377
- return;
5378
- }
5379
-
5380
- if (command === 'install') {
5381
- install(rest);
5382
- return;
5383
- }
5384
-
5385
- if (command === 'fix') {
5386
- fix(rest);
5387
- return;
5388
- }
5389
-
5390
- if (command === 'scan') {
5391
- scan(rest);
5392
- return;
5393
- }
5394
-
5395
- if (command === 'print-agents-snippet') {
5396
- printAgentsSnippet();
5397
- return;
5398
- }
5448
+ if (command === 'status') {
5449
+ const { found: strict, remaining } = extractFlag(rest, '--strict');
5450
+ if (strict) return scan(remaining);
5451
+ return status(remaining);
5452
+ }
5453
+
5454
+ if (command === 'setup') {
5455
+ const installOnly = extractFlag(rest, '--install-only', '--only-install');
5456
+ if (installOnly.found) return install(installOnly.remaining);
5457
+ const repairOnly = extractFlag(installOnly.remaining, '--repair', '--fix-only');
5458
+ if (repairOnly.found) return fix(repairOnly.remaining);
5459
+ return setup(repairOnly.remaining);
5460
+ }
5461
+
5462
+ if (command === 'prompt') return prompt(rest);
5463
+ if (command === 'doctor') return doctor(rest);
5464
+ if (command === 'agents') return agents(rest);
5465
+ if (command === 'finish') return finish(rest);
5466
+ if (command === 'report') return report(rest);
5467
+ if (command === 'protect') return protect(rest);
5468
+ if (command === 'sync') return sync(rest);
5469
+ if (command === 'cleanup') return cleanup(rest);
5470
+ if (command === 'release') return release(rest);
5399
5471
 
5400
5472
  const suggestion = maybeSuggestCommand(command);
5401
5473
  if (suggestion) {