@imdeadpool/guardex 6.1.0 → 7.0.1

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.
@@ -167,158 +167,84 @@ const COMMAND_TYPO_ALIASES = new Map([
167
167
  const SUGGESTIBLE_COMMANDS = [
168
168
  'status',
169
169
  'setup',
170
- 'init',
171
170
  'doctor',
172
- 'review',
173
171
  'agents',
174
172
  'finish',
175
173
  'report',
176
- 'copy-prompt',
177
- 'copy-commands',
178
174
  'protect',
179
175
  'sync',
180
176
  'cleanup',
181
- 'release',
177
+ 'prompt',
178
+ 'help',
179
+ 'version',
180
+ // deprecated aliases still routable with a warning
181
+ 'init',
182
182
  'install',
183
183
  'fix',
184
184
  'scan',
185
+ 'review',
186
+ 'copy-prompt',
187
+ 'copy-commands',
185
188
  'print-agents-snippet',
186
- 'help',
187
- 'version',
189
+ 'release',
188
190
  ];
189
191
  const CLI_COMMAND_DESCRIPTIONS = [
190
192
  ['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'],
193
+ ['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
194
+ ['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
198
195
  ['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)'],
196
+ ['sync', 'Sync agent branches with origin/<base>'],
197
+ ['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
198
+ ['cleanup', 'Prune merged/stale agent branches and worktrees'],
201
199
  ['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'],
200
+ ['prompt', 'Print AI setup checklist (--exec, --snippet)'],
201
+ ['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
207
202
  ['help', 'Show this help output'],
208
203
  ['version', 'Print GuardeX version'],
209
204
  ];
210
- const CORE_COMMAND_NAMES = new Set([
211
- 'setup',
212
- 'doctor',
213
- 'status',
214
- 'finish',
215
- 'cleanup',
216
- 'sync',
217
- 'scan',
205
+ const DEPRECATED_COMMAND_ALIASES = new Map([
206
+ ['init', { target: 'setup', hint: 'gx setup' }],
207
+ ['install', { target: 'setup', hint: 'gx setup --install-only' }],
208
+ ['fix', { target: 'setup', hint: 'gx setup --repair' }],
209
+ ['scan', { target: 'status', hint: 'gx status --strict' }],
210
+ ['copy-prompt', { target: 'prompt', hint: 'gx prompt' }],
211
+ ['copy-commands', { target: 'prompt', hint: 'gx prompt --exec' }],
212
+ ['print-agents-snippet', { target: 'prompt', hint: 'gx prompt --snippet' }],
213
+ ['review', { target: 'agents', hint: 'gx agents start (runs review + cleanup)' }],
218
214
  ]);
219
215
  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'],
216
+ ['agents', 'Start/stop review + cleanup bots for this repo'],
222
217
  ];
223
218
 
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
219
+ const AI_SETUP_PROMPT = `GuardeX (gx) setup checklist for Codex/Claude in this repo.
220
+
221
+ 1) Install: npm i -g @imdeadpool/guardex && gh --version
222
+ 2) Bootstrap: gx setup # installs hooks/templates + verifies; prompts Y/N for global OMX/OpenSpec/codex-auth
223
+ 3) If degraded: gx doctor # repair + re-verify
224
+ 4) Per task: bash scripts/codex-agent.sh "<task>" "<agent>"
225
+ # or manual:
226
+ # bash scripts/agent-branch-start.sh "<task>" "<agent>"
227
+ # python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
228
+ # bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)" --via-pr --wait-for-merge
229
+ 5) Finalize all: gx finish --all
230
+ 6) Cleanup: gx cleanup
231
+ 7) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive (see docs/openspec-getting-started.md)
232
+ 8) Protect: gx protect add release staging (optional)
233
+ 9) Sync: gx sync --check && gx sync (optional; rebase onto base)
234
+ 10) Fork sync: cp .github/pull.yml.example .github/pull.yml (optional; install https://github.com/apps/pull)
235
+ 11) PR review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY in Actions variables (uses .github/workflows/cr.yml)
236
+ 12) GitHub repo: enable Settings -> PRs -> Automatically delete head branches
302
237
  `;
303
238
 
304
239
  const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
305
240
  gh --version
306
241
  gx setup
307
242
  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
243
+ bash scripts/codex-agent.sh "<task>" "<agent>"
313
244
  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
245
+ gx cleanup
318
246
  gx protect add release staging
319
- gx sync --check
320
247
  gx sync
321
- cp .github/pull.yml.example .github/pull.yml
322
248
  `;
323
249
 
324
250
  const SCORECARD_RISK_BY_CHECK = {
@@ -364,15 +290,12 @@ function statusDot(status) {
364
290
  return colorize('●', '33'); // yellow for degraded/unknown
365
291
  }
366
292
 
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(
293
+ function commandCatalogLines(indent = ' ') {
294
+ const maxCommandLength = CLI_COMMAND_DESCRIPTIONS.reduce(
372
295
  (max, [command]) => Math.max(max, command.length),
373
296
  0,
374
297
  );
375
- return entries.map(
298
+ return CLI_COMMAND_DESCRIPTIONS.map(
376
299
  ([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
377
300
  );
378
301
  }
@@ -389,7 +312,7 @@ function agentBotCatalogLines(indent = ' ') {
389
312
 
390
313
  function printToolLogsSummary() {
391
314
  const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
392
- const commandDetails = commandCatalogLines(' ', { coreOnly: true });
315
+ const commandDetails = commandCatalogLines(' ');
393
316
  const agentBotDetails = agentBotCatalogLines(' ');
394
317
 
395
318
  if (!supportsAnsiColors()) {
@@ -434,7 +357,7 @@ function printToolLogsSummary() {
434
357
  }
435
358
  console.log(` ${pipe}${line.slice(2)}`);
436
359
  }
437
- console.log(` ${corner}─ ${colorize(`Try '${SHORT_TOOL_NAME} doctor' to repair drift, or '${SHORT_TOOL_NAME} help' for the full command list.`, '2')}`);
360
+ console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
438
361
  }
439
362
 
440
363
  function usage(options = {}) {
@@ -455,19 +378,12 @@ AGENT BOT
455
378
  ${agentBotCatalogLines().join('\n')}
456
379
 
457
380
  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(', ')}`);
381
+ - No command = ${SHORT_TOOL_NAME} status. ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup.
382
+ - Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
383
+ - Target another repo: ${SHORT_TOOL_NAME} <cmd> --target <repo-path>.
384
+ - On protected main, setup/install/fix/doctor auto-sandbox via agent branch + PR flow.
385
+ - Run '${SHORT_TOOL_NAME} cleanup' to prune merged agent branches/worktrees.
386
+ - Legacy aliases: ${LEGACY_NAMES.join(', ')}.`);
471
387
 
472
388
  if (outsideGitRepo) {
473
389
  console.log(`
@@ -513,6 +429,90 @@ function isGitRepo(targetPath) {
513
429
  return result.status === 0;
514
430
  }
515
431
 
432
+ const NESTED_REPO_DEFAULT_MAX_DEPTH = 6;
433
+ const NESTED_REPO_DEFAULT_SKIP_DIRS = new Set([
434
+ 'node_modules',
435
+ '.git',
436
+ 'dist',
437
+ 'build',
438
+ '.next',
439
+ '.cache',
440
+ 'target',
441
+ 'vendor',
442
+ '.venv',
443
+ '.pnpm-store',
444
+ ]);
445
+ const NESTED_REPO_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
446
+
447
+ function discoverNestedGitRepos(rootPath, opts = {}) {
448
+ const maxDepth = Number.isFinite(opts.maxDepth) ? Math.max(1, opts.maxDepth) : NESTED_REPO_DEFAULT_MAX_DEPTH;
449
+ const extraSkip = new Set(Array.isArray(opts.extraSkip) ? opts.extraSkip : []);
450
+ const includeSubmodules = Boolean(opts.includeSubmodules);
451
+ const resolvedRoot = path.resolve(rootPath);
452
+
453
+ const rootCommonDir = (() => {
454
+ const result = run('git', ['-C', resolvedRoot, 'rev-parse', '--git-common-dir'], { cwd: resolvedRoot });
455
+ if (result.status !== 0) return null;
456
+ const raw = result.stdout.trim();
457
+ if (!raw) return null;
458
+ return path.resolve(resolvedRoot, raw);
459
+ })();
460
+
461
+ const workreeSkipAbsolute = path.join(resolvedRoot, NESTED_REPO_WORKTREE_RELATIVE_DIR);
462
+ const found = new Set();
463
+ found.add(resolvedRoot);
464
+
465
+ function shouldSkipDir(dirName) {
466
+ return NESTED_REPO_DEFAULT_SKIP_DIRS.has(dirName) || extraSkip.has(dirName);
467
+ }
468
+
469
+ function walk(currentPath, depth) {
470
+ if (depth > maxDepth) return;
471
+ let entries;
472
+ try {
473
+ entries = fs.readdirSync(currentPath, { withFileTypes: true });
474
+ } catch {
475
+ return;
476
+ }
477
+
478
+ for (const entry of entries) {
479
+ const entryPath = path.join(currentPath, entry.name);
480
+
481
+ if (entry.name === '.git') {
482
+ if (entry.isDirectory()) {
483
+ if (entryPath === path.join(resolvedRoot, '.git')) continue;
484
+ found.add(path.dirname(entryPath));
485
+ } else if (includeSubmodules && entry.isFile()) {
486
+ found.add(path.dirname(entryPath));
487
+ }
488
+ continue;
489
+ }
490
+
491
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
492
+ if (shouldSkipDir(entry.name)) continue;
493
+ if (entryPath === workreeSkipAbsolute) continue;
494
+ walk(entryPath, depth + 1);
495
+ }
496
+ }
497
+
498
+ walk(resolvedRoot, 0);
499
+
500
+ const filtered = Array.from(found).filter((repoPath) => {
501
+ if (repoPath === resolvedRoot) return true;
502
+ if (!rootCommonDir) return true;
503
+ const childResult = run('git', ['-C', repoPath, 'rev-parse', '--git-common-dir'], { cwd: repoPath });
504
+ if (childResult.status !== 0) return true;
505
+ const childCommonDirRaw = childResult.stdout.trim();
506
+ if (!childCommonDirRaw) return true;
507
+ const childCommonDir = path.resolve(repoPath, childCommonDirRaw);
508
+ return childCommonDir !== rootCommonDir;
509
+ });
510
+
511
+ const [root, ...rest] = filtered;
512
+ rest.sort((a, b) => a.localeCompare(b));
513
+ return [root, ...rest];
514
+ }
515
+
516
516
  function toDestinationPath(relativeTemplatePath) {
517
517
  if (relativeTemplatePath.startsWith('scripts/')) {
518
518
  return relativeTemplatePath;
@@ -709,7 +709,8 @@ function writeLockState(repoRoot, payload, dryRun) {
709
709
  fs.writeFileSync(lockPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
710
710
  }
711
711
 
712
- function ensurePackageScripts(repoRoot, dryRun) {
712
+ function ensurePackageScripts(repoRoot, dryRun, options = {}) {
713
+ const force = Boolean(options.force);
713
714
  const packagePath = path.join(repoRoot, 'package.json');
714
715
  if (!fs.existsSync(packagePath)) {
715
716
  return { status: 'skipped', file: 'package.json', note: 'package.json not found' };
@@ -722,30 +723,15 @@ function ensurePackageScripts(repoRoot, dryRun) {
722
723
  throw new Error(`Unable to parse package.json in target repo: ${error.message}`);
723
724
  }
724
725
 
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
- };
726
+ const existingScripts = pkg.scripts && typeof pkg.scripts === 'object'
727
+ ? pkg.scripts
728
+ : {};
729
+ const hasExistingAgentScripts = Object.keys(existingScripts).some((key) => key.startsWith('agent:'));
730
+ if (hasExistingAgentScripts && !force) {
731
+ return { status: 'unchanged', file: 'package.json', note: 'preserved existing agent:* scripts' };
732
+ }
747
733
 
748
- pkg.scripts = pkg.scripts || {};
734
+ pkg.scripts = existingScripts;
749
735
  let changed = false;
750
736
  for (const [key, value] of Object.entries(REQUIRED_PACKAGE_SCRIPTS)) {
751
737
  if (pkg.scripts[key] !== value) {
@@ -765,7 +751,8 @@ function ensurePackageScripts(repoRoot, dryRun) {
765
751
  return { status: 'updated', file: 'package.json' };
766
752
  }
767
753
 
768
- function ensureAgentsSnippet(repoRoot, dryRun) {
754
+ function ensureAgentsSnippet(repoRoot, dryRun, options = {}) {
755
+ const force = Boolean(options.force);
769
756
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
770
757
  const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
771
758
  const managedRegex = new RegExp(
@@ -782,6 +769,9 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
782
769
 
783
770
  const existing = fs.readFileSync(agentsPath, 'utf8');
784
771
  if (managedRegex.test(existing)) {
772
+ if (!force) {
773
+ return { status: 'unchanged', file: 'AGENTS.md', note: 'preserved existing guardex-managed block' };
774
+ }
785
775
  const next = existing.replace(managedRegex, snippet);
786
776
  if (next === existing) {
787
777
  return { status: 'unchanged', file: 'AGENTS.md' };
@@ -925,10 +915,18 @@ function parseCommonArgs(rawArgs, defaults) {
925
915
  }
926
916
 
927
917
  function parseSetupArgs(rawArgs, defaults) {
928
- const setupDefaults = { ...defaults, parentWorkspaceView: false };
918
+ const setupDefaults = {
919
+ ...defaults,
920
+ parentWorkspaceView: false,
921
+ recursive: true,
922
+ nestedMaxDepth: NESTED_REPO_DEFAULT_MAX_DEPTH,
923
+ nestedSkipDirs: [],
924
+ includeSubmodules: false,
925
+ };
929
926
  const forwardedArgs = [];
930
927
 
931
- for (const arg of rawArgs) {
928
+ for (let index = 0; index < rawArgs.length; index += 1) {
929
+ const arg = rawArgs[index];
932
930
  if (arg === '--parent-workspace-view') {
933
931
  setupDefaults.parentWorkspaceView = true;
934
932
  continue;
@@ -937,6 +935,34 @@ function parseSetupArgs(rawArgs, defaults) {
937
935
  setupDefaults.parentWorkspaceView = false;
938
936
  continue;
939
937
  }
938
+ if (arg === '--no-recursive' || arg === '--no-nested' || arg === '--single-repo') {
939
+ setupDefaults.recursive = false;
940
+ continue;
941
+ }
942
+ if (arg === '--recursive' || arg === '--nested') {
943
+ setupDefaults.recursive = true;
944
+ continue;
945
+ }
946
+ if (arg === '--max-depth') {
947
+ const raw = requireValue(rawArgs, index, '--max-depth');
948
+ const parsed = Number.parseInt(raw, 10);
949
+ if (!Number.isFinite(parsed) || parsed < 1) {
950
+ throw new Error('--max-depth requires a positive integer');
951
+ }
952
+ setupDefaults.nestedMaxDepth = parsed;
953
+ index += 1;
954
+ continue;
955
+ }
956
+ if (arg === '--skip-nested') {
957
+ const raw = requireValue(rawArgs, index, '--skip-nested');
958
+ setupDefaults.nestedSkipDirs.push(raw);
959
+ index += 1;
960
+ continue;
961
+ }
962
+ if (arg === '--include-submodules') {
963
+ setupDefaults.includeSubmodules = true;
964
+ continue;
965
+ }
940
966
  forwardedArgs.push(arg);
941
967
  }
942
968
 
@@ -1092,6 +1118,7 @@ function resolveSandboxTarget(repoRoot, worktreePath, targetPath) {
1092
1118
  function buildSandboxDoctorArgs(options, sandboxTarget) {
1093
1119
  const args = ['doctor', '--target', sandboxTarget];
1094
1120
  if (options.dryRun) args.push('--dry-run');
1121
+ if (options.force) args.push('--force');
1095
1122
  if (options.skipAgents) args.push('--skip-agents');
1096
1123
  if (options.skipPackageJson) args.push('--skip-package-json');
1097
1124
  if (options.skipGitignore) args.push('--no-gitignore');
@@ -3528,11 +3555,11 @@ function runInstallInternal(options) {
3528
3555
  }
3529
3556
 
3530
3557
  if (!options.skipPackageJson) {
3531
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun)));
3558
+ operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3532
3559
  }
3533
3560
 
3534
3561
  if (!options.skipAgents) {
3535
- operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun)));
3562
+ operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3536
3563
  }
3537
3564
 
3538
3565
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -3582,11 +3609,11 @@ function runFixInternal(options) {
3582
3609
  }
3583
3610
 
3584
3611
  if (!options.skipPackageJson) {
3585
- operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun)));
3612
+ operations.push(ensurePackageScripts(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3586
3613
  }
3587
3614
 
3588
3615
  if (!options.skipAgents) {
3589
- operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun)));
3616
+ operations.push(ensureAgentsSnippet(repoRoot, Boolean(options.dryRun), { force: Boolean(options.force) }));
3590
3617
  }
3591
3618
 
3592
3619
  const hookResult = configureHooks(repoRoot, Boolean(options.dryRun));
@@ -4453,23 +4480,87 @@ function setup(rawArgs) {
4453
4480
  }
4454
4481
  }
4455
4482
 
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)));
4483
+ const topRepoRoot = resolveRepoRoot(options.target);
4484
+ const discoveredRepos = options.recursive
4485
+ ? discoverNestedGitRepos(topRepoRoot, {
4486
+ maxDepth: options.nestedMaxDepth,
4487
+ extraSkip: options.nestedSkipDirs,
4488
+ includeSubmodules: options.includeSubmodules,
4489
+ })
4490
+ : [topRepoRoot];
4491
+
4492
+ if (discoveredRepos.length > 1) {
4493
+ console.log(
4494
+ `[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. Installing into each (use --no-recursive to limit to the top-level).`,
4495
+ );
4496
+ for (const repoPath of discoveredRepos) {
4497
+ const marker = repoPath === topRepoRoot ? ' (top-level)' : '';
4498
+ console.log(`[${TOOL_NAME}] - ${repoPath}${marker}`);
4499
+ }
4461
4500
  }
4462
- printOperations('Setup/install', installPayload, options.dryRun);
4463
4501
 
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);
4502
+ let aggregateErrors = 0;
4503
+ let aggregateWarnings = 0;
4504
+ let lastScanResult = null;
4505
+
4506
+ for (const repoPath of discoveredRepos) {
4507
+ const perRepoOptions = { ...options, target: repoPath };
4508
+ const repoLabel = discoveredRepos.length > 1 ? ` [${path.relative(topRepoRoot, repoPath) || '.'}]` : '';
4509
+
4510
+ if (discoveredRepos.length > 1) {
4511
+ console.log(`[${TOOL_NAME}] ── Setup target: ${repoPath} ──`);
4512
+ }
4513
+
4514
+ assertProtectedMainWriteAllowed(perRepoOptions, 'setup');
4515
+ const installPayload = runInstallInternal(perRepoOptions);
4516
+ installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(perRepoOptions.dryRun)));
4517
+ if (perRepoOptions.parentWorkspaceView) {
4518
+ installPayload.operations.push(ensureParentWorkspaceView(installPayload.repoRoot, Boolean(perRepoOptions.dryRun)));
4519
+ }
4520
+ printOperations(`Setup/install${repoLabel}`, installPayload, perRepoOptions.dryRun);
4521
+
4522
+ const fixPayload = runFixInternal({
4523
+ target: repoPath,
4524
+ dryRun: perRepoOptions.dryRun,
4525
+ force: perRepoOptions.force,
4526
+ dropStaleLocks: true,
4527
+ skipAgents: perRepoOptions.skipAgents,
4528
+ skipPackageJson: perRepoOptions.skipPackageJson,
4529
+ skipGitignore: perRepoOptions.skipGitignore,
4530
+ });
4531
+ printOperations(`Setup/fix${repoLabel}`, fixPayload, perRepoOptions.dryRun);
4532
+
4533
+ if (perRepoOptions.dryRun) {
4534
+ continue;
4535
+ }
4536
+
4537
+ if (perRepoOptions.parentWorkspaceView) {
4538
+ const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot);
4539
+ console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`);
4540
+ }
4541
+
4542
+ const scanResult = runScanInternal({ target: repoPath, json: false });
4543
+ const currentBaseBranch = currentBranchName(scanResult.repoRoot);
4544
+ const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
4545
+ baseBranch: currentBaseBranch,
4546
+ dryRun: perRepoOptions.dryRun,
4547
+ });
4548
+ printScanResult(scanResult, false);
4549
+ if (autoFinishSummary.enabled) {
4550
+ console.log(
4551
+ `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
4552
+ );
4553
+ for (const detail of autoFinishSummary.details) {
4554
+ console.log(`[${TOOL_NAME}] ${detail}`);
4555
+ }
4556
+ } else if (autoFinishSummary.details.length > 0) {
4557
+ console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
4558
+ }
4559
+
4560
+ aggregateErrors += scanResult.errors;
4561
+ aggregateWarnings += scanResult.warnings;
4562
+ lastScanResult = scanResult;
4563
+ }
4473
4564
 
4474
4565
  if (options.dryRun) {
4475
4566
  console.log(`[${TOOL_NAME}] Dry run setup done.`);
@@ -4477,32 +4568,11 @@ function setup(rawArgs) {
4477
4568
  return;
4478
4569
  }
4479
4570
 
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`);
4571
+ if (aggregateErrors === 0 && aggregateWarnings === 0) {
4572
+ const repoCount = discoveredRepos.length;
4573
+ const suffix = repoCount > 1 ? ` (${repoCount} repos)` : '';
4574
+ console.log(`[${TOOL_NAME}] ✅ Setup complete.${suffix}`);
4575
+ console.log(`[${TOOL_NAME}] Copy AI setup prompt with: ${SHORT_TOOL_NAME} prompt`);
4506
4576
  console.log(
4507
4577
  `[${TOOL_NAME}] OpenSpec core workflow: /opsx:propose -> /opsx:apply -> /opsx:archive`,
4508
4578
  );
@@ -4512,7 +4582,13 @@ function setup(rawArgs) {
4512
4582
  console.log(`[${TOOL_NAME}] OpenSpec guide: docs/openspec-getting-started.md`);
4513
4583
  }
4514
4584
 
4515
- setExitCodeFromScan(scanResult);
4585
+ if (lastScanResult) {
4586
+ setExitCodeFromScan({
4587
+ ...lastScanResult,
4588
+ errors: aggregateErrors,
4589
+ warnings: aggregateWarnings,
4590
+ });
4591
+ }
4516
4592
  }
4517
4593
 
4518
4594
  function ensureMainBranch(repoRoot) {
@@ -4811,6 +4887,31 @@ function copyCommands() {
4811
4887
  process.exitCode = 0;
4812
4888
  }
4813
4889
 
4890
+ function prompt(rawArgs) {
4891
+ const args = Array.isArray(rawArgs) ? rawArgs : [];
4892
+ let variant = 'prompt';
4893
+ for (const arg of args) {
4894
+ if (arg === '--exec' || arg === '--commands') variant = 'exec';
4895
+ else if (arg === '--snippet' || arg === '--agents') variant = 'snippet';
4896
+ else if (arg === '--prompt' || arg === '--full') variant = 'prompt';
4897
+ else if (arg === '-h' || arg === '--help') variant = 'help';
4898
+ else throw new Error(`Unknown option: ${arg}`);
4899
+ }
4900
+ if (variant === 'help') {
4901
+ console.log(
4902
+ `${SHORT_TOOL_NAME} prompt commands:\n` +
4903
+ ` ${SHORT_TOOL_NAME} prompt Print AI setup checklist\n` +
4904
+ ` ${SHORT_TOOL_NAME} prompt --exec Print setup commands only (shell-ready)\n` +
4905
+ ` ${SHORT_TOOL_NAME} prompt --snippet Print the AGENTS.md managed-block template`,
4906
+ );
4907
+ process.exitCode = 0;
4908
+ return;
4909
+ }
4910
+ if (variant === 'exec') return copyCommands();
4911
+ if (variant === 'snippet') return printAgentsSnippet();
4912
+ return copyPrompt();
4913
+ }
4914
+
4814
4915
  function cleanup(rawArgs) {
4815
4916
  const options = parseCleanupArgs(rawArgs);
4816
4917
  const repoRoot = resolveRepoRoot(options.target);
@@ -5289,6 +5390,29 @@ function normalizeCommandOrThrow(command) {
5289
5390
  return command;
5290
5391
  }
5291
5392
 
5393
+ function warnDeprecatedAlias(aliasName) {
5394
+ const entry = DEPRECATED_COMMAND_ALIASES.get(aliasName);
5395
+ if (!entry) return;
5396
+ console.error(
5397
+ `[${TOOL_NAME}] '${aliasName}' is deprecated and will be removed in a future major release. ` +
5398
+ `Use: ${entry.hint}`,
5399
+ );
5400
+ }
5401
+
5402
+ function extractFlag(args, ...names) {
5403
+ const flagSet = new Set(names);
5404
+ let found = false;
5405
+ const remaining = [];
5406
+ for (const arg of args) {
5407
+ if (flagSet.has(arg)) {
5408
+ found = true;
5409
+ } else {
5410
+ remaining.push(arg);
5411
+ }
5412
+ }
5413
+ return { found, remaining };
5414
+ }
5415
+
5292
5416
  function main() {
5293
5417
  const args = process.argv.slice(2);
5294
5418
 
@@ -5312,90 +5436,42 @@ function main() {
5312
5436
  return;
5313
5437
  }
5314
5438
 
5315
- if (command === 'status') {
5316
- status(rest);
5317
- return;
5318
- }
5319
-
5320
- if (command === 'setup' || command === 'init') {
5321
- setup(rest);
5322
- return;
5439
+ // Deprecated direct aliases — route to new surface and warn once.
5440
+ if (DEPRECATED_COMMAND_ALIASES.has(command)) {
5441
+ warnDeprecatedAlias(command);
5442
+ if (command === 'init') return setup(rest);
5443
+ if (command === 'install') return install(rest);
5444
+ if (command === 'fix') return fix(rest);
5445
+ if (command === 'scan') return scan(rest);
5446
+ if (command === 'copy-prompt') return copyPrompt();
5447
+ if (command === 'copy-commands') return copyCommands();
5448
+ if (command === 'print-agents-snippet') return printAgentsSnippet();
5449
+ if (command === 'review') return review(rest);
5323
5450
  }
5324
5451
 
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
- }
5452
+ if (command === 'status') {
5453
+ const { found: strict, remaining } = extractFlag(rest, '--strict');
5454
+ if (strict) return scan(remaining);
5455
+ return status(remaining);
5456
+ }
5457
+
5458
+ if (command === 'setup') {
5459
+ const installOnly = extractFlag(rest, '--install-only', '--only-install');
5460
+ if (installOnly.found) return install(installOnly.remaining);
5461
+ const repairOnly = extractFlag(installOnly.remaining, '--repair', '--fix-only');
5462
+ if (repairOnly.found) return fix(repairOnly.remaining);
5463
+ return setup(repairOnly.remaining);
5464
+ }
5465
+
5466
+ if (command === 'prompt') return prompt(rest);
5467
+ if (command === 'doctor') return doctor(rest);
5468
+ if (command === 'agents') return agents(rest);
5469
+ if (command === 'finish') return finish(rest);
5470
+ if (command === 'report') return report(rest);
5471
+ if (command === 'protect') return protect(rest);
5472
+ if (command === 'sync') return sync(rest);
5473
+ if (command === 'cleanup') return cleanup(rest);
5474
+ if (command === 'release') return release(rest);
5399
5475
 
5400
5476
  const suggestion = maybeSuggestCommand(command);
5401
5477
  if (suggestion) {