@imdeadpool/guardex 6.0.1 → 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.
@@ -163,149 +163,84 @@ const COMMAND_TYPO_ALIASES = new Map([
163
163
  const SUGGESTIBLE_COMMANDS = [
164
164
  'status',
165
165
  'setup',
166
- 'init',
167
166
  'doctor',
168
- 'review',
169
167
  'agents',
170
168
  'finish',
171
169
  'report',
172
- 'copy-prompt',
173
- 'copy-commands',
174
170
  'protect',
175
171
  'sync',
176
172
  'cleanup',
177
- 'release',
173
+ 'prompt',
174
+ 'help',
175
+ 'version',
176
+ // deprecated aliases still routable with a warning
177
+ 'init',
178
178
  'install',
179
179
  'fix',
180
180
  'scan',
181
+ 'review',
182
+ 'copy-prompt',
183
+ 'copy-commands',
181
184
  'print-agents-snippet',
182
- 'help',
183
- 'version',
185
+ 'release',
184
186
  ];
185
187
  const CLI_COMMAND_DESCRIPTIONS = [
186
188
  ['status', 'Show GuardeX CLI + service health without modifying files'],
187
- ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore, --parent-workspace-view)'],
188
- ['init', 'Alias of setup (bootstrap + repair guardrails in a git repo)'],
189
- ['doctor', 'Repair safety setup drift, then verify repo safety'],
190
- ['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'],
191
- ['finish', 'Auto-commit completed agent branches, then run PR finish flow'],
192
- ['copy-prompt', 'Print the AI-ready setup checklist'],
193
- ['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)'],
194
191
  ['protect', 'Manage protected branches (list/add/remove/set/reset)'],
195
- ['sync', 'Check or sync agent branches with origin/<base>'],
196
- ['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'],
197
195
  ['agents', 'Start/stop repo-scoped review + cleanup bots'],
198
- ['install', 'Install templates/locks/hooks without running full setup (supports --no-gitignore)'],
199
- ['fix', 'Repair broken or missing guardrail files/config (supports --no-gitignore)'],
200
- ['scan', 'Report safety issues and exit non-zero on findings'],
201
- ['print-agents-snippet', 'Print the AGENTS.md snippet template'],
202
- ['release', 'Publish GuardeX from maintainer release repo'],
196
+ ['prompt', 'Print AI setup checklist (--exec, --snippet)'],
197
+ ['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
203
198
  ['help', 'Show this help output'],
204
199
  ['version', 'Print GuardeX version'],
205
200
  ];
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)' }],
210
+ ]);
206
211
  const AGENT_BOT_DESCRIPTIONS = [
207
- ['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
208
- ['agents', 'Start/stop both review and cleanup bots for this repo'],
212
+ ['agents', 'Start/stop review + cleanup bots for this repo'],
209
213
  ];
210
214
 
211
- const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
212
-
213
- 1) Install (if missing):
214
- npm i -g @imdeadpool/guardex
215
-
216
- 2) Bootstrap safety in this repo:
217
- gx setup
218
- # alias: gx init
219
-
220
- - Setup detects global OMX/OpenSpec/codex-auth npm packages first.
221
- - If one is missing and setup asks for approval, reply explicitly:
222
- - y = run: npm i -g oh-my-codex @fission-ai/openspec @imdeadpool/codex-account-switcher (missing ones only)
223
- - n = skip global installs
224
- - Setup also checks GitHub CLI (gh), required for PR/merge automation.
225
- - If gh is missing: install it from https://cli.github.com/ and rerun gx setup.
226
-
227
- 3) If setup reports warnings/errors, repair + re-check:
228
- gx doctor
229
-
230
- 4) Optional: start continuous PR monitor from this repo:
231
- gx review --interval 30
232
-
233
- 5) Confirm next safe agent workflow commands:
234
- bash scripts/codex-agent.sh "task" "agent-name"
235
- bash scripts/agent-branch-start.sh "task" "agent-name"
236
- python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
237
- bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --base dev --via-pr --wait-for-merge
238
- - For every new user message/task, repeat the same cycle:
239
- start isolated agent branch/worktree -> claim file locks -> implement/verify ->
240
- finish via PR/merge cleanup into dev with scripts/agent-branch-finish.sh.
241
- - Finished branches stay available by default for audit/follow-up.
242
- Remove them explicitly when done:
243
- gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
244
- - To finalize all completed agent branches in one pass:
245
- gx finish --all
246
-
247
- 6) OpenSpec default change flow (core profile):
248
- /opsx:propose <change-name>
249
- /opsx:apply
250
- /opsx:archive
251
- - Full guide: docs/openspec-getting-started.md
252
-
253
- 7) Optional: enable expanded OpenSpec workflow commands:
254
- openspec config profile <profile-name>
255
- openspec update
256
- - Expanded path: /opsx:new -> /opsx:ff or /opsx:continue -> /opsx:apply -> /opsx:verify -> /opsx:archive
257
-
258
- 8) Optional: create OpenSpec planning workspace:
259
- bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
260
-
261
- 9) Optional: protect extra branches:
262
- gx protect add release staging
263
-
264
- 10) Optional: sync your current agent branch with latest base branch:
265
- gx sync --check
266
- gx sync
267
-
268
- 11) Optional (GitHub remote cleanup): enable:
269
- Settings -> General -> Pull Requests -> Automatically delete head branches
270
-
271
- 12) Optional (fork sync with Pull app):
272
- cp .github/pull.yml.example .github/pull.yml
273
- # then edit .github/pull.yml:
274
- # - set rules[].base to your fork branch (main/master/dev)
275
- # - set rules[].upstream to upstream-owner:branch
276
- # install app: https://github.com/apps/pull
277
- # validate config: https://pull.git.ci/check/<owner>/<repo>
278
-
279
- 13) Optional (PR review bot with cr-gpt GitHub App):
280
- - install app: https://github.com/apps/cr-gpt
281
- - in GitHub repo Settings -> Secrets and variables -> Actions -> Variables:
282
- add OPENAI_API_KEY (your API key)
283
- - the app reviews new/updated pull requests automatically
284
-
285
- 14) Optional: test PR review action workflow
286
- - gx setup installs .github/workflows/cr.yml
287
- - open or update a PR
288
- - 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
289
233
  `;
290
234
 
291
235
  const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
292
236
  gh --version
293
237
  gx setup
294
238
  gx doctor
295
- gx review --interval 30
296
- bash scripts/codex-agent.sh "task" "agent-name"
297
- bash scripts/agent-branch-start.sh "task" "agent-name"
298
- python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
299
- 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>"
300
240
  gx finish --all
301
- gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
302
- bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
303
- openspec config profile <profile-name>
304
- openspec update
241
+ gx cleanup
305
242
  gx protect add release staging
306
- gx sync --check
307
243
  gx sync
308
- cp .github/pull.yml.example .github/pull.yml
309
244
  `;
