@imdeadpool/guardex 5.0.7 → 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.
package/README.md CHANGED
@@ -69,6 +69,7 @@ gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
69
69
  ```
70
70
 
71
71
  If you use `scripts/codex-agent.sh`, the finish flow is auto-run after the Codex session exits.
72
+ It auto-commits sandbox changes, retries once after syncing if the branch moved behind base during the run, then pushes/opens PR merge flow against the current base branch.
72
73
 
73
74
  ## Visual workflow
74
75
 
@@ -97,6 +98,9 @@ gx status
97
98
  # setup and repair
98
99
  gx setup
99
100
  gx doctor
101
+ # setup + repair another repo without switching your current repo checkout
102
+ gx setup --target /path/to/repo
103
+ gx doctor --target /path/to/repo
100
104
 
101
105
  # protected branch management
102
106
  gx protect list
@@ -108,7 +112,7 @@ gx sync --check
108
112
  gx sync
109
113
 
110
114
  # continuously monitor open PRs targeting current branch and dispatch codex-agent review/merge tasks
111
- bash scripts/review-bot-watch.sh --interval 30
115
+ gx review --interval 30
112
116
 
113
117
  # cleanup merged agent branches and hide clean stale agent worktrees
114
118
  gx cleanup
@@ -123,7 +127,7 @@ gx report scorecard --repo github.com/recodeecom/multiagent-safety
123
127
  Run this in your local shell to keep watching PRs targeting the current branch (or `--base <branch>`):
124
128
 
125
129
  ```sh
126
- bash scripts/review-bot-watch.sh --interval 30
130
+ gx review --interval 30
127
131
  ```
128
132
 
129
133
  Useful flags:
@@ -143,10 +147,12 @@ Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flag
143
147
  - `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
144
148
  - Interactive self-update prompt defaults to **No** (`[y/N]`).
145
149
  - In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
146
- - Direct commits/pushes to protected branches are blocked by default (including VS Code Source Control).
150
+ - Direct commits/pushes to protected branches are blocked by default.
151
+ - Exception: VS Code Source Control commits are allowed on protected branches that exist only locally (no upstream and no remote branch).
147
152
  - Optional repo override for manual VS Code protected-branch writes: `git config multiagent.allowVscodeProtectedBranchWrites true`.
148
153
  - Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
149
154
  - On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
