@imdeadpool/guardex 5.0.5 → 5.0.8

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.
@@ -79,9 +79,11 @@ const CRITICAL_GUARDRAIL_PATHS = new Set([
79
79
 
80
80
  const LOCK_FILE_RELATIVE = '.omx/state/agent-file-locks.json';
81
81
  const AGENTS_MARKER_START = '<!-- multiagent-safety:START -->';
82
+ const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
82
83
  const GITIGNORE_MARKER_START = '# multiagent-safety:START';
83
84
  const GITIGNORE_MARKER_END = '# multiagent-safety:END';
84
85
  const MANAGED_GITIGNORE_PATHS = [
86
+ '.omx/',
85
87
  'scripts/agent-branch-start.sh',
86
88
  'scripts/agent-branch-finish.sh',
87
89
  'scripts/codex-agent.sh',
@@ -97,6 +99,17 @@ const MANAGED_GITIGNORE_PATHS = [
97
99
  '.claude/commands/guardex.md',
98
100
  LOCK_FILE_RELATIVE,
99
101
  ];
102
+ const OMX_SCAFFOLD_DIRECTORIES = [
103
+ '.omx',
104
+ '.omx/state',
105
+ '.omx/logs',
106
+ '.omx/plans',
107
+ '.omx/agent-worktrees',
108
+ ];
109
+ const OMX_SCAFFOLD_FILES = new Map([
110
+ ['.omx/notepad.md', '\n\n## WORKING MEMORY\n'],
111
+ ['.omx/project-memory.json', '{}\n'],
112
+ ]);
100
113
  const COMMAND_TYPO_ALIASES = new Map([
101
114
  ['relaese', 'release'],
102
115
  ['realaese', 'release'],
@@ -114,6 +127,7 @@ const SUGGESTIBLE_COMMANDS = [
114
127
  'setup',
115
128
  'init',
116
129
  'doctor',
130
+ 'review',
117
131
  'report',
118
132
  'copy-prompt',
119
133
  'copy-commands',
@@ -148,8 +162,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
148
162
  ['version', 'Print GuardeX version'],
149
163
  ];
150
164
  const AGENT_BOT_DESCRIPTIONS = [
151
- ['review', 'Monitor open PRs targeting current branch and dispatch codex-agent review flow'],
152
- ['start', 'bash scripts/review-bot-watch.sh --interval 30'],
165
+ ['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
153
166
  ];
154
167
 
155
168
  const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-Rex for your repo) in this repository for Codex or Claude.
@@ -171,7 +184,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
171
184
  3) If setup reports warnings/errors, repair + re-check:
172
185
  gx doctor
173
186
 
174
- 4) Confirm next safe agent workflow commands:
187
+ 4) Optional: start continuous PR monitor from this repo:
188
+ gx review --interval 30
189
+
190
+ 5) Confirm next safe agent workflow commands:
175
191
  bash scripts/codex-agent.sh "task" "agent-name"
176
192
  bash scripts/agent-branch-start.sh "task" "agent-name"
177
193
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
@@ -183,17 +199,17 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
183
199
  Remove them explicitly when done:
184
200
  gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
185
201
 
186
- 5) Optional: create OpenSpec planning workspace:
202
+ 6) Optional: create OpenSpec planning workspace:
187
203
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
188
204
 
189
- 6) Optional: protect extra branches:
205
+ 7) Optional: protect extra branches:
190
206
  gx protect add release staging
191
207
 
192
- 7) Optional: sync your current agent branch with latest base branch:
208
+ 8) Optional: sync your current agent branch with latest base branch:
193
209
  gx sync --check
194
210
  gx sync
195
211
 
196
- 8) Optional (GitHub remote cleanup): enable:
212
+ 9) Optional (GitHub remote cleanup): enable:
197
213
  Settings -> General -> Pull Requests -> Automatically delete head branches
198
214
  `;
199
215
 
@@ -201,6 +217,7 @@ const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
201
217
  gh --version
202
218
  gx setup
203
219
  gx doctor
220
+ gx review --interval 30
204
221
  bash scripts/codex-agent.sh "task" "agent-name"
205
222
  bash scripts/agent-branch-start.sh "task" "agent-name"
206
223
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
@@ -348,7 +365,9 @@ NOTES
348
365
  - ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
349
366
  - ${TOOL_NAME} setup asks for Y/N approval before global installs
350
367
  - ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing
368
+ - For other repos: ${SHORT_TOOL_NAME} setup --target <repo-path> then ${SHORT_TOOL_NAME} doctor --target <repo-path>
351
369
  - In initialized repos, setup/install/fix block in-place writes on protected main by default
370
+ - setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch
352
371
  - doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
353
372
  - agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
354
373
  - use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
@@ -358,7 +377,8 @@ NOTES
358
377
  console.log(`