310
245
 
311
246
  const SCORECARD_RISK_BY_CHECK = {
@@ -439,19 +374,12 @@ AGENT BOT
439
374
  ${agentBotCatalogLines().join('\n')}
440
375
 
441
376
  NOTES
442
- - Running ${TOOL_NAME} with no command defaults to: ${SHORT_TOOL_NAME} status
443
- - Short alias: ${SHORT_TOOL_NAME}
444
- - ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
445
- - ${TOOL_NAME} setup asks for Y/N approval before global installs
446
- - ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
447
- - For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
448
- - Optional parent-folder Source Control view: ${SHORT_TOOL_NAME} setup --target <repo-path> --parent-workspace-view
449
- - In initialized repos, setup/install/fix block in-place writes on protected main by default
450
- - setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch
451
- - doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
452
- - agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
453
- - use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
454
- - 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(', ')}.`);
455
383
 
456
384
  if (outsideGitRepo) {
457
385
  console.log(`
@@ -497,6 +425,90 @@ function isGitRepo(targetPath) {
497
425
  return result.status === 0;
498
426
  }
499
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
+
500
512
  function toDestinationPath(relativeTemplatePath) {
501
513
  if (relativeTemplatePath.startsWith('scripts/')) {
502
514
  return relativeTemplatePath;
@@ -693,7 +705,8 @@ function writeLockState(repoRoot, payload, dryRun) {
693
705
  fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
694
706
  }
695
707
 
696
- function ensurePackageScripts(repoRoot, dryRun) {
708
+ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
709
+ const force = Boolean(options.force);
697
710
  const packagePath = path.join(repoRoot, 'package.json');
698
711
  if (!fs.existsSync(packagePath)) {
699
712
  return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
@@ -706,30 +719,15 @@ function ensurePackageScripts(repoRoot, dryRun) {
706
719
  throw new Error(`Unable to parse package.json in target repo: ${error.message}`);
707
720
  }
708
721
 
709
- const wantedScripts = {
710
- 'agent:codex': 'bash ./scripts/codex-agent.sh',
711
- 'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
712
- 'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
713
- 'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
714
- 'agent:finish': `${SHORT_TOOL_NAME} finish --all`,
715
- 'agent:cleanup': `${SHORT_TOOL_NAME} cleanup`,
716
- 'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
717
- 'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
718
- 'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
719
- 'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
720
- 'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
721
- 'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
722
- 'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
723
- 'agent:protect:list': `${SHORT_TOOL_NAME} protect list`,
724
- 'agent:branch:sync': `${SHORT_TOOL_NAME} sync`,
725
- 'agent:branch:sync:check': `${SHORT_TOOL_NAME} sync --check`,
726
- 'agent:safety:setup': `${SHORT_TOOL_NAME} setup`,
727
- 'agent:safety:scan': `${SHORT_TOOL_NAME} scan`,
728
- 'agent:safety:fix': `${SHORT_TOOL_NAME} fix`,
729
- 'agent:safety:doctor': `${SHORT_TOOL_NAME} doctor`,
730
- };
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
+ }
731
729
 
732
- pkg.scripts = pkg.scripts || {};
730
+ pkg.scripts = existingScripts;
733
731
  let changed = false;
734
732
  for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
735
733
  if (pkg.scripts[key] !== value) {
@@ -749,7 +747,8 @@ function ensurePackageScripts(repoRoot, dryRun) {
749
747
  return { status: 'updated', file: 'package.json' };
750
748
  }
751
749
 
752
- function ensureAgentsSnippet(repoRoot, dryRun) {
750
+ function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
751
+ const force = Boolean(options.force);
753
752
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
754
753
  const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
755
754
  const managedRegex = new RegExp(
@@ -766,6 +765,9 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
766
765
 
767
766
  const existing = fs.readFileSync(agentsPath, 'utf8');
768
767
  if (managedRegex.test(existing)) {
768
+ if (!force) {
769
+ return { status: 'unchanged', file: 'AGENTS.md', note: 'preserved existing guardex-managed block' };
770
+ }
769
771
  const next = existing.replace(managedRegex, snippet);
770
772
  if (next === existing) {
771
773
  return { status: 'unchanged', file: 'AGENTS.md' };
@@ -909,10 +911,18 @@ function parseCommonArgs(rawArgs, defaults) {
909
911
  }
910
912
 
911
913
  function parseSetupArgs(rawArgs, defaults) {
912
- 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
+ };
913
922
  const forwardedArgs = [];
914
923
 
915
- for (const arg of rawArgs) {
924
+ for (let index = 0; index < rawArgs.length; index += 1) {
925
+ const arg = rawArgs[index];
916
926
  if (arg === '--parent-workspace-view') {
917
927
  setupDefaults.parentWorkspaceView = true;
918
928
  continue;
@@ -921,6 +931,34 @@ function parseSetupArgs(rawArgs, defaults) {
921
931
  setupDefaults.parentWorkspaceView = false;
922
932
  continue;
923
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
+ }
924
962
  forwardedArgs.push(arg);
925
963
  }
926
964
 
@@ -1076,6 +1114,7 @@ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
1076
1114
  function buildSandboxDoctorArgs(options, sandboxTarget) {
1077
1115
  const args = ['doctor', '--target', sandboxTarget];
1078
1116
  if (options.dryRun) args.push('--dry-run');
1117
+ if (options.force) args.push('--force');
1079
1118
  if (options.skipAgents) args.push('--skip-agents');
1080
1119
  if (options.skipPackageJson) args.push('--skip-package-json');
1081
1120
  if (options.skipGitignore) args.push('--no-gitignore');
@@ -3512,11 +3551,11 @@ function runInstallInternal(options) {
3512
3551
  }
3513
3552
 
3514
3553
  if (!options.skipPackageJson) {
3515
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun)));
3554
+ operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3516
3555
  }
3517
3556
 
3518
3557
  if (!options.skipAgents) {
3519
- operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun)));
3558
+ operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3520
3559
  }
3521
3560
 
3522
3561
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -3566,11 +3605,11 @@ function runFixInternal(options) {
3566
3605
  }
3567
3606
 
3568
3607
  if (!options.skipPackageJson) {
3569
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun)));
3608
+ operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3570
3609
  }
3571
3610
 
3572
3611
  if (!options.skipAgents) {
3573
- operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun)));
3612
+ operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3574
3613
  }
