@imdeadpool/guardex 7.0.20 → 7.0.21

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.
package/src/cli/main.js CHANGED
@@ -1,364 +1,132 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('node:fs');
4
- const os = require('node:os');
5
- const path = require('node:path');
6
- const cp = require('node:child_process');
7
3
  const hooksModule = require('../hooks');
8
4
  const sandboxModule = require('../sandbox');
9
5
  const toolchainModule = require('../toolchain');
10
6
  const finishModule = require('../finish');
7
+ const {
8
+ fs,
9
+ path,
10
+ cp,
11
+ packageJson,
12
+ TOOL_NAME,
13
+ SHORT_TOOL_NAME,
14
+ OPENSPEC_PACKAGE,
15
+ NPX_BIN,
16
+ GUARDEX_HOME_DIR,
17
+ GLOBAL_TOOLCHAIN_SERVICES,
18
+ GLOBAL_TOOLCHAIN_PACKAGES,
19
+ OPTIONAL_LOCAL_COMPANION_TOOLS,
20
+ GH_BIN,
21
+ REQUIRED_SYSTEM_TOOLS,
22
+ MAINTAINER_RELEASE_REPO,
23
+ NPM_BIN,
24
+ OPENSPEC_BIN,
25
+ SCORECARD_BIN,
26
+ GIT_PROTECTED_BRANCHES_KEY,
27
+ GIT_BASE_BRANCH_KEY,
28
+ GIT_SYNC_STRATEGY_KEY,
29
+ GUARDEX_REPO_TOGGLE_ENV,
30
+ DEFAULT_PROTECTED_BRANCHES,
31
+ DEFAULT_BASE_BRANCH,
32
+ DEFAULT_SYNC_STRATEGY,
33
+ COMPOSE_HINT_FILES,
34
+ TEMPLATE_ROOT,
35
+ HOOK_NAMES,
36
+ TEMPLATE_FILES,
37
+ LEGACY_WORKFLOW_SHIM_SPECS,
38
+ LEGACY_MANAGED_REPO_FILES,
39
+ REQUIRED_MANAGED_REPO_FILES,
40
+ LEGACY_MANAGED_PACKAGE_SCRIPTS,
41
+ PACKAGE_SCRIPT_ASSETS,
42
+ USER_LEVEL_SKILL_ASSETS,
43
+ EXECUTABLE_RELATIVE_PATHS,
44
+ CRITICAL_GUARDRAIL_PATHS,
45
+ LOCK_FILE_RELATIVE,
46
+ AGENTS_BOTS_STATE_RELATIVE,
47
+ AGENTS_MARKER_START,
48
+ AGENTS_MARKER_END,
49
+ GITIGNORE_MARKER_START,
50
+ GITIGNORE_MARKER_END,
51
+ SHARED_VSCODE_SETTINGS_RELATIVE,
52
+ REPO_SCAN_IGNORED_FOLDERS_SETTING,
53
+ AGENT_WORKTREE_RELATIVE_DIRS,
54
+ MANAGED_REPO_SCAN_IGNORED_FOLDERS,
55
+ MANAGED_GITIGNORE_PATHS,
56
+ REPO_SCAFFOLD_DIRECTORIES,
57
+ OMX_SCAFFOLD_DIRECTORIES,
58
+ OMX_SCAFFOLD_FILES,
59
+ TARGETED_FORCEABLE_MANAGED_PATHS,
60
+ DEPRECATED_COMMAND_ALIASES,
61
+ envFlagIsTruthy,
62
+ defaultAgentWorktreeRelativeDir,
63
+ AI_SETUP_PROMPT,
64
+ AI_SETUP_COMMANDS,
65
+ SCORECARD_RISK_BY_CHECK,
66
+ } = require('../context');
67
+ const {
68
+ gitRun,
69
+ resolveRepoRoot,
70
+ isGitRepo,
71
+ discoverNestedGitRepos,
72
+ } = require('../git');
73
+ const {
74
+ run,
75
+ extractTargetedArgs,
76
+ packageAssetEnv,
77
+ runPackageAsset,
78
+ runReviewBotCommand,
79
+ invokePackageAsset,
80
+ } = require('../core/runtime');
81
+ const {
82
+ normalizeManagedForcePath,
83
+ parseCommonArgs,
84
+ parseSetupArgs,
85
+ parseDoctorArgs,
86
+ parseTargetFlag,
87
+ parseReviewArgs,
88
+ parseAgentsArgs,
89
+ parseReportArgs,
90
+ parseSyncArgs,
91
+ parseCleanupArgs,
92
+ parseMergeArgs,
93
+ parseFinishArgs,
94
+ } = require('./args');
95
+ const {
96
+ maybeSuggestCommand,
97
+ normalizeCommandOrThrow,
98
+ warnDeprecatedAlias,
99
+ extractFlag,
100
+ } = require('./dispatch');
101
+ const {
102
+ runtimeVersion,
103
+ colorize,
104
+ colorizeDoctorOutput,
105
+ statusDot,
106
+ printToolLogsSummary,
107
+ usage,
108
+ formatElapsedDuration,
109
+ compactAutoFinishPathSegments,
110
+ detectRecoverableAutoFinishConflict,
111
+ printAutoFinishSummary,
112
+ } = require('../output');
113
+ const {
114
+ toDestinationPath,
115
+ ensureParentDir,
116
+ ensureExecutable,
117
+ isCriticalGuardrailPath,
118
+ shellSingleQuote,
119
+ renderShellDispatchShim,
120
+ renderPythonDispatchShim,
121
+ managedForceConflictMessage,
122
+ printOperations,
123
+ printStandaloneOperations,
124
+ } = require('../scaffold');
11
125
 
12
126
  let sandboxApi;
13
127
  let toolchainApi;
14
128
  let finishApi;
15
129
 