359
378
  [${TOOL_NAME}] No git repository detected in current directory.
360
379
  [${TOOL_NAME}] Start from a repo root, or pass an explicit target:
361
- ${TOOL_NAME} setup --target <path-to-git-repo>`);
380
+ ${TOOL_NAME} setup --target <path-to-git-repo>
381
+ ${TOOL_NAME} doctor --target <path-to-git-repo>`);
362
382
  }
363
383
  }
364
384
 
@@ -501,6 +521,45 @@ function lockFilePath(repoRoot) {
501
521
  return path.join(repoRoot, LOCK_FILE_RELATIVE);
502
522
  }
503
523
 
524
+ function ensureOmxScaffold(repoRoot, dryRun) {
525
+ const operations = [];
526
+
527
+ for (const relativeDir of OMX_SCAFFOLD_DIRECTORIES) {
528
+ const absoluteDir = path.join(repoRoot, relativeDir);
529
+ if (fs.existsSync(absoluteDir)) {
530
+ if (!fs.statSync(absoluteDir).isDirectory()) {
531
+ throw new Error(`Expected directory at ${relativeDir} but found a file.`);
532
+ }
533
+ operations.push({ status: 'unchanged', file: relativeDir });
534
+ continue;
535
+ }
536
+
537
+ if (!dryRun) {
538
+ fs.mkdirSync(absoluteDir, { recursive: true });
539
+ }
540
+ operations.push({ status: 'created', file: relativeDir });
541
+ }
542
+
543
+ for (const [relativeFile, defaultContent] of OMX_SCAFFOLD_FILES.entries()) {
544
+ const absoluteFile = path.join(repoRoot, relativeFile);
545
+ if (fs.existsSync(absoluteFile)) {
546
+ if (!fs.statSync(absoluteFile).isFile()) {
547
+ throw new Error(`Expected file at ${relativeFile} but found a directory.`);
548
+ }
549
+ operations.push({ status: 'unchanged', file: relativeFile });
550
+ continue;
551
+ }
552
+
553
+ if (!dryRun) {
554
+ fs.mkdirSync(path.dirname(absoluteFile), { recursive: true });
555
+ fs.writeFileSync(absoluteFile, defaultContent, 'utf8');
556
+ }
557
+ operations.push({ status: 'created', file: relativeFile });
558
+ }
559
+
560
+ return operations;
561
+ }
562
+
504
563
  function ensureLockRegistry(repoRoot, dryRun) {
505
564
  const absolutePath = lockFilePath(repoRoot);
506
565
  if (fs.existsSync(absolutePath)) {
@@ -608,6 +667,10 @@ function ensurePackageScripts(repoRoot, dryRun) {
608
667
  function ensureAgentsSnippet(repoRoot, dryRun) {
609
668
  const agentsPath = path.join(repoRoot, 'AGENTS.md');
610
669
  const snippet = fs.readFileSync(path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'), 'utf8').trimEnd();
670
+ const managedRegex = new RegExp(
671
+ `${AGENTS_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
672
+ 'm',
673
+ );
611
674
 
612
675
  if (!fs.existsSync(agentsPath)) {
613
676
  if (!dryRun) {
@@ -617,8 +680,19 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
617
680
  }
618
681
 
619
682
  const existing = fs.readFileSync(agentsPath, 'utf8');
683
+ if (managedRegex.test(existing)) {
684
+ const next = existing.replace(managedRegex, snippet);
685
+ if (next === existing) {
686
+ return { status: 'unchanged', file: 'AGENTS.md' };
687
+ }
688
+ if (!dryRun) {
689
+ fs.writeFileSync(agentsPath, next, 'utf8');
690
+ }
691
+ return { status: 'updated', file: 'AGENTS.md', note: 'refreshed guardex-managed block' };
692
+ }
693
+
620
694
  if (existing.includes(AGENTS_MARKER_START)) {
621
- return { status: 'unchanged', file: 'AGENTS.md' };
695
+ return { status: 'unchanged', file: 'AGENTS.md', note: 'existing marker found without managed end marker' };
622
696
  }
623
697
 
624
698
  const separator = existing.endsWith('\n') ? '\n' : '\n\n';
@@ -828,6 +902,33 @@ function isSpawnFailure(result) {
828
902
  return Boolean(result?.error) && typeof result?.status !== 'number';
829
903
  }
830
904
 
905
+ function ensureRepoBranch(repoRoot, branch) {
906
+ const current = currentBranchName(repoRoot);
907
+ if (current === branch) {
908
+ return { ok: true, changed: false };
909
+ }
910
+
911
+ const checkoutResult = run('git', ['-C', repoRoot, 'checkout', branch], { timeout: 20_000 });
912
+ if (isSpawnFailure(checkoutResult)) {
913
+ return {
914
+ ok: false,
915
+ changed: false,
916
+ stdout: checkoutResult.stdout || '',
917
+ stderr: checkoutResult.stderr || '',
918
+ };
919
+ }
920
+ if (checkoutResult.status !== 0) {
921
+ return {
922
+ ok: false,
923
+ changed: false,
924
+ stdout: checkoutResult.stdout || '',
925
+ stderr: checkoutResult.stderr || '',
926
+ };
927
+ }
928
+
929
+ return { ok: true, changed: true };
930
+ }
931
+
831
932
  function doctorSandboxBranchPrefix() {
832
933
  const now = new Date();
833
934
  const stamp = [
@@ -929,12 +1030,26 @@ function startDoctorSandbox(blocked) {
929
1030
  throw startResult.error;
930
1031
  }
931
1032
  if (startResult.status !== 0) {
932
- throw new Error((startResult.stderr || startResult.stdout || 'failed to start doctor sandbox').trim());
1033
+ return startDoctorSandboxFallback(blocked);
933
1034
  }
934
1035
 
935
1036
  const metadata = extractAgentBranchStartMetadata(startResult.stdout);
936
- if (!metadata.worktreePath) {
937
- throw new Error(`Failed to parse sandbox worktree from agent-branch-start output:\n${startResult.stdout}`);
1037
+ const currentBranch = currentBranchName(blocked.repoRoot);
1038
+ const worktreePath = metadata.worktreePath ? path.resolve(metadata.worktreePath) : '';
1039
+ const repoRootPath = path.resolve(blocked.repoRoot);
1040
+ const hasSafeWorktree = Boolean(worktreePath) && worktreePath !== repoRootPath;
1041
+ const branchChanged = Boolean(currentBranch) && currentBranch !== blocked.branch;
1042
+
1043
+ if (!hasSafeWorktree || branchChanged) {
1044
+ const restoreResult = ensureRepoBranch(blocked.repoRoot, blocked.branch);
1045
+ if (!restoreResult.ok) {
1046
+ const detail = [restoreResult.stderr, restoreResult.stdout].filter(Boolean).join('\n').trim();
1047
+ throw new Error(
1048
+ `doctor sandbox startup switched protected base checkout and could not restore '${blocked.branch}'.` +
1049
+ (detail ? `\n${detail}` : ''),
1050
+ );
1051
+ }
1052
+ return startDoctorSandboxFallback(blocked);
938
1053
  }
939
1054
 
940
1055
  return {
@@ -1069,6 +1184,19 @@ function isCommandAvailable(commandName) {
1069
1184
  return run('which', [commandName]).status === 0;
1070
1185
  }
1071
1186
 
1187
+ function extractAgentBranchFinishPrUrl(output) {
1188
+ const match = String(output || '').match(/\[agent-branch-finish\] PR:\s*(\S+)/);
1189
+ return match ? match[1] : '';
1190
+ }
1191
+
1192
+ function doctorFinishFlowIsPending(output) {
1193
+ return (
1194
+ /\[agent-branch-finish\] PR merge not completed yet; leaving PR open\./.test(output) ||
1195
+ /\[agent-branch-finish\] Merge pending review\/check policy\. Branch cleanup skipped for now\./.test(output) ||
1196
+ /\[agent-branch-finish\] PR auto-merge enabled; waiting for required checks\/reviews\./.test(output)
1197
+ );
1198
+ }
1199
+
1072
1200
  function finishDoctorSandboxBranch(blocked, metadata) {
1073
1201
  const finishScript = path.join(metadata.worktreePath, 'scripts', 'agent-branch-finish.sh');
1074
1202
  if (!fs.existsSync(finishScript)) {
@@ -1122,6 +1250,17 @@ function finishDoctorSandboxBranch(blocked, metadata) {
1122
1250
  };
1123
1251
  }
1124
1252
 
1253
+ const combinedOutput = `${finishResult.stdout || ''}\n${finishResult.stderr || ''}`;
1254
+ if (doctorFinishFlowIsPending(combinedOutput)) {
1255
+ return {
1256
+ status: 'pending',
1257
+ note: 'PR created and waiting for merge policy/checks',
1258
+ prUrl: extractAgentBranchFinishPrUrl(combinedOutput),
1259
+ stdout: finishResult.stdout || '',
1260
+ stderr: finishResult.stderr || '',
1261
+ };
1262
+ }
1263
+
1125
1264
  return {
1126
1265
  status: 'completed',
1127
1266
  note: 'doctor sandbox finish flow completed',
@@ -1157,7 +1296,35 @@ function runDoctorInSandbox(options, blocked) {
1157
1296
  status: 'skipped',
1158
1297
  note: 'sandbox doctor did not complete successfully',
1159
1298
  };
1299
+ let postSandboxAutoFinishSummary = {
1300
+ enabled: false,
1301
+ attempted: 0,
1302
+ completed: 0,
1303
+ skipped: 0,
1304
+ failed: 0,
1305
+ details: ['Skipped auto-finish sweep (sandbox doctor did not complete successfully).'],
1306
+ };
1307
+ let omxScaffoldSyncResult = {
1308
+ status: 'skipped',
1309
+ note: 'sandbox doctor did not complete successfully',
1310
+ };
1160
1311
  if (nestedResult.status === 0) {
1312
+ const omxScaffoldOps = ensureOmxScaffold(blocked.repoRoot, Boolean(options.dryRun));
1313
+ const changedOmxPaths = omxScaffoldOps.filter((operation) => operation.status !== 'unchanged');
1314
+ if (changedOmxPaths.length === 0) {
1315
+ omxScaffoldSyncResult = {
1316
+ status: 'unchanged',
1317
+ note: '.omx scaffold already in sync',
1318
+ operations: omxScaffoldOps,
1319
+ };
1320
+ } else {
1321
+ omxScaffoldSyncResult = {
1322
+ status: options.dryRun ? 'would-sync' : 'synced',
1323
+ note: `${options.dryRun ? 'would sync' : 'synced'} ${changedOmxPaths.length} .omx path(s)`,
1324
+ operations: omxScaffoldOps,
1325
+ };
1326
+ }
1327
+
1161
1328
  if (!options.dryRun) {
1162
1329
  autoCommitResult = autoCommitDoctorSandboxChanges(metadata);
1163
1330
  if (autoCommitResult.status === 'committed') {
@@ -1213,6 +1380,12 @@ function runDoctorInSandbox(options, blocked) {
1213
1380
  };
1214
1381
  }
1215
1382
  }
1383
+
1384
+ postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
1385
+ baseBranch: blocked.branch,
1386
+ dryRun: options.dryRun,
1387
+ excludeBranches: [metadata.branch],
1388
+ });
1216
1389
  }
1217
1390
 
1218
1391
  if (options.json) {
@@ -1224,9 +1397,11 @@ function runDoctorInSandbox(options, blocked) {
1224
1397
  JSON.stringify(
1225
1398
  {
1226
1399
  ...parsed,
1400
+ sandboxOmxScaffoldSync: omxScaffoldSyncResult,
1227
1401
  sandboxLockSync: lockSyncResult,
1228
1402
  sandboxAutoCommit: autoCommitResult,
1229
1403
  sandboxFinish: finishResult,
1404
+ autoFinish: postSandboxAutoFinishSummary,
1230
1405
  },
1231
1406
  null,
1232
1407
  2,
@@ -1266,6 +1441,15 @@ function runDoctorInSandbox(options, blocked) {
1266
1441
  console.log(`[${TOOL_NAME}] Auto-finish flow completed for sandbox branch '${metadata.branch}'.`);
1267
1442
  if (finishResult.stdout) process.stdout.write(finishResult.stdout);
1268
1443
  if (finishResult.stderr) process.stderr.write(finishResult.stderr);
1444
+ } else if (finishResult.status === 'pending') {
1445
+ console.log(
1446
+ `[${TOOL_NAME}] Auto-finish pending for sandbox branch '${metadata.branch}': ${finishResult.note}.`,
1447
+ );
1448
+ if (finishResult.prUrl) {
1449
+ console.log(`[${TOOL_NAME}] PR: ${finishResult.prUrl}`);
1450
+ }
1451
+ if (finishResult.stdout) process.stdout.write(finishResult.stdout);
1452
+ if (finishResult.stderr) process.stderr.write(finishResult.stderr);
1269
1453
  } else if (finishResult.status === 'failed') {
1270
1454
  console.log(`[${TOOL_NAME}] Auto-finish flow failed for sandbox branch '${metadata.branch}'.`);
1271
1455
  if (finishResult.stdout) process.stdout.write(finishResult.stdout);
@@ -1283,6 +1467,26 @@ function runDoctorInSandbox(options, blocked) {
1283
1467
  } else {
1284
1468
  console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
1285
1469
  }
1470
+
1471
+ if (postSandboxAutoFinishSummary.enabled) {
1472
+ console.log(
1473
+ `[${TOOL_NAME}] Auto-finish sweep (base=${blocked.branch}): attempted=${postSandboxAutoFinishSummary.attempted}, completed=${postSandboxAutoFinishSummary.completed}, skipped=${postSandboxAutoFinishSummary.skipped}, failed=${postSandboxAutoFinishSummary.failed}`,
1474
+ );
1475
+ for (const detail of postSandboxAutoFinishSummary.details) {
1476
+ console.log(`[${TOOL_NAME}] ${detail}`);
1477
+ }
1478
+ } else if (postSandboxAutoFinishSummary.details.length > 0) {
1479
+ console.log(`[${TOOL_NAME}] ${postSandboxAutoFinishSummary.details[0]}`);
1480
+ }
1481
+ if (omxScaffoldSyncResult.status === 'synced') {
1482
+ console.log(`[${TOOL_NAME}] Synced .omx scaffold back to protected branch workspace.`);
1483
+ } else if (omxScaffoldSyncResult.status === 'unchanged') {
1484
+ console.log(`[${TOOL_NAME}] .omx scaffold already aligned in protected branch workspace.`);
1485
+ } else if (omxScaffoldSyncResult.status === 'would-sync') {
1486
+ console.log(`[${TOOL_NAME}] Dry run: would sync .omx scaffold back to protected branch workspace.`);
1487
+ } else {
1488
+ console.log(`[${TOOL_NAME}] .omx scaffold sync skipped: ${omxScaffoldSyncResult.note}.`);
1489
+ }
1286
1490
  }
1287
1491
  }
1288
1492
 
@@ -1314,6 +1518,18 @@ function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
1314
1518
  return { target, args: remaining };
1315
1519
  }
1316
1520
 
1521
+ function parseReviewArgs(rawArgs) {
1522
+ const parsed = parseTargetFlag(rawArgs, process.cwd());
1523
+ const passthroughArgs = [...parsed.args];
1524
+ if (passthroughArgs[0] === 'start') {
1525
+ passthroughArgs.shift();
1526
+ }
1527
+ return {
1528
+ target: parsed.target,
1529
+ passthroughArgs,
1530
+ };
1531
+ }
1532
+
1317
1533
  function parseReportArgs(rawArgs) {
1318
1534
  const options = {
1319
1535
  target: process.cwd(),
@@ -1579,6 +1795,203 @@ function listLocalUserBranches(repoRoot) {
1579
1795
  return [branchName];
1580
1796
  }
1581
1797
 
1798
+ function listLocalAgentBranches(repoRoot) {
1799
+ const result = gitRun(
1800
+ repoRoot,
1801
+ ['for-each-ref', '--format=%(refname:short)', 'refs/heads/agent/'],
1802
+ { allowFailure: true },
1803
+ );
1804
+ if (result.status !== 0) {
1805
+ return [];
1806
+ }
1807
+ return uniquePreserveOrder(
1808
+ String(result.stdout || '')
1809
+ .split('\n')
1810
+ .map((item) => item.trim())
1811
+ .filter(Boolean),
1812
+ );
1813
+ }
1814
+
1815
+ function mapWorktreePathsByBranch(repoRoot) {
1816
+ const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
1817
+ const map = new Map();
1818
+ if (result.status !== 0) {
1819
+ return map;
1820
+ }
1821
+
1822
+ const lines = String(result.stdout || '').split('\n');
1823
+ let currentWorktree = '';
1824
+ for (const line of lines) {
1825
+ if (line.startsWith('worktree ')) {
1826
+ currentWorktree = line.slice('worktree '.length).trim();
1827
+ continue;
1828
+ }
1829
+ if (line.startsWith('branch refs/heads/')) {
1830
+ const branchName = line.slice('branch refs/heads/'.length).trim();
1831
+ if (currentWorktree && branchName) {
1832
+ map.set(branchName, currentWorktree);
1833
+ }
1834
+ }
1835
+ }
1836
+ return map;
1837
+ }
1838
+
1839
+ function hasSignificantWorkingTreeChanges(worktreePath) {
1840
+ const result = run('git', ['-C', worktreePath, 'status', '--porcelain']);
1841
+ if (result.status !== 0) {
1842
+ return true;
1843
+ }
1844
+
1845
+ const lines = String(result.stdout || '')
1846
+ .split('\n')
1847
+ .map((line) => line.trimEnd())
1848
+ .filter((line) => line.length > 0);
1849
+
1850
+ for (const line of lines) {
1851
+ const pathPart = (line.length > 3 ? line.slice(3) : '').trim();
1852
+ if (!pathPart) continue;
1853
+ if (pathPart === LOCK_FILE_RELATIVE) continue;
1854
+ if (pathPart.startsWith(`${LOCK_FILE_RELATIVE} -> `)) continue;
1855
+ if (pathPart.endsWith(` -> ${LOCK_FILE_RELATIVE}`)) continue;
1856
+ return true;
1857
+ }
1858
+ return false;
1859
+ }
1860
+
1861
+ function autoFinishReadyAgentBranches(repoRoot, options = {}) {
1862
+ const baseBranch = String(options.baseBranch || '').trim();
1863
+ const dryRun = Boolean(options.dryRun);
1864
+ const excludedBranches = new Set(
1865
+ Array.isArray(options.excludeBranches)
1866
+ ? options.excludeBranches.map((branch) => String(branch || '').trim()).filter(Boolean)
1867
+ : [],
1868
+ );
1869
+
1870
+ const summary = {
1871
+ enabled: true,
1872
+ baseBranch,
1873
+ attempted: 0,
1874
+ completed: 0,
1875
+ skipped: 0,
1876
+ failed: 0,
1877
+ details: [],
1878
+ };
1879
+
1880
+ if (!baseBranch || baseBranch === 'HEAD' || baseBranch.startsWith('agent/')) {
1881
+ summary.enabled = false;
1882
+ summary.details.push('Skipped auto-finish sweep (base branch is missing or not a non-agent local branch).');
1883
+ return summary;
1884
+ }
1885
+
1886
+ if (String(process.env.MUSAFETY_DOCTOR_SANDBOX || '') === '1') {
1887
+ summary.enabled = false;
1888
+ summary.details.push('Skipped auto-finish sweep inside doctor sandbox pass.');
1889
+ return summary;
1890
+ }
1891
+
1892
+ if (String(process.env.MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES || '') === '1') {
1893
+ summary.enabled = false;
1894
+ summary.details.push('Skipped auto-finish sweep (MUSAFETY_SKIP_AUTO_FINISH_READY_BRANCHES=1).');
1895
+ return summary;
1896
+ }
1897
+
1898
+ if (dryRun) {
1899
+ summary.enabled = false;
1900
+ summary.details.push('Skipped auto-finish sweep in dry-run mode.');
1901
+ return summary;
1902
+ }
1903
+
1904
+ const finishScript = path.join(repoRoot, 'scripts', 'agent-branch-finish.sh');
1905
+ if (!fs.existsSync(finishScript)) {
1906
+ summary.enabled = false;
1907
+ summary.details.push(`Skipped auto-finish sweep (missing ${path.relative(repoRoot, finishScript)}).`);
1908
+ return summary;
1909
+ }
1910
+
1911
+ const hasOrigin = gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
1912
+ if (!hasOrigin) {
1913
+ summary.enabled = false;
1914
+ summary.details.push('Skipped auto-finish sweep (origin remote missing).');
1915
+ return summary;
1916
+ }
1917
+
1918
+ const ghBin = process.env.MUSAFETY_GH_BIN || 'gh';
1919
+ if (run(ghBin, ['--version']).status !== 0) {
1920
+ summary.enabled = false;
1921
+ summary.details.push(`Skipped auto-finish sweep (${ghBin} not available).`);
1922
+ return summary;
1923
+ }
1924
+
1925
+ const branchWorktrees = mapWorktreePathsByBranch(repoRoot);
1926
+ const agentBranches = listLocalAgentBranches(repoRoot);
1927
+ if (agentBranches.length === 0) {
1928
+ summary.enabled = false;
1929
+ summary.details.push('No local agent branches found for auto-finish sweep.');
1930
+ return summary;
1931
+ }
1932
+
1933
+ for (const branch of agentBranches) {
1934
+ if (excludedBranches.has(branch)) {
1935
+ summary.skipped += 1;
1936
+ summary.details.push(`[skip] ${branch}: excluded from this auto-finish sweep.`);
1937
+ continue;
1938
+ }
1939
+
1940
+ if (branch === baseBranch) {
1941
+ summary.skipped += 1;
1942
+ summary.details.push(`[skip] ${branch}: source branch equals base branch.`);
1943
+ continue;
1944
+ }
1945
+
1946
+ let counts;
1947
+ try {
1948
+ counts = aheadBehind(repoRoot, branch, baseBranch);
1949
+ } catch (error) {
1950
+ summary.failed += 1;
1951
+ summary.details.push(`[fail] ${branch}: unable to compute ahead/behind (${error.message}).`);
1952
+ continue;
1953
+ }
1954
+
1955
+ if (counts.ahead <= 0) {
1956
+ summary.skipped += 1;
1957
+ summary.details.push(`[skip] ${branch}: already merged into ${baseBranch}.`);
1958
+ continue;
1959
+ }
1960
+
1961
+ const branchWorktree = branchWorktrees.get(branch) || '';
1962
+ if (branchWorktree && hasSignificantWorkingTreeChanges(branchWorktree)) {
1963
+ summary.skipped += 1;
1964
+ summary.details.push(`[skip] ${branch}: dirty worktree (${branchWorktree}).`);
1965
+ continue;
1966
+ }
1967
+
1968
+ summary.attempted += 1;
1969
+ const finishArgs = [
1970
+ finishScript,
1971
+ '--branch',
1972
+ branch,
1973
+ '--base',
1974
+ baseBranch,
1975
+ '--via-pr',
1976
+ '--cleanup',
1977
+ ];
1978
+ const finishResult = run('bash', finishArgs, { cwd: repoRoot });
1979
+ const combinedOutput = [finishResult.stdout || '', finishResult.stderr || ''].join('\n').trim();
1980
+
1981
+ if (finishResult.status === 0) {
1982
+ summary.completed += 1;
1983
+ summary.details.push(`[done] ${branch}: auto-finish completed.`);
1984
+ continue;
1985
+ }
1986
+
1987
+ summary.failed += 1;
1988
+ const tail = combinedOutput ? ` ${combinedOutput.split('\n').slice(-2).join(' | ')}` : '';
1989
+ summary.details.push(`[fail] ${branch}: auto-finish failed.${tail}`);
1990
+ }
1991
+
1992
+ return summary;
1993
+ }
1994
+
1582
1995
  function ensureSetupProtectedBranches(repoRoot, dryRun) {
1583
1996
  const localUserBranches = listLocalUserBranches(repoRoot);
1584
1997
  if (localUserBranches.length === 0) {
@@ -1818,6 +2231,7 @@ function parseCleanupArgs(rawArgs) {
1818
2231
  dryRun: false,
1819
2232
  forceDirty: false,
1820
2233
  keepRemote: false,
2234
+ keepCleanWorktrees: false,
1821
2235
  };
1822
2236
 
1823
2237
  for (let index = 0; index < rawArgs.length; index += 1) {
@@ -1861,6 +2275,10 @@ function parseCleanupArgs(rawArgs) {
1861
2275
  options.keepRemote = true;
1862
2276
  continue;
1863
2277
  }
2278
+ if (arg === '--keep-clean-worktrees') {
2279
+ options.keepCleanWorktrees = true;
2280
+ continue;
2281
+ }
1864
2282
  throw new Error(`Unknown option: ${arg}`);
1865
2283
  }
1866
2284
 
@@ -2271,6 +2689,8 @@ function runInstallInternal(options) {
2271
2689
  const repoRoot = resolveRepoRoot(options.target);
2272
2690
  const operations = [];
2273
2691
 
2692
+ operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
2693
+
2274
2694
  for (const templateFile of TEMPLATE_FILES) {
2275
2695
  operations.push(copyTemplateFile(repoRoot, templateFile, Boolean(options.force), Boolean(options.dryRun)));
2276
2696
  }
@@ -2297,6 +2717,8 @@ function runFixInternal(options) {
2297
2717
  const repoRoot = resolveRepoRoot(options.target);
2298
2718
  const operations = [];
2299
2719
 
2720
+ operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
2721
+
2300
2722
  for (const templateFile of TEMPLATE_FILES) {
2301
2723
  operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
2302
2724
  }
@@ -2350,6 +2772,8 @@ function runScanInternal(options) {
2350
2772
  const findings = [];
2351
2773
 
2352
2774
  const requiredPaths = [
2775
+ ...OMX_SCAFFOLD_DIRECTORIES,
2776
+ ...Array.from(OMX_SCAFFOLD_FILES.keys()),
2353
2777
  ...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
2354
2778
  LOCK_FILE_RELATIVE,
2355
2779
  ];
@@ -2701,6 +3125,11 @@ function doctor(rawArgs) {
2701
3125
  assertProtectedMainWriteAllowed(options, 'doctor');
2702
3126
  const fixPayload = runFixInternal(options);
2703
3127
  const scanResult = runScanInternal({ target: options.target, json: false });
3128
+ const currentBaseBranch = currentBranchName(scanResult.repoRoot);
3129
+ const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
3130
+ baseBranch: currentBaseBranch,
3131
+ dryRun: options.dryRun,
3132
+ });
2704
3133
  const safe = scanResult.errors === 0 && scanResult.warnings === 0;
2705
3134
  const musafe = safe;
2706
3135
 
@@ -2722,6 +3151,7 @@ function doctor(rawArgs) {
2722
3151
  warnings: scanResult.warnings,
2723
3152
  findings: scanResult.findings,
2724
3153
  },
3154
+ autoFinish: autoFinishSummary,
2725
3155
  },
2726
3156
  null,
2727
3157
  2,
@@ -2733,6 +3163,16 @@ function doctor(rawArgs) {
2733
3163
 
2734
3164
  printOperations('Doctor/fix', fixPayload, options.dryRun);
2735
3165
  printScanResult(scanResult, false);
3166
+ if (autoFinishSummary.enabled) {
3167
+ console.log(
3168
+ `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
3169
+ );
3170
+ for (const detail of autoFinishSummary.details) {
3171
+ console.log(`[${TOOL_NAME}] ${detail}`);
3172
+ }
3173
+ } else if (autoFinishSummary.details.length > 0) {
3174
+ console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
3175
+ }
2736
3176
  if (safe) {
2737
3177
  console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
2738
3178
  } else {
@@ -2743,6 +3183,27 @@ function doctor(rawArgs) {
2743
3183
  setExitCodeFromScan(scanResult);
2744
3184
  }
2745
3185
 
3186
+ function review(rawArgs) {
3187
+ const options = parseReviewArgs(rawArgs);
3188
+ const repoRoot = resolveRepoRoot(options.target);
3189
+ const reviewScriptPath = path.join(repoRoot, 'scripts', 'review-bot-watch.sh');
3190
+ if (!fs.existsSync(reviewScriptPath)) {
3191
+ throw new Error(
3192
+ `Missing review bot script: ${reviewScriptPath}\n` +
3193
+ `Run '${SHORT_TOOL_NAME} setup --target ${repoRoot}' then '${SHORT_TOOL_NAME} doctor --target ${repoRoot}'.`,
3194
+ );
3195
+ }
3196
+
3197
+ const result = run('bash', [reviewScriptPath, ...options.passthroughArgs], { cwd: repoRoot });
3198
+ if (isSpawnFailure(result)) {
3199
+ throw result.error;
3200
+ }
3201
+
3202
+ if (result.stdout) process.stdout.write(result.stdout);
3203
+ if (result.stderr) process.stderr.write(result.stderr);
3204
+ process.exitCode = typeof result.status === 'number' ? result.status : 1;
3205
+ }
3206
+
2746
3207
  function report(rawArgs) {
2747
3208
  const options = parseReportArgs(rawArgs);
2748
3209
  const subcommand = options.subcommand || 'help';
@@ -2901,7 +3362,22 @@ function setup(rawArgs) {
2901
3362
  }
2902
3363
 
2903
3364
  const scanResult = runScanInternal({ target: options.target, json: false });
3365
+ const currentBaseBranch = currentBranchName(scanResult.repoRoot);
3366
+ const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, {
3367
+ baseBranch: currentBaseBranch,
3368
+ dryRun: options.dryRun,
3369
+ });
2904
3370
  printScanResult(scanResult, false);
3371
+ if (autoFinishSummary.enabled) {
3372
+ console.log(
3373
+ `[${TOOL_NAME}] Auto-finish sweep (base=${currentBaseBranch}): attempted=${autoFinishSummary.attempted}, completed=${autoFinishSummary.completed}, skipped=${autoFinishSummary.skipped}, failed=${autoFinishSummary.failed}`,
3374
+ );
3375
+ for (const detail of autoFinishSummary.details) {
3376
+ console.log(`[${TOOL_NAME}] ${detail}`);
3377
+ }
3378
+ } else if (autoFinishSummary.details.length > 0) {
3379
+ console.log(`[${TOOL_NAME}] ${autoFinishSummary.details[0]}`);
3380
+ }
2905
3381
 
2906
3382
  if (scanResult.errors === 0 && scanResult.warnings === 0) {
2907
3383
  console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
@@ -2996,6 +3472,9 @@ function cleanup(rawArgs) {
2996
3472
  if (options.dryRun) {
2997
3473
  args.push('--dry-run');
2998
3474
  }
3475
+ if (!options.keepCleanWorktrees) {
3476
+ args.push('--only-dirty-worktrees');
3477
+ }
2999
3478
  args.push('--delete-branches');
3000
3479
  if (!options.keepRemote) {
3001
3480
  args.push('--delete-remote-branches');
@@ -3331,6 +3810,11 @@ function main() {
3331
3810
  return;
3332
3811
  }
3333
3812
 
3813
+ if (command === 'review') {
3814
+ review(rest);
3815
+ return;
3816
+ }
3817
+
3334
3818
  if (command === 'report') {
3335
3819
  report(rest);
3336
3820
  return;