@@ -82,6 +82,7 @@ const COMMAND_TYPO_ALIASES = new Map([
8282] ) ;
8383const SUGGESTIBLE_COMMANDS = [
8484 'status' ,
85+ 'sandbox' ,
8586 'setup' ,
8687 'doctor' ,
8788 'report' ,
@@ -99,6 +100,7 @@ const SUGGESTIBLE_COMMANDS = [
99100] ;
100101const CLI_COMMAND_DESCRIPTIONS = [
101102 [ 'status' , 'Show musafety CLI + service health without modifying files' ] ,
103+ [ 'sandbox' , 'Create an isolated agent worktree sandbox while keeping visible repo branch unchanged' ] ,
102104 [ 'setup' , 'Install + repair guardrails in a git repo (supports --no-gitignore)' ] ,
103105 [ 'doctor' , 'Repair safety setup drift, then verify repo safety' ] ,
104106 [ 'report' , 'Generate security/safety reports (for example: OpenSSF scorecard)' ] ,
@@ -132,7 +134,7 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup multi-agent safety in
132134 musafety doctor
133135
1341364) Confirm next safe agent workflow commands:
135- bash scripts/agent-branch-start.sh "task" "agent-name"
137+ musafety sandbox "task" "agent-name"
136138 python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
137139 bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
138140
@@ -150,7 +152,7 @@ const AI_SETUP_PROMPT = `Use this exact checklist to setup multi-agent safety in
150152const AI_SETUP_COMMANDS = `npm i -g musafety
151153musafety setup
152154musafety doctor
153- bash scripts/agent-branch-start.sh "task" "agent-name"
155+ musafety sandbox "task" "agent-name"
154156python3 scripts/agent-file-locks.py claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
155157bash scripts/agent-branch-finish.sh --branch "$(git rev-parse --abbrev-ref HEAD)"
156158bash scripts/openspec/init-plan-workspace.sh "<plan-slug>"
@@ -462,6 +464,7 @@ function ensurePackageScripts(repoRoot, dryRun) {
462464 }
463465
464466 const wantedScripts = {
467+ 'agent:sandbox' : `${ TOOL_NAME } sandbox` ,
465468 'agent:branch:start' : 'bash ./scripts/agent-branch-start.sh' ,
466469 'agent:branch:finish' : 'bash ./scripts/agent-branch-finish.sh' ,
467470 'agent:cleanup' : 'bash ./scripts/agent-worktree-prune.sh --base dev' ,
@@ -1067,6 +1070,131 @@ function parseSyncArgs(rawArgs) {
10671070 return options ;
10681071}
10691072
1073+ function parseSandboxArgs ( rawArgs ) {
1074+ const options = {
1075+ target : process . cwd ( ) ,
1076+ task : 'task' ,
1077+ agent : 'agent' ,
1078+ base : '' ,
1079+ worktreeRoot : '.omx/agent-worktrees' ,
1080+ allowNonBase : false ,
1081+ json : false ,
1082+ } ;
1083+
1084+ const positional = [ ] ;
1085+
1086+ for ( let index = 0 ; index < rawArgs . length ; index += 1 ) {
1087+ const arg = rawArgs [ index ] ;
1088+ if ( arg === '--target' ) {
1089+ const next = rawArgs [ index + 1 ] ;
1090+ if ( ! next ) {
1091+ throw new Error ( '--target requires a path value' ) ;
1092+ }
1093+ options . target = next ;
1094+ index += 1 ;
1095+ continue ;
1096+ }
1097+ if ( arg === '--task' ) {
1098+ const next = rawArgs [ index + 1 ] ;
1099+ if ( ! next ) {
1100+ throw new Error ( '--task requires a value' ) ;
1101+ }
1102+ options . task = next ;
1103+ index += 1 ;
1104+ continue ;
1105+ }
1106+ if ( arg === '--agent' ) {
1107+ const next = rawArgs [ index + 1 ] ;
1108+ if ( ! next ) {
1109+ throw new Error ( '--agent requires a value' ) ;
1110+ }
1111+ options . agent = next ;
1112+ index += 1 ;
1113+ continue ;
1114+ }
1115+ if ( arg === '--base' ) {
1116+ const next = rawArgs [ index + 1 ] ;
1117+ if ( ! next ) {
1118+ throw new Error ( '--base requires a branch value' ) ;
1119+ }
1120+ options . base = next ;
1121+ index += 1 ;
1122+ continue ;
1123+ }
1124+ if ( arg === '--worktree-root' ) {
1125+ const next = rawArgs [ index + 1 ] ;
1126+ if ( ! next ) {
1127+ throw new Error ( '--worktree-root requires a path value' ) ;
1128+ }
1129+ options . worktreeRoot = next ;
1130+ index += 1 ;
1131+ continue ;
1132+ }
1133+ if ( arg === '--allow-non-base' ) {
1134+ options . allowNonBase = true ;
1135+ continue ;
1136+ }
1137+ if ( arg === '--json' ) {
1138+ options . json = true ;
1139+ continue ;
1140+ }
1141+ if ( arg . startsWith ( '-' ) ) {
1142+ throw new Error ( `Unknown option: ${ arg } ` ) ;
1143+ }
1144+ positional . push ( arg ) ;
1145+ }
1146+
1147+ if ( positional . length > 2 ) {
1148+ throw new Error ( `Unexpected argument: ${ positional [ 2 ] } ` ) ;
1149+ }
1150+ if ( positional [ 0 ] && options . task === 'task' ) {
1151+ options . task = positional [ 0 ] ;
1152+ }
1153+ if ( positional [ 1 ] && options . agent === 'agent' ) {
1154+ options . agent = positional [ 1 ] ;
1155+ }
1156+ if ( ! options . target ) {
1157+ throw new Error ( '--target requires a path value' ) ;
1158+ }
1159+
1160+ return options ;
1161+ }
1162+
1163+ function resolveSandboxBaseBranch ( repoRoot , explicitBase ) {
1164+ if ( explicitBase ) {
1165+ return explicitBase . trim ( ) ;
1166+ }
1167+
1168+ const configured = readGitConfig ( repoRoot , GIT_BASE_BRANCH_KEY ) ;
1169+ if ( configured ) {
1170+ return configured ;
1171+ }
1172+
1173+ const current = currentBranchName ( repoRoot ) ;
1174+ if ( current && current !== 'HEAD' && ! current . startsWith ( 'agent/' ) ) {
1175+ return current ;
1176+ }
1177+
1178+ if ( gitRefExists ( repoRoot , 'refs/heads/main' ) || gitRefExists ( repoRoot , 'refs/remotes/origin/main' ) ) {
1179+ return 'main' ;
1180+ }
1181+
1182+ return DEFAULT_BASE_BRANCH ;
1183+ }
1184+
1185+ function parseSandboxStartOutput ( stdout ) {
1186+ const out = String ( stdout || '' ) ;
1187+ const branchMatch = out . match ( / ^ \[ a g e n t - b r a n c h - s t a r t \] C r e a t e d b r a n c h : \s * ( .+ ) $ / m) ;
1188+ const worktreeMatch = out . match ( / ^ \[ a g e n t - b r a n c h - s t a r t \] W o r k t r e e : \s * ( .+ ) $ / m) ;
1189+ if ( ! branchMatch || ! worktreeMatch ) {
1190+ throw new Error ( `Unable to parse agent sandbox output:\n${ out . trim ( ) } ` ) ;
1191+ }
1192+ return {
1193+ branch : branchMatch [ 1 ] . trim ( ) ,
1194+ worktreePath : worktreeMatch [ 1 ] . trim ( ) ,
1195+ } ;
1196+ }
1197+
10701198function syncOperation ( repoRoot , strategy , baseRef , ffOnly ) {
10711199 if ( strategy === 'rebase' ) {
10721200 if ( ffOnly ) {
@@ -2081,6 +2209,70 @@ function copyCommands() {
20812209 process . exitCode = 0 ;
20822210}
20832211
2212+ function sandbox ( rawArgs ) {
2213+ const options = parseSandboxArgs ( rawArgs ) ;
2214+ const repoRoot = resolveRepoRoot ( options . target ) ;
2215+ const startScript = path . join ( repoRoot , 'scripts' , 'agent-branch-start.sh' ) ;
2216+ if ( ! fs . existsSync ( startScript ) ) {
2217+ throw new Error ( `Missing scripts/agent-branch-start.sh in target repo. Run '${ TOOL_NAME } setup' first.` ) ;
2218+ }
2219+
2220+ const baseBranch = resolveSandboxBaseBranch ( repoRoot , options . base ) ;
2221+ const visibleBranchBefore = currentBranchName ( repoRoot ) ;
2222+
2223+ if ( ! options . allowNonBase && visibleBranchBefore !== baseBranch ) {
2224+ throw new Error (
2225+ `Sandbox expects visible repo branch '${ baseBranch } ' but current branch is '${ visibleBranchBefore } '. ` +
2226+ `Switch first, or pass --allow-non-base to override.` ,
2227+ ) ;
2228+ }
2229+
2230+ const startArgs = [
2231+ 'scripts/agent-branch-start.sh' ,
2232+ '--task' , options . task ,
2233+ '--agent' , options . agent ,
2234+ '--base' , baseBranch ,
2235+ '--worktree-root' , options . worktreeRoot ,
2236+ ] ;
2237+
2238+ const started = run ( 'bash' , startArgs , { cwd : repoRoot } ) ;
2239+ if ( started . status !== 0 ) {
2240+ throw new Error ( ( started . stderr || started . stdout || 'Sandbox start failed' ) . trim ( ) ) ;
2241+ }
2242+
2243+ const parsed = parseSandboxStartOutput ( started . stdout || '' ) ;
2244+ const visibleBranchAfter = currentBranchName ( repoRoot ) ;
2245+ if ( visibleBranchAfter !== visibleBranchBefore ) {
2246+ throw new Error (
2247+ `Sandbox changed visible repo branch from '${ visibleBranchBefore } ' to '${ visibleBranchAfter } ', which is not allowed.` ,
2248+ ) ;
2249+ }
2250+
2251+ const payload = {
2252+ repoRoot,
2253+ baseBranch,
2254+ visibleBranch : visibleBranchAfter ,
2255+ branch : parsed . branch ,
2256+ worktreePath : parsed . worktreePath ,
2257+ } ;
2258+
2259+ if ( options . json ) {
2260+ process . stdout . write ( `${ JSON . stringify ( payload , null , 2 ) } \n` ) ;
2261+ } else {
2262+ console . log ( `[${ TOOL_NAME } ] Sandbox ready.` ) ;
2263+ console . log ( `[${ TOOL_NAME } ] Visible repo branch: ${ visibleBranchAfter } ` ) ;
2264+ console . log ( `[${ TOOL_NAME } ] Base branch: ${ baseBranch } ` ) ;
2265+ console . log ( `[${ TOOL_NAME } ] Agent branch: ${ parsed . branch } ` ) ;
2266+ console . log ( `[${ TOOL_NAME } ] Sandbox worktree: ${ parsed . worktreePath } ` ) ;
2267+ console . log ( `[${ TOOL_NAME } ] Open a sandbox terminal:` ) ;
2268+ console . log ( ` cd "${ parsed . worktreePath } "` ) ;
2269+ console . log ( ` # commit + push from sandbox, then finish:` ) ;
2270+ console . log ( ` bash scripts/agent-branch-finish.sh --branch "${ parsed . branch } "` ) ;
2271+ }
2272+
2273+ process . exitCode = 0 ;
2274+ }
2275+
20842276function sync ( rawArgs ) {
20852277 const options = parseSyncArgs ( rawArgs ) ;
20862278 const repoRoot = resolveRepoRoot ( options . target ) ;
@@ -2394,6 +2586,11 @@ function main() {
23942586 return ;
23952587 }
23962588
2589+ if ( command === 'sandbox' ) {
2590+ sandbox ( rest ) ;
2591+ return ;
2592+ }
2593+
23972594 if ( command === 'setup' ) {
23982595 setup ( rest ) ;
23992596 return ;
0 commit comments