16
- const PACKAGE_ROOT = path.resolve(__dirname, '../..');
17
- const CLI_ENTRY_PATH = path.join(PACKAGE_ROOT, 'bin', 'multiagent-safety.js');
18
- const packageJsonPath = path.join(PACKAGE_ROOT, 'package.json');
19
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
20
-
21
- const TOOL_NAME = 'gitguardex';
22
- const SHORT_TOOL_NAME = 'gx';
23
- if (!process.env.GUARDEX_CLI_ENTRY) {
24
- process.env.GUARDEX_CLI_ENTRY = CLI_ENTRY_PATH;
25
- }
26
- if (!process.env.GUARDEX_NODE_BIN) {
27
- process.env.GUARDEX_NODE_BIN = process.execPath;
28
- }
29
- const LEGACY_NAMES = ['guardex', 'multiagent-safety'];
30
- const GLOBAL_INSTALL_COMMAND = `npm i -g ${packageJson.name}`;
31
- const OPENSPEC_PACKAGE = '@fission-ai/openspec';
32
- const OMC_PACKAGE = 'oh-my-claude-sisyphus';
33
- const OMC_REPO_URL = 'https://github.com/Yeachan-Heo/oh-my-claudecode';
34
- const CAVEMEM_PACKAGE = 'cavemem';
35
- const NPX_BIN = process.env.GUARDEX_NPX_BIN || 'npx';
36
- const GUARDEX_HOME_DIR = path.resolve(process.env.GUARDEX_HOME_DIR || os.homedir());
37
- const GLOBAL_TOOLCHAIN_SERVICES = [
38
- { name: 'oh-my-codex', packageName: 'oh-my-codex' },
39
- {
40
- name: 'oh-my-claudecode',
41
- packageName: OMC_PACKAGE,
42
- dependencyUrl: OMC_REPO_URL,
43
- },
44
- { name: OPENSPEC_PACKAGE, packageName: OPENSPEC_PACKAGE },
45
- { name: CAVEMEM_PACKAGE, packageName: CAVEMEM_PACKAGE },
46
- {
47
- name: '@imdeadpool/codex-account-switcher',
48
- packageName: '@imdeadpool/codex-account-switcher',
49
- },
50
- ];
51
- const GLOBAL_TOOLCHAIN_PACKAGES = [
52
- ...GLOBAL_TOOLCHAIN_SERVICES.map((service) => service.packageName),
53
- ];
54
- const OPTIONAL_LOCAL_COMPANION_TOOLS = [
55
- {
56
- name: 'cavekit',
57
- candidatePaths: [
58
- '.cavekit/plugin.json',
59
- '.codex/local-marketplaces/cavekit/.agents/plugins/marketplace.json',
60
- ],
61
- installCommand: `${NPX_BIN} skills add JuliusBrussee/cavekit`,
62
- installArgs: ['skills', 'add', 'JuliusBrussee/cavekit'],
63
- },
64
- {
65
- name: 'caveman',
66
- candidatePaths: [
67
- '.config/caveman/config.json',
68
- '.cavekit/skills/caveman/SKILL.md',
69
- ],
70
- installCommand: `${NPX_BIN} skills add JuliusBrussee/caveman`,
71
- installArgs: ['skills', 'add', 'JuliusBrussee/caveman'],
72
- },
73
- ];
74
- const GH_BIN = process.env.GUARDEX_GH_BIN || 'gh';
75
- const REQUIRED_SYSTEM_TOOLS = [
76
- {
77
- name: 'gh',
78
- displayName: 'GitHub (gh)',
79
- command: GH_BIN,
80
- installHint: 'https://cli.github.com/',
81
- },
82
- ];
83
- const MAINTAINER_RELEASE_REPO = path.resolve(
84
- process.env.GUARDEX_RELEASE_REPO || path.resolve(__dirname, '..'),
85
- );
86
- const NPM_BIN = process.env.GUARDEX_NPM_BIN || 'npm';
87
- const OPENSPEC_BIN = process.env.GUARDEX_OPENSPEC_BIN || 'openspec';
88
- const SCORECARD_BIN = process.env.GUARDEX_SCORECARD_BIN || 'scorecard';
89
- const GIT_PROTECTED_BRANCHES_KEY = 'multiagent.protectedBranches';
90
- const GIT_BASE_BRANCH_KEY = 'multiagent.baseBranch';
91
- const GIT_SYNC_STRATEGY_KEY = 'multiagent.sync.strategy';
92
- const GUARDEX_REPO_TOGGLE_ENV = 'GUARDEX_ON';
93
- const DEFAULT_PROTECTED_BRANCHES = ['dev', 'main', 'master'];
94
- const DEFAULT_BASE_BRANCH = 'dev';
95
- const DEFAULT_SYNC_STRATEGY = 'rebase';
96
- const DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES = 60;
97
- const COMPOSE_HINT_FILES = [
98
- 'docker-compose.yml',
99
- 'docker-compose.yaml',
100
- 'compose.yml',
101
- 'compose.yaml',
102
- ];
103
-
104
- const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
105
-
106
- const HOOK_NAMES = ['pre-commit', 'pre-push', 'post-merge', 'post-checkout'];
107
-
108
- const TEMPLATE_FILES = [
109
- 'scripts/agent-session-state.js',
110
- 'scripts/guardex-docker-loader.sh',
111
- 'scripts/guardex-env.sh',
112
- 'scripts/install-vscode-active-agents-extension.js',
113
- 'github/pull.yml.example',
114
- 'github/workflows/cr.yml',
115
- 'vscode/guardex-active-agents/package.json',
116
- 'vscode/guardex-active-agents/extension.js',
117
- 'vscode/guardex-active-agents/session-schema.js',
118
- 'vscode/guardex-active-agents/README.md',
119
- ];
120
-
121
- const LEGACY_WORKFLOW_SHIM_SPECS = [
122
- { relativePath: 'scripts/agent-branch-start.sh', kind: 'shell', command: ['branch', 'start'] },
123
- { relativePath: 'scripts/agent-branch-finish.sh', kind: 'shell', command: ['branch', 'finish'] },
124
- { relativePath: 'scripts/agent-branch-merge.sh', kind: 'shell', command: ['branch', 'merge'] },
125
- { relativePath: 'scripts/codex-agent.sh', kind: 'shell', command: ['internal', 'run-shell', 'codexAgent'] },
126
- { relativePath: 'scripts/review-bot-watch.sh', kind: 'shell', command: ['internal', 'run-shell', 'reviewBot'] },
127
- { relativePath: 'scripts/agent-worktree-prune.sh', kind: 'shell', command: ['worktree', 'prune'] },
128
- { relativePath: 'scripts/agent-file-locks.py', kind: 'python', command: ['locks'] },
129
- { relativePath: 'scripts/openspec/init-plan-workspace.sh', kind: 'shell', command: ['internal', 'run-shell', 'planInit'] },
130
- { relativePath: 'scripts/openspec/init-change-workspace.sh', kind: 'shell', command: ['internal', 'run-shell', 'changeInit'] },
131
- ];
132
-
133
- const LEGACY_WORKFLOW_SHIMS = LEGACY_WORKFLOW_SHIM_SPECS.map((entry) => entry.relativePath);
134
-
135
- const MANAGED_TEMPLATE_DESTINATIONS = TEMPLATE_FILES.map((entry) => toDestinationPath(entry));
136
- const MANAGED_TEMPLATE_SCRIPT_FILES = MANAGED_TEMPLATE_DESTINATIONS.filter((entry) =>
137
- entry.startsWith('scripts/'),
138
- );
139
-
140
- const LEGACY_MANAGED_REPO_FILES = [
141
- ...LEGACY_WORKFLOW_SHIMS,
142
- 'scripts/agent-session-state.js',
143
- 'scripts/guardex-docker-loader.sh',
144
- 'scripts/install-vscode-active-agents-extension.js',
145
- 'scripts/guardex-env.sh',
146
- 'scripts/install-agent-git-hooks.sh',
147
- '.githooks/pre-commit',
148
- '.githooks/pre-push',
149
- '.githooks/post-merge',
150
- '.githooks/post-checkout',
151
- '.codex/skills/gitguardex/SKILL.md',
152
- '.codex/skills/guardex-merge-skills-to-dev/SKILL.md',
153
- '.claude/commands/gitguardex.md',
154
- ];
155
-
156
- const REQUIRED_MANAGED_REPO_FILES = [
157
- ...MANAGED_TEMPLATE_DESTINATIONS,
158
- ...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
159
- '.omx/state/agent-file-locks.json',
160
- ];
161
-
162
- const LEGACY_MANAGED_PACKAGE_SCRIPTS = {
163
- 'agent:codex': 'bash ./scripts/codex-agent.sh',
164
- 'agent:branch:start': 'bash ./scripts/agent-branch-start.sh',
165
- 'agent:branch:finish': 'bash ./scripts/agent-branch-finish.sh',
166
- 'agent:branch:merge': 'bash ./scripts/agent-branch-merge.sh',
167
- 'agent:cleanup': 'gx cleanup',
168
- 'agent:hooks:install': 'bash ./scripts/install-agent-git-hooks.sh',
169
- 'agent:locks:claim': 'python3 ./scripts/agent-file-locks.py claim',
170
- 'agent:locks:allow-delete': 'python3 ./scripts/agent-file-locks.py allow-delete',
171
- 'agent:locks:release': 'python3 ./scripts/agent-file-locks.py release',
172
- 'agent:locks:status': 'python3 ./scripts/agent-file-locks.py status',
173
- 'agent:plan:init': 'bash ./scripts/openspec/init-plan-workspace.sh',
174
- 'agent:change:init': 'bash ./scripts/openspec/init-change-workspace.sh',
175
- 'agent:protect:list': 'gx protect list',
176
- 'agent:branch:sync': 'gx sync',
177
- 'agent:branch:sync:check': 'gx sync --check',
178
- 'agent:safety:setup': 'gx setup',
179
- 'agent:safety:scan': 'gx status --strict',
180
- 'agent:safety:fix': 'gx setup --repair',
181
- 'agent:safety:doctor': 'gx doctor',
182
- 'agent:docker:load': 'bash ./scripts/guardex-docker-loader.sh',
183
- 'agent:review:watch': 'bash ./scripts/review-bot-watch.sh',
184
- 'agent:finish': 'gx finish --all',
185
- };
186
-
187
- const PACKAGE_SCRIPT_ASSETS = {
188
- branchStart: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-start.sh'),
189
- branchFinish: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-finish.sh'),
190
- branchMerge: path.join(TEMPLATE_ROOT, 'scripts', 'agent-branch-merge.sh'),
191
- codexAgent: path.join(TEMPLATE_ROOT, 'scripts', 'codex-agent.sh'),
192
- reviewBot: path.join(TEMPLATE_ROOT, 'scripts', 'review-bot-watch.sh'),
193
- worktreePrune: path.join(TEMPLATE_ROOT, 'scripts', 'agent-worktree-prune.sh'),
194
- lockTool: path.join(TEMPLATE_ROOT, 'scripts', 'agent-file-locks.py'),
195
- planInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-plan-workspace.sh'),
196
- changeInit: path.join(TEMPLATE_ROOT, 'scripts', 'openspec', 'init-change-workspace.sh'),
197
- };
198
-
199
- const USER_LEVEL_SKILL_ASSETS = [
200
- {
201
- source: path.join(TEMPLATE_ROOT, 'codex', 'skills', 'gitguardex', 'SKILL.md'),
202
- destination: path.join('.codex', 'skills', 'gitguardex', 'SKILL.md'),
203
- },
204
- {
205
- source: path.join(TEMPLATE_ROOT, 'codex', 'skills', 'guardex-merge-skills-to-dev', 'SKILL.md'),
206
- destination: path.join('.codex', 'skills', 'guardex-merge-skills-to-dev', 'SKILL.md'),
207
- },
208
- {
209
- source: path.join(TEMPLATE_ROOT, 'claude', 'commands', 'gitguardex.md'),
210
- destination: path.join('.claude', 'commands', 'gitguardex.md'),
211
- },
212
- ];
213
-
214
- const EXECUTABLE_RELATIVE_PATHS = new Set([
215
- ...MANAGED_TEMPLATE_SCRIPT_FILES,
216
- ...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
217
- ]);
218
-
219
- const CRITICAL_GUARDRAIL_PATHS = new Set([
220
- 'AGENTS.md',
221
- ...HOOK_NAMES.map((entry) => path.posix.join('.githooks', entry)),
222
- 'scripts/guardex-env.sh',
223
- ]);
224
-
225
- const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
226
- const AGENTS_BOTS_STATE_RELATIVE = '.omx/state/agents-bots.json';
227
- const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
228
- const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
229
- const GITIGNORE_MARKER_START = '# multiagent-safety:START';
230
- const GITIGNORE_MARKER_END = '# multiagent-safety:END';
231
- const CODEX_WORKTREE_RELATIVE_DIR = path.join('.omx', 'agent-worktrees');
232
- const CLAUDE_WORKTREE_RELATIVE_DIR = path.join('.omc', 'agent-worktrees');
233
- const SHARED_VSCODE_SETTINGS_RELATIVE = path.posix.join('.vscode', 'settings.json');
234
- const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'git.repositoryScanIgnoredFolders';
235
- const AGENT_WORKTREE_RELATIVE_DIRS = [
236
- CODEX_WORKTREE_RELATIVE_DIR,
237
- CLAUDE_WORKTREE_RELATIVE_DIR,
238
- ];
239
- const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
240
- '.omx/agent-worktrees',
241
- '**/.omx/agent-worktrees',
242
- '.omc/agent-worktrees',
243
- '**/.omc/agent-worktrees',
244
- ];
245
- const MANAGED_GITIGNORE_PATHS = [
246
- '.omx/',
247
- '.omc/',
248
- '!.vscode/',
249
- '.vscode/*',
250
- '!.vscode/settings.json',
251
- 'scripts/agent-session-state.js',
252
- 'scripts/guardex-docker-loader.sh',
253
- 'scripts/guardex-env.sh',
254
- 'scripts/install-vscode-active-agents-extension.js',
255
- '.githooks',
256
- 'oh-my-codex/',
257
- LOCK_FILE_RELATIVE,
258
- ];
259
- const REPO_SCAFFOLD_DIRECTORIES = ['bin'];
260
- const OMX_SCAFFOLD_DIRECTORIES = [
261
- '.omx',
262
- '.omx/state',
263
- '.omx/logs',
264
- '.omx/plans',
265
- CODEX_WORKTREE_RELATIVE_DIR,
266
- '.omc',
267
- CLAUDE_WORKTREE_RELATIVE_DIR,
268
- ];
269
- const OMX_SCAFFOLD_FILES = new Map([
270
- ['.omx/notepad.md', '\n\n## WORKING MEMORY\n'],
271
- ['.omx/project-memory.json', '{}\n'],
272
- ]);
273
- const TARGETED_FORCEABLE_MANAGED_PATHS = new Set([
274
- 'AGENTS.md',
275
- '.gitignore',
276
- ...Array.from(OMX_SCAFFOLD_FILES.keys()),
277
- ...REQUIRED_MANAGED_REPO_FILES,
278
- ...LEGACY_WORKFLOW_SHIMS,
279
- ]);
280
- const COMMAND_TYPO_ALIASES = new Map([
281
- ['relaese', 'release'],
282
- ['realaese', 'release'],
283
- ['relase', 'release'],
284
- ['setpu', 'setup'],
285
- ['inti', 'init'],
286
- ['intsall', 'install'],
287
- ['docter', 'doctor'],
288
- ['doctro', 'doctor'],
289
- ['cleunup', 'cleanup'],
290
- ['scna', 'scan'],
291
- ]);
292
- const SUGGESTIBLE_COMMANDS = [
293
- 'status',
294
- 'setup',
295
- 'doctor',
296
- 'branch',
297
- 'locks',
298
- 'worktree',
299
- 'hook',
300
- 'migrate',
301
- 'install-agent-skills',
302
- 'agents',
303
- 'merge',
304
- 'finish',
305
- 'report',
306
- 'protect',
307
- 'sync',
308
- 'cleanup',
309
- 'prompt',
310
- 'help',
311
- 'version',
312
- // deprecated aliases still routable with a warning
313
- 'init',
314
- 'install',
315
- 'fix',
316
- 'scan',
317
- 'review',
318
- 'copy-prompt',
319
- 'copy-commands',
320
- 'print-agents-snippet',
321
- 'release',
322
- ];
323
- const CLI_COMMAND_DESCRIPTIONS = [
324
- ['status', 'Show GitGuardex CLI + service health without modifying files'],
325
- ['setup', 'Install, repair, and verify guardrails (flags: --repair, --install-only, --target)'],
326
- ['doctor', 'Repair drift + verify (auto-sandboxes on protected main)'],
327
- ['branch', 'CLI-owned branch workflow surface (start/finish/merge)'],
328
- ['locks', 'CLI-owned file lock surface (claim/allow-delete/release/status/validate)'],
329
- ['worktree', 'CLI-owned worktree cleanup surface (prune)'],
330
- ['hook', 'Hook dispatch/install surface used by managed shims'],
331
- ['migrate', 'Convert legacy repo-local installs to the zero-copy CLI-owned surface'],
332
- ['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
333
- ['protect', 'Manage protected branches (list/add/remove/set/reset)'],
334
- ['merge', 'Create/reuse an integration lane and merge overlapping agent branches'],
335
- ['sync', 'Sync agent branches with origin/<base>'],
336
- ['finish', 'Commit + PR + merge completed agent branches (--all, --branch)'],
337
- ['cleanup', 'Prune merged/stale agent branches and worktrees'],
338
- ['release', 'Create or update the current GitHub release with README-generated notes'],
339
- ['agents', 'Start/stop repo-scoped review + cleanup bots'],
340
- ['prompt', 'Print AI setup checklist (--exec, --snippet)'],
341
- ['report', 'Security/safety reports (e.g. OpenSSF scorecard)'],
342
- ['help', 'Show this help output'],
343
- ['version', 'Print GitGuardex version'],
344
- ];
345
- const DEPRECATED_COMMAND_ALIASES = new Map([
346
- ['init', { target: 'setup', hint: 'gx setup' }],
347
- ['install', { target: 'setup', hint: 'gx setup --install-only' }],
348
- ['fix', { target: 'setup', hint: 'gx setup --repair' }],
349
- ['scan', { target: 'status', hint: 'gx status --strict' }],
350
- ['copy-prompt', { target: 'prompt', hint: 'gx prompt' }],
351
- ['copy-commands', { target: 'prompt', hint: 'gx prompt --exec' }],
352
- ['print-agents-snippet', { target: 'prompt', hint: 'gx prompt --snippet' }],
353
- ['review', { target: 'agents', hint: 'gx agents start (runs review + cleanup)' }],
354
- ]);
355
- const AGENT_BOT_DESCRIPTIONS = [
356
- ['agents', 'Start/stop review + cleanup bots for this repo'],
357
- ];
358
- const DOCTOR_AUTO_FINISH_DETAIL_LIMIT = 6;
359
- const DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX = 72;
360
- const DOCTOR_AUTO_FINISH_MESSAGE_MAX = 160;
361
-
362
130
  function getSandboxApi() {
363
131
  if (!sandboxApi) {
364
132
  sandboxApi = sandboxModule.createSandboxApi({
@@ -439,114 +207,6 @@ function getFinishApi() {
439
207
  return finishApi;
440
208
  }
441
209
 
442
- function envFlagIsTruthy(raw) {
443
- const lowered = String(raw || '').trim().toLowerCase();
444
- return lowered === '1' || lowered === 'true' || lowered === 'yes' || lowered === 'on';
445
- }
446
-
447
- function isClaudeCodeSession(env = process.env) {
448
- return envFlagIsTruthy(env.CLAUDECODE) || Boolean(env.CLAUDE_CODE_SESSION_ID);
449
- }
450
-
451
- function defaultAgentWorktreeRelativeDir(env = process.env) {
452
- return isClaudeCodeSession(env) ? CLAUDE_WORKTREE_RELATIVE_DIR : CODEX_WORKTREE_RELATIVE_DIR;
453
- }
454
-
455
- const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in this repo.
456
-
457
- 1) Install: ${GLOBAL_INSTALL_COMMAND} && gh --version
458
- 2) Bootstrap: gx setup
459
- 3) Repair: gx doctor
460
- 4) Task loop: gx branch start "<task>" "<agent>"
461
- then gx locks claim --branch "<agent-branch>" <file...> -> gx branch finish
462
- 5) Integrate: gx merge --branch <agent-a> --branch <agent-b>
463
- 6) Finish: gx finish --all
464
- 7) Cleanup: gx cleanup
465
- 8) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive
466
- 9) Optional: gx protect add release staging
467
- 10) Optional: gx sync --check && gx sync
468
- 11) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY
469
- 12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml
470
- `;
471
-
472
- const AI_SETUP_COMMANDS = `${GLOBAL_INSTALL_COMMAND}
473
- gh --version
474
- gx setup
475
- gx doctor
476
- gx branch start "<task>" "<agent>"
477
- gx locks claim --branch "<agent-branch>" <file...>
478
- gx merge --branch "<agent-a>" --branch "<agent-b>"
479
- gx finish --all
480
- gx cleanup
481
- gx protect add release staging
482
- gx sync --check && gx sync
483
- `;
484
-
485
- const SCORECARD_RISK_BY_CHECK = {
486
- 'Dangerous-Workflow': 'Critical',
487
- 'Code-Review': 'High',
488
- Maintained: 'High',
489
- 'Binary-Artifacts': 'High',
490
- 'Dependency-Update-Tool': 'High',
491
- 'Token-Permissions': 'High',
492
- Vulnerabilities: 'High',
493
- 'Branch-Protection': 'High',
494
- Fuzzing: 'Medium',
495
- 'Pinned-Dependencies': 'Medium',
496
- SAST: 'Medium',
497
- 'Security-Policy': 'Medium',
498
- 'CII-Best-Practices': 'Low',
499
- Contributors: 'Low',
500
- License: 'Low',
501
- };
502
-
503
- function runtimeVersion() {
504
- return `${packageJson.name}/${packageJson.version} ${process.platform}-${process.arch} node-${process.version}`;
505
- }
506
-
507
- function supportsAnsiColors() {
508
- const forced = String(process.env.FORCE_COLOR || '').trim().toLowerCase();
509
- if (['0', 'false', 'no', 'off'].includes(forced)) {
510
- return false;
511
- }
512
- if (forced.length > 0) {
513
- return true;
514
- }
515
- if (process.env.NO_COLOR) {
516
- return false;
517
- }
518
- return Boolean(process.stdout.isTTY) && process.env.TERM !== 'dumb';
519
- }
520
-
521
- function colorize(text, colorCode) {
522
- if (!supportsAnsiColors()) {
523
- return text;
524
- }
525
- return `\u001B[${colorCode}m${text}\u001B[0m`;
526
- }
527
-
528
- function doctorOutputColorCode(status) {
529
- const normalized = String(status || '').trim().toLowerCase();
530
- if (['active', 'done', 'ok', 'safe', 'success'].includes(normalized)) {
531
- return '32';
532
- }
533
- if (normalized === 'disabled') {
534
- return '36';
535
- }
536
- if (['degraded', 'pending', 'skip', 'warn', 'warning'].includes(normalized)) {
537
- return '33';
538
- }
539
- if (['error', 'fail', 'inactive', 'unsafe'].includes(normalized)) {
540
- return '31';
541
- }
542
- return null;
543
- }
544
-
545
- function colorizeDoctorOutput(text, status) {
546
- const colorCode = doctorOutputColorCode(status);
547
- return colorCode ? colorize(text, colorCode) : text;
548
- }
549
-
550
210
  /**
551
211
  * @typedef {Object} AutoFinishSummary
552
212
  * @property {boolean} [enabled]
@@ -601,658 +261,6 @@ function colorizeDoctorOutput(text, status) {
601
261
  * @property {AutoFinishSummary} autoFinish
602
262
  * @property {string | null} sandboxLockContent
603
263
  */
604
-
605
- /**
606
- * @param {string | null | undefined} detail
607
- * @returns {string | null}
608
- */
609
- function detectAutoFinishDetailStatus(detail) {
610
- const trimmed = String(detail || '').trim();
611
- const match = trimmed.match(/^\[(\w+)\]/);
612
- if (match) {
613
- return match[1].toLowerCase();
614
- }
615
- if (/^Skipped\b/i.test(trimmed) || /^No local agent branches found\b/i.test(trimmed)) {
616
- return 'skip';
617
- }
618
- return null;
619
- }
620
-
621
- /**
622
- * @param {AutoFinishSummary | null | undefined} summary
623
- * @returns {string | null}
624
- */
625
- function detectAutoFinishSummaryStatus(summary) {
626
- if (!summary || summary.enabled === false) {
627
- return detectAutoFinishDetailStatus(summary?.details?.[0]);
628
- }
629
- if ((summary.failed || 0) > 0) {
630
- return 'fail';
631
- }
632
- if ((summary.completed || 0) > 0) {
633
- return 'done';
634
- }
635
- if ((summary.skipped || 0) > 0) {
636
- return 'skip';
637
- }
638
- return null;
639
- }
640
-
641
- function statusDot(status) {
642
- if (status === 'active') {
643
- return colorize('●', '32'); // green
644
- }
645
- if (status === 'inactive') {
646
- return colorize('●', '31'); // red
647
- }
648
- if (status === 'disabled') {
649
- return colorize('●', '36'); // cyan
650
- }
651
- return colorize('●', '33'); // yellow for degraded/unknown
652
- }
653
-
654
- function commandCatalogLines(indent = ' ') {
655
- const maxCommandLength = CLI_COMMAND_DESCRIPTIONS.reduce(
656
- (max, [command]) => Math.max(max, command.length),
657
- 0,
658
- );
659
- return CLI_COMMAND_DESCRIPTIONS.map(
660
- ([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
661
- );
662
- }
663
-
664
- function agentBotCatalogLines(indent = ' ') {
665
- const maxCommandLength = AGENT_BOT_DESCRIPTIONS.reduce(
666
- (max, [command]) => Math.max(max, command.length),
667
- 0,
668
- );
669
- return AGENT_BOT_DESCRIPTIONS.map(
670
- ([command, description]) => `${indent}${command.padEnd(maxCommandLength + 2)}${description}`,
671
- );
672
- }
673
-
674
- function repoToggleLines(indent = ' ') {
675
- return [
676
- `${indent}Set repo-root .env: ${GUARDEX_REPO_TOGGLE_ENV}=0 disables Guardex, ${GUARDEX_REPO_TOGGLE_ENV}=1 enables it again`,
677
- ];
678
- }
679
-
680
- function printToolLogsSummary() {
681
- const usageLine = ` $ ${SHORT_TOOL_NAME} <command> [options]`;
682
- const commandDetails = commandCatalogLines(' ');
683
- const agentBotDetails = agentBotCatalogLines(' ');
684
- const repoToggleDetails = repoToggleLines(' ');
685
-
686
- if (!supportsAnsiColors()) {
687
- console.log(`${TOOL_NAME}-tools logs:`);
688
- console.log(' USAGE');
689
- console.log(usageLine);
690
- console.log(' COMMANDS');
691
- for (const line of commandDetails) {
692
- console.log(line);
693
- }
694
- console.log(' AGENT BOT');
695
- for (const line of agentBotDetails) {
696
- console.log(line);
697
- }
698
- console.log(' REPO TOGGLE');
699
- for (const line of repoToggleDetails) {
700
- console.log(line);
701
- }
702
- return;
703
- }
704
-
705
- const title = colorize(`${TOOL_NAME}-tools logs`, '1;36');
706
- const usageHeader = colorize('USAGE', '1');
707
- const commandsHeader = colorize('COMMANDS', '1');
708
- const agentBotHeader = colorize('AGENT BOT', '1');
709
- const repoToggleHeader = colorize('REPO TOGGLE', '1');
710
- const pipe = colorize('│', '90');
711
- const tee = colorize('├', '90');
712
- const corner = colorize('└', '90');
713
-
714
- console.log(`${title}:`);
715
- console.log(` ${tee}─ ${usageHeader}`);
716
- console.log(` ${pipe}${usageLine}`);
717
- console.log(` ${tee}─ ${commandsHeader}`);
718
- for (const line of commandDetails) {
719
- if (!line) {
720
- console.log(` ${pipe}`);
721
- continue;
722
- }
723
- console.log(` ${pipe}${line.slice(2)}`);
724
- }
725
- console.log(` ${tee}─ ${agentBotHeader}`);
726
- for (const line of agentBotDetails) {
727
- if (!line) {
728
- console.log(` ${pipe}`);
729
- continue;
730
- }
731
- console.log(` ${pipe}${line.slice(2)}`);
732
- }
733
- console.log(` ${tee}─ ${repoToggleHeader}`);
734
- for (const line of repoToggleDetails) {
735
- if (!line) {
736
- console.log(` ${pipe}`);
737
- continue;
738
- }
739
- console.log(` ${pipe}${line.slice(2)}`);
740
- }
741
- console.log(` ${corner}─ ${colorize(`Try '${TOOL_NAME} doctor' for one-step repair + verification.`, '2')}`);
742
- }
743
-
744
- function usage(options = {}) {
745
- const { outsideGitRepo = false } = options;
746
-
747
- console.log(`A command-line tool that sets up hardened multi-agent safety for git repositories.
748
-
749
- VERSION
750
- ${runtimeVersion()}
751
-
752
- USAGE
753
- $ ${SHORT_TOOL_NAME} <command> [options]
754
-
755
- COMMANDS
756
- ${commandCatalogLines().join('\n')}
757
-
758
- AGENT BOT
759
- ${agentBotCatalogLines().join('\n')}
760
-
761
- REPO TOGGLE
762
- ${repoToggleLines().join('\n')}
763
-
764
- NOTES
765
- - No command = ${SHORT_TOOL_NAME} status. ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup.
766
- - Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
767
- - Target another repo: ${SHORT_TOOL_NAME} <cmd> --target <repo-path>.
768
- - On protected main, setup/install/fix/doctor auto-sandbox via agent branch + PR flow.
769
- - Run '${SHORT_TOOL_NAME} cleanup' to prune merged agent branches/worktrees.
770
- - Legacy aliases: ${LEGACY_NAMES.join(', ')}.`);
771
-
772
- if (outsideGitRepo) {
773
- console.log(`
774
- [${TOOL_NAME}] No git repository detected in current directory.
775
- [${TOOL_NAME}] Start from a repo root, or pass an explicit target:
776
- ${TOOL_NAME} setup --target <path-to-git-repo>
777
- ${TOOL_NAME} doctor --target <path-to-git-repo>`);
778
- }
779
- }
780
-
781
- function run(cmd, args, options = {}) {
782
- return cp.spawnSync(cmd, args, {
783
- encoding: 'utf8',
784
- stdio: options.stdio || 'pipe',
785
- cwd: options.cwd,
786
- env: options.env ? { ...process.env, ...options.env } : process.env,
787
- timeout: options.timeout,
788
- });
789
- }
790
-
791
- function extractTargetedArgs(rawArgs, defaultTarget = process.cwd()) {
792
- const passthrough = [];
793
- let target = defaultTarget;
794
-
795
- for (let index = 0; index < rawArgs.length; index += 1) {
796
- const arg = rawArgs[index];
797
- if (arg === '--target' || arg === '-t') {
798
- target = requireValue(rawArgs, index, '--target');
799
- index += 1;
800
- continue;
801
- }
802
- passthrough.push(arg);
803
- }
804
-
805
- return { target, passthrough };
806
- }
807
-
808
- function packageAssetEnv(extraEnv = {}) {
809
- return {
810
- GUARDEX_CLI_ENTRY: __filename,
811
- GUARDEX_NODE_BIN: process.execPath,
812
- ...extraEnv,
813
- };
814
- }
815
-
816
- function packageAssetPath(assetKey) {
817
- const assetPath = PACKAGE_SCRIPT_ASSETS[assetKey];
818
- if (!assetPath) {
819
- throw new Error(`Unknown package asset: ${assetKey}`);
820
- }
821
- if (!fs.existsSync(assetPath)) {
822
- throw new Error(`Missing package asset: ${assetPath}`);
823
- }
824
- return assetPath;
825
- }
826
-
827
- function runPackageAsset(assetKey, rawArgs, options = {}) {
828
- const assetPath = packageAssetPath(assetKey);
829
- let cmd = 'bash';
830
- if (assetPath.endsWith('.py')) {
831
- cmd = 'python3';
832
- } else if (assetPath.endsWith('.js')) {
833
- cmd = process.execPath;
834
- }
835
- return run(cmd, [assetPath, ...rawArgs], {
836
- cwd: options.cwd || process.cwd(),
837
- stdio: options.stdio || 'pipe',
838
- timeout: options.timeout,
839
- env: packageAssetEnv(options.env),
840
- });
841
- }
842
-
843
- function repoLocalLegacyScriptPath(repoRoot, relativePath) {
844
- const assetPath = path.join(repoRoot, relativePath);
845
- return fs.existsSync(assetPath) ? assetPath : null;
846
- }
847
-
848
- function runReviewBotCommand(repoRoot, rawArgs, options = {}) {
849
- const legacyScript = repoLocalLegacyScriptPath(repoRoot, 'scripts/review-bot-watch.sh');
850
- if (legacyScript) {
851
- return run('bash', [legacyScript, ...rawArgs], {
852
- cwd: repoRoot,
853
- stdio: options.stdio || 'pipe',
854
- timeout: options.timeout,
855
- env: packageAssetEnv(options.env),
856
- });
857
- }
858
- return runPackageAsset('reviewBot', rawArgs, {
859
- ...options,
860
- cwd: repoRoot,
861
- });
862
- }
863
-
864
- function invokePackageAsset(assetKey, rawArgs, options = {}) {
865
- const result = runPackageAsset(assetKey, rawArgs, options);
866
- if (result.stdout) process.stdout.write(result.stdout);
867
- if (result.stderr) process.stderr.write(result.stderr);
868
- if (result.status !== 0) {
869
- throw new Error(`${assetKey} command failed with status ${result.status}`);
870
- }
871
- process.exitCode = 0;
872
- return result;
873
- }
874
-
875
- function formatElapsedDuration(ms) {
876
- const durationMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
877
- if (durationMs < 1000) {
878
- return `${Math.round(durationMs)}ms`;
879
- }
880
- if (durationMs < 10_000) {
881
- return `${(durationMs / 1000).toFixed(1)}s`;
882
- }
883
- return `${Math.round(durationMs / 1000)}s`;
884
- }
885
-
886
- function truncateMiddle(value, maxLength) {
887
- const text = String(value || '');
888
- const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
889
- if (!limit || text.length <= limit) {
890
- return text;
891
- }
892
-
893
- const visible = limit - 1;
894
- const headLength = Math.ceil(visible / 2);
895
- const tailLength = Math.floor(visible / 2);
896
- return `${text.slice(0, headLength)}…${text.slice(text.length - tailLength)}`;
897
- }
898
-
899
- function truncateTail(value, maxLength) {
900
- const text = String(value || '');
901
- const limit = Number.isFinite(maxLength) ? Math.max(4, maxLength) : 0;
902
- if (!limit || text.length <= limit) {
903
- return text;
904
- }
905
- return `${text.slice(0, limit - 1)}…`;
906
- }
907
-
908
- function compactAutoFinishPathSegments(message) {
909
- return String(message || '').replace(/\((\/[^)]+)\)/g, (_, rawPath) => {
910
- if (
911
- rawPath.includes(`${path.sep}.omx${path.sep}agent-worktrees${path.sep}`) ||
912
- rawPath.includes(`${path.sep}.omc${path.sep}agent-worktrees${path.sep}`)
913
- ) {
914
- return `(${path.basename(rawPath)})`;
915
- }
916
- return `(${truncateMiddle(rawPath, 72)})`;
917
- });
918
- }
919
-
920
- function detectRecoverableAutoFinishConflict(message) {
921
- const text = String(message || '').trim();
922
- if (!text) {
923
- return null;
924
- }
925
-
926
- if (/rebase --continue/i.test(text) && /rebase --abort/i.test(text)) {
927
- return {
928
- rawLabel: 'auto-finish requires manual rebase.',
929
- summary: 'manual rebase required in the source-probe worktree; run rebase --continue or rebase --abort',
930
- };
931
- }
932
-
933
- if (/Rebase\/merge '.+' into '.+' and resolve conflicts before finishing\./i.test(text)) {
934
- return {
935
- rawLabel: 'auto-finish requires manual rebase or merge.',
936
- summary: 'manual rebase or merge required before auto-finish can continue',
937
- };
938
- }
939
-
940
- if (/Merge conflict detected while merging/i.test(text)) {
941
- return {
942
- rawLabel: 'auto-finish requires manual merge resolution.',
943
- summary: 'manual merge resolution required before auto-finish can continue',
944
- };
945
- }
946
-
947
- return null;
948
- }
949
-
950
- function summarizeAutoFinishDetail(detail) {
951
- const trimmed = String(detail || '').trim();
952
- const match = trimmed.match(/^\[(\w+)\]\s+([^:]+):\s*(.*)$/);
953
- if (!match) {
954
- return truncateTail(compactAutoFinishPathSegments(trimmed), DOCTOR_AUTO_FINISH_MESSAGE_MAX);
955
- }
956
-
957
- const [, status, rawBranch, rawMessage] = match;
958
- const branch = truncateMiddle(rawBranch, DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX);
959
- let message = String(rawMessage || '').trim();
960
- const recoverableConflict = status === 'skip' ? detectRecoverableAutoFinishConflict(message) : null;
961
-
962
- if (recoverableConflict) {
963
- message = recoverableConflict.summary;
964
- } else if (status === 'fail') {
965
- message = message.replace(/^auto-finish failed\.?\s*/i, '');
966
- if (/\[agent-sync-guard\]/.test(message) && /Resolve conflicts/i.test(message)) {
967
- message = 'rebase conflict in finish flow; run rebase --continue or rebase --abort in the source-probe worktree';
968
- } else if (/unable to compute ahead\/behind/i.test(message)) {
969
- const aheadBehindMatch = message.match(/unable to compute ahead\/behind(?: \([^)]+\))?/i);
970
- if (aheadBehindMatch) {
971
- message = aheadBehindMatch[0];
972
- }
973
- } else if (/remote ref does not exist/i.test(message)) {
974
- message = 'branch merged, but the remote ref was already removed during cleanup';
975
- }
976
- }
977
-
978
- message = compactAutoFinishPathSegments(message)
979
- .replace(/\s+\|\s+/g, '; ')
980
- .trim();
981
-
982
- return `[${status}] ${branch}: ${truncateTail(message, DOCTOR_AUTO_FINISH_MESSAGE_MAX)}`;
983
- }
984
-
985
- /**
986
- * @param {AutoFinishSummary | null | undefined} summary
987
- * @param {{ baseBranch?: string, verbose?: boolean, detailLimit?: number }} [options]
988
- */
989
- function printAutoFinishSummary(summary, options = {}) {
990
- const enabled = Boolean(summary && summary.enabled);
991
- const details = Array.isArray(summary && summary.details) ? summary.details : [];
992
- const baseBranch = String(options.baseBranch || summary?.baseBranch || '').trim();
993
- const verbose = Boolean(options.verbose);
994
- const detailLimit = Number.isFinite(options.detailLimit)
995
- ? Math.max(0, options.detailLimit)
996
- : DOCTOR_AUTO_FINISH_DETAIL_LIMIT;
997
-
998
- if (enabled) {
999
- console.log(
1000
- colorizeDoctorOutput(
1001
- `[${TOOL_NAME}] Auto-finish sweep (base=${baseBranch}): attempted=${summary.attempted}, completed=${summary.completed}, skipped=${summary.skipped}, failed=${summary.failed}`,
1002
- detectAutoFinishSummaryStatus(summary),
1003
- ),
1004
- );
1005
- const visibleDetails = verbose ? details : details.slice(0, detailLimit).map(summarizeAutoFinishDetail);
1006
- for (const detail of visibleDetails) {
1007
- console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
1008
- }
1009
- if (!verbose && details.length > detailLimit) {
1010
- console.log(
1011
- colorizeDoctorOutput(
1012
- `[${TOOL_NAME}] … ${details.length - detailLimit} more branch result(s). Re-run with --verbose-auto-finish for full details.`,
1013
- 'warn',
1014
- ),
1015
- );
1016
- }
1017
- return;
1018
- }
1019
-
1020
- if (details.length > 0) {
1021
- const detail = verbose ? details[0] : summarizeAutoFinishDetail(details[0]);
1022
- console.log(colorizeDoctorOutput(`[${TOOL_NAME}] ${detail}`, detectAutoFinishDetailStatus(detail)));
1023
- }
1024
- }
1025
-
1026
- function gitRun(repoRoot, args, { allowFailure = false } = {}) {
1027
- const result = run('git', ['-C', repoRoot, ...args]);
1028
- if (!allowFailure && result.status !== 0) {
1029
- throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || '').trim()}`);
1030
- }
1031
- return result;
1032
- }
1033
-
1034
- function resolveRepoRoot(targetPath) {
1035
- const resolvedTarget = path.resolve(targetPath || process.cwd());
1036
- const result = run('git', ['-C', resolvedTarget, 'rev-parse', '--show-toplevel']);
1037
- if (result.status !== 0) {
1038
- const stderr = (result.stderr || '').trim();
1039
- throw new Error(
1040
- `Target is not inside a git repository: ${resolvedTarget}${stderr ? `\n${stderr}` : ''}`,
1041
- );
1042
- }
1043
- return result.stdout.trim();
1044
- }
1045
-
1046
- function isGitRepo(targetPath) {
1047
- const resolvedTarget = path.resolve(targetPath || process.cwd());
1048
- const result = run('git', ['-C', resolvedTarget, 'rev-parse', '--show-toplevel']);
1049
- return result.status === 0;
1050
- }
1051
-
1052
- const NESTED_REPO_DEFAULT_MAX_DEPTH = 6;
1053
- const NESTED_REPO_DEFAULT_SKIP_DIRS = new Set([
1054
- 'node_modules',
1055
- '.git',
1056
- 'dist',
1057
- 'build',
1058
- '.next',
1059
- '.cache',
1060
- 'target',
1061
- 'vendor',
1062
- '.venv',
1063
- '.pnpm-store',
1064
- ]);
1065
- function discoverNestedGitRepos(rootPath, opts = {}) {
1066
- const maxDepth = Number.isFinite(opts.maxDepth) ? Math.max(1, opts.maxDepth) : NESTED_REPO_DEFAULT_MAX_DEPTH;
1067
- const extraSkip = new Set(Array.isArray(opts.extraSkip) ? opts.extraSkip : []);
1068
- const includeSubmodules = Boolean(opts.includeSubmodules);
1069
- const resolvedRoot = path.resolve(rootPath);
1070
-
1071
- const rootCommonDir = (() => {
1072
- const result = run('git', ['-C', resolvedRoot, 'rev-parse', '--git-common-dir'], { cwd: resolvedRoot });
1073
- if (result.status !== 0) return null;
1074
- const raw = result.stdout.trim();
1075
- if (!raw) return null;
1076
- return path.resolve(resolvedRoot, raw);
1077
- })();
1078
-
1079
- const worktreeSkipAbsolutes = AGENT_WORKTREE_RELATIVE_DIRS.map((relativeDir) => path.join(resolvedRoot, relativeDir));
1080
- const found = new Set();
1081
- found.add(resolvedRoot);
1082
-
1083
- function shouldSkipDir(dirName) {
1084
- return NESTED_REPO_DEFAULT_SKIP_DIRS.has(dirName) || extraSkip.has(dirName);
1085
- }
1086
-
1087
- function walk(currentPath, depth) {
1088
- if (depth > maxDepth) return;
1089
- let entries;
1090
- try {
1091
- entries = fs.readdirSync(currentPath, { withFileTypes: true });
1092
- } catch {
1093
- return;
1094
- }
1095
-
1096
- for (const entry of entries) {
1097
- const entryPath = path.join(currentPath, entry.name);
1098
-
1099
- if (entry.name === '.git') {
1100
- if (entry.isDirectory()) {
1101
- if (entryPath === path.join(resolvedRoot, '.git')) continue;
1102
- found.add(path.dirname(entryPath));
1103
- } else if (includeSubmodules && entry.isFile()) {
1104
- found.add(path.dirname(entryPath));
1105
- }
1106
- continue;
1107
- }
1108
-
1109
- if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
1110
- if (shouldSkipDir(entry.name)) continue;
1111
- if (worktreeSkipAbsolutes.includes(entryPath)) continue;
1112
- walk(entryPath, depth + 1);
1113
- }
1114
- }
1115
-
1116
- walk(resolvedRoot, 0);
1117
-
1118
- const filtered = Array.from(found).filter((repoPath) => {
1119
- if (repoPath === resolvedRoot) return true;
1120
- if (!rootCommonDir) return true;
1121
- const childResult = run('git', ['-C', repoPath, 'rev-parse', '--git-common-dir'], { cwd: repoPath });
1122
- if (childResult.status !== 0) return true;
1123
- const childCommonDirRaw = childResult.stdout.trim();
1124
- if (!childCommonDirRaw) return true;
1125
- const childCommonDir = path.resolve(repoPath, childCommonDirRaw);
1126
- return childCommonDir !== rootCommonDir;
1127
- });
1128
-
1129
- const [root, ...rest] = filtered;
1130
- rest.sort((a, b) => a.localeCompare(b));
1131
- return [root, ...rest];
1132
- }
1133
-
1134
- function toDestinationPath(relativeTemplatePath) {
1135
- if (relativeTemplatePath.startsWith('scripts/')) {
1136
- return relativeTemplatePath;
1137
- }
1138
- if (relativeTemplatePath.startsWith('githooks/')) {
1139
- return `.${relativeTemplatePath}`;
1140
- }
1141
- if (relativeTemplatePath.startsWith('codex/')) {
1142
- return `.${relativeTemplatePath}`;
1143
- }
1144
- if (relativeTemplatePath.startsWith('claude/')) {
1145
- return `.${relativeTemplatePath}`;
1146
- }
1147
- if (relativeTemplatePath.startsWith('github/')) {
1148
- return `.${relativeTemplatePath}`;
1149
- }
1150
- if (relativeTemplatePath.startsWith('vscode/')) {
1151
- return relativeTemplatePath;
1152
- }
1153
- throw new Error(`Unsupported template path: ${relativeTemplatePath}`);
1154
- }
1155
-
1156
- function ensureParentDir(repoRoot, filePath, dryRun) {
1157
- if (dryRun) return;
1158
-
1159
- const parentDir = path.dirname(filePath);
1160
- const relativeParentDir = path.relative(repoRoot, parentDir);
1161
- const segments = relativeParentDir.split(path.sep).filter(Boolean);
1162
- let currentPath = repoRoot;
1163
-
1164
- for (const segment of segments) {
1165
- currentPath = path.join(currentPath, segment);
1166
- if (fs.existsSync(currentPath) && !fs.statSync(currentPath).isDirectory()) {
1167
- const blockingPath = path.relative(repoRoot, currentPath) || path.basename(currentPath);
1168
- const targetPath = path.relative(repoRoot, filePath) || path.basename(filePath);
1169
- throw new Error(
1170
- `Path conflict: ${blockingPath} exists as a file, but ${targetPath} needs it to be a directory. ` +
1171
- `Remove or rename ${blockingPath} and rerun '${SHORT_TOOL_NAME} setup'.`,
1172
- );
1173
- }
1174
- }
1175
-
1176
- fs.mkdirSync(parentDir, { recursive: true });
1177
- }
1178
-
1179
- function ensureExecutable(destinationPath, relativePath, dryRun) {
1180
- if (dryRun) return;
1181
- if (EXECUTABLE_RELATIVE_PATHS.has(relativePath)) {
1182
- fs.chmodSync(destinationPath, 0o755);
1183
- }
1184
- }
1185
-
1186
- function isCriticalGuardrailPath(relativePath) {
1187
- return CRITICAL_GUARDRAIL_PATHS.has(relativePath);
1188
- }
1189
-
1190
- function shellSingleQuote(value) {
1191
- return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
1192
- }
1193
-
1194
- function renderShellDispatchShim(commandParts) {
1195
- const rendered = commandParts.map((part) => shellSingleQuote(part)).join(' ');
1196
- return (
1197
- '#!/usr/bin/env bash\n' +
1198
- 'set -euo pipefail\n' +
1199
- '\n' +
1200
- 'if [[ -n "${GUARDEX_CLI_ENTRY:-}" ]]; then\n' +
1201
- ' node_bin="${GUARDEX_NODE_BIN:-node}"\n' +
1202
- ` exec "$node_bin" "$GUARDEX_CLI_ENTRY" ${rendered} "$@"\n` +
1203
- 'fi\n' +
1204
- '\n' +
1205
- 'resolve_guardex_cli() {\n' +
1206
- ' if [[ -n "${GUARDEX_CLI_BIN:-}" ]]; then\n' +
1207
- ' printf \'%s\' "$GUARDEX_CLI_BIN"\n' +
1208
- ' return 0\n' +
1209
- ' fi\n' +
1210
- ' if command -v gx >/dev/null 2>&1; then\n' +
1211
- ' printf \'%s\' "gx"\n' +
1212
- ' return 0\n' +
1213
- ' fi\n' +
1214
- ' if command -v gitguardex >/dev/null 2>&1; then\n' +
1215
- ' printf \'%s\' "gitguardex"\n' +
1216
- ' return 0\n' +
1217
- ' fi\n' +
1218
- ' echo "[gitguardex-shim] Missing gx CLI in PATH." >&2\n' +
1219
- ' exit 1\n' +
1220
- '}\n' +
1221
- '\n' +
1222
- 'cli_bin="$(resolve_guardex_cli)"\n' +
1223
- `exec "$cli_bin" ${rendered} "$@"\n`
1224
- );
1225
- }
1226
-
1227
- function renderPythonDispatchShim(commandParts) {
1228
- return (
1229
- '#!/usr/bin/env python3\n' +
1230
- 'import os\n' +
1231
- 'import shutil\n' +
1232
- 'import subprocess\n' +
1233
- 'import sys\n' +
1234
- '\n' +
1235
- `COMMAND = ${JSON.stringify(commandParts)}\n` +
1236
- '\n' +
1237
- 'entry = os.environ.get("GUARDEX_CLI_ENTRY")\n' +
1238
- 'if entry:\n' +
1239
- ' node_bin = os.environ.get("GUARDEX_NODE_BIN") or shutil.which("node") or "node"\n' +
1240
- ' raise SystemExit(subprocess.call([node_bin, entry, *COMMAND, *sys.argv[1:]]))\n' +
1241
- 'cli = os.environ.get("GUARDEX_CLI_BIN") or shutil.which("gx") or shutil.which("gitguardex")\n' +
1242
- 'if not cli:\n' +
1243
- ' sys.stderr.write("[gitguardex-shim] Missing gx CLI in PATH.\\n")\n' +
1244
- ' raise SystemExit(1)\n' +
1245
- 'raise SystemExit(subprocess.call([cli, *COMMAND, *sys.argv[1:]]))\n'
1246
- );
1247
- }
1248
-
1249
- function managedForceConflictMessage(relativePath) {
1250
- return (
1251
- `Refusing to overwrite existing file without --force: ${relativePath}\n` +
1252
- `Use '--force ${relativePath}' to rewrite only this managed file, or '--force' to rewrite all managed files.`
1253
- );
1254
- }
1255
-
1256
264
  function renderManagedFile(repoRoot, relativePath, content, options = {}) {
1257
265
  const destinationPath = path.join(repoRoot, relativePath);
1258
266
  const destinationExists = fs.existsSync(destinationPath);
@@ -1854,252 +862,29 @@ function configureHooks(repoRoot, dryRun) {
1854
862
  throw new Error(`Failed to set git hooksPath: ${(result.stderr || '').trim()}`);
1855
863
  }
1856
864
 
1857
- return { status: 'set', key: 'core.hooksPath', value: '.githooks' };
1858
- }
1859
-
1860
- function requireValue(rawArgs, index, flagName) {
1861
- const value = rawArgs[index + 1];
1862
- if (!value || value.startsWith('-')) {
1863
- throw new Error(`${flagName} requires a value`);
1864
- }
1865
- return value;
1866
- }
1867
-
1868
- function normalizeManagedForcePath(rawPath) {
1869
- if (typeof rawPath !== 'string') {
1870
- return null;
1871
- }
1872
- const normalized = path.posix.normalize(rawPath.replace(/\\/g, '/'));
1873
- if (!normalized || normalized === '.' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
1874
- return null;
1875
- }
1876
- return normalized.startsWith('./') ? normalized.slice(2) : normalized;
1877
- }
1878
-
1879
- function collectForceManagedPaths(rawArgs, startIndex) {
1880
- const forceManagedPaths = [];
1881
- let nextIndex = startIndex;
1882
-
1883
- while (nextIndex + 1 < rawArgs.length) {
1884
- const candidate = rawArgs[nextIndex + 1];
1885
- if (!candidate || candidate.startsWith('-')) {
1886
- break;
1887
- }
1888
- const normalized = normalizeManagedForcePath(candidate);
1889
- if (!normalized || !TARGETED_FORCEABLE_MANAGED_PATHS.has(normalized)) {
1890
- throw new Error(`Unknown managed path after --force: ${candidate}`);
1891
- }
1892
- forceManagedPaths.push(normalized);
1893
- nextIndex += 1;
1894
- }
1895
-
1896
- return { forceManagedPaths, nextIndex };
1897
- }
1898
-
1899
- function appendForceArgs(args, options) {
1900
- if (!options.force) {
1901
- return;
1902
- }
1903
- args.push('--force');
1904
- for (const managedPath of options.forceManagedPaths || []) {
1905
- args.push(managedPath);
1906
- }
1907
- }
1908
-
1909
- function shouldForceManagedPath(options, relativePath) {
1910
- if (!options.force) {
1911
- return false;
1912
- }
1913
- const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
1914
- if (targetedPaths.length === 0) {
1915
- return true;
1916
- }
1917
- const normalized = normalizeManagedForcePath(relativePath);
1918
- return normalized !== null && targetedPaths.includes(normalized);
1919
- }
1920
-
1921
- function parseCommonArgs(rawArgs, defaults) {
1922
- const options = { ...defaults };
1923
- const supportsForce = Object.prototype.hasOwnProperty.call(options, 'force');
1924
- if (supportsForce && !Array.isArray(options.forceManagedPaths)) {
1925
- options.forceManagedPaths = [];
1926
- }
1927
-
1928
- for (let index = 0; index < rawArgs.length; index += 1) {
1929
- const arg = rawArgs[index];
1930
- if (arg === '--target' || arg === '-t') {
1931
- options.target = requireValue(rawArgs, index, '--target');
1932
- index += 1;
1933
- continue;
1934
- }
1935
- if (arg === '--dry-run') {
1936
- options.dryRun = true;
1937
- continue;
1938
- }
1939
- if (arg === '--skip-agents') {
1940
- options.skipAgents = true;
1941
- continue;
1942
- }
1943
- if (arg === '--skip-package-json') {
1944
- options.skipPackageJson = true;
1945
- continue;
1946
- }
1947
- if (arg === '--force') {
1948
- if (!supportsForce) {
1949
- throw new Error(`Unknown option: ${arg}`);
1950
- }
1951
- options.force = true;
1952
- const parsed = collectForceManagedPaths(rawArgs, index);
1953
- if (parsed.forceManagedPaths.length > 0) {
1954
- options.forceManagedPaths = Array.from(
1955
- new Set([...(options.forceManagedPaths || []), ...parsed.forceManagedPaths]),
1956
- );
1957
- }
1958
- index = parsed.nextIndex;
1959
- continue;
1960
- }
1961
- if (arg === '--keep-stale-locks') {
1962
- options.dropStaleLocks = false;
1963
- continue;
1964
- }
1965
- if (arg === '--json') {
1966
- options.json = true;
1967
- continue;
1968
- }
1969
- if (arg === '--yes-global-install') {
1970
- options.yesGlobalInstall = true;
1971
- continue;
1972
- }
1973
- if (arg === '--no-global-install') {
1974
- options.noGlobalInstall = true;
1975
- continue;
1976
- }
1977
- if (arg === '--no-gitignore') {
1978
- options.skipGitignore = true;
1979
- continue;
1980
- }
1981
- if (arg === '--allow-protected-base-write') {
1982
- options.allowProtectedBaseWrite = true;
1983
- continue;
1984
- }
1985
- if (Object.prototype.hasOwnProperty.call(options, 'waitForMerge') && arg === '--wait-for-merge') {
1986
- options.waitForMerge = true;
1987
- continue;
1988
- }
1989
- if (Object.prototype.hasOwnProperty.call(options, 'waitForMerge') && arg === '--no-wait-for-merge') {
1990
- options.waitForMerge = false;
1991
- continue;
1992
- }
1993
-
1994
- throw new Error(`Unknown option: ${arg}`);
1995
- }
1996
-
1997
- if (!options.target) {
1998
- throw new Error('--target requires a path value');
1999
- }
2000
-
2001
- return options;
2002
- }
2003
-
2004
- function parseRepoTraversalArgs(rawArgs, defaults) {
2005
- const traversalDefaults = {
2006
- ...defaults,
2007
- recursive: true,
2008
- nestedMaxDepth: NESTED_REPO_DEFAULT_MAX_DEPTH,
2009
- nestedSkipDirs: [],
2010
- includeSubmodules: false,
2011
- };
2012
- const forwardedArgs = [];
2013
-
2014
- for (let index = 0; index < rawArgs.length; index += 1) {
2015
- const arg = rawArgs[index];
2016
- if (arg === '--no-recursive' || arg === '--no-nested' || arg === '--single-repo') {
2017
- traversalDefaults.recursive = false;
2018
- continue;
2019
- }
2020
- if (arg === '--recursive' || arg === '--nested') {
2021
- traversalDefaults.recursive = true;
2022
- continue;
2023
- }
2024
- if (arg === '--max-depth') {
2025
- const raw = requireValue(rawArgs, index, '--max-depth');
2026
- const parsed = Number.parseInt(raw, 10);
2027
- if (!Number.isFinite(parsed) || parsed < 1) {
2028
- throw new Error('--max-depth requires a positive integer');
2029
- }
2030
- traversalDefaults.nestedMaxDepth = parsed;
2031
- index += 1;
2032
- continue;
2033
- }
2034
- if (arg === '--skip-nested') {
2035
- const raw = requireValue(rawArgs, index, '--skip-nested');
2036
- traversalDefaults.nestedSkipDirs.push(raw);
2037
- index += 1;
2038
- continue;
2039
- }
2040
- if (arg === '--include-submodules') {
2041
- traversalDefaults.includeSubmodules = true;
2042
- continue;
2043
- }
2044
- forwardedArgs.push(arg);
2045
- }
2046
-
2047
- return parseCommonArgs(forwardedArgs, traversalDefaults);
2048
- }
2049
-
2050
- function parseSetupArgs(rawArgs, defaults) {
2051
- const setupDefaults = {
2052
- ...defaults,
2053
- parentWorkspaceView: false,
2054
- };
2055
- const forwardedArgs = [];
2056
-
2057
- for (let index = 0; index < rawArgs.length; index += 1) {
2058
- const arg = rawArgs[index];
2059
- if (arg === '--parent-workspace-view') {
2060
- setupDefaults.parentWorkspaceView = true;
2061
- continue;
2062
- }
2063
- if (arg === '--no-parent-workspace-view') {
2064
- setupDefaults.parentWorkspaceView = false;
2065
- continue;
2066
- }
2067
- forwardedArgs.push(arg);
2068
- }
2069
-
2070
- return parseRepoTraversalArgs(forwardedArgs, setupDefaults);
865
+ return { status: 'set', key: 'core.hooksPath', value: '.githooks' };
2071
866
  }