155
+ - In-place agent branching is disabled; `scripts/agent-branch-start.sh` always creates a separate worktree to keep your visible local/base branch unchanged.
150
156
  - `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
151
157
 
152
158
  ## Configure protected branches
@@ -239,9 +245,17 @@ npm pack --dry-run
239
245
 
240
246
  ## Release notes
241
247
 
248
+ ### v5.0.8
249
+
250
+ - Fixed `bin/multiagent-safety.js` syntax regressions in the doctor sandbox flow (`Unexpected identifier` / `Unexpected end of input`) that were breaking CLI execution and CI tests.
251
+ - Restored `scripts/codex-agent.sh` from `templates/scripts/codex-agent.sh` so critical runtime helper parity checks pass in clean CI clones.
252
+ - Bumped package version from `5.0.7` to `5.0.8` for the next npm publish.
253
+
242
254
  ### v5.0.7
255
+ ### Unreleased (generated draft, not versioned yet)
243
256
 
244
- - Bumped package version from `5.0.6` to `5.0.7` to stay one patch ahead for the next npm publish.
257
+ - Add the user-facing changes for the next release here before assigning a version number.
258
+ - Keep this section focused on behavior changes (`Added`, `Changed`, `Fixed`) rather than version-bump-only notes.
245
259
 
246
260
  ### v5.0.6
247
261
 
@@ -83,6 +83,7 @@ const AGENTS_MARKER_END = '<!-- multiagent-safety:END -->';
83
83
  const GITIGNORE_MARKER_START = '# multiagent-safety:START';
84
84
  const GITIGNORE_MARKER_END = '# multiagent-safety:END';
85
85
  const MANAGED_GITIGNORE_PATHS = [
86
+ '.omx/',
86
87
  'scripts/agent-branch-start.sh',
87
88
  'scripts/agent-branch-finish.sh',
88
89
  'scripts/codex-agent.sh',
@@ -98,6 +99,17 @@ const MANAGED_GITIGNORE_PATHS = [
98
99
  '.claude/commands/guardex.md',
99
100
  LOCK_FILE_RELATIVE,
100
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
+ ]);
101
113
  const COMMAND_TYPO_ALIASES = new Map([
102
114
  ['relaese', 'release'],
103
115
  ['realaese', 'release'],
@@ -115,6 +127,7 @@ const SUGGESTIBLE_COMMANDS = [
115
127
  'setup',
116
128
  'init',
117
129
  'doctor',
130
+ 'review',
118
131
  'report',
119
132
  'copy-prompt',
120
133
  'copy-commands',
@@ -149,8 +162,7 @@ const CLI_COMMAND_DESCRIPTIONS = [
149
162
  ['version', 'Print GuardeX version'],
150
163
  ];
151
164
  const AGENT_BOT_DESCRIPTIONS = [
152
- ['review', 'Monitor open PRs targeting current branch and dispatch codex-agent review flow'],
153
- ['start', 'bash scripts/review-bot-watch.sh --interval 30'],
165
+ ['review', 'Start PR monitor + codex-agent review flow (default interval: 30s)'],
154
166
  ];
155
167
 
156
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.
@@ -172,7 +184,10 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
172
184
  3) If setup reports warnings/errors, repair + re-check:
173
185
  gx doctor
174
186
 
175
- 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:
176
191
  bash scripts/codex-agent.sh "task" "agent-name"
177
192
  bash scripts/agent-branch-start.sh "task" "agent-name"
178
193
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
@@ -184,17 +199,17 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup GuardeX (Guardian T-R
184
199
  Remove them explicitly when done:
185
200
  gx cleanup --branch "$(git rev-parse --abbrev-ref HEAD)"
186
201
 
187
- 5) Optional: create OpenSpec planning workspace:
202
+ 6) Optional: create OpenSpec planning workspace:
188
203
  bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
189
204
 
190
- 6) Optional: protect extra branches:
205
+ 7) Optional: protect extra branches:
191
206
  gx protect add release staging
192
207
 
193
- 7) Optional: sync your current agent branch with latest base branch:
208
+ 8) Optional: sync your current agent branch with latest base branch:
194
209
  gx sync --check
195
210
  gx sync
196
211
 
197
- 8) Optional (GitHub remote cleanup): enable:
212
+ 9) Optional (GitHub remote cleanup): enable:
198
213
  Settings -> General -> Pull Requests -> Automatically delete head branches
199
214
  `;
200
215
 
@@ -202,6 +217,7 @@ const AI_SETUP_COMMANDS = `npm i -g @imdeadpool/guardex
202
217
  gh --version
203
218
  gx setup
204
219
  gx doctor
220
+ gx review --interval 30
205
221
  bash scripts/codex-agent.sh "task" "agent-name"
206
222
  bash scripts/agent-branch-start.sh "task" "agent-name"
207
223
  python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
@@ -349,7 +365,9 @@ NOTES
349
365
  - ${SHORT_TOOL_NAME} init is an alias of ${SHORT_TOOL_NAME} setup
350
366
  - ${TOOL_NAME} setup asks for Y/N approval before global installs
351
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>
352
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
353
371
  - doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow
354
372
  - agent-branch-finish merges by default and keeps agent branches/worktrees until explicit cleanup
355
373
  - use '${SHORT_TOOL_NAME} cleanup' to remove merged agent branches/worktrees (optionally remote refs too)
@@ -359,7 +377,8 @@ NOTES
359
377
  console.log(`