3575
3614
 
3576
3615
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -4437,23 +4476,87 @@ function setup(rawArgs) {
4437
4476
  }
4438
4477
  }
4439
4478
 
4440
- assertProtectedMainWriteAllowed(options, 'setup');
4441
- const installPayload = runInstallInternal(options);
4442
- installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)));
4443
- if (options.parentWorkspaceView) {
4444
- 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
+ }
4445
4496
  }
4446
- printOperations('Setup/install', installPayload, options.dryRun);
4447
4497
 
4448
- const fixPayload = runFixInternal({
4449
- target: options.target,
4450
- dryRun: options.dryRun,
4451
- dropStaleLocks: true,
4452
- skipAgents: options.skipAgents,
4453
- skipPackageJson: options.skipPackageJson,
4454
- skipGitignore: options.skipGitignore,
4455
- });
4456
- 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
+ }
4457
4560
 
4458
4561
  if (options.dryRun) {
4459
4562
  console.log(`[${TOOL_NAME}] Dry run setup done.`);
@@ -4461,32 +4564,11 @@ function setup(rawArgs) {
4461
4564
  return;
4462
4565
  }
4463
4566
 
4464
- if (options.parentWorkspaceView) {
4465
- const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
4466
- console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
4467
- }
4468
-
4469
- const scanResult = runScanInternal({ target: options.target, json: false });
4470
- const currentBaseBranch = currentBranchName(scanResult.repoRoot);
4471
- const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
4472
- baseBranch: currentBaseBranch,
4473
- dryRun: options.dryRun,
4474
- });
4475
- printScanResult(scanResult, false);
4476
- if (autoFinishSummary.enabled) {
4477
- console.log(
4478
- `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
4479
- );
4480
- for (const detail of autoFinishSummary.details) {
4481
- console.log(`[${TOOL_NAME}] ${detail}`);
4482
- }
4483
- } else if (autoFinishSummary.details.length > 0) {
4484
- console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
4485
- }
4486
-
4487
- if (scanResult.errors === 0 && scanResult.warnings === 0) {
4488
- console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
4489
- 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`);
4490
4572
  console.log(
4491
4573
  `[${TOOL_NAME}] OpenSpec core workflow: /opsx:propose -> /opsx:apply -> /opsx:archive`,
4492
4574
  );
@@ -4496,7 +4578,13 @@ function setup(rawArgs) {
4496
4578
  console.log(`[${TOOL_NAME}] OpenSpec guide: docs/openspec-getting-started.md`);
4497
4579
  }
4498
4580
 
4499
- setExitCodeFromScan(scanResult);
4581
+ if (lastScanResult) {
4582
+ setExitCodeFromScan({
4583
+ ...lastScanResult,
4584
+ errors: aggregateErrors,
4585
+ warnings: aggregateWarnings,
4586
+ });
4587
+ }
4500
4588
  }