2072
867
 
2073
- function parseDoctorArgs(rawArgs) {
2074
- const doctorDefaults = {
2075
- target: process.cwd(),
2076
- force: false,
2077
- dropStaleLocks: true,
2078
- skipAgents: false,
2079
- skipPackageJson: false,
2080
- skipGitignore: false,
2081
- dryRun: false,
2082
- json: false,
2083
- allowProtectedBaseWrite: false,
2084
- waitForMerge: true,
2085
- verboseAutoFinish: false,
2086
- };
2087
- const forwardedArgs = [];
2088
-
2089
- for (let index = 0; index < rawArgs.length; index += 1) {
2090
- const arg = rawArgs[index];
2091
- if (arg === '--verbose-auto-finish') {
2092
- doctorDefaults.verboseAutoFinish = true;
2093
- continue;
2094
- }
2095
- if (arg === '--compact-auto-finish') {
2096
- doctorDefaults.verboseAutoFinish = false;
2097
- continue;
2098
- }
2099
- forwardedArgs.push(arg);
868
+ function appendForceArgs(args, options) {
869
+ if (!options.force) {
870
+ return;
871
+ }
872
+ args.push('--force');
873
+ for (const managedPath of options.forceManagedPaths || []) {
874
+ args.push(managedPath);
2100
875
  }
876
+ }
2101
877
 
2102
- return parseRepoTraversalArgs(forwardedArgs, doctorDefaults);
878
+ function shouldForceManagedPath(options, relativePath) {
879
+ if (!options.force) {
880
+ return false;
881
+ }
882
+ const targetedPaths = Array.isArray(options.forceManagedPaths) ? options.forceManagedPaths : [];
883
+ if (targetedPaths.length === 0) {
884
+ return true;
885
+ }
886
+ const normalized = normalizeManagedForcePath(relativePath);
887
+ return normalized !== null && targetedPaths.includes(normalized);
2103
888
  }
2104
889
 
2105
890
  function normalizeWorkspacePath(relativePath) {
@@ -2931,10 +1716,6 @@ function mergeDoctorSandboxRepairsBackToProtectedBase(options, blocked, metadata
2931
1716
  };
2932
1717
  }
2933
1718
 
2934
- function syncDoctorLocalSupportFiles(repoRoot, dryRun) {
2935
- return [];
2936
- }
2937
-
2938
1719
  /**
2939
1720
  * @param {string} [note]
2940
1721
  * @returns {OperationResult}
@@ -3131,8 +1912,6 @@ function executeDoctorSandboxLifecycle(options, blocked, metadata) {
3131
1912
  execution.finish,
3132
1913
  );
3133
1914
 
3134
- syncDoctorLocalSupportFiles(blocked.repoRoot, dryRun);
3135
-
3136
1915
  execution.omxScaffoldSync = summarizeDoctorOmxScaffoldSync(blocked.repoRoot, dryRun);
3137
1916
  execution.lockSync = syncDoctorLockRegistryAfterMerge(
3138
1917
  blocked.repoRoot,
@@ -3249,7 +2028,6 @@ function emitDoctorSandboxConsoleOutput(options, blocked, metadata, startResult,
3249
2028
  if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
3250
2029
  if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
3251
2030
  } else if (execution.finish.status === 'failed') {
3252
- console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
3253
2031
  console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
3254
2032
  if (execution.finish.stdout) process.stdout.write(execution.finish.stdout);
3255
2033
  if (execution.finish.stderr) process.stderr.write(execution.finish.stderr);
@@ -3399,171 +2177,6 @@ function runSetupInSandbox(options, blocked, repoLabel = '') {
3399
2177
  };
3400
2178
  }
3401
2179
 
3402
- function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
3403
- const remaining = [];
3404
- let target = defaultTarget;
3405
-
3406
- for (let index = 0; index < rawArgs.length; index += 1) {
3407
- const arg = rawArgs[index];
3408
- if (arg === '--target') {
3409
- const next = rawArgs[index + 1];
3410
- if (!next) {
3411
- throw new Error('--target requires a path value');
3412
- }
3413
- target = next;
3414
- index += 1;
3415
- continue;
3416
- }
3417
- remaining.push(arg);
3418
- }
3419
-
3420
- return { target, args: remaining };
3421
- }
3422
-
3423
- function parseReviewArgs(rawArgs) {
3424
- const parsed = parseTargetFlag(rawArgs, process.cwd());
3425
- const passthroughArgs = [...parsed.args];
3426
- if (passthroughArgs[0] === 'start') {
3427
- passthroughArgs.shift();
3428
- }
3429
- return {
3430
- target: parsed.target,
3431
- passthroughArgs,
3432
- };
3433
- }
3434
-
3435
- function parseAgentsArgs(rawArgs) {
3436
- const parsed = parseTargetFlag(rawArgs, process.cwd());
3437
- const [subcommandRaw = '', ...rest] = parsed.args;
3438
- const subcommand = subcommandRaw || 'status';
3439
- const options = {
3440
- target: parsed.target,
3441
- subcommand,
3442
- reviewIntervalSeconds: 30,
3443
- cleanupIntervalSeconds: 60,
3444
- idleMinutes: DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
3445
- };
3446
-
3447
- for (let index = 0; index < rest.length; index += 1) {
3448
- const arg = rest[index];
3449
- if (arg === '--review-interval') {
3450
- const next = rest[index + 1];
3451
- if (!next) {
3452
- throw new Error('--review-interval requires an integer seconds value');
3453
- }
3454
- const parsedValue = Number.parseInt(next, 10);
3455
- if (!Number.isInteger(parsedValue) || parsedValue < 5) {
3456
- throw new Error('--review-interval must be an integer >= 5 seconds');
3457
- }
3458
- options.reviewIntervalSeconds = parsedValue;
3459
- index += 1;
3460
- continue;
3461
- }
3462
- if (arg === '--cleanup-interval') {
3463
- const next = rest[index + 1];
3464
- if (!next) {
3465
- throw new Error('--cleanup-interval requires an integer seconds value');
3466
- }
3467
- const parsedValue = Number.parseInt(next, 10);
3468
- if (!Number.isInteger(parsedValue) || parsedValue < 5) {
3469
- throw new Error('--cleanup-interval must be an integer >= 5 seconds');
3470
- }
3471
- options.cleanupIntervalSeconds = parsedValue;
3472
- index += 1;
3473
- continue;
3474
- }
3475
- if (arg === '--idle-minutes') {
3476
- const next = rest[index + 1];
3477
- if (!next) {
3478
- throw new Error('--idle-minutes requires an integer minutes value');
3479
- }
3480
- const parsedValue = Number.parseInt(next, 10);
3481
- if (!Number.isInteger(parsedValue) || parsedValue < 1) {
3482
- throw new Error('--idle-minutes must be an integer >= 1');
3483
- }
3484
- options.idleMinutes = parsedValue;
3485
- index += 1;
3486
- continue;
3487
- }
3488
- throw new Error(`Unknown option: ${arg}`);
3489
- }
3490
-
3491
- if (!['start', 'stop', 'status'].includes(options.subcommand)) {
3492
- throw new Error(`Unknown agents subcommand: ${options.subcommand}`);
3493
- }
3494
-
3495
- return options;
3496
- }
3497
-
3498
- function parseReportArgs(rawArgs) {
3499
- const options = {
3500
- target: process.cwd(),
3501
- subcommand: '',
3502
- repo: '',
3503
- scorecardJson: '',
3504
- outputDir: '',
3505
- date: '',
3506
- dryRun: false,
3507
- json: false,
3508
- };
3509
-
3510
- for (let index = 0; index < rawArgs.length; index += 1) {
3511
- const arg = rawArgs[index];
3512
- if (arg === '--target') {
3513
- const next = rawArgs[index + 1];
3514
- if (!next) throw new Error('--target requires a path value');
3515
- options.target = next;
3516
- index += 1;
3517
- continue;
3518
- }
3519
- if (arg === '--repo') {
3520
- const next = rawArgs[index + 1];
3521
- if (!next) throw new Error('--repo requires a value like github.com/owner/repo');
3522
- options.repo = next;
3523
- index += 1;
3524
- continue;
3525
- }
3526
- if (arg === '--scorecard-json') {
3527
- const next = rawArgs[index + 1];
3528
- if (!next) throw new Error('--scorecard-json requires a path value');
3529
- options.scorecardJson = next;
3530
- index += 1;
3531
- continue;
3532
- }
3533
- if (arg === '--output-dir') {
3534
- const next = rawArgs[index + 1];
3535
- if (!next) throw new Error('--output-dir requires a path value');
3536
- options.outputDir = next;
3537
- index += 1;
3538
- continue;
3539
- }
3540
- if (arg === '--date') {
3541
- const next = rawArgs[index + 1];
3542
- if (!next) throw new Error('--date requires a YYYY-MM-DD value');
3543
- options.date = next;
3544
- index += 1;
3545
- continue;
3546
- }
3547
- if (arg === '--dry-run') {
3548
- options.dryRun = true;
3549
- continue;
3550
- }
3551
- if (arg === '--json') {
3552
- options.json = true;
3553
- continue;
3554
- }
3555
- if (arg.startsWith('-')) {
3556
- throw new Error(`Unknown option: ${arg}`);
3557
- }
3558
- if (!options.subcommand) {
3559
- options.subcommand = arg;
3560
- continue;
3561
- }
3562
- throw new Error(`Unexpected argument: ${arg}`);
3563
- }
3564
-
3565
- return options;
3566
- }
3567
2180
 
3568
2181
  function todayDateStamp() {
3569
2182
  return new Date().toISOString().slice(0, 10);
@@ -4167,430 +2780,35 @@ function ensureOriginBaseRef(repoRoot, baseBranch) {
4167
2780
  if (hasRemoteBase.status !== 0) {
4168
2781
  throw new Error(`Remote base branch not found: origin/${baseBranch}`);
4169
2782
  }
4170
- }
4171
-
4172
- function aheadBehind(repoRoot, branchRef, baseRef) {
4173
- const result = gitRun(repoRoot, ['rev-list', '--left-right', '--count', `${branchRef}...${baseRef}`], {
4174
- allowFailure: true,
4175
- });
4176
- if (result.status !== 0) {
4177
- throw new Error(`Unable to compute ahead/behind for ${branchRef} vs ${baseRef}`);
4178
- }
4179
- const parts = (result.stdout || '').trim().split(/\s+/).filter(Boolean);
4180
- const ahead = Number.parseInt(parts[0] || '0', 10);
4181
- const behind = Number.parseInt(parts[1] || '0', 10);
4182
- return { ahead: Number.isFinite(ahead) ? ahead : 0, behind: Number.isFinite(behind) ? behind : 0 };
4183
- }
4184
-
4185
- function lockRegistryStatus(repoRoot) {
4186
- const result = gitRun(repoRoot, ['status', '--porcelain', '--', LOCK_FILE_RELATIVE], { allowFailure: true });
4187
- if (result.status !== 0) {
4188
- return { dirty: false, untracked: false };
4189
- }
4190
- const lines = (result.stdout || '').split('\n').filter((line) => line.length > 0);
4191
- if (lines.length === 0) {
4192
- return { dirty: false, untracked: false };
4193
- }
4194
- const untracked = lines.some((line) => line.startsWith('??'));
4195
- return { dirty: true, untracked };
4196
- }
4197
-
4198
- function parseSyncArgs(rawArgs) {
4199
- const options = {
4200
- target: process.cwd(),
4201
- check: false,
4202
- base: '',
4203
- strategy: '',
4204
- ffOnly: false,
4205
- dryRun: false,
4206
- json: false,
4207
- allAgentBranches: false,
4208
- allowNonAgent: false,
4209
- allowDirty: false,
4210
- };
4211
-
4212
- for (let index = 0; index < rawArgs.length; index += 1) {
4213
- const arg = rawArgs[index];
4214
- if (arg === '--target') {
4215
- const next = rawArgs[index + 1];
4216
- if (!next) {
4217
- throw new Error('--target requires a path value');
4218
- }
4219
- options.target = next;
4220
- index += 1;
4221
- continue;
4222
- }
4223
- if (arg === '--base') {
4224
- const next = rawArgs[index + 1];
4225
- if (!next) {
4226
- throw new Error('--base requires a branch value');
4227
- }
4228
- options.base = next;
4229
- index += 1;
4230
- continue;
4231
- }
4232
- if (arg === '--strategy') {
4233
- const next = rawArgs[index + 1];
4234
- if (!next) {
4235
- throw new Error('--strategy requires a value (rebase|merge)');
4236
- }
4237
- options.strategy = next;
4238
- index += 1;
4239
- continue;
4240
- }
4241
- if (arg === '--check') {
4242
- options.check = true;
4243
- continue;
4244
- }
4245
- if (arg === '--ff-only') {
4246
- options.ffOnly = true;
4247
- continue;
4248
- }
4249
- if (arg === '--dry-run') {
4250
- options.dryRun = true;
4251
- continue;
4252
- }
4253
- if (arg === '--json') {
4254
- options.json = true;
4255
- continue;
4256
- }
4257
- if (arg === '--all-agent-branches') {
4258
- options.allAgentBranches = true;
4259
- continue;
4260
- }
4261
- if (arg === '--allow-non-agent') {
4262
- options.allowNonAgent = true;
4263
- continue;
4264
- }
4265
- if (arg === '--allow-dirty') {
4266
- options.allowDirty = true;
4267
- continue;
4268
- }
4269
- throw new Error(`Unknown option: ${arg}`);
4270
- }
4271
-
4272
- return options;
4273
- }
4274
-
4275
- function parseCleanupArgs(rawArgs) {
4276
- const options = {
4277
- target: process.cwd(),
4278
- base: '',
4279
- branch: '',
4280
- dryRun: false,
4281
- forceDirty: false,
4282
- keepRemote: false,
4283
- keepCleanWorktrees: false,
4284
- includePrMerged: false,
4285
- idleMinutes: 0,
4286
- watch: false,
4287
- intervalSeconds: 60,
4288
- once: false,
4289
- maxBranches: 0,
4290
- };
4291
-
4292
- for (let index = 0; index < rawArgs.length; index += 1) {
4293
- const arg = rawArgs[index];
4294
- if (arg === '--target') {
4295
- const next = rawArgs[index + 1];
4296
- if (!next) {
4297
- throw new Error('--target requires a path value');
4298
- }
4299
- options.target = next;
4300
- index += 1;
4301
- continue;
4302
- }
4303
- if (arg === '--base') {
4304
- const next = rawArgs[index + 1];
4305
- if (!next) {
4306
- throw new Error('--base requires a branch value');
4307
- }
4308
- options.base = next;
4309
- index += 1;
4310
- continue;
4311
- }
4312
- if (arg === '--branch') {
4313
- const next = rawArgs[index + 1];
4314
- if (!next) {
4315
- throw new Error('--branch requires an agent branch value');
4316
- }
4317
- options.branch = next;
4318
- index += 1;
4319
- continue;
4320
- }
4321
- if (arg === '--dry-run') {
4322
- options.dryRun = true;
4323
- continue;
4324
- }
4325
- if (arg === '--force-dirty') {
4326
- options.forceDirty = true;
4327
- continue;
4328
- }
4329
- if (arg === '--keep-remote') {
4330
- options.keepRemote = true;
4331
- continue;
4332
- }
4333
- if (arg === '--keep-clean-worktrees') {
4334
- options.keepCleanWorktrees = true;
4335
- continue;
4336
- }
4337
- if (arg === '--include-pr-merged') {
4338
- options.includePrMerged = true;
4339
- continue;
4340
- }
4341
- if (arg === '--idle-minutes') {
4342
- const next = rawArgs[index + 1];
4343
- if (!next) {
4344
- throw new Error('--idle-minutes requires an integer value');
4345
- }
4346
- const parsed = Number.parseInt(next, 10);
4347
- if (!Number.isInteger(parsed) || parsed < 0) {
4348
- throw new Error('--idle-minutes must be an integer >= 0');
4349
- }
4350
- options.idleMinutes = parsed;
4351
- index += 1;
4352
- continue;
4353
- }
4354
- if (arg === '--watch') {
4355
- options.watch = true;
4356
- continue;
4357
- }
4358
- if (arg === '--interval') {
4359
- const next = rawArgs[index + 1];
4360
- if (!next) {
4361
- throw new Error('--interval requires an integer seconds value');
4362
- }
4363
- const parsed = Number.parseInt(next, 10);
4364
- if (!Number.isInteger(parsed) || parsed < 5) {
4365
- throw new Error('--interval must be an integer >= 5 seconds');
4366
- }
4367
- options.intervalSeconds = parsed;
4368
- index += 1;
4369
- continue;
4370
- }
4371
- if (arg === '--once') {
4372
- options.once = true;
4373
- continue;
4374
- }
4375
- if (arg === '--max-branches') {
4376
- const next = rawArgs[index + 1];
4377
- if (!next) {
4378
- throw new Error('--max-branches requires an integer value');
4379
- }
4380
- const parsed = Number.parseInt(next, 10);
4381
- if (!Number.isInteger(parsed) || parsed < 1) {
4382
- throw new Error('--max-branches must be an integer >= 1');
4383
- }
4384
- options.maxBranches = parsed;
4385
- index += 1;
4386
- continue;
4387
- }
4388
- throw new Error(`Unknown option: ${arg}`);
4389
- }
4390
-
4391
- if (options.watch && options.idleMinutes === 0) {
4392
- options.idleMinutes = DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES;
4393
- }
4394
-
4395
- return options;
4396
- }
4397
-
4398
- function parseMergeArgs(rawArgs) {
4399
- const options = {
4400
- target: process.cwd(),
4401
- base: '',
4402
- into: '',
4403
- branches: [],
4404
- task: '',
4405
- agent: '',
4406
- };
4407
-
4408
- for (let index = 0; index < rawArgs.length; index += 1) {
4409
- const arg = rawArgs[index];
4410
- if (arg === '--target') {
4411
- const next = rawArgs[index + 1];
4412
- if (!next) {
4413
- throw new Error('--target requires a path value');
4414
- }
4415
- options.target = next;
4416
- index += 1;
4417
- continue;
4418
- }
4419
- if (arg === '--base') {
4420
- const next = rawArgs[index + 1];
4421
- if (!next) {
4422
- throw new Error('--base requires a branch value');
4423
- }
4424
- options.base = next;
4425
- index += 1;
4426
- continue;
4427
- }
4428
- if (arg === '--into') {
4429
- const next = rawArgs[index + 1];
4430
- if (!next) {
4431
- throw new Error('--into requires an agent/* branch value');
4432
- }
4433
- options.into = next;
4434
- index += 1;
4435
- continue;
4436
- }
4437
- if (arg === '--branch') {
4438
- const next = rawArgs[index + 1];
4439
- if (!next) {
4440
- throw new Error('--branch requires an agent/* branch value');
4441
- }
4442
- options.branches.push(next);
4443
- index += 1;
4444
- continue;
4445
- }
4446
- if (arg === '--task') {
4447
- const next = rawArgs[index + 1];
4448
- if (!next) {
4449
- throw new Error('--task requires a task value');
4450
- }
4451
- options.task = next;
4452
- index += 1;
4453
- continue;
4454
- }
4455
- if (arg === '--agent') {
4456
- const next = rawArgs[index + 1];
4457
- if (!next) {
4458
- throw new Error('--agent requires an agent value');
4459
- }
4460
- options.agent = next;
4461
- index += 1;
4462
- continue;
4463
- }
4464
- throw new Error(`Unknown option: ${arg}`);
4465
- }
2783
+ }
4466
2784
 
4467
- if (options.branches.length === 0) {
4468
- throw new Error('merge requires at least one --branch <agent/*> input');
2785
+ function aheadBehind(repoRoot, branchRef, baseRef) {
2786
+ const result = gitRun(repoRoot, ['rev-list', '--left-right', '--count', `${branchRef}...${baseRef}`], {
2787
+ allowFailure: true,
2788
+ });
2789
+ if (result.status !== 0) {
2790
+ throw new Error(`Unable to compute ahead/behind for ${branchRef} vs ${baseRef}`);
4469
2791
  }
4470
-
4471
- return options;
2792
+ const parts = (result.stdout || '').trim().split(/\s+/).filter(Boolean);
2793
+ const ahead = Number.parseInt(parts[0] || '0', 10);
2794
+ const behind = Number.parseInt(parts[1] || '0', 10);
2795
+ return { ahead: Number.isFinite(ahead) ? ahead : 0, behind: Number.isFinite(behind) ? behind : 0 };
4472
2796
  }
4473
2797
 
4474
- function parseFinishArgs(rawArgs, defaults = {}) {
4475
- const options = {
4476
- target: process.cwd(),
4477
- base: '',
4478
- branch: '',
4479
- all: false,
4480
- dryRun: false,
4481
- waitForMerge: defaults.waitForMerge ?? true,
4482
- cleanup: defaults.cleanup ?? true,
4483
- keepRemote: false,
4484
- noAutoCommit: false,
4485
- failFast: false,
4486
- commitMessage: '',
4487
- mergeMode: defaults.mergeMode || 'pr',
4488
- };
4489
-
4490
- for (let index = 0; index < rawArgs.length; index += 1) {
4491
- const arg = rawArgs[index];
4492
- if (arg === '--target') {
4493
- const next = rawArgs[index + 1];
4494
- if (!next) {
4495
- throw new Error('--target requires a path value');
4496
- }
4497
- options.target = next;
4498
- index += 1;
4499
- continue;
4500
- }
4501
- if (arg === '--base') {
4502
- const next = rawArgs[index + 1];
4503
- if (!next) {
4504
- throw new Error('--base requires a branch value');
4505
- }
4506
- options.base = next;
4507
- index += 1;
4508
- continue;
4509
- }
4510
- if (arg === '--branch') {
4511
- const next = rawArgs[index + 1];
4512
- if (!next) {
4513
- throw new Error('--branch requires an agent/* branch value');
4514
- }
4515
- options.branch = next;
4516
- index += 1;
4517
- continue;
4518
- }
4519
- if (arg === '--commit-message') {
4520
- const next = rawArgs[index + 1];
4521
- if (!next) {
4522
- throw new Error('--commit-message requires a value');
4523
- }
4524
- options.commitMessage = next;
4525
- index += 1;
4526
- continue;
4527
- }
4528
- if (arg === '--all') {
4529
- options.all = true;
4530
- continue;
4531
- }
4532
- if (arg === '--dry-run') {
4533
- options.dryRun = true;
4534
- continue;
4535
- }
4536
- if (arg === '--wait-for-merge') {
4537
- options.waitForMerge = true;
4538
- continue;
4539
- }
4540
- if (arg === '--no-wait-for-merge') {
4541
- options.waitForMerge = false;
4542
- continue;
4543
- }
4544
- if (arg === '--via-pr') {
4545
- options.mergeMode = 'pr';
4546
- continue;
4547
- }
4548
- if (arg === '--direct-only') {
4549
- options.mergeMode = 'direct';
4550
- continue;
4551
- }
4552
- if (arg === '--mode') {
4553
- const next = rawArgs[index + 1];
4554
- if (!next) {
4555
- throw new Error('--mode requires a value');
4556
- }
4557
- if (!['auto', 'direct', 'pr'].includes(next)) {
4558
- throw new Error(`Invalid --mode value: ${next} (expected auto|direct|pr)`);
4559
- }
4560
- options.mergeMode = next;
4561
- index += 1;
4562
- continue;
4563
- }
4564
- if (arg === '--cleanup') {
4565
- options.cleanup = true;
4566
- continue;
4567
- }
4568
- if (arg === '--no-cleanup') {
4569
- options.cleanup = false;
4570
- continue;
4571
- }
4572
- if (arg === '--keep-remote') {
4573
- options.keepRemote = true;
4574
- continue;
4575
- }
4576
- if (arg === '--no-auto-commit') {
4577
- options.noAutoCommit = true;
4578
- continue;
4579
- }
4580
- if (arg === '--fail-fast') {
4581
- options.failFast = true;
4582
- continue;
4583
- }
4584
- throw new Error(`Unknown option: ${arg}`);
2798
+ function lockRegistryStatus(repoRoot) {
2799
+ const result = gitRun(repoRoot, ['status', '--porcelain', '--', LOCK_FILE_RELATIVE], { allowFailure: true });
2800
+ if (result.status !== 0) {
2801
+ return { dirty: false, untracked: false };
4585
2802
  }
4586
-
4587
- if (options.branch && !options.branch.startsWith('agent/')) {
4588
- throw new Error(`--branch must reference an agent/* branch (received: ${options.branch})`);
2803
+ const lines = (result.stdout || '').split('\n').filter((line) => line.length > 0);
2804
+ if (lines.length === 0) {
2805
+ return { dirty: false, untracked: false };
4589
2806
  }
4590
-
4591
- return options;
2807
+ const untracked = lines.some((line) => line.startsWith('??'));
2808
+ return { dirty: true, untracked };
4592
2809
  }
4593
2810
 
2811
+
4594
2812
  function listAgentWorktrees(repoRoot) {
4595
2813
  const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
4596
2814
  if (result.status !== 0) {
@@ -4927,31 +3145,6 @@ function readSingleLineFromStdin() {
4927
3145
  }
4928
3146
  }
4929
3147
 
4930
- function promptYesNo(question, defaultYes = true) {
4931
- const hint = defaultYes ? '[Y/n]' : '[y/N]';
4932
- while (true) {
4933
- process.stdout.write(`${question} ${hint} `);
4934
- const answer = readSingleLineFromStdin().trim().toLowerCase();
4935
-
4936
- if (!answer) {
4937
- return defaultYes;
4938
- }
4939
- if (answer === 'y' || answer === 'yes') {
4940
- return true;
4941
- }
4942
- if (answer === 'n' || answer === 'no') {
4943
- return false;
4944
- }
4945
- process.stdout.write('Please answer with y or n.\n');
4946
- }
4947
- }
4948
-
4949
- function envFlagEnabled(name) {
4950
- const raw = process.env[name];
4951
- if (raw == null) return false;
4952
- return ['1', 'true', 'yes', 'on'].includes(String(raw).trim().toLowerCase());
4953
- }
4954
-
4955
3148
  function parseAutoApproval(name) {
4956
3149
  const raw = process.env[name];
4957
3150
  if (raw == null) return null;
@@ -5079,11 +3272,11 @@ function parseNpmVersionOutput(stdout) {
5079
3272
  }
5080
3273
 
5081
3274
  function checkForGuardexUpdate() {
5082
- if (envFlagEnabled('GUARDEX_SKIP_UPDATE_CHECK')) {
3275
+ if (envFlagIsTruthy(process.env.GUARDEX_SKIP_UPDATE_CHECK)) {
5083
3276
  return { checked: false, reason: 'disabled' };
5084
3277
  }
5085
3278
 
5086
- const forceCheck = envFlagEnabled('GUARDEX_FORCE_UPDATE_CHECK');
3279
+ const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_UPDATE_CHECK);
5087
3280
  if (!forceCheck && !isInteractiveTerminal()) {
5088
3281
  return { checked: false, reason: 'non-interactive' };
5089
3282
  }
@@ -5203,11 +3396,11 @@ function restartIntoUpdatedGuardex(expectedVersion) {
5203
3396
  }
5204
3397
 
5205
3398
  function checkForOpenSpecPackageUpdate() {
5206
- if (envFlagEnabled('GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK')) {
3399
+ if (envFlagIsTruthy(process.env.GUARDEX_SKIP_OPENSPEC_UPDATE_CHECK)) {
5207
3400
  return { checked: false, reason: 'disabled' };
5208
3401
  }
5209
3402
 
5210
- const forceCheck = envFlagEnabled('GUARDEX_FORCE_OPENSPEC_UPDATE_CHECK');
3403
+ const forceCheck = envFlagIsTruthy(process.env.GUARDEX_FORCE_OPENSPEC_UPDATE_CHECK);
5211
3404
  if (!forceCheck && !isInteractiveTerminal()) {
5212
3405
  return { checked: false, reason: 'non-interactive' };
5213
3406
  }
@@ -5432,10 +3625,6 @@ function installGlobalToolchain(options) {
5432
3625
  return getToolchainApi().installGlobalToolchain(options);
5433
3626
  }
5434
3627
 
5435
- function gitRefExists(repoRoot, refName) {
5436
- return gitRun(repoRoot, ['show-ref', '--verify', '--quiet', refName], { allowFailure: true }).status === 0;
5437
- }
5438
-
5439
3628
  function findStaleLockPaths(repoRoot, locks) {
5440
3629
  const stale = [];
5441
3630
 
@@ -5716,21 +3905,6 @@ function runScanInternal(options) {
5716
3905
  };
5717
3906
  }
5718
3907
 
5719
- function printOperations(title, payload, dryRun = false) {
5720
- console.log(`[${TOOL_NAME}] ${title}: ${payload.repoRoot}`);
5721
- for (const operation of payload.operations) {
5722
- const note = operation.note ? ` (${operation.note})` : '';
5723
- console.log(` - ${operation.status.padEnd(12)} ${operation.file}${note}`);
5724
- }
5725
- console.log(
5726
- ` - hooksPath ${payload.hookResult.status} ${payload.hookResult.key}=${payload.hookResult.value}`,
5727
- );
5728
-
5729
- if (dryRun) {
5730
- console.log(`[${TOOL_NAME}] Dry run complete. No files were modified.`);
5731
- }
5732
- }
5733
-
5734
3908
  function printScanResult(scan, json = false) {
5735
3909
  if (json) {
5736
3910
  process.stdout.write(
@@ -6046,17 +4220,18 @@ function doctor(rawArgs) {
6046
4220
  const topRepoRoot = resolveRepoRoot(options.target);
6047
4221
  const discoveredRepos = options.recursive
6048
4222
  ? discoverNestedGitRepos(topRepoRoot, {
6049
- maxDepth: options.nestedMaxDepth,
6050
- extraSkip: options.nestedSkipDirs,
6051
- includeSubmodules: options.includeSubmodules,
6052
- })
4223
+ maxDepth: options.nestedMaxDepth,
4224
+ extraSkip: options.nestedSkipDirs,
4225
+ includeSubmodules: options.includeSubmodules,
4226
+ skipRelativeDirs: AGENT_WORKTREE_RELATIVE_DIRS,
4227
+ })
6053
4228
  : [topRepoRoot];
6054
4229
 
6055
4230
  if (discoveredRepos.length > 1) {
6056
4231
  if (!options.json) {
6057
4232
  console.log(
6058
4233
  `[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. ` +
6059
- `Repairing each with doctor (use --single-repo to limit to the target).`,
4234
+ `Repairing each with doctor (use --single-repo or --current to limit to the target).`,
6060
4235
  );
6061
4236
  }
6062
4237
 
@@ -6680,12 +4855,13 @@ function setup(rawArgs) {
6680
4855
  maxDepth: options.nestedMaxDepth,
6681
4856
  extraSkip: options.nestedSkipDirs,
6682
4857
  includeSubmodules: options.includeSubmodules,
4858
+ skipRelativeDirs: AGENT_WORKTREE_RELATIVE_DIRS,
6683
4859
  })
6684
4860
  : [topRepoRoot];
6685
4861
 
6686
4862
  if (discoveredRepos.length > 1) {
6687
4863
  console.log(
6688
- `[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. Installing into each (use --no-recursive to limit to the top-level).`,
4864
+ `[${TOOL_NAME}] Detected ${discoveredRepos.length} git repos under ${topRepoRoot}. Installing into each (use --no-recursive or --current to limit to the top-level).`,
6689
4865
  );
6690
4866
  for (const repoPath of discoveredRepos) {
6691
4867
  const marker = repoPath === topRepoRoot ? ' (top-level)' : '';
@@ -6733,16 +4909,9 @@ function setup(rawArgs) {
6733
4909
  dryRun: perRepoOptions.dryRun,
6734
4910
  });
6735
4911
  printScanResult(scanResult, false);
6736
- if (autoFinishSummary.enabled) {
6737
- console.log(
6738
- `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
6739
- );
6740
- for (const detail of autoFinishSummary.details) {
6741
- console.log(`[${TOOL_NAME}] ${detail}`);
6742
- }
6743
- } else if (autoFinishSummary.details.length > 0) {
6744
- console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
6745
- }
4912
+ printAutoFinishSummary(autoFinishSummary, {
4913
+ baseBranch: currentBaseBranch,
4914
+ });
6746
4915
  printSetupRepoHints(scanResult.repoRoot, currentBaseBranch, repoLabel);
6747
4916
 
6748
4917
  aggregateErrors += scanResult.errors;
@@ -7039,247 +5208,6 @@ function release(rawArgs) {
7039
5208
  process.exitCode = 0;
7040
5209
  }
7041
5210
 
7042
- function installMany(rawArgs) {
7043
- const options = parseInstallManyArgs(rawArgs);
7044
- const targets = collectInstallManyTargets(options);
7045
-
7046
- if (!targets.length) {
7047
- throw new Error('install-many did not find any targets to process.');
7048
- }
7049
-
7050
- if (options.usedImplicitWorkspaceDefault) {
7051
- console.log(
7052
- `[multiagent-safety] No explicit targets provided. Defaulting to workspace scan: ${path.resolve(
7053
- options.workspace,
7054
- )} (max depth ${options.maxDepth})`,
7055
- );
7056
- }
7057
-
7058
- console.log(
7059
- `[multiagent-safety] install-many starting for ${targets.length} target path(s)${
7060
- options.dryRun ? ' [dry-run]' : ''
7061
- }`,
7062
- );
7063
-
7064
- let installed = 0;
7065
- let duplicateRepos = 0;
7066
- const seenRepoRoots = new Set();
7067
- const failures = [];
7068
-
7069
- for (const targetPath of targets) {
7070
- let repoRoot;
7071
- try {
7072
- repoRoot = resolveRepoRoot(targetPath);
7073
- } catch (error) {
7074
- failures.push({ target: targetPath, message: error.message });
7075
- if (options.failFast) {
7076
- break;
7077
- }
7078
- continue;
7079
- }
7080
-
7081
- if (seenRepoRoots.has(repoRoot)) {
7082
- duplicateRepos += 1;
7083
- console.log(`[multiagent-safety] Skipping duplicate repo target: ${targetPath} -> ${repoRoot}`);
7084
- continue;
7085
- }
7086
-
7087
- seenRepoRoots.add(repoRoot);
7088
-
7089
- try {
7090
- const report = installIntoRepoRoot(repoRoot, options);
7091
- printInstallReport(report);
7092
- installed += 1;
7093
- } catch (error) {
7094
- failures.push({ target: repoRoot, message: error.message });
7095
- if (options.failFast) {
7096
- break;
7097
- }
7098
- }
7099
- }
7100
-
7101
- console.log(
7102
- `[multiagent-safety] install-many summary: installed=${installed}, failures=${failures.length}, duplicate-targets=${duplicateRepos}`,
7103
- );
7104
-
7105
- if (failures.length > 0) {
7106
- console.error('[multiagent-safety] Failed targets:');
7107
- for (const failure of failures) {
7108
- console.error(` - ${failure.target}`);
7109
- console.error(` ${failure.message}`);
7110
- }
7111
- throw new Error(`install-many completed with ${failures.length} failure(s)`);
7112
- }
7113
-
7114
- if (options.dryRun) {
7115
- console.log('[multiagent-safety] Dry run complete. No files were modified.');
7116
- } else {
7117
- console.log('[multiagent-safety] Installed multi-agent safety workflow across all targets.');
7118
- }
7119
- }
7120
-
7121
- function initWorkspace(rawArgs) {
7122
- const options = parseInitWorkspaceArgs(rawArgs);
7123
- const resolvedWorkspace = path.resolve(options.workspace);
7124
- const repos = discoverGitRepos(resolvedWorkspace, options.maxDepth)
7125
- .map((repoPath) => path.resolve(repoPath))
7126
- .sort();
7127
-
7128
- const outputPath = options.output
7129
- ? path.resolve(options.output)
7130
- : path.join(resolvedWorkspace, DEFAULT_WORKSPACE_TARGETS_FILE);
7131
-
7132
- if (fs.existsSync(outputPath) && !options.force) {
7133
- throw new Error(`Refusing to overwrite existing file without --force: ${outputPath}`);
7134
- }
7135
-
7136
- const headerLines = [
7137
- '# multiagent-safety workspace targets',
7138
- `# generated: ${new Date().toISOString()}`,
7139
- `# workspace: ${resolvedWorkspace}`,
7140
- `# max-depth: ${options.maxDepth}`,
7141
- '#',
7142
- '# Run:',
7143
- `# multiagent-safety install-many --targets-file "${outputPath}"`,
7144
- '',
7145
- ];
7146
- const content = `${headerLines.join('\n')}${repos.join('\n')}${repos.length ? '\n' : ''}`;
7147
-
7148
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
7149
- fs.writeFileSync(outputPath, content, 'utf8');
7150
-
7151
- console.log(`[multiagent-safety] Workspace target file written: ${outputPath}`);
7152
- console.log(`[multiagent-safety] Repos discovered: ${repos.length}`);
7153
- if (repos.length === 0) {
7154
- console.log('[multiagent-safety] No git repos found. You can add target paths manually to the file.');
7155
- } else {
7156
- console.log(`[multiagent-safety] Next step: multiagent-safety install-many --targets-file "${outputPath}"`);
7157
- }
7158
- }
7159
-
7160
- function doctorAudit(rawArgs) {
7161
- const options = parseDoctorArgs(rawArgs);
7162
- const repoRoot = resolveRepoRoot(options.target);
7163
- const guardexToggle = resolveGuardexRepoToggle(repoRoot);
7164
- const failures = [];
7165
- const warnings = [];
7166
-
7167
- function ok(message) {
7168
- console.log(` [ok] ${message}`);
7169
- }
7170
- function warn(message) {
7171
- warnings.push(message);
7172
- console.log(` [warn] ${message}`);
7173
- }
7174
- function fail(message) {
7175
- failures.push(message);
7176
- console.log(` [fail] ${message}`);
7177
- }
7178
-
7179
- console.log(`[multiagent-safety] doctor target: ${repoRoot}`);
7180
- if (!guardexToggle.enabled) {
7181
- console.log(
7182
- `[multiagent-safety] Guardex is disabled for this repo (${describeGuardexRepoToggle(guardexToggle)}).`,
7183
- );
7184
- console.log('[multiagent-safety] doctor passed.');
7185
- return;
7186
- }
7187
-
7188
- const hooksPath = run('git', ['-C', repoRoot, 'config', '--get', 'core.hooksPath']);
7189
- if (hooksPath.status !== 0) {
7190
- fail('git core.hooksPath is not configured');
7191
- } else if (hooksPath.stdout.trim() !== '.githooks') {
7192
- fail(`git core.hooksPath is "${hooksPath.stdout.trim()}" (expected ".githooks")`);
7193
- } else {
7194
- ok('git core.hooksPath is .githooks');
7195
- }
7196
-
7197
- for (const relativePath of REQUIRED_MANAGED_REPO_FILES) {
7198
- const absolutePath = path.join(repoRoot, relativePath);
7199
- if (!fs.existsSync(absolutePath)) {
7200
- fail(`missing ${relativePath}`);
7201
- continue;
7202
- }
7203
- ok(`found ${relativePath}`);
7204
-
7205
- if (EXECUTABLE_RELATIVE_PATHS.has(relativePath)) {
7206
- try {
7207
- fs.accessSync(absolutePath, fs.constants.X_OK);
7208
- } catch {
7209
- fail(`${relativePath} exists but is not executable`);
7210
- }
7211
- }
7212
- }
7213
-
7214
- const lockFilePath = path.join(repoRoot, '.omx/state/agent-file-locks.json');
7215
- if (fs.existsSync(lockFilePath)) {
7216
- try {
7217
- const parsed = JSON.parse(fs.readFileSync(lockFilePath, 'utf8'));
7218
- if (!parsed || typeof parsed !== 'object' || typeof parsed.locks !== 'object') {
7219
- fail('.omx/state/agent-file-locks.json does not contain a valid { locks: {} } object');
7220
- } else {
7221
- ok('lock registry JSON is valid');
7222
- }
7223
- } catch (error) {
7224
- fail(`lock registry JSON is invalid: ${error.message}`);
7225
- }
7226
- }
7227
-
7228
- const packagePath = path.join(repoRoot, 'package.json');
7229
- if (!fs.existsSync(packagePath)) {
7230
- warn('package.json not found (legacy agent:* script drift cannot be checked)');
7231
- } else {
7232
- try {
7233
- const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
7234
- const scripts = pkg.scripts || {};
7235
- const legacyAgentScripts = Object.entries(LEGACY_MANAGED_PACKAGE_SCRIPTS)
7236
- .filter(([name, expectedValue]) => scripts[name] === expectedValue)
7237
- .map(([name]) => name);
7238
- if (legacyAgentScripts.length > 0) {
7239
- warn(`legacy agent:* package.json scripts remain (${legacyAgentScripts.join(', ')}); run '${SHORT_TOOL_NAME} migrate' to remove them`);
7240
- } else {
7241
- ok('package.json does not contain Guardex-managed agent:* helper scripts');
7242
- }
7243
- } catch (error) {
7244
- fail(`package.json is invalid JSON: ${error.message}`);
7245
- }
7246
- }
7247
-
7248
- const agentsPath = path.join(repoRoot, 'AGENTS.md');
7249
- if (!fs.existsSync(agentsPath)) {
7250
- warn('AGENTS.md not found (multi-agent contract snippet not present)');
7251
- } else {
7252
- const agentsContent = fs.readFileSync(agentsPath, 'utf8');
7253
- if (!agentsContent.includes(AGENTS_MARKER_START)) {
7254
- warn('AGENTS.md exists but multiagent-safety snippet marker is missing');
7255
- } else {
7256
- ok('AGENTS.md contains multiagent-safety snippet marker');
7257
- }
7258
- }
7259
-
7260
- if (warnings.length) {
7261
- console.log(`[multiagent-safety] warnings: ${warnings.length}`);
7262
- }
7263
- if (failures.length) {
7264
- console.log(`[multiagent-safety] failures: ${failures.length}`);
7265
- }
7266
-
7267
- if (failures.length === 0 && (!options.strict || warnings.length === 0)) {
7268
- console.log('[multiagent-safety] doctor passed.');
7269
- if (warnings.length > 0) {
7270
- console.log('[multiagent-safety] tip: run with --strict to treat warnings as failures.');
7271
- }
7272
- return;
7273
- }
7274
-
7275
- if (options.strict && warnings.length > 0 && failures.length === 0) {
7276
- console.log('[multiagent-safety] strict mode failed due to warnings.');
7277
- } else {
7278
- console.log('[multiagent-safety] doctor failed.');
7279
- }
7280
- throw new Error('doctor detected configuration issues');
7281
- }
7282
-
7283
5211
  function printAgentsSnippet() {
7284
5212
  const snippetPath = path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md');
7285
5213
  process.stdout.write(fs.readFileSync(snippetPath, 'utf8'));
@@ -7320,17 +5248,6 @@ function prompt(rawArgs) {
7320
5248
  return copyPrompt();
7321
5249
  }
7322
5250
 
7323
- function printStandaloneOperations(title, rootLabel, operations, dryRun = false) {
7324
- console.log(`[${TOOL_NAME}] ${title}: ${rootLabel}`);
7325
- for (const operation of operations) {
7326
- const note = operation.note ? ` (${operation.note})` : '';
7327
- console.log(` - ${operation.status.padEnd(12)} ${operation.file}${note}`);
7328
- }
7329
- if (dryRun) {
7330
- console.log(`[${TOOL_NAME}] Dry run complete. No files were modified.`);
7331
- }
7332
- }
7333
-
7334
5251
  function branch(rawArgs) {
7335
5252
  const [subcommand, ...rest] = rawArgs;
7336
5253
  if (subcommand === 'start') {
@@ -7552,78 +5469,6 @@ function protect(rawArgs) {
7552
5469
  throw new Error(`Unknown protect subcommand: ${subcommand}`);
7553
5470
  }
7554
5471
 
7555
- function levenshteinDistance(a, b) {
7556
- const rows = a.length + 1;
7557
- const cols = b.length + 1;
7558
- const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
7559
-
7560
- for (let i = 0; i < rows; i += 1) matrix[i][0] = i;
7561
- for (let j = 0; j < cols; j += 1) matrix[0][j] = j;
7562
-
7563
- for (let i = 1; i < rows; i += 1) {
7564
- for (let j = 1; j < cols; j += 1) {
7565
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
7566
- matrix[i][j] = Math.min(
7567
- matrix[i - 1][j] + 1, // deletion
7568
- matrix[i][j - 1] + 1, // insertion
7569
- matrix[i - 1][j - 1] + cost, // substitution
7570
- );
7571
- }
7572
- }
7573
- return matrix[a.length][b.length];
7574
- }
7575
-
7576
- function maybeSuggestCommand(command) {
7577
- let best = null;
7578
- let bestDistance = Number.POSITIVE_INFINITY;
7579
-
7580
- for (const candidate of SUGGESTIBLE_COMMANDS) {
7581
- const dist = levenshteinDistance(command, candidate);
7582
- if (dist < bestDistance) {
7583
- bestDistance = dist;
7584
- best = candidate;
7585
- }
7586
- }
7587
-
7588
- if (best && bestDistance <= 2) {
7589
- return best;
7590
- }
7591
-
7592
- return null;
7593
- }
7594
-
7595
- function normalizeCommandOrThrow(command) {
7596
- if (COMMAND_TYPO_ALIASES.has(command)) {
7597
- const mapped = COMMAND_TYPO_ALIASES.get(command);
7598
- console.log(`[${TOOL_NAME}] Interpreting '${command}' as '${mapped}'.`);
7599
- return mapped;
7600
- }
7601
- return command;
7602
- }
7603
-
7604
- function warnDeprecatedAlias(aliasName) {
7605
- const entry = DEPRECATED_COMMAND_ALIASES.get(aliasName);
7606
- if (!entry) return;
7607
- console.error(
7608
- `[${TOOL_NAME}] '${aliasName}' is deprecated and will be removed in a future major release. ` +
7609
- `Use: ${entry.hint}`,
7610
- );
7611
- }
7612
-
7613
- function extractFlag(args, ...names) {
7614
- const flagSet = new Set(names);
7615
- let found = false;
7616
- const remaining = [];
7617
- for (const arg of args) {
7618
- if (flagSet.has(arg)) {
7619
- found = true;
7620
- } else {
7621
- remaining.push(arg);
7622
- }
7623
- }
7624
- return { found, remaining };
7625
- }
7626
-
7627
5472
  function main() {
7628
5473
  const args = process.argv.slice(2);
7629
5474