360
378
  [${TOOL_NAME}] No git repository detected in current directory.
361
379
  [${TOOL_NAME}] Start from a repo root, or pass an explicit target:
362
- ${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>`);
363
382
  }
364
383
  }
365
384
 
@@ -502,6 +521,45 @@ function lockFilePath(repoRoot) {
502
521
  return path.join(repoRoot, LOCK_FILE_RELATIVE);
503
522
  }
504
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
+
505
563
  function ensureLockRegistry(repoRoot, dryRun) {
506
564
  const absolutePath = lockFilePath(repoRoot);
507
565
  if (fs.existsSync(absolutePath)) {
@@ -844,6 +902,33 @@ function isSpawnFailure(result) {
844
902
  return Boolean(result?.error) && typeof result?.status !== 'number';
845
903
  }
846
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
+
847
932
  function doctorSandboxBranchPrefix() {
848
933
  const now = new Date();
849
934
  const stamp = [
@@ -945,12 +1030,26 @@ function startDoctorSandbox(blocked) {
945
1030
  throw startResult.error;
946
1031
  }
947
1032
  if (startResult.status !== 0) {
948
- throw new Error((startResult.stderr || startResult.stdout || 'failed to start doctor sandbox').trim());
1033
+ return startDoctorSandboxFallback(blocked);
949
1034
  }
950
1035
 
951
1036
  const metadata = extractAgentBranchStartMetadata(startResult.stdout);
952
- if (!metadata.worktreePath) {
953
- 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);
954
1053
  }
955
1054
 
956
1055
  return {
@@ -1197,7 +1296,35 @@ function runDoctorInSandbox(options, blocked) {
1197
1296
  status: 'skipped',
1198
1297
  note: 'sandbox doctor did not complete successfully',
1199
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
+ };
1200
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
+
1201
1328
  if (!options.dryRun) {
1202
1329
  autoCommitResult = autoCommitDoctorSandboxChanges(metadata);
1203
1330
  if (autoCommitResult.status === 'committed') {
@@ -1253,6 +1380,12 @@ function runDoctorInSandbox(options, blocked) {
1253
1380
  };
1254
1381
  }
1255
1382
  }
1383
+
1384
+ postSandboxAutoFinishSummary = autoFinishReadyAgentBranches(blocked.repoRoot, {
1385
+ baseBranch: blocked.branch,
1386
+ dryRun: options.dryRun,
1387
+ excludeBranches: [metadata.branch],
1388
+ });
1256
1389
  }
1257
1390
 
1258
1391
  if (options.json) {
@@ -1264,9 +1397,11 @@ function runDoctorInSandbox(options, blocked) {
1264
1397
  JSON.stringify(
1265
1398
  {
1266
1399
  ...parsed,
1400
+ sandboxOmxScaffoldSync: omxScaffoldSyncResult,
1267
1401
  sandboxLockSync: lockSyncResult,
1268
1402
  sandboxAutoCommit: autoCommitResult,
1269
1403
  sandboxFinish: finishResult,
1404
+ autoFinish: postSandboxAutoFinishSummary,
1270
1405
  },
1271
1406
  null,
1272
1407
  2,
@@ -1332,6 +1467,26 @@ function runDoctorInSandbox(options, blocked) {
1332
1467
  } else {
1333
1468
  console.log(`[${TOOL_NAME}] Lock registry sync skipped: ${lockSyncResult.note}.`);
1334
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
+ }
1335
1490
  }
1336
1491
  }
1337
1492
 
@@ -1363,6 +1518,18 @@ function parseTargetFlag(rawArgs, defaultTarget = process.cwd()) {
1363
1518
  return { target, args: remaining };
1364
1519
  }
1365
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
+
1366
1533
  function parseReportArgs(rawArgs) {
1367
1534
  const options = {
1368
1535
  target: process.cwd(),
@@ -1628,6 +1795,203 @@ function listLocalUserBranches(repoRoot) {
1628
1795
  return [branchName];
1629
1796
  }
1630
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
+
1631
1995
  function ensureSetupProtectedBranches(repoRoot, dryRun) {
1632
1996
  const localUserBranches = listLocalUserBranches(repoRoot);
1633
1997
  if (localUserBranches.length === 0) {
@@ -2325,6 +2689,8 @@ function runInstallInternal(options) {
2325
2689
  const repoRoot = resolveRepoRoot(options.target);
2326
2690
  const operations = [];
2327
2691
 
2692
+ operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
2693
+
2328
2694
  for (const templateFile of TEMPLATE_FILES) {
2329
2695
  operations.push(copyTemplateFile(repoRoot, templateFile, Boolean(options.force), Boolean(options.dryRun)));
2330
2696
  }
@@ -2351,6 +2717,8 @@ function runFixInternal(options) {
2351
2717
  const repoRoot = resolveRepoRoot(options.target);
2352
2718
  const operations = [];
2353
2719
 
2720
+ operations.push(...ensureOmxScaffold(repoRoot, Boolean(options.dryRun)));
2721
+
2354
2722
  for (const templateFile of TEMPLATE_FILES) {
2355
2723
  operations.push(ensureTemplateFilePresent(repoRoot, templateFile, Boolean(options.dryRun)));
2356
2724
  }
@@ -2404,6 +2772,8 @@ function runScanInternal(options) {
2404
2772
  const findings = [];
2405
2773
 
2406
2774
  const requiredPaths = [
2775
+ ...OMX_SCAFFOLD_DIRECTORIES,
2776
+ ...Array.from(OMX_SCAFFOLD_FILES.keys()),
2407
2777
  ...TEMPLATE_FILES.map((entry) => toDestinationPath(entry)),
2408
2778
  LOCK_FILE_RELATIVE,
2409
2779
  ];
@@ -2755,6 +3125,11 @@ function doctor(rawArgs) {
2755
3125
  assertProtectedMainWriteAllowed(options, 'doctor');
2756
3126
  const fixPayload = runFixInternal(options);
2757
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
+ });
2758
3133
  const safe = scanResult.errors === 0 && scanResult.warnings === 0;
2759
3134
  const musafe = safe;
2760
3135
 
@@ -2776,6 +3151,7 @@ function doctor(rawArgs) {
2776
3151
  warnings: scanResult.warnings,
2777
3152
  findings: scanResult.findings,
2778
3153
  },
3154
+ autoFinish: autoFinishSummary,
2779
3155
  },
2780
3156
  null,
2781
3157
  2,
@@ -2787,6 +3163,16 @@ function doctor(rawArgs) {
2787
3163
 
2788
3164
  printOperations('Doctor/fix', fixPayload, options.dryRun);
2789
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
+ }
2790
3176
  if (safe) {
2791
3177
  console.log(`[${TOOL_NAME}] ✅ Repo is fully safe.`);
2792
3178
  } else {
@@ -2797,6 +3183,27 @@ function doctor(rawArgs) {
2797
3183
  setExitCodeFromScan(scanResult);
2798
3184
  }
2799
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
+
2800
3207
  function report(rawArgs) {
2801
3208
  const options = parseReportArgs(rawArgs);
2802
3209
  const subcommand = options.subcommand || 'help';
@@ -2955,7 +3362,22 @@ function setup(rawArgs) {
2955
3362
  }
2956
3363
 
2957
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
+ });
2958
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
+ }
2959
3381
 
2960
3382
  if (scanResult.errors === 0 && scanResult.warnings === 0) {
2961
3383
  console.log(`[${TOOL_NAME}] ✅ Setup complete.`);
@@ -3388,6 +3810,11 @@ function main() {
3388
3810
  return;
3389
3811
  }
3390
3812
 
3813
+ if (command === 'review') {
3814
+ review(rest);
3815
+ return;
3816
+ }
3817
+
3391
3818
  if (command === 'report') {
3392
3819
  report(rest);
3393
3820
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "5.0.7",
3
+ "version": "5.0.8",
4
4
  "description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,
@@ -10,6 +10,7 @@
10
10
  - Before deleting/replacing code, each agent must read the latest session comments/handoffs first and confirm the target code is in their owned scope.
11
11
  - If ownership is unclear or overlaps, stop that edit, post a blocker comment, and let the leader/integrator reassign scope.
12
12
  - For git isolation, each agent must start on a dedicated branch via `scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"`.
13
+ - In-place branch mode is disallowed: never switch the active local/base checkout to an agent branch.
13
14
  - Treat the base branch (`main` or the user's current local base branch) as read-only while the agent branch is active.
14
15
  - Agent completion defaults to `scripts/codex-agent.sh`, which auto-finishes the branch (auto-commit changed files, push/create PR, attempt merge, and pull the local base branch after merge).
15
16
  - Auto-finish now waits for required checks/merge and then cleans merged sandbox branch/worktree by default.
@@ -24,13 +24,13 @@ if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}"
24
24
  fi
25
25
 
26
26
  is_vscode_git_context=0
27
- if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" || "${TERM_PROGRAM:-}" == "vscode" ]]; then
27
+ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
28
28
  is_vscode_git_context=1
29
29
  fi
30
30
 
31
31
  allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
32
32
  if [[ -z "$allow_vscode_protected_raw" ]]; then
33
- allow_vscode_protected_raw="false"
33
+ allow_vscode_protected_raw="true"
34
34
  fi
35
35
  allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
36
36
 
@@ -55,6 +55,15 @@ for protected_branch in $protected_branches_raw; do
55
55
  fi
56
56
  done
57
57
 
58
+ is_local_only_branch=0
59
+ if [[ "$is_protected_branch" == "1" ]]; then
60
+ upstream_ref="$(git for-each-ref --format='%(upstream:short)' "refs/heads/${branch}" | head -n 1)"
61
+ remote_branch_ref="$(git for-each-ref --format='%(refname:short)' "refs/remotes/*/${branch}" | head -n 1)"
62
+ if [[ -z "$upstream_ref" && -z "$remote_branch_ref" ]]; then
63
+ is_local_only_branch=1
64
+ fi
65
+ fi
66
+
58
67
  codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
59
68
  if [[ -z "$codex_require_agent_branch_raw" ]]; then
60
69
  codex_require_agent_branch_raw="true"
@@ -124,8 +133,10 @@ MSG
124
133
  fi
125
134
 
126
135
  if [[ "$is_protected_branch" == "1" ]]; then
127
- if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
128
- exit 0
136
+ if [[ "$is_codex_session" != "1" && "$is_vscode_git_context" == "1" ]]; then
137
+ if [[ "$allow_vscode_protected_branch_writes" == "1" || "$is_local_only_branch" == "1" ]]; then
138
+ exit 0
139
+ fi
129
140
  fi
130
141
 
131
142
  if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
@@ -144,8 +155,11 @@ Use an agent branch first:
144
155
  After finishing work:
145
156
  bash scripts/agent-branch-finish.sh
146
157
 
147
- Optional repo override for manual VS Code protected-branch commits:
148
- git config multiagent.allowVscodeProtectedBranchWrites true
158
+ Optional repo hard-block for VS Code protected-branch commits:
159
+ git config multiagent.allowVscodeProtectedBranchWrites false
160
+
161
+ VS Code Source Control commits on protected local-only branches
162
+ (no upstream and no remote branch) are allowed automatically.
149
163
 
150
164
  Temporary bypass (not recommended):
151
165
  ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
@@ -12,7 +12,7 @@ fi
12
12
 
13
13
  allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
14
14
  if [[ -z "$allow_vscode_protected_raw" ]]; then
15
- allow_vscode_protected_raw="false"
15
+ allow_vscode_protected_raw="true"
16
16
  fi
17
17
  allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"
18
18
 
@@ -77,8 +77,8 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
77
77
  echo "[agent-branch-guard] Push to protected branch blocked."
78
78
  echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
79
79
  echo "[agent-branch-guard] Use an agent branch and merge via PR."
80
- echo "[agent-branch-guard] Optional VS Code override:"
81
- echo " git config multiagent.allowVscodeProtectedBranchWrites true"
80
+ echo "[agent-branch-guard] Optional repo hard-block for VS Code protected-branch push:"
81
+ echo " git config multiagent.allowVscodeProtectedBranchWrites false"
82
82
  echo
83
83
  echo "Temporary bypass (not recommended):"
84
84
  echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
@@ -5,8 +5,6 @@ TASK_NAME="task"
5
5
  AGENT_NAME="agent"
6
6
  BASE_BRANCH=""
7
7
  BASE_BRANCH_EXPLICIT=0
8
- WORKTREE_MODE=1
9
- ALLOW_IN_PLACE=0
10
8
  WORKTREE_ROOT_REL=".omx/agent-worktrees"
11
9
  POSITIONAL_ARGS=()
12
10
 
@@ -25,13 +23,10 @@ while [[ $# -gt 0 ]]; do
25
23
  BASE_BRANCH_EXPLICIT=1
26
24
  shift 2
27
25
  ;;
28
- --in-place)
29
- WORKTREE_MODE=0
30
- shift
31
- ;;
32
- --allow-in-place)
33
- ALLOW_IN_PLACE=1
34
- shift
26
+ --in-place|--allow-in-place)
27
+ echo "[agent-branch-start] In-place branch mode is disabled." >&2
28
+ echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
29
+ exit 1
35
30
  ;;
36
31
  --worktree-root)
37
32
  WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
@@ -47,7 +42,7 @@ while [[ $# -gt 0 ]]; do
47
42
  ;;
48
43
  -*)
49
44
  echo "[agent-branch-start] Unknown option: $1" >&2
50
- echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
45
+ echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
51
46
  exit 1
52
47
  ;;
53
48
  *)
@@ -59,7 +54,7 @@ done
59
54
 
60
55
  if [[ "${#POSITIONAL_ARGS[@]}" -gt 3 ]]; then
61
56
  echo "[agent-branch-start] Too many positional arguments." >&2
62
- echo "Usage: $0 [task] [agent] [base] [--in-place --allow-in-place] [--worktree-root <path>]" >&2
57
+ echo "Usage: $0 [task] [agent] [base] [--worktree-root <path>]" >&2
63
58
  exit 1
64
59
  fi
65
60
 
@@ -237,34 +232,6 @@ while git show-ref --verify --quiet "refs/heads/${branch_name}"; do
237
232
  branch_suffix=$((branch_suffix + 1))
238
233
  done
239
234
 
240
- if [[ "$WORKTREE_MODE" -eq 0 ]]; then
241
- if [[ "$ALLOW_IN_PLACE" -ne 1 ]]; then
242
- echo "[agent-branch-start] --in-place is blocked by default to prevent accidental edits on protected branches." >&2
243
- echo "[agent-branch-start] If you really need it, pass both: --in-place --allow-in-place" >&2
244
- exit 1
245
- fi
246
-
247
- if ! git diff --quiet || ! git diff --cached --quiet; then
248
- echo "[agent-branch-start] Working tree is not clean. Commit/stash changes before starting an in-place branch." >&2
249
- exit 1
250
- fi
251
-
252
- current_branch="$(git rev-parse --abbrev-ref HEAD)"
253
- if [[ "$current_branch" != "$BASE_BRANCH" ]]; then
254
- git checkout "$BASE_BRANCH"
255
- fi
256
-
257
- if git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}"; then
258
- git pull --ff-only origin "$BASE_BRANCH"
259
- fi
260
-
261
- git checkout -b "$branch_name"
262
- git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
263
- echo "[agent-branch-start] Created in-place branch: ${branch_name}"
264
- echo "$branch_name"
265
- exit 0
266
- fi
267
-
268
235
  worktree_root="${repo_root}/${WORKTREE_ROOT_REL}"
269
236
  mkdir -p "$worktree_root"
270
237
  worktree_path="${worktree_root}/${branch_name//\//__}"
@@ -125,6 +125,106 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
125
125
  fi
126
126
  repo_root="$(git rev-parse --show-toplevel)"
127
127
 
128
+ sanitize_slug() {
129
+ local raw="$1"
130
+ local fallback="${2:-task}"
131
+ local slug
132
+ slug="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g')"
133
+ if [[ -z "$slug" ]]; then
134
+ slug="$fallback"
135
+ fi
136
+ printf '%s' "$slug"
137
+ }
138
+
139
+ resolve_start_base_branch() {
140
+ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
141
+ printf '%s' "$BASE_BRANCH"
142
+ return 0
143
+ fi
144
+
145
+ local configured_base
146
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
147
+ if [[ -n "$configured_base" ]]; then
148
+ printf '%s' "$configured_base"
149
+ return 0
150
+ fi
151
+
152
+ local current_branch
153
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
154
+ if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]]; then
155
+ printf '%s' "$current_branch"
156
+ return 0
157
+ fi
158
+
159
+ printf 'dev'
160
+ }
161
+
162
+ resolve_start_ref() {
163
+ local base_branch="$1"
164
+ git -C "$repo_root" fetch origin "$base_branch" --quiet >/dev/null 2>&1 || true
165
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
166
+ printf 'origin/%s' "$base_branch"
167
+ return 0
168
+ fi
169
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${base_branch}"; then
170
+ printf '%s' "$base_branch"
171
+ return 0
172
+ fi
173
+ return 1
174
+ }
175
+
176
+ restore_repo_branch_if_changed() {
177
+ local expected_branch="$1"
178
+ if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
179
+ return 0
180
+ fi
181
+ local current_branch
182
+ current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
183
+ if [[ -z "$current_branch" || "$current_branch" == "$expected_branch" ]]; then
184
+ return 0
185
+ fi
186
+ git -C "$repo_root" checkout "$expected_branch" >/dev/null 2>&1
187
+ }
188
+
189
+ start_sandbox_fallback() {
190
+ local base_branch start_ref timestamp task_slug agent_slug branch_name_base branch_name suffix
191
+ local worktree_root worktree_path
192
+
193
+ base_branch="$(resolve_start_base_branch)"
194
+ if ! start_ref="$(resolve_start_ref "$base_branch")"; then
195
+ echo "[codex-agent] Unable to resolve base ref for fallback sandbox start: ${base_branch}" >&2
196
+ return 1
197
+ fi
198
+
199
+ timestamp="$(date +%Y%m%d-%H%M%S)"
200
+ task_slug="$(sanitize_slug "$TASK_NAME" "task")"
201
+ agent_slug="$(sanitize_slug "$AGENT_NAME" "agent")"
202
+ branch_name_base="agent/${agent_slug}/${timestamp}-${task_slug}"
203
+ branch_name="$branch_name_base"
204
+ suffix=2
205
+ while git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch_name}"; do
206
+ branch_name="${branch_name_base}-${suffix}"
207
+ suffix=$((suffix + 1))
208
+ done
209
+
210
+ worktree_root="${repo_root}/.omx/agent-worktrees"
211
+ mkdir -p "$worktree_root"
212
+ worktree_path="${worktree_root}/${branch_name//\//__}"
213
+ if [[ -e "$worktree_path" ]]; then
214
+ echo "[codex-agent] Fallback worktree path already exists: $worktree_path" >&2
215
+ return 1
216
+ fi
217
+
218
+ git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" "$start_ref" >/dev/null
219
+ git -C "$repo_root" config "branch.${branch_name}.musafetyBase" "$base_branch" >/dev/null 2>&1 || true
220
+ if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
221
+ git -C "$worktree_path" branch --set-upstream-to="origin/${base_branch}" "$branch_name" >/dev/null 2>&1 || true
222
+ fi
223
+
224
+ printf '[agent-branch-start] Created branch: %s\n' "$branch_name"
225
+ printf '[agent-branch-start] Worktree: %s\n' "$worktree_path"
226
+ }
227
+
128
228
  if [[ ! -x "${repo_root}/scripts/agent-branch-start.sh" ]]; then
129
229
  echo "[codex-agent] Missing scripts/agent-branch-start.sh. Run: gx setup" >&2
130
230
  exit 1
@@ -135,12 +235,53 @@ if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 ]]; then
135
235
  start_args+=("$BASE_BRANCH")
136
236
  fi
137
237
 
138
- start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}")"
139
- printf '%s\n' "$start_output"
238
+ initial_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
239
+ start_output=""
240
+ start_status=0
241
+ set +e
242
+ start_output="$(bash "${repo_root}/scripts/agent-branch-start.sh" "${start_args[@]}" 2>&1)"
243
+ start_status=$?
244
+ set -e
140
245
 
141
246
  worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
247
+ current_repo_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
248
+ resolved_repo_root="$(cd "$repo_root" && pwd -P)"
249
+ resolved_worktree_path=""
250
+ if [[ -n "$worktree_path" && -d "$worktree_path" ]]; then
251
+ resolved_worktree_path="$(cd "$worktree_path" && pwd -P)"
252
+ fi
253
+
254
+ fallback_reason=""
255
+ if [[ "$start_status" -ne 0 ]]; then
256
+ fallback_reason="starter exited with status ${start_status}"
257
+ elif [[ -z "$worktree_path" ]]; then
258
+ fallback_reason="starter did not report worktree path"
259
+ elif [[ -n "$resolved_worktree_path" && "$resolved_worktree_path" == "$resolved_repo_root" ]]; then
260
+ fallback_reason="starter pointed to active checkout path"
261
+ elif [[ -n "$initial_repo_branch" && -n "$current_repo_branch" && "$current_repo_branch" != "$initial_repo_branch" ]]; then
262
+ fallback_reason="starter switched active checkout branch"
263
+ fi
264
+
265
+ if [[ -n "$fallback_reason" ]]; then
266
+ if ! restore_repo_branch_if_changed "$initial_repo_branch"; then
267
+ echo "[codex-agent] agent-branch-start changed the active checkout branch and restore failed." >&2
268
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
269
+ exit 1
270
+ fi
271
+ if [[ -n "$start_output" ]]; then
272
+ printf '%s\n' "$start_output" >&2
273
+ fi
274
+ echo "[codex-agent] Unsafe starter output (${fallback_reason}); creating sandbox worktree directly." >&2
275
+ start_output="$(start_sandbox_fallback)"
276
+ printf '%s\n' "$start_output"
277
+ worktree_path="$(printf '%s\n' "$start_output" | sed -n 's/^\[agent-branch-start\] Worktree: //p' | tail -n1)"
278
+ else
279
+ printf '%s\n' "$start_output"
280
+ fi
281
+
142
282
  if [[ -z "$worktree_path" ]]; then
143
- echo "[codex-agent] Could not determine sandbox worktree path from agent-branch-start output." >&2
283
+ echo "[codex-agent] Could not determine sandbox worktree path from sandbox startup output." >&2
284
+ echo "[codex-agent] Run 'gx setup --target ${repo_root}' and 'gx doctor --target ${repo_root}', then retry." >&2
144
285
  exit 1
145
286
  fi
146
287