4501
4589
 
4502
4590
  function ensureMainBranch(repoRoot) {
@@ -4795,6 +4883,31 @@ function copyCommands() {
4795
4883
  process.exitCode = 0;
4796
4884
  }
4797
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
+
4798
4911
  function cleanup(rawArgs) {
4799
4912
  const options = parseCleanupArgs(rawArgs);
4800
4913
  const repoRoot = resolveRepoRoot(options.target);
@@ -5273,6 +5386,29 @@ function normalizeCommandOrThrow(command) {
5273
5386
  return command;
5274
5387
  }
5275
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
+
5276
5412
  function main() {
5277
5413
  const args = process.argv.slice(2);
5278
5414
 
@@ -5296,90 +5432,42 @@ function main() {
5296
5432
  return;
5297
5433
  }
5298
5434
 
5299
- if (command === 'status') {
5300
- status(rest);
5301
- return;
5302
- }
5303
-
5304
- if (command === 'setup' || command === 'init') {
5305
- setup(rest);
5306
- return;
5307
- }
5308
-
5309
- if (command === 'doctor') {
5310
- doctor(rest);
5311
- return;
5312
- }
5313
-
5314
- if (command === 'review') {
5315
- review(rest);
5316
- return;
5317
- }
5318
-
5319
- if (command === 'agents') {
5320
- agents(rest);
5321
- return;
5322
- }
5323
-
5324
- if (command === 'finish') {
5325
- finish(rest);
5326
- return;
5327
- }
5328
-
5329
- if (command === 'report') {
5330
- report(rest);
5331
- return;
5332
- }
5333
-
5334
- if (command === 'copy-prompt') {
5335
- copyPrompt();
5336
- return;
5337
- }
5338
-
5339
- if (command === 'copy-commands') {
5340
- copyCommands();
5341
- return;
5342
- }
5343
-
5344
- if (command === 'protect') {
5345
- protect(rest);
5346
- return;
5347
- }
5348
-
5349
- if (command === 'sync') {
5350
- sync(rest);
5351
- return;
5352
- }
5353
-
5354
- if (command === 'cleanup') {
5355
- cleanup(rest);
5356
- return;
5357
- }
5358
-
5359
- if (command === 'release') {
5360
- release(rest);
5361
- return;
5362
- }
5363
-
5364
- if (command === 'install') {
5365
- install(rest);
5366
- return;
5367
- }
5368
-
5369
- if (command === 'fix') {
5370
- fix(rest);
5371
- 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);
5372
5446
  }
5373
5447
 
5374
- if (command === 'scan') {
5375
- scan(rest);
5376
- return;
5377
- }
5378
-
5379
- if (command === 'print-agents-snippet') {
5380
- printAgentsSnippet();
5381
- return;
5382
- }
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);
5383
5471
 
5384
5472
  const suggestion = maybeSuggestCommand(command);
5385
5473
  if (suggestion) {